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语言 vs Verilog 需求分析 绘制状态转移图 C语言实现 switch-case / 状态表 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