From 05f4f117b04c0f63b06a3fe04849f857cfd9ed54 Mon Sep 17 00:00:00 2001 From: skiinder Date: Fri, 13 Mar 2026 16:31:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(hids):=20=E6=B7=BB=E5=8A=A0HID=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E5=9D=97=E6=94=AF=E6=8C=81=E9=94=AE=E7=9B=98?= =?UTF-8?q?=E5=92=8C=E5=A4=9A=E5=AA=92=E4=BD=93=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增hids_module.c实现蓝牙HID服务,支持键盘NKRO和Consumer控制 - 添加hid_report_descriptor.h定义统一的HID描述符,包括键盘、多媒体和RAW HID - 在CMakeLists.txt中注册hids模块源文件 - 配置prj.conf启用蓝牙HID相关配置项,设置设备名称和外观 - 修改main.c移除启动LED效果,简化主函数逻辑 - 添加settings_loader_def.h确保模块依赖正确加载 - 配置pm_static.yml分配flash存储空间给mcuboot和settings - 调整电源管理超时时间从20秒增加到300秒 - 启用MCUBOOT引导加载器支持 --- CMakeLists.txt | 1 + inc/hid_report_descriptor.h | 104 +++++++++++++++++++ inc/settings_loader_def.h | 17 ++++ pm_static.yml | 39 ++++++++ prj.conf | 36 ++++++- src/main.c | 19 ---- src/modules/hids_module.c | 193 ++++++++++++++++++++++++++++++++++++ sysbuild.conf | 1 + 8 files changed, 390 insertions(+), 20 deletions(-) create mode 100644 inc/hid_report_descriptor.h create mode 100644 inc/settings_loader_def.h create mode 100644 pm_static.yml create mode 100644 src/modules/hids_module.c create mode 100644 sysbuild.conf diff --git a/CMakeLists.txt b/CMakeLists.txt index 8969bc3..6d89e5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,4 +18,5 @@ target_sources(app PRIVATE src/modules/battery_module.c src/modules/button_map_module.c src/modules/mode_switch_module.c + src/modules/hids_module.c ) diff --git a/inc/hid_report_descriptor.h b/inc/hid_report_descriptor.h new file mode 100644 index 0000000..6e2d1c8 --- /dev/null +++ b/inc/hid_report_descriptor.h @@ -0,0 +1,104 @@ +#ifndef HID_REPORT_DESCRIPTOR_H_ +#define HID_REPORT_DESCRIPTOR_H_ + +#include "hid_types.h" +#include + +/* + * HID_USAGE_PAGE() 只支持 1 字节 Usage Page。 + * Vendor Defined Page(0xFF00) 需要 2 字节编码,因此在本地补一个 16 位版本, + * 避免在描述符里混用裸字节,后续维护时可以一眼看出字段语义。 + */ +#define HID_USAGE_PAGE16(page_lsb, page_msb) \ + HID_ITEM(HID_ITEM_TAG_USAGE_PAGE, HID_ITEM_TYPE_GLOBAL, 2), page_lsb, page_msb + +/* + * 键盘(NKRO) + Consumer 的复合 Report 描述符: + * - USB Report 接口和 BLE HIDS Report Map 统一使用这份定义, + * 避免两边手写常量后长期演进出现不一致。 + */ +#define HID_DESC_KEYBOARD_NKRO_CONSUMER() \ + { \ + /* Generic Desktop 页:声明这是一个 Keyboard Application 集合。 */ \ + HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), \ + HID_USAGE(HID_USAGE_GEN_DESKTOP_KEYBOARD), \ + HID_COLLECTION(HID_COLLECTION_APPLICATION), \ + HID_REPORT_ID(REPORT_ID_KEYBOARD), \ + \ + /* Keyboard/Keypad 页:先定义 8bit Modifier,再定义 232bit NKRO 位图。 */ \ + HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP_KEYPAD), \ + HID_USAGE_MIN8(0xE0), \ + HID_USAGE_MAX8(0xE7), \ + HID_LOGICAL_MIN8(0), \ + HID_LOGICAL_MAX8(1), \ + HID_REPORT_SIZE(1), \ + HID_REPORT_COUNT(8), \ + HID_INPUT(0x02), \ + HID_USAGE_MIN8(0x00), \ + HID_USAGE_MAX8(0xE7), \ + HID_LOGICAL_MIN8(0), \ + HID_LOGICAL_MAX8(1), \ + HID_REPORT_SIZE(1), \ + HID_REPORT_COUNT(0xE7 + 1), \ + HID_INPUT(0x02), \ + \ + /* Report 协议下键盘 LED 输出(NumLock/CapsLock/ScrollLock/Compose/Kana)。 */ \ + HID_USAGE_PAGE(0x08U), \ + HID_USAGE_MIN8(0x01), \ + HID_USAGE_MAX8(0x05), \ + HID_LOGICAL_MIN8(0), \ + HID_LOGICAL_MAX8(1), \ + HID_REPORT_SIZE(1), \ + HID_REPORT_COUNT(5), \ + HID_OUTPUT(0x02), \ + /* 补齐到 1 字节:3bit padding,标记为常量。 */ \ + HID_REPORT_SIZE(3), \ + HID_REPORT_COUNT(1), \ + HID_OUTPUT(0x01), \ + HID_END_COLLECTION, \ + \ + /* Consumer 页:使用 16bit Usage 承载多媒体按键(音量/播放/亮度等)。 */ \ + HID_USAGE_PAGE(0x0CU), \ + HID_USAGE(0x01U), \ + HID_COLLECTION(HID_COLLECTION_APPLICATION), \ + HID_REPORT_ID(REPORT_ID_CONSUMER), \ + HID_LOGICAL_MIN8(0), \ + HID_LOGICAL_MAX16(0xEA, 0x00), \ + HID_USAGE_MIN16(0x00, 0x00), \ + HID_USAGE_MAX16(0xEA, 0x00), \ + HID_REPORT_SIZE(16), \ + HID_REPORT_COUNT(1), \ + HID_INPUT(0x00), \ + HID_END_COLLECTION, \ + } + +/* + * RAW HID 的固定 64 字节输入/输出描述符。 + * 设计意图: + * - 采用 Vendor Defined(0xFF00) 页,避免与标准键盘/多媒体语义冲突; + * - IN/OUT 都固定 64 字节,便于固件与上位机用定长帧做双向透传; + * - 不使用 Report ID,接口只承载一个 RAW Report,减少主机端解析分支。 + */ +#define HID_DESC_RAW_64() \ + { \ + /* Vendor Defined 页(0xFF00):供厂商私有协议传输,不绑定标准 HID 语义。 */ \ + HID_USAGE_PAGE16(0x00, 0xFF), \ + HID_USAGE(0x01), \ + HID_COLLECTION(HID_COLLECTION_APPLICATION), \ + HID_LOGICAL_MIN8(0), \ + HID_LOGICAL_MAX16(0xFF, 0x00), \ + HID_REPORT_SIZE(8), \ + \ + /* 输入页:定义 64 字节 Input 报文,Data|Var|Abs(0x02) 与原描述符一致。 */ \ + HID_REPORT_COUNT(0x40), \ + HID_USAGE(0x01), \ + HID_INPUT(0x02), \ + \ + /* 输出页:定义 64 字节 Output 报文,与输入长度对齐,简化双向协议。 */ \ + HID_REPORT_COUNT(0x40), \ + HID_USAGE(0x01), \ + HID_OUTPUT(0x02), \ + HID_END_COLLECTION, \ + } + +#endif diff --git a/inc/settings_loader_def.h b/inc/settings_loader_def.h new file mode 100644 index 0000000..8d2080e --- /dev/null +++ b/inc/settings_loader_def.h @@ -0,0 +1,17 @@ +/* + * Defines modules that must reach READY before CAF settings_loader + * calls settings_load(). + */ + +/* Enforce single inclusion in the final link unit. */ +const struct {} settings_loader_def_include_once; + +#include + +static inline void get_req_modules(struct module_flags *mf) +{ + module_flags_set_bit(mf, MODULE_IDX(main)); +#ifdef CONFIG_CAF_BLE_STATE + module_flags_set_bit(mf, MODULE_IDX(ble_state)); +#endif +} diff --git a/pm_static.yml b/pm_static.yml new file mode 100644 index 0000000..2f28ea2 --- /dev/null +++ b/pm_static.yml @@ -0,0 +1,39 @@ +mcuboot: + address: 0x0 + end_address: 0xc000 + region: flash_primary + size: 0xc000 + +mcuboot_pad: + address: 0xc000 + end_address: 0xc200 + region: flash_primary + size: 0x200 + +app: + address: 0xc200 + end_address: 0x82000 + region: flash_primary + size: 0x75e00 + +mcuboot_primary: + address: 0xc000 + end_address: 0x82000 + orig_span: &id001 + - mcuboot_pad + - app + region: flash_primary + size: 0x76000 + span: *id001 + +mcuboot_secondary: + address: 0x82000 + end_address: 0xf8000 + region: flash_primary + size: 0x76000 + +settings_storage: + address: 0xf8000 + end_address: 0x100000 + region: flash_primary + size: 0x8000 diff --git a/prj.conf b/prj.conf index 94bc21f..3076c5b 100644 --- a/prj.conf +++ b/prj.conf @@ -1,6 +1,38 @@ CONFIG_CAF=y CONFIG_HEAP_MEM_POOL_SIZE=2048 CONFIG_LOG=y +CONFIG_BOOTLOADER_MCUBOOT=y + +CONFIG_BT=y +CONFIG_BT_PERIPHERAL=y +CONFIG_BT_SMP=y +CONFIG_BT_DEVICE_NAME="new_kbd" +CONFIG_BT_DEVICE_APPEARANCE=961 +CONFIG_BT_MAX_CONN=1 +CONFIG_SETTINGS=y +CONFIG_SETTINGS_NVS=y +CONFIG_NVS=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_BT_SETTINGS=y + +CONFIG_CAF_BLE_STATE=y +CONFIG_CAF_BLE_ADV=y +CONFIG_CAF_SETTINGS_LOADER=y +CONFIG_BT_ADV_PROV_FLAGS=y +CONFIG_BT_ADV_PROV_GAP_APPEARANCE=y +CONFIG_BT_ADV_PROV_DEVICE_NAME=y +CONFIG_BT_ADV_PROV_SWIFT_PAIR=y + +CONFIG_BT_HIDS=y +CONFIG_BT_CONN_CTX=y +CONFIG_BT_GATT_POOL=y +CONFIG_BT_GATT_CHRC_POOL_SIZE=16 +CONFIG_BT_GATT_UUID16_POOL_SIZE=24 +CONFIG_BT_HIDS_ATTR_MAX=32 +CONFIG_BT_HIDS_INPUT_REP_MAX=2 +CONFIG_BT_HIDS_OUTPUT_REP_MAX=1 +CONFIG_BT_HIDS_FEATURE_REP_MAX=0 CONFIG_LED=y CONFIG_LED_GPIO=y @@ -9,7 +41,7 @@ CONFIG_CAF_LEDS_GPIO=y CONFIG_CAF_LEDS_PM_EVENTS=y CONFIG_CAF_POWER_MANAGER=y -CONFIG_CAF_POWER_MANAGER_TIMEOUT=20 +CONFIG_CAF_POWER_MANAGER_TIMEOUT=300 CONFIG_CAF_POWER_MANAGER_ERROR_TIMEOUT=10 CONFIG_REBOOT=y CONFIG_CAF_KEEP_ALIVE_EVENTS=y @@ -22,3 +54,5 @@ CONFIG_CAF_BUTTONS_DEBOUNCE_INTERVAL=10 CONFIG_ADC=y CONFIG_I2C=y CONFIG_IP5305=y + +CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096 diff --git a/src/main.c b/src/main.c index a2a2676..f162000 100644 --- a/src/main.c +++ b/src/main.c @@ -3,33 +3,14 @@ #define MODULE main #include -#include -#include #include LOG_MODULE_REGISTER(MODULE); -/* - * 通过 CAF leds 模块控制第 0 号 LED 常亮。 - * 颜色值在单色 LED 上会被折算为亮度,因此这里使用白色全亮。 - */ -static const struct led_effect startup_led_effect = - LED_EFFECT_LED_ON(LED_COLOR(255, 255, 255)); - int main(void) { if (app_event_manager_init()) { LOG_ERR("Application Event Manager not initialized"); } else { - /* - * 先提交 led_event,再上报 main ready。 - * CAF leds 模块会缓存 effect,并在收到 main ready 完成初始化后开始输出。 - */ - struct led_event *event = new_led_event(); - - event->led_id = 0; - event->led_effect = &startup_led_effect; - APP_EVENT_SUBMIT(event); - module_set_state(MODULE_STATE_READY); } diff --git a/src/modules/hids_module.c b/src/modules/hids_module.c new file mode 100644 index 0000000..0d4b57a --- /dev/null +++ b/src/modules/hids_module.c @@ -0,0 +1,193 @@ +#include + +#include + +#define MODULE hids +#include +#include + +#include "hid_types.h" +#include "hid_report_descriptor.h" + +#include +LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); + +#define INPUT_REPORT_COUNT 2 +#define OUTPUT_REPORT_COUNT 1 +#define KEYBOARD_REPORT_LEN 30 +#define CONSUMER_REPORT_LEN 2 +#define KEYBOARD_LED_REPORT_LEN 1 + +/* 注册 HIDS 实例。此版本聚焦最小可用链路(Boot + Report)。 */ +BT_HIDS_DEF(hids_obj, INPUT_REPORT_COUNT, OUTPUT_REPORT_COUNT, 0); + +static struct bt_conn *active_conn; +static enum bt_hids_pm current_pm = BT_HIDS_PM_REPORT; + + +static void pm_evt_handler(enum bt_hids_pm_evt evt, struct bt_conn *conn) +{ + ARG_UNUSED(conn); + + switch (evt) { + case BT_HIDS_PM_EVT_BOOT_MODE_ENTERED: + current_pm = BT_HIDS_PM_BOOT; + LOG_INF("HIDS protocol: boot"); + break; + case BT_HIDS_PM_EVT_REPORT_MODE_ENTERED: + current_pm = BT_HIDS_PM_REPORT; + LOG_INF("HIDS protocol: report"); + break; + default: + break; + } +} + +static void report_notify_handler(enum bt_hids_notify_evt evt) +{ + ARG_UNUSED(evt); +} + +static void boot_keyboard_notif_handler(enum bt_hids_notify_evt evt) +{ + ARG_UNUSED(evt); +} + +static void boot_keyboard_output_report_handler(struct bt_hids_rep *rep, + struct bt_conn *conn, + bool write) +{ + ARG_UNUSED(conn); + + /* Basic boot protocol support: accept host LED writes and keep state locally. */ + if (!write || !rep || (rep->size == 0) || !rep->data) { + return; + } + + LOG_DBG("Boot KB out report 0x%02x", rep->data[0]); +} + +static void keyboard_output_report_handler(struct bt_hids_rep *rep, + struct bt_conn *conn, + bool write) +{ + ARG_UNUSED(conn); + + /* + * 该回调用于 Report 协议的键盘 LED 输出(NumLock 等)。 + * 这里仅做最小解析并暴露注册回调,具体业务(例如驱动指示灯)留给上层实现。 + */ + if (!write || !rep || !rep->data || (rep->size < KEYBOARD_LED_REPORT_LEN)) { + return; + } + + uint8_t leds = rep->data[0]; + LOG_DBG("Report KB out report 0x%02x", leds); + + /* + * 预留:后续在这里把 LED 输出转换为 CAF 事件(例如 NumLock 状态事件), + * 由上层模块消费并驱动板级指示灯。 + */ + ARG_UNUSED(leds); +} + +static int hids_service_init(void) +{ + static const uint8_t report_map[] = HID_DESC_KEYBOARD_NKRO_CONSUMER(); + struct bt_hids_init_param init_param = { 0 }; + struct bt_hids_inp_rep *input_report = &init_param.inp_rep_group_init.reports[0]; + struct bt_hids_outp_feat_rep *output_report = &init_param.outp_rep_group_init.reports[0]; + + init_param.info.bcd_hid = 0x0101; + init_param.info.b_country_code = 0x00; + init_param.info.flags = BT_HIDS_REMOTE_WAKE | BT_HIDS_NORMALLY_CONNECTABLE; + + init_param.rep_map.data = report_map; + init_param.rep_map.size = sizeof(report_map); + + input_report[0].id = REPORT_ID_KEYBOARD; + input_report[0].size = KEYBOARD_REPORT_LEN; + input_report[0].handler = report_notify_handler; + + input_report[1].id = REPORT_ID_CONSUMER; + input_report[1].size = CONSUMER_REPORT_LEN; + input_report[1].handler = report_notify_handler; + + /* + * Report 协议键盘输出报告: + * 与 Report Map 中 REPORT_ID_KEYBOARD 下定义的 1 字节 LED Output 对齐。 + */ + output_report[0].id = REPORT_ID_KEYBOARD; + output_report[0].size = KEYBOARD_LED_REPORT_LEN; + output_report[0].handler = keyboard_output_report_handler; + + init_param.inp_rep_group_init.cnt = INPUT_REPORT_COUNT; + init_param.outp_rep_group_init.cnt = OUTPUT_REPORT_COUNT; + init_param.pm_evt_handler = pm_evt_handler; + init_param.is_kb = true; + init_param.boot_kb_notif_handler = boot_keyboard_notif_handler; + init_param.boot_kb_outp_rep_handler = boot_keyboard_output_report_handler; + + return bt_hids_init(&hids_obj, &init_param); +} + +static void handle_ble_peer_event(const struct ble_peer_event *event) +{ + switch (event->state) { + case PEER_STATE_CONNECTED: + __ASSERT_NO_MSG(active_conn == NULL); + active_conn = event->id; + if (bt_hids_connected(&hids_obj, active_conn)) { + LOG_WRN("bt_hids_connected failed"); + } + break; + + case PEER_STATE_DISCONNECTED: + if (active_conn == event->id) { + if (bt_hids_disconnected(&hids_obj, active_conn)) { + LOG_WRN("bt_hids_disconnected failed"); + } + active_conn = NULL; + } + break; + + default: + break; + } +} + +static bool app_event_handler(const struct app_event_header *aeh) +{ + if (is_module_state_event(aeh)) { + const struct module_state_event *event = cast_module_state_event(aeh); + + if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) { + static bool initialized; + + __ASSERT_NO_MSG(!initialized); + initialized = true; + + if (hids_service_init()) { + LOG_ERR("Cannot initialize HIDS service"); + module_set_state(MODULE_STATE_ERROR); + } else { + module_set_state(MODULE_STATE_READY); + } + } + + return false; + } + + if (is_ble_peer_event(aeh)) { + handle_ble_peer_event(cast_ble_peer_event(aeh)); + return false; + } + + __ASSERT_NO_MSG(false); + return false; +} + +APP_EVENT_LISTENER(MODULE, app_event_handler); +/* Ensure GATT HIDS is registered before BLE is enabled by ble_state. */ +APP_EVENT_SUBSCRIBE_EARLY(MODULE, module_state_event); +APP_EVENT_SUBSCRIBE_EARLY(MODULE, ble_peer_event); diff --git a/sysbuild.conf b/sysbuild.conf new file mode 100644 index 0000000..721a76f --- /dev/null +++ b/sysbuild.conf @@ -0,0 +1 @@ +SB_CONFIG_BOOTLOADER_MCUBOOT=y \ No newline at end of file