第26章 电容按键检测实验
第二十六章 电容按键检测实验
1. 硬件设计
开发板板载一个电容按键,原理图设计参考图
标示TPAD1在电路板上就是电容按键实体,默认通过一个调帽连接到PA1,即通用定时器TIM5的通道2。 充电电容的阻值为5.1M,电阻的大小决定了电容按键充电的时间。
实验还用到调试串口和蜂鸣器功能,用来打印输入捕获信息和指示按键状态,这两个模块电路可参考之前相关章节。
2. 软件设计
2.1 编程目标
-
编写定时器输入捕获相关函数
-
测量电容按键空载的充电时间T1
-
测量电容按键有手触摸的充电时间T2
-
只需要比较T2与T1的时间即可检测出按键是否有手指触摸
2.2 代码分析
- 电容按键宏定义
#define TPAD_TIM TIM5
#define TPAD_TIM_APBxClock_FUN RCC_APB1PeriphClockCmd
#define TPAD_TIM_CLK RCC_APB1Periph_TIM5
#define TPAD_TIM_Period 0XFFFF
#define TPAD_TIM_Prescaler (72-1)
// TIM 输入捕获通道GPIO相关宏定义
#define TPAD_TIM_CH_GPIO_CLK RCC_APB2Periph_GPIOA
#define TPAD_TIM_CH_PORT GPIOA
#define TPAD_TIM_CH_PIN GPIO_Pin_1
#define TPAD_TIM_CHANNEL_x TIM_Channel_2
// 中断相关宏定义
#define TPAD_TIM_IT_CCx TIM_IT_CC2
#define TPAD_TIM_IRQ TIM5_IRQn
#define TPAD_TIM_INT_FUN TIM5_IRQHandler
// 获取捕获寄存器值函数宏定义
#define TPAD_TIM_GetCapturex_FUN TIM_GetCapture2
// 捕获信号极性函数宏定义
#define TPAD_TIM_OCxPolarityConfig_FUN TIM_OC2PolarityConfig
// 电容按键被按下的时候门限值,需要根据不同的硬件实际测试,
// 减小这个门限值可以提高响应速度
#define TPAD_GATE_VAL 70
// 电容按键空载的时候的最大和最小的充电时间,不同的硬件不一样,指南者稳定在76
#define TPAD_DEFAULT_VAL_MIN 70
#define TPAD_DEFAULT_VAL_MAX 80
#define TPAD_ON 1
#define TPAD_OFF 0
有关宏的具体含义配套注释阅读即可。定时器计数器的时钟分频因子,我们通过宏TPAD_TIM_Prescaler默认配置为71, 则计数器的计数时间为(71+1)/72M =1us,自动重装载寄存器ARR的值通过TPAD_TIM_Period默认配置为0XFFFF,即65535。 所以,计数器在不发生溢出的情况下,能计数的最长时间为65.535ms。而一般电容按键的充电时间都是us级别,所以定时器的这个配置足以。
- 电容按键GPIO配置
static void TPAD_TIM_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 输入捕获通道 GPIO 初始化
RCC_APB2PeriphClockCmd(TPAD_TIM_CH_GPIO_CLK, ENABLE);
GPIO_InitStructure.GPIO_Pin = TPAD_TIM_CH_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(TPAD_TIM_CH_PORT, &GPIO_InitStructure);
}
TPAD_TIM_GPIO_Config()函数初始化了定时器用到的相关的GPIO,当使用不同的GPIO的时候,只需要修改头文件里面的宏定义即可,而不需要修改这个函数。
- 电容按键TIM模式配置
static void TPAD_TIM_Mode_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 开启定时器时钟,即内部时钟CK_INT=72M
TPAD_TIM_APBxClock_FUN(TPAD_TIM_CLK, ENABLE);
/*--------------------时基结构体初始化-------------------------*/
// 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断
TIM_TimeBaseStructure.TIM_Period = TPAD_TIM_Period;
// 驱动CNT计数器的时钟 = Fck_int/(psc+1)
TIM_TimeBaseStructure.TIM_Prescaler = TPAD_TIM_Prescaler;
// 时钟分频因子 ,配置死区时间时需要用到
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
// 计数器计数模式,设置为向上计数
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
// 重复计数器的值,没用到不用管
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
// 初始化定时器
TIM_TimeBaseInit(TPAD_TIM, &TIM_TimeBaseStructure);
/*--------------------输入捕获结构体初始化-------------------*/
// 配置输入捕获的通道,需要根据具体的GPIO来配置
TIM_ICInitStructure.TIM_Channel = TPAD_TIM_CHANNEL_x;
// 输入捕获信号的极性配置
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿
// 输入通道和捕获通道的映射关系,有直连和非直连两种
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // 直连
// 输入的需要被捕获的信号的分频系数
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 不分频
// 输入的需要被捕获的信号的滤波系数
TIM_ICInitStructure.TIM_ICFilter = 0; // 无滤波
// 定时器输入捕获初始化
TIM_ICInit(TPAD_TIM, &TIM_ICInitStructure);
// 使能计数器
TIM_Cmd(TPAD_TIM, ENABLE);
}
TPAD_TIM_Mode_Config()函数中初始化了两个结构体,有关这两个结构体成员的具体含义可参考“定时器初始化结构体详解”小节, 剩下的程序参考注释阅读即可。有个地方要注意的是捕获信号的极性配置,需要配置为上升沿。因为电容按键在放电之后再充电的时候是一个电平由低到高的过程。
关于输入捕获结构体几个成员解释:
TIM_ICInitStructure.TIM_Channel = TPAD_TIM_CHANNEL_x;
- TIM_Channel:设置定时器的输入捕获通道。
TPAD_TIM_CHANNEL_x
应替换为实际的通道,如TIM_Channel_1
、TIM_Channel_2
等,具体取决于所使用的定时器和引脚配置。
// 输入捕获信号的极性配置
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
- TIM_ICPolarity:设置捕获信号的极性。
TIM_ICPolarity_Rising
表示在上升沿时进行捕获。另一种选项是TIM_ICPolarity_Falling
,表示在下降沿进行捕获。
// 输入通道和捕获通道的映射关系,有直连和非直连两种
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
- TIM_ICSelection:选择输入捕获通道的映射方式。
TIM_ICSelection_DirectTI
:直接选择输入信号。TIM_ICSelection_IndirectTI
:选择输入信号经过预分频器处理后再捕获(通常用于更复杂的信号处理)。
// 输入的需要被捕获的信号的分频系数
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
- TIM_ICPrescaler:设置输入捕获的分频系数。
TIM_ICPSC_DIV1
表示不进行分频。可以选择TIM_ICPSC_DIV2
、TIM_ICPSC_DIV4
或TIM_ICPSC_DIV8
,根据实际需要来决定分频级别。
// 输入的需要被捕获的信号的滤波系数
TIM_ICInitStructure.TIM_ICFilter = 0;
- TIM_ICFilter:设置输入信号的滤波系数,滤波系数决定了在捕获前对输入信号的去噪程度。
0
表示没有滤波。可以选择更高的滤波系数来减少噪声的影响,但也会增加延迟。
// 定时器输入捕获初始化
TIM_ICInit(TPAD_TIM, &TIM_ICInitStructure);
- TIM_ICInit:将配置应用到指定的定时器(
TPAD_TIM
)。TPAD_TIM
应该替换为实际的定时器实例,如TIM1
、TIM2
等。
- 电容按键复位
/**
* @brief 复位电容按键,放电,重新充电
* @param 无
* @retval 无
* 说明:
* 开发板上电之后,电容按键默认已经充满了电,要想测得电容按键的充电时间
* 就必须先把电容按键的电放掉,方法为让接电容按键的IO输出低电平即可
* 放电完毕之后,再把连接电容按键的IO配置为输入,然后通过输入捕获的方法
* 测量电容按键的充电时间,这个充电时间是没有手指触摸的情况下的充电时间
* 而且这个空载的充电时间非常稳定,因为电路板的硬件已经确定了
*
* 当有手指触摸的情况下,充电时间会变长,我们只需要对比这两个时间就可以
* 知道电容按键是否有手指触摸
*/
void TPAD_Reset(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 输入捕获通道1 GPIO 初始化
RCC_APB2PeriphClockCmd(TPAD_TIM_CH_GPIO_CLK, ENABLE);
GPIO_InitStructure.GPIO_Pin = TPAD_TIM_CH_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(TPAD_TIM_CH_PORT, &GPIO_InitStructure);
// 连接TPAD的IO配置为输出,然后输出低电平,延时一会,确保电容按键放电完毕
GPIO_ResetBits(TPAD_TIM_CH_PORT,TPAD_TIM_CH_PIN);
// 放电是很快的,一般是us级别
SysTick_Delay_Ms(5);
// 连接TPAD的IO配置为输入,用于输入捕获
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(TPAD_TIM_CH_PORT, &GPIO_InitStructure);
}
开发板上电之后,电容按键默认已经充满了电,要想测得电容按键的充电时间 就必须先把电容按键的电放掉, 方法为让接电容按键的IO输出低电平即可,这个放电的时间一般都是us级别,我们可以稍微延时以下即可。放电完毕之后, 再把连接电容按键的IO配置为输入,然后通过输入捕获的方法测量电容按键的充电时间,这个充电时间T1是没有手指触摸的情况下的充电时间, 而且这个空载的充电时间非常稳定,因为电路板的硬件已经确定了。当有手指触摸的情况下,相当于电容变大,充电时间T2会变长, 我们只需要对比这两个时间就可以 知道电容按键是否有手指触摸。
简单来说就是:由于我们开发板接电后,电容会充满电,所以我们需要放电-即给IO输出低电平,然后再对这个IO配置为输入捕获,用来检测有无触摸电容按键的情况下各自充电时间
- 电容按键初始化
/**
* @brief 初始化触摸按键,获得空载的时候触摸按键的充电时间
* @param 无
* @retval 0:成功,1:失败
* @note 空载值一般很稳定,由硬件电路决定,该函数只需要调用一次即可
* 而且这个空载的充电时间每个硬件都不一样,最好实际测试下
*/
uint8_t TPAD_Init(void)
{
uint16_t temp;
// 电容按键用到的输入捕获的IO和捕获模式参数初始化
TPAD_TIM_Init();
temp = TPAD_Get_Val();
// 电容按键空载的充电时间非常稳定,不同的硬件充电时间不一样
// 需要实际测试所得,指南者 上的电容按键空载充电时间稳定在76
// 如果你觉得单次测量不准确,你可以多次测量然后取个平均值
if ((TPAD_DEFAULT_VAL_MIN<temp) && (temp<TPAD_DEFAULT_VAL_MAX)) {
tpad_default_val = temp;
// 调试的时候可以把捕获的值打印出来,看看默认的充电时间是多少
printf("电容按键默认充电时间为: %d us\n",tpad_default_val);
return 0; // 成功
}
else
{
return 1; // 失败
}
}
TPAD_Init() 函数用来获取电容按键空载的充电时间,当获取到之后,把值存在tpad_default_val这个全局变量当中。 这个空载的充电时间不同的硬件是不一样的,需要实际测试,在调试的过程中,可把捕获到的值打印出来看看。霸道开发板上这个值稳定在218,指南者则稳定在76。
在TPAD_Init() 函数中,我们是通过调用TPAD_Get_Val()函数来获取电容按键的充电时间的。 当电容按键从0开始充电到STM32能够识别的高电平时, 定时器则发生捕获,此时计数器的值会被锁存到输入捕获寄存器,我们只需要读取输入捕获寄存器的值,就可以算出这个充电的时间。 通过TPAD_Get_Val()这个函数,我们可以测出电容按键的空载充电时间T1和有手触摸的情况下的充电时间T2。
- 获取定时器输入捕获值
/**
* @brief 获取定时器捕获值
* @param 无
* @retval 定时器捕获值。如果超时,则直接返回定时器的计数值。
*/
uint16_t TPAD_Get_Val(void)
{
// 每次捕获的时候,必须先复位放电
TPAD_Reset();
// 当电容按键复位放电之后,计数器清0开始计数
TIM_SetCounter(TPAD_TIM, 0);
// 清除相关的标志位
TIM_ClearITPendingBit(TPAD_TIM, TPAD_TIM_IT_CCx | TIM_IT_Update);
// 等待捕获上升沿,当电容按键充电到1.8V左右的时候,就会被认为是上升沿
while(TIM_GetFlagStatus(TPAD_TIM, TPAD_TIM_IT_CCx) == RESET) {
// 如果超时了,直接返回CNT的值
// 一般充电时间都是在ms级别以内,很少会超过定时器的最大计数值
if (TIM_GetCounter(TPAD_TIM) > TPAD_TIM_Period-100) {
return TIM_GetCounter (TPAD_TIM);
}
}
// 获取捕获比较寄存器的值
return TPAD_TIM_GetCapturex_FUN(TPAD_TIM);
}
此乃重点,我们需要代码逐行分析:
uint16_t TPAD_Get_Val(void)
{
// 每次捕获的时候,必须先复位放电
TPAD_Reset();
- TPAD_Reset():复位电容按键。这个函数应该负责将电容放电,以确保测量从一个已知的初始状态开始。
// 当电容按键复位放电之后,计数器清0开始计数
TIM_SetCounter(TPAD_TIM, 0);
- TIM_SetCounter():将定时器计数器的值设置为
0
,以便开始新的测量。
// 清除相关的标志位
TIM_ClearITPendingBit(TPAD_TIM, TPAD_TIM_IT_CCx | TIM_IT_Update);
- TIM_ClearITPendingBit():清除定时器的中断标志位。
TPAD_TIM_IT_CCx
表示捕获比较中断标志,TIM_IT_Update
表示定时器更新中断标志。
// 等待捕获上升沿,当电容按键充电到1.8V左右的时候,就会被认为是上升沿
while(TIM_GetFlagStatus(TPAD_TIM, TPAD_TIM_IT_CCx) == RESET) {
// 如果超时了,直接返回CNT的值
// 一般充电时间都是在ms级别以内,很少会超过定时器的最大计数值
if (TIM_GetCounter(TPAD_TIM) > TPAD_TIM_Period-100) {
return TIM_GetCounter(TPAD_TIM);
}
}
- TIM_GetFlagStatus():检查捕获中断标志位是否已设置,标志位表示捕获事件是否发生。
- 超时处理:如果捕获事件未发生且计数器值超过了预设的超时阈值(
TPAD_TIM_Period-100
),则返回当前计数器的值。这个阈值是为了处理电容按键充电时间超时的情况。
// 获取捕获比较寄存器的值
return TPAD_TIM_GetCapturex_FUN(TPAD_TIM);
}
- TPAD_TIM_GetCapturex_FUN():从捕获比较寄存器中获取捕获值。这个函数应返回捕获事件发生时的计数器值。
- 获取最大输入捕获值
/**
* @brief 读取若干次定时器捕获值,并返回最大值。
* @param num :读取次数
* @retval 读取到的最大定时器捕获值
*/
uint16_t TPAD_Get_MaxVal(uint8_t num)
{
uint16_t temp = 0, res = 0;
while (num--) {
temp = TPAD_Get_Val();
if (temp > res)
res = temp;
}
return res;
}
该函数接收一个参数,用来指定获取电容按键捕获值的循环次数,函数的返回值则为num次发生捕获中最大的捕获值。
当我们用手指触摸电容按键的时候,常常会有干扰或者是误触发,所以我们一般选取最大的值为有效值。
- 电容按键状态扫描
/**
* @brief 按键扫描函数
* @param 无
* @retval 1:按键有效,0:按键无效
*/
uint8_t TPAD_Scan(void)
{
// keyen:按键检测使能标志
// 0:可以开始检测
// >0:还不能开始检测,表示按键一直被按下
// 注意:keytn 这个变量由 static修饰,相当于一个全局变量,但是因为是在函数内部定义,
// 所以是相当于这个函数的全局变量,每次修改之前保留的是上一次的值
static uint8_t keyen = 0;
uint8_t res = 0,sample = 3;
uint16_t scan_val;
// 根据sample值采样多次,并取最大值,小的一般是干扰或者是误触摸
scan_val = TPAD_Get_MaxVal(sample);
// 当扫描的值大于空载值加上默认的门限值之后,表示按键按下
// 这个TPAD_GATE_VAL根据硬件决定,需要实际测试
if (scan_val > (tpad_default_val + TPAD_GATE_VAL)) {
// 再次检测,类似于机械按键的去抖
scan_val = TPAD_Get_MaxVal(sample);
if ((keyen == 0)&& (scan_val > (tpad_default_val + TPAD_GATE_VAL)))
res = 1; // 有效的按键
// 如果按键一直被按下,keyen的值会一直在keyen的初始值和keyen-1之间循环,永远不会等于0
keyen = 2;
}
// 当按键没有被按下或者keyen > 0时,会执行keyen--
if (keyen > 0)
keyen--;
return res;
}
按键扫描函数不断的检测充电时间,当大于tpad_default_val+TPAD_GATE_VAL时,表示按键被按下,其中TPAD_GATE_VAL是一个宏, 具体多大需要实际测试。具体的我们可以通过调用TPAD_Get_Val()函数来测试按键有手触摸的情况下的充电值, 然后再减去tpad_default_val的值就可以得到TPAD_GATE_VAL,当减小这个门限值的时候可以提高按键的灵敏度。
在按键扫描函数中,我们引入了一个按键检测标志keyen,其由关键字static修饰,相当于一个全局变量, 每次修改这个变量的时候其保留的都是上一次的值。引入一个按键检测标志是为了消除按键是否一直按下的情况, 如果按键一直被按下keyen的值会一直在keyen的初始值和keyen-1之间循环,永远不会等于0,则永远都不会被认为按键按下, 需要等待释放。有关函数更加详细的说明看程序的注释即可。
- 主函数
int main(void)
{
// 蜂鸣器初始化
Beep_Init();
// 串口初始化
USART_Config();
printf("\r\n野火STM32 输入捕获电容按键检测实验\r\n");
printf("\r\n触摸电容按键,蜂鸣器则会响\r\n");
// 初始化电容按键
while (TPAD_Init());
while (1) {
if (TPAD_Scan() == TPAD_ON )
{
BEEP_ON();
SysTick_Delay_Ms(25);
BEEP_OFF();
}
}
}
主函数初始化了蜂鸣器和串口,然后等待电容按键初始化成功,如果不成功则会一直等待。初始化成功之后, 在一个while无限循环中不断的扫描按键,当按键按下之后蜂鸣器响25ms,然后关掉。
3. 小结
下面我们来回顾一下基本流程:
- 硬件连接:
- 将电容按键连接到 STM32F103 的输入引脚。
- 将定时器的输入通道(通常是一个捕获输入引脚)连接到电容按键的输出。
- 初始化 STM32F103 的定时器:
- 配置定时器的计数器和输入捕获功能。
- 设置定时器的捕获通道以响应电容按键的状态变化。
- 初始化 GPIO 引脚:
- 配置用于电容按键的 GPIO 引脚为输入模式。
- 配置用于定时器输入捕获的 GPIO 引脚为复用功能模式。
- 编写定时器和捕获功能的初始化代码:
- 设置定时器的预分频器、计数器周期等参数。
- 配置捕获模式(例如上升沿、下降沿或双边沿)。
- 编写中断服务程序(ISR)或轮询代码:
- 处理定时器中断以获取捕获值。
- 根据捕获的值来分析电容按键的状态。
下面是一个使用 STM32F103 定时器输入捕获功能的示例代码,展示了如何配置定时器来检测电容按键的状态。
1. 初始化代码
#include "stm32f10x.h"
// 初始化 GPIO 引脚
void GPIO_Init(void) {
// 启用 GPIOB 时钟(假设 TIM1 的通道连接到 GPIOB)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
// 配置 TIM1_CH1 引脚(假设为 GPIOB.6)为复用功能
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; // TIM1_CH1
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 推挽复用
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
// 初始化定时器
void TIM_Init(void) {
// 启用 TIM1 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 定时器基础配置
TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 自动重装载寄存器的值
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 时钟分频因子
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
// 配置输入捕获
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿捕获
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0;
TIM_ICInit(TIM1, &TIM_ICInitStructure);
// 使能定时器
TIM_Cmd(TIM1, ENABLE);
// 使能输入捕获中断
TIM_ITConfig(TIM1, TIM_IT_CC1, ENABLE);
// 配置 NVIC 中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM1_CC_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
2. 定时器中断服务程序
void TIM1_CC_IRQHandler(void) {
if (TIM_GetITStatus(TIM1, TIM_IT_CC1) != RESET) {
// 读取捕获值
uint16_t capture = TIM_GetCapture1(TIM1);
// 处理捕获值,例如计算电容按键的状态
// ...
// 清除中断标志位
TIM_ClearITPendingBit(TIM1, TIM_IT_CC1);
}
}
2024.9.11 第一次修订,后期不再维护