SMARC与i.MX8X的嵌入式Linux开发实战:从选型到量产
做嵌入式Linux开发这些年接触过不少核心板形态从PC/104到Qseven再到COM Express每种标准都有自己的定位。但真正让我觉得“省心”的还是SMARC。去年我们在一个工业边缘网关项目里要选型主控最终敲定了SMARC模块加i.MX8X的组合整块板子跑Linux从评估到量产大概花了四个多月。这篇文章我把整个过程中的选型思路、Linux适配细节、实操步骤和踩过的坑整理出来希望能给正在评估SMARC模块或者NXP i.MX8X平台的朋友一些参考。先说结论如果你的项目需要低功耗、宽温、长供货周期同时外设接口又比较丰富USB、PCIe、双千兆网口、显示输出那SMARC i.MX8X Linux这套组合是非常能打的组合。它特别适合工业HMI、边缘计算网关、医疗设备、轨道交通等场景。这篇文章适合嵌入式软件工程师、硬件选型人员、以及对NXP平台感兴趣但还没上手的Linux开发者。1. 项目整体设计与选型思路拆解1.1 为什么是SMARC而不是Qseven或COM ExpressSMARCSmart Mobility ARChitecture是SGET组织定义的一套计算机模块标准它的核心设计目标就是低功耗、小尺寸、无风扇。我们当时对比过Qseven和COM Express Mini最后选SMARC的关键点有这么几个尺寸更小SMARC短版82mm x 50mm比Qseven70mm x 70mm在长边方向虽然长一些但整体面积差不多比COM Express Mini95mm x 95mm小很多非常适合紧凑型外壳。接口定义更适合低功耗平台SMARC的载板连接器把LCD、LVDS、eDP、HDMI、USB、PCIe、GbE、I2C、SPI、UART、SDIO、SATA等接口都做了标准化分配而且信号定义充分考虑到了平板类、手持类设备的走线方式。散热设计灵活SMARC模块的功耗通常都在2W到10W之间i.MX8X这颗芯片的典型功耗很低配合SMARC板卡的被动散热片就能搞定完全不需要风扇。当然SMARC也有它的局限性载板连接器是0.5mm间距的高速板对板连接器焊接和维修难度比Qseven的MXM连接器要高一些。但做产品不是搞DIY连接器可靠性反而更重要。1.2 i.MX8X系列处理器低功耗与实时性兼得i.MX8X系列是NXP在工业级和汽车级市场的主力产品之一内部采用异构架构应用处理器用的是Arm Cortex-A35核心可以跑Linux这样的富操作系统同时还有一个或两个Cortex-M4F实时核心专门跑裸机代码或RTOS用来处理对时延敏感的任务比如工业协议栈、PWM控制、IO快速响应。我当时选的是i.MX8QuadXPlus4个Cortex-A35核心主频1.2GHz左右配一个Cortex-M4F。这个性能跑Linux做边缘采集、协议转换、轻量级推理完全够了。如果预算更敏感还有i.MX8DualX双核A35的版本可选BSP是同一套软件改动很小。这里要特别提一下i.MX8X的电源管理。它自带了一个内部的电源管理控制器PMU不需要外部PMIC芯片这在硬件设计上省了不少事。同时它的低功耗模式做得比较好配合Linux内核的cpuidle和cpufreq框架系统空闲时整板功耗可以压到很低。1.3 Linux发行版和BSP的选择逻辑NXP官方主推的是Yocto Project构建的Linux BSP。刚开始接触Yocto的人可能会觉得它又重又慢但说实话做产品级嵌入式LinuxYocto是当前最体系化的方案。它的好处有三个版本锁定所有软件包版本都由NXP和Yocto的release统一维护不需要自己手工交叉编译一堆依赖库。镜像定制灵活可以通过IMAGE_FEATURES、IMAGE_INSTALL这类变量精确控制rootfs里装什么出来的镜像很干净。集成度高NXP的BSP把U-Boot、内核、firmware、GPU驱动、VPU编解码库都集成到了Yocto的layer体系里一条命令就能产出一个完整烧录包。如果你只是做原型验证不想折腾Yocto也可以直接用NXP的imx-mkimage工具配合主线内核编译或者直接在NXP官网下载预编译的image。我个人的建议是原型阶段随便搞能跑起来就行从产品立项开始一定要切到Yocto。注意i.MX8X系列的主流BSP内核版本是5.4和5.10LTS这两个版本在工业设备里用得最多。不建议用太新的内核版本做量产因为NXP的BSP验证周期往往滞后于内核主线。2. 核心细节解析与Linux适配要点2.1 拿到模块之后软件适配从哪里开始SMARC模块的特点是“模块是通用的载板是个性化的”。i.MX8X模块厂商一般会提供一个通用载板的BSP你拿到之后第一件事就是把BSP的**设备树Device Tree**改成自己载板对应的配置。设备树是Linux下描述硬件的标准机制它告诉内核“这块板子有哪些外设、每个外设挂在哪个地址上、用哪个驱动”。SMARC标准已经把模块侧的信号定义固定了所以你主要改的是carrier board侧的内容。常见改动包括设置调试串口通常是UART0或UART1的pinctrl和时钟。配置网卡PHY芯片的地址、复位GPIO、中断GPIO。调整LCD时序、触摸屏的I2C地址和中断脚。根据实际的GPIO按键、LED灯定义调整gpio-leds、gpio-keys节点。我见过不少刚上手的人直接在原厂BSP上重新编译一遍就下载到板子上结果网卡不识别、串口不出log其实就是设备树没改。设备树是嵌入式Linux开发“入门到放弃”的一个坎但它并不难只要学会看芯片手册的管脚复用表IOMUX和参考设备树的写法很快就熟了。2.2 U-Boot启动流程与启动媒体规划U-Boot是这套系统的第一段引导程序它负责初始化DDR、配置时钟、加载内核镜像和设备树到内存最后跳转到内核。i.MX8X的启动流程比老i.MX6系列要复杂一些。它内部有个Boot ROM会根据启动引脚或者烧写在eFuse里的配置去读取启动镜像。i.MX8X的镜像格式不是简单的uImage而是用imx-mkimage工具打包生成的flash.bin里面包含了SCU固件系统控制单元固件、ATFARM可信固件、U-Boot等若干部分按固定偏移组合在一起。实际项目里我们用的启动媒体是eMMCSPI NOR作为备份。U-Boot环境变量里通过bootcmd去加载内核# 从eMMC启动 setenv loadaddr 0x83200000 setenv fdt_addr 0x83000000 setenv bootcmd mmc dev 1; mmc read ${loadaddr} 0x2000 0x8000; mmc read ${fdt_addr} 0x10000 0x1000; booti ${loadaddr} - ${fdt_addr} saveenvbooti用来启动64位ARM内核镜像Image格式不要再用老的bootz或bootm那套了。2.3 内核配置裁剪的几个原则内核这块除非你有特别强的定制需求否则我强烈建议你基于NXP BSP自带的imx_v8_defconfig来改。这个默认配置已经覆盖了i.MX8X全系列的外设直接编出来的内核虽然大一点大概12MB但功能和稳定性是最有保障的。做产品时你会想裁内核但要记住三个“不能动”SCU相关的驱动不能动i.MX8X的系统控制单元SCU是运行在M核心上的固件负责时钟、电源、引脚配置等资源管理。Linux内核通过mailbox和rpmsg机制和SCU通信如果误关了相关配置整个系统会出现各种莫名其妙的问题。电源管理相关不能动cpuidle、cpufreq、devfreq这些框架不要为了省一点空间而裁剪i.MX8X的低功耗特性依赖它们。CMA内存必须保留如果要用GPU、VPUCMA预留内存不够跑图形或视频编解码时会直接崩。还有一个容易被忽略的i.MX8X的GPU驱动GC7000L和VPU固件都是二进制闭源库不属于内核主线Yocto会帮你打包到/lib/firmware和用户库。如果你自己手动编内核记得把这些固件文件拷贝进rootfs不然etnaviv或者NXP的GPU驱动会加载失败。3. 实操过程从零构建可运行的Linux系统3.1 交叉编译环境搭建两条路线这里有两种路线我分开说。路线一Yocto推荐做产品用Yocto搭建一次环境比较费时间但它生成的是整套工具链和文件系统。基本步骤# 以NXP BSP为例拉取release分支 repo init -u https://source.codeaurora.org/external/imx/imx-manifest -b imx-linux-hardknott -m imx-5.10.9-2.2.0.xml repo sync # 设置编译环境 source setup-environment build # 针对i.MX8X系列选择对应machine # 如果模块厂商提供的BSP通常已经有对应的machine配置 MACHINEimx8qxpmek bitbake imx-image-fullimx-image-full是带图形界面的完整镜像编译时间很长我第一次编大概花了两三个小时。做产品时我在这个镜像基础上做了裁剪去掉了GStreamer插件、Qt等不需要的组件只保留busybox、网络工具和应用运行环境镜像体积从2GB降到300MB左右。路线二手动交叉编译原型验证用不想折腾Yocto的话直接装Linaro的aarch64交叉工具链# 在Ubuntu 20.04上安装 sudo apt-get install gcc-aarch64-linux-gnu build-essential # 下载NXP的kernel源码5.10分支编译i.MX8X默认配置 export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make imx_v8_defconfig make -j$(nproc) Image dtbs编译内核其实比想象中简单主要的坑都在rootfs制作上。3.2 根文件系统制作busybox还是Buildroot拿到Linux内核之后你还需要一套根文件系统才能启动。常见的方案有三种busybox手工搭建、Buildroot、Yocto。busybox手工搭建适合极简验证。流程是交叉编译busybox然后mkdir创建/dev、/proc、/sys、/etc等目录把编译好的busybox放进去再写好/etc/inittab。这套方案最轻量但补全依赖库的时候容易漏。Buildroot比busybox高级一些可以用menuconfig选中你需要的库和应用自动生成rootfs。它特别适合那种“不想要Yocto那么重但也不想纯手工搭”的场景。我个人的经验是Buildroot做出来的rootfs在生产环境里实际上很受用尤其是不跑Qt、不需要包管理器的场景。Yocto全功能、可扩展、更贴近工业级产品但要接受它的复杂度和构建时长。我当时做原型验证用了Buildroot跑起来之后确认外设都正常再转Yocto做正式镜像。如果项目时间紧直接用Yocto一步到位也行看个人时间安排。3.3 镜像打包与烧录imx-mkimage用法i.MX8X的启动镜像格式比较特殊你需要用imx-mkimage工具来打包。在Yocto环境下这个工具会自动执行。社区里也有不少人写了打包脚本核心几条命令如下# 以编译出来的flash.bin为例 make SOCiMX8QX flash_evk # 或者手动组合各部分镜像 ./mkimage_fit_atf.sh生成的flash.bin就是可以直接烧到eMMC/SD卡里的完整启动镜像。烧录时用官方推荐的UUU工具NXP的通用烧录工具最省事。把模块设置为下载模式通常通过载板上的拨码开关然后执行uuu flash.bin如果是SD卡启动也可以用dd直接烧写sudo dd ifflash.bin of/dev/sdX bs1M convfsync注意i.MX8X的刷写难度比想象中要低很多但有个细节要特别留意U-Boot环境变量和bootloader是存在同一份flash.bin里的重新烧flash.bin会覆盖环境变量。生产线上如果脚本处理不当很容易把已经调好的环境变量又覆盖回去。我们的做法是把环境变量相关配置固化在U-Boot头文件里保证每次烧录后环境变量都一致。3.4 首次启动照妖镜一样的串口console调试嵌入式Linux串口永远是第一手段。i.MX8X的调试串口默认是UART0波特率通常是1152008N1。接上串口线上电后能看到完整的启动log。第一次启动最重要的三件事确认U-Boot有没有起来。如果串口只有一片空白检查串口选择是不是对的博通SCU固件是不是正常加载了。确认内核解压是否完成。U-Boot加载内核后串口输出Starting kernel ...之后过几秒应该会有内核早期log如果卡在这里很可能是设备树或DDR配置问题。确认rootfs是否挂载成功。内核跑起来后会尝试挂载rootfs常见错误是VFS: Unable to mount root fs这说明rootfs的路径或者格式不对。4. 常见问题与排查技巧实录4.1 串口完全没有输出的排查顺序这个问题的排查顺序很重要千万别一上来就怀疑软件硬件问题导致的概率更大。先用万用表量模块载板上的3.3V电源是否正常。检查串口芯片的TX/RX是否接反了做底板经常犯这种错误。看模块上的启动选择电阻是否设置正确如果模块配置成从eMMC启动而你往SD卡里烧写了镜像自然不会有任何输出。如果以上都正常用示波器抓UART TX引脚的波形确认上电一瞬间有没有数据。软件层面要确认的是你烧写的flash.bin是否包含了正确的SCU固件。i.MX8X的SCU固件是NXP提供的一个二进制blob如果版本不对会导致核心的电源和时钟不工作整个SoC就像死了一样。4.2 内核启动中途panic设备树和CMA的相爱相杀我们调试中最常见的内核panic发生点在设备树里的memory节点。SMARC模块的DDR容量是由模块厂商决定的有些模块出厂默认配2GB你换了一个4GB容量的模块但设备树里CMA预设的起始地址是按2GB预留的就可能出现内存重映射冲突系统启动到一半直接panic。排查方法在内核启动参数里加mem2048M临时限制内存大小等能启动进去后再重新调整设备树。更好的做法是让U-Boot把实际检测到的DDR大小传给内核设备树里的memory节点只作为兜底。4.3 设备树里调GPIO控制外设加载顺序问题有个很坑的问题在设备树里用gpio-leds控制一个LED本来很简单但如果不小心把这个LED的GPIO复用了其他功能比如I2C的SCL就会出现I2C设备无法探测、LED也不亮的现象。原因就是pinctrl的配置冲突同一个引脚被两个节点引用内核后面的节点把前面的配置覆盖了。排查方法启动log里搜索pinmux相关的警告或者直接把设备树中两个冲突节点全部删掉再逐个加回来定位是哪个引脚冲突。在正式项目里建议做一份引脚复用表清单把所有pin的mux模式、电气属性、归属设备列成一个表格硬件和软件共用这一份文档能省很多排查时间。4.4 双千兆网口PHY芯片识别不到SMARC模块标准定义了双PCIe和双千兆网口。i.MX8X集成的MAC模块可以跑RGMII但PHY芯片通常是放在载板上的。我们用的是瑞昱的RTL8211系列第一次上电时发现eth0能起来eth1死活没有link。排查后发现是设备树里PHY的reset GPIO没有配置正确。PHY芯片需要一个上电后的复位释放时序如果复位拉低时间不够PHY就进入不了正常工作模式。在设备树里这样配置fec2 { pinctrl-names default; pinctrl-0 pinctrl_fec2; phy-mode rgmii-id; phy-handle ethphy1; phy-reset-gpios gpio1 8 GPIO_ACTIVE_LOW; phy-reset-duration 10; status okay; };phy-reset-duration控制的是复位低电平持续时间单位是毫秒。如果PHY上电后还是不稳定可以尝试把这个值加大到20甚至50。另外一个容易忽视的点是rgmii-id和rgmii的区别。rgmii-id表示MAC和PHY之间的RX/TX时钟延迟由PHY自己处理rgmii则是MAC侧处理。两个配置错乱的话网卡即使link了ping也不通或者丢包严重。4.5 调试实用小技巧ftrace和sysfs最后分享两个我自己调试时的习惯。第一个是ftrace。当怀疑驱动有性能问题时可以用它跟踪某个函数的调用频率和耗时echo function_graph /sys/kernel/tracing/current_tracer echo func_name /sys/kernel/tracing/set_ftrace_filter echo 1 /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace注意调试完记得关掉不然trace buffer会一直增长影响系统实时性。第二个是直接看sysfs里的资源占用。比如查某个中断在运行过程中是否大量触发cat /proc/interrupts如果某个GPIO中断在系统空闲时也在疯狂跳动那就要去查对应的外设是否有异常。这类问题在排查触摸屏跳点、PMIC中断误触发时极其好用。5. 一些补充的经验和注意事项最后再多说几句我在整个项目过程中沉淀下来的体会。如果你打算用SMARC模块一定要注意载板设计周期比你想的要长。SMARC的连接器是高速板对板差分信号、电源完整性这些都需要做阻抗匹配和仿真。如果硬件团队对SMARC不熟悉建议直接跟模块原厂买一个评估载板把软件先调通载板设计可以并行推进。软件方面我踩过最大的坑是过于相信原厂BSP的默认配置。模块厂商给的BSP可能在他们的载板上一切正常但换到你的载板之后DDR配置、PHY芯片型号、eMMC分区表都可能不一样。所以拿到BSP后先花几个小时把设备树从头到尾看一遍别急着编译烧录。还有一个习惯问题嵌入式Linux开发最好是全程用Git管理。设备树、内核配置、U-Boot环境变量、rootfs构建脚本这些都要纳入版本管理。我自己经历过一次因为改了设备树里一个GPIO配置导致整块板子无法启动的经历如果当时没有Git记录光靠记忆排查会非常痛苦。工具链方面再强调一遍能用Yocto就用Yocto不要觉得自己手动交叉编译更可控。Yocto的构建缓存机制其实很成熟第一次构建虽然慢但后续增量编译很快。最关键的是它能把所有patch和配置都以代码的形式记录下来这对产品的可维护性和团队协作非常重要。如果你的项目需要严格的离线部署环境比如工业现场不允许在线升级记得把Yocto的sstate-cache和downloads目录备份下来之后在隔离网络环境里也能完整构建出相同的镜像。做i.MX8X SMARC Linux这套方案前期会有一段比较陡峭的学习曲线尤其是从单片机转向嵌入式Linux的开发者。但一旦把Yocto、设备树、U-Boot这几块硬骨头啃下来你会发现后面做任何基于NXP平台的Linux产品思路都是通用的。希望这篇文章能帮你少踩一些坑顺利把产品跑起来。