
1. 项目概述与核心价值最近在折腾一块基于NXP i.MX6ULL处理器的开发板想把一个更精简、更现代的Linux系统跑上去。官方的BSPBoard Support Package通常基于Yocto或Buildroot构建虽然稳定但系统定制性高包管理有时不够灵活而且整个镜像体积也偏大。于是我把目光投向了Ubuntu Core 16.04更准确地说是它的最小化基础版本——ubuntu-base。这个项目就是记录如何将ubuntu-base-16.04-core-armhf.tar.gz这个根文件系统成功移植到i.MX6ULL开发板上的全过程。你可能要问为什么是Ubuntu Core 16.04首先它基于Ubuntu 16.04 LTS拥有长达五年的长期支持对于嵌入式设备来说稳定性和长期维护至关重要。其次ubuntu-base是一个极度精简的根文件系统只包含最核心的apt包管理工具、systemd或upstart取决于版本初始化系统以及最基本的命令行工具没有图形界面没有多余的软件包非常适合资源受限的嵌入式环境。最后得益于Ubuntu庞大的软件仓库和成熟的apt生态后续想要安装任何软件如Python、Node.js、数据库都异常方便大大提升了开发的灵活性和效率。对于i.MX6ULL这款芯片来说它是一颗Cortex-A7内核的处理器性能足以流畅运行一个完整的Linux发行版。移植ubuntu-base意味着我们可以在一个熟悉的Ubuntu环境下进行应用开发直接使用apt-get install来管理依赖而无需像在Buildroot里那样从头交叉编译每一个包。这对于需要快速原型验证、或者应用依赖复杂的项目来说是一个巨大的优势。整个过程涉及Bootloader适配、内核配置与编译、根文件系统定制、以及一系列针对特定硬件的驱动和配置调整是深入理解嵌入式Linux系统构成和启动流程的绝佳实践。2. 开发环境搭建与工具链准备工欲善其事必先利其器。在开始动手之前一个稳定、高效的交叉编译环境是成功的基础。i.MX6ULL是ARM Cortex-A7架构我们需要在x86_64的宿主机通常是Ubuntu或Debian系统上搭建针对armhfARM Hard Float的交叉编译工具链。2.1 宿主机环境与工具链选择我使用的宿主机是Ubuntu 20.04 LTS。首先安装一些基础编译工具sudo apt-get update sudo apt-get install build-essential git bc bison flex libssl-dev libncurses5-dev对于交叉编译工具链常见的有Linaro和ARM官方提供的gcc-arm-linux-gnueabihf。我选择使用NXP官方推荐的或者更通用的gcc-arm-linux-gnueabihf。在Ubuntu仓库中可以直接安装sudo apt-get install gcc-arm-linux-gnueabihf g-arm-linux-gnueabihf安装完成后可以通过arm-linux-gnueabihf-gcc -v来验证版本。我使用的是gcc version 9.4.0。这个工具链将用于编译U-Boot和Linux内核。注意确保你安装的工具链支持硬浮点hf后缀因为i.MX6ULL的Cortex-A7内核带有VFPv4浮点单元使用硬浮点能显著提升浮点运算性能。使用错误如gnueabi的工具链可能导致内核或应用无法正常运行。2.2 获取关键源代码我们需要三样东西U-Boot Bootloader、Linux内核、以及Ubuntu Base根文件系统。U-Boot我直接从NXP官方提供的Git仓库获取针对i.MX6ULL的U-Boot版本这通常包含了必要的板级支持。git clone https://github.com/nxp-imx/uboot-imx -b imx_v2020.04_5.4.70_2.3.0这个tag版本与NXP官方发布的BSP版本对齐驱动支持和稳定性最有保障。Linux内核同样从NXP官方仓库获取。git clone https://github.com/nxp-imx/linux-imx -b imx_5.4.70_2.3.0选择与U-Boot相匹配的内核版本可以避免很多兼容性问题。Ubuntu Base根文件系统从Ubuntu官方镜像站下载。wget http://cdimage.ubuntu.com/ubuntu-base/releases/16.04/release/ubuntu-base-16.04-core-armhf.tar.gz这里选择core版本而非basecore更精简。armhf架构与我们的目标板完全匹配。2.3 准备SD卡与分区规划我将使用SD卡作为启动和根文件系统的存储介质。你需要一张至少4GB的SD卡推荐8GB或以上。使用fdisk -l命令找到你的SD卡设备例如/dev/sdb。分区规划是至关重要的一步。一个典型的可启动SD卡至少需要两个分区第一个分区 (FAT32)用于存放BootloaderU-Boot和Linux内核镜像zImage、设备树文件.dtb。容量通常32MB-128MB就够了格式化为FAT。第二个分区 (EXT4)用于存放完整的Ubuntu根文件系统。容量根据你的需求剩余空间都可以给它格式化为EXT4。使用fdisk或图形化工具gparted进行分区。以下是用fdisk的命令行操作概览操作前请务必确认设备名误操作会导致数据丢失sudo fdisk /dev/sdb # 在fdisk交互界面中 # 输入 o 创建新的DOS分区表 # 输入 n 创建新分区 - p 主分区 - 1 分区号 - 起始扇区默认 - 64M 大小 # 输入 t 更改分区类型 - c 设置为W95 FAT32 (LBA) # 输入 n 创建新分区 - p 主分区 - 2 分区号 - 起始扇区默认 - 直接回车使用所有剩余空间 # 输入 w 写入并退出分区完成后格式化sudo mkfs.vfat /dev/sdb1 sudo mkfs.ext4 /dev/sdb23. U-Boot的配置、编译与烧写U-Boot是系统上电后运行的第一段代码它负责初始化硬件、加载内核和设备树到内存并传递启动参数。3.1 配置与编译U-Boot进入U-Boot源码目录首先需要配置为你的具体开发板。i.MX6ULL的开发板型号众多如MYiR的MYD-Y6ULX正点原子的Alpha等配置名通常类似mx6ull_14x14_evk_defconfig。你需要根据自己板子的型号找到最接近的默认配置。这里以NXP官方的评估板配置为例cd uboot-imx make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- mx6ull_14x14_evk_defconfig如果找不到完全匹配的可能需要基于一个最接近的配置然后通过make menuconfig进行微调主要是DRAM大小、网络PHY型号、SD/MMC接口等。配置完成后开始编译make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc)-j$(nproc)表示使用所有CPU核心并行编译以加快速度。编译成功后会在当前目录生成几个关键文件u-boot.imx这是最终需要烧写到SD卡最前端的镜像它包含了U-Boot本身以及i.MX系列ROM Code所需的头部信息。u-boot.bin纯二进制文件。3.2 烧写U-Boot到SD卡i.MX6ULL的ROM Code支持从SD卡启动它会从SD卡的固定偏移量通常是1KB位置加载U-Boot。我们使用dd命令进行烧写sudo dd ifu-boot.imx of/dev/sdb bs1k seek1 convfsync参数解释ifu-boot.imx输入文件。of/dev/sdb输出到整个SD卡设备不是分区如sdb1。bs1k块大小为1KB。seek1跳过输出文件开头的1个块即1KB。这正是i.MX6ULL ROM Code寻找启动镜像的位置。convfsync确保数据完全写入磁盘后再返回。实操心得dd命令非常强大但也危险务必再三确认of参数指向的是正确的SD卡设备。写错盘可能导致宿主机系统数据丢失。一个保险的做法是在插入SD卡前后分别执行lsblk命令对比确认新出现的设备就是你的SD卡。烧写完成后将SD卡插入开发板连接串口调试工具如USB转TTL设置串口终端如minicom或picocom波特率115200。上电后你应该能在串口终端看到U-Boot的启动日志。如果没看到请检查串口连接、波特率以及U-Boot编译配置是否正确对应你的硬件。4. Linux内核的配置、编译与设备树U-Boot启动后它的任务就是加载并启动Linux内核。我们需要一个为i.MX6ULL定制编译的内核。4.1 内核配置与编译进入Linux内核源码目录同样先进行配置。NXP提供的defconfig通常已经为i.MX6ULL做了较好的基础配置。cd linux-imx make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- imx_v7_defconfigimx_v7_defconfig是一个适用于i.MX6/7系列的基础配置。接下来我们通常需要根据实际硬件进行微调特别是网络驱动确保你的板载以太网PHY芯片的驱动被编译进内核或模块如MICREL_PHY,SMSC_PHY。SD/MMC确保SD卡控制器驱动已启用。USB如果要用USB设备需启用相关驱动。文件系统确保EXT4、FAT/VFAT文件系统支持已编译进内核*而不是模块M。可以使用图形化配置工具make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig在菜单中使用空格键切换选项状态*内置M模块空为不编译。对于嵌入式系统为了简化启动过程建议将核心驱动如网络、块设备、文件系统直接编译进内核。配置完成后开始编译内核镜像和设备树make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- zImage dtbs -j$(nproc)zImage压缩的内核镜像文件。dtbs编译所有设备树二进制文件Device Tree Blob。编译完成后关键文件位于内核镜像arch/arm/boot/zImage设备树文件arch/arm/boot/dts/目录下例如对于imx6ull-14x14-evk.dts会生成imx6ull-14x14-evk.dtb。你需要找到与你的开发板最匹配的.dtb文件。4.2 设备树Device Tree的关键作用与适配设备树是描述硬件拓扑结构的数据结构它告诉内核板上有什么硬件、地址如何映射、如何配置。对于移植工作设备树的正确性至关重要。如果你的开发板与官方评估板有差异比如使用了不同的PHY、不同的LCD屏你可能需要修改设备树源文件.dts。常见的修改包括内存大小在memory节点中修改reg属性。网络PHY在fec1或fec2节点下修改phy-mode、phy-handle指向的PHY节点以及PHY节点的compatible属性如ethernet-phy-id0007.c0f1需要匹配你的PHY芯片ID。引脚复用IOMUX确保各个外设所使用的GPIO引脚配置与硬件原理图一致。修改后需要重新编译设备树make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs注意事项设备树编译错误通常是由于语法问题或引用了未定义的节点。仔细查看编译输出信息。一个实用的技巧是先用官方评估板的设备树启动系统通过cat /proc/device-tree来查看当前加载的设备树信息再与你实际的硬件进行比对找出差异点进行修改。5. 构建与定制Ubuntu Base根文件系统这是让系统“活”起来的关键一步。我们将把下载的ubuntu-base压缩包解压到SD卡的第二个分区EXT4分区并进行必要的定制。5.1 挂载与解压根文件系统首先将SD卡的第二个分区EXT4分区挂载到宿主机的一个目录下sudo mkdir -p /mnt/rootfs sudo mount /dev/sdb2 /mnt/rootfs然后解压Ubuntu Base根文件系统到此目录。务必使用sudo并保留文件属性sudo tar -xzvf ubuntu-base-16.04-core-armhf.tar.gz -C /mnt/rootfs解压后/mnt/rootfs目录下就包含了Ubuntu最基础的目录结构/bin,/etc,/usr,/var等。5.2 配置基础系统与软件源接下来我们需要在chroot环境下对这个根文件系统进行配置。chroot可以让我们“切换根目录”到这个新的文件系统仿佛就在目标板上运行命令一样。首先复制宿主机的DNS解析配置否则chroot内无法解析域名sudo cp /etc/resolv.conf /mnt/rootfs/etc/然后挂载必要的虚拟文件系统这些是Linux运行时必需的sudo mount -t proc /proc /mnt/rootfs/proc sudo mount -t sysfs /sys /mnt/rootfs/sys sudo mount -o bind /dev /mnt/rootfs/dev sudo mount -o bind /dev/pts /mnt/rootfs/dev/pts现在使用chroot进入目标根文件系统sudo chroot /mnt/rootfs /bin/bash你现在看到的命令行提示符应该变了表示你已经在“目标系统”内了。首先更新软件源列表。由于Ubuntu 16.04已过主流支持期需要将源替换为旧的存档源old-releases.ubuntu.comcat /etc/apt/sources.list EOF deb http://old-releases.ubuntu.com/ubuntu/ xenial main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ xenial-updates main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ xenial-security main restricted universe multiverse EOF更新软件包列表apt-get update5.3 安装必要软件包与配置系统在chroot环境中安装一些系统运行和开发必备的软件包apt-get install -y sudo ssh net-tools ethtool ifupdown iputils-ping \ vim-tiny bash-completion htop systemd-sysv \ wpasupplicant wireless-tools \ python3 python3-pipsystemd-sysv确保systemd作为init系统并安装sysvinit-utils的兼容性符号链接。网络工具net-tools,ethtool,ifupdown用于网络配置。ssh服务器允许远程登录。wpasupplicant和wireless-tools为Wi-Fi做准备如果硬件支持。设置root密码和创建用户passwd root # 设置root用户的密码 adduser ubuntu # 创建一个普通用户如ubuntu并设置密码 usermod -aG sudo ubuntu # 将ubuntu用户加入sudo组配置网络编辑/etc/network/interfaces配置一个静态IP或DHCP。例如为以太网口eth0配置DHCPauto eth0 iface eth0 inet dhcp配置主机名和时区echo my-imx6ull /etc/hostname ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime配置串口控制台为了让内核启动信息通过串口输出并提供一个登录终端需要编辑/etc/systemd/system/getty.target.wants/gettytty1.service的符号链接但更简单的方法是创建一个systemd的覆盖或直接使用systemctl启用串口tty服务。对于ttyS0通常是UART1可以ln -s /lib/systemd/system/serial-getty.service /etc/systemd/system/getty.target.wants/serial-gettyttymxc0.service注意i.MX6ULL的串口设备名可能是ttymxc0、ttymxc1等需要根据实际硬件连接的内核设备节点来定。可以在后续内核启动后通过ls /dev/tty*查看。完成所有配置后退出chroot环境并卸载挂载点exit # 退出chroot sudo umount /mnt/rootfs/dev/pts sudo umount /mnt/rootfs/dev sudo umount /mnt/rootfs/sys sudo umount /mnt/rootfs/proc sudo umount /mnt/rootfs6. 整合启动文件与配置U-Boot启动参数现在SD卡的两个分区分别准备好了第一个FAT分区放内核和设备树第二个EXT4分区是完整的根文件系统。6.1 复制内核与设备树到启动分区挂载SD卡的第一个分区FAT分区sudo mount /dev/sdb1 /mnt/boot将编译好的内核镜像和设备树文件复制过来sudo cp /path/to/linux-imx/arch/arm/boot/zImage /mnt/boot/ sudo cp /path/to/linux-imx/arch/arm/boot/dts/imx6ull-your-board.dtb /mnt/boot/请将imx6ull-your-board.dtb替换为你实际使用的设备树文件名。6.2 配置U-Boot环境变量BootargsU-Boot启动时会读取环境变量其中bootargs是传递给Linux内核的命令行参数至关重要。我们需要在U-Boot命令行中设置它。将SD卡插入开发板上电在串口终端U-Boot倒计时阶段按任意键进入U-Boot命令行。设置bootargs告诉内核根文件系统在哪里、使用什么控制台。假设根文件系统在SD卡的第二个分区mmc 0:2串口设备是ttymxc0setenv bootargs consolettymxc0,115200 root/dev/mmcblk0p2 rootwait rwconsolettymxc0,115200指定控制台为第一个串口波特率115200。root/dev/mmcblk0p2指定根文件系统设备。mmcblk0是第一个MMC/SD设备p2是第二个分区。rootwait等待根设备就绪后再挂载。rw以读写方式挂载根文件系统。然后设置从SD卡加载内核和设备树并启动的命令setenv loadimage fatload mmc 0:1 ${loadaddr} zImage setenv loadfdt fatload mmc 0:1 ${fdt_addr} imx6ull-your-board.dtb setenv bootcmd run loadimage; run loadfdt; bootz ${loadaddr} - ${fdt_addr}loadimage从SD卡第一个分区mmc 0:1加载zImage到内存地址${loadaddr}U-Boot预定义的环境变量。loadfdt加载设备树文件到内存地址${fdt_addr}。bootcmdU-Boot自动执行的命令依次运行加载内核、加载设备树然后用bootz命令启动内核。bootz ${loadaddr} - ${fdt_addr}表示从${loadaddr}启动内核没有初始RAM磁盘-设备树在${fdt_addr}。最后保存环境变量到SD卡这样下次启动就不需要重新设置了saveenv现在输入boot或直接重启U-Boot就会自动执行bootcmd加载并启动Linux内核。7. 系统启动、调试与常见问题排查激动人心的时刻到了。执行boot命令后你应该会看到内核解压、启动并最终尝试挂载根文件系统。如果一切顺利你将看到Ubuntu的登录提示符。7.1 启动过程观察与问题定位启动过程可能不会一帆风顺。串口终端是你的“眼睛”所有内核和系统日志都输出在这里。重点关注几个阶段U-Boot阶段确认bootargs和bootcmd是否正确打印确认zImage和.dtb文件是否成功加载没有Unable to read file之类的错误。内核解压与启动看到“Uncompressing Linux... done, booting the kernel.”表示内核加载成功。随后是内核初始化信息。设备树与驱动加载观察是否有你的关键硬件如网卡fec、MMC被成功探测到。如果有failed、error或not found很可能是设备树配置或内核驱动问题。根文件系统挂载这是最容易出问题的地方。如果看到“VFS: Unable to mount root fs”或“Please append a correct “root” boot option”说明内核找不到或无法识别根文件系统所在的分区/设备。7.2 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案U-Boot无法加载zImage或.dtb1. SD卡FAT分区文件不存在或路径错误。2.loadaddr或fdt_addr内存地址冲突或不正确。3. SD卡驱动或硬件问题。1. 在U-Boot中用fatls mmc 0:1命令列出文件确认。2. 检查U-Boot环境变量loadaddr通常为0x80800000和fdt_addr如0x83000000确保它们不重叠且在可用RAM内。3. 尝试更换SD卡或读卡器。内核启动后卡住或无输出1. 控制台console参数设置错误。2. 内核或设备树与硬件不匹配如DRAM配置错误。3. 时钟或电源管理初始化失败。1. 确认bootargs中console后的设备名如ttymxc0与硬件连接的串口及内核驱动匹配。2. 检查设备树中的memory节点大小是否正确。尝试使用更简单的设备树或评估板配置。3. 查看内核源码中对应板级的早期调试信息可能需要调整设备树中的时钟相关节点。无法挂载根文件系统 (root/dev/mmcblk0p2)1. 根设备指定错误分区号不对。2. 内核缺少对应文件系统驱动如EXT4。3. 根文件系统镜像损坏或制作有误。1. 在U-Boot中尝试mmc dev 0; mmc part查看SD卡分区表确认根分区号。可尝试root/dev/mmcblk0p1等。2. 在内核menuconfig中确认File systems-The Extended 4 (ext4) filesystem已编译进内核*。3. 重新制作根文件系统确保chroot内安装的软件包完整并使用fsck检查EXT4分区。网络接口eth0不存在或不工作1. 设备树中网络fec节点配置错误PHY地址、引脚复用。2. 内核网络驱动未编译或未加载。3. PHY芯片电源或复位电路问题。1. 检查设备树中fec1或fec2节点的phy-mode、phy-handle以及引用的mdio总线下的PHY子节点。使用ifconfig -a查看所有网络接口。2. 确认内核配置中Device Drivers-Network device support-Ethernet driver support-FEC Ethernet controller已启用。3. 测量硬件PHY芯片的供电和复位信号。系统启动后时间不对或apt-get update失败1. 系统时区未正确设置。2. 网络未连通无法访问软件源。3. Ubuntu 16.04官方源已停止维护。1. 在chroot环境中正确设置时区并安装ntp或systemd-timesyncd。2. 用ping测试网络连通性配置正确的DNS/etc/resolv.conf。3.必须将/etc/apt/sources.list中的源替换为old-releases.ubuntu.com如上文所述。7.3 系统初步优化与验证成功登录后可以进行一些初步验证和优化验证网络ping -c 4 8.8.8.8。更新系统sudo apt-get update sudo apt-get upgrade。安装常用工具如git,curl,build-essential用于本地编译测试。配置SSH修改/etc/ssh/sshd_config允许root登录如需然后sudo systemctl restart ssh。这样你就可以通过网络SSH登录摆脱串口线了。检查服务systemctl list-units --typeservice --staterunning查看运行中的服务禁用不需要的服务以节省资源。8. 进阶定制与性能考量系统跑起来只是第一步。要让其成为一个可靠的产品基础还需要进行一些进阶定制。8.1 构建自定义内核模块与驱动有时你需要使用第三方或自己开发的硬件需要编译内核模块。由于我们使用的是自定义编译的内核任何内核模块都必须用完全相同版本和配置的内核源码来编译。在宿主机上进入Linux内核源码目录先确保环境变量指向正确的交叉编译工具链和内核路径KERNEL_SRCexport ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- export KERNEL_SRC/path/to/your/linux-imx然后在你的内核模块源码目录中使用对应的Makefile进行编译。编译出的.ko文件需要复制到目标板的/lib/modules/$(uname -r)/目录下并运行depmod和modprobe。8.2 系统裁剪与最小化ubuntu-base虽然精简但仍有进一步裁剪的空间特别是对于存储空间极其有限的场景。删除不需要的软件包在目标板上使用apt-get purge删除如man-db,info,python3-doc等文档包以及不用的语言包locale。清理APT缓存sudo apt-get clean。分析空间占用使用ncdu命令扫描目录找出占用空间大的非必要文件。使用更小的Init系统对于极度资源受限的场景可以考虑用busybox init替代systemd但这会牺牲很多便利性需要手动配置启动脚本。8.3 提升启动速度嵌入式设备往往对启动速度有要求。优化点包括内核裁剪在make menuconfig中移除所有不必要的驱动、文件系统、调试支持和内核特性。一个更小的内核加载更快。禁用内核模块尽量将驱动编译进内核*而不是模块M避免加载模块的时间开销和initramfs的需求。优化bootargs移除rootwait如果存储设备稳定但需谨慎。可以添加quiet和logo.nologo参数来减少控制台输出加快启动视觉感受。优化systemd使用systemd-analyze blame分析启动过程各服务耗时禁用不必要的服务如apt-daily-upgrade.timer,systemd-timesyncd如果不需要。使用initramfs可选对于复杂的存储或加密场景initramfs是必要的但它会增加启动时间。如果根文件系统直接在标准块设备上可以不用initramfs。8.4 创建可重复构建的脚本手动执行上述所有步骤容易出错且效率低下。建议将整个过程脚本化。可以编写一个Bash脚本自动化完成下载源码和文件系统。配置和编译U-Boot、内核。格式化SD卡并分区。制作根文件系统包括chroot环境下的软件包安装和配置。复制文件到对应分区。这样每次需要更新或为不同硬件变体构建系统时只需运行脚本即可确保了构建环境的一致性和可重复性。这也是迈向持续集成/持续部署CI/CD的第一步。移植工作到这里就基本完成了。从一个裸板到一个可以运行apt-get、能连接网络、有完整包管理功能的Ubuntu系统这个过程充满了挑战但也极大地加深了对嵌入式Linux底层运作机制的理解。最重要的是你现在拥有了一个极其灵活的开发平台可以像在PC上一样快速部署各种服务器应用、开发复杂的应用程序充分发挥i.MX6ULL这颗处理器的潜力。