4. 嵌入式操作系统与驱动模型

航电计算平台上的驱动代码,最终都要落在某种嵌入式操作系统之上。同一颗外设芯片,在 VxWorks、Linux 和裸机/小型 RTOS 上的驱动写法差异很大,根源在于三者对“设备”的抽象方式不同。理解这些差异,才能判断一段驱动代码该放在哪一层、由谁调用、出错时从哪里查。

三种操作系统的驱动抽象差异

VxWorks 采用扁平化的设备模型。它通过 iosLib 提供统一的 I/O 系统接口,驱动以 DEV_HDR 为头部挂入设备链表,应用层用 open/read/write/ioctl/close 访问。VxWorks 没有 Linux 那样严格的“设备号—主次设备”体系,驱动注册时直接创建设备节点名(如 /ata0、/ttyCo/0),内核通过名字查找设备。它的优势是路径短、实时性好,中断服务程序(ISR)与任务之间用信号量或消息队列衔接,适合对确定性要求高的航电任务。

Linux 则构建了分层的设备驱动模型。核心是“总线—设备—驱动”三者匹配:设备通过设备树、ACPI 或总线枚举被注册到总线上,驱动通过 probe() 与设备绑定,绑定成功后由内核框架回调。Linux 把设备分为字符设备、块设备和网络设备三大类,每类有独立的注册接口和访问路径。这种分层带来了良好的可移植性和复用性,但也引入了更长的调用链和更复杂的生命周期管理。

小型 RTOS(如 FreeRTOS、RT-Thread、Zephyr)通常不提供统一的设备抽象层,驱动往往以“设备结构体 + 操作函数指针”的形式存在。RT-Thread 引入了 rt_device 抽象,Zephyr 用 device 结构体和 DEVICE_DT_DEFINE 宏,FreeRTOS 则更多依赖用户自行封装。它们的共同点是:驱动与硬件耦合更紧,但代码量小、启动快,适合资源受限的航电子模块。

字符设备、块设备与网络设备驱动结构

字符设备以字节流方式访问,典型如串口、CAN 控制器、ADC。Linux 中字符设备驱动需要实现 file_operations 结构体,核心成员包括 open、read、write、ioctl、release。注册时用 alloc_chrdev_region 动态申请设备号,再用 cdev_add 把 cdev 加入内核。用户空间通过 /dev/xxx 节点访问,ioctl 常用于传递配置参数,如波特率、采样率。

块设备以固定大小的数据块为单位访问,典型如 eMMC、NVMe、SD 卡。Linux 块设备驱动围绕 gendisk 和 request_queue 构建,I/O 请求经过电梯调度后由驱动取出并提交给硬件。块设备支持缓冲、合并和乱序完成,适合大吞吐存储,但不适合低延迟的寄存器级访问。

网络设备在 Linux 中不走 /dev 节点,而是通过 net_device 结构体和 socket 接口访问。驱动需要实现 ndo_start_xmit 发送函数,并在收到数据时调用 netif_rx 或 NAPI 的 napi_schedule 上报。航电中的 AFDX、ARINC 664 网卡驱动就属于这一类,通常还要处理多队列、时间戳和流量整形。

Linux 三类设备驱动结构与访问路径 用户空间 open/read/write/ioctl read/write (块缓冲) socket / send / recv 内核框架层 字符设备框架 cdev / file_operations 块设备框架 gendisk / request_queue 网络设备框架 net_device / NAPI 驱动层 串口 / CAN / ADC 驱动 probe / remove / ISR eMMC / NVMe 驱动 request_fn / DMA AFDX / 以太网驱动 ndo_start_xmit / NAPI 硬件层 UART / CAN 控制器 eMMC / PCIe 控制器 MAC / PHY / 交换芯片 设备树 (Device Tree): 描述硬件资源(寄存器基址、中断号、时钟、GPIO),内核解析后生成 platform_device,驱动通过 of_match_table 匹配并 probe

设备树:硬件描述与驱动解耦

设备树(Device Tree)是 Linux 在 ARM、PowerPC 等架构上描述硬件的方式。它把“板子上有什么设备、寄存器在哪、中断接哪根线”从内核代码中剥离出来,写成 .dts 文本,编译为 .dtb 后由 bootloader 传给内核。内核启动时解析设备树,为每个节点创建 platform_device,驱动通过 of_match_table 中的 compatible 字符串与节点匹配,匹配成功后调用 probe()。

一个典型的航电串口节点可以这样描述:

uart0: serial@ff1a0000 {
    compatible = "snps,dw-apb-uart";
    reg = <0xff1a0000 0x1000>;
    interrupts = <0 32 4>;
    clocks = <&clk_uart0>;
    status = "okay";
};

驱动侧对应声明:

static const struct of_device_id dw_uart_match[] = {
    { .compatible = "snps,dw-apb-uart" },
    { }
};
MODULE_DEVICE_TABLE(of, dw_uart_match);

当 compatible 一致时,内核把 reg 中的地址映射为 ioremap 资源,把 interrupts 转换为虚拟中断号,驱动在 probe() 中通过 platform_get_resource 和 platform_get_irq 取出这些资源。这样同一份驱动可以适配不同板卡,只需更换设备树。

在航电项目中,设备树常与硬件抽象层(HAL)配合使用。同一驱动源码面向多个 LRU(外场可更换单元)时,差异尽量收敛到 .dts 和板级配置文件,减少驱动分支。

VxWorks 与 RTOS 的对应做法

VxWorks 没有设备树,硬件信息通常写在 config.h、hwconf.c 或 BSP 的 sysLib.c 中。驱动初始化函数在 usrInit 或 usrRoot 阶段被调用,直接传入基址和中断向量。例如串口驱动会调用 sysSerialHwInit,由 BSP 提供寄存器地址和中断连接。VxWorks 7 引入了 FDT 支持,但传统项目仍以 BSP 硬编码为主。

RT-Thread 用 rt_device 统一抽象,通过 rt_device_register 注册,应用层用 rt_device_open/read/write/control 访问。Zephyr 则用设备树(.dts)加 DEVICE_DT_DEFINE 宏,在编译期生成设备实例,启动时按初始化优先级依次调用 init 函数。FreeRTOS 本身不提供设备模型,通常由项目自行封装一层 xxx_init/xxx_read/xxx_write 接口。

驱动开发中的关键判断

面对一个航电外设,先判断它属于哪类设备:寄存器级、字节流访问的走字符设备;大块存储走块设备;带 MAC 的网络接口走网络设备。然后确认操作系统提供的框架接口,Linux 下查 file_operations、gendisk、net_device,VxWorks 下查 iosLib 和 DEV_HDR,RTOS 下查设备抽象层。最后确认硬件描述方式:Linux/Zephyr 优先用设备树,VxWorks 传统项目查 BSP,FreeRTOS 项目查板级配置。

设备树中的 reg 地址必须与芯片手册一致,interrupts 的中断号和触发类型要与 GIC 配置匹配。地址或中断号写错时,驱动 probe() 可能成功但读写异常,或直接返回 -ENODEV,排查时应先核对设备树与手册。

驱动模型的选择直接影响调试路径。Linux 下用 dmesg 看 probe 日志,用 /sys/bus/platform/devices 确认设备是否注册,用 cat /proc/interrupts 确认中断是否触发。VxWorks 下用 iosDevShow 查看设备链表,用 intShow 查看中断挂接。RTOS 下则依赖各自的 shell 命令或日志输出。掌握这些观察点,才能在航电平台上快速定位驱动问题。