1. 嵌入式Linux驱动开发概述
大家好,欢迎来到这门实战课。我是你们的讲师,一个在嵌入式Linux驱动领域摸爬滚打了十几年的老工程师。
今天咱们聊第一个话题:什么是嵌入式Linux驱动?
说白了,驱动就是操作系统和硬件之间的“翻译官”。你想想看,Linux内核不认识你板子上的那个WiFi芯片,也不认识那个LED灯。驱动的作用,就是告诉内核:“嘿,这个硬件该怎么用,我给你封装好接口。”
我个人的理解是:驱动开发,本质上是在写“硬件操作说明书”,只不过这份说明书是用C语言写的,而且Linux内核能读懂。
1.1 驱动到底长什么样?
一个典型的驱动,包含这几部分:
- 初始化函数:硬件上电、注册设备
- 操作函数集:比如 open、read、write、ioctl
- 中断处理:硬件有事件来了,怎么响应
- 注销函数:卸载驱动时清理资源
嗯,这里要注意:驱动不是应用程序。它没有 main 函数,它是一组被内核调用的函数集合。
核心概念:驱动运行在内核空间,应用程序运行在用户空间。两者通过系统调用交互。驱动崩溃 = 整个系统崩溃,所以写驱动要格外小心。
1.2 驱动开发环境搭建
好,理论说完了,咱们来点实际的。环境搭不好,后面全是白搭。我见过太多新手卡在这一步了。
1.2.1 交叉编译链
为什么需要交叉编译链?因为你的开发电脑是x86架构,而机顶盒是ARM架构。你不能在x86上编译出ARM能跑的程序——除非用交叉编译链。
我个人习惯用 arm-linux-gnueabihf- 这套工具链。安装很简单:
# 以Ubuntu为例
sudo apt-get install gcc-arm-linux-gnueabihf
# 验证安装
arm-linux-gnueabihf-gcc --version
我曾经遇到过一个问题:新手买了开发板,结果板子是ARMv7架构,却装了ARMv8的交叉编译链。编译出来的驱动一加载就报错。嗯,架构一定要匹配。
小技巧:不确定板子的架构?在板子上执行 uname -a 或者 cat /proc/cpuinfo,一看便知。
1.2.2 内核源码树
驱动编译需要内核头文件。你不能随便拿一个内核源码来编译,必须用目标板子上运行的那个内核版本的源码。
为什么?因为内核的API会变。你用5.10的内核头文件编译的驱动,加载到4.19的内核上,大概率会崩溃。
获取内核源码树的方式:
- 从芯片厂商(比如海思、晶晨、全志)获取BSP包
- 或者从开发板官网下载
- 再或者,自己用
git clone拉取对应版本
配置好内核源码树后,记得先编译一遍:
cd /path/to/kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_prepare
这一步会生成 Module.symvers 等必要文件。少了它,你的驱动编译会报一堆未定义符号的错误。
避坑指南:我曾经因为偷懒,直接用 make menuconfig 然后保存退出,结果内核配置和板子实际配置不一致,驱动加载时符号版本不匹配。后来我学乖了,一定用板子自带的 .config 文件。
1.3 Hello World 驱动模块
好了,环境搭好了,咱们写第一个驱动。这是驱动界的“Hello World”,虽然简单,但五脏俱全。
1.3.1 编写代码
创建一个文件 hello.c:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, World! 驱动已加载\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "再见,驱动已卸载\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("一个简单的Hello World驱动");
注意看,这里没有 main 函数。入口是 module_init 指定的函数,出口是 module_exit 指定的函数。
printk 是内核的打印函数,相当于用户空间的 printf。它的输出可以通过 dmesg 命令查看。
1.3.2 编写Makefile
再创建一个 Makefile:
obj-m := hello.o
KDIR := /path/to/your/kernel/tree
CROSS_COMPILE := arm-linux-gnueabihf-
all:
make -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
make -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) clean
这里 obj-m := hello.o 告诉内核:我要编译一个名为 hello 的模块。KDIR 指向你的内核源码树路径。
1.3.3 编译与加载
执行 make,你会得到 hello.ko 文件。这就是编译好的驱动模块。
把它传到板子上,然后:
# 加载驱动
insmod hello.ko
# 查看内核日志
dmesg | tail
# 你应该能看到:Hello, World! 驱动已加载
# 卸载驱动
rmmod hello
# 再次查看日志
dmesg | tail
# 你应该能看到:再见,驱动已卸载
重要提醒:insmod 和 modprobe 的区别?insmod 直接加载指定文件,不处理依赖。modprobe 会处理依赖关系,但需要把模块放到 /lib/modules/ 下。新手先用 insmod 就好。
1.4 我踩过的坑
最后分享几个我当年踩过的坑,希望你们能绕过去:
- 内核版本不匹配:编译环境的内核版本和板子运行的内核版本必须一致,否则加载时报
Invalid module format - 缺少符号导出:如果你的驱动依赖其他模块导出的符号,需要先加载那个模块
- printk 看不到输出:检查日志级别,有时候需要
echo 8 > /proc/sys/kernel/printk才能看到
嗯,今天就到这里。下一章咱们会深入讲解字符设备驱动,那才是真正的实战开始。
记住:驱动开发没有捷径,多动手、多踩坑、多总结。我在下一章等你。