Funpack2-6:基于nRF7002 DK的蓝牙键鼠复合设备
任务一:使用板卡的蓝牙连接,设计一个蓝牙鼠标+键盘复合设备,按键1作为鼠标点击,按键2作为键盘输入按下时输入“eetree”字符,电脑开启大写锁定时,板卡的LED亮起。
标签
嵌入式系统
Funpack活动
flag&刺猬
更新2023-10-13
259

主要内容框架

1、板卡介绍

2、任务目标

3、开发环境

4、应用开发-记录

5、心得体会

活动主页 Funpack第二季第六期:NRF7002-DK - 电子森林

参加硬禾学堂得捷电子Nordic Semiconductor 举办的这一期活动,了解Zephyr操作系统和蓝牙、NFC的使用。

板卡实物图:

 

1 板卡介绍

简单介绍:nRF7002-DK是用于nRF7002 Wi-Fi 6协同IC的开发套件,该开发套件采用nRF5340多协议片上系统 (SoC) 作为nRF7002的主处理器,在单一的电路板上包含了开发工作所需的一切,可让开发人员轻松开启基于nRF7002 的物联网项目。该 DK 包括 Arduino 连接器、两个可编程按钮、一个 Wi-Fi 双频段天线和一个低功耗蓝牙天线,以及电流测量引脚。

特性:

Arduino连接器

两个可编程的按钮

搭载nRF7002 Wi-Fi协同IC

作为主处理器的nRF5340 SoC

电流测量引脚

2.4GHz、2.4/5 GHz和NFC天线

高性能的128MHz Arm Cortex-M33应用内核

超低功率的64MHz Arm Cortex-M33网络核心

 

2 任务目标

任务一:

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

项目的实现可以分为以下几部分:

  1. 蓝牙广播实现配对连接,发送HID信息给主机;
  2. 按键控制实现字符发送的功能和LED的控制;
  3. HID描述符初始化、发送数据封装以及接收数据解析;

整体功能流程图:

FvmMWcBpBb09U6o0ZzTcglkyVXsk

3 开发环境

win10专业版,开发工具:Visual Studio Code

环境搭建:直接参考教程做就行 ,一次不行就在安装一次 反复四次就行。心平气和即可!

参考教程:

3.0 Welcome to the nRF Connect SDK! — nRF Connect SDK 2.4.99 documentation (nordicsemi.com) 官网教程安装也是可以的

3.1 bluetoothlover_doc/source/00_supperthomas/15_zephyr/01_NCS_quick_start/index.md at master · supperthomas/bluetoothlover_doc (github.com) 直接参考这个就行,如果不行就看备注。

备注:

    参考1的做法安装之后可能存在下载的sdk软件包为空的问题,也就是V2.x.0目录下只有一个west的目录。

    还有可能存在的一个问题就是在新建工程之后创建编译环境的时候找不到开发板的BSP的选择。此时就是需要重新安装SDK包。

    这个时候就可以参考开发你的第一个nRF Connect SDK(NCS)/Zephyr应用程序 - iini - 博客园 (cnblogs.com) 这个博客里面的一部分可以参考 就是使用他的百度网盘提供的软件包离线安装就行。搭配视频安装教程来使用上个链接的方法:【nrf7002开发环境搭建】 https://www.bilibili.com/video/BV1Lk4y1F7bG/?share_source=copy_web&vd_source=92dd524240ffe301bd49347e8e28a7ad

其余的搭建环境的方法有很多,请自行搜索解决。谢谢!

 

4 应用开发-记录

蓝牙广播实现配对连接,发送HID信息给主机

err = bt_enable(NULL);
// 初始化广播的线程
k_work_init(&adv_work, advertising_process);
if (IS_ENABLED(CONFIG_BT_HIDS_SECURITY_ENABLED))
{
    // 初始化蓝牙配对处理线程
    k_work_init(&pairing_work, pairing_process
);

// 用于控制广播(advertising)的函数
static void advertising_continue(void)
{
    struct bt_le_adv_param adv_param;

#if CONFIG_BT_DIRECTED_ADVERTISING
    bt_addr_le_t addr;
    // 检查是否可以从消息队列bonds_queue中获取到目标设备的地址
    if (!k_msgq_get(&bonds_queue, &addr, K_NO_WAIT))
    {
        char addr_buf[BT_ADDR_LE_STR_LEN];
        // 如果成功获取到目标设备的地址,则将adv_param设置为定向广播参数,即使用指定的目标地址进行广播
        adv_param = *BT_LE_ADV_CONN_DIR(&addr);
        // 将广播参数的选项中加上BT_LE_ADV_OPT_DIR_ADDR_RPA,表示使用重新生成的随机地址作为目标地址
        adv_param.options |= BT_LE_ADV_OPT_DIR_ADDR_RPA;
        // 启动定向广播,使用指定的广播参数和空的广告数据
        int err = bt_le_adv_start(&adv_param, NULL, 0, NULL, 0);
        // 检查广播启动是否出错
        if (err)
        {
            printk("Directed advertising failed to start\n");
            return;
        }
        // 将目标设备的地址转换为字符串形式
        bt_addr_le_to_str(&addr, addr_buf, BT_ADDR_LE_STR_LEN);
        printk("Direct advertising to %s started\n", addr_buf);
    }
#endif
}    

// 蓝牙配对处理函数    
static void pairing_process(struct k_work *work)
{
    int err;
    struct pairing_data_mitm pairing_data;
    // 存储设备地址
    char addr[BT_ADDR_LE_STR_LEN];
    // 从消息队列mitm_queue中获取配对数据,并将其保存到pairing_data中
    err = k_msgq_peek(&mitm_queue, &pairing_data);
    if (err)
    {
        return;
    }
    // 将连接对象pairing_data.conn的目标设备地址转换为字符串形式,并保存到addr
    bt_addr_le_to_str(bt_conn_get_dst(pairing_data.conn),
                      addr, sizeof(addr));
}

按键控制实现字符发送的功能和LED的控制

// 按键初始化函数
dk_buttons_init(button_changed);
// button_changed() 是按键状态发生变化之后的处理函数-自定义
// 初始化LED灯
dk_leds_init();
// 此函数是一个按键状态变化的回调函数。当与按钮相关的事件发生时,该函数会被调用
void app_button_changed(uint32_t button_state, uint32_t has_changed)
{    
    // eetree 的字符串
    static uint8_t *chr = hello_world_str;
    static uint8_t i = 0;
    bool data_to_send = false;
    struct mouse_pos pos;
    uint32_t buttons = button_state & has_changed;
    memset(&pos, 0, sizeof(struct mouse_pos));

    uint32_t app_button_state = 0;
    uint32_t app_has_changed = 0;

    // 在进行按键状态判断之前,会先判断 cfg_flag 是否为0,如果为0,则函数直接返回
    if(cfg_flag == 0)            /*未进行配置*/
    {
        return ;
    }
    uint32_t val;
    static uint32_t last_val0 = 0;
    // 过读取按键1的状态,更新相应的按钮操作-也就是button0
    val =  gpio_pin_get_dt(&button0);
    // 与上一次的状态值进行比较。如果发生变化,则根据按钮状态进行相应的操作,
    // 并设置 data_to_send 为true,表示有数据需要发送。
    if(last_val0 != val)
    {
        last_val0 = val;
        // 通过判断当前的按键0状态值 val 是否为1,来确定是按钮0被按下还是释放
        if(val == 1)
        {
            pos.check = 1;
            // 将鼠标的X坐标值 pos.x_val 减去10
            pos.x_val -= 10;            //MOVEMENT_SPEED+10);
            printk("%s(): left\n", __func__);
            printk("button0 check\r\n");
        }
        // 如果当前的按钮0状态值不为1,表示按钮0被释放
        // 将 pos.check 的值设置为0,表示鼠标位置信息不需要更新
        else 
        {
            pos.check = 0;
        }
        // 将 pos.check 的值设置为0,表示鼠标位置信息不需要更新
        data_to_send = true;
    }
    // 对应的按键2 button1 这个和按键1的使用相似
    // 按键发送信息 eetree 这个字符串给电脑
    static uint32_t last_val1 = 0;
    val =  gpio_pin_get_dt(&button1);
    if(last_val1 != val)
    {    
        // 按键2的状态信息发生变化也就是按键按下 或者
        last_val1 = val;
        if(val == 1)
        {
            printk("button1 check [%d]\r\n", i);
            // 调用 hid_buttons_press 函数,向鼠标发送按键2对应的按下操作。
            // 这里通过传递参数 &hello_world_str[i] 来指定要发送的按键操作,同时参数1表示按下操作。 
            // 注意,hello_world_str 是一个字符数组,存储了一系列按键操作的信息
            hid_buttons_press(&hello_world_str[i], 1);
        }
        else
        {

            printk("button1 release[%d]\r\n", i);
            // 向鼠标发送按键2对应的释放操作。这里同样通过传递参数 &hello_world_str[i] 来指定要发送的按键操作,同时参数1表示释放操作。
            hid_buttons_release(&hello_world_str[i], 1);
            // 循环发送eetree 这个字符串的6个字符
            i++;
            if(i > sizeof(hello_world_str) - 1)
            {
                i = 0;
            }
        }
    }
    // 如果 data_to_send 为true,将鼠标位置信息放入消息队列 hids_queue 中,
    // 并提交一个工作项 hids_work 来处理队列中的数据
    if (data_to_send) {
        int err;

        err = k_msgq_put(&hids_queue, &pos, 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);
        }
    }
}


// LED 灯的控制是由主机发出的LED通知 使用大小写锁定控制LED1灯的状态信息
void hids_outp_rep_handler(struct bt_hids_rep *rep, struct bt_conn *conn, bool write)
{
    char addr[BT_ADDR_LE_STR_LEN];
    // 如果输出报告仅被读取而不是写入,该函数将打印“Output report read”并返回,
    // 因为在这种情况下没有任何需要处理的东西
    if (!write)
    {
        printk("Output report read\n");
        return;
    };
    // 使用bt_conn_get_dst(conn)获取与连接对应的目标蓝牙设备的地址,并将其格式化为字符串存储在addr数组中。
    // 然后,使用printk函数来打印输出报告已经被接收的地址
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    // Output report has been received B4:D5:BD:DB:DA:34 (public)
    printk("Output report has been received %s\n", addr);
    // 最后的 caps_lock_handler函数会在输出报告收到之后被调用。
    // 该函数读取输出报告数据中的第一字节(代表着键盘状态),
    // 并将其与CAPS LOCK位掩码进行比较以检测CAPS LOCK键是否处于活动状态。
    // 如果该位为1,LED1灯就会点亮,否则关闭。
    caps_lock_handler(rep);
}

static void caps_lock_handler(const struct bt_hids_rep *rep)
{
    uint8_t report_val = ((*rep->data) & OUTPUT_REPORT_BIT_MASK_CAPS_LOCK) ? 1 : 0;
    // 设置LED灯的开关 1-open
    dk_set_led(LED_CAPS_LOCK, report_val);
}

HID描述符初始化、发送数据封装以及接收数据解析

// 使用的map表如下所示
static const uint8_t report_map[] = {        
    0x05, 0x01,     /* Usage Page (Generic Desktop) */
    0x09, 0x02,     /* Usage (Mouse) */

    0xA1, 0x01,     /* Collection (Application) */

    /* Report ID 1: Mouse buttons + scroll/pan */
    0x85, 0x01,       /* Report Id 1 */
    0x09, 0x01,       /* Usage (Pointer) */        
    0xA1, 0x00,       /* Collection (Physical) */
    0x05, 0x09,       /* Usage Page (Buttons) */
    0x19, 0x01,       //     USAGE_MINIMUM (Button 1)
    0x29, 0x03,       //     USAGE_MAXIMUM (Button 3)
    0x15, 0x00,       /* Logical Minimum (0) */
    0x25, 0x01,       /* Logical Maximum (1) */        
    0x95, 0x03,       /* Report Count (3) */
    0x75, 0x01,       /* Report Size (1) */
    0x81, 0x02,       /* Input (Data, Variable, Absolute) */

    0x95, 0x01,       /* Report Count (1) */
    0x75, 13,         /* Report Size (13) */
    0x81, 0x01,       /* Input (Constant) for padding */

    0x95, 0x01,       /* Report Count (1) */
    0x75, 0x08,       /* Report Size (8) */
    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) */
    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 */

/**keyboard*/
    0x05, 0x01,       /* Usage Page (Generic Desktop) */
    0x09, 0x06,       /* Usage (Keyboard) */
    0xA1, 0x01,       /* Collection (Application) */
    0x85, 0x03,            /* Report Id (3) */

    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, 0x03,                     /* Report Id (3) */
    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) */
};
// HID描述符初始化
static void hid_init(void)
{
    int err;
    struct bt_hids_init_param hids_init_param = {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(INPUT_REP_MOVEMENT_LEN, 8)] = {0};
    // 设置为report_map,report_map 是一个报告描述符的数组,用于描述HID设备的输入输出特性
    // 详细请看 https://www.usbzh.com/article/detail-76.html
    hids_init_param.rep_map.data = report_map;
    // 设置描述符数组的大小
    hids_init_param.rep_map.size = sizeof(report_map);
    // HID设备的HID规范版本号 0x0101
    hids_init_param.info.bcd_hid = BASE_USB_HID_SPEC_VERSION;
    // 国家代码
    hids_init_param.info.b_country_code = 0x00;
    // HID设备支持远程唤醒和通常可连接
    hids_init_param.info.flags = (BT_HIDS_REMOTE_WAKE |
                                  BT_HIDS_NORMALLY_CONNECTABLE);

    hids_inp_rep = &hids_init_param.inp_rep_group_init.reports[INPUT_REP_KEYS_IDX]; // 0
#if 1
    ...
    // 报告掩码用于指示该输入报告中哪些数据位有效,这里是用于表示鼠标位移的数据位。
    hids_inp_rep->rep_mask = mouse_movement_mask;
    // 增加输入报告组的计数器,表示已经添加了一个新的输入报告
    hids_init_param.inp_rep_group_init.cnt++;

    hids_inp_rep++; // 2 keyboard
#endif
    ...
#if 1
    ...
    // 键盘的大小写锁定回调函数
    hids_outp_rep->handler = hids_outp_rep_handler;
    hids_init_param.outp_rep_group_init.cnt++;
#endif
    // 初始化的HID设备会启用鼠标功能
    hids_init_param.is_mouse = true;
    // 初始化的HID设备会启用键盘功能
    hids_init_param.is_kb = true;
    // 回调函数用于处理引导键盘的输出报告
    hids_init_param.boot_kb_outp_rep_handler = hids_boot_kb_outp_rep_handler;
    // 回调函数用于处理HID设备的电源管理事件 
    hids_init_param.pm_evt_handler = hids_pm_evt_handler;
    ...
}

// HID 键盘的信息发送的实现 此函数由 key_report_send 函数调用
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(get_hid_obj(), conn, data,
                                           sizeof(data), NULL);
    }
    else
    {
        err = bt_hids_inp_rep_send(get_hid_obj(), conn,
                                   2, data, // 这里的0 对应report_map 中的 Report Id INPUT_REP_KEYS_IDX
                                   sizeof(data), NULL);
    }
    return err;
}

// HID 鼠标的实现 此函数由mouse_handler()中断调用
static void mouse_movement_send(int16_t x_delta, int16_t y_delta, uint8_t check)
{
    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)
        {
            x_delta = MAX(MIN(x_delta, SCHAR_MAX), SCHAR_MIN);
            y_delta = MAX(MIN(y_delta, SCHAR_MAX), SCHAR_MIN);

            bt_hids_boot_mouse_inp_rep_send(get_hid_obj(),
                                            conn_mode[i].conn,
                                            NULL,
                                            (int8_t)x_delta,
                                            (int8_t)y_delta,
                                            NULL);
        }
        else
        {
            uint8_t x_buff[2];
            uint8_t y_buff[2];
            uint8_t buffer[INPUT_REP_MOVEMENT_LEN];

            int16_t x = MAX(MIN(x_delta, 0x07ff), -0x07ff);
            int16_t y = MAX(MIN(y_delta, 0x07ff), -0x07ff);

            /* Convert to little-endian. */
            sys_put_le16(x, x_buff);
            sys_put_le16(y, y_buff);

            /* Encode report. */
            BUILD_ASSERT(sizeof(buffer) == 3,
                         "Only 2 axis, 12-bit each, are supported");

            buffer[0] = (check&0x01);
            buffer[1] = 0;
            buffer[2] = 0;

            bt_hids_inp_rep_send(get_hid_obj(), conn_mode[i].conn,
                                 // INPUT_REP_MOVEMENT_INDEX,
                                 INPUT_REP_BUTTONS_INDEX, 
                                 buffer, sizeof(buffer), NULL);
        }
    }
}

在主函数中直接调用即可

int main(void)
{
    int err;
    // 注册了一个连接身份验证(authentication)回调函数,即在连接身份验证过程中自动调用该回调函数
    bt_conn_auth_cb_register(&conn_auth_callbacks);
    // 注册了一个连接身份验证信息(authentication information)回调函数,即在需要连接身份验证信息的时候自动调用该回调函数
    bt_conn_auth_info_cb_register(&conn_auth_info_callbacks);
    // HID设备的初始化
    hid_init();
    // 启用蓝牙协议栈
    bt_enable(NULL);
    printk("Bluetooth initialized\n");
    // 初始化鼠标处理线程
    k_work_init(&hids_work, mouse_handler);
    // 初始化广播的线程
    k_work_init(&adv_work, advertising_process);
    // 初始化蓝牙配对处理线程
    k_work_init(&pairing_work, pairing_process);
    ...
    // 开始广播线程
    advertising_start();
    // 初始化按键
    configure_buttons();
    // 初始化LED灯
    dk_leds_init();
    while (1)
    {
        k_sleep(K_MSEC(ADV_LED_BLINK_INTERVAL));
        // 鼠标和键盘的使用
        app_button_changed(1, 1);

        /* Battery level simulation */
        // 电量信息函数
        bas_notify();
    }
}

效果展示

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

FtwnpZ9ma563IDc5uzFoTmvDYBPu

板上按键模拟鼠标左键单击,进行选中关闭串口和打开串口的鼠标左键测试

Fnh8BiBIK5c-TIET_gpY6d_cfufdFo0MuFD4jgUpAgI21ZVvx6WfcOZ-

5 心得体会

非常感谢硬禾学堂提供funpack活动。使用NRF7002-DK开发板使用Zypher操作系统进行开发,开发环境有点难搭建。其他的都还好,这次活动的任务比较有意思,针对zephyr比较感兴趣,学习设备树和Kconfig的配置使用。总体上这次活动还是学习嵌入式操作系统的相关知识。

备注:代码在附件里面

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