基于XG24-EK2703A实现的蓝牙鼠标+键盘复合设备
一. 项目描述
本项目依托于funpack3-1活动,是一个基于XG24-EK2703A开发套件的蓝牙功能开发项目,目标是实现一个蓝牙鼠标+键盘复合设备。用该开发套件上的两个按钮结合该套件本身的低功耗蓝牙来实现以下任务。
项目任务:
1. 开发套件板载2个按键分别模拟鼠标的上下滚轮翻页
2. 开发套件板载2个按键同时按下2S后模拟键盘依次发送字符EETREE.CN
二. 硬件及原理介绍
1. 硬件介绍:
XG24-EK2703A板卡硬件布局、引脚图和结构框图:
XG24-EK2703A是一款基于EFR32MG24片上系统的开发套件,具备超低成本、低功耗和小巧的特点。该套件支持2.4GHz无线通信,兼容蓝牙LE、蓝牙mesh、Zigbee、Thread和Matter协议,该评估板所用芯片EFR32MG24B210F153-6IM48无线SoC芯片,该芯片是以ARM Cortex-M3为核心、主频为78MHz且支持2.4GHz无线通信,带有1536KB闪存和256KBRAM,除此之外,该芯片还搭载了人工智能和机器学习硬件加速功能,基本能够满足一些轻量级的AI部署应用。
该评估板上所搭载的硬件:
1.一个USB接口
2.一个板载SEGGER J-Link 调试器,支持SWD
3.两个LED和两个按钮
4.虚拟COM端口
5.数据包跟踪接口(PTI)
6.一个支持外部硬件连接的mikroBus插座和一个Qwiic连接器
7.32 位 ARM Cortex-M33,78 MHz最高工作频率
8.1536 kB 闪存和 256 kB RAM
对于该评估板有兴趣可以了解该评估板的Datasheet。
2.原理介绍:
主机和设备是 HID 协议中的两个实体,设备是直接与人类交互的实体,例如键盘、鼠标、游戏机等,主机则是与设备通信,以接收设备的数据,并以此了解设备所进行的操作。
HID over GATT(HOGP)是一种蓝牙低能耗无线通信设备,可以支持HID服务的配置文件,是一种通过BLE实现USB HID协议的BLE服务,允许开发使用BLE作为其标准通信形式的HID设备(一般HID为支持USB作为通信形式)。有兴趣可以看下HID over GATT官方文档。
在HID over GATT中,主机与设备之间的通信和数据交换一般有如下步骤:连接建立、使用GATT协议、配置设备所支持的服务和特征。而主机是怎么跟设置通信的?答案是通过报告描述符,所以报告描述符HID中是最重要的。
下面是键盘和鼠标的报告描述符:
由于第一次接触HID,对于报告描述符我的理解是,HID有自己的脚本语言(即报告描述符),我们可以使用这些简单的脚本语言来让我们定义HID设备支持多少数据包,数据包有多大,以及数据包中每个字节和位的用途。以此来达到控制HID设备的作用。具体的HID报告描述符可以查看以下人机接口设备(HID)的设备类定义。
下面就是如何控制键盘和鼠标了:
HID协议中的键盘和鼠标的各个功能和通信方式已经在报告描述符里面所定义了,接下我们只要按照这些描述符写出键盘和鼠标与主机的通信数据格式和数据大小就可以实现相应功能。
对于鼠标,上面的报告描述符定义了4个字节来发送数据,其中的byte0的0~2的bit位分别代表对鼠标的左键、中键、右键的控制,3~7的bit位为保留位,byte1代表鼠标沿x轴方向移动,byte2代表鼠标沿y轴运动,byte3代表鼠标的滚轮,要注意的是鼠标的方向变化是相对坐标,下面给出鼠标的报文格式:
同理,对于键盘,我们在报告描述符中定义了8个字节来发送数据,其中byte0为修饰符字节(修饰符字节是用来发键盘的特殊按键如CTRL键、SHIFT键等),byte1为保留字节,byte2~7为 6 个键代码字节(同时击键),下面给出鼠标的报文格式:
而对于复合设备,由于我们要将2个设备结合起来,我们就要规定每一个设备的报文ID,我们可以给设备加上Report ID。
修改完的设备的报告描述符如下:
由于我们加了给鼠标和键盘加了Report ID,所以我们的报文格式就还要多加一位byte位来代表所用的设备。
修改完的设备的鼠标和键盘报文格式如下:
三. 项目流程
对于蓝牙鼠标+键盘复合设备任务的整体思路如下:
代码思路:初始化完成后,按键中断回调函数主要是用来处理Btn0和Btn1事件,即按键是否按下事件,如果按键按下,则置Btn0或者Btn1为1,而在主函数中,我们会对该事件进行判断,我们分为3种情况,Btn0按下Btn1未按下,Btn0未按下Btn1按下,相应会给一个枚举变量BT_status赋值为SEND_WheelDown,SEND_WheelUp,当Btn0、Btn1都按下时,就开一个2s的定时器,当2s计时结束,就给BT_status赋值为SEND_String,而在蓝牙事件处理函数中,我们可以根据BT_status的值来确定所要发送的数据。
下面给出程序流程图:
四. 具体的实现过程和代码片段
1. Sdk和demo的注意事项:
首先我们要先安装好SSV5软件,并且在官方下载好sdk和官方示例。
软件安装可以在芯科官网进行下载:https://cn.silabs.com/developers/simplicity-studio
Sdk可以到官方的GitHub上面下载:https://github.com/SiliconLabs/gecko_sdk/releases/tag/v4.4.0
软件下载部分可以跟着官网教程走,sdk我们主要说一下,下载好以下的sdk和官方demo之后。
下载好sdk之后,建议将sdk放到安装软件路径下的developer\sdks路径下,方便之后下载其他sdk进行管理。
安装软件之后可以打开软件->点击左上角Window->搜索sdks->Add SDK->Browse选下载好之后的sdk。可以在以下界面查找所要烧写的demo,但是在烧写蓝牙前要先烧写一个bootloader:Bluetooth AppLoader OTA DFU。
如果没有,则要查看官方GitHub 所给的demo中的redeme要求进行配置。或者根据官网所给redeme操作步骤中的Requirements和Working with Projects进行配置。SiliconLabs/bluetooth_applications:Bluetooth wireless applications. Go to https://github.com/SiliconLabs/application_examples。
首先,我们在官方的Github上的demo库里找到bluetooth_hid_keyboard,按照readme的Start with a "Bluetooth - SoC Empty" project进行配置:
点击进行创建项目,之后跟着官网的配置走。
此外我们还要在HID over GATT里面进行配置,修改HID的报告描述符:
2. 代码片段的讲解
初始化部分:
在初始化部分,由于我们需要让键盘按下大写,大写关闭就是0x00,大写打开就是0x02,分别可以用宏定义CAPSLOCK_KEY_OFF和CAPSLOCK_KEY_ON代替,鼠标上滚轮就是0x01,下滚轮就是0xff,分别可以用宏定义Wheel_Up和Wheel_Down。而数组EETREE就是我们要发送的EETREE.CN的数据,而该数据里面的0x00就是起到让键盘先释放再按下的作用。枚举变量bt_status_t分别指代按键的事件,结构体btn_control为2个按键的状态变化。
#define MODIFIER_INDEX 1
#define Keyboarddata_INDEX 3
#define Mousedata_INDEX 4
#define ReportID_Index 0
#define Keyboard_ID 0x01
#define Mouse_ID 0x02
#define CAPSLOCK_KEY_OFF 0x00
#define CAPSLOCK_KEY_ON 0x02
#define Wheel_Up 0x01
#define Wheel_Down 0xff
#define TIMEOUT_MS 2000
static sl_sleeptimer_timer_handle_t periodic_timer;
sta static uint8_t KBinput_report_data[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0}; //键盘和鼠标的报文格式
sta static uint8_t MSinput_report_data[] = { 0, 0, 0, 0, 0};
static const uint8_t EETREE[] = {
0x08, /* e */
0x00, /* */
0x08, /* e */
0x17, /* t */
0x15, /* r */
0x08, /* e */
0x00, /* */
0x08, /* e */
0x37, /* . */
0x06, /* c */
0x11, /* n */
};
typedef enum
{
IDLE_Status = 0U,
SEND_STRING = 1U,
SEND_WheelUp = 2U,
SEND_WheelDown = 3U
} bt_status_t;
typedef struct {
uint8_t button0;
uint8_t button1;
bt_status_t BT_status;
} btn_control;
btn_control control = {0, 0, IDLE_Status};
按键中断回调函数部分:
按键中断回调函数部分主要对Btn0和Btn1的上跳沿进行处理并赋值,当检测到按键为上跳沿时(按键按下),就相应的置按键为1,为下跳沿时(按键松开),相应的按键置为0,且在执行完按键中断回调函数后就会进行蓝牙事件处理。
void sl_button_on_change(const sl_button_t *handle)
{
if (&sl_button_btn0 == handle) {
if (sl_button_get_state(handle) == SL_SIMPLE_BUTTON_PRESSED) {
control.button0 = 1;
app_log("Button0 pushed - callback\r\n");
} else {
control.button0 = 0;
app_log("Button0 released - callback \r\n");
}
}
if (&sl_button_btn1 == handle) {
if (sl_button_get_state(handle) == SL_SIMPLE_BUTTON_PRESSED) {
control.button1 = 1;
app_log("Button1 pushed - callback\r\n");
} else {
control.button1 = 0;
app_log("Button1 released - callback \r\n");
}
}
sl_bt_external_signal(1); //蓝牙事件处理
}
按键事件处理函数部分:
根据按键中断回调函数中对Btn0和Btn1的值来置BT_status的值或者开定时器,并且如果按键不是2个按键按下的话就会关闭定时器,以免干扰定时,且在开启定时器时将会判断定时器是否已经开启,如果没有开启才会执行发送字符串,以免短时间内多次按下2个按键导致发送字符串错乱。
void upDateButtonState(void)
{
bool is_running = false;
if(control.button0 == 1){
if(control.button1 == 1)
{
if (sl_sleeptimer_is_timer_running(&periodic_timer, &is_running) == 0){
if(!is_running){
sl_sleeptimer_restart_periodic_timer_ms(&periodic_timer,
TIMEOUT_MS,
timer_callback, NULL,
0,
SL_SLEEPTIMER_NO_HIGH_PRECISION_HF_CLOCKS_REQUIRED_FLAG);
}
}
}
else
{
control.BT_status = SEND_WheelUp;
sl_sleeptimer_stop_timer(&periodic_timer);
}
}
else if(control.button1 == 1){
if(control.button0 == 1)
{
if (sl_sleeptimer_is_timer_running(&periodic_timer, &is_running) == 0){
if(!is_running){
sl_sleeptimer_restart_periodic_timer_ms(&periodic_timer,
TIMEOUT_MS,
timer_callback, NULL,
0,
SL_SLEEPTIMER_NO_HIGH_PRECISION_HF_CLOCKS_REQUIRED_FLAG);
}
}
}
else
{
control.BT_status = SEND_WheelDown;
sl_sleeptimer_stop_timer(&periodic_timer);
}
}
}
蓝牙事件处理函数部分:
在蓝牙事件处理函数中,对BT_status的值进行处理,分别为,同时按下2个按键后2s发送字符串,按下Btn0发送上滚轮,按下Btn1发送下滚轮。
void sl_bt_on_event(sl_bt_msg_t *evt)
{
//GATT的各种配置
...
//蓝牙事件处理实际上要修改的地方
case sl_bt_evt_system_external_signal_id:
if (notification_enabled == 1) {
delay_ms(15);
switch(control.BT_status)
{
case SEND_STRING:
Send_EETREE(sc);
break;
case SEND_WheelUp:
Wheel_UpFun(sc);
break;
case SEND_WheelDown:
Wheel_DownFun(sc);
break;
default:
break;
}
}
break;
...
}
模拟键盘发送字符串的函数,我们要在发送之后再发送键盘按键全部释放的指令,否则就会出现键盘一直按下的情况,且由于我们要发送的字符串中包含点(.),且我们发送的字符串位大写,而在大写状态下发送字符点(.)其实为字符(》),故我们要对发送的点(.)的字符进行判断处理。
void Send_EETREE(sl_status_t sc)
{
memset(KBinput_report_data, 0, sizeof(KBinput_report_data));
for(uint16_t i = 0; i < sizeof(EETREE); i++){
KBinput_report_data[ReportID_Index] = Keyboard_ID;
if(i == 8) //对点(.)进行处理
{
KBinput_report_data[MODIFIER_INDEX] = CAPSLOCK_KEY_OFF;
}
else{
KBinput_report_data[MODIFIER_INDEX] = CAPSLOCK_KEY_ON;
}
KBinput_report_data[Keyboarddata_INDEX] = EETREE[i];
sc = sl_bt_gatt_server_notify_all(gattdb_report,
sizeof(KBinput_report_data),
KBinput_report_data);
app_assert_status(sc);
}
for(uint16_t j = 1; j < sizeof(KBinput_report_data); j++) //发送完之后要将键盘释放
{
KBinput_report_data[j] = 0;
}
sc = sl_bt_gatt_server_notify_all(gattdb_report,
sizeof(KBinput_report_data),
KBinput_report_data);
app_assert_status(sc);
control.BT_status = IDLE_Status;
app_log("Keyboard report was sent\r\n");
}
模拟鼠标滚轮就很容易了,我们只要发送上滚轮0x01和下滚轮0xff的值就行了。
void Wheel_UpFun(sl_status_t sc)
{
memset(MSinput_report_data, 0, sizeof(MSinput_report_data));
MSinput_report_data[ReportID_Index] = Mouse_ID;
MSinput_report_data[Mousedata_INDEX] = Wheel_Up;
sc = sl_bt_gatt_server_notify_all(gattdb_report,
sizeof(MSinput_report_data),
MSinput_report_data);
app_assert_status(sc);
control.BT_status = IDLE_Status;
app_log("WheelUp report was sent\r\n");
}
void Wheel_DownFun(sl_status_t sc)
{
memset(MSinput_report_data, 0, sizeof(MSinput_report_data));
MSinput_report_data[ReportID_Index] = Mouse_ID;
MSinput_report_data[Mousedata_INDEX] = Wheel_Down;
sc = sl_bt_gatt_server_notify_all(gattdb_report,
sizeof(MSinput_report_data),
MSinput_report_data);
app_assert_status(sc);
control.BT_status = IDLE_Status;
app_log("WheelDown report was sent\r\n");
}
定时器中断回调函数部分:
在定时器中我们所要执行的操作是:2S之后执行回调函数时将BT_status赋值为SEND_STRING即可。
//timer callback
static void timer_callback(sl_sleeptimer_timer_handle_t *handle,
void *data)
{
(void)&handle;
(void)&data;
control.BT_status = SEND_STRING;
sl_bt_external_signal(1);
sl_sleeptimer_stop_timer(&periodic_timer);
}
3.项目演示效果:
烧录好程序之后:
先让电脑连接好开发板的蓝牙
好了之后我们为了演示,可以打开浏览器,下面图片为滚轮没有变化之前的滑条
按下BTN0后滚轮向上滚动
按下BTN1后滚轮向下滚动
2个按键同时按下2s过后发送EETREE.CN
五. 心得体会和未来展望
本人是第一次参加Funpack活动,由于之前在学校只接触过STM32,一直感觉自己的知识面很窄,由于9月份就要毕业了,看了许多的招聘信息感觉目前的嵌入式要求的知识面要很广,所以当看到得捷和电子森林举办的本次Funpack3-1活动就心动参加了,参加本次活动让自己受益匪浅,由于之前很少接触过低功耗蓝牙,参加本次比赛之后也算是入了半个低功耗蓝牙的门,对于HID协议也有了一定的理解,相信这些知识对于将来的就业一定会有所帮助,以后也会多参加电子森林所举办的活动,努力自我学习和持续提升。