十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

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

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时钟举例目标波特率115200100000000 / (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中溢出、奇偶、帧等错误位置位读LSR2接收数据可用010接收FIFO达到触发电平读RBR直到FIFO低于触发电平3字符超时110FIFO非空且超过约4个字符时间没有新数据读RBR4发送保持寄存器空001TX FIFO由非空变为空写THR5最低调制解调器状态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或115200FIFO深度选16Input Clock Frequency跟实际总线时钟严格一致。在Block Design里把中断线连好但软件里先不使能中断用轮询方式跑通发送和接收。轮询的代码更简单排查问题更直接。然后打开MCR的loopback位写一串字节再读回来对比确认UART核本身工作正常。回环通过后关掉loopback接外部串口工具验证实际接线和设备。外部通信正常以后再加接收中断。IER先只开RDA确认能进ISR、能收到完整数据。再加THRE中断但记得发送队列空时关闭THRE使能。最后再把字符超时、接收线路错误中断全部打开做完整功能验证。这样每一步只引入一个变量出问题的时候定位会非常快。我见过太多人把中断、回环、外部设备一次性全接上出问题之后根本分不清是IP核配置、软件逻辑还是外部硬件的问题。分步调通反而是最快的路。如果整套系统在Vitis里用官方驱动步骤会简化不少但前面这些寄存器级知识在排查驱动异常时依然是最可靠的抓手。FPGA调试这个东西很多时候不是靠感觉是把链路一节一节拆开找到最先断开的那一环。
返回列表