目录
电容触摸按键实验
电容触摸按键的基本原理(原理图层面)
新电容的产生与作用
我们学过模电的同学知道:我们的手其实相当于一块可以存储感应电荷的金属板,当我们的手靠近屏幕时,我们的手与屏幕下的金属板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
结果展示