第三章 开发环境搭建:交叉编译工具链、内核配置、设备树与驱动加载

好,咱们进入第三章。这一章是真正动手前的“地基工程”。很多初学者一上来就急着写代码,结果编译都过不了,或者烧进去系统起不来——我当年也干过这种蠢事。环境搭不对,后面全是白费功夫。

嵌入式Camera开发,说白了就是在PC上写代码,编译成ARM架构能跑的程序,再扔到板子上运行。这个“PC编译、ARM运行”的过程,就是交叉编译。咱们一步步来。

3.1 交叉编译工具链:你的“翻译官”

工具链是什么?就是一套把C代码变成机器码的工具集合:编译器、链接器、调试器、库文件等等。对于嵌入式开发,我们用的是交叉编译工具链——在x86的PC上生成ARM的二进制文件。

核心概念:交叉编译 = 宿主机(x86)上编译,目标机(ARM)上运行。

3.1.1 工具链的选择

我个人习惯用Linaro提供的GCC工具链,稳定、社区活跃。针对Camera开发,我建议选择arm-linux-gnueabihf-系列,因为大多数Camera SoC都带硬件浮点单元(FPU),用hf版本能充分发挥性能。

工具链前缀 适用场景 浮点支持
arm-linux-gnueabi- 无硬件FPU的ARM芯片 软浮点
arm-linux-gnueabihf- 带硬件FPU的ARM芯片(如Cortex-A系列) 硬浮点
aarch64-linux-gnu- 64位ARM芯片(如树莓派4、RK3588) 硬浮点

我的经验:如果你用的是全志V3s、君正T31这类Camera芯片,选arm-linux-gnueabihf-准没错。我之前有个项目用了软浮点工具链,结果图像处理算法跑起来慢得离谱——后来才发现是浮点单元没使上劲。

3.1.2 安装与验证

安装其实很简单,下载解压就行。以Ubuntu 22.04为例:

# 下载Linaro 7.5版本(稳定,兼容性好)
wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 解压到 /opt 目录
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt

# 添加环境变量(建议写到 ~/.bashrc 里)
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin

# 验证是否安装成功
arm-linux-gnueabihf-gcc --version

看到版本号输出,就说明工具链装好了。嗯,这里要注意:一定要把路径加到~/.bashrc里,不然每次开新终端都要手动export,烦得很。

3.2 Linux内核配置:裁剪出你的Camera系统

内核配置,说白了就是告诉Linux:你要支持哪些硬件、哪些功能。Camera开发中,我们需要开启V4L2框架、ISP驱动、视频输入接口等。

3.2.1 获取内核源码

我建议直接从芯片厂商的官方仓库拉代码,比如全志的linux-sunxi、Rockchip的rk-linux。用主线内核也行,但有些Camera驱动可能没合入。

# 以全志V3s为例
git clone https://github.com/Lichee-Pi/linux.git -b zero-4.14.y

3.2.2 配置内核选项

配置内核有三种方式:make menuconfig(文本界面)、make xconfig(图形界面)、make defconfig(默认配置)。我习惯用menuconfig,轻量、快速。

Camera开发必须开启的选项:

  • V4L2框架:CONFIG_VIDEO_V4L2=y
  • Video Capture接口:CONFIG_VIDEO_CAPTURE_DRIVERS=y
  • 具体的Camera传感器驱动:比如OV5640、GC2053等
  • ISP驱动:如果SoC内置ISP,要选上对应的驱动模块
  • DMA-BUF:用于零拷贝传输图像数据,CONFIG_DMA_SHARED_BUFFER=y

我曾经踩过的坑:有一次我忘了开启CONFIG_VIDEO_V4L2,结果编译出来的内核根本没有/dev/video节点。查了两天才发现是内核配置漏了——从那以后我每次配置完都会grep一遍关键选项。

3.2.3 编译内核

# 配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

# 编译内核镜像
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 zImage

# 编译设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

# 编译内核模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

编译完成后,在arch/arm/boot/下会生成zImage,在arch/arm/boot/dts/下生成.dtb文件。

3.3 设备树编写:告诉内核硬件长什么样

设备树(Device Tree)是Linux用来描述硬件信息的机制。以前老内核用板级文件(board-xxx.c),硬编码一大堆,维护起来想死。设备树把硬件描述和内核代码分开了,改硬件只需要改.dts文件,不用重新编译内核。

3.3.1 设备树的基本结构

一个典型的Camera设备树节点长这样:

/ {
    model = "MyCameraBoard";
    compatible = "vendor,myboard";

    &i2c0 {
        status = "okay";
        clock-frequency = <400000>;

        ov5640: camera@3c {
            compatible = "ovti,ov5640";
            reg = <0x3c>;
            clocks = <&clks CLK_CAMERA_MCLK>;
            clock-names = "xvclk";
            reset-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
            powerdown-gpios = <&gpio0 14 GPIO_ACTIVE_HIGH>;
            port {
                ov5640_ep: endpoint {
                    remote-endpoint = <&csi_ep>;
                    bus-width = <8>;
                    data-shift = <2>;
                    hsync-active = <1>;
                    vsync-active = <0>;
                    pclk-sample = <1>;
                };
            };
        };
    };

    &csi {
        status = "okay";
        port {
            csi_ep: endpoint {
                remote-endpoint = <&ov5640_ep>;
            };
        };
    };
};

这里有几个关键点:

  • compatible:驱动匹配的关键字符串,必须和驱动里的一模一样
  • reg:I2C地址,Camera传感器一般是0x3c或0x30
  • clocks:主时钟(MCLK),Camera必须有时钟才能工作
  • port/endpoint:描述数据连接关系,告诉内核图像数据从哪来、到哪去

我的习惯:写设备树时,先看芯片的参考手册,找到CSI接口的引脚复用关系。然后对照原理图,把GPIO、I2C、时钟这些信息填进去。别凭想象写,一定要看文档。

3.3.2 编译设备树

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

# 或者在内核源码目录下
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

3.4 驱动编译与加载:让硬件动起来

驱动有两种编译方式:编译进内核(built-in)和编译成模块(module)。我建议Camera驱动用模块方式,方便调试——改驱动不用重新烧整个内核。

3.4.1 编写简单的Camera驱动模块

#include <linux/module.h>
#include <linux/videodev2.h>
#include <media/v4l2-device.h>

static int __init mycam_init(void)
{
    pr_info("MyCamera driver loaded\n");
    return 0;
}

static void __exit mycam_exit(void)
{
    pr_info("MyCamera driver unloaded\n");
}

module_init(mycam_init);
module_exit(mycam_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple Camera Driver");

3.4.2 编译外部模块

# 编写Makefile
obj-m += mycam.o

all:
    make -C /path/to/kernel/source M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

clean:
    make -C /path/to/kernel/source M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean

编译后会生成mycam.ko文件。把它拷贝到板子上,用insmod加载:

# 加载驱动
insmod mycam.ko

# 查看加载是否成功
lsmod | grep mycam

# 卸载驱动
rmmod mycam

3.4.3 驱动加载的常见问题

问题现象 可能原因 解决方法
insmod: ERROR: could not insert module 内核版本不匹配 用板子上运行的内核源码重新编译驱动
Unknown symbol 依赖的符号未导出 检查/proc/kallsyms,确认符号是否存在
加载后没有/dev/video节点 设备树匹配失败或V4L2注册失败 检查dmesg输出,确认compatible是否匹配

我曾经遇到的情况:有一次驱动加载成功,但/dev/video0死活不出来。查了半天,发现是设备树里compatible写成了"ovti,ov5640",而驱动里匹配的是"ovti,ov5640"——等等,我写对了啊?后来发现是驱动里有个空格没对齐。嗯,设备树和驱动的字符串匹配是严格逐字节的,多一个空格都不行。

3.5 本章知识体系总览

下面这张图把本章的核心流程串起来了,你一看就明白:

嵌入式Camera开发环境搭建流程 嵌入式Camera开发环境搭建流程 交叉编译工具链 Linux内核源码 设备树源文件(.dts) 驱动源码(.c) 配置工具链路径 make menuconfig 编写/修改.dts 编写驱动代码 环境就绪 zImage + dtbs .dtb文件 .ko模块 烧录/部署到目标板(zImage + .dtb + .ko) Camera系统启动成功! 宿主机(x86 PC)编译阶段 → 目标板(ARM)运行阶段

这张图把整个流程串起来了:从工具链、内核源码、设备树、驱动源码开始,经过配置和编译,生成zImage、dtb、ko文件,最后部署到板子上。每一步都环环相扣,缺一不可。

本章核心要点:

  • 交叉编译工具链是基础,选对前缀(gnueabihf)很重要
  • 内核配置要开启V4L2、Camera传感器、ISP等关键选项
  • 设备树是硬件描述文件,compatible字符串必须和驱动严格匹配
  • 驱动用模块方式编译,方便调试和更新