ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA UART通信模块设计与Verilog实现指南

FPGA UART通信模块设计与Verilog实现指南 简介基于FPGA的UART串口通信设计与实现代码包面向FPGA初学者、嵌入式系统开发者和数字逻辑设计人员适用于学习异步串口通信原理并解决FPGA平台上的串口收发工程实现问题。压缩包共167个文件约2.09MB核心包含VHDL源码、Verilog的.v文件、UCF引脚约束、ISE工程配置、bit下载文件以及综合仿真生成的ngc、xrpt、html报告等辅助文件覆盖从代码设计、时序约束到板级验证的完整链路。目前已有304人学习下载属于体量紧凑、可直接借鉴的工程参考。代码经过实验验证内含波特率发生器、发送模块、接收模块、状态机控制、奇偶校验与错误检测等关键逻辑并对数据采样点选择、时钟同步等设计要点有清晰体现配有必要的调试说明。无论是用于课程设计、竞赛准备还是作为二次开发的基础模板都能帮助快速掌握FPGA UART通信的完整实现方法。1. FPGA 工程的第一个外设为什么是 UARTFPGA 工程的第一个外设几乎是 UART。不管是 Xilinx 还是 Altera 的板卡上电后的第一件事多半是让串口能打印“Hello FPGA”或者把一组采样数据挪到 PC 上去。原因很简单UART 协议足够简单不需要复杂时钟恢复接口只有一根发送线、一根接收线占用的 IO 和逻辑资源都极少。但真正动手写fpga uart代码时很多问题会冒出来起始位判断得太早导致采样到毛刺、停止位没校验导致乱码、波特率分频取整导致高波特率下误码、testbench 里对着波形数了半天天数才确认发送时序。这篇内容会按“协议要点 → 接收实现 → 发送实现 → 验证排错”的顺序把一套可综合、可复现的uart verilog代码讲清楚参数的边界和坑也一并说透适合把 UART 当作学习模板、以及要在工程里快速搭日志通道的 FPGA 开发者。2. UART 协议与 FPGA 时钟域的采样策略2.1 一帧数据的位结构从起始位到停止位UART 是异步串行协议收发双方没有共享时钟靠约定波特率和对线电平的采样来还原数据。空闲状态下 TX 和 RX 线保持高电平发送方拉低电平至少一个位时间表示起始位开始。然后是 5 到 8 个数据位按 LSB 先行的顺序排列后面可以跟一个可选的奇偶校验位最后拉高电平持续至少一个位时间作为停止位。一帧结束线恢复高电平等待下一帧。器件间通信时最容易出问题的是参数不匹配。常见的组合是 8 数据位、无校验、1 停止位也就是配置里经常看到的8N1。实际工程中还会遇到 7 数据位加偶校验、或者 2 位停止位的车载设备。FPGA 侧代码里如果只把数据位写死为 8 位遇到 7 位数据帧会把校验位当成数据读进来出现偶发的错值且不好定位。参数化设计里把DATA_WIDTH作为可配置项比后期改状态机省事得多。停止位在高电平期间发送方至少保持一个位时间但很多设备会保持更久所以接收方只要在停止位时刻采到高电平就算帧有效。这也是 UART 容错性的来源它不是靠精确的相位对齐而是靠起始位下降沿重新同步每帧都重新对齐一次时钟偏差不会累积到下一帧。理解这一点才能明白为什么采样点要落在位中间而不是在位的边界附近。2.2 波特率分频整数分频与误差上限FPGA 侧实现 UART 的核心是把系统时钟分频成位时钟。假设系统时钟是 100 MHz目标波特率是 115200那么每个位的时间是系统时钟周期的多少倍计算方式是系统时钟频率 / 波特率也就是100_000_000 / 115_200 868.05。分频计数器无法处理小数取 868 个时钟周期为一个位时间实际波特率为100_000_000 / 868 115207.4误差约 0.006%远低于 UART 协议允许的 2% 误差上限。这个精度在实际通信里完全没有问题。但把场景换到较低的系统时钟和高波特率误差就会迅速放大。例如 25 MHz 系统时钟跑 1 Mbps 波特率分频值算出来是 25取整后没有误差但系统时钟本身若有 50 ppm 的晶振偏差叠加后可能接近接收端容忍极限。另一个常见场景是 50 MHz 时钟跑 921600 波特率分频值为 54.25取 54 后实际波特率约为 925926误差 0.47%仍在 2% 以内但如果有其他设备端时钟偏差叠加接收的误码率就会上升。表格中给出几组典型组合的误差对比。系统时钟目标波特率理论分频值取整分频实际波特率误差100 MHz115200868.058681152070.006%50 MHz115200434.034341152070.006%50 MHz92160054.25549259260.47%25 MHz115200217.012171152070.006%25 MHz92160027.13279259260.47%从表格能看出分频值越小取整误差的影响越大。所以设计代码时DIV_CNT_MAX CLK_FREQ / BAUD_RATE这个参数必须用足够宽的位宽来承载不然在低波特率下计数器会溢出。常见的一个代码坑是计数器位宽只留了 16 位100 MHz 时钟跑 9600 波特率时需要 10416 个周期的计数16 位刚好够但如果换成 200 MHz 时钟跑 9600需要 2083316 位仍然够跑到 1200 波特率时需要 83333此时 16 位溢出UART 直接变成乱码而且仿真里因为时间太短还不容易发现。2.3 中点采样时间基准对齐逻辑接收端的核心问题不是生成波特率而是确定“现在采样的是哪一位”。UART 没有时钟线接收端能依赖的只有 RX 线上的下降沿。所以接收模块的典型流程是空闲态检测到 RX 拉低判定为起始位到来此时启动一个计数器等待半个位时间也就是DIV_CNT_MAX / 2再对 RX 采样一次确认这一位确实是低电平而不是毛刺确认成功后从起始位的中点到第一个数据位的中点正好经过一个位周期所以之后再计数满一个DIV_CNT_MAX就采样一次依次采完数据位和停止位。为什么要在位的中间采样因为数据变化沿附近电平可能处于不稳定状态如果采样点太靠近上升沿或下降沿亚稳态或者线路上反射造成的振铃会直接让结果错误。位中间是电平最稳定的位置允许一定程度的时钟偏差。正因如此FPGA 代码里通常不追求在边沿精确采样而是让采样点尽量落在位的中心位置。起始位检测完之后的第一次等待是HALF_BIT DIV_CNT_MAX / 2后续每次都是DIV_CNT_MAX这是一个非常关键的设计细节。DIV_CNT_MAX为奇数时/2会取整半位时间的误差在一个系统时钟周期内对整体采样精度几乎没有影响。但需要留意实现方式不要直接写localparam HALF_BIT DIV_CNT_MAX / 2;时把DIV_CNT_MAX用成 real 类型综合工具不支持浮点参数。Verilog 里整数除法直接舍去小数部分写parameter integer CLK_FREQ 100_000_000即可不需要额外处理。采样策略定好之后接收模块的状态机结构基本就固定了空闲态等待下降沿起始态确认低电平数据态按位移位存储停止态校验高电平并输出结果。这套逻辑在所有 FPGA 平台上都一样不管是 Xilinx Vivado 还是 Altera Quartus代码可以直接迁移。3. 用 Verilog 写出可综合的 UART 接收模块3.1 模块接口与参数化配置接收模块的接口设计要考虑两个使用场景直接给上层逻辑提供移位后的并行数据以及给调试总线提供带有效标志的读取接口。参数化波特率是必须的因为板卡上的系统时钟可能不同代码里把时钟频率和波特率做成参数换板子时只需要改实例化的参数不需要动状态机内部逻辑。module uart_rx #( parameter CLK_FREQ 100_000_000, parameter BAUD_RATE 115_200, parameter DATA_WIDTH 8 )( input wire clk, input wire rst_n, input wire rx, output reg [DATA_WIDTH-1:0] rx_data, output reg rx_valid );rx_data在停止位采样确认后更新rx_valid同时拉高一个时钟周期作为数据有效的脉冲信号。接口只用了最基本的信号没有做 FIFO 握手这样逻辑最透明方便初学者对照状态理解。如果工程里要对接 AXI-Stream 或者其他总线可以在外层包一层 FIFO 或者握手逻辑接收模块本身保持纯粹。参数设计上CLK_FREQ和BAUD_RATE的单位都是整数赫兹。常见错误是把 100 MHz 写成100而忽略了后面的单位导致 counter 计数时长变成原来的百万分之一仿真时看起来波形正常但实际时序一塌糊涂。建议在代码里加注释明确单位比如// system clock frequency in Hz。3.2 接收状态机的核心代码接收状态机的三个主要状态是空闲、起始位确认和数据采集。停止位不需要单独作为核心状态存在可以在数据移位完成后直接校验停止位然后回空闲。计数器cnt负责位时间计时bit_cnt负责记录当前采集的是第几个数据位。localparam IDLE 3d0; localparam START 3d1; localparam DATA 3d2; localparam STOP 3d3; localparam DIV_CNT_MAX CLK_FREQ / BAUD_RATE; localparam HALF_BIT DIV_CNT_MAX / 2; reg [15:0] cnt; reg [ 2:0] bit_cnt; reg [DATA_WIDTH-1:0] shift_reg; reg [ 2:0] state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; cnt 16d0; bit_cnt 3d0; shift_reg {DATA_WIDTH{1b0}}; rx_data {DATA_WIDTH{1b0}}; rx_valid 1b0; end else begin rx_valid 1b0; case (state) IDLE: begin cnt 16d0; if (rx 1b0) begin state START; end end START: begin if (cnt HALF_BIT - 1) begin if (rx 1b0) begin state DATA; cnt 16d0; bit_cnt 3d0; end else begin state IDLE; end end else begin cnt cnt 16d1; end end DATA: begin if (cnt DIV_CNT_MAX - 1) begin cnt 16d0; shift_reg {rx, shift_reg[DATA_WIDTH-1:1]}; if (bit_cnt DATA_WIDTH - 1) begin state STOP; bit_cnt 3d0; end else begin bit_cnt bit_cnt 3d1; end end else begin cnt cnt 16d1; end end STOP: begin if (cnt DIV_CNT_MAX - 1) begin if (rx 1b1) begin rx_data shift_reg; rx_valid 1b1; end state IDLE; end else begin cnt cnt 16d1; end end endcase end end这段代码的关键点在移位方向。UART 是 LSB 先行所以每采集一个 bit把它放到移位寄存器的高位还是低位决定了最终数据的字节序。我习惯用shift_reg {rx, shift_reg[DATA_WIDTH-1:1]}也就是每次把新采到的 bit 放在最高位数据位全部采完后shift_reg里的内容正好是 MSB 在前直接赋值给rx_data就是正确的数值。如果把方向反了收到的字节会是位反转的比如上位机发 0x55FPGA 收到 0xAA这是新手最容易犯的字节序错误。起始位确认的逻辑在START状态里先等半个位周期采样 RX 仍然是低才认为是真正的起始位。如果采样到高说明刚才的下降沿只是毛刺直接回空闲。这个机制能有效过滤窄脉冲干扰代价是起始位检测的响应时间延后了半个位周期但这个延迟不会累积因为每帧都重新同步。3.3 停止位校验与数据有效脉冲停止位状态里在位的中间采样 RX如果采到高电平说明这一帧格式正确输出数据如果采到低电平说明对端设备发送了错误的停止位常见原因是波特率不匹配也可能是线路被拉低。这段代码里验到错误只是丢弃数据不回传错误信号。在调试初期这会带来一个困惑图省事把停止位判断删掉数据照常输出结果误码率会明显增高而且看起来毫无规律。rx_valid只拉高一个周期是为了方便上层逻辑直接用它做写使能。如果上层需要一个持续高电平的有效信号可以用rx_valid作为 FIFO 的写使能或者自己打一拍生成锁存信号。在仿真里检查这段逻辑时最有效的观测点是停止位采样之后的rx_valid是否与数据值对齐。如果rx_valid提前或者延后了一个周期问题几乎都出在状态切换与计数器的时序关系上。停止位状态结束后一定要回到IDLE但如果对端连续发送两个字节第二个字节的起始位紧跟着第一个字节的停止位此时回到IDLE后需要立即检测到 RX 的低电平所以IDLE状态不能有额外的等待延迟计数器需要在停止位结束后立即清零。代码里state IDLE和cnt的复位是同一个时钟沿可以保证这种连续性。4. 发送模块与 testbench 回环验证4.1 发送状态机的移位输出写法发送逻辑比接收简单因为发送方知道自己什么时刻发送不需要检测边沿只需要在启动信号到来时把数据移位输出到 TX 线。发送端的核心接口有三个tx_data写入待发送的数据、tx_start拉高一个周期启动发送、tx_busy指示发送忙防止上层重复写入。module uart_tx #( parameter CLK_FREQ 100_000_000, parameter BAUD_RATE 115_200, parameter DATA_WIDTH 8 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] tx_data, input wire tx_start, output reg tx, output reg tx_busy ); localparam DIV_CNT_MAX CLK_FREQ / BAUD_RATE; localparam IDLE 2d0; localparam START 2d1; localparam DATA 2d2; localparam STOP 2d3; reg [15:0] cnt; reg [ 2:0] bit_cnt; reg [DATA_WIDTH-1:0] shift_reg; reg [ 1:0] state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; tx 1b1; tx_busy 1b0; cnt 16d0; bit_cnt 3d0; shift_reg {DATA_WIDTH{1b0}}; end else begin case (state) IDLE: begin tx 1b1; if (tx_start) begin state START; tx_busy 1b1; shift_reg tx_data; cnt 16d0; end end START: begin tx 1b0; if (cnt DIV_CNT_MAX - 1) begin cnt 16d0; state DATA; bit_cnt 3d0; end else begin cnt cnt 16d1; end end DATA: begin tx shift_reg[0]; if (cnt DIV_CNT_MAX - 1) begin cnt 16d0; shift_reg {1b0, shift_reg[DATA_WIDTH-1:1]}; if (bit_cnt DATA_WIDTH - 1) begin state STOP; end else begin bit_cnt bit_cnt 3d1; end end else begin cnt cnt 16d1; end end STOP: begin tx 1b1; if (cnt DIV_CNT_MAX - 1) begin cnt 16d0; tx_busy 1b0; state IDLE; end else begin cnt cnt 16d1; end end endcase end end endmodule数据移位逻辑是每次输出shift_reg[0]然后把寄存器右移一位。shift_reg {1b0, shift_reg[DATA_WIDTH-1:1]}与接收模块的移位写法互为镜像接收端是左移采入发送端是右移输出。逻辑上发送数据的 LSB 会最先到 TX 线上正好对应对端接收时最先采到的数据位。tx_busy在tx_start有效后的下一个时钟沿才拉高所以上层逻辑不能把tx_start持续拉高多个周期否则会重复触发发送。工程里常见做法是检测到tx_busy为低之后再把tx_start拉高一个周期。如果想省一个握手反馈信号可以在发送模块内部把tx_start存入一个标志等 STOP 结束后自动清零但这样就让上层失去了发送完成的指示调试时不方便。4.2 testbench 模拟接收端回环测试写完发送和接收模块验证方式是把发送模块的 TX 输出直接连接到接收模块的 RX 输入形成一个自发自收的回环。这种方式不需要外部串口调试工具纯仿真就能验证两个模块的配合是否正常。testbench 里需要产生一个复位信号、一个发送启动脉冲然后观测rx_data和rx_valid。module uart_loopback_tb; reg clk 0; reg rst_n 0; reg tx_start 0; reg [7:0] test_data 8h55; wire tx_line; wire [7:0] rx_data; wire rx_valid; uart_tx dut_tx( .clk (clk), .rst_n (rst_n), .tx_data (test_data), .tx_start (tx_start), .tx (tx_line), .tx_busy () ); uart_rx dut_rx( .clk (clk), .rst_n (rst_n), .rx (tx_line), .rx_data (rx_data), .rx_valid (rx_valid) ); always #5 clk ~clk; initial begin repeat (10) (posedge clk); rst_n 1b1; repeat (10) (posedge clk); tx_start 1b1; (posedge clk); tx_start 1b0; wait (rx_valid 1b1); $display(RX data 0x%02x, rx_data); if (rx_data test_data) $display(PASS); else $display(FAIL); $finish; end endmodule这个 testbench 的核心技巧是用wait (rx_valid 1b1)让仿真自动停在接收完成的位置而不是手工数仿真时间。因为 UART 发送一帧数据在 115200 波特率下大约需要 87 微秒在仿真器里这个时间对应大量时钟周期手工计算停止时间很容易出错。用wait语句直接把关注点聚焦在结果上。回环测试只能验证模块之间的时序匹配验证不了外部的波特率偏差和线路噪声所以上板后还要用真实串口工具做一轮抓包确认。仿真里 PASS 不等于板上通信可靠需要对端设备做实际数据传输。4.3 上板联调的排查清单板级联调时如果串口输出乱码或者完全没有输出按下面顺序排查。先确认上位机串口工具的参数与 FPGA 代码参数一致数据位 8、无校验、停止位 1、波特率与BAUD_RATE参数相同。参数不匹配的典型症状是输出乱码但偶尔能收到正确字符因为帧错位后偶尔会对齐。再检查外部时钟频率是否与代码里的CLK_FREQ一致。用 PLL 分频出 100 MHz 和直接用板载 100 MHz 晶振代码里的参数也许相同但如果实际时钟差了 1 MHz115200 波特率下误差会接近 1%。还要求确认 UART 接收线是否接反了 TX 和 RX板上丝印的 TX 是相对板载 USB 转串口芯片而言的FPGA 的 TX 要接芯片的 RX。这个接线错误最隐蔽因为回环测试测不出来只有上板后才暴露。如果数据和预期存在位反转也就是收到的字节 bit7 和 bit0 互换优先检查发送模块的移位方向。接收模块的shift_reg {rx, shift_reg[DATA_WIDTH-1:1]}与发送模块的tx shift_reg[0]必须匹配任何一个写反了字节序就反了而且代码检查和仿真都不容易发现因为回环测试里收发端同时写反的话数值又会变回正确值。5. 让 UART 更可靠三个实测技巧5.1 异步复位同步释放FPGA 的复位信号通常来自按键或者外部控制器属于异步信号。直接拿它做状态机的异步复位容易在复位释放时产生亚稳态导致状态机进入未定义状态。常见的处理办法是先用两级触发器同步复位信号再把同步后的信号用作模块的复位输入。这个操作在 UART 模块中看起来微不足道但整个工程里所有外设模块都依赖同一个复位树UART 模块如果在复位释放时跑飞串口日志会丢失启动信息。reg rst_sync_1, rst_sync_2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rst_sync_1 1b0; rst_sync_2 1b0; end else begin rst_sync_1 1b1; rst_sync_2 rst_sync_1; end end wire rst_sync ~rst_sync_2;同步后的复位信号rst_sync可以作为 UART 模块的rst_n输入。注意两级同步器本身也需要异步复位这样才能保证复位沿到来时两级寄存器都能被可靠清掉。写同步器时的一个常见错误是只同步了复位信号没有同步按键引入的其他外部信号这在 RX 线直接接到板卡扩展口时会留下隐患。5.2 接收线毛刺过滤SPI 和 I2C 这类同步协议有主设备提供的时钟作为采样基准UART 没有这个条件。当 RX 线上出现窄脉冲毛刺时接收模块的起始位检测有可能把毛刺误认为起始位导致接收一帧全部错位。除了在START状态加半位采样确认之外另一个实用做法是连续采样三个系统时钟周期检测到 RX 三次都是低电平才确认下降沿有效。这个方法的代价是增加了几个周期的检测延迟但能滤掉绝大多数短于三个时钟周期的干扰脉冲。毛刺过滤在低速传输、线缆比较长的场景下尤其重要比如 UART 线从设备间穿过整个机箱。如果 FPGA 的 RX 引脚附近有开关电源或者电机驱动线路上的感应噪声会让误码率明显上升。配合外部硬件上拉电阻以及 PCB 走线远离电源区软件层面的多重采样能把残余干扰基本消除。5.3 帧错误与溢出标志停止位校验失败说明发生了帧错误常见原因是波特率不匹配或线路噪声。数据还没被读取下一帧数据就已经接收完成则发生溢出。这两种情况在实际嵌入式调试中暴露的问题很典型帧错误通常是参数配置问题溢出则说明上层逻辑消费数据的速度低于 UART 接收速度需要补 FIFO 或降低数据吞吐。调试期给接收模块加上帧错误和溢出标志可以发现很多偶发性通信故障的根因。错误类型判断方式常见根因处理办法帧错误停止位采样到低电平波特率偏差、噪声干扰校准分频参数、加强滤波溢出错误数据未被读取时新帧完成上层逻辑处理速度慢增加 FIFO 深度或中断优先级毛刺误触发起始位确认失败线路边沿噪声多周期采样、硬件滤波溢出标志的实现不复杂但要注意它的生命周期溢出状态不能一直保持否则后续帧即使正常读取上层逻辑也永远认为处于错误状态。常用的做法是在收到新起始位时清除溢出标志或者在数据读取完成时清除。帧错误标志同样需要手动或自动清除避免阻塞后续通信。把这三个标志接进调试寄存器的某个比特位配合串口打印或逻辑分析仪就能快速定位通讯不稳的具体环节。本文还有配套的精品资源点击获取
返回列表