2. Linux内核源码结构:内核源码树解析、Kconfig与Makefile机制、设备树(DTS)基础语法
好,咱们正式开始啃内核源码。很多新手一看到Linux内核源码目录就头大——几千个文件夹,上百万行代码,从哪下手?
别慌。我当年第一次编译内核时也懵过,对着arch/和drivers/发了好几天呆。后来带我的老工程师扔给我一句话:「你不需要看懂所有代码,你只需要知道代码在哪、怎么配、怎么编。」
今天这一章,我就带你把这三点彻底搞明白。
2.1 内核源码树:先认路,再开车
先看一张整体结构图,把内核源码的骨架印在脑子里。
内核源码顶层目录,每个都有明确分工。我挑几个跟咱们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 设备树文件体系
| 后缀 | 全称 | 作用 |
|---|---|---|
.dts | Device Tree Source | 板级设备树源文件,一个板子一个 |
.dtsi | Device Tree Source Include | 公共头文件,SoC厂商提供 |
.dtb | Device Tree Blob | 编译后的二进制,内核直接使用 |
.dtbo | Device 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,
},
};
流程是这样的:
- 内核启动,解析.dtb,建立设备树
- 遍历所有节点,根据compatible找驱动
- 找到匹配的驱动,调用probe函数
- probe里通过
of_get_property()或device_property_read_*()读取设备树属性
我曾经犯过的错:设备树里compatible写的是 "bosch,bmp280",驱动里写的是 "bosh,bmp280"——少了个c。内核启动时日志显示 No driver found,我查了两天才发现是拼写错误。所以,compatible字符串一定要复制粘贴,别手敲。
2.4 本章小结:一张图串起来
把Kconfig、Makefile、设备树、驱动代码的关系理清楚,就是下面这张图:
嗯,这一章内容不少。但你只要记住三件事:
- Kconfig 管「要不要」
- Makefile 管「怎么编」
- 设备树 管「硬件长什么样」
后面咱们写BMP280驱动时,这三样东西一个都跑不掉。到时候你会回来翻这一章的——我保证。