RTL状态机低功耗与时序优化 - 第4节
4. 状态机综合优化:资源共享与状态合并
状态机优化,说白了就是两件事:省面积和提速度。我做了十几年RTL设计,见过太多人把状态机写得又大又慢,其实很多地方是可以优化的。
4.1 资源共享:别让硬件闲着
先说说资源共享。你想想看,如果两个状态都要做同样的运算,干嘛不共用一套电路?
核心思想:把多个状态中相同的操作提取出来,共用同一个硬件模块。
举个例子,一个简单的状态机,状态A和状态B都要计算 data_in + 8'h10:
// 优化前:每个状态各自计算
always @(*) begin
case(state)
STATE_A: result = data_in + 8'h10;
STATE_B: result = data_in + 8'h10;
STATE_C: result = data_in + 8'h20;
default: result = 8'h00;
endcase
end
// 优化后:共用加法器
wire [7:0] add_result = data_in + 8'h10;
always @(*) begin
case(state)
STATE_A: result = add_result;
STATE_B: result = add_result;
STATE_C: result = data_in + 8'h20;
default: result = 8'h00;
endcase
end
我在项目中遇到过这样的情况:一个通信协议的状态机,有4个状态都要做CRC校验。一开始每个状态都例化了一个CRC模块,面积直接炸了。后来改成共用,面积省了将近40%。
我的习惯:写状态机之前,先列个表,看看哪些操作是重复的。重复超过2次,就考虑共用。
4.2 状态合并:能省则省
状态合并,就是把功能相似的状态合并成一个。这招对面积优化特别有效。
什么时候可以合并?我总结了几条经验:
- 输出完全相同:两个状态的所有输出信号都一样
- 转移条件互补:一个状态的条件是另一个的取反
- 中间状态可省略:比如等待1个周期和等待2个周期,可以用计数器代替
举个例子,一个简单的序列检测器:
// 优化前:状态太多
parameter IDLE = 3'b000;
parameter DETECT_1 = 3'b001;
parameter DETECT_2 = 3'b010;
parameter DETECT_3 = 3'b011;
parameter DONE = 3'b100;
// 优化后:合并中间状态
parameter IDLE = 2'b00;
parameter DETECT = 2'b01;
parameter DONE = 2'b10;
嗯,这里要注意:合并状态不能影响功能。我曾经为了省面积,把两个时序要求不同的状态合并了,结果时序收敛不了,得不偿失。
避坑指南:我曾经把两个输出相同但转移条件不同的状态合并,结果导致状态机进入了非法状态。后来我养成了一个习惯:合并前先画状态转移图,确认没有歧义。
5. 状态化简算法:隐含表法
状态化简,说白了就是找冗余状态。手工做的话,隐含表法是最实用的。
5.1 隐含表法的基本原理
隐含表法的思路很简单:如果两个状态在所有输入下的输出和下一状态都相同,那它们就是等价的,可以合并。
具体步骤:
- 画一个三角形表格,行和列都是状态
- 逐对比较状态,标记不等价对
- 递归检查隐含条件
- 找出所有等价状态对
- 合并等价状态
我刚开始学的时候觉得这方法挺麻烦的,但用熟了之后发现,它比凭感觉找冗余要靠谱得多。
5.2 实战案例
假设我们有4个状态:S0、S1、S2、S3。输入是x,输出是y。
| 当前状态 | x=0时下一状态 | x=0时输出 | x=1时下一状态 | x=1时输出 |
|---|---|---|---|---|
| S0 | S1 | 0 | S2 | 0 |
| S1 | S0 | 0 | S3 | 0 |
| S2 | S1 | 0 | S2 | 0 |
| S3 | S0 | 0 | S3 | 0 |
用隐含表法分析:
- S0和S2:输出相同,但下一状态不同(S1 vs S1,S2 vs S2)—— 等价?
- S1和S3:输出相同,下一状态也相同(S0 vs S0,S3 vs S3)—— 等价!
所以S1和S3可以合并。合并后状态数从4个减到3个。
我的经验:隐含表法适合状态数少于20个的情况。状态太多的话,建议用工具自动化简。
6. 状态机综合指导
综合工具怎么处理你的状态机?了解这个,你才能写出对综合友好的代码。
6.1 综合工具的状态编码策略
综合工具通常支持三种编码方式:
| 编码方式 | 面积 | 速度 | 适用场景 |
|---|---|---|---|
| 二进制编码 | 最小 | 较慢 | 状态数多,面积敏感 |
| 格雷编码 | 中等 | 中等 | 跨时钟域,减少毛刺 |
| 独热编码 | 最大 | 最快 | 状态数少,速度敏感 |
我个人习惯:状态数少于8个用独热码,8到16个用格雷码,超过16个用二进制码。当然,这也不是的,具体要看你的设计目标。
6.2 给综合工具的指导
你可以通过综合指令告诉工具你的偏好:
// 告诉工具用独热码
(* fsm_encoding = "one-hot" *) reg [3:0] state;
// 或者用综合指令
// synopsys fsm_encoding one-hot
注意:综合指令只是建议,不是强制。工具可能会根据你的设计自动选择更优的编码方式。
6.3 综合优化技巧
我总结了几条实用的综合优化技巧:
- 避免全并行case:用
unique case或priority case告诉工具你的意图 - 不要用default状态做复位:复位逻辑单独写,别混在状态机里
- 状态机输出用寄存器:组合逻辑输出容易产生毛刺,建议打一拍
- 状态转移用非阻塞赋值:这是RTL设计的基本功,别搞错了
嗯,说到这个,我想起一个案例。有个同事写状态机,输出直接用组合逻辑,结果后仿的时候发现毛刺导致功能错误。后来改成寄存器输出,问题就解决了。
核心总结:状态机优化不是一蹴而就的,需要根据具体需求权衡面积和速度。资源共享和状态合并是面积优化的利器,隐含表法是状态化简的实用工具,而了解综合工具的行为能帮你写出更高效的代码。
好了,这一章的内容就到这里。状态机优化是个实践性很强的工作,光看理论是不够的,建议你找个实际项目练练手。下一章我们会聊状态机的时序分析和优化,到时候见。