feat(hids): 添加HID服务模块支持键盘和多媒体功能

- 新增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引导加载器支持
This commit is contained in:
2026-03-13 16:31:02 +08:00
parent b3516b988a
commit 05f4f117b0
8 changed files with 390 additions and 20 deletions

View File

@@ -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
)

104
inc/hid_report_descriptor.h Normal file
View File

@@ -0,0 +1,104 @@
#ifndef HID_REPORT_DESCRIPTOR_H_
#define HID_REPORT_DESCRIPTOR_H_
#include "hid_types.h"
#include <zephyr/usb/class/usbd_hid.h>
/*
* 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

17
inc/settings_loader_def.h Normal file
View File

@@ -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 <caf/events/module_state_event.h>
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
}

39
pm_static.yml Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -3,33 +3,14 @@
#define MODULE main
#include <caf/events/module_state_event.h>
#include <caf/events/led_event.h>
#include <caf/led_effect.h>
#include <zephyr/logging/log.h>
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);
}

193
src/modules/hids_module.c Normal file
View File

@@ -0,0 +1,193 @@
#include <bluetooth/services/hids.h>
#include <app_event_manager.h>
#define MODULE hids
#include <caf/events/module_state_event.h>
#include <caf/events/ble_common_event.h>
#include "hid_types.h"
#include "hid_report_descriptor.h"
#include <zephyr/logging/log.h>
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);

1
sysbuild.conf Normal file
View File

@@ -0,0 +1 @@
SB_CONFIG_BOOTLOADER_MCUBOOT=y