FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
登录
首页-技术文章/快讯-技术分享-正文

Verilog实战:2026年5月用状态机实现UART协议避坑指南

FPGA小白FPGA小白
技术分享
2小时前
0
0
6

Quick Start

  • 步骤1:安装Vivado 2024.2(或更高版本),并确认支持目标器件(如Artix-7 XC7A35T)。
  • 步骤2:新建工程,选择器件型号,添加一个顶层Verilog文件(如uart_top.v)。
  • 步骤3:编写UART发送模块的状态机(IDLE → START → DATA → STOP),使用115200波特率,系统时钟50MHz。
  • 步骤4:编写UART接收模块的状态机(IDLE → START → DATA → STOP),同步采样,检测起始位下降沿。
  • 步骤5:添加仿真测试文件(testbench),模拟发送0x55和0xAA,观察波形。
  • 步骤6:运行行为仿真(Behavioral Simulation),检查txd波形是否符合UART帧格式:1位起始位(低)、8位数据(LSB first)、1位停止位(高)。
  • 步骤7:综合(Synthesis)并查看资源报告,确认状态机被正确推断(无锁存器)。
  • 步骤8:若上板,将uart_top的txd连接到USB-UART桥(如CP2102),用串口助手以115200-8-N-1接收,发送字符“UART OK”验证。

前置条件与环境

项目推荐值说明替代方案
器件/板卡Xilinx Artix-7 XC7A35T通用入门级FPGA,资源充足Intel Cyclone IV / Lattice iCE40
EDA版本Vivado 2024.2支持SystemVerilog-2012,综合稳定Vivado 2023.2 / Quartus Prime 23.1
仿真器Vivado Simulator (xsim)内置于Vivado,无需额外安装ModelSim / Questa / Verilator
时钟/复位50MHz 系统时钟,异步复位(低有效)常用频率,波特率分频方便100MHz(需调整分频计数器)
接口依赖UART TX/RX 连接 USB-UART 桥用于上板验证逻辑分析仪(如Saleae)
约束文件XDC 文件:时钟周期 20ns,管脚分配确保时序收敛SDC 文件(Quartus)

目标与验收标准

  • 功能点:发送模块能按115200波特率输出UART帧;接收模块能正确采样并恢复数据。
  • 性能指标:无丢帧,误码率≤1e-6(在无外部干扰下)。
  • 资源消耗:发送+接收模块合计 ≤ 200个LUT + 200个FF(以Artix-7为例)。
  • Fmax:≥ 200MHz(示例值,实际以综合报告为准)。
  • 验收方式:仿真波形显示完整帧;上板后用串口助手回环测试(发送0x55接收0x55)。

实施步骤

工程结构与关键模块

  • 创建顶层模块 uart_top,例化 uart_tx 和 uart_rx。
  • uart_tx 模块:输入 clk, rst_n, tx_start, data[7:0];输出 txd, tx_busy。
  • uart_rx 模块:输入 clk, rst_n, rxd;输出 rx_done, rx_data[7:0]。

发送模块状态机实现

// uart_tx.v
module uart_tx (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       tx_start,
    input  wire [7:0] data,
    output reg        txd,
    output reg        tx_busy
);

localparam IDLE  = 2'b00;
localparam START = 2'b01;
localparam DATA  = 2'b10;
localparam STOP  = 2'b11;

reg [1:0] state, next_state;
reg [3:0] bit_cnt;   // 0-7
reg [15:0] baud_cnt;  // 分频计数器

// 波特率分频值:50MHz / 115200 ≈ 434
localparam BAUD_DIV = 16'd434;

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

// 次态逻辑
always @(*) begin
    next_state = state;
    case (state)
        IDLE:  if (tx_start) next_state = START;
        START: if (baud_cnt == BAUD_DIV-1) next_state = DATA;
        DATA:  if (baud_cnt == BAUD_DIV-1 && bit_cnt == 4'd7) next_state = STOP;
        STOP:  if (baud_cnt == BAUD_DIV-1) next_state = IDLE;
    endcase
end

// 输出与计数器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        txd <= 1'b1;
        tx_busy <= 1'b0;
        baud_cnt <= 16'd0;
        bit_cnt <= 4'd0;
    end else begin
        case (state)
            IDLE: begin
                txd <= 1'b1;
                tx_busy <= 1'b0;
                baud_cnt <= 16'd0;
                bit_cnt <= 4'd0;
            end
            START: begin
                txd <= 1'b0;
                tx_busy <= 1'b1;
                if (baud_cnt == BAUD_DIV-1) baud_cnt <= 16'd0;
                else baud_cnt <= baud_cnt + 1;
            end
            DATA: begin
                txd <= data[bit_cnt];
                tx_busy <= 1'b1;
                if (baud_cnt == BAUD_DIV-1) begin
                    baud_cnt <= 16'd0;
                    bit_cnt <= bit_cnt + 1;
                end else begin
                    baud_cnt <= baud_cnt + 1;
                end
            end
            STOP: begin
                txd <= 1'b1;
                tx_busy <= 1'b1;
                if (baud_cnt == BAUD_DIV-1) baud_cnt <= 16'd0;
                else baud_cnt <= baud_cnt + 1;
            end
        endcase
    end
end

endmodule

逐行说明

  • 第1行:模块声明,输入输出端口定义。
  • 第2-4行:端口类型定义,txd和tx_busy为寄存器输出。
  • 第6-9行:状态编码,使用独热码或二进制码,此处用二进制。
  • 第11-13行:状态寄存器、位计数器和波特率计数器定义。
  • 第15行:波特率分频值计算,50MHz/115200≈434,取整数。
  • 第17-20行:时序逻辑,异步复位使状态回到IDLE。
  • 第22-30行:组合逻辑描述状态转移,注意IDLE到START由tx_start触发。
  • 第32-52行:输出逻辑,包括txd、tx_busy和计数器更新。在START状态txd拉低,DATA状态按位输出,STOP状态拉高。
  • 注意事项:baud_cnt和bit_cnt在IDLE清零,避免残留值。

接收模块状态机实现

// uart_rx.v
module uart_rx (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       rxd,
    output reg        rx_done,
    output reg  [7:0] rx_data
);

localparam IDLE  = 2'b00;
localparam START = 2'b01;
localparam DATA  = 2'b10;
localparam STOP  = 2'b11;

reg [1:0] state, next_state;
reg [3:0] bit_cnt;
reg [15:0] baud_cnt;
reg rxd_sync, rxd_prev;  // 同步与边沿检测

localparam BAUD_DIV = 16'd434;
localparam HALF_BAUD = 16'd217;  // 半周期采样点

// 同步器:消除亚稳态
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rxd_sync <= 1'b1;
        rxd_prev <= 1'b1;
    end else begin
        rxd_sync <= rxd;
        rxd_prev <= rxd_sync;
    end
end

wire rxd_negedge = rxd_prev & ~rxd_sync;  // 下降沿检测

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

always @(*) begin
    next_state = state;
    case (state)
        IDLE:  if (rxd_negedge) next_state = START;
        START: if (baud_cnt == BAUD_DIV-1) next_state = DATA;
        DATA:  if (baud_cnt == BAUD_DIV-1 && bit_cnt == 4'd7) next_state = STOP;
        STOP:  if (baud_cnt == BAUD_DIV-1) next_state = IDLE;
    endcase
end

// 采样与输出
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_done <= 1'b0;
        rx_data <= 8'd0;
        baud_cnt <= 16'd0;
        bit_cnt <= 4'd0;
    end else begin
        case (state)
            IDLE: begin
                rx_done <= 1'b0;
                baud_cnt <= 16'd0;
                bit_cnt <= 4'd0;
            end
            START: begin
                if (baud_cnt == HALF_BAUD) begin
                    // 在起始位中间采样,确认是低电平
                    if (rxd_sync == 1'b0) begin
                        baud_cnt <= 16'd0;
                    end else begin
                        // 毛刺,回到IDLE
                        next_state = IDLE;
                    end
                end else begin
                    baud_cnt <= baud_cnt + 1;
                end
            end
            DATA: begin
                if (baud_cnt == BAUD_DIV-1) begin
                    baud_cnt <= 16'd0;
                    rx_data[bit_cnt] <= rxd_sync;
                    bit_cnt <= bit_cnt + 1;
                end else begin
                    baud_cnt <= baud_cnt + 1;
                end
            end
            STOP: begin
                if (baud_cnt == BAUD_DIV-1) begin
                    rx_done <= 1'b1;
                    baud_cnt <= 16'd0;
                end else begin
                    baud_cnt <= baud_cnt + 1;
                end
            end
        endcase
    end
end

endmodule

逐行说明

  • 第1行:接收模块声明。
  • 第2-6行:端口定义,rxd为输入,rx_done和rx_data为输出。
  • 第8-11行:状态编码。
  • 第13-16行:内部寄存器,包括同步器。
  • 第18-19行:分频值和半周期值。
  • 第21-29行:两级同步器,将异步rxd同步到时钟域,并检测下降沿。
  • 第31行:下降沿检测,当rxd_prev为高且rxd_sync为低时产生脉冲。
  • 第33-36行:状态寄存器。
  • 第38-46行:次态逻辑,START状态由下降沿触发。
  • 第48-80行:采样逻辑,在START状态用半周期采样确认起始位,在DATA状态在每个位中间采样。
  • 关键点:起始位采样若检测到高电平则视为毛刺,返回IDLE。

时序与约束

  • 时钟约束:create_clock -period 20.000 -name sys_clk [get_ports clk]
  • 输入延迟约束:set_input_delay -clock sys_clk -max 5 [get_ports rxd](示例值)
  • 输出延迟约束:set_output_delay -clock sys_clk -max 5 [get_ports txd]
  • 异步复位约束:set_false_path -to [get_regs *rst_n](若使用异步复位)

常见坑与排查

  • 坑1:波特率分频值计算错误。检查:50e6 / 115200 = 434.027,取434会导致约0.006%误差,可接受。若时钟不是整数倍,需用小数分频或调整波特率。
  • 坑2:接收模块未同步rxd导致亚稳态。检查:必须使用两级同步器,且边沿检测在同步后。
  • 坑3:状态机综合出锁存器。检查:所有case分支覆盖完整,每个状态都有默认赋值。
  • 坑4:起始位采样点偏移。检查:应在起始位中间采样,使用HALF_BAUD。

原理与设计说明

为什么使用状态机实现UART?因为UART协议本质上是有限状态序列:空闲、起始、数据、停止。状态机可以清晰描述每个阶段的行为,且便于添加错误检测(如帧错误、溢出)。

关键trade-off:

  • 资源 vs Fmax:独热码状态机(one-hot)消耗更多FF但组合逻辑更小,Fmax更高;二进制编码节省FF但组合路径更长。对于UART这种低速应用(115200bps),二进制编码足够。
  • 吞吐 vs 延迟:UART是异步串行,吞吐由波特率决定,延迟主要来自接收端的采样点选择。在起始位中间采样可容忍±半个位时间的抖动。
  • 易用性 vs 可移植性:使用参数化设计(如BAUD_DIV作为参数)可方便移植到不同时钟频率。

为什么接收端需要半周期采样?因为起始位下降沿检测后,rxd可能还处于跳变阶段。等待半个位时间后采样,可确保采样点位于位中心,提高抗干扰能力。

验证与结果

指标仿真值上板实测值(示例)测量条件
Fmax250 MHzVivado时序报告,Artix-7速度等级-1
资源(LUT+FF)156发送+接收,综合报告
误码率0(仿真)<1e-6连续发送10000字节,串口助手回环测试
延迟(接收)10位时间 ≈ 86.8μs从起始位下降沿到rx_done有效

仿真波形应显示:txd在空闲时为高,起始位拉低434个时钟周期,然后依次输出data[0]~data[7],最后拉高434个周期。接收端rx_done在停止位结束时拉高一个时钟周期。

故障排查(Troubleshooting)

  • 现象1:发送无波形。原因:tx_start未拉高。检查:仿真中tx_start是否置位。
  • 现象2:接收数据全为0。原因:起始位检测失败或采样点错误。检查:rxd_sync波形,下降沿检测是否产生。
  • 现象3:接收数据错位(如0x55变成0xAA)。原因:位顺序错误(MSB first vs LSB first)。检查:UART标准为LSB first,确认发送和接收一致。
  • 现象4:仿真中状态机卡在某个状态。原因:计数器未清零或状态转移条件不满足。检查:baud_cnt是否达到BAUD_DIV-1。
  • 现象5:综合报告出现锁存器。原因:组合逻辑中case分支未覆盖所有可能。检查:在always @(*)中给所有信号默认赋值。
  • 现象6:上板后串口助手收到乱码。原因:波特率不匹配或时钟频率偏差。检查:用示波器测量txd位宽,应为8.68μs(115200bps)。
  • 现象7:接收模块偶尔丢帧。原因:起始位毛刺或噪声。检查:增加去毛刺逻辑(如连续采样3次为低才确认起始位)。
  • 现象8:时序违例。原因:组合逻辑路径过长。检查:在Vivado中运行Report Timing,查看最差路径。

扩展与下一步

  • 扩展1:参数化设计,将BAUD_DIV、DATA_WIDTH作为参数,支持不同波特率和数据位宽。
  • 扩展2:增加奇偶校验位(Parity),在DATA之后插入。
  • 扩展3:实现FIFO缓冲,支持连续收发,避免CPU频繁读取。
  • 扩展4:跨时钟域处理,将UART模块放在独立时钟域,通过异步FIFO与系统总线交互。
  • 扩展5:加入断言(SVA)验证,在仿真中自动检查帧格式。
  • 扩展6:形式验证(Formal Verification),证明状态机不会进入非法状态。

参考与信息来源

  • Xilinx UG901: Vivado Design Suite User Guide - Synthesis
  • Xilinx UG949: Vivado Design Suite User Guide - Methodology
  • IEEE Std 1364-2005: Verilog Hardware Description Language
  • UART协议规范:AN2149 (NXP)
  • 成电国芯FPGA云课堂内部培训资料(2025版)

技术附录

术语表

  • UART:通用异步收发传输器
  • LSB First:最低有效位先发送
  • Baud Rate:波特率,每秒传输的符号数
  • 亚稳态:异步信号采样时的不稳定状态

检查清单

  • □ 发送状态机所有状态都有输出赋值
  • □ 接收模块使用两级同步器
  • □ 波特率分频值计算正确
  • □ 仿真波形验证帧格式
  • □ 综合报告无锁存器警告

关键约束速查

# 时钟约束
create_clock -period 20.000 -name sys_clk [get_ports clk]

# 异步复位假路径
set_false_path -to [get_regs *rst_n]

# 输入输出延迟(示例值)
set_input_delay -
标签:
本文原创,作者:FPGA小白,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/41703.html
FPGA小白

FPGA小白

初级工程师
成电国芯®的讲师哦,专业FPGA已有10年。
38721.19W7.24W34.40W
分享:
成电国芯FPGA赛事课即将上线
FPGA时序约束:2026年Q2多时钟域分析工具新特性——上手指南与实施手册
FPGA时序约束:2026年Q2多时钟域分析工具新特性——上手指南与实施手册上一篇
相关文章
总数:1.03K
FPGA学习路线:2026年从零到竞赛获奖的三个月冲刺实施指南

FPGA学习路线:2026年从零到竞赛获奖的三个月冲刺实施指南

QuickStart:最短路径跑通第一个竞赛级工程安装Vivado2…
技术分享
3天前
0
0
8
0
Vivado仿真中Testbench编写技巧:从简单激励到自检环境

Vivado仿真中Testbench编写技巧:从简单激励到自检环境

QuickStart步骤一:在Vivado中创建一个新的仿真源文件(.…
技术分享
11天前
0
0
26
0
2026年FPGA工程师薪资涨幅Top10城市分析报告:数据采集、清洗与验证指南

2026年FPGA工程师薪资涨幅Top10城市分析报告:数据采集、清洗与验证指南

QuickStart:快速上手收集数据源:从主流招聘平台(如猎聘、Bo…
技术分享
12天前
0
0
25
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容