news 2026/9/24 11:48:57

AXI UART 16550 IP核配置与中断调试全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AXI UART 16550 IP核配置与中断调试全攻略

如果你正在Vivado里搭MicroBlaze或者Zynq PL端的串口,大概率会在IP Catalog里看到AXI UART 16550这个IP核。这不是那种“拖进来就能跑”的简单外设,我见过太多人卡在配置和中断上:有的上板后串口完全没有反应,有的乱码乱到怀疑人生,有的中断只触发一次就再也没动静。这篇文章把我在多个项目里踩过的坑、排查过的链路、以及最后沉淀下来的调通顺序完整写出来,涉及IP配置、寄存器机制、中断连接和上板调试四个部分。适合正在用这个IP核做串口通信的FPGA工程师,也适合准备选型却还没弄清楚它深浅的同学。

1. 为什么大家都用这个IP核:AXI UART 16550的适用边界

1.1 相比自写UART和UART Lite,它到底强在哪

FPGA上做串口通信,说白了有三条路:自己写UART RTL、用AXI UART Lite、用AXI UART 16550。自写UART高度可控,想怎么改都行,但代价是从头验证波形、时序、以及AXI接口和中断逻辑,尤其当系统里还有DMA和中断控制器的时候,这部分工作量大到超预期。

AXI UART Lite胜在资源少、结构简单,适合只做调试打印的场景。但它寄存器集很精简,FIFO也做得比较浅,数据量一旦上来就很容易丢。而且它的中断能力比较弱,想用它做稳定的大流量收发,软件要花不少功夫去兜底。

AXI UART 16550就不一样,它把PC时代经典UART控制器的架构整个搬到了AXI总线上,寄存器布局和PC16550D保持一致,自带发送和接收FIFO,中断系统完整,波特率可配,官方驱动支持也成熟。很多做嵌入式的人对16550驱动再熟悉不过,Linux内核里的8250驱动都能直接参考。对于MicroBlaze系统、Zynq PL端串口,或者要跟PC串口调试工具对接的项目,它基本是首选项。

1.2 什么情况下别硬上这个IP核

虽然它好用,但也不是所有场合都合适。如果项目只是需要一个调试串口,打印个日志,115200都跑不满,那AXI UART Lite更合适——配置少、不折腾、逻辑简单,出了问题一眼就能看穿。

再比如你要做自定义帧格式、半双工多机通信、或者比较特殊的奇偶校验策略,16550这套寄存器模型反而会限制你。它的一切行为都围绕“标准UART协议”设计,想突破这个框架就得自己写逻辑,那就没必要绕这个弯。

还有一点是资源开销。16550内部的FIFO、完整中断机制和寄存器译码逻辑,LUT和寄存器用量确实比UART Lite高一截。当然在现代FPGA上这点资源不算什么,但如果你的设计对资源咬得很紧,或者PL端逻辑非常简单,用Lite会清爽很多。

2. 配置界面里藏着的大坑:时钟、波特率、寄存器兼容

2.1 Input Clock Frequency 填错,等于白干

IP配置界面的Basic页签里,有一个看起来不太起眼的参数:Input Clock Frequency (Hz),默认值通常是100000000。这个参数表示的是s_axi_aclk,也就是AXI总线时钟的实际频率。IP核内部要靠它来计算波特率分频系数,相当于整个UART心跳的基准。

坑就出在这里。很多人在Block Design里搭完系统,发现总线时钟其实是50MHz或者125MHz,却没有回过来改这个值。结果IP核以为自己工作在100MHz,实际接的是50MHz,算出来的分频系数自然全错,UART线上输出的波特率跟目标差了一倍。表现出来就是发送乱码,或者干脆不通。

有一个项目我印象很深,调试串口怎么都不出数据,引脚约束、回环测试、软件代码全查了个遍。最后发现是clk_wiz的输出频率从100MHz改成了50MHz,但IP核里仍然填的100MHz。把配置改回50MHz,重新生成IP,重跑综合实现,问题当场消失。这个参数改完以后一定要同步检查Block Design里的实际时钟频率,两个地方不一致,所有跟波特率相关的调试都是白费功夫。

2.2 波特率分频的数学账:从100MHz跑115200为什么有误差

经典16550的分频公式并不复杂:

分频系数 = 参考时钟频率 / (16 × 目标波特率)

结果存到DLL和DLH两个寄存器里,低字节和高字节各8位。16倍这个数,是UART过采样率的标准做法,用来帮助接收端在每一位的中心采样。

拿100MHz时钟举例,目标波特率115200:

100000000 / (16 × 115200) ≈ 54.253

寄存器只能存整数,取54之后,实际波特率变成:

100000000 / (16 × 54) ≈ 115740.74

误差0.47%。UART接收端通常能容忍2%到3%的波特率偏差,0.47%完全安全。但如果你把目标设成921600,分频系数约等于6.78,取6或7的误差都大得吓人,通信基本不可用。

这里我习惯列一张表,方便选时钟的时候直接算:

目标波特率参考时钟分频系数(理论)取整后实际波特率误差
9600100 MHz651.049600.610.006%
115200100 MHz54.25115740.740.47%
460800100 MHz13.56134或14 → 446428.573.1%左右
921600100 MHz6.786 → 1041666.6713.09%

PC端串口工具显示的波特率是“目标值”,FPGA端UART线路上真正跑的是“实际值”。两边误差一大,短帧可能侥幸对上,长帧数据采样点就会错位。很多人说高速串口不稳定,第一步先算这笔账,别急着怀疑硬件。

2.3 Use UART 16550 Compatible Register Set 勾不勾

配置界面里有个选项叫Use UART 16550 Compatible Register Set,默认通常是勾上的。勾上之后,IP核的寄存器布局跟经典PC16550D保持一致:RBR/THR、IER、IIR/FCR、LCR、MCR、LSR、MSR、SCR一个不少,DLL/DLH在LCR的最高位置1时映射到前两个地址。网上找来的16550驱动代码基本可以直接移植,Linux内核里8250驱动的操作方式也能对上。

如果不勾选,IP核会对寄存器组做裁剪,有些情况下MCR、MSR都被去掉了,软件按经典地址去访问,读到的值没有意义,写进去的操作也可能无效。后面如果接Xilinx官方驱动或者Linux驱动,这个选项直接决定驱动能不能正确识别IP核。

我的建议很直白:没有特殊理由就勾上。我自己做MicroBlaze串口项目,基本都是标准兼容模式。省掉那些奇怪寄存器差异带来的心智负担,排查问题会快很多。

2.4 FIFO深度与接收触发阈值怎么选

发送和接收FIFO深度在配置界面里可选,常见有16、32、64字节。FIFO越深,给CPU的响应时间越宽裕,代价是内部存储逻辑占用的资源更多。接收中断的触发电平由FCR寄存器的位7和位6设置,但不同FIFO深度下档位含义不同。例如16字节FIFO时,档位大约是1、4、8、14字节;64字节FIFO时又不一样。

触发阈值设成1字节,响应最快,但每来一个字节都进一次中断,CPU开销很大;设成接近满,中断次数少,但对零散数据非常不友好,对方发两三字节之后就不发了,数据一直躺在FIFO里,迟迟不进ISR。所以裸机应用我一般取中间档位,同时打开字符超时中断做兜底,保证零散数据也能及时被软件取走。

发送方向也容易出问题。发送FIFO满的时候再往THR写数据,新数据进不去。很多初学的人直接在一个for循环里连续写几十个字节,结果只发出去一部分,剩下的全被吞了。发送之前必须检查LSR的THRE位或者TEMT位,或者干脆用THRE中断来控制发送节奏。

3. 中断处理全链路:从寄存器机制到RD连线

3.1 五种中断源和IIR的编码表

真正把AXI UART 16550用熟,得先弄清IIR寄存器。IIR读出来之后,bit0是中断挂起标志:0表示有未处理的中断,1表示当前没有中断。bit3到bit1是中断类型编码。IP核在任意时刻只会向CPU报告当前最高优先级的中断,处理完当前这个,如果还有挂起,IIR的bit0会继续为0。

中断源优先级和清除条件如下表:

优先级中断源IIR[3:1]触发条件清除方式
1(最高)接收线路状态错误011LSR中溢出、奇偶、帧等错误位置位读LSR
2接收数据可用010接收FIFO达到触发电平读RBR,直到FIFO低于触发电平
3字符超时110FIFO非空,且超过约4个字符时间没有新数据读RBR
4发送保持寄存器空001TX FIFO由非空变为空写THR
5(最低)调制解调器状态000MSR状态位发生变化读MSR

字符超时中断值得单独拿出来说,它是FIFO模式特有的。如果对方只发过来几个字节,没到接收触发阈值,RDA中断不会触发。没有超时中断的话,这些零散数据会一直躺在FIFO里,看起来就像丢了。超时中断保证了无论数据多零散,软件最终都能把它取走。

3.2 中断服务程序的正确打开方式

下面这段代码是我在MicroBlaze裸机上常用的中断处理骨架,直接操作寄存器。Xilinx官方驱动里封装了类似逻辑,但自己理解了这套流程,排查驱动异常时会省很多事。

#define UART_BASE XPAR_AXI_UART16550_0_BASEADDR #define RBR (UART_BASE + 0x00) #define THR (UART_BASE + 0x00) #define IER (UART_BASE + 0x01) #define IIR (UART_BASE + 0x02) #define FCR (UART_BASE + 0x02) #define LCR (UART_BASE + 0x03) #define MCR (UART_BASE + 0x04) #define LSR (UART_BASE + 0x05) #define MSR (UART_BASE + 0x06) #define IER_RDA (0x01) #define IER_THRE (0x02) void UartIsr(void *param) { u32 iir, lsr, data; while (((iir = Xil_In32(IIR)) & 0x01) == 0) { switch ((iir >> 1) & 0x07) { case 0x3: // 接收线路状态错误 lsr = Xil_In32(LSR); break; case 0x2: // 接收数据可用 while ((Xil_In32(LSR) & 0x01) && rx_len < RX_BUF_SIZE) { rx_buf[rx_len++] = Xil_In32(RBR); } break; case 0x6: // 字符超时 while ((Xil_In32(LSR) & 0x01) && rx_len < RX_BUF_SIZE) { rx_buf[rx_len++] = Xil_In32(RBR); } break; case 0x1: // 发送保持寄存器空 if (tx_len < tx_cnt) { Xil_Out32(THR, tx_buf[tx_len++]); } else { Xil_Out32(IER, Xil_In32(IER) & ~IER_THRE); } break; default: // 调制解调器状态等 lsr = Xil_In32(MSR); break; } } }

这段代码有几个关键点。第一,用while循环把所有挂起中断处理完,避免漏掉优先级较低的中断。第二,RDA和字符超时都通过读RBR来清,接收路径上循环读取直到FIFO空,才不会残留数据。第三,THRE中断里如果发送队列已经发完,必须马上把IER里的THRE位关掉,否则发送FIFO空的状态会一直触发中断,把CPU淹没在低优先级中断里。

读取IIR本身并不会清除任何中断,真正清除中断的是读对应的状态寄存器。很多人第一次接触时会误以为读了IIR就完事,结果ISR里反复循环,程序直接卡死。这不是IP核有问题,是中断清除机制没理解透。

3.3 Block Design里interrupt引脚接哪里

Block Design里这块看起来只是一根连线,但interrupt引脚的接法决定了整条中断链路能不能通。MicroBlaze系统一般用AXI Interrupt Controller,也就是axi_intc,把axi_uart16550_0的interrupt接到axi_intc_0的intr向量某一个bit上,比如intr[0],然后把axi_intc_0的输出接到MicroBlaze的INTERRUPT端口。

Zynq系统里,PL端的串口中断可以接到PS的IRQ_F2P端口,然后在Vitis里对应到GIC的中断号。要注意,IRQ_F2P上的中断类型和GIC的配置必须匹配,中断类型不对,中断根本送不到CPU。

接线完成之后,去Address Editor确认一下寄存器基地址。MicroBlaze系统如果忘了给UART分配地址,CPU访问的时候会直接产生总线错误。之前遇到过一次地址自动分配把两个外设分到了重叠区域,访问时相互干扰,现象非常诡异,查了很久才定位到是地址空间冲突。

3.4 电平触发还是边沿触发:一个能把串口打断魂的设置

AXI UART 16550的interrupt输出是高电平有效,这是它和很多简单外设中断不一样的地方。有中断挂起时,输出拉高并保持;所有中断都处理干净之后,输出才拉低。

所以中断控制器必须配置成电平触发,典型是HIGH_LEVEL。如果配成边沿触发,中断信号拉高之后保持不动,不会产生新的边沿,中断控制器只会在第一次上升沿响应,后面再也不进来。反过来,如果ISR清中断太慢,电平一直保持高位,边沿触发也补不了第二次触发。

在Vivado的Block Design里配置axi_intc时,每个intr端口都有中断类型属性,务必选HIGH_LEVEL。如果选错了,改完配置之后要重新生成Block Design,再走综合实现。这个点我见得太多次了,好几个同事卡了半天,最后发现就是中断类型配成了一个边沿触发。

4. 上板调试实录:乱码、无响应、中断丢数据的完整排查链路

4.1 现象一:下载完比特流,串口完全没反应

比特流下载进去,打开串口助手,发送没反应,接收也没反应。遇到这种情况别急着改代码,按顺序排查。

第一步看管脚约束。UART_TX和UART_RX是否被约束到了实际有引出的引脚,有没有被其它模块占用,或者TX和RX对调了。最蠢也最常见的错误,是硬件上TX接TX、RX接RX,没有做交叉连接,电脑那边怎么等都等不到数据。

第二步确认系统时钟和复位。如果MicroBlaze本身就没跑起来,串口自然没反应。先用一个GPIO点灯程序验证CPU、时钟、复位是否正常。很多“串口不工作”其实根本不是串口的问题,而是CPU压根没执行到串口初始化。

第三步用ILA抓UART的TX信号。如果串口发送时顶层uart_tx永远是高电平,说明数据根本没到发送移位寄存器。这时候往回查AXI总线访问是否正常。软件层面最快的方式是往SCR寄存器写一个测试值,比如0x5A,再读回来对比。能写回说明AXI接口和地址没问题;读不到或者异常,先查地址分配和连线。

如果Implement Design变红或者生成比特流失败,那也是板级验证之前要解决的前置问题。这种更多是综合时序、约束的问题,跟IP核本身不一定有直接关系,先把工程跑干净了再说。

4.2 现象二:能收到数据,但全是乱码

串口能连通,说明基本链路已经打通,问题多数出在参数匹配或时钟配置上。

优先检查波特率误差。按前面2.2的公式算一下当前参考时钟下目标波特率的实际偏差。100MHz时钟跑115200,误差0.47%,肯定没问题。但如果你设的是460800甚至更高,先算清楚误差再排查别的。很多所谓的高速串口乱码,就是分频系数取整误差太大导致的。

接着确认IP核配置里的Input Clock Frequency跟实际s_axi_aclk一致。这个参数不一致,波特率误差会被放大到无法容忍。

数据位、停止位、校验位也要和电脑端严格一致。IP核里设了8N1,电脑却选了7E1,收到的字节必然乱码。另外,如果板上还有RS232/RS485电平转换芯片,要确认电平标准与外部设备匹配。

排查乱码时有个很高效的招数:配置MCR的loopback位,也就是MCR[4]=1,让发送端在芯片内部直接环回到接收端。如果自环收发数据完全正确,说明UART核本身没问题,问题在引脚连线或者外部设备。自环测试通过后撤掉loopback,再连外部设备,两个方向分开验证。

4.3 现象三:中断触发一次后,CPU像死了一样

上电后接收第一帧数据,中断进来了,ISR也进了,但之后再也没中断,或者整个CPU卡死在中断里。这个现象让我排查过很多次,最后归因基本落在三个方向。

第一,中断没有被正确清除。RDA中断需要读RBR直到FIFO低于触发电平。如果ISR里只读了IIR和RBR一字节,但FIFO里的数据量还是高于阈值,interrupt引脚继续拉高,电平触发的中断控制器会不断触发,形成死循环。反过来如果中断源一直挂起但又被某种方式屏蔽了,就会表现为只响应一次。

第二,IER里的中断使能被误关。有些ISR在进入时会保存IER,退出时恢复,但如果恢复逻辑写错,或者某个分支里把RDA使能关了没打开,之后数据来了也不会上报。

第三,中断控制器触发类型配错。16550输出电平信号,控制器配成边沿触发,这是典型的“只中断一次”。调试时先用ILA抓一下interrupt引脚:如果一直是低,说明IP核压根没有发起中断请求,问题在IP核侧;如果一直是高,说明IP核在等软件清中断,问题在ISR处理逻辑。这一个动作能把排查范围缩小一半。

4.4 现象四:大流量收发时丢数据

短传没问题,长传或者大流量下发时,接收端时不时少字节,那问题基本在缓冲和处理速度上。

先查接收溢出。接收FIFO满之后,新到的字节无法写入,LSR的溢出错误位会被置位,数据直接丢。提高FIFO深度、降低接收触发阈值,让软件更早来取数据,都能改善这个问题。ISR的优先级也要检查,如果系统里中断很多,UART中断一直被延迟,FIFO会被填满,自然丢数据。

发送方向丢数据,常见原因是发送FIFO满时继续写THR。之前提过,标准16550在FIFO满时,写THR不会把数据送进FIFO。所以发送前必须查LSR的THRE或者TEMT位,或者在THRE中断里严格控制发送节奏:有数据才写,没数据就关中断。

如果使用中断发送,THRE中断的使能逻辑要格外小心。发送FIFO空时,THRE会一直有效,处理完一批数据后必须关掉THRE位,否则就会在ISR里空转,拖累整个系统。

大数据量场景下的调优方向一般是这样:接收FIFO触发点适当调低,让ISR提前介入;发送侧用THRE中断或者轮询加FIFO判断,避免写满;整个ISR里避免重操作,能读就读,能清就清。普通115200速率下,16字节FIFO加合理的ISR已经足够跑得很稳。

4.5 写在最后:推荐一套省时间的调通顺序

这套顺序是我自己一直在用的,基本可以避开绝大多数组合问题。

先把IP核配置里的波特率设成9600或115200,FIFO深度选16,Input Clock Frequency跟实际总线时钟严格一致。在Block Design里把中断线连好,但软件里先不使能中断,用轮询方式跑通发送和接收。轮询的代码更简单,排查问题更直接。然后打开MCR的loopback位,写一串字节再读回来对比,确认UART核本身工作正常。

回环通过后,关掉loopback,接外部串口工具,验证实际接线和设备。外部通信正常以后,再加接收中断。IER先只开RDA,确认能进ISR、能收到完整数据。再加THRE中断,但记得发送队列空时关闭THRE使能。最后再把字符超时、接收线路错误中断全部打开,做完整功能验证。

这样每一步只引入一个变量,出问题的时候定位会非常快。我见过太多人把中断、回环、外部设备一次性全接上,出问题之后根本分不清是IP核配置、软件逻辑还是外部硬件的问题。分步调通,反而是最快的路。

如果整套系统在Vitis里用官方驱动,步骤会简化不少,但前面这些寄存器级知识在排查驱动异常时依然是最可靠的抓手。FPGA调试这个东西,很多时候不是靠感觉,是把链路一节一节拆开,找到最先断开的那一环。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 11:48:51

写好Project Change Report:变更管理全流程实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 11:47:05

魔百盒M401A刷Armbian安装Home Assistant Supervised全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 11:46:08

消防安防一体化:执行器智能化联动实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 11:44:34

GitHub热榜全解析:从趋势追踪到高效使用与自动化采集

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 11:44:25

设备互联互通与接口芯片设计:从协议转换到SerDes物理层实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 11:42:30

LAN9252从站开发:SSC工具生成EtherCAT XML避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华