简介:系统梳理了将Linux操作系统移植到ARM开发板的完整流程,是一份面向嵌入式开发者和初学者的专业文献。内容涵盖开发环境搭建、交叉编译工具链安装、Linux内核编译与配置、设备驱动程序移植、文件系统配置与优化等关键环节,并针对硬件选型、内核适配、驱动调试等常见问题给出清晰的技术路径,从开发板选型到系统测试均有实操性建议。资源还结合手机、家电、汽车电子、医疗设备等典型应用场景,帮助读者理解嵌入式Linux的实际部署方式,建立从底层移植到上层应用的完整认知。PDF文档为单文件资源,共1个PDF文件,大小280KB,内容精炼、结构清晰,既可作为系统教材,也可作为项目开发中的快速参考手册。已有1066人学习使用,适合在校学生、嵌入式工程师及对Linux底层技术感兴趣的读者下载参考,尤其是希望掌握嵌入式Linux移植全流程的开发者,可借此快速建立知识框架、降低初次实践时的试错成本。 前几天帮一个朋友把一块吃灰两年的开发板救活了。板子本身没坏,就是原厂BSP老旧、内核版本跑不动新功能,加上他想在上面加一路新的MIPI摄像头,只能从系统层面重新来过。整个过程走下来,其实就是一次标准的“嵌入式Linux系统移植”,而这恰好也是很多刚入行嵌入式方向的朋友最容易卡住的一关。
先说清楚它到底是干什么的。嵌入式Linux系统移植,说白了就是让Linux内核、Bootloader和根文件系统这套软件,在一块指定的硬件平台上跑起来,并且能稳定跑完你想要的功能。它解决的问题不是“把代码编译一遍”那么简单,而是要把芯片手册、电路原理图、内核配置、驱动模型全部串起来,最终交付一个“上电就能起、外设都能动、资源够用”的软件底座。
这篇文章适合的读者有两类:一类是刚接触嵌入式Linux、手里有一块板子但不知道怎么下手的新手;另一类是已经能点亮开发板,但遇到内核崩溃、外设不通、文件系统挂载失败这类问题还经常挠头的人。我会从整体思路、环境准备、三件套移植流程、坑点排查到进阶路径,完整拆解一遍,中间穿插这几年实际踩过的教训。
1. 系统移植到底在移什么
1.1 别把“移植”想得太玄乎
很多新手一听到“移植”两个字,下意识觉得是把代码从一个平台搬到另一个平台,像搬家一样。实际干过就会发现,嵌入式Linux移植的本质,其实是“配置工程”,而不是“代码搬运”。市面上主流的SoC,比如i.MX系列、STM32MP1、全志、瑞芯微,芯片厂商基本都提供了可用的Linux内核源码和Bootloader源码,绝大多数时候你不需要从零写代码,而是要把这套通用代码“适配”到你的具体板卡上。
所谓适配,大概包括三件事:第一,告诉内核你的CPU是哪颗、内存多大、频率跑多少;第二,告诉内核你的外设接在哪个地址上、用的什么时序协议;第三,告诉系统你的根文件系统放在哪、用什么介质启动。这三件事做完,系统就能跑起来。剩下的才是驱动开发、性能调优这些进阶活。
这样一说,你会发现问题不在于“移植”本身有多深奥,而在于这三件事背后牵涉的知识面很宽。比如配置DDR时序需要看懂芯片手册里的寄存器说明,配置设备树需要理解硬件的连接关系,配置根文件系统需要了解内核启动流程。但好消息是,这些知识是可以通过一个完整的移植案例串起来的,先有全局图景,再补局部细节,比盲目啃书高效得多。
1.2 三件套:Bootloader、Kernel、Rootfs
一套能启动的嵌入式Linux系统,自下而上分成三层,业界习惯叫“三件套”:
- Bootloader(引导加载程序):上电后最先执行的代码,负责初始化CPU、DDR、存储控制器,加载内核镜像到内存并跳转执行。嵌入式领域最常用的是U-Boot。
- Kernel(内核):Linux内核本体,负责进程管理、内存管理、文件系统、网络协议栈,以及把硬件驱动框架跑起来。内核本身不直接操作每一个外设寄存器,而是靠驱动去管。
- Rootfs(根文件系统):整个用户空间的“家”,包含init进程、shell、各种库和应用程序。常见选择有BusyBox、Buildroot产物、完整的发行版文件系统。
这三个部分不是独立的,它们之间有严格的启动顺序和参数传递约定。U-Boot负责把内核镜像和设备树二进制文件加载到内存,然后跳转;内核拿到设备树后,解析硬件信息;根文件系统挂载成功后,内核执行它里面的第一个用户进程(通常是init)。任何一个环节断链,系统都起不来。
1.3 为什么非要从源码构建
你可能会问:芯片厂商不是已经给好了镜像吗?直接用不行吗?答案是可以,但你很快就会碰到三个问题:一是原厂镜像往往只适配官方评估板,你自己画的板子引脚定义不一样,跑不起来;二是原厂内核版本更新缓慢,你想用新特性或新驱动,只能自己移植;三是镜像里的配置是通用的,体积大、冗余多,对量产产品来说完全不可接受。
所以,正统的做法是自己动手从源码构建三件套。这不仅是为了“能跑”,更是为了理解整个系统的耦合关系。只有亲手配置过内核、裁剪过文件系统,你才可能在生产项目里做到“出问题时能定位到是配置问题还是驱动问题”,而不是只会重刷固件。
2. 动手前的准备:环境、工具链与文件系统
2.1 宿主机环境搭建
系统移植必须在Linux环境里做,Windows下只能靠虚拟机或者WSL,但WSL在串口、USB设备直通方面坑很多,所以如果你有条件,强烈建议直接装一台Ubuntu 20.04或22.04的物理机。没条件的话,虚拟机也够用,只是USB设备直通偶尔会掉线,后面调试的时候心态容易崩。
宿主机上需要装的基础工具有:交叉编译工具链、git、make、gcc、bison、flex、libncurses-dev、u-boot-tools、nfs-kernel-server、tftpd-hpa。其中交叉编译工具链是整个流程里最重要的,后面单独说。别的工具大多是编译过程的依赖,缺哪个装哪个,用apt就能解决,不用刻意记。
2.2 交叉编译链的选择
交叉编译的意思是在x86的电脑上编译出ARM平台能运行的代码。这里有个容易被忽略的点:你用的工具链版本,必须和内核源码版本、芯片厂商推荐的版本匹配。原因很简单,工具链的GCC版本决定了内联汇编语法、内建函数的兼容性,太老或太新的工具链编内核,偶尔会冒出莫名其妙的语法错误。
我的习惯是先用芯片厂商SDK里预置的工具链,比如NXP的Linaro GCC工具链、Rockchip的交叉编译工具链,这些厂商长期维护,兼容性验证过。只有厂商没提供时,才自己去用Linaro或ARM官网的预编译工具链。拿到工具链后,第一件事是把它的bin目录加到环境变量里,然后确认一下交叉编译器能正常工作:
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin arm-linux-gnueabihf-gcc -v看到版本输出后,别忘了测试一下编译一个小程序,看看动态库依赖是不是正常。有些精简版工具链缺少libstdc++,编应用没问题,编内核时报错,这时候就得换完整版。
2.3 必要的调试基础设施:串口、TFTP、NFS
移植调试阶段,大多数问题集中在系统起不来或起来后外设不通。如果每次改一点代码都要烧SD卡或eMMC,效率太低。强烈建议先搭好三套调试设施:
- 串口控制台:通过UART线连接开发板和电脑,用minicom或putty看启动日志、操作U-Boot命令行。这是整个移植过程里最重要的观察窗口。
- TFTP服务:把内核镜像和设备树放在宿主机上,开发板在U-Boot里用TFTP拉取,省去反复烧写的麻烦。
- NFS服务:开发板挂载宿主机上某个目录作为根文件系统,改文件系统内容不需要重新烧写存储介质。
这三套设施里,串口是必须的,TFTP和NFS属于强烈建议。搭好之后,你的开发节奏会变成:改配置、编译、在U-Boot里敲两行命令、看日志、再改。相比每次烧卡,省下的时间不是一点半点。
2.4 拿到板卡之后,先别急着编译
我在带人做移植时有个固定要求:动手之前,必须把三份资料看一遍。第一是芯片数据手册里关于启动模式、内存映射的部分,搞清楚上电后CPU从哪里取指令;第二是开发板原理图,确认LED、串口、网卡、DDR分别接在哪些引脚上;第三是原厂BSP的README或移植指南,看看有没有针对你的板卡的已知问题和补丁。
这一步看起来费时间,但能避免后面走大弯路。比如有些板卡的调试串口不是默认的UART0,如果你不看原理图,串口永远没输出,你会以为系统没启动,其实是日志发到了别的口上。再比如DDR颗粒型号和原厂评估板不一致,直接用原厂配置大概率起不来,必须按数据手册改参数。这些信息全在“看资料”这一步里。
3. 移植三大核心环节实操拆解
3.1 U-Boot移植要点
U-Boot是系统上电后运行的第一个程序,它为内核运行准备环境。移植U-Boot的核心工作,是让它在你的板卡上完成三件事:初始化DDR、加载内核镜像、传递启动参数。
以一块典型的i.MX6ULL板卡为例,U-Boot的源码目录里,board/目录下按厂商和板型分目录管理。如果你的板子是自研的,最快的方式是复制一块原厂评估板的配置,在此基础上修改。需要重点关注的几个文件类型:
defconfig:板级默认配置,决定编译哪些功能。dts/.dts:U-Boot自身的设备树,主要用于告诉U-Boot外设(如网卡、SD卡控制器)挂在哪里。include/configs/<board>.h:包含环境变量、启动参数等宏定义。
实际移植时,改动最多的是环境变量里的启动命令。比如,你要让U-Boot从网络加载内核和设备树,需要设置bootcmd和bootargs:
setenv bootcmd 'tftp 0x80800000 zImage; tftp 0x83000000 imx6ull-14x14-evk.dtb; bootz 0x80800000 - 0x83000000' setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs/rootfs rw ip=dhcp' saveenv boot注意root=/dev/nfs表示根文件系统走NFS网挂,ip=dhcp让板子自动获取IP。这种配置在开发阶段非常好用,但量产时一定要改回从eMMC或SD卡启动,并设置静态IP或关闭网络启动功能。
我说一个U-Boot阶段最常见的坑:DDR初始化失败。表现是U-Boot完全没有串口输出,或者输出到一半卡死。排查时要先用示波器或者逻辑分析仪看DDR地址线、数据线上有没有波形,同时确认DDR型号和配置参数是否匹配。大部分情况下,问题出在DDR时序参数复制粘贴时没改全,或者是没在board_init_f阶段正确配置时钟树。
3.2 内核移植:从配置到设备树
内核移植是“三件套”里技术含量最高、也最容易让人迷失的部分。新手常犯的一个错误是:拿到厂商内核代码后直接make xxx_defconfig,编完烧进去,然后发现网络不通、LCD不亮,就不知道从哪里下手了。
正确的姿势是理解内核的配置和硬件描述是分离的。内核代码分为两层:一层是逻辑代码(协议栈、调度器、文件系统等),这些基本不需要改;另一层是硬件相关代码,包括驱动和硬件描述信息。在现在的Linux内核中,硬件描述信息被集中放到了设备树(Device Tree)里。设备树的基本语法是节点+属性,每一个节点描述一个硬件设备:
&i2c1 { clock-frequency = <100000>; pinctrl-0 = <&pinctrl_i2c1>; status = "okay"; touchscreen: ft5x06@38 { compatible = "edt,edt-ft5x06"; reg = <0x38>; reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>; }; };这段描述的意思是:I2C1总线上挂了一个FT5x06触摸芯片,设备地址是0x38,复位引脚接到了GPIO5的第9脚。内核里的触摸屏驱动会用compatible字段去匹配这个节点,匹配成功后,驱动就能通过标准的接口去读写寄存器。
移植内核的具体步骤可以总结为:
- 先选一个离你板子最近的defconfig,比如
imx_v6_v7_defconfig,编译出默认内核。 - 编译你板卡对应的设备树,烧进去,启动系统。
- 逐个外设调试:网卡、存储、显示、触摸、音频,缺什么就在设备树里补什么节点。
- 每次改完设备树,用
savedefconfig更新defconfig,用make dtbs重新编译设备树。
配置内核的时候,最常用的命令是make menuconfig,它在终端里提供一个图形化配置界面。内核功能多到离谱,刚接触时容易眼花缭乱。我的建议是:不要试图一个个看过去,先搞清楚你需要哪些子系统,按子系统搜索对应的配置选项。比如要让内核支持设备树,必须确保选上CONFIG_OF,要让内核支持NFS根文件系统,需要选上CONFIG_ROOT_NFS和CONFIG_NFS_V4。
很多人第一次编译内核,最常遇到的问题就是不晓得哪些驱动要编成模块(M),哪些要编进内核(*)。原则其实很简单:启动早期就要用的驱动,比如DDR、存储控制器、串口,必须编成*;后期才需要的功能,比如USB Wi-Fi、录音播歌,可以编成M。编成模块的好处是减小内核镜像体积,坏处是如果根文件系统里没有对应的ko文件,功能照样起不来。
设备树这块,新手最搞不懂的是“为什么我改了设备树,系统反而不启动了”。如果你把一个外设节点从disabled改成okay,那么内核启动时就会尝试初始化这个设备。如果这个设备没有真正接好,或者驱动有bug,初始化过程就会卡住,导致整个内核启动失败。所以,改设备树时不要一次改太多,每次只开一个外设,启动验证过了再开下一个,这一点非常重要。
3.3 Rootfs构建:BusyBox还是完整发行版
内核起来之后,必须要有根文件系统才能进入用户空间。这里的选择决定了你的系统是“嵌入式风格”还是“服务器风格”。
开发阶段我强烈推荐先用NFS挂载宿主机上准备好的根文件系统,这样不用反复烧写存储介质。最简单的方案是直接用BusyBox来构建根文件系统,因为它体积小、依赖少、功能够用。构建过程大致是:
mkrootfs_dir=/srv/nfs/rootfs mkdir -p $mkrootfs_dir/{bin,sbin,etc,dev,proc,sys,lib,usr,tmp} cd busybox-1.36.1 make menuconfig # 设置静态编译 CONFIG_STATIC=y make && make install CONFIG_PREFIX=$mkrootfs_dir cd .. cp -a /opt/toolchain/arm-linux-gnueabihf/libc/lib/. $mkrootfs_dir/lib/ touch $mkrootfs_dir/etc/inittab这里有两件容易被忽略的事:第一,BusyBox编译时最好选CONFIG_STATIC=y,也就是静态链接,这样生成的busybox可执行文件不依赖glibc的so文件,省去复制库的麻烦。第二,如果选了动态链接,那必须把工具链的libc/lib目录里的库文件拷贝到根文件系统的lib目录里,否则启动时会报“No such file or directory”。
构建完BusyBox之后,还要手动创建几个关键节点,否则系统启动时没有输入输出设备:
mkdir $mkrootfs_dir/dev sudo mknod $mkrootfs_dir/dev/console c 5 1 sudo mknod $mkrootfs_dir/dev/null c 1 3有不少人在这里翻车,启动后内核起来一段就停住,报Kernel panic - not syncing: VFS: Unable to mount root fs。导致这个问题的原因很多,但只要你的串口终端能输出,就能通过加内核启动参数init=/bin/sh来手动检查根文件系统是不是真的挂上了,以及/sbin/init是否存在、权限对不对。
如果你嫌BusyBox功能太少,也可以直接用一个精简的Ubuntu/Debian根文件系统。国内常用的做法是用debootstrap一键构建:
sudo debootstrap --arch=armhf focal /srv/nfs/rootfs http://ports.ubuntu.com/ubuntu-ports/这种方式得到的文件系统功能完整,自带apt包管理,装软件特别方便。代价是体积动辄几百MB,启动速度比BusyBox慢不少,所以只适合开发调试,量产产品还是回到BusyBox或Buildroot。
3.4 启动流程联调:从日志里找真相
系统能跑起来后,很多人以为移植就结束了,其实恰恰相反,真正的调试从这时候才开始。我建议养成一个习惯:把完整的启动日志从串口复制到文件里,然后分成三个片段去读:U-Boot阶段、内核启动阶段、用户空间阶段。
U-Boot阶段的日志会打印板卡型号、DDR信息、启动介质,以及环境变量的相关输出。内核启动阶段的日志会打印内核版本、设备树解析情况、每个驱动初始化的信息。用户空间阶段的日志会打印init进程启动、挂载分区、启动服务的过程。分段读日志的核心价值在于:根据日志停在哪个阶段,快速判断问题属于哪一层。
举个例子,如果内核日志停在Waiting for root device /dev/nfs...,说明内核找不到根设备,问题大概率在设备树或启动参数上。如果日志停在了某个驱动初始化的打印上,比如i2c: error,说明这个设备初始化失败了,问题大概率在硬件连接、设备树配置或驱动源码上。
我在实际项目中总结出一个很实用的土办法:串口日志是最后一道保险,无论你烧了什么稀奇古怪的东西,只要串口还能出字,系统就还有救;如果串口完全没反应,优先怀疑启动镜像本身、DDR配置和U-Boot环境变量,而不是内核配置。
4. 常见问题与排查技巧实录
移植过程踩坑是必然的,我把自己和身边同事这些年遇到的高频问题整理成一个速查表,纯经验向,不一定能覆盖所有平台,但流程具有通用性。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 上电后串口无输出 | 启动介质没选对;DDR初始化失败;串口引脚配置错误 | 检查启动拨码/跳线;确认串口接到正确的UART;查DDR配置参数 |
| U-Boot启动后卡住,无法进入命令行 | 环境变量损坏;存储驱动异常 | 进入u-boot后输入env default -a恢复;检查SD/eMMC驱动是否编入 |
内核启动后报Kernel panic: VFS: Unable to mount root fs | 根文件系统路径错;设备树中存储控制器节点状态不对;文件系统类型不支持 | 加root=/dev/ram0测试initrd方案;检查内核是否选上对应文件系统驱动 |
| 能启动但网卡ping不通 | 设备树中MAC节点错误;PHY芯片复位时序不对;驱动未编译进内核 | 检查/sys/class/net/eth0/address;用mii-tool eth0查看PHY链接状态 |
内核日志反复刷rcu_sched detected stalls | 某个驱动死锁;中断处理有问题 | 打开CONFIG_PROVE_LOCKING重新编译;查护日志,定位卡住的驱动 |
| LCD屏幕不亮 | 显示节点时序参数不对;背光GPIO配置错误;framebuffer驱动没编译 | 检查设备树display节点;确认背光引脚的GPIO配置;查看/dev/fb0是否存在 |
| 系统日志乱码 | 波特率不匹配;串口驱动程序未适配 | 确认终端软件波特率,常见115200/921600,用示波器量波形看波特率 |
下面挑三个我印象最深的展开说说。
第一个是“U-Boot能启动,但内核起不来”。现象是U-Boot正常打印,但执行到bootz后就卡死或者重启。排查时我先用TFTP单独加载内核和设备树,确认两个文件都在内存里;然后在U-Boot里手动执行booti或bootz命令,观察具体卡在哪个地址。最后发现是原厂DDR配置的容量参数出了问题,内核解压时覆盖到了内存边界。
第二个是NFS挂载根文件系统失败。宿主机配置好NFS服务后,开发板一直报VFS: Unable to mount root fs via NFS。排查后发现问题是宿主机防火墙挡住了2049和111端口,以及NFS的no_subtree_check参数没配全。这里要提醒一下,很多教程里的NFS配置只写了/srv/nfs *(rw,sync,no_root_squash),但现代NFS版本对insecure选项很敏感,客户端如果用了非标准端口,要加上insecure。
第三个是设备树里GPIO配置错了导致驱动起不来。我的触摸屏驱动在init的时候一直报request_irq failed。检查原理图发现,本来应该用GPIO5的IO09作为中断脚,我写成了GPIO4_IO09,导致内核在申请中断时发现该GPIO被复用成了别的功能。这种问题不细看原理图,很难发现。所以再看一遍那句老话:改设备树前先看图,这个习惯能省下一整天的调试时间。
5. 进阶路线:从“能跑”到“跑得稳”
5.1 裁剪优化与构建工具
产品开发阶段,板卡移植完成只是起点,接下来还有很多优化空间。比如内核体积和启动时间的优化:用make menuconfig关闭用不到的功能,走make savedefconfig保存最小配置;用CONFIG_CC_OPTIMIZE_FOR_SIZE优化体积;去掉不必要的内核打印,让启动日志更干净。
如果不想手动管理一堆.config和rootfs目录,建议从项目一开始就用Buildroot或Yocto。Buildroot更轻量,适合中小型产品,配置简单,一条命令就能生成工具链、内核、根文件系统镜像和烧写工具。Yocto功能更强大,能够精确控制整个发行版构建流程,适合大型产品和定制化需求极高的场景。我自己做量产项目,90%的情况用Buildroot就够了,Yocto的学习曲线太陡,维护成本高,小团队慎用。
5.2 驱动开发与内核调试
系统跑稳之后,真正有价值的工作是驱动开发和内核调试。这部分内容展开能写一本书,我只说几个方向:第一,学会阅读芯片数据手册的寄存器描述,这是写驱动的底层能力;第二,掌握Linux内核驱动模型,尤其要弄清楚platform bus、device、driver三者的匹配关系;第三,学会用工具调试——printk打印各级日志、ftrace跟踪函数调用、kgdb在断点处调试内核。
很多自学的朋友问我要不要去看Linux设备驱动开发详解,我的答案是:要,但别只看书,要配合一个具体的硬件平台去写驱动。比如在你移植好的板子上写一个GPIO按键驱动、写一个I2C触摸屏驱动,遇到问题再看书,效率远高于纯啃书。
5.3 把“移植”能力迁移到新平台
嵌入式Linux系统移植的核心能力不是某一颗芯片的配置,而是“阅读文档、理解硬件、分析日志、定位问题”这一整套方法论。只要掌握了这套方法论,换一颗SoC、换一块板卡,只是换一套资料去读、换一组配置去改而已。
我自己接手过不少新项目,每次看到一颗没用过的芯片、一块全新的板卡,心里其实不太慌。因为我知道,按着“看手册、配设备树、调驱动、查串口日志”这套流程走,总能把系统带起来。如果说有什么“独门绝技”,那就是一个习惯:永远保留第一次跑通系统时的串口日志和配置文件。那些看似脏乱差的记录,反而是未来排查问题的最宝贵参考。
最后分享一个小技巧:在你移植成功那一刻,别急着发朋友圈,先把U-Boot环境变量、内核.config、设备树dts、根文件系统的构建脚本全部备份到Git仓库里。再过一个季度回来看,你会发现,这套“脏乱差”的记录,才是整个项目里最保值的技术资产。
本文还有配套的精品资源,点击获取