简介这份资源面向FPGA初学者与数字电路设计人员提供一套基于纯Verilog实现的串口通信与串口回环工程帮助理解串行通信协议与板级验证流程。工程实现FPGA接收串口助手数据后原样回传并可按需改为纯发送或纯接收模式涉及TX/RX交叉连接、起始位、8位数据位、奇偶校验位与停止位等数据帧结构。资源包共187个文件约5.92MB以Quartus工程文件为主包含qdb、cdb、hdb、qtl、qpg等数据库与工程配置类型以及v源码、sof烧录文件、rpt报告、wlf波形、txt说明和readme等覆盖综合、布局布线与仿真全流程。已有367人学习。作者在描述中坦诚指出连续多字节回环时存在漏字节问题单字节发送正常并说明后续会给出解决方案为读者提供了真实的排错线索与改进方向适合作为串口协议入门与工程调试的参考案例。1. 从一根回环线说起FPGA纯Verilog串口通信到底能解决什么手头有一块FPGA开发板想验证板子能不能跑、串口能不能通、时钟和复位有没有接错最省事的办法不是上逻辑分析仪而是把串口的收发引脚短接让FPGA自己发、自己收看数据能不能原样回来。这就是串口回环也是很多人拿到新板子后跑的第一个工程。它不依赖外部芯片不需要上位机协议栈纯Verilog从零写一个UART收发器把波特率、起始位、数据位、停止位这些参数全部用计数器算出来再把收发两端对接就能构成一个自检闭环。对刚接触FPGA开发的人来说这个工程覆盖了分频、状态机、移位寄存器、跨时钟域同步这几个核心概念对熟手来说它是一块干净的底板往上叠Modbus、叠自定义帧协议、叠DMA搬运都行。串口回环本身不复杂但要把波特率误差压到2%以内、要把亚稳态处理干净、要让综合工具不报时序违例里面有不少值得拆开讲的细节。2. 串口收发器的Verilog结构从波特率计数器到状态机2.1 波特率发生器的参数计算与实现UART是异步通信收发双方没有共享时钟靠约定的波特率对齐每一位。FPGA系统时钟通常是50MHz或100MHz要得到115200bps的波特率就得算分频系数。公式很简单分频系数 系统时钟频率 / 波特率。以50MHz、115200bps为例50_000_000 / 115200 ≈ 434。这个434就是每个bit需要计数的时钟周期数。实际写代码时我一般用一个计数器从0数到434-1数满一个bit时间就产生一个使能脉冲收发逻辑都基于这个脉冲推进。// 波特率发生器50MHz系统时钟115200bps parameter CLK_FREQ 50_000_000; parameter BAUD_RATE 115200; localparam BAUD_CNT_MAX CLK_FREQ / BAUD_RATE; // 434 reg [15:0] baud_cnt; reg baud_tick; always (posedge clk or negedge rst_n) begin if (!rst_n) begin baud_cnt 16d0; baud_tick 1b0; end else if (baud_cnt BAUD_CNT_MAX - 1) begin baud_cnt 16d0; baud_tick 1b1; // 每个bit周期产生一个脉冲 end else begin baud_cnt baud_cnt 1b1; baud_tick 1b0; end end这里有个细节BAUD_CNT_MAX是整数除法434.027取整后是434实际波特率变成50_000_000/434 ≈ 115207bps误差约0.006%远小于UART容忍的2%。但如果系统时钟是100MHz、波特率是9600100_000_000/9600 10416.67取10416后误差约0.006%也没问题。真正容易翻车的是低时钟配高波特率比如12MHz跑11520012_000_000/115200 104.17取104后误差约0.16%勉强能用但取105就变成1.6%了接近临界值。所以参数计算不能只看整数部分要算一下实际误差。2.2 发送模块的状态机设计发送模块的核心是一个状态机空闲态等发送请求加载态把数据锁进移位寄存器并拉低起始位移位态按波特率脉冲逐位输出停止态拉高停止位再回空闲。状态机用三段式写组合逻辑判断跳转条件时序逻辑更新状态和输出。// 发送模块状态机 localparam S_IDLE 2d0; localparam S_START 2d1; localparam S_DATA 2d2; localparam S_STOP 2d3; reg [1:0] tx_state; reg [7:0] tx_data; reg [2:0] tx_bit_cnt; reg tx_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_state S_IDLE; tx_reg 1b1; tx_bit_cnt 3d0; end else begin case (tx_state) S_IDLE: begin tx_reg 1b1; if (tx_start) begin tx_data tx_byte; tx_state S_START; end end S_START: begin if (baud_tick) begin tx_reg 1b0; // 起始位 tx_state S_DATA; tx_bit_cnt 3d0; end end S_DATA: begin if (baud_tick) begin tx_reg tx_data[tx_bit_cnt]; if (tx_bit_cnt 3d7) tx_state S_STOP; else tx_bit_cnt tx_bit_cnt 1b1; end end S_STOP: begin if (baud_tick) begin tx_reg 1b1; // 停止位 tx_state S_IDLE; end end endcase end end这段代码里tx_start是外部给的发送触发信号tx_byte是要发的数据。注意S_START状态里等baud_tick才拉低这样起始位的宽度才是一个完整bit周期。如果直接在S_IDLE里拉低起始位会多出几个时钟周期的宽度接收端可能采错。这是新手常踩的坑后面避坑章节会细说。2.3 接收模块的采样与同步处理接收比发送麻烦因为要处理异步信号。rxd从外部进来跟clk没有相位关系直接采样会引入亚稳态。常见做法是打两拍同步再用一个过采样计数器在bit中间位置采样。我一般用16倍过采样每个bit周期内采16次在第8次的时候取当前值作为该bit的电平。这样即使波特率有微小偏差也能采到稳定值。// 接收同步与过采样 reg rxd_sync1, rxd_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rxd_sync1 1b1; rxd_sync2 1b1; end else begin rxd_sync1 rxd; rxd_sync2 rxd_sync1; // 两拍同步消除亚稳态 end end // 过采样计数每个bit采16次 reg [3:0] os_cnt; reg [3:0] bit_cnt; reg [7:0] rx_data; reg rx_done; always (posedge clk or negedge rst_n) begin if (!rst_n) begin os_cnt 4d0; bit_cnt 4d0; rx_done 1b0; end else if (rx_state S_RX_START) begin if (os_cnt 4d15) begin os_cnt 4d0; if (rxd_sync2 1b0) // 确认起始位 rx_state S_RX_DATA; end else os_cnt os_cnt 1b1; end // ... 数据位采样逻辑类似 end过采样倍数选16是折中倍数越高抗抖动越好但计数器位宽和逻辑资源也越多。8倍也能用但遇到波特率偏差大的发送端容易采错。16倍是工业上比较稳的选择。3. 回环对接与板级验证从引脚约束到数据比对3.1 收发对接的两种方式回环有两种做法一种是在FPGA内部把tx_reg直接连到接收模块的rxd输入不经过引脚这叫内部回环另一种是把tx和rx分配到同一个物理引脚或者用杜邦线短接叫外部回环。内部回环用来验证逻辑功能外部回环用来验证引脚约束和电平标准。我一般先跑内部回环确认收发逻辑没问题再切到外部回环确认板级连接没问题。内部回环的代码很简单把发送模块的tx_reg接到接收模块的rxd端口就行// 内部回环tx直连rx wire loopback_rx; assign loopback_rx tx_reg; // 内部回环 uart_rx u_rx ( .clk (clk), .rst_n (rst_n), .rxd (loopback_rx), // 接内部回环信号 .rx_data (rx_data), .rx_done (rx_done) );外部回环则需要约束文件里把tx和rx都分配到有意义的引脚然后用线短接。注意有些FPGA的IO引脚默认是弱上拉短接tx和rx时如果tx输出低电平rx会读到低没问题但如果tx是开漏输出就需要外部上拉电阻。普通推挽输出不用管。3.2 引脚约束与时序检查以常见的Xilinx 7系列为例约束文件里要写清楚引脚位置和电平标准。假设tx接在A12rx接在B12电平标准是LVCMOS33# 引脚约束 set_property PACKAGE_PIN A12 [get_ports tx] set_property PACKAGE_PIN B12 [get_ports rx] set_property IOSTANDARD LVCMOS33 [get_ports tx] set_property IOSTANDARD LVCMOS33 [get_ports rx] # 时钟约束 create_clock -period 20.000 -name sys_clk [get_ports clk]时钟约束必须写否则综合工具不知道系统时钟频率时序分析没法做。50MHz对应周期20ns100MHz对应10ns。写完约束后跑综合和实现看时序报告里的WNS最差负裕量。如果WNS是负数说明有时序违例常见原因是波特率计数器位宽太大导致组合逻辑延迟过长或者跨时钟域路径没处理好。解决办法是把计数器拆成两级或者给跨时钟域信号加两级同步。3.3 数据比对与自检逻辑回环验证的核心是发什么收什么。我一般写一个简单的自检模块发送一个递增的字节序列接收端每收到一个字节就跟期望值比对不相等就拉高错误标志。这样不用上位机就能判断回环是否正常。// 自检逻辑发送递增序列比对接收值 reg [7:0] tx_cnt; reg [7:0] expected; reg error_flag; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_cnt 8d0; error_flag 1b0; end else if (tx_done tx_cnt 8d255) begin tx_cnt tx_cnt 1b1; expected tx_cnt 1b1; end else if (rx_done) begin if (rx_data ! expected) error_flag 1b1; // 数据不匹配 end enderror_flag可以接到板子上的LED灯亮说明回环有问题。这个自检逻辑跑通后再换成上位机发数据、FPGA回显的模式就完成了从自检到交互的过渡。4. 避坑与排查串口回环最常见的五个翻车点4.1 波特率误差累积导致接收错位现象发送端发0x55接收端收到0xAA或者0x54数据位错位。原因波特率分频系数取整后误差偏大或者收发双方用了不同的系统时钟频率但没重新算分频系数。比如发送端50MHz算的434接收端实际是100MHz但代码里还写434接收端波特率就变成230400了差一倍。解决先确认收发双方的系统时钟频率一致再重新算分频系数。用示波器或者逻辑分析仪抓一下tx波形量一个bit的宽度跟理论值对比。如果误差超过2%换一个更接近的时钟频率或者调整分频系数。4.2 起始位检测毛刺导致误触发现象接收端偶尔收到一个全0或者全1的字节但发送端根本没发。原因rxd空闲时是高电平但线上有毛刺或者干扰接收状态机把毛刺当成起始位下降沿开始采样结果采到一堆无效数据。解决起始位检测不能只看一次采样要在过采样计数器数到中间位置时再确认一次。如果中间位置还是低电平才认为是真起始位否则丢弃回到空闲态。这就是前面代码里S_RX_START状态等os_cnt到15再判断的原因。4.3 跨时钟域信号未同步导致亚稳态现象回环功能时好时坏温度变化或者重新上电后表现不一样。原因rxd信号直接从引脚进到接收状态机没有打拍同步。FPGA内部触发器对异步信号的建立保持时间没有保证可能进入亚稳态导致状态机跳错。解决rxd进FPGA后先打两拍用同步后的信号做后续逻辑。两拍同步能把亚稳态概率降到可接受范围。如果波特率很高比如1Mbps以上还要考虑用更快的系统时钟或者专用同步器。4.4 综合工具优化掉关键逻辑现象仿真波形正常下载到板子后完全不工作。原因某些中间信号没有被后续逻辑使用综合工具把它优化掉了。比如调试用的计数器、没接输出的状态机状态综合时会被裁掉。解决给关键信号加(* keep true *)属性或者在顶层模块把调试信号接到LED或者测试引脚上。另外检查综合报告里的Warning有optimized away字样的要重点关注。4.5 引脚约束错误导致tx和rx接反现象外部回环时数据完全不通但内部回环正常。原因约束文件里tx和rx的引脚分配反了或者板子上的丝印跟实际引脚不对应。解决先用万用表量一下板子上串口座子的tx和rx对应哪个FPGA引脚再对照原理图确认。如果不确定可以写一个最简单的测试逻辑tx输出1kHz方波用示波器或者LED看哪个引脚有波形就能定位tx引脚。5. 从回环到实用把UART封装成可复用的IP核回环跑通只是第一步真正干活的时候不会每次都写一遍收发逻辑。我一般会把UART封装成一个带FIFO的IP核对外只暴露几个简单接口发送端给一个字节和使能信号接收端给一个有效信号和数据总线。FIFO用厂商提供的IP或者自己写一个同步FIFO都行深度16或者32足够应付大多数场景。// UART IP核顶层接口 module uart_ip #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200, parameter FIFO_DEPTH 16 )( input wire clk, input wire rst_n, // 发送接口 input wire tx_valid, input wire [7:0] tx_data, output wire tx_ready, // 接收接口 output wire rx_valid, output wire [7:0] rx_data, // 物理引脚 output wire uart_tx, input wire uart_rx );封装好之后换一个FPGA型号或者换一个波特率只需要改parameter不用动内部逻辑。FIFO的加入让收发可以独立进行发送端不用等接收端处理完再发下一个字节吞吐率能提高不少。验证IP核的时候我习惯用Python写一个简单的上位机脚本通过USB转串口模块跟FPGA通信发一批随机数据比对回显是否一致。脚本用pyserial库几十行就能搞定import serial import random ser serial.Serial(COM3, 115200, timeout1) test_data [random.randint(0, 255) for _ in range(1000)] for byte in test_data: ser.write(bytes([byte])) echo ser.read(1) if echo ! bytes([byte]): print(fMismatch: sent {byte}, got {echo}) break else: print(All passed)这个脚本跑通说明从FPGA到上位机的整条链路没问题。之后要加协议、加校验、加流控都在这个基础上叠就行。从那以后我每次拿到新板子或者换新FPGA型号都强制走一遍内部回环、外部回环、上位机随机数据测试这三步确认底层通信没问题再往上做应用。这套流程帮我省了不少调试时间希望帮到你。本文还有配套的精品资源点击获取