目录
1. 电容触摸按键简介
2. 触摸按键的原理
3. 硬件分析
4. 检测电容触摸按键的过程
5. 库函数配置触摸按键
6. 实验程序
6.1 main.c
6.2 tpad.c
6.3 tpad.h
在STM32F4的开发板的右下侧,有一小块的覆铜区域TPAD,此区域称为STM32的触摸按键;
触摸按键相对于传统的机械按键有着寿命长、占用空间少、易于操作等诸多优点。如今的手机,触摸屏,触摸按键大行其道,而传统的机械按键,正在逐渐从手机上面消失。在本节,我们将学习一种简单的触摸按键:电容式触摸按键。
触摸按键实质上是通过一个RC电路来实现的,RC电路如下图:
最上边的 V1 是电路电源电压,对于开发板来说就是3.3V供电电源电压;如图,电阻R是外接的电容充电电阻,图中CX为电容,当KEY未闭合时,电容充电电阻R的上端电压是0V,下端电压也是0V;充电电容CX的上端电压和电阻R下端的电压是一样的,都是0V;电容CX的下端接GND,所以电容下端的电压也是0V。
当闭合KEY的瞬间,电容充电电阻的上端电压就是电路供电电压,电阻R的下端电压还是0V;此时电阻R两端有压差,所以有电流流经R为电容CX充电;随着电子的累积,电压值也不断的增加,电阻R两端的电压变为V1和Vt,电容两端的电压都会变成Vt;此时,电阻R两端仍然存在电压差,会有源源不断的电子流经电容,电容两端的电压Vt不断增大,电容持续充电。
电容两端的电压值随时间的变化曲线如图所示,因为电阻R两端的电压的压差在不断的减少,因此充电的效率会相应的变低,所以该曲线的变化趋势越来越平缓。该曲线表明:电容两边的电压和时间成正比,时间越长,对应的充电电容的电压也就越大。
本节中,使用PA5的复用TIM2的1通道来检测TPAD是否被触摸;在每次检测之前,我们先配置PA5为推挽输出,对电容进行放电,然后配置PA5为浮空输入,利用外部上拉电阻给电容充电。同一时刻开启输入捕获,每当检测到上升沿,就认为电容充电完成了。
在MCU每次复位重启的时候,我们执行一次捕获检测,记录此时的值,记为 tpad_default_val,作为判断的依据。在后续的检测中,通过与 tpad_default_val 进行比较,来判断是不是有触摸发生。
①TPAD引脚设置为推挽输出,输出0,实现电容放电到 0。 首先通过推挽输出将电容的电放至0;
②TPAD引脚设置为浮空输入,电容开始充电。 通过浮空输入对电容进行充电
③开启TPAD引脚的输入捕获开始捕获。 使能TPAD对应芯片的引脚输入捕获计数器的值
④等待充电完成 检测上升沿
⑤计算充电时间
通过该框图的过程配置相关的库函数: 注意:纠正图中的TIM5_CH2_Cap_Init,本实验使用TIM2的1通道;
①void TPAD-Reset(void)函数:复位TPAD
设置IO口为推挽输出输出0,电容放电。等待放电完成之后,设置为浮空输入,从而开始充电。同时把计数器的CNT设置为0。
②TPAD_Get_Val()函数:获取一次捕获值(得到充电时间)
复位TPAD,等待捕获上升沿,捕获之后,得到定时器的值,计算充电时间。
③TPAD_Get_MaxVal()函数:
多次调用TPAD_Get_Val函数获取充电时间。获取最大的值。
④TPAD_Init()函数:初始化TPAD
在系统启动后,初始化输入捕获。先10次调用TPAD_Get_Val()函数获取10次充电时间,然后获取中间N(N等于8或者6)次的平均值,作为在没有电容触摸按键按下的时候的充电时间比较值tpad_default_val;
⑤TPAD_Scan()函数:扫描TPAD
调用TPAD_Get_MaxVal函数获取多次充电中最大的充电时间,跟tpad_default_val比较,如果大于某个值,则认为有触摸动作。
⑥void TIM2_CH1_Cap_Init(u16 arr,u16 psc)
输入捕获通道初始化;
#include "stm32f4xx.h"
#include "delay.h"
#include "LED.h"
#include "BEEP.h"
#include "Key.h"
#include "usart.h"
#include "exti.h"
#include "iwdg.h"
#include "wwdg.h"
#include "Timer.h"
#include "pwm.h"
#include "IntputCapture.h"
#include "tpad.h"
int main()
{
u8 t=0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置中断优先级分组
delay_init(168); //初始化延迟函数
uart_init(115200); //初始化串口 这里再次强调一下,如果没有初始化串口,是不能使用printf打印函数的
LED_Init(); //LED初始化
TPAD_Init(8); //初始化触摸按键 预分频值设置为8
while(1)
{
if(TPAD_Scan(0))//成功捕获一次上升沿
{
LED1=!LED1;
}
// if(TPAD_Scan(1)) //支持连按
// {
// LED0=!LED0;
// LED1=!LED1;
// delay_ms(10);
// }
t++;
if(t==15)
{
t=0;
LED0=!LED0;
}
delay_ms(10);
}
}
#include "stm32f4xx.h"
#include "delay.h"
#include "usart.h"
#include "tpad.h"
//PrioritySendCount:预分频值(预分频值越小,触摸的灵敏度越高) AutomaticReload:自动重装载值
#define TPAD_ARR_MAX_VAL 0xFFFFFFFF //TPAD最大自动重装载值 也就是计数器能达到的最大值
vu16 tpad_default_val=0;//没有手按下时,计数器需要的时间 vu16的意思是:typedef __IO uint16_t vu16;
//TPAD_Init初始化所要实现的事:
//1. TIM2_CH1_Cap_Init初始化输入捕获
//2. 10次调用TPAD_Get_Val;
//3. 取中间6次的平均值赋值给tpad_default_val
u8 TPAD_Init(u8 PrioritySendCount)
{
u16 buf[10],temp; //数组用来接收10次充电的时间;
u8 i,j;
TIM2_CH1_Cap_Init(TPAD_ARR_MAX_VAL,PrioritySendCount-1); //初始化输入捕获
for(i=0;i<10;i++) //连续捕获10次充电时间
{
buf[i]=TPAD_Get_Val();//TPAD_Get_Val用来获取充电时间
delay_ms(10);
}
for(i=0;i<9;i++) //冒泡排序
{//之所以说i=0;i<9是因为排序是建立在数组的基础之上的,需要从下角标0到下角标9开始排序
for(j=i+1;j<10;j++)
{//j=i+1;j<10是因为冒泡排序是当前元素和下一个元素进行比较,所以j永远要是i的下一个元素,当下一个元素比前一个元素大,就交换顺序
if(buf[i]>buf[j]) //以升序的方式进行排列
{
temp=buf[i];
buf[i]=buf[j];
buf[j]=temp;
}
}
}
temp=0;//temp是一个中间变量,此时将temp置为0,以免被temp刚刚离开for循环的值所影响
for(i=2;i<8;i++)//取中间的6个数据进行加和
{//存储在数组中的10个元素的下角标分别是0 1 2 3 4 5 6 7 8 9,所以i=2;i<8表示取出中间的6个元素
temp=temp+buf[i];
}//离开该for循环,temp的值表示中间6个值的和
tpad_default_val=temp/6;//取中间6个数的平均值赋值给tpad_default_val作为比较值
printf("tpad_default_val:%d\r\n",tpad_default_val);//在串口打印出tpad_default_val的值
if(tpad_default_val>TPAD_ARR_MAX_VAL/2)//初始化遇到超过TPAD_ARR_MAX_VAL/2的数值,不正常!
return 1;//初始化失败
return 0;//初始化成功
}//tpad_default_val是 MCU 重启后输入捕获检测的值,也可以说是没有手指触摸时检测到的值
//TPAD_Reset所要实现的事:
//1. 初始化PA5为推挽输出,输出0,放电
//2. 计数器的值初始化为0
//3. 初始化PA5位浮空输入,等待按下充电
void TPAD_Reset(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;//模式为输出
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5;//PA5引脚
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_DOWN;//下拉
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_ResetBits(GPIOA,GPIO_Pin_5);//Reset输出低电平,放电
delay_ms(5);
TIM_ClearITPendingBit(TIM2,TIM_IT_CC1|TIM_IT_Update);//清除中断标志
TIM_SetCounter(TIM2,0);//TIM2定时器归0
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;//模式为复用
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5;
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_NOPULL;//不带上下拉,浮空模式,进行充电
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
//TPAD_Get_Val所要实现的事:
//1. TPAD_Reset 复位TPAD
//2. 等待捕获上升沿
//3. 捕获到上升沿,返回捕获的值
u16 TPAD_Get_Val(void)
{
TPAD_Reset(); //复位TPAD
while(TIM_GetFlagStatus(TIM2, TIM_IT_CC1) == RESET)//TIM_IT_CC1为捕获标志 等待捕获上升沿 ==Reset意味着此时还没有捕获
{//因为TIM2_CH1_Cap_Init函数设置的是上升沿捕获,所以只要是在while中,就一定还没有捕获到上升沿
if(TIM_GetCounter(TIM2)>TPAD_ARR_MAX_VAL-500)
return TIM_GetCounter(TIM2);//超时了,直接返回CNT的值
};
return TIM_GetCapture1(TIM2);//只要离开while循环,就意味着捕获到了上升沿,此时要返回定时器2的计数器值
}
//获取N次中的最大值
u16 TPAD_Get_MaxVAL(u8 n)
{
u16 temp=0,res=0;
while(n--) //n是连续获取的次数,读取n次,获取n次里面的最大值,返回
{
temp=TPAD_Get_Val();//得到一次的值
if(temp>res)
{
res=temp;
}
}
return res;//最后得到的res是N次读取中的最大值
}
#define TPAD_GATE_VAL 100 //触摸的门限值,也就是必须大于tpad_default_val+TPAD_GATE_VAL,才认为是有效触摸.
//tpad_default_val是没有手指触摸时的值,为了防止数值接近这个值而被误认为是手指触摸,
//所以在没有手指触摸的值上给定一个范围值,防止接近于tpad_default_val而被错误识别。
u8 TPAD_Scan(u8 mode)//该程序的逻辑等同于按键扫描的逻辑 mode等于0表示不支持连续触摸 mode等于1表示支持连续触摸
{
static u8 key_up=0;//key_up=0可以开始检测;>0:还不能开始检测
u8 res=0,sample=3;//默认的采样次数为3次
u16 rval;
if(mode)
{
sample=6;//支持连按的时候,默认的采样次数是6次
key_up=0;//支持连按
}
rval=TPAD_Get_MaxVAL(sample);//获取最高的3次样本,赋值给rval
if(rval>(tpad_default_val+TPAD_GATE_VAL)&&rval<(10*tpad_default_val))//有效的情况:3次样本的值大于tpad_default_val+TPAD_GATE_VAL
//且小于10倍的tpad_default_val,则有效
{
if((key_up==0)&&(rval>(tpad_default_val+TPAD_GATE_VAL)))//key_up=0可以开始检测,并且tpad_default_val+TPAD_GATE_VAL有效
res=1; //res状态标志位
key_up=3;//至少要再过3次之后才能按键有效
}
if(key_up)
key_up--;
return res;//res等于0表示没有按下;res等于1表示有按下,res=1有效(res=0是初始化时为0,一旦res=1表示进入了if判断语句,也就表示是有效的)
}
//TIM2通道1初始化输入
void TIM2_CH1_Cap_Init(u32 AutomaticReload,u16 PrioritySendCount)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//使能TIM2时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);//使能GPIOA时钟
GPIO_PinAFConfig(GPIOA,GPIO_PinSource5,GPIO_AF_TIM2);//设置引脚复用,将GPIOA5复用为TIM2定时器
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5;
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_NOPULL;//不带上下拉
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStructure.TIM_Period=AutomaticReload;
TIM_TimeBaseInitStructure.TIM_Prescaler=PrioritySendCount;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ICInitTypeDef TIM2_InitStructure;//初始化TIM2通道1
TIM2_InitStructure.TIM_Channel=TIM_Channel_1;//通道1
TIM2_InitStructure.TIM_ICFilter=0x00;
TIM2_InitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
TIM2_InitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;
TIM2_InitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;
TIM_ICInit(TIM2,&TIM2_InitStructure);
TIM_Cmd(TIM2,ENABLE);//使能定时器TIM2
}
#ifndef _TPAD__H_
#define _TPAD__H_
extern vu16 tpad_default_val;
void TPAD_Reset(void);
u16 TPAD_Get_Val(void);
u16 TPAD_Get_MaxVAL(u8 n);
u8 TPAD_Scan(u8 mode);
void TIM2_CH1_Cap_Init(u32 AutomaticReload,u16 PrioritySendCount);
u8 TPAD_Init(u8 PrioritySendCount);
#endif