【ESP32】ESP32-C3 NimBLE SIMP 服务配置与初始化方案
ESP32-C3 NimBLE SIMP 服务配置与初始化方案
适用平台:ESP32-C3 | ESP-IDF 版本:v5.3.5 | 蓝牙协议栈:NimBLE| GATT 服务:SIMP(Simple Message Protocol)
一、概述
SIMP 服务是一个基于 BLE GATT 的自定义数据传输服务,通过NimBLE Host 协议栈实现。它提供 3 个特征(Characteristic)完成与客户端设备的数据交互。
| 特征 | UUID 后缀 | 方向 | 属性 | 说明 |
|---|---|---|---|---|
| READ | 6E40-xxxx-...-0002 | 客户端 → 读取设备数据 | BLE_GATT_CHR_F_READ | 客户端主动读取设备缓冲区数据 |
| WRITE | 6E40-xxxx-...-0003 | 客户端 → 写入设备 | `BLE_GATT_CHR_F_WRITE | F_WRITE_NO_RSP` |
| NOTIFY | 6E40-xxxx-...-0004 | 客户端 ← 设备推送 | BLE_GATT_CHR_F_NOTIFY | 设备主动推送数据(需客户端订阅 CCCD) |
为什么选择 NimBLE 而不是 Bluedroid?
NimBLE 是 Apache MyNewt 项目的一部分,由 ESP-IDF 以组件形式提供。相比 Bluedroid,它:
- 占用更小的 Flash/RAM:协议栈核心更小,适合资源受限的 MCU
- 代码更紧凑:API 设计更精简,GATT 服务声明式注册(不需要事件回调链)
- 与 FreeRTOS 集成更直接:通过
nimble_port_run()阻塞式运行 - CCCD 自动创建:当特征标记为 NOTIFY/INDICATE 时,CCCD 描述符由协议栈自动创建
二、硬件抽象层架构
本项目采用 HAL(Hardware Abstraction Layer)架构,BLE 模块通过HALDescriptor_t结构体注册到运行时框架:
constHALDescriptor_t g_hal_ble={"ble",// 模块名称HAL_BLE_Init,// 初始化函数指针HAL_BLE_Load,// 加载(校准)函数指针HAL_BLE_Tick,// 周期 Tick 函数指针HAL_BLE_Load_GetState// 状态快照函数指针};框架启动时会自动调用HAL_BLE_Init()完成整个 BLE 初始化链。
三、宏定义详解
3.1 模块总开关
#defineHAL_BLE_ENABLED1- 作用:控制整个 BLE 模块是否编译和运行
- 设为 0:所有函数指针置 0,
HAL_BLE_Init()内部代码不编译 - 设为 1:启用 BLE 模块,并允许 SIMP 服务注册
3.2 SIMP 服务开关
#ifHAL_BLE_ENABLED#defineHAL_BLE_SIMP_SERVICE_ENABLED1#endif- 设计原因:通过条件编译实现服务注册的可选性
- 当
HAL_BLE_ENABLED = 0时,HAL_BLE_SIMP_SERVICE_ENABLED不会被定义,HAL_BLE_COM_Init()中 SIMP 相关的#if代码块不被编译
3.3 设备名称
#defineHAL_BLE_DEVICE_NAME"TEST-ESP32C3"- BLE 广播包中携带的设备名称,手机 APP 扫描时显示的设备名
- 通过
ble_svc_gap_device_name_set()设置(NimBLE 的 Gap 服务内部函数)
3.4 广播间隔
#defineHAL_BLE_ADV_INTERVAL_MIN48// 48 × 0.625ms = 30ms#defineHAL_BLE_ADV_INTERVAL_MAX64// 64 × 0.625ms = 40ms- BLE 广播间隔单位是0.625ms(一个 BLE Slot)
- NimBLE 中范围:
BLE_GAP_ADV_ITVL_MIN(0x0020 = 20ms) ~BLE_GAP_ADV_ITVL_MAX(0x4000 = 10.24s) - 推荐值 48~64:兼顾低功耗和快速发现
3.5 缓冲区大小
#defineHAL_BLE_SIMP_READ_BUFFER_SIZE256U#defineHAL_BLE_SIMP_WRITE_BUFFER_SIZE256U#defineHAL_BLE_SIMP_NOTIFY_BUFFER_SIZE256U- 定义 256 字节是因为 BLE 单次 MTU 默认 23 字节,最大可协商到 517 字节
HAL_BLE_SIMP_NOTIFY_BUFFER_SIZE用于限制HAL_BLE_SIMP_SendNotify()的单次发送长度,避免超出 MTU- 发送 Notify 时,NimBLE 会将数据切分为 Link Layer 数据包
3.6 测试模式
#defineHAL_BLE_SIMP_TEST_MODE1- 测试模式启用后,
HAL_BLE_Init()中会:- 向 READ 缓冲区写入测试数据
{0x23, 0x55, 0xAA, 0x0D, 0x0A} - 注册一个测试回调
HAL_BLE_SIMP_TEST_Callback用于日志打印 WRITE 收到的数据
- 向 READ 缓冲区写入测试数据
- 实际应用层应设置为 0,由应用代码注册自己的回调
四、UUID 配置与字节序转换
4.1 UUID 定义
#defineHAL_BLE_SIMP_SERVICE_UUID"6E400001-B5A3-F393-E0A9-E50E24DCCA9E"#defineHAL_BLE_SIMP_READ_CHAR_UUID"6E400002-B5A3-F393-E0A9-E50E24DCCA9E"#defineHAL_BLE_SIMP_WRITE_CHAR_UUID"6E400003-B5A3-F393-E0A9-E50E24DCCA9E"#defineHAL_BLE_SIMP_NOTIFY_CHAR_UUID"6E400004-B5A3-F393-E0A9-E50E24DCCA9E"采用128-bit 自定义 UUID,格式为标准的xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。NimBLE 内部提供ble_uuid_init_from_buf()函数负责字节序转换。
4.2 UUID 字节序转换
Bluetooth 协议栈中 UUID 使用**小端(little-endian)**字节序存储,而标准字符串格式为大端(big-endian)。代码中通过自定义函数完成转换:
staticvoidble_uuid_init_from_str_hex(ble_uuid_any_t*uuid,constchar*str){uint8_tbytes[16];intbyte_idx=0;// 1. 解析字符串为 16 字节大端数组(跳过连字符)for(inti=0;i<36&&byte_idx<16;i++){if(str[i]=='-')continue;uint8_thigh=HAL_BLE_HexCharToByte(str[i]);uint8_tlow=HAL_BLE_HexCharToByte(str[++i]);bytes[byte_idx++]=(high<<4)|low;}// 2. 反转为小端字节序uint8_ttemp_bytes[16];memcpy(temp_bytes,bytes,16);for(inti=0;i<16;i++){bytes[i]=temp_bytes[15-i];}// 3. 调用 NimBLE 内部函数初始化 UUID 结构ble_uuid_init_from_buf(uuid,bytes,16);}为什么要自己写?NimBLE 提供了ble_uuid_init_from_buf(),但没有提供ble_uuid_from_str(),因此需要在应用层完成字符串解析 + 反转,然后交给 NimBLE 处理。
五、GATT 服务声明式注册
NimBLE 与 Bluedroid最大区别在于 GATT 服务注册方式:
| 特性 | Bluedroid | NimBLE |
|---|---|---|
| 注册方式 | 事件驱动,分步调用create_service→add_char→add_descr→start_service | 声明式数组,一次性定义服务和特征的完整结构 |
| 回调链 | 异步事件链:REG_EVT→CREATE_EVT→ADD_CHAR_EVT× N →ADD_CHAR_DESCR_EVT→START_EVT | 同步调用:ble_gatts_count_cfg()→ble_gatts_add_svcs(),完成后立即可用 |
| CCCD 创建 | 必须手动调用esp_ble_gatts_add_char_descr()添加 CCCD 描述符 | CCCD 由 NimBLE自动创建(当特征标志包含BLE_GATT_CHR_F_NOTIFY或BLE_GATT_CHR_F_INDICATE,且descriptors = NULL) |
| 句柄 | 由协议栈在异步回调中返回 | 由数组元素的.val_handle字段直接接收 |
5.1 GATT 服务数据结构
// 变量声明区(static 全局)staticble_uuid_any_tg_simp_service_uuid;staticble_uuid_any_tg_simp_read_uuid;staticble_uuid_any_tg_simp_write_uuid;staticble_uuid_any_tg_simp_notify_uuid;staticstructble_gatt_svc_defg_simp_svc_def[2];// 一个有效服务 + 终止符staticstructble_gatt_chr_defg_simp_chr_defs[4];// 3个特征 + 1个终止符关键要点:
ble_gatt_svc_def[]必须以{ .type = 0 }结尾(NimBLE 通过检测.type == 0识别数组结束)ble_gatt_chr_def[]必须以{ .uuid = NULL }结尾(NimBLE 通过检测.uuid == NULL识别数组结束)- 每个特征的
.access_cb不能为 NULL,即使是 NOTIFY 特征也需要一个空的 access 回调(ble_gatts_chr_is_sane()会断言检查)
5.2 特征定义步骤
// 清空数组(确保终止符正确)memset(g_simp_chr_defs,0,sizeof(g_simp_chr_defs));/* 特征 1: READ */g_simp_chr_defs[0].uuid=&g_simp_read_uuid.u;g_simp_chr_defs[0].access_cb=HAL_BLE_SIMP_GattAccessCallback;g_simp_chr_defs[0].flags=BLE_GATT_CHR_F_READ;g_simp_chr_defs[0].val_handle=&g_ble_simp_read_char_handle;/* 特征 2: WRITE */g_simp_chr_defs[1].uuid=&g_simp_write_uuid.u;g_simp_chr_defs[1].access_cb=HAL_BLE_SIMP_GattAccessCallback;g_simp_chr_defs[1].flags=BLE_GATT_CHR_F_WRITE|BLE_GATT_CHR_F_WRITE_NO_RSP;g_simp_chr_defs[1].val_handle=&g_ble_simp_write_char_handle;/* 特征 3: NOTIFY */g_simp_chr_defs[2].uuid=&g_simp_notify_uuid.u;g_simp_chr_defs[2].access_cb=HAL_BLE_SIMP_GattAccessCallback;g_simp_chr_defs[2].flags=BLE_GATT_CHR_F_NOTIFY;g_simp_chr_defs[2].val_handle=&g_ble_simp_notify_char_handle;g_simp_chr_defs[2].descriptors=NULL;// 关键:= NULL 让 NimBLE 自动创建 CCCDble_gatt_chr_def字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
uuid | ble_uuid_t * | 指向特征 UUID(必须非 NULL,非终止符时) |
access_cb | ble_gatt_access_fn * | 读写回调函数(必须非 NULL) |
flags | uint16_t | 特征属性标志(BLE_GATT_CHR_F_*) |
val_handle | uint16_t * | 指向接收特征值句柄的变量(NimBLE 注册时回填) |
descriptors | struct ble_gatt_dsc_def * | 自定义描述符数组。= NULL时 NimBLE 自动创建 CCCD |
5.3 服务定义与注册
memset(g_simp_svc_def,0,sizeof(g_simp_svc_def));g_simp_svc_def[0].type=BLE_GATT_SVC_TYPE_PRIMARY;// 主服务g_simp_svc_def[0].uuid=&g_simp_service_uuid.u;g_simp_svc_def[0].characteristics=g_simp_chr_defs;// 指向特征数组// Step 1: 计算属性所需资源(内部会调整 ble_hs_max_attrs 等计数)intrc=ble_gatts_count_cfg(g_simp_svc_def);if(rc!=0){HAL_BLE_ErrorLog("ble_gatts_count_cfg failed: %d",rc);return2;}// Step 2: 注册服务到 NimBLErc=ble_gatts_add_svcs(g_simp_svc_def);if(rc!=0){HAL_BLE_ErrorLog("ble_gatts_add_svcs failed: %d",rc);return2;}ble_gatts_count_cfg()的作用:
- 内部遍历服务/特征/描述符数组,计算需要多少个属性句柄、CCCD 等资源
- 自动调整全局变量
ble_hs_max_attrs、ble_hs_max_client_configs(注意:不是ble_hs_cfg.max_attrs) - 失败返回
BLE_HS_EINVAL:通常是因为ble_gatts_chr_is_sane()检测到access_cb == NULL,或者数组缺少终止符
六、CCCD(Client Characteristic Configuration Descriptor)
CCCD 是 NOTIFY 功能必需的描述符(UUID = 0x2902)。客户端写入 CCCD 的值决定是否接收推送:
0x0000= 取消订阅0x0001= 启用 Notify0x0002= 启用 Indicate
6.1 NimBLE 的自动 CCCD
当ble_gatt_chr_def.descriptors = NULL且特征的 flags 包含BLE_GATT_CHR_F_NOTIFY或BLE_GATT_CHR_F_INDICATE时,NimBLE 自动:
- 创建一个 CCCD 描述符属性(UUID = 0x2902)
- 设置 CCCD 的权限为可读/可写(
BLE_ATT_F_READ | BLE_ATT_F_WRITE) - 在 GATT 层维护每个连接的订阅状态
- 通过
BLE_GAP_EVENT_SUBSCRIBE事件通知应用层订阅状态变化
这就是为什么代码中g_simp_chr_defs[2].descriptors = NULL是正确的设置。
6.2 订阅事件(SUBSCRIBE)
caseBLE_GAP_EVENT_SUBSCRIBE:{// 检查是否是 Notify 特征的句柄if(event->subscribe.attr_handle==g_ble_simp_notify_char_handle){if(event->subscribe.cur_notify>0||event->subscribe.cur_indicate>0){HAL_BLE_DRAM_InfoLog("NOTIFY ENABLED for SIMP service!");}else{HAL_BLE_DRAM_InfoLog("Notify disabled for SIMP service");}}break;}ble_gap_event.subscribe关键字段:
| 字段 | 含义 |
|---|---|
conn_handle | 连接句柄 |
attr_handle | 特征值的属性句柄(与g_ble_simp_notify_char_handle比较) |
prev_notify | 之前的 Notify 订阅状态(0=未订阅,1=已订阅) |
cur_notify | 当前的 Notify 订阅状态(客户端写入 CCCD 后的状态) |
prev_indicate/cur_indicate | Indicate 订阅状态 |
七、编译配置(sdkconfig)
7.1 NimBLE 必需配置
# ========== 蓝牙总开关 ========== CONFIG_BT_ENABLED=y CONFIG_BT_CONTROLLER_ENABLED=y # ========== NimBLE 协议栈(取代 Bluedroid) ========== CONFIG_BT_NIMBLE_ENABLED=y CONFIG_BT_NIMBLE_HOST_ENABLED=y # ========== 控制器配置 ========== CONFIG_BT_CTRL_MODE_EFF=y # ========== NimBLE 内存配置 ========== CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=12 CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=256 CONFIG_BT_NIMBLE_ACL_BUF_COUNT=12 CONFIG_BT_NIMBLE_ACL_BUF_SIZE=255 # ========== GATT / GAP ========== CONFIG_BT_NIMBLE_GATT_MAX_PROCS=4 CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="nimble" # ========== 安全/配对 ========== CONFIG_BT_NIMBLE_SM_LEGACY=y CONFIG_BT_NIMBLE_SM_SC=y # ========== 存储(绑定信息保存到 NVS) ========== CONFIG_BT_NIMBLE_STORE_CONFIG=y # ========== 调试 ========== CONFIG_BT_NIMBLE_LOG_LEVEL=0 CONFIG_BT_NIMBLE_DEBUG=y7.2 CMakeLists.txt 组件依赖
idf_component_register( REQUIRES driver esp_event esp_partition freertos log lwip nvs_flash mbedtls bt )7.3 头文件引入
#include"nimble/nimble_port.h"#include"nimble/nimble_port_freertos.h"#include"host/ble_hs.h"#include"host/ble_hs_id.h"#include"host/ble_gap.h"#include"host/ble_gatt.h"#include"host/ble_uuid.h"#include"host/util/util.h"#include"services/gap/ble_svc_gap.h"#include"services/gatt/ble_svc_gatt.h"#include"store/config/ble_store_config.h"#include"esp_nimble_hci.h"#include"esp_mac.h"#include"nvs_flash.h"八、NimBLE 初始化完整流程
8.1 总体流程
app_main() └── 框架自动调用 HAL_BLE_Init() ├── 1. nvs_flash_init() └── 2. HAL_BLE_COM_Init() ├── 2.1 nimble_port_init() ├── 2.2 配置 ble_hs_cfg 回调 ├── 2.3 ble_svc_gap_init() + ble_svc_gatt_init() ├── 2.4 转换 4 个 UUID ├── 2.5 构造 g_simp_chr_defs[0..2] ├── 2.6 构造 g_simp_svc_def[0] ├── 2.7 ble_gatts_count_cfg() ├── 2.8 ble_gatts_add_svcs() ├── 2.9 ble_store_config_init() └── 2.10 xTaskCreate(HAL_BLE_NimbleHostTask) 【异步】nimble_port_run() → sync_cb(HAL_BLE_OnSync) → 设置 MAC 地址 → 设置设备名 → 启动广播8.2ble_hs_cfg回调配置详解
| 回调 | 类型 | 触发时机 | 用途 |
|---|---|---|---|
reset_cb | void (*)(int) | 协议栈复位 | 打印错误日志 |
sync_cb | void (*)(void) | Host-Controller 同步 | 设置地址/名称 + 启动广播 |
gatts_register_cb | void (*)(ctxt, arg) | 特征注册完成 | 调试日志 |
store_status_cb | int (*)(event, arg) | NVS 存储操作 | 默认使用ble_store_util_status_rr |
8.3HAL_BLE_OnSync详解
staticvoidHAL_BLE_OnSync(void){ble_addr_taddr;// Step 1: esp_read_mac() 获取 Wi-Fi MAC(大端)// Step 2: 反转为小端存入 addr.val// Step 3: ble_hs_id_set_rnd(addr.val)// Step 4: own_addr_type = BLE_OWN_ADDR_RANDOM// Step 5: ble_svc_gap_device_name_set(HAL_BLE_DEVICE_NAME)// Step 6: HAL_BLE_StartAdvertising()}8.4 NimBLE Host 任务
staticvoidHAL_BLE_NimbleHostTask(void*param){nimble_port_run();// 阻塞式运行vTaskDelete(NULL);// 理论上不会执行到这里}九、GAP 事件处理
核心回调HAL_BLE_GapEventHandler处理以下事件:
BLE_GAP_EVENT_ADV_COMPLETE:广播结束,未连接则重启广播BLE_GAP_EVENT_CONNECT:连接完成,设置g_ble_conn_handle和g_ble_connectedBLE_GAP_EVENT_DISCONNECT:连接断开,重置状态并重启广播BLE_GAP_EVENT_MTU:MTU 协商完成(日志观察)BLE_GAP_EVENT_SUBSCRIBE:客户端订阅/取消 NotifyBLE_GAP_EVENT_NOTIFY_TX:Notify 发送完成,释放 busy_flag
十、GATT 读写回调
核心:HAL_BLE_SIMP_GattAccessCallback通过ctxt->op判断操作类型,通过attr_handle判断是哪个特征。
BLE_GATT_ACCESS_OP_READ_CHR:客户端读取 → 返回HAL_BLE_SIMP_ReadBuffer内容,无数据则返回默认{0x23, 0x55, 0x00, 0x0D, 0x0A}BLE_GATT_ACCESS_OP_WRITE_CHR:客户端写入 → 从ctxt->om拷贝数据到HAL_BLE_SIMP_WriteBuffer,调用应用层回调BLE_GATT_ACCESS_OP_WRITE_DSC:CCCD 写入(日志观察)
十一、Notify 发送机制
11.1HAL_BLE_SIMP_SendNotify
关键要点:
- 参数校验(连接状态、指针、长度)
busy_flag防止重入ble_hs_mbuf_from_flat(pBuffer, len)创建 mbuf(官方推荐方式)ble_gatts_notify_custom(conn_handle, val_handle, om)发送- 失败时手动
os_mbuf_free(om)
注意:ble_gatts_notify_custom的第二个参数是特征值句柄(g_ble_simp_notify_char_handle),不是服务句柄。
11.2 为什么必须用ble_hs_mbuf_from_flat()
❌ 错误写法:os_mbuf_get_pkthdr(NULL, 0)+os_mbuf_append()
NULL表示未指定内存池,NimBLE 处理 mbuf 时反向获取归属池信息失败- 触发
BLE_HS_DBG_ASSERT断言失败 →芯片复位
✅ 正确写法:ble_hs_mbuf_from_flat(pBuffer, len)
- 内部从
ble_hs_mbuf_pool(NimBLE 专用内存池)分配 - 所有官方示例(blehr、bleuart、blecsc)统一使用此方式
十二、广播数据构造与广播启动
staticvoidHAL_BLE_StartAdvertising(void){// AD 1: Flags (0x02 0x01 0x06) - LE General Discoverable + BR/EDR Not Supported// AD 2: TX Power Level (0x02 0x0A 0x00)// AD 3: Complete Local Name (name_len+1, 0x09, name)// ble_gap_adv_set_data(adv_data_buf, adv_data_len)// ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, ¶ms, cb, NULL)}十三、应用层 API 接口
| API | 用途 |
|---|---|
HAL_BLE_SIMP_SetReadBuf(pBuffer, len) | 填充 READ 缓冲区,供客户端读取 |
HAL_BLE_SIMP_RegisterReceiveCallback(pCallback) | 注册 WRITE 接收回调,客户端写入数据时通知应用层 |
HAL_BLE_SIMP_SendNotify(pBuffer, len) | 向已连接并订阅 Notify 的客户端推送数据 |
十四、常见问题排查
| 问题 | 原因 | 解决 |
|---|---|---|
ble_gatts_count_cfg failed: 2(BLE_HS_EINVAL) | 特征缺少终止符、access_cb = NULL、服务数组缺少终止符 | 检查 memset 归零、确保g_simp_chr_defs[3].uuid = NULL |
struct ble_hs_cfg' has no member named 'max_attrs' | 手动设置了不存在的结构体成员 | 移除该代码,ble_gatts_count_cfg()内部自动处理 |
| 调用 SendNotify 后设备复位 | os_mbuf_get_pkthdr(NULL, 0)创建的 mbuf 归属池为空,NimBLE 处理时断言失败 | 改用ble_hs_mbuf_from_flat() |
| 客户端无法发现设备 | HAL_BLE_OnSync未调用,或own_addr_type与地址设置类型不匹配 | 检查 sync 回调,确保ble_hs_id_set_rnd()成功 |
| Notify 发送无响应 | 客户端未订阅 CCCD,或busy_flag未被正确释放 | 检查SUBSCRIBE事件cur_notify,检查NOTIFY_TX回调 |
十五、关键 NimBLE API 速查
| API | 作用 |
|---|---|
nimble_port_init() | 初始化 NimBLE 协议栈 |
ble_svc_gap_init()/ble_svc_gatt_init() | 初始化标准 GAP/GATT 服务 |
ble_hs_id_set_rnd(addr) | 设置随机设备地址(48-bit) |
ble_svc_gap_device_name_set(name) | 设置设备名 |
ble_uuid_init_from_buf(uuid, bytes, 16) | 从字节数组初始化 UUID |
ble_gatts_count_cfg(svc_def) | 计算服务所需资源 |
ble_gatts_add_svcs(svc_def) | 注册服务到 NimBLE |
ble_gap_adv_set_data(buf, len) | 设置广播数据 |
ble_gap_adv_start(...) | 启动广播 |
ble_hs_mbuf_from_flat(data, len) | 创建 Notify 用的 mbuf |
ble_gatts_notify_custom(conn, attr, om) | 发送 Notify |
os_mbuf_free(om) | 失败时手动释放 mbuf |
十六、完整初始化流程参考
Step 1: nimble_port_init() Step 2: 配置 ble_hs_cfg 回调 Step 3: ble_svc_gap_init() + ble_svc_gatt_init() Step 4: ble_uuid_init_from_str_hex() × 4 Step 5: memset 数组 → 构造 g_simp_chr_defs[0..2](3 个特征) Step 6: 构造 g_simp_svc_def[0](引用特征数组) Step 7: ble_gatts_count_cfg(g_simp_svc_def) ← 计算资源 Step 8: ble_gatts_add_svcs(g_simp_svc_def) ← 注册服务 Step 9: ble_store_config_init() ← 初始化 NVS 存储 Step 10: xTaskCreate(HAL_BLE_NimbleHostTask) ← 启动 Host 任务 【异步】nimble_port_run() → sync_cb(HAL_BLE_OnSync) → 设置 MAC 地址 → 设置设备名 → 启动广播