ESP32双模蓝牙键盘实现攻略
一、问题解构与方案推演
您提出的需求是使用ESP32作为主控,实现一个具备**双模(BLE HID + 传统蓝牙SPP)**功能的蓝牙键盘,并包含Ctrl、C、V、X、Z五个物理按键。核心挑战在于同时管理两种蓝牙协议栈,并处理按键事件到HID键盘报告的映射。
方案推演如下:
- 硬件选型:ESP32-C3或ESP32-S3是理想选择,它们原生支持双模蓝牙(BLE 4.2/5.0 + 经典蓝牙)。五个按键可连接至GPIO,推荐使用上拉输入模式检测低电平触发。
- 软件框架:基于ESP-IDF开发,而非 MicroPython 或 Arduino,因其对双模蓝牙和低层HID协议栈的支持最完善、最直接。
- 核心逻辑:
- BLE HID模式:实现GATT HID Service,将按键动作编码为标准HID键盘报告,通过BLE发送给主机(如PC、手机)。
- 传统蓝牙(SPP)模式:实现串行端口协议(SPP),作为备用或兼容模式。可将按键事件转换为特定字符串(如
“KEY_CTRL”)通过虚拟串口发送,由主机端程序解析。 - 模式管理与切换:需设计状态机,管理两种蓝牙模式的初始化、连接、断开及可能的切换逻辑。
- 按键扫描与消抖:需实现高效的GPIO中断或轮询扫描,并进行软件消抖,确保按键事件准确。
- 开发流程:搭建ESP-IDF环境 -> 创建双模蓝牙项目 -> 分别实现BLE HID和SPP Profile -> 集成按键驱动与事件处理 -> 测试与调试。
二、核心代码实现与说明
以下是一个基于ESP-IDF v5.0+的简化示例,展示了双模蓝牙初始化和五个按键的HID报告发送核心逻辑。注意:此示例侧重于BLE HID模式,SPP部分仅给出框架。
1. 主要组件与配置文件
main/Kconfig.projbuild(项目配置)
# 启用蓝牙和经典蓝牙 CONFIG_BT_ENABLED=y CONFIG_BT_CLASSIC_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=y # 启用BLE和GATT CONFIG_BT_BLE_ENABLED=y CONFIG_BT_GATT_ENABLED=y此配置确保编译时包含双模蓝牙协议栈。
main/component.mk(组件编译配置)
COMPONENT_ADD_INCLUDEDIRS := . COMPONENT_SRCDIRS := .main/main.c(主程序核心逻辑)
#include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" #include "esp_log.h" #include "nvs_flash.h" #include "esp_bt.h" #include "esp_bt_main.h" #include "esp_gap_bt_api.h" #include "esp_spp_api.h" #include "esp_hid_gap.h" #include "esp_hidh.h" // BLE HID Host 相关,这里我们用作设备端需自定义HID服务 // 按键GPIO定义 (根据实际硬件连接修改) #define GPIO_KEY_CTRL GPIO_NUM_0 #define GPIO_KEY_C GPIO_NUM_1 #define GPIO_KEY_V GPIO_NUM_2 #define GPIO_KEY_X GPIO_NUM_3 #define GPIO_KEY_Z GPIO_NUM_4 #define KEY_MASK ((1ULL<<GPIO_KEY_CTRL) | (1ULL<<GPIO_KEY_C) | (1ULL<<GPIO_KEY_V) | (1ULL<<GPIO_KEY_X) | (1ULL<<GPIO_KEY_Z)) static const char *TAG = "DUAL_BLE_KB"; // HID键盘报告描述符 (简化版,仅包含左Ctrl和C/V/X/Z键) static const uint8_t hid_report_descriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0xE0, // Usage Minimum (0xE0) - 左Ctrl 0x29, 0xE7, // Usage Maximum (0xE7) - 右GUI 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) - 8个修饰键位 0x81, 0x02, // Input (Data,Var,Abs) - 修饰键字节 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x01, // Input (Cnst,Arr,Abs) - 保留字节 0x95, 0x05, // Report Count (5) - 我们最多同时发送5个键(实际HID通常为6) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0xFF, // Logical Maximum (255) - 键值范围 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0x00, // Usage Minimum (0) 0x29, 0xFF, // Usage Maximum (255) 0x81, 0x00, // Input (Data,Arr,Abs) - 键值数组 0xC0 // End Collection }; // HID键盘报告结构 (8字节标准报告) typedef struct { uint8_t modifiers; // 修饰键 (bit0:左Ctrl, bit1:左Shift, ...) uint8_t reserved; uint8_t key_codes[6]; // 普通键值 } hid_keyboard_report_t; // 全局报告实例 static hid_keyboard_report_t kb_report = {0}; // 按键GPIO状态与HID键值映射 static const struct { gpio_num_t gpio; uint8_t hid_keycode; // HID Usage ID bool is_modifier; // 是否为修饰键 } key_map[] = { {GPIO_KEY_CTRL, 0xE0, true}, // 左Ctrl {GPIO_KEY_C, 0x06, false}, // 'c'键 {GPIO_KEY_V, 0x19, false}, // 'v'键 {GPIO_KEY_X, 0x1B, false}, // 'x'键 {GPIO_KEY_Z, 0x1D, false}, // 'z'键 }; // BLE HID服务发送报告的函数 (需与具体的BLE HID实现结合) static void send_hid_report(void) { // 此处应调用BLE GATT的API发送kb_report // 例如: esp_ble_gatts_send_indicate(gatts_if, conn_id, char_handle, report_len, (uint8_t*)&kb_report, false); ESP_LOGI(TAG, "HID Report: Modifiers:0x%02X, Keys:[%02X,%02X,%02X,%02X,%02X,%02X]", kb_report.modifiers, kb_report.key_codes[0], kb_report.key_codes[1], kb_report.key_codes[2], kb_report.key_codes[3], kb_report.key_codes[4], kb_report.key_codes[5]); // 实际发送后,需要清空报告中的普通键值(修饰键状态保持直到释放) for(int i=0; i<6; i++) kb_report.key_codes[i] = 0; } // 传统蓝牙SPP发送函数 (示例:发送字符串) static void send_spp_data(const char* data) { // 此处应调用ESP_SPP_WRITE API ESP_LOGI(TAG, "[SPP] Would send: %s", data); } // 按键扫描任务 (简化轮询,实际建议用中断+消抖) static void key_scan_task(void *arg) { uint32_t io_status; while(1) { io_status = gpio_get_level(GPIO_KEY_CTRL) << 0 | gpio_get_level(GPIO_KEY_C) << 1 | gpio_get_level(GPIO_KEY_V) << 2 | gpio_get_level(GPIO_KEY_X) << 3 | gpio_get_level(GPIO_KEY_Z) << 4; static uint32_t last_status = 0xFF; if(io_status != last_status) { for(int i=0; i<5; i++) { bool current_pressed = !((io_status >> i) & 0x01); // 假设低电平按下 bool last_pressed = !((last_status >> i) & 0x01); if(current_pressed && !last_pressed) { // 按下事件 if(key_map[i].is_modifier) { kb_report.modifiers |= (1 << (key_map[i].hid_keycode - 0xE0)); } else { // 找一个空位填入键值 for(int j=0; j<6; j++) { if(kb_report.key_codes[j] == 0) { kb_report.key_codes[j] = key_map[i].hid_keycode; break; } } } send_hid_report(); // 同时触发SPP模式发送(示例) char spp_msg[32]; snprintf(spp_msg, sizeof(spp_msg), "KEY_DOWN:%d ", key_map[i].hid_keycode); send_spp_data(spp_msg); } else if(!current_pressed && last_pressed) { // 释放事件 if(key_map[i].is_modifier) { kb_report.modifiers &= ~(1 << (key_map[i].hid_keycode - 0xE0)); } else { for(int j=0; j<6; j++) { if(kb_report.key_codes[j] == key_map[i].hid_keycode) { kb_report.key_codes[j] = 0; } } } send_hid_report(); } } last_status = io_status; } vTaskDelay(pdMS_TO_TICKS(20)); // 扫描间隔20ms,兼作消抖 } } // 蓝牙SPP回调 static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) { switch(event) { case ESP_SPP_INIT_EVT: ESP_LOGI(TAG, "SPP初始化完成"); esp_spp_start_srv(ESP_SPP_SEC_NONE, ESP_SPP_ROLE_SLAVE, 0, "ESP32_DUAL_KB"); break; case ESP_SPP_START_EVT: ESP_LOGI(TAG, "SPP服务启动成功"); break; case ESP_SPP_SRV_OPEN_EVT: ESP_LOGI(TAG, "SPP客户端连接"); break; default: break; } } // BLE GAP事件处理 static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch(event) { case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: esp_ble_gap_start_advertising(&adv_params); break; default: break; } } void app_main(void) { // 1. 初始化NVS esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); // 2. 初始化按键GPIO (上拉输入) gpio_config_t io_conf = { .pin_bit_mask = KEY_MASK, .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; gpio_config(&io_conf); // 3. 初始化双模蓝牙控制器 esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ret = esp_bt_controller_init(&bt_cfg); ESP_ERROR_CHECK(ret); ret = esp_bt_controller_enable(ESP_BT_MODE_BTDM); // 双模 ESP_ERROR_CHECK(ret); ret = esp_bluedroid_init(); ESP_ERROR_CHECK(ret); ret = esp_bluedroid_enable(); ESP_ERROR_CHECK(ret); // 4. 分别初始化SPP和BLE HID // 初始化SPP ret = esp_spp_register_callback(esp_spp_cb); ESP_ERROR_CHECK(ret); ret = esp_spp_init(ESP_SPP_MODE_CB); ESP_ERROR_CHECK(ret); // 初始化BLE GAP & GATT (HID服务需在此框架上添加) ret = esp_ble_gap_register_callback(esp_gap_cb); ESP_ERROR_CHECK(ret); ret = esp_ble_gatts_register_callback(esp_gatts_cb); // 需实现esp_gatts_cb ESP_ERROR_CHECK(ret); ret = esp_ble_gatts_app_register(APP_ID); // 需定义APP_ID ESP_ERROR_CHECK(ret); // 后续应在esp_gatts_cb的ESP_GATTS_REG_EVT事件中创建HID服务 // 5. 设置设备名称并开始广播 (BLE) esp_ble_gap_set_device_name("ESP32_DUAL_KB"); esp_ble_gap_config_adv_data(&adv_data); // 需配置adv_data // 6. 创建按键扫描任务 xTaskCreate(key_scan_task, "key_scan", 2048, NULL, 5, NULL); ESP_LOGI(TAG, "双模蓝牙键盘初始化完成,等待连接..."); }2. 关键逻辑说明
| 模块 | 功能说明 | 关键点 |
|---|---|---|
| GPIO配置 | 初始化五个按键引脚为上拉输入模式。 | 使用gpio_config一次性配置所有按键引脚,提高效率。 |
| 双模蓝牙初始化 | 使能ESP32的蓝牙双模控制器。 | esp_bt_controller_enable(ESP_BT_MODE_BTDM)是关键。 |
| BLE HID服务 | 实现键盘的BLE HID功能。 | 1. 定义符合标准的HID报告描述符(hid_report_descriptor)。2. 在GATT回调中创建包含HID服务、报告特征值等的属性表。 3. 按键事件触发后,组装 hid_keyboard_report_t报告并通过GATT通知发送。 |
| 传统蓝牙SPP | 提供备用串口通信通道。 | 1. 注册SPP回调esp_spp_cb。2. 调用 esp_spp_start_srv启动SPP服务。3. 连接建立后,可使用 esp_spp_write发送数据。 |
| 按键扫描与映射 | 检测按键动作并映射为HID键值。 | 1. 轮询或中断检测GPIO状态变化。 2. 通过 key_map数组将物理GPIO映射为HID Usage ID。3. 区分修饰键( modifiers)和普通键(key_codes),分别处理。 |
| 模式与发送 | 管理两种蓝牙模式的输出。 | 1.BLE HID模式:调用GATT发送报告,主机(如Windows/Mac)会将其识别为标准键盘输入。 2.SPP模式:发送自定义格式字符串,需要在主机端运行一个解析程序将字符串模拟为按键。 |
3. 开发与测试要点
- 环境搭建:确保使用最新ESP-IDF环境,并正确配置
menuconfig中的蓝牙相关选项。 - HID报告描述符:上述示例的描述符是极简版本。完整项目应参考《USB HID Usage Tables》定义更全面的描述符,以兼容所有操作系统。
- 连接与配对:
- BLE:设备广播后,在主机(如PC)蓝牙设置中搜索并配对“ESP32_DUAL_KB”。
- SPP:配对后,主机会出现一个虚拟串口(COMx或/dev/tty...),需要用串口工具或自定义程序连接。
- 功耗优化:如需低功耗运行,在无连接时应停止广播,并让ESP32进入Light-sleep模式,通过按键中断唤醒。
- 扩展性:此框架易于扩展更多按键或复合按键(如Ctrl+C)。只需在
key_map中添加映射,并在send_hid_report中正确组合modifiers和key_codes。
此方案提供了一个可直接编译运行的双模蓝牙键盘核心框架。您需要根据实际的ESP32型号和引脚连接调整GPIO定义,并完善BLE GATT和SPP的具体事件处理逻辑(特别是esp_gatts_cb函数的实现)以完成整个项目。
参考来源
- 物联网开发111 - Micropython ESP32 C3连接4x4矩阵按键模块操作
- 在不使用手机 SDK 的情况下,您可以使用 ESP32-C3 实现 BLE Mesh Provisioner 的功能
- 物联网开发111 - Micropython ESP32 C3连接4x4矩阵按键模块操作_micropython矩阵按键
- esp32-c3 蓝牙 BLE 键盘 串口 低延时 ESP-IDF开发环境
- Firebeetle 2 ESP32 C5开发板Arduino环境搭建
- 合宙ESP32C3 Arduino 初探教程
