嵌入式开发 嵌入式 Linux 驱动开发入门:编写你的第一个字符设备驱动
为什么需要学驱动开发?
做嵌入式开发这么久,一直都是在用户空间折腾:写应用、调库、玩框架。但有时候你会发现,有些功能用户空间就是搞不定——比如精确的时序控制、直接操作寄存器、或者硬件中断处理。这时候,你就需要踏入内核空间,写驱动了。
很多人对内核驱动有畏惧心理,觉得”内核编程很危险,搞错了系统就挂了”。确实如此,但没那么可怕。今天我们就从零开始,写一个最简单的字符设备驱动,让你迈出第一步。
需要准备什么?
| 物品 | 型号/规格 | 价格 |
|---|---|---|
| 开发板 | 树莓派 4B / Jetson Nano | ¥350-500 |
| 或者 | 任何运行 Linux 的 ARM 板子 | - |
| 电脑 | 用于交叉编译 | 已有 |
| 杜邦线 | 公对母若干 | ¥10 |
| LED | 5mm 红色 | ¥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
-
$1
-
$1
-
$1
字符设备驱动是最基础的驱动类型,掌握了这个,后续学习平台设备驱动、设备树、中断处理等高级内容会容易很多。
进阶建议:
-
学习设备树(Device Tree)配置
-
理解中断处理和底半部机制
-
研究平台设备驱动模型
-
阅读内核源码中的参考驱动
希望这篇博客文章对您有所帮助!
相关资源: