全志ARM Linux内核编译指南:从交叉编译到SD卡启动全流程
1. 项目概述:为什么我们要亲手编译ARM Linux内核?
如果你手头有一块全志方案的开发板,比如Orange Pi Zero 2或者NanoPi,想在上面跑点自己的程序,或者想深入理解嵌入式系统是怎么从一堆代码变成能点亮屏幕、连接网络的智能设备,那么“从零开始编译并启动ARM Linux内核”就是你绕不开的一步。这不仅仅是把官方提供的现成镜像烧录到SD卡那么简单,而是一个从源码到二进制,从裸机到操作系统的完整构建过程。我之所以花时间折腾这个,是因为现成的镜像往往只包含了最通用的驱动和配置,当你需要启用某个特定的硬件功能、裁剪掉不需要的模块以节省资源,或者单纯想学习内核的启动流程时,自己动手编译就成了唯一的选择。
全志的芯片,比如常见的V3s、F1C100s、T527等,在开源硬件和创客圈子里非常流行,其对应的Linux内核源码在社区维护得也相当不错。这个过程能让你彻底搞清楚:一个为特定ARM架构(比如ARMv7或ARMv8)定制的Linux内核是如何被交叉编译工具链“翻译”出来的;内核镜像、设备树(Device Tree)和根文件系统这三者之间是什么关系;以及最终,我们如何让这块小小的开发板按照我们的意愿启动起来。对于嵌入式Linux驱动开发者来说,这更是基本功,因为驱动代码最终是要编译进内核或者以模块形式加载的。接下来,我就以手头一块全志V3s的开发板为例,带你走一遍这个充满细节的旅程。
2. 开发环境搭建与工具链选型
工欲善其事,必先利其器。编译Linux内核需要一个高效、可靠的交叉编译环境。所谓“交叉编译”,就是在我们常用的x86_64架构的电脑(宿主机)上,生成能在ARM架构开发板(目标机)上运行的代码。
2.1 交叉编译工具链的选择与安装
市面上工具链很多,比如Linaro、ARM官方提供的gcc-arm-none-eabi,以及各大芯片厂商自己定制的工具链。对于全志平台,我强烈推荐使用社区验证过的、与内核版本匹配的工具链。这里我选择gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf。选择7.5.0版本是因为它和较新的内核兼容性好,同时稳定性经过考验。“gnueabihf”中的“hf”代表硬浮点(Hard Float),能利用ARM芯片的浮点运算单元,提升性能。
在Ubuntu 20.04宿主机上,安装步骤如下:
# 1. 下载工具链(假设放在/home/username/目录下) wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz # 2. 解压 tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz # 3. 将工具链路径加入系统环境变量,方便调用 echo 'export PATH=$PATH:/home/username/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin' >> ~/.bashrc source ~/.bashrc # 4. 验证安装 arm-linux-gnueabihf-gcc --version如果终端能正确输出gcc的版本信息,说明工具链安装成功。这里有个关键细节:一定要将工具链的bin目录路径添加到PATH环境变量中,否则后续编译时会找不到arm-linux-gnueabihf-为前缀的命令(如gcc, ld等)。
2.2 获取Linux内核源码与全志平台支持
全志芯片的内核源码主要维护在几个地方:一是全志官方为某些芯片提供的SDK(通常比较庞大);二是Linux内核主线(Mainline),社区开发者会逐步将全志芯片的驱动并入主线;三是诸如linux-sunxi(现为sunxi-linux)这样的社区维护分支,它对全志(Allwinner)系列芯片的支持非常活跃和及时。
对于初学者或追求稳定性的开发者,我建议从社区分支开始。这里我们使用sunxi-linux的某个稳定分支。以linux-5.10.y分支为例,这个版本长期支持,驱动相对完善。
# 克隆sunxi-linux仓库,并切换到特定分支 git clone https://github.com/sunxi-linux/linux -b sunxi-5.10 cd linux注意:内核源码体积较大,下载需要一定时间。确保你的宿主机磁盘空间充足(建议预留15GB以上)。克隆完成后,你可以通过
git branch -a查看所有远程分支,选择适合你芯片的版本。例如,对于较新的T527芯片,可能需要更新版本的分支。
2.3 宿主机基础依赖包安装
编译内核需要宿主机安装一些必要的开发工具和库。在Ubuntu/Debian系统上,可以一次性安装:
sudo apt-get update sudo apt-get install -y build-essential libncurses5-dev libssl-dev bc flex bison libelf-devbuild-essential: 包含gcc, make等基础编译工具。libncurses5-dev: 用于支持make menuconfig图形化配置界面。libssl-dev和bc: 内核编译过程中某些功能(如模块签名)需要。flex和bison: 语法分析器生成工具,编译某些内核组件时需要。libelf-dev: 处理ELF格式文件所需的库。
安装这些依赖可以避免后续编译时出现令人头疼的“找不到xxx.h文件”或“命令未找到”的错误。
3. 内核配置与裁剪:为你的板子量身定做
拿到内核源码后,直接编译是不行的,我们必须告诉编译系统,我们要为哪种架构的哪种芯片,启用哪些功能。这就是内核配置。
3.1 导入默认配置与图形化配置
全志社区通常为热门开发板提供了默认配置文件(defconfig)。以Orange Pi Zero(使用全志H2+芯片)为例,其配置可能是sun8i-h3-defconfig。对于V3s,社区常用sun8i-v3s-licheepi-zero-defconfig(针对Lichee Pi Zero开发板,核心是V3s)。
# 在linux源码根目录下执行 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- sun8i-v3s-licheepi-zero-defconfig这条命令做了几件事:ARCH=arm指定目标架构为ARM;CROSS_COMPILE指定交叉编译工具链前缀;最后是defconfig的名称。执行后,会在源码根目录生成一个.config文件,这是内核编译的“总蓝图”。
但默认配置可能包含了你不需要的驱动,也可能缺少你需要的功能。我们需要进行微调:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig这会启动一个基于ncurses的文本图形界面。在这里,你可以像逛菜单一样浏览和修改成千上万个配置选项。
3.2 关键配置项解析与实操心得
面对海量选项,新手容易懵。我建议重点关注以下几类,它们直接影响内核能否在你的板子上正常启动和工作:
系统类型(System Type):
- 确保在
ARM system type下选择了正确的CPU系列,如Allwinner A1x SoCs或Allwinner sun8i Family。对于V3s,它属于sun8i家族。 - 在
Allwinner SoC Support子菜单中,务必选中你的具体SoC型号,例如Allwinner V3s。这确保了最基础的芯片支持被编译进去。
- 确保在
设备树支持(Device Tree):
- 确保
CONFIG_OF(Flattened Device Tree support)是开启的。现代ARM Linux几乎都使用设备树来描述硬件。 - 在
Allwinner SoC Support下,找到你的开发板对应的设备树源文件(.dts)编译选项并启用。例如,对于Lichee Pi Zero,需要找到CONFIG_MACH_SUN8I_V3S_LICHEEPI_ZERO并设为y(编译进内核)。
- 确保
驱动配置:
- 串口驱动:这是你最重要的调试窗口!确保
CONFIG_SERIAL_8250和CONFIG_SERIAL_8250_CONSOLE启用。并在其子选项中找到对应全志平台的UART驱动,如Allwinner A10 UART。 - SD/MMC驱动:你的根文件系统很可能在SD卡上。确保
CONFIG_MMC、CONFIG_MMC_SUNXI启用。 - 网络驱动:如果你的板子有网络(有线或无线),找到对应的驱动。例如V3s的EMAC网络控制器对应
CONFIG_SUN8I_EMAC。 - USB驱动:如果需要连接USB设备,确保对应的Host控制器驱动(如
CONFIG_USB_OHCI_HCD,CONFIG_USB_EHCI_HCD)已启用。
- 串口驱动:这是你最重要的调试窗口!确保
文件系统支持:
- 你的根文件系统是什么格式?如果是EXT4,确保
CONFIG_EXT4_FS=y。如果是SquashFS(常用于只读系统),则启用CONFIG_SQUASHFS。 - 如果使用
initramfs(初始内存文件系统)作为临时根文件系统来引导,需要在General setup->Initial RAM filesystem and RAM disk (initramfs/initrd) support中配置。
- 你的根文件系统是什么格式?如果是EXT4,确保
内核调试与打印:
- 初期调试,内核的打印信息至关重要。确保
CONFIG_PRINTK、CONFIG_EARLY_PRINTK是开启的。EARLY_PRINTK能在内核非常早期的阶段就通过串口输出信息,对于排查启动卡死的问题帮助巨大。
- 初期调试,内核的打印信息至关重要。确保
实操心得:第一次配置时,可以在默认defconfig的基础上,只修改你明确知道需要改动的选项。对于不确定的选项,保持默认(通常默认值是最安全或最通用的选择)。配置完成后,记得保存。菜单界面底部有操作提示,按
Y编译进内核,M编译为模块,N不编译。模块的好处是可以减小内核体积,在需要时动态加载,但启动时需要额外的initramfs来加载模块,对初学者稍复杂。建议初期将核心驱动直接编译进内核(Y),简化启动流程。
4. 内核编译与产物生成
配置完成后,就可以开始编译了。这个过程比较耗时,取决于你电脑的CPU核心数和内核配置的复杂程度。
4.1 启动编译与参数优化
使用-j参数指定并行编译任务数,可以大幅缩短编译时间。通常设为你的CPU核心数+1或+2。
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)$(nproc)命令会自动获取你系统的CPU核心数。编译过程中,终端会滚动输出大量的编译信息。如果没有出现error,只有warning,通常可以忽略,编译会继续进行。
编译最终会生成几个核心文件:
arch/arm/boot/zImage: 压缩的内核镜像文件。这是最主要的启动内核。arch/arm/boot/dts/<板子名称>.dtb: 设备树二进制文件(Device Tree Blob)。它由我们之前配置时选择的.dts文件编译而来,包含了板级的硬件描述信息,比如内存地址、外设寄存器、中断号等。对于V3s Lichee Pi Zero,这个文件可能是sun8i-v3s-licheepi-zero.dtb。- 如果配置了模块,模块文件会分散在
lib/modules/<内核版本>/kernel/目录下。
4.2 常见编译错误排查
即使按照步骤来,也可能遇到编译错误。这里记录几个我踩过的坑:
“recipe for target ‘xxx’ failed”:这通常是缺少某个依赖包。仔细看错误信息的上方,通常会提示缺少某个
.h头文件或某个命令。根据提示安装对应的开发包即可。例如,如果提示openssl/xxx.h找不到,那就安装libssl-dev。工具链版本不兼容:如果你使用了太新或太旧的工具链,可能会遇到奇怪的语法错误或链接错误。错误信息可能包含“unrecognized command line option”或“undefined reference”。解决方案是换用社区推荐的工具链版本,确保与内核版本匹配。
配置冲突:有时手动修改
.config文件不当,会导致选项依赖关系出错。一个干净的解决方法是,备份你的自定义配置(可以用make savedefconfig生成精简的defconfig文件),然后make mrproper彻底清理源码树,再重新导入defconfig并进入menuconfig修改。
编译成功后,建议将zImage和对应的.dtb文件复制到一个单独的目录,方便后续打包。
cp arch/arm/boot/zImage /path/to/your/sd_card_boot/ cp arch/arm/boot/dts/sun8i-v3s-licheepi-zero.dtb /path/to/your/sd_card_boot/5. 构建根文件系统与启动介质准备
只有内核是启动不了一个完整系统的,我们还需要根文件系统(rootfs)。根文件系统包含了操作系统运行所必需的所有目录结构、系统工具(如ls,mount)、库文件、配置文件(如/etc/fstab,/etc/inittab)等。
5.1 使用BusyBox构建最小根文件系统
对于嵌入式环境,BusyBox是一个瑞士军刀式的工具集,它把许多常用的Unix工具(如sh,echo,mount,ifconfig)集成进一个单一的可执行文件,通过创建符号链接来提供各种命令,极大地节省了空间。
构建步骤:
# 1. 下载并解压BusyBox源码(以1.35.0稳定版为例) wget https://busybox.net/downloads/busybox-1.35.0.tar.bz2 tar -xvf busybox-1.35.0.tar.bz2 cd busybox-1.35.0 # 2. 配置BusyBox,同样使用交叉编译 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig # 使用默认配置 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig # 可选,进行定制在menuconfig中,一个重要的设置是:进入Settings->Build Options,选中Build static binary (no shared libs)。这将BusyBox编译成静态链接,运行时不需要额外的动态库,简化了根文件系统的制作,但体积会稍大。对于新手,静态编译更省心。
# 3. 编译并安装到指定目录 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install CONFIG_PREFIX=/path/to/your/rootfs安装后,在/path/to/your/rootfs目录下,你会看到bin,sbin,usr等目录以及linuxrc文件。
5.2 完善根文件系统目录与关键文件
BusyBox只提供了基础命令,一个可用的根文件系统还需要更多目录和关键配置文件。
cd /path/to/your/rootfs mkdir -p dev proc sys tmp run etc/init.ddev: 设备文件目录,内核启动后会在这里创建设备节点(如console,ttyS0)。proc和sys: 虚拟文件系统,内核通过它们向用户空间暴露系统信息和控制接口。tmp: 临时文件目录。etc/init.d: 初始化脚本目录。
接下来创建最关键的初始化文件。在/path/to/your/rootfs/etc/init.d/下创建一个名为rcS的脚本(注意赋予可执行权限chmod +x rcS):
#!/bin/sh # rcS - 系统启动初始化脚本 # 挂载虚拟文件系统 mount -t proc none /proc mount -t sysfs none /sys mount -t tmpfs none /tmp # 设置主机名 hostname MyAllwinnerBoard # 挂载/dev为tmpfs,允许内核动态创建设备节点 mount -t tmpfs none /dev # 创建设备节点 mknod /dev/console c 5 1 mknod /dev/null c 1 3 mknod /dev/zero c 1 5 # 设置网络(如果需要,这里可以调用ifconfig配置IP) # /sbin/ifconfig lo 127.0.0.1 up # /sbin/ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up # 打印欢迎信息 echo "Welcome to My Custom ARM Linux on Allwinner V3s!"然后,在/path/to/your/rootfs/etc/下创建一个简单的inittab文件,告诉init进程(由BusyBox提供)如何启动:
::sysinit:/etc/init.d/rcS ::askfirst:-/bin/sh ::restart:/sbin/init ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r这个配置的意思是:系统启动时执行/etc/init.d/rcS;然后启动一个登录shell(askfirst);定义重启和关机的动作。
5.3 制作SD卡启动盘
我们需要将编译好的内核、设备树和根文件系统放到SD卡的正确位置。全志芯片通常支持从SD卡的特定分区启动。
分区:将SD卡插入读卡器并连接到电脑。假设SD卡设备是
/dev/sdb(操作前务必用lsblk命令确认,误操作会格式化你的硬盘!)。sudo fdisk /dev/sdb在fdisk交互界面中:
- 输入
o创建新的DOS分区表。 - 输入
n创建新分区,分区类型选p(主分区),分区号1,起始扇区默认,大小例如+64M(用于存放内核和dtb)。 - 输入
t,然后选择分区1,将分区类型改为c(W95 FAT32 (LBA))。 - 输入
n创建第二个分区,类型p,分区号2,起始扇区默认,大小分配剩余所有空间(用于根文件系统)。 - 输入
w写入并退出。
- 输入
格式化与拷贝:
# 格式化第一个分区为FAT32(启动分区) sudo mkfs.vfat /dev/sdb1 # 格式化第二个分区为EXT4(根文件系统分区) sudo mkfs.ext4 /dev/sdb2 # 挂载并拷贝文件 sudo mkdir -p /mnt/boot /mnt/rootfs sudo mount /dev/sdb1 /mnt/boot sudo mount /dev/sdb2 /mnt/rootfs # 拷贝内核和设备树到启动分区 sudo cp /path/to/your/sd_card_boot/zImage /mnt/boot/ sudo cp /path/to/your/sd_card_boot/sun8i-v3s-licheepi-zero.dtb /mnt/boot/ # 拷贝根文件系统 sudo cp -a /path/to/your/rootfs/* /mnt/rootfs/ # 卸载 sudo umount /mnt/boot sudo umount /mnt/rootfs sync # 确保所有数据写入磁盘
6. 上电启动、串口调试与问题排查
激动人心的时刻到了!将制作好的SD卡插入开发板,连接串口调试工具(如USB转TTL模块)到电脑。串口连接通常有三根线:TX(板子)接调试工具的RX,RX(板子)接调试工具的TX,GND对接。板子的供电口接上5V电源。
在电脑上使用串口终端软件(如minicom,picocom或Windows下的Putty、MobaXterm)。设置正确的串口设备(如/dev/ttyUSB0)、波特率(全志平台通常是115200)、数据位8、停止位1、无校验、无流控。
给开发板上电,在串口终端里你应该能看到如瀑布般的内核启动信息。如果一切顺利,最后会出现类似/ #的BusyBox shell提示符。恭喜你,你的自定义内核成功启动了!
6.1 启动失败常见问题与排查技巧
然而,现实往往更骨感。以下是几种常见的启动失败现象及排查思路:
串口无任何输出(一片空白):
- 检查硬件连接:确认串口线是否接反(TX/RX交叉),地线是否接好,供电是否正常。
- 检查串口终端设置:波特率
115200是否正确?串口设备号是否正确(有时需要sudo权限或用户加入dialout组)? - 检查内核配置:确认
CONFIG_EARLY_PRINTK和正确的UART驱动已启用并编译进内核。对于全志V3s,其调试串口通常是UART0。 - 检查SD卡和启动介质:SD卡是否接触不良?是否烧录了正确的引导程序(如U-Boot)?对于全志很多芯片,SD卡启动需要有一个特殊的“启动头”,但如果我们使用U-Boot SPL(Secondary Program Loader)或者某些板子的ROM代码可以直接加载FAT分区里的
zImage和dtb。最稳妥的方式是参考你具体开发板的官方或社区Wiki,使用他们推荐的烧录工具(如sunxi-tools包里的sunxi-fel或dd命令)来制作完整的启动SD卡镜像,其中包含了必要的引导代码。
内核启动到一半卡住(例如卡在“Starting kernel ...”之后):
- 查看卡住前的最后几条信息:这是最重要的线索。可能的原因有:
- 设备树不匹配:
.dtb文件与你的实际硬件不匹配。确认你编译和使用的dtb文件是否对应你的确切板型。一个V3s的dtb可能不适用于另一个不同外设设计的V3s板子。 - 内存配置错误:内核或设备树中定义的内存大小、地址与硬件不符。检查设备树源文件(.dts)中的
memory节点。 - 驱动初始化失败:某个关键驱动(如时钟、MMC)初始化失败。尝试在menuconfig中简化配置,去掉非必要的驱动,或者将可疑驱动的日志级别调高(
CONFIG_DYNAMIC_DEBUG或驱动本身的debug选项)。
- 设备树不匹配:
- 查看卡住前的最后几条信息:这是最重要的线索。可能的原因有:
内核恐慌(Kernel Panic):
- 错误信息通常会明确指出原因,例如:
“Unable to mount root fs”: 内核找不到根文件系统。检查内核命令行参数(bootargs)是否正确传递了root=信息(例如root=/dev/mmcblk0p2 rootwait)。检查根文件系统分区格式是否与内核配置支持的文件系统(如EXT4)一致。检查根文件系统目录是否完整,特别是/init或/linuxrc是否存在且可执行。“VFS: Cannot open root device”: 类似上述问题,根设备指定错误或驱动未加载。确保MMC/SD卡驱动已编译进内核。
- 错误信息通常会明确指出原因,例如:
成功进入shell但网络/USB等外设不工作:
- 这通常是驱动未启用或配置不正确。首先用
lsmod查看已加载的模块,用ifconfig -a查看网络接口。如果接口不存在,说明网络驱动未加载。你需要返回内核配置,确保对应驱动已编译(Y或M)。如果是模块(M),你还需要在根文件系统中包含这个模块文件(位于/lib/modules/下),并在启动脚本中insmod加载它。
- 这通常是驱动未启用或配置不正确。首先用
6.2 进阶:配置内核启动参数与使用U-Boot
更常见的启动流程是:芯片ROM代码 -> U-Boot SPL -> U-Boot -> Linux Kernel。U-Boot是一个功能强大的引导加载程序,它可以灵活地设置内核启动参数。
在U-Boot命令行中,你可以设置bootargs环境变量来传递参数给内核:
setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait panic=10 saveenv bootconsole=ttyS0,115200: 指定控制台为串口0,波特率115200。root=/dev/mmcblk0p2: 指定根文件系统在SD卡的第二个分区。rootwait: 等待根设备就绪。panic=10: 内核发生panic后10秒重启。
你也可以让U-Boot直接从SD卡加载内核和设备树并启动:
fatload mmc 0:1 0x41000000 zImage fatload mmc 0:1 0x41800000 sun8i-v3s-licheepi-zero.dtb bootz 0x41000000 - 0x41800000这种方式更加灵活,便于调试不同的内核和参数。
整个从零编译并启动全志ARM Linux内核的过程,就像在组装一个精密的数字时钟。你需要正确的零件(源码、工具链)、按照图纸(配置)进行组装(编译)、提供动力(根文件系统)、最后校准(调试)才能让它准确运行。这个过程充满挑战,但每一步的成功都会带来巨大的成就感,并且让你对嵌入式Linux系统的理解深入到骨髓。当你第一次看到自己编译的内核在屏幕上打印出启动日志,并最终弹出那个熟悉的#提示符时,你会觉得所有的折腾都是值得的。这不仅仅是启动了一个系统,更是为你打开了一扇通往嵌入式世界深处的大门。
