一、项目描述
项目介绍
本次项目任务,是完成Funpack第二季第六期指定完成任务。
我选择的是任务一:
- 使用板卡的蓝牙连接,设计一个蓝牙鼠标+键盘复合设备,按键1作为鼠标点击,按键2作为键盘输入按下时输入“eetree”字符,电脑开启大写锁定时,板卡的LED亮起。
简单的硬件介绍
这次的板子是来自Nordic Semi的nRF7002-DK开发板,由于我一直对键盘比较感兴趣。而很多市售的键盘鼠标所使用的主控芯片都是用的Nordic家的蓝牙芯片,可以说Nordic的蓝牙芯片在这个圈子里是老大哥的地位。
我们可以看到,此次Funpack的nRF7002-DK板卡上面有三颗主要芯片,型号分别是nRF5340,nRF5340和nRF7002。第一颗nRF5340充当的是专用Interface MCU的角色,也就是我们大家熟知的Jlink OB的功能,配合Nordic自己的软件nRF Connect或者Segger公司的J-Link软件,就能对nRF5340芯片进行烧写/擦除等工作。
设计和开发思路
这次的板子其实是一个nRF7002的开发板,7002是一颗WiFi芯片。但是这颗芯片是一个辅芯片,他需要配合主芯片来工作。而这个板子上还有一个nRF5340的芯片,这颗芯片才是我们这次项目任务的主角。nRF5340是现在Nordic在售的芯片里的旗舰:双核、高主频,带有USB和蓝牙,非常适合做键鼠外设。
对于这个任务,我的设计思路是:官方有提供一个蓝牙键盘例程和蓝牙鼠标例程,我在看了代码之后,决定把这两个例程合并起来。
我是以蓝牙键盘例程为基础,把蓝牙鼠标例程的HID描述符迁移过来,放在蓝牙键盘HID例程的后面,然后在按下板卡上面的按键 1 的时候,输出鼠标左键的按下动作,然后输出鼠标左键的抬起动作。在蓝牙鼠标例程中,是没有鼠标按下的代码的,这个是我修改了HID之后,自己添加上的。
然后我修改了键盘的字符串,并设计了一个小状态机,让他按照顺序输出eetree的字符。最后,由于蓝牙键盘例程自带LED大写指示灯的切换,但是默认是板卡上面的LED3是大写指示灯,我把LED改成了LED1。
二、软件流程图及各功能对应的主要代码片段及说明
这个程序的整体工作流程如上图所示
首先程序上电开始初始化,会对蓝牙服务进行注册,然后再做HID的初始化,等待蓝牙配对后,再检测按键的输入。
在HID初始化的时候,会配置输入端点和输出端点。输入端点并不是指对MCU的输入,而是面向电脑而言的输入,也就是说输入端点是MCU向电脑发送消息的端点。
而配置HID,最重要的部分就是HID描述符,也可以说是这个项目最最核心的部分。我是这样修改的
static const uint8_t report_map[] = {
0x05, 0x01, /* Usage Page (Generic Desktop) */
0x09, 0x06, /* Usage (Keyboard) */
0xA1, 0x01, /* Collection (Application) */
/* Keys */
0x85, INPUT_REP_KEYS_REF_ID,
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 */
0x85, OUTPUT_REP_KEYS_REF_ID,
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) */
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_Button_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) */
};
因为是复合设备,所以HID描述符既要有键盘的描述符,又要有鼠标的描述符。
我是基于键盘的例程修改的,所以这里键盘描述符的内容不动,我把鼠标历程的描述符直接拿过来了,就粘贴在键盘描述符的后面。
但是还是要稍作修改的。因为鼠标描述符的功能很多,他包含了3个部分。第一部分是鼠标左键+滚轮;第二部分是鼠标移动;第三部分是多媒体按键。
由于这次的活动的任务1只需要实现鼠标左键的功能就可以了。所以我就只移植了鼠标左键的部分,并稍作修改。后面两部分的HID描述符我就丢掉不要了。
可以看到,HID描述符的结构也是合集性质的。
他有好多0xA1 0x01的Collection作为开始标志、以0xC0作为结束标志的合集。
合集中间包含了详细的描述符。这些描述符详细的说了作用、每个消息的长度、消息的数量、数据的有效范围等等。
就拿鼠标的例子来说(并非完全按照描述符的顺序)
0x09, 0x01, /* Usage (Pointer) */这是描述这个描述符使用的是鼠标指针
0xA1, 0x00, /* Collection (Physical) */这个是合集的开始标志
由于这个是合集了,所以这里的注释用了一个tab占位来说明以下内容是包含在合集中,是层级关系
0x75, 0x01, /* Report Size (1) */这个是说一个字节有8个位,而现在的消息包是一个消息或者说信号使用了1个位
0x95, 0x05, /* Report Count (5) */这是说上面的Report Size需要重复几次,也就是有几个“带有功能”的位,这里数量是5
到这里为止,我们就把消息包的前5个字节的占用都描述清楚了。
接下来的
0x19, 0x01, /* Usage Minimum (01) */
0x29, 0x05, /* Usage Maximum (05) */
0x15, 0x00, /* Logical Minimum (0) */
0x25, 0x01, /* Logical Maximum (1) */
这4个只是描述了刚才的消息的逻辑最大值和最小值。
前面的这5个位信号的定义,是以
0x81, 0x02, /* Input (Data, Variable, Absolute) */
为结束标志的。
再接下来
0x95, 0x01, /* Report Count (1) */
0x75, 0x03, /* Report Size (3) */
0x81, 0x01, /* Input (Constant) for padding */
同理,这里是描述了3个位的功能。但是这里因为没有说明usage和logical的最大最小值,直接用了input来结束这3个位功能的描述,是仅仅用来填充数据用的。
到这里为止,我们就写好了一个字节的功能描述了。
其他的描述符原理相近,这里就不过多解释了。
写完了HID描述符,接下来我们要修改配置代码,让HID描述符生效了。
因为只是修改了HID描述符还不够,还需要做端点的配置,让MCU可以正确的把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_button_MAX_LEN;
hids_inp_rep->id = INPUT_REP_Mouse_Button_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描述符的部分是最麻烦的,剩下的都很简单了。
static const uint8_t hello_world_str[] = {
0x08, /* Key e */
0x08, /* Key e */
0x17, /* Key t */
0x15, /* Key r */
0x08, /* Key e */
0x08, /* Key e */
0x28, /* Key Return */
};
首先是修改了hello_world_str,这个部分的代码并不是直接输入ASCII的值,所以我自己做了个简单的对照表,把对应的数字符号填上即可。
但是由于原来键盘例程的功能是按一下输出一个字符,所以还要对其稍作修改。
接下来我自己写了2个状态,分别作为键盘和鼠标状态的记录标志位
static int kbstrstatus = 0;
static int mousestatus = 0;
然后修改main函数中的for循环代码
先把ADV_STATUS_LED相关的LED灯闪烁代码注释掉。
因为在这个for循环中,正好有个k_sleep(K_MSEC(ADV_LED_BLINK_INTERVAL))的函数控制延时,所以在这里加入了button_text_changed的函数调用,就可以实现按照1秒1个字符的频率输出eetree的字符了。
for (;;) {
// if (is_adv) {
// dk_set_led(ADV_STATUS_LED, (++blink_status) % 2);
// } else {
// dk_set_led_off(ADV_STATUS_LED);
// }
if (kbstrstatus==1)
{
printk("keyboard index %d press %d status %d\n", kbstrindex, kbstrindex%2, kbstrstatus);
button_text_changed(kbstrindex%2);
printk("keyboard index %d press %d status %d\n", kbstrindex, kbstrindex%2, kbstrstatus);
button_text_changed(kbstrindex%2);
}
printk("keyboard 3 2151\n");
k_sleep(K_MSEC(ADV_LED_BLINK_INTERVAL));
/* Battery level simulation */
bas_notify();
}
而鼠标按键的修改,就是直接把原本btn2的按键检测并输出shift键的功能,改成了输出鼠标左键点击就行了。
至此,所有功能做完。
三、功能展示及说明
第一步:连接蓝牙,然后按下按键1,确认已经和电脑蓝牙配对;
第二步:打开记事本,用键盘随便输入几个字符,通过按键1的双击可以看到文字被选中了。
切换一下位置,再按一下按键1,效果是一样的;
第三步:按下按键2,会在记事本中输出“eetree”的字样;
第四步:按下电脑键盘的CapsLock键,板卡上面的LED1灯会有相应的动作。
四、对本活动的心得体会
作为一个非电子行业的工程师,参加这种活动对我个人的挑战性不算小。当然,这个项目的三个子任务对参加活动的别的小伙伴来说很可能是轻车熟路的,我的代码不少部分是请教和借鉴的,班门弄斧,让各位见笑了。因为之前的工作经历的缘故,我对Nordic这家公司还算有一些了解,它们家的资料非常的Open和完备,不用注册复杂的账号和NDA就可以获得,这使得我对它们家的印象非常好,在一些DIY项目中,我也非常乐于使用它的芯片和模组。
该项目已经成功实现了简易的蓝牙鼠标+键盘复合设备功能,并达到了预期指标。然而通过更加充分的利用硬件和软件,还有许多可以提升与扩展的地方:
1、充分使用芯片上面的USB接口,做成有线/无线双模设备;
2、利用Nordic SDK中的ESB/Gzell协议来支持2.4G私有无线。
这些功能,我希望我自己能在后续的学习过程中逐渐的来实现,进而达成自身的进步。