前言
在查看ARM7TDMI源码的时候,我注意到他的内核ALU中的乘法器使用了一种不寻常的状态机,经过一番搜索终于确定了它就是独热码状态机。其实关于独热码状态机(one-hot state machine)念书的时候就有所了解,但是当时的了解仅限于一种状态机的编码方式,就跟格雷码或者二进制码一样,没有什么区别,直到看了大量英文资料才了解到这样一种状态机的优势所在——速度超快。
1. 什么是独热码?
其实独热码一点也不神秘,4’b0001,4’b0010,4’b0100,4’b1000这四个数值就组成了一组独热码。独热码的特点就是,每一个编码中只有1位是1,其他位都是0,这也是独热(one-hot)这个名字的来由。
2. 为什么使用独热码
首先,很明显,这样的一种编码方式是极其浪费硬件资源的,我们通过一组对比就可以很快知道这一点。如果一个状态机的设计状态是16个,使用二进制码的话,一共需要4个比特表示,也就是对应着4个寄存器资源,如果使用格雷码也是同理。而如果使用独热码的话,则一共需要16个寄存器资源。由此可见,当状态数量上升时,使用二进制码或格雷码可以有效节省寄存器资源,那为什么还要用独热码呢?
首先我们来想一想,为什么使用二进制码可以节省那么多寄存器资源?这是因为在4个寄存器的输入端我们设计了非常复杂的组合电路,用来对不同状态之间复杂的跳转关系进行译码。套用前面的举例来说,在16个状态的状态机中,任意比特为1(需要对应位的寄存器输出1)的状态都有8个,所以对任意位寄存器进行译码的组合逻辑都必须考虑到8种情况。因此,当状态数量大幅增加的时候,这些组合电路的规模增长也是非常可观的。寄存器之间的组合电路复杂度越高,系统能支持的最高频率就会下降。而在独热码状态机中,由于每一个寄存器只对应一种状态,译码电路就会非常简单,速度就会快很多。说起来,独热码状态机就是一种典型的以面积和功耗换取速度的设计方法。
3. 如何正确地设计独热码状态机
了解了独热码状态机的优势和工作原理,下面让我们来看看如何正确的设计一个独热码状态机。是不是把普通的二进制状态机编码方式改成独热码就可以了呢?当然不是!这个错误我在很多公司的设计中都看到过。设计错误的独热码状态机性能不仅不如普通的二进制状态机,浪费的寄存器资源更是后者的指数倍,因此学习正确的设计方法非常重要。下面我们来看一个标准的独热码状态机Verilog源代码。
localparam IDLE = 0;
localparam RUN = 1;
localparam DONE = 2;
reg [2:0] state;
always @ (posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= 3'd1;
end
else begin
state <= 3'd0;
case (1'd1) // synthesis parallel_case full_case
state[IDLE]: begin
if (go) state[RUN] <= 1'd1;
else state[IDLE] <= 1'd1;
end
state[RUN]: begin
if (finished) state[DONE] <= 1'd1;
else state[RUN] <= 1'd1;
end
state[DONE]: begin
state[IDLE] <= 1'd1;
end
default: begin
state[IDLE] <= 1'd1;
end
endcase
end
end
首先是state <= 3’d0语句,实际上在一个设计正确的独热码状态机中,3’d0是一种绝对不可以出现的非法状态(甚至建议在代码中内置assertion对该状态进行监测以确保验证过程中不会出现与之有关的错误),但在这里作为首句写上是为了让后面的语句更简洁明晰。然后是case(1)分支块,在这个块中会依次对state的3个比特位(寄存器值)进行判断,如果某个比特位的值是1,则根据输入信号的情况,在下一个时钟沿到来时发生对应的状态跳转。这里值得注意的是,由于硬件设计的并行性,我们并不希望这几种状态之间出现竞争关系,因此需要加上// synthesis parallel_case 以确保综合出来的逻辑与设计意图吻合。
第十二行的case(1’d1)是什么意思?
普通状态机的典型写法是 case(signal_name) 1’d0: begin end 1’d1: begin end 表示,当signal_name = 1’d0时怎样,signal_name = 1’d1时怎样 而独热码是反过来的 case(1’d1) signal_name[0]: begin end signal_name[1]: begin end 表示,当为1’d1的信号是signal_name[0]时怎样,当为1’d1的信号是signal_name[1]时怎样。由于独热码的编码结构决定了signal_name[0]和[1]不可能同时为1,因此他是可以正常工作的。
请问是不是写成两段式的状态机这种编码方式才有效。一段式的话,如何确保在状态跳转时,之前的状态索引位清0了?例如状态从IDLE跳到RUN,那么state[IDLE]这时候还是1吗
这个跟一段式还是两段式关系不大,只要always块里用的是<=赋值,就可以保证状态跳转之后,前一个状态的索引位被清零。else begin下面的第一句state <= 3'd0是用来完成清零的,而所有的位的下一个状态都会在下一个时钟沿同时生效,不管是从0到1还是从1到0。