第二章:字符设备驱动框架——从Hello World开始

说实话,我刚开始学驱动开发那会儿,最困惑的就是:驱动到底是个什么东西?

后来我明白了。驱动说白了就是一层软件,它告诉内核怎么跟硬件打交道。而字符设备驱动,是所有驱动里最基础、最容易上手的一种。今天我们就从它开始。

2.1 字符设备 vs 块设备 vs 网络设备

Linux把设备分成三大类。我简单说一下:

  • 字符设备:按字节流读写,比如串口、LED、按键。你读一个字节就是一个字节。
  • 块设备:按块读写,比如硬盘、eMMC。一次读512字节或4K。
  • 网络设备:通过socket访问,比如以太网、WiFi。

我们做车规芯片,大部分外设都是字符设备。像GPIO、I2C、SPI、UART,全是字符设备。所以学好这个,后面就顺了。

2.2 file_operations 结构体——驱动的灵魂

每个字符设备驱动,核心就是一个结构体:struct file_operations。它定义了驱动能干什么。

我习惯把它叫做「驱动的函数表」。内核通过这个表,知道用户调用open()时该执行哪个函数,调用read()时又该执行哪个。

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    int (*open) (struct inode *, struct file *);
    int (*release) (struct inode *, struct file *);
    // ... 还有很多
};

嗯,这里要注意:owner 一般填 THIS_MODULE,防止模块被卸载时还有人用。

我在项目中遇到过一个问题:有个同事忘了填 owner,结果模块卸载后,用户程序还在调用驱动,直接内核崩溃。从那以后,我每次写驱动第一件事就是检查这个字段。

2.3 主次设备号——驱动的身份证

每个字符设备在内核里都有一个编号。主设备号标识驱动类型,次设备号标识具体设备。

举个例子:

设备主设备号次设备号
/dev/ttyS0464
/dev/ttyS1465
/dev/ttyUSB01880

你看,同一个主设备号(4)对应同一个驱动(串口驱动),次设备号区分不同串口。

分配设备号有两种方式:

  • 静态分配:自己指定主设备号。用 register_chrdev_region()。适合你知道哪些号没人用的情况。
  • 动态分配:让内核帮你分配。用 alloc_chrdev_region()。我推荐新手用这个,省心。
我的习惯:开发阶段用动态分配,产品定型后改成静态分配。这样设备节点不会乱跳。

2.4 设备节点的创建与注销

设备节点就是 /dev/xxx 那个文件。用户程序通过它访问驱动。

创建节点有两种方法:

  1. 手动创建:用 mknod /dev/hello c 240 0。调试时方便。
  2. 自动创建:用 class_create() + device_create()。产品中必须用这个。

我建议你直接学自动创建。为什么?因为车规芯片的板子,你不可能让产线工人去敲 mknod 命令。

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

// 在模块初始化时
hello_class = class_create(THIS_MODULE, "hello_class");
hello_device = device_create(hello_class, NULL, devno, NULL, "hello");

// 在模块退出时
device_destroy(hello_class, devno);
class_destroy(hello_class);
我曾经踩过的坑:注销设备节点时,顺序搞反了。先销毁了class,再销毁device,结果内核报错。记住:先device后class,跟创建顺序相反。

2.5 Hello World 驱动——完整的例子

好了,理论说完了。我们来写一个真正的驱动。它什么都不做,就是打印一句 "Hello World"。

你想想看,一个驱动再复杂,骨架也就是这样:

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

#define DEVICE_NAME "hello"
#define CLASS_NAME  "hello_class"

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

// open 函数
static int hello_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Hello World: device opened\n");
    return 0;
}

// release 函数
static int hello_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Hello World: device closed\n");
    return 0;
}

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

// 模块初始化
static int __init hello_init(void) {
    dev_t devno;

    // 动态分配设备号
    alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
    major = MAJOR(devno);

    // 注册字符设备
    register_chrdev_region(devno, 1, DEVICE_NAME);
    cdev_init(&hello_cdev, &hello_fops);
    cdev_add(&hello_cdev, devno, 1);

    // 创建设备节点
    hello_class = class_create(THIS_MODULE, CLASS_NAME);
    hello_device = device_create(hello_class, NULL, devno, NULL, DEVICE_NAME);

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

// 模块退出
static void __exit hello_exit(void) {
    dev_t devno = MKDEV(major, 0);

    device_destroy(hello_class, devno);
    class_destroy(hello_class);

    cdev_del(&hello_cdev);
    unregister_chrdev_region(devno, 1);

    printk(KERN_INFO "Hello World driver unloaded\n");
}

module_init(hello_init);
module_exit(hello_exit);

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

这个驱动加载后,你在终端执行:

# insmod hello.ko
# echo "test" > /dev/hello
# cat /dev/hello
# rmmod hello

dmesg 就能看到打印信息。

核心要点:
  • file_operations 是驱动的接口表
  • 主次设备号是驱动的身份证
  • 设备节点是用户访问驱动的入口
  • 初始化顺序:分配设备号 → 注册cdev → 创建class → 创建device
  • 退出顺序:销毁device → 销毁class → 删除cdev → 释放设备号

2.6 避坑指南

我做了这么多年驱动,有些坑是新人必踩的。今天先给你打个预防针:

  • printk 不打印? 检查一下日志级别。用 dmesg 看,别用 cat /var/log/messages
  • insmod 失败? 看看内核版本对不对。车规芯片经常用老内核,别拿新内核的API去编。
  • open 设备节点返回 -1? 权限问题。先 chmod 666 /dev/hello 试试。
  • 卸载模块时卡死? 有进程还在用你的设备。用 lsof 查一下。

嗯,今天就到这儿。下一章我们讲如何给驱动添加读写功能,让驱动真正「动」起来。