1.项目需求
- 利用板载的capsense电容滑条传感器,读取用户手指触摸位置或滑动方向,采集处理后传输到电脑上用于控制音量大小;
- 将开发板配置为USB HID设备,通过USB与电脑建立通信;
- 可以将手指触摸位置作为音量绝对大小,也可以根据手指滑动或手势,以相对数值增加或减小音量。
2.功能完成情况
2.1 wireshark端口抓包确认
图片左边为wireshark窗口通过usbmon捕捉usb通信,通过lsusb和人工确认表明开发板usb连接在了电脑usb 3.82.1口上,应用简单过滤规则即可看到开发板成功和电脑通过usb协议连接,并以bulk in模式传输数据(而非interrupt模式或output模式)。对比图片右边UART串口的调试信息,可以确认传输的数据无误。
2.2 音量增大减小控制
由于电脑调整声音手速不够没法截屏,这里仍旧通过wireshark确认。注意到为什么direction是2和3,在capsense支持库里对应常量CY_CAPSENSE_GESTURE_DIRECTION_LEFT和CY_CAPSENSE_GESTURE_DIRECTION_RIGHT,所以我一直都用错朝向了,这个坑我甚至去翻了支持库的汇编代码寻找问题,最后发现原来竟如此简单 。另一个坑是,音量增加和减小在电脑端是一个状态位,如果不取消这个状态位,音量会一直增加/减小不停。
2.3 音量绝对值控制
HID中有consumer control + usage volume代表音量,在手册中也有提到,可惜电脑端并不识别。因此此项功能虽正确传输,但未能最终实现。或许需要单独开发电脑端驱动支持或依赖相对音量。
2.4 按键切换滑条朝向映射
按板子上的user按键可切换滑条方向/位置对应的音量,如按在下边沿为音量0,在按下按键之后就变成了音量100,或者按下后下划从音量减小变成了映射到音量增加。这样设计是为了方便capsense使用和调试。
3. 实现过程
3.1 系统流程
我们首先从整体上理解,系统流程图如上图所示,其中按键中断可以反转音量与触摸条结果的映射方向。相对值模式与绝对值模式通过宏来定义,是编译期选项。
3.2 端口与时钟设置
pin配置可参考 https://bbs.21ic.com/icview-3341466-1-1.html , 在此基础上usb可参考modusttoolbox自带例程emUSB-Device_HID_Mouse 。这个开发板由于较新,需要更改pin脚,参考上图,注意打勾的会生成定义和启用配置代码,未打勾的只定义名字,所以不管打勾与否都要修改,当然部分不用改参考开发板电路图。usb对时钟和抖动有要求,因此如果不是使用emUSB_Device_HID_Mouse例程,还需要更改时钟设置如下图所示,注意Clock_HF3
还要需要注意的是,官方usb提供了两个支持库,usbdev和emusb-client,usbdev稍微比较底层。
usb report表配置
static const U8 usb_HIDReport[] = {
#ifdef MODE_RELATIVE
USB_HID_GLOBAL_USAGE_PAGE + 1, USB_HID_USAGE_PAGE_CONSUMER,
USB_HID_LOCAL_USAGE + 1, 0x01U,
USB_HID_MAIN_COLLECTION + 1, USB_HID_COLLECTION_APPLICATION,
USB_HID_LOCAL_USAGE + 1, 0xe9U, // Volume up
USB_HID_LOCAL_USAGE + 1, 0xeaU, // VOlume down
USB_HID_LOCAL_USAGE_MINIMUM + 1, 0,
USB_HID_LOCAL_USAGE_MAXIMUM + 1, 1,
USB_HID_GLOBAL_REPORT_SIZE + 1, 1,
USB_HID_GLOBAL_REPORT_COUNT + 1, 2,
USB_HID_MAIN_INPUT + 1, (USB_HID_VARIABLE | USB_HID_ABSOLUTE),
USB_HID_GLOBAL_REPORT_SIZE + 1, 6, // padding
USB_HID_MAIN_INPUT + 1, (USB_HID_CONSTANT | USB_HID_ABSOLUTE | USB_HID_ARRAY),
USB_HID_MAIN_ENDCOLLECTION
#else
USB_HID_GLOBAL_USAGE_PAGE + 1, USB_HID_USAGE_PAGE_CONSUMER,
USB_HID_LOCAL_USAGE + 1, 0x01U,
USB_HID_MAIN_COLLECTION + 1, USB_HID_COLLECTION_APPLICATION,
USB_HID_LOCAL_USAGE + 1, 0xE0U,
USB_HID_LOCAL_USAGE_MINIMUM + 1, 0,
USB_HID_LOCAL_USAGE_MAXIMUM + 1, 0xFFU,
USB_HID_GLOBAL_REPORT_SIZE + 1, 8,
USB_HID_GLOBAL_REPORT_COUNT + 1, 1,
USB_HID_MAIN_INPUT + 1, (USB_HID_VARIABLE | USB_HID_ABSOLUTE),
USB_HID_MAIN_ENDCOLLECTION
#endif
};
usb hid的report表定义了附带数据的格式如何被hid驱动解读。注意,虽然modusttoolbox提供了USB Configurator,但此工具近支持usbdev自动配置,不支持emusb,且编写report表时非常不好用,要手动输入数值而无列表选择。因此建议的方法是chatgpt(划去),比较方便的是使用Modusttoolbox 的Bluetooth^{\copyright} Configurator。usb官方和微软也提供了常用配置的工具,可惜的是不支持linux平台。
主要代码
void main_loop(void) {
led_data_t led_data = {LED_ON, LED_MAX_BRIGHTNESS};
update_led_state(&led_data);
/* Wait for configuration */
printf("Waiting for connection...\n");
while ((USBD_GetState() & USB_STAT_CONFIGURED ) != USB_STAT_CONFIGURED) {
cyhal_system_delay_ms(50);
}
printf("Connected!\n");
led_data.state = LED_OFF;
update_led_state(&led_data);
// uint8_t volume[2] = {0}; fuck emusb, first byte is report id according to doc, but why the hell device is refusing to send it
uint8_t val;
while (1){
button_state_t button = poll_button_and_release();
if (button == BUTTON_PRESSED) {
toggle_volume_inc_direction();
}
int have_result = poll_capsense();
if (have_result) {
capsense_result_t cap_result = get_capsense_result();
#ifdef MODE_RELATIVE
scroll_t s = maybe_swap_direction(cap_result.scrollx);
if (s == SCROLL_UP) {
val = 0x01;
printf("increment volume\n");
USBD_HID_Write(hid_instance_handle, &val, 1, 0);
// emmm, seems laptop treats this as setting a flag, so have to unset to avoid blowing my volume
val = 0x00;
USBD_HID_Write(hid_instance_handle, &val, 1, 0);
} else if (s == SCROLL_DOWN) {
val = 0x02;
printf("decrement volume\n");
USBD_HID_Write(hid_instance_handle, &val, 1, 0);
// emmm, seems laptop treats this as setting a flag, so have to unset to avoid blowing my volume
val = 0x00;
USBD_HID_Write(hid_instance_handle, &val, 1, 0);
} else {}
#else
val = SCALE_TO(cap_result.x, get_xresolution(), 1<<8);
val = smoother(val);
val = maybe_swap_direction_abs(val);
if (changed(val)) {
printf("set volume to 0x%02x\n", (int)val);
int check_write = USBD_HID_Write(hid_instance_handle, &val, 1, 0);
printf("written %d bytes\n", check_write);
}
#endif
cyhal_system_delay_ms(1);
}
}
}
主循环逻辑非常清晰,使用轮训的方式poll并更新按键和capsense状态,poll_capsense会阻塞等待上一次触摸条扫描完成,再非阻塞地开启下一次触摸条。have_result设计不太好,当时想让capsense也和按键一样单独运行并更新结果,主循环只需要获取结果,但实现时还是做成了阻塞模式的。
注意MODE_RELATIVE模式下,先发送滑动方向,再立刻发送清空滑动。
void process_gesture(void){
uint32_t gestureStatus = Cy_CapSense_DecodeWidgetGestures(CY_CAPSENSE_LINEARSLIDER0_WDGT_ID, &cy_capsense_context);
//printf("Detected gesture val %d\n", gestureStatus);
if (CY_CAPSENSE_GESTURE_NO_GESTURE != (gestureStatus & CY_CAPSENSE_GESTURE_ONE_FNGR_SCROLL_MASK))
{
/* Get gesture direction */
uint32_t direction = (gestureStatus >> CY_CAPSENSE_GESTURE_DIRECTION_OFFSET) & CY_CAPSENSE_GESTURE_DIRECTION_MASK_ONE_SCROLL;
printf("direction: %d\n", direction);
switch (direction >> CY_CAPSENSE_GESTURE_DIRECTION_OFFSET_ONE_SCROLL)
{
case CY_CAPSENSE_GESTURE_DIRECTION_UP:
_capsense_result.scrollx = SCROLL_UP;
/* UP is detected */
break;
case CY_CAPSENSE_GESTURE_DIRECTION_DOWN:
_capsense_result.scrollx = SCROLL_DOWN;
/* DOWN is detected */
break;
case CY_CAPSENSE_GESTURE_DIRECTION_LEFT:
// weird, swipe up is left
_capsense_result.scrollx = SCROLL_UP;
break;
case CY_CAPSENSE_GESTURE_DIRECTION_RIGHT:
_capsense_result.scrollx = SCROLL_DOWN;
break;
default:
_capsense_result.scrollx = NO_SCROLL;
break;
}
}
else {
_capsense_result.scrollx = NO_SCROLL;
}
}
触摸条处理代码中,滑动方向的手势处理的代码片段如上。调用了cypress capsense的手势识别API,需要在CAPSENSE^{TM} Configurator的高级选项中启用手势,这样就可以在board init中自动生成对应的初始化代码了。此外,手势识别的API需要每隔一段时间通过Cy_CapSense_IncrementGestureTimestamp更新时间戳,具体使用参考API文档中有例程参考。
4. 思考与可改进的方向
首先,如上面提到相对值模式与绝对值模式是编译期选项,如果实现运行时可以调整会更好。其实,我在这方面投入了很多时间尝试。这有三种方式,最简单的是重新配置usb协议栈,第二是设置两个service/endpoint,第三种方式是在一个endpoint下加入多个report ID。笔者仅对第三种进行了实验。在emUSB的文档中提到如果有多个report ID需要在传输的第一个byte中包含report ID,可笔者实测没有走通,可以在附带的代码中找到注释掉的部分,协议栈拒绝发送该数据,造成阻塞式的writeUSB进入无限循环卡住。 感叹对于这些闭源代码,如果文档和示例不详细,实在很头疼。
其次,细心的读者可能很容易主要到在wireshark抓包中,usb的表头很长,而实际发送的数据非常的短只有1 byte。虽然在音量调整的应用下,这无关紧要,但如果设备希望发送很多短消息,这是很大的浪费。usb协议或其他协议是否有选项或机制可以降低这方面的浪费,是之后要进一步学习研究的地方。