4. RTL代码风格与综合:可综合RTL风格、避免Latch推断、组合逻辑与时序逻辑的平衡

大家好,我是老李。做数字芯片设计这些年,我见过太多因为RTL代码风格不好,导致综合后出各种怪问题的案例。今天咱们就聊聊这个看似基础、实则坑很多的话题——可综合的RTL风格,以及怎么避免综合出你不想要的Latch。

4.1 可综合RTL风格:别让工具猜你的心思

综合工具其实挺“笨”的。它严格按照语法规则来推断电路。你写出来的代码,它就会翻译成对应的门级网表。所以,写RTL就是在画电路,这个观念一定要有。

我个人习惯,写代码前先在脑子里过一遍:这个模块大概长什么样?组合逻辑还是时序逻辑?有没有状态机?想清楚了再动笔。

4.1.1 组合逻辑的写法

组合逻辑,说白了就是输入一变,输出立刻跟着变。在Verilog里,常用 assign 或者 always @(*) 来描述。

// 好的风格:用 assign 描述简单组合逻辑
assign sum = a ^ b;
assign carry = a & b;

// 好的风格:用 always @(*) 描述复杂组合逻辑
always @(*) begin
    if (sel == 1'b0)
        out = data_a;
    else
        out = data_b;
end

这里有个关键点:always @(*) 里的敏感列表,一定要写 *,让工具自动推导。我见过有人手写敏感列表,漏了一个信号,结果综合出来的电路功能不对。嗯,这种坑我踩过。

4.1.2 时序逻辑的写法

时序逻辑靠时钟沿触发。最标准的写法就是 always @(posedge clk or negedge rst_n)。

// 标准的D触发器写法
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

注意,时序逻辑里一定要用 非阻塞赋值 <=。组合逻辑里用 阻塞赋值 =。这个规矩不能乱。为什么?因为综合工具就是按这个规矩来推断电路类型的。你混着用,它可能就推断出你不想看到的东西。

警告: 同一个 always 块里,不要同时给组合逻辑和时序逻辑赋值。我曾经接手过一个项目,有人把组合逻辑和时序逻辑写在一个 always 块里,结果综合后仿真一直对不上,查了三天才找到原因。

4.2 避免Latch推断:综合工具最讨厌的“隐形炸弹”

Latch,也就是锁存器。它本身不是坏东西,但在你不想要的地方出现,就是大麻烦。Latch 对毛刺敏感,时序分析也麻烦,而且很多后端工具不喜欢它。

综合工具推断出 Latch 的典型场景,就是 if-else 或 case 语句没有写全所有分支。

4.2.1 if-else 不完整

// 坏风格:缺少 else,会推断出 Latch
always @(*) begin
    if (en)
        out = data_in;
    // 没有 else,当 en=0 时,out 保持原值
end

// 好风格:补全 else
always @(*) begin
    if (en)
        out = data_in;
    else
        out = 1'b0;  // 或者 out = 'b0;
end

为什么会这样?你想想看,当 en=0 时,你没有告诉 out 应该是什么值。综合工具只能认为:那就保持之前的值吧。这不就是 Latch 的行为吗?

4.2.2 case 语句不完整

// 坏风格:case 没有 default
always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        // 缺少 2'b11 和 default
    endcase
end

// 好风格:加上 default
always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        default: out = 1'b0;
    endcase
end
小技巧: 我习惯在写组合逻辑的 case 语句时,第一行就写上 default: out = 'b0;。这样既避免了 Latch,也明确了所有未列出的情况输出为0。省心。

4.2.3 组合逻辑反馈环路

还有一种情况,就是组合逻辑的输出又绕回来作为输入。这种反馈环路,综合工具也会推断出 Latch。

// 坏风格:组合逻辑反馈
always @(*) begin
    if (load)
        count = data_in;
    else
        count = count + 1;  // count 既是输出又是输入,形成环路
end

这种写法,正确的做法是用时序逻辑来实现计数器。

4.3 组合逻辑与时序逻辑的平衡:速度与面积的博弈

做设计,说白了就是在速度、面积、功耗之间找平衡。组合逻辑和时序逻辑的分配,直接影响这三个指标。

4.3.1 组合逻辑太多会怎样?

组合逻辑太多,意味着路径延迟大。路径延迟一大,你的时钟频率就上不去。我记得有个项目,为了省几个寄存器,把一大段逻辑全用组合逻辑实现。结果综合后时序分析一片红,最后不得不拆成多级流水线。

组合逻辑的深度,一般建议控制在 10-15 级以内。超过这个数,时序就很难收敛了。

4.3.2 时序逻辑太多会怎样?

时序逻辑多,就是寄存器多。寄存器多,面积就大,功耗也高。但好处是时序容易满足,流水线深度够,频率可以跑得高。

所以,怎么平衡?我的经验是:

  • 关键路径:用流水线(时序逻辑)打断长组合逻辑
  • 非关键路径:尽量用组合逻辑,省面积
  • 控制逻辑:多用时序逻辑,稳定可靠
  • 数据通路:根据吞吐率要求,决定流水线级数

4.3.3 一个实际例子:加法器树

假设你要把8个数加起来。你可以用组合逻辑直接加,也可以用流水线分两级加。

// 组合逻辑实现:一级加法器树
always @(*) begin
    sum = a + b + c + d + e + f + g + h;
end
// 路径延迟大,可能跑不快

// 时序逻辑实现:两级流水线
always @(posedge clk) begin
    sum1 <= a + b + c + d;
    sum2 <= e + f + g + h;
end
always @(posedge clk) begin
    sum <= sum1 + sum2;
end
// 路径延迟小,频率可以跑高,但多了两个时钟周期延迟

你看,这就是典型的平衡。你要高频,就得多花面积和延迟。你要低延迟,就得接受频率上不去。

核心思想: 没有的好与坏,只有适合与不适合。做设计前,先搞清楚你的目标是什么——是要最高频率?还是最小面积?还是最低功耗?然后根据目标来分配组合逻辑和时序逻辑的比例。

4.4 知识体系总览

下面这张图,是我自己总结的本章知识体系。你可以把它当作一个检查清单,写代码时对照着看。

RTL代码风格与综合知识体系 可综合RTL风格 组合逻辑:assign / always @(*) 时序逻辑:always @(posedge clk) 阻塞 vs 非阻塞赋值 避免Latch推断 if-else 补全 else case 加 default 避免组合反馈环路 组合 vs 时序平衡 路径深度控制 流水线设计 速度-面积-功耗权衡 核心原则:写RTL就是画电路 代码风格决定综合结果,综合结果决定芯片成败

4.5 避坑指南:我踩过的那些坑

最后,分享几个我亲身经历过的教训,希望能帮你少走弯路。

  • 坑1:敏感列表不全 —— 我曾经在 always @(a or b) 里漏了 c,结果综合前后仿真不一致。后来一律用 @(*),再也没出过这个问题。
  • 坑2:组合逻辑里用非阻塞赋值 —— 有一次赶项目,手一抖在组合逻辑里写了 <=,综合出来一堆奇怪的Latch。查了整整一天。
  • 坑3:case 语句忘记写 default —— 这个最隐蔽。功能仿真时所有case都覆盖到了,没问题。但综合后多了一个Latch,时序分析怎么都过不了。

嗯,这些坑说起来都是小细节,但每一个都让我付出了实实在在的时间代价。希望你看完这一章,能把这些原则刻在脑子里。写代码时多留个心眼,综合工具就不会给你“惊喜”。

一句话总结: 可综合的RTL风格,就是让综合工具能准确理解你的设计意图,不多不少,刚刚好。