ARTICLE DETAIL

资讯详情

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

FPGA UART串口通信实现:从协议原理到上板调试的完整指南

FPGA UART串口通信实现:从协议原理到上板调试的完整指南 简介面向FPGA初学者、电子设计竞赛选手及有串口通信开发需求的工程师这份基于Altera平台的UART串口通信Verilog源码提供了完整可用的工程实现。源码包含A4_Uart_Top顶层模块并分解出UART发送模块、接收模块、波特率发生器及蜂鸣器控制等多个子模块详细覆盖起始位、数据位、奇偶校验、停止位的时序处理通过状态机完整管理空闲等待、同步接收、校验判断与数据输出的收发流程。压缩包大小约7.05MB共835个文件既有V、TDF硬件描述源码和Quartus工程配置文件也有大量综合仿真过程文件、波形文件和可下载的SOF配置目录结构完整既可直接下载运行也便于逐个模块研读。附带的编译、综合与仿真报告能辅助理解从RTL代码到FPGA布局布线的全流程并可根据实际需求调整波特率参数与校验方式。目前已有343人学习适合希望深入掌握UART协议并动手实践FPGA串口设计的软硬件开发者。 做FPGA的人几乎没人绕得过UART。哪怕你做的是DDR、PCIe、图像处理最终调试阶段大概率还是要靠一个串口往电脑上打印点数据。这个协议简单到用几根线就能讲完但真到写源码、上板调试的时候各种问题就冒出来了——接收乱码、丢字节、时序不对、驱动装不上。而且网上随便一搜“FPGA uart串口源码”能出来几百个版本代码水平参差不齐有的能用有的是给仿真“特供”的上板就翻车。我写这篇文章就是想把这层窗户纸捅破。我会从UART协议本身聊起把发送、接收、波特率生成、仿真验证、上板调试这条链路完整梳理一遍把每个模块的关键代码逻辑讲透最后再聊一聊从“能通”到“好用”的几个改造方向。无论你是刚接触FPGA的入门者还是正在调一个复杂项目需要UART做辅助通道的开发者这篇文章应该都能给你一些实在的参考。1. UART帧格式与设计边界先把“简单”两个字拆开看1.1 协议层一帧数据是怎么走的UART的全称是Universal Asynchronous Receiver/Transmitter异步收发器。“异步”是它的核心特征——收发双方不需要共享时钟只要约定好波特率按相同的时间节拍解析电平变化。一帧标准UART数据包含以下几个部分空闲态总线保持高电平。起始位发送端拉低1个位周期告诉接收端“数据要开始了”。数据位通常5~8位多数场景用8位低位先发。校验位可选奇校验、偶校验或无校验用于简单检错。停止位拉高1位、1.5位或2位标志一帧结束。比如发送0x55二进制0101_0101无校验、1位停止位波形就是一个低电平起始位然后是8个数据位先是LSB再是MSB最后拉高一个停止位。这里面有个关键点UART收发双方只靠“约定波特率”来同步。接收端不知道数据什么时候来只能通过检测起始位的下降沿来对齐采样时刻。这跟SPI、I2C这类带时钟线的同步协议有本质区别。很多初学者搞混uart、i2c、spi三者的适用场景其实一句话就能分清楚UART适合两个设备之间点对点低速通信不需要额外时钟线I2C用两根线挂多个设备速度慢但省引脚SPI速度快、全双工但需要额外的CS线来选设备。串口调试、GPS模块、蓝牙模块这些场景UART基本是标配。1.2 实现层源码该分成几块拿到一份UART源码先别急着复制粘贴先把模块边界理清楚。一个工程上可用的UART源码至少应该包含以下三块uart_tx发送模块负责把并行数据变成串行位流按波特率逐位发送。uart_rx接收模块负责检测起始位、按位采样、把串行数据拼成并行字节。顶层封装如uart_top或uart_loopback例化收发模块处理握手信号。如果源码里没有清晰的模块划分所有逻辑堆在一个文件里那只能用来交作业不建议直接用在项目里。因为一旦出问题你根本没法定位是发送的问题还是接收的问题。另外需要注意很多网上的源码把接口设计得特别“教学化”——只有时钟、复位、数据线、发送使能没有busy信号、没有握手。这种代码仿真没问题但接到真实系统里你根本不知道什么时候能发下一个字节很容易丢数据。后面我会详细讲工程化改造。2. 波特率生成器分频参数背后的误差账2.1 从50MHz得到115200波特率波特率生成是所有UART逻辑的地基。一个位周期多长取决于系统时钟和分频参数。假设系统时钟是50MHz目标波特率是115200。那么一个位周期需要的时钟个数是$$50_000_000 / 115_200 434.0278$$这个结果不是整数所以分频后必然存在误差。取接近的整数值434实际波特率为$$50_000_000 / 434 115_207.37$$误差只有0.006%完全在UART的容错范围之内。Verilog里常见的写法是localparam BAUD_CNT 50_000_000 / 115_200 - 1; // 433这里减1是因为计数器从0开始计数数到BAUD_CNT时正好经历了BAUD_CNT1个时钟周期也就是434个周期。注意分频参数务必用localparam在模块内部定义不要直接写死数字。这样换时钟频率或换波特率时只需要改顶层参数不用到处改代码。2.2 误差容限与参数化设计UART接收端能容忍多大的波特率误差理论上只要采样点不落入相邻位区间就不会错。一位周期误差若是超过一定范围累积误差会导致采样点偏移尤其在连续接收多字节时更容易出错。工程经验值是双方波特率误差控制在2%~3%以内基本没问题5%是极限工况不建议挑战。所以做波特率生成器时标准的做法是发送端每个位周期计数器从0数到BAUD_CNT后翻转生成bit_tick脉冲。接收端检测到起始位后先等半个位周期BAUD_CNT 1让采样点落在数据位中央然后每隔一个BAUD_CNT采样一次。接收端为什么要等半拍因为起始位下降沿的检测和实际位边沿之间有不确定性直接在每个位周期起点采样万一刚好赶在电平变化瞬间就可能采到毛刺或不确定值。等半拍后再采样能最大程度避开边沿抖动。工程上我见过不少人直接用一个计数器从0数到BAUD_CNT/2再加到BAUD_CNT逻辑也能跑但代码可读性很差。更清晰的做法是维护一个baud_cnt计数器到了BAUD_CNT清零然后在baud_cnt (BAUD_CNT 1)时采样逻辑直观也方便调参。3. 接收端采样为什么“读电平”这件事没有想象的简单3.1 起始位检测与亚稳态处理接收端的核心任务是准确判断每一位的电平。但RXD引脚输入的是外部异步信号和FPGA内部时钟没有任何相位关系直接拿这个信号来做边沿检测存在亚稳态风险。亚稳态的通俗理解是触发器的建立/保持时间得不到满足输出处于一种“既不是0也不是1”的中间状态需要一定时间才能稳定下来。虽然这种状态最终会落回0或1但落回哪个值不确定还可能把不确定值传播给后面的逻辑。标准做法是两级同步器reg rxd_ff1, rxd_ff2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rxd_ff1 1b1; rxd_ff2 1b1; end else begin rxd_ff1 rxd; rxd_ff2 rxd_ff1; end end经过两级寄存器同步后rxd_ff2可以作为后续逻辑的输入。然后再检测下降沿assign start_flag rxd_ff2_latched ~rxd_ff2;实际工程中我习惯再打一拍生成rxd_ff2_d1用rxd_ff2_d1 ~rxd_ff2来产生下降沿脉冲。这一步看似多余其实能避免在检测到下降沿的同一个时钟周期里采样逻辑和边沿逻辑打架。3.2 中点采样策略与毛刺抑制检测到起始位后接收逻辑进入采样状态。核心思路是从起始位下降沿开始计数到半个位周期时确认起始位仍为低避免毛刺误触发然后每隔一个完整位周期采样一次数据位。这里的“采样点落在位中央”并不只是理论上的好看它直接决定抗干扰能力。真实电平信号在边沿附近会有振铃、过冲如果采样点靠边沿太近采到的值可能是乱的。采位中央是最稳妥的。有的源码为了增强抗干扰会在采样点连续采样多拍比如在baud_cnt MID时连续采3个时钟取多数表决。对于115200这种速度、FPGA时钟50MHz来说3个时钟周期约60ns相对于8.68us的位周期来说窗口很小属于一种轻量滤波。实际项目中我用得不多因为多数场景下中点单次采样已经够用但如果总线较长、环境噪声大可以考虑加一个。毛刺抑制还有一个土办法检测到起始位下降沿后先等BAUD_CNT/2再检查RXD是不是真的低。如果是低说明确实来了起始位如果又变回高说明是毛刺直接放弃等待。这个逻辑简单有效强烈建议在源码里保留。4. 发送端状态机从“把字节发出去”到“不丢字节”4.1 三段式状态机与发送时序发送端比接收端简单它只需要在tx_start有效时把并行数据按位发出即可。但简单不等于可以随便写。我见过太多人用一堆计数器硬凑发送时序代码又长又难维护。推荐用三段式状态机空闲态txd保持高电平tx_busy拉低等待tx_start。起始态txd拉低1个位周期tx_busy拉高。数据态按位发送8个数据位每个位周期发送1位。停止态txd拉高1个位周期然后回到空闲态释放tx_busy。核心代码如下localparam IDLE 3d0; localparam START 3d1; localparam DATA 3d2; localparam STOP 3d3; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; txd 1b1; tx_busy 1b0; end else begin case (state) IDLE: begin tx_busy 1b0; if (tx_start) begin state START; tx_busy 1b1; end end START: begin txd 1b0; if (bit_done) state DATA; end DATA: begin txd tx_data[bit_index]; if (bit_done) begin if (bit_index 7) state STOP; else bit_index bit_index 1; end end STOP: begin txd 1b1; if (bit_done) begin state IDLE; tx_busy 1b0; end end default: state IDLE; endcase end end这里面的bit_done由波特率计数器产生每个位周期拉高一个时钟。4.2 顶层封装与回环测试设计发送和接收模块都完成后把它们封装到顶层。工程上最常用的第一个测试是回环测试FPGA把收到的每一个字节原封不动发回去。module uart_loopback #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115_200 )( input wire clk, input wire rst_n, input wire uart_rxd, output wire uart_txd ); wire [7:0] rx_data; wire rx_done; uart_rx #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE)) u_rx ( .clk(clk), .rst_n(rst_n), .rxd(uart_rxd), .rx_data(rx_data), .rx_done(rx_done) ); uart_tx #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE)) u_tx ( .clk(clk), .rst_n(rst_n), .tx_data(rx_data), .tx_start(rx_done), .txd(uart_txd), .tx_busy() ); endmodule回环测试的价值在于它能把发送和接收串起来任何一端有问题都会在PC端的串口助手里暴露出来。这一关过了后面接入你真正的业务逻辑比如把ADC数据、图像数据通过UART传回上位机才有个可靠的基础。5. 仿真通过不等于上板OK实测环节的坑5.1 testbench怎么搭才有参考价值说句实话很多网上的testbench写得很敷衍。有的直接把发送模块的输出接到接收模块的输入然后仿真出一段波形就算完事。这在功能验证层面也许够用但真到了上板阶段你会发现仿真的“理想世界”和现实差了十万八千里。一个有参考价值的testbench至少应该做到这几件事模拟真实PC端发送写一个发数据的task按波特率逐位生成串行波形包括起始位、数据位、停止位送到接收模块的rxd。这样能顺便验证你的接收逻辑是否能在正确的时刻采样。验证回环发送端发一个字节检查接收端是否收回来相同的数据打印对比结果。测试连续发送一次发多个字节中间不留大间隔验证模块在连续数据流下不丢字节。检查时序边缘把testbench里生成数据的波特率稍微调偏一点比如调1%~2%看接收端还能不能正确接收这能模拟真实世界双方时钟不同源的场景。task uart_send_byte(input [7:0] data); integer i; begin txd 1b1; #(BIT_TIME); txd 1b0; // start bit #(BIT_TIME); for (i 0; i 8; i i 1) begin txd data[i]; #(BIT_TIME); end txd 1b1; // stop bit #(BIT_TIME); end endtask这个task直接把串行波形“喂”给接收模块比单纯把tx模块接到rx模块更接近真实场景。5.2 USB转串口驱动与连接器接线仿真通过后上板遇到的第一个坑往往不在FPGA侧而在PC侧——USB转串口芯片的驱动。现在很多Windows 10/11系统能自动识别FT232R、CP2102这类常见芯片但也有系统精简、驱动冲突、或者芯片型号太老的情况需要手动安装驱动。FTDI的FT232R/FT231X用FTDI的VCP驱动Silicon Labs的CP2102N/CP2102用官方CP210x驱动这两个是最常见的建议提前下载好备用。驱动装好了设备管理器里才能看到COM口号串口助手才能打开。接线是第二个坑。最经典也最常见的错误是TXD接TXD、RXD接RXD结果收发两头都堵死。串口连接必须交叉USB转串口芯片的TXD接FPGA的RXDUSB转串口芯片的RXD接FPGA的TXD地线GND接GND。很多开发板板载了USB转串口芯片比如黑金、正点原子、Altera原厂开发板PCB上已经把交叉连线做完了你只需要确保板子上的跳线或开关拨到正确位置即可。如果你是自己画板子或者用独立USB转串口模块连接一定先查一遍原理图确认收发交叉。电平匹配也值得单独提出来。3.3V供电的FPGA和3.3V的USB转串口芯片直接相连没问题。但如果你的USB转串口模块是5V电平的FPGA引脚未必支持5V容忍接进去有烧毁风险。处理方案是加电平转换芯片或者在串口线上分压/加限流电阻。调试前用万用表量一下模块供电和数据线电平省得莫名其妙炸引脚。5.3 上板调试三板斧串口通信上板失败大多数跑不出这三类原因。我把排查顺序整理成一个表格遇到问题按这个顺序查效率最高现象大概率原因排查方向完全没反应串口助手收不到任何数据引脚约束错误、时钟没起来、复位没释放、驱动没装好查引脚约束文件用ILA抓clk和rst_n信号收到数据但是乱码波特率不匹配、收发波特率误差过大、电平不匹配核对串口助手波特率设置用逻辑分析仪看实际波特率能收到一部分但丢字节没有握手信号、上位机连续发送过快、FIFO深度不够查tx_busy信号发一个字节等一个字节再加FIFO回显多了一个字节或错位发送启动时序与接收rx_done配合不当检查rx_done信号是否持续多拍需打一拍或做脉冲化处理其中“乱码”这个问题特别容易在FT232R/CP2102这类USB转串口上出现。这是因为USB转串口芯片内部的波特率发生器同样存在误差两个误差叠加后可能超出容限尤其是非整数分频的波特率比如9600在12MHz时钟下分频就不整。遇到这种情况优先把目标波特率改成能整除的整数或者调高系统时钟频率。6. 源码进阶从“能通”到“好用”6.1 参数化与通用化改造真正的工程源码不应该是一份“写死了”的代码。参数化设计是第一步把CLK_FREQ、BAUD_RATE、DATA_WIDTH、PARITY、STOP_BIT全部提升为模块参数。这样同一份源码既能在50MHz的板子上跑115200也能在100MHz的板子上跑9600改一个顶层参数就行。在此基础上发送端一定要补上tx_busy信号。tx_busy的作用是告诉上游“我现在正忙着你不要再发数据”。如果上游不管这个信号连续给多个tx_start脉冲后面的数据就会被吞掉。加一个简单的req/ack握手也是同样的道理。接收端还建议增加rx_error信号用于检测帧错误。当接收端在停止位采到低电平时说明这帧数据有问题把rx_error拉高一个周期方便上层协议层做校验或重传。这一步看似简单但在实际项目中排查噪声干扰和数据错位时非常有用。6.2 与FIFO、图像采集、SoC调试结合UART本身的速率瓶颈很明显。115200波特率意味着理论最大吞吐量约11.5KB/s实际扣除起始位、停止位和协议开销有效数据率还要再打个折。所以一旦数据量上来不可能让业务逻辑直接往UART发送模块里塞数据中间必须加FIFO缓存。典型架构是业务模块把数据写入FIFOUART发送模块空闲时从FIFO读出并发送。这样业务模块不用等UART的慢速时序数据到达率高于发送速率时也不会丢数据只要FIFO深度够。我在做FPGA图像采集项目的时候就用过类似的架构摄像头数据流比如mipi csi-2接口采集的数据经过简单打包写入一个FIFO再通过UART逐包回传到上位机。调试模式下UART只用来回传关键帧或状态信息不要求高速率这个小通道非常好用。这也是UART在大型FPGA项目中最常见的定位——不是主力数据传输通道而是调试、监控、指令下发的“生命线”。另外在SoC或嵌入式软核项目中UART源码还经常被封装成系统外设作为printf的底层输出通道。集成时需要注意中断或轮询方式的选择轮询方式简单但占CPU中断方式复杂一点但更高效。尤其在调试嵌入式内核或驱动源码时串口打印基本是唯一能实时观察系统状态的窗口可靠性比性能更重要。关于“Corundum项目硬件移植”这类场景我多说一句。像Corundum这种大型开源项目UART通常是整个移植流程里的“第一站”——先把UART跑通验证PCIe枚举、DMA、中断链路是否正常再一步步往上层功能走。所以在大型项目中UART源码往往不只是几个模块它和时钟复位管理单元、AXI总线、寄存器组都是绑定的。拿到这类源码时别只盯着收发逻辑还要看它挂在哪个总线上、怎么被CPU访问才能真正改得动。最后再分享一个我自己反复踩过坑之后的习惯调试UART链路时永远先做回环测试再接业务数据。回环通过说明物理链路和收发逻辑都没问题回环不通过问题大概率在底层先别去查业务逻辑。把“链路问题”和“逻辑问题”分开排查能省下一大半调试时间。这个习惯也适用于你调任何一根看似简单的串口线。本文还有配套的精品资源点击获取
返回列表