NimBLE 是什么?ESP32 蓝牙协议栈一次讲透:从"协议栈是个啥"到手机收发数据
前言:最近在 ESP32 上用 NimBLE 做了个功能------ESP32 发起蓝牙广播,手机搜到后连上来,直接给板子发数据。一开始我以为要啃几百页蓝牙规范,后来发现调几个函数就跑通了。这篇文章把我搞懂的过程整理出来:协议栈到底是个什么东西、NimBLE 和 Bluedroid 怎么选、GATT 数据模型,以及新手必踩的三个坑。
目录
- 一、我做了个什么事
- 二、协议栈到底是个啥(本文核心)
- [三、我干的事,在 BLE 里叫什么?](#三、我干的事,在 BLE 里叫什么?)
- [四、NimBLE vs Bluedroid 怎么选](#四、NimBLE vs Bluedroid 怎么选)
- 五、数据怎么流:GATT"数据表格"模型
- 六、代码骨架长啥样
- 七、新手必踩的三个坑
- 八、总结
一、我做了个什么事
需求很简单:
- ESP32 上电后发起蓝牙广播
- 手机搜索到设备,通过蓝牙连上来
- 手机发数据到 ESP32,板子收到后处理
就用 ESP-IDF 里的 NimBLE 协议栈,几十行代码搞定。但在它跑通之前,"协议栈"这三个字对我来说特别抽象------下面先把它讲透。
二、协议栈到底是个啥(本文核心)
2.1 从一个问题说起
ESP32 上那颗蓝牙射频,本质上就是个会发电磁波的"喇叭+耳朵"------它只会把 0 和 1 变成电波发出去、收回来。
但光会发比特没用,对方听不懂。手机和 ESP32 之间得先约定一大堆规矩:
- 广播的时候多久喊一次?在哪几个信道喊?
- 数据包怎么打包?包头写什么?地址填谁的?
- 连接后多久通信一次?丢包了要不要重发?
- 数据怎么加密,防止隔壁人偷听?
- 长数据发不下怎么切、怎么拼回来?
这些规矩如果都要自己写,你得啃几百页蓝牙规范文档。没有人这么干------芯片/系统厂商把这套规矩的"执行代码"提前写好、测试好,打包成一个库给你用。
这个库,就是协议栈。
2.2 为什么叫"栈"(Stack)?
因为这些规矩不是一坨,而是分层的,一层摞一层,像叠盘子:
text
┌─────────────────────────┐
│ 你写的应用代码 │ "我要广播!设备名叫XXX"
├─────────────────────────┤
│ GATT / GAP 层 │ 定"数据表格"和"广播/连接"的规矩
├─────────────────────────┤
│ 主机层(Host) │ 打包、加密、重传、分片
├─────────────────────────┤
│ 控制器层(Controller) │ 跳频、收发电波、精确时序
├─────────────────────────┤
│ 射频硬件 │ 真正的"喇叭和耳朵"
└─────────────────────────┘
每层只管自己那摊事:你说一句"开始广播",下面每层自动接力,把电波发出去。
为什么要分层?因为要互通。 乐鑫的芯片、苹果的手机、安卓的手机------不同公司的硬件和系统,只要每一层都遵守同一套规范,互相说话就能听懂。分层就是标准化分工。
2.3 一句话总结
协议栈 = "蓝牙规范的代码化身"。规范是法律,协议栈是执法机构;你写的代码只是来办事的群众。
当你调 ble_gap_adv_start() 时,感觉"一行代码就广播了"------实际上协议栈在幕后替你干了:组广播包、填设备名、按规范间隔在 37/38/39 三个信道轮着喊、应答手机的扫描请求、建立连接时分配时隙、协商 MTU、维护加密密钥......你雇了个专业团队,你只是前台。
三、我干的事,在 BLE 里叫什么?
用大白话描述的功能,在 BLE 规范里都有标准术语,看文档、搜资料时用得上:
| 我说的 | BLE 术语 | 角色说明 |
|---|---|---|
| "发起蓝牙广播" | GAP 广播(Advertising) | ESP32 是外设(Peripheral) |
| "手机连接上 ESP32" | 建立连接 | 手机是中心设备(Central) |
| "手机发数据到 ESP32" | 对特征值执行 Write | 走 GATT 数据模型 |
| "ESP32 收到数据了" | 写回调被触发 | 你的回调函数开始干活 |
| "ESP32 主动给手机推数据" | Notify / Indicate | 反向推送,无需手机轮询 |
外设(Peripheral)/ 中心设备(Central) 这对角色别搞反:谁广播谁就是外设,谁扫谁就是中心。我们 ESP32 是外设,手机是中心。
四、NimBLE vs Bluedroid 怎么选
ESP-IDF 里自带两套蓝牙协议栈,都是同一份 BLE 规范的两种实现:
| 对比项 | NimBLE | Bluedroid |
|---|---|---|
| 来源 | Apache Mynewt 项目移植 | 从安卓系统移植 |
| 支持范围 | 仅 BLE | BLE + 经典蓝牙(SPP/A2DP) |
| 内存占用 | 小(约省一半 RAM/Flash) | 大 |
| API 风格 | 简洁,事件集中一个回调 | 接口较繁琐 |
| 官方态度 | 主推 | 维护兼容 |
选型一句话:只玩 BLE 选 NimBLE(省资源、代码清爽);需要经典蓝牙(SPP 串口、A2DP 音频)才用 Bluedroid。
小彩蛋:Mynewt 怎么读?
读 "My-newt"(迈·纽特)。newt 是蝾螈 ,合起来谐音英文 minute(微小)------小巧得像只小蝾螈,正好呼应它"给小设备用的轻量系统"的定位。前面的 Apache 是 Apache 软件基金会(开源孵化组织)的名字。
五、数据怎么流:GATT"数据表格"模型
NimBLE 把你的 ESP32 组织成一张"数据表格",手机按表读写:
text
ESP32(外设 Peripheral)
└── Service 服务(UUID 标识,比如"环境传感器服务")
└── Characteristic 特征值(真正的数据格子)
├── Read 读 → 手机读 ESP32 的数据
├── Write 写 → 手机发数据给 ESP32 ← 本文用的就是这个
└── Notify 通知 → ESP32 主动推数据给手机
手机"写数据"的那一瞬间,NimBLE 会调用你提前注册好的写回调函数 ,把 (连接句柄, 属性句柄, 数据指针, 长度) 递给你------你只管处理内容,传输细节一概不用操心。
六、代码骨架长啥样
下面是 ESP-IDF 下 NimBLE 的最小骨架(去掉了错误处理和细节,保留主干):
c
// ------ 初始化:雇用这个团队 ------
nimble_port_init(); // 启动协议栈
ble_hs_cfg.sync_cb = on_stack_sync; // 栈就绪后回调你(在里面开广播)
ble_gatts_count_cfg(gatt_svcs); // 注册 GATT 服务表
ble_gatts_add_svcs(gatt_svcs);
nimble_port_run(); // 进入事件循环
// ------ 广播:开始"喊话" ------
ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
&adv_params, gap_event_cb, NULL);
// ------ 所有连接类事件都汇到一个回调 ------
static int gap_event_cb(struct ble_gap_event *event, void *arg) {
switch (event->type) {
case BLE_GAP_EVENT_CONNECT: /* 手机连上了 */ break;
case BLE_GAP_EVENT_DISCONNECT: /* 断开,要重新广播!*/ break;
case BLE_GAP_EVENT_MTU: /* MTU 协商完成 */ break;
}
return 0;
}
// ------ 手机写数据 → 这里被调用 ------
static int chr_write_cb(uint16_t conn, uint16_t attr,
struct ble_gatt_access_ctxt *ctxt, void *arg) {
uint16_t len = OS_MBUF_PKTLEN(ctxt->om); // 取数据长度
// os_mbuf_copydata(ctxt->om, 0, len, buf); 拷出来自己用
return 0;
}
注意 NimBLE 的事件模型:连接、断开、MTU 变化全走 gap_event_cb 一个口 ,数据读写走 gatt_access_ctxt,分工很清晰。
七、新手必踩的三个坑
坑一:MTU 默认只有 23 字节
BLE 默认 MTU 是 23 字节,有效载荷只有 20 字节 。手机要发大数据,得先协商更大的 MTU(关注 BLE_GAP_EVENT_MTU 事件)。手机发数据"发不全",八成是这个原因。
坑二:断开后要重新广播
手机断开连接后,ESP32 会自动停止广播、直接"隐身" ------不重启就再也连不上。必须在 BLE_GAP_EVENT_DISCONNECT 回调里重新调 ble_gap_adv_start()。
坑三:广播包只有 31 字节
BLE 广播包上限 31 字节,设备名稍微长点就放不下。解决办法:把部分内容放进扫描响应包(Scan Response),广播包+响应包两段拼一起用,手机扫描时会自动合并。
八、总结
- 协议栈 = 蓝牙规范的代码化身,分层实现、替你干所有传输苦力活;你写应用层,只管"说一句话 + 等回调"
- NimBLE = ESP-IDF 里轻量级的 BLE 协议栈实现,只支持 BLE 但省一半内存,官方主推;需要经典蓝牙才用 Bluedroid
- 数据流 = 广播(GAP)→ 连接 → 手机对特征值 Write(GATT)→ 你的写回调收到数据
- 三个坑 = MTU 23 字节、断开要重新广播、广播包 31 字节
搞懂"协议栈是个啥"之后,再看 NimBLE 的 API 就不神秘了------它不过是把规范里每一层的规矩,翻译成一个个 C 函数等你调用而已。
本文基于笔者在 ESP32 项目中的实际使用经验整理。如果对你有帮助,欢迎点赞收藏~