1.任务要求
诸如台式机和笔记本电脑,键盘上常常集成有音量调节或屏幕亮度调整的便捷控制键。想象一下,若我们采用一种独特的板卡,它有着一个滑动控制器,当轻轻用手指在滑动控制器上移动时,无论是电脑的音量大小、屏幕亮度,还是视频播放的进度条,都能随着指尖滑动而流畅变化。这样的设计不仅让操作更加直观和便捷,同时也为日常的数字生活增添了一份优雅与乐趣。
2.项目功能
为实现上述想法,RT-Thread 联合英飞凌(Infineon)推出了一款集成32位双核CPU( ARM Cortex-M4 和 ARM Cortex-M0)子系统的开发板,它自带了五段式的电容触摸滑条,可以帮助我们将各种创意想法变为现实。而英飞凌开发的CYW43012模块为使用蓝牙控制设备音量提供了可能。在英飞凌为自家产品设计的开发工具ModusToolBox的帮助下,我成功使用RT-Thread Studio实现了控制蓝牙设备音量的功能,并且拥有相对值模式和绝对值模式两种模式,在app_bt_gatt_handler.h文件中更改不同的宏定义即可切换。
3.设计思路
3.1寻找例程
ModusToolBox中为每一块开发板提供了丰富的例程供开发者参考,但是很可惜,没有与BLE HID(Bluetooth Low Energy Human Interface Device)有关的例程。此外,这里的蓝牙例程们还有一个问题,它们都需要英飞凌推出的蓝牙调试工具连接,如果下载到开发板上不能用手机直接进行连接,不便于调试,所以我选择了更改开发平台,在同样支持这块开发板的RT-Thread Studio上开发。
这里只有一个蓝牙例程,并且不是HID有关的例程。于是我又在英飞凌的论坛浏览了三天三夜【汗】,终于找到了一个英飞凌另一块开发板的蓝牙键盘例程。
3.2移植代码
现在的情况比做USB项目时更加棘手,因为蓝牙键盘的例程没法直接配置好下载到板子上,所以代码的移植变得十分困难,我找出RTT例程和ModusToolBox例程的相似之处,然后修改一些差异,终于让手机识别开发板为HID设备了,又经理诸多艰难险阻之后(且听后文细细道来),实现了音量的调节。
4.实现过程
4.1程序流程图
4.2代码解析
4.2.1 重要参数——HID报告描述符
const app_hids_report_map[] = {
0x05, 0x0c, // USAGE_PAGE (Consumer Devices)
0x09, 0x01, // USAGE (Consumer Control)
0xa1, 0x01, // COLLECTION (Application)
0x79, 0x00, // STRING_MINIMUM (0)
0x89, 0x01, // STRING_MAXIMUM (1)
0x09, 0xe9, // USAGE (Volume Up)
0x09, 0xea, // USAGE (Volume Down)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x02, // REPORT_COUNT (2)
0x81, 0x02, // INPUT (Data,Var,Abs)
0x95, 0x06, // REPORT_COUNT (6)
0x81, 0x01, // INPUT (Cnst,Ary,Abs)
0xc0 // END_COLLECTION
};
注意:
- 定义了一对按钮——音量+和音量-,在改变音量时必须先按下后松开,否则音量会直接增加/减小至最大值/最小值
- 在定义了两个按钮之后(占两位)后,还需要添加6位的常量,使报告描述符凑够八位,即一字节,否则在设备连接之后设备会警告HID设备报告中字节未对齐
4.2.2主函数
int main(void)
{
rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT);
Slider_ctrl_sample();//启动滑条
for(;;)
{
if(app_hids_vol_clear_report_client_char_config[0])//连接成功,常量
{
rt_pin_write(LED_PIN, PIN_LOW);
rt_thread_mdelay(500);
}
else if(Inited)//蓝牙初始化成功,未连接,快闪
{
rt_pin_write(LED_PIN, PIN_HIGH);
rt_thread_mdelay(500);
rt_pin_write(LED_PIN, PIN_LOW);
rt_thread_mdelay(500);
}
else//蓝牙初始化未成功,慢闪
{
rt_pin_write(LED_PIN, PIN_HIGH);
rt_thread_mdelay(500);
rt_thread_mdelay(500);
rt_thread_mdelay(500);
rt_pin_write(LED_PIN, PIN_LOW);
rt_thread_mdelay(500);
rt_thread_mdelay(500);
rt_thread_mdelay(500);
}
}
}
主函数主要负责激活滑块有关的线程,并且根据蓝牙初始化、连接状态进行不同的闪灯。下载完成或者上电之后按一下RESET按键开始蓝牙初始化,此时灯慢闪;初始化完成后手机才能检测到蓝牙设备,此时可以连接,灯快闪;连接成功后灯会保持常亮。
4.3.3滑块控制
static void process_touch(void)
{
cy_stc_capsense_touch_t *slider_touch_info;
uint16_t slider_pos;
uint8_t slider_touch_status;
bool led_update_req = false;
static uint16_t slider_pos_prev;
static led_data_t led_data = {LED_ON, LED_MAX_BRIGHTNESS};
/* Get slider status */
slider_touch_info = Cy_CapSense_GetTouchInfo(
CY_CAPSENSE_LINEARSLIDER0_WDGT_ID, &cy_capsense_context);
slider_touch_status = slider_touch_info->numPosition;
slider_pos = slider_touch_info->ptrPosition->x;
/* Detect the new touch on slider */
if ((RT_NULL != slider_touch_status) &&
(slider_pos != slider_pos_prev))
{
#ifdef Relative
led_data.brightness = (slider_pos * 100)
/ cy_capsense_context.ptrWdConfig[CY_CAPSENSE_LINEARSLIDER0_WDGT_ID].xResolution;
led_update_req = true;
app_bt_send_message(slider_pos < slider_pos_prev);
#endif
#ifdef Absolute
float volume = ((float)(100-((slider_pos * 100)
/ cy_capsense_context.ptrWdConfig[CY_CAPSENSE_LINEARSLIDER0_WDGT_ID].xResolution))) / 6.6666;
led_data.brightness = (slider_pos * 100)
/ cy_capsense_context.ptrWdConfig[CY_CAPSENSE_LINEARSLIDER0_WDGT_ID].xResolution;
led_update_req = true;
app_bt_send_message(volume);
#endif
}
#ifndef RT_USING_PWM
#error You need enable PWM to use this sample
#else
/* Update the LED state if requested */
if (led_update_req)
{
update_led_state(&led_data);
}
#endif
slider_pos_prev = slider_pos;
}
根据模式的不同(相对值模式/绝对值模式)给app_bt_send_message函数传递的值也不同,相对值模式传递的是一个布尔值,也就是本次是上滑还是下滑,从而进行相应的音量加减。而绝对值模式则相对复杂,需要根据触摸的位置计算目标音量,并把目标音量传递过去。
4.2.4发送蓝牙信息
/**
* Function Name: app_bt_send_message
*
* Function Description:
* @brief Check if client has registered for notification/indication and send
* message if appropriate
*
* @param None
*
* @return None
*
*/
#ifdef Relative
void app_bt_send_message(bool i)
#endif
#ifdef Absolute
void app_bt_send_message(float volume)
#endif
{
wiced_bt_gatt_status_t status;
printf("hello_sensor_send_message: CCCD:%d\n", app_hids_vol_clear_report_client_char_config[0]);
/* If client has not registered for indication or notification, no action */
if(0 == app_hids_vol_clear_report_client_char_config[0])
{
return;
}
else if(app_hids_vol_clear_report_client_char_config[0] & GATT_CLIENT_CONFIG_NOTIFICATION)
{
#ifdef Relative
if(i)
{
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_UP_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_CLEAR_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
printf("Notification Status: %d \n volume up \n", status);
}
else
{
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_DOWN_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_CLEAR_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
printf("Notification Status: %d \n volume down \n", status);
}
#endif
#ifdef Absolute
static uint8_t volume_prev = 0;
static int f = 1;
if(f)
{
for(int i=0;i<20;i++)
{
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_DOWN_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_CLEAR_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
printf("Notification Status: %d \n volume down \n", status);
f = 0;
}
}
while(volume_prev < volume)
{
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_UP_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_CLEAR_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
printf("Notification Status: %d \n volume up \n", status);
volume_prev++;
}
while(volume_prev > volume)
{
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_DOWN_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
status = wiced_bt_gatt_server_send_notification(hello_sensor_state.conn_id,
HDLC_HIDS_VOL_CLEAR_REPORT_VALUE ,
app_hids_vol_clear_report_len,
app_hids_vol_clear_report,
NULL);
printf("Notification Status: %d \n volume down \n", status);
volume_prev--;
}
#endif
}
}
/**
* This API will send a long (1 upto (MTU -3) bytes) notification to the client for the
* specified handle with a persistent buffer in \p p_app_buffer.
* The send complete of the notification is indicated by an GATT_ATTRIBUTE_REQUEST_EVT
* with wiced_bt_gatt_attribute_request_t.opcode = GATT_HANDLE_VALUE_NOTIF
*
* @param[in] conn_id : connection identifier.
* @param[in] attr_handle : Attribute handle of this handle value notification.
* @param[in] val_len : Length of notification value passed.
* @param[in] p_app_buffer : Notification Value, peristent till the data is sent out to the controller
* @param[in] p_app_ctxt : Application context for \p p_app_buffer
*
* @note The application may free/release/deallocate the \p p_app_buffer pointer
* on receiving a \ref GATT_APP_BUFFER_TRANSMITTED_EVT with
* \ref wiced_bt_gatt_buffer_transmitted_t.p_app_data = \p p_app_buffer and
* \ref wiced_bt_gatt_buffer_transmitted_t.p_app_ctxt = \p p_app_ctxt
*
* @return @link wiced_bt_gatt_status_e wiced_bt_gatt_status_t @endlink
* @ingroup gattsr_api_functions
*/
wiced_bt_gatt_status_t wiced_bt_gatt_server_send_notification(uint16_t conn_id, uint16_t attr_handle,
uint16_t val_len, uint8_t *p_app_buffer, wiced_bt_gatt_app_context_t p_app_ctxt);
这是本项目中最重要的函数,同时我也将讲解相对值模式和绝对值模式的区别。
可以看到,这个函数中多次调用了wiced_bt_gatt_server_send_notification函数,这是发送蓝牙GATT(Generic Attribute Profile)信息的函数。在发布这个项目的同时,我还发送了通过USB调节电脑音量的项目,在那个项目中,我通过定义一个botton变量来模拟按压HID音量调节按钮的操作,botton为1则按下第一个按钮,为2则按下第二个按钮,为0则松开按钮。原本我因为蓝牙中也可以按照这种方法操作,可惜并不行。屡屡碰壁之后我发现HID按钮的按下与松开与发送信息的内容无关,而是与发送信息的句柄(handle)有关,即wiced_bt_gatt_server_send_notification函数的第二个变量,句柄的值就相当与USB项目中botton的值。可惜句柄的值无法修改(我也不清楚为什么),所以我定义了三个句柄,他们分别是:
- HDLC_HIDS_VOL_CLEAR_REPORT_VALUE 0x00
- HDLC_HIDS_VOL_UP_REPORT_VALUE 0x01
- HDLC_HIDS_VOL_DOWN_REPORT_VALUE 0x02
所谓相对值模式,就是如果本次触摸位置的值小于上一次的,即往下滑动,那么就会按下第一个按钮——音量+并松开,反之则会按下第二个按钮——音量-,从而实现音量的调节。但是,这种音量的调节只能相对于现在的音量进行调节,而且调节的多少与滑动的距离没有直接关系,因为检测位置的间隔是固定的,所以调整的多少与滑动时间成正比。也就是说如果滑动的很快,那么既是从一端滑到另一端也不会改变很多音量,反之,如果滑动较慢,则可以在滑动相同距离的前提下调整更大幅度的音量。
绝对值模式的整体流程与相对值模式相似,只是把滑动的位置转化为了目标音量大小,如果目前音量始终没有到达目标音量则会持续调整,直到与目标音量相等。这样的效果就是滑条好似成为了一个音量条,你的手指滑到上方四分之一处音量就会调到75%,可谓是量随指动,同时还支持点按,轻触滑条一个位置就可以调整到那个音量。这一切看起来比相对值模式好的多,但是为什么还要设置两个模式呢?自然是因为本模式有如下问题:
- HID设备无法读取电脑/手机目前音量,所以连接后必须将音量清零,从而保证手机音量与程序中的变量的值相等
- 绝对值模式由于每次检测和音量调整之间有间隔,可能会出现手机音量与程序中存储的目前音量产生差值,出现明明滑到最上方了,本应调到100%却只有85%,其实程序里的音量已经是100了
- 绝对值模式效果与设备的系统也有关系,比如WIN11中按一次键盘上的音量+/-调整2%的音量,但是手机上是一次调整6.66%,所以程序中储存的音量的值(主要是一个除数,即用100除以几,WIN11是除以2,手机则是除以6.66)需要做出相应的调整。
- 绝对值模式调整有一些波动,比如我滑到75%,现实是可能增大音量过度达到88%又下降到75%,带来使用的不适。
总上所述,理论上绝对值模式是更符合使用逻辑的,但是实现起来困难比较大,而且效果不尽如人意,所以我做了两种模式,可供选择与调试,也算是提供一种优化方向吧。
5.遇到的主要难题
5.1蓝牙例程的寻找
本来我想和USB项目一样,使用ModusToolBox开发,但是我尝试了所有蓝牙例程,没错,每一个蓝牙例程我都尝试了,它们都需要英飞凌的蓝牙调试APP。这个APP只能在Google商城下载,而我的手机是华为手机,不能使用Google的框架,所以我花钱在淘宝上找人代下了APP,但是这个APP又不方便调试,所以在浪费了好几天时间后我放弃了ModusToolBox上的蓝牙例程。
解决方法:转战RT-Thread Studio,使用那上面的蓝牙例程,同时在英飞凌的社区寻找其他开发板的蓝牙HID例程
5.2HID按键的按下
同样的,我按着USB项目的思路,用wiced_bt_gatt_server_send_notification传递botton变量企图更改HID按键的状态,可是刚开始一点反应都没有,我隐约觉得是句柄的问题,在我更换了好几个句柄之后终于有反应了,可是我先发一个1再发一个0,按理说是增大一次音量,但音量直接到顶了,这个问题困扰了我好久。
解决方法:在走投无路的情况下尝试再次改变句柄,发现了句柄的秘密(详见上文)。
5.3库文件中的Bug
在遇到第一个难题——寻找例程无果后,我在wiced_include文件夹中发现了HID初始化和信息发送的函数,但是我只能找到.h头文件,找不到对应的源文件,在软件中也找不到函数的定义。按理说这样是没法调用的,但是我发现其他函数居然也出现了只有声明没有定义的情况而且还能正常调用(比如wiced_bt_gatt_server_send_notification函数),我就尝试自己调用函数初始化HID,可是那几个与HID有关的函数居然不能调用,编译时会报错函数未定义。这个问题直接堵死了自己写HID有关内容的道路,让我被迫去找蓝牙HID的例程。
期间,为了解决这个问题,我问了许多嵌入式方面的学长和前辈,他们都无法解决。于是我前往英飞凌的社区发布了帖子询问这件事(我发布的帖子),同时也开始在论坛中搜索BLE HID相关内容。突然间,我发现了这样一篇帖子(这篇帖子),原来在2023年4月就有人遇到了和我相同的问题(甚至是尝试调用同一个函数),而工程师回复他说是一个Bug,但它至今仍未被修复,希望英飞凌的工程师能够修复这个Bug。
6.未来的计划建议
由于不知名的原因,我的程序不能实现让已经配对过的设备重新连接,也就是说每次配对、连接结束后若要下次成功连接,必须在手机上取消配对,而不是像一般的蓝牙耳机一样下次可以直接连接,必须重新配对,否则将连接失败。未来我将尝试修复这个问题。
同时我的绝对值模式仍存在波动,偏差等诸多问题,希望以后能够优化一下绝对值模式,毕竟这种模式比相对值模式更符合操作逻辑。同时,希望英飞凌的工程师们修复BLE HID库中函数无法调用的Bug,便于我们使用这一块开发板。
7.收获与感悟
在拿到这块板子之前我只接触过Arduino,看到官方的例程直接汗流浃背,看不懂一点。所以我在寒假期间简单学习了STM32的开发,因为它资源比较丰富,上手稍微简单一点。在这期间,我最主要的收获是学会如何查看函数的定义,以及通过函数参数的类型查找可以填写那个参数的能力。终于,我能看懂程序了,可是一移植代码又是一堆报错。于是,我又一点一点地摸索如何移植程序,如何修复一个个Error,到最后我已经能比较熟练地移植代码,在各个地方修改对应的内容,并且初步添加我需要的内容。同时,在完成了USB的项目后我满怀信心地着手尝试蓝牙的项目,认为就是换了个通信方式,但是蓝牙项目的完成过程中我遇到了前所未有的困难,各种方法都被否定。在完成项目前两天我连蓝牙都没连上(不使用英飞凌蓝牙APP的情况下),而我已经研究一个星期了。而当终于发现了正确的方向后,完成任务的喜悦更是令我心旷神怡,激动许久。
8.完整代码网盘链接
建议使用RT-Thread Studio打开项目,由于项目完整代码过大无法直接上传,仅上传小部分代码,完整代码的网盘链接如下:
链接:https://pan.baidu.com/s/1ALwpk3clvnOQBT2IhKiS3Q?pwd=57l8
提取码:57l8