4. 实现设备内存映射

任务目标

在QNX驱动中实现mmap设备接口,将设备物理内存映射到用户空间,使用户程序可直接读写设备寄存器或内存缓冲区,避免每次访问都经过devctl()系统调用。

实现原理

QNX资源管理器框架中,mmap通过io_mmap消息处理函数实现。驱动在io_funcs表中注册mmap回调,当用户程序调用mmap()时,内核将请求路由到驱动。驱动需要将设备物理地址转换为QNX的uint64_t物理地址格式,并通过mmap_device_io()或mmap_device_memory()建立映射。

核心代码实现

// dev_mmap.c - 设备内存映射驱动示例
#include <sys/iofunc.h>
#include <sys/dispatch.h>
#include <hw/inout.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

// 设备物理地址配置(示例:假设设备寄存器位于0x28000000,长度4KB)
#define DEVICE_PHYS_ADDR  0x28000000ULL
#define DEVICE_MAP_SIZE   0x1000

// 驱动属性结构
typedef struct {
    iofunc_attr_t attr;
    uintptr_t     mapped_addr;   // 驱动内部映射地址(用于验证)
} device_attr_t;

// mmap回调:将设备物理内存映射到用户空间
int io_mmap_handler(resmgr_context_t *ctp, io_mmap_t *msg,
                    iofunc_attr_t *attr, void *extra)
{
    device_attr_t *dev_attr = (device_attr_t *)attr;
    uint64_t phys_addr = DEVICE_PHYS_ADDR;
    uint64_t map_len = DEVICE_MAP_SIZE;

    // 检查用户请求的映射范围是否合法
    if (msg->offset + msg->len > map_len) {
        return EINVAL;
    }

    // 计算实际物理地址(支持偏移映射)
    phys_addr += msg->offset;

    // 返回物理地址给内核,由内核完成用户空间映射
    msg->phys = phys_addr;
    msg->len = msg->len ? msg->len : (map_len - msg->offset);

    // 标记为设备内存(非缓存)
    msg->flags = IO_MMAP_FLAG_DEVICE_MEMORY;

    return EOK;
}

// 设备打开回调
int io_open_handler(resmgr_context_t *ctp, io_open_t *msg,
                    iofunc_attr_t *attr, void *extra)
{
    device_attr_t *dev_attr = (device_attr_t *)attr;

    // 驱动内部建立映射,用于验证设备可访问
    dev_attr->mapped_addr = (uintptr_t)mmap_device_io(DEVICE_MAP_SIZE,
                                                      DEVICE_PHYS_ADDR);
    if (dev_attr->mapped_addr == (uintptr_t)MAP_FAILED) {
        return errno;
    }

    return iofunc_open_default(ctp, msg, attr, extra);
}

// 设备关闭回调
int io_close_handler(resmgr_context_t *ctp, void *reserved,
                     iofunc_attr_t *attr, void *extra)
{
    device_attr_t *dev_attr = (device_attr_t *)attr;

    if (dev_attr->mapped_addr) {
        munmap_device_io(dev_attr->mapped_addr, DEVICE_MAP_SIZE);
        dev_attr->mapped_addr = 0;
    }

    return iofunc_close_default(ctp, reserved, attr, extra);
}

int main(int argc, char *argv[])
{
    dispatch_t *dpp;
    resmgr_io_funcs_t io_funcs;
    resmgr_connect_funcs_t connect_funcs;
    device_attr_t dev_attr;
    int id;

    // 初始化属性
    memset(&dev_attr, 0, sizeof(dev_attr));
    iofunc_attr_init(&dev_attr.attr, S_IFCHR | 0666, 0, 0);

    // 创建分发器
    dpp = dispatch_create();
    if (dpp == NULL) {
        perror("dispatch_create");
        return 1;
    }

    // 注册连接函数
    iofunc_funcs_init(&connect_funcs, &io_funcs);
    connect_funcs.open = io_open_handler;
    connect_funcs.close = io_close_handler;
    io_funcs.mmap = io_mmap_handler;  // 注册mmap回调

    // 创建资源管理器
    id = resmgr_attach(dpp, NULL, "/dev/devmap", _FTYPE_ANY, 0,
                       &connect_funcs, &io_funcs, &dev_attr);
    if (id == -1) {
        perror("resmgr_attach");
        return 1;
    }

    printf("Device mmap driver started at /dev/devmap\n");
    printf("Physical address: 0x%llx, size: 0x%x\n",
           DEVICE_PHYS_ADDR, DEVICE_MAP_SIZE);

    // 进入消息循环
    dispatch_block(dpp);
    return 0;
}

编译与运行

# 编译驱动(QNX SDP环境)
qcc -Vgcc_ntox86_64 -o dev_mmap dev_mmap.c -lresmgr -lposix

# 运行驱动(需root权限)
./dev_mmap

# 用户空间测试程序(test_mmap.c)
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdint.h>

int main() {
    int fd = open("/dev/devmap", O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    // 映射设备内存(映射全部4KB)
    volatile uint8_t *map = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                                 MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) { perror("mmap"); return 1; }

    // 读取设备寄存器(示例:读取偏移0x00处的值)
    uint32_t reg_val = *(volatile uint32_t *)map;
    printf("Read register at offset 0x00: 0x%08x\n", reg_val);

    // 写入测试(注意:实际设备寄存器可能只读)
    // *(volatile uint32_t *)map = 0x12345678;

    munmap((void *)map, 0x1000);
    close(fd);
    return 0;
}

# 编译测试程序
qcc -Vgcc_ntox86_64 -o test_mmap test_mmap.c

# 运行测试
./test_mmap

验证方法

  1. 驱动启动后,检查/dev/devmap设备节点是否存在:ls -l /dev/devmap
  2. 运行test_mmap,若映射成功,程序将打印设备寄存器偏移0x00处的值(具体值取决于硬件);若映射失败,mmap()返回MAP_FAILED并打印错误信息。
  3. 使用pidin syspage确认驱动进程存在,使用pidin mem可查看进程内存映射状态。

通过标准:测试程序能成功完成mmap()调用并读取到设备内存值,且驱动无错误输出。若设备物理地址0x28000000在目标硬件上不存在,可替换为实际设备地址后重新编译测试。

关键说明

  • 本示例使用IO_MMAP_FLAG_DEVICE_MEMORY标记设备内存,禁止缓存,确保读写直接访问硬件寄存器。
  • 驱动内部通过mmap_device_io()建立映射用于验证设备可访问性,实际驱动中可根据需要保留或移除。
  • 用户空间mmap()的offset参数对应设备物理地址偏移,本实现支持按页偏移映射。
  • 若设备内存大于4KB,需调整DEVICE_MAP_SIZE并确保物理地址连续。