1. 项目概述:从调试口到通用串口的价值重塑
在嵌入式Android开发,尤其是基于瑞芯微(Rockchip)平台进行产品原型开发或设备维护时,我们经常会遇到一个矛盾:设备上宝贵的硬件串口(UART)资源往往被系统默认的调试串口(如/dev/ttyFIQ0)所占用。这个调试串口在开发阶段至关重要,是查看内核启动日志(printk)、系统日志(logcat)乃至进入loader或maskrom模式的救命通道。然而,当产品进入量产或需要连接多个外设(如GPS模块、扫码枪、工业传感器、打印机等)时,每一个物理串口都显得弥足珍贵。此时,如果能把默认占用的调试串口“解放”出来,配置为一个普通的、可供应用层读写的数据串口,无疑能极大提升硬件资源的利用率和系统集成的灵活性。
这个操作的核心,远不止是修改一个设备树(DTS)节点那么简单。它涉及到对瑞芯微平台启动流程、内核串口驱动框架、以及Android系统服务层的深入理解。盲目操作可能导致系统无法启动、日志丢失难以调试,甚至损坏bootloader。因此,我们需要一套清晰、安全、可回滚的方案。本文将基于RK3566、RK3588等主流芯片,详细拆解将调试串口(通常是UART2)转换为普通串口的完整流程、底层原理、实操要点以及避坑指南。无论你是正在调试设备的嵌入式工程师,还是负责产品定型的系统架构师,这套方法都能帮助你更从容地规划硬件资源。
2. 核心原理与方案选型:为什么可以以及如何安全地做
2.1 瑞芯微平台串口与调试口设计解析
要安全地修改,必须先理解其默认的工作机制。在瑞芯微的参考设计中,通常会指定一个特定的UART作为“调试串口”或“FIQ串口”。以常见的配置为例:
- 硬件映射:UART2(对应芯片引脚
GPIO1_C0(TX),GPIO1_C1(RX))常被用作调试口。 - 内核驱动:在Linux内核中,这个串口被
8250_dw或serial8250等驱动接管,但其设备节点(如/dev/ttyFIQ0或/dev/ttyS2)被赋予了特殊属性。 - 系统角色:
- Bootloader阶段:U-Boot或Rockchip自己的
miniloader会初始化该串口,用于输出启动信息和接收中断命令(如进入Ctrl+C进入下载模式)。 - 内核早期阶段:内核在解压和初始化核心设备时,会将该串口注册为
console(控制台)和earlycon(早期控制台),所有printk信息都输出至此。 - Android系统阶段:
init进程会启动console服务,关联到这个串口设备。同时,logd(日志守护进程)也可能配置为从该串口抓取内核日志。
- Bootloader阶段:U-Boot或Rockchip自己的
转换的本质,就是逐步剥离这个串口在上述各个阶段的“特殊身份”,将其降级为一个普通的、由tty驱动管理的字符设备,最终在/dev/下生成一个类似/dev/ttyS2的节点,供上层应用通过标准open()、read()、write()接口访问。
2.2 三种配置方案的权衡与选型
根据产品阶段和风险承受能力,主要有三种方案:
| 方案 | 操作层级 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 方案A:仅禁用内核控制台 | 内核启动参数 | 改动最小,最安全,可快速验证。 | 仅移除console参数,Bootloader和早期内核日志仍占用,不是完全释放。应用层可能仍无法正常打开(因驱动仍被占用)。 | 初步测试,风险厌恶的初期评估。 |
| 方案B:修改内核设备树 | 内核源码/设备树 | 彻底在驱动层面解除关联,释放串口资源。是标准、彻底的方案。 | 需要编译内核,有一定操作门槛。修改错误可能导致无法启动。 | 产品定型阶段,需要稳定、完全释放串口。 |
| 方案C:修改Bootloader | U-Boot源码 | 从最底层释放,可完全自定义Bootloader输出。 | 风险最高,编译复杂,一旦出错设备可能“变砖”。 | 极度定制化需求,如需要改变Bootloader的调试端口。 |
对于绝大多数应用场景,我们推荐并详细展开方案B(修改设备树)。它是功能完整性、操作可行性和风险可控性的最佳平衡点。方案A可作为临时测试,方案C则仅建议在非常熟悉Bootloader且有必要时进行。
注意:在进行任何修改前,务必确保你有另一个可用的调试手段,例如:
- 另一个可用的串口(如果板子有)。
- ADB(Android Debug Bridge)网络调试已开启且稳定。
- 屏幕输出(如果系统支持
framebuffer console)。 这是你的“安全绳”,一旦修改导致主调试口失效,你可以通过这些备用方式登录系统进行恢复。
3. 实操全流程:基于设备树修改的详细步骤
我们以将RK3568平台的UART2从调试口改为普通串口为例,假设你的SDK源码目录为/home/project/rk3568_android_sdk。
3.1 环境准备与源码定位
首先,进入内核源码目录,并找到对应板级的设备树文件。
cd /home/project/rk3568_android_sdk/kernel # 查找你的板级设备树文件,通常位于 arch/arm64/boot/dts/rockchip/ 下 # 文件名可能类似 rk3568-evb.dtsi, rk3568-xxx.dts, 或 rk3566-xxx.dts find . -name "*.dts" | grep rk3568 | head -20假设你的设备树文件是rk3568-evb1-ddr4-v10.dts。用文本编辑器(如vim或vscode)打开它,同时打开其包含的通用头文件(通常是rk3568-evb.dtsi),因为串口配置可能在那里。
3.2 关键代码修改:注释或删除调试口关联
你需要修改两个关键部分:
1. 修改chosen节点中的stdout-path在设备树中,chosen节点用于传递由固件(Bootloader)到内核的运行时参数。stdout-path属性指定了默认的控制台。
// 在 .dts 或 .dtsi 文件中找到类似段落 chosen { // 将这一行注释掉或删除,或者将其指向另一个串口,如 uart0 // stdout-path = "serial2:1500000n8"; bootargs = "...其他启动参数..."; };将stdout-path属性注释掉,意味着内核启动时不再自动将serial2(即UART2)注册为默认控制台。
2. 修改 具体UART节点 的status和dma属性接下来,找到UART2的设备节点定义。它可能长这样:
&uart2 { status = "okay"; dmas = <&dmac0 4>, <&dmac0 5>; dma-names = "tx", "rx"; pinctrl-names = "default"; pinctrl-0 = <&uart2m0_xfer>; };你需要确保它的status是“okay”(启用),并且移除或注释掉任何将其标记为控制台的属性。在某些板级文件中,可能会看到这样的代码:
// 错误的示例:这会将uart2设置为控制台 &uart2 { status = "okay"; }; // 或者可能在 aliases 节点中有定义 aliases { // 如果看到 serial2 = &uart2; 且与console相关,需要注意,但通常只需修改chosen节点即可。 };在我们的例子中,uart2节点本身没有console相关属性,所以只需确保status = “okay”;即可。一个重要的实操心得:瑞芯微的调试串口有时会启用DMA(直接内存访问)以提高日志输出效率。但在转为普通串口后,特别是用于与低速外设通信时,DMA可能不是必须的,且在某些驱动版本下可能引发问题。一个更稳妥的做法是同时禁用DMA:
&uart2 { status = "okay"; // 注释掉DMA配置,让驱动使用PIO(编程输入输出)模式 // dmas = <&dmac0 4>, <&dmac0 5>; // dma-names = "tx", "rx"; pinctrl-names = "default"; pinctrl-0 = <&uart2m0_xfer>; };3.3 编译内核与打包固件
修改保存后,需要重新编译内核并打包到系统镜像中。
# 在SDK根目录下,通常有编译脚本 cd /home/project/rk3568_android_sdk # 根据你的编译环境,选择对应的命令,例如: source build/envsetup.sh lunch rk3568_xxx-userdebug # 选择对应的午餐配置 # 单独编译内核和dtb make bootimage -j8 # 或者编译整个系统(更彻底) make -j8编译完成后,新的内核镜像boot.img和资源镜像resource.img(内含设备树)会生成在rockdev/目录下。使用瑞芯微的AndroidTool或upgrade_tool工具,将它们烧录到设备中。强烈建议在烧录时,只勾选boot.img和resource.img进行部分烧录,这样即使失败,系统其他部分(如recovery)仍是好的,可以通过备用方式恢复。
3.4 系统验证与功能测试
设备重启后,进行以下验证:
检查内核启动日志:由于我们移除了默认控制台,内核的
printk日志将没有默认的输出路径。这时,你可以通过备用调试手段(如ADB)使用dmesg命令查看内核日志。如果看到系统正常启动,且没有关于uart2的报错,说明修改初步成功。adb shell dmesg | grep -i uart # 应该能看到uart2被成功 probe,类似:ff1a0000.serial: ttyS2 at MMIO 0xff1a0000 (irq = 45, base_baud = 1500000) is a 16550A检查设备节点:通过ADB shell查看
/dev目录下是否生成了对应的串口设备节点。adb shell ls -l /dev/ttyS* # 应该能看到 /dev/ttyS2,其主次设备号可能是 4, 66应用层读写测试:这是最终验证。你可以编写一个简单的测试程序,或者使用
cat和echo命令进行基础测试。注意:需要先设置正确的波特率等参数,这通常需要stty命令或在自己的程序中用termios结构体配置。# 在设备端,配置串口参数(以115200波特率为例) adb shell stty -F /dev/ttyS2 115200 cs8 -parenb -cstopb # 在一个终端监听串口输出 cat /dev/ttyS2 & # 在另一个终端向串口发送数据 echo "Hello UART2" > /dev/ttyS2如果能在监听终端看到“Hello UART2”,则证明串口功能完全正常。你也可以将串口的TX和RX引脚短接,进行自发自收的环回测试,这是最可靠的硬件测试方法。
4. 深度配置与高级技巧
4.1 配置Android HAL层串口权限
默认情况下,普通应用可能没有权限访问/dev/ttyS2设备节点。为了让你的App能使用这个串口,需要在Android系统层面进行配置。
方法一:修改SELinux策略(推荐用于产品定型)在设备树同级目录或系统源码中,找到SELinux策略文件(通常是device/rockchip/common/sepolicy/目录下的.te文件)。你需要为你的串口设备添加允许规则。例如,创建一个device/rockchip/common/sepolicy/vendor/my_uart.te文件:
# 允许 init 进程创建设备节点 type my_uart_device, dev_type; # 允许 hal_uart 域访问 allow hal_uart my_uart_device:chr_file { open read write ioctl }; # 将 /dev/ttyS2 关联到新类型 file_contexts: /dev/ttyS2 u:object_r:my_uart_device:s0这需要重新编译系统镜像。对于快速测试,可以先使用adb shell setenforce 0临时关闭SELinux,但这不是生产环境的安全做法。
方法二:在init.rc中修改设备节点权限在device/rockchip/common/rootdir/init.rc或你板级特定的init.xxx.rc文件中,添加一行:
# 设置 /dev/ttyS2 为全局可读写(不安全,仅用于调试) chmod 0666 /dev/ttyS2或者,更精细地设置为某个特定用户组(如system)可访问:
chown system system /dev/ttyS2 chmod 0660 /dev/ttyS2修改后需要重新编译boot.img或system.img。
4.2 处理系统服务对串口的占用
有时,即使按照上述步骤操作,应用层仍然无法打开串口,提示“Device or resource busy”。这通常是因为有系统服务(如console、getty或某个自定义守护进程)占用了该设备。
排查与解决方法:
- 检查进程占用:使用
lsof或fuser命令查看哪个进程打开了/dev/ttyS2。adb shell lsof /dev/ttyS2 - 禁用相关服务:
- Console服务:确保在
init.rc中没有为ttyS2启动console服务。检查所有service console的定义。 - Getty服务:有些系统会为串口启动
getty(用于登录)。检查init.rc中是否有service getty关联到该串口,并注释掉。 - Logd配置:检查
/system/etc/logd/logd.conf或相关配置,确保logd没有配置从该串口读取内核日志。
- Console服务:确保在
4.3 性能调优与稳定性保障
将调试口转为通用串口后,在一些高波特率或大数据量场景下,可能需要关注性能。
- 缓冲区大小:Linux内核为每个
tty设备分配了输入/输出环形缓冲区。默认大小可能不适合高速数据流。你可以在驱动层(重新配置内核参数CONFIG_SERIAL_8250_RUNTIME_UARTS等)或应用层(使用termios的VMIN和VTIME,或ioctl设置FIONREAD)进行优化。 - 中断与轮询:禁用DMA后,串口完全依赖CPU中断处理数据。在高负载系统中,中断过于频繁可能影响整体性能。如果确实需要高性能,可以考虑保留并正确配置DMA,但这需要对驱动有更深理解,并做好充分的稳定性测试。
- 电源管理:确保串口在系统休眠时不会被错误地关闭或进入错误状态。检查设备树中该串口节点的
power-domains属性是否正确,以及在驱动中是否实现了适当的pm_ops。
5. 常见问题排查与修复实录
即使按照指南操作,你也可能会遇到一些问题。以下是我在实际项目中踩过的坑和解决方案:
问题1:修改设备树并烧录后,系统无法启动,串口无任何输出。
- 现象:设备上电后毫无反应,备用调试手段(如ADB)也无法连接。
- 原因:最可能的原因是设备树修改有语法错误,或者修改了不该改的节点(例如错误地禁用了系统必需的UART),导致内核无法正确解析DTB而崩溃。
- 解决方案:
- 紧急恢复:使用瑞芯微的
Maskrom模式强制烧录一个已知正常的resource.img(包含正确的dtb)。长按设备上的RECOVERY或MASKROM键(或短接测试点)上电,通过工具烧录。 - 仔细检查:核对修改的DTS文件语法,确保所有括号匹配,属性值正确。使用
dtc(设备树编译器)工具预先编译检查:dtc -I dts -O dtb -o test.dtb your_board.dts。 - 增量修改:一次只做一处修改,验证通过后再做下一处。先注释
stdout-path,验证能启动后,再修改UART节点。
- 紧急恢复:使用瑞芯微的
问题2:系统能启动,但/dev下没有出现ttyS2设备节点。
- 现象:
dmesg中能看到uart2 probe成功,但ls /dev/ttyS*找不到。 - 原因:驱动probe成功,但可能由于资源冲突(如GPIO引脚被其他功能占用)、时钟未开启、或内核配置中该串口驱动未编译进内核(而是模块)。
- 排查:
# 查看内核驱动日志,过滤uart2 adb shell dmesg | grep -A5 -B5 ff1a0000.serial # 假设uart2寄存器基址是0xff1a0000 # 检查GPIO复用情况 adb shell cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins | grep gpio1-c0 # 检查时钟状态(需要内核开启DEBUG_FS) adb shell cat /sys/kernel/debug/clk/clk_summary | grep uart - 解决:确保设备树中该UART节点的
pinctrl-0引用的引脚配置组(如&uart2m0_xfer)是正确的,并且没有其他节点(如&i2c3)复用了同一组引脚。确认内核配置CONFIG_SERIAL_8250和CONFIG_SERIAL_8250_CONSOLE(如果还需要其他控制台)已启用。
问题3:应用可以打开/dev/ttyS2,但读写数据全是乱码或根本无数据。
- 现象:能
open()成功,但read()不到数据,或收到的数据与发送的不符。 - 原因:波特率、数据位、停止位、校验位等参数不匹配。这是串口通信最常见的问题。
- 排查与解决:
- 确认双方参数:确保你的应用程序设置的串口参数(波特率、8N1等)与对端设备(如GPS模块)完全一致。一个字节都不能错。
- 硬件流控:检查是否启用了硬件流控(RTS/CTS),但硬件连线却没有接。如果启用但未连接,会导致数据无法发送。在测试阶段,可以在代码中明确禁用流控(
CLOCAL标志)。 - 环回测试:将板子上的UART2_TX和UART2_RX引脚用杜邦线短接,运行一个自发自收的测试程序。如果这样能成功,说明板端配置和驱动是好的,问题出在与外部设备的连接或参数匹配上。
- 示波器/逻辑分析仪:这是终极武器。用示波器测量TX引脚上的波形,可以直观看到波特率是否正确(测量一个位的时间宽度,计算其倒数),数据内容是否正常。
问题4:串口工作不稳定,偶尔丢数据,特别是在高波特率下。
- 现象:115200波特率以下正常,但提高到921600或1.5M时,数据出现随机错误或丢失。
- 原因:时钟精度、缓冲区溢出、中断延迟或PCB布线问题。
- 解决思路:
- 降低波特率:首先确认是否必须使用如此高的波特率。如果不是,降低波特率是最简单的解决方案。
- 检查时钟源:UART的波特率依赖于输入时钟(
SCLK_UARTx)。确保设备树中为该UART分配的时钟源是稳定的,并且没有与其他高功耗设备共享一个容易受干扰的时钟。 - 启用并使用DMA:如前所述,高波特率下强烈建议启用DMA。重新在设备树中启用
dmas属性,并确保内核配置支持DMA(CONFIG_SERIAL_8250_DMA)。 - 增大内核缓冲区:可以尝试修改驱动源码,增大
uart_port结构体中的fifosize或调整环形缓冲区大小,但这需要重新编译内核。 - 硬件检查:检查PCB上UART走线是否过长,是否有平行高速信号线造成干扰。确保TX/RX线上有正确的上拉电阻。
将瑞芯微开发板的Android调试串口配置为普通串口,是一个典型的“知其然,更要知其所以然”的嵌入式系统调试案例。它要求开发者跨越硬件、内核驱动、系统框架多个层面进行思考。整个过程最关键的,不是记住那几行DTS修改,而是建立起一套安全的修改、验证和回滚流程。我的个人体会是,永远要在修改前留好“后路”,每次改动都要小步快跑、及时验证。当看到那个曾经只输出冰冷日志的调试口,终于能与外部世界进行你设计的对话时,这种对系统底层掌控带来的满足感,正是嵌入式开发的乐趣所在。如果在操作中遇到上面未覆盖的奇怪问题,不妨回到最基础的点:用dmesg看内核说了什么,用示波器看硬件信号是什么,大部分难题都能在这两者之间找到答案。