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 异步设计

异步设计,就是没有统一的时钟。信号靠握手、互锁等方式传递。这种设计,说实话,我尽量避开。

为什么?因为异步设计太容易出问题了。最典型的就是亚稳态——信号在时钟沿附近变化,导致触发器输出不确定。

核心要点: 跨时钟域的信号,必须做同步处理。常用的方法有:两级触发器同步、异步FIFO、握手协议。不要偷懒,不要想当然。

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代码风格知识体系 可综合RTL风格 不可综合语法 同步与异步设计 组合逻辑:assign / always @(*) 时序逻辑:always @(posedge clk) 阻塞 vs 非阻塞赋值 initial / #delay / fork-join wait / force / release 变量循环次数 同步设计:统一时钟沿 异步设计:跨时钟域处理 异步复位同步释放 核心原则:可综合、同步化、避免亚稳态

好了,这一章的内容就这些。记住:写RTL代码,心里要时刻想着电路。你写的每一行代码,最终都会变成实实在在的门和连线。想清楚了再写,比写完了再改,要高效得多。