第二章:RTL级面积优化——资源共享、操作数分离、状态机优化、避免不必要的锁存器

大家好,我是你们的后端工程师老K。今天咱们聊聊RTL级面积优化。说实话,很多前端工程师觉得面积优化是后端的事,跟我没关系。这个想法其实挺危险的。我见过太多案例,RTL写得随意,后端再怎么折腾也救不回来。

面积优化,说白了就是让芯片更小、更省电、更便宜。你想想看,同样功能,你的芯片比别人小20%,成本优势就出来了。今天我就把我在项目中踩过的坑、积累的经验,一次性分享给你们。

核心观点:面积优化要从RTL阶段开始。后端能做的优化有限,RTL决定了面积的天花板。

2.1 资源共享——别让硬件闲着

资源共享,是我个人习惯用的第一个优化手段。什么意思呢?就是让同一个硬件模块,在不同时间干不同的事。

举个例子。假设你有两个加法操作,但不会同时发生。那你完全可以用一个加法器,分时复用。

// 不优化的写法:两个加法器
always @(posedge clk) begin
  if (sel)
    result <= a + b;
  else
    result <= c + d;
end

// 优化后的写法:一个加法器
wire sum1 = a + b;
wire sum2 = c + d;
always @(posedge clk) begin
  if (sel)
    result <= sum1;
  else
    result <= sum2;
end

等等,上面这个例子其实没优化好。我重新写一个真正共享的版本:

// 真正的资源共享
wire [7:0] op1 = sel ? a : c;
wire [7:0] op2 = sel ? b : d;
wire [7:0] sum = op1 + op2;

always @(posedge clk) begin
  result <= sum;
end

看到了吗?这里只用一个加法器。通过MUX选择输入,加法器被复用了。

我的经验:资源共享特别适合数据通路中的运算单元。比如多个乘法器、加法器、比较器,只要它们不会同时工作,就可以共享。

我在项目中遇到过这样一个案例:一个视频处理芯片,有8个并行的滤波通道。每个通道都需要一个乘法器。但实际工作时,只有4个通道同时激活。我建议把乘法器减少到4个,通过时分复用。面积直接砍了一半。

2.2 操作数分离——把大运算拆开

操作数分离,这个名字听起来有点学术。其实很简单:把大的运算拆成小的。

为什么会这样?因为大的运算需要大的硬件。比如一个32位乘法器,面积比两个16位乘法器大得多。如果你能保证操作数不会同时用到32位,那就可以拆。

// 不优化的写法:32位乘法
wire [31:0] result = a * b;

// 优化后的写法:拆成16位
wire [15:0] a_low = a[15:0];
wire [15:0] a_high = a[31:16];
wire [15:0] b_low = b[15:0];
wire [15:0] b_high = b[31:16];

wire [31:0] part1 = a_low * b_low;
wire [31:0] part2 = a_low * b_high;
wire [31:0] part3 = a_high * b_low;
wire [31:0] part4 = a_high * b_high;

wire [31:0] result = part1 + (part2 << 16) + (part3 << 16) + (part4 << 32);

嗯,这个例子其实有点复杂。实际项目中,我更常用的是另一种场景:

// 大位宽加法拆成小位宽
wire [63:0] sum = a + b;  // 64位加法器

// 拆成两个32位加法器
wire carry;
wire [31:0] sum_low = a[31:0] + b[31:0];
assign carry = sum_low[31] ? 1'b1 : 1'b0;
wire [31:0] sum_high = a[63:32] + b[63:32] + carry;

注意:操作数分离不是万能的。拆得太碎,反而会增加连线延迟和面积。我建议只在位宽超过32位时考虑。

2.3 状态机优化——少即是多

状态机是数字芯片的"大脑"。但很多人的状态机写得像迷宫,面积自然就大了。

我个人习惯,状态机优化就三个原则:

  1. 减少状态数量——能用4个状态,绝不用8个
  2. 使用独热码——面积换速度,但状态数少时更省
  3. 避免嵌套状态机——一个状态机能搞定,别用两个

举个例子:

// 不优化的状态机:8个状态
typedef enum {IDLE, READ, WRITE, WAIT, CHECK, DONE, ERROR, RESET} state_t;

// 优化后的状态机:4个状态
typedef enum {IDLE, BUSY, DONE, ERROR} state_t;

你可能会问:功能一样吗?当然不一样。但很多时候,我们不需要那么细的状态划分。比如WAIT和CHECK,完全可以合并到BUSY里。

我的建议:写状态机前,先画状态转移图。把能合并的状态合并掉。我曾经把一个16状态的状态机优化到6个,面积减少了30%。

2.4 避免不必要的锁存器——Latch是面积杀手

锁存器(Latch),是很多新手容易踩的坑。它比触发器小,但时序难控,而且容易产生毛刺。

为什么会生成Latch?最常见的原因是:条件分支不完整。

// 会生成Latch的写法
always @(*) begin
  if (sel)
    out = a;
  // 缺少else分支
end

// 正确的写法
always @(*) begin
  if (sel)
    out = a;
  else
    out = b;
end

还有一种情况:case语句没有default。

// 会生成Latch
always @(*) begin
  case (sel)
    2'b00: out = a;
    2'b01: out = b;
    // 缺少2'b10和2'b11
  endcase
end

// 正确的写法
always @(*) begin
  case (sel)
    2'b00: out = a;
    2'b01: out = b;
    default: out = c;
  endcase
end

我曾经踩过的坑:有一次流片回来,芯片功耗比预期高了20%。查了三天,发现是一个always块里漏了else分支,生成了128个Latch。这些Latch在输入变化时不断翻转,功耗自然就上去了。

所以我的习惯是:写组合逻辑时,先给所有输出赋默认值。这样不会生成Latch。

// 安全的写法
always @(*) begin
  out = 0;  // 默认值
  if (sel)
    out = a;
end

2.5 知识体系总结

说了这么多,我画个图帮你们理一理思路:

RTL级面积优化 资源共享 操作数分离 状态机优化 避免Latch • 运算单元复用 • 数据通路共享 • 时分复用 • 大位宽拆小 • 乘法器拆分 • 加法器拆分 • 减少状态数 • 独热码编码 • 避免嵌套 • 补全else分支 • case加default • 赋默认值 面积优化 = 更小的芯片 + 更低的功耗 + 更低的成本

2.6 实战建议

最后,我给你们几个实战中的小建议:

优化手段 适用场景 面积节省 注意事项
资源共享 数据通路、运算单元 30%-50% 注意时序冲突
操作数分离 大位宽运算 20%-40% 位宽超过32位时考虑
状态机优化 控制逻辑 10%-30% 功能验证要全面
避免Latch 组合逻辑 5%-15% 综合时检查Latch报告

我的习惯:每次写完RTL,我都会用综合工具跑一遍,看看面积报告。重点关注有没有意外的Latch,有没有可以共享的运算单元。这个习惯帮我省了不少流片成本。

好了,这一章的内容就到这里。记住,面积优化不是后端一个人的事。RTL写得好,后端才能发挥。希望你们在实际项目中,能把这些技巧用起来。

可可精品资料库