在Zynq平台上做Petalinux开发时,AXI UART Lite大概是那种“越简单越容易栽跟头”的外设。从Vivado里拖出这个IP、分配好地址,最多十分钟;可等到板子上Linux跑起来,你会发现这个挂在AXI总线上的串口,比PS自带的ttyPS0难伺候得多。明明ttyPS0刷日志刷得飞起,UART Lite就是黑屏或者满屏乱码,而且排查路径还不止一条——设备树时钟、内核驱动、终端工具配置、硬件连接,每一环都可能出问题。
这篇文章是我实际调试AXI UART Lite的完整记录:从Petalinux侧的设备树与内核配置,到minicom和screen这两种常见串口工具,再到用devmem直接操作寄存器做硬件验证,三条路都会给出具体命令和实测结果。如果你正卡在“minicom打开一片黑”或“echo出来的全是乱码”这个阶段,按照这篇里的顺序排查,大概率能找到问题所在。
1. 先从硬件特性说起:AXI UART Lite调试难的根源
1.1 这个IP到底“简单”在哪,又“坑”在哪
AXI UART Lite在Xilinx IP家族里属于极简派。它不像PS端集成的Cadence UART那样带一堆调制解调器控制信号、自动流控和深度可配置的FIFO,它能做的事只有两件:往TX FIFO里写字节、从RX FIFO里读字节。寄存器一共四个:RX FIFO、TX FIFO、状态寄存器、控制寄存器。单看这个设计,确实比16550好搞定得多。
但恰恰是这个“简单”,让很多人都掉进了同一个坑:以为它和ttyPS0一样,上电就有、开个minicom就能用。实际上,它挂在PL侧,依赖bitstream加载,依赖AXI总线和PS侧互连,更依赖一个完整无误的设备树节点把地址、中断、时钟信息告诉内核。调试链路从应用层一路沉到硬件层,任何一层出错,表象都是“串口不通”,但排查方向却天差地别。
1.2 四条排查链路拆解
可以把一次UART Lite调试问题拆成四层:
| 层次 | 常见故障表现 | 检查手段 |
|---|---|---|
| 应用层 | /dev/ttyULx 不存在或打不开 | ls /dev/ttyUL*、权限检查 |
| 驱动层 | dmesg 无 probe 信息,或报错 | dmesg、cat /proc/tty/drivers |
| 设备树层 | probe 失败,时钟错误 | 设备树 dtsi 检查 |
| 硬件层 | TX/RX 引脚无电平变化 | devmem 读寄存器 |
很多人在第一层(应用层)死磕,反复换minicom、重启板子,实际根因在第三层。这就是为什么我强烈建议先熟悉devmem操作,后面你会看到这个“原始”方法怎么一锤定音。
1.3 和 ttyPS0 的本质区别
一个很重要的区别:ttyPS0是PS硬核自带的UART控制器,只要bootrom级别就开始工作,根本不依赖PL配置。而AXI UART Lite的物理逻辑在PL里,如果bitstream没加载、或者PL侧的时钟没起来,Linux里即便设备树写对了,硬件也不响应。调试时先确认两件事:一是PL bitstream确实加载了(看dmesg里的FPGA manager信息,或U-Boot阶段确认),二是Vivado Address Editor里的地址和设备树reg一致。
我自己的经历里有一个典型例子:板子启动后ttyUL0节点已经出现,但是读写完全没反应,用devmem读状态寄存器也一直是0x05(两个FIFO都空),最后查了很久才发现是FSBL打包时没有把bitstream合进BOOT.BIN,PL侧逻辑根本没跑起来。这种问题,光在minicom里折腾是永远找不到答案的。
2. 把它点亮:Petalinux侧的内核配置与设备树写法
2.1 内核驱动:CONFIG_SERIAL_UARTLITE
Petalinux默认可能没开Xilinx UART Lite驱动,尤其是用Petalinux向导建工程时如果没把UART Lite设为console,内核配置里大概率是空的。手动打开方法:
petalinux-config -c kernel Device Drivers -> Character devices -> Serial drivers [*] Xilinx UART lite serial port support对应Kconfig项就是CONFIG_SERIAL_UARTLITE。如果还想让内核启动日志打在这个串口上,还需要开Console support并把console=ttyUL0传给内核。一般调试不太建议这么做,毕竟系统启动日志走ttyPS0更稳,UART Lite单独作为应用串口就够了。
设置完保存退出,编译:
petalinux-build2.2 设备树:自动生成的不一定够用
Vivado导出的hdf经过Petalinux的device tree generator,会自动生成AXI UART Lite的节点,类似这样:
axi_uartlite_0: serial@42c00000 { compatible = "xlnx,xps-uartlite-1.00.a"; reg = <0x42c00000 0x10000>; clocks = <&misc_clk_0>; clock-names = "s_axi_aclk"; current-speed = <115200>; port-number = <0>; };自动生成的节点有几个容易出问题的地方:
clocks引用的是misc_clk_0而不是PS端的FCLK_CLK0。如果UART Lite实际挂在FCLK_CLK0上,但设备树引用了不存在的时钟节点,驱动probe就会报“failed to get clock”。current-speed可能没生成,或者和Vivado IP配置的波特率不一致。这个属性影响用户空间获取默认波特率,虽然驱动本身不靠它给出硬件速率,但终端工具打开设备时会用它作为初始值,不一致就有概率出乱码。reg地址空间大小取决于Vivado分配,0x1000、0x10000都有,别照抄别人的板子。
所以更稳的做法是在system-user.dtsi里显式覆盖:
/include/ "system-conf.dtsi" &axi_uartlite_0 { clocks = <&clkc 15>; /* FCLK_CLK0 */ clock-names = "s_axi_aclk"; current-speed = <115200>; status = "okay"; };clkc 15对应Zynq-7000的FCLK_CLK0,如果挂在FCLK_CLK1就改为16,以此类推。如果拿不准,直接去Vivado的Address Editor和Clock Configurator里确认,这是最不容易出错的方式。
2.3 编译、打包与SD卡启动
设备树改完后:
petalinux-build petalinux-package --boot --fsbl --fpga --u-boot --force第二条命令会从images/linux目录找到FSBL、bitstream和U-Boot,生成BOOT.BIN。image.ub已经在编译时生成。如果你的U-Boot环境需要boot.scr(脚本方式加载镜像),可以把boot.cmd用mkimage封装:
mkimage -T script -C none -n "Boot Script" -d boot.cmd boot.scrboot.cmd内容最基本的就两行:
fatload mmc 0:1 0x2000000 image.ub bootm 0x2000000SD卡分区建议:第一分区FAT32,放BOOT.BIN、boot.scr、image.ub;第二分区ext4,放rootfs。上电后进入系统,先验证节点:
dmesg | grep -i uartlite ls -l /dev/ttyUL*正常会看到:
uartlite: ttyUL0 at MMIO 0x42c00000 (irq = 45) is a uartlite如果这一行都没有,优先回头看驱动配置和设备树,而不是去折腾minicom。
2.4 驱动加载失败的典型报错
uartlite: probe of 42c00000.serial failed with error -22:绝大多数是时钟问题。检查设备树clocks的phandle和clock-names。clk: failed to enable 42c00000.serial:同样指向时钟,确认FCLK_CLK0确实使能且频率和Vivado一致。- 没有任何输出但地址对:检查bitstream有没有加载,以及
/proc/device-tree里节点是否真的存在。
提示:在内核menuconfig里按“/”搜索UARTLITE,可以快速定位到对应的配置项,不用一层层翻菜单。
3. 方法一:minicom交互调试与乱码根因排查
3.1 minicom的配置流程与常用参数
minicom是Linux下最经典的串口终端工具,Petalinux的rootfs里如果没装可以用petalinux-config -c rootfs勾选,或者直接在开发机上用。首次使用推荐走一遍配置:
minicom -s进入配置菜单后选Serial port setup,重点改四个参数:
- 按A,把串口设备改成/dev/ttyUL0(注意不是ttyPS0)
- 按E,改成115200 8N1,和Vivado IP配置的波特率一致
- 按F,硬件流控选No,UART Lite没有流控引脚,开着必出问题
- 按G,软件流控一般也关掉
保存为默认配置后,直接运行minicom即可。快速验证链路是否通,最简单的就是回环:在minicom里敲几个字符,看另一端(或短接回环)是否能收到。如果没有任何反应,先别急着改minicom参数,按下面顺序排查:
ls -l /dev/ttyUL0 cat /proc/tty/drivers | grep uartlite dmesg | grep -i uartlite设备节点不存在就回到第二章查驱动;节点存在但minicom打不开,就是权限问题,把自己加进dialout组或者用sudo。
3.2 minicom乱码:从现象到根因的四步排查
minicom乱码应该是UART Lite调试里最热门的问题了。我的排查顺序是固定的:
第一步,排除工具配置。先确认minicom里波特率、流控设置和硬件一致。这里有个隐蔽的点:AXI UART Lite的波特率是在Vivado IP配置时固化的,Linux驱动没有波特率分频寄存器可以改,所以应用层设的波特率必须和IP配置完全一致。如果IP配的是9600,终端里设115200,基本必乱。
第二步,排除硬件时钟。把设备树clocks写明,确保和实际FCLK频率一致。这一步的坑在于,即使时钟信息不对,驱动也可能正常probe,但UART Lite内部波特率生成依赖时钟,时钟频率偏差会导致实际波特率偏移,表现出来就是乱码。最直接的验证是用devmem去读状态寄存器,如果RX FIFO一直在进数据,说明硬件确实在收,问题多半在波特率或终端配置。
第三步,短接回环测试。把板子上UART Lite的TX和RX引脚短接,然后在minicom里输入一个字符。正常情况下字符会立即回显(实际上就是直接从TX到RX,绕过了外部设备)。如果回显正常,说明IP内部收发链路和驱动都OK,乱码只可能出在外部接线、电平转换或另一端设备的参数上。这一步非常干净利落。
第四步,换工具交叉验证。如果minicom乱码,立刻用screen或echo/cat方式发一个已知字符,再用另一个终端读回来。这样可以区分“minicom自身问题”和“设备链路问题”。
举个实际案例。有个朋友调一块板子,minicom打开后全是乱码,改了波特率也没用。我让他先跑一句devmem读状态寄存器。正常情况下空闲状态应该读到0x05(TX FIFO空 + RX FIFO空),但他返回0x45,多了一个RX underrun标志(bit6)。虽然underrun未必直接造成乱码,但这个异常状态说明时钟配置有问题。回查设备树,clocks写错了,设备树里引用了一个不存在的时钟节点。修正之后STAT_REG稳定在0x05,minicom输出恢复正常。这个案例说明乱码的根因常常不在minicom本身。
3.3 minicom的日志与批量发送
调试时经常需要保留串口会话记录。minicom在运行中可以用Ctrl+A,然后按L开启日志,日志会持续写入文件,这对复现问题很有用。批量发送测试数据时,minicom的Ctrl+A,再按S可以调出上传菜单,但板端一般没有rz代理,更常见的方式是直接在终端里:
echo -n "hello uartlite" > /dev/ttyUL0这句虽然不属于minicom,但作为快速验证链路非常实用,和minicom配合起来效率很高。
4. 方法二:screen轻量连接:零配置快速看串口
4.1 screen一条命令接管串口
screen是Linux自带的终端复用器,大部分人拿它管理远程会话,但它也能直接接管串口。连接AXI UART Lite只需要一条命令:
screen /dev/ttyUL0 115200没有菜单、没有配置文件,启动即连接。要断开时先按Ctrl+A,再按K,最后按Y确认退出。如果你开了多个screen会话,screen -ls可以查看,screen -r 会话ID可以恢复。
4.2 滚屏与日志:它不只是“能连”
screen连接串口后被很多人吐槽“没法看历史输出”,其实是可以的:按Ctrl+A,再按Esc进入Copy Mode,方向键就能上下翻历史滚动,按q退出。记录日志用Ctrl+A,再按H,会在当前目录生成screenlog.0并持续写入。
还有一个容易忽略的点:screen的滚屏缓冲区大小默认是1000行左右,如果你需要更长的历史,可以在启动前用defscrollback 5000这个命令设置,写在~/.screenrc里即可。
4.3 什么时候用screen而不是minicom
我的习惯是这样:如果只是快速看一眼UART Lite有没有输出、顺手发个测试字符,screen是最快的,打开即用,退出也干净。但如果是较长时间的调试、需要反复切换参数、保存多组配置,minicom更靠谱,因为screen每次都要手动输设备名和波特率,连错一个参数很难一眼看出来(其实命令行里也能看到,但不如minicom菜单直观)。
还有一个坑:screen进程如果不退出就一直占着/dev/ttyUL0,另一个程序再去打开就会报device busy。这时候要么用screen -d -r把会话拉回来,要么直接kill掉对应screen进程,否则你会白排查半天。我现在凡是遇到“设备被占用”的报错,第一反应就是查是不是有残留的screen或者minicom进程。
5. 方法三:devmem直接寄存器操作:不依赖驱动的硬件体检
5.1 devmem是什么,为什么调试时它最管用
devmem是busybox里一个极简单的工具,直接在用户态通过mmap访问物理内存对应的寄存器。在Petalinux文件系统里基本都内置。它最大的价值在于:绕过一切驱动和上层协议,直接把寄存器的值和状态摆在你面前。当/ttyUL0设备节点消失、minicom报“No such device”、dmesg里驱动报错时,devmem是唯一还能验证“这颗IP本身是不是好的”的手段。
使用格式很简单:
devmem ADDRESS [WIDTH [VALUE]]不写VALUE就是读,写上VALUE就是写。WIDTH可以是8、16、32。对AXI UART Lite,数据寄存器建议用8位宽度(UART Lite数据寄存器低8位有效),读状态用32位更直观。
注意:Petalinux的rootfs一般带busybox,因此通常也有devmem命令。如果发现没有,用
busybox devmem或者从开发环境拷贝一个静态编译版本即可。另外,如果驱动已经加载并且ttyUL0被终端工具占用,devmem再去操作同一地址会和驱动打架,建议先关掉minicom等进程。
5.2 寄存器速查:PG142里最常用的几个位
把PG142的寄存器表浓缩成下面这张,调试时基本只看这几个:
| 偏移 | 名称 | 方向 | 用途 |
|---|---|---|---|
| 0x00 | RX FIFO | 只读 | 读一个接收字节,低8位有效 |
| 0x04 | TX FIFO | 只写 | 写一个发送字节,低8位有效 |
| 0x08 | STAT_REG | 只读 | 状态寄存器 |
| 0x0C | CTRL_REG | 只写 | 控制寄存器 |
STAT_REG里调试时最关心的是bit0、bit1、bit2、bit3:
| bit | 名称 | 读到时为1的含义 |
|---|---|---|
| 0 | TX FIFO Empty | 可以发数据 |
| 1 | TX FIFO Full | FIFO满了,不能写 |
| 2 | RX FIFO Empty | 没有收到数据 |
| 3 | RX FIFO Full | FIFO满了,要尽快读 |
CTRL_REG初始化时最低限度要置位bit2(TX Enable)和bit3(RX Enable),也就是写入0x0C。
5.3 实操:初始化、发一个字节、读回状态
假设基地址0x42C00000(以Vivado Address Editor为准),完整流程:
# 1. 使能TX和RX devmem 0x42C0000C 8 0x0C # 2. 查询状态,看看是否就绪 devmem 0x42C00008 # 3. 发送字符 'A' (0x41) devmem 0x42C00004 8 0x41发送后,如果对端串口工具收到'A',说明从寄存器到引脚整条链路都是通的。想发一串字符,可以写一个shell循环:
for ch in 48 65 6c 6c 6f; do devmem 0x42C00004 8 $ch done接收方向就轮询STAT_REG的bit2,为0说明RX FIFO里有数据:
while [ $(( $(devmem 0x42C00008) & 0x4 )) -eq 0 ]; do :; done devmem 0x42C00000这种轮询方式在CPU占用率上比较粗糙,但作为验证已经够用。如果要做更高效的收发测试,建议写个小的C程序直接mmap,不过那就是另一个话题了。
5.4 用回环测试确认硬件
最实用的一次体检是这样:把板子上UART Lite的TX引脚和RX引脚直接用杜邦线短接,然后:
# 清空状态 devmem 0x42C00008 8 0x30 # 发送 0x41 devmem 0x42C00004 8 0x41 # 回读状态,bit2应变为0,说明有数据进来 devmem 0x42C00008 # 读取RX FIFO,应该返回0x41 devmem 0x42C00000如果读回的不是0x41,要么寄存器地址、基地址不对,要么IP未工作。这一步会把“硬件问题”和“软件问题”彻底分开:回环不行,大概率是Vivado配置或bitstream没加载;回环正确,那就往上修设备树或驱动。
这里有个细节值得多说一句:写STAT_REG清状态时,写0x30(bit4和bit5)是清TX/RX overrun的标准写法,如果你同时把bit6也置1,会把RX underrun标志一起清掉。可以直接写0x70一次性清干净,但是只清overrun也足够后续判断了。
5.5 U-Boot阶段的寄存器验证
比devmem更早的验证窗口在U-Boot阶段。Petalinux生成的BOOT.BIN在加载kernel前会有U-Boot命令行,此时可以用md和mw直接操作相同的物理地址:
mw.l 0x42C00004 0x41 1 md.l 0x42C00008 1如果U-Boot阶段发送数据后,逻辑分析仪能看到TX引脚波形,那PL、时钟、引脚都确定是好的,问题范围立刻缩小到内核侧。这个技巧在调试启动阶段特别有用,很多人不知道U-Boot下也能这么干,白在Linux里折腾半天。
6. 三种方法实测对比:什么时候该用哪一个
6.1 同一块板子上的横向对比
拿一块Zynq-7020板子,AXI UART Lite挂在FCLK_CLK0上,地址0x42C00000,IP配置115200 8N1。三种方法的实测体验如下表:
| 场景 | minicom | screen | devmem |
|---|---|---|---|
| 驱动正常,日常收发 | 稳定,功能多,可存日志 | 简单直接,启动快 | 能收发,但只适合验证 |
| 驱动加载失败,无ttyUL0 | 不可用 | 不可用 | 可读寄存器,判断硬件和时钟 |
| 出现乱码 | 能看到乱码但不知根因 | 同样 | 读STAT_REG,看RX FIFO是否有数据,快速区分终端配置问题和硬件问题 |
| 需要自动化脚本测试 | 不够方便 | 不够方便 | 配合shell脚本很方便 |
| 看历史滚屏 | 关联缓冲,一般 | Ctrl+A Esc,比较方便 | 不支持 |
6.2 我的建议顺序
实际项目里我一般按这种方式组合:先解决“通不通”,再解决“好不好用”。
驱动和设备树阶段,优先用devmem做硬件体检,确认IP和时钟正常;应用层联调时用minicom做主力工具,因为它的配置、日志、上传下载都是全的;需要快速穿插验证或者临时看一眼输出时,用screen顶上也够。
如果你现在正遇到“minicom打开一片黑”或者“输出乱码”,我的建议是别急着在minicom里反复改参数,按这个顺序走一遍:devmem读STAT_REG确认硬件和时钟 → 确认IP波特率 → 配置minicom流控和波特率 → 短接回环测试。绝大多数问题都能在这一串操作里定位。
最后分享一个我自己吃亏换来的经验:UART Lite的调试,最忌讳的就是在应用层软件里反复试,却不去确认硬件和时钟。自从我把devmem操作当成“调试第一板斧”之后,UART Lite相关的问题排查速度快了很多。另外强烈建议项目一开始就把Vivado里的IP配置、地址分配、时钟频率这三组信息记录在板卡设计文档里,谁调试、谁接手,都能少走很多弯路。要是你手头也有一块调不通的UART Lite板子,试试按文章里的顺序走一遍,有结论了欢迎回来交流。