Funpack4-1
——板卡一 自命题:桌面控制台
一、项目介绍
本项目依托于Funpack4-1活动,使用嵌入式板卡-上位机驱动配合的模式,实现了一个桌面控制台。基于CY8CKIT-149板载的滑条和按键等,实现了滑动和按键触摸的识别。上位机驱动实现了滑动去抖,并将按键触摸等行为模拟为一系列按键操作,包括系统音量加减,音乐上一首、下一首,音乐的开始和暂停,纵向滚轮,Windows键,回车键和删除键。
项目内容:
1、识别按键和滑动
2、实现上位机与板卡通信
3、滑条数据处理
4、模拟系统按键
应用场景
本项目是一个多功能桌面控制台。其模拟的按键可以极大程度补充按键,如84键以下的键盘往往缺失较多的功能按键。可以简化按键操作,如实现复制、粘贴等操作。也可以使得操作更舒适,如用非线性滑条代替滚轮,可以同时做到低速精准和高速移动快。
二、总体架构
系统的总体结构为板卡识别滑条和按键的数据,通过串口连接至上位机。上位机驱动接收串口数据,对数据进行处理,转义为系统操作,并与系统交互。
三、受控端软件
受控端使用ModusToolBox开发,基于案例CAPSENSE_SmartSense_buttons_slider开发。
系统上电后首先进行初始化,包含最基本的bsp(板级支持包)内所含的初始化,中断使能,初始化capsense,以及初始化串口。
result = cybsp_init();
/* Board initialization failed. Stop program execution */
if (result != CY_RSLT_SUCCESS)
{
CY_ASSERT(CY_ASSERT_FAILED);
}
/* Enable global interrupts */
__enable_irq();
/* Initialize EZI2C */
initialize_capsense_tuner();
/* Initialize CAPSENSE */
initialize_capsense();
Cy_SCB_UART_Init(CYBSP_UART_HW, &CYBSP_UART_config, &CYBSP_UART_context);
Cy_SCB_UART_Enable(CYBSP_UART_HW);
然后,初始化PWM,这部分是呼吸灯。
if (CY_TCPWM_SUCCESS != Cy_TCPWM_PWM_Init(CYBSP_PWM1_HW, CYBSP_PWM1_NUM, &CYBSP_PWM1_config))
{
CY_ASSERT(CY_ASSERT_FAILED);
}
if (CY_TCPWM_SUCCESS != Cy_TCPWM_PWM_Init(CYBSP_PWM2_HW, CYBSP_PWM2_NUM, &CYBSP_PWM2_config))
{
CY_ASSERT(CY_ASSERT_FAILED);
}
/* Enable the initialized PWM */
Cy_TCPWM_PWM_Enable(CYBSP_PWM1_HW, CYBSP_PWM1_NUM);
Cy_TCPWM_PWM_Enable(CYBSP_PWM2_HW, CYBSP_PWM2_NUM);
/* Then start the PWM */
Cy_TCPWM_TriggerStart(CYBSP_PWM1_HW, CYBSP_PWM1_MASK);
Cy_TCPWM_TriggerStart(CYBSP_PWM2_HW, CYBSP_PWM2_MASK);
smart_io_start();
最后,开始捕获触摸。至此,系统初始化部分完成,进入主循环。
Cy_CapSense_ScanAllWidgets(&cy_capsense_context);
主循环中刨除为了控制数据量的延时,就只有触摸的识别和处理。
Cy_SysLib_Delay(2);
if(CY_CAPSENSE_NOT_BUSY == Cy_CapSense_IsBusy(&cy_capsense_context))
{
/* Process all widgets */
Cy_CapSense_ProcessAllWidgets(&cy_capsense_context);
capsense_led();
/* Establishes synchronized communication with the CAPSENSE Tuner tool */
Cy_CapSense_RunTuner(&cy_capsense_context);
/* Start the next scan */
Cy_CapSense_ScanAllWidgets(&cy_capsense_context);
}
与上位机的交互以及按键和滑条对应LED的处理都在函数capsense_led()中,代码如下。
static void capsense_led(void)
{
/* LED ON/OFF based on the CAPSENSE linear slider position */
if(0 != Cy_CapSense_IsWidgetActive(CY_CAPSENSE_LINEARSLIDER0_WDGT_ID, &cy_capsense_context))
{
/* CAPSENSE Slider info */
cy_stc_capsense_touch_t *slider_touch_info;
uint8_t slider_touch_status;
uint16_t slider_pos = 0;
char st[20];
/* Get slider status */
slider_touch_info = Cy_CapSense_GetTouchInfo(
CY_CAPSENSE_LINEARSLIDER0_WDGT_ID, &cy_capsense_context);
slider_touch_status = slider_touch_info->numPosition;
if (slider_touch_status != 0)
{
slider_pos = slider_touch_info->ptrPosition->x;
sprintf(st,"nsv%03d",slider_pos,STEP_SIZE);
Cy_SCB_UART_PutString(CYBSP_UART_HW,st);
Cy_SCB_UART_PutString(CYBSP_UART_HW, "\r\n");
f_s=1;
}
if(slider_pos > 0 || slider_pos == 0)
{
Cy_GPIO_Write(CYBSP_LED_SLD0_PORT, CYBSP_LED_SLD0_NUM, LED_ON);
}
if(slider_pos > (1*STEP_SIZE))
{
Cy_GPIO_Write(CYBSP_LED_SLD1_PORT, CYBSP_LED_SLD1_NUM, LED_ON);
}
else
{
Cy_GPIO_Write(CYBSP_LED_SLD1_PORT, CYBSP_LED_SLD1_NUM, LED_OFF);
}
if(slider_pos > (2*STEP_SIZE))
{
Cy_GPIO_Write(CYBSP_LED_SLD2_PORT, CYBSP_LED_SLD2_NUM, LED_ON);
}
else
{
Cy_GPIO_Write(CYBSP_LED_SLD2_PORT, CYBSP_LED_SLD2_NUM, LED_OFF);
}
if(slider_pos > (3*STEP_SIZE))
{
Cy_GPIO_Write(CYBSP_LED_SLD3_PORT, CYBSP_LED_SLD3_NUM, LED_ON);
}
else
{
Cy_GPIO_Write(CYBSP_LED_SLD3_PORT, CYBSP_LED_SLD3_NUM, LED_OFF);
}
if(slider_pos > (4*STEP_SIZE))
{
Cy_GPIO_Write(CYBSP_LED_SLD4_PORT, CYBSP_LED_SLD4_NUM, LED_ON);
}
else
{
Cy_GPIO_Write(CYBSP_LED_SLD4_PORT, CYBSP_LED_SLD4_NUM, LED_OFF);
}
if(slider_pos > (5*STEP_SIZE))
{
Cy_GPIO_Write(CYBSP_LED_SLD5_PORT, CYBSP_LED_SLD5_NUM, LED_ON);
}
else
{
Cy_GPIO_Write(CYBSP_LED_SLD5_PORT, CYBSP_LED_SLD5_NUM, LED_OFF);
}
}
else
{
Cy_GPIO_Write(CYBSP_LED_SLD0_PORT, CYBSP_LED_SLD0_NUM, LED_OFF);
Cy_GPIO_Write(CYBSP_LED_SLD1_PORT, CYBSP_LED_SLD1_NUM, LED_OFF);
Cy_GPIO_Write(CYBSP_LED_SLD2_PORT, CYBSP_LED_SLD2_NUM, LED_OFF);
Cy_GPIO_Write(CYBSP_LED_SLD3_PORT, CYBSP_LED_SLD3_NUM, LED_OFF);
Cy_GPIO_Write(CYBSP_LED_SLD4_PORT, CYBSP_LED_SLD4_NUM, LED_OFF);
Cy_GPIO_Write(CYBSP_LED_SLD5_PORT, CYBSP_LED_SLD5_NUM, LED_OFF);
if(f_s==1)
{
Cy_SCB_UART_PutString(CYBSP_UART_HW, "nse\r\n");
}
}
/* LED ON/OFF based on the CAPSENSE buttons status */
if(0 != Cy_CapSense_IsSensorActive(CY_CAPSENSE_BUTTON0_WDGT_ID, CY_CAPSENSE_BUTTON0_SNS0_ID, &cy_capsense_context))
{
Cy_GPIO_Write(CYBSP_LED_BTN0_PORT, CYBSP_LED_BTN0_NUM, LED_ON);
Cy_SCB_UART_PutString(CYBSP_UART_HW, "np1b\r\n");
f_b1=1;
}
else
{
Cy_GPIO_Write(CYBSP_LED_BTN0_PORT, CYBSP_LED_BTN0_NUM, LED_OFF);
if(f_b1==1)
{
// f_b1=0;
Cy_SCB_UART_PutString(CYBSP_UART_HW, "np1e\r\n");
}
}
if(0 != Cy_CapSense_IsSensorActive(CY_CAPSENSE_BUTTON1_WDGT_ID, CY_CAPSENSE_BUTTON1_SNS0_ID, &cy_capsense_context))
{
Cy_GPIO_Write(CYBSP_LED_BTN1_PORT, CYBSP_LED_BTN1_NUM, LED_ON);
Cy_SCB_UART_PutString(CYBSP_UART_HW, "np2b\r\n");
f_b2=1;
}
else
{
Cy_GPIO_Write(CYBSP_LED_BTN1_PORT, CYBSP_LED_BTN1_NUM, LED_OFF);
if(f_b2==1)
{
// f_b2=0;
Cy_SCB_UART_PutString(CYBSP_UART_HW, "np2e\r\n");
}
}
if(0 != Cy_CapSense_IsSensorActive(CY_CAPSENSE_BUTTON2_WDGT_ID, CY_CAPSENSE_BUTTON2_SNS0_ID, &cy_capsense_context))
{
Cy_GPIO_Write(CYBSP_LED_BTN2_PORT, CYBSP_LED_BTN2_NUM, LED_ON);
Cy_SCB_UART_PutString(CYBSP_UART_HW, "np3b\r\n");
f_b3=1;
}
else
{
Cy_GPIO_Write(CYBSP_LED_BTN2_PORT, CYBSP_LED_BTN2_NUM, LED_OFF);
if(f_b3==1)
{
// f_b3=0;
Cy_SCB_UART_PutString(CYBSP_UART_HW, "np3e\r\n");
}
}
}
这里主要是对按键和滑条状态的判断,然后是控制LED的通断,最后是将状态发送至上位机。
上位机驱动和板卡间的通信协议如下,以n为起始位,p代表按键,后面1、2、3代表按键标号,b代表按下,e代表抬起;s代表滑条,v表示处于触摸中,后接以0填充的3为整数,表示触摸的位置。按键和滑条均以\r\n作为结束。
四、上位机软件
上位机软件基于QT实现,由于需要系统级别操作,本程序程序需要引用系统API,但是作者仅有Windows电脑,因此本项目仅在装有WIndows的电脑上有良好效果。上位机的串口通信与数据处理和按键模拟分开处理。串口通信基于串口数据可读的槽函数,按键模拟基于定时器超时的槽函数。
串口数据可读的槽函数如下。首先读取全部数值,然后循环每一位处理,如果不是\n就将本位存入buffer,如果是,就处理buffer。本段代码中的通信协议解析此处不做赘述。需要注意的是,对于滑条数据,由于手指离开触摸区域的瞬间会有数据突变,因而,要做限幅。同样,为了去抖,加入线性IIR滤波器,后接死区截断。
QObject::connect(serialport,&QSerialPort::readyRead,this,[&](){
QByteArray data = serialport->readAll();
foreach(char b , data)
{
if (b != '\n')
{
buffer.append(b);
}
else
{
QString str(buffer);
buffer.clear();
if(str[0]=="n")
{
if(str[1]=="p")
{
if(str[2]=="1")
{
if(str[3]=="b")
{
p1_s=1;
}
else if(str[3]=="e")
{
if(p1_s==1)
{
p1_ss=1;
p1_s=0;
}
}
}
else if(str[2]=="2")
{
if(str[3]=="b")
{
p2_s=1;
}
else if(str[3]=="e")
{
if(p2_s==1)
{
p2_ss=1;
p2_s=0;
}
}
}
else if(str[2]=="3")
{
if(str[3]=="b")
{
p3_s=1;
}
else if(str[3]=="e")
{
if(p3_s==1)
{
p3_ss=1;
p3_s=0;
}
}
}
}
else if(str[1]=="s")
{
if(str[2]=="v")
{
l_v=(str[3].digitValue())*100+(str[4].digitValue())*10+(str[5].digitValue());
if(l_s>1)
{
l_d=(l_v-l_vv)*0.8+l_d*0.2;
if(abs(l_d)>5)
l_d=0;
if(abs(l_d)<0.5)
l_d=0;
qDebug()<<l_d<<"\n";
}
else if (l_s==0)
l_s=1;
else if (l_s==1)
l_s=2;
l_vv=l_v;
}
else if(str[3]=="e")
{
l_s=0;
l_v=0;
l_vv=0;
l_d=0;
l_b=0;
}
}
}
}
}
});
定时器超时的槽函数如下,根据不同状态向系统输入不同的按键即可。
QObject::connect(tim1,&QTimer::timeout,this,[&](){
if(p1_ss)
{
switch(state)
{
case 1:
simulateKeyPressWin(VK_LWIN);
break;
case 0:
simulateKeyPressWin(VK_MEDIA_PLAY_PAUSE);
break;
}
p1_ss=0;
}
if(p2_ss)
{
switch(state)
{
case 1:
simulateKeyPressWin(VK_RETURN);
break;
case 0:
simulateKeyPressWin(VK_MEDIA_NEXT_TRACK);
break;
}
p2_ss=0;
}
if(p3_ss)
{
switch(state)
{
case 1:
simulateKeyPressWin(VK_DELETE);
break;
case 0:
simulateKeyPressWin(VK_MEDIA_PREV_TRACK);
break;
}
p3_ss=0;
}
if((l_s)&&(l_d!=0))
{
switch(state)
{
case 0:
if(l_d>0)
{
simulateKeyPressWin(VK_VOLUME_DOWN);
if(high)
simulateKeyPressWin(VK_VOLUME_DOWN);
}
else if(l_d<0){
simulateKeyPressWin(VK_VOLUME_UP);
if(high)
simulateKeyPressWin(VK_VOLUME_UP);
}
break;
case 1:
sroll(l_d);
break;
}
l_d=0;
}
});
五、最终效果
具体效果请参考视频,实物如图。
六、展望
本项目实现了一个多功能的桌面控制台,其可以控制系统音量加减,音乐上一首、下一首,音乐的开始和暂停,并能模拟纵向滚轮,Windows键,回车键和删除键等输入。但是,由于滑条存在的段落式定位问题,会使纵向滚轮存在段式响应的问题,降低操作手感。可以通过精确校准,来降低这一问题。同时,上位机函待完善以支持自定义按键,而非更改源码。