STM32F103你学不会系列(十七)电容触摸按键实现
Posted 自信且爱笑‘
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32F103你学不会系列(十七)电容触摸按键实现相关的知识,希望对你有一定的参考价值。
学习板:STM32F103ZET6
参考:
电容触摸按键
前言
上一博客总结了定时器输入捕获的内容,本章总结一下电容触摸按键相关内容,触摸按键会用到输入捕获相关内容,本博也对这方面的东西不在赘述。
一、电容触摸按键原理
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