《FPGA经典例程及解读--基于xilinx K325T平台》系列导航
本专栏主要针对与想学习FPGA的同学,从基础的点灯到之后的复杂功能实战例程,从入门到进阶,通过这些例程的学习和了解,希望可以帮助你从一个FPGA小白进阶到FPGA中级阶段,能够处理工作中大多数的FPGA使用场景。
本篇是该系列的第七篇内容
上一篇:FPGA例程(6):UART串口通讯协议解析_fpga串口通信程序详解-CSDN博客
下一篇:关注我,第一时间获取更新!!!
1 概述
上一篇我们介绍了UART串口的协议,本篇我们以UART串口接收程序为案例,说明一下状态机的写法。
2 状态机的结构简述
状态机的详细理论介绍,大家可以查看这一篇FPGA基础知识(六):状态机设计实战--从概念到可靠实现的完整指南-CSDN博客
2.1 状态机三要素
状态:不是简单的计数器,而是系统当前所处的"工作状态"
转移条件:状态变化的"触发事件",需要明确且无歧义
输出:在特定状态下产生的"动作响应”
2.2 状态机常见的的编码方式
- 顺序二进制编码:按00-01-10-11的顺序编码,资源最省,但多个状态位同时变化易产生毛刺
- 格雷码编码:按00-01-11-10的顺序编码,状态转化时只有一位变化,可靠性高,功耗低,但编码没有顺序二进制的方式直观
- 热编码:按0001-0010-0100-1000的方式编码,时序性能好,但需要更多的资源。
2.3 三段式状态机
经典模板如下:
// 第一段:状态寄存器(时序逻辑) always @(posedge clk or negedge rst_n) begin if (!rst_n) current_state <= IDLE; else current_state <= next_state; end // 第二段:下一状态逻辑(组合逻辑) always @(*) begin next_state = current_state; // 默认保持当前状态 case (current_state) IDLE: begin if (start_signal) next_state = START; end START: begin if (ready_signal) next_state = WORK; else if (timeout) next_state = IDLE; end WORK: begin if (done_signal) next_state = DONE; end DONE: begin next_state = IDLE; end default: next_state = IDLE; endcase end // 第三段:输出逻辑(Moore风格 - 时序逻辑) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin output_bus <= 'b0; control_sig <= 1'b0; end else begin // 默认输出值 output_bus <= 'b0; control_sig <= 1'b0; case (current_state) // Moore:输出只依赖于当前状态 START: control_sig <= 1'b1; WORK: begin output_bus <= work_data; control_sig <= 1'b1; end DONE: output_bus <= result_data; endcase end end为什么推荐这种结构?
时序清晰:状态注册和输出生成都在时钟沿完成
避免毛刺:组合逻辑只用于状态转移判断
综合友好:工具能够很好地进行时序分析和优化
3 UART串口接收模块程序设计
参考FPGA例程(6):UART串口通讯协议解析_fpga串口通信程序详解-CSDN博客的串口接收协议。
3.1 接收模块配置说明
串口接收模块是个参数可配置模块:
BAUD_DIV:依据波特率和采样时钟计算出的分频系数,例如采样时钟50MHz,我们希望波特率为115200bps,那BAUD_DIV = 50_000000 / 115200 = 434
D_WORD_NUM:数据位,我们使用的是8位,那这里就是8,有的时候会用到6位或者7位,修改这个值即可。
3.2 接收模块状态转移流程说明
接收模块状态机转移流程如下:
IDLE:是空闲状态,上电后进入IDLE状态,如果rx为低,我们认为接收到了串口的起始位,进入状态START
START:接收完起始位(即等待一个数据位的时间)后进入数据接收状态RECEIVE
RECEIVE:接收数据位,我们设定为8位,等待8位都接收完成之后进入STOP
STOP:停止位,之后再一次进入IDLE状态,等待下一包数据即可。
3.3 接收状态机设计
我们使用三段式状态机完成接收模块的设计
下面是第一段和第二段:主要控制状态机的状态转移逻辑,其中,
第一段是时序逻辑,内部使用<=;
第二段是组合逻辑,内部使用=;
这里一定要注意,在同一个always块中,绝对不可以混用阻塞赋值和非阻塞赋值。
// ======================================================= // 状态机定义 localparam IDLE = 4'd0; localparam START = 4'd1; localparam RECEIVE = 4'd2; localparam STOP = 4'd3; // --------- 控制状态机 reg [3:0] current_state,next_state; always @ (posedge clk or negedge rstn) begin if (!rstn) begin current_state <= IDLE; end else begin current_state <= next_state; end end // ----------- 主状态机 always @ (*) begin next_state = current_state; case(current_state) IDLE:begin if (!rx_c1) begin next_state = START; end end START:begin if(baud_clk)begin next_state = RECEIVE; end end RECEIVE:begin if ((bit_cnt == D_WORD_NUM-1 ) && (baud_cnt == BAUD_DIV-1)) begin // 已接收8位 next_state = STOP; end end STOP:begin next_state = IDLE; end default:begin next_state = IDLE; end endcase end第三段为状态输入输出逻辑,一般按照我们的功能分为独立的always块
例如1:接收数据位计数块,always块内部仅产生bit_cnt,当在RECEIVE状态下,每采样一次bit_cnt+1;
// ------------ 接收数据位计数 reg [3:0] bit_cnt; // 位计数器 always @ (posedge clk or negedge rstn) begin if (!rstn) begin bit_cnt <= 4'd0; end else begin if (baud_clk) begin if (current_state == START) begin bit_cnt <= 4'd0; end else if (current_state == RECEIVE) begin bit_cnt <= bit_cnt + 4'd1; end else begin bit_cnt <= bit_cnt; end end end end例如2:数据包有效的使能信号,当我们接收到rx的下降沿时,认为接收到有效数据,当我们接收够D_WORD_NUM的数据位后,认为该包数据接收完成,always块中仅产生start信号。
// ----------- 生成 单帧接收使能信号 reg start; // start有效时 才接收数据 always @ (posedge clk or negedge rstn) begin if (!rstn) begin start <= 1'b0; end else begin if (!rx_c0 & rx_c1) begin start <= 1'b1; end else if ((baud_cnt == BAUD_DIV-1) && (bit_cnt == D_WORD_NUM-1)) begin start <= 1'b0; end else begin start <= start; end end end最终的实现代码如下:
`timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // Company: // Engineer: // // Create Date: 2026/01/16 16:29:11 // Design Name: // Module Name: uart_rx // Project Name: // Target Devices: // Tool Versions: // Description: // // Dependencies: // // Revision: // Revision 0.01 - File Created // Additional Comments: // ////////////////////////////////////////////////////////////////////////////////// module uart_rx # ( parameter D_WORD_NUM = 4'd8, parameter BAUD_DIV = 16'd434 )( input clk, input rstn, input uart_rx_i, output [D_WORD_NUM-1:0] uart_rx_data_o, output uart_rx_done ); // parameter D_WORD_NUM = 8; // parameter BAUD_DIV = 16'd434; // ======================================================= // 状态机定义 localparam IDLE = 4'd0; localparam START = 4'd1; localparam RECEIVE = 4'd2; localparam STOP = 4'd3; // --------- 控制状态机 reg [3:0] current_state,next_state; always @ (posedge clk or negedge rstn) begin if (!rstn) begin current_state <= IDLE; end else begin current_state <= next_state; end end // ----------- 主状态机 always @ (*) begin next_state = current_state; case(current_state) IDLE:begin if (!rx_c1) begin next_state = START; end end START:begin if(baud_clk)begin next_state = RECEIVE; end end RECEIVE:begin if ((bit_cnt == D_WORD_NUM-1 ) && (baud_cnt == BAUD_DIV-1)) begin // 已接收8位 next_state <= STOP; end end STOP:begin next_state = IDLE; end default:begin next_state = IDLE; end endcase end // ========================================================== // ------------- 输入信号同步 reg rx_c0,rx_c1; always @ (posedge clk) begin rx_c0 <= uart_rx_i; rx_c1 <= rx_c0; end // ------------ 接收数据位计数 reg [3:0] bit_cnt; // 位计数器 always @ (posedge clk or negedge rstn) begin if (!rstn) begin bit_cnt <= 4'd0; end else begin if (baud_clk) begin if (current_state == START) begin bit_cnt <= 4'd0; end else if (current_state == RECEIVE) begin bit_cnt <= bit_cnt + 4'd1; end else begin bit_cnt <= bit_cnt; end end end end // ----------- 生成 单帧接收使能信号 reg start; // start有效时 才接收数据 always @ (posedge clk or negedge rstn) begin if (!rstn) begin start <= 1'b0; end else begin if (!rx_c0 & rx_c1) begin start <= 1'b1; end else if ((baud_cnt == BAUD_DIV-1) && (bit_cnt == D_WORD_NUM-1)) begin start <= 1'b0; end else begin start <= start; end end end // ------------ 波特率时钟生成 reg [15:0] baud_cnt; // 波特率计数器 always @(posedge clk or negedge rstn) begin if (!rstn) begin baud_cnt <= 16'd0; end else begin if (start) begin if (baud_cnt >= BAUD_DIV - 1) begin baud_cnt <= 16'd0; end else begin baud_cnt <= baud_cnt + 16'd1; end end else begin baud_cnt <= 16'd0; end end end wire baud_clk; // 波特率时钟 assign baud_clk = (baud_cnt == BAUD_DIV - 1) ? 1'b1:1'b0; // ------------ 串并转换 wire data_en ; assign data_en = (baud_cnt == BAUD_DIV >> 1)?1'b1:1'b0; reg [ D_WORD_NUM-1:0] rx_shift; // 接收移位寄存器 always @(posedge clk or negedge rstn) begin if (!rstn) begin rx_shift <= 8'd0; end else begin if (data_en && (current_state == RECEIVE)) begin rx_shift <= {rx_c1, rx_shift[ D_WORD_NUM-1:1]}; // 右移,LSB在前 end else begin rx_shift <=rx_shift; end end end // -------------- 数据锁存 reg [7:0] data; always @ (posedge clk or negedge rstn) begin if (!rstn) begin data <= 8'd0; end else begin if (current_state == STOP) begin data <= rx_shift; // 保存接收的数据 end else begin data <= data; end end end assign uart_rx_data_o = data; // ----------------- 生成一帧数据接收完成的标志 reg rx_done; always @ (posedge clk or negedge rstn) begin if (!rstn) begin rx_done <= 1'b0; end else begin if (current_state == STOP) begin rx_done <= 1'b1; end else begin rx_done <= 1'b0; end end end assign uart_rx_done = rx_done; // ========================================================== endmodule4 总结
本文以串口接收程序为案例,讲述了状态机的编写方式,我很推荐大家进行这样编写,因为这种每个always块中仅产生一个信号的编写方式能让我们更明确这个信号的时序,当出现问题或者bug的时候,我们也仅需要对一个always块进行分析即可。那下一篇我们继续来说一说串口发送程序。