
1. 为什么要在 RK3399 上折腾 openEuler 20.03 LTS把 openEuler 20.03 LTS 移植到 RK3399 这类 ARM 开发板上本质上是在做一件把服务器级发行版塞进嵌入式板卡的事。openEuler 官方对 ARM64 的支持主要集中在服务器平台比如 TaiShan 系列、鲲鹏系列这些平台有完整的 ACPI/UEFI 固件支持内核和引导链路都是标准化的。而 RK3399 是典型的嵌入式 SoC用的是 U-Boot 设备树 分区镜像那一套两者的启动模型完全不在一个频道上。我最初动这个念头是因为手头有几块 RK3399 的开发板闲置想拿它跑一个带完整包管理、能长期维护的 ARM64 发行版做点边缘侧的小服务。试过几个面向嵌入式的轻量发行版包生态和长期支持都不太理想最后还是决定回到 openEuler 这条线上来。20.03 LTS 是个长期支持版本软件源稳定社区文档相对齐全作为移植基底比较合适。这个系列的第一篇我不打算一上来就讲设备树怎么改、U-Boot 怎么编而是先把最基础也最容易卡住的一环讲透内核和资源镜像的编译与制作。原因很简单很多人移植失败不是死在驱动上而是死在对编译产物到底该长什么样、该放到哪个分区、用什么格式打包这件事没有清晰认知。你连镜像都没做对后面调驱动就是无源之水。需要提前说明的是本文面向的是有一定 Linux 基础、玩过交叉编译、对嵌入式启动流程有基本概念的读者。如果你连交叉编译工具链都没配过建议先补一下这块基础。另外RK3399 的移植涉及大量平台相关的细节不同厂家的核心板在 DDR 初始化、电源管理上差异很大本文给出的是一套通用可复现的流程框架具体参数需要你结合自己手上的板子做调整。2. 编译环境的搭建与工具链选型2.1 宿主机环境的最低要求编译 openEuler 的内核和制作镜像对宿主机是有硬性要求的。我实测下来最低配置建议是 4 核 CPU、8GB 内存、100GB 可用磁盘空间。内核编译本身是高度并行的核越多越快但内存是更关键的瓶颈——如果内存不足链接阶段很容易被 OOM Killer 干掉报出一堆莫名其妙的错误。操作系统层面我推荐用 openEuler 20.03 LTS 或者 Ubuntu 20.04 作为宿主机。选 openEuler 做宿主的好处是编译内核所需的依赖包版本和目标是同源的能少踩很多版本兼容的坑。如果你用 Ubuntu注意 gcc 版本不要太新太新的 gcc 编译老内核经常会遇到警告升级成错误的情况。依赖包这块一次性装齐比较省事# openEuler / CentOS 系 sudo dnf install -y gcc gcc-c make bc bison flex \ openssl-devel elfutils-libelf-devel \ ncurses-devel dwarves rsync cpio \ python3 python3-devel dtc # Debian / Ubuntu 系 sudo apt install -y build-essential bc bison flex \ libssl-dev libelf-dev libncurses-dev \ dwarves rsync cpio python3 python3-dev device-tree-compiler这里有几个包容易被忽略但很关键。bc是内核编译脚本里做算术运算用的缺了会在编译早期就报错dwarves提供 pahole 工具用于生成 BTF 信息现代内核默认开启 CONFIG_DEBUG_INFO_BTF 时会用到dtc是设备树编译器RK3399 强依赖设备树这个必须有。2.2 交叉编译工具链的选择逻辑交叉编译工具链的选择直接决定了你编出来的内核能不能在板子上跑起来。RK3399 是 ARM Cortex-A72 A53 的 big.LITTLE 架构属于 ARMv8-A64 位。所以你需要的是 aarch64 的交叉工具链。市面上常见的有三类选择工具链来源优点缺点适用场景发行版自带如 gcc-aarch64-linux-gnu安装方便版本稳定版本可能偏旧快速验证Linaro 官方工具链针对 ARM 优化充分下载慢版本管理麻烦追求性能芯片原厂 SDK 自带与平台最匹配绑定 SDK不通用原厂方案我的建议是如果你只是想把流程跑通直接用发行版自带的gcc-aarch64-linux-gnu就够了。等流程验证通过、要追求极致性能或者遇到奇怪的编译问题时再考虑换 Linaro 的工具链。原厂 SDK 自带的工具链虽然匹配度最高但往往和特定内核版本绑定用来编译 openEuler 的内核反而容易出问题。设置环境变量的时候注意前缀要写对export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu-CROSS_COMPILE的值是工具链前缀不带gcc。内核的 Makefile 会自动在这个前缀后面拼上gcc、ld、objcopy等。如果你写成了aarch64-linux-gnu-gcc编译时会变成aarch64-linux-gnu-gccgcc直接报找不到命令。提示验证工具链是否可用执行aarch64-linux-gnu-gcc -v能看到版本信息就说明 PATH 配置正确。如果提示 command not found检查一下工具链的 bin 目录有没有加到 PATH 里。2.3 源码获取与目录规划openEuler 20.03 LTS 的内核源码可以从 openEuler 的官方源码仓库获取。需要注意的是openEuler 的内核是基于某个上游长期支持版本做了大量补丁的直接拿上游内核编译虽然也能跑但会缺失 openEuler 的一些特性支持比如特定的安全加固、cgroup 增强等。我习惯把工作目录规划清楚避免后期文件混乱mkdir -p ~/rk3399-porting/{kernel,build,output,rootfs} cd ~/rk3399-porting/kernel # 此处获取 openEuler 20.03 LTS 内核源码目录规划这件事看起来不起眼但移植过程中你会反复在源码、编译产物、镜像文件之间切换目录乱了非常影响效率。build放编译中间产物output放最终镜像rootfs放根文件系统各司其职。3. 内核配置从 defconfig 到 RK3399 可用配置3.1 起点 defconfig 的挑选内核配置是整个移植里最考验经验的一环。openEuler 的内核源码里自带了不少 defconfig但绝大多数是给服务器平台用的。你要做的第一件事是找到一个尽可能接近 ARM64 通用嵌入式场景的起点。通常我会从arch/arm64/configs/目录下找一个通用的 defconfig 作为基础比如defconfig或者某个通用 ARM64 配置。但直接用服务器 defconfig 会有两个极端问题一是开了太多服务器专属特性比如大量 PCIe、NUMA 相关配置编译出来的内核体积巨大且启动慢二是缺了嵌入式平台必需的驱动比如 MMC、USB PHY、GPIO 子系统。我的做法是以通用 ARM64 defconfig 为底然后系统性地做减法去掉服务器特性和加法补嵌入式驱动。这个过程不是一次性的而是编译、启动、看日志、再调整的循环。3.2 RK3399 必须打开的核心配置项下面这些配置项是 RK3399 能正常启动和工作的底线缺一不可。我按功能模块分类说明。架构与平台相关CONFIG_ARCH_ROCKCHIPy CONFIG_ARM64y CONFIG_ARM64_VA_BITS_48y CONFIG_PCIy CONFIG_PCI_HOST_GENERICyCONFIG_ARCH_ROCKCHIP是 Rockchip 平台的总开关不开这个后面所有 Rockchip 相关的驱动都不会被编译进去。VA_BITS 选 48 位是 ARM64 的常规选择除非你有特殊的内存布局需求。存储相关启动必需CONFIG_MMCy CONFIG_MMC_BLOCKy CONFIG_MMC_DWy CONFIG_MMC_DW_ROCKCHIPy CONFIG_MMC_SDHCIy CONFIG_MMC_SDHCI_OF_ARASANyRK3399 的 eMMC 和 SD 卡控制器用的是 DesignWare 的 IP所以CONFIG_MMC_DW_ROCKCHIP必须开。如果你打算从 SD 卡启动SDHCI 相关配置也要确保打开。这里有个坑有些 defconfig 默认把 MMC 编译成模块m但根文件系统在 eMMC 上的话内核启动阶段就需要访问 MMC模块方式加载来不及必须编译进内核y。串口调试强烈建议CONFIG_SERIAL_8250y CONFIG_SERIAL_8250_CONSOLEy CONFIG_SERIAL_8250_DWy调试串口是移植阶段的救命稻草。RK3399 的调试串口通常是 UART2波特率 1500000。注意这个波特率不是常见的 115200很多串口工具默认不支持需要手动设置。如果串口配置不对你连内核启动到哪一步都看不到只能盲调。电源与时钟CONFIG_ROCKCHIP_IODOMAINy CONFIG_COMMON_CLK_ROCKCHIPy CONFIG_PINCTRL_ROCKCHIPy CONFIG_REGULATORy CONFIG_REGULATOR_FIXED_VOLTAGEy CONFIG_REGULATOR_GPIOyRockchip 的 IO 域配置、时钟控制器、引脚控制器都是平台强相关的必须打开。电源管理这块很多开发板用的是 GPIO 控制的固定电压 regulator所以REGULATOR_GPIO和REGULATOR_FIXED_VOLTAGE要开。文件系统CONFIG_EXT4_FSy CONFIG_EXT4_USE_FOR_EXT2y CONFIG_TMPFSy CONFIG_PROC_FSy CONFIG_SYSFSy CONFIG_DEVTMPFSy CONFIG_DEVTMPFS_MOUNTyCONFIG_DEVTMPFS_MOUNT这个选项特别重要它让内核自动挂载 devtmpfs 到 /dev否则你启动后会发现 /dev 下空空如也连 console 设备都没有。这个坑我踩过当时排查了半天才发现是配置问题。3.3 用 menuconfig 做精细化调整配置项太多纯手工改 .config 文件容易出错。我习惯用make menuconfig做交互式调整改完保存后再用make savedefconfig生成一个精简的 defconfig 存档。make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- menuconfig在 menuconfig 里有几个搜索技巧能大幅提升效率。按/键可以搜索配置项输入关键词就能定位。比如你想找 MMC 相关配置搜MMC_ROCKCHIP它会告诉你这个选项在哪个菜单下、依赖哪些条件。依赖关系这点很关键有时候你发现某个选项是灰色的选不了就是因为它的依赖项没满足。调整完配置后建议把最终的 .config 备份一份命名带上日期和板子型号比如config_rk3399_20231015。移植过程中你会反复回到某个能工作的配置版本有备份能省很多事。注意menuconfig 里改完配置退出时一定要选保存。我有一次改了一堆配置手快直接退出没保存白忙活半小时。另外如果你在图形界面里改了配置但编译报错先检查是不是某些选项之间的依赖冲突menuconfig 有时不会自动帮你处理反向依赖。4. 内核编译与产物解析4.1 编译命令与并行度设置配置确定后编译本身反而是最简单的环节make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -j$(nproc) Image dtbs这里我特意只编Image和dtbs而不是笼统的make all。原因是make all会连带编译模块而移植初期我们往往先把内核跑起来再说模块可以后面再补。Image是 ARM64 内核的标准镜像格式未压缩U-Boot 可以直接引导。dtbs会编译arch/arm64/boot/dts/rockchip/下的设备树文件。-j$(nproc)用满所有 CPU 核心。但如果你内存比较紧张比如只有 8GB建议把并行度降到核心数的一半避免链接阶段内存爆掉。我遇到过 16 核 8GB 的机器-j16编译到链接阶段直接被 OOM改成-j8就稳了。编译时间取决于机器性能一般 4 核机器大概 20 到 40 分钟16 核机器 5 到 10 分钟。第一次编译最慢后续增量编译会快很多。4.2 编译产物的位置与含义编译完成后你需要知道几个关键产物在哪、分别是什么产物路径用途Imagearch/arm64/boot/Image内核镜像主体dtbarch/arm64/boot/dts/rockchip/*.dtb设备树二进制System.map源码根目录符号表调试用vmlinux源码根目录未压缩的 ELF 内核调试用Image是我们要打包进资源镜像的核心。设备树文件要选对RK3399 不同板子的 dtb 不一样比如有的是rk3399-evb.dtb有的是厂商自定义的。你得确认自己板子对应的 dtb 文件名选错了设备树内核可能连串口都初始化不了。System.map和vmlinux在正常启动时用不到但一旦内核 panic你需要用它们来解析调用栈定位问题代码。建议编译完就把这两个文件连同 Image 一起归档别等出问题了再回头找。4.3 常见编译错误与应对编译过程中最容易遇到几类错误我按出现频率排个序。第一类是工具链版本不匹配导致的语法错误。典型表现是某个头文件里用了新语法老编译器不认识或者反过来。解决办法是换工具链版本或者给编译加-Wno-error把警告降级。但后者是治标不治本能换工具链还是换。第二类是依赖包缺失。比如报openssl/ssl.h: No such file就是 openssl-devel 没装。这类错误信息通常很明确缺什么装什么就行。第三类是配置冲突。比如你开了某个驱动但它依赖的子系统没开编译时就会报未定义符号。这种错误最难查因为报错位置和真正的问题配置可能隔了很远。我的经验是遇到未定义符号先用grep在 .config 里搜相关配置确认依赖链是否完整。第四类是内存不足。前面提过链接阶段最吃内存。如果看到Killed或者signal 9基本就是 OOM。降并行度或者加 swap 都能缓解。5. 资源镜像的制作从分区规划到镜像打包5.1 RK3399 的启动链路与分区布局要理解资源镜像怎么制作得先搞清楚 RK3399 的启动链路。它和服务器那套 UEFI 完全不同走的是 Rockchip 自己的流程上电后SoC 内部的 BootROM 先运行它会从 eMMC 或 SD 卡的固定偏移位置读取一级引导程序通常是 U-Boot 的 SPL 部分然后 SPL 初始化 DDR加载完整的 U-BootU-Boot 再去加载内核和设备树最后启动内核。这个链路决定了镜像的分区布局。RK3399 的存储介质上通常有这么几个关键区域Loader 区存放 SPL 和 U-Boot位置固定由 Rockchip 的工具烧写boot 区存放内核 Image 和设备树 dtbrootfs 区存放根文件系统其他可能还有 misc、recovery 等分区资源镜像主要涉及 boot 区和 rootfs 区。boot 区一般用 Android 的 bootimg 格式或者简单的 FIT 格式rootfs 区就是标准的 ext4 文件系统镜像。5.2 boot 镜像的组成与打包boot 镜像里装的是内核和设备树。RK3399 常见的做法是打成 Android boot image 格式这个格式虽然名字带 Android但本质就是一个带头部信息的容器里面可以放内核、ramdisk、设备树等。一个典型的 boot 镜像包含kernel就是前面编译出来的 Imageramdisk可选的初始根文件系统openEuler 这种完整发行版通常不需要dtb设备树二进制cmdline内核启动参数打包工具可以用 Rockchip 提供的mkbootimg或者开源的mkbootimg。命令大致长这样mkbootimg --kernel arch/arm64/boot/Image \ --dtb arch/arm64/boot/dts/rockchip/rk3399-evb.dtb \ --cmdline consolettyS2,1500000n8 root/dev/mmcblk0p6 rw rootwait \ --pagesize 2048 \ --output output/boot.img--pagesize这个参数要注意RK3399 通常是 2048 字节但有些板子可能不同得查板子手册。cmdline里的consolettyS2,1500000n8指定调试串口root指定根文件系统所在分区。这个分区号要和实际分区布局对上写错了内核找不到根文件系统会直接 panic。提示cmdline 里的rootwait很重要。它让内核等待存储设备就绪后再挂载根文件系统。RK3399 的 eMMC 初始化有时比较慢不加这个参数可能因为设备还没就绪就尝试挂载而失败。5.3 根文件系统镜像的制作根文件系统这块openEuler 20.03 LTS 有现成的 ARM64 rootfs 可以拿来用也可以自己用dnf --installroot构建一个最小系统。移植初期我建议先用现成的把启动流程跑通后面再按需裁剪。制作 ext4 镜像的流程是这样的# 创建一个指定大小的空文件 dd if/dev/zero ofoutput/rootfs.img bs1M count2048 # 格式化为 ext4 mkfs.ext4 output/rootfs.img # 挂载并拷贝根文件系统内容 mkdir -p /mnt/rootfs sudo mount -o loop output/rootfs.img /mnt/rootfs sudo cp -a rootfs/* /mnt/rootfs/ sudo umount /mnt/rootfs镜像大小要估算好。太小了装不下太大了浪费空间。一个最小化的 openEuler 根文件系统大概 500MB 到 1GB加上一些常用工具和库2GB 是比较稳妥的起点。如果你后面要装开发工具可能得 4GB 以上。拷贝的时候用cp -a保留权限和符号链接这点很关键。根文件系统里有大量符号链接和特殊权限的文件用普通cp会破坏这些属性导致系统启动后各种奇怪问题。比如/sbin/init可能是个指向/lib/systemd/systemd的符号链接权限丢了就启动不了。5.4 分区表与镜像烧写boot.img 和 rootfs.img 做好后还需要一个分区表来告诉引导程序它们在哪。RK3399 用的是 GPT 分区表可以用gptfdisk或者 Rockchip 的工具生成。一个典型的分区表参数如下分区起始扇区大小用途loader0x404MBSPL U-Bootboot按需64MB内核 dtbrootfs按需2GB根文件系统起始扇区这块loader 从 0x40 开始是 Rockchip 的约定不能改。boot 和 rootfs 的起始位置要按实际大小算确保不重叠。烧写的时候可以用dd直接写也可以用 Rockchip 的升级工具。dd的好处是通用坏处是慢且没有校验。命令大致是sudo dd ifoutput/boot.img of/dev/sdX seekboot起始扇区 convnotrunc sudo dd ifoutput/rootfs.img of/dev/sdX seekrootfs起始扇区 convnotruncconvnotrunc必须加否则dd会把后面的内容截断。seek的值是起始扇区号不是字节偏移算错了会覆盖到别的分区。6. 移植初期的验证与排错思路6.1 串口日志是第一手信息镜像烧进去、板子上电后第一件事是看串口输出。如果串口一片空白说明连 BootROM 或 SPL 阶段都没过问题出在更底层可能是 loader 没烧对或者 DDR 初始化参数不对。如果能看到 U-Boot 的输出但卡在加载内核说明 loader 没问题问题在内核镜像或设备树。如果内核起来了但卡在挂载根文件系统那就是 cmdline 或 rootfs 的问题。这个分层排查的思路很重要。串口日志会明确告诉你卡在哪一步你顺着日志往下查就行不要盲目乱试。6.2 内核启动阶段的典型问题内核启动阶段最常见的问题是设备树不匹配。表现是内核起来了但某些外设不工作比如网卡找不到、USB 没反应。这时候要检查 dtb 是不是选对了以及设备树里的节点和实际硬件是否对应。另一个常见问题是时钟和电源初始化失败。RK3399 的外设依赖特定的时钟和电压如果设备树里这些配置不对驱动 probe 就会失败。日志里通常会打印probe failed或者timeout之类的信息顺着这些信息去查对应的驱动和设备树节点。还有一种情况是内核直接 panic报Unable to mount root fs。这基本就是 cmdline 里的 root 参数写错了或者 rootfs 镜像本身有问题。先确认分区号对不对再确认 rootfs 镜像有没有正确烧写。6.3 根文件系统启动后的收尾工作根文件系统挂载成功后系统会继续启动 init 进程。openEuler 用的是 systemd如果 systemd 起不来通常是根文件系统里的库或配置有问题。常见的是/etc/fstab配置错误导致挂载失败或者某个关键服务依赖的库缺失。这个阶段建议先用单用户模式或者紧急模式进去把基本环境确认一遍。确认网络、存储、串口都正常后再逐步启用其他服务。移植初期不要追求功能全先把系统能稳定启动、能登录、能执行基本命令这个底线守住。我在实际移植中最大的体会是镜像制作这一步的规范性直接决定了后面调试的效率。很多人内核编得没问题但镜像打包时分区算错、cmdline 写错、权限丢失结果把大量时间浪费在排查这些本可以避免的低级问题上。把镜像制作流程标准化、脚本化每次改动都有据可查是移植工作能顺利推进的前提。下一篇我会接着讲 U-Boot 的适配和设备树的精细化调整那才是真正啃硬骨头的部分。