2. RTL代码风格:可综合的RTL代码风格、不可综合的语法结构、同步与异步设计
这一章,我们来聊聊RTL代码风格。说白了,就是怎么写代码,综合工具才能认,才能给你变出电路来。
我刚开始做设计那会儿,觉得Verilog嘛,不就是写代码吗?能跑仿真不就行了?结果第一次跑综合,工具报了一堆错,我才意识到——仿真通过和能综合,完全是两码事。
2.1 可综合的RTL代码风格
什么叫可综合?就是综合工具能把你写的代码,映射成真实的门级电路。你想想看,工具不是人,它得按规则来。
我个人习惯,写代码前先想清楚:我要的是组合逻辑,还是时序逻辑?是触发器,还是锁存器?想清楚了再动笔。
2.1.1 组合逻辑的写法
组合逻辑,说白了就是没有记忆功能的电路。输入一变,输出立马跟着变。最常用的写法就是 assign 和 always @(*)。
// 用 assign 写组合逻辑
assign sum = a + b;
assign sel = (cnt > 10) ? 1'b1 : 1'b0;
// 用 always @(*) 写组合逻辑
always @(*) begin
if (en) begin
data_out = data_in;
end else begin
data_out = 8'h00;
end
end
嗯,这里要注意:always @(*) 里的敏感列表,一定要写全。我见过有人偷懒写成 always @(a or b),结果漏了控制信号,仿真没问题,综合出来的电路却不对。这种坑,我踩过。
2.1.2 时序逻辑的写法
时序逻辑,就是带记忆的。最典型的就是触发器。写法也很固定:用 always @(posedge clk) 或者 always @(negedge clk)。
// 标准的D触发器写法
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
q <= 1'b0;
end else begin
q <= d;
end
end
// 带使能的计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 8'd0;
end else if (en) begin
cnt <= cnt + 1'b1;
end
end
我个人习惯,复位信号一律用异步复位、同步释放。为什么?因为异步复位能保证上电瞬间电路状态确定,但释放时容易出问题。这个后面会细说。
<=(非阻塞赋值),写组合逻辑时用 =(阻塞赋值)。这个规则,我建议你死记硬背下来。刚开始可能不理解,但照着做,能省掉90%的仿真问题。
2.2 不可综合的语法结构
有些语法,仿真用着很爽,但综合工具根本不认。我列几个常见的,你写代码时绕着走。
| 语法结构 | 为什么不可综合 | 替代方案 |
|---|---|---|
initial |
没有对应的硬件电路 | 用复位信号初始化 |
#delay |
硬件没有延迟控制 | 用时钟周期控制时序 |
fork/join |
并行语句块,无硬件对应 | 用多个always块实现并行 |
wait |
事件控制,无硬件对应 | 用状态机实现等待 |
force/release |
仿真调试用,无硬件对应 | 不要用 |
我曾经在项目中看到有人用 #10 来产生时钟,仿真跑得挺好,一综合直接报错。嗯,这种错误,犯一次就记住了。
for 循环。不是不能用,但循环次数必须是常数。如果你写成 for(i=0; i<N; i=i+1) 而 N 是个变量,综合工具会直接罢工。
2.3 同步与异步设计
这个知识点,我觉得是RTL设计里最重要的概念之一。你想想看,一个芯片里那么多信号,怎么保证它们不乱跑?靠的就是同步设计。
2.3.1 同步设计
同步设计,就是所有时序逻辑都在同一个时钟沿触发。说白了,大家步调一致。
// 同步设计示例
always @(posedge clk) begin
data_reg <= data_in;
data_reg2 <= data_reg;
end
同步设计的优点很明显:时序分析简单,STA(静态时序分析)容易做。我建议你,只要条件允许,一律用同步设计。
2.3.2 异步设计
异步设计,就是没有统一的时钟。信号靠握手、互锁等方式传递。这种设计,说实话,我尽量避开。
为什么?因为异步设计太容易出问题了。最典型的就是亚稳态——信号在时钟沿附近变化,导致触发器输出不确定。
2.3.3 异步复位与同步释放
这个是我特别想讲的。很多新手写复位,直接写 always @(posedge clk or negedge rst_n),然后复位信号直接连到触发器的复位端。这样写,仿真没问题,但实际芯片里容易出问题。
为什么?因为复位信号释放时,如果刚好在时钟沿附近,就会导致亚稳态。我遇到过这种情况:芯片上电后,有些寄存器复位了,有些没复位,整个状态机乱套了。
解决方案就是异步复位、同步释放:
// 异步复位、同步释放
reg rst_sync1, rst_sync2;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rst_sync1 <= 1'b0;
rst_sync2 <= 1'b0;
end else begin
rst_sync1 <= 1'b1;
rst_sync2 <= rst_sync1;
end
end
// 用 rst_sync2 作为全局复位
always @(posedge clk or negedge rst_sync2) begin
if (!rst_sync2) begin
cnt <= 8'd0;
end else begin
cnt <= cnt + 1'b1;
end
end
这样写,复位是异步的(一拉低就复位),释放是同步的(等时钟沿来才释放)。既保证了上电瞬间的确定性,又避免了亚稳态。
好了,这一章的内容就这些。记住:写RTL代码,心里要时刻想着电路。你写的每一行代码,最终都会变成实实在在的门和连线。想清楚了再写,比写完了再改,要高效得多。