一、驱动开发概述:从零认识驱动
各位同学好,我是老李。做驱动开发十几年了,今天咱们聊聊最基础的问题——驱动到底是什么。
很多人一听到「驱动开发」就觉得高深莫测,其实说白了,驱动就是操作系统和硬件之间的翻译官。你想想看,CPU 不认识网卡、显卡这些外设,操作系统也不懂它们的寄存器怎么操作。驱动就是那个两头都懂的人——它知道硬件怎么工作,也懂操作系统怎么调度。
1.1 什么是驱动
驱动,全称设备驱动程序,是一段直接和硬件打交道的软件。它的核心任务就三件:
- 初始化硬件——上电后让设备进入可用状态
- 收发数据——把操作系统的请求转成硬件指令,再把硬件结果返回给系统
- 处理中断——硬件有事了,驱动得第一时间响应
我记得刚入行时,带我的师傅说过一句话:「驱动就是硬件的影子,硬件怎么动,驱动就得怎么跟。」这句话我一直记着。
核心理解:驱动是操作系统与硬件之间的中间层。没有驱动,操作系统就是个光杆司令,再好的硬件也白搭。
1.2 驱动在操作系统中的角色
驱动在操作系统里到底扮演什么角色?我用一个图来说明:
从这张图你能看到,驱动处于操作系统内核的最底层,直接和硬件对话。上层应用想读写设备,都得经过驱动。我做过一个项目,当时应用层死活读不到传感器数据,查了两天才发现是驱动里中断处理写错了——你看,驱动一出问题,整个系统都跟着遭殃。
1.3 驱动的分类
驱动按工作方式可以分为几类,我整理了一个表格:
| 驱动类型 | 特点 | 典型场景 |
|---|---|---|
| 字符设备驱动 | 按字节流读写,顺序访问 | 串口、键盘、鼠标、传感器 |
| 块设备驱动 | 按块读写,支持随机访问 | 硬盘、SD卡、U盘 |
| 网络设备驱动 | 通过套接字接口收发数据包 | 以太网卡、WiFi模块 |
| 总线驱动 | 管理设备枚举和通信协议 | USB、PCI、I2C、SPI |
我个人习惯把字符设备驱动当作入门首选,因为它逻辑最简单,适合新手练手。块设备驱动涉及缓存和调度,网络驱动涉及协议栈,复杂度都上去了。
1.4 Linux 驱动开发环境搭建
好,理论说完了,咱们来点实际的。做 Linux 驱动开发,环境搭建是第一步。我见过太多新手卡在这一步,其实没那么复杂。
1.4.1 交叉编译工具链
什么是交叉编译?说白了就是在你的 PC 上编译出能在目标板(比如 ARM 开发板)上运行的代码。为什么需要?因为目标板性能太弱,跑不了编译器。
安装交叉编译工具链,以 ARM 为例:
# 下载工具链(以 Linaro GCC 为例)
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
# 解压到指定目录
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/
# 添加环境变量
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
# 验证安装
arm-linux-gnueabihf-gcc --version
小提示:我建议把工具链路径写到 ~/.bashrc 或 ~/.zshrc 里,这样每次打开终端就不用重新 export 了。我以前总忘,后来干脆写了个脚本一键配置。
1.4.2 内核源码树
驱动是内核的一部分,编译驱动必须依赖内核源码树。内核源码树就是 Linux 内核的完整源代码,里面包含了编译驱动所需的所有头文件和 Makefile 规则。
获取内核源码树有两种方式:
- 下载官方内核——从 kernel.org 下载,适合通用开发
- 使用板厂提供的内核——比如树莓派、NXP、TI 等厂商会提供适配好的内核源码
我个人更推荐第二种,因为板厂已经帮你处理好了硬件相关的配置,省去很多麻烦。
# 以树莓派为例,下载内核源码
git clone --depth=1 https://github.com/raspberrypi/linux.git
# 配置内核(使用默认配置)
cd linux
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bcm2711_defconfig
# 编译内核(第一次编译时间较长)
make -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules dtbs
注意:编译内核时,ARCH 和 CROSS_COMPILE 这两个参数必须指定,否则默认会编译成 x86 架构的代码,放到 ARM 板上跑不了。我曾经帮一个同事排查问题,他编译了一整天,结果发现忘了指定 CROSS_COMPILE……
1.4.3 第一个驱动模块:Hello World
环境搭好了,咱们写个最简单的驱动模块验证一下:
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Driver World!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Driver World!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World driver");
对应的 Makefile:
obj-m := hello.o
KDIR := /path/to/your/kernel/source
CROSS := arm-linux-gnueabihf-
all:
make -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS) modules
clean:
make -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS) clean
编译后生成 hello.ko 文件,这就是驱动模块。把它拷贝到目标板上,用 insmod hello.ko 加载,用 rmmod hello 卸载。
关键点:驱动模块的入口是 module_init 和 module_exit,不是 main 函数。内核模块运行在内核空间,不能调用用户空间的库函数(比如 printf),只能用 printk 打印日志。
1.5 避坑指南
最后分享几个我踩过的坑:
- 内核版本必须匹配——驱动编译用的内核源码版本,必须和目标板上运行的内核版本一致,否则加载时会报版本不匹配错误。我曾经图省事用了不同版本的内核头文件,结果折腾了一下午。
- 交叉编译工具链版本别太新——太新的工具链可能不支持老内核的某些语法,建议用 Linaro 的长期支持版本。
- printk 的日志级别——默认情况下,
printk的日志可能不会显示在控制台,需要调整内核日志级别或查看dmesg命令的输出。
嗯,第一章就到这里。环境搭好了,后面咱们就能真正开始写驱动了。