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核心数来设,别设太大,否则电脑卡死别怪我。

注意: 交叉编译工具链必须跟你的目标架构匹配。ARM板子用x86的工具链编译,那是行不通的。我曾经犯过这个错,浪费了一下午。

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_usercopy_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 本章小结

这一章内容不少,但都是硬通货。内核编译是基本功,模块加载是日常操作,字符设备驱动是核心模型,设备树是硬件描述标准。四者缺一不可。

下一章,我们就用这些知识,真正开始写摄像头驱动了。到时候你会发现,前面这些基础打得越牢,后面写代码就越顺手。

记住我一句话:驱动开发没有捷径,但踩过的坑都是财富。加油!