STM32F103你学不会系列(十七)电容触摸按键实现

Posted 自信且爱笑‘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32F103你学不会系列(十七)电容触摸按键实现相关的知识,希望对你有一定的参考价值。

学习板:STM32F103ZET6

参考:

STM32F103五分钟入门系列(十五)输入捕获

前言

上一博客总结了定时器输入捕获的内容,本章总结一下电容触摸按键相关内容,触摸按键会用到输入捕获相关内容,本博也对这方面的东西不在赘述。

一、电容触摸按键原理

1、原理图分析

先察看原理图:

上图中红色框框标注连接芯片引脚,红色箭头标注为电容触摸按键。


上图触摸按键的TPAD不直接连接在芯片引脚上,而是连接在1×4排针上,而STM_ADC连接在单片机PA1引脚,如果板子上用跳线帽把排针的1、2引脚连接在一起,则触摸按键的TPAD引脚就连接在了芯片的PA1引脚。

这样设计电路的目的是为了在PA1引脚上使用定时器2通道2或定时器5通道2的输入捕获来完成触摸按键检测。

当然没有把触摸按键的TPAD引脚直接连接在PA1上,可以让我们有更多的选择。比如想使用定时器3通道1捕获触摸按键,就取消跳线帽,直接在板子上把跳线帽上TPAD与PA6用飞线连接即可。

综上:只要跳线帽把TPAD与STM_ADC连接起来,就完成了触摸按键TPAD与芯片引脚PA1的连接。

2、原理分析

上图中红色框框连接芯片PA1引脚,蓝色框框为触摸按键,触摸按键相当于一个电容,在没有按下的时候有一个初始电容,按下后,手指与触摸按键之间又会形成一个电容,导致蓝色框框内的电容变大。

上图中不好理解,现重新画一下原理图:

下图所示,Cs为未按下时触摸按键电容,Cx为按下后手指与触摸按键之间新生成的电容。

现分析一下原理:

如果PA1设置为推挽输出且高电平,此时外接3V3、电阻、电容Cs、地,构成一条回路,最终稳定后电容Cs两端电压为3.3V,电阻两端电压为0。(Cs充电)

如果PA1设置为推挽输出且低电平,则A点与B点电势为0,C点电势为3.3V,C点会向PA1放电。最终稳定后电容Cs两端电压为0。(Cs放电)

如此可以测得Cs充放电时间与R、C、U0的关系:
R:电阻
C:Cs
U0:3.3V

从上图的公式推导可得,当按下触摸按键后,手指与电容触摸按键直接生成一个电容Cx,导致充放电时间存在一个时间差。

所以电容触摸按键可以这样实现:

开始时先设置PA1为通用推挽输出,将PA1设置为低电平放电;放电完成后将PA1设置为浮空输入,同时将计数器清零,清除中断标志,开始捕获上升沿。当电容充电完成后A点(即PA1引脚)变为高电平,此时定时器会捕获到一个上升沿,此时计数器的值就是此次充电期间计数器计数次数。再次将PA1设置为推挽输出,置低电平放电,再设置浮空输入,进行输入捕获。如此循环多次,得到多个充电期间的计数值,去掉几个最小值和最大值,再求平均值减小误差。这样就获得了未按下时的充电期间的计数次数。

如何检查按下?当然函数输入捕获,可以在主函数while循环中一直重复上一段话的内容,即一直获取充电期间的计数次数,当计数值次数明显增大时,说明电容触摸按键按下。

二、例1-为例2做准备

1、题

编写一个电容触摸按键的捕获函数,获取不按下、按下时的充电时间、同时测试在捕获期间是否发生计数器更新中断,计算充电时间。

2、分析

这个充电时间可能很短,而程序每执行一条语句的时间是固定的,为了捕获到上升沿时计数器的值稍微大一点,可以把计数器的时钟频率设置的稍微高一点。

之前也总结过,可以采用定时器5通道2,也可以采用定时器2通道2.这里不妨采用定时器5通道2,定时器时钟输入为72MHZ,预分频系数设置为72,则定时器的时钟为1MHZ。为了尽可能在一个计数周期内捕获到上升沿,可将重装载初值设置为最大:0xffff。

创建cap.h、cap.c来初始化定时器5通道2;创建tpad.h、tpad.c来编写充电时间获取函数。在充电获取函数中,先将PA1设置为通用推挽输出并值低电平放电,放电结束后将PA1设置为浮空输入,开始捕获上升沿。

3、代码实现

(1)cap.h、cap.c代码

定时器的设置代码之前博客都总结过了。这里不需要捕获中断,而为了测试是否发生更新中断(测试在捕获期间是否发生过计数器重装),这里可以只使能更新中断。

cap.h代码:

//cap.h
#ifndef _CAP_
#define _CAP_
void CAP_Iint(void);
#endif
//cap.c
#include "cap.h"
#include "stm32f10x.h"
#include "usart.h"
void CAP_Iint()
{
	GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_ICInitTypeDef  TIM_ICInitStructure;
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);

	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);

	TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period=0xFFFF;
	TIM_TimeBaseInitStructure.TIM_Prescaler=71;//1MHZ计数
	TIM_TimeBaseInit(TIM5,&TIM_TimeBaseInitStructure);
	
	TIM_ICInitStructure.TIM_Channel=TIM_Channel_2;
	TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
	TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;
	TIM_ICInitStructure.TIM_ICFilter=0x0011;//8次检测
	TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;
	TIM_ICInit(TIM5,&TIM_ICInitStructure);
	
	TIM_ITConfig(TIM2,TIM_IT_Update, ENABLE);//允许更新中断
	TIM_Cmd(TIM5,ENABLE);

(2)tpad.h、tpad.c代码

tpad.h中声明一个充电时间捕获函数:

//tpad.h
#ifndef _TPAD_
#define _TPAD_
void Get_Charge_Timer(void);
#endif

tpad.c代码:

//tpad.c
#include "tpad.h"
#include "stm32f10x.h"
#include "delay.h"
#include "usart.h"
u16  updata_count=0;//记录更新中断次数
u32  count=0;
void Get_Charge_Timer()
{
	GPIO_InitTypeDef   GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);    //设置为通用推挽输出,准备获得充电时间
	GPIO_ResetBits(GPIOA, GPIO_Pin_1);//置低电平放电
	
	delay_ms(50);//等待放电完成(需调试这个时间)
	
	TIM_SetCounter(TIM5,0);//计数器的值清零
	TIM_ClearITPendingBit(TIM5, TIM_IT_Update|TIM_IT_CC2);//清除计数器更新中断和捕获通道中断标志
	updata_count=0;
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;//PA1设置为浮空输入,开始充电
	GPIO_Init(GPIOA,&GPIO_InitStructure);

//	while(TIM_GetITStatus(TIM5,TIM_IT_CC2)==RESET)//等待上升沿捕获
//	{
//		//程序停在这里

//	}
	
	while((TIM5->SR & TIM_IT_CC2)==RESET)//等待上升沿捕获
	{
	
	}
//while(TIM_GetFlagStatus(TIM5,TIM_FLAG_CC2)==RESET)//等待上升沿捕获
//{

//}
	printf("updata_count=%d \\r\\n",updata_count);
	
	printf("count=%d \\r\\n",TIM5->CCR2);
	
	delay_ms(500);
	
}

void TIM5_IRQHandler()
{
	updata_count++;//记录发生重装载的次数
	
	TIM_ClearITPendingBit(TIM5,TIM_IT_Update);
}

具体解释看代码注释即可。

经过测试,上升沿捕获等待方式有三种:

//	while(TIM_GetITStatus(TIM5,TIM_IT_CC2)==RESET)//等待上升沿捕获
//	{
//		//程序停在这里

//	}
	
	while((TIM5->SR & TIM_IT_CC2)==RESET)//等待上升沿捕获
	{
	
	}
//while(TIM_GetFlagStatus(TIM5,TIM_FLAG_CC2)==RESET)//等待上升沿捕获
//{

//}

但是上面的第一种方法无法识别到上升沿,即程序会一直卡在while循环里面,分析发现,是因为执行TIM_GetITStatus(TIM5,TIM_IT_CC2)==RESET这个函数花费了太多时间,充电完成后,该函数还没有执行完毕,导致while条件判断语句一直不成立。采用第二、第三种方式就可以了。

(3)主函数代码

//main.c
#include "stm32f10x.h"
#include "cap.h"
#include "usart.h"
#include "tpad.h"
#include "delay.h"

 int main(void)
 {	
	 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	 CAP_Iint();
	 delay_init();
	 uart_init(115200);	 //串口初始化为115200
	 
	 while(1)
	 {
	 Get_Charge_Timer();
	 }
 }

4、测试

下载程序后,在串口监视器中察看:

未按下时:

按下时:

程序中定义的updata_count是来检测发生更新事件的次数的。通过本次测试,可以断定在一个计数周期内就可以充电完成,不会发生计数器重装。

充电时间也很好计算。定时器时钟为1MHZ,则充电时间为count/1M

三、例2

1、题

按下电容触摸按键后LED0点亮,取消按下后LED0熄灭。

2、分析

经过例1测试,充电时间在一个范围内波动。其实,在没有按下时充电时间基本是固定的,为19/(1M);按下时才波动,是因为手指与触摸按键之间接触问题,导致电容大小一直在变。为了避免错误,可以同时捕获多个值,去掉几个最小值,去掉几个最大值,求平均值。

不过问题又来了,在放电时,需要一个等待放电完成的时间,如果这个时间不自己去调试(按照例1方法,不断改变这个等待时间,察看输出的计数值大小变化),时间过短会导致放电不完全,可能放完电还是高电平,无法捕获上升沿;时间过长,采用上一自然段说的,取平均值,会浪费很多时间,按下电容触摸按键后,需要执行一段非常可观的时间才会响应,导致点亮LED会滞后。

经过调试,我的板子只需要10ms即可(采用5ms时按下触摸按键跳变很严重)

3、代码实现

(1)led.h、led.c

之前博客总结过了,直接附代码:

//led.h
#ifndef __LED_H 
#define __LED_H	 
void LED_Init(void);	
#endif
//led.c
#include "led.h"
#include "stm32f10x.h"
void LED_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStruct;
	 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);
	
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStruct);
	 
	GPIO_SetBits(GPIOB, GPIO_Pin_5);
}

(2)cap.h、cap.c

这里代码和例1代码相似,唯一不同是没有使能更新中断,没有中断服务函数(因为测试发现,一个计数周期就完成捕获,没有计数器重装)
cap.h代码:

//cap.h
#ifndef _CAP_
#define _CAP_
void CAP_Iint(void);
#endif
//cap.c
#include "cap.h"
#include "stm32f10x.h"
#include "usart.h"
void CAP_Iint()
{
	GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_ICInitTypeDef  TIM_ICInitStructure;
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);

	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);

	TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
	TIM_TimeBaseInitStructure.TIM_Period=0xFFFF;
	TIM_TimeBaseInitStructure.TIM_Prescaler=71;//1MHZ计数
	TIM_TimeBaseInit(TIM5,&TIM_TimeBaseInitStructure);
	
	TIM_ICInitStructure.TIM_Channel=TIM_Channel_2;
	TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
	TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;
	TIM_ICInitStructure.TIM_ICFilter=0x0011;//8次检测
	TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;
	TIM_ICInit(TIM5,&TIM_ICInitStructure);
	TIM_Cmd(TIM5,ENABLE);

(3)tpad.h、tpad.c

在tpad.h中声明两个函数,分别获取充电时间和取平均值。

//tpad.h
#ifndef _TPAD_
#define _TPAD_
#include "stm32f10x.h"

u16 Get_Charge_Timer(void);
u16  Aver_Time(void);

#endif

tpad中Get_Charge_Timer()函数例1有过说明,唯一不同是去掉串口调试的printf()代码和添加了返回值,返回充电完成后计数器的值。

取平均值函数也好理解,就是for循环取10次充电时间,然后存放在数组里面,按从小到大排序后,取中间的6个数的平均值(去掉2个最大值、2个最小值)。

tpad.c完整代码:

#include "tpad.h"
#include "stm32f10x.h"
#include "delay.h"
#include "usart.h"

u16 Get_Charge_Timer()
{
	
	GPIO_InitTypeDef   GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);    //设置为通用推挽输出,准备获得充电时间
	GPIO_ResetBits(GPIOA, GPIO_Pin_1);//置低电平放电
	
	delay_ms(10);//等待放电完成(需调试这个时间)
	
	TIM_SetCounter(TIM5,0);//计数器的值清零
	TIM_ClearITPendingBit(TIM5, TIM_IT_Update|TIM_IT_CC2);//清除计数器更新中断和捕获通道中断标志
	

	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;//PA1设置为浮空输入,开始充电
	GPIO_Init(GPIOA,&GPIO_InitStructure);

//	while(TIM_GetITStatus(TIM5,TIM_IT_CC2)==RESET)//等待上升沿捕获
//	{
//		//程序停在这里

//	}
	
	while((TIM5->SR & TIM_IT_CC2)==RESET)//等待上升沿捕获
	{
	
	}
//while(TIM_GetFlagStatus(TIM5,TIM_FLAG_CC2)==RESET)//等待上升沿捕获
//{

//}
	return TIM5->CCR2;
}



u16 Aver_Time()//取几次时间的平均值,减小误差
 {
	u16 sum=0;
	u16 a[10];
	u16 temp;
	int i,j;
	for( i=0;i<10;i++)
	 a[i]=Get_Charge_Timer();//获取10次充电时间(其实是充电时间段的计数值)
	 
	 for( i=0;i<9;i++)
	  for(j=i+1;j<10;j++)//从小到大排序
		{
			if(a[i]>a[j])
			{
				temp=a[i];
				a[i]=a[j];
				a[j]=temp;
			}
		}
	for(i=2;i<8;i++)//去掉2个最小值,去掉2个最大值
	sum+=a[i];
	return sum/6;
 }

(4)main.c

开始时初始化输入捕获、LED、延时函数,并通过Aver_Time()函数获取未按下时电容充电的平均时间。

在while循环中一直用Aver_Time()函数获取电容充电时间,当发现电容充电时间变大后,说明按下了电容触摸按键。

#include "stm32f10x.h"
#include "cap.h"
#include "usart.h"
#include "tpad.h"
#include "delay.h"
#include "led.h"
u16 Time=0;
 int main(void)
 {	
	 CAP_Iint();
	 delay_init();
	 LED_Init();
	 Time=Aver_Time();//获取未按下时电容充电时间
	// uart_init(115200);	 //串口初始化为115200
	//printf("Time= %d \\r\\n",Time);
	 while(1)
	 {
		if(Aver_Time()>(Time+30))//表示按下,这个30需测试
			PBout(5)=0;//点亮LED0
		else
			PBout(5)=1;
	 }	 
 }

4、测试

下载程序后,按下电容触摸按键:

以上是关于STM32F103你学不会系列(十七)电容触摸按键实现的主要内容,如果未能解决你的问题,请参考以下文章

STM32F103(二十七)超长篇解读STM32访问外部flash

STM32F103(二十七)超长篇解读STM32访问外部flash

STM32F103(二十七)超长篇解读STM32访问外部flash

STM32F103(二十七)超长篇解读STM32访问外部flash

STM32F103五分钟入门系列按键实验(库函数+寄存器)

基于STM32F103的智能门锁系统