
那天下午的事我到现在还记得手头只剩一台 Android Q 的平板要交差的 EFR32MG21 Zigbee 网关 NCP 固件还得重编。笔记本被同事拿走现场网络又不稳云编译这条路直接断掉。于是我把主意打回到这台平板上——Android 10 底层就是 LinuxTermux 又能提供完整的用户态环境理论上完全可以跑 Gecko SDK 的构建链。实际做下来确实可行但过程比想象中曲折工具链选型、SDK 版本、内存限制、脚本解释器路径每一步都可能卡你半小时。这篇是我那两天完整折腾过程的记录适合需要在 Android 设备上编译 EFR32MG21或者同类 ARM 无线 SoC 固件的开发者。尤其做 IoT 网关、现场调试的朋友应该能从中直接抄到一条能跑通的路。1. 为什么要在 Android 设备上编译 EFR32MG21 固件1.1 这不是“把芯片移植到安卓”而是把安卓当构建主机“Androidq下编译efr32mg21”这句话最容易让人误会成“把这颗芯片塞进 Android 驱动栈”其实完全不是一回事。EFR32MG21 是 Silicon Labs 的无线 SoC核心是 Arm Cortex-M33主要跑 Zigbee、Thread 这类低功耗短距通信。它内部有独立的 Flash 和 RAM运行的是一个不依赖操作系统的裸机固件。我们说的“编译”是编译这个裸机固件本身产物是.hex、.bin最终烧录到 EFR32MG21 自己的 Flash 里。Android 在这里扮演的角色仅仅是一个 Linux 构建主机编译器跑在 Android 的 CPU 上交叉生成 Cortex-M33 能执行的机器码。所以你不需要改 Android 内核也不需要写什么 JNI 驱动跟“把芯片移植进安卓”完全是两个维度。理解这两点整条技术路径就清晰了环境是 Android 上的 Linux 用户态目标是标准的 ARM 裸机交叉编译核心工作是让工具链和 SDK 在这个容器化环境里正常协作。1.2 一个典型场景Zigbee 网关项目里的快速验证需求我这次折腾背景是一个基于 Android 方案板的 Zigbee 网关项目。板子通过 UART 外接一颗 EFR32MG21 模组跑 EmberZNet 的 NCP 固件帮网关承担 Zigbee 组网和数据转发。方案验证阶段固件参数要反复调串口波特率、流控方式、发射功率、NCP 的 UART 引脚映射每改一次都意味着重新编译一版固件。按正常流程改完参数需要回到电脑上开 Simplicity Studio 重新构建再把固件通过烧录器灌进模组。但到了现场或者出差场景电脑不在身边唯一的计算设备就是这台 Android 平板。把编译环境直接搬到设备上就省去了“拷贝-传输-等待”的循环。终端里一条 make 命令出固件再用 Android 上的串口工具把新固件刷给模组整个验证链路在设备本地闭合。这种需求在 IoT 网关开发里并不奇怪网关本身就是 Android 系统开发者经常要现场调优协调器固件如果你也有类似的仓库只有代码、手边只有 Android 设备的情况这篇文章的方法就能派上用场。1.3 EFR32MG21 的构建依赖其实很简单难在版本绑定单独看 EFR32MG21 的编译流程并不复杂。GSDK 用的是标准 GNU Make 体系底层就是 arm-none-eabi-gcc 把协议栈源码、autogen 自动生成代码和应用代码编到一起。它不依赖 IDE不需要图形界面更没有什么私有的编译服务所以理论上任何能跑 Linux 用户态工具链的环境都能承担构建任务。但真正劝退很多人的是两个坑第一工具链版本必须和 GSDK 小版本匹配错一个版本就可能冒出各种符号找不到、编译特性不支持的怪问题第二自动生成的工程文件Makefile、autogen 等需要提前准备好这一步通常依赖 PC 上的 Simplicity Studio。把这两个核心问题搞明白换到 Android 上编译就只是环境层面的事了。下面我会从环境搭建开始一步步讲我是怎么在这台 Android 平板上把构建跑通的。2. 用 Termux proot Ubuntu 铺出编译地基2.1 先装 Termux拿到一个能用的 Linux 用户态环境Android Q 上最成熟的 Linux 用户态方案还是 Termux。我这边用的是从 F-Droid 渠道安装的版本这里也建议用 F-Droid 或 GitHub Release 的包不要从应用商店装旧版本旧版本缺包很烦。装完 Termux 后第一件事是打开存储权限termux-setup-storage然后更新包管理器并安装 proot-distropkg update pkg upgrade -y pkg install proot-distro -y这里有第一个实操细节Termux 默认的 HOME 在 App 私有目录下Android 文件系统对外置存储的中文路径、符号链接支持都不友好。所以不要跑到/sdcard里去拉代码、建工程尽量所有操作都在 Termux 的$HOME下进行。我一开始图方便把 GSDK 直接放在 /sdcard 上结果编译时生成的符号链接和文件权限各种不对把整个操作挪进私有目录之后才恢复正常。2.2 用 proot-distro 装 Ubuntu并安装构建依赖Termux 本身提供了 proot-distro 这类工具可以在不 root 的情况下运行一个轻量 Ubuntu 环境思路类似 chroot但不需要系统权限。安装 Ubuntu 的命令如下proot-distro install ubuntu proot-distro login ubuntu第一次登录后先把基础依赖装上apt update apt install -y wget make git python3 perl xz-utils这里make是构建的核心python3和perl是 GSDK 生成脚本经常调用的解释器。很多教程只提醒装 make没有装 python3结果编译到一半报/usr/bin/env: python3: No such file or directory然后又回来补费时间。2.3 为什么我建议用 Ubuntu 而不是 Termux 原生环境硬编有人可能觉得既然 Termux 已经能跑 bash为什么还要多套一层 proot 去装 Ubuntu原因在于 Termux 的 libc 是 bionic而 GSDK 里很多生成脚本、perl 模块甚至 make 的某些行为在 bionic 环境下会有诡异问题。最典型的是/bin/sh路径、/usr/bin/env解析这些 POSIX 习惯bionic 和 glibc 的差异会导致脚本在莫名的地方挂掉。proot 切到 Ubuntu 之后整个环境跟标准 x86/aarch64 Linux 完全对齐工具链、编译器、宿主脚本的行为都变得可预测。多套一层的代价是性能和一点点启动时间但对编译这种计算密集型任务来说性能损失完全在可接受范围内换来的是稳定和少踩坑这笔账不亏。3. 工具链和 GSDK 版本锁死组合才能少踩坑3.1 arm-none-eabi-gcc版本差一个 Level 都不行构建 EFR32MG21 固件编译器首选 GNU Arm Embedded Toolchain。GSDK 4.4 官方验证过的版本是 10.3-2021.10这也是我最终锁定的版本。为什么强调版本因为 Silicon Labs 的 SDK 会针对特定 GCC 版本做编译期特性和头文件兼容性适配你换个 9.x可能在链接阶段遇到符号对齐问题或者被__attribute__的解析差异坑到。下载时注意一个关键点目标设备如果是主流 Android 平板的 ARM64 架构一定要选aarch64-linux的宿主版本。到 ARM 官网下载页找arm-gnu-toolchain-10.3-2021.10-aarch64-arm-none-eabi.tar.bz2这类包。如果一时找不到就在包里先看有没有 Linux/AArch64 分类别拿成 x86_64 那份。解压到/opt:cd /opt tar xjf arm-gnu-toolchain-10.3-2021.10-aarch64-arm-none-eabi.tar.bz2 export PATH/opt/arm-gnu-toolchain-10.3-2021.10/bin:$PATH顺便说一句export PATH在 proot 里是临时的重新登录后需要重新设置。为了避免每次都手动敲可以在~/.profile里追加一行 PATH 配置。3.2 拉取 GSDK 时不要直接 checkout mainGecko SDK 通过 GitHub 分发命令很简单但有个容易犯的错误不要无脑 checkout 最新 main 分支。GSDK 的主分支提交很频繁可能昨天还能编译今天就因为一个提交变化导致工具链不匹配。正确做法是绑定 release tag。我这边用的组合是 GSDK 4.4对应命令git clone --depth 1 --branch v4.4.0 https://github.com/SiliconLabs/gecko_sdk.git拉下来之后先不要急着编译检查一下目录里有没有platform、protocol这些必要的子模块目录。有的 GSDK 版本子模块是独立仓库需要同步执行git submodule update --init --recursive。如果漏掉这一步后面编译时会报某个头文件找不到排查半天才发现是子模块没拉全。3.3 工程模板从哪里来先用 PC 生成一次再进仓库这里要说一个比较关键的认知GSDK 本身只是 SDK 源码你不能直接拿一个空目录就开始编。真正可以编译的工程至少需要三样东西工程级别的 Makefile或 .mak 文件、autogen 自动生成目录、以及应用源码。而 autogen 通常需要 Simplicity Studio 根据工程配置生成。我的做法是先在 PC 上用 Simplicity Studio 生成一个 NCP-UART 的 Zigbee 工程确认无误后整个工程目录提交到项目仓库。之后在 Android 端只需要把仓库拉下来直接执行 make 就能编译。如果你手上没有现成工程那就老老实实找台 PC 先生成一次。这步不能省因为手写 autogen 代码完全不现实付出的时间成本远超预期。工程目录结构大致如下ncp_uart/ ├── Makefile ├── ncp_uart.mak ├── autogen/ ├── gecko_sdk/ ├── app/ └── build/关键就是autogen和.mak文件它们一起决定了构建系统如何解析工程配置。4. 实际编译 NCP 固件的流程与报错排障链路4.1 进入工程找对环境变量和目标在 proot Ubuntu 里进入工程目录先检查一下当前 PATH 是否已经包含工具链which arm-none-eabi-gcc如果返回路径说明工具链生效。如果显示 not found回到上面加 PATH 的步骤。之后直接执行cd /your/project/path/ncp_uart make -f ncp_uart.mak -j4这里的-j4表示 4 个并行编译任务。Android 设备的 CPU 核心数普遍在 4 到 8 核之间但并行度不宜直接拉满。原因我下面会详细说先按 4 跑基本没问题。如果你的工程没有 .mak 文件那通常可以直接make -j4GNU Make 会自动寻找 Makefile。还有的 SDK 版本需要在命令里显式指定工具链目录make -f ncp_uart.mak GNU_ARM_TOOLCHAIN_PATH/opt/arm-gnu-toolchain-10.3-2021.10/bin -j4具体变量名不同版本会有差异但大多数 GSDK 4.x 工程支持GNU_ARM_TOOLCHAIN_PATH。4.2 编译产物到底去哪了编译成功后默认产物一般出现在build/debug/bin或build/release/bin目录下主要看构建配置。常见的产物包括app.axf带调试信息的 ELF 文件主要给调试器用。app.hexIntel HEX 格式普遍用于烧录器。app.bin纯二进制适合 Bootloader 应用或 OTA 场景。我的经验是先看文件大小确认不是 0 字节然后跑一下 size 命令arm-none-eabi-size build/debug/bin/app.axf输出会显示 text/data/bss 三段大小大概判断一下固件是不是符合预期。比如 EFR32MG21 Flash 是 1MB如果编译出的 text 不到几十 KB说明配置可能是最小裁剪版本如果异常膨胀到接近 1MB就要回头查配置是不是把调试信息编进去了。4.3 三个典型的“Android 环境下编译”错误这个环节是最值得记录的因为每个错误都让我多花了一两个小时。我把排障链路按顺序写出来方便你复现排查思路。第一个错误/usr/bin/env: python3: No such file or directory这个报错一般出现在 make 执行一段时间后位置通常在生成代码或处理配置的脚本里。原因是 GSDK 的某个生成脚本用#!/usr/bin/env python3指定解释器但 proot 环境里没装 python3env 找不到就直接退出。解决办法很简单apt install -y python3但要注意别只用 python3 别名检查一下/usr/bin/python3是否存在。有些精简版 Ubuntu 镜像会把python指向 python2而 GSDK 4.x 需要的是 python3所以最好确认版本python3 --version第二个错误cannot execute binary file: Exec format error这是所有错误里最坑的一次。我一开始下载工具链时电脑上习惯性选了 x86_64 的包放到 ARM 平板的 proot 环境里执行gcc 直接罢工。Exec format error等同于告诉你这不是本机能运行的格式。解决方法是重新下载aarch64-linux版本的 toolchain。判断当前架构用uname -m如果你是 ARM64 设备它会输出aarch64。这个坑其实很容易避免但很多人包括我会因为惯性踩上去。工具链是交叉编译器交叉这个词很容易让人忽略“编译器本身也有宿主架构”这件事。第三个错误virtual memory exhausted: Cannot allocate memory编译到某个较大的协议栈源文件时GCC 偶尔会直接崩溃退出终端提示virtual memory exhausted或者干脆整个进程被杀掉。原因是 Android 系统对单进程可分配内存有限制而 GCC 在解析复杂头文件时内存占用会瞬时飙高。最直接的做法是把并行度降到-j1。有人觉得降并行度太慢但我实测下来-j1编这个规模的工程也就多花一两分钟和排查 OOM 的时间成本比起来完全不值一提。如果切到-j1还是崩溃可以再看一下 proot 环境的/proc/meminfo和 Android 的设备实际内存考虑换一台内存更大的平板。排障链路拉通后你会发现这个错误大多不是代码问题而是 Android 这个特殊宿主环境带来的资源限制问题。4.4 编译速度够不够用很多人关心在 Android 上编译会不会慢到没法用。我实测下来的结论是完全可接受。整个 NCP 工程在 4 并行任务下大约两三分钟在 8 核设备上尝试过-j8稳定在 1 分半左右。如果设备处理器弱一些-j2也就五六分钟。编译过程还有个额外好处完全不依赖网络。GSDK、工具链、工程代码都在本地make 过程中不会联网拉取任何东西。这正好适合现场网络不通、云编译没法用的场景。从这个角度看Android 设备其实是一个还不错的便携编译机适合“最后一公里”式的快速验证。5. 编译完成之后的闭环地址检查、bootloader 与烧录5.1 检查产物的下载地址是否合理编译成功不等于烧进去就能跑EFR32MG21 的 Flash 布局是必须关注的点。Cortex-M33 的向量表默认在地址 0x00000000但你的工程可能不是从 0 开始链接的因为很多模组出厂带 bootloaderbootloader 会占掉低地址区。拿到app.axf后我习惯用下面命令确认 section 地址arm-none-eabi-readelf -S build/debug/bin/app.axf看.text段的起始地址比如如果你的工程配置的是带 bootloader 方式.text可能从0x00000000的某个偏移如 0x4000 或 0x8000开始而不是 0。这个偏移必须和 bootloader 的分区设置一致否则固件烧进去会出现启动就跑飞、或者升级引导后路由表丢失的诡异问题。我第一次编完直接烧 0 地址结果整个设备启动黑屏后来用 readelf 一查才发现工程里的链接脚本默认是“不带 bootloader”的配置而模组出厂预烧了一个 bootloader两者分区重叠了。5.2 bootloader 和应用区域的分区关系EFR32MG21 的 Flash 空间有限常见的分区方式是 bootloader 占据低位地址应用区从固定偏移开始。Gecko Bootloader 还会配套一个存储区域用于保存应用镜像、启动参数等。编译应用固件之前要先确认模组出厂时烧的 bootloader 版本和支持的分区表。我踩过的坑是SDK 默认的链接脚本并不总是适合当前模组。有的模组厂家用自定义 bootloader 分区偏移跟 SDK 默认值不同这时候就要修改链接脚本或使用官方提供的 partition 配置。建议从模组厂家的技术支持文档拿一份分区表然后反向检查你工程的链接地址。这一步没有捷径只能确认清楚。别想着靠运气直接烧无线固件整错了轻则功能异常重则把串口升级通道堵死。5.3 Android 端可以怎么烧录到了烧录这一步Android 上没有图形化的 Simplicity Commander但实际可行的路线至少有三条按项目需求选择。第一条如果 EFR32MG21 使用的是 Gecko Bootloader并且支持 UART XMODEM 升级那 Android 上可以直接用支持 XMODEM 的串口工具发送.hex或.s37文件。很多第三方安卓串口终端都内置 XMODEM 发送功能把模组的 UART 引脚接到 Android 板卡的串口上进入 bootloader 模式后发送即可。第二条如果模组走的是 J-Link/SWD 调试接口Android 端理论上可以通过 TCP/IP 连接到电脑上的 J-Link 服务端但硬件链路比较复杂属于远程调试的玩法平时现场一般不这么干。第三条如果最终用户是批量设备这种方式最推荐的是通过 Android 网关本身做 OTA 升级。把编译好的固件打包成.ota文件通过 Zigbee 协议或 UART 通道下发由应用端配合 bootloader 完成升级。这种方式不需要专门的烧录器也不需要拆机但编译时要额外生成 OTA 文件别漏了这一步。我个人最推荐的还是 XMODEM 升级配置简单、依赖少、现场可复现性高。前提是你在编译前就把串口引脚分配和 bootloader 版本确认好这些信息在项目开始时就要写进设计文档。最后分享一个实际操作里的小经验整个流程跑通之后我把编译环境和工具链的安装步骤写成了一个脚本放到项目的 scripts 目录里。这样不管谁来只要有一台 Android 设备clone 仓库、跑一遍脚本就能进入可编译状态。这个动作节省了很多重复沟通的时间也降低了新同事的上手成本。如果你也要在 Android 上维护这类嵌入式编译任务建议同样把这个环境固化下来而不是依赖某一个人记住全部步骤。