Verilog UART实现:从原理到FPGA集成的完整指南
1. 项目概述与核心价值最近在折腾一个FPGA的小项目需要和上位机进行简单的数据交互第一时间就想到了UART通用异步收发传输器。这玩意儿可以说是数字通信里的“老黄牛”了结构简单、可靠性高几乎是所有嵌入式开发者和硬件工程师的入门必修课。虽然市面上现成的IP核一抓一大把但自己动手用Verilog从头实现一个对于深入理解串行通信的时序、状态机设计以及跨时钟域处理这些核心概念有着不可替代的价值。这次我们要剖析的就是一个来自开源社区的、结构清晰、非常适合学习和二次开发的8位UART Verilog实现。这个UART模块麻雀虽小五脏俱全。它严格遵循了最经典的异步串行通信格式每个数据帧包含1个起始位、8个数据位和1个停止位。没有使用奇偶校验这在实际的简单调试和低速通信中非常常见能让我们更专注于核心收发逻辑。模块的接口设计得相当规整将接收RX和发送TX路径清晰地分离并提供了完备的状态指示信号如rxBusy、txDone、rxErr等这使得它与外部控制器无论是软核CPU还是纯逻辑状态机的交互变得非常直观和友好。注意自己编写UART的核心目的绝不仅仅是“让它跑起来”。更重要的是理解其内部三个核心子模块——波特率发生器、接收器、发送器——是如何协同工作的以及如何根据系统时钟和目標波特率来精确地生成采样点。这是后续进行性能优化、添加硬件流控或自定义协议的基础。对于初学者而言这个项目是一个绝佳的起点。对于有经验的开发者其简洁的架构也便于你快速集成到自己的项目中或者以此为蓝本扩展出支持可变数据位宽、奇偶校验、多停止位等更复杂的功能。接下来我们就一层层剥开它的设计看看里面到底是如何运作的。2. 模块架构与接口深度解析一个完整的UART模块可以清晰地划分为三个功能性子模块波特率发生器、接收器和发送器。这个项目的结构图uart_structure.png直观地展示了这一点。整个顶层模块uart主要扮演了“接线板”和“参数传递者”的角色将三个子模块实例化并把正确的信号连接起来。2.1 顶层接口信号详解让我们逐一审视顶层模块的每一个输入输出端口理解其设计意图和时序要求。这是将模块成功集成到你工程中的第一步。控制信号clk这是整个模块的“心跳”所有同步逻辑都基于此时钟的上升沿动作。它通常直接连接到FPGA的全局时钟网络。模块内部没有使用任何异步复位其初始状态依赖于Verilog的初始值或依赖于后续使能信号进入正确状态。接收端RX接口rx串行数据输入线。空闲时为高电平。当检测到持续一个波特率周期的低电平时模块将其识别为起始位。rxEn接收使能信号。高电平有效。当它为低时接收器应被禁用可能忽略rx线上的数据并复位内部状态。这是一个重要的节能和控制特性。out[7:0]并行数据输出。当一帧数据接收完成且无误时接收到的8位数据会出现在这组端口上。数据会保持稳定直到下一帧数据接收完成并被覆盖。rxDone接收完成脉冲信号。高电平有效仅持续一个时钟周期。它标志着out总线上的数据是新鲜且有效的可以供外部电路读取。这是读取数据最可靠的握手信号。rxBusy接收忙状态指示。高电平表示接收器正在处理一帧数据从检测到起始位开始到停止位采样结束。在此期间新的起始位将被忽略。rxErr接收错误标志脉冲。高电平有效持续一个时钟周期。当接收器检测到帧错误例如在预期的停止位位置采样到的不是高电平时会拉高此信号。同时rxDone信号不会产生因为该帧数据被视为无效。发送端TX接口txEn发送使能信号。高电平有效。禁用时发送器应停止工作并将tx输出置为高电平空闲状态。txStart发送启动脉冲信号。高电平有效仅需持续一个时钟周期。当txBusy为低且txEn为高时一个txStart的上升沿会触发发送器开始发送一帧数据同时锁存此时in总线上的值。in[7:0]并行数据输入。需要发送的8位数据。在txStart有效时被锁存进发送器内部。tx串行数据输出线。空闲时为高电平。发送时依次输出起始位低、8位数据位LSB先发、停止位高。txDone发送完成脉冲信号。高电平有效持续一个时钟周期。它标志着一帧数据已完全送出发送器回归空闲可以接受新的发送任务。txBusy发送忙状态指示。高电平表示发送器正在发送一帧数据。在此期间新的txStart脉冲将被忽略。2.2 关键参数CLOCK_RATE与BAUD_RATE模块的两个参数CLOCK_RATE和BAUD_RATE是理解其工作原理的钥匙。它们不是直接以Hz为单位的频率值而是一种“比例因子”或“分频系数”。CLOCK_RATE可以理解为系统时钟clk频率是目标波特率时钟的多少倍。目标波特率时钟是指波特率发生器输出的、一个周期等于一个比特位时间的时钟使能脉冲。BAUD_RATE通常设为1。它定义了波特率发生器输出脉冲的占空比或模式。在这个实现中BAUD_RATE1意味着波特率时钟是一个单周期脉冲。那么如何根据实际的FPGA系统时钟频率和所需的通信波特率来计算这两个参数呢公式如下CLOCK_RATE FPGA系统时钟频率 / 目标波特率举个例子你的FPGA板载晶振是50MHz即clk频率为50,000,000 Hz你想要实现115200的波特率。 计算CLOCK_RATE 50,000,000 / 115,200 ≈ 434这意味着波特率发生器需要每计数434个系统时钟周期就产生一个单周期脉冲。这个脉冲的周期就是1/115200秒即一个比特位的时间。在代码中你需要这样实例化模块uart #( .CLOCK_RATE(434), // 50MHz / 115200 ≈ 434 .BAUD_RATE(1) ) my_uart_instance ( .clk(clk_50m), // ... 其他端口连接 );实操心得计算出的CLOCK_RATE可能不是整数。例如50MHz时钟对于9600波特率分频系数是5208.333...。这时必须取整5208这会引入微小的时序误差。误差率 (理论值 - 实际值) / 理论值。对于5208.333取整到5208误差约为0.006%远低于RS-232标准允许的误差范围通常3%完全可接受。但如果你使用低频主时钟如1MHz去实现高波特率如115200分频系数仅为8.68取整为9带来的误差就高达3.7%可能导致通信失败。因此选择主时钟频率时应使其能被目标波特率整除或得到足够大的分频系数。3. 核心子模块原理与实现细节理解了顶层框架后我们深入三个核心子模块看看它们是如何用Verilog状态机来实现的。3.1 波特率发生器精准的“节拍器”波特率发生器是整个UART的时序基准。它的任务不是产生一个新的时钟而是产生一个周期性的使能脉冲通常称为tick或baud_en其频率等于目标波特率。在这个设计中它本质上是一个自由运行的分频器。内部有一个计数器从0计数到CLOCK_RATE-1然后归零同时输出一个单周期的高电平脉冲。这个脉冲的上升沿标志着“一个比特位时间窗口”的开始接收器和发送器都依据这个脉冲来推进各自的状态。代码逻辑简析always (posedge clk) begin if (counter CLOCK_RATE - 1) begin counter 0; baud_tick 1b1; // 产生波特率使能脉冲 end else begin counter counter 1; baud_tick 1b0; end end接收器和发送器内部会有更细粒度的计数器例如计数16个baud_tick来定位一个比特位的中间采样点但所有时序的根源都来自于这个稳定的baud_tick。3.2 接收器在噪声中捕捉数据接收器是UART设计中挑战性较高的部分因为它需要从异步的串行信号中可靠地恢复出数据。其核心是一个状态机通常包含以下状态IDLE空闲、START_BIT检测起始位、DATA_BITS接收数据位、STOP_BIT检测停止位。工作流程与关键技巧空闲与起始位检测在IDLE状态且rxEn有效时持续监测rx线。一旦检测到rx变为低电平起始位开始并非立即跳转状态而是等待半个比特位时间例如在波特率使能脉冲baud_tick计数到CLOCK_RATE/2时。这样做是为了避开起始位开始边沿可能存在的毛刺和不确定性并将采样点对准每个数据位的中央这里是数据最稳定的时刻。数据位采样进入DATA_BITS状态后每等待一个完整的比特位时间即baud_tick计数满一个周期就在该比特位时间的中心点对rx线进行一次采样并将采样值移入移位寄存器。通常从最低有效位LSB开始接收。停止位验证与完成在接收完第8个数据位后状态机进入STOP_BIT状态。在停止位的中心点再次采样rx线。这里是一个关键的检错点如果采样到高电平则认为帧格式正确产生rxDone脉冲并将移位寄存器中的数据输出到out总线如果采样到低电平则产生rxErr脉冲表示发生了帧错误可能是波特率不匹配、线路干扰或发送方故障。抗抖动与过采样在一些更稳健的设计中会对每个比特位进行多次采样如16次然后取中间值或多数值作为最终采样结果这能有效抑制信号上的毛刺。本实现采用了经典的“中点单次采样”在环境良好时完全够用。注意事项接收器的rx信号对于FPGA内部同步逻辑来说是异步的。虽然本模块可能没有显式地进行同步器处理这依赖于外部输入但在实际工程中强烈建议在顶层模块对rx信号使用两级D触发器进行同步化以防止亚稳态传播到接收状态机中。这是一个常见的可靠性设计。3.3 发送器按部就班的“广播员”相比接收器发送器的逻辑更为直接因为它完全由内部时钟和状态机控制。它也是一个状态机包含IDLE、START_BIT、DATA_BITS、STOP_BIT等状态。工作流程触发与锁存当txEn有效且txBusy为低时一个txStart的上升沿会触发发送过程。发送器首先将当前in总线上的数据锁存到内部寄存器中以防止发送过程中外部数据变化。顺序发送状态机从IDLE进入START_BIT将tx线拉低一个比特位时间。然后依次遍历DATA_BITS状态将锁存数据的每一位从LSB开始放到tx线上每位持续一个比特位时间。最后进入STOP_BIT状态将tx线拉高一个比特位时间。完成与空闲停止位发送完毕后状态机回到IDLE产生txDone脉冲并将txBusy拉低。tx线保持高电平空闲状态。发送器的设计难点较少主要确保时序精确即可。tx信号是由模块内部同步产生的不存在异步问题。4. 仿真验证与测试平台搭建作者提供的功能仿真波形图uart_func_model.png等是理解模块行为的绝佳资料。从图中可以看到在设定的极低频时钟和波特率下方便观察各个信号rx,tx,busy,done等之间的时序关系一目了然。4.1 如何解读仿真波形以接收器仿真为例rx_func_model.pngen(即rxEn)为高使能接收器。rx输入线上出现一个低电平脉冲起始位随后是8位数据例如01010101最后是一个高电平停止位。可以看到busy信号在起始位后立即拉高在整个帧接收期间保持高电平。帧接收完成后busy拉低同时done信号产生一个单周期脉冲此时out总线上的数据有效。如果rx线上的停止位为低则err信号会拉高而done不会产生。这些波形完美印证了之前描述的状态机行为。4.2 构建自己的测试平台虽然项目TODO里提到了testbench但我们完全可以自己动手编写一个简单的测试来验证模块功能。使用Verilog或SystemVerilog编写测试平台Testbench是硬件设计的基本功。一个基础的UART测试平台通常包括时钟生成用always块产生周期性的clk信号。任务驱动编写两个主要任务task uart_tx_byte模拟上位机行为根据设定的波特率将1位起始位、8位数据、1位停止位依次驱动到UART模块的rx输入端。task uart_rx_check监控UART模块的tx输出端按照波特率采样将接收到的串行数据转换为并行字节并与预期值比较。测试序列在initial块中初始化信号然后使能收发器接着用uart_tx_byte任务发送几个特定字节如8’h55、8’hAA、8’h00、8’hFF同时用uart_rx_check任务检查发送端输出的数据是否正确。可以故意发送一个错误的停止位验证rxErr功能。波形输出与断言使用$display在控制台打印测试信息或使用assert语句进行自动检查。实操心得在仿真中为了加快速度可以使用比实际高得多的“虚拟”系统时钟频率和“虚拟”波特率只要保持它们的比例CLOCK_RATE正确即可。例如设置clk周期为10ns100MHz目标“虚拟波特率”为10MHz那么CLOCK_RATE就设为10。这样仿真发送一个字节只需要1微秒左右能极大提升仿真调试效率。等到功能验证无误后再替换为真实的时钟和波特率参数进行时序仿真。5. 集成到FPGA项目从仿真到上板将仿真通过的UART模块集成到真实的FPGA项目中并连接到物理引脚还需要一些步骤。5.1 引脚分配与约束你需要根据FPGA开发板的原理图将UART模块的rx和tx信号分配到特定的芯片引脚上这些引脚应连接到板载的USB-UART桥接芯片如CH340、CP2102、FT232等的对应RXD和TXD。以Quartus为例你需要编写一个.qsf文件或通过GUI进行设置set_location_assignment PIN_AB12 -to rx set_location_assignment PIN_AB13 -to tx set_location_assignment PIN_Y2 -to clk同时还需要为clk引脚创建时钟约束告诉时序分析工具时钟的频率create_clock -name sys_clk -period 20.000 [get_ports clk]5.2 添加同步器与消抖如前所述对于来自外部世界的rx信号务必添加同步器。可以在顶层模块中实例化UART之前这样做reg rx_sync1, rx_sync2; always (posedge clk) begin rx_sync1 rx_pin; // rx_pin是直接来自FPGA引脚的信号 rx_sync2 rx_sync1; end // 将rx_sync2连接到uart模块的rx端口对于按键等机械开关产生的信号可能还需要消抖电路但对于UART通信通常由桥接芯片处理直接连接即可。5.3 环回测试最简单的上板验证方法是进行“环回测试”。将FPGA内部UART的tx输出引脚直接连接到rx输入引脚可以在PCB上用杜邦线短接或者在顶层代码中将tx信号直接赋给rx输入逻辑。然后编写一个简单的逻辑让FPGA每隔一秒发送一个递增的字节同时接收该字节并验证是否一致。通过LED显示结果或者通过另一个UART端口打印调试信息。一个简单的环回测试顶层模块框架module top_loopback( input clk, input uart_rx_pin, output uart_tx_pin, output reg led_ok ); // 时钟分频产生1秒周期脉冲 reg [31:0] counter; wire send_pulse (counter 32‘d50_000_000); // 假设50MHz时钟 always (posedge clk) counter (send_pulse) ? 0 : counter 1; // 待发送数据 reg [7:0] data_to_send 8‘h00; wire [7:0] data_received; wire tx_busy, rx_done; // UART实例化 uart #(.CLOCK_RATE(434), .BAUD_RATE(1)) uart_inst( .clk(clk), .rx(uart_rx_pin), // 实际测试时可改为.rx(uart_tx_pin) 进行内部环回 .rxEn(1‘b1), .out(data_received), .rxDone(rx_done), .txEn(1‘b1), .txStart(send_pulse !tx_busy), .in(data_to_send), .tx(uart_tx_pin) ); // 逻辑每秒发送一个字节并检查接收到的字节 always (posedge clk) begin if (send_pulse !tx_busy) begin data_to_send data_to_send 1; // 数据递增 end if (rx_done) begin // 比较发送和接收的数据注意内部环回时收到的是自己刚发的 led_ok (data_received data_to_send); end end endmodule5.4 常见上板问题排查完全没有数据收发检查引脚分配确认tx/rx引脚是否与USB-UART芯片连接正确。一个常见的坑是交叉连接FPGA的tx应接桥接芯片的rxFPGA的rx应接桥接芯片的tx。检查波特率用示波器或逻辑分析仪测量tx引脚。发送一个固定的字节如0x55二进制01010101测量一个比特位的时间换算成波特率看是否与PC端串口工具的设置一致。检查空闲电平UART空闲时tx线应为持续高电平。如果为低可能是模块未正确初始化或使能。收到乱码波特率不匹配这是最常见的原因。重新计算CLOCK_RATE确保FPGA系统时钟频率准确有些开发板可能使用PLL分频后的时钟。数据位/停止位设置不一致确保PC端串口工具设置为8数据位、1停止位、无校验。信号质量问题对于长导线或高速波特率可能需要进行阻抗匹配或使用差分UART如RS-422。只能发送不能接收或反之检查使能信号确认rxEn和txEn是否已上拉为高电平。检查流控确保PC端串口工具和代码中都没有启用硬件流控RTS/CTS。环回测试进行内部环回测试排除外部线路和PC软件的问题。通过仿真、约束、上板调试这个完整的流程你不仅能将这个UART模块用起来更能深刻理解数字系统设计中“从代码到物理信号”的每一个环节。这个简洁的Verilog UART实现就像一块优质的积木为你构建更复杂的FPGA通信系统打下了坚实的基础。