4、Linux内核移植与定制:内核配置与裁剪、设备树(Device Tree)编写、驱动模块加载机制
好,咱们进入第四章。这一章可以说是整个小基站软件栈的「地基工程」。你想想看,一个基站跑在Linux上,内核不对路,后面全是白搭。我这些年调过不少板子,最怕的就是内核一启动就panic,连个串口日志都来不及看。所以,内核移植与定制这件事,咱们得掰开了揉碎了讲清楚。
4.1 内核配置与裁剪:别让基站背着「全家桶」跑
小基站不是你的桌面电脑。它的Flash和内存都很金贵。我见过有人直接把PC上的内核.config文件扔到嵌入式板子上,结果内核镜像大了好几兆,启动慢不说,还占了一堆根本用不上的驱动。说白了,内核裁剪就是「断舍离」。
怎么做?我习惯用make menuconfig。它是个基于ncurses的图形化配置界面,操作起来直观。你可以在里面逐项选择要编译进内核的功能,或者编译成模块。
核心原则:
- Y:编译进内核(built-in),适合必须有的功能,比如根文件系统驱动、串口驱动。
- M:编译成模块(module),适合按需加载的驱动,比如网卡、USB设备。
- N:不编译,彻底去掉。
我个人的经验是:对于小基站,先把所有不相关的硬件驱动全关掉。比如你的板子上没有显卡,那就把DRM、GPU相关的全干掉。没有触摸屏,Input子系统里除了键盘鼠标,其他都可以砍。我曾经把一个4MB的内核裁剪到1.2MB,启动时间从8秒降到了3秒。嗯,这感觉挺爽的。
另外,别忘了配置实时性相关的选项。小基站对时延敏感,我建议开启CONFIG_PREEMPT_RT(如果内核支持)或者至少开启CONFIG_PREEMPT。否则,一个中断处理不及时,基站的同步信号就可能出问题。
4.2 设备树(Device Tree)编写:给内核一张「硬件地图」
设备树,说白了就是告诉内核:「你的板子上有哪些硬件,它们挂在哪条总线上,地址是多少,中断号是多少。」没有设备树,内核就像个瞎子,不知道该怎么跟硬件打交道。
设备树的源文件是.dts,编译后生成.dtb。我刚开始写设备树时,经常犯一个低级错误——忘记加status = "okay";。结果驱动加载了,但硬件没使能,折腾了半天才发现是设备树里默认把外设禁用了。
来看一个典型的小基站设备树片段:
/dts-v1/;
#include "soc_base.dtsi"
/ {
model = "SmallCell-Baseband-v2";
compatible = "vendor,smallcell-bb-v2";
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw";
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512MB RAM */
};
&uart0 {
status = "okay";
clock-frequency = <50000000>;
};
&spi1 {
status = "okay";
cs-gpios = <&gpio2 5 GPIO_ACTIVE_LOW>;
fpga: fpga@0 {
compatible = "vendor,baseband-fpga";
reg = <0>;
spi-max-frequency = <20000000>;
interrupts = <0 45 4>;
};
};
};
这里有几个关键点:
- compatible:内核驱动通过这个字符串来匹配设备。名字要唯一,我习惯用「厂商,设备名」的格式。
- reg:描述地址和大小。比如内存的起始地址和大小,或者SPI设备的片选地址。
- interrupts:中断号。注意,不同架构的编码方式不同,ARM常用的是
<中断类型 中断号 触发方式>。
避坑指南:我曾经在调试一块FPGA加速卡时,设备树里写错了中断号,结果驱动一直收不到中断。查了两天才发现,原来设备树里的中断号是硬件中断号,而内核里用的是IRQ号,中间差了一个偏移量。嗯,这个坑我替你们踩过了。
4.3 驱动模块加载机制:动态还是静态?这是个问题
驱动怎么加载?两种方式:编译进内核(built-in),或者编译成模块(.ko)动态加载。在小基站场景下,我建议这样选:
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 根文件系统驱动(MMC、NAND) | built-in | 启动时必须先挂载根文件系统 |
| 串口驱动(UART) | built-in | 调试用,必须尽早可用 |
| 基带FPGA驱动 | 模块 | 按需加载,方便升级 |
| 网络驱动(PHY、MAC) | 模块 | 可热插拔,灵活管理 |
| 电源管理驱动 | built-in | 系统休眠唤醒必须依赖 |
模块加载的命令很简单:insmod、rmmod、modprobe。但这里有个细节——modprobe会自动处理依赖关系,而insmod不会。我建议你写一个启动脚本,在系统初始化时用modprobe批量加载模块。
举个例子,假设你的基带驱动叫baseband_fpga.ko,它依赖spi_dev.ko:
#!/bin/sh
# 加载基带驱动及其依赖
modprobe spi_dev
modprobe baseband_fpga
# 检查是否加载成功
lsmod | grep baseband
注意:模块加载顺序很重要。如果基带驱动依赖SPI总线,那必须先加载SPI控制器驱动,再加载基带驱动。否则你会看到Unknown symbol的错误。我刚开始做时,就因为这个顺序问题,浪费了一整个下午。
另外,如果你想让模块在系统启动时自动加载,可以把模块名写入/etc/modules文件,或者放在/etc/modules-load.d/目录下。这样每次开机,系统就会自动帮你搞定。
4.4 实战经验:一次内核移植的完整流程
最后,我分享一下我在一个真实小基站项目中的操作流程。这个板子用的是ARM Cortex-A72 + FPGA的架构:
- 获取内核源码:从厂商的SDK里拿到打了补丁的内核,版本是5.10 LTS。
- 配置内核:先用
make defconfig生成默认配置,再用make menuconfig裁剪。我关掉了蓝牙、WiFi、GPU、Sound等所有无关功能。 - 编写设备树:参考厂商的
.dtsi文件,添加FPGA、PHY芯片、时钟芯片的节点。特别注意中断号和GPIO的映射。 - 编译内核和设备树:
make -j4 Image dtbs。生成Image和.dtb文件。 - 编译驱动模块:
make modules,然后把.ko文件拷贝到根文件系统的/lib/modules/目录下。 - 烧录测试:把内核、设备树、根文件系统烧到板子上。上电后,通过串口观察启动日志。如果内核崩溃,检查设备树里的
reg和interrupts是否正确。
说实话,第一次启动时,我连串口都没看到输出。后来发现是设备树里chosen节点的bootargs写错了控制台参数。改过来后,看到熟悉的Starting kernel ...,心里那块石头才算落地。
好了,这一章的内容就到这里。内核移植这件事,说难不难,说简单也不简单。关键是多动手、多踩坑。下一章我们会聊聊根文件系统的构建,到时候见。