4. PWM驱动开发环境搭建:Linux内核源码获取、交叉编译工具链配置、设备树基础语法
好,咱们正式开始动手了。这一章我带你把开发环境搭起来。你想想看,做驱动开发,最怕什么?环境没配好,编译都过不了,后面全是白搭。我这些年带过不少新人,十有八九的问题都出在环境上。所以这一章,咱们把地基打牢。
4.1 获取Linux内核源码
做驱动开发,内核源码就是你的“圣经”。你得知道内核怎么工作的,才能写出对的驱动。我个人习惯用LTS(长期支持)版本,稳定,坑少。
获取源码主要有两种方式:
- 从内核官网下载:访问
kernel.org,找到你需要的版本,下载.tar.xz包。 - 从芯片厂商获取:比如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能跑的代码。
配置步骤:
- 下载工具链:从芯片厂商官网下载,比如ARM官方的
gcc-arm-none-eabi,或者Linaro的gcc-linaro-arm-linux-gnueabihf。 - 解压到指定目录:我习惯放在
/opt/toolchains/下。
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/toolchains/
- 设置环境变量:编辑
~/.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 使其生效。
- 验证:
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 设备树与驱动的匹配过程
我画了一张图,帮你理清设备树和驱动是怎么“对上眼”的:
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 环境验证
环境搭好了,怎么知道对不对?我一般做三步验证:
- 编译内核:执行
make defconfig生成默认配置,然后make -j4 zImage。如果能编译出zImage,说明工具链和源码都没问题。 - 编译设备树:执行
make dtbs,看看有没有报错。 - 检查模块编译:进入
drivers/pwm/目录,执行make -C /path/to/kernel M=$(pwd) modules,如果能生成.ko文件,说明驱动编译环境也好了。
ARCH 和 CROSS_COMPILE,结果编译出来的模块是x86的,放到ARM板上一加载就崩溃。所以每次编译前,我都习惯性地检查一下这两个环境变量。