嵌入式调试里最让人抓狂的场景之一就是串口突然不听话了。你明明配置好了波特率、数据位、停止位线也插对了minicom或者串口助手打开一看——要么一片空白要么满屏乱码要么打印到一半戛然而止。更诡异的是换一个串口就好了偏偏某个特定串口怎么调都不对。这种情况在RK平台的开发板上尤其常见十有八九是FIQ串口和普通串口抢了同一路硬件资源。我自己在几个基于RK芯片的项目里反复踩过这个坑从最开始对着设备树一脸懵到后来能快速定位寄存器级的冲突中间交了不少学费。这篇就把FIQ串口与普通串口配置冲突这件事掰开揉碎讲清楚冲突到底怎么产生的、设备树里哪些字段是罪魁祸首、怎么用最少的改动把问题修掉以及调试过程中那些文档里不会写的实操细节。不管你是刚上手RK平台的新手还是被串口问题折磨过的老鸟应该都能从下面这些内容里找到能直接抄作业的东西。1. 先搞清楚FIQ串口和普通串口到底在争什么1.1 两种串口的本质区别与共存逻辑RK平台的串口控制器UART本身是通用外设同一颗芯片上通常挂着好几路UART比如UART0到UART5甚至更多。这些UART在硬件层面是平等的谁都可以被配置成普通串口使用也谁都可以被指定为FIQ串口。所谓FIQ串口指的是把某一路UART的中断映射到ARM处理器的FIQFast Interrupt Request中断上而不是普通的IRQ。FIQ的特点是响应优先级最高、中断延迟最小而且FIQ模式下有自己独立的寄存器组banked registers进入中断时不需要像IRQ那样保存大量上下文所以特别适合做高速、低延迟的串口数据搬运。普通串口走的是IRQ路径中断处理要经过通用中断控制器GIC的常规流程延迟相对大一些但对于常规的日志打印、命令交互完全够用。问题就出在这里FIQ串口和普通串口在软件配置上是两套逻辑但它们共享同一批硬件资源——同一组UART控制器、同一片引脚复用pinctrl配置、同一段寄存器地址空间。如果设备树里把某一路UART同时声明给了FIQ和普通串口两个消费者或者FIQ串口的配置没有正确地把对应UART从普通串口的管理范围里摘出去冲突就发生了。打个比方这就像一间办公室被两个部门同时登记使用FIQ部门拿着最高优先级的钥匙随时能进门普通串口部门以为房间是自己的也往里搬东西结果两边互相踩脚。表现出来就是普通串口打印异常、FIQ通道数据错乱严重的时候系统启动阶段就直接卡死在某一路串口的初始化上。1.2 冲突的典型症状从乱码到启动挂死我在实际项目里见过的冲突症状大致分几档严重程度递增。最轻的是普通串口能出数据但夹杂乱码尤其是高波特率下更明显因为FIQ中断频繁抢占普通串口的FIFO被反复打断数据流不连续。往上一档是普通串口完全无输出但用示波器或者逻辑分析仪抓TX引脚能看到波形说明硬件在发只是软件层的数据被FIQ路径截胡了。再严重一点是系统启动到某一路串口初始化时直接hang住串口一行日志都不出只能靠JTAG或者重新烧录来救。还有一种隐蔽的情况FIQ串口本身工作正常普通串口在低负载下也正常但一旦两边同时有大量数据收发普通串口就开始丢包。这种间歇性故障最难查因为单独测试每一路都好的只有并发时才暴露。我印象很深的一次某块板子上调试口用普通串口同时业务逻辑通过FIQ串口和另一个模块通信平时都正常压力测试跑十分钟必丢一包查了两天才定位到是两路配置指向了同一个UART实例的不同别名。提示遇到串口问题时先别急着怀疑线材和波特率。用cat /proc/interrupts看一眼中断分布如果某一路UART的中断计数异常增长或者干脆不增长基本就能锁定是中断映射层面的问题而不是物理层。1.3 为什么RK平台特别容易出这类冲突RK平台的BSP里FIQ串口的支持通常是通过一套独立的fiq_debugger机制实现的这套机制在设备树里有自己的节点和标准串口节点是分开描述的。标准串口走的是serialxxxx这类节点由8250或者dw-apb-uart驱动接管FIQ调试串口则往往有一个类似fiq-debugger的节点里面用rockchip,serial-id之类的属性指定它要用哪一路UART。两套节点各自独立解析内核在启动时如果没做好互斥就会出现同一路UART被两个驱动同时probe的情况。更麻烦的是很多参考设计里FIQ调试串口默认就占用了UART2具体编号因芯片而异而开发者拿到板子后往往习惯性地把普通调试口也配在UART2上因为这是最顺手的一路。两边的配置一叠加冲突几乎是必然的。这也是为什么这个问题的搜索热度一直不低——它不是偶发bug而是配置习惯和默认值叠加出来的系统性陷阱。2. 设备树里的冲突源头逐项拆解2.1 定位FIQ调试串口节点及其关键属性排查的第一步永远是先看清楚现状。在RK平台的设备树里FIQ调试串口的配置一般出现在chosen节点或者一个独立的fiq-debugger节点下。典型的写法长这样fiq_debugger: fiq-debugger { compatible rockchip,fiq-debugger; rockchip,serial-id 2; rockchip,wake-irq 0; rockchip,baudrate 1500000; pinctrl-names default; pinctrl-0 uart2m0_xfer; status okay; };这里每个属性都有讲究。rockchip,serial-id 2明确告诉FIQ调试器去接管UART2这个数字就是冲突排查的第一嫌疑对象。rockchip,baudrate设的是FIQ通道自己的工作波特率注意它和普通串口节点里的波特率是两套互不影响但如果两路指向同一个UART实际生效的会是先完成初始化的那一路后初始化的要么失败要么把前一路覆盖掉。pinctrl-0引用的引脚复用配置也必须和普通串口节点里的一致否则会出现引脚被两套配置反复切换的情况波形上表现为TX电平抖动。我建议排查时先把整个设备树里所有引用到目标UART编号的地方都搜一遍。用grep -rn serial-id arch/arm64/boot/dts/rockchip/加上grep -rn uart2 arch/arm64/boot/dts/rockchip/两条命令交叉比对基本能把所有相关节点捞出来。很多时候冲突不是显式写错而是某个include进来的dtsi里悄悄配了一路主dts又配了一路编译时不报错运行时才打架。2.2 普通串口节点的status与别名陷阱普通串口节点通常是这样的uart2 { status okay; pinctrl-names default; pinctrl-0 uart2m0_xfer; };看起来人畜无害但如果上面的FIQ节点也指向了serial-id 2这两个节点就撞车了。内核启动时fiq-debugger驱动会先尝试接管UART2把它从标准串口驱动的手里抢过来而标准串口驱动在probe时如果发现这个UART已经被占用行为取决于具体内核版本的实现——有的版本会直接返回错误导致该串口不可用有的版本会强行初始化覆盖掉FIQ的配置结果就是FIQ通道失效。还有一个容易被忽略的点是aliases节点。有些板级dts里会给串口起别名比如serial2 uart2;然后用户空间通过/dev/ttyS2去访问。如果FIQ占用了UART2这个/dev/ttyS2要么打不开要么打开后读写异常。排查时用ls -l /dev/ttyS*看清楚设备节点和实际硬件路的对应关系别想当然地认为ttyS2就是UART2——不同内核配置下这个映射可能错位。注意修改设备树前一定先备份原始dts和编译出来的dtb。我吃过一次亏改完没留底结果新配置引入别的问题想回退发现原始文件被覆盖了只能从版本库里翻半天。2.3 引脚复用pinctrl的隐性冲突引脚复用是比节点冲突更隐蔽的一层。假设FIQ用UART2的m0复用组普通串口节点却引用了UART2的m1复用组两个节点都status okay编译也能过但运行时引脚会被两套pinctrl配置来回拉扯。表现是串口时好时坏或者干脆两路都不工作。RK平台的pinctrl配置里同一路UART的不同复用组m0、m1、m2对应不同的物理引脚。冲突排查时要把FIQ节点和普通串口节点的pinctrl-0拿出来对比确认它们引用的是同一个phandle。如果不是要么统一成同一个要么干脆把其中一路的status改成disabled。我见过一个案例硬件设计上UART2只引出了m0组的引脚但软件里普通串口配了m1FIQ配了m0结果FIQ能用、普通串口TX有波形RX没反应——因为m1的RX引脚在板子上根本没连出来。用cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins可以查看当前引脚的复用状态确认目标引脚到底被哪个设备占着。这个debugfs节点在排查引脚级冲突时非常好用比翻设备树直观得多。3. 一步步把冲突修掉从定位到验证的完整流程3.1 冲突定位的实操命令组合真到了板子上光看设备树还不够得结合运行时信息交叉验证。我通常按这个顺序走一遍第一步确认FIQ调试串口当前占的是哪一路。看内核启动日志里fiq-debugger相关的打印或者直接dmesg | grep -i fiq。如果FIQ初始化成功日志里会有类似fiq_debugger: fiq debugger on uart2这样的信息直接告诉你答案。第二步确认普通串口驱动接管了哪些UART。dmesg | grep -i ttyS能看到每个ttyS设备的注册情况包括它对应的物理地址。把物理地址和芯片手册里的UART基地址对一下就知道哪一路被普通驱动拿走了。第三步看中断。cat /proc/interrupts里找UART相关的中断行FIQ串口的中断通常不会出现在这个表里因为走的是FIQ不是IRQ如果某一路UART既在FIQ日志里出现、又在/proc/interrupts里有IRQ计数那基本可以确认是双重占用。第四步验证设备节点。ls -l /dev/ttyS*配合cat /proc/tty/driver/serial如果内核开了这个选项能看到每个串口的硬件地址和中断号和前面几步的信息对得上就说明定位准确了。这套组合拳打下来冲突点基本无所遁形。我一般会把这几步的输出存成一个文本文件改配置前后各跑一次做对比这样能清楚看到改动到底生效在哪。3.2 修改方案一把FIQ串口挪到独立的一路UART最干净的修法是让FIQ和普通串口各用各的UART物理上就不重叠。具体做法是把FIQ节点的rockchip,serial-id改成一个普通串口没在用的编号同时把对应的pinctrl引用改成那一路UART的复用配置。比如原来FIQ占UART2、普通调试口也用UART2而板子上UART4是空闲的那就把FIQ改成serial-id 4pinctrl换成uart4对应的复用组。改完编译dtb、烧录、重启用前面的定位命令确认FIQ日志里显示的是uart4普通串口那边UART2正常注册两边互不干扰。这个方案的前提是硬件上确实有富余的UART且引脚引出来了。如果板子设计得比较紧凑所有UART都名花有主那就得走方案二。改的时候注意rockchip,serial-id的数字和pinctrl引用必须匹配同一路UART改了一个忘了另一个是新手常犯的错结果就是FIQ指向UART4但引脚还配着UART2的复用串口直接哑掉。3.3 修改方案二禁用普通串口对冲突UART的占用如果硬件上没有富余UART那就只能二选一。通常FIQ调试串口在量产固件里是要保留的用于低延迟调试和崩溃现场抓取所以更常见的做法是把普通串口节点里对冲突UART的引用去掉也就是把那个uart2节点的status改成disabled或者干脆删掉这个节点。改完之后用户空间就看不到对应的/dev/ttySx了所有对那一路串口的访问都走FIQ通道。这里有个细节要注意如果应用层代码里硬编码了打开某个ttyS设备改完配置后这个打开操作会失败得同步调整应用层的设备路径。我在一个项目里就遇到过驱动层改好了结果上层日志模块还在往被禁用的ttyS上写程序启动就报错查了半天才发现是应用配置没跟着改。还有一种折中做法保留普通串口节点但把它的pinctrl去掉只留status okay。这样驱动能probe成功、设备节点也存在但引脚没配置实际收发不了。这种做法我不太推荐因为它制造了一个看起来能用实际不能用的假象后续维护的人容易被误导。要么彻底禁用要么彻底让出来别留半吊子状态。3.4 改完之后的验证清单配置改完不等于问题解决必须有一套验证流程。我自己的验证清单是这样的验证项操作方法预期结果FIQ通道可用触发一次内核崩溃或看启动日志FIQ串口有完整输出无乱码普通串口可用通过该串口登录或收发数据数据完整高波特率下不丢包中断无重叠cat /proc/interrupts冲突UART不出现在IRQ表中若归FIQ引脚状态正确查pinctrl debugfs目标引脚只被一个设备占用并发压力两路同时高速收发持续运行无丢包、无卡死这套清单跑一遍大概十几分钟但能避免很多改完当时好、跑一会儿又坏的返工。尤其是并发压力那一项别省间歇性故障往往就是并发时才暴露。4. 踩坑实录那些文档里不会写的经验4.1 波特率不一致导致的假冲突有一次排查一个串口乱码问题按冲突的思路查了半天设备树最后发现根本不是资源冲突而是FIQ节点里的rockchip,baudrate和普通串口节点里的波特率设得不一样而两路又恰好指向同一个UART。FIQ先初始化把波特率设成1500000普通串口后初始化又改成115200结果两边都按自己以为的波特率收发数据自然对不上。这个案例给我的教训是排查冲突前先确认两路的波特率配置是否一致。如果确实需要两路共存于同一个UART虽然不推荐至少把波特率统一。更稳妥的做法还是物理上分开别让两套逻辑共享一个硬件实例。判断方法很简单看FIQ日志里打印的实际波特率和普通串口stty查到的波特率是否一致不一致就是这个问题。4.2 内核版本差异带来的行为变化同一个设备树配置在不同内核版本上表现可能完全不同。早期版本的内核里fiq-debugger驱动对UART的抢占比较温和如果标准串口驱动先probe了FIQ可能就静默失败新版本内核里抢占逻辑更激进FIQ优先标准串口驱动probe失败。这意味着你在A版本上调好的配置升级内核后可能又出问题。我的应对办法是把FIQ和普通串口的配置在设备树里做成互斥的宏或者条件编译针对不同内核版本走不同分支。虽然麻烦一点但比每次升级都重新排查一遍强。另外升级内核后一定要重跑前面的验证清单别假设配置没动就没问题。4.3 启动早期日志丢失的排查技巧冲突严重的时候系统可能在串口初始化阶段就挂了一行日志都看不到这时候常规的dmesg手段全失效。我的做法是接JTAG或者用芯片的早期调试接口在更早的阶段抓输出。如果这些都没有可以尝试把FIQ调试串口的初始化提前让它在标准串口驱动之前完成这样至少能看到FIQ通道的输出。还有一个土办法在bootloader阶段就把目标UART的引脚状态和寄存器值打印出来和内核启动后的状态对比能看出是哪一步把配置改掉了。这个方法比较费劲但对付那种内核一启动串口就死的疑难杂症很有效。我一般会先在U-Boot里加几行打印确认硬件初始状态正常再往内核里查。4.4 常见问题速查表现象可能原因排查动作普通串口无输出但TX有波形FIQ抢占了同一UART查FIQ serial-id与普通串口是否重叠高波特率下丢包两路中断互相抢占看/proc/interrupts中断计数启动卡在串口初始化两驱动同时probe同一UART查dmesg中probe顺序与错误码串口时好时坏pinctrl复用组不一致查pinctrl debugfs引脚占用换内核后问题复现驱动抢占逻辑变化对比新旧内核fiq-debugger实现这张表我贴在工位上过排查时对着走一遍能省不少时间。当然表是死的实际情况可能更复杂但至少能帮你快速排除掉最常见的那几类。5. 从根上避免冲突配置规范与工程习惯5.1 建立串口资源分配表冲突反复出现的根本原因是串口资源没有一份权威的分配记录。我的做法是在项目初期就维护一张串口分配表写清楚每一路UART的用途FIQ调试、普通调试、业务通信、预留、对应的引脚复用组、波特率、以及归属的软件模块。这张表放在项目文档里任何人改串口配置前先查表、改完更新表。听起来很形式主义但实际效果很好。我参与过的一个项目前期没做这个三个人各自改设备树最后串口配置乱成一锅粥光理清楚就花了两天。后来补了分配表类似的冲突再没出现过。表格不用复杂几列就够关键是有人维护、改配置必须同步更新。5.2 设备树改动的review要点串口相关的设备树改动review时重点看三样serial-id和pinctrl是否匹配、status是否和分配表一致、有没有引入新的别名冲突。这三样看完大部分低级错误都能拦住。另外建议把FIQ节点和普通串口节点放在同一个dtsi文件里相邻位置改的时候一眼能看到彼此减少改了一个忘了另一个的概率。还有个小技巧在设备树里给每个串口节点加注释写明用途和负责人。编译不影响但后来的人看代码时能快速理解意图。我现在的习惯是每个uartX节点上面都有一行注释比如/* UART2: FIQ debug, owner: bringup team */团队里其他人接手时省了很多沟通成本。5.3 自动化检查脚本的思路人肉review总会漏能自动化的就别靠自觉。我写过一个简单的脚本解析编译出来的dtb提取所有UART相关节点的serial-id、pinctrl引用和status检查有没有两路同时okay却指向同一个UART的情况有就报错。脚本不长用dtc反编译成dts再grep加awk就能实现集成到CI里每次提交自动跑。这个脚本帮我拦下过好几次手误。有一次改一个不相关的节点顺手把某个uart的status从disabled改成了okay自己都没注意CI直接报冲突省了一次线上排查。脚本的逻辑不复杂核心就是同一路UART不允许被两个消费者同时声明为可用把这条规则固化下来比任何文档都管用。6. 几个容易被忽略的边界情况6.1 多核启动时的竞争条件RK平台的多核芯片上FIQ通常绑定在某个特定核心上比如CPU0而普通串口的中断可能被分发到任意核心。如果FIQ和普通串口指向同一个UART在多核启动的早期阶段两个核心可能同时去初始化这个UART产生竞争。这种竞争导致的故障很随机有时候能启动有时候不能日志也看不出明显规律。排查这类问题可以在启动参数里临时把系统限制到单核比如maxcpus1如果单核下问题消失基本就是多核竞争。解决思路还是回到源头——别让两路共享同一个UART。如果实在要共享至少保证初始化顺序确定比如在FIQ驱动里加个标志位等FIQ初始化完成后再放行普通串口的probe。6.2 低功耗唤醒场景下的串口状态带低功耗功能的板子上系统休眠时串口状态会被保存和恢复。如果FIQ串口被配置为唤醒源rockchip,wake-irq相关配置而普通串口也管着同一路UART唤醒恢复时两套状态恢复逻辑会打架表现是唤醒后串口失灵或者数据错乱。这个场景比较小众但做低功耗产品的话迟早会遇到。处理办法是在休眠前明确哪一路负责唤醒把另一路的status临时禁用唤醒后再恢复。或者干脆让FIQ和普通串口用不同的UART从设计上避开这个问题。低功耗相关的串口问题往往和电源管理框架耦合排查时除了看串口本身还要看pm相关的日志。6.3 调试器连接对串口的影响用JTAG调试器连接板子时某些调试器会占用或影响串口的引脚状态尤其是当调试器和串口共用引脚或者调试器本身带串口透传功能时。这种情况下串口异常不一定是配置冲突可能是调试器干扰。判断方法是断开调试器单独跑一遍如果问题消失就是调试器的影响。这个坑我在早期项目里踩过当时死活查不出配置问题最后发现是调试器的地线没接好导致串口电平异常。听起来很蠢但实际排查时很容易忽略物理层的因素一头扎进软件配置里出不来。所以我的建议是软件排查之前先用最简单的回环测试确认硬件链路本身没问题。7. 写在最后的几句实在话串口冲突这类问题说到底考验的是对资源归属这件事的敏感度。RK平台的FIQ机制本身设计得没问题问题出在配置的分散性和默认值的惯性上——FIQ节点在一处、普通串口节点在另一处、pinctrl又在第三处三处信息对不上就出事。我现在的习惯是拿到任何一块新板子先把所有串口相关的设备树节点、引脚复用、中断映射梳理成一张表确认没有重叠再开始写业务代码这个前置动作花不了半小时但能省掉后面可能几天的排查。另外一点体会是遇到串口问题时别急着改配置先花十分钟把现象记录清楚哪一路、什么波特率、什么负载下出问题、dmesg和/proc/interrupts长什么样。这些信息记录下来哪怕自己一时查不出拿去请教别人或者对比正常板子时也是关键线索。我见过太多人一上来就乱改设备树改到最后连原始配置是什么都忘了反而把简单问题复杂化。最后分享一个我常用的快速判断法如果一路串口单独测试完全正常两路一起用就出问题那几乎可以肯定是资源冲突而不是配置错误如果一路单独测试就不正常那先查这一路自己的配置别往冲突上想。这个二分法能帮你快速缩小排查范围少走弯路。
阅读完成 · 觉得有帮助?