前言
很久没开发蓝牙相关的项目,找到一块Noridc蓝牙kit板,正好配合6轴来做一个拟合姿态的无线鼠标
如有异议,欢迎指正
方案实现
使用了6轴传感器和Nordic kit开发板,6轴获取姿态换算获得欧拉角,然后对应的角度拟合到HID鼠标描述符的XY轴移动改变量,通过蓝牙传给到PC端,用于控制鼠标指针动作
HID基本概念
HID(The Human Interface Device)人机交互设备,定义了在人机接口设备中的协议、特征和使用规范,蓝牙HID协议以USB HID协议规范为基础
HID规范
HID角色
- HID device:HID 设备,PC主机
- HID Host:HID 主机,鼠标、键盘
设备描述符结构
HID的定义在接口描述符中,HID接口描述包含了报告描述符和物理描述符
HID report
HID Device支持三种报告
- Input report(输入报告): hid device发送给 hid host的封包;例如鼠标将移动和点击事件信息发送给PC端
- Output report(输出报告):hid host发送给 hid device的封包;例如PC端发送数据来控制键盘灯
- Feature report(特征报告):传输双向数据
HID逻辑链路
- HID Control:通道主要用于传输控制封包,这个通道传输的封包为同步封包(synchronous reports),L2CAP的psm为0x0011
- HID Interrupt:通道传输的封包不需要确认,所有称为异步封包(asynchronous reports),L2CAP的psm为0x0013
HID Report Models报告模式
两种协议
- boot protocol:boot protocl不解析报告描述符,设备支持该协议,在PC系统处于BIOS时就可以识别设备
- report protocol:需要解析报告描述符,PC系统启动完成后进行枚举描述符
蓝牙HID
工程实例
使用nordic52832 Kit开发板,SDK为当前最新版本nRF5_SDK_17.0.2_d674dde
,工程目录examples\ble_peripheral\ble_app_hids_mouse
,协议栈使用S132
广播报文
广播报文包含服务Serivice UUIDs为 0x1812,指示为HID服务,Apperance外观(Type 0x19)为962(960~968),标识HID子类型为鼠标Mouse
代码分析
主函数
- 主函数初始化按键与协议栈,在循环中加入6轴的拟合数据后发送鼠标移动量到PC端
int main(void)
{
bool erase_bonds;
// Initialize.
log_init();
timers_init(); //初始化定时模块
buttons_leds_init(&erase_bonds); //按键led初始化,注册事件回调
power_management_init();电源管理
/*
* 下面是对蓝牙相关配置的初始化,一般不需要修改
*/
ble_stack_init();//协议栈初始化
scheduler_init();//调度器
gap_params_init();//gap初始化
gatt_init();//gatt初始化
advertising_init();//广播初始化
services_init();//服务初始化
sensor_simulator_init();//模拟电池
conn_params_init();//连接参数初始化
peer_manager_init();//配对绑定初始化
// Start execution.
NRF_LOG_INFO("HID Mouse example started.");
timers_start();// 开启定时器
//erase_bonds = true;//擦除绑定
advertising_start(erase_bonds);//广播开启后,蓝牙内部协议栈的调度就开始跑起来了
Lsm6dlSensor_Init();//6轴初始化
// Enter main loop.
for (;;)
{
x_mov = 0, y_mov = 0;
Lsm6dl_Read(&x_mov, &y_mov);//获取移动量
if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
{
if(x_mov != 0 || y_mov != 0)
mouse_movement_send(x_mov, y_mov);//发送
}
idle_state_handle();//等待事件
}
}
服务初始化
static void services_init(void)
{
qwr_init();//
dis_init();//初始化设备信息服务(产品信息)
bas_init();//注册电池服务
hids_init();//hid服务初始化
}
报告描述符
HID设备是通过报告(report)来传输数据的,包含有输入报告(Input)和输入(Output)报告
- bSize(bit0-bit1):用来标识后面所带的数据字节数,00 - 11指代0~4字节(0、1、2、4)
- bType(bit2-bit3):标识条目类型,0-主条目、1-全局条目、2-局部条目、3-保留
- 主条目:分为Input(输入)、Output(输出)、Feature(特性)、Collection(集合)、End Collection(结束集合)
- 全局条目:选择用途页,定义数据域长度、数量、报告ID,对后面出现的主条目都有效
- 局部条目:局部有效,遇到主条目后失效,定义控制的特性,如最大最小值
- bTag(bit4-bit7):标识条目功能,具体可以查看HID协议
查看hids_init
函数
- 数组rep_map_data为HID的报告描述符,按域划分,用来描述报告的结构与通信数据中具体的作用;代码中主要描述了鼠标的按键与滚轮数据、鼠标移动组织结构,还描述了播放音乐的高级按键
static void hids_init(void)
{
ret_code_t err_code;
ble_hids_init_t hids_init_obj;
ble_hids_inp_rep_init_t * p_input_report;
uint8_t hid_info_flags;
static ble_hids_inp_rep_init_t inp_rep_array[INPUT_REPORT_COUNT];
static uint8_t rep_map_data[] = //HID 报告描述
{
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)鼠标
0xA1, 0x01, // Collection (Application)指示下面对mouse解释
// Report ID 1: Mouse buttons + scroll/pan 按键滚轮
0x85, 0x01, // Report Id 1 id = 1 报告Id
0x09, 0x01, // Usage (Pointer) 指针形式
0xA1, 0x00, // Collection (Physical) 指针定义
0x95, 0x05, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x05, 0x09, // Usage Page (Buttons) //接下来定义按键
0x19, 0x01, // Usage Minimum (01) button1 左键
0x29, 0x05, // Usage Maximum (05) button5
0x15, 0x00, // Logical Minimum (0) 键值0
0x25, 0x01, // Logical Maximum (1) 键值1
0x81, 0x02, // Input (Data, Variable, Absolute)
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x81, 0x01, // Input (Constant) for padding
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
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)
0x05, 0x0C, // Usage Page (Consumer)
0x0A, 0x38, 0x02, // Usage (AC Pan)
0x95, 0x01, // Report Count (1)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
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)
// Report ID 3: Advanced buttons 高级按键
0x05, 0x0C, // Usage Page (Consumer)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, 0x03, // Report Id (3)
0x15, 0x00, // Logical minimum (0)
0x25, 0x01, // Logical maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x01, // Report Count (1)
0x09, 0xCD, // Usage (Play/Pause)播放暂停
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x0A, 0x83, 0x01, // Usage (AL Consumer Control Configuration)
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xB5, // Usage (Scan Next Track)下一首
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xB6, // Usage (Scan Previous Track)//上一首
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xEA, // Usage (Volume Down)音量-
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x09, 0xE9, // Usage (Volume Up)音量+
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x0A, 0x25, 0x02, // Usage (AC Forward)应用控制
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0x0A, 0x24, 0x02, // Usage (AC Back)返回键
0x81, 0x06, // Input (Data,Value,Relative,Bit Field)
0xC0 // End Collection
};
memset(inp_rep_array, 0, sizeof(inp_rep_array));
// Initialize HID Service.
p_input_report = &inp_rep_array[INPUT_REP_BUTTONS_INDEX];//按键输入报告
p_input_report->max_len = INPUT_REP_BUTTONS_LEN;
p_input_report->rep_ref.report_id = INPUT_REP_REF_BUTTONS_ID;
p_input_report->rep_ref.report_type = BLE_HIDS_REP_TYPE_INPUT;
p_input_report->sec.cccd_wr = SEC_JUST_WORKS;
p_input_report->sec.wr = SEC_JUST_WORKS;
p_input_report->sec.rd = SEC_JUST_WORKS;
p_input_report = &inp_rep_array[INPUT_REP_MOVEMENT_INDEX];//移动输入报告
p_input_report->max_len = INPUT_REP_MOVEMENT_LEN;
p_input_report->rep_ref.report_id = INPUT_REP_REF_MOVEMENT_ID;
p_input_report->rep_ref.report_type = BLE_HIDS_REP_TYPE_INPUT;
p_input_report->sec.cccd_wr = SEC_JUST_WORKS;
p_input_report->sec.wr = SEC_JUST_WORKS;
p_input_report->sec.rd = SEC_JUST_WORKS;
p_input_report = &inp_rep_array[INPUT_REP_MPLAYER_INDEX];//播放输入报告
p_input_report->max_len = INPUT_REP_MEDIA_PLAYER_LEN;
p_input_report->rep_ref.report_id = INPUT_REP_REF_MPLAYER_ID;
p_input_report->rep_ref.report_type = BLE_HIDS_REP_TYPE_INPUT;
p_input_report->sec.cccd_wr = SEC_JUST_WORKS;
p_input_report->sec.wr = SEC_JUST_WORKS;
p_input_report->sec.rd = SEC_JUST_WORKS;
hid_info_flags = HID_INFO_FLAG_REMOTE_WAKE_MSK | HID_INFO_FLAG_NORMALLY_CONNECTABLE_MSK;
memset(&hids_init_obj, 0, sizeof(hids_init_obj));
hids_init_obj.evt_handler = on_hids_evt;
hids_init_obj.error_handler = service_error_handler;
hids_init_obj.is_kb = false; //禁用键盘
hids_init_obj.is_mouse = true; //使能鼠标
hids_init_obj.inp_rep_count = INPUT_REPORT_COUNT;//3个输入
hids_init_obj.p_inp_rep_array = inp_rep_array;//报告描述符
hids_init_obj.outp_rep_count = 0;//无输出报告
hids_init_obj.p_outp_rep_array = NULL;
hids_init_obj.feature_rep_count = 0;//无特征报告
hids_init_obj.p_feature_rep_array = NULL;//
hids_init_obj.rep_map.data_len = sizeof(rep_map_data);
hids_init_obj.rep_map.p_data = rep_map_data;
hids_init_obj.hid_information.bcd_hid = BASE_USB_HID_SPEC_VERSION;
hids_init_obj.hid_information.b_country_code = 0;
hids_init_obj.hid_information.flags = hid_info_flags;
hids_init_obj.included_services_count = 0;
hids_init_obj.p_included_services_array = NULL;
hids_init_obj.rep_map.rd_sec = SEC_JUST_WORKS;
hids_init_obj.hid_information.rd_sec = SEC_JUST_WORKS;
hids_init_obj.boot_mouse_inp_rep_sec.cccd_wr = SEC_JUST_WORKS;
hids_init_obj.boot_mouse_inp_rep_sec.wr = SEC_JUST_WORKS;
hids_init_obj.boot_mouse_inp_rep_sec.rd = SEC_JUST_WORKS;
hids_init_obj.protocol_mode_rd_sec = SEC_JUST_WORKS;
hids_init_obj.protocol_mode_wr_sec = SEC_JUST_WORKS;
hids_init_obj.ctrl_point_wr_sec = SEC_JUST_WORKS;
err_code = ble_hids_init(&m_hids, &hids_init_obj);//添加HID服务 0x1812
APP_ERROR_CHECK(err_code);
}
按键回调
kit板上带有四个按键,例程中是用来做XY轴的鼠标移动行为,这里我改成了鼠标按键的功能
- 新增发送按键功能
//发送按键
void mouse_button_send(int8_t click, int8_t wheel, int8_t pan)
{
ret_code_t err_code;
uint8_t buffer[INPUT_REP_BUTTONS_LEN];
APP_ERROR_CHECK_BOOL(INPUT_REP_BUTTONS_LEN == 3);
buffer[0] = click;
buffer[1] = wheel;
buffer[2] = wheel;
err_code = ble_hids_inp_rep_send(&m_hids,
INPUT_REP_BUTTONS_INDEX,
INPUT_REP_BUTTONS_LEN,
buffer,
m_conn_handle);
if ((err_code != NRF_SUCCESS) &&
(err_code != NRF_ERROR_INVALID_STATE) &&
(err_code != NRF_ERROR_RESOURCES) &&
(err_code != NRF_ERROR_BUSY) &&
(err_code != BLE_ERROR_GATTS_SYS_ATTR_MISSING)
)
{
APP_ERROR_HANDLER(err_code);
}
}
- 按键事件回调
static void bsp_event_handler(bsp_event_t event)
{
ret_code_t err_code;
switch (event)
{
case BSP_EVENT_SLEEP:
sleep_mode_enter();
break;
case BSP_EVENT_DISCONNECT:
err_code = sd_ble_gap_disconnect(m_conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
if (err_code != NRF_ERROR_INVALID_STATE)
{
APP_ERROR_CHECK(err_code);
}
break;
case BSP_EVENT_WHITELIST_OFF:
if (m_conn_handle == BLE_CONN_HANDLE_INVALID)
{
err_code = ble_advertising_restart_without_whitelist(&m_advertising);
if (err_code != NRF_ERROR_INVALID_STATE)
{
APP_ERROR_CHECK(err_code);
}
}
break;
case BSP_EVENT_KEY_0:
if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
{
mouse_button_send(1, 0, 0);//左键 //mouse_movement_send(-MOVEMENT_SPEED, 0);
}
break;
case BSP_EVENT_KEY_1:
if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
{
mouse_button_send(2, 0, 0);//右键 //mouse_movement_send(0, -MOVEMENT_SPEED);
}
break;
case BSP_EVENT_KEY_2:
if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
{
mouse_button_send(4, 0, 0);//中键 mouse_movement_send(MOVEMENT_SPEED, 0);
}
break;
case BSP_EVENT_KEY_3:
if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
{
mouse_button_send(0, 0, 0);//取消 mouse_movement_send(0, MOVEMENT_SPEED);
}
break;
default:
break;
}
}
总结
PC端绑定设备后会在设备管理器中出现Sim_Mouse的鼠标设备,此时已经实现了鼠标的功能。BLE工程代码并未做太多的深入,这个后续有时间在进行解读,蓝牙HID协议以USB HID协议规范为基础,新增功能可以在报告描述中添加,具体完整版本可以阅读USB HID规范