瑞芯微Android调试串口改造:内核DTS与SELinux全链路配置
1. 项目概述从调试口到通用串口的价值转换在嵌入式开发尤其是基于瑞芯微Rockchip平台进行Android系统定制时开发板或主板上那个标记为“调试串口”通常指UART0的接口是工程师最熟悉的老朋友。它承载着系统启动的日志输出、uboot命令行交互以及早期内核调试的重任是开发阶段不可或缺的“生命线”。然而当产品进入量产阶段或者我们需要在Android应用层与外部传感器、控制器、显示屏等设备进行串行通信时这个宝贵的硬件资源如果仅仅用作调试就显得有些“大材小用”了。“将Android调试串口配置为普通串口”这个需求本质上是对硬件资源的深度复用和功能重定义。它意味着我们要在保持系统核心调试能力通常通过其他方式如ADB的前提下释放出这个物理串口使其能够像/dev/ttyS1、/dev/ttyS2一样被上层JAVA应用或本地Native服务通过标准的串口API如Android Things的PeripheralManager、开源库android-serialport-api或JNI调用打开、读写和控制。这不仅能省下一个额外的串口芯片成本对于接口资源紧凑的紧凑型设备来说更是解决了连接外部设备的刚需。这个过程涉及从内核驱动、设备树DTS、Android HAL层到权限管理的全链路修改绝非简单的配置文件改动。它要求开发者对Linux内核的设备模型、Android系统启动流程以及权限安全机制有清晰的认识。下面我将结合多年在瑞芯微平台如RK3566, RK3588等上的实战经验为你拆解每一步的原理与操作避开我当年踩过的那些坑。2. 核心思路与方案选型为什么不能直接“拿来就用”在动手之前我们必须理解为什么默认的调试串口通常是/dev/ttyS0或/dev/ttyFIQ0不能被应用层直接使用。核心原因在于它的“特殊身份”和由此带来的资源占用冲突。2.1 调试串口的“特权”与冲突1. 控制台Console绑定在Linux内核中调试串口通常被指定为系统控制台console内核命令行参数。这意味着内核所有的启动信息、printk日志以及某些情况下的登录终端都定向到了这个串口。在Android环境下console服务也可能关联它。如果应用层试图打开一个已经被内核作为控制台占用的TTY设备会导致EBUSY设备忙错误。2. FIQ快速中断模式瑞芯微平台为了确保调试串口在系统崩溃等极端情况下依然能输出日志常常为调试串口UART0启用FIQFast Interrupt中断模式。这是一种比普通IRQ优先级更高、处理更快的机制。驱动代码如drivers/tty/serial/8250_fiq.c会以特殊方式接管这个串口。应用层通用的串口驱动无法正确处理处于FIQ模式的UART。3. 早期内核与Bootloader占用从Bootloader如U-Boot开始这个串口就已经被初始化并使用。内核早期启动阶段在完整的设备驱动模型和sysfs建立之前它就已经在运作。这种“先到先得”的占用关系使得在用户空间简单地操作难以解除其绑定。因此我们的方案必须系统性地解决这三个问题。主流方案有两种其选择取决于产品的最终形态和调试需求。2.2 方案一禁用控制台保留普通驱动推荐用于量产产品这是最彻底、最标准的方案。目标是将该串口完全“平民化”。操作在内核配置中取消该串口作为控制台在设备树中将其FIQ属性移除确保它由标准的8250或平台串口驱动管理。优点应用层可以像使用其他任何串口一样使用它稳定可靠符合Linux设备模型。缺点完全失去了通过该物理串口进行内核调试的能力。系统卡死时将看不到内核panic信息。这要求你的产品必须具备其他可靠的调试手段如ADB over USB/Ethernet、网络控制台netconsole或者另一个预留的调试串口。适用场景量产产品调试主要通过ADB或网络进行该串口需要用于连接业务功能设备如扫码头、打印机。2.3 方案二动态切换方案适用于持续开发的原型机这是一种更灵活但更复杂的方案旨在“鱼与熊掌兼得”。操作系统正常启动后通过一个内核模块或特定的ioctl命令动态地将串口从FIQ/控制台模式释放并重新初始化为普通模式。或者利用串口多路复用技术如serdev框架但调试输出和应用数据共享链路需要协议区分。优点在需要时保留调试功能在运行时可以提供应用层访问。缺点实现复杂稳定性需要充分测试。切换过程中可能导致数据丢失或短暂不可用。不是所有内核版本和驱动都支持良好的动态切换。适用场景开发板、原型机需要频繁在调试和功能测试间切换。对于绝大多数追求稳定性的产品开发我强烈推荐方案一。它逻辑清晰结果确定。接下来的内容我们将以方案一为主线详细阐述在瑞芯微Android平台上如何实现。注意在进行任何内核和DTS修改前请务必备份原始文件并确保你有编译和烧写完整Android系统镜像的能力。操作失误可能导致系统无法启动。3. 内核与设备树DTS深度修改这是整个改造过程的核心直接决定了硬件资源的分配和驱动行为。我们需要修改两处内核配置Kconfig和设备树源文件.dts。3.1 定位与修改内核配置首先找到你所用内核的配置文件。对于Android项目这通常在kernel/arch/arm64/configs/或arm/目录下比如rockchip_defconfig或android-11.0-rockchip-rk3568_defconfig这样的文件。我们需要搜索并修改与控制台CONSOLE和串口驱动相关的配置。# 进入内核源码目录 cd path/to/your/kernel # 使用grep搜索当前defconfig中关于console和串口的配置 grep -i console\|serial\|fiq .config # 如果.config已存在 # 或者搜索defconfig文件 grep -i console\|8250.*fiq arch/arm64/configs/your_defconfig关键配置项通常包括CONFIG_SERIAL_8250_CONSOLEy 这是允许8250系列串口作为控制台的总开关。我们需要将其设为n来禁止串口控制台。CONFIG_FIQ_DEBUGGERy或CONFIG_ROCKCHIP_SERIAL_FIQy 这是瑞芯微平台调试串口使用FIQ模式的关键配置。必须将其设为n来禁用FIQ调试器。CONFIG_SERIAL_8250y和CONFIG_SERIAL_8250_DWy如果使用DesignWare IP确保标准串口驱动是启用的。修改示例直接编辑defconfig文件或使用make menuconfig进行图形化配置更推荐能处理依赖关系。make ARCHarm64 your_defconfig # 加载默认配置 make ARCHarm64 menuconfig # 进入配置界面在菜单中进入Device Drivers - Character devices - Serial drivers。找到8250/16550 and compatible serial support和Console on 8250/16550 and compatible serial port确保前者被选中*后者不被选中空格键取消。在同一级或上级菜单中仔细查找任何带有FIQ或Debugger字样的串口相关选项确保它们都被禁用。保存并退出。新的配置会保存在.config中记得将其更新回你的defconfig文件cp .config arch/arm64/configs/your_modified_defconfig。3.2 解密与修改设备树DTS设备树描述了硬件资源是驱动匹配设备的依据。调试串口的FIQ属性和状态通常在这里定义。1. 找到正确的DTS文件瑞芯微的DTS文件有清晰的层级关系。你首先需要确定你的板型对应的DTS文件。它通常在kernel/arch/arm64/boot/dts/rockchip/目录下例如rk3568-evb1-ddr4-v10-linux.dts。这个文件会包含include一个SoC级的DTSI文件如rk3568.dtsi其中定义了UART0等核心外设。2. 修改板级DTS文件我们主要在板级DTS文件中进行覆盖修改。目标是移除serialfdd50000UART0的设备节点的status okay;属性如果它默认启用然后重新以我们想要的方式定义。更常见的做法是在板级DTS中重新声明该节点覆盖掉默认的“disabled”或特殊配置。关键修改点示例// 在板级DTS文件比如 rk3568-my-board.dts 的末尾添加 uart0 { status okay; // 确保节点启用 pinctrl-names default; pinctrl-0 uart0_xfer uart0_cts uart0_rts; // 确认引脚复用正确 // 关键移除所有与fiq和console相关的属性 // 删除类似这样的行 // rockchip,serial-id 0; // rockchip,irq-mode-enable 1; // rockchip,baudrate 1500000; // interrupts GIC_SPI 116 IRQ_TYPE_LEVEL_HIGH, // GIC_SPI 117 IRQ_TYPE_LEVEL_HIGH; // 可能有两个中断其中一个用于FIQ // 替换为标准的中断定义通常可以在SoC的.dtsi文件中找到模板 interrupts GIC_SPI 116 IRQ_TYPE_LEVEL_HIGH; // 确保没有 dmas 属性除非你确认硬件支持且需要DMA否则先注释掉 // dmas dmac0 0, dmac0 1; // dma-names tx, rx; };3. 验证引脚控制Pinctrl务必检查pinctrl-0引用的引脚组如uart0_xfer是否正确。这些定义通常在同一个DTS文件的pinctrl部分或包含的pinctrl文件中。错误的引脚复用会导致数据无法收发。4. 编译DTS修改后编译内核或使用DTC工具单独编译DTS生成DTB文件。make ARCHarm64 your_dtb_image.dtb实操心得修改DTS后最快速的验证方法是将其反编译dtc -I dtb -O dts -o extracted.dts your_new.dtb然后搜索uart0节点确认fiq-debugger、console等相关属性已消失中断、时钟、引脚配置符合一个普通串口的样子。4. Android系统层适配与权限配置内核修改完成后串口设备文件如/dev/ttyS0应该已经可以正常生成。接下来我们需要让Android系统能够识别并允许应用访问它。4.1 配置ueventd.rc创建设备节点在Android系统中/dev目录下的设备节点是由init进程根据ueventd.rc文件中的规则在启动时创建的。我们需要确保我们的串口有正确的节点名称和权限。找到你的设备对应的ueventd.rc文件可能在device/rockchip/common/、device/rockchip/rk3568/或vendor/rockchip/common/等目录下。在其中添加一行/dev/ttyS0 0660 system system这表示/dev/ttyS0设备节点路径。0660权限所有者system和同组用户可读写其他用户无权限。system system所有者和组。如果你的串口驱动生成了不同的名字如/dev/ttyFIQ0变成/dev/ttyS0请以实际/proc/tty/drivers或/sys/class/tty/下的信息为准。4.2 配置SELinux策略Android 8.0 至关重要这是新手最容易忽略导致权限被拒绝Permission denied的一步。SELinux会严格限制进程对设备文件的访问。1. 找到设备对应的SELinux上下文在系统运行后通过ADB连接执行adb shell ls -laZ /dev/ttyS0你会看到类似输出u:object_r:tty_device:s0 /dev/ttyS0。记下这个上下文tty_device。2. 添加.te策略文件在你的设备SEPolicy目录如device/rockchip/rk3568/sepolicy/下找到或创建一个与串口相关的.te文件例如vendor_my_serial.te。在其中添加允许特定域domain访问该设备的规则。例如如果你希望一个名为my_serial_service的守护进程或者platform_app系统应用能访问这个串口# 允许 my_serial_service 对 tty_device 类型的文件进行读写、打开操作 allow my_serial_service tty_device:chr_file { open read write ioctl }; # 或者允许所有平台应用访问范围较广测试用 allow platform_app tty_device:chr_file { open read write ioctl };3. 更精细的控制推荐更好的做法是定义一个自定义的类型type然后只允许特定的服务访问。这需要更多的SEPolicy知识但更安全。# 定义一个新类型 type my_custom_serial_device, dev_type; # 在 file_contexts 中关联设备 /dev/ttyS0 u:object_r:my_custom_serial_device:s0 # 在 .te 文件中允许特定服务访问这个新类型 allow my_serial_service my_custom_serial_device:chr_file { open read write ioctl };4. 编译并刷入新系统镜像测试权限是否生效。4.3 应用层访问示例当底层配置全部正确后应用层访问就与访问其他串口无异了。这里以使用流行的android-serialport-api库基于JNI为例在AndroidManifest.xml中声明权限如果需要uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / !-- 可能用于日志 -- !-- 对于系统应用可能需要声明共享UID或使用signature权限 --在JAVA代码中打开串口import android.serialport.SerialPort; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class SerialManager { private SerialPort mSerialPort; private OutputStream mOutputStream; private InputStream mInputStream; public boolean openSerialPort(String path, int baudrate) { try { // 关键路径就是我们配置的 /dev/ttyS0 mSerialPort new SerialPort(new File(path), baudrate, 0); mOutputStream mSerialPort.getOutputStream(); mInputStream mSerialPort.getInputStream(); return true; } catch (IOException | SecurityException e) { e.printStackTrace(); // SecurityException 很可能就是SELinux策略未配置好 return false; } } // ... 读写数据的方法 }5. 完整流程验证与问题排查实录修改完成后必须进行系统性的验证确保每一步都正确无误。5.1 验证流程检查表你可以按照以下步骤像流水线一样检查你的配置步骤操作预期结果问题排查方向1. 内核启动日志连接调试串口修改前或通过ADB看dmesg不再有“Console: switching to colour frame buffer device”或“Fiq debugger enabled”等字样。内核启动信息可能通过其他控制台如fbcon输出。检查内核配置CONFIG_SERIAL_8250_CONSOLE和CONFIG_FIQ_DEBUGGER是否确实被禁用。2. 设备节点生成adb shell ls -l /dev/ttyS*能看到/dev/ttyS0且权限为crw-rw----主次设备号正常。检查DTS中status是否为okay检查ueventd.rc规则是否正确添加并编译进镜像。3. 驱动绑定状态adb shell cat /proc/tty/drivers找到serial对应的驱动如serial /dev/ttyS 4, 64-67 serial。如果ttyS0不在列说明驱动未成功绑定。检查DTS中串口节点是否被正确解析中断、时钟资源是否冲突。4. 内核消息adb shell dmesg | grep -i uart应有类似“fdd50000.serial: ttyS0 at MMIO 0xfdd50000 (irq 116) is a 16550A”的成功注册信息。出现probe failed等错误检查DTS引脚配置、时钟名。5. 简单回环测试短接串口的TX和RX引脚通过一个简单的C程序或cat命令发送数据。发送的数据能被自己接收。硬件连接问题引脚复用错误波特率不匹配。6. 应用层打开运行一个具有权限的测试App尝试打开/dev/ttyS0。打开成功返回一个有效的文件描述符。权限问题检查ls -laZ的SELinux上下文和应用进程的域ps -Z对照策略文件。设备忙可能仍有内核线程或服务占用lsof /dev/ttyS0查看。5.2 常见问题与解决技巧问题1应用打开串口返回Permission denied(13)。排查这是最常见的问题。首先ls -l /dev/ttyS0看Unix权限。如果是660且用户组不对修改ueventd.rc。如果权限正确那几乎肯定是SELinux。技巧临时将SELinux设置为宽容模式测试adb shell setenforce 0。如果此时能打开就是SELinux策略问题。查看内核日志获取AVC拒绝信息adb shell cat /proc/kmsg | grep avc或adb shell dmesg | grep avc。根据拒绝信息精准添加allow规则。问题2打开串口返回Device or resource busy(16)。排查说明有别的进程占用了。使用adb shell lsof /dev/ttyS0查看是哪个进程。很可能是内核控制台printk依然绑定在上面或者某个系统服务如console启动了。技巧确保内核命令行cmdline中没有consolettyS0,115200这样的参数。这个参数在BootloaderU-Boot的启动参数中设置。需要修改U-Boot环境变量或编译时的bootargs。对于瑞芯微检查parameter.txt文件或U-Boot源码中的CONFIG_BOOTARGS。问题3发送/接收数据全为乱码或根本无数据。排查硬件连接和软件配置不匹配。技巧确认波特率、数据位、停止位、校验位确保发送端和接收端以及驱动配置完全一致。瑞芯微DTS里可以设置默认波特率clock-frequency属性但应用层打开时设置的波特率会覆盖它。检查引脚复用这是硬件工程师和软件工程师交接的“重灾区”。用adb shell cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins命令查看对应GPIO引脚例如UART0_TX对应的GPIO的当前复用功能function是否是uart0。如果不是DTS中的pinctrl-0引用就有问题。测量电平用万用表或示波器测量TX引脚在发送时是否有电平变化。没有的话可能是引脚复用错误或驱动根本没工作。问题4系统运行不稳定偶尔死机。排查可能是串口中断IRQ与其他设备冲突。瑞芯微SoC的UART0中断号是固定的如116检查DTS中是否有其他设备错误地分配了相同的中断。技巧查看/proc/interrupts文件看UART0对应的中断号如116的触发次数CPU0列是否在发送数据时正常增加。如果异常高或与另一个设备共享就是冲突。将瑞芯微Android的调试串口改造为普通串口是一个典型的“动一发而牵全身”的系统级定制工作。它要求你清晰地理解从硬件描述、内核驱动到系统服务的整条链路。整个过程最关键的三个点是彻底的内核控制台与FIQ禁用、正确的设备树引脚与中断配置、以及完备的SELinux策略。当你成功配置后这个串口将成为你产品中一个稳定可靠的通信接口无论是连接工业PLC、智能传感器还是传统串口设备都能游刃有余。我个人在多个量产项目中的体会是前期花时间彻底理解DTS和SELinux远比后期盲目试错要高效得多。每次修改DTS后养成反编译DTB验证的习惯。对于权限问题善用setenforce 0和dmesg | grep avc来快速定位SELinux的拦路虎。最后一定要做长时间的稳定性压力测试特别是高波特率下的持续双向数据传输确保在资源紧张时不会出现数据丢失或系统异常。