FPGA串口通信实战:AXI UART Lite IP核配置与收发控制详解
1. 串口通信在FPGA开发中的真实地位搞FPGA的人都有一个绕不开的坎——串口通信。不管你是做图像处理、电机控制还是数据采集调试阶段总得有个通道把FPGA内部的数据吐出来看看或者从上位机灌点参数进去。RS232串口虽然老但胜在简单可靠PC端随便一个串口助手就能收发不需要额外驱动也不挑操作系统。所以几乎每个FPGA项目里都会留一个UART通道作为调试接口。问题在于很多初学者第一次接触FPGA串口第一反应是打开编辑器手写一个UART收发模块。波特率分频、起始位检测、中间采样、停止位判断、奇偶校验……一套下来少说两三百行Verilog写完还得仿真验证时序稍微偏一点就收不到数据。更麻烦的是手写的UART模块往往只支持固定波特率换个速率就得重新算分频系数改完还得重新综合布线一轮下来半小时就没了。实际上Xilinx在Vivado里早就内置了一个叫AXI UART Lite的IP核专门解决这个问题。它把UART的物理层协议全部封装好对外只暴露AXI总线接口你只需要像读写寄存器一样往发送寄存器写数据、从接收寄存器读数据就行。波特率、数据位、校验方式这些参数在IP核配置界面里点几下就设好了不需要碰一行底层时序代码。我实测下来从新建工程到串口收发跑通熟练的话五分钟确实够用。这篇文章面向的是已经装好Vivado、手上有一块FPGA开发板、想快速把串口通信跑起来的开发者。不管你是刚入门的电子专业学生还是从单片机转过来的嵌入式工程师只要你会用Vivado建工程、会写简单的Verilog状态机就能跟着走完整个流程。我会把IP核的配置逻辑、AXI总线的读写时序、收发数据的完整代码、以及实际调试中踩过的坑全部讲清楚让你不只是“抄完能跑”而是真正理解每一步在干什么。2. 为什么选AXI UART Lite而不是自己写2.1 手写UART模块的隐性成本很多人觉得UART协议简单自己写一个不就完了。确实UART的帧格式就那么几个组成部分一个起始位低电平、5到9个数据位、可选的校验位、一到两个停止位高电平。接收端在起始位下降沿开始计数在数据位中间采样逻辑不算复杂。但真正写起来坑远比想象的多。首先是波特率分频的精度问题。假设系统时钟100MHz要产生115200bps的波特率分频系数是100000000/115200≈868.05。你只能取868实际波特率变成100000000/868≈115207误差约0.006%这个误差在可接受范围内。但如果系统时钟是50MHz呢50000000/115200≈434.03取434实际波特率50000000/434≈115207误差同样很小。可如果系统时钟是25MHz25000000/115200≈217.01取217实际波特率25000000/217≈115207误差依然不大。看起来没问题对吧但如果你用的时钟频率比较特殊比如33.333MHz33333000/115200≈289.35取289的话实际波特率33333000/289≈115339误差0.12%接近UART容忍极限通常要求误差小于2%到3%但多字节连续传输时累积误差会放大。手写模块时这些计算都得自己来算错了就得重新调。其次是接收端的采样策略。理想情况下你在每个数据位的正中间采样最稳但实际信号经过线缆传输后边沿会有抖动加上FPGA输入引脚上的噪声采样点选得不好就容易误判。手写模块通常用16倍过采样在起始位下降沿后计数到7或8时确认起始位有效然后每隔16个时钟采样一次数据位。这个逻辑写起来不难但调试时如果采样点偏了现象就是偶尔收到错误数据很难定位。还有一个容易被忽略的点跨时钟域处理。UART的收发速率和FPGA内部逻辑时钟通常不在一个频率上接收到的串行数据需要同步到系统时钟域发送数据也需要从系统时钟域串行化出去。手写模块如果没做好同步亚稳态问题会在长时间运行后随机出现表现为“跑几个小时突然收到一帧乱码”。2.2 AXI UART Lite IP核帮你做了什么AXI UART Lite IP核把上面这些问题全部封装在内部。你不需要关心波特率分频怎么算、采样点怎么选、跨时钟域怎么同步这些Xilinx的IP设计团队已经帮你验证过了。你拿到的接口非常干净一个AXI4-Lite从机接口用于寄存器读写一个UART物理接口用于连接外部引脚。从寄存器映射的角度看AXI UART Lite只有四个寄存器寄存器名称偏移地址功能说明RX FIFO0x00接收数据寄存器读操作取出FIFO中最旧的一个字节TX FIFO0x04发送数据寄存器写操作将数据推入发送FIFOSTAT0x08状态寄存器包含RX FIFO有效数据数、TX FIFO空满标志等CTRL0x0C控制寄存器使能UART、复位TX/RX FIFO、使能中断你只需要写一个AXI4-Lite主机状态机往TX FIFO写数据就完成发送读RX FIFO就完成接收。状态寄存器告诉你当前FIFO里有没有数据、能不能继续写。整个交互逻辑清晰明了不需要跟波特率、采样、同步这些底层细节打交道。2.3 什么场景适合用IP核什么场景不适合AXI UART Lite适合绝大多数调试和低速通信场景。它的最高波特率取决于你给IP核的时钟频率通常可以做到几Mbps对于打印调试信息、传输控制指令、上传传感器数据这些应用绰绰有余。IP核占用的逻辑资源也很少一个典型的配置大概消耗200到300个LUT和差不多数量的寄存器对大多数FPGA来说微不足道。但如果你需要高速连续数据传输比如把ADC采样的大量数据实时传到PCAXI UART Lite的FIFO深度只有16字节中断响应也有延迟可能就不太够用。这种场景通常会用FTDI的USB转FIFO芯片或者以太网接口。另外如果你需要多路UART比如同时跟四个外设通信每个AXI UART Lite IP核都是独立的例化四个也行但会占用更多资源这时候可以考虑用带多通道的UART IP或者自己写一个共享波特率发生器的多通道模块。不过对于90%的FPGA入门和调试场景AXI UART Lite就是最优解。下面我带你从零开始走一遍完整流程。3. Vivado中AXI UART Lite IP核的配置细节3.1 新建工程与IP核例化打开Vivado新建一个RTL工程选好你开发板对应的FPGA型号。工程建好后在Flow Navigator里点击IP Catalog在搜索框输入“UART Lite”找到“AXI Uartlite”这个IP核双击打开配置界面。配置界面里几个关键参数需要留意波特率这是最核心的参数。AXI UART Lite的波特率是通过一个分频系数计算出来的你在界面上直接填目标波特率比如115200Vivado会自动算出最接近的分频值。但要注意波特率的精度取决于你给IP核的时钟频率。如果你给的时钟是100MHz115200的分频系数是868实际波特率115207误差0.006%完全没问题。如果你给的时钟是50MHz分频系数434实际波特率115207误差一样很小。但如果时钟频率很低比如10MHz分频系数87实际波特率114943误差0.22%虽然还在容忍范围内但建议尽量给IP核一个较高的时钟频率比如50MHz或100MHz这样波特率精度更有保障。数据位宽支持5到8位默认8位。除非你有特殊协议要求否则保持8位。校验方式支持无校验、奇校验、偶校验。调试场景通常选无校验简单省事。如果传输环境噪声较大可以选偶校验增加一点检错能力。FIFO深度AXI UART Lite的收发FIFO深度固定为16字节不可配置。这个深度对于调试信息打印足够了但如果你的应用会突发写入大量数据需要注意在状态机里判断TX FIFO满标志满了就等待不要硬写。配置完成后点击OKVivado会生成IP核并自动添加到工程中。接下来你需要把IP核例化到你的顶层模块里或者用Block Design的方式把它和MicroBlaze、Zynq PS等组件连接起来。这里我以纯RTL的方式为例因为这种方式最通用不依赖特定的处理器架构。3.2 时钟与复位信号的正确处理AXI UART Lite IP核需要一个时钟信号和一个复位信号。时钟信号就是你的系统时钟比如50MHz或100MHz。复位信号是低电平有效也就是说复位期间需要保持低电平正常工作期间保持高电平。这里有一个容易踩的坑复位信号的同步化。如果你的复位信号来自外部按键或者另一个时钟域直接接到IP核的复位引脚上可能会因为亚稳态导致IP核内部状态机跑飞。正确的做法是用一个复位同步器把异步复位同步到IP核的时钟域。下面是一个标准的复位同步器代码module reset_sync ( input wire clk, input wire ext_rst_n, output wire rst_n ); reg rst_meta, rst_sync; always (posedge clk or negedge ext_rst_n) begin if (!ext_rst_n) begin rst_meta 1b0; rst_sync 1b0; end else begin rst_meta 1b1; rst_sync rst_meta; end end assign rst_n rst_sync; endmodule这个模块的作用是当外部复位有效时立即将内部复位拉低当外部复位释放时经过两个时钟周期的同步后再将内部复位拉高。这样IP核看到的复位信号就是与时钟同步的不会出现亚稳态。3.3 AXI4-Lite接口的信号梳理AXI UART Lite使用AXI4-Lite协议进行寄存器访问。AXI4-Lite是AXI4的简化版本只有五个通道读地址通道、读数据通道、写地址通道、写数据通道、写响应通道。每个通道都是独立的握手协议用VALID和READY信号进行流控。对于AXI UART Lite来说你只需要实现一个AXI4-Lite主机即可。主机发起写操作时先在写地址通道上发送地址和控制信息然后在写数据通道上发送数据最后等待从机的写响应。读操作类似先在读地址通道上发送地址然后等待从机在读数据通道上返回数据。AXI4-Lite的信号列表看起来很多但实际用到的核心信号就那么几个信号名方向说明s_axi_aclk输入AXI总线时钟s_axi_aresetn输入AXI总线复位低有效s_axi_awaddr输入写地址对于UART Lite只需要低2位有效s_axi_awvalid输入写地址有效s_axi_awready输出写地址就绪s_axi_wdata输入写数据32位s_axi_wstrb输入写字节选通通常为4b1111s_axi_wvalid输入写数据有效s_axi_wready输出写数据就绪s_axi_bresp输出写响应2b00表示OKAYs_axi_bvalid输出写响应有效s_axi_bready输入写响应就绪s_axi_araddr输入读地址s_axi_arvalid输入读地址有效s_axi_arready输出读地址就绪s_axi_rdata输出读数据s_axi_rresp输出读响应s_axi_rvalid输出读数据有效s_axi_rready输入读数据就绪看起来信号很多但如果你用状态机来实现每个状态下只需要关注当前通道的几个信号。下面我会给出一个完整的AXI4-Lite主机状态机代码。4. 手把手实现AXI UART Lite的收发控制4.1 发送逻辑往TX FIFO写数据发送数据的过程就是通过AXI4-Lite总线往TX FIFO寄存器偏移地址0x04写一个32位数据其中低8位是你要发送的字节。写操作需要依次完成写地址通道和写数据通道的握手然后等待写响应。下面是一个简化的发送状态机代码// 发送状态机 localparam SEND_IDLE 3d0; localparam SEND_AW 3d1; localparam SEND_W 3d2; localparam SEND_B 3d3; reg [2:0] send_state; reg [31:0] send_data; reg send_start; always (posedge clk or negedge rst_n) begin if (!rst_n) begin send_state SEND_IDLE; s_axi_awvalid 1b0; s_axi_wvalid 1b0; s_axi_bready 1b0; end else begin case (send_state) SEND_IDLE: begin if (send_start) begin s_axi_awaddr 32h04; // TX FIFO地址 s_axi_awvalid 1b1; s_axi_wdata {24b0, send_data[7:0]}; s_axi_wstrb 4b1111; s_axi_wvalid 1b1; s_axi_bready 1b1; send_state SEND_AW; end end SEND_AW: begin if (s_axi_awready s_axi_awvalid) begin s_axi_awvalid 1b0; send_state SEND_W; end end SEND_W: begin if (s_axi_wready s_axi_wvalid) begin s_axi_wvalid 1b0; send_state SEND_B; end end SEND_B: begin if (s_axi_bvalid s_axi_bready) begin s_axi_bready 1b0; send_state SEND_IDLE; end end endcase end end这段代码的逻辑是当send_start信号有效时状态机从IDLE进入AW状态同时拉高awvalid和wvalid把地址和数据都准备好。AXI协议允许地址和数据同时有效从机会分别握手。当awready和awvalid同时为高时写地址握手完成进入W状态等待写数据握手。当wready和wvalid同时为高时写数据握手完成进入B状态等待写响应。当bvalid和bready同时为高时写响应握手完成一次写操作结束。实际使用中你可以在发送状态机外面加一个FIFO把要发送的数据先缓存起来状态机从FIFO里取数据发送。这样上层逻辑只需要往FIFO里写数据不需要关心AXI总线的握手细节。4.2 接收逻辑从RX FIFO读数据接收数据的过程稍微复杂一点因为你需要先读状态寄存器偏移地址0x08判断RX FIFO里有没有数据如果有再去读RX FIFO寄存器偏移地址0x00把数据取出来。状态寄存器的位定义如下位名称说明0RX FIFO Valid为1时表示RX FIFO中有有效数据1RX FIFO Full为1时表示RX FIFO已满2TX FIFO Empty为1时表示TX FIFO为空3TX FIFO Full为1时表示TX FIFO已满4Interrupt为1时表示有中断待处理5Overrun Error为1时表示接收溢出错误6Frame Error为1时表示帧错误7Parity Error为1时表示校验错误你只需要关注位0RX FIFO Valid当它为1时说明有数据可读。读数据的状态机跟写数据类似只是把写通道换成读通道// 接收状态机 localparam RECV_IDLE 3d0; localparam RECV_AR 3d1; localparam RECV_R 3d2; reg [2:0] recv_state; reg [31:0] recv_data; reg recv_done; always (posedge clk or negedge rst_n) begin if (!rst_n) begin recv_state RECV_IDLE; s_axi_arvalid 1b0; s_axi_rready 1b0; recv_done 1b0; end else begin case (recv_state) RECV_IDLE: begin recv_done 1b0; if (recv_start) begin s_axi_araddr 32h00; // RX FIFO地址 s_axi_arvalid 1b1; s_axi_rready 1b1; recv_state RECV_AR; end end RECV_AR: begin if (s_axi_arready s_axi_arvalid) begin s_axi_arvalid 1b0; recv_state RECV_R; end end RECV_R: begin if (s_axi_rvalid s_axi_rready) begin recv_data s_axi_rdata; s_axi_rready 1b0; recv_done 1b1; recv_state RECV_IDLE; end end endcase end end读操作比写操作少了一个响应通道所以状态机只有三个状态。当recv_done拉高时recv_data的低8位就是接收到的字节。4.3 状态轮询与数据流控制在实际应用中你通常不会每时每刻都在收发数据而是需要轮询状态寄存器来判断什么时候该发、什么时候该收。一个典型的做法是每隔一段时间读一次状态寄存器检查RX FIFO Valid位和TX FIFO Full位。读状态寄存器的状态机跟读RX FIFO完全一样只是地址换成0x08。你可以把读状态和读数据合并成一个状态机用一个寄存器选择地址。数据流控制方面发送时如果TX FIFO Full为1说明FIFO满了这时候不能再写数据否则会丢失。接收时如果RX FIFO Valid为0说明没有新数据不需要去读。如果Overrun Error为1说明接收溢出有数据被覆盖了需要软件处理。注意AXI UART Lite的FIFO深度只有16字节如果你在短时间内连续发送超过16字节的数据必须等待TX FIFO有空位再继续写。否则超出的数据会被丢弃而且不会有任何错误提示。4.4 顶层模块的整合与引脚约束把发送状态机、接收状态机、状态轮询逻辑整合到一个顶层模块里再加上复位同步器和时钟分频如果需要的话整个设计就完成了。顶层模块的端口包括系统时钟、复位、UART的TX和RX引脚以及可选的调试LED。引脚约束方面你需要根据开发板的原理图找到UART接口对应的FPGA引脚在XDC约束文件里指定。比如set_property PACKAGE_PIN Y11 [get_ports uart_tx] set_property IOSTANDARD LVCMOS33 [get_ports uart_tx] set_property PACKAGE_PIN Y12 [get_ports uart_rx] set_property IOSTANDARD LVCMOS33 [get_ports uart_rx]具体的引脚编号取决于你的开发板型号一定要查原理图确认不要随便填。另外UART的TX和RX是交叉连接的FPGA的TX接PC的RXFPGA的RX接PC的TX。如果你用USB转串口模块模块上通常有标注。5. 实际调试中遇到的典型问题与排查方法5.1 收不到数据怎么办这是最常见的问题。现象是PC端串口助手能打开但发送数据后FPGA没有任何反应或者FPGA发送的数据PC端收不到。排查思路按以下顺序进行第一步检查波特率是否匹配。PC端串口助手设置的波特率必须和IP核配置的波特率一致。如果你在IP核里设的是115200PC端也必须是115200。这个看似简单但实际调试中至少有30%的问题出在这里。第二步检查TX和RX是否接反。FPGA的TX应该接USB转串口模块的RXFPGA的RX接模块的TX。如果接反了双方都收不到数据。你可以用示波器或者逻辑分析仪看一下TX引脚上有没有波形如果有波形但PC收不到大概率是接反了或者波特率不对。第三步检查时钟和复位。用ILA集成逻辑分析仪抓一下IP核的s_axi_aclk和s_axi_aresetn信号确认时钟在跑、复位已经释放。如果复位一直有效IP核不会工作。第四步检查AXI握手是否完成。用ILA抓一下awvalid、awready、wvalid、wready、bvalid、bready这几个信号看写操作是否正常完成。如果awvalid拉高了但awready一直不响应说明IP核没有正确响应可能是地址不对或者IP核没有使能。5.2 收到的数据乱码或错位如果PC端能收到数据但内容是乱码通常有以下几个原因波特率误差过大。虽然UART协议对波特率误差有一定容忍度但如果误差超过3%接收端就可能采样错误。检查你的IP核时钟频率和波特率设置确保分频系数计算正确。比如你给IP核的时钟是50MHz波特率设成9600分频系数是50000000/9600≈5208实际波特率50000000/5208≈9600.6误差0.006%没问题。但如果时钟是12MHz12000000/96001250实际波特率9600误差0%也没问题。关键是不要用太低的时钟频率去产生高波特率。数据位和停止位设置不匹配。PC端串口助手通常默认8位数据位、1位停止位、无校验。如果你的IP核配置成了7位数据位或者2位停止位收到的数据就会错位。检查IP核配置和串口助手设置是否一致。采样点偏移。AXI UART Lite IP核内部已经优化了采样点一般不会出现这个问题。但如果你用的是自己写的UART模块采样点选得不好就会导致误码。用示波器观察RX引脚上的波形确认起始位下降沿干净、数据位稳定。5.3 TX FIFO满导致数据丢失前面提到过AXI UART Lite的TX FIFO只有16字节深。如果你在状态机里连续写数据而不检查TX FIFO Full标志第17个字节开始就会被丢弃。正确的做法是在每次写TX FIFO之前先读状态寄存器检查TX FIFO Full位。如果为1等待一段时间再试。或者你可以用中断方式当TX FIFO从满变为不满时IP核会产生中断你在中断服务程序里继续写数据。下面是一个带流控的发送逻辑示例// 带流控的发送逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin send_start 1b0; end else begin if (tx_fifo_full) begin send_start 1b0; // FIFO满不发送 end else if (send_fifo_valid !send_start) begin send_start 1b1; // FIFO不满且有数据启动发送 end else if (send_done) begin send_start 1b0; end end end5.4 常见问题速查表现象可能原因排查方法PC收不到任何数据TX/RX接反交换TX和RX连线PC收不到任何数据波特率不匹配检查IP核和串口助手波特率设置PC收不到任何数据复位未释放用ILA抓s_axi_aresetn信号PC收不到任何数据AXI握手失败用ILA抓awvalid/awready等信号收到乱码数据位/停止位不匹配检查IP核和串口助手配置收到乱码波特率误差过大提高IP核时钟频率或调整波特率偶尔丢数据TX FIFO满增加流控逻辑检查TX FIFO Full标志偶尔丢数据RX FIFO溢出提高轮询频率或使用中断方式长时间运行后出错跨时钟域亚稳态检查复位同步和信号同步提示调试UART时ILA是你最好的朋友。把AXI总线的关键信号和UART的TX/RX引脚都接到ILA上触发条件设在起始位下降沿这样你能清楚地看到每一帧数据的收发过程。6. 从调试工具到产品级应用的扩展思路6.1 用中断替代轮询提升效率轮询方式虽然简单但会占用CPU或状态机的时间。如果你的FPGA设计里还有其他任务要处理轮询UART状态会浪费大量时钟周期。AXI UART Lite支持中断输出你可以配置IP核在接收到新数据或发送FIFO变空时产生中断。使用中断的步骤是在IP核配置界面使能中断输出把中断信号连接到你的中断控制器比如MicroBlaze的中断输入或Zynq的PL中断。在中断服务程序里读取状态寄存器判断中断类型然后进行相应的收发操作。这样主程序可以专注于其他任务UART数据到达时再响应效率更高。6.2 添加命令解析层实现协议通信单纯的字节收发只是基础实际项目中你通常需要定义一套简单的命令协议。比如用第一个字节表示命令类型后面的字节表示参数或数据。你可以在AXI UART Lite之上加一个命令解析状态机根据接收到的字节序列执行不同的操作。一个典型的命令帧格式可以是帧头0xAA 命令码 数据长度 数据 校验和。接收状态机先找帧头然后依次接收命令码、长度、数据和校验校验通过后执行命令。发送时按照同样的格式组帧发送。这样PC端就可以用串口助手发送十六进制命令来控制FPGA的行为了。6.3 多路UART的扩展方案如果你需要多个串口比如一个用于调试打印一个用于跟外部设备通信一个用于跟另一个FPGA板卡通信可以在Block Design里例化多个AXI UART Lite IP核每个IP核分配不同的基地址。然后用一个AXI Interconnect把多个IP核连接到同一个AXI主机上。如果资源紧张也可以考虑用SelectIO或者LVDS实现高速串行通信但那又是另一个话题了。对于大多数应用来说两到三个AXI UART Lite IP核完全够用资源开销也在可接受范围内。6.4 与MicroBlaze或Zynq PS的配合使用如果你用的是MicroBlaze软核或者Zynq的PS端AXI UART Lite可以直接挂到AXI总线上用C语言通过读写寄存器的方式操作。Xilinx提供了现成的驱动函数比如XUartLite_Send和XUartLite_Recv你只需要调用这些函数就能完成收发比写Verilog状态机更简单。在Vitis或者旧版的SDK里你可以用XUartLite_Initialize初始化UART用XUartLite_SendByte发送单个字节用XUartLite_RecvByte接收单个字节。中断方式也有对应的API。这种方式适合需要复杂协议处理的应用因为C语言写业务逻辑比Verilog方便得多。6.5 实际项目中的经验参数最后分享几个我在实际项目中总结的参数和经验值时钟频率选择给AXI UART Lite的时钟建议不低于50MHz。虽然理论上10MHz也能工作但高时钟频率下波特率分频更精细误差更小。如果系统时钟是100MHz直接给IP核用100MHz不需要分频。波特率选择调试场景用115200最方便PC端串口助手默认支持传输速度也够快。如果线缆较长或环境噪声大可以降到9600或19200提高可靠性。FIFO使用策略发送时如果数据量小于16字节可以一次性写入如果大于16字节必须分批写入并检查TX FIFO Full标志。接收时建议每1ms轮询一次状态寄存器这个频率对于115200波特率来说足够及时不会导致RX FIFO溢出。复位策略IP核的复位信号一定要用同步复位不要直接用外部按键的异步信号。复位至少保持4个时钟周期确保IP核内部状态机完全复位。引脚约束UART引脚的电平标准要根据开发板原理图设置常见的是LVCMOS333.3V或LVCMOS181.8V。如果电平不匹配通信会不稳定甚至损坏引脚。我在多个项目中用AXI UART Lite作为调试通道从Artix-7到Zynq-7000再到Kintex-7这个IP核的表现一直很稳定。唯一需要注意的是FIFO深度有限在高吞吐场景下要做好流控。如果你刚开始学FPGA建议先用这个IP核把串口通信跑通理解AXI总线的读写时序然后再去研究手写UART模块这样学习曲线会更平滑。