第11章 电容触摸按键实验

第十一章 电容触摸按键实验

1. 硬件设计

本实验用到的硬件资源有:

  • 指示灯DS0和DS1

  • 定时器TIM2

  • 触摸按键TPAD

前面两个之前均有介绍,我们需要通过 TIM2_CH1(PA5)采集 TPAD 的信号,所以本实验需要用跳线帽短接多功能端口(P12)的 TPAD 和 ADC,以实现 TPAD 连接到 PA5。

屏幕截图 2024 09 18 083717

硬件设置(用跳线帽短接多功能端口的 ADC 和 TPAD 即可)好之后,下面我们开始软件设计。

2. 软件设计

2.1 TIM2通道1配置输入捕获

// 定时器2通道1输入捕获配置
// arr:自动重装值(TIM2是32位的!!)
// psc:时钟预分频数
void TIM2_CH1_Cap_Init(u32 arr,u16 psc)
{  
    TIM_IC_InitTypeDef TIM2_CH1Config;  

    TIM2_Handler.Instance = TIM2;                        // 通用定时器2
    TIM2_Handler.Init.Prescaler = psc;                   // 分频
    TIM2_Handler.Init.CounterMode = TIM_COUNTERMODE_UP;  // 向上计数器
    TIM2_Handler.Init.Period = arr;                      // 自动装载值
    TIM2_Handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 不分频
    HAL_TIM_IC_Init(&TIM2_Handler);

    // 输入捕获配置
    TIM2_CH1Config.ICPolarity = TIM_ICPOLARITY_RISING;    // 上升沿捕获
    TIM2_CH1Config.ICSelection = TIM_ICSELECTION_DIRECTTI;// 映射到TI1上
    TIM2_CH1Config.ICPrescaler = TIM_ICPSC_DIV1;          // 配置输入分频,不分频
    TIM2_CH1Config.ICFilter = 0;                          // 配置输入滤波器,不滤波
    HAL_TIM_IC_ConfigChannel(&TIM2_Handler,&TIM2_CH1Config,TIM_CHANNEL_1); // 配置TIM2通道1
    HAL_TIM_IC_Start(&TIM2_Handler,TIM_CHANNEL_1);        // 开始捕获TIM2的通道1
}

// 定时器2底层驱动,时钟使能,引脚配置
// 此函数会被HAL_TIM_IC_Init()调用
// htim:定时器2句柄
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
    GPIO_InitTypeDef GPIO_Initure;
    __HAL_RCC_TIM2_CLK_ENABLE();        // 使能TIM2时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();        // 开启GPIOA时钟

    GPIO_Initure.Pin=GPIO_PIN_5;         // PA5
    GPIO_Initure.Mode=GPIO_MODE_AF_PP;   // 推挽复用
    GPIO_Initure.Pull=GPIO_NOPULL;       // 不带上下拉
    GPIO_Initure.Speed=GPIO_SPEED_HIGH;  // 高速
    GPIO_Initure.Alternate=GPIO_AF1_TIM2;// PA5复用为TIM2通道1
    HAL_GPIO_Init(GPIOA,&GPIO_Initure);
}

函数TIM2_CH1_Cap_Init和上一章输入捕获实验中函数TIM5_CH1_Cap_Init的配置过程几乎是一模一样的,不同的是上一章实验开 TIM5_CH1_Cap_Init 函数最后调用的是函数HAL_TIM_IC_Start_IT,使能输入捕获通道的同事开启了输入捕获中断,而该函数最后调用的函数是 HAL_TIM_IC_Start,只是开启了输入捕获通道,并没有开启输入捕获中断。

函数 HAL_TIM_IC_MspInit 是输入捕获通用 MSP 回调函数,该函数的作用是使能定时器和 GPIO 时钟,配置 GPIO 复用映射关系。该函数功能和输入捕获实验中该函数作用基本类似。

2.2 复位电容按键和定时器

// 复位一次
// 释放电容电量,并清除定时器的计数值
void TPAD_Reset(void)
{
    GPIO_InitTypeDef GPIO_Initure;

    GPIO_Initure.Pin = GPIO_PIN_5;           // PA5
    GPIO_Initure.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
    GPIO_Initure.Pull = GPIO_PULLDOWN;       // 下拉
    GPIO_Initure.Speed = GPIO_SPEED_HIGH;    // 高速
    HAL_GPIO_Init(GPIOA,&GPIO_Initure);
    HAL_GPIO_WritePin(GPIOA,GPIO_PIN_5,GPIO_PIN_RESET); // PA5输出0,放电
    delay_ms(5); // 简单延时
    __HAL_TIM_CLEAR_FLAG(&TIM2_Handler,TIM_FLAG_CC1|TIM_FLAG_UPDATE); // 清除标志位
    __HAL_TIM_SET_COUNTER(&TIM2_Handler,0); // 计数器值归0

    GPIO_Initure.Mode = GPIO_MODE_AF_PP;     // 推挽复用
    GPIO_Initure.Pull = GPIO_NOPULL;         // 不带上下拉
    GPIO_Initure.Alternate = GPIO_AF1_TIM2;  // PA5复用为TIM2通道1
    HAL_GPIO_Init(GPIOA,&GPIO_Initure);         
}

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

2.3 取得定时器捕获值

// 得到定时器捕获值
// 如果超时,则直接返回定时器的计数值.
// 返回值:捕获值/计数值(超时的情况下返回)
u16 TPAD_Get_Val(void)
{
    TPAD_Reset(); // 首先复位,确保无干扰
    while(__HAL_TIM_GET_FLAG(&TIM2_Handler,TIM_FLAG_CC1) == RESET) //等待捕获上升沿
    {
        if(__HAL_TIM_GET_COUNTER(&TIM2_Handler) > TPAD_ARR_MAX_VAL-500) 
            return __HAL_TIM_GET_COUNTER(&TIM2_Handler); // 超时了,直接返回CNT的值
    };
    return HAL_TIM_ReadCapturedValue(&TIM2_Handler, TIM_CHANNEL_1); // 读取捕获值
}

这段代码我们可以详细解释一下:

u16 TPAD_Get_Val(void)
{
    TPAD_Reset(); // 首先复位,确保无干扰
  • TPAD_Reset():调用这个函数来重置TPAD(触摸按键)相关的设置,以确保在读取值时没有干扰。
    while(__HAL_TIM_GET_FLAG(&TIM2_Handler,TIM_FLAG_CC1) == RESET) //等待捕获上升沿
    {
  • 进入一个循环,直到定时器2的捕获比较寄存器1(CC1)产生上升沿。__HAL_TIM_GET_FLAG是一个宏,用于检查定时器的状态。
        if(__HAL_TIM_GET_COUNTER(&TIM2_Handler) > TPAD_ARR_MAX_VAL-500) 
            return __HAL_TIM_GET_COUNTER(&TIM2_Handler); // 超时了,直接返回CNT的值
  • 在循环中,检查定时器的当前计数值是否超过设定的最大值减去500,这里假设TPAD_ARR_MAX_VAL是一个定义的最大计数值。如果超出这个范围,说明发生了超时。
  • 如果超时,调用__HAL_TIM_GET_COUNTER获取当前计数值并返回。
    };
    return HAL_TIM_ReadCapturedValue(&TIM2_Handler, TIM_CHANNEL_1); // 读取捕获值
}
  • 如果在超时之前捕获到上升沿,程序将跳出循环,并通过HAL_TIM_ReadCapturedValue读取捕获的值,然后返回该值。

2.4 电容按键初始化

// 初始化触摸按键
// 获得空载的时候触摸按键的取值.
// psc:分频系数,越小,灵敏度越高.
// 返回值:0,初始化成功;1,初始化失败
u8 TPAD_Init(u8 psc)
{
    u16 buf[10]; // 10个数据进行排序
    u16 temp;    // 临时变量
    u8 j,i;      // 循环变量
    TIM2_CH1_Cap_Init(TPAD_ARR_MAX_VAL, psc-1); // 设置分频系数
    for(i = 0; i < 10; i++) // 连续读取10次
    {                 
        buf[i] = TPAD_Get_Val(); // 读取一次值
        delay_ms(10);        
    }                    
    for(i = 0; i < 9; i++)
    {
        for(j = i+1; j < 10; j++)
        {
            if(buf[i] > buf[j]) // 升序排列
            {
                temp = buf[i];
                buf[i] = buf[j];
                buf[j] = temp;
            }
        }
    }
    temp = 0;
    for(i = 2; i < 8; i++)
        temp += buf[i]; // 取中间的8个数据进行平均
    tpad_default_val = temp/6; // 再对这中间6个数取平均值
    printf("tpad_default_val:%d\r\n",tpad_default_val);    
    if(tpad_default_val > (vu16)TPAD_ARR_MAX_VAL/2)
        return 1; // 初始化遇到超过TPAD_ARR_MAX_VAL/2的数值,不正常!
    return 0;                                            
}

函数 TPAD_Init 用于初始化输入捕获,并获取默认的 TPAD 值。该函数有一个参数,用来传递分频系数,其实是为了配置 TIM2_CH1_Cap_Init 的计数周期。在该函数中连续 10 次读取TPAD 值,将这些值升序排列后取中间 6 个值再做平均(这样做的目的是尽量减少误差),并赋值给 tpad_default_val,用于后续触摸判断的标准。


电容按键初始化及其下面马上要介绍的电容按键扫描很重要,就是我们整个工程的核心,下面我们分步骤解释一下:

u8 TPAD_Init(u8 psc)
{
  • 函数定义:这是一个名为 TPAD_Init 的函数,它接受一个参数 psc(分频系数),并返回一个 u8 类型的值(通常表示成功或失败)。
    u16 buf[10]; // 10个数据进行排序
    u16 temp;    // 临时变量
    u8 j, i;      // 循环变量
  • 变量声明
    • buf[10]:用于存储 10 次读取的触摸传感器的值。
    • temp:临时变量,用于在排序中交换值。
    • j 和 i:循环变量。
    TIM2_CH1_Cap_Init(TPAD_ARR_MAX_VAL, psc - 1); // 设置分频系数
  • 初始化定时器:调用 TIM2_CH1_Cap_Init 函数,设置定时器及其分频系数。TPAD_ARR_MAX_VAL 是一个常量,代表最大计数值,psc - 1 表示所选择的分频系数(越小灵敏度越高)。
    for(i = 0; i < 10; i++) // 连续读取10次
    {                 
        buf[i] = TPAD_Get_Val(); // 读取一次值
        delay_ms(10);        
    }                    
  • 读取数据
    • 这个循环执行 10 次,每次调用 TPAD_Get_Val() 函数读取触摸传感器的值,并将其存储在 buf 数组中。
    • 每次读取后,程序会暂停 10 毫秒,以确保传感器有时间稳定下来。
    for(i = 0; i < 9; i++)
    {
        for(j = i + 1; j < 10; j++)
        {
            if(buf[i] > buf[j]) // 升序排列
            {
                temp = buf[i];
                buf[i] = buf[j];
                buf[j] = temp;
            }
        }
    }
  • 排序数据
    • 这段代码使用了简单的冒泡排序算法,将 buf 数组中的 10 个值按升序排列。
    • 外层循环遍历数组的每个元素,内层循环用于比较和交换元素。
    temp = 0;
    for(i = 2; i < 8; i++)
        temp += buf[i]; // 取中间的8个数据进行平均
  • 计算平均值
    • 将排序后的数组中索引为 2 到 7 的 6 个值相加(即中间的 6 个值),以减少极端值的影响。
    tpad_default_val = temp / 6; // 再对这中间6个数取平均值
  • 计算最终默认值
    • 计算的总和除以 6,得到 tpad_default_val,这是触摸传感器的默认值。
    printf("tpad_default_val:%d\r\n", tpad_default_val);
  • 输出默认值:打印出计算得到的 tpad_default_val,便于调试。
    if(tpad_default_val > (vu16)TPAD_ARR_MAX_VAL / 2)
        return 1; // 初始化遇到超过TPAD_ARR_MAX_VAL/2的数值,不正常!
  • 检查有效性
    • 如果 tpad_default_val 超过了 TPAD_ARR_MAX_VAL / 2,则认为初始化失败,返回 1。
    return 0;                                            
}
  • 返回成功:如果没有问题,则返回 0,表示初始化成功。

流程

  1. 初始化定时器。
  2. 读取触摸传感器的值 10 次。
  3. 对这些值进行排序。
  4. 计算中间 6 个值的平均值。
  5. 检查这个平均值是否合理。

2.5 电容按键扫描

// 扫描触摸按键
// mode:0,不支持连续触发(按下一次必须松开才能按下一次);1,支持连续触发(可以一直按下)
// 返回值:0,没有按下;1,有按下;                                          
u8 TPAD_Scan(u8 mode)
{
    static u8 keyen = 0; // 0,可以开始检测;>0,还不能开始检测     
    u8 res = 0;
    u8 sample = 3; // 默认采样次数为3次     
    u16 rval;
    if(mode)
    {
        sample = 6;    // 支持连按的时候,设置采样次数为6次
        keyen = 0;    // 支持连按      
    }
    rval = TPAD_Get_MaxVal(sample); // 采样n次 
    if(rval>(tpad_default_val*4/3)&&rval<(10*tpad_default_val))
    // 大于tpad_default_val+(1/3)*tpad_default_val,且小于10倍tpad_default_val,则有效
    {                             
        if(keyen==0)
            res = 1; // keyen==0,有效 
        //printf("r:%d\r\n",rval);                                            
        keyen = 3; // 之后至少要再过3次之后才能按键有效   
    } 
    if(keyen)
        keyen--; // 计数器减1                                                                                      
    return res;
}    

函数 TPAD_Scan 用于扫描 TPAD 是否有触摸,该函数的参数 mode,用于设置是否支持连续触发。返回值如果是 0,说明没有触摸,如果是 1,则说明有触摸。该函数同样包含了一个静态变量,用于检测控制,类似第七章的 KEY_Scan 函数。所以该函数同样是不可重入的。在函数中,我们通过连续读取 3 次(不支持连续按的时候)TPAD 的值,取最大值和 tpad_default_val*4/3比较,如果大于则说明有触摸,如果小于,则说明无触摸。其中 tpad_default_val 是我们在调用TPAD_Init 函数的时候得到的值,然后取其 4/3 为门限值。


同样这个函数值得我们分步骤讲解一下:

函数定义

u8 TPAD_Scan(u8 mode)
  • 这是一个返回 u8 类型值的函数,接收一个参数 mode,用于设置按键的触发模式。

静态变量

static u8 keyen = 0; // 0,可以开始检测;>0,还不能开始检测     
  • keyen 是一个静态变量,用于控制按键检测的状态。初始值为 0,表示可以开始检测。

变量声明

u8 res = 0;
u8 sample = 3; // 默认采样次数为3次     
u16 rval;
  • res 用于存储返回值,初始为 0,表示没有按下。
  • sample 表示采样次数,默认为 3 次。

模式判断

if(mode)
{
    sample = 6;    // 支持连按的时候,设置采样次数为6次
    keyen = 0;    // 支持连按      
}

采样获取最大值

rval = TPAD_Get_MaxVal(sample); // 采样n次 
  • 调用 TPAD_Get_MaxVal 函数进行 sample 次的采样,并将结果存储在 rval 中。

有效性检测

if(rval>(tpad_default_val*4/3)&&rval<(10*tpad_default_val)) // 大于tpad_default_val+(1/3)*tpad_default_val,且小于10倍tpad_default_val,则有效
{                             
    if(keyen==0)
        res = 1; // keyen==0,有效 
    keyen = 3; // 至少要再过3次之后才能按键有效   
} 
  • 检查 rval 是否在有效范围内:
    • 大于 tpad_default_val 的 4/3 倍,小于 tpad_default_val 的 10 倍。
  • 如果有效并且 keyen 为 0,则将返回值 res 设置为 1,表示按键被按下。
  • 将 keyen 设置为 3,意味着需要经过 3 次扫描后才能再次确认按键有效。

计数器递减

if(keyen)
    keyen--; // 计数器减1                                                                                      
  • 如果 keyen 大于 0,每次调用函数时将其减 1,以便在下一次调用时检查状态。

返回值

return res;
  • 返回 res,如果返回值为 1,表示触摸按键被按下;返回 0,表示未按下。

2.6 多次采样取得最大捕获值

// 读取n次,取最大值
// n:连续获取的次数
// 返回值:n次读数里面读到的最大读数值
u16 TPAD_Get_MaxVal(u8 n)
{ 
    u16 temp = 0; 
    u16 res = 0; 
    u8 lcntnum = n*2/3; // 至少2/3*n的有效个触摸,才算有效
    u8 okcnt = 0;
    while(n--)
    {
        temp = TPAD_Get_Val(); // 得到一次值
        if(temp > (tpad_default_val*5/4))
            okcnt++; // 至少大于默认值的5/4才算有效
        if(temp > res)
            res = temp;
    }
    if(okcnt >= lcntnum)
        return res; // 至少2/3的概率,要大于默认值的5/4才算有效
    else 
        return 0;
}  

函数 TPAD_Get_MaxVal 就非常简单了,它通过 n 次调用函数 TPAD_Get_Val 采集捕获值,然后进行比较后获取 n 次采集值中的最大值。

  • 主函数
int main(void)
{
    u8 t=0;
    HAL_Init(); //初始化 HAL 库
    Stm32_Clock_Init(336,8,2,7); //设置时钟,168Mhz
    delay_init(168); //初始化延时函数
    uart_init(115200); //初始化 USART
    LED_Init(); //初始化 LED
    TPAD_Init(8); //初始化触摸按键
    while(1)
    {
        if(TPAD_Scan(0)) // 成功捕获到了一次上升沿(此函数执行时间至少 15ms)
        {
            LED1 =! LED1; //LED1 取反
        }
        t++;
        if(t == 15) // 超时处理
        {
            t = 0; 
            LED0=!LED0; //LED0 取反,提示程序正在运行
        }
        delay_ms(10);
    }
}

该main 函数比较简单, TPAD_Init(8)函数执行之后,就开始触摸按键的扫描,当有触摸的时候,对 DS1 取反,而 DS0 则有规律的间隔取反,提示程序正在运行。

3. 小结

实际上这个实验就是运用的定时器的输入捕获功能,下面简单回顾一下:

硬件连接

  1. LED连接
  • 将LED1(比如连接到GPIOA的引脚5)连接到STM32的相应引脚。
  1. 电容按键连接
  • 使用一个电容按键(如连接到GPIOB的引脚6),并设置为输入模式。
  1. 定时器设置
  • 使用TIM2设置为输入捕获模式,监测电容按键的按下事件。

软件配置步骤

1. 初始化工程

  • 使用STM32CubeMX创建一个新的STM32F407项目。
  • 启用GPIOAGPIOB,配置PA5为输出(LED),PB6为输入(电容按键)。
  • 启用TIM2,设置为输入捕获模式,选择适当的通道(例如通道1)。
  • 生成代码并打开Keil/STM32CubeIDE进行代码编写。

2. 编写代码

main.c文件中添加以下代码:

#include "main.h"

TIM_HandleTypeDef htim2;
GPIO_InitTypeDef GPIO_InitStruct;

// LED状态
uint8_t ledState = 0;

// 输入捕获回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        // 触发LED状态取反
        ledState ^= 1; // 取反
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, ledState); // 更新LED状态
    }
}

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM2_Init(void);

// 主函数
int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM2_Init();
    
    // 启动输入捕获
    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);

    while (1) {
        // 主循环可以做其他事情
    }
}

static void MX_GPIO_Init(void) {
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();

    // 初始化LED引脚
    GPIO_InitStruct.Pin = GPIO_PIN_5;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 初始化电容按键引脚
    GPIO_InitStruct.Pin = GPIO_PIN_6;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL; // 或根据需要选择上拉或下拉
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

static void MX_TIM2_Init(void) {
    __HAL_RCC_TIM2_CLK_ENABLE();

    // 配置TIM2
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 8399; // 让定时器计数频率为1kHz (84MHz/8400)
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 0xFFFF; // 最大计数值
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_Base_Init(&htim2);

    // 配置输入捕获
    TIM_IC_InitTypeDef sConfigIC;
    sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING; // 上升沿触发
    sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
    sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
    sConfigIC.ICFilter = 0;
    HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);
}

void SystemClock_Config(void) {
    // 系统时钟配置
    // 这里省略具体配置,可以使用CubeMX生成的代码
}

代码说明

  • LED状态管理:使用ledState变量来跟踪LED的状态,并在输入捕获中触发时取反。
  • 输入捕获回调:当TIM2接收到上升沿信号时,会调用HAL_TIM_IC_CaptureCallback函数,改变LED状态。
  • 定时器配置:TIM2被配置为输入捕获模式,监测PB6引脚的变化。

2024.10.4 第一次修订,后期不再维护

posted @ 2024-10-04 10:18  hazy1k  阅读(1)  评论(0编辑  收藏  举报