第三讲:字符设备驱动基础

各位同学,今天我们来聊聊字符设备驱动的基础。说实话,这部分内容是我当年入门嵌入式驱动开发时,啃得最久的一块硬骨头。但一旦啃下来,后面的路就顺多了。

Linux设备驱动模型:别被名字吓到

Linux设备驱动模型,说白了就是一套管理硬件的框架。你想想看,系统里插了那么多设备——USB、I2C、SPI、PCI——总得有个统一的方式去管理它们吧?

这个模型的核心思想其实很简单:把设备抽象成文件。对,你没听错。在Linux世界里,一切皆文件。你的鼠标是文件,键盘是文件,连你插的U盘也是文件。

我个人习惯把设备驱动模型理解成三层结构:

  • 总线层:负责设备和驱动之间的匹配。比如你插了个USB鼠标,USB总线就会去找对应的驱动。
  • 设备层:描述硬件本身的信息,比如厂商ID、设备ID、中断号等。
  • 驱动层:真正干活的部分,负责初始化硬件、处理数据。

我在项目中遇到过一个问题:一个I2C传感器死活不工作,查了两天才发现是设备树里把地址写错了。嗯,这种低级错误,犯过一次就记住了。

file_operations结构体:驱动的灵魂

这个结构体,可以说是字符设备驱动的核心。它定义了用户空间程序怎么跟你的硬件打交道。

来看一个典型的file_operations结构体:

struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
    .llseek  = my_llseek,
};

每个成员都是一个函数指针。当用户程序调用open()时,内核就会调用你注册的my_open函数。说白了,这就是一张函数跳转表。

常用的操作有这些:

操作 说明 我踩过的坑
open 打开设备,做初始化 记得检查设备是否已被占用
release 关闭设备,释放资源 别忘了释放申请的内存
read 从设备读取数据 注意用户空间指针的合法性
write 向设备写入数据 数据长度检查不能省
llseek 改变文件读写位置 字符设备一般不支持随机访问
注意:千万不要在open/release里做耗时操作。我曾经在open里做了个硬件复位,结果用户程序打开设备卡了500ms,直接被上层投诉了。

模块加载与卸载:驱动的一生

Linux驱动有两种加载方式:编译进内核,或者作为模块动态加载。我们做开发时,99%的情况都用模块方式。为什么?方便调试啊!改个代码重新编译模块,insmod一下就行了,不用重启整个系统。

模块的生命周期很简单:

  1. 加载:调用module_init指定的函数
  2. 运行:等待用户程序调用
  3. 卸载:调用module_exit指定的函数

来看一个标准的模块框架:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>

static int __init my_init(void)
{
    printk(KERN_INFO "Hello, driver world!\n");
    return 0;
}

static void __exit my_exit(void)
{
    printk(KERN_INFO "Goodbye, driver world!\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character driver");

这里有几个细节要注意:

  • __init__exit宏:告诉内核这些函数只在初始化/退出时使用,用完后可以释放内存
  • MODULE_LICENSE:必须声明,否则内核会报"tainted"警告
  • printk:内核空间的打印函数,相当于用户空间的printf
小技巧:调试时多用printk,但正式发布前记得删掉或改成pr_debug。我曾经有个驱动,上线后dmesg被刷爆了,全是调试信息...

Hello World驱动:从零开始

好了,理论说完了,我们来写一个真正的Hello World驱动。这个驱动什么都不做,只是注册一个设备节点,让用户程序能打开它。

完整代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define DEVICE_NAME "hello_dev"
#define CLASS_NAME  "hello_class"

static int major;
static struct class *hello_class;
static struct device *hello_device;

static int hello_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device opened\n");
    return 0;
}

static int hello_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device closed\n");
    return 0;
}

static struct file_operations hello_fops = {
    .owner   = THIS_MODULE,
    .open    = hello_open,
    .release = hello_release,
};

static int __init hello_init(void)
{
    // 动态分配主设备号
    major = register_chrdev(0, DEVICE_NAME, &hello_fops);
    if (major < 0) {
        printk(KERN_ALERT "Failed to register device\n");
        return major;
    }

    // 创建设备类
    hello_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(hello_class)) {
        unregister_chrdev(major, DEVICE_NAME);
        return PTR_ERR(hello_class);
    }

    // 创建设备节点
    hello_device = device_create(hello_class, NULL, 
                                 MKDEV(major, 0), NULL, DEVICE_NAME);
    if (IS_ERR(hello_device)) {
        class_destroy(hello_class);
        unregister_chrdev(major, DEVICE_NAME);
        return PTR_ERR(hello_device);
    }

    printk(KERN_INFO "Hello driver loaded, major=%d\n", major);
    return 0;
}

static void __exit hello_exit(void)
{
    device_destroy(hello_class, MKDEV(major, 0));
    class_destroy(hello_class);
    unregister_chrdev(major, DEVICE_NAME);
    printk(KERN_INFO "Hello driver unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded AI Driver Expert");
MODULE_DESCRIPTION("Hello World Character Driver");

编译和测试步骤:

  1. 写一个Makefile,编译生成.ko文件
  2. sudo insmod hello.ko 加载模块
  3. 查看/dev/hello_dev设备节点是否创建成功
  4. cat /dev/hello_dev测试打开设备
  5. sudo rmmod hello 卸载模块

关键点总结:

  • register_chrdev分配主设备号,0表示让内核自动分配
  • class_create和device_create自动创建设备节点
  • 卸载时一定要按相反顺序释放资源

为什么会强调卸载顺序?我曾经在项目里先卸载了设备类,再卸载字符设备,结果内核直接panic了。从那以后,我每次写驱动都会在注释里标清楚资源释放的顺序。

好了,这一讲的内容就到这里。字符设备驱动是嵌入式AI芯片驱动开发的基础,后面的I2C、SPI、DMA等复杂驱动,都是在这个框架上搭建的。下一讲我们会深入设备树,看看怎么让驱动自动匹配硬件。