尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ESP32-S3 BLE OTA实战:无Wi-Fi设备如何手机直连升级固件
简介ESP32-S3 BLE OTA升级代码包专为使用ESP-IDF开发的嵌入式工程师提供针对ESP32-S3通过低功耗蓝牙实现固件无线更新的需求可免去频繁插拔USB的麻烦适合产品批量部署后的远程维护场景。压缩包仅5KB共3个文件html文档梳理了从BLE服务初始化、固件分包传输、数据校验、写入FLASH到重启生效的完整OTA链路inscode为可运行的代码入口便于在线查看和修改关键实现gitignore用于规范版本管理整体结构清晰。目前已有101人学习下载非常适合需要快速掌握BLE OTA核心流程或借鉴代码框架的开发者。资源中的核心代码涵盖BLE服务建立、OTA数据接收与解析、分段写入及校验逻辑并配有详细注释同时提供分块大小调整、超时处理和电源管理等方面的部署建议能有效规避升级中断或数据损坏风险显著缩短二次开发与调试周期。 做低功耗无线产品的人迟早会遇到一个尴尬时刻设备装好了、外壳封了、现场部署了然后发现固件有bug。如果产品带Wi-Fi直接走网络OTA就好但很多BLE设备压根没有Wi-Fi模块唯一的无线通道就是蓝牙。去年我手头一个用ESP32-S3做的锂电池传感器项目外壳是超声波焊接的一次性结构每次改固件都得撬壳、焊线、接USB转串口折腾一次一上午就没了一半。索性把BLE OTA做进了固件手机App点一下512KB的镜像几十秒刷完再也没拆过壳。这篇文章就是把那套方案完整拆开思路、架构、核心代码、踩过的坑一次讲透。适合正在用ESP32-S3做低功耗BLE设备、想通过手机直连升级固件、或者刚接触OTA想做技术预研的开发者。代码基于ESP-IDF用的是官方的GATT服务框架迁移到nRF52832、DA14531这类芯片时协议层的思路也完全通用。1. 没有Wi-Fi也能OTABLE升级的定位与适用场景1.1 为什么偏偏是BLE先回答一个很多人纠结的问题既然ESP32-S3有Wi-Fi为什么还要用BLE做OTA最直接的原因你的产品可能根本没有Wi-Fi。ESP32-S3同时支持Wi-Fi和BLE但实际产品为了功耗和成本很多只启用了BLE。我做的那个传感器就是典型——锂电池供电目标待机电流是微安级开了Wi-Fi整板功耗直接失控而且部署现场也没有路由器覆盖。这种情况下OTA可以选择串口、USB或者BLE串口和USB都要求物理接触只有BLE是隔空升级。还有一个现实因素手机生态对BLE太友好了。iOS和Android从系统层面就支持BLE GATT你可以写一个跨平台App或者直接用nRF Connect这类现成工具做升级器不需要给现场人员配电脑、装驱动。如果你用的是Arduino框架做ESP32开发网上也能找到现成的BLE OTA库比如BleOTA移植几行代码就能跑通。我这边更习惯ESP-IDF下面的代码也以IDF为例但库的封装思路完全一致ART-Pi、M5Stack这类基于ESP32-S3的设备都可以照着改。1.2 三种OTA路线对比别选错BLE OTA不是万金油选错方案会让开发进度白白折进去。我根据自己的实测经验把三种常见路线拉出来对比升级方式传输速度是否需要额外硬件典型场景痛点BLE OTA10~100KB/s无需BLE是标配手机直连的穿戴、传感器、门锁速度上限受MTU和连接参数限制Wi-Fi OTA500KB/s以上需要Wi-Fi模块/天线智能音箱、摄像头、联网网关功耗高无网络时不可用串口/UART OTA100KB/s~2MB/s需要USB转串口线产线烧录、研发调试必须物理接触无法远程怎么判断要不要上BLE OTA我总结了三条硬性标准固件体积不大ESP32-S3的固件通常在几百KB到1MB出头BLE OTA完全顶得住。如果固件超过2MBBLE的升级体验会非常煎熬建议重新考虑分区策略或Wi-Fi通道。设备无网络接入设备要么没Wi-Fi模块要么部署位置没有路由器覆盖。升级频率低BLE OTA更适合偶尔修bug、加功能不适合天天刷机。如果产品需要频繁迭代固件还是优先把Wi-Fi OTA做好。还有一点要提前想清楚BLE OTA不只是传输层的事它牵扯到分区表、bootloader、GATT服务设计、主机端协议栈是一个完整的系统工程。下面从底层往上拆。2. 升级链路设计双分区、回滚机制与BLE服务三通道2.1 分区表给固件一个“备胎”BLE OTA最核心的底层机制是双分区备份。ESP32-S3的Flash上至少要规划两个APP分区OTA升级时新固件写入空闲的另一个分区成功后切换启动分区。这样即使升级过程断电、传输中断、固件损坏bootloader仍能启动旧固件设备不会变砖。我的分区表配置长这样4MB Flash# Name, Type, SubType, Offset, Size nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x180000, app1, app, ota_1, 0x190000, 0x180000, spiffs, data, spiffs, 0x310000, 0xF0000,这里app0和app1各分配了1.5MB分别承载当前固件和新固件。otadata分区专门保存当前应该从哪个分区启动的元数据由ESP-IDF的esp_ota_*API自动管理。如果你的固件很大可以把spiffs压缩甚至砍掉把空间匀给APP分区。注意分区表必须在首次烧录时就规划好不能等设备量产了再改。改分区表通常意味着要全片擦除重新烧录现场设备可没法这么操作。2.2 bootloader回滚新固件不行就自动退回有了双分区还要解决一个问题新固件写入成功、设备重启后如果新固件有严重bug运行几秒就崩了怎么办ESP-IDF提供了CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE选项开启后bootloader会检查新固件是否主动“确认自己工作正常”。如果新固件启动后没有在超时时间内调用esp_ota_mark_app_valid_cancel_rollback()bootloader就认为新固件有问题自动回滚到上一个分区。默认超时时间可以配置我用的是10秒有些设备冷启动流程长可能需要调到30秒以上。这个机制的价值在于OTA升级不是“写完就走”而是“写完还要验证”。没有回滚机制的OTA本质上只是把“刷机”这件事无线化了风险一点没降。2.3 BLE服务三通道控制、数据、状态各司其职BLE设备端要提供三个特征Characteristic构成一条完整的OTA通道。我当时的设计是控制特征UUID: 0xFF01接收主机下发的命令比如开始升级、结束升级、查询版本。Write属性主机往这里写一字节命令即可。数据特征UUID: 0xFF02接收固件内容主机把固件切成一块一块的逐块写入。Write属性。状态特征UUID: 0xFF03设备主动上报升级进度和错误码。Notify属性主机必须提前订阅CCCD才能收到通知。为什么一定要拆成三个通道而不是一个因为BLE单次传输的数据量和MTU有关数据必然要分块那就需要一个独立的通道承载“块数据”同时控制命令必须能和数据流并发处理如果混在同一个特征里主机的读写缓冲很容易互相干扰。三个通道逻辑分离后设备端的处理代码也清晰很多——控制特征写命令数据特征写数据状态特征上报进度职责单一。3. ESP32-S3端核心代码从GATT回调到固件落盘3.1 特征定义与初始化我用ESP-IDF的ble_gatt_server示例作为框架自己补了OTA逻辑。首先是特征UUID和句柄的声明#include esp_ota_ops.h #define GATTS_SERVICE_UUID_OTA 0x00FF #define GATTS_CHAR_UUID_OTA_CTRL 0xFF01 #define GATTS_CHAR_UUID_OTA_DATA 0xFF02 #define GATTS_CHAR_UUID_OTA_STATUS 0xFF03 static uint16_t ota_ctrl_handle; // 控制特征句柄 static uint16_t ota_data_handle; // 数据特征句柄 static uint16_t ota_status_handle; // 状态特征句柄 static uint16_t ota_cccd_handle; // 状态特征对应的CCCD句柄 static const esp_partition_t *ota_partition NULL; static esp_ota_handle_t ota_handle 0; static bool ota_in_progress false;初始化时通过esp_ble_gatts_create_attr_tab注册GATT属性表其中状态特征要加ESP_GATT_PERM_READ权限CCCD属性用ESP_GATT_AUTO_RSP自动响应。ESP-IDF的GATT服务框架会自动维护CCCD的值不需要手动处理订阅事件。3.2 写入事件处理分块接收固件BLE OTA的核心逻辑全部集中在GATT事件回调的ESP_GATTS_WRITE_EVT分支里。设备收到主机的write请求后从param-write结构体里取出值和长度再根据句柄判断是控制命令还是固件数据static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { switch (event) { case ESP_GATTS_WRITE_EVT: { uint16_t handle param-write.handle; uint8_t *val param-write.value; uint16_t len param-write.len; if (handle ota_ctrl_handle) { if (val[0] 0x01 ota_in_progress false) { // 开始升级擦除/准备目标分区 ota_partition esp_ota_get_next_update_partition(NULL); esp_ota_begin(ota_partition, OTA_WITH_SEQUENTIAL_WRITES, ota_handle); ota_in_progress true; send_ota_status(OTA_STATUS_STARTED); } else if (val[0] 0x02 ota_in_progress) { // 结束升级校验、设置启动分区、重启 esp_err_t err esp_ota_end(ota_handle); if (err ESP_OK) { esp_ota_set_boot_partition(ota_partition); send_ota_status(OTA_STATUS_FINISHED); vTaskDelay(pdMS_TO_TICKS(100)); esp_restart(); } else { send_ota_status(OTA_STATUS_ERROR); ota_in_progress false; } } } else if (handle ota_data_handle ota_in_progress) { // 固件数据块直接写入目标分区 esp_ota_write(ota_handle, val, len); received_len len; send_ota_status(OTA_STATUS_PROGRESS); } break; } default: break; } }这段代码有几个关键细节值得单独解释esp_ota_get_next_update_partition(NULL)会自动返回当前固件所在分区的“另一个”分区不需要硬编码分区名这样即使从app0启动还是从app1启动代码逻辑都一样。esp_ota_write每次写入的数据会被缓冲在内存里底层以4KB扇区为单位映射到Flash。为了提高性能建议主机端保证每个数据包的大小尽量一致并且最好是4KB扇区的整数倍或约数否则会加剧Flash的擦写次数。esp_ota_end是升级成功与否的分水岭——它会把最后剩余的数据冲刷进Flash并校验整个镜像的hash。如果固件不完整、被截断这里会返回错误必须中止升级。3.3 状态通知与升级完成重启状态通知函数要特别处理一下CCCD否则设备端的notify永远发不出去static void send_ota_status(uint8_t status) { if (ota_cccd_handle 0) return; // 尚未订阅直接丢弃 uint8_t data[2] {status, (uint8_t)(received_len 16)}; esp_ble_gatts_send_indicate(gatts_if, ota_status_handle, 2, data, false); }我上报的状态码定义如下0x01开始升级、0x02接收进度、0x03升级完成、0x04升级失败。进度值只传了个高字节实际工程建议传完整的文件偏移量或者直接传百分比主机端显示更直观。固件接收完毕、esp_ota_set_boot_partition设置好启动分区后调用esp_restart()重启设备。重启后bootloader会根据otadata分区的内容加载新分区随后进入2.2节提到的回滚检查逻辑。4. 主机端联调要点MTU、连接参数与速率实测4.1 MTU协商决定速度上限BLE 4.2/5.0的默认ATT_MTU是23字节这意味着单次write最多只能传20字节有效载荷传512KB固件要写2.5万次慢到怀疑人生。升级前主机端必须先发起MTU协商。ESP32-S3的协议栈支持ATT_MTU最大到512主机请求后设备端会自动接受。Android侧可以这样请求BluetoothGatt.requestMtu(512);iOS侧使用maximumWriteValueLength(for:)获取可用长度后按这个长度分块。实测下来MTU提到185速度大约能到18~22KB/sMTU提到512配合2M PHY和短连接间隔速度能跑到70~100KB/s。OTA一个512KB固件大约5到8秒体验已经相当接近串口刷机了。4.2 write command与write request怎么选BLE的write操作分为两类write request主机发送后设备必须回复确认。每一包都有ACK不会丢但一包一交互速度慢。write command也叫write without response主机只管发设备不回复。速度快但链路丢包时主机无法感知。OTA场景我建议用write command为主中间夹带确认的策略。也就是主机连续发若干包no response数据每发一定数量比如100包后切换成一包write request设备端收到这包“哨兵”后回复确认主机收到确认再继续发下一批。这种“批式确认”既保证了速度又能及时发现链路异常。如果你图省事全程write request也能用只是速度会损失一半以上。4.3 分块大小与扇区对齐除了MTU分块大小还要考虑Flash扇区对齐。ESP32-S3的嵌入式Flash扇区是4KB底层擦除操作以扇区为单位。esp_ota_write内部会把数据缓存并尽量对齐但如果主机每次发送的包大小不固定或者最后一块数据不是4KB的整数倍可能会触发额外的部分页编程Flash状态不稳定时偶尔会失败。我的习惯是MTU 247时单包数据取244字节每16包凑成3904字节接近4KB的整数倍最后一块按实际剩余长度发送设备端esp_ota_write能正确处理非对齐尾部。不要自己把固件补零到4KB倍数再发那会让最终镜像的hash校验失败。5. 踩坑记录CCCD、超时回滚与从OTA到无线调试5.1 经典坑CCCD没订阅状态通知全丢第一次联调时主机端明明连上了设备但设备端调用esp_ble_gatts_send_indicate后主机死活收不到任何通知。查了半天发现是主机没有向状态特征的CCCD写入0x0001。CCCD是BLE协议里的一个“开关”客户端主机要先往这个特征里写值设备端才被允许发送notify。很多人沿用了桌面蓝牙串口那套“连上就能收发”的思维惯性一上来就掉这个坑。解决方案是主机连接成功后主动执行一次gatt.setCharacteristicNotification(statusChar, true); BluetoothGattDescriptor descriptor statusChar.getDescriptor( UUID.fromString(00002902-0000-1000-8000-00805f9b34fb)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor);Android的setCharacteristicNotification只是本地设置真正生效必须配合writeDescriptor。两个都做才完整。5.2 升级后设备反复重启新固件被判无效另一个坑发生在新固件本身有bug的情况下。我有一版固件启动后2秒内会进入一个死循环结果每次OTA完成、设备重启后不到5秒又跳回旧固件从主机端看到的现象是“设备闪断重连、版本号不变”。这正是2.2节的回滚机制在正常工作。排查方法很简单新固件启动后第一时间调用esp_ota_mark_app_valid_cancel_rollback()把确认点前移。如果新固件能稳定执行到这行说明应用初始化之前没问题问题一定出在初始化之后。我当时把标记函数放在vTaskDelay之后就能区分是系统启动阶段崩溃还是业务逻辑崩溃比盲改代码高效很多。注意esp_ota_mark_app_valid_cancel_rollback()应该在确认系统可以“长期工作”后再调用。比如设备联网成功、核心外设初始化完成而不是上电第一行就mark。非要提前mark的话回滚机制就失去意义了。5.3 扩展思路OTA通道还能当无线调试器用OTA做完以后我意识到一件事这条双向BLE通道本质上是设备对外的一个无线数据管道。它不仅能传固件还能传日志、传参数、传调试命令。后来我在项目里基于这个思路做了个“无线调试模式”设备把log输出重定向到BLE状态特征电脑或手机端用Python脚本监听并实时展示日志同时通过控制特征下发调试指令。这样现场设备出问题时无需拆壳就能拉日志、切参数联调效率提升非常明显。这也正好呼应当下很火的“无线AI调试器”方向——把BLE通道和云端AI分析对接设备日志先走BLE到网关再做智能诊断。做法不复杂在设备初始化时把esp_log_set_vprintf重定向到自定义函数将格式化后的日志通过esp_ble_gatts_send_indicate发送出去。带万级日志缓冲都能撑住关键是要控制阻塞时间别把BLE事件循环卡死。回到最开始那个撬壳的问题。做嵌入式开发这些年我最大的体会是像OTA这种“低概率但高代价”的事故救助能力一定要在产品设计阶段预留好等后期在现场出问题再补代价往往是成倍的。BLE OTA的代码量不大但它把设备运维的最后一公里打通了——而这条通道在项目后续的开发调试、远程诊断中还会持续给你回报。如果你正准备给设备加OTA别拖硬件定型前就把分区表和BLE服务设计进去。本文还有配套的精品资源点击获取
RELATED

相关推荐

Proxima:基于全局KV Cache管理,实现vLLM推理吞吐量4倍提升

Proxima:基于全局KV Cache管理,实现vLLM推理吞吐量4倍提升

最近在部署大语言模型推理服务时,你是否也遇到了这样的困境:GPU显存明明还有不少空闲,但服务的吞吐量(QPS)却怎么也上不去,增加并发请求就导致延迟飙升?尤其是在使用流行的 vLLM 框架时&#xf…

📅 2026/10/2 15:25:08
wewin268a打印机驱动从安装到排错全指南:装不上、卡队列一次解决

wewin268a打印机驱动从安装到排错全指南:装不上、卡队列一次解决

简介:伟文wewin268a打印机驱动是一款专为伟文268A标签打印机型号定制的驱动安装程序,面向需要部署该打印设备的生产物流、仓储办公等场景用户,解决系统无法识别设备、打印乱码或异常等常见问题。压缩包内共包含19个文件,主体为dll…

📅 2026/10/30 22:07:14
告别VLOOKUP局限:用INDEX+MATCH构建多条件数据匹配系统

告别VLOOKUP局限:用INDEX+MATCH构建多条件数据匹配系统

这次我们来看一个在数据处理场景下,如何用“陈西表格”替代传统 VLOOKUP 函数,解决乱序、多条件查找匹配与数据筛选的痛点。对于经常处理复杂表格的 Excel 用户来说,VLOOKUP 在数据顺序不一致、需要匹配多个条件时,往往显得力不从…

📅 2026/10/2 2:36:44
MORE NEWS

更多资讯

📰

全网收藏|全球 137 款主流 CAE 仿真软件完整清单,29 大细分领域一次看懂

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

具身智能创新架构解析(11):TVA-EIS跨学科融合创新方法论研究

前沿技术探索:TVA智能体(简称TVA) TVA智能体(Transformer-based Vision Agent)是依托Transformer架构与“因式智能体”理论构建的新型工业视觉系统,也是当前最具代表性的具身视觉技术之一。它有机融合深度强化学习(DRL)、卷积神经网络(CNN)与因式分解算法(FRA),构…

📰

知网AI检测原理详解:读懂这5个判定维度,人工优化事半功倍

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

零安装本地AI运行工具:KoboldCpp让大模型在你电脑上开箱即用

零安装本地AI运行工具:KoboldCpp让大模型在你电脑上开箱即用 【免费下载链接】koboldcpp KoboldCpp - Run GGUF AI models locally with a KoboldAI UI. One File. Zero Install. 项目地址: https://gitcode.com/gh_mirrors/ko/koboldcpp KoboldCpp 是一款免…

📰

Photobooth源码解析(上):PHP 8.4拍照亭Web应用的总体架构

【免费下载链接】photobooth Self-hosted photo booth / photo box web app for events with live preview, collages, and printing. 项目地址: https://gitcode.com/gh_mirrors/photo/photobooth 点击查看 免费下载 Photobooth 是一个自托管(self-hos…

📰

Bootstrap Table 实战:筛选、服务端分页与虚拟滚动的高频配置

Bootstrap Table 实战:筛选、服务端分页与虚拟滚动的高频配置 【免费下载链接】bootstrap-table An extended table for integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation, V…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬