一、项目介绍
本项目使用Nordic Semiconductor推出的 NRF 7002DK实现蓝牙低功耗的鼠标+键盘复合设备。
板卡使用的主控芯片nRF5340是一颗Cortex-M33双核无线Soc,分别为128MHz/1MB Flash/512KB SRAM的应用处理核和64MHz/256KB Flash/64KB SRAM的网络处理核。
芯片支持蓝牙、Thread、Zigbee和NFC等无线通信功能,另外开发板上还集成了Nordic的首颗Wifi芯片,提供了双频(2.4GHz和5GHz)Wi-Fi接入能力,支持Wi-Fi 6技术。为开发板提供了Matter智能家居协议完整的无线接入能力。
二、预备知识和参考资料
2.1 USB HID协议
目前常用的鼠标键盘绝多数都是USB接口的,其使用的就是基于USB的HID协议;HID是一类相对复杂的协议,但也提供了非常灵活的方便外设与主机交互。
USB HID类是USB设备的一个标准设备类,包括的设备非常多。HID类设备定义它属于人机交互操作的设备,用于控制计算机操作的一些方面,如USB鼠标、USB键盘、USB游戏操纵杆等。
常见的无线键鼠有两种实现方式;一个是生产厂家提供配套的USB接口收发器,收发器被电脑识别为USB键鼠设备,并通过无线通讯接收鼠标键盘的消息完成输入。另一种则是基于BLE蓝牙低功耗的实现,BLE提供了一套规范将满足规范的蓝牙设备映射成USB HID类设备,本项目要实现的就是基于这套规范的蓝牙键鼠设备。
相关协议可以在USB组织官网上查询
[Human Interface Devices (HID) Specifications and Tools | USB-IF](https://www.usb.org/hid)
2.2 蓝牙ATT/GATT规范
BLE采用了client/server (C/S) 架构来进行数据交互,一般而言外围设备作为Server提供服务,手机、平板、电脑等中心设备作为Client请求访问外围设备提供的服务。
ATT协议可以类比为服务器上的数据库,在Server上组织了一条条attribute作为服务器提供的数据条目,Client则可以通过ATT协议定义的规范来访问这些attribute数据。
GATT规范在ATT协议的基础上,将一条条没有明确物理意义的attribute组织成有明确意义的服务;比如将几条attribute按照规范组合在一起可以表示一个心率监测服务,它可以提供心率、检测位置等数据。
而在本项目中,主要就是通过GATT规范中的HID Service来实现将BLE设备模拟成USB HID设备。
相关协议可以在蓝牙组织官网上下载
[Core Specification | Bluetooth® Technology Website](https://www.bluetooth.com/specifications/specs/core-specification-5-3/)
2.3 HOGP和HIDS规范
前面提到本次项目的主要目标是实现GATT中的HID Service,那么HOGP和HIDS两篇文档就提供了详细的实现方式,也是本项目中需要着重阅读理解的规范。
HOGP全称HID OVER GATT PROFILE SPECIFICATION,它描述了一个将BLE映射成USB HID设备的需求,包括外围设备和作为中心设备的手机电脑各需要实现哪些功能。本项目主要实现的是外围设备端,主要参考对外围设备的要求。在规范中可以看到外围设备是必须要实现HID Service、Battery Service和Device Information Service三个服务,其中HID Service是本次的重点。
HIDS全称Human Interface Device Service,这篇文档详细描述了上面提到的HID Service的实现细节。
两篇文档也都可以在蓝牙组织官网上下载
[HID over GATT Profile | Bluetooth® Technology Website](https://www.bluetooth.com/specifications/specs/hid-over-gatt-profile-1-0/)
[Human Interface Device Service | Bluetooth® Technology Website](https://www.bluetooth.com/specifications/specs/human-interface-device-service-1-0/)
三、软件实现
软件实现使用Nordic官方开发板提供的基于ZephyrOS的SDK,基于Zephyr的示例代码项目peripheral_hids修改而来,示例代码实现了一个基于BLE HID的标准鼠标,我们需要在代码中添加BLE HID键盘的实现。
3.1 HID报表描述符
static const uint8_t report_map[] = {
0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */
0x09, 0x02, /* Usage (Mouse) */
0xA1, 0x01, /* Collection (Application) */
0x85, 0x01, /* Report Id (1) */
0x09, 0x01, /* Usage (Pointer) */
0xA1, 0x00, /* Collection (Physical) */
0x05, 0x09, /* Usage Page (Button) */
0x19, 0x01, /* Usage Minimum (0x01) */
0x29, 0x03, /* Usage Maximum (0x03) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x01, /* Logical Maximum (1) */
0x95, 0x03, /* Report Count (3) */
0x75, 0x01, /* Report Size (1) */
0x81, 0x02, /* Input (Data,Var,Abs,No Wrap,Linear,...) */
0x95, 0x01, /* Report Count (1) */
0x75, 0x05, /* Report Size (5) */
0x81, 0x03, /* Input (Const,Var,Abs,No Wrap,Linear,...) */
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, 0x06, /* Input (Data,Var,Rel,No Wrap,Linear,...) */
0xC0, /* End Collection */
0xC0, /* End Collection */
0x05, 0x01, /* Usage Page (Generic Desktop Ctrls) */
0x09, 0x06, /* Usage (Keyboard) */
0xA1, 0x01, /* Collection (Application) */
0x85, 0x02, /* Report ID (2) */
0x05, 0x07, /* Usage Page (Key Codes) */
0x19, 0xe0, /* Usage Minimum (224) */
0x29, 0xe7, /* Usage Maximum (231) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x01, /* Logical Maximum (1) */
0x75, 0x01, /* Report Size (1) */
0x95, 0x08, /* Report Count (8) */
0x81, 0x02, /* Input (Data, Variable, Absolute) */
0x95, 0x01, /* Report Count (1) */
0x75, 0x08, /* Report Size (8) */
0x81, 0x01, /* Input (Constant) reserved byte(1) */
0x95, 0x06, /* Report Count (6) */
0x75, 0x08, /* Report Size (8) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x65, /* Logical Maximum (101) */
0x05, 0x07, /* Usage Page (Key codes) */
0x19, 0x00, /* Usage Minimum (0) */
0x29, 0x65, /* Usage Maximum (101) */
0x81, 0x00, /* Input (Data, Array) Key array(6 bytes) */
0x85, 0x03, /* Report ID (3) */
0x95, 0x05, /* Report Count (5) */
0x75, 0x01, /* Report Size (1) */
0x05, 0x08, /* Usage Page (Page# for LEDs) */
0x19, 0x01, /* Usage Minimum (1) */
0x29, 0x05, /* Usage Maximum (5) */
0x91, 0x02, /* Output (Data, Variable, Absolute), */
0x95, 0x01, /* Report Count (1) */
0x75, 0x03, /* Report Size (3) */
0x91, 0x01, /* Output (Data, Variable, Absolute), */
0xC0, /* End Collection (Application) */
};
修改HID报表描述符如上,实现鼠标+键盘复合设备;报表描述符描述了3个报表,报表1一共3个字节,用于上报鼠标按键状态和xy位移;报表2一共8个字节,用于上报键盘按键;报表3一个字节,用于输出键盘灯状态。
3.2 HID服务定义
/* HID Service Declaration */
BT_GATT_SERVICE_DEFINE(hog_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_HIDS), /* attr 0 */
/* hids_info */
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_INFO, /* attr 1, 2 */
BT_GATT_CHRC_READ,
BT_GATT_PERM_READ,
hog_gatt_attr_read, NULL, (void *)HOG_HIDS_CHAR_INFO),
/* report map */
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT_MAP, /* attr 3, 4 */
BT_GATT_CHRC_READ,
BT_GATT_PERM_READ,
hog_gatt_attr_read, NULL, (void *)HOG_HIDS_CHAR_REPORT_MAP),
/* mouse report in */
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, /* attr 5, 6*/
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
SAMPLE_BT_PERM_READ,
hog_gatt_attr_read, NULL, (void *)HOG_HIDS_CHAR_REPORT_MOUSE_IN),
BT_GATT_CCC(hog_ccc_changed, /* attr 7 */
SAMPLE_BT_PERM_READ | SAMPLE_BT_PERM_WRITE),
BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, /* attr 8 */
BT_GATT_PERM_READ,
hog_gatt_attr_desc_read, NULL, (void *)HOG_HIDS_DESC_REPORT_MOUSE_IN),
/* keyboard report in */
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, /* attr 9, 10 */
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
SAMPLE_BT_PERM_READ,
hog_gatt_attr_read, NULL, (void *)HOG_HIDS_CHAR_REPORT_KEYBOARD_IN),
BT_GATT_CCC(hog_ccc_changed, /* attr 11 */
SAMPLE_BT_PERM_READ | SAMPLE_BT_PERM_WRITE),
BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, /* attr 12 */
BT_GATT_PERM_READ,
hog_gatt_attr_desc_read,
NULL,
(void*)HOG_HIDS_DESC_REPORT_KEYBOARD_IN),
/* keyboard led out */
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, /* attr 13, 14 */
BT_GATT_CHRC_READ|BT_GATT_CHRC_WRITE|BT_GATT_CHRC_WRITE_WITHOUT_RESP,
SAMPLE_BT_PERM_WRITE|SAMPLE_BT_PERM_READ,
hog_gatt_attr_read,
hog_gatt_attr_write,
(void *)HOG_HIDS_CHAR_REPORT_KEYBOARD_LED_OUT),
BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, /* attr 15 */
BT_GATT_PERM_READ,
hog_gatt_attr_desc_read,
NULL,
(void *)HOG_HIDS_DESC_REPORT_KEYBOARD_LED_OUT),
BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_CTRL_POINT, /* attr 16, 17 */
BT_GATT_CHRC_WRITE_WITHOUT_RESP,
BT_GATT_PERM_WRITE,
NULL,
hog_gatt_attr_write,
(void *)HOG_HIDS_CHAR_CTRL_POINT)
);
基于要实现的功能和规范要求定义HID服务如上。
服务中需有一个HIDS_INFO特征值(CHaracteristic)用于映射HID INFO到USB HID、
一个HIDS_REPORT_MAP特征值用于上报HID报表描述符、三个HIDS_REPORT特征值分别用于传输上一小节中的报表,这三个报表通过附属的HIDS_REPORT_REF描述符来进行区分。
3.3 LED输出
在3.2小节的HID服务定义中,键盘LED的输出报表关联回调函数hog_gatt_attr_write
ssize_t hog_gatt_attr_write(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{
...
switch (ud)
{
case HOG_HIDS_CHAR_REPORT_KEYBOARD_LED_OUT:
data = (uint8_t *)hid.reports[2].report_data;
value_len = hid.reports[2].report_data_len;
if (offset + len > value_len) {
ret = BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
else {
memcpy(data + offset, buf, len);
ret = len;
printk("led status: %d\n", hid.reports[2].report_data[0]);
}
break;
...
}
当主机该表LED状态时,数据会被写入表示LED状态的全局变量`hid.reports[2].report_data[0]`中;另有一个线程读取该状态并开关LED灯
static void keyboard_led_out()
{
static int8_t led_status = 0;
/* led状态无变化,直接返回 */
if (led_status == report_keyboard_led[0])
{
return;
}
else
{
led_status = report_keyboard_led[0];
}
if ((led_status & (1 << (LED_CAPS_LOCK - 1))) != 0)
{
dk_set_led_on(1);
printk("CAPS_LOCK ON\n");
}
else
{
dk_set_led_off(1);
printk("CAPS_LOCK OFF\n");
}
}
3.4 鼠标和键盘输入
和上一小节类似,鼠标和键盘的数据发送可以通过修改对应的报表内容实现。
键盘的扫描和上报
static void keyboard_key_scan()
{
int8_t mod_key_state;
int8_t pressed_keys[6];
get_mod_keys((uint8_t *)&mod_key_state);
get_normal_keys((uint8_t *)pressed_keys, 6);
/* 扫描键盘状态无变化,可以不用上报 */
if ((mod_key_state != report_keyboard[0]) ||
(memcmp(pressed_keys, &report_keyboard[2], 6) != 0))
{
if ((reports[1].cccd_value & 0x01) != 0)
{
report_keyboard[0] = mod_key_state;
memcpy(&report_keyboard[2], pressed_keys, 6);
bt_gatt_notify(NULL, &hog_svc.attrs[9], reports[1].report_data, reports[1].report_data_len);
}
}
}
鼠标按键的扫描和上报
static void mouse_key_scan()
{
if (report_mouse[0] == mouse_btn_state)
{
return;
}
else
{
report_mouse[0] = mouse_btn_state;
}
if ((reports[0].cccd_value & 0x01) != 0)
{
bt_gatt_notify(NULL, &hog_svc.attrs[5], reports[0].report_data, reports[0].report_data_len);
}
}
四、功能展示
参考视频分享
五、活动体会
这次Nordic的开发板提供的基于ZephyrOS的开发环境与以往其他的MCU开发方式有着很大的区别,特别是它类似Linux的设备树驱动那一套工具,对于芯片厂商和拥有众多产品线的设备厂家来说可以节省大量的重复工作,只需要修改设备树文件就可以完整的定义一个全新的设备,极大的方便了驱动开发,应用移植等工作。虽然初看上去有点难以上手,但是其实深入了解其原理后发现其实这套工具也并不需要投入太多精力学习。
另外非常感谢硬禾学堂和得捷电子联合推出这系列活动,让我们能接触到这些优秀的开发板。目前的项目大多是个人独立完成,但实际的项目往往需要多人的合作,希望硬禾后期也能推出一些可以多人合作的项目,提升大家的团队协作能力。