Funpack2-6 nRF7002DK 蓝牙键盘鼠标复合设备的实现
Funpack2-6 nRF7002DK 蓝牙键盘鼠标复合设备的实现
标签
蓝牙
Funpack2-6
nRF5340
Nordic
发呆二极管
更新2023-10-13
677

大家好,我是Choco。

很高兴参加这次电子森林和digikey合作的Funpack活动。

本次活动是使用nRF7002DK开发板完成任务。

作为一个程序员,键盘是必不可少的。而我也很喜欢折腾键盘鼠标这些外设。

经朋友推荐,这次的活动因为有个任务我很有兴趣,所以就来参加Funpack活动了!

 

一、任务介绍

这次我做的是任务1:

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

因为这次就是对这个键鼠的任务感兴趣,所以就选了任务1。

以前参加过电子森林组织的silicon labs的efr32bg22的活动,所以这次蓝牙+键盘的活动,本以为会是轻车熟路,结果发现依旧困难重重~

总的来说,思路还是有的,先搭建环境、然后跑跑例程,看看例程里有没有可以借鉴和参考的。

 

二、项目实现流程图

Aeee5CvoQrB6AAAAAElFTkSuQmCC

在参考和研究了官方的例程后,设计了这次整个项目的功能流程。

首先是开发板通电后,先做初始化。初始化分为3个部分:基础硬件初始化、蓝牙初始化、HID初始化。

其中硬件初始化,主要是设置GPIO的中断,毕竟需要检测按键。

蓝牙的初始化,是因为这次是需要蓝牙连接。

HID的初始化,主要是配置IN和OUT端点。IN端点配置键盘和鼠标的HID描述符,并向电脑发送键盘和鼠标指令。而OUT端点主要是负责接收电脑发来的CAPS LED的信息,然后控制板载LED的亮灭。

在蓝牙连接成功后,开始循环检测状态机。GPIO的中断负责修改状态机的标志位。此时只要在循环检测中检测到标志位的状态,就可以执行相应的操作了。

三、项目实现具体介绍过程和代码片段

在拿到开发板后,首先我进行环境的搭建。

不得不说现在厂商提供的环境搭建的方法已经非常简单了。

首先在https://www.nordicsemi.com/Products/Development-tools/nRF-Connect-for-Desktop页面,下载nRF Connect for Desktop并安装

01

然后我只需要安装Bluetooth Low Energy和Toolchain Manager两个App即可。

 

打开Toolchain Manager后,我只安装了nRF Connect SDK v2.4.2。

A+jyWTSaDTubs4i8v8o7hK6FmX0nQAAAABJRU5ErkJggg==

nRF Connect SDK以下简称ncs。

其实这次的任务,用zephyr和ncs都可以。只是这里我选择了ncs。

整个环境搭建的过程中,下载ncs是最慢的。一是ncs比较大,二是下载ncs可能要科学上网才能快一些。

在下载、安装好ncs后,打开vs code。此时还需要再安装点其他的工具链。按照提示一步一步来就可以了。

 

在一切都安装好后,就可以进行开发了。

在Toolchain Manager中,还有个First steps的按钮。这就是告诉你如何使用vs code进行开发的流程。

 

这是我第一次使用vs code开发嵌入式相关的经历。

但是Nordic的ncs环境搭建居然如此的简单。让我非常的惊讶。

 

Ncs里有很多例子。

在https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/nrf/samples.html里有例子的说明。

因为这次我要做的是蓝牙键盘鼠标复合设备。

wDsuXpFaYBKPQAAAABJRU5ErkJggg==

所以这次我主要只看Bluetooth: Peripheral HIDS mouse和Bluetooth: Peripheral HIDS keyboard的例子就可以了。

用vs code先按照First steps的步骤复制一个Peripheral HIDS keyboard例程。

编译、烧录。先把例子跑起来熟悉一下操作流程。

在编译的时候遇到一个错误。

IMG_256

解决办法是关闭vs code,打开cmd输入code --no-sandbox,然后重开vs code即可解决。

在成功把程序烧录到开发板后。我便开始研究如何修改例程了。

 

首先,测试了一下keyboard例程。

不出意外,蓝牙搜不到任何设备……

然后在keyboard例程的说明页里,看到例程默认开启了NFC_OOB_PAIRING,需要先进行NFC的配对,如果NFC没配对则蓝牙不会工作。

AbWWzofUAwO8AAAAAElFTkSuQmCC

此时只要在项目的Kconfig文件里把NFC_OOB_PAIRING 的default改为n即可。

 

继续看例程的说明。

Keyboard例程有4个按键3个LED灯。

但是这些按键和LED灯可能是nRF5340的开发板才有的。而这次活动使用的nRF7002只有2个按键2个LED灯。

button1是输出字符hello\n,button2按住Shift键。

LED1是闪烁,LED2是蓝牙配对指示灯,LED3是大小写指示灯。

看到这里,接下来要做什么就很清楚了。

LED1的闪烁是我不需要的,而这个板子上又没有LED3。所以我把LED3的大小写指示灯改成LED1即可。

button1是输出字符,我先把原本的hello\n的字符串改成了eetree\n。

但是改完后,按一下button1他才出一个字符。

所以我又自己写了个简单的小状态机。让他按照一定的流程顺序输出字符。

static int KeyboardStringIndex = 0;
static int StateMachine_Keyboard = 0;
KeyboardStringIndex是eetree字符串的顺序索引。
StateMachine_Keyboard是一个小状态机,表示当前是否持续输出字符。
我修改了button_text_changed函数
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;
			StateMachine_Keyboard=0;
		}
	}
	KeyboardStringIndex++;
}

简单解释一下,先是把eetree的字符串的数组指针给chr,当按键按下的时候,if (down)是true,调用hid_buttons_press的函数,发送chr字符。

当按键抬起的时候,进入else分支,此时先调用hid_buttons_release发送消息给电脑,释放刚才的按下的按钮。

此时++chr,也就是字符指针向后移动一个。这里之所以用++chr而不是chr++,是因为这里要先自增,并且用自增后的结果去做对比。如果chr当前的指针位置与hello_world_str的最后一个字符的指针位置相同,说明整个eetree的字符串已经输出完了。此时StateMachine_Keyboard置0,停止字符输出。

就这样,任务1已经完成一半了。

 

就在我觉得任务1挺简单的时候。才发现真正困难在后面。

由于要做复合设备,所以要修改键盘的HID描述符。

HID这个东西因为接触太少了。啃起来非常头疼……

看了半天Mouse的例程,也有好多地方没看明白的。在这里卡了好几天了……

先是改完HID描述符后,蓝牙刚连上就报错。

而且一直找不到原因。

对比了半天代码,才发现可能是因为某些配置没有写好。

例如在HID描述符修改完后,后面的配置代码要这样写:

BT_HIDS_DEF(hids_obj,
        INPUT_REPORT_KEYS_MAX_LEN,
        INPUT_REPORT_MOUSE_MAX_LEN,
        OUTPUT_REPORT_MAX_LEN
);

在原来的键盘HID描述符后还要增加鼠标的HID描述符

		/* Mouse */
		0x05, 0x01,       /* Usage Page (Generic Desktop) */
		0x09, 0x02,       /* Usage (Mouse) */
		0xA1, 0x01,       /* Collection (Application) */

		/* Report ID 1: Mouse buttons + scroll/pan */
		0x85, INPUT_REP_MOUSE_REF_ID,       /* Report Id 1 */
		0x09, 0x01,       /* Usage (Pointer) */
		0xA1, 0x00,       /* Collection (Physical) */
		0x95, 0x05,       /* 	Report Count (5) */
		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 */

		0x05, 0x01,       /* 	Usage Page (Generic Desktop) */
		0x09, 0x30,       /* 		Usage (Wheel) */
		0x09, 0x31,       /* 		Usage (Wheel) */
		0x09, 0x38,       /* 		Usage (Wheel) */
		0x15, 0x81,       /* 		Logical Minimum (-127) */
		0x25, 0x7F,       /* 		Logical Maximum (127) */
		0x75, 0x08,       /* 		Report Size (8) */
		0x95, 0x03,       /* 		Report Count (1) */
		0x81, 0x06,       /* 		Input (Data, Variable, Relative) */
		0xC0,             /* 	End Collection (Physical) */
		0xC0              /* End Collection (Physical) */

		// /* Report ID 2: Mouse motion */
		// 0x85, 0x02,       /* Report Id 2 */
		// 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) */

最后还需要修改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[0];
	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_inp_rep++;
	// hids_inp_rep->size = INPUT_REPORT_mouse_move_MAX_LEN;
	// hids_inp_rep->id = INPUT_REP_Mouse_Move_ID;
	// hids_inp_rep->rep_mask = mouse_movement_mask;
	// hids_init_obj.inp_rep_group_init.cnt++;

	hids_inp_rep++;
	hids_inp_rep->size = INPUT_REPORT_MOUSE_MAX_LEN;
	hids_inp_rep->id = INPUT_REP_MOUSE_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++;

	hids_init_obj.is_mouse = true;
	hids_init_obj.is_kb = 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;

	printk("bt_hids_init start\n");
	err = bt_hids_init(&hids_obj, &hids_init_obj);
	printk("bt_hids_init end\n");
	__ASSERT(err == 0, "HIDS initialization failed\n");

其实最开始我是想移植完整的鼠标例程里的HID描述符和HID配置代码的。

但是发现那样会报错,没办法,最后只能屏蔽掉一部分的HID描述符和HID的配置代码才通过的。

之前就是因为这些没有处理好,导致蓝牙连上就报错。

在折腾和研究了很长时间,改好HID的初始化代码后,蓝牙终于可以正常配对不报错了!

 

但是新问题又来了。

连上蓝牙后,只要一发送字符串,就会报错。

然后发现是我鼠标的HID描述符和发送的字符串内容不匹配。

修改HID发送的代码如下:

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;

	uint8_t mouse_data[4];

	if (StateMachine_Mouse > 0)
	{
		if (StateMachine_Mouse==1)
		{
			mouse_data[0] = 0x01;
		}
		if (StateMachine_Mouse==2)
		{
			mouse_data[0] = 0x00;
			StateMachine_Mouse=0;
		}
		mouse_data[1] = 0;
		mouse_data[2] = 0;
		mouse_data[3] = 0;

		err = bt_hids_inp_rep_send(&hids_obj, conn,
				1,
				mouse_data, sizeof(mouse_data), NULL);
	}
	else
	{
		data[0] = 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;
}

简单解释一下,通过StateMachine_Mouse来判断,是否是鼠标按键被按下了。

static int StateMachine_Mouse = 0;

StateMachine_Mouse是我创建的一个鼠标用的小状态机,

StateMachine_Mouse为0则是鼠标没按下;

StateMachine_Mouse为1则是鼠标左键按下;

StateMachine_Mouse为2则是鼠标左键松开;

mouse_data的长度一共是4字节,根据前面的描述符

Fqy5VeJyjhvAsLhBZOYA_XNBs0tc

可以看到,这4个字节的含义分别是:

第一个字节包含鼠标按键的值。bit1代表左键、bit2代表右键、bit3代表中健。

第二个字节代表鼠标X轴移动的偏移量

第三个字节代表鼠标Y轴移动的偏移量

第四个字节代表鼠标滚轮的滚动量。

因为任务只需要实现左键的功能,所以通过判断StateMachine_Mouse的值,给mouse_data[0]填充数据1或0即可。

 

在被HID折磨了好几天后,终于把鼠标左键点击的功能做好了。

wADmYjVoLHP4gAAAABJRU5ErkJggg==

任务完成!

 

四、总结和心得

经过这次学习。确实收获蛮多的。

通过这次的项目,学习了蓝牙的基础知识,为了做键盘鼠标复合设备,也找了很多hid的资料看。不得不说hid的协议啃起来真的很让人头大!

其实这次活动,遇到最让人头疼的问题,并不是代码上的问题。反而是项目编译上的问题。因为做这个项目,没有去用git或者svn,每做完一个小功能我都会复制一份代码用来备份。但是就因为这样所以出了问题。虽然我复制了新的文件,但是项目路径还是旧的,导致做这个项目的时候有3天晚上我都修改了代码、烧录、编译后,发现修改没效果……白忙活了好几天!

这次遇到的困难真是挺大的。还好在最后一天顺利完成了。

遇到困难的时候,真的是非常痛苦。但是回过头看看,这都是知识不够导致的。学无止境,还需要继续学习才行!

 

再次感谢电子森林举办的这次活动。

希望以后能有更多人在这里受益!

附件下载
Funpack_Keyboard源码.rar
源码,因上传尺寸删掉了build内的内容,需要自己新建build
团队介绍
个人
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号