1. 项目概述为什么选择Ubuntu Core 16.04与i.MX6ULL的组合在嵌入式开发领域为一块特定的开发板构建一个稳定、高效且可维护的根文件系统是项目从原型走向产品化的关键一步。我手头这块基于NXP i.MX6ULL处理器的开发板性能适中、功耗优秀非常适合物联网网关、工业HMI等场景。而选择Ubuntu Core 16.04更准确地说是其最小化版本ubuntu-base作为移植目标背后有几点核心考量。首先i.MX6ULL是一颗典型的ARM Cortex-A7内核处理器它完全有能力运行一个完整的Linux发行版而不仅仅是使用BusyBox构建的极简系统。使用ubuntu-base意味着我们直接站在了Ubuntu这个庞大软件生态的肩膀上。apt-get仓库里海量的软件包能让我们在开发后期轻松集成各种功能从Python环境到数据库服务省去了大量交叉编译的麻烦。其次Ubuntu Core 16.04是一个长期支持版本其内核和基础库经过长期测试稳定性有保障这对于工业产品至关重要。最后这个过程本身极具学习价值它能让你透彻理解Linux系统从Bootloader到用户空间的完整启动链条以及根文件系统的核心组成。这次移植的目标很明确在一台x86_64的Ubuntu主机上为ARM架构的i.MX6ULL开发板从头构建一个可启动、具备基本命令行功能的Ubuntu根文件系统。整个过程涉及工具链准备、根文件系统定制、内核适配、Bootloader配置等多个环节。无论你是嵌入式Linux的新手还是想深化对系统构建理解的老手跟着走一遍收获都会远超仅仅烧录一个现成的镜像。2. 开发环境搭建与核心工具链解析工欲善其事必先利其器。为ARM板卡构建系统我们主要的工作都在一台高性能的x86_64 Linux主机上完成这台主机我们称之为“构建主机”。整个环境搭建围绕“交叉编译”这个核心概念展开。2.1 构建主机的系统准备我强烈建议使用Ubuntu 18.04或20.04 LTS版本的物理机或虚拟机作为构建主机。一方面其软件源较新能提供所需的工具另一方面其作为Debian系发行版与我们要构建的目标系统同源能减少很多兼容性麻烦。首先更新软件源并安装一些基础工具sudo apt-get update sudo apt-get install -y vim git make gcc g bc u-boot-tools device-tree-compiler \ flex bison libssl-dev libncurses-dev gawk wget cpio python unzip rsync这些工具里u-boot-tools包含了制作U-Boot镜像的工具mkimagedevice-tree-compiler用于编译设备树源文件.dts为二进制.dtbbc用于内核配置时的数学计算其他则是经典的开发工具。2.2 交叉编译工具链的选择与安装这是最关键的一步。交叉编译工具链是一套运行在x86主机上但能生成ARM架构可执行代码的编译器、链接器和库。对于i.MX6ULLARMv7-A架构我们通常选择arm-linux-gnueabihf-前缀的工具链。hf代表硬浮点i.MX6ULL的Cortex-A7支持VFPv4浮点单元使用硬浮点工具链能大幅提升浮点运算性能。有两种主流获取方式使用Linaro官方工具链稳定且兼容性好。我们可以下载Linaro GCC 7.5版本。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 sudo tar -xJf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/解压后将工具链路径加入系统环境变量echo export PATH/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH ~/.bashrc source ~/.bashrc执行arm-linux-gnueabihf-gcc -v验证安装应能看到gcc版本信息和目标平台为arm-linux-gnueabihf。使用芯片厂商提供的工具链NXP官方也提供针对其芯片优化过的工具链通常在Yocto Project或MCUXpresso SDK包中。其与自家BSP板级支持包的兼容性可能更好。注意工具链的版本需要与目标系统内核和库的版本大致匹配。太新的工具链编译旧内核可能会报错太旧的工具链可能不支持某些C语言特性。对于Ubuntu Core 16.04对应Linux内核4.x时代GCC 7.x是一个稳妥的选择。2.3 获取Ubuntu Base根文件系统Ubuntu Base是一个最小的根文件系统只包含最基本的命令行工具和apt包管理器。我们从Ubuntu官方镜像站获取针对ARM架构的16.04版本mkdir ~/imx6ull-ubuntu cd ~/imx6ull-ubuntu wget http://cdimage.ubuntu.com/ubuntu-base/releases/16.04/release/ubuntu-base-16.04.6-base-armhf.tar.gz这里选择armhfARM hard float版本正是为了匹配我们的硬浮点工具链和处理器。下载的tar包就是我们构建系统的“种子”。3. 构建根文件系统从骨架到可运行有了基础骨架我们需要将其填充、配置使其能在一个真实的硬件上启动并运行。3.1 创建并解压基础根文件系统我们首先创建一个目录作为根文件系统的挂载点并解压ubuntu-basesudo rm -rf rootfs # 清理旧目录 mkdir rootfs sudo tar -xzf ubuntu-base-16.04.6-base-armhf.tar.gz -C rootfs解压后rootfs目录里就是最基础的Linux目录结构/bin,/etc,/lib,/usr等。3.2 配置网络与APT源为了让目标系统能联网安装软件我们需要在chroot环境下进行配置。但首先我们需要在主机环境下为目标系统准备好/etc/apt/sources.list文件。由于目标系统是ARM架构我们必须使用ARM的软件源。# 进入rootfs目录创建apt源配置文件 cd rootfs sudo cat etc/apt/sources.list EOF deb http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe multiverse deb http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe multiverse deb http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse EOF注意这里的ubuntu-ports和xenial16.04的代号。ports仓库专门用于非x86架构。3.3 准备Chroot环境与安装核心软件ChrootChange Root能让我们将当前进程的根目录切换到rootfs下仿佛我们“进入”了目标系统进行操作。但ARM的可执行文件无法在x86主机上直接运行我们需要借助qemu-user-static这个“翻译器”。# 在主机上安装qemu-user-static sudo apt-get install -y qemu-user-static # 将qemu-arm-static拷贝到目标根文件系统 sudo cp /usr/bin/qemu-arm-static rootfs/usr/bin/ # 拷贝主机DNS配置保证chroot内能解析域名 sudo cp /etc/resolv.conf rootfs/etc/接下来挂载必要的虚拟文件系统并chroot# 挂载proc, sys, dev等系统文件系统 sudo mount -t proc /proc rootfs/proc sudo mount -t sysfs /sys rootfs/sys sudo mount -o bind /dev rootfs/dev sudo mount -o bind /dev/pts rootfs/dev/pts # 使用chroot进入目标系统环境 sudo chroot rootfs /bin/bash此时命令行提示符会变化你已经在“目标系统”内部了。在chroot环境下我们首先更新软件包列表并安装一些最核心的软件和工具# 更新apt缓存此时网络应通畅 apt-get update # 安装基础系统、网络和常用工具 apt-get install -y language-pack-en-base sudo net-tools ethtool \ iputils-ping ifupdown network-manager vim-tiny bash-completion \ udev ssh rsync # 安装系统启动和管理工具 apt-get install -y systemd-sysv sysvinit-utils安装systemd-sysv是为了提供systemd作为init系统并移除可能存在的sysvinit冲突。sysvinit-utils提供了一些基本的启动脚本工具。3.4 配置系统关键信息仍在chroot环境下进行以下关键配置设置root密码passwd root输入两遍新密码。创建新用户可选但推荐adduser ubuntu # 将用户加入sudo组 usermod -aG sudo ubuntu配置主机名echo imx6ull-ubuntu /etc/hostname配置网络静态IP示例编辑/etc/network/interfaces为有线网卡eth0配置静态IP。cat /etc/network/interfaces EOF auto lo iface lo inet loopback auto eth0 iface eth0 inet static address 192.168.1.100 netmask 255.255.255.0 gateway 192.168.1.1 EOF配置串口控制台为了让内核启动信息通过串口输出并允许通过串口登录需要编辑/etc/systemd/system/getty.target.wants/gettytty1.service的符号链接指向。更简单的方法是修改inittab如果使用sysvinit或为systemd创建串口服务。对于systemd一个可靠的方法是# 启用串口ttyAMA0具体设备名需根据内核实际注册名调整可能是ttymxc0等的getty服务 ln -s /lib/systemd/system/serial-getty.service /etc/systemd/system/getty.target.wants/serial-gettyttymxc0.service实操心得i.MX6ULL的串口设备名取决于内核配置。最准确的方法是在后续编译好内核并启动后通过ls /dev/tty*查看。这里先留个伏笔我们可以在制作最终镜像前再确认并创建这个链接。完成所有配置后退出chroot环境并卸载挂载点exit # 退出chroot sudo umount rootfs/dev/pts sudo umount rootfs/dev sudo umount rootfs/sys sudo umount rootfs/proc至此一个具备基本功能的根文件系统就准备好了。4. 内核与U-Boot的配置与编译根文件系统需要内核来驱动硬件需要Bootloader来加载内核。我们需要为i.MX6ULL定制编译这两者。4.1 获取并配置Linux内核源码NXP官方维护了其i.MX系列芯片的内核源码。我们可以从GitHub获取稳定分支。cd ~/imx6ull-ubuntu git clone https://github.com/Freescale/linux-fslc.git -b 5.10-2.1.x-imx linux-imx cd linux-imx这里选择5.10-2.1.x-imx分支这是一个相对较新且稳定的LTS内核版本对i.MX6ULL支持完善。在编译前我们需要一个基础的配置文件。通常开发板供应商会提供默认配置。# 假设你的开发板配置文件名为imx6ull_xxx_defconfig请替换为实际名称 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- imx_v7_defconfigimx_v7_defconfig是NXP为i.MX6/7系列提供的通用配置起点。执行后会在源码根目录生成.config文件。接下来我们可以根据需要进行内核菜单配置make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig在图形界面中确保以下关键选项被启用[*]或MDevice Drivers-Network device support-Ethernet driver support- 找到你开发板所用的PHY驱动如Microchip LAN8720/LAN8740等。Device Drivers-Input device support-Touchscreens- 如果你的板子有触摸屏启用对应驱动。File systems- 确保你根文件系统所用的格式被支持如Second extended fs support (ext2/ext3)The Extended 4 (ext4) filesystem。对于嵌入式系统ext4是性能与可靠性的良好平衡。确保Kernel Features-Use the ARM EABI to compile the kernel和Allow old ABI binaries to run with this kernel被选中以保持用户空间兼容性。配置完成后保存退出。4.2 编译内核与设备树开始编译内核镜像和模块# 编译内核镜像zImage和设备树 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- zImage dtbs -j$(nproc) # 编译内核模块并安装到我们之前构建的rootfs中 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules -j$(nproc) sudo make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- INSTALL_MOD_PATH~/imx6ull-ubuntu/rootfs modules_install编译完成后在arch/arm/boot/目录下得到zImage内核镜像在arch/arm/boot/dts/目录下得到对应的设备树二进制文件.dtb如imx6ull-14x14-evk.dtb。设备树文件非常重要它描述了板级的硬件信息内存、外设等内核依赖它来正确初始化硬件。4.3 获取并编译U-BootU-Boot是广泛使用的Bootloader。同样从NXP官方仓库获取cd ~/imx6ull-ubuntu git clone https://github.com/Freescale/u-boot-fslc.git -b 2021.04fslc u-boot-imx cd u-boot-imx配置U-Boot需要找到与你开发板匹配的配置文件通常以_defconfig结尾。# 例如对于常见的i.MX6ULL EVK板 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- mx6ull_14x14_evk_defconfig # 然后进行编译 make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc)编译完成后在根目录下会生成u-boot-dtb.imx或u-boot.imx文件这就是我们需要烧写到SD卡或eMMC启动设备最前端的Bootloader镜像。这个文件已经包含了i.MX6ULL所需的IVTImage Vector Table等头部信息。5. 制作可启动的SD卡镜像我们将所有组件打包成一个完整的SD卡镜像文件方便烧录和测试。5.1 创建空白镜像文件并分区使用dd命令创建一个大小合适的空镜像文件例如1GBcd ~/imx6ull-ubuntu dd if/dev/zero ofimx6ull-ubuntu.img bs1M count1024接下来使用fdisk或parted工具对这个镜像文件进行分区。通常我们需要两个分区FAT32分区约50-100MB用于存放U-Boot、内核镜像zImage、设备树文件.dtb。i.MX6ULL的ROM Code支持从FAT分区加载。EXT4分区剩余所有空间用于存放我们构建的根文件系统。sudo fdisk imx6ull-ubuntu.img在fdisk交互界面中依次输入o(创建新的DOS分区表)n(新建分区),p(主分区),1(分区号), 回车 (起始扇区默认),64M(大小64MB)t(修改分区类型),c(设置为W95 FAT32 (LBA))n(新建分区),p(主分区),2(分区号), 回车 (起始扇区默认), 回车 (使用所有剩余空间)w(写入并退出)现在将镜像文件关联到本地回环设备并格式化分区# 关联镜像文件到回环设备 sudo losetup -P /dev/loop0 imx6ull-ubuntu.img # 此时应出现/dev/loop0p1和/dev/loop0p2 # 格式化第一个分区为FAT32 sudo mkfs.vfat /dev/loop0p1 # 格式化第二个分区为EXT4 sudo mkfs.ext4 /dev/loop0p25.2 安装BootloaderU-Boot将编译好的U-Boot镜像写入SD卡镜像的头部而非某个分区。这步很关键# 假设u-boot-dtb.imx路径为~/imx6ull-ubuntu/u-boot-imx/u-boot-dtb.imx sudo dd if~/imx6ull-ubuntu/u-boot-imx/u-boot-dtb.imx of/dev/loop0 bs1k seek1 convfsyncseek1表示跳过前1KB的块。这是因为i.MX6ULL的ROM Code要求Bootloader镜像从存储设备的第1KB偏移处开始。5.3 复制内核与根文件系统挂载两个分区并将相应文件复制进去# 创建挂载点 mkdir -p sdcard_boot sdcard_root # 挂载分区 sudo mount /dev/loop0p1 sdcard_boot sudo mount /dev/loop0p2 sdcard_root # 复制内核和设备树到FAT分区 sudo cp ~/imx6ull-ubuntu/linux-imx/arch/arm/boot/zImage sdcard_boot/ sudo cp ~/imx6ull-ubuntu/linux-imx/arch/arm/boot/dts/imx6ull-14x14-evk.dtb sdcard_boot/ # 请替换为你的dtb文件 # 复制整个根文件系统到EXT4分区 sudo cp -a ~/imx6ull-ubuntu/rootfs/* sdcard_root/ # 同步并卸载 sync sudo umount sdcard_boot sdcard_root sudo losetup -d /dev/loop05.4 配置U-Boot启动参数最后我们需要告诉U-Boot去哪里加载内核和根文件系统。这通过U-Boot的环境变量bootargs和bootcmd来设置。我们可以创建一个boot.scr脚本文件。首先创建一个文本文件boot.cmdcat boot.cmd EOF setenv bootargs consolettymxc0,115200 root/dev/mmcblk1p2 rootwait rw load mmc 1:1 ${loadaddr} zImage load mmc 1:1 ${fdt_addr} imx6ull-14x14-evk.dtb bootz ${loadaddr} - ${fdt_addr} EOFconsolettymxc0,115200: 指定内核控制台为i.MX6ULL的第一个串口波特率115200。这是关键ttymxc0是内核中该串口的设备名必须与之前根文件系统中配置的getty服务名匹配。root/dev/mmcblk1p2: 指定根文件系统在SD卡的第二个分区。mmcblk1通常代表SD卡mmcblk0可能是eMMCp2是第二个分区。rootwait: 等待根设备就绪。load mmc 1:1 ...: 从MMC设备1SD卡的第1个分区加载zImage和.dtb文件到内存地址${loadaddr}和${fdt_addr}。bootz: 启动zImage格式的内核。使用U-Boot工具mkimage将这个脚本编译成U-Boot可识别的格式# 确保mkimage命令可用来自u-boot-tools包 mkimage -A arm -T script -C none -n Boot Script -d boot.cmd boot.scr将生成的boot.scr文件复制到SD卡镜像的FAT分区sudo mount /dev/loop0p1 sdcard_boot sudo cp boot.scr sdcard_boot/ sudo umount sdcard_boot sudo losetup -d /dev/loop0至此一个完整的可启动SD卡镜像imx6ull-ubuntu.img就制作完成了。你可以使用dd命令将其烧录到物理SD卡sudo dd ifimx6ull-ubuntu.img of/dev/sdX bs1M statusprogress其中/dev/sdX是你的SD卡设备。6. 上电启动、调试与问题排查实录将烧录好的SD卡插入i.MX6ULL开发板连接串口线到电脑使用串口终端工具如minicom,picocom,screen或Windows下的Putty、MobaXterm打开对应串口如/dev/ttyUSB0波特率设为115200 8N1。上电后你应该能在终端看到U-Boot和内核的启动日志。6.1 典型启动流程与日志分析U-Boot阶段你会看到U-Boot的版本信息、CPU检测、DRAM初始化、MMC/SD卡初始化等日志。如果环境变量bootcmd设置正确它会自动执行加载zImage和.dtb并跳转到内核。内核解压与启动内核开始解压Uncompressing Linux...然后初始化CPU、内存、设备树并打印大量硬件初始化信息。关键看以下几点Machine model: Freescale i.MX6 ULL 14x14 EVK Board表明设备树被正确识别。console [ttymxc0] enabled表明串口控制台已启用。mmc0: SDHCI controller on 2190000.usdhc [2190000.usdhc]等表明SD卡控制器初始化成功。EXT4-fs (mmcblk1p2): mounted filesystem表明根文件系统被成功挂载。用户空间启动内核最后会尝试执行根文件系统中的/sbin/init通常是systemd。你会看到systemd启动各个服务的日志最终出现登录提示符imx6ull-ubuntu login:。6.2 常见问题与排查技巧在实际操作中你几乎一定会遇到问题。以下是我踩过坑后总结的排查清单问题现象可能原因排查思路与解决方案U-Boot无法启动无输出1. SD卡烧录不正确。2. U-Boot镜像与板子不匹配。3. 启动拨码开关设置错误。1. 用sudo dd if/dev/sdX bs1M count1U-Boot启动后卡住不加载内核1.boot.scr脚本未找到或语法错误。2.zImage或.dtb文件不在FAT分区根目录或文件名不对。3.bootargs环境变量中根设备(root)指定错误。1. 在U-Boot命令行启动时按任意键中断手动输入fatls mmc 1:1查看FAT分区文件。2. 手动执行加载和启动命令load mmc 1:1 ${loadaddr} zImage; load mmc 1:1 ${fdt_addr} your.dtb; bootz ${loadaddr} - ${fdt_addr}。3. 检查内核启动日志看是否在等待根设备。尝试在bootargs中增加rootdelay2。内核panic无法挂载根文件系统1. 根文件系统分区格式不对内核未编译对应文件系统驱动。2.root参数指定的设备节点不存在。3. 根文件系统损坏或不完整。1. 在内核menuconfig中确认已启用EXT4支持或你使用的文件系统。2. 在内核启动早期日志中查看mmc设备枚举情况确认mmcblk1p2是否存在。3. 将SD卡EXT4分区挂载到主机检查或尝试重新复制根文件系统。内核启动后串口无登录提示符1. 串口设备名console参数错误。2. 根文件系统中未启用串口登录服务getty。3. 根文件系统/sbin/init损坏或缺失。1.这是最常见的问题。在U-Boot的bootargs中尝试不同的串口名如ttyAMA0,ttymxc1等。最准确的方法是查看内核日志中console [xxxx] enabled的xxxx是什么。2. 检查根文件系统/etc/systemd/system/getty.target.wants/目录下是否有指向串口的服务链接。3. 检查/sbin/init文件是否存在且可执行。网络无法连接1. 内核未编译对应的网卡驱动。2. 设备树中网络PHY配置错误。3. 根文件系统中网络配置错误。1. 内核日志中查看是否有eth0或类似网络接口被识别。2. 检查设备树源文件中关于fec1i.MX6ULL以太网控制器和PHY如phy-reset-gpios的配置是否与你的板子硬件匹配。3. 在系统启动后使用ifconfig -a查看所有接口手动配置IP测试。独家避坑技巧串口登录问题终极排查法。如果内核正常启动但无登录提示可以尝试在bootargs中加入init/bin/bash。这会让内核直接启动一个bash shell而不是init系统。如果此时串口出现shell提示符说明串口驱动和基础控制台是通的问题100%出在根文件系统的init或getty配置上。然后你就可以在这个最小shell里检查/etc/inittab或systemd的getty服务了。7. 系统优化与功能扩展系统成功启动并登录后这只是一个开始。一个产品化的系统还需要进行诸多优化。7.1 基础系统优化时区与语言设置在chroot环境或启动后的系统中设置时区。echo Asia/Shanghai /etc/timezone dpkg-reconfigure -f noninteractive tzdata安装常用工具根据你的应用场景安装必要的软件包如iperf网络测试、htop进程监控、i2c-tools、spi-tools、python3等。禁用不必要的服务为了加快启动速度和减少资源占用禁用一些桌面或服务器环境下才需要的服务。systemctl disable apt-daily-upgrade.timer systemctl disable apt-daily.timer # 根据实际情况可能还有bluetooth, avahi-daemon等7.2 构建自定义应用与开机自启动将你的应用程序交叉编译后放入根文件系统。对于C/C程序使用交叉编译工具链对于Python/Shell脚本直接拷贝即可。配置开机自启动对于systemd系统创建一个服务文件是最规范的方式。例如为你的应用myapp创建服务sudo vim /etc/systemd/system/myapp.service内容如下[Unit] DescriptionMy Custom Application Afternetwork.target [Service] Typesimple ExecStart/usr/local/bin/myapp Restarton-failure Userroot [Install] WantedBymulti-user.target然后启用它sudo systemctl enable myapp.service。7.3 创建精简的发行版镜像对于产品发布你可能需要创建一个包含所有定制内容的、更精简的镜像。可以基于当前运行良好的SD卡内容使用dd命令备份整个SD卡或EXT4分区并进行压缩。更进一步可以使用像buildroot或Yocto Project这样的专业嵌入式构建框架来从头构建一个高度定制化、可重复构建的完整系统镜像但这需要更深入的学习。整个移植过程从工具链准备到镜像制作再到问题排查是一个系统工程。它要求你对Linux的引导流程、文件系统、内核驱动和交叉编译有连贯的理解。成功启动的那一刻意味着你完全掌控了从硬件上电到软件运行的全链条这种成就感是单纯使用现成系统无法比拟的。后续的优化和定制则是让这个系统真正为你所用的开始。