嵌入式 Linux 驱动概述:分类、内核模块、开发环境
嵌入式 Linux 驱动是连接硬件和应用的桥梁。这篇从零讲起:驱动是什么、分哪几类、内核模块机制、开发环境怎么搭,带你入门 Linux 驱动开发。
大家好,我是黒漂技术佬。
做嵌入式的,早晚要碰驱动。应用层调用 open/read/write 就能操作硬件,背后都是驱动在干活。驱动直接跟内核、跟硬件打交道,门槛比应用层高,但也更底层、更有意思。
这个系列 12 篇,从内核模块到字符设备、中断、定时器、并发同步、设备树、I2C/SPI,最后来个传感器驱动实战。
第一篇先讲基础:驱动是什么、分类、内核模块机制、开发环境搭建。
一、Linux 驱动是什么?
作用
驱动程序是硬件和应用之间的桥梁:
应用程序 → 系统调用 → 驱动程序 → 硬件
- 应用层:用 open、read、write、ioctl 这些标准接口
- 驱动层:把这些操作翻译成硬件能懂的读写寄存器、中断处理等
- 硬件:实际干活的外设(GPIO、I2C、SPI、显示屏、传感器......)
为什么需要驱动?
- 统一接口:不管什么硬件,应用层都用 open/read/write,不用管硬件差异
- 权限隔离:硬件操作在内核态,应用在用户态,安全稳定
- 复用性:一套驱动可以给多个应用用
二、Linux 驱动的分类
Linux 驱动主要分三大类:
1. 字符设备驱动(Character Device)
- 按字节流顺序读写的设备
- 最常见、最简单的一类
- 例子:串口、GPIO、LED、按键、I2C 设备、SPI 设备
- 设备文件在
/dev/xxx,主设备号+次设备号标识
2. 块设备驱动(Block Device)
- 按块(一般 512 字节或 4KB)随机读写的设备
- 主要是存储类设备
- 例子:硬盘、SD 卡、eMMC、U盘
- 有缓存机制,比字符设备复杂
3. 网络设备驱动(Network Device)
- 负责网络数据包收发
- 不走
/dev文件系统,用 socket 接口 - 例子:以太网、WiFi
- 最复杂的一类驱动
其他子系统
除了三大类,还有很多专门的子系统:
- input 子系统:按键、触摸屏、鼠标
- framebuffer / DRM:显示驱动
- V4L2:摄像头驱动
- ALSA:音频驱动
- USB 驱动:USB 设备
- PCIe 驱动:PCIe 设备
- pinctrl / GPIO 子系统:引脚控制
这些底层还是基于字符设备/平台设备,但内核提供了专门的框架。
本系列重点
入门系列以字符设备驱动为主,这是最基础、最常用的。学会了字符设备,其他类别的驱动思路是通的。
三、内核模块(Kernel Module)
是什么
Linux 驱动一般以内核模块的形式存在,可以动态加载和卸载,不用重新编译内核。
bash
# 加载模块
insmod xxx.ko
modprobe xxx
# 卸载模块
rmmod xxx
# 查看已加载模块
lsmod
.ko 文件就是内核模块文件。
为什么用模块?
- 灵活:需要的时候加载,不用就卸载,节省内存
- 开发方便:改了驱动重新加载就行,不用重启内核
- 可裁剪:嵌入式系统按需加载,减小内核体积
最简单的内核模块
c
#include <linux/init.h>
#include <linux/module.h>
// 模块加载时调用
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Linux Kernel!\n");
return 0;
}
// 模块卸载时调用
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel!\n");
}
module_init(hello_init); // 注册入口函数
module_exit(hello_exit); // 注册出口函数
MODULE_LICENSE("GPL"); // 许可证,必须有
MODULE_AUTHOR("Heipiao");
MODULE_DESCRIPTION("Hello World Module");
关键宏说明
| 宏 | 作用 |
|---|---|
module_init |
模块加载时调用的函数 |
module_exit |
模块卸载时调用的函数 |
__init |
标记初始化函数,加载完后释放内存 |
__exit |
标记退出函数,只在卸载时用 |
MODULE_LICENSE |
模块许可证,GPL 才能用 GPL 的内核函数 |
MODULE_AUTHOR |
作者 |
MODULE_DESCRIPTION |
描述 |
MODULE_ALIAS |
别名 |
printk
内核里打印用 printk,不是 printf。带日志级别:
KERN_EMERG 紧急
KERN_ALERT 警报
KERN_CRIT 严重
KERN_ERR 错误
KERN_WARNING 警告
KERN_NOTICE 注意
KERN_INFO 信息
KERN_DEBUG 调试
用 dmesg 看内核日志。
四、驱动开发环境
1. 硬件开发板
入门推荐:
- 树莓派:资料多,社区大,适合入门
- 正点原子 / 野火 IMX6ULL:国产教程多,配套齐全
- RK3588:性能强,AI 相关开发常用
- QEMU 模拟:没有板子也能学,用虚拟机
2. 交叉编译工具链
在 x86 电脑上编译 ARM 平台的代码,需要交叉编译器:
bash
# ARM 32位
arm-linux-gnueabihf-gcc
# ARM 64位
aarch64-linux-gnu-gcc
3. Linux 内核源码
编译驱动需要对应版本的内核源码(或内核头文件),版本要跟开发板上运行的内核一致。
内核源码树/
├── arch/ 架构相关
├── drivers/ 驱动源码
├── include/ 头文件
├── fs/ 文件系统
├── kernel/ 内核核心
├── mm/ 内存管理
├── net/ 网络
└── Makefile
4. Makefile
编译内核模块的 Makefile 很固定:
makefile
# 内核源码路径,根据实际情况改
KERNELDIR := /path/to/linux-kernel
# 当前目录
PWD := $(shell pwd)
# 交叉编译器
CROSS_COMPILE := arm-linux-gnueabihf-
ARCH := arm
obj-m := hello.o # 要编译的模块
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE)
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
执行 make 就能生成 hello.ko。
5. 加载测试
bash
# 传到开发板上
scp hello.ko root@192.168.1.100:/tmp/
# 开发板上加载
insmod hello.ko
dmesg | tail # 看打印
# 卸载
rmmod hello
dmesg | tail
五、内核态 vs 用户态
区别
| 用户态 | 内核态 | |
|---|---|---|
| 权限 | 受限,不能直接操作硬件 | 最高权限,操作所有硬件 |
| 内存 | 独立的用户空间 | 共享内核空间 |
| API | C 库、系统调用 | 内核函数(printk, kmalloc...) |
| 稳定性 | 崩溃了只是进程挂了 | 崩溃了整个系统挂 |
| 调试 | gdb 方便 | 调试麻烦,kgdb / printk |
驱动运行在内核态
驱动代码运行在内核态,所以:
- 不能用 C 库函数(printf、malloc 不行)
- 要用内核提供的函数(printk、kmalloc、copy_to_user 等)
- 出 bug 可能导致内核 panic,系统死机
- 内存分配要小心,不能泄漏
六、驱动的基本结构
一个典型的字符设备驱动包含:
1. 模块入口出口
c
module_init(xxx_init);
module_exit(xxx_exit);
2. 设备号申请
c
alloc_chrdev_region(&devid, 0, 1, "xxx"); // 动态分配
register_chrdev_region(devid, 1, "xxx"); // 静态指定
3. cdev 注册
c
cdev_init(&cdev, &fops); // 绑定操作函数
cdev_add(&cdev, devid, 1); // 加入内核
4. 文件操作集合(file_operations)
c
static const struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.open = xxx_open,
.release = xxx_release,
.read = xxx_read,
.write = xxx_write,
.unlocked_ioctl = xxx_ioctl,
};
应用层的 open/read/write 对应到驱动里的这些函数。
5. 硬件操作
读写寄存器、配置 GPIO、处理中断......跟具体硬件相关。
6. 自动创建设备节点
c
class_create();
device_create();
加载模块自动在 /dev 下创建设备文件,不用手动 mknod。
七、学习路线建议
入门阶段
- 内核模块 hello world
- 字符设备驱动基本框架
- open/read/write 实现
- GPIO 驱动(LED、按键)
进阶阶段
- 中断处理
- 定时器
- 并发与同步(自旋锁、信号量、互斥锁)
- 设备树
深入阶段
- I2C 子系统
- SPI 子系统
- platform 总线
- 输入子系统、Framebuffer 等
这个系列就按这个顺序来,从简单到复杂。
八、常见坑
坑 1:内核版本不匹配
编译驱动的内核源码版本必须跟开发板运行的内核版本一致,不然加载会失败(version magic 错误)。
坑 2:printk 不一定立刻打印
内核有缓冲,不一定立刻输出。用 dmesg 看,或者 printk 级别设高一点。
坑 3:在内核里用 C 库函数
不能用 printf、malloc、strcpy 这些用户态函数,要用内核对应的:printk、kmalloc、strlcpy......
坑 4:用户空间指针直接访问
应用传下来的指针(用户空间地址)不能直接在内核里读写,要用 copy_from_user / copy_to_user。
坑 5:忘了加 MODULE_LICENSE
没加 GPL 许可证,很多内核函数用不了,会报错 unknown symbol。
坑 6:驱动 bug 导致系统死机
内核态出错直接 panic,不像应用层只是段错误。调试要小心,多打日志。
九、本系列内容安排
- 嵌入式 Linux 驱动概述:分类、内核模块、开发环境
- 内核模块开发:module_init/module_exit 与模块机制
- 字符设备驱动:file_operations 与读写
- 设备号与设备节点:动态分配、udev 自动创建
- 中断处理:顶半部底半部、tasklet/workqueue
- 定时器与延时:内核定时器、hrtimer
- 并发与同步:自旋锁、信号量、互斥锁
- 平台设备驱动:platform bus 与设备树
- GPIO 子系统与 pinctrl
- I2C 设备驱动开发
- SPI 设备驱动开发
- 实战:传感器驱动从 0 到 1
十、本篇小结
- Linux 驱动分三类:字符设备(字节流)、块设备(存储)、网络设备(网卡)
- 驱动以内核模块形式存在,可动态加载卸载(.ko 文件)
- 模块两个关键函数:module_init(加载)、module_exit(卸载)
- 驱动运行在内核态,权限高但出错代价大,不能用 C 库
- 开发环境:开发板、交叉编译器、内核源码树、Makefile
- 字符设备驱动基本结构:设备号 + cdev + file_operations + 硬件操作
- 学习路线:模块 → 字符设备 → 中断/定时器 → 并发 → 设备树 → I2C/SPI → 实战
下一篇讲内核模块开发:深入 module_init/module_exit、模块参数、模块依赖、导出符号。
我是黒漂技术佬。