3、NPU驱动框架设计:Linux内核驱动框架(platform driver)、字符设备驱动模型、file_operations接口实现、设备树(Device Tree)配置

好,咱们今天聊聊NPU驱动的骨架怎么搭。说白了,驱动框架就是让NPU这个硬件在Linux系统里“活”起来的基础设施。我做了这么多年嵌入式,见过太多驱动跑不起来,最后发现是框架没搭对——不是设备树配错了,就是file_operations没注册上。嗯,咱们一步步来。

3.1 Linux内核驱动框架:为什么选platform driver?

先问个问题:NPU在系统里算个什么设备?它不是USB,不是PCIe,也不是I2C设备。它通常挂在系统总线上,比如AMBA总线。那Linux里怎么描述它?答案就是platform driver。

我个人习惯把platform driver理解成“万能插座”。只要你的设备不满足那些标准总线(USB、PCI等)的规范,就用platform driver来驱动。NPU正好符合这个特征——它有自己的寄存器、中断、DMA通道,但不需要走USB协议栈。

核心要点:platform driver = 设备 + 驱动 + 匹配规则。匹配规则通常通过设备树中的compatible属性完成。

我在项目中遇到过一个问题:同事把NPU写成了I2C客户端驱动,结果中断响应延迟高得离谱。后来改成platform driver,问题迎刃而解。为什么?因为platform driver直接操作内存映射寄存器,没有协议栈的额外开销。

3.2 字符设备驱动模型:NPU的用户态接口

NPU驱动最终要给用户态程序用。怎么用?最常见的方式就是字符设备。你想想看,用户程序打开一个设备文件,比如/dev/npu0,然后通过read/write/ioctl来和NPU交互。这就是字符设备模型。

字符设备的核心数据结构是struct cdev。我建议你把它想象成一个“接线板”——每个cdev对应一个设备号,用户程序通过设备号找到对应的驱动函数。

// 字符设备注册的典型流程
static int __init npu_init(void)
{
    dev_t dev_num;
    struct cdev *npu_cdev;

    // 1. 分配设备号
    alloc_chrdev_region(&dev_num, 0, 1, "npu");

    // 2. 初始化cdev
    npu_cdev = cdev_alloc();
    cdev_init(npu_cdev, &npu_fops);

    // 3. 添加到内核
    cdev_add(npu_cdev, dev_num, 1);

    return 0;
}

避坑指南:我曾经在cdev_add之前忘了检查alloc_chrdev_region的返回值,结果设备号冲突,驱动加载后用户态打不开设备文件。记住:每一步都要检查返回值!

3.3 file_operations接口实现:驱动与用户态的数据通道

用户程序调用open、read、write时,最终会落到驱动里的file_operations结构体。这个结构体就是驱动的“门面”。NPU驱动里,我通常实现以下几个关键接口:

接口 作用 NPU场景下的实现要点
open 打开设备 检查设备是否被占用,初始化硬件状态
release 关闭设备 清理DMA缓冲区,复位NPU
ioctl 控制命令 加载模型、启动推理、获取结果
mmap 内存映射 将NPU的DDR缓冲区映射到用户态,减少拷贝

这里我特别想强调ioctl。NPU不像串口那样读写字节流,它需要传递复杂的控制结构——比如模型地址、输入输出张量描述。ioctl正好适合干这个。我习惯把ioctl命令定义成宏,比如:

#define NPU_IOCTL_LOAD_MODEL   _IOW('N', 1, struct npu_model_info)
#define NPU_IOCTL_RUN_INFERENCE _IOWR('N', 2, struct npu_inference_cfg)
#define NPU_IOCTL_GET_RESULT   _IOR('N', 3, struct npu_result)

注意:ioctl的第三个参数是用户态指针,一定要用copy_from_user/copy_to_user来访问。直接解引用用户指针会导致内核崩溃。我曾经在这个坑里摔过两次,后来养成了习惯:所有用户数据都先拷贝到内核缓冲区再操作。

3.4 设备树(Device Tree)配置:让内核认识你的NPU

设备树是啥?说白了就是硬件的“身份证”。内核启动时通过设备树知道:哦,这里有个NPU,它的寄存器地址是0xA0000000,中断号是42。没有设备树,platform driver就找不到设备。

NPU的设备树节点通常长这样:

npu: npu@a0000000 {
    compatible = "mycompany,npu-v1";
    reg = <0x0 0xa0000000 0x0 0x10000>;
    interrupts = <0 42 4>;
    clocks = <&ccu CLK_NPU>;
    resets = <&ccu RST_NPU>;
    status = "okay";
};

这里每个字段都有讲究:

  • compatible:驱动匹配的关键。驱动里用of_match_table来匹配这个字符串。
  • reg:寄存器基地址和大小。驱动里通过platform_get_resource获取。
  • interrupts:中断号。NPU推理完成通常会产生中断。
  • clocks/resets:时钟和复位。NPU上电前必须使能时钟。

个人经验:我建议在设备树里把NPU的DMA缓冲区大小也配进去,比如加一个dma-pool-size = <0x1000000>;。这样驱动初始化时可以直接从设备树读取,不用硬编码。硬编码的教训我吃过——换了一个NPU型号,缓冲区大小变了,还得重新编译驱动。

3.5 知识体系总览

说了这么多,咱们用一张图把整个框架串起来。下面这张SVG图展示了NPU驱动的核心层次:

NPU驱动框架核心层次 用户态应用程序 open / ioctl / mmap / close 字符设备层 (file_operations) npu_open / npu_ioctl / npu_mmap / npu_release Platform驱动层 (platform_driver) probe / remove / suspend / resume 硬件抽象层 (HAL) 寄存器读写 / 中断处理 / DMA管理 / 时钟控制 设备树 (Device Tree) 配置 用户空间 内核空间

从这张图可以清楚看到:用户态程序通过字符设备接口进入内核,经过file_operations分发到platform驱动,platform驱动再调用HAL层操作硬件。而设备树则贯穿始终,为每一层提供硬件配置信息。

3.6 小结

嗯,这一章的内容就这些。总结一下:

  • platform driver是NPU驱动的骨架,负责和设备树匹配、管理硬件资源。
  • 字符设备模型是用户态和内核态的桥梁,通过cdev注册设备节点。
  • file_operations是具体的数据通道,ioctl是NPU控制的核心接口。
  • 设备树是硬件的描述文件,驱动通过它获取寄存器地址、中断号等关键信息。

我个人觉得,框架设计就像盖房子打地基。地基没打好,后面加再多功能也容易塌。所以这一章的内容,我建议你动手写一遍代码,哪怕只是注册一个空的platform driver,也比只看不练强得多。

一句话记住:NPU驱动 = platform driver做骨架 + 字符设备做接口 + file_operations做通道 + 设备树做配置。四者缺一不可。

想要更多资料,联系可可