这里先不说触摸(Touch)按键的优缺点,只从GPIO实现Touch Key的方法上来讲下这个Demo。Touch Key的实现方式有多种,效果和稳定性也相差很大。有些是硬件上专门做了设计,且有不少是有专利的。记得ST有支持Touch Key的型号,也有对应的库,但没用过。进入正题-->
1、Touch Key 硬件原理:
Touch Key检测的原理网络上也有许多,方法各异,此文中的Touch Key 如下图:
实现原理是将Touch Pad 和 人体 等效为一个电容,Touch Pad的电容固定,和一个电阻并联到GND。IO输出High后,再转为Input,Touch Pad上的电荷通过电阻放电,只要Touch Pad的等效电容和电阻阻值不变,放电时间就是固定的。当有触摸时,人体等效电容相当于并联到了Touch Pad上,总的等效电容增大,放电时间增加,通过判断这个增加的“电容”大小,就能判断是否有触摸。如下图:红色的曲线为无触摸时的放电曲线,放电时间为t0,蓝色曲线为有触摸时的放电曲线,放电时间为t1,两者的差值为Δt。由Δt的大小可判断是否有触摸。VIL是IO读到低电平的阈值
下面是实际测量的电容放电波形:
可以看到,放电时间在10us左右(330K下拉电阻,有触摸时大概增加5us左右),这还是加了示波器探头的放电时间,如果不加探头,时间大概小一半,
下图是一个简易的Touch Pad。铜箔大小越1cm2,覆盖了透明胶带。下拉330K电阻,铜箔等效电容估计个位数pF。
以上是硬件方面和检测的原理。下面来看如何检测这个电容变化量?
2、Touch Key 软件原理:
2.1、电路原理知道了,电容放电的波形也看到了,如何检测变化量呢?用ADC?有无触摸的电压变化量在mV级别,时间为us级别,用多大速度和分辨率的ADC暂且不管,主要是有很多MCU并不具备ADC模块,如果外接满足这个要求的ADC还不如用一颗触摸芯片来的简单。测量电压行不通,那就测量时间吧。(以下的方法可以等效为一个简易的ADC)
上面有提到,有无触摸,放电波形会有5us的变化量。5us对于几十MHz的MCU来说能做太多事了。我这里采用的方法并不是采用中断,因为5us的变化量测量得需要上MHz的中断且获得的数据变化也不明显,所以我采用了do while(Loop)方式,IO输出High充电完成转为Input,通过电阻放电,直到IO读到低电平,在放电期间,放一个计数器,基本是以一条指令的时间来计数的,而且获得的数据变化量够大,便于后期计算,比如10MHz主频执行单周期指令计数,1us计数10次,5us计数50次,还可以连续做多次充放电,将计数累加,有无触摸就会有上百次的计数值变化,完全可以被检测出来。这是我的计数函数,在IO设置为Input后,查询IO是否为低,并且做计数,函数中连续做了16次累加,因为整个while循环中,执行while判断的时间是最长的,所以减少while判断可以计数更多的值,我的TouchKey是在PE0上,如果为High,计数值递增为1,如果换作其他IO时,在读到值之后做相应的移位,使每次递增为1即可,
/*----------------------------------------------------------------------------*/ uint16_t Get_Touch_Value(void) { //HAL_GPIO_WritePin(GPIOC,GPIO_PIN_10,GPIO_PIN_SET); Touch_Count = 0; GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); //while(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_0)) //2 while(GPIOE->IDR & GPIO_PIN_0) //1 { Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); Touch_Count +=(GPIOE->IDR & GPIO_PIN_0); } GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(GPIOE, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOE,GPIO_PIN_0,GPIO_PIN_SET); //HAL_GPIO_WritePin(GPIOC,GPIO_PIN_10,GPIO_PIN_RESET); return Touch_Count; }
2.2、因为手指本身有干扰,并且手指与Touch Pad的接触面积也不固定,导致增量时间时刻在变化,必须要经过滤波处理,如下图中的Touch按下部分,波形有杂波,在Touch隔离物较厚时,干扰问题会更严重且数据变化量较小,不经过滤波处理,误触发的几率会很大。
我做了两次滤波:
1、间隔100us进行一次充放电采集,连续采集12个点,将这12个点进行从大到小排序,之后去掉队首最大的4个值和队尾最小的4个值,取中间的4个值做平均,通过这样将一段时间内的数据累加平均为一个点,能有效去掉脉冲干扰。
/*----------------------------------------------------------------------------*/ uint16_t Touch_Heap(void) { uint8_t i,j; uint8_t DataTemp; uint32_t DataSum = 0; for(i=0; i<Heap_Num-1;i++) { for(j=0;j<Heap_Num-i;j++) { if(Data_Buffer[j]<Data_Buffer[j+1]) { DataTemp = Data_Buffer[j]; Data_Buffer[j] = Data_Buffer[j+1]; Data_Buffer[j+1] = DataTemp; } } } for(i=Dis_Num;i<Heap_Num-Dis_Num;i++) { DataSum += Data_Buffer[i]; } DataSum = DataSum/(Heap_Num-2*Dis_Num); return (uint16_t) DataSum; }
2、间隔10ms进行一次连续的采集,采集后的点再进行平滑滤波,我这里采用了一个一阶低通滤波器,通过调整滤波系数能调节波形的延时和滤波效果。(ST提供了各种滤波器函数,效率高效果好,有兴趣的同学可以深入研究一下)
/*----------------------------------------------------------------------------*/ /* LPF: Y(n) = α*X(n)+(1-α)*Y(n-1) Y[n]: = Y[n-1] + α * (X[n] - Y[n-1]) LPF_Yn[0:1] = Yn-1:Yn */ uint16_t IIR_LowPass_Filter(uint16_t LPF_Data) { float LPF_Temp; LPF_Temp = LPF_Pre_Data+ LPF_Alpha*(LPF_Data - LPF_Pre_Data); LPF_Pre_Data = LPF_Temp; return (uint16_t)LPF_Temp; }
经过两次滤波后,充放电数据波形改善很多。
3、Touch Key 状态判断:
这里是指判断按下和释放,通常的做法是设置一个阈值,高于多少或者低于多少就认为是按下或释放,有些程序会做到动态追踪最低点等等,对于元件一致性好的产品,误差都在可控范围内,这样做通常没有太大问题。但对于一些成本敏感,元器件误差较大的产品,问题就会比较明显,因为每一个产品的元件都有误差,这些误差的范围有可能已超过了你的阈值范围,无论你阈值设置过大或过小,总有一部分产品会有问题,即使是同一个产品,不同的环境也会导致阈值超出范围,比如Touch Pad受到灰尘、磨损等改变了自身的等效电容,电阻阻值随温度进行变化等等。
我这里采用数据变化的“趋势”来判断Touch 按下或释放,如图:
把数据点放入队列,依次计算前后两个点的差值,判断数值是增大还是减小,通过计数符合差值的点的次数,判断Touch按下还是释放,这也能有效排除干扰,最重要的是,这种方法受外界因素的影响比较小,元器件之间的差异被排除,因为这里只判断增量而非绝对值。
/*----------------------------------------------------------------------------*/ // 按下时:符合差值的计数增减,释放时,符合差值的计数减小。 void Check_Touch_State(void) { uint8_t i; uint8_t Touch_DebCnt = Data_Num; //数据点个数 int8_t Touch_D_Value = 0; for(i=0;i<Data_Num;i++) { Touch_Data_Buffer[i] = Touch_Data_Buffer[i+1]; //队列左移 } Touch_Data_Buffer[i] = IIR_LowPass_Filter(Touch_Heap()); //最新数据点补在队列尾部 if(Touch_POR_Deb > 200) //上电先做采集不判断按键状态,防止上电时数据为0,被误判为按下。 { for(i=0;i<Data_Num;i++) { Touch_D_Value = Touch_Data_Buffer[i] - Touch_Data_Buffer[i+1]; if(Touch_D_Value > Release_Sen_Value) { Touch_DebCnt--; } else if(Touch_D_Value < Press_Sen_Value ) { Touch_DebCnt++; } } if(Touch_DebCnt > (Data_Num+3)) //判断递增次数(Press) { Press_Flag = 0x01; HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_SET); } else if(Touch_DebCnt < (Data_Num-3)) //判断递减次数(Release) { Press_Flag = 0xF1; HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_RESET); } } else { Touch_POR_Deb++; } }
以上是Touch Key的实现过程,作为一个原理性的Demo,还有很多细节工作未作,如果采用这种方式到产品中,还需要仔细优化和调试,如防止放电超时,功耗等问题,以上Demo是用NucleoF429上完成的,原理较为简单,可以方便移植到其他MCU上完成,(事实上我先是用NucleoF031K6完成之后再移植到F429上的)。
以上Demo没有写关于CubeMX的部分,因为只需要一个或者两个中断,并不局限于具体的MCU。另外此文中的所有参数都是针对我的电路的,可以按照具体的电路和应用来调整参数。
另外提一点CubeMX的使用技巧,我之前用CubeMX生成Code之后,再用CubeMX修改配置后重新生成Code,就会把我自己写的Code给覆盖掉,经微信群(微信群?中文社区微信交流群 )中ecson_2006 和 liposlt 的提醒,CubeMX中用户的Code只需要放在USER CODE BEGIN和USER CODE END 之间,就不会被CubeMX再次覆盖,省了不少事。感谢两位!按照惯例,此文的PDF档和Source Code在附件中,欢迎留言探讨。
---------------------------------------------------------------------------分割线---------------------------------------------------------------------------
下面是我抽奖送开发板的事,这个帖子发的晚了几天,很抱歉。抽奖的原因是版主songbin 发起2016Computex 酷评集赞有奖竞赛 我之后发帖拉票,承诺活动结束后从给我点赞的人中间抽出一人送NucleoL053R8开发板,需要说明的是,这个开发板好久不用,之前也说过会拿出来送给社区的同学,刚好碰到这个活动。二是我并没有限制只能给我点赞。。截至活动结束,我共有37个赞并从先后顺序编序号0~36号。
因为只抽一人,所以选择一个固定的有记录方便查询的随机数作为基数来抽奖,找来找去发现 【双色球】比较符合预期,今天是2016/06/21星期三,明天周四是第72期的双色球开奖,我以红球作为基数,对36取余数,得到的结果即为中奖的同学。明天我会在这个帖子下面发布抽奖结果,有异议的同学可以留言。
songbin armiddu franco slotg 海king fengjunyang ecson_2006 momososo liposlt yanghanyu 。。。好吧,艾特人太慢了,页面老是奔溃,抽奖结果我再通知各位!
祝各位好运!