第二章:Linux内核与驱动基础
大家好,我是你们的嵌入式Linux讲师。今天我们来聊聊驱动开发的基础。说实话,很多初学者一听到「内核模块」、「设备树」就头大。我当年也一样,第一次编译内核模块时,连Makefile都写不对,折腾了一整天。
但这些东西,说白了就是套路。你掌握了框架,剩下的就是填代码。咱们一步步来。
2.1 内核模块编译——别怕,就是个动态库
内核模块是什么?你可以把它理解成Linux内核的「插件」。内核跑起来了,你想加个功能,不用重新编译整个内核,直接insmod一个模块进去就行。我习惯把模块编译和应用程序编译分开想——应用编译用gcc,模块编译用内核的构建系统。
先看一个最简单的模块代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, camera driver!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, camera driver!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple camera module");
代码很简单,但有个坑——printk不会打印到终端,你得用dmesg看。我曾经在调试摄像头驱动时,死活看不到打印信息,后来才发现忘了看内核日志。
然后是Makefile。嗯,这里要注意:
obj-m := hello.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。加载:sudo insmod hello.ko。卸载:sudo rmmod hello。就这么简单。
modinfo hello.ko查看模块信息,能帮你确认版本和依赖。
2.2 字符设备驱动框架——打开、读写、关闭
字符设备驱动,说白了就是实现open、read、write、close这几个函数。你想想看,摄像头采集图像,本质上就是应用程序通过read从驱动拿数据。
框架代码长这样:
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
static int camera_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Camera device opened\n");
return 0;
}
static ssize_t camera_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
// 这里将来会填充图像数据
printk(KERN_INFO "Camera read called\n");
return 0;
}
static struct file_operations camera_fops = {
.owner = THIS_MODULE,
.open = camera_open,
.read = camera_read,
};
static int __init camera_init(void)
{
// 注册字符设备
// 创建设备节点
printk(KERN_INFO "Camera driver initialized\n");
return 0;
}
module_init(camera_init);
module_exit(camera_exit);
这里有个关键点:file_operations结构体。你实现的每个函数指针,都会被内核在对应操作时调用。我在项目中遇到过一个问题——read函数里忘了用copy_to_user,直接给用户空间传内核指针,结果系统直接崩溃。记住,内核和用户空间的数据拷贝必须用专用函数。
copy_to_user和copy_from_user。
2.3 platform驱动模型——设备与驱动的「相亲」
为什么要有platform驱动?我打个比方:传统PCI设备有标准总线,内核能自动枚举。但嵌入式设备里很多外设(比如摄像头I2C控制器)是直接挂在CPU上的,没有标准总线。这时候就需要platform总线来「牵线搭桥」。
platform驱动模型分两部分:
- 设备端:描述硬件资源(地址、中断号等)
- 驱动端:实现操作逻辑
驱动端代码示例:
static int camera_probe(struct platform_device *pdev)
{
struct resource *res;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// 映射寄存器地址
printk(KERN_INFO "Camera probed\n");
return 0;
}
static int camera_remove(struct platform_device *pdev)
{
printk(KERN_INFO "Camera removed\n");
return 0;
}
static struct platform_driver camera_driver = {
.probe = camera_probe,
.remove = camera_remove,
.driver = {
.name = "camera_drv",
.of_match_table = camera_of_match,
},
};
module_platform_driver(camera_driver);
设备端以前用板级文件(arch/arm/mach-xxx),现在都用设备树了。我个人习惯把设备树和驱动分开看——设备树描述「有什么」,驱动描述「怎么用」。
2.4 设备树(DTS)基础——硬件说明书
设备树,说白了就是一份硬件配置清单。内核启动时读这份清单,就知道CPU连了哪些外设、地址是多少、中断用哪个引脚。
一个摄像头节点的例子:
/ {
camera: camera@1c00000 {
compatible = "vendor,camera-sensor";
reg = <0x01c00000 0x1000>;
interrupts = <0 42 4>;
clocks = <&clkc 15>;
status = "okay";
};
};
关键字段解释:
| 字段 | 含义 | 我的经验 |
|---|---|---|
compatible |
匹配驱动的名字 | 必须和驱动里of_match_table一致 |
reg |
寄存器基地址和大小 | 地址写错会导致驱动probe失败 |
interrupts |
中断号、触发类型 | 不同芯片的编码方式不同,查手册 |
status |
设备是否启用 | 调试时设为"disabled"可临时屏蔽 |
设备树编译命令:dtc -I dts -O dtb -o xxx.dtb xxx.dts。反编译:dtc -I dtb -O dts -o xxx.dts xxx.dtb。我曾经在调试摄像头时,设备树里reg地址少写了一个0,结果驱动probe返回-ENXIO,查了两天才发现。
2.5 避坑指南
最后分享几个我踩过的坑:
- 模块加载失败:先
dmesg看内核日志,多半是符号依赖或版本不匹配。 - 设备节点不出现:检查
udev规则,或者手动mknod测试。 - probe不调用:确认
compatible字符串和设备树完全一致,包括大小写。 - 中断不触发:用
cat /proc/interrupts查看中断注册情况。
嗯,今天就到这里。下一章我们会把这些知识串起来,真正写一个摄像头驱动的雏形。到时候你会觉得,嗯,其实也没那么难。