行业资讯
【ESP32】ESP32-C3 NimBLE SIMP 服务配置与初始化方案
ESP32-C3 NimBLE SIMP 服务配置与初始化方案适用平台ESP32-C3 | ESP-IDF 版本v5.3.5 | 蓝牙协议栈NimBLE| GATT 服务SIMPSimple Message Protocol一、概述SIMP 服务是一个基于 BLE GATT 的自定义数据传输服务通过NimBLE Host 协议栈实现。它提供 3 个特征Characteristic完成与客户端设备的数据交互。特征UUID 后缀方向属性说明READ6E40-xxxx-...-0002客户端 → 读取设备数据BLE_GATT_CHR_F_READ客户端主动读取设备缓冲区数据WRITE6E40-xxxx-...-0003客户端 → 写入设备BLE_GATT_CHR_F_WRITEF_WRITE_NO_RSPNOTIFY6E40-xxxx-...-0004客户端 ← 设备推送BLE_GATT_CHR_F_NOTIFY设备主动推送数据需客户端订阅 CCCD为什么选择 NimBLE 而不是 BluedroidNimBLE 是 Apache MyNewt 项目的一部分由 ESP-IDF 以组件形式提供。相比 Bluedroid它占用更小的 Flash/RAM协议栈核心更小适合资源受限的 MCU代码更紧凑API 设计更精简GATT 服务声明式注册不需要事件回调链与 FreeRTOS 集成更直接通过nimble_port_run()阻塞式运行CCCD 自动创建当特征标记为 NOTIFY/INDICATE 时CCCD 描述符由协议栈自动创建二、硬件抽象层架构本项目采用 HALHardware 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所有函数指针置 0HAL_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_NAMETEST-ESP32C3BLE 广播包中携带的设备名称手机 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 40msBLE 广播间隔单位是0.625ms一个 BLE SlotNimBLE 中范围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 收到的数据实际应用层应设置为 0由应用代码注册自己的回调四、UUID 配置与字节序转换4.1 UUID 定义#defineHAL_BLE_SIMP_SERVICE_UUID6E400001-B5A3-F393-E0A9-E50E24DCCA9E#defineHAL_BLE_SIMP_READ_CHAR_UUID6E400002-B5A3-F393-E0A9-E50E24DCCA9E#defineHAL_BLE_SIMP_WRITE_CHAR_UUID6E400003-B5A3-F393-E0A9-E50E24DCCA9E#defineHAL_BLE_SIMP_NOTIFY_CHAR_UUID6E400004-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_idx0;// 1. 解析字符串为 16 字节大端数组跳过连字符for(inti0;i36byte_idx16;i){if(str[i]-)continue;uint8_thighHAL_BLE_HexCharToByte(str[i]);uint8_tlowHAL_BLE_HexCharToByte(str[i]);bytes[byte_idx](high4)|low;}// 2. 反转为小端字节序uint8_ttemp_bytes[16];memcpy(temp_bytes,bytes,16);for(inti0;i16;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 服务注册方式特性BluedroidNimBLE注册方式事件驱动分步调用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].uuidg_simp_read_uuid.u;g_simp_chr_defs[0].access_cbHAL_BLE_SIMP_GattAccessCallback;g_simp_chr_defs[0].flagsBLE_GATT_CHR_F_READ;g_simp_chr_defs[0].val_handleg_ble_simp_read_char_handle;/* 特征 2: WRITE */g_simp_chr_defs[1].uuidg_simp_write_uuid.u;g_simp_chr_defs[1].access_cbHAL_BLE_SIMP_GattAccessCallback;g_simp_chr_defs[1].flagsBLE_GATT_CHR_F_WRITE|BLE_GATT_CHR_F_WRITE_NO_RSP;g_simp_chr_defs[1].val_handleg_ble_simp_write_char_handle;/* 特征 3: NOTIFY */g_simp_chr_defs[2].uuidg_simp_notify_uuid.u;g_simp_chr_defs[2].access_cbHAL_BLE_SIMP_GattAccessCallback;g_simp_chr_defs[2].flagsBLE_GATT_CHR_F_NOTIFY;g_simp_chr_defs[2].val_handleg_ble_simp_notify_char_handle;g_simp_chr_defs[2].descriptorsNULL;// 关键 NULL 让 NimBLE 自动创建 CCCDble_gatt_chr_def字段说明字段类型说明uuidble_uuid_t *指向特征 UUID必须非 NULL非终止符时access_cbble_gatt_access_fn *读写回调函数必须非 NULLflagsuint16_t特征属性标志BLE_GATT_CHR_F_*val_handleuint16_t *指向接收特征值句柄的变量NimBLE 注册时回填descriptorsstruct ble_gatt_dsc_def *自定义描述符数组。 NULL时 NimBLE 自动创建 CCCD5.3 服务定义与注册memset(g_simp_svc_def,0,sizeof(g_simp_svc_def));g_simp_svc_def[0].typeBLE_GATT_SVC_TYPE_PRIMARY;// 主服务g_simp_svc_def[0].uuidg_simp_service_uuid.u;g_simp_svc_def[0].characteristicsg_simp_chr_defs;// 指向特征数组// Step 1: 计算属性所需资源内部会调整 ble_hs_max_attrs 等计数intrcble_gatts_count_cfg(g_simp_svc_def);if(rc!0){HAL_BLE_ErrorLog(ble_gatts_count_cfg failed: %d,rc);return2;}// Step 2: 注册服务到 NimBLErcble_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或者数组缺少终止符六、CCCDClient Characteristic Configuration DescriptorCCCD 是 NOTIFY 功能必需的描述符UUID 0x2902。客户端写入 CCCD 的值决定是否接收推送0x0000 取消订阅0x0001 启用 Notify0x0002 启用 Indicate6.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 订阅事件SUBSCRIBEcaseBLE_GAP_EVENT_SUBSCRIBE:{// 检查是否是 Notify 特征的句柄if(event-subscribe.attr_handleg_ble_simp_notify_char_handle){if(event-subscribe.cur_notify0||event-subscribe.cur_indicate0){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_indicateIndicate 订阅状态七、编译配置sdkconfig7.1 NimBLE 必需配置# 蓝牙总开关 CONFIG_BT_ENABLEDy CONFIG_BT_CONTROLLER_ENABLEDy # NimBLE 协议栈取代 Bluedroid CONFIG_BT_NIMBLE_ENABLEDy CONFIG_BT_NIMBLE_HOST_ENABLEDy # 控制器配置 CONFIG_BT_CTRL_MODE_EFFy # NimBLE 内存配置 CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT12 CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE256 CONFIG_BT_NIMBLE_ACL_BUF_COUNT12 CONFIG_BT_NIMBLE_ACL_BUF_SIZE255 # GATT / GAP CONFIG_BT_NIMBLE_GATT_MAX_PROCS4 CONFIG_BT_NIMBLE_MAX_CONNECTIONS1 CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAMEnimble # 安全/配对 CONFIG_BT_NIMBLE_SM_LEGACYy CONFIG_BT_NIMBLE_SM_SCy # 存储绑定信息保存到 NVS CONFIG_BT_NIMBLE_STORE_CONFIGy # 调试 CONFIG_BT_NIMBLE_LOG_LEVEL0 CONFIG_BT_NIMBLE_DEBUGy7.2 CMakeLists.txt 组件依赖idf_component_register( REQUIRES driver esp_event esp_partition freertos log lwip nvs_flash mbedtls bt )7.3 头文件引入#includenimble/nimble_port.h#includenimble/nimble_port_freertos.h#includehost/ble_hs.h#includehost/ble_hs_id.h#includehost/ble_gap.h#includehost/ble_gatt.h#includehost/ble_uuid.h#includehost/util/util.h#includeservices/gap/ble_svc_gap.h#includeservices/gatt/ble_svc_gatt.h#includestore/config/ble_store_config.h#includeesp_nimble_hci.h#includeesp_mac.h#includenvs_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_cbvoid (*)(int)协议栈复位打印错误日志sync_cbvoid (*)(void)Host-Controller 同步设置地址/名称 启动广播gatts_register_cbvoid (*)(ctxt, arg)特征注册完成调试日志store_status_cbint (*)(event, arg)NVS 存储操作默认使用ble_store_util_status_rr8.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_MTUMTU 协商完成日志观察BLE_GAP_EVENT_SUBSCRIBE客户端订阅/取消 NotifyBLE_GAP_EVENT_NOTIFY_TXNotify 发送完成释放 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_DSCCCCD 写入日志观察十一、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_poolNimBLE 专用内存池分配所有官方示例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_len1, 0x09, name)// ble_gap_adv_set_data(adv_data_buf, adv_data_len)// ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, params, 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 NULLstruct 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-bitble_svc_gap_device_name_set(name)设置设备名ble_uuid_init_from_buf(uuid, bytes, 16)从字节数组初始化 UUIDble_gatts_count_cfg(svc_def)计算服务所需资源ble_gatts_add_svcs(svc_def)注册服务到 NimBLEble_gap_adv_set_data(buf, len)设置广播数据ble_gap_adv_start(...)启动广播ble_hs_mbuf_from_flat(data, len)创建 Notify 用的 mbufble_gatts_notify_custom(conn, attr, om)发送 Notifyos_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 地址 → 设置设备名 → 启动广播最后效果
郑州网站建设
网页设计
企业官网