2. Linux内核与驱动基础:内核编译与模块加载、字符设备驱动模型、设备树(DTS)解析
好,咱们进入第二章。这一章是硬核基础,也是很多初学者觉得「劝退」的地方。但我跟你说,这部分搞明白了,后面摄像头驱动开发就是水到渠成的事。
我刚开始学嵌入式Linux时,最头疼的就是内核编译。动不动就报错,一编译就是半小时。后来踩的坑多了,慢慢就摸清了门道。今天我把这些经验都倒给你。
2.1 内核编译——别怕,其实就是个配置过程
很多人一听到「编译内核」就发怵。其实说白了,内核编译就是三步:配置、编译、安装。跟你在PC上编译一个软件没啥本质区别,只是规模大了点。
第一步:获取内核源码
你可以从 kernel.org 下载,或者直接用芯片厂商提供的。我个人习惯用厂商的,因为里面已经包含了板级支持包(BSP),省事。
# 以树莓派为例
git clone --depth=1 https://github.com/raspberrypi/linux
cd linux
第二步:配置内核
配置内核有几种方式:
make menuconfig—— 图形化菜单,最常用make defconfig—— 默认配置,适合快速上手make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bcm2711_defconfig—— 指定架构和板子
我建议新手先用 defconfig,能编译通过再说。别一上来就搞定制,容易心态崩。
/ 可以搜索选项。比如你想找摄像头相关的驱动,输入 camera 就能定位到。
第三步:编译
make -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
make -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
-j4 表示用4个线程并行编译。我建议根据你电脑CPU核心数来设,别设太大,否则电脑卡死别怪我。
2.2 内核模块加载——动态插拔,灵活得很
内核模块,你可以理解成「热插拔」的驱动。需要时加载,不需要时卸载,不用重新编译整个内核。摄像头驱动通常就做成模块。
编写一个最简单的模块
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, camera world!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, camera world!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
嗯,这里要注意:printk 不是 printf,它输出到内核日志,你用 dmesg 才能看到。
编译和加载
# Makefile
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
make
sudo insmod hello.ko # 加载模块
sudo rmmod hello # 卸载模块
dmesg | tail # 查看输出
你想想看,如果每次改个驱动都要重新编译整个内核,那得多痛苦?模块化开发就是干这个的。
2.3 字符设备驱动模型——一切皆文件
Linux的设计哲学是「一切皆文件」。摄像头设备也不例外。你操作摄像头,本质上就是读写一个文件。
字符设备驱动有三个核心结构体:
struct file_operations—— 文件操作函数集struct cdev—— 字符设备描述符dev_t—— 设备号(主设备号 + 次设备号)
驱动骨架代码
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 len, loff_t *off)
{
// 这里将来放摄像头数据读取逻辑
return 0;
}
static struct file_operations camera_fops = {
.owner = THIS_MODULE,
.open = camera_open,
.read = camera_read,
};
我在项目中遇到过一个问题:用户态程序读不到数据,查了半天发现是 __user 指针没做地址转换。记住,内核空间和用户空间的数据不能直接拷贝,要用 copy_to_user 和 copy_from_user。
open/read/write/ioctl 这些函数,然后注册到内核。应用层调用 open("/dev/video0"),最终就调到你写的驱动函数里。
2.4 设备树(DTS)解析——硬件描述语言
设备树,说白了就是描述硬件信息的配置文件。以前用板级文件(board-xxx.c)硬编码,现在都改用设备树了。好处是:换硬件不用改内核,改dts文件就行。
一个简单的设备树节点
/dts-v1/;
#include "imx6ull.dtsi"
/ {
model = "My Camera Board";
compatible = "mycompany,imx6ull-camera";
camera: ov5640@3c {
compatible = "ovti,ov5640";
reg = <0x3c>;
clocks = <&clks IMX6ULL_CLK_CAM>;
interrupt-parent = <&gpio1>;
interrupts = <9 IRQ_TYPE_EDGE_RISING>;
power-gpios = <&gpio5 7 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
};
};
你看,这个节点描述了:
- 摄像头型号是 OV5640
- I2C地址是 0x3c
- 用了哪个时钟
- 中断引脚是 GPIO1_9
- 电源和复位引脚
驱动中如何读取设备树?
static int camera_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct gpio_desc *reset_gpio;
// 读取GPIO
reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW);
if (IS_ERR(reset_gpio)) {
dev_err(dev, "Failed to get reset GPIO\n");
return PTR_ERR(reset_gpio);
}
// 读取时钟
struct clk *cam_clk = devm_clk_get(dev, NULL);
if (IS_ERR(cam_clk)) {
dev_err(dev, "Failed to get camera clock\n");
return PTR_ERR(cam_clk);
}
return 0;
}
我曾经调试一个摄像头驱动,死活初始化失败。后来用 dtc -I dtb -O dts 反编译设备树,发现GPIO编号写错了。嗯,这种低级错误最坑人。
dtc—— 编译/反编译设备树ls /proc/device-tree/—— 查看当前系统设备树of_*系列API —— 驱动中解析设备树
2.5 本章小结
这一章内容不少,但都是硬通货。内核编译是基本功,模块加载是日常操作,字符设备驱动是核心模型,设备树是硬件描述标准。四者缺一不可。
下一章,我们就用这些知识,真正开始写摄像头驱动了。到时候你会发现,前面这些基础打得越牢,后面写代码就越顺手。
记住我一句话:驱动开发没有捷径,但踩过的坑都是财富。加油!