2. 设备驱动框架基础:驱动类型与注册流程
在 QNX 实时操作系统中,设备驱动按照其管理的设备特性与数据交换方式,划分为三种核心类型:字符驱动(Char)、块驱动(Block) 和 网络驱动(Network)。每种类型对应不同的资源管理模型、入口点集合与注册机制。理解这些基础分类与注册流程,是后续实现中断映射与高效数据传输的前提。
2.1 驱动类型对比与选择
下表从数据模型、访问方式、缓存策略与典型应用场景四个维度,对三种驱动类型进行对比:
| 对比维度 | 字符驱动(Char) | 块驱动(Block) | 网络驱动(Network) |
|---|---|---|---|
| 数据模型 | 字节流,无固定块大小 | 固定大小块(如 512B/4KB) | 数据包(Packet) |
| 访问方式 | open/read/write/ioctl | 块设备接口 + 文件系统挂载 | Socket / io-pkt 协议栈 |
| 缓存策略 | 无内核缓存,直接传递用户缓冲区 | 内核块缓存层(Buffer Cache) | mbuf 链管理 |
| 典型场景 | 串口、GPIO、SPI、传感器 | SD/MMC、NAND Flash、硬盘 | 以太网、CAN、无线网卡 |
| QNX 注册接口 | io_open / io_read / io_write | devb-* 框架 + blkio 接口 | netmap / ifnet 结构体 |
选型建议: 对于大多数嵌入式外设(如 ADC、PWM、中断控制器),优先选择字符驱动模型,因为它接口简洁、延迟可控,且无需经过文件系统层。块驱动适用于需要挂载文件系统的存储设备;网络驱动则专为协议栈数据包收发设计。
2.2 字符驱动入口点与注册流程
字符驱动是 QNX 驱动开发中最常用的模型。其核心入口点通过 iofunc_funcs_t 和 resmgr_connect_funcs_t 结构体注册到资源管理器(Resource Manager)。
2.2.1 核心入口点结构
// 连接函数表:处理 open/close/unblock 等连接事件
resmgr_connect_funcs_t connect_funcs;
// I/O 函数表:处理 read/write/ioctl/devctl 等数据操作
iofunc_funcs_t io_funcs;
// 初始化连接函数表
memset(&connect_funcs, 0, sizeof(connect_funcs));
connect_funcs.open = my_open;
connect_funcs.close = my_close;
// 初始化 I/O 函数表
memset(&io_funcs, 0, sizeof(io_funcs));
io_funcs.read = my_read;
io_funcs.write = my_write;
io_funcs.ioctl = my_ioctl;
2.2.2 注册流程步骤
- 初始化 I/O 资源层: 调用
iofunc_func_init()填充默认函数指针。 - 分配并初始化属性结构体: 使用
iofunc_attr_init()设置设备权限、拥有者、设备类型。 - 创建资源管理器附加点: 调用
resmgr_attach()将设备路径(如/dev/mydevice)与函数表绑定。 - 进入消息分发循环: 调用
resmgr_block()或resmgr_loop()等待客户端请求。
关键注册代码片段:
// 步骤1:初始化 I/O 函数表
iofunc_func_init(RESMGR_IO_NULL, 0, 0, 0, &io_funcs);
// 步骤2:初始化设备属性
iofunc_attr_init(&attr, S_IFCHR | 0666, NULL, NULL);
// 步骤3:注册到路径 /dev/mydevice
resmgr_attach(
&dpp, // 资源管理器句柄
&res_attr, // 资源属性
"/dev/mydevice", // 设备路径
_FTYPE_ANY, // 文件类型
0, // 标志
&connect_funcs, // 连接函数表
&io_funcs, // I/O 函数表
&attr // 设备属性
);
// 步骤4:进入事件循环
resmgr_block(dpp);
2.3 块驱动入口点与注册流程
块驱动在 QNX 中通常基于 devb-* 框架实现。该框架封装了块缓存层与分区管理,驱动开发者只需实现底层的 blkio 接口。
2.3.1 块驱动核心接口
| 接口函数 | 功能描述 | 触发时机 |
|---|---|---|
blkio_open() |
打开块设备,初始化硬件 | 设备挂载或首次访问 |
blkio_read() |
读取指定 LBA 的数据块 | 文件系统下发读请求 |
blkio_write() |
写入数据块到指定 LBA | 文件系统下发写请求 |
blkio_capacity() |
返回设备总块数 | 分区表扫描时 |
blkio_flush() |
刷新写缓存到物理介质 | sync 或缓存回写 |
2.3.2 注册流程
- 实现 blkio 接口结构体: 填充
blkio_ops_t中的函数指针。 - 调用
blkio_register(): 将设备注册到 devb 框架,指定设备名称与容量。 - 启动 I/O 处理线程: 框架自动调用
blkio_read/write处理来自文件系统的请求。
注意: 块驱动必须正确处理对齐访问与部分块读写。QNX 块缓存层可能下发非对齐请求,驱动内部需进行缓冲与合并。
2.4 网络驱动入口点与注册流程
网络驱动在 QNX 中通过 ifnet 结构体与 io-pkt 协议栈交互。驱动需要实现数据包发送、接收、中断处理以及接口统计等回调。
2.4.1 ifnet 核心入口点
struct ifnet my_ifnet;
// 初始化 ifnet 结构体
memset(&my_ifnet, 0, sizeof(my_ifnet));
strcpy(my_ifnet.name, "en0"); // 接口名称
my_ifnet.if_start = my_net_start; // 启动发送队列
my_ifnet.if_input = my_net_input; // 接收数据包
my_ifnet.if_ioctl = my_net_ioctl; // 控制命令(如设置 MAC)
my_ifnet.if_watchdog = my_net_watchdog; // 看门狗定时器
// 设置接口能力标志
my_ifnet.if_capabilities = IFCAP_HWCSUM | IFCAP_VLAN_MTU;
2.4.2 注册流程
- 分配并初始化 ifnet: 设置接口名称、MTU、MAC 地址。
- 调用
if_attach(): 将接口注册到协议栈全局链表。 - 注册中断处理: 使用
InterruptAttach()绑定接收/发送完成中断。 - 启动接口: 通过
if_up()或 ioctl SIOCSIFFLAGS 使能接口。
2.5 驱动注册流程总览(SVG 架构图)
下图展示了三种驱动类型从初始化到注册完成的统一流程框架,突出资源管理器、devb 框架与 io-pkt 协议栈三种注册路径的差异:
2.6 驱动注册关键数据结构对比
三种驱动类型在注册过程中依赖的核心数据结构差异显著,下表汇总了关键结构体及其作用:
| 驱动类型 | 核心结构体 | 注册函数 | 设备路径/名称 |
|---|---|---|---|
| 字符驱动 | resmgr_connect_funcs_t + iofunc_funcs_t |
resmgr_attach() |
/dev/xxx(POSIX 路径) |
| 块驱动 | blkio_ops_t |
blkio_register() |
/dev/hd0(块设备名) |
| 网络驱动 | ifnet |
if_attach() |
en0(接口名) |
调试提示: 在 QNX 系统启动后,使用
ls -l /dev/ 查看已注册的字符/块设备;使用 ifconfig -a 查看已注册的网络接口。若设备未出现,优先检查注册函数返回值与错误日志(sloginfo)。
2.7 本章小结
- 字符驱动 是最通用的模型,通过资源管理器注册,适合大多数外设。
- 块驱动 基于 devb-* 框架,需要实现 blkio 接口,适合存储类设备。
- 网络驱动 通过 ifnet 结构体与 io-pkt 协议栈集成,处理数据包收发。
- 三种驱动类型的注册流程均包含 初始化 → 填充函数表 → 注册 → 进入服务循环 四个阶段。
- 理解入口点与注册流程是后续实现中断映射与 DMA 数据传输的基础。