第9章 定时器输入捕获应用-电容触摸按键

第九章 定时器输入捕获应用-电容触摸按键

1. 电容触摸按键简介

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

屏幕截图 20250719 163700png

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

其中,除了 Cs 和 Cx 我们需要计算,其他都是已知的,根据电容充放电公式:

屏幕截图 20250719 163823png

其中 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);
    }
}


posted @ 2025-07-19 17:43  hazy1k  阅读(12)  评论(0)    收藏  举报