“电容触摸按键实验”实例解析

目录

电容触摸按键实验

电容触摸按键的基本原理(原理图层面)

新电容的产生与作用

充放电性能的变化

脉冲如何被捕获

原理图层面的连接

硬件配置的大致流程

电容触摸按键的基本原理(软件配置层面)

函数说明

TPAD_InitConfig()函数

TPAD_Reset()函数

TPAD_GetValue()函数

TPAD_GetMaxValue()函数

TPAD_Scan()

程序示例

Main.c

Led.c

Led.h

Tpad.c

Tpad.h

Timer.c

Timer.h

结果展示


电容触摸按键实验

电容触摸按键的基本原理(原理图层面)

 

新电容的产生与作用

我们学过模电的同学知道:我们的手其实相当于一块可以存储感应电荷的金属板,当我们的手靠近屏幕时,我们的手与屏幕下的金属板Cx构成了一个平行电容板,这个电容与Cs杂散电容相并联,“并联电容C=C1+C2”。

充放电性能的变化

我们看见“VCC是一定的;电容C越大代表一定电压下,存储的电荷数量越多(Q=CU),反之,充电到U所花费的时间也更多;电阻R的值一定代表着对电荷的阻碍作用是固定的”。

充电到V=Vth,花费时间TA明显小于TB;我们真实情况下的矩形脉冲如下图所示:

 

脉冲如何被捕获

当电容从0充电到Vth时,就相当于一个上升沿脉冲。其实这个充放电过程很快,我们一般用的RC充放电电路是为了观察现象因此将时间常数做的很大,但是现实的话我们要快速识别必须将时间常数缩小到一定范围内才OK。

原理图层面的连接

 

STM_ADC其实只是这个引脚功能的其中一个,我们这里并不是用的引脚的ADC功能,以下为该引脚的复用:

 

我们这里使用的是PA1的TIM5_CH2功能,用于快速捕获TPAD的有效脉冲沿变化。

硬件配置的大致流程

第一步

TPAD引脚的电压先置零,进行电容Cs的放电

第二步

TPAD引脚设置为浮空输入(由于TPAD与PA1接在了一起,因此就是将PA1置为浮空输入模式,为了识别有效的脉冲沿)

第三步

TIM5计数器初始值置0并开启上升沿计数模式

第四步

开启TIM5_CH2的输入捕获模式

第五步

等待充电完成,读取此时的捕获值,“脉冲沿持续时间 = (捕获值-初始值)*单位递增时间”

注:没有按下的时候,充电时间为T1(default)。按下TPAD,电容变大,所以充电时间为T2。我们可以通过检测充放电时间,来判断是否按下。如果T2-T1大于某个值,就可以判断

有按键按下。

 

电容触摸按键的基本原理(软件配置层面)

函数名称

返回值类型

函数功能

TPAD_Init()

void

用于捕获无接触情况下的电容充电时间,并且配置TIM5_CH2引脚的初始属性

TPAD_Get_Val()

int

获取一次电容充电时间

TPAD_Get_MaxVal()

int

获取4次读取“电容充电时间”的最大值

TPAD_Scan()

char(true or false)

用于扫描判断TPAD是否动作

TPAD_Reset()

void

重置计数器与捕获值(将前一次读取结果重置)

函数说明

TPAD_InitConfig()函数

void TPAD_InitConfig() // 读取默认电容充电时间  
{  
    u16 RecordChargeTime[10] = {0};  
    u8 i = 0, temp = 0;  
      
    uart_init(115200); // 初始化USART1  
    TIM_CAP_InitConfig(0xFFFF, 72-1); // 初始化TIM5_CH2的输入配置  
      
    for(; i<10; i++)  
    {  
        RecordChargeTime[i] = TPAD_GetValue(); // 连续计算10次默认充电时间  
    }  
	for(i=0; i<9; i++)
	{
		if(RecordChargeTime[i] < RecordChargeTime[i+1]) // 冒泡排序(大->小)
		{
			temp = RecordChargeTime[i];
			RecordChargeTime[i] = RecordChargeTime[i+1];
			RecordChargeTime[i+1] = temp;
		}
	} 
    for(i=1, temp=0; i<9; i++)  
    {  
        temp += RecordChargeTime[i]; // 去掉MAX与MIN求平均充电时间  
    }  
    TPAD_DefaultValue = temp/8; // 求出TPAD无动作时的默认充电时间  
    printf("Default:%dus\r\n",TPAD_DefaultValue); // 向串口打印未触摸时的电容充电时长  
}  

 

TPAD_Reset()函数

这里值得注意的是:PA1端口输入输出模式的变化以及TIM_ClearFlag()函数的使用。

PA1端口输入输出模式的变化:

我们会疑问“PA1端口为何要变化输入输出模式?”,在我们上述流程中提及过,我们要对电容充电所造成的上升沿重新计数就必须先将TPAD(PA1)端口置0进行充分的放电。那我们如何放电呢?

 

在图中,PA1与TPAD通过跳线帽连接在一起,然后为了读取TPAD中产生的上升沿,我们必须将PA1设置为TIM5_CH2的功能,此时PA1为浮空输入模式,但是我们读取完有效的脉冲沿之后,若继续读取必须先进行放电才可以。此时,我们配置PA1为推挽输出模式,放完电后再变成浮空输入模式。

TIM_ClearFlag()库函数的含义:

我们使用库函数的同学会有些疑问:TIM_ClearFlag()函数与 TIM_ClearITPendingBit()函数的区别在哪里?这里我来解说一下。

TIM_ClearFlag()函数是用于“清除标志位的”,比如:当检测到上升沿脉冲(有效的脉冲沿),相应的TIM_IT_CC2标志会出现,其实这些标志位和TIM_ClearITPendingBit ()函数中的中断标志位是一摸一样的,只不过由于未开启中断,事件一旦发生仅仅会使得标志位置1,不会触发中断。 其实如下的程序中我们就是想清除标志位,因此也可以使用TIM_ClearITPendingBit(TIM5, TIM_IT_CC2),这两个函数的功能就是去除相应的标志位,功能相同可以相互转换使用。

TIM_ClearFlag()与TIM_ClearITPendingBit()的函数体对比

TIM_ClearFlag()函数体

void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT)  
{  
  /* Check the parameters */  
  assert_param(IS_TIM_ALL_PERIPH(TIMx));  
  assert_param(IS_TIM_IT(TIM_IT));  
  /* Clear the IT pending Bit */  
  TIMx->SR = (uint16_t)~TIM_IT;  // 操作SR寄存器
}  

 

TIM_ClearITPendingBit()的函数体

void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG)  
{    
  /* Check the parameters */  
  assert_param(IS_TIM_ALL_PERIPH(TIMx));  
  assert_param(IS_TIM_CLEAR_FLAG(TIM_FLAG));  
     
  /* Clear the flags */  
  TIMx->SR = (uint16_t)~TIM_FLAG;  // 操作SR寄存器
}  

 

我们从TIM_ClearFlag()与TIM_ClearITPendingBit()的函数体对比发现,完全一摸一样,可以互换使用。

//复位一次  
void TPAD_Reset(void)  
{  
    GPIO_InitTypeDef  GPIO_InitStructure;   
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);    //使能PA端口时钟  
      
    //设置GPIOA.1为推挽使出  
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;                //PA1 端口配置  
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;         //推挽输出  
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  
    GPIO_Init(GPIOA, &GPIO_InitStructure);  
    GPIO_ResetBits(GPIOA,GPIO_Pin_1);                        //PA.1输出0,放电  
  
    delay_ms(5);  // 给予充足的时间进行放电
  
    TIM_SetCounter(TIM5,0);     //计数器的初始计数值置零
    TIM_ClearFlag(TIM5, TIM_IT_CC2);   // 清除TIM5_CH2的上升沿标志位
    //重新设置GPIOA.1为浮空输入  
    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;  //浮空输入  
    GPIO_Init(GPIOA, &GPIO_InitStructure);   
} 

 

TPAD_GetValue()函数

u16 TPAD_GetValue() // 获取电容充电时间  
{  
    TPAD_Reset(); // 复位一次  
      
    while(TIM_GetFlagStatus(TIM5, TIM_IT_CC2) == RESET)//不断轮询等待捕获上升沿  
    {  
        if(TIM_GetCounter(TIM5)>MAX_ARR-500)  
        {  
            return TIM_GetCounter(TIM5);//超时了,直接返回CNT的值  
        }  
    };    
    return TIM_GetCapture2(TIM5);     
}  

 

这里有几行代码特别值得关注:

if(TIM_GetCounter(TIM5)>MAX_ARR-500)    
{    
    return TIM_GetCounter(TIM5);//超时了,直接返回CNT的值    
} 

   

这里的提出了一个“检查错误”的概念,我们可以在检查是否有上升沿脉冲触发的同时检测“电容充电时间是否过长”,如果充电时间过长直接return值结束轮询(while(1)),此时我们认为有效脉冲沿持续时间为此时的计数器的值。

TPAD_GetMaxValue()函数

u16 TPAD_GetMaxValue(u8 SampleNumber) // 捕获4次取MAX  
{  
    u8 temp = TPAD_GetValue();  
      
    while(1)  
    {  
        temp = temp>TPAD_GetValue()?temp:TPAD_GetValue();  
        if(--SampleNumber) break;  
    }  
      
    return temp; // 求出TPAD无动作时的默认充电时间  
}  

 

TPAD_Scan()

u8 TPAD_Scan() // 扫描TPAD状态且不支持连续按
{  
    u16 TPAD_MaxValue = TPAD_GetMaxValue(3); // 连续采集3次  
    static u8 PressedFlag = 1;  
      
    uart_init(115200); // 初始化USART1  
      
    printf("Moment:%dus\r\n",TPAD_MaxValue); // 向串口打印当前充电时长  
      
    if((TPAD_MaxValue >= TPAD_Threshold + TPAD_DefaultValue)&&PressedFlag)  
    {  
        PressedFlag = 0;  
        return 1;  
    }  
    else if(TPAD_MaxValue < TPAD_Threshold + TPAD_DefaultValue)  
    {  
        PressedFlag = 1;  
        return 0;  
    }  
    else  
    {  
        return 0;  
    }  
}  

 

不支持连续按的精髓代码:

    if((TPAD_MaxValue >= TPAD_Threshold + TPAD_DefaultValue)&&PressedFlag)  
    {  
        PressedFlag = 0;  
        return 1;  
    }  
    else if(TPAD_MaxValue < TPAD_Threshold + TPAD_DefaultValue)  
    {  
        PressedFlag = 1;  
        return 0;  
    }  
    else  
    {  
        return 0;  
    }  
// 希望大家理解代码执行的逻辑,我在这里就不仔细讲解了。

 

我们要清楚的一点是:我们前面编写的所有函数都是为了编写TPAD_Scan()做准备,我们真正在main函数中使用的只有TPAD_Scan()函数。我们在编写函数时的好习惯是:注意哪些是辅助函数,哪些是要在main函数中使用得到的函数,函数的功能一定不要过于集中也不要过于分散。

程序示例

Main.c

#include "tpad.h"  
#include "led.h"  
#include "timer.h"  
#include "stm32f10x.h"  
#include "delay.h"  
#include "usart.h"  
  
int main()  
{  
    extern u16 TPAD_DefaultValue; // TPAD的默认充电时间  
    u8 TPAD_Status = 0, temp = 0;  
      
    delay_init(); // systick时钟初始化  
    LED_InitConfig(); // 初始化LED1&LED0  
    TPAD_InitConfig(); // 计算TPAD_DefaultValue  
    uart_init(115200); // 初始化USART1  
      
    while(1)  
    {  
        TPAD_Status = TPAD_Scan(); // 实时捕获TPAD的动作  
          
        if(TPAD_Status == 1)  
        {  
            LED1 = !LED1; // 触摸按键动作触发LED1状态翻转  
        }  
        temp++;  
        if(temp == 15)  
        {  
            LED0 = !LED0; // LED0作为状态灯使用  
        }  
  
    }  
}  

 

Led.c

#include "led.h"  
#include "stm32f10x.h"  
  
void LED_InitConfig()  
{  
    GPIO_InitTypeDef GPIO_InitStructure;  
      
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE); // 使能LED1,LED0的时钟  
      
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;  
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  
    GPIO_Init(GPIOB, &GPIO_InitStructure); // LED0配置为推挽输出模式  
      
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;  
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;  
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  
    GPIO_Init(GPIOE, &GPIO_InitStructure); // LED1配置为推挽输出模式  
      
    GPIO_SetBits(GPIOB, GPIO_Pin_5); // LED0初始化为高电平  
    GPIO_SetBits(GPIOE, GPIO_Pin_5); // LED1初始化为高电平  
}  

 

Led.h

#ifndef _LED_H  
#define _LED_H  
  
#include "sys.h"  
  
void LED_InitConfig();  
  
#define LED1 PEout(5)  
#define LED0 PBout(5)  
  
#endif 

 

Tpad.c

#include "tpad.h"  
#include "stm32f10x.h"  
#include "timer.h"  
#include "delay.h"  
#include "usart.h"  
  
u16 TPAD_DefaultValue = 0;  
u16 MAX_ARR = 0xFFFF; // 计数器最计数值为0xFFFF  
u16 TPAD_Threshold = 30; // 有无触摸TPAD的电容充电时间差值为30  
  
void TPAD_InitConfig() // 读取默认电容充电时间  
{  
    u16 RecordChargeTime[10] = {0};  
    u8 i = 0, temp = 0;  
      
    uart_init(115200); // 初始化USART1  
    TIM_CAP_InitConfig(0xFFFF, 72-1); // 初始化TIM5_CH2的输入配置  
      
    for(; i<10; i++)  
    {  
        RecordChargeTime[i] = TPAD_GetValue(); // 连续计算10次默认充电时间  
    }  
    for(i=0; i<10; i++)  
    {  
        if(RecordChargeTime[i] < RecordChargeTime[0]) // 冒泡排序(大->小)  
        {  
            temp = RecordChargeTime[i];  
            RecordChargeTime[i] = RecordChargeTime[0];  
            RecordChargeTime[0] = temp;  
        }  
    }  
    for(i=1, temp=0; i<9; i++)  
    {  
        temp += RecordChargeTime[i]; // 去掉MAX与MIN求平均充电时间  
    }  
    TPAD_DefaultValue = temp/8; // 求出TPAD无动作时的默认充电时间  
    printf("Default:%dus\r\n",TPAD_DefaultValue); // 向串口打印未触摸时的电容充电时长  
}  
  
//复位一次  
void TPAD_Reset(void)  
{  
    GPIO_InitTypeDef  GPIO_InitStructure;   
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);    //使能PA端口时钟  
      
    //设置GPIOA.1为推挽使出  
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;                //PA1 端口配置  
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;         //推挽输出  
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  
    GPIO_Init(GPIOA, &GPIO_InitStructure);  
    GPIO_ResetBits(GPIOA,GPIO_Pin_1);                        //PA.1输出0,放电  
  
    delay_ms(5);  
  
    TIM_SetCounter(TIM5,0);     // 计数器初始计数值置零  
    TIM_ClearFlag(TIM5, TIM_IT_CC2);   
    //设置GPIOA.1为浮空输入  
    GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;  //浮空输入  
    GPIO_Init(GPIOA, &GPIO_InitStructure);   
}  
  
u16 TPAD_GetValue() // 获取电容充电时间  
{  
    TPAD_Reset(); // 复位一次  
      
    while(TIM_GetFlagStatus(TIM5, TIM_IT_CC2) == RESET)//等待捕获上升沿  
    {  
        if(TIM_GetCounter(TIM5)>MAX_ARR-500)  
        {  
            return TIM_GetCounter(TIM5);//超时了,直接返回CNT的值  
        }  
    };    
    return TIM_GetCapture2(TIM5);     
}  
  
u16 TPAD_GetMaxValue(u8 SampleNumber) // 捕获4次取MAX  
{  
    u8 temp = TPAD_GetValue();  
      
    while(1)  
    {  
        temp = temp>TPAD_GetValue()?temp:TPAD_GetValue();  
        if(--SampleNumber) break;  
    }  
      
    return temp; // 求出TPAD无动作时的默认充电时间  
}  
  
u8 TPAD_Scan() // 扫描TPAD状态  
{  
    u16 TPAD_MaxValue = TPAD_GetMaxValue(3); // 连续采集3次  
    static u8 PressedFlag = 1;  
      
    uart_init(115200); // 初始化USART1  
      
    printf("Moment:%dus\r\n",TPAD_MaxValue); // 向串口打印当前充电时长  
      
    if((TPAD_MaxValue >= TPAD_Threshold + TPAD_DefaultValue)&&PressedFlag)  
    {  
        PressedFlag = 0;  
        return 1;  
    }  
    else if(TPAD_MaxValue < TPAD_Threshold + TPAD_DefaultValue)  
    {  
        PressedFlag = 1;  
        return 0;  
    }  
    else  
    {  
        return 0;  
    }  
}  

 

Tpad.h

#ifndef _TPAD_H  
#define _TPAD_H  
  
#include "sys.h"  
  
void TPAD_InitConfig(); // 用于初始化TPAD  
u16 TPAD_GetValue(); // 捕获一次电容充电时间  
u16 TPAD_GetMaxValue(u8 SampleNumber); // 捕获10次电容电容充电时间的MAX  
u8 TPAD_Scan(); // 用于扫描TPAD是否动作  
  
#endif  

 

Timer.c

#include "timer.h"  
#include "stm32f10x.h"  
#include "sys.h"  
  
void TIM_CAP_InitConfig(u16 ARR, u16 PR)  
{  
    GPIO_InitTypeDef GPIO_InitStructure;  
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;  
    TIM_ICInitTypeDef TIM_ICInitStructure;  
      
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); // 使能TIM5的时钟  
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA的时钟  
      
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;  
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;  
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  
    GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置PA1为浮空输入  
      
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;  
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;  
    TIM_TimeBaseInitStructure.TIM_Period = ARR;  
    TIM_TimeBaseInitStructure.TIM_Prescaler = PR;  
    TIM_TimeBaseInit(TIM5, &TIM_TimeBaseInitStructure); // 配置TIM5计数器的属性  
      
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;  
    TIM_ICInitStructure.TIM_ICFilter = 0;  
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;  
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;  
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;  
    TIM_ICInit(TIM5, &TIM_ICInitStructure); // 配置TIM5_CH2的输入属性  
      
    TIM_Cmd(TIM5, ENABLE); // 使能TIM5  
}  
// 我们不开启捕获中断,但是我们用“识别脉冲沿标志位”来判断事件是否成功发生  

 

Timer.h

#ifndef _TIMER_H  
#define _TIMER_H  
  
#include "sys.h"  
  
void TIM_CAP_InitConfig(u16 ARR, u16 PR);  
  
#endif  

 

结果展示

 

 

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

肥肥胖胖是太阳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值