一、项目目标:
任务一:
二、项目背景
本项目预期实现的简易蓝牙键鼠复合设备需要用到nRF7002板卡上的nRF5340芯片,这颗芯片支持低功耗蓝牙、蓝牙Mesh、NFC、Thread和Zigbee的双核蓝牙5.2 SoC。这颗芯片在板子上的位置如下所示。
nRF5340是一款全合一(all-in-one) SoC器件,嵌入了一个 128MHz Arm Cortex-M33 应用处理器和一个 64MHz 高效率网络处理器,是全球首款拥有两个Arm® Cortex®-M33处理器的无线连接SoC。两个灵活的处理器、先进的功能以及最高105°C的工作温度,使其成为低功耗音频、专业照明、高级可穿戴设备和其他复杂物联网应用的理想选择。
nRF5340 系统级芯片支持各种无线协议。它支持低功耗蓝牙,并且蓝牙测向可实现所有到达角(AoA)和出发角(AoD)的测量功能。此外,它支持低功耗蓝牙音频,2 Mbps高吞吐量、广播扩展和长距离。像蓝牙Mesh、Thread和Zigbee这样的Mesh协议可以与低功耗蓝牙同时运行,从而使智能手机能够配网、入网、配置和控制Mesh节点。还支持NFC、ANT、802.15.4和2.4 GHz专有协议。nRF Connect SDK是nRF5340 SoC的软件开发套件,它提供完整的解决方案,集成了Zephyr RTOS、协议栈、应用示例和硬件驱动程序。
特性:
- 高性能128MHz Arm Cortex-M33应用内核
- 超低功耗64MHz Arm Cortex-M33网络内核
- 多协议无线电支持(低功耗蓝牙、蓝牙Mesh、Thread和Zigbee)
此外,为了实现蓝牙键鼠复合设备还需要了解USB-HID、Hid-Over GATT以及BLE基础,本文涉及到的相关知识点也尽可能详细的在第四章中描述。本文的实现也是借鉴了Nordic提供的例程:hid_mouse与hid_keyboard,在此基础之上添加了一些代码,从而能快速的完成本任务。
三、软件流程图
本项目使用的是Nordic最新的nrf connect SDK version 2.4.0。这是一套基于Zephyr RTOS的软件框架,对nrf7002dk板卡资源进行了全面的支持。比如流程图中用到的GPIO模块,就由lbs组件即Led Button Service来提供支持。
四、项目详细实现描述
本项目中按键的检测,LED灯的亮灭都离不开GPIO模块,在main()函数中通过调用config_gpio()函数来对相关的按键事件回调函数进行注册。这样按键按下后就可以进入button_changed()函数做相应的处理。
接下来就是比较重要的hid_init(),其中非常重要的一个地方就是报告地图,它决定了一个HID设备能够收发的数据及其格式。本项目中使用的报告地图定义如下,其包含三个INPUT报告(键盘上报,鼠标按键上报,鼠标X,Y位置上报),一个OUTPUT报告(键盘大小写状态CAPS lock)。
static const uint8_t report_map[] = {
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x06, /* Usage (Keyboard) */
0xA1, 0x01, /* Collection (Application) */
/* Keys */
#if INPUT_REP_KEYS_REF_ID
0x85, INPUT_REP_KEYS_REF_ID,
#endif
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) */
/* LED */
#if OUTPUT_REP_KEYS_REF_ID
0x85, OUTPUT_REP_KEYS_REF_ID,
#endif
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), */
/* Led report */
0x95, 0x01, /* Report Count (1) */
0x75, 0x03, /* Report Size (3) */
0x91, 0x01, /* Output (Data, Variable, Absolute), */
/* Led report padding */
0xC0, /* End Collection (Application) */
//***************************************Following are for MOUSE**************************
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x02, /* Usage (Mouse) */
0xA1, 0x01, /* Collection (Application) */
/* Report ID 2: Mouse buttons + scroll/pan */
0x85, 0x02, /* Report Id 2 */
0x09, 0x01, /* Usage (Pointer) */
0xA1, 0x00, /* Collection (Physical) */
0x95, 0x05, /* Report Count (3) */
0x75, 0x01, /* Report Size (1) */
0x05, 0x09, /* Usage Page (Buttons) */
0x19, 0x01, /* Usage Minimum (01) */
0x29, 0x05, /* Usage Maximum (05) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x01, /* Logical Maximum (1) */
0x81, 0x02, /* Input (Data, Variable, Absolute) */
0x95, 0x01, /* Report Count (1) */
0x75, 0x03, /* Report Size (3) */
0x81, 0x01, /* Input (Constant) for padding */
0x75, 0x08, /* Report Size (8) */
0x95, 0x01, /* Report Count (1) */
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x38, /* Usage (Wheel) */
0x15, 0x81, /* Logical Minimum (-127) */
0x25, 0x7F, /* Logical Maximum (127) */
0x81, 0x06, /* Input (Data, Variable, Relative) */
0x05, 0x0C, /* Usage Page (Consumer) */
0x0A, 0x38, 0x02, /* Usage (AC Pan) */
0x95, 0x01, /* Report Count (1) */
0x81, 0x06, /* Input (Data,Value,Relative,Bit Field) */
0xC0, /* End Collection (Physical) */
/* Report ID 3: Mouse motion */
0x85, 0x03, /* Report Id 3 */
0x09, 0x01, /* Usage (Pointer) */
0xA1, 0x00, /* Collection (Physical) */
0x75, 0x0C, /* Report Size (12) */
0x95, 0x02, /* Report Count (2) */
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x30, /* Usage (X) */
0x09, 0x31, /* Usage (Y) */
0x16, 0x01, 0xF8, /* Logical maximum (2047) */
0x26, 0xFF, 0x07, /* Logical minimum (-2047) */
0x81, 0x06, /* Input (Data, Variable, Relative) */
0xC0, /* End Collection (Physical) */
0xC0, /* End Collection (Application) */
/* Report ID 3: Advanced buttons */
};
之后根据上述报告地图中定义的数据格式,在hid_init_obj中对相关的参数进行初始化,最后通过调用初始化函数bt_hids_init()来完成对hid对象的初始化。
hids_init_obj.rep_map.data = report_map;
hids_init_obj.rep_map.size = sizeof(report_map);
hids_init_obj.info.bcd_hid = BASE_USB_HID_SPEC_VERSION;
hids_init_obj.info.b_country_code = 0x00;
hids_init_obj.info.flags = (BT_HIDS_REMOTE_WAKE |
BT_HIDS_NORMALLY_CONNECTABLE);
hids_inp_rep =
&hids_init_obj.inp_rep_group_init.reports[INPUT_REP_KEYS_IDX];
hids_inp_rep->size = INPUT_REPORT_KEYS_MAX_LEN;
hids_inp_rep->id = INPUT_REP_KEYS_REF_ID;
hids_init_obj.inp_rep_group_init.cnt++;
hids_outp_rep =
&hids_init_obj.outp_rep_group_init.reports[OUTPUT_REP_KEYS_IDX];
hids_outp_rep->size = OUTPUT_REPORT_MAX_LEN;
hids_outp_rep->id = OUTPUT_REP_KEYS_REF_ID;
hids_outp_rep->handler = hids_outp_rep_handler;
hids_init_obj.outp_rep_group_init.cnt++;
//add mouse button inp_rep and movement inp_rep <begin.>
hids_inp_rep++;
hids_inp_rep->size = INPUT_REP_BUTTONS_LEN;
hids_inp_rep->id = INPUT_REP_REF_BUTTONS_ID;
hids_init_obj.inp_rep_group_init.cnt++;
hids_inp_rep++;
hids_inp_rep->size = INPUT_REP_MOVEMENT_LEN;
hids_inp_rep->id = INPUT_REP_REF_MOVEMENT_ID;
hids_inp_rep->rep_mask = mouse_movement_mask;
hids_init_obj.inp_rep_group_init.cnt++;
//add mouse button inp_rep and movement inp_rep <end.>
hids_init_obj.is_kb = true;
//hids_init_obj.is_mouse = true;
hids_init_obj.boot_kb_outp_rep_handler = hids_boot_kb_outp_rep_handler;
hids_init_obj.pm_evt_handler = hids_pm_evt_handler;
err = bt_hids_init(&hids_obj, &hids_init_obj);
初始化hid设备后,开启ble,启动广播,连接成功后如果有按键按下,则进入到相应的按键回调函数static void button_changed(uint32_t button_state, uint32_t has_changed)进行处理。本项目定义的发送字符串为按键2,模拟鼠标左键的是按键1.
#define KEY_LEFT_MASK DK_BTN1_MSK
#define KEY_TEXT_MASK DK_BTN2_MSK
按键2按下后,会调用button_text_changed()进一步处理。
if (has_changed & KEY_TEXT_MASK) {
button_text_changed((button_state & KEY_TEXT_MASK) != 0);
}
这里有一个特别注意的地方:想要发送键盘上的一个字符,需要先发送1,表示按下;然后再发送一次0,表示抬起。这样上位机才能准备解析成按下一个按键。
static void button_text_changed(bool down)
{
static const uint8_t *chr = hello_world_str;
if (down) {
hid_buttons_press(chr, 1);
} else {
hid_buttons_release(chr, 1);
if (++chr == (hello_world_str + sizeof(hello_world_str))) {
chr = hello_world_str;
}
}
}
之后经过一些列的函数调用,最终调用:bt_hids_inp_rep_send()来完成按键按下发送一个字符的操作。注意boot_mode参数表示hid设备的工作状态,本项目中上位机处于PROTOCOL模式,并非启动模式。
static int key_report_con_send(const struct keyboard_state *state,
bool boot_mode,
struct bt_conn *conn)
{
int err = 0;
uint8_t data[INPUT_REPORT_KEYS_MAX_LEN];
uint8_t *key_data;
const uint8_t *key_state;
size_t n;
data[0] = state->ctrl_keys_state;
data[1] = 0;
key_data = &data[2];
key_state = state->keys_state;
for (n = 0; n < KEY_PRESS_MAX; ++n) {
*key_data++ = *key_state++;
}
if (boot_mode) {
err = bt_hids_boot_kb_inp_rep_send(&hids_obj, conn, data,
sizeof(data), NULL);
} else {
err = bt_hids_inp_rep_send(&hids_obj, conn,
INPUT_REP_KEYS_IDX, data,
sizeof(data), NULL);
}
return err;
}
在模拟鼠标左键点击部分,首先还在按键回调函数中判断是否BUTTON1被按下了,如果被按下,那么通过k_msgq_put()往消息队列发一个消息。
//left btn: mouse button left button
if (buttons & KEY_LEFT_MASK) {
//pos.x_val -= MOVEMENT_SPEED;
mouse_button_byte |= KEY_LEFT_MASK;
printk("%s(): left button pressed. mouse_button_byte = %d\n", __func__, mouse_button_byte);
data_to_send = true;
}else if(~button_state & has_changed & KEY_LEFT_MASK){
mouse_button_byte &= ~KEY_LEFT_MASK;
printk("%s(): left button released mouse_button_byte = %d\n", __func__, mouse_button_byte);
data_to_send = true;
}
if (data_to_send) {
int err;
err = k_msgq_put(&hids_queue, &mouse_button_byte, K_NO_WAIT);
if (err) {
printk("No space in the queue for button pressed\n");
return;
}
if (k_msgq_num_used_get(&hids_queue) == 1) {
k_work_submit(&hids_work);
}
}
之后在mouse_button_handler()函数中会轮询消息队列有没有hids_queue新的待处理消息,如果有,继续调用mouse_button_send()。
static void mouse_button_handler(struct k_work *work)
{
//struct mouse_pos pos;
uint8_t mouse_button;
while (!k_msgq_get(&hids_queue, &mouse_button, K_NO_WAIT)) {
mouse_button_send(mouse_button);
}
}
要让上位机知道button1是鼠标左键而不是鼠标右键,就需要了解报告地图中关于按键输入报告中每个bit的定义。
这是从网络找到一个说明图,其中第一个字节的bit0表示左键,因此本项目中尝试buffer[0]写入,button_pressed_state这个变量。上文提到,按键也是需要发送两次,先发1,再发0。这两次调用在上文按键1回调处理中已经体现出来了。
static void mouse_button_send(uint8_t button_pressed_state)
{
for (size_t i = 0; i < CONFIG_BT_HIDS_MAX_CLIENT_COUNT; i++) {
if (!conn_mode[i].conn) {
continue;
}
if (conn_mode[i].in_boot_mode) {
bt_hids_boot_mouse_inp_rep_send(&hids_obj,
conn_mode[i].conn,
NULL,
(int8_t) 0,
(int8_t) 0,
NULL);
} else {
/* Encode report. */
BUILD_ASSERT(sizeof(buffer) == 3,
"The INPUT_REP_BUTTONS_LEN is 3 Byte. Don't know why");
buffer[0] = button_pressed_state;
buffer[1] = 0;
buffer[2] = 0;
bt_hids_inp_rep_send(&hids_obj, conn_mode[i].conn,
INPUT_REP_BUTTONS_INDEX,
buffer, sizeof(buffer), NULL);
printk("%s()->bt_hids_inp_rep_send() to send: %d\n", __func__, buffer[0]);
printk("\n");
}
}
}
五、功能展示及说明
首先电脑打开设置,进入蓝牙设置,找到Nordic设备,点击connect,之后开发板按下button1,完成连接。
- 鼠标左键测试
2. 键盘输入测试
3. CAPS状态灯测试
心得体会
这次的funpack活动我学习了蓝牙BLE基础知识,了解了蓝牙广播,蓝牙配对,蓝牙profile,Service和Characteristic。最开心的是实现了简易蓝牙键鼠复合设备。掌握了蓝牙BLE HID的基本知识,拓宽了自己的视野。最后祝funpack越办越好!