4. 嵌入式Linux内核移植:内核源码获取与配置、AMD GPU/CPU驱动集成

好,咱们进入第四讲。这一章是实操性最强的一节,也是很多工程师最头疼的部分——把Linux内核跑在AMD的嵌入式平台上。说白了,就是让操作系统认识你的硬件,并且能高效地调度它。

我见过不少团队,硬件设计得挺漂亮,结果卡在内核移植上,一卡就是几个月。其实没那么玄乎,只要把几个关键点吃透,这事儿就能顺下来。

4.1 内核源码获取与配置

先聊源码。你可能会问,从哪搞?两个主流渠道:一是kernel.org的官方主线,二是AMD或板卡厂商提供的BSP(板级支持包)。

我个人习惯用主线内核,因为社区支持好,bug修得快。但如果你用的是比较新的AMD锐龙嵌入式芯片,比如V2000系列,我建议先拿厂商的BSP做起点。为什么?因为有些补丁还没合入主线,你硬要自己backport,那工作量就大了。

小技巧: 下载源码后,第一件事不是急着配置,而是先看 arch/x86/configs/ 目录下有没有现成的defconfig。比如 amd_defconfigx86_64_defconfig。能省不少事。

配置内核,我推荐用 make menuconfig。别用 make xconfig,那玩意儿依赖Qt,嵌入式环境里经常缺库,折腾半天装不上。menuconfig虽然界面朴素,但稳。

配置时,有几个关键选项必须打开:

  • CONFIG_AMD_IOMMU:AMD的IOMMU,虚拟化和大内存场景必须开。
  • CONFIG_DRM_AMDGPU:GPU驱动,后面细说。
  • CONFIG_X86_AMD_PLATFORM_DEVICE:平台设备支持。

嗯,这里要注意:别一股脑把所有AMD相关的选项都打开。我见过有人把 CONFIG_CPU_SUP_AMD 以外的所有AMD选项全选了,结果编译出来的内核比原本大了三倍,还一堆冲突。挑你硬件上实际用到的就行。

4.2 AMD GPU/CPU驱动集成

这部分是重头戏。AMD的驱动在Linux社区里口碑不错,尤其是amdgpu,比NVIDIA的开源驱动成熟得多。

4.2.1 amdgpu驱动集成

amdgpu驱动支持从GCN架构到最新的RDNA架构。在嵌入式场景里,我们通常用它来做显示输出或GPGPU计算。

配置时,确保以下选项开启:

CONFIG_DRM=y
CONFIG_DRM_AMDGPU=y
CONFIG_DRM_AMDGPU_CIK=y  # 如果你用的是老芯片
CONFIG_DRM_AMDGPU_SI=y   # 同上
CONFIG_HSA_AMD=y          # 异构计算支持
警告: 如果你不需要显示输出,只想用GPU做计算,可以关掉 CONFIG_DRM_AMDGPU_DC(Display Core)。这样能省下不少内存,大概20-30MB。我在一个工业视觉项目里这么干过,效果很明显。

我曾经遇到过一个坑:板子上的GPU死活不亮,dmesg里报 amdgpu: Fatal error during GPU init。查了两天,最后发现是固件没放对位置。amdgpu需要 amdgpu/<chip_name>.bin 这类固件文件,你得从 linux-firmware 仓库里拷到 /lib/firmware/amdgpu/ 目录下。缺一不可。

4.2.2 amd_pstate驱动集成

amd_pstate是AMD的CPU频率调控驱动,替代了老的acpi-cpufreq。在锐龙嵌入式芯片上,这个驱动能显著提升能效比。

配置选项:

CONFIG_X86_AMD_PSTATE=y
CONFIG_X86_AMD_PSTATE_UT=y

启用后,你可以通过 cpupower 工具来设置调度策略。我个人习惯用 powersave 模式做低功耗场景,用 performance 模式做实时控制场景。

但要注意,amd_pstate在部分早期锐龙芯片上有bug,会导致系统卡死。如果你用的是V1000系列,我建议先保留acpi-cpufreq作为备选。怎么切换?在kernel cmdline里加 amd_pstate=disable 就行。

4.3 设备树(Device Tree)编写与调试

设备树,说白了就是告诉内核你的硬件长什么样。x86平台传统上用ACPI,但嵌入式场景里,设备树更灵活。

AMD锐龙嵌入式芯片的设备树文件通常放在 arch/x86/boot/dts/amd/ 目录下。一个典型的设备树节点长这样:

&i2c2 {
    status = "okay";
    clock-frequency = <400000>;

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
        pagesize = <8>;
    };
};

写设备树时,最容易犯的错误是地址冲突。我曾经调试一个I2C触摸屏,怎么都读不到数据。后来用 i2cdetect 一扫描,发现设备地址是0x5a,但我在dts里写成了0x5b。就这一个字节的差异,折腾了我一整天。

调试设备树的黄金法则: 编译时加 DTC_FLAGS=-@ 生成符号表,然后用 dtc -I dtb -O dts 反编译,对比你写的源文件和实际加载的二进制。很多时候,问题出在编译器的隐式类型转换上。

另外,别忘了在kernel config里打开 CONFIG_OFCONFIG_OF_UNITTEST。后者可以在启动时做设备树的自检,帮你快速定位语法错误。

4.4 内核编译与模块管理

编译内核,我推荐用 make -j$(nproc)。但别贪心,如果你在虚拟机里编译,-j 参数设成物理核心数的一半就行,否则内存容易爆。

编译完成后,模块安装:

make modules_install INSTALL_MOD_PATH=/path/to/rootfs

这里有个细节:INSTALL_MOD_PATH 指向你的目标根文件系统,而不是本机的 /lib/modules。我见过有人直接 make modules_install,结果把本机的内核模块给覆盖了,重启后系统挂了。

模块管理,常用命令就三个:

  • lsmod:查看已加载模块
  • modprobe:加载模块(会自动处理依赖)
  • modinfo:查看模块信息

你想想看,如果amdgpu模块加载失败,第一件事就是 modinfo amdgpu,看看依赖的固件文件是否存在。我80%的驱动问题,都是靠这招定位的。

避坑指南: 我曾经在编译内核时忘了开 CONFIG_MODULES,结果所有模块都编进了内核,导致zImage大了将近一倍。嵌入式设备对镜像大小很敏感,能用模块就别编进内核。除非是根文件系统驱动,那必须编进去。

最后,提一下内核的启动参数。在GRUB或U-Boot里,你可以加 amdgpu.dc=0 来禁用Display Core,或者 amd_pstate=guided 来启用引导式调频。这些参数在调试阶段特别有用,不用重新编译内核,改个字符串就行。

嗯,这一章内容不少,但都是实打实的经验。下一章我们会聊实时性优化,到时候再细讲如何用PREEMPT_RT补丁让AMD芯片跑出硬实时的效果。