FPGA中AXI UART 16550 IP核配置与中断处理实战指南
2026/9/24 13:10:25 网站建设 项目流程

在FPGA里做串口通信,只要你是用MicroBlaze软核或者Zynq的ARM核,波特率、收发状态、中断这些,最后基本都会落到Xilinx的AXI UART 16550 IP核上。这个IP核本质上是把经典的PC级16550 UART控制器搬到了AXI总线上,内核逻辑一模一样,但挂在总线之后,配置方式、中断处理还有调试套路,跟传统裸寄存器玩法差别不小。

我最初接触这个IP的时候,是在Zynq-7020上给PS端串口之外又扩了一个PL端串口。刚开始以为就是配置个波特率、开个中断的事,结果一路踩坑:波特率算出来的分频值不对导致上板全是乱码,FIFO触发等级设太高导致CPU中断里一次要搬一堆数据,还有那个电平触发的中断信号,ISR里没读干净IIR,直接进死循环。折腾下来我才发现,这IP核本身不难,难的是把IP配置、寄存器计算、中断时序这几层东西串起来。

这篇文章就把我从Vivado配置到SDK/Vitis中断处理的全过程,连同调试中踩过的坑一起写出来。适合两类人看:一类是第一次在Block Design里用AXI UART 16550,想知道各项参数怎么填、中断怎么接的;另一类是已经跑通收发但偶尔出现乱码、丢字节、中断异常,想系统排查问题的。从头到尾没有玄学,全是能直接抄的配置和代码。

1. 先把话说清楚:AXI UART 16550是什么,什么时候用得到它

1.1 从UART到AXI总线,这个IP是怎么长出来的

要理解这个IP核,先拆一下名字。AXI说明它挂在AMBA AXI总线上,具体是AXI4-Lite接口;UART说明它是个串口控制器;16550则是来自PC时代的经典UART芯片型号。16550芯片当年就是为了解决早期8250/16450在高速率下容易丢数据的问题,加入了16字节收发FIFO而闻名。Xilinx把这个架构做成了软核IP,挂在FPGA内部总线上,寄存器级兼容标准16550。

换句话说,你在Vivado里拉出来的这个IP核,打开寄存器手册看到的DLL、DLM、IER、IIR、LCR、LSR这些名字,和上世纪PC主板上那颗16550芯片是一模一样的。这意味着两件事:第一,网上能找到大量关于标准16550的中文和英文资料,基本都能参考;第二,如果你以前搞过单片机或者老PC串口开发,看到寄存器会非常亲切。

但它和传统16550芯片有个根本区别:数据不是通过IO端口读写,而是通过AXI4-Lite总线访问。在Block Design里,处理器的AXI互联主端口通过AXI SmartConnect或者直接连接挂到IP的s_axi接口上,读写就变成了对内存映射地址的访问。硬件上多了一层总线转换,但软件上其实更简单,直接拿指针操作地址就行。

1.2 和AXI UART Lite、标准UART 16550怎么选

Xilinx在Vivado里其实提供了两个常被混淆的串口IP:AXI UART Lite和AXI UART 16550。很多人第一次选型时容易纠结,我把两者的核心区别列一下:

特性AXI UART 16550AXI UART Lite
寄存器集完整16550寄存器(DLL、DLM、IER、IIR等)精简寄存器,只有少量控制寄存器
FIFO可配置深度(常见16/32/64等)固定16字节
中断源接收数据可用、接收超时、发送空、线路状态错误、调制解调器状态仅发送空、接收有数据
波特率运行时可通过DLL/DLM寄存器修改仅在IP配置时固定,运行期一般不变
自动波特率部分版本支持不支持
典型用途需要兼容PC软件、需要丰富错误状态的场景简单透传,不需要复杂状态管理

如果只是把数据从A点搬到B点,UART Lite确实够用,代码也更简单。但一旦牵扯到和PC端调试工具对接、需要处理帧错误和溢出错误、或者希望把FIFO深度调大来减少中断频率,16550的优势就出来了。我自己做PL端扩展串口,基本都用16550,因为它的寄存器行为和成熟驱动兼容性更好,调试时拿PC上现成的16550工具和经验都能对上。

1.3 什么场景下必须用这个IP,什么场景不适合

AXI UART 16550最适用的场景是:你有一个软核(MicroBlaze)或者硬核(Zynq/Zynq UltraScale+),需要通过AXI总线挂一个PL逻辑里的串口,并且这个串口的行为要像标准PC串口一样健壮。典型例子是上位机通过UART和FPGA交互,FPGA里跑着处理器运行协议栈,串口负责传输日志或者控制命令。

如果只是纯逻辑收发、不经过CPU,那根本不用这个IP,直接写个简单的UART收发器模块就行,资源还更省。如果是要做极高波特率或者特殊协议(比如RS485自动方向控制、LIN总线等),16550 IP也能做,但可能需要外部加控制逻辑,不如自己写灵活。所以选型之前先想清楚:有没有CPU、要不要寄存器接口、中断要怎么处理,这三个问题答案定了,IP也就定了。

2. Vivado里的添加、配置和连线,每一步都别想当然

2.1 在Block Design里添加IP并完成基础连线

打开Vivado工程,创建一个Block Design或者打开已有的,在Diagram窗口空白处点加号,搜索框里输入“axi_uart16550”,注意拼写是axi后面直接跟uart,数字是16550,不要搜成“uart_16550”或者“axi_uart_16550”。搜索结果里会出现“AXI UART 16550”这个IP,双击即可添加到画布。

添加之后,IP默认带这些接口:s_axi(AXI4-Lite从接口)、s_axi_aclk、s_axi_aresetn、uart_clk、uart_rxd_in、uart_txd_out、int_o。如果你的版本里还看到modem相关引脚,那是因为勾选了Use Modem Ports,一般用不到。

连线有两种方式。一种是手动连:把s_axi接到处理器的AXI互联主端口上,把s_axi_aclk和uart_clk接到同一个时钟(比如100MHz),s_axi_aresetn接到复位网络,int_o接到中断控制器。另一种是懒人方式:右键IP选择Connection Automation,让Vivado自动连接总线和时钟,我建议新手先用自动连接,再手动补一下中断引脚。

关于时钟,这里有个特别容易踩的坑:s_axi_aclk和uart_clk可以是同一个时钟,也可以是不同时钟,但在IP配置页面里填的UART Clock频率,必须和uart_clk实际接入的时钟频率严格一致。后面实操部分我会详细说为什么。如果两个时钟频率不同,s_axi_aclk管AXI总线访问时序,uart_clk管波特率生成,两者可以异步,但IP内部没有做跨时钟域的FIFO同步,建议尽量用同一个时钟域,避免奇奇怪怪的问题。

2.2 配置页面逐项拆解:时钟、波特率、帧格式、FIFO

双击IP进入配置界面,你会看到这么几个关键项:

Component Name,保持默认,也可以改成uart_pl这种好认的名字,注意一旦生成IP后名字就不能乱改了,否则Block Design里所有的引用都会断。

UART Clock这一栏,填的是uart_clk引脚的实际频率,单位是MHz。比如你在Block Design里把uart_clk接到了100MHz时钟上,这里就填100。IP内部要根据这个频率计算波特率分频值。如果填错,后面寄存器里算出来的分频值全是错的,表现就是上板后乱码或者干脆收发不了。

Desired Baud Rate,填目标波特率,常用115200或者9600。IP会根据UART Clock和Desired Baud Rate自动计算一个分频值,并Preview在下面。如果计算出来的分频值超过16位寄存器范围(65535),IP会报错或者红色提示,这时候要么降低UART Clock,要么降低目标波特率,要么换用外部波特率时钟模式。

Data Bits、Parity、Stop Bits,一般选8、None、1,也就是常说的8N1。这些决定了LCR寄存器的初始值。如果和PC调试助手的设置不一致,表现为收到数据但内容对不上,偶尔还会触发帧错误。

FIFO Depth,这个和中断行为有直接关系。默认16字节,如果你希望CPU少被打断,可以选更大的深度。注意FIFO深度变了,FCR寄存器里RX触发等级对应的字节数也会变,这块到中断部分再说。

配置完这些,IP引脚一般会自动更新,比如有些版本在配置里勾选了“Use Modem Ports”就会多出几个引脚。

2.3 中断信号怎么接,接法直接影响后面调试

AXI UART 16550的中断输出是int_o,这是关键。它和很多其他IP的中断不一样,不是脉冲,而是电平信号:只要IP内部还有未处理的中断源,int_o就会一直保持高电平,直到你把产生中断的原因清掉。

这个特性在Block Design里接中断控制器时就要注意。如果你用MicroBlaze,通常会有一个AXI Interrupt Controller,把int_o接到它的某一路中断输入上,然后中断控制器再把汇总后的中断信号接到MicroBlaze的interrupt引脚。如果是Zynq,可以把int_o直接连到PS端的IRQ_F2P引脚,或者也先经过PL侧的中断控制器再进GIC。

连接方式本身不复杂,但之后写ISR时一定要记住电平触发这个特性。处理完一个中断源,必须重新读取IIR寄存器,确认是否还有挂起的中断,直到IIR的bit0变为1才能返回。否则中断控制器会认为中断没有处理完,不停地触发,CPU就卡死在中断里了。后面我会给完整的ISR代码。

2.4 地址分配、综合、导出硬件,一个都不能漏

Block Design连线完成后,切换到Address Editor,给IP分配一个地址区间。手动连线时Vivado可能不会自动分配,需要你点一下自动分配,或者手动填一个基地址,比如0x40000000。这个基地址后面在SDK/Vitis里会用到,xparameters.h里会生成XPAR_AXI_UART16550_0_BASEADDR宏,但如果你自己写寄存器访问代码,最好确认一下实际值。

之后就是常规流程:Validate Design、Generate Output Products、Create HDL Wrapper、Run Synthesis、Run Implementation、Generate Bitstream。导出硬件时,Vitis/SDK用户记得勾选Include bitstream,这样生成应用工程后可以边调试边下载PL逻辑,省得单独烧Flash。

导出完硬件,在Vitis/SDK里新建platform工程时,BSP里会自动带上xuartns550驱动,默认是轮询模式的。这里有个小坑:BSP默认配置不一定会初始化FIFO和中断,很多驱动API要你自己设置调用,所以新手最好从寄存器操作入手,先把整个链路走通,再考虑用官方驱动简化代码。

3. 寄存器操作与波特率分频计算,这是整个IP的核心

3.1 寄存器映射速查表,建议收藏

AXI UART 16550的寄存器偏移和标准16550一致,但由于挂在AXI总线上,同一个地址在不同条件下会映射到不同寄存器,最关键的就是DLAB(Divisor Latch Access Bit)这个开关。我整理了一张常用表:

地址偏移DLAB=0 时DLAB=1 时说明
0x00RBR(读)/ THR(写)DLL接收缓冲/发送保持/波特率分频低8位
0x04IERDLM中断使能/波特率分频高8位
0x08IIR(读)/ FCR(写)IIR/FCR中断标志/ FIFO控制
0x0CLCRLCR线路控制,bit7是DLAB
0x10MCRMCR调制解调器控制
0x14LSRLSR线路状态
0x18MSRMSR调制解调器状态
0x1CSCRSCR暂存寄存器

在Vitis/SDK里做寄存器访问时,基地址加上表里的偏移就是绝对地址,然后用Xil_In32/Xil_Out32读写。注意IP核的寄存器数据只在低8位有效,读出来要&0xFF,写的时候也只改低8位。

3.2 DLAB锁存原理:为什么0x00一会儿是数据一会儿是分频值

刚开始的人很容易在这个地方卡住。地址0x00,你在DLAB=0时读它,得到的是接收数据;在DLAB=1时读它,得到的是波特率分频值的低8位。同一个地址,含义完全不一样。

控制这个开关的是LCR寄存器的bit7。LCR本身在0x0C地址,你要设置波特率分频值时,流程是:先把LCR写成0x80(也就是bit7置1,其他位随意,一般写0x80),这时候0x00和0x04被解释为DLL和DLM;然后写分频值进去;写完还要把LCR恢复成正常的帧格式配置(比如0x03代表8N1),把DLAB关掉。如果不恢复,后续收发数据时寄存器解释就会错位,读到的全是分频值,数据肯定不对。

这个锁存机制是16550的祖传设计,早期是为了在有限的寄存器地址空间里塞下更多功能。理解它之后,初始化代码的顺序就变得非常清晰了。

3.3 波特率分频的数学:公式、实例、误差判断

波特率分频值的官方公式在PG143里写得明确:

divisor = UART_Clock_Frequency / (Baud_Rate * 16)

分子是uart_clk的赫兹数,分母是目标波特率乘16,结果向下取整。这个divisor就是写入DLM:DLL的16位值。之所以有乘16,是因为UART标准规定每个bit再采样16次,保证在噪声环境下也能正确恢复数据。

举个例子。uart_clk是100MHz,目标波特率115200:

divisor = 100000000 / (115200 * 16) = 100000000 / 1843200 = 54.253,取54。那么实际波特率 = 100000000 / (54 * 16) = 115740.74,误差约0.47%,完全在串口通信容忍范围内。

再算一个9600常用的:

divisor = 100000000 / (9600 * 16) = 651.04,取651,实际波特率 = 100000000 / (651 * 16) = 9600.61,误差只有0.006%,非常理想。

但如果你把uart_clk改成50MHz,还想跑到115200:

divisor = 50000000 / (115200 * 16) = 27.126,取27,实际波特率 = 50000000 / (27 * 16) = 115740.74,误差还是0.47%。看起来没问题,但如果目标是1836000这种高频波特率,分频值可能很小,一个整数的误差就会带来几个百分点的偏差,直接导致通信失败。

所以有两个实用建议:第一,尽量用接近整数分频的时钟和波特率组合;第二,误差超过2%就慎重,超过3%基本必挂。实际调板子时,如果碰到“偶尔能收到但内容不对”的怪现象,先怀疑波特率误差,用示波器或者逻辑分析仪抓TXD引脚,量一下单个bit的宽度,换算成实际波特率,一量便知。

4. SDK/Vitis里的驱动与中断处理,代码直接抄

4.1 初始化流程:关中断、设分频、配帧格式、使能FIFO、开中断

在SDK或Vitis里写代码,我习惯直接操作寄存器,不依赖xuartns550官方驱动,这样对IP理解最深,以后换平台也通用。先定义寄存器偏移:

#include "xil_io.h" #include "xparameters.h" #define UART_BASE XPAR_AXI_UART16550_0_BASEADDR #define RBR_OFFSET 0x00 #define THR_OFFSET 0x00 #define IER_OFFSET 0x04 #define IIR_OFFSET 0x08 #define FCR_OFFSET 0x08 #define LCR_OFFSET 0x0C #define MCR_OFFSET 0x10 #define LSR_OFFSET 0x14

然后写初始化函数。注意顺序不能乱:

void uart16550_init(uint32_t base, uint32_t clk_hz, uint32_t baud) { uint32_t divisor = clk_hz / (baud * 16); // 1. 先关中断,防止配置过程中产生异常中断 Xil_Out32(base + IER_OFFSET, 0x00); // 2. DLAB=1,准备写分频寄存器 Xil_Out32(base + LCR_OFFSET, 0x80); // 3. 写DLL和DLM,注意先低后高 Xil_Out32(base + RBR_OFFSET, divisor & 0xFF); Xil_Out32(base + IER_OFFSET, (divisor >> 8) & 0xFF); // 4. 恢复LCR为8N1,即0x03,同时关闭DLAB Xil_Out32(base + LCR_OFFSET, 0x03); // 5. 写FCR,使能FIFO并设置RX触发等级 // 0x01表示FIFO使能,(0x01<<6)表示16字节FIFO时触发等级为4字节 Xil_Out32(base + FCR_OFFSET, 0x01 | (0x01 << 6)); // 6. 使能中断:接收数据可用(bit0) + 接收线路状态(bit2) // 注意:没使能发送中断,发送中断在需要发数据时再打开 Xil_Out32(base + IER_OFFSET, 0x01 | 0x04); }

这里面有细节要展开。第二步和第四步一开一关DLAB,是波特率配置的标准流程,写完DLL/DLM之后LCR必须恢复成帧格式,否则后面收发全乱。第三步里往RBR_OFFSET和IER_OFFSET写数据,看起来像是往接收缓冲和中断使能寄存器写东西,实际上因为DLAB=1,这两个地址被解释成DLL和DLM,这正好呼应了前面讲的寄存器映射复用规则。

FCR配置里,0x01是FIFO使能,一旦使能,IIR的bit6和bit7会变成11,用来表示FIFO已经工作。RX触发等级通过bit7:6选择,对于16字节FIFO,00表示1字节触发、01表示4字节触发、10表示8字节触发、11表示14字节触发。选4字节是比较平衡的选择,既不会因为CPU响应慢丢数据,也不会中断太频繁。

4.2 中断服务程序的标准写法,清中断是灵魂

ISR写不好,最容易出现的就是中断风暴。AXI UART 16550是电平触发,处理完一个中断类型后必须检查是否还有别的挂起中断。标准写法如下:

#define TX_BUF_LEN 256 static uint8_t tx_buf[TX_BUF_LEN]; static volatile uint32_t tx_len; static volatile uint32_t tx_index; void uart16550_isr(void *param) { uint32_t iir; uint8_t ch; iir = Xil_In32(UART_BASE + IIR_OFFSET); // bit0为0表示有中断挂起,循环直到处理完所有挂起中断 while ((iir & 0x01) == 0) { switch (iir & 0x06) // 取bit2:1判断中断类型 { case 0x06: // 接收线路状态错误:读LSR即可清中断源 // 实际项目中要逐个bit判断是溢出、帧错误还是校验错误 Xil_In32(UART_BASE + LSR_OFFSET); break; case 0x04: // 接收数据超时:FIFO里有数据但一直没凑满触发等级 // 需要把FIFO里所有数据读完 while (Xil_In32(UART_BASE + LSR_OFFSET) & 0x01) { ch = Xil_In32(UART_BASE + RBR_OFFSET) & 0xFF; // 放入你的接收队列,这里略 } break; case 0x02: // 接收数据可用:FIFO达到触发等级 // 循环读取,直到LSR的bit0为0 while (Xil_In32(UART_BASE + LSR_OFFSET) & 0x01) { ch = Xil_In32(UART_BASE + RBR_OFFSET) & 0xFF; // 放入你的接收队列 } break; case 0x00: // 发送保持寄存器空:可以发送下一字节 if (tx_index < tx_len) { Xil_Out32(UART_BASE + THR_OFFSET, tx_buf[tx_index++]); } else { // 如果发送完成,关闭发送中断,避免不断触发 Xil_Out32(UART_BASE + IER_OFFSET, 0x01 | 0x04); } break; default: break; } // 重新读IIR,确认还有没有挂起的中断 iir = Xil_In32(UART_BASE + IIR_OFFSET); } }

这个循环是关键。很多人的ISR只在入口读一次IIR,处理完一种类型就return,结果另一个中断源还挂着,电平触发逻辑让int_o始终保持高,CPU刚出ISR又被拉进去,表现出来就是程序卡死在中断里。

接收超时中断值得单独讲一下。当FIFO里的数据没达到触发等级,但已经有数据时,16550会产生接收超时中断,保证你不漏掉零散到达的字节。所以ISR案例0x04和0x02做的事情本质一样:把FIFO里的残留数据全部读走。

4.3 发送函数、接收队列和中断控制器的注册

发送时,先往THR写第一个字节,然后使能发送保持寄存器空中断,后续字节在ISR的case 0x00里继续填。这样做的好处是,发送一个长字符串时CPU只需要启动一下,剩下的由中断逐字节搬运,不需要阻塞等待。代码大致是:

void uart16550_send(uint8_t *data, uint32_t len) { // 拷贝到发送缓冲区,设置索引 // 为了简单,这里省略对tx_buf的拷贝过程 tx_len = len; tx_index = 0; // 写第一个字节 Xil_Out32(UART_BASE + THR_OFFSET, tx_buf[tx_index++]); // 使能发送保持寄存器空中断(IER bit1) uint32_t ier = Xil_In32(UART_BASE + IER_OFFSET); Xil_Out32(UART_BASE + IER_OFFSET, ier | 0x02); }

注意IERP使能发送中断之前,要确认第一字节已经写进THR,否则THRE中断会在你写THR之前就触发,逻辑上虽然问题不大,但会多一次空转中断,干扰你的状态判断。

中断控制器注册这一步,要根据平台区分。Zynq的话,用XScuGic,把PL端中断号注册到异常表;MicroBlaze则用XIntc或AXI Interrupt Controller的API。这里给一个Zynq的简化示例:

#include "xscugic.h" static XScuGic intc; #define UART_INT_ID XPAR_XSCUGIC_0_AXI_UART16550_0_INT_O_INTR void uart16550_intc_init(void) { XScuGic_Config *cfg = XScuGic_LookupConfig(XPAR_SCUGIC_0_DEVICE_ID); XScuGic_CfgInitialize(&intc, cfg, cfg->CpuBaseAddress); Xil_ExceptionInit(); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, &intc); XScuGic_Connect(&intc, UART_INT_ID, (Xil_InterruptHandler)uart16550_isr, NULL); XScuGic_Enable(&intc, UART_INT_ID); Xil_ExceptionEnable(); }

这里的UART_INT_ID宏要和你实际的硬件连接对应,不同工程的xparameters.h生成的宏名可能有差别,务必去头文件里确认,不要照搬。

5. 避坑实录:从仿真到上板的经验总结

5.1 问题速查表,先对号入座

现象可能原因解决办法
上板后接收全是乱码波特率分频值不对;uart_clk频率填错;两侧帧格式不一致核对IP配置里UART Clock与真实时钟频率;重新计算divisor;确认都是8N1
中断一直触发,CPU像卡死ISR没读完所有挂起中断;没清中断源;IER误开了不需要的中断按示例在ISR里循环直到IIR bit0为1;正确清除RBR/LSR/THR对应状态
发送不出去,或只发第一个字节THRE中断没使能;发送中断被误关;tx_index计数错误按流程先写THR再开IER bit1;检查ISR里发送分支逻辑
接收偶尔丢字节FIFO触发等级太高;ISR处理不及时;中断优先级不够降低FCR里的RX触发等级;ISR里循环读完FIFO;检查中断控制器优先级
AXI读回数据固定为0或0xFF地址映射错误;s_axi_aclk没跑;aresetn无效核对xparameters.h地址;检查Block Design时钟和复位连接;用ILA抓总线
和PC调试助手连不上流控被使能;TX/RX接反;引脚约束没做关闭PC端流控;确认FPGA引脚约束正确;在板级回环验证
第一次收发正常,热复位后异常复位时序问题;FIFO没被复位初始化时写FCR先清零FIFO;检查复位信号是否满足IP要求

5.2 波特率误差带来的“假死”现象

我曾经遇到过一个很典型的场景:uart_clk用了33.333MHz,这是从某个PLL分出来的,目标波特率设成57600。算一下分频值,33333300 / (57600 * 16),结果不是整数,取整之后误差超过3%。现象很诡异:PC端发送数据,FPGA有时候能收到几个正确字节,有时候全是错的,甚至完全收不到。用示波器抓TXD,发现每个bit的宽度和标准57600差了明显的一截。

这类问题排查思路很简单,就三步:第一步,用示波器或逻辑分析仪量FPGA的TXD引脚,数一个bit的时间,换算成实际波特率;第二步,反向计算当前DLL/DLM里写进去的分频值,看看IP实际是打算跑多少波特率;第三步,对比两者是否一致,不一致找链路中哪个环节填错了。

还有一个很容易忽略的点:IP配置界面的UART Clock和Block Design里实际连接的时钟频率可能不一致。比如Block Design里时钟原来100MHz,后来因为某个模块改了PLL配置变成75MHz,但是IP里UART Clock还停留在100MHz,IP会按照100MHz去算分频值,实际跑75MHz,波特率自然偏掉。每次改过时钟树,都要回头核对IP参数。

5.3 中断风暴:电平触发带来的麻烦

电平触发VS脉冲触发,在FPGA中断处理里是天壤之别。脉冲触发时,哪怕ISR没清干净中断源,下一拍脉冲不来就没事;电平触发不同,只要中断源还挂着,中断线永远是有效电平,中断控制器就会反复上报。16550就是电平触发,ISR里如果只处理了接收,而发送中断也挂起了,或者接收FIFO没读完,CPU就会疯狂进ISR。

我踩过的一个具体坑是:ISR里处理完接收数据后直接return,没重新读IIR,结果因为IER同时开了接收和线路状态中断,ISR只清了接收,线路状态那个中断源还挂着,CPU就死锁在中断里。打出来的现象是程序跑一会儿就卡住,用调试器暂停一看,PC落在中断服务函数里,而且每次暂停位置都不一样。

解决办法就是我前面代码里写的那个while循环:处理完一个类型后,重新读IIR,再看bit0,如果仍然为0,说明还有挂起中断,继续处理,直到IIR显示没有挂起为止。这个习惯一旦养成,基本能杜绝中断风暴。

5.4 一些不为人注意的小细节

第一,LSR的bit0表示接收数据就绪,但FIFO模式下它表示FIFO里有至少一个数据。在ISR的接收分支里,我习惯用while (Xil_In32(base + LSR_OFFSET) & 0x01)循环读取,保证把FIFO里所有数据都取走,而不是只取一个就停。这样即使FIFO在读取期间又收到新数据,也能一并处理,避免残留数据触发无谓的中断。

第二,FCR的bit1和bit2分别是RX FIFO和TX FIFO的复位位。初始化时把这两个位置1,给一个短暂的脉冲清零,能确保上电瞬间FIFO状态干净。Xilinx的IP有时会出现复位后FIFO里残留数据的情况,特别是热复位时,主动复位一次FIFO能省很多麻烦。

第三,用Zynq时注意把中断引脚正确的连到IRQ_F2P。很多人Block Design里忘了连int_o,或者连到了IRQ_F2P[1]但SDK里查的是IRQ_F2P[0]的中断号,结果中断完全进不去。建议在Block Design里专门打开连接图检查一下这条路径,再在xparameters.h里搜UART_INT_O_INTR,确保宏存在且值和你预期一致。

第四,如果板子上的UART芯片带硬件流控,而IP又没有启用Modem端口,PC端调试助手的RTS/CTS相关选项一定要关掉。否则上位机可能因为CTS无效而拒绝发送,表现是FPGA收不到任何数据,但寄存器看都正常。

最后再说几句实在话

踩过这么多坑之后,我个人最深的体会是:AXI UART 16550这个IP核,本身不复杂,寄存器就是老16550那一套,关键是要把“寄存器复用规则”“波特率分频计算”“电平触发中断”这三个点彻底想明白。只要这三个点通了,网上的资料加上这篇笔记,基本能解决百分之九十的问题。

我自己的习惯是,新板子首次上电跑串口时,不急着写业务逻辑,先写一个回环自测程序:PC发一串数据给FPGA,FPGA通过16550中断接收,再原样发回PC,PC收到后比对,确认无误再往上面叠功能。这个回环测试看似简单,但它能把FPGA引脚约束、AXI总线访问、波特率、帧格式、中断处理这整条链路一次性验证完,后面再出问题,至少知道不是最底层串口的问题。

如果还要提一个最容易被忽略的坑,我选uart_clk的时钟频率。每次改完PLL或者时钟向导再回来调串口,第一件事就是去IP配置里确认UART Clock是不是最新值。就这一条,好几次帮我避开了整夜的调板子时间。希望这篇笔记也能帮你少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询