1. 为什么我们需要定制Radio分区的OTA升级如果你正在开发一款基于高通平台的Android设备特别是那些需要稳定蜂窝网络连接的智能硬件比如车载中控、工业平板或者智能POS机那你肯定对“Radio”这个词不陌生。简单来说Radio分区里存放的是设备与外界通信的“灵魂”——基带固件。它负责管理你的手机信号、Wi-Fi、蓝牙甚至是GPS。没有它你的设备就只是一块会发光的砖头。在Android R也就是Android 11及更高版本上高通平台普遍采用了A/B无缝升级机制。这套机制很棒用户可以在后台下载更新重启后直接进入新系统几乎无感。系统镜像像boot、system、vendor这些的OTA流程已经相当成熟。但问题来了Radio分区的升级往往被排除在这个“自动化流水线”之外。为什么因为Radio固件太特殊了。它通常由高通直接提供是一堆.mbn、.elf、.bin文件不同项目、不同硬件版本、不同运营商定制需求对应的Radio镜像都可能不同。原生的A/B OTA构建脚本默认只会打包那些在AB_OTA_PARTITIONS列表中明确声明的、并且存在于标准输出目录IMAGES/的分区镜像。而Radio镜像按照高通默认的构建流程是放在一个单独的RADIO/目录下的。这就导致了一个尴尬的局面你明明编译出了最新的基带固件但在制作OTA升级包时脚本却“找不到”它们最终生成的升级包无法更新Radio为设备留下网络功能的隐患。我遇到过好几次现场反馈设备OTA升级后4G信号变弱了或者蓝牙连接不稳定。一查果然是Radio分区还是老版本新系统配老基带不出问题才怪。所以定制化地修改OTA构建流程让Radio分区也能被正确识别、打包和校验就成了一个刚需。这不仅仅是增加一个分区那么简单它涉及到构建脚本的逻辑修改、分区列表的动态管理以及镜像文件的路径映射是一个典型的“脏活累活”但做好了设备稳定性的提升是立竿见影的。2. 理解Android OTA构建流程与Radio的“失联”要解决问题得先搞清楚标准流程是怎么“丢”掉Radio的。我们得钻进Android构建系统的肚子里看看。当你执行make otapackage时系统会先编译出所有镜像包括Radio镜像。这些Radio镜像比如NON-HLOS.bin调制解调器固件、abl.elf应用引导加载器等默认会被输出到$(PRODUCT_OUT)/目录下但不会被自动拷贝到生成OTA包所需的临时目录IMAGES/下。OTA包的制作核心脚本是build/make/tools/releasetools/add_img_to_target_files.py。它的任务是把需要的镜像文件塞进一个中间文件——target_files.zip。这个脚本里有个关键函数叫CheckAbOtaImages。它的工作就是检查AB_OTA_PARTITIONS列表里声明的每个分区在IMAGES/目录下有没有对应的.img文件。如果有万事大吉如果没有脚本就直接报错断言失败。而Radio镜像呢它们安静地躺在RADIO/目录里名字可能也不是简单的“分区名.img”的格式例如modem分区对应的文件可能是NON-HLOS.bin。这就好比老师点名名单上有“张三”但张三的座位在隔壁教室老师在本教室当然找不到人于是判定张三旷课。所以我们的核心目标就明确了要引导构建脚本让它能去RADIO/目录找到正确的“张三”Radio镜像并把他“请”到IMAGES/教室来或者至少让老师知道可以去隔壁教室找到他。原始文章给出的Patch正是围绕这个目标展开的一套组合拳。它没有粗暴地改变整个构建流程而是巧妙地增加了适配层这种思路非常值得借鉴。3. 实战一步步解析Radio OTA的修改补丁光说原理太抽象我们直接上代码看看怎么动手改。原始文章提供了一个完整的diff补丁我们把它拆开揉碎了讲。我会结合我自己的经验补充一些他可能没细说的“坑点”。3.1 第一步建立分区名与镜像文件的映射表这是整个方案最巧妙的一步。Radio镜像文件名五花八门我们需要一个“字典”来告诉系统“modem分区对应的文件是NON-HLOS.bin”“keymaster分区对应的是km41.mbn”。这个字典就是新增的MPI_config文件。# 文件位置device/qcom/your_device/radio/MPI_config # 格式分区名镜像文件名 ablabl.elf modemNON-HLOS.bin dspdspso.bin devcfgdevcfg.mbn keymasterkm41.mbn rpmrpm.mbn xblxbl.elf xbl_configxbl_config.elf tztz.mbn hyphyp.mbn bluetoothBTFM.bin imagefvimagefv.elf uefisecappuefi_sec.mbn qupfwqupv3fw.elf featenablerfeatenabler.mbn cmnlibcmnlib.mbn cmnlib64cmnlib64.mbn为什么需要这个文件因为高通不同平台、不同版本的镜像文件名可能变化。比如keymaster有的项目用km4.mbn有的用km41.mbn。写死在脚本里会失去灵活性。用一个配置文件项目差异就通过这个文件来管理构建脚本的逻辑可以保持通用。在add_img_to_target_files.py中新增的GetMPI_config函数就是用来读取这个映射表的。def GetMPI_config(MP_list): MPI_config os.path.join(OPTIONS.input_tmp, RADIO, MPI_config) assert os.path.exists(MPI_config) with open(MPI_config) as f: for line in f.readlines(): if line.startswith(#): logger.info(Note:line) continue MP_infoline.strip().split() logger.info(partition: MP_info[0] imagename: MP_info[1]) MP_list.update({MP_info[0]: MP_info[1]})这里有个细节要注意assert os.path.exists(MPI_config)。这意味着MPI_config文件必须随Radio镜像一起被放到RADIO/目录。这通常需要在你的设备Makefile里添加拷贝操作。如果这个文件缺失脚本会直接报错这是一个很好的失败快速反馈。3.2 第二步改造镜像检查与拷贝逻辑有了映射表接下来就是修改核心的检查函数。原始脚本里是CheckAbOtaImages我们需要一个它的“增强版”——CheckAbOtaImagesNonQssi。def CheckAbOtaImagesNonQssi(output_zip, ab_partitions): MP_list {} GetMPI_config(MP_list) # 读取映射表 for partition in ab_partitions: partition_name partition.strip() img_name partition.strip() .img logger.info(radio img_name :%s,img_name) # 关键逻辑如果分区在映射表中就去RADIO/目录找对应的真实文件 if partition_name in MP_list.keys(): radio_src_img_name MP_list[partition_name] radio_path os.path.join(OPTIONS.input_tmp, RADIO, radio_src_img_name) if os.path.exists(radio_path): # 找到后将其拷贝到IMAGES/目录下并重命名为标准格式分区名.img images_path os.path.join(OPTIONS.input_tmp, IMAGES, img_name) shutil.copy(radio_path, images_path) continue # 如果不在映射表中或者拷贝失败则回退到原始检查逻辑在IMAGES/或RADIO/找分区名.img if output_zip: images_path IMAGES/ img_name radio_path RADIO/ img_name available (images_path in output_zip.namelist() or radio_path in output_zip.namelist()) else: images_path os.path.join(OPTIONS.input_tmp, IMAGES, img_name) radio_path os.path.join(OPTIONS.input_tmp, RADIO, img_name) available os.path.exists(images_path) or os.path.exists(radio_path) assert available, Failed to find img_name这个函数干了三件事查字典遍历AB_OTA_PARTITIONS列表对于每个分区先去MP_list映射表里查它对应的真实文件名。找文件并搬家如果找到了映射关系就去RADIO/目录下找这个真实文件。找到了就用shutil.copy把它拷贝到IMAGES/目录下并且按照OTA脚本的预期重命名为分区名.img例如modem.img。保底检查如果某个分区不在映射表里比如boot、vendor等标准分区或者拷贝操作后文件仍然不存在就执行原来的检查逻辑看看IMAGES/或RADIO/里有没有现成的分区名.img。这里有个非常重要的点为什么一定要拷贝到IMAGES/并重命名因为后续的OTA包生成、校验、刷写脚本都预期在IMAGES/目录下找到以.img结尾的标准命名文件。我们提前做好这个“翻译”和“搬运”工作后面的所有流程就都通畅了无需再做修改。3.3 第三步动态管理AB分区列表光有检查逻辑还不够我们得告诉构建系统“哪些Radio分区需要被加入OTA流程”这就是修改BoardConfig.mk和新增radio.mk的目的。在BoardConfig.mk中我们扩展了AB_OTA_PARTITIONS列表至少要把abl加进去因为它是A/B更新中非常关键的一环。同时引入一个专门的radio.mk文件。# 在 BoardConfig.mk 中 AB_OTA_PARTITIONS ? boot vendor vbmeta dtbo abl -include $(LOCAL_PATH)/radio/radio.mkradio.mk这个文件是整个方案的“自动化管家”。它的逻辑非常实用HAVE_FILE : $(shell test -f $(RADIO_DIR)/NON-HLOS.bin echo yes) ifeq ($(HAVE_FILE),yes) ifeq (,$(filter modem, $(AB_OTA_PARTITIONS))) AB_OTA_PARTITIONS modem endif endif它对radio/目录下的每一个可能的Radio镜像文件进行检查。如果这个文件存在比如NON-HLOS.bin并且当前AB_OTA_PARTITIONS列表里还没有对应的分区名比如modem就自动把这个分区名加进去。这样做的好处巨大项目迭代中Radio镜像集合可能会变。今天这个项目需要modem和dsp明天那个项目可能多了featenabler。你不需要每次都手动去修改BoardConfig.mk里的列表只需要保证radio/目录下有正确的文件以及MPI_config里有正确的映射构建系统就会自动帮你把该加的分区都加上。这大大减少了配置错误和维护成本。3.4 第四步确保Radio镜像被安装到输出目录最后一步看起来简单但很容易遗漏我们得确保编译出来的Radio镜像真的被复制到了生成OTA包时脚本能找到的地方也就是$(PRODUCT_OUT)/RADIO/目录。这通常在设备的AndroidBoard.mk里通过INSTALLED_RADIOIMAGE_TARGET变量来实现。原始文章的补丁也做了这个事ifneq ($(TARGET_PRODUCT),qssi) INSTALLED_RADIOIMAGE_TARGET $(addprefix $(PRODUCT_OUT)/,abl.elf) endif这里只加了abl.elf是因为其他Radio镜像可能通过别的机制比如radio_filesmap已经安装了。你需要根据自己项目的实际情况确保所有在MPI_config里列出的镜像文件最终都出现在了$(PRODUCT_OUT)/RADIO/下。你可以用find $(PRODUCT_OUT) -name *.mbn -o -name *.elf -o -name *.bin | grep -v obj这样的命令在编译后检查一下。4. 踩坑记录与关键注意事项照着补丁改完就能一次成功理想很丰满现实往往给你挖几个坑。下面是我在实际项目中遇到的几个典型问题。第一个坑镜像文件名不匹配。这是最常遇到的。MPI_config里写的是keymasterkm41.mbn但实际编译出来放在RADIO/目录里的文件叫km4.mbn。结果就是脚本根据映射表去找km41.mbn找不到回退到原始逻辑去找keymaster.img也找不到最终断言失败。解决方案编译完成后第一时间去out/target/product/device/RADIO/目录下用ls命令列出所有文件然后逐一对MPI_config进行校准。务必保证配置里的文件名和实际文件名一字不差。第二个坑分区大小校验失败。即使镜像被成功找到并打包在设备端进行OTA升级时可能会在刷写Radio分区时报错提示“image larger than partition size”。这是因为Radio分区的大小在BoardConfig.mk中通过BOARD_PARTITIONIMAGE_PARTITION_SIZE定义。如果你更新了Radio固件新镜像的大小超过了这个定义值就会失败。解决方案更新Radio固件时一定要确认其大小。如果变大了必须同步调整BoardConfig.mk中对应分区的大小定义。例如BOARD_MODEMIMAGE_PARTITION_SIZE。可以使用ls -l查看镜像大小并预留一定的余量。第三个坑A/B槽位切换导致的Radio版本错乱。这是A/B系统特有的问题。假设设备当前运行在A槽Radio也是A槽的版本。OTA升级包被下载并安装到B槽包括新的Radio镜像。重启后设备从B槽启动系统但Radio分区是全局的不属于A/B槽如果OTA包里的Radio镜像版本与B槽的系统不兼容就可能出现信号问题。更复杂的是如果升级后回滚到A槽系统是旧的Radio却是新的也可能不兼容。解决方案这没有银弹需要高通和驱动层做兼容性保证。作为系统开发者我们能做的是1) 在发布OTA前严格测试新Radio固件与新旧两套系统的兼容性2) 考虑在OTA更新脚本中加入版本校验逻辑如果当前Radio版本已经高于或等于OTA包中的版本则跳过Radio刷写避免降级风险。第四个坑filesmap文件未更新。原始补丁里修改了filesmap将km4.mbn改成了km41.mbn。这个文件定义了Radio镜像应该被刷写到设备的具体哪个块设备节点。如果你增加了新的Radio分区比如featenabler但忘记在filesmap里为它添加一行映射那么即使OTA包里有这个镜像刷机脚本也不知道该把它写到哪去会导致升级失败。解决方案每在AB_OTA_PARTITIONS和MPI_config里增加一个分区就必须同步检查并更新device/qcom/your_device/radio/filesmap文件。5. 验证与调试如何确认你的修改真的生效了改完代码编译通过只是第一步。我们必须验证OTA包是否真的包含了正确的Radio镜像以及升级流程是否顺畅。验证方法一解压target_files.zip查看。这是最直接的验证方式。在编译输出目录找到生成的target_files.zip通常在out/target/product/device/obj/PACKAGING/target_files_intermediates/目录下。# 解压查看IMAGES目录 unzip -l target_files.zip | grep IMAGES/你应该能看到abl.img,modem.img,dsp.img等文件。再用unzip解压出其中一个用file命令或hexdump看看它是否真的是你期望的Radio镜像格式。验证方法二分析生成的OTA包ota.zip。最终发布的OTA包是target_files.zip经过ota_from_target_files脚本加工生成的。你可以用同样的方法解压它查看payload.bin或者META-INF/com/google/android/updater-script对于A/B更新是update-binary和payload_properties.txt。在updater-script中应该能看到针对abl、modem等分区的刷写指令。验证方法三开启详细日志跟踪构建过程。在修改的Python脚本中加入logger.info或logger.warning就像原始补丁里做的然后在构建时通过环境变量控制日志级别观察输出。export ANDROID_LOG_TAGS*:i # 设置日志级别为INFO source build/envsetup.sh lunch your_target make otapackage -j8 21 | tee build.log在build.log里搜索你添加的日志关键词比如“radio img_name”或“OTA add image”就能看到脚本是否走了你修改的逻辑分支以及它处理了哪些分区。验证方法四实机升级测试。这是终极测试。准备两台同型号设备一台作为基线一台用于升级。确保基线设备可以正常联网。然后使用你生成的OTA包进行本地升级或通过恢复模式刷入。升级完成后重点检查设备能否正常开机。进入系统后在“设置-关于手机-基带版本”中查看基带版本号是否已更新为预期版本。实际测试蜂窝网络打电话、上网、Wi-Fi、蓝牙等功能是否正常。如果支持A/B测试一次回滚操作确认回滚后Radio版本和网络功能是否正常。这个过程可能很枯燥但每一步都至关重要。特别是对于车载、工业这种对稳定性要求极高的场景任何关于基础通信功能的更新都必须慎之又慎。6. 扩展思考这套方案的通用性与优化点原始文章给出的方案是针对特定设备mc565x的但它的设计思想具有很强的通用性。如果你在做其他高通平台甚至其他芯片平台只要遇到类似“非标准分区OTA”的问题都可以借鉴这个思路“建立映射 - 动态检查与搬运 - 自动配置”。你可以对这套方案进行一些优化让它更加强大和鲁棒错误处理更友好现在脚本里用的是assert一旦失败整个构建就中止。可以考虑改为try-except记录详细的错误信息比如哪个文件没找到映射关系是什么方便快速定位问题。支持版本化Radio镜像有时我们需要在同一OTA包中为不同硬件版本提供不同的Radio镜像。可以扩展MPI_config的格式支持基于硬件版本或项目代号选择不同的镜像文件。例如modem-[HW_VERSION_A]NON-HLOS-verA.bin。与构建系统更深集成目前radio.mk是条件式地添加分区。可以考虑定义一个设备变量比如DEVICE_RADIO_FILES在AndroidBoard.mk里明确列出本项目需要的所有Radio分区和文件。这样配置更集中一目了然。增加完整性校验在拷贝镜像到IMAGES/后可以计算一下文件的SHA256校验和并与RADIO/目录下的一个预计算的校验和文件对比确保拷贝过程没有出错。最后我想说处理Radio OTA升级这类问题没有一成不变的“标准答案”。高通的平台在变Android的版本在变项目的需求也在变。最关键的是理解底层机制OTA构建流程是怎样的分区表如何定义镜像如何被打包和刷写。掌握了这些无论遇到什么变种问题你都能找到解决的路径。我在这上面踩过的坑可能比有些朋友写的代码行数还多但每次解决问题的过程都是对系统理解的一次深化。希望这篇啰嗦的长文能帮你少走一些弯路。