30. 电容触摸按键

一、电容触摸按键简介

  与机械按键不同,这里我们使用的是检测电容充放电时间的方法来判断是否有触摸,图中的 A、B 分别表示有无人体按下时电容的充放电曲线。其中 R 是外接的电容充电电阻,Cs 是没有触摸按下时 TPAD 与 PCB 之间的杂散电容。而 Cx 则是有手指按下的时候,手指与TPAD 之间形成的电容。图中的开关是电容放电开关(实际使用时,由 STM32F407 的 IO 代替),\(V_{th}\) 是 STM32 认为高电平的最低电压值(1.8V)。

电容触摸按键

  先用开关将 \(C_{s}\)(或 \(C_{s} + C_{x}\))上的电放尽,然后断开开关,让 R 给 \(C_{s}\)(或 \(C_{s}+C_{x}\))充电,当没有手指触摸的时候,\(C_{s}\) 的充电曲线如图中的 A 曲线。而当有手指触摸的时候,手指和 TPAD 之间引入了新的电容 \(C_{x}\),此时 \(C_{s} + C_{x}\) 的充电曲线如图中的 B 曲线。从上图可以看出,A、B 两种情况下,\(V_{c}\) 达到 \(V_{th}\) 的时间分别为 \(T_{cs}\)\(T_{cs} + T_{cx}\)

  其中,除了 \(C_{s}\)\(C_{x}\) 我们需要计算,其他都是已知的,根据电容充放电公式:

\[V_{c} = V_{0} * (1 - e^{-\frac{t}{RC}}) \]

  其中 \(V_{c}\) 为电容电压,\(V_{0}\) 为充电电压,R 为充电电阻,C 为电容容值,e 为自然底数,t 为充电时间。根据这个公式,我们就可以计算出 \(C_{s}\)\(C_{x}\)

  当充电时间在 \(T_{cs}\) 附近,就可以认为没有触摸,而当充电时间大于 \(T_{cs} + T_{x}\) 时,就认为有触摸按下(\(T_{x}\) 为检测阀值)。

二、电容触摸按键检测过程

  1. TPAD 引脚设置为推挽输出,输出低电平,实现电容放电到地。
  2. TPAD 引脚设置为浮空输入,电容开始充电。
  3. 同时开启 TPAD 引脚的输入捕获功能,开始捕获高电平。
  4. 等待充电过程中,上升沿触发(充电到 \(V_{th}\))。
  5. 计算充电时间(定时器捕获/比较寄存器获取)

  没有按下的时候,充电时间为 \(T_{1}\)。按下 TPAD,电容变大,所以充电时间为 \(T_{2}\)。我们可以通过检测充电时间来判断是否按下。如果 \(T_{2} - T{1}\) 大于某个值,就可以判断触摸按键按下。

三、原理图

触摸按键模块

触摸按键引脚接线端子

触摸按键引脚接线图

  通过原理图,我们可得使用 PA5(TIM2_CH1)来检测 TPAD 是否有触摸,在每次检测之前,我们先配置 PA5 为推挽输出,将电容 Cs(或 Cs+Cx)放电,然后配置 PA5 为浮空输入,利用外部上拉电阻给电容 Cs(Cs+Cx) 充电,同时开启 TIM2_CH1 的输入捕获,检测上升沿,当检测到上升沿的时候,就认为电容充电完成了,完成一次捕获检测。

  在 MCU 每次复位重启的时候,我们执行一次捕获检测(可以认为没触摸),记录此时的值,记为 tpad_default_value,作为判断的依据。在后续的捕获检测,我们就通过与 tpad_default_value 的对比,来判断是不是有触摸发生。

四、程序源码

4.1、定时器初始化

  定时器初始化函数:

TIM_HandleTypeDef g_tim2_handle;

/**
 * @brief 定时器输入捕获初始化函数
 * 
 * @param htim 定时器句柄
 * @param TIMx 定时器寄存器基地址,可选值: TIMx, x可选范围: 1 ~ 5, 8 ~ 14
 * @param prescaler 预分频系数,可选值: 0 ~ 65535
 * @param period 自动重装载值,可选值: 0 ~ 65535
 * @param channel 输入捕获的通道,可选值: TIM_CHANNEL_x, x可选范围: 1 ~ 4
 * @param polarity 捕获极性,可选值: [TIM_ICPOLARITY_RISING, TIM_ICPOLARITY_FALLING, TIM_ICPOLARITY_BOTHEDGE]
 */
void TIM_IC_Init(TIM_HandleTypeDef *htim, TIM_TypeDef *TIMx, uint16_t prescaler, uint16_t period, uint32_t channel, uint32_t polarity)
{
    TIM_IC_InitTypeDef TIM_IC_InitStruct = {0};

    htim->Instance = TIMx;                                                      // 定时器寄存器基地址
    htim->Init.CounterMode = TIM_COUNTERMODE_UP;                                // 计数模式
    htim->Init.Prescaler = prescaler;                                           // 预分频系数
    htim->Init.Period = period;                                                 // 自动重装载值
    HAL_TIM_IC_Init(htim);

    TIM_IC_InitStruct.ICPolarity = polarity;                                    // 捕获极性
    TIM_IC_InitStruct.ICSelection = TIM_ICSELECTION_DIRECTTI;                   // 直接映射
    TIM_IC_InitStruct.ICPrescaler = TIM_ICPSC_DIV1;                             // 1分频
    TIM_IC_InitStruct.ICFilter = 0;                                             // 不滤波
    HAL_TIM_IC_ConfigChannel(htim, &TIM_IC_InitStruct, channel);
}

  定时器输入捕获底层初始化函数

/**
 * @brief 定时器输入捕获底层初始化函数
 * 
 * @param htim 定时器句柄
 */
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    if (htim->Instance == TIM2)
    {
        __HAL_RCC_TIM2_CLK_ENABLE();                                            // 使能TIM2的时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();                                           // 使能TIM2的Channel 1对应的GPIO时钟

        GPIO_InitStruct.Pin = GPIO_PIN_5;                                       // TIM2的Channel 1对应的GPIO引脚
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;                                 // 复用功能
        GPIO_InitStruct.Pull = GPIO_NOPULL;                                     // 使用下拉
        GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;                              // 复用功能选择
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    } 
}

4.2、按键初始化

  电容触摸按键初始化函数:

uint16_t g_tpad_default_value = 0;

/**
 * @brief 电容触摸按键初始化函数
 * 
 */
void TPAD_Init(void)
{
    int buff[10] = {0};
    uint8_t temp = 0;

    // 循环取10次值
    for (uint8_t i = 0; i < 10; i++)
    {
        buff[i] = TPAD_GetValue();
        HAL_Delay(10);
    }

    // 冒泡排序
    BubbleSort(buff, sizeof(buff)/sizeof(buff[0]));

    // 对中间6个值取平均
    for (uint8_t i = 2; i < 8; i++)
    {
        temp += buff[i];
    }

    g_tpad_default_value = temp / 6;
}

  我们通过控制变量法去检测 TPAD 电容触摸按键按下和没有按下的情况。每次先给 TPAD 放电(STM32 输出低电平)相同时间,然后释放,检测 VCC 每次给 TPAD 的充电时间,由此可以得到一个充电时间。

/**
 * @brief 电容触摸按键复位函数
 * 
 */
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_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);

    HAL_Delay(5);

    g_tpad_tim_handle_ptr->Instance->SR = 0;                                    // 清除标记
    g_tpad_tim_handle_ptr->Instance->CNT = 0;                                   // 清空计数器

    // 复用功能
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
#define TPAD_MAX_VALUE  0xFFFF

/**
 * @brief 获取定时器捕获值函数
 * 
 * @return uint16_t 定时器的捕获值
 */
static uint16_t TPAD_GetValue(void)
{
    TPAD_Reset();

    while (__HAL_TIM_GET_FLAG(g_tpad_tim_handle_ptr, TIM_FLAG_CC1) == RESET)    // 检测通道捕获到上升沿
    {
        // 超时了直接返回计数器寄存器的值
        if (g_tpad_tim_handle_ptr->Instance->CNT > TPAD_MAX_VALUE  - 500)
        {
            return g_tpad_tim_handle_ptr->Instance->CNT;
        }
    }
    // 返回捕获/比较寄存器的值
    return g_tpad_tim_handle_ptr->Instance->CCR1;
}

  TPAD_Reset() 函数,该函数顾名思义就是进行一次复位操作。先设置 PA5 输出低电平,电容放电,同时清除中断标志位并且计数器值清零,然后配置 PA5 为复用功能浮空输入,利用外部上拉电阻给电容 C 充电,同时开启 TIM2_CH1 的的输入捕获。

  而实现充电时间的获取主要是通过调用 TPAD_GetValue() 函数。函数 TPAD_GetValue() 得到定时器的一次捕获值,函数内部先调用 TPAD_Reset(),复位 TPAD 电容触摸按键,让其处于被充电状态,后面捕获到上升沿就把捕获到的值(或溢出值)作为返回值返回。

  得到充电时间后,接下来我们要做的就是获取没有按下 TPAD 时的充电时间,并把它作为基准来确认后续有无按下操作,我们定义全局变量 g_tpad_default_value 来保存这个值,通过多次平均的滤波算法来减小误差。

/**
 * @brief 冒泡排序函数
 * 
 * @param data 待排序的数组
 * @param length 待排序数组的长度
 */
void BubbleSort(int data[], int length)
{
    for (int i = 0; i < length; i++)
    {
        for (int j = 0; j < length - 1 - i; j++)
        {
            if (data[j] > data[j + 1])
            {
                int temp = data[j + 1];
                data[j + 1] = data[j];
                data[j] = temp;
            }
        }
    }
}
#define TPAD_GATE_VALUE 10                                                      // 修改此值,控制电容触摸按键灵敏度

/**
 * @brief 电容触摸按键扫描函数
 * 
 * @param mode 0:不支持连续按;1:支持连续按
 * @return uint8_t 0:未按下;1:按下
 */
uint8_t TPAD_Scan(uint8_t mode)
{
    static uint8_t keyen = 0;
    uint8_t result = 0;
    uint8_t sample = 3;                                                         // 默认采样次数为3
    uint8_t value = 0;

    if (mode)
    {
        sample = 6;                                                             // 支持连按的时候,设置采样次数为6次
        keyen = 0;                                                              // 支持连按,每次调用该函数都可以检测
    }

    value = TPAD_GetMaxValue(sample);
  
    if (value > (g_tpad_default_value + TPAD_GATE_VALUE))                       // 有效按下
    {
        result = (keyen == 0) ? 1 : result;                                     // keyen==0时表示有按键按下
        keyen = 3;
    }
    keyen ? --keyen : keyen;
  
    return result;
}

  函数 TPAD_Scan() 用于扫描 TPAD 是否有触摸,该函数的参数 mode,用于设置是否支持连续触发。返回值如果是 0,说明没有触摸,如果是 1,则说明有触摸。该函数同样包含了一个静态变量,用于检测控制。

  在函数中,我们通过连续读取 3 次(不支持连续按的时候)TPAD 的值,取最大值和 g_tpad_default_value 作比较,如果他们的差值大于 TPAD_GATE_VALUE 阈值(自定义的阈值)说明有触摸,如果小于则说明无触摸。上述取最大值的操作会依靠 TPAD_GetMaxVluae() 函数,通过 n 次调用 TPAD_GetValue() 采集捕获值,然后进行比较后获取 n 次采样值中的最大值。

/**
 * @brief 获取最大值函数
 * 
 * @param n 数据个数
 * @return uint16_t 最大的值
 */
static uint16_t TPAD_GetMaxValue(uint8_t n)
{
    uint16_t temp = 0;
    uint16_t maxValue = 0;

    while (n--)
    {
        temp = TPAD_GetValue();
        maxValue = (temp > maxValue) ? temp : maxValue;
    }
    return maxValue;
}

4.3、main()函数

  main() 函数内容如下:

int main(void)
{
    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);

    TIM_IC_Init(&g_tim2_handle, TIM2, 83, 0xFFFF, TIM_CHANNEL_1, TIM_ICPOLARITY_RISING);

    HAL_TIM_IC_Start(&g_tim2_handle, TIM_CHANNEL_1);                            // 使能通道输入以及使能捕获中断

    g_tpad_tim_handle_ptr = &g_tim2_handle;

    LED_Init();
    TPAD_Init();

    while (1)
    {
        if (TPAD_Scan(0))
        {
            HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
        }
        HAL_Delay(10);
    }
  
    return 0;
}
posted @ 2023-12-18 21:46  星光映梦  阅读(44)  评论(0编辑  收藏  举报