第9章 定时器输入捕获应用-电容触摸按键
第九章 定时器输入捕获应用-电容触摸按键
1. 电容触摸按键简介
与机械按键不同, 这里我们使用的是检测电容充放电时间的方法来判断是否有触摸。A、 B 分别表示有无人体按下时电容的充放电曲线。其中 R 是外接的电容充电电阻, Cs 是没有触摸按下时 TPAD 与 PCB 之间的杂散电容。而 Cx 则是有手指按下的时候,手指与TPAD 之间形成的电容。图中的开关是电容放电开关

先用开关将 Cs(或 Cs+Cx)上的电放尽,然后断开开关,让 R 给 Cs(或 Cs+Cx)充电,当没有手指触摸的时候, Cs 的充电曲线如图中的 A 曲线。而当有手指触摸的时候,手指和 TPAD之间引入了新的电容 Cx,此时 Cs+Cx 的充电曲线如图中的 B 曲线。从上图可以看出, A、 B 两种情况下, Vc 达到 Vth 的时间分别为 Tcs 和 Tcs+Tcx。
其中,除了 Cs 和 Cx 我们需要计算,其他都是已知的,根据电容充放电公式:

其中 Vc 为电容电压, V0 为充电电压, R 为充电电阻, C 为电容容值, e 为自然底数, t 为充电时间。根据这个公式,我们就可以计算出 Cs 和 Cx。
在本章中,其实我们只要能够区分 Tcs 和 Tcs+Tcx,就已经可以实现触摸检测了,当充电时间在 Tcs 附近,就可以认为没有触摸,而当充电时间大于 Tcs+Tx 时,就认为有触摸按下(Tx 为检测阀值)。
本章,我们使用 PA5(TIM2_CH1)来检测 TPAD 是否有触摸,在每次检测之前,我们先配置PA5 为推挽输出,将电容 Cs(或 Cs+Cx)放电,然后配置 PA5 为浮空输入,利用外部上拉电阻给电容 Cs(Cs+Cx)充电,同时开启 TIM2_CH1 的输入捕获,检测上升沿,当检测到上升沿的时候,就认为电容充电完成了,完成一次捕获检测。
在 MCU 每次复位重启的时候,我们执行一次捕获检测(可以认为没触摸),记录此时的值,记为 tpad_default_val,作为判断的依据。在后续的捕获检测,我们就通过与 tpad_default_val 的对比,来判断是不是有触摸发生。
2. 程序设计
2.1 相关参数宏定义
#ifndef __ATIM_H
#define __ATIM_H
#include "sys.h"
#define TPAD_GATE_VAL 50 // 触摸检测阈值
#define TPAD_ARR_MAX_VAL 0xFFFFFFFF // 定时器最大计数值
#define TPAD_DISCHARGE_TIME 10 // 放电时间(ms)
#define TPAD_SAMPLE_SIZE 20 // 初始化采样次数
#define TPAD_TRIM_SAMPLES 10 // 初始化有效样本数
// 函数声明
uint8_t tpad_scan(uint8_t mode);
uint8_t tpad_init(uint16_t psc);
void tim_cap_deinit(void);
#endif /* __ATIM_H */
2.2 TIM2输入捕获初始化
// 定时器输入捕获初始化
static void tim_cap_init(uint32_t arr, uint16_t psc)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能时钟
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置GPIO PA5 (TIM2_CH1)
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置定时器
tim_cap_handler.Instance = TIM2;
tim_cap_handler.Init.Prescaler = psc;
tim_cap_handler.Init.CounterMode = TIM_COUNTERMODE_UP;
tim_cap_handler.Init.Period = arr;
tim_cap_handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_IC_Init(&tim_cap_handler);
// 配置输入捕获
tim_cap_ic_init.ICPolarity = TIM_ICPOLARITY_RISING;
tim_cap_ic_init.ICSelection = TIM_ICSELECTION_DIRECTTI;
tim_cap_ic_init.ICPrescaler = TIM_ICPSC_DIV1;
tim_cap_ic_init.ICFilter = 0x8; // 增加输入滤波器
HAL_TIM_IC_ConfigChannel(&tim_cap_handler, &tim_cap_ic_init, TIM_CHANNEL_1);
// 启动输入捕获
HAL_TIM_IC_Start(&tim_cap_handler, TIM_CHANNEL_1);
}
2.3 TPAD放电
// 释放定时器资源
void tim_cap_deinit(void)
{
HAL_TIM_IC_Stop(&tim_cap_handler, TIM_CHANNEL_1);
HAL_TIM_IC_DeInit(&tim_cap_handler);
__HAL_RCC_TIM2_CLK_DISABLE();
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_5);
}
// 复位TPAD(放电)
static void tpad_reset(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 配置为推挽输出放电
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
delay_ms(TPAD_DISCHARGE_TIME);
// 复位定时器
tim_cap_handler.Instance->SR = 0;
tim_cap_handler.Instance->CNT = 0;
// 重新配置为输入捕获
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
2.4 获取单次捕获值
// 获取单次捕获值(带超时保护)
static uint16_t tpad_get_val(void)
{
uint32_t timeout = 0;
const uint32_t max_timeout = 1000000; // 超时保护
tpad_reset();
while(__HAL_TIM_GET_FLAG(&tim_cap_handler, TIM_FLAG_CC1) == RESET)
{
if((tim_cap_handler.Instance->CNT > TPAD_ARR_MAX_VAL - 500) ||
(timeout++ > max_timeout))
{
return tim_cap_handler.Instance->CNT;
}
}
return TIM2->CCR1;
}
2.5 获取多次捕获中最大值
// 获取多次捕获中的最大值
static uint16_t tpad_get_max(uint8_t n)
{
uint16_t max_val = 0;
while(n--)
{
uint16_t val = tpad_get_val();
if(val > max_val) max_val = val;
delay_ms(1); // 采样间隔
}
return max_val;
}
2.6 按键扫描
// 电容按键扫描
uint8_t tpad_scan(uint8_t mode)
{
static uint8_t keyen = 0;
static uint16_t last_val = 0; // 上次检测值
uint16_t val = tpad_get_max(5); // 采样5次取最大值
if(val > (tpad_default_val + TPAD_GATE_VAL))
{
// 检测上升趋势(防止干扰)
if(val > last_val + 5)
{
if(keyen == 0) // 首次触发
{
keyen = 3; // 设置去抖时间
last_val = val;
return 1;
}
}
last_val = val;
}
else
{
last_val = val;
}
// 按键状态更新
if(keyen) keyen--;
return 0;
}
2.7 初始化TPAD采样
// 初始化电容按键
uint8_t tpad_init(uint16_t psc)
{
uint16_t buf[TPAD_SAMPLE_SIZE];
uint32_t temp = 0;
tim_cap_init(TPAD_ARR_MAX_VAL, psc - 1);
// 多次采样
for(uint8_t i = 0; i < TPAD_SAMPLE_SIZE; i++)
{
buf[i] = tpad_get_val();
delay_ms(5);
}
// 冒泡排序(升序)
for(uint8_t i = 0; i < TPAD_SAMPLE_SIZE - 1; i++)
{
for(uint8_t j = 0; j < TPAD_SAMPLE_SIZE - 1 - i; j++)
{
if(buf[j] > buf[j+1])
{
uint16_t tmp = buf[j];
buf[j] = buf[j+1];
buf[j+1] = tmp;
}
}
}
// 取中间样本计算平均值
const uint8_t start_idx = (TPAD_SAMPLE_SIZE - TPAD_TRIM_SAMPLES) / 2;
for(uint8_t i = start_idx; i < start_idx + TPAD_TRIM_SAMPLES; i++)
{
temp += buf[i];
}
tpad_default_val = temp / TPAD_TRIM_SAMPLES;
printf("TPAD Default Value: %u\r\n", tpad_default_val);
// 检查基准值是否合理
if(tpad_default_val > TPAD_ARR_MAX_VAL / 2)
{
printf("Error: Invalid baseline value!\r\n");
return 1;
}
printf("TPAD Initialized. Sensitivity: %d\r\n", TPAD_GATE_VAL);
return 0;
}
2.8 主函数测试
#include "bsp_init.h"
#include "atim.h"
#include "stdio.h"
int main(void)
{
bsp_init();
printf("System Initialized\r\n");
// 电容按键初始化(分频系数2,计数频率=84MHz/2=42MHz)
if(tpad_init(2))
{
printf("TPAD Init Failed!\r\n");
while(1); // 初始化失败,停机
}
uint32_t last_led_toggle = 0;
while(1)
{
if(tpad_scan(0))
{
LED_TOGGLE(LED1_GPIO_Pin);
printf("Touch Detected!\r\n");
}
if(HAL_GetTick() - last_led_toggle > 500)
{
LED_TOGGLE(LED0_GPIO_Pin);
last_led_toggle = HAL_GetTick();
}
delay_ms(50);
}
}

浙公网安备 33010602011771号