25、状态机在硬件描述语言中的对应:Verilog状态机与C状态机对比
做嵌入式的人,迟早要跟硬件描述语言打交道。我最早接触Verilog时,第一反应是——这不就是C语言吗?有if、有case、有赋值。但真正上手写状态机,才发现完全是两码事。
今天我们就来聊聊,Verilog里的状态机和C语言里的状态机,到底有什么异同。搞懂这个,你写FPGA驱动或者做软硬件协同设计时,思路会清晰很多。
一、本质差异:并行 vs 顺序
先说最根本的区别。C语言是顺序执行的,一条指令跑完才跑下一条。Verilog呢?它描述的是硬件电路,所有模块在时钟沿到来时同时动作。
你想想看,C语言里写一个switch-case状态机,每次只进入一个分支。但Verilog里,always块里的赋值是并行的——同一个时钟沿,多个寄存器同时更新。
我在项目中遇到过一位同事,把C状态机的思路直接搬到Verilog里,结果状态跳转乱成一锅粥。为什么?因为他忘了:C语言里你写完状态转移代码,下一条指令才执行;Verilog里所有赋值都在时钟沿瞬间完成,你看到的“顺序”其实是仿真器给你的假象。
核心差异总结:
- C状态机:顺序执行,一次只做一件事
- Verilog状态机:并行触发,所有状态更新同时发生
- C状态机:状态变量是内存中的值
- Verilog状态机:状态变量是寄存器,有物理延迟
二、状态编码:二进制 vs 独热码
C语言里,状态枚举就是整数。0、1、2、3,随便用。省空间,效率也高。
但Verilog里,状态编码有讲究。我刚开始做FPGA时,也习惯用二进制编码。后来发现,当状态数超过8个时,组合逻辑的路径延迟会变得很大,时序容易出问题。
这时候,独热码(one-hot)就派上用场了。每个状态对应一个bit,状态寄存器里只有1位为1。好处是译码逻辑简单,速度快。代价是多用寄存器。
| 编码方式 | C语言 | Verilog |
|---|---|---|
| 二进制 | 常用,省内存 | 可用,但状态多时时序差 |
| 独热码 | 几乎不用,浪费内存 | 常用,速度快,适合FPGA |
| 格雷码 | 偶尔用(如通信协议) | 跨时钟域时用 |
我个人习惯,FPGA里状态数少于8个用二进制,多于8个用独热码。嗯,这个经验值是我调了无数次时序后总结出来的。
三、状态跳转:阻塞赋值 vs 非阻塞赋值
这是新手最容易踩的坑。C语言里赋值就是赋值,没有阻塞非阻塞一说。但Verilog里,这俩区别大了去了。
写状态机时,状态跳转必须用非阻塞赋值(<=)。为什么?因为阻塞赋值(=)会立即更新,导致同一个always块里后面的逻辑读到新值,产生竞争。
我曾经在调试一个SPI控制器时,状态机死活跳不对。查了两天,最后发现是一个阻塞赋值惹的祸。从那以后,我写状态机always块,第一件事就是检查赋值符号。
// C语言状态机 - 顺序执行
void state_machine() {
switch(state) {
case IDLE:
if(trigger) state = WORK; // 立即生效
break;
case WORK:
// 此时state已经是WORK
break;
}
}
// Verilog状态机 - 并行更新
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
state <= IDLE;
else
case(state)
IDLE: if(trigger) state <= WORK; // 时钟沿才生效
WORK: // 此时state还是IDLE,下一个时钟沿才变
endcase
end
注意:Verilog状态机的输出逻辑,建议单独用组合逻辑always块或assign语句。不要把状态跳转和输出混在一个always块里,否则容易产生锁存器。
四、状态机结构:两段式 vs 三段式
C语言里,状态机通常就一个函数,switch-case搞定。但Verilog里,写法有讲究。
我最早学的是两段式:一个always块做状态跳转,一个always块做输出。后来发现,两段式容易产生组合逻辑输出毛刺。三段式就解决了这个问题:状态跳转、次态组合逻辑、输出寄存器,各司其职。
说白了,三段式就是把“下一步要去哪”和“当前要干啥”分开。这样代码清晰,时序也干净。
// 三段式Verilog状态机示例
// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
if(!rst_n) state <= IDLE;
else state <= next_state;
end
// 第二段:次态组合逻辑
always @(*) begin
case(state)
IDLE: next_state = trigger ? WORK : IDLE;
WORK: next_state = done ? IDLE : WORK;
default: next_state = IDLE;
endcase
end
// 第三段:输出寄存器
always @(posedge clk or negedge rst_n) begin
if(!rst_n) out <= 0;
else begin
case(next_state) // 注意用next_state
WORK: out <= 1;
default: out <= 0;
endcase
end
end
我的建议:FPGA项目里,一律用三段式。虽然代码量多了一点,但可读性和可靠性都高。C语言里,状态机简单时用switch-case,复杂时可以考虑状态表+函数指针。
五、状态机图:从思维到代码
不管是C还是Verilog,写状态机之前,我建议先画图。下面这张图,展示了我常用的状态机设计流程。
你看,前期画状态图这一步,C和Verilog是一样的。但到了实现阶段,分道扬镳。C语言关注的是逻辑顺序,Verilog关注的是硬件结构。
六、实际项目中的选择
说了这么多,到底什么时候用C状态机,什么时候用Verilog状态机?
我的经验是:
- 如果你在做MCU固件开发,用C状态机。灵活,好调试。
- 如果你在做FPGA逻辑设计,用Verilog状态机。硬件并行,速度快。
- 如果你在做软硬件协同设计(比如Zynq),那就两边都得会。C状态机做协议解析,Verilog状态机做高速数据通路。
我记得有个项目,用FPGA做图像采集,Cortex-M3做控制。FPGA里用Verilog状态机处理像素流,M3里用C状态机处理命令队列。两边通过AXI总线通信,配合得天衣无缝。
一句话总结:C状态机是“软件思维”,Verilog状态机是“硬件思维”。两者本质相通,但实现方式天差地别。搞懂了这一点,你就能在嵌入式世界里游刃有余。
好了,今天就聊到这里。下次你写状态机时,不妨想想:我是在写软件,还是在描述硬件?想清楚了,代码自然就写对了。
公众号:蓝海资料掘金营,微信deep3321