第三章 Linux内核编译与设备树
说实话,很多做驱动的朋友,第一步就卡在内核编译上。不是编译不过,就是设备树写错了导致系统起不来。我当年在Zynq上栽的第一个跟头,就是设备树里一个GPIO的pinmux配错了,查了整整两天。
这一章,咱们就把内核编译和设备树这两个基本功彻底打通。你跟着我的路子走,能少踩不少坑。
3.1 获取Linux内核源码
Zynq平台主流的内核版本是4.x和5.x系列。我个人习惯用Xilinx官方维护的版本,因为对Zynq的硬件支持最全。
推荐源码获取方式:
- Xilinx官方Git仓库:
git://github.com/Xilinx/linux-xlnx.git - 或者直接下载对应版本的tar包
拿到源码后,先切到稳定分支。我记得有一次图省事直接拉了master,结果编译出来的内核在板子上跑几分钟就panic。后来发现是某个驱动提交还没合入稳定版。
# 克隆Xilinx内核
git clone git://github.com/Xilinx/linux-xlnx.git
cd linux-xlnx
# 切换到稳定分支,比如xilinx-v2019.2
git checkout xilinx-v2019.2
3.2 配置内核
内核配置说白了就是告诉编译器:哪些功能要,哪些不要。Zynq有现成的默认配置,咱们直接拿来改就行。
| 配置命令 | 用途 | 我常用的场景 |
|---|---|---|
make ARCH=arm xilinx_zynq_defconfig |
加载Zynq默认配置 | 第一次编译,先跑这个打底 |
make ARCH=arm menuconfig |
图形化菜单配置 | 添加或删除某个驱动模块 |
make ARCH=arm savedefconfig |
生成精简的defconfig | 记录你的最终配置,方便复用 |
配置时要注意几个关键点:
- CPU类型:选Cortex-A9(Zynq-7000系列)
- 设备驱动:把你要用的外设驱动选上,比如UART、I2C、SPI、GPIO
- 文件系统:至少支持ext4和initramfs
我的小技巧:第一次配置时,先加载defconfig,再用menuconfig搜索你需要的驱动。比如搜"GPIO",把Zynq GPIO controller选上。这样不会漏东西。
3.3 编译内核与设备树
配置好了,就该编译了。Zynq是ARM架构,所以需要交叉编译工具链。
# 设置环境变量
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# 编译内核镜像
make -j4 UIMAGE_LOADADDR=0x8000 uImage
# 编译设备树
make -j4 dtbs
# 编译内核模块
make -j4 modules
这里有个坑,我必须要说。UIMAGE_LOADADDR=0x8000这个参数,很多人会忘。Zynq的BootROM要求内核加载地址是0x8000,不设的话,生成的uImage启动时会跑飞。
曾经踩过的坑:有一次我编译完内核,烧到SD卡里,上电后串口只打印了一行"Starting kernel..."就卡死了。查了半天,发现是忘了设UIMAGE_LOADADDR。从那以后,我每次编译前都先检查这个参数。
编译完成后,你会得到几个关键文件:
arch/arm/boot/uImage—— 内核镜像arch/arm/boot/dts/zynq-zed.dtb—— 设备树二进制文件(以ZedBoard为例)arch/arm/boot/zImage—— 未打包的内核
3.4 设备树语法基础
设备树(Device Tree)是Linux用来描述硬件信息的机制。说白了,就是告诉内核:板子上有哪些外设,它们挂在哪个地址上,用哪个中断。
咱们来看一个Zynq设备树的典型结构:
/dts-v1/;
#include "zynq-7000.dtsi"
/ {
model = "Zynq ZedBoard";
compatible = "xlnx,zynq-7000";
chosen {
bootargs = "console=ttyPS0,115200 root=/dev/mmcblk0p2 rw";
};
&uart1 {
status = "okay";
};
&i2c0 {
status = "okay";
clock-frequency = <100000>;
adxl345@53 {
compatible = "adi,adxl345";
reg = <0x53>;
interrupt-parent = <&gpio0>;
interrupts = <0 29 4>;
};
};
};
这段代码里,有几个关键概念:
- /dts-v1/ —— 版本声明,必须写在第一行
- #include —— 引用芯片级dtsi文件,里面定义了Zynq的CPU、内存、时钟等基础信息
- / { } —— 根节点,整个板子的硬件描述都放在这里
- compatible —— 匹配字符串,内核通过它找到对应的驱动
- &uart1 —— 引用dtsi里定义好的节点,然后覆盖它的属性
设备树的核心语法:
| 语法元素 | 说明 | 示例 |
|---|---|---|
| 节点 | 用node-name@address表示 |
uart1@e0001000 |
| 属性 | key = value; |
reg = <0xe0001000 0x1000>; |
| 引用 | 用&label引用已有节点 |
&gpio0 { ... }; |
| 包含 | #include "file.dtsi" |
#include "zynq-7000.dtsi" |
3.5 设备树编译与调试
写好的dts文件,需要编译成dtb才能被内核使用。
# 编译单个dts文件
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译dtb,查看二进制内容
dtc -I dtb -O dts -o myboard.dts myboard.dtb
调试设备树时,我最常用的方法是:
- 在板子上挂载
/sys/firmware/devicetree,直接看内核解析后的设备树 - 用
ls /sys/firmware/devicetree/base查看所有节点 - 用
cat /sys/firmware/devicetree/base/model查看具体属性值
曾经踩过的坑:有一次我写了一个I2C设备的节点,reg地址写成了0x53,但实际芯片地址是0x1A。结果内核一直报"i2c: adapter failed to register"。查了两天,最后用i2cdetect扫了一遍才发现地址写错了。所以,写设备树时一定要对照原理图确认地址。
3.6 本章知识体系
下面这张图,把内核编译和设备树的整体流程串起来了。你照着这个思路走,不会乱。
嗯,这一章的内容就这些。内核编译和设备树是驱动开发的基本功,你多练几次就熟了。记住,遇到问题先查设备树,八成问题都出在那里。