第二章: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 状态机优化——少即是多
状态机是数字芯片的"大脑"。但很多人的状态机写得像迷宫,面积自然就大了。
我个人习惯,状态机优化就三个原则:
- 减少状态数量——能用4个状态,绝不用8个
- 使用独热码——面积换速度,但状态数少时更省
- 避免嵌套状态机——一个状态机能搞定,别用两个
举个例子:
// 不优化的状态机: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 知识体系总结
说了这么多,我画个图帮你们理一理思路:
2.6 实战建议
最后,我给你们几个实战中的小建议:
| 优化手段 | 适用场景 | 面积节省 | 注意事项 |
|---|---|---|---|
| 资源共享 | 数据通路、运算单元 | 30%-50% | 注意时序冲突 |
| 操作数分离 | 大位宽运算 | 20%-40% | 位宽超过32位时考虑 |
| 状态机优化 | 控制逻辑 | 10%-30% | 功能验证要全面 |
| 避免Latch | 组合逻辑 | 5%-15% | 综合时检查Latch报告 |
我的习惯:每次写完RTL,我都会用综合工具跑一遍,看看面积报告。重点关注有没有意外的Latch,有没有可以共享的运算单元。这个习惯帮我省了不少流片成本。
好了,这一章的内容就到这里。记住,面积优化不是后端一个人的事。RTL写得好,后端才能发挥。希望你们在实际项目中,能把这些技巧用起来。
可可精品资料库