2. Linux内核源码结构:内核源码树解析、Kconfig与Makefile机制、设备树(DTS)基础语法

好,咱们正式开始啃内核源码。很多新手一看到Linux内核源码目录就头大——几千个文件夹,上百万行代码,从哪下手?

别慌。我当年第一次编译内核时也懵过,对着arch/和drivers/发了好几天呆。后来带我的老工程师扔给我一句话:「你不需要看懂所有代码,你只需要知道代码在哪、怎么配、怎么编。」

今天这一章,我就带你把这三点彻底搞明白。

2.1 内核源码树:先认路,再开车

先看一张整体结构图,把内核源码的骨架印在脑子里。

Linux内核源码顶层目录结构 Linux Kernel 源码根 arch/ drivers/ include/ kernel/ ... arm/ arm64/ x86/ riscv/ i2c/ spi/ gpio/ busses/ algos/ i2c-core.c 💡 驱动开发重点关注目录 • arch/arm/boot/dts/ —— 设备树源文件(.dts/.dtsi),I2C传感器移植必改 • drivers/i2c/ —— I2C核心框架 + 总线适配器驱动 • include/linux/i2c.h —— I2C驱动API头文件,写驱动时反复引用

内核源码顶层目录,每个都有明确分工。我挑几个跟咱们I2C传感器移植最相关的说说:

目录干什么的咱们用在哪
arch/CPU架构相关代码设备树文件在 arch/arm/boot/dts/
drivers/所有外设驱动I2C驱动在 drivers/i2c/
include/内核头文件include/linux/i2c.h 天天见
kernel/核心调度/进程管理一般不动,了解即可
Documentation/内核文档查设备树绑定说明,很有用

我的习惯:拿到一个新内核,先看 arch/ 和 drivers/ 这两个目录。尤其是 drivers/i2c/ 下面,busses/ 放的是I2C控制器驱动,algos/ 是算法,顶层 i2c-core.c 是核心框架。搞清楚这三个,I2C驱动移植就成功了一半。

2.2 Kconfig与Makefile:内核的「菜单」和「菜谱」

内核不是把所有代码都编译进去的。你想想看,一个嵌入式板子才多大空间?所以内核用了一套配置系统,让你只选需要的功能。

这套系统就两个核心文件:Kconfig 和 Makefile。

2.2.1 Kconfig —— 配置菜单怎么来的

你在 make menuconfig 里看到的那些选项,就是Kconfig生成的。每个驱动目录下都有一个Kconfig文件。

# drivers/i2c/Kconfig  (简化示例)
menu "I2C support"
    config I2C
        tristate "I2C bus support"
        help
          I2C is a low-speed bus used by many sensors...

    config I2C_CHARDEV
        tristate "I2C device interface"
        depends on I2C
        help
          Say Y here to use /dev/i2c-N interface...

    config SENSOR_BMP280
        tristate "BMP280 pressure sensor"
        depends on I2C
        help
          Driver for Bosch BMP280 pressure sensor.
endmenu

关键语法就几个:

  • tristate:三态——不编译(Y)、编译进内核(N)、编译成模块(M)。驱动一般选M,方便调试
  • depends on:依赖关系。比如传感器驱动依赖I2C核心
  • select:反向选择。A选了,B自动被选上
  • help:帮助信息,写清楚这个驱动是干啥的

我曾经踩过的坑:有一次移植一个温湿度传感器,在Kconfig里忘了写 depends on I2C。结果menuconfig里能看到选项,选上后编译直接报错——找不到I2C相关的结构体。查了半天才发现是依赖没写。所以,依赖关系一定要写清楚,内核不会帮你自动推导。

2.2.2 Makefile —— 编译规则怎么写

Kconfig决定了「要不要编译」,Makefile决定了「怎么编译」。

# drivers/i2c/Makefile (简化)
obj-$(CONFIG_I2C)          += i2c-core.o
obj-$(CONFIG_I2C_CHARDEV)  += i2c-dev.o
obj-$(CONFIG_SENSOR_BMP280) += bmp280.o

规则很简单:

  • obj-y —— 编译进内核
  • obj-m —— 编译成模块
  • obj-$(CONFIG_XXX) —— 根据配置自动决定

比如 CONFIG_SENSOR_BMP280=y,就等价于 obj-y += bmp280.o,编译进内核。

我建议:写驱动时,先在Kconfig里加好选项,再到Makefile里加一行。顺序别搞反。我见过有人先改Makefile,结果Kconfig没配,编译时提示「未定义的配置项」——虽然也能编过,但menuconfig里看不到,别人接手你的代码会一脸懵。

2.3 设备树(DTS):硬件怎么告诉内核的

早期Linux,硬件信息硬编码在C文件里。换一个板子就要改代码、重新编译。后来有了设备树,把硬件描述和驱动代码分开了。

说白了,设备树就是一份「硬件配置单」。内核启动时读这份配置单,才知道「哦,这个I2C总线上挂了一个BMP280,地址是0x76」。

2.3.1 设备树文件体系

后缀全称作用
.dtsDevice Tree Source板级设备树源文件,一个板子一个
.dtsiDevice Tree Source Include公共头文件,SoC厂商提供
.dtbDevice Tree Blob编译后的二进制,内核直接使用
.dtboDevice Tree Blob Overlay叠加层,运行时动态修改

编译命令很简单:

# .dts → .dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 反编译看看
dtc -I dtb -O dts -o myboard.dts myboard.dtb

2.3.2 基础语法:一个I2C传感器的设备树节点

直接看例子。这是咱们后面项目会用到的BMP280传感器节点:

/dts-v1/;

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

    // 根节点下的I2C控制器
    i2c0: i2c@ff010000 {
        compatible = "snps,designware-i2c";
        reg = <0xff010000 0x1000>;
        #address-cells = <1>;
        #size-cells = <0>;
        clock-frequency = <100000>;
        status = "okay";

        // 挂在这个总线上的传感器
        bmp280@76 {
            compatible = "bosch,bmp280";
            reg = <0x76>;
        };
    };
};

拆开来看每个属性:

  • compatible:驱动匹配的关键字。内核通过这个字符串找到对应的驱动
  • reg:地址信息。对I2C设备来说就是从设备地址(0x76)
  • #address-cells / #size-cells:地址和长度用几个32位整数表示
  • status:设备状态,"okay"表示启用,"disabled"表示禁用
  • clock-frequency:I2C总线频率,100KHz是标准模式

核心要点:设备树节点名格式为 设备名@地址。比如 bmp280@76,意思是「地址0x76上的BMP280」。这不是随便写的,内核解析时会用reg的值去匹配。

2.3.3 驱动怎么和设备树匹配

驱动里用 of_match_table 来匹配设备树节点:

// 驱动中的匹配表
static const struct of_device_id bmp280_of_match[] = {
    { .compatible = "bosch,bmp280" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, bmp280_of_match);

// 在platform_driver中注册
static struct platform_driver bmp280_driver = {
    .probe  = bmp280_probe,
    .remove = bmp280_remove,
    .driver = {
        .name = "bmp280",
        .of_match_table = bmp280_of_match,
    },
};

流程是这样的:

  1. 内核启动,解析.dtb,建立设备树
  2. 遍历所有节点,根据compatible找驱动
  3. 找到匹配的驱动,调用probe函数
  4. probe里通过 of_get_property() 或 device_property_read_*() 读取设备树属性

我曾经犯过的错:设备树里compatible写的是 "bosch,bmp280",驱动里写的是 "bosh,bmp280"——少了个c。内核启动时日志显示 No driver found,我查了两天才发现是拼写错误。所以,compatible字符串一定要复制粘贴,别手敲。

2.4 本章小结:一张图串起来

把Kconfig、Makefile、设备树、驱动代码的关系理清楚,就是下面这张图:

内核驱动编译与匹配全流程 Kconfig 定义配置选项 Makefile 条件编译规则 编译内核/模块 .ko / zImage 设备树 .dts 硬件描述 dtc 编译 .dts → .dtb 内核启动 解析.dtb + 加载驱动 probe() 被调用

嗯,这一章内容不少。但你只要记住三件事:

  • Kconfig 管「要不要」
  • Makefile 管「怎么编」
  • 设备树 管「硬件长什么样」

后面咱们写BMP280驱动时,这三样东西一个都跑不掉。到时候你会回来翻这一章的——我保证。