4、QNX驱动框架:资源管理器模型、驱动入口点、open/close/read/write/devctl实现

好,咱们今天聊点硬核的。QNX的驱动开发,说白了就是写一个资源管理器(Resource Manager)。你想想看,在QNX的世界里,一切皆文件——GPIO是文件,串口是文件,连LED灯也是文件。那驱动怎么跟用户态程序打交道?就是通过这套文件操作接口。

我个人习惯把资源管理器模型比作一个“中间人”。用户程序说“我要读GPIO电平”,资源管理器就帮它去读硬件寄存器。用户程序说“我要设置输出高”,资源管理器就去写寄存器。这个模型的好处是——用户程序根本不需要知道底层细节,它只管open、read、write、devctl就完事了。

4.1 资源管理器模型的核心思想

QNX的资源管理器,本质上是一个消息驱动的服务进程。它注册到进程管理器(proc)后,就会监听来自客户端的消息。客户端通过路径名(比如/dev/gpio0)来访问它。

我刚开始接触这个模型时,总觉得有点绕。后来想明白了——其实就是把Linux里的VFS那一套,搬到了用户态。QNX的微内核只负责消息传递,文件系统的具体实现全在用户态进程里。

核心流程:

  • 驱动进程调用resmgr_attach()注册路径
  • 客户端调用open()时,内核把消息发给驱动
  • 驱动处理消息,返回结果
  • 客户端拿到文件描述符,继续read/write/devctl

4.2 驱动入口点:iofunc_funcs与resmgr_io_funcs

写驱动时,有两个结构体你必须搞清楚:iofunc_funcsresmgr_io_funcs。前者是标准POSIX操作,后者是扩展操作。

我记得第一次写QNX驱动时,把这两个结构体搞混了,结果open能成功,read却一直返回错误。排查了半天才发现是函数指针挂错了地方。

结构体 包含的函数 说明
iofunc_funcs open, close, read, write, lseek, stat, devctl 标准POSIX操作,必须实现
resmgr_io_funcs 其他IO操作(如mmap、msg_read等) 扩展操作,按需实现

实际代码中,我们通常这样初始化:

iofunc_funcs_t my_iofuncs = {
    .open = my_open,
    .close = my_close,
    .read = my_read,
    .write = my_write,
    .devctl = my_devctl,
    .lseek = my_lseek,
    .stat = my_stat
};

resmgr_io_funcs_t my_resmgr_funcs = {
    .read = my_resmgr_read,  // 如果需要更底层的控制
    .write = my_resmgr_write
};

4.3 open/close实现

open是驱动被访问的第一个入口。用户程序调用fd = open("/dev/gpio0", O_RDWR)时,驱动会收到一个_IO_OPEN消息。

这里有个坑——open函数里不要做硬件初始化。为什么?因为open可能被多次调用,但硬件只需要初始化一次。我建议在驱动启动时(main函数里)做硬件初始化,open里只做权限检查和上下文分配。

我的经验:open函数里分配一个per-client结构体,保存打开时的标志位。这样每个客户端可以有不同的访问模式。

int my_open(resmgr_context_t *ctp, io_open_t *msg, 
            RESMGR_OCB_T *ocb, io_func_t *iofunc) {
    // 权限检查
    if ((msg->attr->mode & S_IWOTH) && !is_authorized()) {
        return EACCES;
    }
    
    // 分配客户端上下文
    my_client_t *client = calloc(1, sizeof(my_client_t));
    ocb->extra = client;
    
    return EOK;
}

int my_close(resmgr_context_t *ctp, io_close_t *msg, 
             RESMGR_OCB_T *ocb) {
    // 清理客户端上下文
    my_client_t *client = ocb->extra;
    free(client);
    ocb->extra = NULL;
    
    return EOK;
}

4.4 read/write实现

read和write是驱动最常用的接口。对于GPIO驱动,read通常返回当前电平值,write则设置输出电平。

嗯,这里要注意——read和write的缓冲区大小。QNX的消息传递有大小限制,默认是64KB。如果你要传输大量数据,得用devctl或者mmap。

我曾经遇到过一个bug:用户程序read时只传了1个字节的缓冲区,但驱动返回了4个字节。结果用户程序栈被踩坏了,程序莫名其妙崩溃。排查了一下午才发现是缓冲区长度没对齐。

int my_read(resmgr_context_t *ctp, io_read_t *msg, 
            RESMGR_OCB_T *ocb) {
    int status;
    uint32_t gpio_val;
    
    // 获取当前GPIO电平
    gpio_val = gpio_read_reg(GPIO_DATA_REG);
    
    // 计算实际可读字节数
    int nbytes = min(msg->nbytes, sizeof(gpio_val));
    
    // 将数据拷贝到返回缓冲区
    status = resmgr_msgwrite(ctp, &gpio_val, nbytes, 0);
    
    if (status == -1) {
        return errno;
    }
    
    // 返回实际读取的字节数
    _IO_SET_READ_NBYTES(ctp, nbytes);
    return EOK;
}

int my_write(resmgr_context_t *ctp, io_write_t *msg, 
             RESMGR_OCB_T *ocb) {
    uint32_t gpio_val;
    
    // 从消息中读取数据
    resmgr_msgread(ctp, &gpio_val, msg->nbytes, 0);
    
    // 写入GPIO寄存器
    gpio_write_reg(GPIO_DATA_REG, gpio_val);
    
    // 返回实际写入的字节数
    _IO_SET_WRITE_NBYTES(ctp, msg->nbytes);
    return EOK;
}

4.5 devctl实现——驱动控制的瑞士军刀

devctl是QNX驱动里最灵活、最强大的接口。它允许用户程序发送自定义命令,携带任意结构体数据。说白了,read/write搞不定的事情,都交给devctl。

对于GPIO驱动,我通常用devctl实现以下功能:

  • 设置引脚方向(输入/输出)
  • 配置上下拉电阻
  • 设置中断触发方式
  • 读取引脚复用配置

避坑指南:devctl的命令码要使用_DIOTF_宏来生成,不要自己随便定义数字。我曾经见过有人用0x01、0x02这种命令码,结果跟系统命令冲突了,驱动行为变得不可预测。

// 定义命令码
#define GPIO_SET_DIRECTION _DIOTF(_DCMD_GPIO, 1, int)
#define GPIO_GET_DIRECTION _DIOTF(_DCMD_GPIO, 2, int)
#define GPIO_SET_PULL      _DIOTF(_DCMD_GPIO, 3, gpio_pull_t)

typedef struct {
    int pin;
    int pull_type;  // 0: none, 1: pull-up, 2: pull-down
} gpio_pull_t;

int my_devctl(resmgr_context_t *ctp, io_devctl_t *msg, 
              RESMGR_OCB_T *ocb) {
    int status;
    
    switch (msg->i.dcmd) {
        case GPIO_SET_DIRECTION: {
            int direction;
            status = resmgr_msgread(ctp, &direction, 
                                    sizeof(direction), 0);
            if (status != sizeof(direction)) {
                return EFAULT;
            }
            gpio_set_direction(direction);
            break;
        }
        
        case GPIO_GET_DIRECTION: {
            int direction = gpio_get_direction();
            status = resmgr_msgwrite(ctp, &direction, 
                                     sizeof(direction), 0);
            if (status != sizeof(direction)) {
                return EFAULT;
            }
            break;
        }
        
        case GPIO_SET_PULL: {
            gpio_pull_t pull_cfg;
            status = resmgr_msgread(ctp, &pull_cfg, 
                                    sizeof(pull_cfg), 0);
            if (status != sizeof(pull_cfg)) {
                return EFAULT;
            }
            gpio_set_pull(pull_cfg.pin, pull_cfg.pull_type);
            break;
        }
        
        default:
            return ENOTTY;  // 不认识这个命令
    }
    
    // 设置返回状态
    msg->o.ret_val = EOK;
    msg->o.nbytes = 0;
    
    return EOK;
}

4.6 完整的驱动注册流程

把上面这些函数串起来,就是完整的驱动注册流程。我一般这样写main函数:

int main(int argc, char *argv[]) {
    resmgr_attr_t resmgr_attr;
    dispatch_t *dpp;
    dispatch_context_t *ctp;
    int id;
    
    // 1. 初始化硬件
    gpio_hw_init();
    
    // 2. 创建dispatch对象
    dpp = dispatch_create();
    if (dpp == NULL) {
        return EXIT_FAILURE;
    }
    
    // 3. 初始化资源管理器属性
    memset(&resmgr_attr, 0, sizeof(resmgr_attr));
    resmgr_attr.nparts_max = 1;
    resmgr_attr.msg_max_size = 2048;
    
    // 4. 注册路径
    id = resmgr_attach(dpp, &resmgr_attr, "/dev/gpio0",
                       _FTYPE_ANY, 0,
                       &my_resmgr_funcs, &my_iofuncs);
    if (id == -1) {
        return EXIT_FAILURE;
    }
    
    // 5. 进入消息循环
    ctp = dispatch_context_alloc(dpp);
    while (1) {
        ctp = dispatch_block(ctp);
        if (ctp == NULL) {
            break;
        }
        dispatch_handler(ctp);
    }
    
    return EXIT_SUCCESS;
}

你看,整个流程其实很清晰。先初始化硬件,然后创建dispatch,注册路径,最后进入消息循环等待客户端请求。每个客户端请求进来,框架会自动调用对应的处理函数。

个人建议:消息循环里可以加一个信号处理,比如捕获SIGTERM做优雅退出。我在产品中就是这样做的——收到退出信号时,先关闭所有GPIO输出,再释放资源,最后退出进程。这样能避免驱动退出后GPIO引脚状态不确定的问题。

好了,这一章的内容就到这里。资源管理器模型是QNX驱动的基石,理解了它,后面写任何驱动都会顺手很多。下一章我们会深入GPIO中断的处理,那才是真正考验驱动功底的地方。