
最近在调BlueNRG-LP的OTA升级翻到ST的LAT1284应用笔记按里面的思路在HigherLower工程上把静态协议栈方式下的OTA升级完整跑通了。整个过程比想象中坑多也比想象中有意思。这篇就当是给自己整理一份带注释的实操记录也分享给最近在做BLE OTA的兄弟。先说清楚这篇笔记的边界。它讲的是在BlueNRG-LP这颗芯片上用静态协议栈方式编译工程然后在HigherLower这个示例APP里加入BLE OTA升级能力。硬件是STEVAL-IDB011V1开发板软件基于ST的BlueNRG-LP SDK手机端用ST BLE Toolbox完成固件包下发。适合正在做可穿戴、传感器节点、智能家居控制器这类BLE产品并且有OTA需求、又不想把协议栈动态加载那套复杂逻辑引到项目里的工程师参考。1. 项目拆解BlueNRG-LP静态协议栈OTA为什么值得做1.1 HigherLower示例APP到底在演示什么HigherLower是BlueNRG-LP SDK里一个很典型的BLE示例工程。功能上用一句话说就是手机和设备之间通过GATT服务玩“猜大小”游戏。设备端维护一个随机数或者某个状态量手机往特定特征值写一个猜测值设备比较后回一个“高了”“低了”或者“猜对了”的结果顺便可以控制板载LED亮灭来反馈。这个示例单独拿出来说并没有多复杂但它有一个非常好的特点GATT服务结构清晰代码量小依赖少。这就让它成了验证OTA改造的最佳载体——你不需要在一大堆业务逻辑里找哪里插OTA只需关注“服务注册、连接事件、写回调”这几个关键点。很多ST示例工程里HigherLower默认就能跑手机用nRF Connect或者ST BLE Toolbox连上就能看到服务和特征值。我在实际做的时候没有直接拿SDK里的OTA例程来改而是把OTA能力“移植”到HigherLower工程里。这样做的原因是大多数OTA例程为了适配各种平台加了太多条件编译和跳转逻辑直接看容易一头雾水。从HigherLower这个小工程入手反而能一条线看清OTA服务的完整链路。1.2 静态协议栈与动态协议栈的本质区别BlueNRG-LP的协议栈有两种使用方式这个区别是整个OTA方案选型的分水岭。动态方式下蓝牙协议栈固件是独立的一份镜像不跟应用代码链接在一起系统启动时由bootloader把它加载到RAM里运行静态方式下协议栈以静态库形式直接链接进应用镜像编译出来就是一个完整的.bin文件协议栈跟着应用一起跑。两者在OTA上的差异非常明显。动态方式下应用和协议栈是两块镜像升级时要么挨个处理要么设计一套复杂的双分区切换逻辑一旦协议栈版本和应用版本不匹配还要做兼容性判断。静态方式下协议栈和应用打包成一个文件手机下发一个升级包就够了bootloader只需要做“搬运和校验”这一件事。打个比方动态协议栈像是电脑里驱动放在外置移动硬盘系统坏了你还要找驱动盘重新装静态协议栈像是驱动直接打进系统镜像重装系统时一并恢复省心得多。对于没有专业量产工具、想快速交付OTA能力的中小项目静态方式明显更合适。这也是LAT1284里比较推荐的处理路径。对比项静态协议栈动态协议栈镜像数量单个应用镜像应用协议栈两块镜像OTA升级包一个包搞定需要处理双镜像升级逻辑启动流程bootloader直接跳应用bootloader先加载协议栈再跳应用实现复杂度较低较高适合场景资源紧张、追求快速交付需要频繁单独升级协议栈的复杂产品1.3 LAT1284应用笔记带来的核心思路LAT1284不是一篇泛泛的入门文档它给出的是一套可以直接落地的OTA改造路径。核心思路可以拆成四点第一bootloader工程和应用工程分开管理第二App区从固定地址开始并在链接脚本里写死第三通过OTA GATT服务接收固件包完整写入预留的Download区第四应用收到升级完成指令后设置升级标志并软复位bootloader检测到标志后完成擦写和跳转。这套思路的好处是环环相扣每个环节都有明确的触发条件和退出条件。我在复现过程中明显感觉到它把“OTA升级流程”这个容易被做成玄学的东西拆成了“接收数据、保存数据、切换分区、重启应用”几个确定性很强的步骤每个步骤都能单独验证。对新手尤其友好你可以在手机上看到固件包发到多少字节了再判断是下载阶段出问题还是校验阶段出问题。2. 环境准备硬件、SDK和工程导入一次搞定2.1 硬件平台与手机工具缺一不可硬件方面我用的是ST官方STEVAL-IDB011V1开发板上面有一颗BlueNRG-LP板载ST-LINK调试器USB线连电脑就能烧录和查看串口日志。如果你是自己画的板子也没关系只要引出SWD接口、电源稳定、天线匹配没问题流程完全一样。有一点提醒BlueNRG-LP和老的BlueNRG-1/2的引脚定义不完全一样别拿旧板子的原理图直接套。手机端必须有的两个工具是ST BLE Toolbox和nRF Connect。ST BLE Toolbox用来执行OTA升级它能识别ST定义的那套OTA服务直接把固件包拖进去就能下发nRF Connect用来调试GATT服务查看服务UUID、特征值、读写权限是否正确。调试阶段这两个App来回切换基本能定位80%以上的连接和通信问题。2.2 SDK版本选择与静态协议栈宏开关软件部分我在ST官网下载了BlueNRG-LP的SDK安装后里面包含了bootloader例程、OTA例程、HigherLower例程以及各种底层驱动库。这里有一个容易被忽略的点SDK默认的很多示例工程可能用的是动态协议栈配置编译出来的image本身没问题但你一旦接了OTA就要想清楚“协议栈在哪、要不要一并升级”。我这次明确改成了静态协议栈方式做法是在工程配置里确认协议栈相关的宏定义指向静态库链接。如果你手头SDK版本比较新建议先打开一个官方OTA例程看它的启动文件和链接脚本是怎么写的再回到HigherLower工程里做同样的修改。不同版本SDK的工程组织方式略有出入不要死记路径重点是理解“bootloader工程、应用工程、OTA服务”之间的编译关系。我自己就因为在SDK版本上纠结太久多花了大半天。2.3 先把原生HigherLower工程编译烧录跑通这一步听起来简单但非常重要。拿到SDK后先不要改任何东西直接编译HigherLower工程烧录到开发板然后用手机连接并控制LED。如果这一步跑不通后面所有OTA调试都会叠加“环境没搭好”的干扰因素排查起来特别痛苦。我当时卡在了一个很低级的问题上开发板之前烧过其他固件导致广播名称和预期不一致手机连了半天没反应。解决方式是先做一次全片擦除再烧录HigherLower确保Flash里没有残留数据影响启动流程。这也是个习惯问题现在每次烧录前我都会先擦除杜绝隐藏问题。3. 原理拆解OTA分区规划与升级完整流程3.1 Flash分区怎么划直接影响后面省不省心做OTA之前必须先把Flash分区规划清楚。BlueNRG-LP内部Flash不大规划原则就是“Bootloader最小化、App区足够、Download区用来存临时固件、NVDS区不能动”。我自己用的是这样一个布局你根据自己实际Flash大小调整分区起始地址大小用途Bootloader0x0000000032KB启动引导、OTA触发和固件搬移Application0x00008000192KB应用固件含静态协议栈Download0x0003800032KB通过BLE接收的新固件临时存放区NVDS/Config末尾固定区域若干KB蓝牙地址、绑定信息、用户配置这里有个非常重要的点Download区不能和App区重叠也不能把NVDS区放在会被bootloader擦除的范围内。否则会出现一个极其诡异的问题OTA升级成功了但设备的蓝牙地址变了、绑定信息丢了相当于“升级一次丢一次配置”。另外bootloader和App的分区边界要跟链接脚本严格对齐。我在第一次改造时就因为App起始地址多写了几十个字节导致App能烧进去但一跳转就HardFault。后来把地址对齐到了Flash扇区边界问题才彻底消失。3.2 OTA升级流程拆解从开机到升级完成整个“OTA升级流程”从设备上电那一刻就已经开始了。boolloader先跑起来检查App区是否有有效固件检查OTA标志位是否需要进入升级模式如果一切正常就直接跳转到App地址。App启动后正常广播、连接手机端只要发现OTA服务并开始下发固件包设备就先通过GATT的写请求把数据包写入Download区。这里特别要注意判断“数据包完整性和写入顺序”。BLE单次传输数据量有限固件包会被切分成很多个小包。应用每收到一包就要回写一个确认或者记录当前包号手机端根据反馈决定发下一包。这个过程如果中间断连Download区里就是残缺数据bootloader校验失败后不能盲目启动。当所有数据包都下发完成后手机App会写入一个“升级完成”命令。应用收到这个命令不是马上复位而是先做三件事设置OTA升级标志到非易失存储区、主动断开当前蓝牙连接、延时几十毫秒后软复位。这个“先断开再复位”的顺序是我踩过坑之后才深刻理解的如果不主动断开蓝牙bootloader起来后协议栈状态可能还是连接态容易引起抢占和死锁。bootloader复位后检测到OTA标志就会把Download区的新固件整段搬移到Application区擦写过程中会校验CRC校验通过后清除标志并再次复位进入新App。这就是完整闭环。3.3 升级包格式与CRC校验不能一句话带过手机App下发的不是一个裸.bin文件而是带固定头部的OTA包。头部一般包含魔数、固件版本、固件长度、CRC校验值等信息。Bootloader在正式搬移前会先用头部信息校验整个固件的完整性。很多第一次做OTA的人在这里栽跟头编译生成了.bin直接发给手机结果bootloader不认。原因就是缺少头部信息或者CRC算法不一致。我自己的做法是写了一个很简单的Python脚本读入编译输出的.bin文件填充头部字段计算CRC然后输出成正式的.ota升级包每次编译后自动生成。import struct, zlib, sys def build_ota(src_bin, dst_ota, version0x0100): with open(src_bin, rb) as f: data f.read() crc zlib.crc32(data) 0xFFFFFFFF header struct.pack(IIIH, 0x544F5441, len(data), crc, version) with open(dst_ota, wb) as f: f.write(header) f.write(data) print(fOTA package generated: {dst_ota}, size{len(data)}) if __name__ __main__: build_ota(app.bin, app.ota)这个脚本只是个雏形正式产品里最好再加上固件加密、回滚版本号等逻辑。但用它跑通流程足够了重点是让“编译产出物”和“手机升级包”之间的关系变得确定。4. 实操落地在HigherLower工程中跑通OTA升级4.1 加入OTA GATT服务并处理控制点HigherLower工程本身只有一套业务GATT服务我们要做的是增加一套OTA服务。SDK的OTA例程里一般会有现成的OTA服务源码你可以把相关的服务注册、属性表定义、写回调函数文件直接复制过来或者对照着抄到工程里。核心工作是注册一个OTA Service里面至少要有一个“数据接收特征值”和一个“控制点特征值”。控制点特征值负责接收手机端的命令尤其是“开始升级”和“升级完成”这两个关键命令。写回调里要根据命令类型做不同处理其中升级完成命令的处理逻辑最关键。我贴上我自己工程里的一个简化版处理逻辑方便理解static void OTA_ControlPoint_Handler(uint16_t handle, uint8_t *data, uint8_t len) { if (len 1) { return; } if (data[0] 0x01) { // 进入OTA模式 // 保存OTA标志到NVDS OTA_Save_Flag(); // 主动断开蓝牙连接 aci_gap_terminate(0, 0x13); // 延时后软复位 HAL_Delay(100); NVIC_SystemReset(); } else if (data[0] 0x02) { // 完成升级bootloader会处理 // 这里通常只做状态记录 } }不要直接在BLE事件回调函数里调用软复位否则容易出现在协议栈还没有完全释放资源的时候强行重启导致bootloader起来后状态混乱。我前面说到的“延时几毫秒再复位”就是给协议栈一个收尾时间。4.2 修改链接脚本和启动偏移容易翻车的地方这部分是整个工程改造里最容易出问题、但文档里又很少细讲的环节。HigherLower原生工程默认是从Flash起始地址0x00000000开始的但加入bootloader之后应用必须偏移到App分区起始地址。需要修改的有两个地方一个是链接脚本里的FLASH_ORIGIN另一个是启动文件里的向量表偏移VECT_TAB_OFFSET。这两个地方不匹配的话症状非常有迷惑性程序能烧录串口日志也能打印但一发生中断就死机或者蓝牙协议栈完全起不来。原因是向量表还停留在原来的Flash起始地址读取中断入口而实际中断向量已经在App区了。我当时在Keil的工程里只改了IROM1起始地址没改启动文件里的向量表偏移结果app一跑起来就进HardFault。排查了很久才发现是VECT_TAB_OFFSET没有对应修改。这个坑值得单独拎出来提醒只要做带bootloader的项目就一定会遇到。4.3 烧录顺序和参数配置按这个顺序执行不会错工程改好后烧录顺序也有讲究。我的操作习惯是先全片擦除然后编译烧录bootloader到Flash起始地址再编译烧录App到App分区地址。千万不要两次都点“全片烧录”否则第二次会把bootloader覆盖掉。如果开发板有ST-Link可以用STM32CubeProgrammer在IDE里配置两个烧录脚本一键完成bootloader和App的烧录。没有脚本也可以手工操作前提是记住两个烧录地址不要搞混。烧录完先上电看bootloader日志确认它能正确跳转App再做下一步OTA测试。4.4 手机端实测一次完整升级手机操作部分比我预想的直观。用ST BLE Toolbox连接开发板找到OTA升级入口选择之前生成的.ota升级包点击开始升级。升级过程中手机App会显示进度条设备端的Download区持续写入日志会按包号打印接收进度。升级完成后设备自动重启重新连接后打开nRF Connect看一下服务列表。如果新固件里改了LED闪烁频率这类明显特征可以直接通过现象确认版本是否生效。我当时第一次实测成功的时候感受是“原来BLE OTA没有想象中那么玄”整个过程从打包到重启大概几十秒比串口烧录快得多。5. 避坑指南OTA调试中的典型问题与解法5.1 升级中途断电或断连设备变砖了怎么救这是OTA开发里最让人心里发慌的问题。先给结论只要SWD接口没坏绝大多数“变砖”都能救回来。设备升级时断电最坏情况是bootloader或者App区被写了一半导致上电后没有任何可执行程序。解决办法是连接ST-Link用STM32CubeProgrammer做一次全片擦除然后重新烧录bootloader和App。如果确认bootloader还在只是App区损坏也可以直接烧录App不用每次全片擦。但有一点要小心如果调试工具连不上先检查芯片是不是因为上次OTA设置了读保护。读保护开启后SWD默认无法读取Flash内容需要在工具里选择“解除读保护”并执行一次全片擦除芯片才能恢复可写状态。5.2 编译时协议栈函数undefined多半是静态库没接进来用静态协议栈方式编译时经常会看到类似Undefined symbol的链接错误而且报错的函数全是蓝牙协议栈API。这基本可以断定是协议栈静态库没有正确参与链接。检查点有两个一是工程里是否添加了协议栈静态库文件路径和库文件名二是工程配置里是否有宏把编译导到了动态协议栈分支。我遇到过一次很隐晦的情况从官方动态协议栈工程拷贝文件时把某个头文件的宏定义也带过来了导致编译时以动态方式声明了协议栈入口拿到静态库却链接不上。排查方式是把工程配置和官方静态协议栈例程逐个对比特别是预处理宏定义部分。5.3 OTA成功后蓝牙地址变了、绑定信息丢了升级成功但蓝牙地址变了这个现象在踩坑过程中最容易让人懵。造成这个问题的原因基本是全片擦除或者bootloader搬移固件时把NVDS区域也一起擦掉了。蓝牙地址、绑定信息存在NVDS里被擦掉后芯片会使用随机地址或者默认地址表现出来就是设备“换了个身份”。解决思路是在bootloader的擦除逻辑里明确排除NVDS区域。分区表里已经规划了NVDS区bootloader执行Flash擦除时只擦Application区不碰NVDS区。这个问题的隐蔽之处在于“升级本身成功了”对于只做功能验证的工程师来说容易被忽略但对量产产品来说是要命的。5.4 手机端进度条一直不动或者升级中途断开OTA升级看起来是“手机向设备发数据”本质上是一个需要实时确认的可靠传输过程。进度条不动的常见原因是连接参数不合理。BLE默认连接间隔可能比较长如果单次传输量小整个升级过程会被拉得特别长手机端一旦判断超时就会断连。我建议在进入OTA模式后主动把连接间隔调短、把MTU调大。MTU越大单个包能携带的数据越多OTA总耗时越短。比如默认传20字节一包MTU调成247之后同样一包能传更多数据最直观的表现就是升级速度翻了好几倍。这是纯软件配置不需要改电路。5.5 首次调OTA强烈建议先用小固件验证全链路第一次做OTA时不要拿正式功能固件直接试。我当时的做法是只改一个LED闪烁延时的参数编译出一个“明显能看出变化”的固件包。这样升级成功后通过LED闪烁快慢就能确认是否进了新固件不需要额外接调试器看日志。这个习惯帮我排掉了很多干扰因素。如果升级后LED闪得跟旧固件一样说明升级包没生效如果闪得快了说明OTA链路完全正常问题只可能出在业务逻辑里。对于刚开始接触OTA的工程师这一步非常值得做。问题表现可能原因解决方式升级后无法启动App区写入不完整或地址错误全片擦除后重烧bootloader和App编译器报协议栈函数未定义静态库未加入工程或宏定义错误对比官方静态协议栈例程配置蓝牙地址改变NVDS区被擦除bootloader只擦App区保留NVDS区进度条卡住连接参数不合理、MTU过小进入OTA模式后调短连接间隔、调大MTU能启动但BLE无广播向量表偏移未改修改启动文件VECT_TAB_OFFSET6. 收尾心得项目做完后的几点思考静态协议栈方式下的OTA升级跑通之后我最大的感受是OTA不是一个独立功能而是一套和启动流程、Flash布局、BLE服务、固件打包强耦合的工程体系。如果你在定义产品的时候就想清楚bootloader和分区方案后面接入OTA会顺利很多如果等固件写大了、Flash快满了再回头加OTA能操作的空间就非常有限。我再分享一个小技巧项目里可以把应用版本号分成“功能版本号”和“OTA版本号”两段。功能版本号跟着业务走OTA版本号每次升级自增手机App在连接设备后第一件事就是读取这个版本号。这样现场排查“到底升没升上去”时不用抓日志、不用拆机打开手机App一眼就能确认省下大量沟通成本。OTA升级是一个上线之后才能发现价值、上线之前却容易被低估的功能。希望这份基于LAT1284思路的实操记录能帮你少走一些弯路。