STM32电容触摸按键检测

STM32电容触摸按键检测

  • 电容触摸按键
    • 简介
    • 检测原理
  • CubeMX配置
  • 代码展示&讲解
    • TPAD.c
    • TPAD.h

本期内容我们将学习电容触摸按键的检测原理。以及代码实现思路

电容触摸按键

简介

STM32电容触摸按键检测_第1张图片

  • 电容触摸按键依赖的是电容的充放电
  • 相对于机械按键更加耐用,不容易受外界环境干扰
  • 在我们的开发板(正点原子的STM32F411RCT6 NANO板)上长这样:

检测原理

前面将电容触摸按键依赖的是电容的充放电,然而我们看开发板上的元件摆放图,发现没有电容在TPAD位置:

STM32电容触摸按键检测_第2张图片

这个要从PCB的层叠结构说起;

  • 我们的NANo板使用的是双层PCB结构,大概结构就是有两个铜箔中间夹着一个基材,
  • 铜箔构成PCB的顶层和底层,负责PCB的走线,完成信号的传递,
  • 铜箔外侧有丝印,阻焊等等,同时也负责元器件的摆放。
  • 铜箔内部有基材,通常以RF-4为多,这个部分占PCB的大部分厚度,为此PCB的基本外观的硬度
  • 具体结构大体如下(这个手画的有点抽象):
    STM32电容触摸按键检测_第3张图片

有了这个概念,我们就可以进行理论推导了:

首先,电容的充电公式如下:Vc=E (1-e (-t/R*C))
其中:

  • Vc为电容两端的电压,其随时间变化
  • E为电容充电的最大值,即充满电的的电压
  • e为自然对数的底数
  • t为时间
  • R为电容充电回路的电阻
  • C为电容的容值

通过以上公式,我们可以得到曲线
STM32电容触摸按键检测_第4张图片

再看原理图:

STM32电容触摸按键检测_第5张图片
STM32电容触摸按键检测_第6张图片

  • 通过通过电阻R32和前面板材PCB铜箔之间的电容1(这里叫Cs),构成充放电回路,STM_ADC充当开关,控制PB1的充放电
  • 当手指触摸板材时,铜箔与手指之间的电容2(这里叫Cx)加入充电回路,电容增加,充电速度变慢,换句话说,电容达到相同电压的时间变成,利用这一区别,我们就能实现电容触摸按键的检测
    STM32电容触摸按键检测_第7张图片
  • 图中Cth表示单片机GPIO识别为高电平的最小值。当Vc小于Vth时,识别为低电平,反之识别为高电平。越过Vth的过程可以被单片机识别为上升沿。可通过定时器的输入捕获功能捕捉
    具体配置过程如下:
  1. TPAD引脚设置为推挽输出,输出低电平,使电容放电
  2. TPAD引脚设置为浮空输入,电容开始充电
  3. 开启TPAD输入捕获功能,捕获上升沿(即电压达到Vth的时刻)
  4. 等待充电过程,直至捕获成功
  5. 计算充电时间。

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


不得不说,正点原子写的代码还是太妙了,所以我们在这里斗胆来窥探一下:

  1. void TPAD_Reset(void);
    STM32电容触摸按键检测_第8张图片

这里负责的是TPAD引脚的复位,完成TPAD引脚的放电,并设置为输入捕获功能,这是进行充电时间读取前的必备操作

  1. uint16_t TPAD_Get_Val(void);
    STM32电容触摸按键检测_第9张图片

这里读取的是电容充电时间,不论是否超时都会被读取

  1. uint16_t TPAD_Get_MaxVal(uint8_t n);
    STM32电容触摸按键检测_第10张图片

这里是读取有手指按下的最大充电时间,当没有手指按下时,返回0
这里的5/4只是个判断标准,确保是有手指按下的情况
2/3则是用来判误触,静电等情况

  1. 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次手指未按下时的充电时间
排序去除最大值和最小值是为了减小未捕获上升沿,或过快捕获到上升沿导致的误差

  1. uint8_t TPAD_Scan(uint8_t mode);

STM32电容触摸按键检测_第11张图片

  • 扫描函数可以分为支持连续触发和单次触发
  • 单次触发:第一次触发时会将res置1,后返回表示按键按下,后续计入函数,如果手指一直不拿开,会导致res无法置1(keyen一直等于3),返回0表示按键为按下
  • 连续触发:无论那一次触发进入函数,keyen都会被置1,后返回1表示按键按下。

以上就是本期的全部内容,emmm

你可能感兴趣的:(stm32,嵌入式硬件,单片机)