STM32电容触摸按键检测
- 电容触摸按键
- 简介
- 检测原理
- CubeMX配置
- 代码展示&讲解
- TPAD.c
- TPAD.h
本期内容我们将学习电容触摸按键的检测原理。以及代码实现思路
电容触摸按键
简介
- 电容触摸按键依赖的是电容的充放电
- 相对于机械按键更加耐用,不容易受外界环境干扰
- 在我们的开发板(正点原子的STM32F411RCT6 NANO板)上长这样:
检测原理
前面将电容触摸按键依赖的是电容的充放电,然而我们看开发板上的元件摆放图,发现没有电容在TPAD位置:
这个要从PCB的层叠结构说起;
- 我们的NANo板使用的是双层PCB结构,大概结构就是有两个铜箔中间夹着一个基材,
- 铜箔构成PCB的顶层和底层,负责PCB的走线,完成信号的传递,
- 铜箔外侧有丝印,阻焊等等,同时也负责元器件的摆放。
- 铜箔内部有基材,通常以RF-4为多,这个部分占PCB的大部分厚度,为此PCB的基本外观的硬度
- 具体结构大体如下(这个手画的有点抽象):
有了这个概念,我们就可以进行理论推导了:
首先,电容的充电公式如下:Vc=E (1-e (-t/R*C))
其中:
- Vc为电容两端的电压,其随时间变化
- E为电容充电的最大值,即充满电的的电压
- e为自然对数的底数
- t为时间
- R为电容充电回路的电阻
- C为电容的容值
通过以上公式,我们可以得到曲线
再看原理图:
- 通过通过电阻R32和前面板材PCB铜箔之间的电容1(这里叫Cs),构成充放电回路,STM_ADC充当开关,控制PB1的充放电
- 当手指触摸板材时,铜箔与手指之间的电容2(这里叫Cx)加入充电回路,电容增加,充电速度变慢,换句话说,电容达到相同电压的时间变成,利用这一区别,我们就能实现电容触摸按键的检测
- 图中Cth表示单片机GPIO识别为高电平的最小值。当Vc小于Vth时,识别为低电平,反之识别为高电平。越过Vth的过程可以被单片机识别为上升沿。可通过定时器的输入捕获功能捕捉
具体配置过程如下:
- TPAD引脚设置为推挽输出,输出低电平,使电容放电
- TPAD引脚设置为浮空输入,电容开始充电
- 开启TPAD输入捕获功能,捕获上升沿(即电压达到Vth的时刻)
- 等待充电过程,直至捕获成功
- 计算充电时间。
CubeMX配置
由于电容触摸按键检测过程中TPAD引脚模式会频繁切换,所以我们再CubeMX中不进行配置,直接从代码中自行编写配置过程。
代码展示&讲解
代码源码来自正点原子,经稍加修改便于移植和理解
TPAD.c
#include "tpad.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK NANO STM32F4开发板
//TPAD驱动代码
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//创建日期:2019/4/23
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2019-2029
//All rights reserved
//
TIM_HandleTypeDef TIM3_Handler; //定时器3句柄
#define TPAD_ARR_MAX_VAL 0XFFFF //最大的ARR值(TIM3是16位定时器)
vu16 tpad_default_val=0; 没有手指按下获取的充电时间(这里指Vc从0到Vth的充电时间)
//触摸按键初始化函数,获得没有手指按下时的充电时间平均值
//psc:Timer3时钟(100MHz)分频系数,越小,计数器加1时间间隔越短,灵敏度越高.
//返回值:0,初始化成功;1,初始化失败
uint8_t TPAD_Init(uint8_t psc)
{
uint16_t buf[10]; //存放十次没有手指按下时的充电时间
uint16_t temp; //临时变量
uint8_t j,i; //计数变量
TIM3_CH4_Cap_Init(TPAD_ARR_MAX_VAL,psc-1);//设置时钟分频系数
for(i=0;i<10;i++)//连续读取10次没有触摸时的充电时间
{
buf[i]=TPAD_Get_Val(); //将读取的充电时间存入数组
// HAL_Delay(10); //延时,实测好像没有什么用,不过源码中有
}
//对读取的时间进行排序,去除两个最大值和两个最小值,算平均数
for(i=0;i<9;i++)//排序
{
for(j=i+1;j<10;j++)
{
if(buf[i]>buf[j])//升序排列
{
temp=buf[i];
buf[i]=buf[j];
buf[j]=temp;
}
}
}
temp=0;
for(i=2;i<8;i++)temp+=buf[i];//取中间的6个数据进行平均
tpad_default_val=temp/6;
printf("tpad_default_val:%d\r\n",tpad_default_val); //获取到十次手指未按下时读取时间的平均值
if(tpad_default_val>(vu16)TPAD_ARR_MAX_VAL/2)return 1;//初始化遇到超过TPAD_ARR_MAX_VAL/2的数值,不正常!
return 0;
}
//复位一次
//释放电容电量,并清除定时器的计数值
void TPAD_Reset(void)
{
GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin=GPIO_PIN_1; //PB1
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLDOWN; //下拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_RESET); //PB1输出0,放电
HAL_Delay(5); //等待放电完成
__HAL_TIM_CLEAR_FLAG(&TIM3_Handler,TIM_FLAG_CC4|TIM_FLAG_UPDATE); //清除标志位
__HAL_TIM_SET_COUNTER(&TIM3_Handler,0); //计数器值归0
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //推挽复用
GPIO_Initure.Pull=GPIO_NOPULL; //不带上下拉
GPIO_Initure.Alternate=GPIO_AF2_TIM3; //PB1复用为TIM3通道4
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
}
//得到定时器捕获值
//如果超时,则直接返回定时器的计数值.
//返回值:捕获值/计数值(超时的情况下返回)
uint16_t TPAD_Get_Val(void)
{
TPAD_Reset(); //TPAD引脚放电,设置为定时器输入捕获上升沿触发模式
while(__HAL_TIM_GET_FLAG(&TIM3_Handler,TIM_FLAG_CC4)==RESET) //等待TPAD引脚捕获到上升沿
{
if(__HAL_TIM_GET_COUNTER(&TIM3_Handler)>TPAD_ARR_MAX_VAL-500) //一直未检测到上升沿,且时间过大时返回cnt
{ //此时过大的cnt返回,会被当成最大值去除掉
return __HAL_TIM_GET_COUNTER(&TIM3_Handler);//超时了,直接返回CNT的值
}
};
return HAL_TIM_ReadCapturedValue(&TIM3_Handler,TIM_CHANNEL_4); //捕获到上升沿,返回正常充电时间
}
//读取n次,取最大值
//n:连续获取的次数
//返回值:n次读数里面读到的最大读数值
uint16_t TPAD_Get_MaxVal(uint8_t n)
{
uint16_t temp=0;
uint16_t res=0; //最大值结果
uint8_t lcntnum=n*2/3;//至少2/3*n的有效个触摸,才算有效
uint8_t okcnt=0;
while(n--)
{
temp=TPAD_Get_Val();//得到一次值
if(temp>(tpad_default_val*5/4))okcnt++;//至少大于默认值的5/4才算有效
if(temp>res)res=temp;
}
if(okcnt>=lcntnum)return res;//至少2/3的概率,要大于默认值的5/4才算有效
else return 0;
}
//扫描触摸按键
//mode:0,不支持连续触发(按下一次必须松开才能按下一次);1,支持连续触发(可以一直按下)
//返回值:0,没有按下;1,有按下;
#define TPAD_GATE_VAL 30 //触摸的门限值,也就是必须大于tpad_default_val+TPAD_GATE_VAL,才认为是有效触摸.
uint8_t TPAD_Scan(uint8_t mode)
{
static uint8_t keyen=0; //0,可以开始检测;>0,还不能开始检测
uint8_t res=0;
uint8_t sample=3; //默认采样次数为3次
uint16_t rval;
if(mode)
{
sample=6; //支持连按的时候,设置采样次数为6次
keyen=0; //支持连按
}
rval=TPAD_Get_MaxVal(sample); //采集的最大值
if(rval>(tpad_default_val+TPAD_GATE_VAL))//大于tpad_default_val+TPAD_GATE_VAL,有效
{
if(keyen==0)res=1; //keyen==0,有效
//printf("r:%d\r\n",rval);
keyen=3; //至少要再过3次之后才能按键有效
}
if(keyen)keyen--;
return res;
}
//定时器3通道4输入捕获配置
//arr:自动重装值(TIM2是16位的!!)
//psc:时钟预分频数
void TIM3_CH4_Cap_Init(uint32_t arr,uint16_t psc)
{
TIM_IC_InitTypeDef TIM3_CH4Config;
TIM3_Handler.Instance=TIM3; //通用定时器3
TIM3_Handler.Init.Prescaler=psc; //分频系数
TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM3_Handler.Init.Period=arr; //自动装载值
TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//分频因子
HAL_TIM_IC_Init(&TIM3_Handler);
TIM3_CH4Config.ICPolarity=TIM_ICPOLARITY_RISING; //上升沿捕获
TIM3_CH4Config.ICSelection=TIM_ICSELECTION_DIRECTTI;//映射到TI1上
TIM3_CH4Config.ICPrescaler=TIM_ICPSC_DIV1; //配置输入分频,不分频
TIM3_CH4Config.ICFilter=0; //配置输入滤波器,不滤波
HAL_TIM_IC_ConfigChannel(&TIM3_Handler,&TIM3_CH4Config,TIM_CHANNEL_4);//配置TIM3通道4
HAL_TIM_IC_Start(&TIM3_Handler,TIM_CHANNEL_4); //开始捕获TIM3的通道4
}
//定时器3底层驱动,时钟使能,引脚配置
//此函数会被HAL_TIM_IC_Init()调用
//htim:定时器3句柄
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_TIM3_CLK_ENABLE(); //使能TIM3时钟
__HAL_RCC_GPIOB_CLK_ENABLE(); //开启GPIOB时钟
GPIO_Initure.Pin=GPIO_PIN_1; //PB1
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //推挽复用
GPIO_Initure.Pull=GPIO_NOPULL; //不带上下拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
GPIO_Initure.Alternate=GPIO_AF2_TIM3; //PB1复用为TIM3通道4
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
}
TPAD.h
#ifndef __TPAD_H
#define __TPAD_H
#include "main.h"
#include "usart.h"
//
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK NANO STM32F4开发板
//TPAD驱动代码
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//创建日期:2019/4/23
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2019-2029
//All rights reserved
//
//空载的时候(没有手按下),计数器需要的时间
//这个值应该在每次开机的时候被初始化一次
typedef __IO uint16_t vu16;
extern vu16 tpad_default_val;
void TPAD_Reset(void);
uint16_t TPAD_Get_Val(void);
uint16_t TPAD_Get_MaxVal(uint8_t n);
uint8_t TPAD_Init(uint8_t systick);
uint8_t TPAD_Scan(uint8_t mode);
void TIM3_CH4_Cap_Init(uint32_t arr,uint16_t psc);
#endif
不得不说,正点原子写的代码还是太妙了,所以我们在这里斗胆来窥探一下:
- void TPAD_Reset(void);
这里负责的是TPAD引脚的复位,完成TPAD引脚的放电,并设置为输入捕获功能,这是进行充电时间读取前的必备操作
- uint16_t TPAD_Get_Val(void);
这里读取的是电容充电时间,不论是否超时都会被读取
- uint16_t TPAD_Get_MaxVal(uint8_t n);
这里是读取有手指按下的最大充电时间,当没有手指按下时,返回0
这里的5/4只是个判断标准,确保是有手指按下的情况
2/3则是用来判误触,静电等情况
- uint8_t TPAD_Init(uint8_t systick);
//触摸按键初始化函数,获得没有手指按下时的充电时间平均值
//psc:Timer3时钟(100MHz)分频系数,越小,计数器加1时间间隔越短,灵敏度越高.
//返回值:0,初始化成功;1,初始化失败
uint8_t TPAD_Init(uint8_t psc)
{
uint16_t buf[10]; //存放十次没有手指按下时的充电时间
uint16_t temp; //临时变量
uint8_t j,i; //计数变量
TIM3_CH4_Cap_Init(TPAD_ARR_MAX_VAL,psc-1);//设置时钟分频系数
for(i=0;i<10;i++)//连续读取10次没有触摸时的充电时间
{
buf[i]=TPAD_Get_Val(); //将读取的充电时间存入数组
// HAL_Delay(10); //延时,实测好像没有什么用,不过源码中有
}
//对读取的时间进行排序,去除两个最大值和两个最小值,算平均数
for(i=0;i<9;i++)//排序
{
for(j=i+1;j<10;j++)
{
if(buf[i]>buf[j])//升序排列
{
temp=buf[i];
buf[i]=buf[j];
buf[j]=temp;
}
}
}
temp=0;
for(i=2;i<8;i++)temp+=buf[i];//取中间的6个数据进行平均
tpad_default_val=temp/6;
printf("tpad_default_val:%d\r\n",tpad_default_val); //获取到十次手指未按下时读取时间的平均值
if(tpad_default_val>(vu16)TPAD_ARR_MAX_VAL/2)return 1;//初始化遇到超过TPAD_ARR_MAX_VAL/2的数值,不正常!
return 0;
}
这里则是完成TPAD模块的初始化。读取10次手指未按下时的充电时间
排序去除最大值和最小值是为了减小未捕获上升沿,或过快捕获到上升沿导致的误差
- uint8_t TPAD_Scan(uint8_t mode);
- 扫描函数可以分为支持连续触发和单次触发
- 单次触发:第一次触发时会将res置1,后返回表示按键按下,后续计入函数,如果手指一直不拿开,会导致res无法置1(keyen一直等于3),返回0表示按键为按下
- 连续触发:无论那一次触发进入函数,keyen都会被置1,后返回1表示按键按下。
以上就是本期的全部内容,emmm