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 = <®_cam_dovdd>;
AVDD-supply = <®_cam_avdd>;
DVDD-supply = <®_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
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 环境验证:确保一切就绪
搭建完环境,我建议你做一个完整的验证:
- 编译一个空驱动:确保工具链和内核源码树配合正常
- 加载到板子上:用
insmod或modprobe加载,看dmesg是否有输出 - 检查设备树:在板子上运行
ls /proc/device-tree/,确认你的节点存在
这三个步骤都过了,恭喜你,开发环境算是搭好了。接下来就可以真正开始写Camera驱动了。