嵌入式 Linux 驱动开发入门:编写你的第一个字符设备驱动

嵌入式 Linux 驱动开发入门:编写你的第一个字符设备驱动

为什么需要学驱动开发?

做嵌入式开发这么久,一直都是在用户空间折腾:写应用、调库、玩框架。但有时候你会发现,有些功能用户空间就是搞不定——比如精确的时序控制、直接操作寄存器、或者硬件中断处理。这时候,你就需要踏入内核空间,写驱动了。

很多人对内核驱动有畏惧心理,觉得”内核编程很危险,搞错了系统就挂了”。确实如此,但没那么可怕。今天我们就从零开始,写一个最简单的字符设备驱动,让你迈出第一步。

需要准备什么?

物品型号/规格价格
开发板树莓派 4B / Jetson Nano¥350-500
或者任何运行 Linux 的 ARM 板子-
电脑用于交叉编译已有
杜邦线公对母若干¥10
LED5mm 红色¥0.5
电阻220Ω¥0.1
总计¥360-510

如果你已经有树莓派或者 Jetson Nano,那成本几乎为零。没有的话,用虚拟机跑 Ubuntu 也能学习大部分内容(只是没法操作真实 GPIO)。

步骤 1:搭建内核开发环境

首先得安装内核头文件和编译工具链。以 Ubuntu/Debian 为例:

# 更新包列表
sudo apt-get update

# 安装内核头文件和编译工具
sudo apt-get install linux-headers-$(uname -r) build-essential dkms

# 验证安装
ls /lib/modules/$(uname -r)/build

如果看到一堆头文件,说明安装成功了。⚠️ 注意: 内核头文件版本必须和当前运行的内核版本一致,否则编译出来的模块加载会失败。

检查内核版本:

uname -r
# 输出示例:5.15.0-76-generic

步骤 2:编写第一个内核模块

先来个”Hello World”级别的模块,感受一下内核编程的套路。创建文件 hello_module.c

#include 
#include 
#include 

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Kernel! Module loaded.\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel! Module unloaded.\n");
}

module_init(hello_init);
module_exit(hello_exit);

几个关键点:

  • MODULE_LICENSE("GPL"):必须声明许可证,否则内核会”污染”警告

  • __init__exit:告诉内核这些函数只在加载/卸载时调用,可以释放内存

  • printk:内核里的 printf,用 KERN_INFO 等宏指定日志级别

编写 Makefile:

obj-m += hello_module.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
    make -C $(KDIR) M=$(PWD) modules

clean:
    make -C $(KDIR) M=$(PWD) clean

编译:

make

如果一切顺利,会生成 hello_module.ko 文件。加载它:

# 加载模块
sudo insmod hello_module.ko

# 查看内核日志
dmesg | tail -5
# 应该看到:Hello, Kernel! Module loaded.

# 卸载模块
sudo rmmod hello_module

# 再次查看日志
dmesg | tail -5
# 应该看到:Goodbye, Kernel! Module unloaded.

恭喜,你已经成功编写并运行了第一个内核模块!

步骤 3:编写字符设备驱动

Hello World 太简单了,来点真的。我们要写一个字符设备驱动,可以通过 /dev 文件进行读写。

创建 char_device.c

#include 
#include 
#include 
#include 
#include 
#include 

#define DEVICE_NAME "mychardev"
#define CLASS_NAME "myclass"

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

// 设备号
static int major_number;
static struct class* char_class = NULL;
static struct device* char_device = NULL;
static struct cdev char_cdev;

// 设备缓冲区
static char message_buffer[256] = {0};
static int message_size = 0;

// 文件操作函数
static int dev_open(struct inode *inodep, struct file *filep)
{
    printk(KERN_INFO "Device opened\n");
    return 0;
}

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

static ssize_t dev_read(struct file *filep, char __user *buffer, 
                        size_t len, loff_t *offset)
{
    int bytes_to_read;
    int bytes_read;

    // 计算要读取的字节数
    bytes_to_read = min((int)(message_size - *offset), (int)len);
    if (bytes_to_read  /dev/mychardev

# 读取数据
cat /dev/mychardev
# 应该输出:Hello from user space!

# 查看内核日志
dmesg | tail -10

步骤 4:添加 GPIO 控制功能

光读写字符串太无聊了,我们来点硬件交互。添加 GPIO 控制,让 LED 闪烁。

在原有代码基础上添加 GPIO 操作(以树莓派 GPIO 17 为例):

#include 
#include 

#define LED_GPIO 17

// 在初始化函数中添加
ret = gpio_request(LED_GPIO, "mychardev-led");
if (ret) {
    printk(KERN_ALERT "Failed to request GPIO %d\n", LED_GPIO);
    // 错误处理...
}
gpio_direction_output(LED_GPIO, 0);

// 添加 ioctl 命令控制 LED
#include 

#define IOCTL_MAGIC 'k'
#define IOCTL_LED_ON _IO(IOCTL_MAGIC, 1)
#define IOCTL_LED_OFF _IO(IOCTL_MAGIC, 2)
#define IOCTL_LED_TOGGLE _IO(IOCTL_MAGIC, 3)

static long dev_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
    switch (cmd) {
        case IOCTL_LED_ON:
            gpio_set_value(LED_GPIO, 1);
            printk(KERN_INFO "LED turned ON\n");
            break;
        case IOCTL_LED_OFF:
            gpio_set_value(LED_GPIO, 0);
            printk(KERN_INFO "LED turned OFF\n");
            break;
        case IOCTL_LED_TOGGLE:
            gpio_set_value(LED_GPIO, !gpio_get_value(LED_GPIO));
            printk(KERN_INFO "LED toggled\n");
            break;
        default:
            return -EINVAL;
    }
    return 0;
}

// 更新 file_operations
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = dev_open,
    .release = dev_release,
    .read = dev_read,
    .write = dev_write,
    .unlocked_ioctl = dev_ioctl,
};

在退出函数中释放 GPIO:

gpio_free(LED_GPIO);

用户空间测试程序 test_gpio.c

#include 
#include 
#include 
#include 

#define IOCTL_MAGIC 'k'
#define IOCTL_LED_ON _IO(IOCTL_MAGIC, 1)
#define IOCTL_LED_OFF _IO(IOCTL_MAGIC, 2)
#define IOCTL_LED_TOGGLE _IO(IOCTL_MAGIC, 3)

int main()
{
    int fd = open("/dev/mychardev", O_RDWR);
    if (fd ` 和 ``,ioctl 命令定义要一致

**问题 5:内核日志太多刷屏**

- **原因:** printk 级别设置过低

- **解决:** 使用 `KERN_DEBUG` 代替 `KERN_INFO`,或者调整控制台日志级别:

```bash
echo "4" > /proc/sys/kernel/printk

总结

从零开始写内核驱动,其实没有想象中那么可怕。关键步骤:

  1. $1

  2. $1

  3. $1

  4. $1

  5. $1

字符设备驱动是最基础的驱动类型,掌握了这个,后续学习平台设备驱动、设备树、中断处理等高级内容会容易很多。

进阶建议:

  • 学习设备树(Device Tree)配置

  • 理解中断处理和底半部机制

  • 研究平台设备驱动模型

  • 阅读内核源码中的参考驱动

希望这篇博客文章对您有所帮助!


相关资源: