4、传感器驱动开发:QNX下字符设备驱动框架、资源管理器编写、中断处理

好,咱们进入第四章。这一章是实战味道最浓的一章。你想想看,传感器接上去了,数据也读到了,但怎么让QNX系统认识它?怎么让上层应用像读文件一样读传感器数据?这就得靠驱动了。

在QNX下写驱动,跟Linux那套思路不太一样。我个人习惯把QNX的驱动模型理解成「文件系统的延伸」。说白了,你写一个驱动,就是在给系统增加一种新的文件操作方式。嗯,这里要注意,QNX里没有「驱动模块」这种说法,我们管它叫资源管理器(Resource Manager)

4.1 字符设备驱动框架

先说说字符设备。传感器通常都是字符设备——你一次读一个数据包,或者读一个连续的数据流。QNX的字符设备驱动,核心就是实现一组POSIX接口:open、close、read、write、ioctl、devctl。

我刚开始做QNX驱动时,犯过一个低级错误:把Linux的file_operations结构体直接往QNX上套。结果编译都过不了。QNX用的是iofunc_funcs_tresmgr_io_funcs_t这两套回调函数表。

一个最简的字符设备驱动框架长这样:

#include <sys/iofunc.h>
#include <sys/resmgr.h>

// 设备上下文结构体
typedef struct {
    iofunc_attr_t attr;   // 标准属性
    int sensor_fd;        // 传感器硬件句柄
    int irq_id;           // 中断ID
} sensor_dev_t;

// open回调
static int sensor_open(resmgr_context_t *ctp, io_open_t *msg,
                       RESMGR_OCB_t *ocb, void *handle) {
    sensor_dev_t *dev = (sensor_dev_t *)handle;
    // 我习惯在这里做设备上电初始化
    return iofunc_open_default(ctp, msg, ocb, &dev->attr);
}

// read回调
static int sensor_read(resmgr_context_t *ctp, io_read_t *msg,
                       RESMGR_OCB_t *ocb, void *handle) {
    sensor_dev_t *dev = (sensor_dev_t *)handle;
    int nbytes;
    uint8_t buffer[64];

    // 从硬件读取传感器数据
    nbytes = read_sensor_hw(dev->sensor_fd, buffer, sizeof(buffer));

    // 把数据拷贝回用户空间
    return resmgr_write(ctp, buffer, nbytes);
}

// 回调函数表
static resmgr_io_funcs_t io_funcs = {
    .open = sensor_open,
    .read = sensor_read,
    .close = iofunc_close_default,
};

// 主函数
int main(int argc, char *argv[]) {
    sensor_dev_t *dev;
    resmgr_attr_t resmgr_attr;
    dispatch_t *dpp;
    int id;

    // 分配设备上下文
    dev = calloc(1, sizeof(sensor_dev_t));
    // 初始化属性(权限、设备ID等)
    iofunc_attr_init(&dev->attr, S_IRUSR | S_IWUSR, 0, 0);

    // 创建分发器
    dpp = dispatch_create();
    resmgr_attr.nparts_max = 1;
    resmgr_attr.msg_max_size = 2048;

    // 注册资源管理器路径
    id = resmgr_attach(dpp, &resmgr_attr, "/dev/sensor0",
                       _FTYPE_ANY, 0, &io_funcs, NULL, dev);

    // 进入事件循环
    dispatch_block(dpp);
    return 0;
}

这段代码看着简单,但每个函数背后都有讲究。比如iofunc_attr_init里那个权限位,我建议你给传感器设备设成S_IRUSR | S_IWUSR | S_IRGRP,这样普通用户也能读,但只有root能写配置。

4.2 资源管理器编写

资源管理器(Resource Manager)是QNX驱动开发的核心概念。它本质上是一个用户态进程,通过resmgr_attach把自己挂到路径空间里。应用层调用open("/dev/sensor0")时,QNX的进程管理器(proc)会找到你这个资源管理器进程,把请求转发过来。

我曾经在项目里遇到一个坑:两个传感器驱动都注册了/dev/sensor路径,结果第二个resmgr_attach返回了-1。QNX不允许路径冲突,你得用/dev/sensor0/dev/sensor1这种命名方式,或者用设备ID做子目录。

资源管理器有两种工作模式:

模式 特点 适用场景
同步模式 回调函数在分发器线程中直接执行 简单传感器,响应快
异步模式 回调函数在工作线程池中执行 需要阻塞等待的传感器

我个人习惯用同步模式,除非传感器响应时间超过10毫秒。为什么?因为同步模式代码简单,调试方便。你想想看,一个加速度计读数据也就几十微秒,没必要搞线程池。

资源管理器还有一个重要概念叫OCB(Open Control Block)。每个open调用都会创建一个OCB,里面保存了当前打开实例的状态。比如你可以把「采样率」存在OCB的iofunc_ocb_t扩展字段里,这样不同应用打开同一个设备,可以有不同的采样率配置。

4.3 中断处理

传感器驱动里,中断处理是重头戏。QNX的中断处理跟Linux不太一样——它是在用户态处理的。你没看错,QNX允许用户态进程直接注册中断处理函数。

这样做的好处很明显:驱动崩溃了不会导致整个系统挂掉。坏处呢?中断响应延迟会比内核态高那么一点点。但对于传感器来说,微秒级的延迟完全不是问题。

注册中断的流程:

#include <sys/neutrino.h>
#include <hw/inout.h>

// 中断服务例程(ISR)
static const struct sigevent *sensor_isr(void *area, int id) {
    sensor_dev_t *dev = (sensor_dev_t *)area;
    // 读取中断状态寄存器,清除中断标志
    uint32_t status = in32(dev->base_addr + INT_STATUS);
    out32(dev->base_addr + INT_CLEAR, status);

    // 通知主线程有数据到达
    dev->event.sigev_value.sival_int = status;
    return &dev->event;
}

// 注册中断
int setup_interrupt(sensor_dev_t *dev, int irq_num) {
    // 创建事件对象
    SIGEV_INTR_INIT(&dev->event);

    // 中断号:IRQ0 + 硬件中断线
    // 我建议用InterruptAttachEvent,而不是InterruptAttach
    dev->irq_id = InterruptAttachEvent(irq_num, &dev->event,
                                        _NTO_INTR_FLAGS_TRK_MSK);
    if (dev->irq_id == -1) {
        return -1;
    }

    // 使能硬件中断
    out32(dev->base_addr + INT_ENABLE, 1);
    return 0;
}

// 主循环中等待中断
void interrupt_loop(sensor_dev_t *dev) {
    int ret;
    uint32_t status;

    while (1) {
        // 等待中断事件
        ret = InterruptWait(0, NULL);
        if (ret == -1) break;

        // 读取中断状态
        status = dev->event.sigev_value.sival_int;

        // 处理数据
        process_sensor_data(dev, status);

        // 解锁中断,允许下一次中断
        InterruptUnlock(dev->irq_id);
    }
}
注意:ISR里绝对不能做阻塞操作!不能malloc、不能printf、不能加锁。我见过有人把printf写在ISR里,结果系统直接死锁。ISR只做两件事:清除中断标志、通知主线程。其他事情都放到主循环里做。

中断处理还有一个容易被忽略的点:中断共享。有些硬件平台上,多个设备共用一条中断线。这时候你的ISR必须检查中断状态寄存器,确认是不是自己的设备触发了中断。如果不是,直接返回NULL,让下一个ISR处理。

4.4 实战经验:一个IMU驱动的完整流程

我拿一个实际的IMU(惯性测量单元)驱动来串一下整个流程。这个IMU是SPI接口的,有数据就绪中断引脚。

  1. 初始化阶段main函数里先做硬件初始化(SPI配置、IMU寄存器配置),然后注册资源管理器路径/dev/imu0,最后注册中断。
  2. 中断触发:IMU数据准备好后,拉高中断引脚。ISR被调用,读取中断状态,通过SIGEV_INTR通知主线程。
  3. 数据读取:主线程在InterruptWait返回后,通过SPI读取IMU的FIFO数据,存到环形缓冲区。
  4. 应用读取:用户程序open("/dev/imu0")后调用read,驱动从环形缓冲区拷贝最新数据返回。
小技巧:环形缓冲区的大小要跟中断频率匹配。我一般设成中断频率的2倍。比如IMU输出100Hz,缓冲区就放200个样本。这样即使应用读得慢一点,也不会丢数据。

嗯,这里还要提一个我曾经踩过的坑:中断优先级。QNX默认中断优先级很高,如果你的ISR里做了太多事情,会影响其他实时任务。我建议ISR里只做最轻量级的处理,复杂计算放到主线程里,用pulse或者event通知即可。

最后总结一下:QNX的传感器驱动开发,核心就是资源管理器 + 中断处理。资源管理器让传感器设备像文件一样被访问,中断处理让数据采集做到实时。这两块搞明白了,剩下的就是具体传感器的寄存器配置了。