
1. 项目概述为什么要在FPGA上做UART串口通信做FPGA开发的人几乎没有人能绕开UART串口通信。它可能是你接触的第一个通信协议也是调试板上最常用的“眼睛”。我见过不少朋友一上来就想做PCIe、DDR、高速SerDes这些看起来很唬人的东西结果板子一上电数据不对了连个打印通道都没有只能靠LED灯闪烁来猜代码跑到哪里那种痛苦我太熟悉了。所以这篇内容咱就踏踏实实把UART串口通信在FPGA上的实现从头到尾捋一遍把该懂的原理讲透把能直接抄作业的Verilog代码给出来再把我在实际调试中踩过的坑一并分享。项目标题是“UART串口通信FPGA实现”对应的核心需求其实就两个一是把PC或者单片机发过来的串口数据正确收下来二是能按串口协议把数据发出去。如果你手里有Altera、Xilinx或者国产FPGA开发板完全可以通过这个项目把时钟分频、状态机设计、跨时钟域采样这些基本功全部练一遍。更关键的是UART这个协议足够简单非常适合作为你从“看得懂Verilog”到“能独立完成一个通信模块”之间的过渡项目。UART的全称是Universal Asynchronous Receiver/Transmitter即通用异步收发器。它最大的特点就是“异步”——收发双方不需要共享时钟只要约定好波特率就能完成数据传输。这个特点决定了它的实现方式和SPI、I2C完全不同也让它在FPGA实现时有了很多值得琢磨的细节。我在博客和论坛上看到很多相关的热搜词比如“uart时序图”“uart电平转换电路3.3 1.8”“51单片机串口通信原理图”等等其实这些都是围绕串口通信的周边问题。这篇博文会以FPGA实现为主线把这些周边问题也一并带出来讲清楚。2. UART协议核心拆解一帧数据到底是怎么传的2.1 帧格式与时序起始位、数据位、停止位一个都不能少UART通信的本质是链路层协议一条数据线上用高低电平表示逻辑0和逻辑1。空闲状态下TX线保持高电平。发送方要发送一帧数据时先把TX线拉低这个从高到低的跳变就是起始位接收方靠这个下降沿来同步接收时序。起始位之后是数据位常见配置是8位传输顺序是从最低位LSB开始逐位发送。最后是停止位通常为1位高电平表示一帧数据结束。这里有一个初学者特别容易搞混的点UART发送的是数据位的最低位在前也就是LSB first这和SPI、I2C的时序习惯不一样。比如你要发送0x55二进制0101_0101在UART线上看到的电平顺序是1、0、1、0、1、0、1、0而不是0、1、0、1、0、1、0、1。如果你在调试时用示波器抓波形发现数据反了别急着怀疑代码先想想是不是把MSB和LSB的顺序搞反了。帧格式可以带校验位也可以不带。校验位分为奇校验和偶校验作用是在接收端检查这一帧数据是否有传输错误。不过在实际的FPGA项目中除非你做的协议栈明确要求否则一般不用校验位因为UART本身适合短距离、低速率场景出错概率不高。真到了需要可靠传输的场合直接上CRC校验或者用更高层的协议比如Modbus来保证。空闲状态是高电平这个特性很重要它意味着发送端在不上电或者不发送时接收端读到的都是高电平。这也是为什么串口调试时经常用“拉低发送线”来模拟一个字节的起始位——方便做硬件自检。2.2 波特率与时间基准9600、115200这些数字怎么来的波特率表示每秒传输的码元数在UART这里就等于每秒传输的比特数。常见的波特率有9600、19200、38400、57600、115200。波特率越高每比特占用的时间越短对时钟精度和采样方式的要求也越高。比特时间的计算公式很简单比特时间 1 / 波特率以9600波特率为例一比特的时间约等于104.17微秒以115200波特率为例一比特的时间约等于8.68微秒。这个时间参数是整个UART模块设计的核心基准FPGA内部所有跟串口相关的信号最终都要换算成这个时间。系统时钟和波特率之间的关系决定了你要怎么设计分频器。假设FPGA系统时钟是50MHz要产生9600波特的比特时钟需要的分频系数是50_000_000 / 9600 ≈ 5208.33。注意这个结果不是整数这就引出了一个很现实的问题UART的波特率分频往往伴随着误差误差在可接受范围内不会导致通信失败但误差大了就一定会出错。实际工程中我们取5208对应的实际波特率变成了50_000_000 / 5208 ≈ 9600.61误差只有万分之六左右完全够用。我把常见时钟频率下的分频参数整理成了表格方便你直接查系统时钟目标波特率分频系数整数实际波特率误差50MHz960052089600.610.006%50MHz115200434115207.370.006%100MHz9600104179600.460.005%100MHz115200868115207.370.006%27MHz115200234115384.620.16%从表格可以看出只要系统时钟频率远高于波特率整数分频的误差完全可以控制在可接受范围内。但如果你用的是低速晶振比如12MHz又想跑115200分频系数只有104误差会明显增大这时候就需要考虑小数分频或者直接更换晶振。这也是为什么很多开发板用50MHz或100MHz时钟的原因之一——对串口通信来说容错空间大。2.3 电平标准TTL、RS232、RS485FPGA到底接的是什么聊串口就绕不开电平标准。很多朋友第一次接触串口时看到DB9接口、RS232电平、TTL电平、USB转串口模块这些名词直接一头雾水。这里我帮你彻底理清楚。FPGA的IO引脚工作在TTL电平标准输出高电平约为3.3V或2.5V、1.8V取决于bank电压输出低电平为0V。PC的串口接口如果还有的话是RS232电平逻辑高为-3V到-15V逻辑低为3V到15V和TTL电平完全相反。所以FPGA不能直接连电脑的DB9串口需要外接MAX3232这类电平转换芯片。现代开发板上最常见的是USB转串口方案比如FT232R、CH340、CP2102这些芯片内部已经集成了USB转UART的功能输出端是TTL电平。这种情况下FPGA的TX、RX引脚可以直接和USB转串口芯片的RXD、TXD对接。搜索热词里频繁出现的“ft231x usb uart驱动”“ft232r usb uart驱动win10”指的就是这类芯片在Windows下的驱动程序。RS485则是另一种物理层标准它用差分信号传输适合长距离、抗干扰场景。FPGA需要外接RS485收发器芯片如MAX3485并且要控制DE/RE方向引脚。搜索热词“verilog uart rs485”说明很多人在做这一块的FPGA实现后面我会专门讲一下处理要点。3. FPGA实现UART的整体架构设计3.1 模块划分发送、接收、波特率生成各司其职FPGA设计讲究模块化UART也不例外。一个完整的UART IP核通常包含三个子模块波特率发生器Baud Generator、发送器Transmitter和接收器Receiver。有些工程还会加上FIFO缓冲用来解决收发速率不匹配的问题。我建议你从最基础的开始先实现这三个核心模块跑通仿真和上板验证之后再考虑加FIFO、加中断等功能。项目一开始就把模块拆得太细反而会因为接口太多而把自己绕晕。顶层模块对外暴露的接口很简洁无非是系统时钟、复位信号、发送数据输入、发送使能、串行发送输出、串行接收输入、接收数据输出以及接收有效标志。这组接口做出来后你既可以把它封装成一个可复用的IP核也可以直接把它集成到更大的系统里比如配合FMC的STM32H743通信、配合PCIE采集卡的数据上传通道都能用上。3.2 设计难点与选型为什么接收比发送难那么多发送方向比较简单。发送端知道每一帧数据什么时候开始也知道每个比特的持续时间只要按照时序把数据一位一位搬出去就行。但接收方向就复杂了因为UART是异步协议接收端不知道数据什么时候会来只能靠RX线上的下降沿来检测起始位。这里引出一个关键问题检测到起始位之后接收端用什么时机去采集数据位如果一个比特的宽度是8.68微秒115200波特率接收端在什么时候读取RX线才是最稳的答案是在每个比特的中心位置采样。因为信号在传输过程中可能有上升沿/下降沿的抖动边缘区域最不稳定而比特中心是最远离边缘的地方采样成功率最高。这就需要在检测到起始位后把时钟延迟半比特时间然后在每个比特的中心点采样一次连续采样8个数据位和1个停止位。另外还有一个更稳健的做法是过采样。也就是用一个远高于波特率的时钟通常是波特率的16倍或8倍去采样RX线用多数表决的方式判断当前电平。比如16倍过采样每个比特会被采样16次去掉开头16和结尾的部分取中间的若干次做投票能有效滤除毛刺和抖动。我在FPGA实现里通常采用16倍过采样的方案可靠性比单纯的中点采样高很多。3.3 对外接口设计兼容FMC、PCIe、Zynq等场景的思路很多朋友看到搜索热词里有“stm32h743和fpga实现fmc通信”“fpga pcie rc例子”“zynq linux动态加载fpga”这些内容会好奇UART串口和这些高级接口有什么关系。其实串口在系统联调中扮演的角色非常关键——往往是拿到一块新FPGA开发板后第一个要跑通的接口。它承担的功能包括打印启动日志、读取调试寄存器、配置参数、升级固件等。比如在STM32H743通过FMC总线和FPGA通信的场景里FPGA作为FMC从设备STM32通过FMC接口配置FPGA的寄存器和工作模式而后续的调试信息可以通过UART输出到PC端。这样设计的优势是FMC总线负责高速数据交互UART负责低速调试信息输出两者各司其职互不干扰。在Xilinx Zynq平台上因为PS端自带ARM核FPGA的UART模块可以直接作为AXI外挂接到PS端的AXI总线上Linux系统下会生成/dev/ttyPS0节点用起来非常方便。我建议你在设计UART模块时就留好接口余量输出端口做成“握手式”而不是“脉冲式”。所谓握手式就是发送端拉高请求信号接收端回一个应答信号这样在集成到复杂系统时不容易出现丢数据的问题。4. 波特率发生器实现从理论到Verilog代码4.1 整数分频器的设计与边界条件波特率发生器的核心是一个计数器。系统时钟每来一个周期计数器加1当计数值到达分频系数减1时计数器清零并输出一个脉冲。这个脉冲就是比特时钟的节拍。以50MHz系统时钟、波特率115200为例分频系数是434也就是说计数器从0数到433每434个系统时钟周期产生一个比特脉冲。下面是Verilog实现代码module baud_gen #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200, parameter FULL_COUNT 0 )( input wire clk, input wire rst_n, output reg baud_pulse ); // 如果外部没传入FULL_COUNT则内部自行计算分频参数 localparam BAUD_DIV (FULL_COUNT 0) ? FULL_COUNT : (CLK_FREQ / BAUD_RATE - 1); reg [15:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 16d0; baud_pulse 1b0; end else if (cnt BAUD_DIV) begin cnt 16d0; baud_pulse 1b1; end else begin cnt cnt 16d1; baud_pulse 1b0; end end endmodule这个模块只需要注意一点分频系数的计算用的是“CLK_FREQ / BAUD_RATE - 1”减1是因为计数器从0开始计数。如果你不减1实际波特率会略低于预期值。很多初学者在这里踩坑仿真里看时序好像没问题但上板后数据就是错乱原因往往就出在这个“多减一位”或者“少减一位”上。4.2 小数分频的实现思路与工程取舍前面提到过有些场景下整数分频的误差会超出可接受范围。比如27MHz时钟做115200波特率整数分频234后实际波特率是115384.62误差0.16%对于8位数据的UART帧来说勉强可用但如果你跑的是长帧或者波特率更高就会开始出错。这时候可以考虑小数分频。小数分频在FPGA里通常用累加器实现核心思想是平均每N个比特时钟中有M次实际分频系数为K有N-M次实际分频系数为K1通过加权累加来逼近小数分频效果。具体到Verilog实现可以用一个相位累加器来调整计数周期算法上类似于DDS直接数字频率合成的思路。不过我的实际建议是如果项目允许优先选用更高速率的晶振来解决精度问题而不是花太多时间在FPGA内部做小数分频。因为UART本身是低速通信协议提高参考时钟频率是性价比最高的方案。只有在晶振频率无法修改、且波特率确实需要精确匹配的场景比如和某种仪器对接才值得上小数分频。4.3 倍频采样时钟16倍过采样为什么能抗抖动接收端需要细腻的采样策略我常用的方法是16倍波特率采样。也就是说设置一个采样时钟频率等于波特率乘以16然后在这个采样时钟的驱动下对RX线持续采样并判断电平变化。以115200波特率为例16倍采样时钟的频率是1.8432MHz。系统时钟50MHz分频得到这个采样时钟对应的计数器分频值是50_000_000 / 1_843_200 ≈ 27.13取27。采样时钟下每个比特时间被平均分成16份在每个数据位的第8个采样点附近读取电平正好落在比特中心位置。16倍过采样的另一个好处是能够检测起始位的边沿。因为采样时钟比波特率快16倍所以RX线下降沿的发生时刻可以被定位到1/16比特时间以内这对于后续的时序判断非常有帮助。如果直接用比特时钟采样下降沿检测的精度只能达到一个比特周期误判的概率会增大。5. 发送模块实现状态机写清楚数据就不会乱5.1 发送状态机的设计与状态迁移发送模块用状态机实现这是FPGA设计中最基础最常用的设计范式。我定义的状态包括IDLE空闲、START起始位、DATA数据位、STOP停止位。状态机的工作流程是这样的空闲状态下TX线保持高电平。当检测到发送使能信号tx_start拉高时状态机跳转到START状态TX线拉低持续一个比特时间。然后进入DATA状态根据数据位的索引依次发送bit0到bit7。8个数据位发送完毕后进入STOP状态TX线拉高持续一个比特时间然后回到IDLE状态。数据传输的时序控制通过比特计数器实现。每个波特率脉冲到来时计数器加1状态机根据计数器的值判断当前该发送哪个比特并决定是否切换状态。5.2 完整Verilog发送代码与逐段讲解下面是一份可直接用的UART发送模块Verilog代码module uart_tx #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200 )( input wire clk, input wire rst_n, input wire [7:0] tx_data, input wire tx_start, output reg tx_ready, output reg tx_line ); localparam BAUD_DIV CLK_FREQ / BAUD_RATE - 1; localparam IDLE 2d0; localparam START 2d1; localparam DATA 2d2; localparam STOP 2d3; reg [1:0] state; reg [15:0] baud_cnt; reg baud_pulse; reg [2:0] bit_index; reg [7:0] tx_data_reg; // 波特率脉冲生成 always (posedge clk or negedge rst_n) begin if (!rst_n) begin baud_cnt 16d0; baud_pulse 1b0; end else if (baud_cnt BAUD_DIV) begin baud_cnt 16d0; baud_pulse 1b1; end else begin baud_cnt baud_cnt 16d1; baud_pulse 1b0; end end // 主状态机 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; tx_ready 1b1; tx_line 1b1; bit_index 3d0; tx_data_reg 8d0; end else begin case (state) IDLE: begin tx_line 1b1; tx_ready 1b1; if (tx_start) begin tx_data_reg tx_data; bit_index 3d0; state START; end end START: begin tx_ready 1b0; tx_line 1b0; if (baud_pulse) begin state DATA; end end DATA: begin tx_line tx_data_reg[bit_index]; if (baud_pulse) begin if (bit_index 3d7) begin bit_index 3d0; state STOP; end else begin bit_index bit_index 3d1; end end end STOP: begin tx_line 1b1; if (baud_pulse) begin state IDLE; end end default: state IDLE; endcase end end endmodule这段代码的设计重点在DATA状态tx_line的输出用的是组合逻辑直接根据bit_index索引发送数据而在波特率脉冲到来时才切换索引。这样做可以保证每一比特持续的时间和波特率严格对应不会出现半个比特时间的偏移。5.3 发送时序仿真要点看毛刺、看时序、看握手写仿真testbench时我习惯重点检查两个场景。第一个场景是连续发送多字节确保状态机能够在发送完一帧后正确回到IDLE状态并且在收到新的tx_start信号时能正常启动下一次发送。第二个场景是tx_start信号持续时间很短只拉高一个时钟周期验证状态机不会漏掉这个请求。仿真中还有一个容易忽略的点是tx_ready和tx_start的握手关系。tx_ready表示发送模块空闲可以接收新数据。外部逻辑应该在tx_ready拉高时再拉高tx_start避免在模块忙的时候强塞数据导致数据被丢弃。如果你把tx_ready当成一个“电平有效”的信号而不是“脉冲有效”的信号来用整个集成过程会顺利很多。6. 接收模块实现过采样、边沿检测与数据恢复6.1 起始位检测为什么下降沿要多确认一次才算数接收器的第一步是检测起始位。RX线空闲时为高电平数据到来时会被拉低所以理论上检测到下降沿就可以确认起始位。但在实际环境中RX线上可能存在毛刺比如共地不良时电源噪声耦合到信号线上一个短暂的负向毛刺很容易被误判成起始位。我的做法是检测到下降沿后不立即确认起始位而是延迟到比特时间的中心点再次确认电平确实为低。如果中心点采样到的是低电平则确认这是一个有效的起始位如果中心点采样到的是高电平则判定为毛刺状态机自动取消这次接收过程返回IDLE状态重新等待。这个“二次确认”机制非常简单但是对误码率的改善非常明显。6.2 16倍过采样数据恢复的完整Verilog代码下面给出接收模块的完整代码重点展示边沿检测和数据采样逻辑module uart_rx #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200 )( input wire clk, input wire rst_n, input wire rx_line, output reg [7:0] rx_data, output reg rx_valid ); localparam BAUD_DIV CLK_FREQ / BAUD_RATE - 1; localparam HALF_DIV BAUD_DIV / 2; localparam SMP_DIV CLK_FREQ / BAUD_RATE / 16 - 1; localparam IDLE 3d0; localparam START 3d1; localparam DATA 3d2; localparam STOP 3d3; reg [2:0] state; reg [15:0] clk_cnt; reg [7:0] sample_cnt; reg [3:0] bit_cnt; reg [7:0] shift_reg; wire falling_edge rx_reg2 ~rx_line; reg rx_reg1, rx_reg2; // 同步打拍消除亚稳态 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_reg1 1b1; rx_reg2 1b1; end else begin rx_reg1 rx_line; rx_reg2 rx_reg1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; rx_data 8d0; rx_valid 1b0; clk_cnt 16d0; sample_cnt 8d0; bit_cnt 4d0; shift_reg 8d0; end else begin case (state) IDLE: begin rx_valid 1b0; if (falling_edge) begin state START; clk_cnt 16d0; end end START: begin // 延迟到起始位中心点再次确认电平 if (clk_cnt HALF_DIV) begin if (rx_reg2 1b0) begin state DATA; bit_cnt 4d0; clk_cnt 16d0; end else begin state IDLE; end end else begin clk_cnt clk_cnt 16d1; end end DATA: begin // 每个比特采集16次取第8次作为有效点 if (clk_cnt SMP_DIV) begin clk_cnt 16d0; if (sample_cnt 8d8) begin shift_reg {rx_reg2, shift_reg[7:1]}; sample_cnt 8d0; if (bit_cnt 4d8) begin state STOP; clk_cnt 16d0; end else begin bit_cnt bit_cnt 4d1; end end else begin sample_cnt sample_cnt 8d1; end end else begin clk_cnt clk_cnt 16d1; end end STOP: begin if (clk_cnt HALF_DIV) begin rx_data shift_reg; rx_valid 1b1; state IDLE; end else begin clk_cnt clk_cnt 16d1; end end default: state IDLE; endcase end end endmodule这段代码有一个值得解释的地方shift_reg的移位方向。UART是LSB first所以接收到的第一个数据位应该移入最高位。我采用的方式是{rx_reg2, shift_reg[7:1]}也就是把最新收到的bit放到最高位然后整体右移8次之后shift_reg[7:0]正好对应发送端TX数据的bit7到bit0。如果你用的是左移最终接收到字节会和发送字节呈镜像关系数据完全不对。6.3 接收模块的采样时序验证与毛刺滤除实测在仿真验证接收模块时我建议构造几个典型的测试用例正常数据帧、带毛刺的数据帧、起始位后立即丢失的数据帧。正常数据帧用来验证功能正确性带毛刺的用来验证边沿检测的鲁棒性起始位异常用于验证状态机的自动恢复能力。实测中我发现用16倍过采样并取每个比特时间第8个采样点的方案对于20厘米左右的杜邦线连接稳定运行一晚上没有任何误码。但如果把采样点设置在比特的最前面或者最后面偶尔会在气温变化或线缆移动时出现丢字节的现象。这个差异在仿真中完全看不出来只有上板才能体会到。7. 上板调试实战连线、驱动、串口助手一个环节都不能马虎7.1 硬件环境准备USB转串口模块、开发板、杜邦线上板调试前需要准备的硬件包括FPGA开发板带USB转串口芯片最佳、USB转串口模块如果你用的是裸板、杜邦线若干。我推荐使用带CH340或FT232R的USB转串口模块这类芯片驱动成熟稳定可靠。开发板的UART引脚复用情况需要特别注意。很多FPGA开发板把UART引脚和EEPROM、JTAG或者其他外设复用在同一个FPGA引脚上使用前必须查看原理图确认。我在实际项目中见过不止一次因为引脚冲突导致串口完全无法工作的案例排查了半天才发现是引脚被占用。7.2 上位机工具选型串口助手、Python、VS Code插件怎么选上位机调试工具的选择有几个层次。最简单的方案是直接用现成的串口调试助手比如SSCOM、XCOM、MobaXterm的串口会话适合发送固定字节序列做功能验证。如果你想做自动化测试比如连续发10000字节并统计误码率用Python的pyserial库写个小脚本最高效。如果你已经在用VSCode开发装一个Serial Monitor插件就能在编辑器里直接看串口输出非常方便。搜索热词里有“qt串口通信”和“unity串口通信”说明很多朋友在做带界面的上位机。我的建议是先把FPGA端调通验证回环数据没有丢失再考虑上位机界面开发。否则你分不清楚到底是FPGA的问题还是QT程序的问题排查起来非常痛苦。7.3 实测步骤与回环验证流程上板实测推荐从“回环”开始把FPGA的TX引脚直接连到RX引脚自己在内部把接收到的数据原样发送回去。这样即使不接PC也可以通过LED闪灯的规律验证收发链路是否正常。接PC之后的完整流程是使用USB线连接开发板USB转串口接口和PC安装对应驱动并确认设备管理器中出现了COM口号。打开串口助手设置波特率115200、数据位8、停止位1、无校验打开串口。FPGA下载UART回环固件通过串口助手发送一个字节0xA5观察是否原样返回。发送一组长数据比如“Hello FPGA UART”观察是否有丢失或乱码。如果正常再下载收发分离的固件把PC发来的数据经过FPGA处理后显示到LED灯上验证接收链路。这是最经典的验证路径每一步都能明确排查范围。如果第一步就失败优先检查驱动第二步失败优先检查串口参数和波特率配置第三步失败就得用示波器抓FPGA引脚波形来判断数据是否真正被发送出来了。8. 常见问题排查思路与独家避坑技巧8.1 乱码问题的根源与解决方向乱码是串口调试中最常见的问题表现形式是PC串口助手收到的数据和自己发送的数据对不上。乱码的根源通常集中在三个方面波特率不匹配、电平标准错误、时钟来源异常。波特率不匹配的判断方法很简单如果你发0x55收到的是0xAA说明数据位全部反转了如果收到的数据时好时坏而且错误规律不固定大概率是波特率偏差太大。电平标准错误常见于用了RS232转接板但FPGA侧却是TTL电平或者TTL电平的TX/RX接反了。时钟来源异常是指PLL配置错误导致系统时钟频率和你计算分频系数时假设的频率不一致解决方法是打印一个周期翻转信号用示波器实测频率。8.2 跑时序踩过的坑亚稳态和跨时钟域FPGA串口接收最容易踩的硬件坑是亚稳态。RX数据来自外部设备和FPGA系统时钟没有任何相位关系如果你直接用系统时钟采RX信号建立时间和保持时间可能不满足触发器的输出就会进入亚稳态表现为采样值不确定、有时候0有时候1。解决亚稳态的标准方法就是“打两拍”也就是用两个级联的触发器把外部信号同步进来。我在接收模块代码里已经写入了这个逻辑。注意打两拍会引入两个时钟周期的延时对于低速UART来说完成可以忽略但如果将来你做高速接口这个延时就需要纳入时序预算考虑。8.3 一根线没接好通信就是不通最后分享一个我职业生涯中踩过的坑。有一次做两块板子之间的UART通信板A发送板B接收信号死活过不去。示波器看板A TX引脚波形完全正常板B RX引脚却什么都没有。排查了半天最后发现是两根板的GND没有连在一起。UART是单端信号所有电平判断都是相对于GND的两块板如果不共地发送端输出的3.3V高电平在接收端看来是浮空的自然无法正确解析信号。这个问题在EDA工具和仿真中完全看不出来只有上板才能暴露。所以每次做串口联调第一件事就是把两边GND接好检查一遍电源和地线再谈协议和数据。看似简单的道理但新手在这里栽跟头的比例相当高。我把常见问题整理成了一张速查表现象可能原因排查建议完全无数据TX/RX接反、共地不良、引脚复用冲突先用回环测试再用示波器抓波形乱码但规律固定波特率偏差、LSB/MSB顺序错误检查分频参数核对移位方向偶尔丢字节采样点位置不对、未处理亚稳态改用16倍过采样检查打拍逻辑上位机收不到任何内容驱动未装、COM口号选错、USB线不传数据检查设备管理器换数据线发送正常但PC端没收发USB转串口芯片供电不足换短粗的USB线避免延长线9. 实操心得串口这关过了FPGA算入门一半UART串口通信在FPGA实现的全部内容到这里就讲完了代码可以直接拿去用但我更希望你能把这些代码背后的设计思路吃透。发送端的核心是状态机的时序控制接收端的核心是过采样和边沿检测波特率生成的核心是分频计算和误差控制这三块技术点覆盖了FPGA开发中最基础也最重要的方法论。在实际调试过程中我的体会是串口模块花的时间远远不止写代码那点时间更多是在排查环境和电路问题。代码写得再好如果引脚接错、共地不良、驱动没装一切都白搭。所以我总是建议身边的新人遇到问题先检查硬件再检查参数配置最后才怀疑逻辑设计。这个顺序能帮你省下大量宝贵时间。如果你做完了基础的回环测试接下来可以试试这些扩展方向给UART加上FIFO缓冲实现连续收发不丢数、把波特率改成可配置寄存器参数、把8位数据宽度扩展为可配置的数据位长度、加入Modbus协议解析等等。当你把这些扩展功能一个个做下来你就已经具备了独立完成一个中等复杂度通信模块的能力。等回头看这篇UART实现时你会发现它带来的收获远不止一套串口代码那么简单。