第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;
    };
};

这里 modelcompatible 是必须的,内核通过它们来匹配对应的驱动。 reg 属性描述地址和大小,格式是 <起始地址 长度>

避坑指南: 我曾经在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>;
        };
    };
};
关键点: GPIO的编号规则是 <pio 组号 引脚号 极性>。组号从0开始(PA=0, PB=1, PC=2...),引脚号从0开始。极性用 GPIO_ACTIVE_HIGHGPIO_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 本章小结

这一章咱们干了三件事:

  1. 搞懂了内核源码树的结构,知道东西放哪
  2. 学会了设备树语法,并给智能音箱写了完整的DTS
  3. 掌握了内核裁剪和编译的方法

说实话,设备树这部分,刚开始会觉得有点绕。但只要你动手写几次,就会发现它其实很直观。下一章咱们会讲根文件系统的构建,到时候这些配置就派上用场了。

记住:纸上得来终觉浅,绝知此事要躬行。建议你拿一块开发板,照着今天的DTS自己写一遍,编译烧录看看效果。遇到问题别怕,调试的过程才是真正学到东西的时候。