尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA UART串口源码详解:从协议到回环验证的完整指南

FPGA UART串口源码详解:从协议到回环验证的完整指南 简介基于Altera平台的FPGA UART串口源码采用Verilog实现完整异步串口通信链路面向FPGA学习者、嵌入式开发人员及电子设计竞赛备赛者。资源包含UART_TX发送器、UART_RX接收器、波特率发生器及A4_Uart_Top顶层封装模块覆盖状态机控制、奇偶校验、中断处理等关键设计点可直接用于学习参考或二次移植。压缩包共835个文件约7.05MB除Verilog源文件.v外还包含Quartus II工程配置.qpf、.qsf、编译综合中间文件cdb、hdb、ddb、仿真激励.vwf、.vcd以及脚本说明等便于完整还原工程环境并观察仿真时序。通过阅读源码可深入理解UART帧格式、波特率分频计算、状态机跳转逻辑以及异步收发电路的实现思路同时工程内附带的编译报告与波形文件也能帮助排查常见时序问题。目前已有343人学习浏览适合希望快速掌握FPGA串口通信原理并动手实践的开发者。 做FPGA开发串口基本是第一道门槛。不管是刚把开发板点亮、跑通流水灯的新手还是已经在折腾DDR3、PCIe的老手只要涉及调试和通信UART永远是那个最朴实也最刚需的接口。很多朋友找我聊FPGA uart串口源码其实核心诉求很一致想要一套能直接用、能讲清楚原理、遇到问题还能自己排查的串口收发方案。这篇文章我就把整套UART串口源码的设计思路、协议细节、完整代码和上板验证过程一次性讲透。内容面向从零开始的初学者也适合想深入理解串口时序细节的进阶玩家。我会把源代码里每个模块为什么这么写、波特率怎么算、采样点怎么定、回环测试怎么做都交代清楚最后再分享一些我自己在调试中踩过的坑和排查技巧。1 整体设计与思路拆解1.1 为什么用UART作为FPGA入门通信接口串口这个东西协议简单、硬件成本低、调试手段成熟几乎是所有嵌入式系统默认标配的调试通道。对于FPGA来说UART还有一层特殊意义它是理解时序和状态机最好的练手项目。我做FPGA这几年一个很深的感受是很多人一开始就奔着复杂协议去比如PCIe、DDR控制器、Ethernet MAC结果被时序约束和跨时钟域问题折磨到怀疑人生。而UART协议足够简单——一帧数据就那么几个bit收发双方只需要约定好波特率不需要复杂的握手过程不需要仲裁机制非常适合作为第一个真正意义上的通信协议来学习。另外UART调试本身也是一项基础技能。FPGA上板后程序运行情况没法像软件那样用printf打印而串口恰好是最接近printf的调试手段。你可以在FPGA内部把关键信号通过串口发出来在电脑上观察这样就能快速定位逻辑问题。所以不管从学习还是实用角度把UART源码吃透都非常值。1.2 UART协议的核心一帧数据的组成UART的全称是Universal Asynchronous Receiver/Transmitter通用异步收发器。它的异步体现在收发双方共用一个约定的波特率而不是像I2C那样有专门的时钟线。理解这一点非常关键UART通信不依赖时钟线同步而是靠波特率匹配和数据帧结构来判断每个bit的边界。标准的一帧UART数据由下面几部分组成空闲状态线路保持高电平。这是UART最容易忽略但很重要的特性——空闲时TX/RX引脚都是高电平。起始位一个低电平脉冲标志一帧数据的开始。接收端正是靠检测这个下降沿来启动接收逻辑。数据位通常是5到8位常见配置是8位先发LSB最低位再发MSB最高位。校验位可选有偶校验、奇校验、无校验三种模式用于简单的错误检测。停止位1位、1.5位或2位高电平标志一帧数据的结束。举个例子如果我们要发送字符AASCII码0x41二进制0100_0001按照LSB-first的顺序线上实际发送的bit流是起始位0然后是1、0、0、0、0、0、1、0最后是停止位1。所以完整的一帧就是0_10000010_1。这个顺序很多新手第一次写代码时容易搞反需要特别注意。一帧的数据格式可以用一个简单的表格来表示字段电平宽度作用空闲位高电平任意线路空闲标识起始位低电平1 bit时间告知接收端数据开始数据位LSB先发5~8 bit实际传输的数据校验位奇/偶校验0或1 bit简单检错停止位高电平1/1.5/2 bit告知接收端数据结束1.3 波特率是怎么算出来的波特率Baud Rate是UART通信最核心的参数表示每秒传输的bit数。常见的波特率有9600、115200、460800等其中115200是最常用的因为它在常规时钟频率和误差容忍度下能达到不错的平衡。波特率的计算公式很简单分频系数 系统时钟频率 / 波特率。以我的开发板为例系统时钟是50MHz目标波特率115200。计算如下50000000 / 115200 434.02取整数就是434。这意味着每计数434个时钟周期就对应一个bit的时间。这里有个关键取舍因为434是取整后的值实际波特率是50000000/434 115207.4误差约为0.006%远小于UART允许的3%误差范围所以完全没问题。但如果你用的是50MHz要跑9600波特率分频系数是5208.33取整5208实际波特率9600.6误差更小。反过来如果用33MHz这种非标准时钟跑115200分频系数是286.46取整286实际波特率是115384误差0.16%虽然仍然在容许范围内但余量就比较小了。所以设计UART模块时分频系数的计算和校验一定要做别直接照搬别人的参数。波特率误差的公差有个经验法则收发双方各自波特率误差不超过3%且总误差不超过5%基本就能稳定通信。但实际情况中线缆长度、电平转换芯片的延迟都会增加误差所以建议把单端误差控制在1%以内更稳妥。2 核心细节解析与实操要点2.1 模块划分与信号定义完整的UART串口源码不会是单一大模块而是拆成几个职责单一的子模块。这样做的好处是便于调试、便于复用、便于维护。我自己常用的划分方式是顶层模块uart_top负责例化发送和接收模块以及处理回环或数据通路逻辑。发送模块uart_tx把并行数据转换成串行bit流发送出去。接收模块uart_rx检测起始位采样串行数据恢复成并行数据。这种划分方式对应到工程结构里非常清晰每个模块就干一件事。顶层模块的信号定义也很直观信号名方向宽度说明clkinput1系统时钟50MHzrst_ninput1异步复位低有效tx_datainput8待发送的数据tx_startinput1发送启动信号高电平有效tx_busyoutput1发送忙标志txoutput1串行发送引脚rxinput1串行接收引脚rx_dataoutput8接收到的数据rx_doneoutput1接收完成标志这里的tx_busy和rx_done非常重要它们是上层逻辑判断当前模块状态的依据。比如你要实现收到数据后自动回发就得靠rx_done触发tx_start同时检查tx_busy不为高才能发起发送。2.2 发送模块的设计要点状态机的核心思路发送模块本质上是一个状态机它的任务是把并行数据按时间顺序一位一位发出去。我在代码里用了一个经典的四状态状态机IDLE、START、DATA、STOP。IDLE状态是空闲态此时tx引脚保持高电平。一旦检测到tx_start信号就跳转到START状态。START状态持续一个bit时间将tx拉低发出起始位。DATA状态则按bit位编号从bit 0到bit 7依次把数据放到tx引脚上每个bit持续一个bit时间。最后进入STOP状态tx恢复高电平持续一个bit时间然后回到IDLE。有个细节值得单独说tx_data的数据锁存时机。如果顶层逻辑在发送过程中又修改了tx_data那发送的数据就会错乱。所以我通常会在IDLE状态检测到tx_start时先把tx_data锁存到内部寄存器这样后续状态机里的数据源就是稳定的了。发送过程的时序驱动采用分频计数器定义bit_cnt计数一个bit周期内的时钟数当计数到434时清零并切换bit状态。这里需要注意计数器清零和状态跳变的配合我在实际调试中发现如果计数器清零晚了半个时钟周期整个时序会整体偏移导致对端采到的电平不对。2.3 接收模块的设计要点中间采样法接收模块比发送模块稍难一点难就难在异步两个字上。接收端不知道发送端什么时候会发数据只能靠检测起始位的下降沿来同步。这里我用了一个在实际工程中很常用的技术中间采样法。中间采样法的思路是这样的检测到起始位下降沿后并不立即读取数据位电平而是等半个bit时间在起始位的正中心位置再确认一次电平为低。如果确认是低说明这确实是起始位可以开始接收。然后每个数据位的中心位置再采一次样连续采样8次就得到了完整的8位数据。为什么要等半bit时间再确认起始位因为信号在传输过程中可能存在毛刺或干扰一个窄的低脉冲可能让接收端误判为起始位。加入半bit时间的确认机制能有效过滤这类毛刺。采样点的计算方法很重要直接用分频计数器实现。假设一个bit时间是434个时钟周期那么起始位确认点是217个时钟周期处434的一半后续每个数据位的采样点则在起始位确认点之后依次增加434。这个半bit等待 整bit采样的节奏是整个接收模块的精髓。这个方案有一个明显的优点和一个小缺陷。优点是实现简单、稳定性好只要波特率误差不大采样点始终能落在数据位的中心区域。缺陷在于它没有采用更高倍的过采样比如16倍过采样所以在极高速率或极端干扰下的鲁棒性会弱一些。但对于常规学习和一般工业应用中间采样法完全够用。3 实操过程与核心环节实现3.1 完整Verilog源码实现下面给出一个经过上板验证的完整UART回环源码功能是FPGA接收PC发来的数据然后把同样的数据自动回传给PC。这是验证串口通路最直接的方式。发送模块代码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_busy, output reg tx ); localparam BIT_TIME CLK_FREQ / BAUD_RATE - 1; reg [15:0] cnt; reg [2:0] bit_index; reg [7:0] tx_data_reg; reg [1:0] state; localparam IDLE 2d0; localparam START 2d1; localparam DATA 2d2; localparam STOP 2d3; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; tx 1b1; tx_busy 1b0; cnt 16d0; bit_index 3d0; tx_data_reg 8d0; end else begin case (state) IDLE: begin tx 1b1; tx_busy 1b0; if (tx_start) begin tx_data_reg tx_data; tx 1b0; state START; tx_busy 1b1; cnt 16d0; end end START: begin if (cnt BIT_TIME) begin cnt 16d0; bit_index 3d0; state DATA; tx tx_data_reg[0]; end else begin cnt cnt 1b1; end end DATA: begin if (cnt BIT_TIME) begin cnt 16d0; if (bit_index 3d7) begin state STOP; tx 1b1; end else begin bit_index bit_index 1b1; tx tx_data_reg[bit_index 1b1]; end end else begin cnt cnt 1b1; end end STOP: begin if (cnt BIT_TIME) begin cnt 16d0; state IDLE; tx_busy 1b0; end else begin cnt cnt 1b1; end end default: state IDLE; endcase end end endmodule接收模块代码module uart_rx #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200 )( input wire clk, input wire rst_n, input wire rx, output reg [7:0] rx_data, output reg rx_done ); localparam BIT_TIME CLK_FREQ / BAUD_RATE - 1; localparam HALF_BIT CLK_FREQ / BAUD_RATE / 2 - 1; reg [15:0] cnt; reg [2:0] bit_index; reg [7:0] rx_data_reg; reg rx_d1; reg rx_d2; reg rx_negedge; reg [1:0] state; localparam IDLE 2d0; localparam START 2d1; localparam DATA 2d2; localparam STOP 2d3; // 两级同步 下降沿检测 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_d1 1b1; rx_d2 1b1; rx_negedge 1b0; end else begin rx_d1 rx; rx_d2 rx_d1; rx_negedge rx_d2 ~rx_d1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; cnt 16d0; bit_index 3d0; rx_data_reg 8d0; rx_data 8d0; rx_done 1b0; end else begin case (state) IDLE: begin rx_done 1b0; if (rx_negedge) begin state START; cnt 16d0; end end START: begin // 半bit后再确认起始位 if (cnt HALF_BIT) begin if (rx_d2 1b0) begin cnt 16d0; bit_index 3d0; state DATA; end else begin state IDLE; end end else begin cnt cnt 1b1; end end DATA: begin if (cnt BIT_TIME) begin cnt 16d0; rx_data_reg[bit_index] rx_d2; if (bit_index 3d7) begin state STOP; end else begin bit_index bit_index 1b1; end end else begin cnt cnt 1b1; end end STOP: begin if (cnt BIT_TIME) begin cnt 16d0; state IDLE; rx_data rx_data_reg; rx_done 1b1; end else begin cnt cnt 1b1; end end default: state IDLE; endcase end end endmodule顶层回环模块代码module uart_loopback #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200 )( input wire clk, input wire rst_n, input wire rx, output wire tx ); wire [7:0] rx_data; wire rx_done; wire tx_busy; reg [7:0] tx_data; reg tx_start; // 接收完成且发送空闲时自动发起回发 always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_data 8d0; tx_start 1b0; end else begin if (rx_done !tx_busy) begin tx_data rx_data; tx_start 1b1; end else begin tx_start 1b0; end end end uart_rx #( .CLK_FREQ (CLK_FREQ), .BAUD_RATE (BAUD_RATE) ) u_uart_rx ( .clk (clk), .rst_n (rst_n), .rx (rx), .rx_data (rx_data), .rx_done (rx_done) ); uart_tx #( .CLK_FREQ (CLK_FREQ), .BAUD_RATE (BAUD_RATE) ) u_uart_tx ( .clk (clk), .rst_n (rst_n), .tx_data (tx_data), .tx_start (tx_start), .tx_busy (tx_busy), .tx (tx) ); endmodule3.2 管脚约束与硬件连接源码写完之后还需要正确绑定管脚FPGA才能跟外部硬件通信。这一步看似简单但很多新手在这里翻车尤其是对USB转串口芯片工作原理不熟悉的话。我用的开发板板载了一个USB转UART芯片它的TX连接FPGA的RX引脚它的RX连接FPGA的TX引脚。这段线是交叉的类似RS-232的2、3脚交叉。千万别把FPGA的TX和RX直接接到同一条线上那样就收不到数据了。XDC约束文件的关键内容大致如下create_clock -period 20.000 -name sys_clk [get_ports clk] set_property -dict { PACKAGE_PIN P17 IOSTANDARD LVCMOS33 } [get_ports clk] set_property -dict { PACKAGE_PIN R18 IOSTANDARD LVCMOS33 } [get_ports rx] set_property -dict { PACKAGE_PIN R17 IOSTANDARD LVCMOS33 } [get_ports tx]这里需要说明P17、R18、R17只是我手头这个板子的引脚编号不同的开发板管脚定义完全不同。实际使用时一定要参照你自己板卡原理图。另外UART信号通常不严格要求等长或特性阻抗只要连线稳定、地线良好即可这跟LVDS或者高速SerDes相比要宽松得多。连接电脑时还需要注意USB转串口芯片的驱动问题。目前市面上常见的芯片有CH340、CP2102、FT232等。CH340是很多国产开发板的标配Windows系统通常会自动识别个别情况下需要手动安装驱动。CP2102和FT232的驱动也很成熟官方都会提供。驱动装好之后在设备管理器里能看到对应的COM口编号记下来后面调试要用。3.3 上板验证流程用串口调试助手做回环测试代码下载到FPGA后第一步验证就是回环测试。回环测试的思路很简单PC通过串口调试助手发送一串字符FPGA收到后原样返回如果调试助手里能看到一样的字符说明整个链路是通的。具体操作步骤是这样的打开串口调试助手选择正确的COM口。设置波特率115200数据位8停止位1无校验。在发送区输入Hello FPGA点击发送。观察接收区是否出现Hello FPGA。如果一切正常接收区会原样显示发送的内容。这个测试通过说明FPGA的UART收发链路已经打通。但这里我要提醒一点回环测试通过不等于UART模块就100%没问题。因为回环只验证了收发通路没有验证更复杂的多字节连续传输和丢帧处理。所以我会建议再做一步压力测试一次性发几百个字节看是否有丢数据或乱码。如果压力测试也OK那这个源码基本可以放心用了。除了串口调试助手还有一种更专业的验证方式用逻辑分析仪抓取UART波形或者如果你用的Vivado可以在工程里插入ILA集成逻辑分析仪核来观察FPGA内部信号。我个人很推荐ILA方式它能同时看到内部的rx_state、tx_state、rx_data寄存器等信号排查问题时比盲猜要高效得多。4 常见问题与排查技巧实录4.1 问题排查速查表我在调试UART的过程中积累了一些常见问题的排查经验整理成下面这张速查表覆盖了从源码仿真到实际硬件的各种情况现象可能原因排查方法发送端发出接收端全是乱码波特率误差过大重新计算分频系数用逻辑分析仪测实际bit宽度完全收不到数据管脚约束错误核对原理图确认TX/RX交叉连接回环测试一半成功一半乱码USB转串口驱动不稳定换一根数据线或换一个USB口再试发送一次PC收到两次顶层握手信号没有清除检查tx_start是否在发送完成后及时拉低接收数据某一位总是不对采样点跟数据位边缘重合增大波特率余量或改用16倍过采样策略多字节连续发送时丢数据PC发送速度超过FPGA处理能力在顶层增加FIFO做缓冲或减慢发送节奏4.2 调试心得从仿真开始再上板我给新手最重要的建议是先仿真后上板。很多朋友代码写完就直接烧到板子上结果一个乱码就开始怀疑人生。实际上UART模块的仿真非常容易做只需要写一个简单的testbench生成一个激励信号即可。在Vivado的仿真环境里testbench可以模拟PC发送一帧数据。比如模拟发送0x55二进制0101_0101这个数据的特征非常明显每个bit的电平都在跳变非常适合观察采样点是否正确。如果仿真波形中接收端rx_data正确恢复出0x55再上板测试成功率会大幅提升。另一个很有用的技巧是在调试阶段可以实现一个计数器的自发自收功能——FPGA内部每秒钟通过串口发送一个递增的计数值。如果PC端能连续收到0, 1, 2, 3...这样的数据说明发送通路没问题。然后再把rx数据回环到tx测试接收通路。这种分步验证的方式能帮你迅速定位问题出在发送侧还是接收侧。关于FPGA管脚连接器我多说一句。如果你的板卡没有板载USB转串口芯片而是需要外接串口模块要注意模块和FPGA之间是TTL电平还是RS-232电平。大部分USB转串口小板是TTL电平3.3V或5V可以直接连接FPGA的IO引脚。但如果是标准的RS-232电平电压范围是-12V到12V绝对不能直接连FPGA必须经过MAX3232等电平转换芯片。虽然现在的FPGA开发板大多板载了转换电路但这块知识还是要有的。在我自己的项目中这个UART回环源码后来做了不少扩展。比如在顶层加了一个FIFO让PC可以连续高速发送数据而不会丢帧又比如把UART和FPGA内部的寄存器读写协议结合起来实现通过串口远程配置FPGA内部参数的功能。可以说UART回环虽然基础但它是一把钥匙打开了FPGA与外部世界通信的大门。如果真让我说这几个月做FPGA串口通信最大的感受那就是不要迷信复杂的设计技巧把基础协议吃透、把每一个时序细节弄清楚才是真正的捷径。希望这篇文章能帮你在FPGA串口开发路上少走一些弯路。本文还有配套的精品资源点击获取
返回列表