Funpack2-6:基于nRF7002实现的简易蓝牙键鼠复合设备
本项目使用Nordic新一代集成了Wi-Fi和BLE的板卡,使用蓝牙连接技术,设计了一个蓝牙鼠标+键盘复合设备,按键1作为鼠标点击,按键2作为键盘输入按下时输入“eetree”字符,电脑开启大写锁定时,板卡的LED亮起
标签
BLE
鼠标
HID
键盘
Funpack2-6
nRF7002
复合设备
lixiang
更新2023-10-09
605

一、项目目标:

任务一:

• 使用板卡的蓝牙连接,设计一个蓝牙鼠标+键盘复合设备,按键1作为鼠标点击,按键2作为键盘输入按下时输入“eetree”字符,电脑开启大写锁定时,板卡的LED亮起

二、项目背景

本项目预期实现的简易蓝牙键鼠复合设备需要用到nRF7002板卡上的nRF5340芯片,这颗芯片支持低功耗蓝牙、蓝牙Mesh、NFC、Thread和Zigbee的双核蓝牙5.2 SoC。这颗芯片在板子上的位置如下所示。

FjBJh5I-Ch4LvVHTqc_yp0SaED1v

nRF5340是一款全合一(all-in-one) SoC器件,嵌入了一个 128MHz Arm Cortex-M33 应用处理器和一个 64MHz 高效率网络处理器,是全球首款拥有两个Arm® Cortex®-M33处理器的无线连接SoC。两个灵活的处理器、先进的功能以及最高105°C的工作温度,使其成为低功耗音频、专业照明、高级可穿戴设备和其他复杂物联网应用的理想选择。

nRF5340 系统级芯片支持各种无线协议。它支持低功耗蓝牙,并且蓝牙测向可实现所有到达角(AoA)和出发角(AoD)的测量功能。此外,它支持低功耗蓝牙音频,2 Mbps高吞吐量、广播扩展和长距离。像蓝牙MeshThreadZigbee这样的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来提供支持。

Fh2jdvxZD4ELq5D6Gq_x4aHx4b6a

四、项目详细实现描述

本项目中按键的检测,LED灯的亮灭都离不开GPIO模块,在main()函数中通过调用config_gpio()函数来对相关的按键事件回调函数进行注册。这样按键按下后就可以进入button_changed()函数做相应的处理。

FgTKol3BCyQKz4cPZofJGUruXTB4

接下来就是比较重要的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的定义。

Fq2z8lbkZKFQPG1zVdWmx-EFsVUB

这是从网络找到一个说明图,其中第一个字节的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,完成连接。

FurzQBFXuuV2_pFKanwWWl-N-vuA

  1. 鼠标左键测试

FqnpVuzzHPox4_v9jW7hCCLXq6Sa

 

FitGD4y6Jryi_4LtqVVfNaJSS1Qp

2. 键盘输入测试

FpUTFt04bfyxXoWG59IPGSNRDbRD

3. CAPS状态灯测试

FvAmPDNMi_fsENW1-nNzdmb9WDXh

心得体会

这次的funpack活动我学习了蓝牙BLE基础知识,了解了蓝牙广播,蓝牙配对,蓝牙profile,Service和Characteristic。最开心的是实现了简易蓝牙键鼠复合设备。掌握了蓝牙BLE HID的基本知识,拓宽了自己的视野。最后祝funpack越办越好!

附件下载
peripheral_hids_keyboard-eetree.zip
团队介绍
电子工程师小菜鸟
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号