4、Linux内核移植:从源码到跑起来

好,咱们进入正题。内核移植这事儿,说白了就是让Linux内核认识你的硬件平台。我刚开始做自动驾驶平台移植时,觉得这步就是改改配置、编译一下,结果第一次启动直接黑屏——嗯,从那以后我再也不敢小看这一步了。

4.1 内核源码获取与配置

先说说源码。我个人习惯从kernel.org拉主线代码,但做自动驾驶平台时,我建议你用芯片厂商提供的BSP内核。为什么?因为厂商已经帮你适配好了大部分驱动,你只需要做平台相关的微调。

获取源码后,第一件事就是配置。这里我要强调:千万别直接make menuconfig从头配。我见过太多新手这么干,配了三天三夜,最后编译还报错。

我的做法:先找厂商提供的defconfig文件,比如arch/arm64/configs/xxx_defconfig。然后基于它做修改。
# 加载厂商默认配置
make ARCH=arm64 xxx_defconfig

# 进入图形化配置界面
make ARCH=arm64 menuconfig

menuconfig里要注意几个关键项:

  • System Type:选择你的SoC型号,比如NVIDIA Orin、Qualcomm SA8295
  • Device Drivers:这里要勾选你平台的外设驱动,比如CAN控制器、以太网PHY、摄像头接口
  • File systems:自动驾驶常用ext4、squashfs(只读根文件系统)
避坑指南:我曾经在配置时漏掉了RTC驱动,结果系统时间一直不对,导致传感器时间戳全乱套。排查了两天才发现是内核配置里没勾选RTC。

4.2 设备树编写与编译

设备树(Device Tree)是Linux内核用来描述硬件信息的机制。说白了,就是告诉内核:你的板子上有哪些外设、它们挂在哪条总线上、中断号是多少。

我刚开始写设备树时,最头疼的就是地址映射。你想想看,一个SoC内部有几十个外设,每个外设的寄存器基地址都得对得上芯片手册。

设备树的基本结构是这样的:

/dts-v1/;

/ {
    model = "My Autonomous Driving Platform";
    compatible = "vendor,platform-name";

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw";
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x0 0x80000000 0x0 0x80000000>;
    };

    soc {
        #address-cells = <2>;
        #size-cells = <2>;
        compatible = "simple-bus";
        ranges;

        uart0: serial@28000000 {
            compatible = "vendor,uart";
            reg = <0x0 0x28000000 0x0 0x1000>;
            interrupts = <0 33 4>;
            clock-frequency = <24000000>;
        };

        can0: can@31000000 {
            compatible = "vendor,can";
            reg = <0x0 0x31000000 0x0 0x1000>;
            interrupts = <0 45 4>;
        };
    };
};

编译设备树用DTC工具:

# 编译设备树
dtc -I dts -O dtb -o myplatform.dtb myplatform.dts

# 反编译(调试用)
dtc -I dtb -O dts -o myplatform.dts myplatform.dtb
关键点:设备树中的reg属性必须和芯片手册完全一致。我遇到过最坑的一次,是CAN控制器的中断号写错了,结果CAN报文收发一直超时。查了三天,最后发现是设备树里中断号偏移了一位。

4.3 内核启动参数设置

Bootargs是内核启动时传递给内核的参数。它决定了内核怎么找到根文件系统、用什么控制台输出、内存怎么分配。

常见的bootargs参数:

参数 说明 示例
console 指定控制台设备 console=ttyS0,115200
root 指定根文件系统位置 root=/dev/mmcblk0p2
rw 以读写方式挂载根文件系统 rw
init 指定init进程路径 init=/sbin/init
mem 限制可用内存大小 mem=4G

在自动驾驶平台上,我通常会加这几个参数:

console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait
cma=256M@0x10000000
isolcpus=2-3
rcupdate.rcu_expedited=1

解释一下:

  • cma:预留连续内存,给摄像头DMA用
  • isolcpus:隔离CPU核心,用于实时任务
  • rcupdate:加速RCU处理,减少延迟
小技巧:调试阶段可以在bootargs里加一个earlyprintk,这样内核在初始化早期就能输出日志。我曾经靠这个参数定位到一个内存初始化时序问题。

4.4 常见启动问题排查

内核启动不起来,这是家常便饭。我总结了几类常见问题:

4.4.1 内核恐慌(Kernel Panic)

最常见的是找不到根文件系统。你会看到类似这样的输出:

VFS: Cannot open root device "mmcblk0p2" or unknown-block(0,0)
Please append a correct "root=" boot option; here are the available partitions:
...

排查步骤:

  1. 检查bootargs里的root参数是否正确
  2. 确认设备树里MMC/SD控制器节点是否正确
  3. 检查内核是否编译了对应的文件系统驱动(比如ext4)

4.4.2 控制台无输出

启动后串口没任何输出。我遇到过这种情况,原因是:

  • 串口引脚配置不对(检查设备树里的pinctrl)
  • 波特率不匹配(bootargs里写115200,但终端设成了9600)
  • 内核没编译串口驱动

4.4.3 设备驱动加载失败

比如CAN控制器没起来,或者以太网不通。排查方法:

# 查看内核日志
dmesg | grep -i can

# 查看设备树是否被正确解析
ls /proc/device-tree/

# 检查驱动是否加载
lsmod
我曾经踩过的坑:有一次以太网死活不通,查了三天,最后发现是设备树里PHY的复位GPIO号写错了。复位信号没拉起来,PHY一直处于复位状态。从那以后,我每次写设备树都会用示波器量一下关键信号。

4.4.4 内存相关问题

自动驾驶平台内存需求大,经常遇到内存分配失败。排查方法:

  • 检查bootargs里的mem参数是否限制了内存
  • 查看/proc/meminfo确认可用内存
  • cat /proc/iomem查看内存映射

嗯,内核移植这块内容不少,但核心就三点:配置要对、设备树要准、bootargs要合理。我建议你从最简单的配置开始,先让内核能启动到shell,再逐步添加外设驱动。别想着一步到位,那只会让你debug到怀疑人生。

下一章我们会聊根文件系统的构建,到时候再细说怎么让系统真正跑起来。