4. PWM驱动开发环境搭建:Linux内核源码获取、交叉编译工具链配置、设备树基础语法

好,咱们正式开始动手了。这一章我带你把开发环境搭起来。你想想看,做驱动开发,最怕什么?环境没配好,编译都过不了,后面全是白搭。我这些年带过不少新人,十有八九的问题都出在环境上。所以这一章,咱们把地基打牢。

4.1 获取Linux内核源码

做驱动开发,内核源码就是你的“圣经”。你得知道内核怎么工作的,才能写出对的驱动。我个人习惯用LTS(长期支持)版本,稳定,坑少。

我的建议: 别追新。新内核特性多,但文档不全,社区支持也少。做产品,稳定第一。

获取源码主要有两种方式:

  1. 从内核官网下载:访问 kernel.org,找到你需要的版本,下载 .tar.xz 包。
  2. 从芯片厂商获取:比如NXP、TI、Allwinner,它们会提供定制好的内核源码包。我建议用这个,因为厂商已经帮你适配好了板级支持包(BSP)。

下载后解压:

tar -xvf linux-5.10.168.tar.xz
cd linux-5.10.168

解压完你会看到一堆目录。别慌,核心的几个我列出来:

目录 作用
arch/ 架构相关代码,比如ARM、ARM64、x86。我们做嵌入式,主要看 arch/arm/ 或 arch/arm64/
drivers/ 驱动代码大本营。PWM驱动在 drivers/pwm/,I2C在 drivers/i2c/
Documentation/ 内核文档。遇到不懂的,先来这里翻翻
arch/arm/boot/dts/ 设备树源文件(.dts)存放位置。后面我们会频繁修改这里

4.2 交叉编译工具链配置

嵌入式开发,目标板(比如ARM开发板)性能有限,没法直接在上面编译。我们得在PC上编译,然后把二进制文件丢到板子上跑。这就是交叉编译。

说白了,就是你的PC是x86架构,目标板是ARM架构。你得用一套工具链,让PC生成ARM能跑的代码。

注意: 工具链版本必须与内核版本匹配。版本差太多,编译会报一堆莫名其妙的错误。我曾经因为工具链太新,内核太老,折腾了两天才找到原因。

配置步骤:

  1. 下载工具链:从芯片厂商官网下载,比如ARM官方的 gcc-arm-none-eabi,或者Linaro的 gcc-linaro-arm-linux-gnueabihf。
  2. 解压到指定目录:我习惯放在 /opt/toolchains/ 下。
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/toolchains/
  1. 设置环境变量:编辑 ~/.bashrc,添加以下内容:
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm
export PATH=$PATH:/opt/toolchains/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin

然后执行 source ~/.bashrc 使其生效。

  1. 验证:
arm-linux-gnueabihf-gcc --version

如果能看到版本信息,说明配置成功。

关键点: CROSS_COMPILE 这个变量,内核编译系统会用它来拼接出完整的工具链命令。比如编译内核时,系统会自动调用 arm-linux-gnueabihf-gcc 而不是 gcc。

4.3 设备树基础语法

设备树(Device Tree),是嵌入式Linux里用来描述硬件信息的一种数据结构。以前老内核用板级文件(board-xxx.c),硬编码硬件信息,改起来特别麻烦。设备树把硬件描述和内核代码分开了,改硬件只需要改 .dts 文件,不用重新编译内核。

嗯,这里要注意:设备树不是万能的。它只描述硬件“有什么”,不描述“怎么用”。怎么用,还是驱动代码说了算。

4.3.1 设备树文件结构

设备树源文件后缀是 .dts,编译后的二进制文件是 .dtb。内核启动时,bootloader会把 .dtb 传给内核,内核解析后就知道板子上有哪些外设了。

一个简单的 .dts 文件长这样:

/dts-v1/;

/ {
    model = "My ARM Board";
    compatible = "mycompany,myboard";

    chosen {
        stdout-path = &uart0;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;
    };

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";

        uart0: serial@10000000 {
            compatible = "ns16550";
            reg = <0x10000000 0x1000>;
            interrupts = <0 5 4>;
            clock-frequency = <24000000>;
        };

        pwm0: pwm@20000000 {
            compatible = "mycompany,pwm";
            reg = <0x20000000 0x1000>;
            clocks = <&clk_pwm>;
            #pwm-cells = <3>;
        };
    };
};

4.3.2 核心语法要素

要素 说明 示例
节点 用 { } 表示一个硬件设备或总线 uart0: serial@10000000 { };
属性 描述节点的特性,比如地址、中断、时钟 reg = <0x10000000 0x1000>;
标签 给节点起个别名,方便其他地方引用 uart0: 就是标签,后面可以用 &uart0 引用
兼容性 compatible 属性,驱动通过它来匹配设备 compatible = "ns16550";
地址编码 #address-cells 和 #size-cells 决定地址和长度用几个32位整数表示 #address-cells = <1>; 表示地址用1个32位整数

4.3.3 设备树与驱动的匹配过程

我画了一张图,帮你理清设备树和驱动是怎么“对上眼”的:

设备树与驱动匹配流程 设备树与驱动匹配流程 设备树 (.dts) compatible = "mycompany,pwm" 内核解析 生成 platform_device 驱动模块 of_match_table = {"mycompany,pwm"} 匹配成功 → probe() 被调用 关键点 驱动通过 .of_match_table 中的 compatible 值与设备树节点匹配 匹配成功后,内核调用驱动的 probe() 函数,驱动开始工作

4.3.4 编译设备树

写好 .dts 文件后,需要编译成 .dtb。命令很简单:

make dtbs

或者单独编译某个文件:

make myboard.dtb

编译好的 .dtb 文件在 arch/arm/boot/dts/ 目录下。把它烧到板子上,内核启动时就会加载它。

我的习惯: 每次修改 .dts 后,我都会用 dtc -I dtb -O dts myboard.dtb 反编译一下,看看生成的 .dtb 是不是我想要的。这招帮我抓出过不少低级错误。

4.4 环境验证

环境搭好了,怎么知道对不对?我一般做三步验证:

  1. 编译内核:执行 make defconfig 生成默认配置,然后 make -j4 zImage。如果能编译出 zImage,说明工具链和源码都没问题。
  2. 编译设备树:执行 make dtbs,看看有没有报错。
  3. 检查模块编译:进入 drivers/pwm/ 目录,执行 make -C /path/to/kernel M=$(pwd) modules,如果能生成 .ko 文件,说明驱动编译环境也好了。
避坑指南: 我曾经在编译模块时,忘了指定 ARCH 和 CROSS_COMPILE,结果编译出来的模块是x86的,放到ARM板上一加载就崩溃。所以每次编译前,我都习惯性地检查一下这两个环境变量。