2、开发环境搭建:交叉编译工具链配置、内核源码树准备、设备树基础

说实话,很多刚入行的朋友觉得驱动开发最难的是写代码。但我个人经验告诉我,环境搭不好,后面全是坑。我见过太多人花了一周调一个bug,最后发现是工具链版本不对。嗯,咱们先把地基打牢。

2.1 交叉编译工具链:为什么非它不可?

你想想看,你的电脑是x86架构,但摄像头芯片通常是ARM或RISC-V。x86编译出来的程序,ARM根本跑不了。这时候就需要交叉编译——在PC上编译出目标平台能跑的可执行文件。

说白了,交叉编译工具链就是一套跨架构的编译器+链接器+库。我习惯用Linaro提供的工具链,稳定且社区支持好。

2.1.1 工具链的安装与配置

以ARM64为例,我常用的安装方式:

# 下载Linaro工具链(我习惯放在/opt目录下)
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/aarch64-linux-gnu/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz

# 解压
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz -C /opt/

# 配置环境变量(建议写入~/.bashrc)
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin
export CROSS_COMPILE=aarch64-linux-gnu-
export ARCH=arm64
我的小习惯:每次换项目,我都会在项目根目录下放一个 setenv.sh 脚本,里面写好环境变量。这样不同项目之间不会互相干扰。你试试看,真的省心。

2.1.2 验证工具链是否可用

配置完别急着写代码。先跑个简单的测试:

# 查看编译器版本
aarch64-linux-gnu-gcc --version

# 编译一个hello world
echo '#include <stdio.h>
int main() { printf("Hello Camera!\n"); return 0; }' > test.c

aarch64-linux-gnu-gcc test.c -o test_arm64

# 查看文件类型,确认是ARM架构
file test_arm64
# 输出应该包含:ELF 64-bit LSB executable, ARM aarch64
我曾经踩过的坑:有一次我忘了设置 CROSS_COMPILE 环境变量,结果编译出来的还是x86的。烧到板子上直接段错误。排查了整整一天才发现是这个问题。所以每次编译前,我都习惯先 echo $CROSS_COMPILE 确认一下。

2.2 内核源码树准备:驱动开发的“地图”

驱动不是凭空写的。它需要依赖内核提供的各种头文件、宏定义和函数接口。所以你得先准备好内核源码树。

我个人建议,永远使用与目标板相同版本的内核源码。版本不匹配,编译时各种宏定义找不到,那叫一个痛苦。

2.2.1 获取内核源码

两种方式:

  • 从官方仓库克隆:适合需要定制的情况
  • 从芯片厂商获取:比如树莓派、瑞芯微、全志等,他们通常提供带补丁的内核
# 以Linux 5.10 LTS为例
git clone --depth 1 --branch v5.10 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
cd linux

# 配置内核(使用默认配置)
make ARCH=arm64 defconfig

# 编译内核头文件(驱动开发只需要头文件,不需要完整编译内核)
make ARCH=arm64 headers_install INSTALL_HDR_PATH=/path/to/your/workspace/headers
关键点:驱动开发中,你真正需要的是 include/linux/include/uapi/linux/arch/arm64/include/ 这三个目录下的头文件。其他可以不用管。

2.2.2 编写第一个驱动Makefile

有了内核源码树,就可以写Makefile了。我常用的模板:

# Makefile for camera driver
obj-m += camera_sensor.o

KERNEL_DIR := /path/to/your/linux-5.10
CROSS_COMPILE := aarch64-linux-gnu-
CC := $(CROSS_COMPILE)gcc

all:
    make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
    make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) clean

这里要注意,KERNEL_DIR 必须指向你刚才准备的内核源码树路径。我见过有人直接指向了系统的 /lib/modules/$(uname -r)/build,那是x86的,编译出来肯定不对。

2.3 设备树基础:硬件描述的语言

设备树(Device Tree)是什么?说白了,它就是一份硬件配置清单。告诉内核:你的I2C总线接在哪几个GPIO上,摄像头的复位引脚是哪个,电源电压是多少等等。

我刚开始学的时候,总觉得设备树很玄乎。后来发现,它就是一堆键值对,加上一些层级关系。嗯,其实没那么复杂。

2.3.1 设备树的基本结构

一个典型的摄像头设备树节点:

/dts-v1/;
#include "soc.dtsi"

/ {
    model = "My Camera Board";
    compatible = "mycompany,camera-board";

    i2c0: i2c@ff010000 {
        compatible = "snps,designware-i2c";
        reg = <0x0 0xff010000 0x0 0x1000>;
        #address-cells = <1>;
        #size-cells = <0>;

        camera_sensor: camera@36 {
            compatible = "sony,imx219";
            reg = <0x36>;  // I2C地址
            clocks = <&clk_cam_mclk>;
            clock-names = "xvclk";
            reset-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
            powerdown-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>;
            DOVDD-supply = <&reg_cam_dovdd>;
            AVDD-supply = <&reg_cam_avdd>;
            DVDD-supply = <&reg_cam_dvdd>;
        };
    };
};
我的经验:写设备树时,compatible 属性最重要。驱动就是靠这个字符串来匹配设备的。命名规则一般是 "厂商,型号",比如 "sony,imx219"。别自己瞎编,要跟驱动里的 of_match_table 保持一致。

2.3.2 设备树编译与反编译

设备树源文件(.dts)不能直接被内核使用,需要编译成二进制(.dtb)。反过来,你也可以从二进制反编译回源文件,方便调试。

# 编译设备树
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 反编译设备树(排查问题时很有用)
dtc -I dtb -O dts -o myboard_decompiled.dts myboard.dtb

# 查看当前系统使用的设备树
cat /proc/device-tree/model
我曾经犯过的错:有一次我修改了设备树,但忘了重新编译,直接把旧的.dtb烧到板子上。结果摄像头怎么都枚举不到。排查了半天,才发现是设备树没更新。从那以后,我每次修改.dts后,都会在Makefile里加一条自动编译的规则。

2.3.3 设备树与驱动的绑定关系

驱动如何找到设备树中的节点?靠的就是 of_match_table。看个例子:

static const struct of_device_id imx219_of_match[] = {
    { .compatible = "sony,imx219" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx219_of_match);

static struct i2c_driver imx219_i2c_driver = {
    .driver = {
        .name = "imx219",
        .of_match_table = imx219_of_match,
    },
    .probe = imx219_probe,
    .remove = imx219_remove,
};

当内核启动时,它会遍历设备树中的所有节点。如果某个节点的 compatible 属性与驱动中的 of_match_table 匹配,内核就会调用该驱动的 probe 函数。这就是整个驱动加载的起点。

2.4 环境验证:确保一切就绪

搭建完环境,我建议你做一个完整的验证:

  1. 编译一个空驱动:确保工具链和内核源码树配合正常
  2. 加载到板子上:insmodmodprobe 加载,看 dmesg 是否有输出
  3. 检查设备树:在板子上运行 ls /proc/device-tree/,确认你的节点存在

这三个步骤都过了,恭喜你,开发环境算是搭好了。接下来就可以真正开始写Camera驱动了。

总结一下:交叉编译工具链让你能在PC上编译ARM代码;内核源码树提供了驱动开发所需的接口;设备树则描述了硬件配置。这三者缺一不可。我每次开始一个新项目,都会花半天时间把环境彻底验证一遍。磨刀不误砍柴工,真的。