资讯动态

BLE只读数字输入服务模板:按键状态低功耗上报设计

发布时间:2026/8/24 5:38:18 来源:尧图企业网站定制
1. 项目概述ble-button是一个面向嵌入式 BLEBluetooth Low Energy应用的轻量级服务模板其核心目标是为物理按键、拨动开关、触摸感应等只读型数字输入设备提供标准化的蓝牙 GATTGeneric Attribute Profile服务封装。它并非通用 BLE 协议栈实现而是一个高度聚焦的“输入服务”参考设计专用于将硬件引脚的电平状态高/低以布尔值形式通过 BLE 特征Characteristic暴露给手机 App 或中央设备Central Device且该特征仅支持Read 操作不接受写入或通知使能控制——这一设计决策直接源于其工程定位最小化功耗、简化协议交互、规避权限与状态同步复杂性。在资源受限的 MCU如 Nordic nRF52、ESP32、STM32WB 系列上频繁的 GATT 写操作会触发额外的中断处理、内存拷贝和协议栈状态机切换而允许 Central 主动使能 Notify/Indicate则需维护客户端配置描述符CCCD状态、处理多客户端连接时的并发写入冲突并引入非必要功耗。ble-button通过强制只读特性彻底规避了上述问题Peripheral外设端仅需在按键状态变化时主动发送一次 Notify若已使能或等待 Central 发起 Read 请求后返回当前快照值。这种“被动响应 可选主动推送”的混合模式在保证数据时效性的同时将协议栈负担降至最低。该模板的典型部署场景包括电池供电的无线门磁/窗磁传感器开/关状态上报工业现场的紧急停止按钮E-Stop状态监控智能家居中的机械式场景开关如“离家模式”一键触发教学实验板上的 GPIO 输入状态可视化配合 nRF Connect 等调试工具其设计哲学可概括为用最简的 GATT 结构承载最明确的语义以确定性的行为替代可配置的灵活性。这使其成为 BLE 外设固件开发中“输入类服务”的事实标准起点。2. 核心架构与 GATT 模型2.1 GATT 服务结构定义ble-button实现了一个自定义 GATT 服务其 UUID 采用 128 位格式标准定义如下层级类型UUID (128-bit)说明ServicePrimary Service00001812-0000-1000-8000-00805F9B34FB基于 Bluetooth SIG 的Human Interface Device (HID) ServiceUUID 修改而来确保唯一性与可识别性。实际部署中建议替换为厂商专属 UUID。CharacteristicInput State00002A56-0000-1000-8000-00805F9B34FB对应Digital Input State特征承载布尔值。此 UUID 引用自 SIG 的Digital Input State特征定义0x2A56符合规范兼容性要求。该服务仅包含一个特征Characteristic无其他描述符Descriptor或子特征。其属性Properties被严格限定为Read允许 Central 发起GATT_READ_REQNotify允许 Peripheral 在状态变化时发送GATT_HANDLE_VALUE_NOTIF关键约束Write,Write Without Response,Indicate,Authenticated Signed Writes等属性必须禁用。此约束在底层协议栈初始化阶段即通过ble_gatts_char_md_tNordic SDK或esp_ble_gatts_register_attr_tab()ESP-IDF等 API 的char_props字段硬编码实现无法在运行时动态修改。2.2 数据格式与编码规范特征值Characteristic Value采用单字节uint8_t二进制编码严格遵循以下映射物理状态逻辑值特征值字节十六进制说明按键按下 / 开关闭合 / 触发有效true0x01通常对应 GPIO 输入低电平Active-Low需在驱动层完成电平翻转按键释放 / 开关断开 / 未触发false0x00保持高电平Pull-Up或浮空Pull-Down的默认态此设计摒弃了字符串true/false、JSON 或 TLV 等冗余编码将传输开销压缩至极致一次 Notify 报文总长度仅为 3 字节2 字节 ATT Header 1 字节 Payload。在 nRF52832 上实测启用 Notify 后单次状态更新的空中时间Air Time低于 1.2ms功耗增量可忽略。2.3 状态同步机制ble-button不维护独立的状态缓存其特征值始终与硬件 GPIO 寄存器的实时读取结果保持一致。同步流程如下初始化阶段MCU 配置 GPIO 为输入模式启用内部上拉/下拉电阻依据按键电路设计并注册中断或轮询任务。状态变更检测中断模式GPIO 边沿触发中断 → ISR 中调用nrf_drv_gpiote_in_event_enable(pin, true)→ 触发状态读取与 Notify。轮询模式RTOS 任务如 FreeRTOSxTaskCreate创建以固定周期如 20ms调用nrf_gpio_pin_read(pin)。Notify 触发条件仅当新读取值与上次 Notify 发送的值不同时才执行sd_ble_gatts_hvx()Nordic或esp_ble_gatts_send_indicate()ESP-IDF。此去抖逻辑必须在应用层实现避免因机械抖动产生误报。工程实践提示在中断 ISR 中直接调用 BLE 协议栈 API 存在风险如 Nordic SoftDevice 要求sd_ble_gatts_hvx()必须在主循环上下文执行。推荐方案是ISR 仅设置一个volatile bool button_state_changed true;标志由主循环或高优先级任务检查该标志并执行 Notify。3. 关键 API 接口详解3.1 服务注册 APINordic nRF5x SDKS132/S140 SoftDevice// 定义服务结构 static ble_uuid_t m_button_service_uuid; static uint8_t m_button_char_value 0x00; // 初始状态未按下 // 初始化服务 void ble_button_service_init(void) { ret_code_t err_code; ble_uuid128_t base_uuid {{0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x12, 0x18, 0x00, 0x00}}; // 注册 128-bit UUID err_code sd_ble_uuid_vs_add(base_uuid, m_button_service_uuid.type); APP_ERROR_CHECK(err_code); m_button_service_uuid.uuid 0x1812; // LSB of 128-bit UUID // 配置特征元数据 ble_gatts_char_md_t char_md; memset(char_md, 0, sizeof(char_md)); char_md.char_props.read 1; char_md.char_props.notify 1; char_md.p_char_user_desc NULL; char_md.p_char_pf NULL; char_md.p_user_desc_md NULL; char_md.p_cccd_md m_cccd_md; // 必须提供 CCCD 元数据以支持 Notify char_md.p_sccd_md NULL; // 配置特征值属性 ble_gatts_attr_md_t attr_md; memset(attr_md, 0, sizeof(attr_md)); BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(attr_md.read_perm); // 无加密要求 BLE_GAP_CONN_SEC_MODE_SET_NO_ACCESS(attr_md.write_perm); attr_md.vloc BLE_GATTS_VLOC_STACK; // 值存储在 SoftDevice 栈内 attr_md.rd_auth 0; attr_md.wr_auth 0; attr_md.vlen 0; // 固定长度 // 定义特征值 ble_gatts_attr_t attr_char_value; memset(attr_char_value, 0, sizeof(attr_char_value)); attr_char_value.p_uuid button_char_uuid; attr_char_value.p_attr_md attr_md; attr_char_value.init_len sizeof(uint8_t); attr_char_value.init_offs 0; attr_char_value.max_len sizeof(uint8_t); attr_char_value.p_value m_button_char_value; // 注册服务 ble_gatts_service_t service_handle; err_code sd_ble_gatts_service_add(BLE_GATTS_SRVC_TYPE_PRIMARY, m_button_service_uuid, service_handle); APP_ERROR_CHECK(err_code); // 添加特征 err_code sd_ble_gatts_characteristic_add(service_handle, char_md, attr_char_value, m_button_char_handles); APP_ERROR_CHECK(err_code); }参数解析p_cccd_md指向ble_gatts_attr_md_t结构声明 CCCD 描述符的访问权限。BLE_GAP_CONN_SEC_MODE_SET_OPEN(cccd_md.read_perm)表示任何连接均可读取 CCCD这是 Notify 功能的前提。vloc BLE_GATTS_VLOC_STACK指示特征值由 SoftDevice 管理内存应用层仅需提供初始值指针。后续 Notify 时需调用sd_ble_gatts_value_set()更新栈内值。max_len 1硬编码为 1 字节杜绝越界风险。ESP-IDFBluedroid Stack// GATT 数据表定义 static const uint16_t button_service_uuid 0x1812; static const uint16_t button_char_uuid 0x2A56; static const esp_gatts_attr_db_t button_gatt_db[] { // Service Declaration [BUTTON_IDX_SVC] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t*)button_service_uuid, ESP_GATT_PERM_READ}}, // Characteristic Declaration [BUTTON_IDX_CHAR] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t*)character_declaration_uuid, ESP_GATT_PERM_READ}}, // Characteristic Value [BUTTON_IDX_CHAR_VAL] {{ESP_GATT_RSP_BY_APP}, {ESP_UUID_LEN_16, (uint8_t*)button_char_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE}}, // 注意此处 write perm 为占位实际不处理写 // Client Characteristic Configuration Descriptor (CCCD) [BUTTON_IDX_CHAR_CFG] {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t*)character_client_config_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE}} }; // 注册服务 void ble_button_gatts_create(void) { esp_err_t ret esp_ble_gatts_create_attr_tab(button_gatt_db, BUTTON_IDX_NB, BUTTON_SVC_INST_ID, false); if (ret ! ESP_OK) { ESP_LOGE(TAG, Create attribute table failed, error code %x, ret); } }关键差异ESP-IDF 要求显式声明 CCCDcharacter_client_config_uuid 0x2902且其属性必须包含WRITE权限否则 Central 无法写入 CCCD 使能 Notify。应用层需在ESP_GATTS_WRITE_EVT事件中解析写入的 CCCD 值0x0001启用 Notify0x0000禁用并更新本地标志。3.2 状态更新与 Notify APINordic安全的 Notify 流程// 全局变量记录 CCCD 状态 static bool m_is_notification_enabled false; // CCCD 写入回调在 ble_gatts_evt_handler 中处理 void on_cccd_write(ble_gatts_evt_t * p_gatts_evt) { if (p_gatts_evt-params.authorize_request.request.write.handle m_button_char_handles.cccd_handle) { uint16_t cccd_value; uint8_t *p_data p_gatts_evt-params.authorize_request.request.write.data; cccd_value uint16_decode(p_data); m_is_notification_enabled (cccd_value BLE_GATT_HVX_NOTIFICATION); } } // 发送 Notify 的安全封装 void ble_button_notify_state(uint8_t new_state) { if (!m_is_notification_enabled) return; // 1. 更新 SoftDevice 栈内值 ret_code_t err_code sd_ble_gatts_value_set(m_button_char_handles.value_handle, 0, sizeof(uint8_t), new_state); if (err_code ! NRF_SUCCESS) return; // 2. 构造 Notify 包 ble_gatts_hvx_params_t hvx_params; memset(hvx_params, 0, sizeof(hvx_params)); hvx_params.handle m_button_char_handles.value_handle; hvx_params.type BLE_GATT_HVX_NOTIFICATION; hvx_params.offset 0; hvx_params.p_len sizeof(uint8_t); hvx_params.p_data new_state; // 3. 触发发送异步 err_code sd_ble_gatts_hvx(m_conn_handle, hvx_params); if (err_code NRF_SUCCESS) { m_button_char_value new_state; // 同步本地缓存 } }ESP-IDF事件驱动模型// 在 GATTS event handler 中处理 case ESP_GATTS_WRITE_EVT: { esp_gatt_status_t status ESP_GATT_OK; if (param-write.handle button_handle_table[BUTTON_IDX_CHAR_CFG]) { // 解析 CCCD 写入 if (param-write.len 2) { uint16_t value param-write.value[0] (param-write.value[1] 8); m_is_notification_enabled (value 0x0001); } } else if (param-write.handle button_handle_table[BUTTON_IDX_CHAR_VAL]) { // 忽略对特征值的写入只读 status ESP_GATT_WRITE_NOT_PERMIT; } esp_ble_gatts_send_response(gatts_if, param-write.conn_id, param-write.trans_id, status, NULL); break; } // 主动 Notify 函数 void ble_button_send_notify(uint16_t conn_id, uint8_t state) { if (!m_is_notification_enabled) return; esp_ble_gatts_send_indicate(gatts_if, conn_id, button_handle_table[BUTTON_IDX_CHAR_VAL], sizeof(state), state, false); }4. 硬件驱动集成与低功耗优化4.1 GPIO 驱动适配ble-button的硬件抽象层HAL需屏蔽 MCU 差异提供统一接口// button_hal.h typedef enum { BUTTON_ACTIVE_LOW 0, // 按下时 GPIO 为低电平 BUTTON_ACTIVE_HIGH 1 // 按下时 GPIO 为高电平 } button_polarity_t; typedef struct { uint32_t pin; // GPIO 编号如 NRF_GPIO_PIN_MAP(0,10) button_polarity_t polarity; void (*on_state_change)(bool is_pressed); // 状态变更回调 } button_config_t; // 初始化函数 void button_hal_init(const button_config_t *config); // 读取当前状态阻塞 bool button_hal_read(void); // 启用中断边沿触发 void button_hal_enable_irq(bool enable_falling, bool enable_rising);电路设计要点上拉/下拉选择若按键一端接地另一端接 GPIO则必须启用内部上拉电阻NRF_GPIO_PIN_PULLUP此时BUTTON_ACTIVE_LOW。防抖处理硬件层面建议串联 100nF 电容软件层面在 ISR 中添加 5~10ms 延时使用nrf_delay_ms()再读取确认。4.2 低功耗模式协同在电池供电场景MCU 绝大部分时间处于System OFF或Low Power Sleep模式。ble-button的功耗优化策略如下模式MCU 状态BLE 状态按键响应延迟适用场景常唤醒CPU 运行Advertising Connected 10ms高实时性需求如游戏手柄Sleep GPIO WakeupCPU SleepAdvertising Only~100ms唤醒连接建立门磁传感器事件驱动System OFF LPCOMPCPU OFFAdvertising Only~500msLPCOMP 唤醒系统启动极低功耗年续航推荐配置nRF52832// 进入 System OFF 前配置 nrf_gpio_cfg_sense_input(CONFIG_BUTTON_PIN, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); // 下降沿唤醒 // 进入 System OFF nrf_power_system_off(); // 唤醒后立即广播连接成功后启用 Notify此时按键按下直接触发 MCU 唤醒无需 BLE 协议栈参与功耗降至 0.5μA 以下。5. 实际部署案例nRF52840 Dongle Zephyr RTOS以下为基于 Zephyr OS 的完整移植示例展示如何将ble-button集成到现代 RTOS 环境// main.c #include zephyr/kernel.h #include zephyr/drivers/gpio.h #include zephyr/bluetooth/bluetooth.h #include zephyr/bluetooth/conn.h #include zephyr/bluetooth/gatt.h #include zephyr/bluetooth/services/bas.h #define BUTTON_NODE DT_ALIAS(button0) #define BUTTON_GPIO_LABEL DT_GPIO_LABEL(BUTTON_NODE, gpios) #define BUTTON_GPIO_PIN DT_GPIO_PIN(BUTTON_NODE, gpios) #define BUTTON_GPIO_FLAGS DT_GPIO_FLAGS(BUTTON_NODE, gpios) static const struct gpio_dt_spec button GPIO_DT_SPEC_GET(BUTTON_NODE, gpios); static struct bt_conn *current_conn; static bool button_state false; // GATT 特征定义 BT_GATT_SERVICE_DEFINE(button_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128( 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x12, 0x18, 0x00, 0x00)), BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_16(BT_UUID_DIGITAL_INPUT_STATE), BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ, read_button_state, NULL, NULL), BT_GATT_CCC_MANAGED)); // Read 回调 ssize_t read_button_state(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { uint8_t value button_state ? 0x01 : 0x00; return bt_gatt_attr_read(conn, attr, buf, len, offset, value, sizeof(value)); } // 按键中断处理 static void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { button_state !button_state; if (current_conn) { // 通知所有订阅者 bt_gatt_notify(NULL, button_svc.attrs[2], button_state, sizeof(button_state)); } } // 连接回调 static void connected(struct bt_conn *conn, uint8_t err) { if (err) { printk(Connection failed (err %u)\n, err); } else { current_conn bt_conn_ref(conn); printk(Connected\n); } } // 断开回调 static void disconnected(struct bt_conn *conn, uint8_t reason) { if (current_conn) { bt_conn_unref(current_conn); current_conn NULL; } printk(Disconnected (reason %u)\n, reason); } // 初始化 void main(void) { // 初始化 GPIO const struct device *dev device_get_binding(BUTTON_GPIO_LABEL); gpio_pin_configure(dev, BUTTON_GPIO_PIN, GPIO_INPUT | BUTTON_GPIO_FLAGS); gpio_pin_interrupt_configure(dev, BUTTON_GPIO_PIN, GPIO_INT_EDGE_TO_ACTIVE); gpio_init_callback(button_cb_data, button_pressed, BIT(BUTTON_GPIO_PIN)); gpio_add_callback(dev, button_cb_data); // 初始化 BLE bt_enable(NULL); bt_conn_cb_register(conn_callbacks); printk(BLE Button Service Ready\n); }Zephyr 优势BT_GATT_SERVICE_DEFINE宏自动处理服务注册与内存管理。bt_gatt_notify()封装了 CCCD 检查与底层协议栈调用开发者无需手动解析 CCCD。gpio_init_callback()提供标准化中断注册兼容所有 Zephyr 支持的 SoC。6. 调试与验证方法6.1 使用 nRF Connect 进行功能验证手机安装 nRF Connect 。扫描设备找到名为BLE Button的外设并连接。在服务列表中展开自定义服务UUID00001812-...点击Digital Input State特征。验证 Read点击READ按钮观察返回值是否为00或01。验证 Notify点击ENABLE NOTIFICATIONS状态变为ON。按下开发板按键nRF Connect 应实时显示NOTIFICATION事件及新值。若未收到检查 CCCD 是否正确写入0001及m_is_notification_enabled标志。6.2 抓包分析nRF Sniffer使用 nRF Sniffer 捕获空中数据包正常 Notify 流程ATT Handle Value Notification→Handle: 0x000d, Value: 0x01CCCD 写入ATT Write Request→Handle: 0x000e, Value: 0x0100错误场景ATT Error ResponseAttribute Not Found表示服务未注册Write Not Permitted表示属性配置错误6.3 功耗测量Power Profiler Kit II在System OFF模式下使用 Nordic Power Profiler Kit II 测量广播期间平均电流~15μAnRF52840间隔 1s按键唤醒瞬间峰值 5mA持续 2ms广播连接建立后~700μAConnected Notify enabled此数据证实ble-button模板在事件驱动场景下可实现5 年以上电池寿命CR2032 220mAh。7. 常见问题与解决方案问题现象根本原因解决方案Central 无法发现服务SoftDevice 未初始化或sd_ble_gatts_service_add()返回NRF_ERROR_NO_MEM检查BLE_GATTS_DEF配置是否预留足够服务槽位确认BLE_GATTS_ATTR_TAB_SIZE大于服务所需属性数Notify 无响应CCCD 未使能或m_is_notification_enabled未更新在BLE_GATTS_EVT_WRITE事件中添加日志确认写入0x0001检查cccd_md权限是否设为OPEN按键抖动导致多次 Notify硬件未加 RC 滤波或软件未做延时消抖在 ISR 中添加nrf_delay_ms(10)或改用定时器轮询20ms 周期连接后 Read 返回旧值sd_ble_gatts_value_set()未在 Notify 前调用确保ble_button_notify_state()函数中sd_ble_gatts_value_set()在sd_ble_gatts_hvx()之前执行Zephyr 编译报错undefined reference to bt_gatt_notify未启用CONFIG_BT_GATT_NOTIFYy在prj.conf中添加该配置项并确认CONFIG_BT_PERIPHERALy8. 扩展方向与工程演进ble-button模板虽小但可作为更复杂系统的基石进行扩展多按键支持复用同一服务 UUID为每个按键分配独立特征不同 Handle共享 CCCD。需在read_button_state()回调中根据attr-handle分发读取逻辑。长按/双击事件在 HAL 层增加状态机区分PRESS,LONG_PRESS,DOUBLE_CLICK将事件编码为 2 字节0x01短按0x02长按。与传感器融合将按钮作为“触发键”按下后启动温湿度传感器采样并通过另一特征如0x2A1CTemperature推送数据形成“事件驱动采集”模式。OTA 安全升级在服务中添加DFU Control Point特征0x2A51利用按钮物理确认升级包防止误升级。这些扩展均不破坏原有ble-button的核心契约——只读、布尔、低开销。工程师可根据产品需求在此坚实基座上构建稳健的 BLE 输入生态。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价