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 网卡驱动就属于这一类,通常还要处理多队列、时间戳和流量整形。
设备树:硬件描述与驱动解耦
设备树(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 取出这些资源。这样同一份驱动可以适配不同板卡,只需更换设备树。
.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 命令或日志输出。掌握这些观察点,才能在航电平台上快速定位驱动问题。