首页 / 资讯中心 / 文章详情

RK平台FIQ串口与普通串口冲突:从乱码定位到修复的完整指南

RK平台FIQ串口与普通串口冲突:从乱码定位到修复的完整指南 ★ FEATURED ARTICLE
1. 串口打印乱成一锅粥问题到底出在哪嵌入式调试里最让人抓狂的场景之一就是系统跑起来之后串口终端突然开始刷乱码、丢字符甚至两个串口互相抢着输出日志像被搅碎的纸片一样完全没法读。这次要聊的就是RK平台上FIQ串口和普通串口配置冲突导致打印混乱的问题从现象定位到根因分析再到最终修复完整走一遍。如果你正在用RK系列芯片做底层开发尤其是涉及调试串口重映射、多串口共存、FIQ中断调试的场景这篇内容应该能帮你少走不少弯路。先说清楚这个问题的本质RK平台的串口控制器支持FIQFast Interrupt Request模式这个模式原本是为了让串口在中断上下文中也能快速响应、低延迟输出而设计的。但FIQ串口的寄存器配置和普通串口是共享同一套硬件资源的一旦设备树里两个节点指向了同一个物理串口或者FIQ模式的配置没有正确隔离就会出现两个逻辑串口争抢同一个物理外设的局面。表现出来就是打印内容交错、波特率漂移、甚至系统直接卡死在串口中断里。这个问题的受众其实很明确做RK平台BSP开发的工程师、调试内核早期打印的底层开发者、以及需要在生产环境中同时使用调试串口和业务串口的嵌入式团队。哪怕你刚接触设备树配置只要跟着排查思路走一遍也能理解冲突的来龙去脉。下面我按实际排查的顺序把整个分析和修复过程拆开讲。2. 先搞清楚FIQ串口和普通串口的本质区别2.1 两种模式在硬件层面的分工RK平台的串口模块在硬件上是一个完整的UART控制器但软件层面可以通过不同的驱动路径去访问它。普通串口走的是标准的8250/16550兼容驱动框架注册到tty子系统用户态通过/dev/ttyS*访问中断走的是常规IRQ通道。而FIQ串口则是把UART的中断信号路由到FIQ通道上FIQ在ARM架构里优先级高于普通IRQ且FIQ模式下有独立的寄存器组banked registers不需要像IRQ那样先保存大量上下文所以响应延迟极低。这个设计初衷是好的内核早期启动阶段常规中断子系统还没初始化完但你又需要看打印这时候FIQ串口就能顶上。问题在于FIQ通道在ARM上通常只有一个而且FIQ处理程序一旦注册它会独占这个通道。如果设备树里同时配置了FIQ串口节点和普通串口节点指向同一个UART控制器两个驱动都会去操作同一个控制器的寄存器——一个在FIQ上下文里写THR寄存器发数据另一个在普通中断里也在写寄存器状态就彻底乱了。我见过最典型的现象是系统启动前期打印正常等到普通串口驱动probe完成、tty注册之后打印开始出现规律性的乱码每隔几个字符就插入一段无关内容。这就是两个驱动在交替往同一个FIFO里塞数据接收端自然解析不出来。2.2 设备树里的隐形重叠很多人排查时第一反应是看驱动代码但其实根因往往藏在设备树里。RK平台的设备树中串口节点通常长这样uart2: serialff1a0000 { compatible rockchip,rk3568-uart, snps,dw-apb-uart; reg 0x0 0xff1a0000 0x0 0x100; interrupts GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH; clocks cru SCLK_UART2, cru PCLK_UART2; status okay; };而FIQ串口的配置可能通过chosen节点或者单独的fiq-debugger节点来指定chosen { bootargs earlyconuart8250,mmio32,0xff1a0000 fiq_debugger; };注意这里的0xff1a0000——如果它和上面uart2的reg地址相同那就是同一个物理串口被两套机制同时占用了。earlycon在早期启动时用这个地址输出fiq_debugger也用它等普通串口驱动起来后又注册了ttyS2三套路径叠在一起不乱才怪。提示排查第一步永远是核对设备树里所有涉及串口地址的节点把reg属性、bootargs里的mmio地址、fiq-debugger配置的地址列成一张表看有没有重复。2.3 为什么FIQ模式更容易抢赢从优先级上说FIQ高于IRQ所以当两个驱动同时想发数据时FIQ路径的写入会打断普通中断的处理流程。更麻烦的是FIQ处理程序通常不经过完整的中断框架它直接操作寄存器不会去拿普通串口驱动持有的锁。这就导致普通驱动以为自己在独占访问实际上FIFO已经被FIQ那边改过了。这种无锁竞争在单核上表现为打印交错在多核上可能直接触发数据 abort。我在一次实际调试中遇到过这样的情况普通串口驱动在serial8250_console_write里关中断写FIFO但FIQ不受这个关中断影响FIQ有独立的屏蔽位结果FIFO的写指针被两边同时推进接收端收到的字节流里混入了FIQ调试器的心跳字符。定位这个问题花了大半天最后是靠抓UART控制器的FIFO状态寄存器才确认的。3. 冲突排查的完整实操路径3.1 从现象反推先确认是不是双写拿到一个打印混乱的板子别急着改代码。先做三件事第一把串口线接到逻辑分析仪或者用另一台机器抓原始字节流看乱码是规律性的还是随机的第二在内核启动参数里临时去掉fiq_debugger只保留普通串口看打印是否恢复正常第三如果去掉FIQ后正常那基本可以锁定是双写冲突。我习惯用一张表来记录不同配置组合下的现象这样能快速缩小范围配置组合早期打印内核启动后打印结论指向仅earlycon正常无输出普通驱动未接管仅fiq_debugger正常正常但无法交互FIQ独占earlycon 普通串口正常乱码地址重叠fiq_debugger 普通串口正常严重乱码/卡死FIQ与IRQ竞争三者全开正常完全不可读多重冲突这张表是我踩过几次坑之后总结的实际排查时按行去验证基本两三轮就能定位到冲突层级。3.2 核对设备树与bootargs的地址一致性确认是双写之后下一步是把所有可能碰这个串口的配置源找出来。RK平台上主要有四个地方需要检查设备树的uart节点reg属性、chosen节点里的bootargs、fiq-debugger节点的配置、以及可能存在的stdout-path属性。# 从编译出的dtb反查串口相关配置 fdtdump board.dtb | grep -A5 -i uart\|fiq\|stdout\|earlycon这一步的关键是确认物理地址。比如uart2的reg是0xff1a0000那bootargs里的earlycon地址、fiq_debugger的地址都必须是这个值或者明确指向不同的串口。如果发现fiq_debugger配的是uart2的地址而业务又需要uart2做普通打印那就必须做取舍或者重映射。注意有些板子的fiq-debugger配置藏在rockchip,fiq-debugger节点里不在chosen中用grep搜fiq关键字能快速找到。3.3 用寄存器状态确认竞争现场如果地址核对下来没有明显重叠但现象依然存在那就需要看运行时状态了。UART控制器的FIFO状态寄存器偏移0x00附近的USR寄存器能反映FIFO的填充情况。在冲突发生时你会看到FIFO的读写指针异常跳动或者FIFO里残留着不属于当前输出流的数据。我通常会在普通串口驱动的serial8250_console_write入口和FIQ处理程序入口各加一个GPIO翻转用示波器看两个信号的时序关系。如果发现FIQ的翻转出现在普通驱动关中断-写FIFO-开中断这个临界区内部那就实锤了——FIQ绕过了普通驱动的临界区保护。这个方法的成本略高但比盲改代码高效得多。没有示波器的话退而求其次可以在两个入口各打一个时间戳到内存buffer事后dump出来对比也能看出交错关系。4. 修复方案的设计与落地4.1 方案选型隔离还是复用定位到冲突之后修复思路其实就两条路要么把两个逻辑串口彻底隔离到不同的物理串口上要么让它们共享同一个物理串口但做严格的互斥。前者简单粗暴但受限于硬件串口数量后者灵活但实现复杂度高。我的建议是优先做物理隔离。RK平台一般有多个UART控制器把FIQ调试串口挪到一个独立的UART上比如用uart0做FIQuart2做业务打印设备树里改一下地址和引脚复用就行风险最低。只有在串口资源实在紧张、必须复用时才考虑软件互斥方案。物理隔离的具体操作是在设备树里确认fiq-debugger节点指向的地址与业务串口不同同时检查pinctrl配置确保两个串口的TX/RX引脚没有复用冲突。RK的引脚复用表里同一个物理引脚可能对应多个UART功能改地址之后一定要同步改pinctrl。/* FIQ调试串口独占uart0 */ fiq_debugger: fiq-debugger { rockchip,serial-id 0; rockchip,wake-irq 0; rockchip,baudrate 1500000; pinctrl-names default; pinctrl-0 uart0_xfer; status okay; }; /* 业务打印走uart2与FIQ隔离 */ uart2 { status okay; pinctrl-names default; pinctrl-0 uart2m0_xfer; };4.2 软件互斥方案的实现要点如果确实需要复用同一个物理串口那就得在驱动层做互斥。核心思路是让FIQ处理程序在写FIFO之前检查一个原子标志位普通驱动在进入临界区时置位该标志退出时清除。FIQ看到标志置位就丢弃本次输出或者缓存到环形buffer里延后发送。这里有个坑FIQ处理程序里不能调用任何可能睡眠的函数也不能用普通的spinlock因为FIQ不参与普通锁的竞争逻辑。可行的做法是用一个atomic_t或者直接操作一个volatile变量配合内存屏障保证可见性。/* FIQ侧写之前检查普通驱动是否在临界区 */ static volatile int uart_busy; static void fiq_uart_putc(char c) { if (uart_busy) return; /* 丢弃或缓存避免竞争 */ while (!(readl(uart_base UART_USR) UART_USR_TX_FIFO_NOT_FULL)) ; writel(c, uart_base UART_THR); } /* 普通驱动侧临界区置位 */ static void serial8250_console_write(...) { uart_busy 1; smp_wmb(); /* 原有写FIFO逻辑 */ ... smp_wmb(); uart_busy 0; }这个方案能解决大部分交错问题但代价是FIQ输出在冲突窗口内会被丢弃调试信息可能不完整。所以它只适合业务打印为主、FIQ仅做兜底的场景反过来就不合适了。4.3 波特率与时钟配置的一致性校验还有一个容易被忽略的冲突点FIQ串口和普通串口如果对同一个UART设置了不同的波特率分频寄存器会被反复改写导致实际波特率在两个值之间跳变接收端解析出来的就是乱码。这种情况在日志里表现为前半段正常、后半段乱码或者每隔固定时间乱一次。修复方法是在设备树和bootargs里统一波特率。RK平台的fiq-debugger节点有rockchip,baudrate属性bootargs里的earlycon可以带,115200n8这样的参数普通串口的tty配置也要对齐。三处必须一致否则分频寄存器就会被三方拉扯。# bootargs中显式指定波特率与fiq-debugger和tty配置对齐 bootargs earlyconuart8250,mmio32,0xff1a0000,115200n8 consolettyS2,115200n8;我实测下来波特率不一致导致的乱码比双写冲突更隐蔽因为它的现象是间歇性的容易误判成硬件干扰。排查时用示波器量一下TX线上的位宽如果位宽在两种值之间跳变那就是分频寄存器被改了。5. 实操心得与常见问题速查5.1 几个反直觉的排查经验第一个经验先怀疑设备树再怀疑驱动。我早期排查这类问题时总喜欢从驱动代码入手结果绕了一大圈才发现是设备树里两个节点地址写重了。设备树是配置的源头源头错了驱动怎么改都是治标。第二个经验FIQ串口的日志不要和普通串口混在一个终端看。如果两个串口物理上接到同一个USB转串口模块的不同通道有些模块的内部buffer会串数据让你误以为是软件冲突。排查阶段尽量用两个独立的接收端。第三个经验注意status okay的继承关系。RK的设备树里有些uart节点在soc级dtsi里默认是disabled板级dts里再enable。如果板级dts里enable了uart2但soc级dtsi里uart2的reg地址和fiq-debugger默认地址相同冲突就悄悄发生了。改的时候要顺着包含关系往上查。5.2 常见问题速查表现象可能原因排查动作修复方向启动后打印规律性乱码双驱动写同一FIFO去掉fiq_debugger对比物理隔离或加互斥打印间歇性乱码波特率被反复改写示波器量TX位宽统一三处波特率配置系统卡死在串口中断FIQ与IRQ死锁看中断计数和调用栈检查FIQ处理程序是否拿锁早期打印正常、后期无输出普通驱动接管后地址错核对ttyS编号与reg修正设备树地址映射多核下偶发数据abort无锁竞争导致总线异常抓异常地址加内存屏障和原子标志5.3 修复后的验证方法改完之后不能只看打印正常了就收工。我一般会做三轮验证第一轮连续跑24小时压力测试让业务串口持续输出日志同时FIQ调试器周期性打印心跳看有没有偶发乱码第二轮人为在普通串口输出高峰期触发FIQ中断比如通过调试命令观察是否出现字符丢失第三轮用逻辑分析仪抓一段完整的字节流逐字节比对发送端和接收端确认没有插入或丢失。只有三轮都过了才算真正修复。我见过太多改完看着好了、量产又出问题的案例根因就是验证不充分冲突在低负载下不显现高负载才暴露。6. 从这次排查延伸出的配置管理建议这类冲突的本质是同一个硬件资源被多个配置源描述而配置源分散在设备树、bootargs、驱动默认值三个地方缺乏统一的校验机制。我在后续的项目里养成了一个习惯在板级dts里加一段注释把所有串口的物理地址、用途、占用者列清楚改任何一处之前先看这张地图。/* * UART资源分配表 * uart0 (0xff180000) - FIQ调试串口独占 * uart2 (0xff1a0000) - 业务打印consolettyS2 * uart4 (0xff1c0000) - 预留未启用 */另外编译流程里可以加一个脚本自动提取dtb里所有uart节点的reg地址和bootargs里的mmio地址做重复性检查有重叠就报错。这个脚本不复杂几十行Python就能搞定但能在编译阶段就拦住大部分配置冲突比运行时排查省事得多。# 简化的地址冲突检查思路 import re, subprocess dtb_dump subprocess.check_output([fdtdump, board.dtb]).decode() uart_addrs re.findall(rserial([0-9a-f]), dtb_dump) bootargs re.search(rbootargs\s*\s*([^]), dtb_dump) mmio_addrs re.findall(rmmio32,([0-9a-fx]), bootargs.group(1)) if bootargs else [] overlap set(uart_addrs) set(a.replace(0x, ) for a in mmio_addrs) if overlap: print(f警告以下地址存在串口配置重叠 - {overlap})这个检查逻辑很粗糙但对付常见的地址写重已经够用了。实际项目里可以根据自己的设备树结构细化匹配规则。最后分享一个我在实际调试中的体会串口冲突这类问题现象吓人但根因往往很简单难的是在众多配置源里快速定位到那一个写错的地址。与其在驱动里加各种防御性代码不如把配置源头管好。设备树是嵌入式系统的宪法宪法写矛盾了下面怎么执行都是乱的。把资源分配表维护好、把编译期检查加上这类问题基本就能绝迹。
阅读完成 · 觉得有帮助?
咨询建站