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 层次化状态机架构图
4.3 状态机与DMA协同工作
好了,前面讲的都是状态机内部的玩法。但图像处理中,最耗时的其实是数据搬运。你想想看,一个1080p的图像,一帧就有200多万个像素。如果让状态机一个一个地搬数据,那CPU得等到天荒地老。
这时候,DMA(直接存储器访问)就登场了。DMA说白了就是一个「硬件搬运工」,它可以在不占用CPU的情况下,自动把数据从内存搬到外设,或者反过来。而状态机的任务,就是告诉DMA「搬什么、搬多少、搬到哪里」。
4.3.1 状态机与DMA的典型交互流程
- 配置阶段: 状态机向DMA控制器写入源地址、目的地址、传输长度等参数。
- 启动阶段: 状态机发出启动信号,DMA开始工作。
- 等待阶段: 状态机进入等待状态,轮询DMA的完成标志位,或者等待中断。
- 完成阶段: 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
4.4 三者结合:一个完整的图像加速案例
好了,我们把今天讲的三样东西串起来。假设我们要做一个图像缩放加速器,输入是1080p,输出是720p。整个控制流程是这样的:
| 层级 | 状态机 | 职责 |
|---|---|---|
| 顶层 | 帧调度器 | 控制帧开始/结束,协调子状态机 |
| 中层1 | DMA控制状态机 | 配置DMA,从DDR搬运数据到行缓存 |
| 中层2 | 缩放处理状态机 | 控制缩放算法流水线,逐像素处理 |
| 中层3 | 输出控制状态机 | 将处理后的数据通过DMA写回DDR |
| 底层 | 嵌套子状态机 | 行扫描、像素计数等细粒度控制 |
这个架构的好处是:每个状态机只关心自己的事。DMA控制状态机只管搬数据,缩放处理状态机只管算像素,互不干扰。而且,由于DMA是独立工作的,数据搬运和像素处理可以「流水线」进行——DMA在搬下一行数据的同时,状态机在处理当前行。这样效率就上来了。
嗯,今天就讲到这里。这些内容看起来多,但说白了就是「拆」字诀——把复杂问题拆成简单问题,然后用状态机去控制每一个简单问题。你们回去可以试着用今天讲的思路,重新设计一下自己项目中的控制逻辑,相信会有不一样的收获。