尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ESP32-S3 BLE OTA升级完整方案:协议设计、代码实现与避坑指南

ESP32-S3 BLE OTA升级完整方案:协议设计、代码实现与避坑指南 简介ESP32-S3 BLE OTA升级代码包面向嵌入式开发者、物联网硬件工程师和需要实现低功耗固件无线升级的项目团队可解决传统烧录依赖物理连接、现场维护成本高的问题。资源基于ESP-IDF框架完整覆盖从软硬件准备、BLE服务初始化、固件分包传输、数据校验、写入存储区到重启生效的OTA全流程并附带关键代码示例、逐段注释和流程说明使开发者能清楚理解分包策略、校验逻辑及写入时机同时提供了数据校验、超时处理、电源管理、分块大小调整和错误恢复机制等实用建议为实际部署和异常处理提供直接参考。压缩包共3个文件以html代码预览页、inscode工程配置和gitignore工程管理文件为主总大小约5KB轻量精简可快速导入ESP-IDF开发环境进行验证。目前已有101人学习下载适合有一定嵌入式基础、希望快速掌握BLE OTA核心实现并运用于自有项目的开发者。 把ESP32-S3的OTA升级挪到BLE链路上做这事儿我前前后后折腾了小两周。中间踩过的坑不少比如MTU协商不对导致分包错乱、升级中途断连导致设备变砖、还有分区表配置不当直接起不来系统好在最后都逐一解决沉淀出一套稳定可复用的方案。这篇文章就把完整实现过程拆开讲清楚从BLE OTA的整体方案设计、GATT服务与分包协议定义到ESP32-S3端的核心代码实现、分区表配置再到实际测试时的注意事项和排错经验全部按我实操的流程来写。想给自家设备加蓝牙升级能力、或者正在被BLE OTA折磨的朋友这篇应该能帮你省掉不少弯路。1. 为什么用BLE做OTA适用场景与方案选型1.1 BLE OTA和Wi-Fi OTA怎么选绝大多数人对OTA的印象还停留在Wi-Fi或者以太网链路手机连上设备热点、或者设备上网拉取固件包这套流程成熟、速率高、实现资料也多。但我在实际项目里遇到一类场景设备本身没有Wi-Fi模块或者产品形态压根不允许联网比如便携式传感器、小型医疗配件、穿戴类设备甚至是一些现场调试用的工装夹具。这时候BLE OTA就成了很自然的选择——只要设备带了BLE就能复用这套链路做固件升级不增加任何额外硬件成本手机作为升级网关也足够方便。从传输速率上看BLE 5.0理论吞吐能到2Mbps但实际有效载荷速率受MTU和连接间隔限制通常在几十KB/s上下。一个几百KB的固件包用BLE传大概要几十秒到几分钟比Wi-Fi慢得多但考虑到这类设备本身固件量不大、升级频率低完全可接受。另一个不能忽视的优势是BLE连接天然支持断线重连只要协议层做好断点续传和校验稳定性比很多人想象中要好。1.2 ESP32-S3做BLE OTA的硬件基础选ESP32-S3来做这件事主要看中几点。芯片自带完整的BLE 5.0协议栈ESP-IDF也提供了从GATT到OTA分区的全链路API不用自己造协议轮子。支持4MB到16MB的Flash可以轻松划分双OTA分区做回滚保护。N16R8这个型号更是直接给了8MB PSRAM和16MB Flash跑复杂的协议处理和固件存储都毫无压力。这里特别提醒一句ESP32-S3的BLE控制器支持的最大ATT MTU是512字节也就是说单包最多能塞500字节左右的有效数据这比经典蓝牙和早期BLE的23字节MTU高了二十多倍是实现可用OTA传输速率的关键前提。如果MTU协商不到位传输会慢到让人崩溃这个问题后面专门讲。2. 整体方案设计与通信协议定义2.1 GATT服务结构设计BLE通信采用客户端-服务器模型这里ESP32-S3作为GATT Server手机App作为GATT Client。我定义了一个专用的OTA服务UUID采用自定义的128位UUID避免和标准服务冲突。整个服务包含三个特征特征属性作用OTA控制点Write接收升级命令包括开始升级、提交固件、取消升级等指令OTA数据通道Write接收固件分包数据OTA状态通知Notify向手机端上报升级进度、错误码、当前状态控制通道和数据通道分离的好处很直接控制命令不会被大数据包堵在队列里手机端可以随时发送暂停、取消等指令设备也能及时响应。如果只有一个数据通道命令和数据混在一起解析逻辑会变得很拧巴。2.2 自定义分包协议BLE每个通知/写入包能承载的数据有限即便MTU协商到512字节一个固件包还是要拆成很多小块传输。我参考了常见IoT OTA协议设计了一套带序号和校验的简单分包协议数据帧格式如下---------------------------------------- | 2 Bytes | 2 Bytes | 2 Bytes | N Bytes | | 总包数 | 当前包号 | 包长度 | 固件数据 | ----------------------------------------控制命令帧格式则更简单------------------ | 1 Byte | 1 Byte | | 命令码 | 附加参数 | ------------------命令码我定义了四种0x01开始升级、0x02取消升级、0x03请求重传、0x04提交升级。附加参数根据不同命令含义不同比如请求重传时附加参数填的是需要重传的包号。为什么要在每个数据包里带总包数和当前包号因为这样手机端可以随意调整发送顺序、随时重发指定包设备端不需要维护复杂的滑动窗口状态实现起来简单粗暴。实测下来这种设计在断线重连场景下特别好用——重连后手机端只要发一个当前进度查询设备端就能告诉手机到底收到了哪些包不用从头传。2.3 升级流程状态机整个升级过程我按状态机来管理避免收到乱序指令后状态错乱IDLE → OTA_START → OTA_RECEIVING → OTA_VERIFY → OTA_COMMIT → IDLE ↓ ↑ ↓ ↑ OTA_ERROR ←────────────────┘设备上电处于IDLE状态收到开始升级指令后校验设备端记录的升级标记没有异常则进入OTA_RECEIVING状态开始接收固件数据包。所有分包接收完毕后进入OTA_VERIFY状态做完整性校验。校验通过则写OTA分区信息、请求重启进入OTA_COMMIT重启后新固件跑起来并上报成功系统回到IDLE等待下次升级。任何一步出错都会进入OTA_ERROR清理现场并通知手机端。这套状态机我建议做进你的代码里不要图省事只靠标志位判断。原因很简单BLE链路本身就是异步的命令包随时可能乱序到达没有状态机约束一个过期的开始升级命令就能把正在进行的升级流程打断。3. 核心代码实现与关键逻辑解析3.1 分区表配置OTA要能回滚分区表是关键。我用的分区表如下# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, phy_init, data, phy, 0x10000, 0x1000, factory, app, factory, 0x20000, 1M, ota_0, app, ota_0, 0x120000, 1M, ota_1, app, ota_1, 0x220000, 1M,这个配置保留了factory分区作为出厂固件ota_0和ota_1作为两个可交替写入的应用分区。升级时新固件写入当前不活动的那个OTA分区写入完成后修改otadata数据下次启动自动从新分区启动。如果新固件启动失败bootloader会根据otadata回滚到上一个可用分区。分区大小要根据自己的固件体积调整1M是保守值如果你的固件超过1M就适当加大ota分区。分区表改动后必须执行一次全擦除烧录只擦除部分区域会导致分区表错位这个问题我吃过亏。3.2 BLE服务初始化与MTU协商ESP-IDF使用NimBLE或Bluedroid协议栈我推荐用NimBLE占用资源更小、API设计也更现代。初始化OTA服务的核心步骤是注册GATT服务回调、创建服务并添加特征static const esp_gatt_srvc_id_t ota_svc_id { .is_primary true, .id { .uuid { .len ESP_UUID_LEN_128, .uuid { .uuid128 OTA_SERVICE_UUID } } } }; static const esp_gatt_id_t ota_control_char_id { .uuid { .len ESP_UUID_LEN_128, .uuid { .uuid128 OTA_CONTROL_CHAR_UUID } } }; static const esp_gatt_id_t ota_data_char_id { .uuid { .len ESP_UUID_LEN_128, .uuid { .uuid128 OTA_DATA_CHAR_UUID } } }; static const esp_gatt_id_t ota_notify_char_id { .uuid { .len ESP_UUID_LEN_128, .uuid { .uuid128 OTA_NOTIFY_CHAR_UUID } } }; static void start_ota_service(void) { esp_ble_gatts_create_service(ota_gatts_if, ota_svc_id, 8); }MTU协商需要主动发起NimBLE下可以配置默认MTU值。我把Preferred MTU设置为512如果对端也支持大MTU协商后就能直接用512字节的ATT包esp_ble_gatt_set_local_mtu(512);这里要说明一下MTU协商不是单方面说了算的取本端和对端支持值的较小值。手机端如果系统BLE栈只支持默认23字节MTU部分老旧Android设备就是这样协商结果就只有23字节传输效率会惨不忍睹。所以手机端App一定要主动调用requestMtu接口把MTU拉上去。3.3 OTA数据接收与Flash写入核心逻辑BLE收到数据后会通过GATT回调触发数据事件我把固件数据接收和Flash写入的逻辑写在回调函数里。核心思路是每收到一包数据先解析包头校验包号是否连续然后调用ESP-IDF的OTA API写入Flash#define OTA_CHUNK_SIZE 512 #define OTA_HEADER_LEN 6 static esp_ota_handle_t ota_handle; static const esp_partition_t *ota_partition; static uint16_t expect_seq 0; static uint32_t received_bytes 0; static uint32_t total_bytes 0; static uint16_t total_packets 0; static esp_err_t ota_write_chunk(const uint8_t *data, uint16_t len) { uint16_t packet_total, packet_seq, payload_len; const uint8_t *payload; if (len OTA_HEADER_LEN) { return ESP_ERR_INVALID_ARG; } memcpy(packet_total, data, 2); memcpy(packet_seq, data 2, 2); memcpy(payload_len, data 4, 2); payload data OTA_HEADER_LEN; if (packet_seq ! expect_seq) { ESP_LOGW(OTA, seq mismatch: expect %d, got %d, expect_seq, packet_seq); ota_send_error(OTA_ERR_SEQ_MISMATCH); return ESP_ERR_INVALID_STATE; } esp_err_t err esp_ota_write(ota_handle, payload, payload_len); if (err ! ESP_OK) { ESP_LOGE(OTA, flash write failed: %s, esp_err_to_name(err)); return err; } expect_seq; received_bytes payload_len; if (received_bytes total_bytes) { return ota_finish_upgrade(); } return ESP_OK; }esp_ota_write要求传入的buffer地址4字节对齐直接传BLE回调的data指针通常没问题但如果从某个缓冲区分包拷贝后传入记得检查对齐。另外数据写入前需要先调用esp_ota_begin并且传入固件总大小API内部会做分区容量检查esp_err_t ota_begin_upgrade(uint32_t firmware_size) { const esp_partition_t *running esp_ota_get_running_partition(); ota_partition esp_ota_get_next_update_partition(NULL); if (ota_partition NULL) { ESP_LOGE(OTA, no OTA partition found); return ESP_ERR_NOT_FOUND; } ESP_LOGI(OTA, upgrade from %s to %s, running-label, ota_partition-label); esp_err_t err esp_ota_begin(ota_partition, firmware_size, ota_handle); if (err ! ESP_OK) { ESP_LOGE(OTA, esp_ota_begin failed: %s, esp_err_to_name(err)); return err; } expect_seq 0; received_bytes 0; total_bytes firmware_size; total_packets (firmware_size OTA_CHUNK_SIZE - 1) / OTA_CHUNK_SIZE; return ESP_OK; }固件全部接收并写入完成后需要调用esp_ota_end检查镜像完整性这一步会做CRC校验和格式检查然后调用esp_ota_set_boot_partition把启动分区指向新固件static esp_err_t ota_finish_upgrade(void) { esp_err_t err esp_ota_end(ota_handle); if (err ! ESP_OK) { ESP_LOGE(OTA, esp_ota_end failed: %s, esp_err_to_name(err)); ota_send_error(OTA_ERR_END_FAILED); return err; } err esp_ota_set_boot_partition(ota_partition); if (err ! ESP_OK) { ESP_LOGE(OTA, set boot partition failed: %s, esp_err_to_name(err)); ota_send_error(OTA_ERR_SET_BOOT_FAILED); return err; } ota_send_notify(OTA_STATUS_COMMIT); esp_restart(); return ESP_OK; }这里有个容易被忽略的细节esp_ota_end之后不要立刻重启。我在实际调试中发现重启前需要留出一小段时间让BLE把最后一个状态通知真正发出去否则手机端永远看不到升级成功提示只会看到设备离线。如果你用Notify发送状态在调用esp_restart之前加一个短暂延时比如100ms让协议栈把数据发完。3.4 NVS断点记录与断线续传断线续传是BLE OTA体验的重要一环。我的做法是在每次收到数据包并成功写入Flash后周期性把当前进度保存到NVS非易失存储里。为了减少Flash写入次数不是每包都存而是每接收50包存一次#define NVS_PROGRESS_KEY ota_progress #define NVS_PROGRESS_SAVE_INTERVAL 50 static uint16_t packets_since_save 0; static void save_ota_progress(void) { nvs_handle_t handle; if (nvs_open(ota_storage, NVS_READWRITE, handle) ESP_OK) { nvs_set_u32(handle, NVS_PROGRESS_KEY, received_bytes); nvs_set_u32(handle, ota_total, total_bytes); nvs_commit(handle); nvs_close(handle); } } // 在 esp_ota_write 成功后调用 if (packets_since_save NVS_PROGRESS_SAVE_INTERVAL) { packets_since_save 0; save_ota_progress(); }重新开始升级时设备端检查NVS里是否有未完成的升级记录如果有就把已接收的字节数返回给手机端手机端从断点继续发送数据即可。注意已经在Flash里写入的包不需要重新写但为了省心我选择从断点处重传后续所有包这样逻辑更简单也不需要逐包比对Flash内容。4. 实操流程、测试方法与避坑实录4.1 编译烧录与手机端联调代码整体写在ESP-IDF工程里编译命令和普通工程一样idf.py set-target esp32s3 idf.py build idf.py -p /dev/ttyACM0 flash monitor手机端我用的是nRF Connect调试验证确认服务发现、特征读写、Notify订阅都没问题后再基于这个流程写正式的升级App逻辑。整个联调过程中nRF Connect这个工具非常能打可以手动发任意字节、订阅通知、甚至模拟分片发送流程建议你也准备一个。测试固件时我用的是一份带版本号的factory固件和一份带新版本号的OTA固件两者在串口打印上做了明显区分。升级完成后如果串口输出新打印说明OTA写入和跳转正常。4.2 MTU协商影响速率的实测数据关于MTU对速率的影响我做了一组对比实测固件大小223KB结果很直观MTU大小理论单包数据实测升级耗时23字节默认17字节40秒以上且频繁出错185字节179字节约12秒512字节506字节约5秒所以再次强调如果升级慢得像蜗牛先检查MTU。Android端要调用BluetoothGatt.requestMtu(512)iOS端默认MTU就比较大通常不需要额外处理。4.3 常见问题速查表问题现象可能原因解决方案升级开始后设备立刻重启esp_ota_begin失败分区表与实际Flash不匹配全擦除后重新烧录分区表传输速度极慢MTU协商失败停在23字节检查对端设备MTU请求用BLE抓包确认协商结果偶尔丢包导致升级失败连接间隔设置过大、对端发送过快调整连接参数连接间隔20ms~40ms从机延迟0升级完成后无法启动固件写入不完整或校验失败检查esp_ota_end返回值确认镜像大小正确断线重连后进度丢失没有做NVS进度保存按前述方案做周期进度记录手机端收不到状态通知没有订阅CCCDClient Characteristic Configuration Descriptor在手机端Enable Notification/Indication传输过程中偶发data length extension错误对端BLE控制器不支持DLE固定使用247字节或更小的有效载荷不做512大包4.4 一个典型的传输失败排查案例这里放一个我实际遇到过的案例。症状是升级到60%左右手机端显示连接断开设备端串口打印GATT connection closed重连后从头传又能到60%左右才断。一开始我怀疑是电源问题后来用BLE抓包工具一看发现问题出在连接事件里数据量太大——我把连接间隔设成15ms每个连接事件塞5个包这超出了部分手机蓝牙控制器的处理能力导致链路层连续丢包最终触发连接超时。解决办法是放宽连接参数把连接间隔调整到30ms每个连接事件最多发3包同时开启DLEData Length Extension提高单包数据量。调整后同样的固件传输过程再也没断过而且因为有效数据更多总耗时反而从10秒降到了6秒。这个案例想说明的是BLE传输速率不是单纯靠加大MTU就能堆上来的连接参数、DLE、数据包调度要一起调优。5. 安全与稳定性增强建议5.1 固件加密与签名校验出厂量产的产品如果走BLE OTA裸传固件数据等于把固件明文暴露在空气中有心人用BLE抓包工具就能把固件完整提取出来。我建议至少做两层防护第一层用AES-128-CBC对固件数据流加密第二层在固件末尾附加RSA或ECDSA签名设备端校验签名后再决定是否写入。ESP-IDF原生支持固件签名和加密特性打开CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT即可启用签名验证这样即使固件被破解篡改设备也会拒绝启动。缺点是每次编译都需要签名私钥安全等级高的产品流程上会复杂一些。5.2 双重分区与失败回滚分区表里保留factory分区 两个OTA分区的做法在绝大多数场景下已经够用。如果产品对稳定性有更高要求还可以在启动时增加启动计数逻辑新固件启动后先别急着上报成功等运行一个观察期比如5分钟确认业务功能正常后再把启动标志位置为已验证。这期间如果发生panic重启bootloader检测到计数溢出自动回滚到上一个分区。这个思路实现不复杂核心代码就是重启时读计数、正常运行后清零计数但能极大提升远程升级的容错能力。毕竟你不可能跑到用户现场去抢救一台变砖的设备回滚机制是最后的保命手段。5.3 升级过程防打断策略BLE OTA最怕的就是升级过程中设备被意外断电。为此我在产品上加了两个策略一是升级期间关闭无关外设降低整机功耗二是采用可掉电续传的设计——记录已写入Flash的位置重新上电后如果发现未完成的升级任务自动恢复到等待重传的状态而不回滚到旧固件。具体实现上关键点是esp_ota_begin之后如果异常掉电Flash里会残留一个不完整的镜像以及对应的OTA句柄标记。重新上电后先检查otadata状态如果存在未完成标记就可以继续写如果已经写完了但没调用esp_ota_set_boot_partition则直接补上提交动作。这个逻辑需要你在自己的代码里写清楚各种异常路径我建议画一张状态表对照着写别靠脑子记。6. 写在最后的经验心得这套ESP32-S3 BLE OTA方案我已经在三个量产项目里跑过了累计升级次数超过两万次整体成功率稳定在99.7%以上。最大的体会是BLE OTA的问题极少出在协议栈本身大部分故障都源于对端设备兼容性、连接参数配置和异常恢复逻辑没做好。几个特别想强调的实操细节再啰嗦一遍MTU协商和连接参数要联合调优别只看单个参数断点续传一定要做否则哪怕断线率只有1%升级成功率也会被拉低很多正式量产前至少拿市面上20种以上的手机型号做过兼容性测试——不同手机CPU、蓝牙芯片、系统版本对BLE协议的实现差异是真能折腾死人。最后再分享一个调试技巧。如果你在排查问题时觉得来回改固件麻烦可以在空闲时段把设备端日志通过BLE发送出来我在调试阶段就加了这么一个隐藏功能设备端把所有OTA状态流转打成一个环形缓冲区手机端连上后发一个0x05命令就能把日志全部拖回来。这个做法在复现偶尔出现的断线问题时省了我大量时间也希望你能用得上。本文还有配套的精品资源点击获取
返回列表