拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA UART通信链路:从Verilog代码到稳定实现的完整指南

1. 项目概述从零构建一个可靠的FPGA UART通信链路在FPGA开发中UART通用异步收发传输器串口通信几乎是每个项目都会遇到的“老朋友”。它简单、通用是连接FPGA与上位机如PC、微控制器或其他外设进行数据交换和调试的基石。然而这个看似简单的“老朋友”背后却藏着不少让新手甚至老手都容易栽跟头的细节。无论是波特率时钟的精准生成、起始位/停止位的稳定检测还是接收数据时的亚稳态处理任何一个环节的疏忽都可能导致通信失败或数据错乱。这次我们不依赖现成的IP核而是基于Xilinx Vivado设计套件从最底层的Verilog代码开始手把手搭建一个完整的UART发送与接收模块。我们的目标不仅仅是“跑通”而是要构建一个稳定、可靠、可调试的通信链路。我会结合自己多次在项目中调试UART的经验把那些数据手册里不会写、但实际开发中一定会遇到的“坑”和“技巧”都讲清楚。无论你是刚开始接触FPGA逻辑设计还是想深入理解串口通信的时序细节这篇文章都能为你提供一条清晰的实践路径。2. UART通信协议的核心原理与FPGA实现要点在动手写代码之前我们必须彻底吃透UART协议。很多人觉得它简单无非就是“起始位数据位停止位”但正是这种轻视往往导致了后续无尽的调试噩梦。2.1 协议帧格式与关键时序参数UART是一种异步通信协议这意味着通信双方没有统一的时钟信号。数据的同步完全依赖于预先约定好的波特率Baud Rate。一帧标准的数据格式如下空闲状态传输线保持在高电平逻辑‘1’。起始位一个比特时间的低电平逻辑‘0’标志着数据帧的开始。数据位紧接着起始位之后通常是5-9位数据从最低有效位LSB开始传输。校验位可选用于简单的错误检测可以是奇校验、偶校验或无校验。停止位1位、1.5位或2位的高电平标志着一帧数据的结束。对于FPGA实现有几个关键时序概念必须精确把控波特率时钟这是整个模块的“心跳”。例如要实现115200的波特率意味着每秒传输115200个比特。那么每个比特的持续时间比特时间就是 1/115200 ≈ 8.68微秒。我们的FPGA系统时钟频率如100MHz远高于此因此需要一个计数器来分频产生一个波特率时钟使能信号而不是直接使用分频后的时钟这有利于保持全局时钟网络的纯净。采样点由于是异步通信接收方必须在每个数据比特位的中间位置进行采样以避开信号边沿可能的不稳定区域。通常的做法是在检测到起始位下降沿后等待1.5个比特时间进行第一次采样即起始位的中点之后每隔1个比特时间采样一次。这1.5个比特的延迟是对起始位检测和同步过程的关键补偿。2.2 FPGA内部实现的架构设计思路在FPGA内部我们将设计两个相对独立的模块uart_tx发送器和uart_rx接收器。它们共享同一个波特率生成器或各自独立但参数一致。发送器 (uart_tx)其核心是一个状态机。状态包括IDLE空闲、START_BIT发送起始位、DATA_BITS依次发送数据位、STOP_BIT发送停止位。当上层逻辑给出发送请求和待发送数据时状态机启动严格按照波特率时钟的节奏将并行数据转换为串行比特流输出。接收器 (uart_rx)这部分的难度和坑点更多。它需要一个稳健的起始位检测逻辑。不能简单地用系统时钟检测到低电平就认为是起始位这极易受到毛刺干扰。常见的抗干扰做法是以高于波特率16倍甚至更高的频率例如用系统时钟对rxd线进行采样当连续采样到多个低电平时如8个才确认为有效的起始位下降沿。确认后再利用波特率时钟使能信号在计算好的采样点上读取数据位。注意很多人直接使用波特率时钟去检测起始位这在理论上可行但抗噪声能力很弱。使用更高频率的系统时钟进行过采样和滤波是工程上实现稳定接收的通用技巧。3. 基于Vivado的UART发送模块(uart_tx)详细实现让我们从相对简单的发送模块开始。假设系统时钟clk为100MHz目标波特率BAUD_RATE为115200数据位DATA_WIDTH为8位无校验1位停止位。3.1 波特率时钟生成逻辑首先我们需要计算波特率分频计数值。系统时钟周期T_clk 10 ns(1/100e6)。波特率周期T_baud 1/115200 ≈ 8.68 us。因此分频比BAUD_CNT_MAX T_baud / T_clk 8680 ns / 10 ns 868。我们需要一个计数器从0计数到867每计满一次就产生一个时钟周期高脉冲的使能信号baud_tick。这个baud_tick就是驱动发送状态机步进的节拍器。// 波特率生成器代码片段 parameter CLK_FREQ 100_000_000; // 100 MHz parameter BAUD_RATE 115200; localparam BAUD_CNT_MAX CLK_FREQ / BAUD_RATE - 1; reg [15:0] baud_cnt 0; reg baud_tick 0; always (posedge clk) begin if (baud_cnt BAUD_CNT_MAX) begin baud_cnt 0; baud_tick 1b1; end else begin baud_cnt baud_cnt 1; baud_tick 1b0; end end3.2 发送状态机与数据移位输出发送模块的接口通常包括时钟clk、复位rst_n、发送使能tx_en、待发送并行数据tx_data[7:0]、串行输出txd以及发送完成标志tx_done。状态机设计如下IDLE状态等待tx_en信号。当tx_en有效时锁存tx_data到内部寄存器进入START状态。START状态当baud_tick有效时将txd输出拉低起始位并进入DATA状态。同时初始化一个位计数器bit_cnt。DATA状态在此状态停留8个波特率周期。每个baud_tick到来时将内部数据寄存器的当前最低位赋值给txd然后数据寄存器右移一位位计数器加一。发送完8位后进入STOP状态。STOP状态当baud_tick有效时将txd输出拉高停止位。通常在此状态停留1个或2个波特率周期后产生tx_done脉冲并跳转回IDLE状态。// 发送状态机核心片段 localparam IDLE 2b00; localparam START 2b01; localparam DATA 2b10; localparam STOP 2b11; reg [1:0] state IDLE; reg [7:0] tx_data_reg 8h0; reg [2:0] bit_cnt 0; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; txd 1b1; // 空闲时为高电平 tx_done 1b0; end else if (baud_tick) begin // 状态转移仅在波特率节拍发生 case (state) IDLE: begin tx_done 1b0; if (tx_en) begin tx_data_reg tx_data; txd 1b0; // 输出起始位 state START; end end START: begin txd tx_data_reg[0]; // 输出数据最低位 bit_cnt 0; state DATA; end DATA: begin if (bit_cnt 7) begin txd 1b1; // 输出停止位 state STOP; end else begin tx_data_reg {1b0, tx_data_reg[7:1]}; // 右移 txd tx_data_reg[1]; // 输出下一位 bit_cnt bit_cnt 1; end end STOP: begin tx_done 1b1; // 发送完成标志 state IDLE; end endcase end end实操心得tx_done信号非常重要。它告知上层逻辑“当前帧已发送完毕可以准备下一帧数据”。上层逻辑必须等待这个信号才能再次拉高tx_en否则会导致发送数据帧重叠、错乱。这是实现流控制的最基础形式。4. UART接收模块(uart_rx)的稳健性设计挑战与实现接收模块是UART实现中的难点核心挑战在于如何从异步的串行比特流中可靠地恢复出数据和时钟。直接使用波特率时钟采样rxd线风险极高因为发送端和接收端的时钟不可能完全同步微小的频率偏差会随着时间累积导致采样点漂移最终错位。4.1 过采样与起始位检测为了提高抗干扰能力和同步可靠性工业级的做法是采用过采样。我们使用系统时钟如100MHz对rxd信号进行高速采样。假设目标波特率是115200那么一个比特时间内系统时钟可以采样约868次100e6 / 115200。我们不需要这么高的过采样率通常16倍就已足够。但为了设计简单我们可以直接利用系统时钟。稳健的起始位检测流程空闲监测模块上电后或上一帧接收完成后处于IDLE状态持续监测rxd线是否为高电平空闲态。下降沿检测与滤波当检测到rxd变为低电平时启动一个计数器。这个计数器的阈值设置很关键。如果只是短暂的低电平毛刺比如几个系统时钟周期计数器会在达到阈值前因rxd变回高电平而清零。只有当rxd低电平持续足够长的时间例如超过半个比特时间对应的系统周期数我们才认为这是一个有效的起始位。这有效滤除了噪声。同步与采样点计算一旦确认有效起始位我们就认为找到了帧的起点。接下来为了在数据位的中心采样我们需要等待1.5个比特时间后进行第一次采样即数据位0的中心。之后每隔1个比特时间采样一次。// 接收模块起始位检测与同步逻辑片段 localparam IDLE 3b000; localparam START_DET 3b001; localparam WAIT_CENTER 3b010; localparam SAMPLE_DATA 3b011; localparam STOP 3b100; reg [2:0] rx_state IDLE; reg [15:0] sample_cnt 0; // 用于比特时间计数 reg [3:0] bit_idx 0; // 已接收数据位计数 reg [7:0] rx_data_reg 8h0; reg rx_done 0; // 计算常数基于100MHz时钟115200波特率 localparam BIT_TIME_CNT CLK_FREQ / BAUD_RATE; // 约868 localparam HALF_BIT_TIME BIT_TIME_CNT / 2; // 约434 localparam ONE_POINT_FIVE_BIT BIT_TIME_CNT * 3 / 2; // 约1302 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_state IDLE; sample_cnt 0; rxd_sync 1b1; end else begin // 对输入rxd进行两级寄存器同步消除亚稳态 rxd_sync {rxd_sync[0], rxd}; case (rx_state) IDLE: begin rx_done 1b0; if (!rxd_sync[1]) begin // 检测到潜在起始位低电平 rx_state START_DET; sample_cnt 0; end end START_DET: begin // 持续监测低电平滤除毛刺 if (rxd_sync[1]) begin // 如果是毛刺又变高了 rx_state IDLE; end else if (sample_cnt HALF_BIT_TIME) begin // 低电平持续了约半个比特时间确认是有效起始位 rx_state WAIT_CENTER; sample_cnt 0; // 重新计数等待1.5个比特时间 end else begin sample_cnt sample_cnt 1; end end WAIT_CENTER: begin if (sample_cnt ONE_POINT_FIVE_BIT - 1) begin // 到达第一个数据位的中心采样点 rx_state SAMPLE_DATA; sample_cnt 0; bit_idx 0; end else begin sample_cnt sample_cnt 1; end end // ... SAMPLE_DATA 和 STOP 状态后续展开 endcase end end4.2 数据位采样与停止位处理进入SAMPLE_DATA状态后逻辑就相对规整了。在每个比特时间的中心点由sample_cnt计时对rxd_sync[1]进行采样并存入移位寄存器。接收完指定数量的数据位后进入STOP状态等待停止位。// 接续上面的状态机 SAMPLE_DATA: begin if (sample_cnt BIT_TIME_CNT - 1) begin // 到达每个比特周期的末尾 sample_cnt 0; // 在周期中心点采样的值这里简化处理实际应在中心点采样 // 假设我们在状态进入时即中心点已经采样此处进行移位存储 rx_data_reg {rxd_sync[1], rx_data_reg[7:1]}; // LSB first if (bit_idx 7) begin // 假设8位数据 rx_state STOP; end else begin bit_idx bit_idx 1; end end else begin sample_cnt sample_cnt 1; end end STOP: begin // 等待停止位周期结束。可以检查停止位是否为高作为帧错误判断 if (sample_cnt BIT_TIME_CNT - 1) begin rx_done 1b1; // 产生接收完成信号 rx_data rx_data_reg; // 输出有效数据 rx_state IDLE; end else begin sample_cnt sample_cnt 1; end end关键技巧在SAMPLE_DATA状态更精确的做法不是在周期末尾移位而是在每个比特时间的中心点例如sample_cnt HALF_BIT_TIME时进行一次采样。这需要更精细的计数器控制。上面代码是一种简化在低速或时钟精度很高时可行。但对于高波特率或时钟偏差敏感的场景必须实现中心采样。4.3 亚稳态处理与同步器注意上面代码中的rxd_sync信号。rxd是来自FPGA引脚的外部异步信号其变化与FPGA内部系统时钟clk没有固定关系。如果直接用clk去采样变化的rxd极易产生亚稳态导致系统行为异常。标准的做法是使用两级D触发器进行同步如代码中所示rxd_sync {rxd_sync[0], rxd};。这虽然不能完全消除亚稳态但能将其概率降到极低并将可能存在的亚稳态输出限制在短时间内后续逻辑用同步后的rxd_sync[1]进行判断电路是安全的。5. 在Vivado中进行仿真验证与上板调试代码写完了但工作只完成了一半。充分的仿真和严谨的调试是保证项目成功的关键。5.1 编写Testbench进行行为级仿真我们需要编写一个测试平台模拟上位机向FPGA发送数据以及检查FPGA发送的数据。在Testbench中我们需要模拟上位机发送用一个task或initial块按照UART时序包括起始位、数据位、停止位将txd信号拉低/拉高模拟串行数据输入到FPGA的rxd端口。检查FPGA发送监控FPGA的txd输出端口按照UART协议解析出数据并与预期值比较。加入抖动和偏差为了测试鲁棒性可以在模拟的rxd信号中故意加入一些小的时序抖动或者让模拟的波特率与FPGA设置的波特率有微小偏差例如±2%观察接收模块是否还能正确工作。// 简单的Testbench片段模拟发送字节 8‘h55 (01010101b) initial begin // 初始化 rxd 1b1; // 空闲高电平 #1000; // 发送起始位 rxd 1b0; #8680; // 模拟115200波特率的一个比特时间单位根据timescale可能是ns/ps // 发送数据位 LSB first rxd 1b1; // bit0 #8680; rxd 1b0; // bit1 #8680; // ... 依次发送所有位 rxd 1b0; // bit7 #8680; // 发送停止位 rxd 1b1; #8680; // 可以继续发送下一个字节... end在Vivado中运行仿真查看波形。重点关注baud_tick信号是否规律出现。接收状态机rx_state是否按照IDLE - START_DET - WAIT_CENTER - SAMPLE_DATA - STOP - IDLE正确跳转。接收到的数据rx_data是否与发送的数据一致。发送状态机tx_state是否正常跳转txd输出波形是否符合UART格式。5.2 上板调试与串口助手联调仿真通过后进行上板测试。你需要硬件连接将FPGA板上UART接口的TX、RX引脚通过USB转串口模块如CH340、CP2102、FT232等连接到电脑。务必注意FPGA的TX要接模块的RXFPGA的RX接模块的TX并且共地。驱动安装确保电脑已安装对应的USB转串口驱动在设备管理器中能看到正确的COM口。使用串口调试助手打开XCOM、SSCOM或其他串口调试工具选择正确的COM口设置与FPGA代码中一致的波特率、数据位、停止位、校验位。设计简单的回环测试这是最有效的调试方法。编写一个顶层模块将接收模块收到的数据rx_data在收到rx_done信号后稍微延迟例如几个时钟周期再送给发送模块的tx_data并触发tx_en。这样FPGA就把从电脑接收到的数据原样发回。观察与排查电脑发送电脑接收在串口助手中发送一个字节如0x55看是否能收到相同的字节。如果收不到首先检查硬件连接和COM口设置。查看信号如果可能使用Vivado的ILA集成逻辑分析仪IP核抓取FPGA内部的rxd、txd、rx_state、tx_state、rx_data、tx_data等关键信号。这是最强大的调试手段可以直观地看到信号时序是否与仿真一致。常见问题乱码99%的原因是波特率不匹配。请双精度检查FPGA代码中的CLK_FREQ和BAUD_RATE参数计算是否正确以及串口助手设置是否正确。时钟频率是否准确开发板外部晶振是25MHz还是50MHz你的时钟管理模块如MMCM/PLL输出真的是100MHz吗只能接收一次检查接收完成信号rx_done产生后状态机是否正确回到了IDLE状态并能够重新检测起始位。丢失字节或数据错误可能是起始位检测滤波阈值设置不当或者采样点计算有误。尝试调整过采样和滤波的计数器阈值。用ILA观察rxd信号和内部采样点的关系。6. 性能优化与高级功能扩展一个基础可靠的UART收发器搭建完成后我们可以在此基础上考虑优化和扩展使其更实用、更高效。6.1 添加FIFO缓冲与流控制基础的模块每收/发一帧数据都需要CPU/逻辑干预。当数据流量大时容易丢失数据。添加FIFO先入先出存储器是必然选择。发送FIFO上层逻辑可以连续写入多字节数据到FIFO发送模块从FIFO中读取数据并自动连续发送直到FIFO为空。tx_done信号可以作为从FIFO读取下一个数据的触发条件。接收FIFO接收模块每收到一个完整字节就将其写入接收FIFO。上层逻辑可以定期或当FIFO非空时从FIFO中读取数据。这避免了因为上层逻辑处理不及时而丢失后续数据。流控制如RTS/CTS在高速或实时性要求高的场景下非常重要。当接收FIFO快满时通过RTS信号通知发送端暂停发送当接收FIFO有空间时再通知发送端继续。这需要额外的硬件流控引脚和协议支持。6.2 参数化设计与自动波特率检测为了使模块更具复用性我们应该将其设计为参数化模块CLK_FREQ系统时钟频率。BAUD_RATE波特率。DATA_WIDTH数据位宽5-9。PARITY_EN,PARITY_TYPE校验使能和类型奇/偶。STOP_BITS停止位数1, 1.5, 2。自动波特率检测是一个高级功能特别适用于需要自适应不同设备的场景。其基本原理是在未知波特率的情况下通过测量接收到的起始位低电平的持续时间来反推出发送端的波特率。例如检测从rxd下降沿到上升沿第一个停止位的时间这个时间对应了1位起始位 N位数据位 校验位 停止位的总时间。知道了数据帧格式就能计算出比特时间从而得到波特率。实现起来需要高精度的计数器通常用系统时钟并且要求发送端发送一个特定的、易识别的字节如0x55其位模式是01010101有丰富的边沿。6.3 资源利用与时序收敛分析在Vivado中完成综合与实现后需要关注以下几点资源报告查看UART模块消耗了多少查找表LUT、寄存器FF和块RAM如果用了FIFO。一个基础的收发器通常只消耗几百个LUT/FF非常节省。时序报告检查是否有时序违例。UART逻辑通常运行在较低的波特率时钟使能下时序很容易满足。但要确保同步器两级触发器和FIFO的读写时钟域如果FIFO使用独立时钟之间的时序路径得到正确约束。如果系统时钟频率很高如200MHz以上而UART逻辑部分路径的组合延迟较大也可能出现建立/保持时间违例需要通过流水线或重新设计来优化。引脚约束确保在XDC约束文件中正确地将顶层端口的txd和rxd分配到了FPGA芯片对应的硬件引脚上。这些引脚通常与板载USB转串口芯片相连。从仿真到上板从基础功能到优化扩展一步步构建并理解UART的每一个细节这个过程中积累的调试经验和对时序的深刻认识其价值远超一个能工作的IP核。当你下次再遇到串口通信的问题时你看到的将不再是神秘的电平跳变而是一系列清晰的状态转换和精确的时钟计数。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门