第3章:Linux内核移植(Kernel)——内核源码树结构、设备树与实战编写
好,咱们进入第三章节。说实话,内核移植这部分,是很多BSP工程师的“拦路虎”。我记得刚入行那会儿,看着几万个文件的内核源码,头都是大的。但别怕,今天咱们就把这块硬骨头啃下来。
3.1 内核源码树结构——你得知道东西放哪
Linux内核源码,说白了就是一个巨大的“乐高积木盒”。你得知道每个零件放在哪个格子里,才能拼出你想要的东西。
我个人习惯,拿到一个新内核版本,第一件事就是跑一遍 make help,看看有哪些编译目标。然后我会重点关注这几个目录:
| 目录 | 作用 | 我的经验 |
|---|---|---|
| arch/ | 架构相关代码(ARM、ARM64、x86等) | 智能音箱用ARM Cortex-A系列,重点关注arch/arm/ |
| drivers/ | 设备驱动(I2C、SPI、GPIO、音频等) | 这里文件最多,别慌,按子系统找 |
| dts/ | 设备树源文件(Device Tree Source) | 咱们今天的主角,后面重点讲 |
| include/ | 头文件,特别是设备树绑定 | 写DTS时经常要翻这里的文档 |
| kernel/ | 核心调度、进程管理 | 一般不动,除非做深度定制 |
嗯,这里要注意:不要试图一次性搞懂所有目录。我刚开始做BSP时,花了整整一周去读drivers/下的每个子目录,结果发现大部分都用不上。你只需要关注你的硬件平台相关的部分就好。
make ARCH=arm menuconfig 配合搜索功能(按/键),可以快速定位某个驱动或配置项在源码树中的位置。这比手动翻目录快多了。
3.2 设备树(DTS)——硬件描述的语言
设备树是什么?说白了,它就是一份“硬件说明书”。告诉内核:你的CPU是什么、内存多大、I2C总线上挂了哪些设备、GPIO怎么用。
为什么要有设备树?以前的做法是把硬件信息硬编码在C代码里。换一个板子就要改代码,重新编译。有了DTS,硬件描述和内核代码分开了。换板子?改DTS文件就行,内核不用动。
我曾经在一个项目里吃过这个亏:早期用板级文件(board-xxx.c)的方式,客户换了颗Flash芯片,我得改三四个文件。后来切到设备树,改几行DTS就搞定了。
3.2.1 DTS语法基础
DTS的语法其实很简单,我总结成三句话:
- 节点(Node):用花括号
{}表示一个设备或总线 - 属性(Property):用
key = value;描述设备特性 - 引用(Reference):用
&label引用其他节点
看个最简单的例子:
/dts-v1/;
/ {
model = "SmartSpeaker v1.0";
compatible = "mycompany,smartspeaker";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; // 512MB内存
};
chosen {
stdout-path = &serial0;
};
};
这里 model 和 compatible 是必须的,内核通过它们来匹配对应的驱动。 reg 属性描述地址和大小,格式是 <起始地址 长度>。
3.3 为智能音箱编写DTS文件
好,现在咱们来实战。假设我们的智能音箱用的是全志H3芯片(四核Cortex-A7),需要配置以下外设:
- CPU:4核Cortex-A7,最高1.2GHz
- 内存:512MB DDR3
- I2C:挂载音频编解码器(ES8316)和触摸按键
- SPI:连接Flash存储(W25Q128)
- GPIO:控制LED指示灯、按键、功放使能
3.3.1 CPU和内存配置
/dts-v1/;
#include "sun8i-h3.dtsi" // 引用芯片级DTSI
/ {
model = "SmartSpeaker H3";
compatible = "mycompany,smartspeaker-h3", "allwinner,sun8i-h3";
memory@40000000 {
device_type = "memory";
reg = <0x40000000 0x20000000>; // 512MB,从0x40000000开始
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0x0>;
clocks = <&ccu CLK_CPUX>;
clock-frequency = <1200000000>; // 1.2GHz
};
// cpu1, cpu2, cpu3 类似,略
};
};
嗯,这里要注意:#address-cells 和 #size-cells 决定了reg属性里地址和长度各占几个32位字。对于32位ARM,通常都是1。
3.3.2 I2C设备配置
I2C总线上挂载音频编解码器,地址是0x18:
&i2c0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&i2c0_pins>;
es8316: audio-codec@18 {
compatible = "everest,es8316";
reg = <0x18>;
clocks = <&codec_osc>;
clock-names = "mclk";
#sound-dai-cells = <0>;
};
touch_key: touch-key@38 {
compatible = "mycompany,touch-key";
reg = <0x38>;
interrupt-parent = &<pio>;
interrupts = <6 2 IRQ_TYPE_EDGE_FALLING>; // PG2引脚
};
};
我个人习惯,每个I2C设备都加上 status = "okay" 和 pinctrl 配置。曾经有个项目,I2C设备死活不工作,最后发现是忘了配引脚复用。从那以后,pinctrl成了我的必填项。
3.3.3 SPI Flash配置
&spi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins>;
cs-gpios = <&pio 2 3 GPIO_ACTIVE_LOW>; // PC3作为片选
flash: w25q128@0 {
compatible = "winbond,w25q128", "jedec,spi-nor";
reg = <0x0>;
spi-max-frequency = <50000000>; // 50MHz
m25p,fast-read;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot@0 {
label = "boot";
reg = <0x0 0x100000>; // 1MB
};
rootfs@100000 {
label = "rootfs";
reg = <0x100000 0xE00000>; // 14MB
};
};
};
};
这里我用了 fixed-partitions 来定义分区。你想想看,如果没有分区表,内核怎么知道哪里是boot,哪里是rootfs?
3.3.4 GPIO配置
GPIO在智能音箱里用得最多:LED指示灯、按键、功放使能、复位信号等。
&pio {
// 定义引脚复用功能
i2c0_pins: i2c0-pins {
pins = "PA11", "PA12";
function = "i2c0";
bias-pull-up;
};
spi0_pins: spi0-pins {
pins = "PC0", "PC1", "PC2", "PC3";
function = "spi0";
};
led_pins: led-pins {
pins = "PG0", "PG1"; // 两个LED
function = "gpio_out";
drive-strength = <10>;
};
};
/ {
leds {
compatible = "gpio-leds";
power_led: power {
label = "power:blue";
gpios = <&pio 6 0 GPIO_ACTIVE_HIGH>; // PG0
default-state = "on";
};
wifi_led: wifi {
label = "wifi:green";
gpios = <&pio 6 1 GPIO_ACTIVE_HIGH>; // PG1
default-state = "off";
};
};
gpio-keys {
compatible = "gpio-keys";
aut-repeat;
power_key {
label = "Power Key";
gpios = <&pio 6 2 GPIO_ACTIVE_LOW>; // PG2
linux,code = <KEY_POWER>;
};
volume_up {
label = "Volume Up";
gpios = <&pio 6 3 GPIO_ACTIVE_LOW>; // PG3
linux,code = <KEY_VOLUMEUP>;
};
};
};
<pio 组号 引脚号 极性>。组号从0开始(PA=0, PB=1, PC=2...),引脚号从0开始。极性用 GPIO_ACTIVE_HIGH 或 GPIO_ACTIVE_LOW。
3.4 内核裁剪与编译
写完DTS,接下来就是裁剪和编译内核了。智能音箱的资源有限,咱们得把不需要的功能统统去掉。
3.4.1 内核裁剪原则
我总结了一个“三砍三留”原则:
- 砍掉:不用的文件系统(如XFS、Btrfs)、不用的网络协议(如IPX、AppleTalk)、不用的驱动(如显卡、USB HID)
- 保留:音频子系统、I2C/SPI核心、GPIO框架、网络核心、闪存文件系统(如UBIFS、F2FS)
具体操作:
# 先加载默认配置
make ARCH=arm sun8i_h3_defconfig
# 进入菜单配置
make ARCH=arm menuconfig
# 裁剪示例(在menuconfig中操作)
# 1. 去掉不用的文件系统
# File systems → 取消勾选 XFS、Btrfs、FAT(如果不用)
# 2. 去掉不用的网络协议
# Networking support → Networking options → 取消 IPX、Appletalk
# 3. 保留音频
# Device Drivers → Sound card support → 勾选 ALSA、ES8316驱动
嗯,这里要注意:不要一次性砍太多。我曾经为了追求极致大小,把printk都砍了,结果系统出问题连日志都看不到。建议分步裁剪,每步都编译测试一下。
3.4.2 编译内核和设备树
# 设置环境变量
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# 编译内核
make -j4 zImage
# 编译设备树
make -j4 dtbs
# 编译模块
make -j4 modules
# 安装模块到目标文件系统
make modules_install INSTALL_MOD_PATH=/path/to/rootfs
# 最终产物
# arch/arm/boot/zImage —— 内核镜像
# arch/arm/boot/dts/sun8i-h3-smartspeaker.dtb —— 设备树二进制
V=1 参数(如 make V=1 zImage),可以看到详细的编译命令。如果报错,能快速定位是哪个文件出了问题。
3.4.3 验证设备树
编译好的DTB文件,可以用 dtc 工具反编译验证:
# 反编译DTB为DTS
dtc -I dtb -O dts -o output.dts sun8i-h3-smartspeaker.dtb
# 检查是否有语法错误
dtc -I dtb -O dtb -o /dev/null sun8i-h3-smartspeaker.dtb
如果反编译出来的DTS和你写的差不多,说明语法没问题。如果报错,多半是属性值写错了或者引用了不存在的节点。
#include 的路径不对,引用了旧版本的dtsi文件。建议每次修改DTS后,都检查一下引用的头文件是不是最新的。
3.5 本章小结
这一章咱们干了三件事:
- 搞懂了内核源码树的结构,知道东西放哪
- 学会了设备树语法,并给智能音箱写了完整的DTS
- 掌握了内核裁剪和编译的方法
说实话,设备树这部分,刚开始会觉得有点绕。但只要你动手写几次,就会发现它其实很直观。下一章咱们会讲根文件系统的构建,到时候这些配置就派上用场了。
记住:纸上得来终觉浅,绝知此事要躬行。建议你拿一块开发板,照着今天的DTS自己写一遍,编译烧录看看效果。遇到问题别怕,调试的过程才是真正学到东西的时候。