RTL编码风格优化:资源共享与状态机设计
各位同学好,我是老李。今天咱们聊聊RTL编码风格对面积的影响。说实话,我刚入行那会儿,总觉得代码能跑就行,面积优化那是综合工具的事。直到有一次,一个简单的FIR滤波器设计,因为我的编码习惯不好,硬生生多占了30%的面积……从那以后,我才真正重视起编码风格来。
这一章,我把自己这些年踩过的坑、总结的经验,挑几个最实用的点分享给大家。说白了,就是怎么用最小的代价,写出面积更优的RTL代码。
一、资源共享:加法器与乘法器的复用
先问大家一个问题:如果你的设计里有两个加法操作,但这两个操作永远不会同时发生,你会怎么写?
很多新手会这样写:
always @(*) begin
case (sel)
2'b00: result = a + b;
2'b01: result = c + d;
2'b10: result = e + f;
default: result = 0;
endcase
end
嗯,这样写功能没问题。但综合出来是什么结果?三个加法器!每个加法器都在吃面积。你想想看,明明一次只会执行一个加法,为什么要例化三个加法器?
我建议改成这样:
always @(*) begin
case (sel)
2'b00: begin
add_a = a;
add_b = b;
end
2'b01: begin
add_a = c;
add_b = d;
end
2'b10: begin
add_a = e;
add_b = f;
end
default: begin
add_a = 0;
add_b = 0;
end
endcase
result = add_a + add_b;
end
看到了吗?只用一个加法器,通过MUX选择输入。面积直接减少三分之二。我在一个视频处理芯片的项目中,用这个技巧优化了十几个类似的运算单元,整体面积节省了约15%。
核心原则:当多个运算互斥执行时,优先考虑复用运算单元。MUX的面积远小于加法器或乘法器。
乘法器的复用也是同理。不过要注意,乘法器的面积更大,复用的收益也更明显。我曾经遇到一个设计,里面用了8个乘法器做矩阵运算,但仔细分析后发现,同一时刻最多只有2个乘法器在工作。优化后只用了2个乘法器,面积直接砍掉一大块。
小技巧:写代码前先画个数据流图,标出所有运算操作,看看哪些是互斥的。这个习惯帮我省了不少面积。
二、状态机优化:独热码 vs 格雷码
状态机的编码方式,对面积的影响其实挺大的。我见过不少工程师,不管三七二十一,上来就用二进制编码。其实选哪种编码,得看你的状态数量和时序要求。
咱们先看看三种常见编码方式的对比:
| 编码方式 | 状态数 | 触发器数量 | 组合逻辑 | 适用场景 |
|---|---|---|---|---|
| 二进制 | N | log₂(N) | 较多 | 状态数多,面积敏感 |
| 独热码 | N | N | 较少 | 状态数少(≤16),速度要求高 |
| 格雷码 | N | log₂(N) | 中等 | 相邻状态切换,低功耗 |
我个人习惯是:状态数少于8个时,用独热码。为什么?因为独热码的状态译码逻辑非常简单,每个状态只需要一个与门判断。虽然触发器用得多,但组合逻辑省下来的面积往往更大。
举个例子,一个4状态的状态机:
// 独热码编码
parameter IDLE = 4'b0001;
parameter S1 = 4'b0010;
parameter S2 = 4'b0100;
parameter DONE = 4'b1000;
// 状态跳转逻辑
always @(*) begin
case (current_state)
IDLE: next_state = (start) ? S1 : IDLE;
S1: next_state = (done_s1) ? S2 : S1;
S2: next_state = (done_s2) ? DONE : S2;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
你看,每个状态的判断就是一条简单的等式。换成二进制编码,你得用casex或者复杂的逻辑表达式,综合出来的面积反而更大。
注意:独热码的状态数不要超过16个。我见过有人用32状态的独热码状态机,触发器数量直接爆炸,得不偿失。
格雷码呢?我一般在状态切换频繁、且相邻状态切换的场景下用。比如计数器、地址发生器这类。格雷码每次只变化一位,切换时的动态功耗更低。但说实话,对于面积优化来说,格雷码不是首选。
三、避免不必要的锁存器
锁存器(Latch)这个话题,我每次讲课都要强调。不是因为锁存器不能用,而是因为很多工程师在无意中产生了锁存器,自己还不知道。
最常见的产生原因:if-else或case语句的条件覆盖不完整。
看看这个反面教材:
always @(*) begin
if (sel)
data_out = data_in;
// 缺少else分支!
end
综合工具会怎么处理?它会认为:当sel=0时,data_out要保持原值。于是,一个锁存器就诞生了。你想想看,锁存器比触发器面积小吗?不一定。而且锁存器对时序分析不友好,容易出问题。
我建议的写法:
always @(*) begin
data_out = 0; // 赋默认值
if (sel)
data_out = data_in;
end
或者:
always @(*) begin
if (sel)
data_out = data_in;
else
data_out = 0;
end
这两种写法都不会产生锁存器。我个人更倾向于第一种,先赋默认值,再根据条件覆盖。这样代码更简洁,也不容易漏掉分支。
避坑指南:我曾经在一个项目中,因为case语句的default分支写成了default: ;(空语句),结果综合出来一堆锁存器。排查了两天才找到原因。从那以后,我写case语句一定会把default分支写完整。
四、操作数顺序对面积的影响
这个知识点可能很多人没注意过。操作数的顺序,真的会影响综合后的面积。
举个例子:
// 写法A
assign result = a + b + c + d;
// 写法B
assign result = (a + b) + (c + d);
写法A是左结合的,综合工具会生成一个加法器链:((a+b)+c)+d。写法B是两两相加再求和,综合工具可能会生成两个加法器并行计算,再用一个加法器求和。
哪种面积更小?这取决于你的加法器位宽和工艺库。但一般来说,写法A的面积更小,因为它只需要一个加法器链,而写法B需要三个加法器。
再比如乘法:
// 写法A
assign result = a * b * c * d;
// 写法B
assign result = (a * b) * (c * d);
乘法器的面积和位宽密切相关。写法A中,第一个乘法器是a*b(假设都是8位,结果16位),第二个乘法器是16位*c(结果24位),第三个是24位*d(结果32位)。乘法器的位宽越来越大,面积也越来越大。
写法B中,两个乘法器都是8位乘8位(结果16位),最后一个乘法器是16位乘16位(结果32位)。虽然多了一个乘法器,但每个乘法器的位宽更小,总面积反而可能更优。
我的经验:对于多级运算,先估算一下中间结果的位宽。如果位宽增长很快,考虑用并行结构;如果位宽增长缓慢,用链式结构更省面积。
嗯,这一章的内容就到这里。总结一下:资源共享能直接减少运算单元数量;状态机编码要根据状态数灵活选择;避免锁存器要从编码习惯入手;操作数顺序会影响综合工具对运算结构的推断。这些技巧看起来简单,但用好了,面积优化的效果立竿见影。
可可精品资料库