4. 复杂状态机设计:嵌套状态机、层次化状态机、状态机与DMA协同工作

各位同学,咱们今天聊点硬核的。前面几章我们讲了基础的状态机,说白了就是一根筋地从头走到尾。但实际项目中,哪有这么简单?你想想看,一个图像处理芯片,要同时处理数据流、控制DMA、还要响应外部中断,一个扁平的状态机根本扛不住。

我个人习惯,遇到复杂控制逻辑,第一反应就是拆。怎么拆?用嵌套状态机、层次化状态机,再配合DMA协同工作。今天我就把这几招全教给你们。

4.1 嵌套状态机:状态机里的状态机

什么叫嵌套状态机?说白了就是一个大状态机里,某个状态本身又是一个完整的状态机。我刚开始做设计时,总觉得这样搞太复杂,后来发现,这其实是处理「多级控制」最优雅的方式。

举个例子,图像处理中的「行扫描」过程。主状态机负责帧级别的控制,比如「帧开始→行扫描→帧结束」。而行扫描这个状态,内部又需要完成「行起始→像素处理→行结束」这一套子流程。这就是典型的嵌套。

核心思路: 主状态机只负责「什么时候进入子状态机」,子状态机负责「具体怎么做」。两者通过握手信号通信。

4.1.1 代码示例:嵌套状态机实现行扫描

module line_scan_nested (
    input clk, rst_n,
    input start_frame,
    output reg frame_done
);

    // 主状态机:帧级别
    typedef enum {IDLE, LINE_SCAN, FRAME_DONE} main_state_t;
    main_state_t main_state, main_next;

    // 子状态机:行级别
    typedef enum {LINE_IDLE, LINE_START, PIXEL_PROC, LINE_END} sub_state_t;
    sub_state_t sub_state, sub_next;

    reg [9:0] pixel_cnt;
    reg [9:0] line_cnt;
    reg sub_busy;

    // 主状态机转移
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) main_state <= IDLE;
        else main_state <= main_next;
    end

    always @(*) begin
        main_next = main_state;
        case (main_state)
            IDLE: if (start_frame) main_next = LINE_SCAN;
            LINE_SCAN: if (!sub_busy && line_cnt == 480) main_next = FRAME_DONE;
            FRAME_DONE: main_next = IDLE;
        endcase
    end

    // 子状态机:只在LINE_SCAN状态下工作
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) sub_state <= LINE_IDLE;
        else if (main_state == LINE_SCAN) sub_state <= sub_next;
        else sub_state <= LINE_IDLE;
    end

    always @(*) begin
        sub_next = sub_state;
        sub_busy = 1'b1;
        case (sub_state)
            LINE_IDLE: begin sub_next = LINE_START; sub_busy = 1'b0; end
            LINE_START: sub_next = PIXEL_PROC;
            PIXEL_PROC: if (pixel_cnt == 640) sub_next = LINE_END;
            LINE_END: begin
                sub_next = LINE_IDLE;
                sub_busy = 1'b0;
            end
        endcase
    end

    // 计数器逻辑(略)
endmodule
避坑指南: 我曾经在嵌套状态机里犯过一个低级错误——子状态机的复位没有和主状态机联动。结果主状态机跳走了,子状态机还卡在中间状态,整个系统就乱了。记住:子状态机必须在退出时强制复位。

4.2 层次化状态机:把复杂系统「分而治之」

嵌套状态机解决的是「纵向」的层级问题。但实际项目中,往往有多个并行的控制流。比如一个图像加速器,既要控制数据读取,又要控制处理单元,还要控制输出。这时候,层次化状态机就派上用场了。

层次化状态机的核心思想是:把系统拆成多个独立的状态机,每个负责一个功能模块,然后通过一个「调度器」来协调它们。说白了,就是让每个状态机各司其职,互不干扰。

4.2.1 层次化状态机架构图

顶层调度器 数据读取状态机 处理单元状态机 输出控制状态机 FIFO / 总线接口 像素处理流水线 输出FIFO / 显示接口 握手 握手 调度器 功能状态机 执行单元
关键点: 层次化状态机中,每个子状态机都是独立的。它们之间通过「事件」或「标志位」通信,而不是直接修改对方的状态。这样设计的好处是:任何一个子状态机出问题,都不会影响其他模块。

4.3 状态机与DMA协同工作

好了,前面讲的都是状态机内部的玩法。但图像处理中,最耗时的其实是数据搬运。你想想看,一个1080p的图像,一帧就有200多万个像素。如果让状态机一个一个地搬数据,那CPU得等到天荒地老。

这时候,DMA(直接存储器访问)就登场了。DMA说白了就是一个「硬件搬运工」,它可以在不占用CPU的情况下,自动把数据从内存搬到外设,或者反过来。而状态机的任务,就是告诉DMA「搬什么、搬多少、搬到哪里」。

4.3.1 状态机与DMA的典型交互流程

  1. 配置阶段: 状态机向DMA控制器写入源地址、目的地址、传输长度等参数。
  2. 启动阶段: 状态机发出启动信号,DMA开始工作。
  3. 等待阶段: 状态机进入等待状态,轮询DMA的完成标志位,或者等待中断。
  4. 完成阶段: DMA传输完成,状态机收到通知,继续下一步操作。
注意: 状态机在等待DMA完成时,千万不要「死等」。我见过有人用while循环一直轮询,结果把整个系统卡死了。正确的做法是:状态机进入一个「等待」状态,同时允许其他模块继续工作。等DMA中断来了,再跳转到下一个状态。

4.3.2 代码示例:状态机控制DMA传输

module dma_ctrl_fsm (
    input clk, rst_n,
    input dma_done,          // DMA完成信号
    output reg dma_start,    // 启动DMA
    output reg [31:0] dma_src, dma_dst,
    output reg [15:0] dma_len
);

    typedef enum {IDLE, CONFIG, START, WAIT_DMA, DONE} state_t;
    state_t state, next;

    // 状态转移
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) state <= IDLE;
        else state <= next;
    end

    // 次态逻辑
    always @(*) begin
        next = state;
        case (state)
            IDLE: if (start_transfer) next = CONFIG;
            CONFIG: next = START;
            START: next = WAIT_DMA;
            WAIT_DMA: if (dma_done) next = DONE;
            DONE: next = IDLE;
        endcase
    end

    // 输出逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            dma_start <= 1'b0;
            dma_src <= 32'd0;
            dma_dst <= 32'd0;
            dma_len <= 16'd0;
        end else begin
            case (state)
                CONFIG: begin
                    dma_src <= frame_buffer_addr;  // 源地址
                    dma_dst <= line_buffer_addr;   // 目的地址
                    dma_len <= 640 * 3;            // 一行RGB数据
                end
                START: dma_start <= 1'b1;
                WAIT_DMA: dma_start <= 1'b0;
                default: ;
            endcase
        end
    end

endmodule
经验之谈: 我在一个项目中,DMA传输完成后,状态机需要立即处理数据。但DMA完成信号只持续一个时钟周期,状态机如果没及时采样到,就会漏掉。我的解决方案是:在DMA完成信号后加一个「脉冲展宽」电路,或者用寄存器打一拍。这样状态机就有足够的时间去采样了。

4.4 三者结合:一个完整的图像加速案例

好了,我们把今天讲的三样东西串起来。假设我们要做一个图像缩放加速器,输入是1080p,输出是720p。整个控制流程是这样的:

层级 状态机 职责
顶层 帧调度器 控制帧开始/结束,协调子状态机
中层1 DMA控制状态机 配置DMA,从DDR搬运数据到行缓存
中层2 缩放处理状态机 控制缩放算法流水线,逐像素处理
中层3 输出控制状态机 将处理后的数据通过DMA写回DDR
底层 嵌套子状态机 行扫描、像素计数等细粒度控制

这个架构的好处是:每个状态机只关心自己的事。DMA控制状态机只管搬数据,缩放处理状态机只管算像素,互不干扰。而且,由于DMA是独立工作的,数据搬运和像素处理可以「流水线」进行——DMA在搬下一行数据的同时,状态机在处理当前行。这样效率就上来了。

总结一下: 嵌套状态机解决「深度控制」问题,层次化状态机解决「广度控制」问题,DMA协同解决「数据搬运」问题。三者结合,就能构建出高效、可维护的图像加速系统。

嗯,今天就讲到这里。这些内容看起来多,但说白了就是「拆」字诀——把复杂问题拆成简单问题,然后用状态机去控制每一个简单问题。你们回去可以试着用今天讲的思路,重新设计一下自己项目中的控制逻辑,相信会有不一样的收获。