1. 项目缘起为什么我们要从零编译ARM Linux内核如果你手头有一块全志方案的开发板比如Orange Pi Zero 2或者Orange Pi 5并且已经玩腻了官方提供的现成镜像那么“从零开始编译并启动一个自己定制的Linux内核”这个念头大概率会冒出来。这不仅仅是技术极客的浪漫更是一个嵌入式Linux开发者必须掌握的硬核技能。官方镜像固然方便但它就像一套精装修的公寓你只能使用预设的布局和功能。而自己编译内核则意味着你拿到了这栋建筑的施工蓝图和所有原材料可以决定在哪里多开一扇窗启用某个驱动在哪里加固承重墙优化调度算法甚至拆掉不必要的隔断裁剪不需要的模块最终打造一个完全贴合自己项目需求的、精简且高效的系统。对于全志平台这个过程尤其具有代表性。全志的芯片如H3、H5、H6、A64、T507等在开源硬件社区非常流行但其官方BSPBoard Support Package的文档往往散落在不同的Wiki、Git仓库和论坛帖子里对新手来说信息比较碎片化。网络上能找到的教程要么过于简略跳过了关键步骤要么是针对特定老版本在新版工具链或内核上已经失效。因此一个完整的、基于当前主流环境的实操指南就显得非常必要。我最近在为一个基于全志T507的核心板项目做系统移植重新完整走了一遍内核编译、设备树配置、根文件系统制作到最终启动的全流程。过程中踩了不少坑也总结出了一套相对稳定可靠的方法。这篇文章我就以一块常见的全志H616开发板例如Orange Pi Zero 2为例带你从纯净的Ubuntu环境开始一步步编译出属于你自己的ARM Linux内核并让它成功在板子上跑起来。无论你是嵌入式新手想深入理解系统启动流程还是老手需要为特定项目定制内核相信这篇详尽的“踩坑实录”都能给你提供直接的参考。2. 环境搭建构建可靠的交叉编译工具链编译ARM架构的内核我们显然不能在x86的电脑上直接进行这就需要搭建交叉编译环境。简单说就是在一台x86的Linux主机上安装一套能生成ARM机器码的编译器、链接器等工具集合。2.1 主机系统与工具链选型首先我强烈建议使用一台物理机或者虚拟机安装一个干净的Ubuntu 22.04 LTS作为编译主机。这个版本长期支持软件包较新且稳定社区资源丰富。避免使用Windows下的WSL1因为其对文件系统IO和符号链接的支持可能有问题WSL2可以但需要处理Windows和Linux文件系统之间的性能差异。关于交叉编译工具链主流有两种选择Linaro GCCARM官方合作维护的工具链历史悠久稳定性好对ARM架构的优化比较成熟。ARM官方GCC (Arm GNU Toolchain)ARM公司直接维护和发布更新更及时与最新ARM架构特性同步快。对于全志大多数Cortex-A系列的芯片如A7, A53, A55两者都可以。我这里选择Arm GNU Toolchain因为它下载方便版本清晰。我们选择aarch64-none-linux-gnu这个版本它适用于64位的ARMv8-A架构全志H616就是Cortex-A53属于ARMv8-A。# 1. 更新系统并安装必要的基础软件包 sudo apt update sudo apt upgrade -y sudo apt install -y git wget make gcc g flex bison libssl-dev libncurses-dev bc device-tree-compiler lz4 # 2. 下载并安装ARM GNU Toolchain (以12.3.rel1为例) cd ~ wget https://developer.arm.com/-/media/Files/downloads/gnu/12.3.rel1/binrel/arm-gnu-toolchain-12.3.rel1-x86_64-aarch64-none-linux-gnu.tar.xz sudo tar -xvf arm-gnu-toolchain-12.3.rel1-x86_64-aarch64-none-linux-gnu.tar.xz -C /opt/安装完成后需要将工具链路径加入到系统的环境变量中这样我们才能在任意目录下使用aarch64-none-linux-gnu-gcc这样的命令。# 编辑当前用户的bashrc配置文件 echo export PATH$PATH:/opt/arm-gnu-toolchain-12.3.rel1-x86_64-aarch64-none-linux-gnu/bin ~/.bashrc # 立即生效 source ~/.bashrc # 3. 验证工具链是否安装成功 aarch64-none-linux-gnu-gcc --version如果成功你会看到GCC的版本信息确认目标是aarch64-none-linux-gnu。这一步是后续所有编译工作的基石务必确保成功。2.2 获取Linux内核源码全志芯片的内核支持主要存在于两个地方一是Linux内核主线Mainline二是全志自己维护的长期支持分支比如linux-sunxi。对于新手和追求稳定性的项目我建议先从全志的官方BSP内核开始因为它包含了所有必要的驱动和板级支持成功率高。以Orange Pi Zero 2H616为例其官方内核仓库在GitHub上。我们使用git来获取代码方便后续切换版本和管理补丁。# 创建一个专门的工作目录 mkdir -p ~/workspace/orangepi cd ~/workspace/orangepi # 克隆Orange Pi官方维护的Linux内核仓库以Orange Pi Zero 2为例 git clone https://github.com/orangepi-xunlong/linux-orangepi.git -b orange-pi-6.1-rk35xx cd linux-orangepi注意不同的全志开发板其官方内核仓库可能不同。例如对于使用全志T507芯片的板子你可能需要寻找供应商提供的特定BSP内核。关键在于找到与你的硬件匹配的设备树Device Tree文件。克隆后可以通过git tag查看可用的版本标签选择一个稳定的版本进行编译例如git checkout tag/v6.1.31-opi5。3. 内核配置与编译打造量身定制的系统核心拿到内核源码后直接编译是不行的。我们需要进行配置告诉内核我们需要哪些功能不需要哪些功能以及我们的目标硬件平台是什么。3.1 理解内核配置系统.config与make menuconfig内核的配置保存在源码根目录下的.config文件中。我们可以从一个基础配置开始。对于全志平台通常存在一个针对某系列芯片的默认配置文件defconfig。# 在linux-orangepi源码根目录下 # 加载针对Sunxi全志64位平台的基础配置 make ARCHarm64 CROSS_COMPILEaarch64-none-linux-gnu- sunxi_64_defconfig这条命令做了几件事ARCHarm64指定目标架构是ARM 64位CROSS_COMPILE指定我们使用的交叉编译工具前缀sunxi_64_defconfig则是一个预置的配置文件目标它包含了全志64位芯片如A64, H5, H6, H616最基本的驱动和选项。执行后会生成一个.config文件。但这只是基础配置我们通常需要根据实际硬件进行微调比如启用特定的USB网卡驱动、文件系统支持如OverlayFS或调试功能。# 启动一个基于终端的图形化配置菜单 make ARCHarm64 CROSS_COMPILEaarch64-none-linux-gnu- menuconfig进入menuconfig界面后你可以使用方向键导航空格键选中[*]表示编译进内核[M]表示编译为模块[ ]表示不编译或取消选中某个功能/键可以搜索配置项。对于初次编译我的建议是确保关键驱动存在在Device Drivers里确认你的网络芯片如Realtek 8211E、SD/MMC、USB Host等驱动已经启用通常是*或M。精简不必要的模块在General setup-Local version可以添加一个自定义后缀如-myopi以便在启动时区分内核版本。启用内核模块自动加载确保Enable loadable module support是打开的。文件系统根据你的根文件系统类型如ext4在File systems里确保对应支持是编译进内核*的而不是模块。因为根文件系统必须在任何模块加载之前被挂载。保存并退出配置完成后选择 Save 直接回车使用默认的.config文件名然后选择 Exit 直到退出。3.2 执行编译与处理常见错误配置完成后就可以开始编译了。编译内核是一个资源密集型任务使用-j参数可以指定并行编译的作业数通常设置为CPU核心数的1到2倍以加快速度。# 开始编译内核镜像、设备树和模块使用8个并行任务 make ARCHarm64 CROSS_COMPILEaarch64-none-linux-gnu- -j8这个过程视主机性能可能需要10分钟到1小时。请耐心等待并注意观察终端输出。如果出现错误最常见的集中在以下几点缺少头文件或库错误信息通常类似fatal error: xxx.h: No such file or directory。这通常意味着你的主机系统缺少某个开发包。根据错误提示的包名使用apt search查找并安装对应的-dev包。例如缺少openssl/bio.h就安装libssl-dev。工具链版本不兼容某些非常老的内核版本可能无法用太新的GCC编译。如果遇到奇怪的语法错误可以考虑换用稍旧版本的Linaro工具链如gcc-linaro-7.5.0。配置冲突如果.config文件配置不当可能导致编译失败。可以尝试make mrproper警告这会清除所有编译输出和.config文件后重新执行defconfig和menuconfig。编译成功结束后在arch/arm64/boot/目录下会生成内核镜像文件Image在arch/arm64/boot/dts/allwinner/目录下会生成对应的设备树二进制文件*.dtb例如sun50i-h616-orangepi-zero2.dtb。同时所有编译好的内核模块会散落在各个源码目录中。我们需要将模块文件整理到一个单独的目录以便后续打包进根文件系统。# 创建模块安装目录 mkdir -p ~/workspace/orangepi/output/modules # 安装模块到指定目录INSTALL_MOD_PATH指定了模块的安装前缀 make ARCHarm64 CROSS_COMPILEaarch64-none-linux-gnu- INSTALL_MOD_PATH~/workspace/orangepi/output/modules modules_install执行后在~/workspace/orangepi/output/modules/lib/modules/下会出现一个以内核版本命名的目录如6.1.31-opi5里面就是所有编译好的内核模块.ko文件。4. 设备树与引导加载告诉内核硬件长什么样Linux内核本身是硬件无关的它需要一份“地图”来知道当前这块板子上具体有什么硬件、地址在哪里、如何初始化。这份地图就是设备树Device Tree。它由一个文本文件.dts或.dtsi编译成二进制文件.dtb。4.1 定位与修改设备树源文件在全志内核源码中设备树源文件通常位于arch/arm64/boot/dts/allwinner/。你需要找到与你的开发板最匹配的那个.dts文件。例如对于Orange Pi Zero 2文件就是sun50i-h616-orangepi-zero2.dts。有时预置的设备树配置可能不完全符合你的需求。比如你的板载以太网PHY芯片型号略有不同或者你需要启用某个默认被禁用的接口如SPI1。这时就需要修改设备树。一个实战案例启用UART3调试串口假设你的项目需要多一个串口而Orange Pi Zero 2的UART3默认可能被其他功能占用比如蓝牙。你需要编辑设备树文件来释放它。# 编辑设备树文件 vim arch/arm64/boot/dts/allwinner/sun50i-h616-orangepi-zero2.dts在文件中找到关于UART3的节点它可能看起来像这样uart3 { pinctrl-names default; pinctrl-0 uart3_ph_pins; status disabled; // 默认状态是 disabled };你需要将status改为okay。同时要确认pinctrl-0引用的引脚组uart3_ph_pins没有和其他功能如蓝牙的RTS/CTS冲突。这需要查阅芯片的数据手册和引脚复用表。修改后保存。4.2 单独编译设备树修改了设备树源文件后不需要重新编译整个内核可以单独编译设备树。# 在源码根目录单独编译设备树 make ARCHarm64 CROSS_COMPILEaarch64-none-linux-gnu- dtbs编译后新的.dtb文件会覆盖之前生成在arch/arm64/boot/dts/allwinner/目录下的同名文件。4.3 准备引导加载器U-Boot全志芯片通常使用U-Boot作为引导加载器。对于从SD卡启动我们需要将U-Boot写入SD卡的特定扇区。最方便的方法是使用全志官方提供的sunxi-tools工具包中的sunxi-fel用于FEL模式烧录和从SD卡镜像中提取出的U-Boot二进制文件。更常见的做法是直接使用开发板社区制作好的现成SD卡镜像里面已经包含了正确的U-Boot、SPL等。我们只需要替换其中的内核和设备树。这里以手动准备为例获取U-Boot二进制文件可以从官方镜像如Armbian中提取或从U-Boot源码编译这又是一个复杂的过程。假设我们有一个现成的u-boot-sunxi-with-spl.bin。准备SD卡将SD卡插入读卡器并连接到电脑。使用lsblk命令确认SD卡设备名例如/dev/sdb。写入U-Boot# 警告请务必确认/dev/sdb是你的SD卡否则会损坏主机磁盘 sudo dd ifu-boot-sunxi-with-spl.bin of/dev/sdb bs1k seek8 convfsync这条命令将U-Boot写入SD卡从第8个扇区1KB为单位开始的位置这是全志芯片启动的约定位置。写入U-Boot后SD卡的第一个分区通常是FAT32格式就可以被U-Boot识别并加载内核了。5. 制作根文件系统构建系统的“家”内核启动后需要挂载一个根文件系统Root Filesystem里面包含了操作系统运行所必需的所有工具、库、配置文件和应用程序。没有根文件系统内核会恐慌Kernel Panic并停止运行。5.1 选择与构建根文件系统对于嵌入式系统常见的根文件系统有Buildroot高度可定制从零开始构建生成的系统非常精简适合产品化。Yocto/OpenEmbedded更强大、更复杂用于构建完整的Linux发行版。Debian/Ubuntu Base使用现成的deb包管理系统软件生态丰富适合开发和快速原型。这里我们选择Debian/Ubuntu Base因为它最简单快捷适合学习和初期开发。我们可以从Ubuntu官方下载一个最小化的根文件系统。cd ~/workspace/orangepi/output # 下载Ubuntu Base 22.04 (Jammy) for arm64 wget https://cdimage.ubuntu.com/ubuntu-base/releases/22.04/release/ubuntu-base-22.04.4-base-arm64.tar.gz # 创建一个ext4格式的镜像文件大小约1GB可根据需要调整 dd if/dev/zero ofrootfs.img bs1M count1024 mkfs.ext4 rootfs.img # 挂载这个镜像文件到一个临时目录 mkdir mnt sudo mount -o loop rootfs.img mnt # 解压Ubuntu Base到镜像中 sudo tar -xzvf ubuntu-base-22.04.4-base-arm64.tar.gz -C mnt5.2 安装内核模块与基础配置现在我们需要把之前编译好的内核模块安装到这个根文件系统里。# 将编译好的模块拷贝到根文件系统的lib/modules目录下 sudo cp -r ~/workspace/orangepi/output/modules/lib/modules/* mnt/lib/modules/接下来需要为这个最小系统配置一些必要的内容比如网络、主机名、软件源等。我们可以通过chroot进入这个虚拟的根环境进行操作。# 挂载必要的虚拟文件系统 sudo mount -t proc /proc mnt/proc sudo mount -t sysfs /sys mnt/sys sudo mount -o bind /dev mnt/dev sudo mount -o bind /dev/pts mnt/dev/pts # 复制主机DNS配置保证chroot内能解析域名 sudo cp /etc/resolv.conf mnt/etc/resolv.conf # 使用chroot进入目标根文件系统 sudo chroot mnt /bin/bash现在终端提示符应该变了你“进入”了目标根文件系统。在这里执行的操作都会影响rootfs.img。# 1. 设置主机名 echo orangepi-zero2 /etc/hostname # 2. 设置root密码重要 passwd root # 输入并确认新密码 # 3. 更新软件源并安装最基础的软件包 apt update apt upgrade -y apt install -y sudo net-tools iputils-ping ifupdown vim systemd-sysv ssh # 4. 创建一个普通用户并加入sudo组 adduser orangepi usermod -aG sudo orangepi # 5. 配置网络使用DHCP cat /etc/network/interfaces.d/eth0 EOF auto eth0 iface eth0 inet dhcp EOF # 6. 退出chroot环境 exit退出后卸载之前挂载的虚拟文件系统。sudo umount mnt/dev/pts sudo umount mnt/dev sudo umount mnt/sys sudo umount mnt/proc sudo umount mnt现在一个包含我们自定义内核模块和基础配置的根文件系统镜像rootfs.img就准备好了。6. 整合与启动组装最后的拼图现在我们有了编译好的内核镜像Image、设备树二进制文件*.dtb和根文件系统镜像rootfs.img。接下来需要把它们放到SD卡的合适位置并配置U-Boot来正确引导。6.1 分区与拷贝文件到SD卡假设SD卡已经写入了U-Boot并且连接在/dev/sdb。我们将在SD卡上创建两个分区第一个分区FAT32用于存放内核镜像和设备树文件U-Boot可以直接读取。第二个分区ext4用于存放根文件系统。# 使用fdisk对SD卡进行分区警告操作会清空SD卡数据 sudo fdisk /dev/sdb # 在fdisk交互界面中 # 输入 o 创建新的DOS分区表 # 输入 n 创建新分区 - p 主分区 - 1 分区号 - 起始扇区默认 - 256M 大小用于FAT32分区 # 输入 t 改变分区类型 - c (W95 FAT32 (LBA)) # 输入 n 创建第二个分区 - p 主分区 - 2 分区号 - 起始扇区默认 - 直接回车使用所有剩余空间 # 输入 w 写入并退出 # 格式化分区 sudo mkfs.vfat /dev/sdb1 # 格式化第一个分区为FAT32 sudo mkfs.ext4 /dev/sdb2 # 格式化第二个分区为ext4 # 挂载第一个分区boot分区 mkdir -p /tmp/sd_boot sudo mount /dev/sdb1 /tmp/sd_boot # 拷贝内核和设备树到boot分区 sudo cp ~/workspace/orangepi/linux-orangepi/arch/arm64/boot/Image /tmp/sd_boot/ sudo cp ~/workspace/orangepi/linux-orangepi/arch/arm64/boot/dts/allwinner/sun50i-h616-orangepi-zero2.dtb /tmp/sd_boot/ # 卸载boot分区 sudo umount /tmp/sd_boot # 将根文件系统镜像写入第二个分区 # 注意rootfs.img是我们之前创建的镜像文件 sudo dd if~/workspace/orangepi/output/rootfs.img of/dev/sdb2 bs4M statusprogress convfsync写入根文件系统镜像后可以尝试挂载第二个分区检查文件是否完整。6.2 配置U-Boot引导参数U-Boot需要知道从哪里加载内核、设备树以及根文件系统在哪里。这通过U-Boot的环境变量来设置。我们可以创建一个boot.scr脚本文件让U-Boot启动时自动执行。首先创建一个文本文件boot.cmd# boot.cmd 内容 setenv bootargs consolettyS0,115200 earlyprintk root/dev/mmcblk0p2 rootwait rw load mmc 0:1 ${kernel_addr_r} /Image load mmc 0:1 ${fdt_addr_r} /sun50i-h616-orangepi-zero2.dtb booti ${kernel_addr_r} - ${fdt_addr_r}解释一下bootargs传递给Linux内核的命令行参数。consolettyS0,115200指定调试串口为UART0波特率115200。root/dev/mmcblk0p2指定根文件系统在SD卡的第二个分区。rootwait等待根设备就绪。rw以读写方式挂载根文件系统。load mmc 0:1 ...从SD卡mmc设备0的第一个分区FAT32分区加载文件到内存地址。booti启动ARM64内核镜像。然后使用U-Boot的工具mkimage将这个脚本文件转换成U-Boot可以识别的格式。# 首先安装 uboot-tools sudo apt install uboot-tools # 将 boot.cmd 编译为 boot.scr mkimage -C none -A arm -T script -d boot.cmd boot.scr # 将 boot.scr 拷贝到SD卡的boot分区 sudo mount /dev/sdb1 /tmp/sd_boot sudo cp boot.scr /tmp/sd_boot/ sudo umount /tmp/sd_boot6.3 上电启动与调试现在将SD卡插入Orange Pi Zero 2连接串口调试线USB转TTL连接板子的UART0通常是引脚6-GND8-TX10-RX注意交叉连接然后上电。在串口终端软件如minicom,picocom,screen中你应该能看到U-Boot的启动日志接着是Linux内核解压、初始化硬件、加载驱动、最后尝试挂载根文件系统的信息。成功启动的标志是出现登录提示符例如Ubuntu 22.04 LTS orangepi-zero2 ttyS0 orangepi-zero2 login:此时你可以用之前设置的root用户和密码登录。常见启动失败问题排查无任何输出检查串口连接TX/RX是否接反、波特率115200、电源是否正常。U-Boot启动后卡住检查boot.scr是否正确生成并拷贝检查Image和.dtb文件是否在FAT分区根目录。内核Panic提示“VFS: Unable to mount root fs”这是最常见的问题。原因包括root参数指定的设备节点不对不是/dev/mmcblk0p2。内核缺少对应文件系统如ext4的支持必须编译进内核不能是模块。根文件系统镜像损坏或内容不完整。可以重新制作并dd写入。设备树中MMC控制器配置错误。确认你的板子对应的.dts文件正确。内核Panic提示“Not syncing: No working init found”内核找到了根文件系统但找不到可以执行的init程序。检查根文件系统里/sbin/init或/bin/sh是否存在且可执行。在Ubuntu Base中安装了systemd-sysv后/sbin/init会链接到/lib/systemd/systemd。7. 进阶与优化让系统更贴合你的项目系统成功启动只是一个开始。要让这个自编译的系统真正用于项目还需要做一些优化工作。7.1 内核裁剪与尺寸优化默认配置的内核可能包含大量你用不到的驱动和功能导致内核镜像偏大。你可以通过make menuconfig进行深度裁剪去掉调试信息在Kernel hacking中关闭Compile-time checks and compiler options下的许多调试选项可以显著减小内核大小。精简设备驱动在Device Drivers中只保留你板上确实存在的硬件驱动如网卡、Wi-Fi、声卡等去掉所有其他无关的驱动如老旧ISA设备、不用的GPU驱动等。使用模块对于不常用的驱动可以编译为模块[M]在需要时通过insmod动态加载减少内核初始镜像大小。 裁剪后重新编译你会发现Image文件小了很多。7.2 构建Initramfs在某些场景下比如根文件系统在网络上NFS或者需要解密加密分区内核需要先加载一个临时的根文件系统initramfs来执行一些前置任务。我们可以将必要的驱动和工具打包进内核。# 在menuconfig中启用 General setup --- [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support (/path/to/your/initramfs.cpio) Initramfs source file(s)你需要先使用cpio工具创建一个initramfs.cpio归档文件。这通常包含busybox和一些简单的启动脚本。7.3 集成到Buildroot/Yocto构建系统对于产品级开发手动操作每一步太低效且不易复现。应该将内核编译、设备树生成、根文件系统制作全部集成到像Buildroot或Yocto这样的构建系统中。你只需要定义一个配置文件defconfig构建系统就能自动下载源码、打补丁、配置、编译并生成一个完整的、可烧录的镜像文件如.img或.wic。这保证了构建过程的一致性和可维护性。以Buildroot为例你可以在make menuconfig中选择Target options-Target Architecture-AArch64 (little endian)Toolchain- 使用自定义的外部工具链指向我们之前安装的ARM GNU Toolchain。Kernel- 选择使用自定义的内核版本和源码路径指向我们本地的linux-orangepi目录并指定自定义的内核配置片段defconfig和额外的.config选项。Target packages- 选择你需要的应用程序如ssh server, python3等。配置完成后运行makeBuildroot就会自动完成所有工作最终在output/images/目录下生成sdcard.img等镜像文件。从零开始编译并启动一个ARM Linux内核是全志平台嵌入式开发中极具成就感的一步。这个过程强迫你去理解系统启动的每一个环节从U-Boot加载、内核解压初始化、设备树解析硬件、到最终挂载根文件系统并启动第一个用户进程。虽然初次尝试可能会遇到各种问题但每一次排错都是对系统理解的加深。当你看到自己定制的内核在板子上顺利运行那种对系统的掌控感是使用现成镜像无法比拟的。希望这篇超过五千字的详细指南能为你扫清障碍顺利开启你的嵌入式Linux定制之旅。