FPGA例程(7):UART串口接收程序--状态机的编写

《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;
// ==========================================================


endmodule

4 总结

本文以串口接收程序为案例,讲述了状态机的编写方式,我很推荐大家进行这样编写,因为这种每个always块中仅产生一个信号的编写方式能让我们更明确这个信号的时序,当出现问题或者bug的时候,我们也仅需要对一个always块进行分析即可。那下一篇我们继续来说一说串口发送程序

相关推荐
FPGA小c鸡3 小时前
FPGA做AI:从入门到实战 | 边缘智能时代的硬件加速秘密
人工智能·fpga开发
思尔芯S2C3 小时前
思尔芯、MachineWare与Andes晶心科技联合推出RISC-V协同仿真方案,加速芯片开发
人工智能·科技·fpga开发·risc-v·prototyping
松涛和鸣6 小时前
63、IMX6ULL ADC驱动开发
c语言·arm开发·驱动开发·单片机·gpt·fpga开发
扣脑壳的FPGAer7 小时前
Xilinx Dual Quad Flash SPI加载方式下的文件处理
fpga开发
ShiMetaPi10 小时前
GM-3568JHF丨ARM+FPGA异构开发板应用开发教程:07 FPGA FSPI 通信案例
arm开发·fpga开发·rk3568
hoiii18719 小时前
16APSK/32APSK调制解调MATLAB仿真实现
开发语言·matlab·fpga开发
我爱C编程20 小时前
【硬件片内测试】基于FPGA的BPSK扩频通信链路测试,包含帧同步,定时点,扩频伪码同步,信道,误码统计
fpga开发·帧同步·定时点·扩频伪码同步·bpsk扩频
runningshark20 小时前
【FPGA】使用高云FPGA与stm32进行FMC通信协议
fpga开发
最遥远的瞬间2 天前
四、Xilinux在线调试方法和XADC的使用
fpga开发