ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FPGA实战:从零实现UART串口通信的完整指南

FPGA实战:从零实现UART串口通信的完整指南 1. 先从最底层说起UART 到底在干什么搞 FPGA 的人十有八九第一个实战项目就是 UART。原因很简单它简单、直观、调试方便而且几乎每个嵌入式系统都离不开它。但越是这样入门级的东西越值得把它吃透——后面做 PCIe、DDR、以太网的时候很多调试手段可都建立在会玩串口的基础上。拿我自己的经历来说刚接触 FPGA 那会儿手里一块开发板、一根 USB 转 TTL 线、一个串口助手就能把上板验证这件事跑通。当时觉得不就是收发几个字节吗后来做的项目多了发现 UART 的坑其实比想象中多波特率误差、亚稳态处理、FIFO 溢出、丢字节、时序约束……每一个都够你喝一壶。这篇文章我想从一个踩过坑、填过坑的从业者角度把 UART 串口通信在 FPGA 上的实现完整过一遍。从协议原理讲到 RTL 设计从仿真验证讲到上板调试再附上我实际操作中总结出来的经验教训。不管你是刚接触 FPGA 的新手还是想回头把基础打牢的老手这篇应该都能给你点东西。先交代一下本文的定位不涉及具体开发板型号以 Verilog 为例代码思路可以平移到任意 FPGA 平台Xilinx、Intel、国产厂商都行。核心就是让你真正理解 UART 模块内部发生了什么以及怎么把它用得稳。2. 协议层拆解为什么 UART 能对得上话2.1 串行通信的“约定”起始位、数据位、校验位、停止位UARTUniversal Asynchronous Receiver/Transmitter是一种异步串行通信协议。所谓异步就是收发双方没有独立的时钟线靠的是事先约定好的波特率Baud Rate来采样数据。也就是大家常说的 9600、115200 这些数字它代表每秒传输多少个比特。一帧 UART 数据长这样空闲状态线路保持高电平。起始位发送方拉低一个比特周期告诉接收方我要开始发数据了。数据位从最低位LSB开始依次发送 5~8 个数据位最常用的是 8 位。校验位可选奇校验或偶校验用来做简单的错误检测实际项目中用得越来越少但工业现场总线上偶尔还会见到。停止位拉高 1 个、1.5 个或 2 个比特周期表示一帧结束。为什么一定要有起始位和停止位因为接收端就是靠起始位的下降沿来对齐采样时钟的。这是 UART 能可靠通信的关键——不是靠绝对时间对齐而是靠边沿触发重新同步。2.2 波特率与采样时钟不是越快越好波特率决定了每个比特占多长时间。以 115200 为例一个比特周期大约是 8.68 微秒。FPGA 内部通常跑 50MHz 或 100MHz 的时钟所以我们需要用一个计数器来数出这个比特周期。这里有一个非常关键的设计点接收端应该在每个比特的中间位置采样。为什么因为信号从发送端到接收端有传播延迟而且收发双方的时钟存在微小偏差如果在一个比特刚开始或快结束的时候采样很容易采到边沿附近的跳变造成误码。在比特正中间采样容错能力最强。一般做法是FPGA 内部用波特率的 16 倍频时钟来做过采样。比如 115200 波特率16 倍就是 1.8432MHz。每个比特周期采 16 个点取中间那个点作为有效数据。当然直接用系统时钟分频产生一个采样时钟也行只要误差控制在可接受范围内。我把常见波特率的比特周期和分频计数值整理成了表格大家可以对照参考假设系统时钟 50MHz波特率比特周期us50MHz 分频计数值16倍过采样计数值9600104.1752083251920052.0826041623840026.041302815760017.36868541152008.6843427注意分频计数值 系统时钟频率 / 波特率然后取整。取整带来的误差只要小于 2%~3%一般都能正常通信。115200 在 50MHz 下取整误差很小这也是它成为默认波特率的原因之一。2.3 RS232、TTL 电平与 USB 转串口实物链路里发生了什么很多人一开始会被 RS232、TTL、USB 转串口这些词搞晕。我理一下TTL 电平FPGA 的引脚直接输出的就是这种电平0V 表示逻辑 03.3V或 1.8V、2.5V取决于 Bank 电压表示逻辑 1。FPGA 和单片机之间短距离通信直接连 TTL 就行。RS232 电平这是老式串口DB9 接头用的标准逻辑 1 是 -12V ~ -3V逻辑 0 是 3V ~ 12V。之所以搞这么极端的电压是为了提高抗干扰能力和传输距离。FPGA 不能直接接 RS232 电平必须经过 MAX3232 这类电平转换芯片。USB 转 TTL现在笔记本电脑上没有串口了大家普遍用 USB 转串口线比如 FT232、CH340 这类芯片做的连接 FPGA 和电脑。这类线内部已经做好了电平转换输出端就是 TTL 电平可以直接接 FPGA 引脚。所以说FPGA 项目里说的UART 串口通信绝大多数场景其实是FPGA 的 TTL 引脚 —— USB 转 TTL 模块 —— 电脑上的串口助手。理解这条链路很重要因为很多通信不上的问题根源就出在这条链路的某个环节上后面我会专门讲排查方法。3. 从零手写 RTL发送端、接收端与顶层模块3.1 发送端设计状态机驱动层层拆解发送端相对简单核心就是一个状态机空闲、发起始位、发数据位、发停止位。我直接给出一个保守但好用的设计思路module uart_tx #( parameter CLK_FREQ 50_000_000, // 系统时钟 50MHz parameter BAUD_RATE 115200 ) ( input wire clk, input wire rst_n, input wire tx_start, // 发送使能上升沿触发 input wire [7:0] tx_data, // 要发送的数据 output reg tx, // 串行输出 output reg tx_busy // 忙信号 ); localparam BIT_PERIOD CLK_FREQ / BAUD_RATE; reg [15:0] cnt; // 波特率分频计数器 reg [2:0] bit_index; // 当前发送到第几个 bit0~7 数据位 reg [3:0] state; localparam IDLE 4d0; localparam START 4d1; localparam DATA 4d2; localparam STOP 4d3; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; tx 1b1; // 空闲时拉高 tx_busy 1b0; cnt 0; bit_index 0; end else begin case (state) IDLE: begin tx 1b1; tx_busy 1b0; if (tx_start) begin state START; tx_busy 1b1; cnt 0; end end START: begin tx 1b0; // 拉低一个比特周期 if (cnt BIT_PERIOD - 1) begin cnt 0; bit_index 0; state DATA; end else begin cnt cnt 1b1; end end DATA: begin tx tx_data[bit_index]; if (cnt BIT_PERIOD - 1) begin cnt 0; 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 tx 1b1; // 停止位拉高 if (cnt BIT_PERIOD - 1) begin cnt 0; state IDLE; end else begin cnt cnt 1b1; end end endcase end end endmodule这段代码的核心逻辑就是每个状态停留 BIT_PERIOD 个时钟周期然后切换到下一个状态。tx_start 信号由上层逻辑给出tx_busy 用来告诉上层我现在忙别往我这儿塞数据。一个我吃了亏才明白的细节tx 在空闲状态必须保持高电平。UART 协议里线路空闲就是高电平。如果复位后 tx 误拉低了接收端会误判为起始位然后收到一堆垃圾数据。这个问题在仿真里往往看不出来上板后表现为主机一直收到乱码。3.2 接收端设计边沿检测、过采样与数据恢复接收端比发送端复杂因为接收方不知道数据什么时候会来必须时刻监视线路。设计上分三块空闲检测、起始位确认、数据采样。我在实际项目中用的是一个带 16 倍过采样的版本。思路是检测到下降沿后启动一个采样时钟在每个比特周期的第 8 个采样点也就是中点附近读取电平值。这样能最大化避开边沿区域的毛刺。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 CLK_PER_BIT_16X CLK_FREQ / BAUD_RATE / 16; reg [15:0] cnt; reg [3:0] sample_cnt; reg [3:0] bit_cnt; reg rx_d1, rx_d2; // 打两拍消除亚稳态 reg rx_negedge; reg [2:0] state; localparam IDLE 3d0; localparam START 3d1; localparam DATA 3d2; localparam STOP 3d3; // 同步打拍 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_d1 1b1; rx_d2 1b1; end else begin rx_d1 rx; rx_d2 rx_d1; end end assign rx_negedge rx_d2 ~rx_d1;这里有一个非常关键的细节rx 是外部异步信号必须打两拍同步后再使用。为什么因为如果不打拍外部信号可能刚好在时钟上升沿附近发生变化触发寄存器进入亚稳态导致整个状态机跑飞。打两拍之后亚稳态被消化掉的概率在工程上可以接受。接收状态机的核心逻辑always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; rx_data 0; rx_done 1b0; end else begin rx_done 1b0; case (state) IDLE: begin cnt 0; sample_cnt 0; bit_cnt 0; if (rx_negedge) begin state START; cnt 0; sample_cnt 0; end end START: begin // 在起始位中点再做一次确认排除毛刺干扰 if (cnt CLK_PER_BIT_16X - 1) begin cnt 0; sample_cnt sample_cnt 1b1; if (sample_cnt 4d7) begin if (~rx_d2) begin state DATA; sample_cnt 0; end else begin state IDLE; // 误触发回到空闲 end end end else begin cnt cnt 1b1; end end DATA: begin if (cnt CLK_PER_BIT_16X - 1) begin cnt 0; sample_cnt sample_cnt 1b1; if (sample_cnt 4d15) begin sample_cnt 0; rx_data[bit_cnt] rx_d2; if (bit_cnt 3d7) begin state STOP; bit_cnt 0; end else begin bit_cnt bit_cnt 1b1; end end end else begin cnt cnt 1b1; end end STOP: begin if (cnt CLK_PER_BIT_16X - 1) begin cnt 0; sample_cnt sample_cnt 1b1; if (sample_cnt 4d7) begin // 停止位中点采样应为高电平 if (rx_d2) begin rx_done 1b1; end state IDLE; end end else begin cnt cnt 1b1; end end endcase end end endmodule很多教材上只做一次起始位采样其实不够严谨。在起始位中点再确认一次电平是否为低可以有效地把窄毛刺造成的误触发挡在门外。这是我做工业级产品时总结下来的经验——在强电磁干扰环境下这个二次确认能明显降低误码率。3.3 顶层模块与回环测试先证明自己能发能收发送端和接收端写完之后把它们封装成一个顶层模块对外暴露的接口就三个时钟、复位、以及收发引脚。为了便于板级验证我在顶层做了一个回环逻辑收到的数据直接发回去。这样在电脑串口助手上就能看到发什么、回什么的效果。module uart_loopback #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200 ) ( input wire clk, input wire rst_n, input wire uart_rx, output wire uart_tx ); wire [7:0] rx_data; wire rx_done; wire tx_busy; reg tx_start; reg [7:0] tx_data; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_start 1b0; tx_data 8d0; end else if (rx_done !tx_busy) begin tx_data rx_data; tx_start 1b1; // 产生一拍脉冲 end else begin tx_start 1b0; end end uart_tx #( .CLK_FREQ (CLK_FREQ), .BAUD_RATE (BAUD_RATE) ) u_tx ( .clk (clk), .rst_n (rst_n), .tx_start (tx_start), .tx_data (tx_data), .tx (uart_tx), .tx_busy (tx_busy) ); uart_rx #( .CLK_FREQ (CLK_FREQ), .BAUD_RATE (BAUD_RATE) ) u_rx ( .clk (clk), .rst_n (rst_n), .rx (uart_rx), .rx_data (rx_data), .rx_done (rx_done) ); endmodule这里有一个工程上很容易踩的坑tx_start 必须是单脉冲信号不能一直拉高。我见过有人直接写成tx_start rx_done;结果发送端在同一帧数据期间反复触发状态机直接乱套。正确的做法是检测到 rx_done 后把 tx_start 拉高一拍下一拍立刻拉低。4. 仿真验证上板之前先让波形开口说话4.1 为什么仿真能发现 90% 的设计问题我个人的习惯是代码写完先不着急综合布线先跑行为仿真。很多逻辑错误状态机跳转不对、计数器边界算错、信号时序不对在仿真里一眼就能看出来。等仿真通过了再上板这时候抓到的问题基本只剩物理层面的问题引脚约束、时钟约束、电气特性等。给发送端写一个简单的 testbench模拟一个 115200 波特率发送端往我们的接收模块发数据timescale 1ns / 1ps module tb_uart_rx; reg clk; reg rst_n; reg uart_rx_in; wire [7:0] rx_data; wire rx_done; // 生成 50MHz 时钟 initial clk 0; always #10 clk ~clk; // 20ns 一个周期 // 生成复位 initial begin rst_n 0; #100; rst_n 1; end // 模拟发送一个字节 0x55 (01010101) // 波特率 115200 - 位周期约 8680ns reg [7:0] send_data; integer i; task uart_send_byte; input [7:0] data; begin uart_rx_in 1b1; // 空闲 #8680; uart_rx_in 1b0; // 起始位 #8680; for (i 0; i 8; i i 1) begin uart_rx_in data[i]; // LSB 先发 #8680; end uart_rx_in 1b1; // 停止位 #8680; end endtask initial begin uart_rx_in 1b1; #200; send_data 8h55; uart_send_byte(send_data); #100000; $finish; end uart_rx #( .CLK_FREQ (50_000_000), .BAUD_RATE (115200) ) u_rx ( .clk (clk), .rst_n (rst_n), .rx (uart_rx_in), .rx_data (rx_data), .rx_done (rx_done) ); endmodule跑完这个 testbench重点看三个信号rx_done 是否在停止位之后拉高一拍、rx_data 是否等于 0x55、以及采样点是否落在每个比特的中点附近。4.2 验证波特率误差8700ns 与 8680ns 的较量这里补一个仿真中的小细节我在 testbench 里模拟发送端时故意把 bit 周期写成 8680ns根据 115200 算出来是 8680.56ns取整后是 8680而接收端内部用的是 50MHz 时钟分频实际比特周期也是 8680ns因为 50MHz 分频计数 434 个周期434 × 20ns 8680ns。两边几乎完全一致所以采样没问题。但实际工程中如果收发两端的时钟精度不同比如发送方是 52MHz 的 MCU接收方是 50MHz 的 FPGA连续传几十个字节就可能因为误差累积导致采样点偏移出比特区间。怎么检查在仿真里把 testbench 里#8680改成#8800再看看还能不能正确接收。如果还能收到说明你的接收端容错能力不错如果收不到那就得考虑用小数分频或者更高倍过采样来提升容错。5. 上板调试引脚约束、硬件连接与常见问题5.1 第一步正确分配引脚综合通过之后进入实现Implementation阶段这时候需要写 XDC 约束文件Xilinx 平台或 QSF 文件Intel 平台。以 Xilinx 为例至少要有以下约束set_property PACKAGE_PIN U9 [get_ports {uart_rx}] set_property IOSTANDARD LVCMOS33 [get_ports {uart_rx}] set_property PACKAGE_PIN V11 [get_ports {uart_tx}] set_property IOSTANDARD LVCMOS33 [get_ports {uart_tx}] create_clock -period 20.000 -name sys_clk [get_ports {clk}]这里有个高频翻车点FPGA 引脚所在的 Bank 供电电压必须与 IOSTANDARD 匹配。如果你的 uart_rx 引脚所在 Bank 的 VCCO 是 1.8V那你必须把 IOSTANDARD 写成 LVCMOS18否则电气不匹配轻则通信不稳定重则烧引脚。我见过有人在 1.8V 的 Bank 上接了 3.3V 的 USB 转 TTL 模块结果 FPGA 引脚被灌入过高电压那一片 Bank 直接报废。选引脚之前一定先看原理图或者开发板手册确认 Bank 电压。5.2 第二步硬件连接与电平匹配USB 转 TTL 模块和 FPGA 开发板连接时除了 TX/RX 交叉连接FPGA 的 TX 接模块的 RXFPGA 的 RX 接模块的 TX还要注意共地。必须把 GND 连上否则信号没有参考地通信时有时无现象非常诡异。另外很多 USB 转 TTL 模块支持 3.3V 和 5V 电平切换。FPGA 的 GPIO 绝大多数是 3.3V或更低电平所以跳线帽要拨到 3.3V 位置。如果你接到 5V 上哪怕 FPGA 引脚本身有钳位二极管保护长期运行也有隐患。注意不同开发板的引脚位置差异很大接线前务必以板卡原理图为准。哪怕同一个芯片型号不同厂家的开发板引脚定义也可能完全不同。5.3 一台电脑如何同时调试 Windows 和虚拟机里的串口现在很多开发环境是 Windows 宿主机 VMware 虚拟机里面跑 Ubuntu 做编译或跑上位机。这里有一个很实用的经验不要在 Windows 和虚拟机里同时打开同一个串口会冲突。而且 VMware 里挂载串口的方式有讲究。我的做法是先在 Windows 设备管理器里确认 USB 转串口的 COM口号比如 COM5然后 把串口从 Windows 中释放在 VMware 虚拟机的虚拟机设置里添加串行端口选择使用物理串口指定 COM5然后在 Ubuntu 里用 minicom 或者 Python 的 pyserial 打开/dev/ttyS0或/dev/ttyUSBx取决于映射方式。调试时的选择建议发几个字节看回显直接在 Windows 上用串口助手就行快速又直观。跑脚本做压力测试比如连续发几百 KB 随机数据建议在 Ubuntu 里写 Python串口库成熟数据分析也方便。避免 Windows 串口助手和虚拟机串口工具同时打开同一个串口——打开会失败或者出现设备被占用报错。5.4 经典故障排查发不出 / 收不到 / 乱码我把这些年遇到的 UART 通信问题整理成了一张速查表基本覆盖了 90% 的故障现象现象可能原因排查方法完全无响应助手没有任何显示引脚约束错误、TX/RX 接反、共地问题、模块没供电先用万用表量模块 VCC 是否有 3.3V核对原理图引脚交换 TX/RX收到乱码波特率不一致、时钟频率设置错误、电平不匹配确认 PC 端波特率与 RTL 参数一致检查系统时钟实际频率用示波器看波形发送方 busy 一直为高tx_start 持续拉高、状态机卡死检查 tx_start 是否为单脉冲在仿真里看状态机是否回到 IDLE偶尔丢字节上层逻辑在 rx_done 后未及时取走数据、FIFO 溢出加 FIFO 缓存提高取数频率降低持续发送速率连续发送若干字节后死掉波特率误差累积、接收端状态机收到错误起始位重新计算分频参数增加起始位二次确认考虑改用 16 倍过采样这里特别说下乱码这种最常见的故障。我调试时第一个反应不是怀疑 RTL 代码而是先确认串口助手右下角的波特率是不是 115200。多少次了都是设置被无意改动或者是波特率和数据位/停止位/校验位组合不对比如默认是 8-N-1结果被改成 7-E-1。协议参数先对齐再谈代码问题。6. 进阶扩展从裸收发到工程化UART 还能怎么玩6.1 用 FIFO 解决数据处理速度不匹配实际项目中UART 很少单独作为一个裸收发模块用基本都是挂在系统总线上。比如 FPGA 收到上位机指令后需要解析、执行、返回数据。这时如果上位机连续发来 100 个字节而 FPGA 里的处理逻辑比较慢后面的字节就会覆盖前面的数据。解决办法是在接收端加 FIFO。Xilinx 的 Vivado 里有现成的 FIFO IPIntel 的 Quartus 里也有甚至可以直接用 FPGA 原语例如 Xilinx 的xpm_fifo_async例化一个异步 FIFO。设计思路是UART 接收模块把 rx_done 当作 FIFO 写使能rx_data 当作写数据上层逻辑通过读 FIFO 取数据。这样即使上层处理慢数据也不会丢前提是 FIFO 深度够用。FIFO 深度的选择有个简单估算方法如果上位机一次突发发 1KB 数据而你的处理速率只有接收速率的十分之一那 FIFO 至少需要 900 字节左右的余量。保守一点配 2KB 深度。6.2 指令协议设计为什么不能直接发裸字节做产品级项目时我强烈建议不要直接发裸字节而是定义一套简单的帧格式。我常用的格式是帧头命令字数据长度数据区校验和0xAA 0x551 字节1 字节N 字节1 字节接收端状态机从找帧头开始找到了 0xAA再等 0x55然后读命令字和数据长度最后对数据区做累加校验和帧尾的校验和比对。这样的好处是即使线路噪声造成某个字节出错校验和能发现即使数据流中某个 0xAA 是偶然出现的数据内容帧头双字节机制也能降低误判概率。使用 51 单片机或者 MCU 做上位机配合 FPGA 调试时这套协议可以直接复用两边代码逻辑一致对接很顺畅。6.3 高速场景UART 的极限与替代方案有人会问UART 最高能跑到多快如果直接用 FPGA 做标准 UART 跑 921600 甚至 2M 波特率都没问题但实际瓶颈在于另一端电脑上的 USB 转串口芯片或 MCU 的 UART 外设是否支持。很多 USB 转 TTL 芯片标称支持 3Mbps但实际在 Windows 驱动下跑 2M 以上时丢包率明显上升。如果大数据量传输比如 FPGA 采集的图像要回传到电脑就别死磕 UART 了——这时候应该上 USB、千兆以太网或者 PCIe。UART 的定位是低速控制、调试、指令交互不是大吞吐数据通道。这也是很多做 FPGA 图像处理项目的人踩过的坑以为提高波特率就能把图像数据传完结果发现 USB 转串口芯片根本稳不住最后换成了 USB 3.0 或者以太网方案。6.4 在 SoC 平台上的扩展Zynq 与软核如果用的是 Zynq 这类带 ARM 核的 FPGAUART 不仅可以用 PL 逻辑实现还能直接用 PS 端自带的 UART 控制器PL 端通过 AXI 总线读写。这个方案的优点是波特率配置、FIFO 管理、中断处理都由 ARM 端搞定PL 逻辑只需要通过 AXI-Lite 接口读写寄存器即可。我个人的建议是如果你在 Zynq 上跑 Linux优先用 PS 端 UART省事又稳定。PL 端写一个 UART 裸机收发模块更适合做资源紧张的纯逻辑设计或者追求极低延迟的硬实时场景。再补充一个热词里提到的Zyng Linux 动态加载 FPGA场景我们在 Linux 下通过设备树和 FPGA Manager 框架可以在系统运行过程中动态加载比特流配合 PL 端的 UART 模块能实现ARM 端 Linux 应用 FPGA 高速逻辑 UART 调试的完整链路。这种架构在工业控制、仪器仪表里很常见。7. 性能提升与资源优化把 UART 做到极致7.1 用 16 倍过采样还是直接用分频时钟前面接收端设计里我给的代码带 16 倍过采样这是工业上最稳妥的方案。但有些资源紧张的项目希望把逻辑做小一点也可以直接用分频时钟驱动采样。16 倍过采样相比直接分频有以下优势可以在比特周期内多次采样然后取多数表决三取二或五取三有效抑制毛刺。起始位检测后可以精确地从比特中点开始采样容错能力强。波特率误差容忍度更高。实测下来16 倍过采样方案可以容忍 ±4%~5% 的时钟偏差直接分频方案只能容忍 ±1%~2%。代价是计数器位数更多、状态机逻辑更复杂一点。对于现代 FPGA 来说这点资源差异几乎可以忽略。所以我一律推荐 16 倍过采样。7.2 多通道 UART复用模块减少代码量如果项目里需要多路串口比如 4 路 RS485 分别控制不同的传感器不需要把代码复制四份。把发送端和接收端写成参数化模块然后用 generate 语句例化多路genvar i; generate for (i 0; i 4; i i 1) begin : uart_ch uart_rx #( .CLK_FREQ (CLK_FREQ), .BAUD_RATE (BAUD_RATE) ) u_rx ( .clk (clk), .rst_n (rst_n), .rx (rx_in[i]), .rx_data (rx_data[i]), .rx_done (rx_done[i]) ); uart_tx #( .CLK_FREQ (CLK_FREQ), .BAUD_RATE (BAUD_RATE) ) u_tx ( .clk (clk), .rst_n (rst_n), .tx_start (tx_start[i]), .tx_data (tx_data[i]), .tx (tx_out[i]), .tx_busy (tx_busy[i]) ); end endgenerate多路复用带来的一个问题是如果四条链路同时收发每一路都需要独立的 FIFO否则数据会互相污染。这是一个很容易被忽视的资源规划问题——看着逻辑面积不大但 FIFO 的 BRAM 消耗会悄悄增加。7.3 时序约束与严谨性不仅仅是能跑很多初学者上板验证通过之后就认为大功告成了这在教学场景没问题但做产品必须严谨。我建议至少完成以下检查时序收敛综合实现后看时序报告确保建立时间和保持时间都满足。UART 频率不高一般不会有时序问题但如果系统里还有其他高速逻辑串扰可能影响 UART 引脚上的信号质量。上电复位时序FPGA 上电后状态机和外部芯片需要时间完成初始化。如果上位机一上电立刻发数据而 FPGA 还没完成配置数据就丢了。在设计中可以加一个上电延时逻辑等 FPGA 初始化完成后才允许 UART 模块工作。跨时钟域处理如果你的 UART 模块工作在 50MHz而系统总线工作在 100MHz那么 UART 模块与总线之间的握手信号需要通过异步 FIFO 或脉冲同步器处理。UART 本身是低速接口跨时钟域问题容易被忽视但恰恰是造成随机死机的原因。8. 一个完整的实战记录从零到回环通信成功这里记录一次真实的调试验证过程给大家一个完整的操作范本。假设环境是Xilinx Artix-7 开发板50MHz 系统时钟使用 FT232 芯片的 USB 转 TTL 模块波特率 115200。第一步生成比特流在 Vivado 里创建工程添加顶层uart_loopback和子模块uart_tx、uart_rx写好约束文件。综合、实现、生成比特流这一步大约耗时十分钟。如果时序报告有警告先不急着上板回到代码里检查是否有明显的异步逻辑问题。第二步连接硬件USB 转 TTL 模块插入电脑 USB 口确认设备管理器里出现 COM 端口。把模块的 TX 接到 FPGA 的 uart_rx 引脚模块的 RX 接到 FPGA 的 uart_tx 引脚GND 和 GND 相连。这里最容易犯的错就是 TX/RX 没有交叉连接完成后我习惯用万用表量一下模块 VCC 和 GND 之间的电压确认是 3.3V 左右再上电。第三步下载比特流打开串口助手打开串口助手选择对应 COM 口波特率 115200数据位 8停止位 1无校验。然后发送十六进制字节0x55正常情况下立刻能看到回显0x55。如果没反应按顺序检查引脚约束有没有写对这里划重点约束文件的引脚号必须和板卡原理图一一对应不能用开发板默认的示例引脚除非你确认该引脚接到了 PMOD 或其他可用接口串口助手波特率是否一致TX/RX 是否接反。第四步压力测试回显成功后我习惯做一轮压力测试在串口助手里选择按十六进制发送连续发送 1000 字节递增数据比如 0x00 到 0xFF 循环 4 次观察回显是否完全一致。如果中间有缺失或乱码多半是拔插线不稳、共地不好或者驱动出了问题。提示如果连续发送时出现丢字节先检查 USB 转 TTL 模块的驱动版本。FT232 老版本驱动在 Windows 10/11 下偶尔出现兼容问题表现为波特率设置不生效或丢数据换一个较新的驱动版本往往能解决。另外Windows 的 USB 选择性暂停设置也可能导致串口设备睡过去在电源管理里关掉该选项能提升长时间传输的稳定性。第五步仿真对照压力测试通过后再用仿真验证几个边界场景波特率偏移 3% 能否正确接收、起始位毛刺能否被过滤、连续发送能否保持不丢帧。这些在仿真里都验证过上板基本就不用再担心 UART 本身的问题了。9. 卡尔曼滤波、图像处理、Biss-C…… UART 和它们的交集热词里有不少看起来和 UART 无关的词比如卡尔曼滤波、图像处理、Biss-C。既然提到了我简单聊聊它们在工程中如何和 UART 产生交集这也是多项目背景下绕不开的经验。卡尔曼滤波与 FPGA常见的路径是FPGA 读取传感器数据可能是 UART 接口的 IMU、GPS 模块先用 UART 模块把数据解出来再在 FPGA 内部做卡尔曼滤波最后把融合后的结果通过另一路 UART 发送给上位机。整个链路里UART 是数据进出的门卡尔曼是处理器。很多初学者一上来就想把卡尔曼滤波的矩阵运算写好结果卡在传感器数据根本读不出来——所以先把 UART 搞稳定后续算法才有意义。图像处理与 FPGA图像处理的核心数据通道一般是 MIPI、LVDS 或千兆以太网进HDMI 或 DisplayPort 出。UART 在这里通常扮演控制信道的角色上位机通过 UART 给 FPGA 下发参数比如曝光时间、增益、处理算法的阈值FPGA 处理完图像后再把状态信息通过 UART 上报。所以哪怕你在做图像项目UART 依然是一个必须稳的辅助模块。Biss-C 编码器与 FPGABiss-C 是一种高速串行编码器协议它的时序和 UART 完全不同同步串行带时钟线但在 FPGA 实现层面很多经验是相通的要用状态机、要处理边沿、要注意异步信号的同步化。如果你已经熟练掌握了 UART 的实现再去看 Biss-C 的时序图会发现本质都是边沿检测 按位采样 状态切换。RS485 与 FPGA工业现场常用 RS485 总线它和 UART 的关系是RS485 是物理层标准差分信号UART 是数据链路层协议数据帧格式。FPGA 的 UART 模块输出的 TTL 电平经过一个 RS485 收发器比如 MAX3485转换成差分信号就能在长距离、多节点的工业总线上通信。代码层面的区别只是多了一个方向控制信号DE/RE发送时使能驱动接收时禁用驱动。10. 常见名词与概念速查别再被面试题问懵最后整理一份和 UART 强相关的名词解释方便在面试或项目汇报时快速查阅。这些概念我见过太多次被混淆了放在一起对比更容易记住。名词全称/含义与 UART 的关系与区别UARTUniversal Asynchronous Receiver/Transmitter通用异步收发器定义了数据帧格式和时序是协议控制器的统称USARTUniversal Synchronous/Asynchronous Receiver/Transmitter在 UART 基础上增加了同步模式带时钟线比如 SPI 模式。MCU 上常见的 STM32 的 USART 可以配置成异步模式这时它就是一个 UARTRS232老牌串行通信标准定义了电气特性±12V、连接器DB9和信号定义。UART 是逻辑协议RS232 是物理层标准RS485差分串行总线标准物理层采用差分信号抗干扰强支持多点通信。协议层常用 UART 帧格式TTLTransistor-Transistor Logic泛指 0~3.3V/5V 的数字电平UART 在 FPGA/MCU 上输出的就是 TTL 电平SPISerial Peripheral Interface同步串行接口需要时钟线4 根线SCLK/MOSI/MISO/CS速度比 UART 高协议简单但占用引脚多I2CInter-Integrated Circuit同步串行接口2 根线SDA/SCL支持多设备总线带应答机制速度通常比 UART 高CANController Area Network差分串行总线专为工业/汽车设计带仲裁、错误检测速度中等可靠性高MIPIMobile Industry Processor Interface高性能串行接口规范用于摄像头/显示屏与 UART 完全不同层级属于高速 SerDes 范畴LVDSLow-Voltage Differential Signaling低电压差分信号高速传输FPGA 中用于引脚到引脚的高速数据传输Biss-CBiSS-C 编码器协议同步串行协议类似 SPI 但有更严格的时序约定用于高精度位置反馈FIFOFirst In First Out先进先出数据缓存UART 接收/发送侧常挂 FIFO 解决速率不匹配问题亚稳态Metastability时序器件在时钟边沿附近采样外部信号时出现的中间状态跨时钟域处理的核心概念。UART 接收外部异步信号时必须用两级寄存器同步面试官如果要问 UART 相关的问题多半会从这些角度切入UART 和 SPI/I2C 的区别、异步通信为什么需要起始位和停止位、波特率误差怎么算、接收端为什么要用 16 倍过采样、如何处理亚稳态。把这些概念吃透面试和实战都能稳得住。更重要的是理解这些协议之间的本质区别后你在做 FPGA 系统集成时会明白什么事该用 UART、什么事该上 SPI、什么时候需要 RS485。选型不盲目方案才有底气。写在最后的一点个人经验回头看这些年用 FPGA 做过的项目UART 几乎贯穿始终。它是最容易上手的通信协议也是系统联调时最可靠的调试手段。用逻辑分析仪或者示波器抓 UART 波形看着那串高低电平的变化其实就是在阅读 FPGA 和外部世界之间最朴素的语言。我个人最深刻的体会是越是简单的东西越要把它做稳。UART 模块几十行代码就能写完但要做到在工业现场长时间无差错运行需要关心的地方一点不少——引脚约束是否正确、电平是否匹配、FIFO 是否够深、协议是否有校验、驱动是否有兼容问题。每一个细节都可能成为系统稳定性的短板。如果你正准备入门 FPGA不妨从 UART 开始不只是照着教程把代码敲一遍而是把发送、接收、回环、仿真、上板、排查这一整套流程完整走一遍。这个过程积累的不只是 UART 本身的知识更是一套FPGA 工程化的方法论状态机怎么设计、异步信号怎么处理、怎么用波形调试、遇到问题怎么定位。这套方法论到了后面做 PCIe、DDR、千兆以太网的时候依然管用。
返回列表