大家好,我是Choco。
很高兴参加这次电子森林和digikey合作的Funpack活动。
本次活动是使用nRF7002DK开发板完成任务。
作为一个程序员,键盘是必不可少的。而我也很喜欢折腾键盘鼠标这些外设。
经朋友推荐,这次的活动因为有个任务我很有兴趣,所以就来参加Funpack活动了!
一、任务介绍
这次我做的是任务1:
使用板卡的蓝牙连接,设计一个蓝牙鼠标+键盘复合设备,按键1作为鼠标点击,按键2作为键盘输入按下时输入“eetree”字符,电脑开启大写锁定时,板卡的LED亮起。
因为这次就是对这个键鼠的任务感兴趣,所以就选了任务1。
以前参加过电子森林组织的silicon labs的efr32bg22的活动,所以这次蓝牙+键盘的活动,本以为会是轻车熟路,结果发现依旧困难重重~
总的来说,思路还是有的,先搭建环境、然后跑跑例程,看看例程里有没有可以借鉴和参考的。
二、项目实现流程图
在参考和研究了官方的例程后,设计了这次整个项目的功能流程。
首先是开发板通电后,先做初始化。初始化分为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并安装
然后我只需要安装Bluetooth Low Energy和Toolchain Manager两个App即可。
打开Toolchain Manager后,我只安装了nRF Connect SDK v2.4.2。
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里有例子的说明。
因为这次我要做的是蓝牙键盘鼠标复合设备。
所以这次我主要只看Bluetooth: Peripheral HIDS mouse和Bluetooth: Peripheral HIDS keyboard的例子就可以了。
用vs code先按照First steps的步骤复制一个Peripheral HIDS keyboard例程。
编译、烧录。先把例子跑起来熟悉一下操作流程。
在编译的时候遇到一个错误。
解决办法是关闭vs code,打开cmd输入code --no-sandbox,然后重开vs code即可解决。
在成功把程序烧录到开发板后。我便开始研究如何修改例程了。
首先,测试了一下keyboard例程。
不出意外,蓝牙搜不到任何设备……
然后在keyboard例程的说明页里,看到例程默认开启了NFC_OOB_PAIRING,需要先进行NFC的配对,如果NFC没配对则蓝牙不会工作。
此时只要在项目的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;
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字节,根据前面的描述符
可以看到,这4个字节的含义分别是:
第一个字节包含鼠标按键的值。bit1代表左键、bit2代表右键、bit3代表中健。
第二个字节代表鼠标X轴移动的偏移量
第三个字节代表鼠标Y轴移动的偏移量
第四个字节代表鼠标滚轮的滚动量。
因为任务只需要实现左键的功能,所以通过判断StateMachine_Mouse的值,给mouse_data[0]填充数据1或0即可。
在被HID折磨了好几天后,终于把鼠标左键点击的功能做好了。
任务完成!
四、总结和心得
经过这次学习。确实收获蛮多的。
通过这次的项目,学习了蓝牙的基础知识,为了做键盘鼠标复合设备,也找了很多hid的资料看。不得不说hid的协议啃起来真的很让人头大!
其实这次活动,遇到最让人头疼的问题,并不是代码上的问题。反而是项目编译上的问题。因为做这个项目,没有去用git或者svn,每做完一个小功能我都会复制一份代码用来备份。但是就因为这样所以出了问题。虽然我复制了新的文件,但是项目路径还是旧的,导致做这个项目的时候有3天晚上我都修改了代码、烧录、编译后,发现修改没效果……白忙活了好几天!
这次遇到的困难真是挺大的。还好在最后一天顺利完成了。
遇到困难的时候,真的是非常痛苦。但是回过头看看,这都是知识不够导致的。学无止境,还需要继续学习才行!
再次感谢电子森林举办的这次活动。
希望以后能有更多人在这里受益!