第3章 内核配置入门:menuconfig图形化配置、defconfig与dts分离、内核模块与内建驱动选择策略

说实话,很多刚接触Rockchip平台的朋友,第一件事就是拿个默认配置文件,make menuconfig进去瞎改一通。改完编译,烧进去,系统起不来——然后就开始怀疑人生。

我当年也干过这种事。记得第一次给RK3288做BSP,我把所有驱动都编进内核,结果内核镜像大了快一倍,启动慢得像蜗牛。后来才明白,内核配置这件事,看似简单,其实门道很深。

这一章,我就带你把这些门道捋清楚。

3.1 menuconfig图形化配置:不只是点点菜单

make menuconfig是Linux内核配置的标配工具。但很多人只把它当成一个菜单浏览器,其实它的交互逻辑里藏着不少信息。

3.1.1 基本操作与符号含义

进入menuconfig后,你会看到各种选项前面有不同符号。这些符号代表什么?我整理了一张表:

符号 含义 说明
[ ] 未选中 该功能不编译
[*] 内建 编译进内核镜像
<M> 模块 编译为独立.ko文件
< > 未选择 与[ ]类似,但可切换为M
---> 子菜单 按Enter进入

这里有个小技巧:按 / 键可以搜索配置项。比如你想找RK3588的MIPI DSI驱动,直接按 / 输入 DSI,所有相关选项就列出来了。这个功能我几乎每天都在用。

我的习惯:每次改完配置,先按 ? 查看选项的帮助信息。很多坑都在帮助文档里写着,比如依赖关系、需要哪些前置驱动。别嫌麻烦,这一步能省你半天调试时间。

3.1.2 依赖关系与反向依赖

menuconfig里最让人头疼的就是依赖关系。你选了一个选项,结果它自动帮你选了另外一堆。有时候你取消一个选项,又发现其他选项跟着变灰了。

为什么会这样?因为内核的Kconfig系统有 depends on 和 select 两种机制:

  • depends on:当前选项依赖某个选项被选中。如果依赖项没选,当前选项不可见或不可选。
  • select:当前选项被选中时,自动选中另一个选项。这是反向依赖,常用于硬件平台必须启用的功能。

我在做RK3566项目时遇到过一个问题:我明明选了GPU驱动,但menuconfig里死活找不到Mali驱动选项。后来查了Kconfig文件才发现,它 depends on DRM,而DRM我没开。这就是典型的依赖链断裂。

避坑指南:不要手动去改 .config 文件来绕过依赖检查。我曾经这么干过,结果编译出来的内核在运行时直接panic。依赖关系是硬约束,绕不过去的。

3.2 defconfig与dts分离:Rockchip平台的标准做法

Rockchip平台的内核配置,有一个非常清晰的架构:defconfig负责软件功能开关,dts负责硬件描述。这两者必须分离,不能混在一起。

3.2.1 为什么需要分离?

你想想看,一块RK3588芯片,可以做成开发板、平板、电视棒、NAS主板……每种产品的硬件都不一样。如果每次改硬件都要重新配内核,那工作量就太大了。

分离的好处很明显:

  • defconfig:定义芯片平台通用的功能开关,比如文件系统、网络协议栈、调度器类型。这些跟具体硬件无关。
  • dts:描述具体硬件,比如GPIO复用、I2C地址、时钟频率。这些跟具体板子相关。

在Rockchip的SDK里,你通常能看到这样的文件结构:

arch/arm64/configs/
  rockchip_linux_defconfig      # 通用配置
  rockchip_linux_evb1_defconfig # EVB板专用配置

arch/arm64/boot/dts/rockchip/
  rk3588.dtsi                   # 芯片级描述
  rk3588-evb1.dts              # 板级描述
  rk3588-evb1-lp4.dts          # 不同内存配置的变体

3.2.2 如何正确管理defconfig?

我个人习惯的做法是:

  1. 从官方defconfig开始:make rockchip_linux_defconfig
  2. 用menuconfig做增量修改:只改你需要的选项
  3. 保存修改后的配置:make savedefconfig,生成精简版defconfig
  4. 对比差异:用diff工具看改了哪些

这里有个关键点:make savedefconfig 会生成一个 defconfig 文件,它只包含非默认值的选项。这个文件比完整的 .config 小得多,也更容易维护。

核心原则:defconfig里只放芯片平台必须的选项,不要放板级特有的配置。板级差异全部通过dts来体现。

3.2.3 dts的覆盖机制

Rockchip的dts采用多层覆盖结构:

rk3588.dtsi                  # 芯片级:CPU、GPU、内存控制器、时钟
  └── rk3588-evb1.dtsi      # 板级公共:UART、I2C、GPIO默认配置
       └── rk3588-evb1.dts  # 具体产品:差异化配置

这种结构的好处是,芯片升级时只需要改 .dtsi,板级文件基本不用动。我在RK3399升级到RK3588时,就靠这个机制省了大量移植工作。

3.3 内核模块与内建驱动选择策略

这是内核配置里最需要权衡的部分。选模块还是选内建,没有绝对的对错,只有合不合适的场景。

3.3.1 内建(built-in)的适用场景

内建驱动会直接编译进内核镜像,系统启动时自动初始化。适合以下情况:

  • 根文件系统驱动:比如MMC/SD卡、SATA、NVMe。如果这些驱动是模块,内核就挂载不了根文件系统。
  • 系统核心功能:比如中断控制器、定时器、时钟管理。这些必须在系统启动早期就可用。
  • 实时性要求高的设备:模块加载有延迟,内建驱动没有。

3.3.2 模块(module)的适用场景

模块是独立的 .ko 文件,可以在系统运行时动态加载或卸载。适合以下情况:

  • 不常用的外设:比如蓝牙、WiFi、USB摄像头。平时不用就不加载,节省内存。
  • 调试中的驱动:开发阶段用模块,方便反复修改和测试,不用每次都重编内核。
  • 需要热插拔的设备:模块可以按需加载,配合udev自动处理。

3.3.3 Rockchip平台的实际选择策略

基于我在多个Rockchip项目上的经验,我总结了一套选择策略:

驱动类型 推荐方式 原因
MMC/SDIO 内建 根文件系统依赖
eMMC控制器 内建 系统启动必须
DDR控制器 内建 内存管理核心
GPU驱动 模块 体积大,按需加载
VPU编解码 模块 多媒体场景才需要
WiFi/BT 模块 热插拔,可卸载
USB控制器 内建 早期初始化,支持键盘/鼠标
以太网MAC 内建 网络根文件系统(NFS)需要
我的经验:如果你不确定某个驱动该选什么,先选模块。系统启动后,用 lsmod 看哪些模块被自动加载了。如果某个模块每次启动都加载,那就说明它应该内建。这叫"从实践中来,到实践中去"。

3.3.4 模块加载顺序的坑

模块之间也有依赖关系。比如一个I2C设备驱动,依赖I2C总线驱动先加载。如果顺序错了,设备就找不到。

内核通过 modules.dep 文件管理依赖关系。你可以在编译后查看:

# 查看模块依赖
modinfo rk3588-mipi-dsi.ko
# 输出中会显示 depends: drm, phy-rockchip-samsung-hdptx

# 手动加载时,依赖模块会自动加载
modprobe rk3588-mipi-dsi

这里要注意:modprobe 会自动处理依赖,但 insmod 不会。所以生产环境一定要用 modprobe。

我曾经踩过的坑:在RK3588上调试MIPI DSI屏幕,我直接用 insmod 加载驱动,结果报错说找不到 drm_panel 符号。查了半天才发现,drm_panel 是另一个模块,必须先加载。换成 modprobe 就一切正常了。

3.4 本章知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张流程图:

内核配置知识体系 menuconfig图形化配置 defconfig与dts分离 模块与内建选择策略 符号含义 搜索功能 依赖关系 芯片级配置 板级配置 dts覆盖机制 内建场景 模块场景 加载顺序 核心原则:软件功能用defconfig,硬件差异用dts 启动必须的驱动内建,可热插拔的用模块

3.5 实战:从零配置一个Rockchip内核

光说不练假把式。我们走一遍完整的配置流程:

  1. 选择基础defconfig:
    make ARCH=arm64 rockchip_linux_defconfig
  2. 进入menuconfig做定制:
    make ARCH=arm64 menuconfig
    比如你要添加一个SPI显示屏驱动,按 / 搜索 SPI,找到对应选项设为 <M>。
  3. 保存精简配置:
    make ARCH=arm64 savedefconfig
    cp defconfig arch/arm64/configs/my_board_defconfig
  4. 编写dts文件:
    // arch/arm64/boot/dts/rockchip/my-board.dts
    #include "rk3588.dtsi"
    #include "rk3588-evb1.dtsi"
    
    / {
        model = "My Custom Board";
        compatible = "my,board", "rockchip,rk3588";
    };
    
    &spi2 {
        status = "okay";
        display: ili9341@0 {
            compatible = "ilitek,ili9341";
            reg = <0>;
            spi-max-frequency = <40000000>;
        };
    };
  5. 编译并验证:
    make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8
    # 检查模块是否生成
    find . -name "*.ko" | grep ili9341
小提示:第一次编译建议加 V=1 参数,能看到详细的编译命令。如果报错,能快速定位是哪个文件出了问题。

嗯,这一章的内容就到这里。内核配置看似是体力活,但背后有清晰的逻辑。你只要掌握了menuconfig的操作、defconfig与dts的分离原则、以及模块与内建的选择策略,Rockchip平台的内核配置对你来说就不再是难题。