|
|
本帖最后由 hackhack 于 2026-6-29 17:41 编辑
完整的12按键游戏手柄好了。有两个版本,看你们需要哪个。
main.c内容如下
- #include <stdio.h>
- #include <string.h>
- // #include "esp_log.h"
- #include <stdbool.h>
- #include "nvs_flash.h"
- #include "freertos/FreeRTOS.h"
- #include "freertos/task.h"
- #include "driver/gpio.h"
- /* NimBLE 核心头文件 */
- #include "nimble/nimble_port.h"
- #include "nimble/nimble_port_freertos.h"
- #include "host/ble_hs.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"
- #define TAG "BLE_GAMEPAD"
- /* --- 13个 GPIO 引脚分配 --- */
- // 方向键 (帽子开关)
- #define GPIO_DPAD_UP GPIO_NUM_0
- #define GPIO_DPAD_DOWN GPIO_NUM_1
- #define GPIO_DPAD_LEFT GPIO_NUM_2
- #define GPIO_DPAD_RIGHT GPIO_NUM_4
- // 功能键 (HID 按钮 1-4)
- #define GPIO_BTN_A GPIO_NUM_5
- #define GPIO_BTN_B GPIO_NUM_6
- #define GPIO_BTN_X GPIO_NUM_18
- #define GPIO_BTN_Y GPIO_NUM_7
- // 肩键 (HID 按钮 5-6)
- #define GPIO_BTN_L GPIO_NUM_19
- #define GPIO_BTN_R GPIO_NUM_20
- // 菜单键 (HID 按钮 7-8)
- #define GPIO_BTN_MINUS GPIO_NUM_10
- #define GPIO_BTN_PLUS GPIO_NUM_3
- // LED 指示灯
- #define GPIO_LED_STATUS GPIO_NUM_8
- /* 全局连接句柄 */
- static uint16_t current_conn_handle = BLE_HS_CONN_HANDLE_NONE;
- /* GATT 特征值句柄(用于发送通知) */
- static uint16_t hid_input_report_handle;
- static uint8_t protocol_mode = 0x01; // 0x01 = Report Mode
- /* * 1. 标准 HID 游戏手柄报告描述符
- * 报告结构:
- * Byte 0: 8个常规按钮 (A, B, X, Y, L, R, -, +)
- * Byte 1: 低4位为帽子开关 (0-7表示方向,0x0F释放), 高4位填零对齐
- * Byte 2: X 轴模拟量固定置中 (0)
- * Byte 3: Y 轴模拟量固定置中 (0)
- */
- static const uint8_t hid_report_map[] = {
- 0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
- 0x09, 0x05, // Usage (Game Pad)
- 0xa1, 0x01, // Collection (Application)
- 0x85, 0x01, // Report ID (1)
-
- // 8个标准功能按键 (Bit 0~7)
- 0x05, 0x09, // Usage Page (Button)
- 0x19, 0x01, // Usage Minimum (0x01)
- 0x29, 0x08, // Usage Maximum (0x08)
- 0x15, 0x00, // Logical Minimum (0)
- 0x25, 0x01, // Logical Maximum (1)
- 0x75, 0x01, // Report Size (1)
- 0x95, 0x08, // Report Count (8)
- 0x81, 0x02, // Input (Data,Var,Abs)
-
- // 帽子开关 (D-Pad 方向键)
- 0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
- 0x09, 0x39, // Usage (Hat switch)
- 0x15, 0x00, // Logical Minimum (0) - 对应正上(Up)
- 0x25, 0x07, // Logical Maximum (7) - 对应左上(Up-Left)
- 0x35, 0x00, // Physical Minimum (0)
- 0x46, 0x3B, 0x01, // Physical Maximum (315)
- 0x65, 0x14, // Unit (System: English Rotary, Direction: Angular Position)
- 0x75, 0x04, // Report Size (4 bits)
- 0x95, 0x01, // Report Count (1)
- 0x81, 0x42, // Input (Data,Var,Abs,Null State) - 0x0F 代表无触发
-
- // 4位常数填充(对齐到字节边界)
- 0x75, 0x04, // Report Size (4)
- 0x95, 0x01, // Report Count (1)
- 0x81, 0x03, // Input (Const,Var,Abs)
-
- // 虚拟 X/Y 轴(固定为0维持系统兼容)
- 0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
- 0x09, 0x30, // Usage (X)
- 0x09, 0x31, // Usage (Y)
- 0x15, 0x81, // Logical Minimum (-127)
- 0x25, 0x7f, // Logical Maximum (127)
- 0x75, 0x08, // Report Size (8)
- 0x95, 0x02, // Report Count (2)
- 0x81, 0x02, // Input (Data,Var,Abs)
-
- 0xc0 // End Collection
- };
- /* HID 基础信息 */
- static const uint8_t hid_info[4] = {
- 0x11, 0x01, // bcdHID (v1.11)
- 0x00, // bCountryCode
- 0x03 // Flags: RemoteWake | NormallyConnectable
- };
- /* 报告引用描述符 (指向 Report ID 1, Type: Input) */
- static const uint8_t hid_report_reference[2] = {0x01, 0x01};
- /* PnP ID 信息 (伪装成 Xbox 360 控制器架构,提高 Windows 识别率) */
- static const uint8_t pnp_id[7] = {
- 0x02, // Source: USB IF
- 0x5e, 0x04, // VID: 0x045E (Microsoft)
- 0x8e, 0x02, // PID: 0x028E (Xbox 360 Controller)
- 0x00, 0x01 // Version: 1.0.0
- };
- /* GATT 回调声明 */
- static int gatt_svr_chr_access_dis(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);
- static int gatt_svr_chr_access_hid(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);
- static int ble_app_gap_event(struct ble_gap_event *event, void *arg);
- static void ble_app_advertise(uint8_t addr_type);
- /* GATT 服务及特征值定义 */
- static const struct ble_gatt_svc_def gatt_svr_svcs[] = {
- {
- /* 设备信息服务 (DIS) */
- .type = BLE_GATT_SVC_TYPE_PRIMARY,
- .uuid = BLE_UUID16_DECLARE(0x180A),
- .characteristics = (struct ble_gatt_chr_def[]) { {
- .uuid = BLE_UUID16_DECLARE(0x2A50), // PnP ID
- .access_cb = gatt_svr_chr_access_dis,
- .flags = BLE_GATT_CHR_F_READ,
- }, {
- 0,
- } }
- },
- {
- /* 人机接口设备服务 (HID) */
- .type = BLE_GATT_SVC_TYPE_PRIMARY,
- .uuid = BLE_UUID16_DECLARE(0x1812),
- .characteristics = (struct ble_gatt_chr_def[]) { {
- .uuid = BLE_UUID16_DECLARE(0x2A4A), // HID Information
- .access_cb = gatt_svr_chr_access_hid,
- .flags = BLE_GATT_CHR_F_READ,
- }, {
- .uuid = BLE_UUID16_DECLARE(0x2A4B), // Report Map
- .access_cb = gatt_svr_chr_access_hid,
- .flags = BLE_GATT_CHR_F_READ,
- }, {
- .uuid = BLE_UUID16_DECLARE(0x2A4C), // HID Control Point
- .access_cb = gatt_svr_chr_access_hid,
- .flags = BLE_GATT_CHR_F_WRITE_NO_RSP,
- }, {
- .uuid = BLE_UUID16_DECLARE(0x2A4D), // Report (Input)
- .access_cb = gatt_svr_chr_access_hid,
- .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
- .val_handle = &hid_input_report_handle,
- .descriptors = (struct ble_gatt_dsc_def[]) { {
- .uuid = BLE_UUID16_DECLARE(0x2908), // Report Reference
- .access_cb = gatt_svr_chr_access_hid,
- .att_flags = BLE_ATT_F_READ,
- }, {
- 0,
- } }
- }, {
- .uuid = BLE_UUID16_DECLARE(0x2A4E), // Protocol Mode
- .access_cb = gatt_svr_chr_access_hid,
- .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE_NO_RSP,
- }, {
- 0,
- } }
- },
- { 0 }
- };
- static int gatt_svr_chr_access_dis(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) {
- if (ble_uuid_u16(ctxt->chr->uuid) == 0x2A50) {
- return os_mbuf_append(ctxt->om, pnp_id, sizeof(pnp_id)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
- }
- return BLE_ATT_ERR_UNLIKELY;
- }
- static int gatt_svr_chr_access_hid(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) {
- uint16_t uuid = ble_uuid_u16(ctxt->chr->uuid);
- if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR || ctxt->op == BLE_GATT_ACCESS_OP_READ_DSC) {
- if (uuid == 0x2A4A) return os_mbuf_append(ctxt->om, hid_info, sizeof(hid_info)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
- if (uuid == 0x2A4B) return os_mbuf_append(ctxt->om, hid_report_map, sizeof(hid_report_map)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
- if (uuid == 0x2908) return os_mbuf_append(ctxt->om, hid_report_reference, sizeof(hid_report_reference)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
- if (uuid == 0x2A4E) return os_mbuf_append(ctxt->om, &protocol_mode, sizeof(protocol_mode)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
- if (uuid == 0x2A4D) {
- uint8_t empty_report[4] = {0, 0x0F, 0, 0}; // 帽子开关默认释放值为0x0F
- return os_mbuf_append(ctxt->om, empty_report, sizeof(empty_report)) == 0 ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
- }
- } else if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
- if (uuid == 0x2A4C) return 0;
- if (uuid == 0x2A4E && ctxt->om->om_len == 1) {
- protocol_mode = ctxt->om->om_data[0];
- return 0;
- }
- }
- return BLE_ATT_ERR_UNLIKELY;
- }
- /* 蓝牙 GAP 状态机广播控制 */
- static void ble_app_advertise(uint8_t addr_type) {
- struct ble_gap_adv_params adv_params;
- struct ble_hs_adv_fields fields;
- int rc;
- memset(&fields, 0, sizeof(fields));
- fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
-
- fields.appearance = 0x03C4; // Gamepad 外观代码
- fields.appearance_is_present = 1;
- const char *name = "ESP8684-Gamepad";
- fields.name = (uint8_t *)name;
- fields.name_len = strlen(name);
- fields.name_is_complete = 1;
- ble_uuid16_t hid_uuid = BLE_UUID16_INIT(0x1812);
- fields.uuids16 = &hid_uuid;
- fields.num_uuids16 = 1;
- fields.uuids16_is_complete = 1;
- rc = ble_gap_adv_set_fields(&fields);
- if (rc != 0) {
- //ESP_LOGE(TAG, "设置广播数据失败: %d", rc);
- return;
- }
- memset(&adv_params, 0, sizeof(adv_params));
- adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
- adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
- rc = ble_gap_adv_start(addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_app_gap_event, NULL);
- //if (rc != 0) {
- // ESP_LOGE(TAG, "开启广播失败: %d", rc);
- //} else {
- // ESP_LOGI(TAG, "手柄蓝牙广播已开启...");
- //}
- }
- static int ble_app_gap_event(struct ble_gap_event *event, void *arg) {
- struct ble_gap_conn_desc desc;
- switch (event->type) {
- case BLE_GAP_EVENT_CONNECT:
- //ESP_LOGI(TAG, "设备已连接, 状态=%d", event->connect.status);
- if (event->connect.status == 0) {
- current_conn_handle = event->connect.conn_handle;
- ble_gap_security_initiate(current_conn_handle);
- } else {
- uint8_t addr_type;
- ble_hs_id_infer_auto(0, &addr_type);
- ble_app_advertise(addr_type);
- }
- return 0;
- case BLE_GAP_EVENT_DISCONNECT:
- //ESP_LOGI(TAG, "断开连接,原因=%d", event->disconnect.reason);
- current_conn_handle = BLE_HS_CONN_HANDLE_NONE;
- uint8_t addr_type;
- ble_hs_id_infer_auto(0, &addr_type);
- ble_app_advertise(addr_type);
- return 0;
- case BLE_GAP_EVENT_REPEAT_PAIRING:
- if (ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc) == 0) {
- ble_store_util_delete_peer(&desc.peer_id_addr);
- }
- return BLE_GAP_REPEAT_PAIRING_RETRY;
- case BLE_GAP_EVENT_ENC_CHANGE:
- //ESP_LOGI(TAG, "加密状态改变: %d", event->enc_change.status);
- return 0;
- }
- return 0;
- }
- static void ble_app_on_sync(void) {
- uint8_t addr_type;
- ble_hs_id_infer_auto(0, &addr_type);
- ble_app_advertise(addr_type);
- }
- /* 4. 手柄 HID 完整 4 字节报告发送函数 */
- void send_gamepad_report(uint8_t buttons, uint8_t hat) {
- if (current_conn_handle == BLE_HS_CONN_HANDLE_NONE) {
- return;
- }
- // 结构:[按键字节, 帽子开关字节, X轴字节(0), Y轴字节(0)]
- uint8_t report_data[4] = { buttons, hat, 0x00, 0x00 };
- struct os_mbuf *om = ble_hs_mbuf_from_flat(report_data, sizeof(report_data));
-
- if (om != NULL) {
- int rc = ble_gattc_notify_custom(current_conn_handle, hid_input_report_handle, om);
- if (rc != 0) {
- //ESP_LOGE(TAG, "HID 报告发送失败, 错误码=%d", rc);
- }
- }
- }
- /* 5. 12个按键扫描及 LED 控制任务 */
- void button_scan_task(void *pvParameters) {
- // 汇聚并配置全量 12 个输入引脚掩码
- uint64_t pin_mask = (1ULL << GPIO_DPAD_UP) | (1ULL << GPIO_DPAD_DOWN) |
- (1ULL << GPIO_DPAD_LEFT) | (1ULL << GPIO_DPAD_RIGHT) |
- (1ULL << GPIO_BTN_A) | (1ULL << GPIO_BTN_B) |
- (1ULL << GPIO_BTN_X) | (1ULL << GPIO_BTN_Y) |
- (1ULL << GPIO_BTN_L) | (1ULL << GPIO_BTN_R) |
- (1ULL << GPIO_BTN_MINUS) | (1ULL << GPIO_BTN_PLUS);
- gpio_config_t io_conf = {
- .pin_bit_mask = pin_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);
- // 配置 LED 状态灯引脚
- gpio_config_t led_conf = {
- .pin_bit_mask = (1ULL << GPIO_LED_STATUS),
- .mode = GPIO_MODE_OUTPUT,
- .pull_up_en = GPIO_PULLUP_DISABLE,
- .pull_down_en = GPIO_PULLDOWN_DISABLE,
- .intr_type = GPIO_INTR_DISABLE
- };
- gpio_config(&led_conf);
- gpio_set_level(GPIO_LED_STATUS, 0); // 默认初始熄灭
- uint8_t last_buttons = 0;
- uint8_t last_hat = 0x0F;
- while (1) {
- // 读取原始低电平状态 (0 为按下,取反转为 true 为按下)
- bool up = !gpio_get_level(GPIO_DPAD_UP);
- bool down = !gpio_get_level(GPIO_DPAD_DOWN);
- bool left = !gpio_get_level(GPIO_DPAD_LEFT);
- bool right = !gpio_get_level(GPIO_DPAD_RIGHT);
- bool btn_a = !gpio_get_level(GPIO_BTN_A);
- bool btn_b = !gpio_get_level(GPIO_BTN_B);
- bool btn_x = !gpio_get_level(GPIO_BTN_X);
- bool btn_y = !gpio_get_level(GPIO_BTN_Y);
- bool btn_l = !gpio_get_level(GPIO_BTN_L);
- bool btn_r = !gpio_get_level(GPIO_BTN_R);
- bool minus = !gpio_get_level(GPIO_BTN_MINUS);
- bool plus = !gpio_get_level(GPIO_BTN_PLUS);
- // LED 触发判断:12个按键有任意一个按下就拉高,无按键拉低
- bool any_pressed = up || down || left || right || btn_a || btn_b ||
- btn_x || btn_y || btn_l || btn_r || minus || plus;
- gpio_set_level(GPIO_LED_STATUS, any_pressed ? 1 : 0);
- /* * 帽子开关(8方向 D-Pad)解算逻辑:
- * 0=正上, 1=右上, 2=正右, 3=右下, 4=正下, 5=左下, 6=正左, 7=左上, 0x0F=释放
- */
- uint8_t current_hat = 0x0F;
- if (up && !down && !left && !right) current_hat = 0; // 上
- else if (up && !down && !left && right) current_hat = 1; // 右上
- else if (!up && !down && !left && right) current_hat = 2; // 右
- else if (!up && down && !left && right) current_hat = 3; // 右下
- else if (!up && down && !left && !right) current_hat = 4; // 下
- else if (!up && down && left && !right) current_hat = 5; // 左下
- else if (!up && !down && left && !right) current_hat = 6; // 左
- else if (up && !down && left && !right) current_hat = 7; // 左上
- // 组装常规按键数据(字节0)
- uint8_t current_buttons = 0;
- if (btn_a) current_buttons |= (1 << 0); // 1号键 - A
- if (btn_b) current_buttons |= (1 << 1); // 2号键 - B
- if (btn_x) current_buttons |= (1 << 2); // 3号键 - X
- if (btn_y) current_buttons |= (1 << 3); // 4号键 - Y
- if (btn_l) current_buttons |= (1 << 4); // 5号键 - L肩键
- if (btn_r) current_buttons |= (1 << 5); // 6号键 - R肩键
- if (minus) current_buttons |= (1 << 6); // 7号键 - 减号键
- if (plus) current_buttons |= (1 << 7); // 8号键 - 加号键
- // 发现手柄输入有状态改变,立即上报
- if (current_buttons != last_buttons || current_hat != last_hat) {
- last_buttons = current_buttons;
- last_hat = current_hat;
- send_gamepad_report(current_buttons, current_hat);
- //ESP_LOGI(TAG, "状态更新 - Btns: 0x%02X, Hat: %02X", current_buttons, current_hat);
- }
- vTaskDelay(pdMS_TO_TICKS(15)); // 15ms 周期轮询,自带消抖滤毛刺效果
- }
- }
- void nimble_host_task(void *param) {
- //ESP_LOGI(TAG, "NimBLE 主任务已启动");
- nimble_port_run();
- nimble_port_freertos_deinit();
- }
- /* 程序主入口 */
- void app_main(void) {
- esp_err_t ret = nvs_flash_init();
- if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
- ESP_ERROR_CHECK(nvs_flash_erase());
- ret = nvs_flash_init();
- }
- ESP_ERROR_CHECK(ret);
- ESP_ERROR_CHECK(nimble_port_init());
-
- ble_svc_gap_init();
- ble_svc_gatt_init();
-
- int rc = ble_gatts_count_cfg(gatt_svr_svcs);
- if (rc != 0) return;
- rc = ble_gatts_add_svcs(gatt_svr_svcs);
- if (rc != 0) return;
- ble_hs_cfg.sm_io_cap = BLE_SM_IO_CAP_NO_IO;
- ble_hs_cfg.sm_bonding = 1;
- ble_hs_cfg.sm_mitm = 0;
- ble_hs_cfg.sm_sc = 1;
- ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
- ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
- ble_hs_cfg.store_read_cb = ble_store_config_read;
- ble_hs_cfg.store_write_cb = ble_store_config_write;
- ble_svc_gap_device_name_set("ESP8684-Gamepad");
- ble_hs_cfg.sync_cb = ble_app_on_sync;
- nimble_port_freertos_init(nimble_host_task);
- // 启动12按键和1LED的处理任务
- xTaskCreate(button_scan_task, "button_scan_task", 2048, NULL, 5, NULL);
- }
复制代码
sdkconfig.defaults内容如下
- # 芯片目标
- CONFIG_IDF_TARGET="esp32c2"
- # 26MHz 外置晶振关键配置
- CONFIG_XTAL_FREQ_26=y
- CONFIG_XTAL_FREQ=26
- CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_120=y
- # 闪存大小配置 (2MB)
- CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
- CONFIG_PARTITION_TABLE_CUSTOM=y
- CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
- # 编译器优化:优化大小 (-Os) 极致省闪存
- CONFIG_COMPILER_OPTIMIZATION_SIZE=y
- # 彻底关闭 Wi-Fi,根除共存丢包,极大释放内存空间
- CONFIG_ESP_WIFI_ENABLED=n
- # 蓝牙底层配置:启用 NimBLE,禁用臃肿的 Bluedroid
- CONFIG_BT_ENABLED=y
- CONFIG_BT_BLUEDROID_ENABLED=n
- CONFIG_BT_NIMBLE_ENABLED=y
- # 绑定信息持久化(第二版增加,不开会导致无法自动连接已连接过的蓝牙)
- CONFIG_BT_NIMBLE_NVS_PERSIST=y
- # NimBLE 内存极致裁剪
- CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
- CONFIG_BT_NIMBLE_MAX_BONDS=3
- CONFIG_BT_NIMBLE_MAX_CCCDS=8
- CONFIG_BT_NIMBLE_MSYS1_BLOCK_COUNT=12
- # 关闭UART0,因为要用19、20两个引脚
- # 彻底禁用标准控制台输出(适用于 ESP-IDF v5.x / v6.x)
- CONFIG_ESP_CONSOLE_NONE=y
- # 如果是较老版本的 ESP-IDF,请使用这行:
- # CONFIG_ESP_CONSOLE_UART_NONE=y
复制代码
初步测试正常。把编译的固件也放出来吧。没有家元的留下邮箱。
版本1:不会绑定蓝牙,每次断电重启需要重新连接。
版本2:开启了绑定蓝牙,连接过的设备会自动连接。
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
打赏
-
查看全部打赏
|