基于nRF7002 DK的蓝牙键鼠复合设备
nordic的NRF53xx系列芯片在低功耗蓝牙方面有着众多应用,本项目正是使用NRF5340的蓝牙做一个蓝牙键鼠复合设备
标签
嵌入式系统
Funpack活动
starry-m
更新2023-10-10
249

Funpack2-6:基于nRF7002 DK的蓝牙键鼠复合设备

一、项目要求

使用板卡的蓝牙连接,虚拟成一个键鼠复合设备

1、按下按键1作为鼠标点击

2、按下按键2作为键盘输入"eetree"字符

3、电脑开启大写锁定时,板卡LED亮起

二、设计思路

nRF7002-DK是nordic推出的用于开发蓝牙和Wi-Fi 6的评估开发套件,其中的主处理器:nRF5340——支持低功耗蓝牙、蓝牙Mesh、NFC、Thread和Zigbee的双核蓝牙5.2 SoC,从处理器:nRF7002 ——低功耗Wi-Fi 6协同IC提供无缝连接和基于Wi-Fi的定位功能。一张板卡完成多种场景功能的开发。

Nordic官方提供的NCS开发包中有丰富的例程,其中就有单独的蓝牙键盘和蓝牙鼠标例程,最简单的方法是在其中一个例程的HID描述符中增加另一功能,让电脑或手机蓝牙将板卡识别为复合设备。然后根据HID的鼠标和键盘标准发送相应信息。

而进一步的深入研究,可以去查看蓝牙协议的标准文档(在这方面的经验还为0,只能加入计划表了)。

以下为软件流程图

FnLx9meK7mxLxiPDLK_uS7NNKtW3

1、板卡结构

FkZAiLAW412TVHWQJbcSb8mdoKzb

从上面的结构框图我们可以看到,是板载调试器的。并且板子上的GPIO引脚、按键、灯、外扩存储等都是接在5340上的,7002只作为一个跑WIFI任务的从机。当然,本项目的无线部分只用到了蓝牙,不需要管7002的部分。

2、软件环境搭建

在对这个板卡有了大致的了解后,我们就可以去看看官方的开发环境和SDK是怎样的了。官方的环境部署教程。除此之外,还有一篇博客详细介绍了环境搭建和使用方法,以及在linux下通过doker快速使用的方法。 在此感谢这些大佬们的分享。 我最终选择的开发方式是在win下使用nRF Connect for Desktop配合vs code下的nrf插件进行开发。

3、例程学习与使用

俗话说,站在巨人的肩膀上,看的更宽,走得更远。所以在学习新的芯片的软件开发时,最快的学习方法是,先把官方提供的例程跑起来,再把这些例程进行组合实现我们需要的功能。后续如果需要对一些很细致的地方处理,再去细看芯片的参考手册这些底层资料。 从我们的项目要求出发,我们只需要把例程中的蓝牙键盘,蓝牙鼠标都跑通,然后再组合起来就可以了。

蓝牙键盘例程介绍  

FmSuZ4tWJV1JaYd8N1JrhGX5-A9i

蓝牙鼠标例程介绍 

FqRf6qngRJ9kGAOorsQVqK_LC9X1

三、实现过程

虽说官方已经提供了蓝牙键盘和鼠标的例程,但要在我们的板卡上跑起来也是需要进行一定的修改与配置。而且也需要熟悉一下工程的软件框架。

1、从蓝牙键盘例程开始

在该例程的readme中有说明

When the application starts, it initializes and starts the NFCT peripheral that is used for pairing.
The application does not start advertising immediately, but only when the NFC tag is read by an NFC polling device, for example a smartphone or a tablet with NFC support.
To trigger advertising without NFC, press **Button 4**.
The NDEF message that the tag sends to the NFC device contains data required to initiate pairing.
To start the NFC data transfer, the NFC device must touch the NFC antenna that is connected to the nRF52 device.

使用该例程建立蓝牙连接需要先使用智能设备的nfc识别后才建立连接。但我们想要自由的建立连接,所以需要禁用该功能。 在prj.conf中加入CONFIG_NFC_OOB_PAIRING=n 但还会出现如下错误 。

FgwGogaK6svfroLUdqsHJhPDPq-H

经群中大佬帮忙,最后问题定位是pool太小。于是在prj.conf中加入CONFIG_HEAP_MEM_POOL_SIZE=8192 CONFIG_MAIN_STACK_SIZE=4096 然后编译通过,并能够实现功能

Button 1:
   Sends one character of the predefined input ("hello\\n") to the computer.
   When pairing, press this button to confirm the passkey value that is printed on the COM listener to pair with the other device.

Button 2:
   Simulates the Shift key.
   When pairing, press this button to reject the passkey value which is printed on the COM listener to prevent pairing with the other device.

LED 1:
   Blinks with a period of two seconds with the duty cycle set to 50% when the main loop is running and the device is advertising.

LED 2:
   Lit when at least one device is connected.

因为例程中带了LED:Indicates if Caps Lock is enabled.则只需要修改该功能的LED引脚为板卡上的LED即可。字符输入也只需要进行简单修改即可,例程中是每按下一次按键输入一个字符,循环输入。但我对项目要求的理解是应当按下一次按键输入所有字符。 故改成如下,完成一次性输入所有字符。

static const uint8_t eetree_str[] = {
    0x08,   /* Key e */
    0x08,   /* Key e */
    0x17,   /* Key t */
    0x15,   /* Key r */
    0x08,   /* Key e */
    0x08,   /* Key e */
};
uint8_t  key_send_flag=0;
//循环改变数组指针位置
static void button_text_changed(bool down)
{
    static const uint8_t *chr = eetree_str;
    if(down){
        // err = k_msgq_put(&hids_queue, &pos, K_NO_WAIT);
        hid_buttons_press(chr,1);
    }
    else
    {
        hid_buttons_release(chr, 1);
        if(key_send_flag==0)
            key_send_flag=1;
    }
}

//main函数中增加如下
uint8_t *chnr = eetree_str;
while(1)
{
    switch (key_send_flag)
    {
    case 1:
    // hid_buttons_press(chnr,1);
    /* code */
    break;
    case 2:
    hid_buttons_press(chnr+1,1);
    /* code */
    break;
    case 3:
    hid_buttons_press(chnr+2,1);
    /* code */
    break;
    case 4:
    hid_buttons_press(chnr+3,1);
    /* code */
    break;
    case 5:
    hid_buttons_press(chnr+4,1);
    /* code */
    break;
    case 6:
    hid_buttons_press(chnr+5,1);
    /* code */
    break;
    default:
        break;
    }   
    // k_sleep(K_MSEC(ADV_LED_BLINK_INTERVAL));
    k_sleep(K_MSEC(20));
    time_tick++;
    switch (key_send_flag)
    {
    case 1:
    // hid_buttons_release(chnr,1);
    key_send_flag=2;
    /* code */
    break;
    case 2:
    hid_buttons_release(chnr+1,1);
    key_send_flag=3;
    /* code */
    break;
    case 3:
    hid_buttons_release(chnr+2,1);
    key_send_flag=4;
    /* code */
    break;
    case 4:
    hid_buttons_release(chnr+3,1);
    /* code */
    key_send_flag=5;
    break;
    case 5:
    hid_buttons_release(chnr+4,1);
    key_send_flag=6;
    /* code */
    break;
    case 6:
    hid_buttons_release(chnr+5,1);
    key_send_flag=0;
    /* code */
    break;
    default:
        break;
    }

}

2、在蓝牙键盘例程上增加鼠标功能

在此前,因为例程蓝牙键盘的所有功能都有,我们只是进行了简单的修改,但现在需要在原来的基础上增加鼠标,需要我们自己修改HID Reports。 我的HID初始化如下

static void hid_init(void)
{
    int err;
    struct bt_hids_init_param    hids_init_obj = { 0 };
    struct bt_hids_inp_rep       *hids_inp_rep;
    struct bt_hids_outp_feat_rep *hids_outp_rep;
    static const uint8_t mouse_movement_mask[DIV_ROUND_UP(3, 8)] = {0};

    static const uint8_t report_map[] = {
        //键盘部分描述符
        0x05, 0x01,       /* Usage Page (Generic Desktop) */
        0x09, 0x06,       /* Usage (Keyboard) */
        0xA1, 0x01,       /* Collection (Application) main class*/

        /* 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) */
        //一共8个byte
        /*CAPS LOCK 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) */

        //鼠标部分描述符
        0x05, 0x01,     /* Usage Page (Generic Desktop) */
        0x09, 0x02,     /* Usage (Mouse) */
        0xA1, 0x01,     /* Collection (Application) */

        /* Report ID 1: Mouse buttons + scroll/pan */
        0x85, 0x02,       /* 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)  global classs*/
        0x09, 0x30,       /* Usage (X)  local class*/
        0x09, 0x31,       /* Usage (Y) */
        0x09, 0x38,       /* Usage (Wheel) */
        0x15, 0x81,       /* Logical Minimum (-127) */
        0x25, 0x7F,       /* Logical Maximum (127) */
        0x75, 0x08,         /* Report Size (8) */
        0x95, 0x03,         /* Report Count (3) */
        0x81, 0x06,         /* Input (Data, Variable, Relative) */
        //一共4个byte
        0xC0,             /* End Collection (Physical) */
        0xC0,             /* End Collection (Application)  main class*/

    };

    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_inp_rep++;
    hids_inp_rep->size = 4;
    hids_inp_rep->id = 2;
    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_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);
    printk("input report cnt is %d\n",hids_obj.inp_rep_group.cnt);
    __ASSERT(err == 0, "HIDS initialization failed\n");
}

重点在于对HID描述符的理解,从USB_HID协议中文版的PDF中,主要看表 8-4 HID 项目列表 表8-6 Usage Page定义 表8-7 Generic Desktop Controls 用法定义就能大致的理解HID描述符的意思。如上每个描述符后面的注释可见,一般先使用PAGE,确定HID设备类型,然后定义各项功能参数,数据大小长度,再确定输入、输出。而复合设备只需要再后面再加上对应设别的描述即可。需要注意的是不同设备的报表ID不能相同,且输入输出的报表ID是分开记录的。后面的部分就是依葫芦画瓢,跟着写就行。 这样,在我们的电脑蓝牙连接板卡时,就会显示这是一个键鼠复合设备。 最后就是完善鼠标点击的发送函数

void mouseButtonClick(uint8_t button) {
    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 {
            uint8_t buffer[4];
            buffer[0] = 0;
            buffer[1] = 0;
            buffer[2] =0;
            buffer[3] = 0;
            if(button==1)
            {
                buffer[0] = 1;
            }
            else if(button==2)
            {
                buffer[0]=2;
            }
            else if(button==3)
            {
                buffer[0]=4;
            }
            // printk("buffer: %d %d %d\n",buffer[2],buffer[1],buffer[0]);
            //INPUT_REP_BUTTONS_INDEX    INPUT_REP_MOVEMENT_INDEX
            bt_hids_inp_rep_send(&hids_obj, conn_mode[i].conn,
                          1,
                          buffer, sizeof(buffer), NULL);
        }
    }
}

这里的形参button表示输入的是哪个按键,而至于发送的数据为何这样写可见下图 

Fun-m4ZB_g7B3OeFAda84yCheGPf

这样就完成了所有的项目要求。

四、效果展示与遇到的问题

效果展示

板上按键模拟鼠标左键单击  

FvF0jSY2VPslMnswtc6LUAEmb09d

板上按键模拟键盘输入字符  

FrDFE81HqL9UjENm_djSbLzy30ik

板上LED表示大小写状态 

Fh-Tl7EMUEgUdE4ovQNkQYtW7bIP

问题

1、做这个项目暂时还只是对项目框架zephr rtos有了粗浅的了解,对我来说还过于复杂。

2、还只是对HID描述符有了一些理解,但具体的蓝牙协议怎么实现传输抓换还是完全不懂。

3、还没把7002的WIFI功能好好用起来,下次一定!

五、感想与未来计划

1、转眼又参加完了一次电子森科的活动,每次参加电子森林的新活动都能学会不同的知识。我很是感谢电子森林为我们提供了这样的一个平台。

2、每次使用新的平台,都能开阔我的眼界,接触更多的工程框架,提高自己的code能力。

3、希望我后面能把板卡的WIFI功能用起来,做些好玩的东西。

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