32. TIM—电容按键检测

前面章节我们讲解了基本定时器和高级控制定时器的功能,这一章我们将介绍定时器输入捕获一个应用实例—电容按键检测,帮助我们更加深入理解定时器。

32.1. 电容按键原理

电容器(简称为电容)就是可以容纳电荷的器件,两个金属块中间隔一层绝缘体就可以构成一个最简单的电容。如图 片状电容器 (俯视图), 有两个金属片,之间有一个绝缘介质,这样就构成了一个电容。这样一个电容在电路板上非常容易实现, 一般设计四周的铜片与电路板地信号连通,这样一种结构就是电容按键的模型。当电路板形状固定之后,该电容的容量也是相对稳定的。

片状电容器

电路板制作时都会在表面上覆盖一层绝缘层,用于防腐蚀和绝缘,所以实际电路板设计时情况如图 无手指触摸情况。 电路板最上层是绝缘材料,下面一层是导电铜箔,我们根据电路走线情况设计决定铜箔的形状,再下面一层一般是FR-4板材。 金属感应片与地信号之间有绝缘材料隔着,整个可以等效为一个电容Cx。一般在设计时候,把金属感应片设计成方便手指触摸大小。

无手指触摸情况

在电路板未上电时,可以认为电容Cx是没有电荷的,在上电时,在电阻作用下,电容Cx就会有一个充电过程,直到电容充满,即Vc电压值为3.3V, 这个充电过程的时间长短受到电阻R阻值和电容Cx容值的直接影响。但是在我们选择合适电阻R并焊接固定到电路板上后,这个充电时间就基本上不会变了, 因为此时电阻R已经是固定的,电容Cx在无外界明显干扰情况下基本上也是保持不变的。

现在,我们来看看当我们用手指触摸时会是怎样一个情况?如图 有手指触摸情况 , 当我们用手指触摸时,金属感应片除了与地信号形成一个等效电容Cx外,还会与手指形成一个Cs等效电容。

有手指触摸情况

此时整个电容按键可以容纳的电荷数量就比没有手指触摸时要多了,可以看成是Cx和Cs叠加的效果。 在相同的电阻R情况下,因为电容容值增大了,导致需要更长的充电时间。也就是这个充电时间变长使得我们区分有无手指触摸,也就是电容按键是否被按下。

现在最主要的任务就是测量充电时间。充电过程可以看出是一个信号从低电平变成高电平的过程,现在就是要求出这个变化过程的时间。 我们可以利用定时器输入捕获功能计算充电时间,即设置TIMx_CH为定时器输入捕获模式通道。这样先测量得到无触摸时的充电时间作为比较基准, 然后再定时循环测量充电时间与无触摸时的充电时间作比较,如果超过一定的阈值就认为是有手指触摸。

Vc电压与充电时间关系 为Vc跟随时间变化情况, 可以看出在无触摸情况下,电压变化较快;而在有触摸时,总的电容量增大了,电压变化缓慢一些。

Vc电压与充电时间关系

为测量充电时间,我们需要设置定时器输入捕获功能为上升沿触发, 图 Vc电压与充电时间关系 中VH就是被触发上升沿的电压值, 也是STM32认为是高电平的最低电压值,大约为1.8V。t1和t2可以通过定时器捕获/比较寄存器获取得到。

不过,在测量充电时间之前,我们必须想办法制作这个充电过程。之前的分析是在电路板上电时会有充电过程, 现在我们要求在程序运行中循环检测按键,所以必须可以控制充电过程的生成。我们可以控制TIMx_CH引脚作为普通的GPIO使用, 使其输出一小段时间的低电平,为电容Cx放电,即Vc为0V。当我们重新配置TIMx_CH为输入捕获时电容Cx在电阻R的作用下就可以产生充电过程。

32.2. 电容按键检测实验

电容按键不需要任何外部机械部件,使用方便,成本低,很容易制成与周围环境相密封的键盘,以起到防潮防湿的作用。 电容按键优势突出使得越来越多电子产品使用它代替传统的机械按键。

本实验实现电容按键状态检测方法,提供一个编程实例。

32.2.1. 硬件设计

开发板板载一个电容按键,原理图设计参考图 电容按键电路设计

电容按键电路设计

标示TPAD1在电路板上就是电容按键实体,默认通过一个调帽连接到PA1,即通用定时器TIM5的通道2。 充电电容的阻值为5.1M,电阻的大小决定了电容按键充电的时间。

实验还用到调试串口和蜂鸣器功能,用来打印输入捕获信息和指示按键状态,这两个模块电路可参考之前相关章节。

32.2.2. 软件设计

这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 我们创建了两个文件:bsp_tpad.c和bsp_tpad.h文件用来存放电容按键检测相关函数和宏定义。

32.2.2.1. 编程要点

  1. 编写定时器输入捕获相关函数

  2. 测量电容按键空载的充电时间T1

  3. 测量电容按键有手触摸的充电时间T2

  4. 只需要比较T2与T1的时间即可检测出按键是否有手指触摸

32.2.2.2. 软件分析

电容按键宏定义

#define TOUCHPAD_GATE_VAL   100
// 定义定时器预分频,定时器实际时钟频率为:72MHz/(TOUCHPAD_TIMx_PRESCALER+1)
#define TOUCHPAD_TIM_PRESCALER              47 //1.5MHz
// 定义定时器周期
#define TOUCHPAD_TIM_ARR                     0xFFFF
/******************** TPAD 引脚配置参数定义 **************************/
#define TOUCHPAD_TIMx                        TIM5
#define TOUCHPAD_TIM_RCC_CLK_ENABLE()        __HAL_RCC_TIM5_CLK_ENABLE()
#define TOUCHPAD_TIM_RCC_CLK_DISABLE()       __HAL_RCC_TIM5_CLK_DISABLE()
#define TOUCHPAD_GPIO_RCC_CLK_ENABLE()       __HAL_RCC_GPIOA_CLK_ENABLE()
#define TOUCHPAD_GPIO_PIN                    GPIO_PIN_1
#define TOUCHPAD_GPIO                        GPIOA
#define TOUCHPAD_TIM_CHANNEL                 TIM_CHANNEL_2
#define TOUCHPAD_TIM_FLAG_CCR                TIM_FLAG_CC2

有关宏的具体含义配套注释阅读即可。

电容按键GPIO配置

void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    /* 基本定时器外设时钟使能 */
    TOUCHPAD_TIM_RCC_CLK_ENABLE();
    /* 定时器通道引脚时钟使能 */
    TOUCHPAD_GPIO_RCC_CLK_ENABLE();
    /* 定时器通道引脚配置:捕获功能设置为输入模式
    */
    GPIO_InitStruct.Pin = TOUCHPAD_GPIO_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLDOWN;
    HAL_GPIO_Init(TOUCHPAD_GPIO, &GPIO_InitStruct);
}

HAL_TIM_Base_MspInit ()函数初始化了定时器用到的相关的GPIO,当使用不同的GPIO的时候, 只需要修改头文件里面的宏定义即可,而不需要修改这个函数。

电容按键TIM模式配置

static void TPAD_TIM_Mode_Config(void)
{
    TIM_ClockConfigTypeDef sClockSourceConfig;
    TIM_MasterConfigTypeDef sMasterConfig;
    TIM_IC_InitTypeDef sConfigIC;

    TIM_Handle.Instance = TOUCHPAD_TIMx;
    TIM_Handle.Init.Prescaler = TOUCHPAD_TIM_PRESCALER;
    TIM_Handle.Init.CounterMode = TIM_COUNTERMODE_UP;
    TIM_Handle.Init.Period = TOUCHPAD_TIM_ARR;
    TIM_Handle.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_Base_Init(&TIM_Handle);

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
    HAL_TIM_ConfigClockSource(&TIM_Handle, &sClockSourceConfig);

    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    HAL_TIMEx_MasterConfigSynchronization(&TIM_Handle, &sMasterConfig);

    sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
    sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
    sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
    sConfigIC.ICFilter = 3;
    HAL_TIM_IC_ConfigChannel(&TIM_Handle, &sConfigIC,
                            TOUCHPAD_TIM_CHANNEL);
}

TPAD_TIM_Mode_Config()函数中初始化了两个结构体,有关这两个结构体成员的具体含义可参考“定时器初始化结构体详解”小节, 剩下的程序参考注释阅读即可。有个地方要注意的是捕获信号的极性配置,需要配置为上升沿。因为电容按键在放电之后再充电的时候是一个电平又低到高的过程。

电容按键复位

static void TPAD_Reset(void)
{
    /* 定义IO硬件初始化结构体变量 */
    GPIO_InitTypeDef GPIO_InitStruct;

    /* 使能电容按键引脚对应IO端口时钟 */
    TOUCHPAD_GPIO_RCC_CLK_ENABLE();

    /* 设置引脚输出为低电平 */
    HAL_GPIO_WritePin(TOUCHPAD_GPIO, TOUCHPAD_GPIO_PIN, GPIO_PIN_RESET)
                    ;

    /* 设定电容按键对应引脚IO编号 */
    GPIO_InitStruct.Pin = TOUCHPAD_GPIO_PIN;
    /* 设定电容按键对应引脚IO为输出模式 */
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    /* 设定电容按键对应引脚IO操作速度 */
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    /* 初始化电容按键对应引脚IO */
    HAL_GPIO_Init(TOUCHPAD_GPIO, &GPIO_InitStruct);

    HAL_Delay(5);

    __HAL_TIM_SET_COUNTER(&TIM_Handle,0); // 清零定时器计数
    __HAL_TIM_CLEAR_FLAG(&TIM_Handle, TIM_FLAG_UPDATE|TIM_FLAG_CC2);
                        //清除中断标志

    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(TOUCHPAD_GPIO, &GPIO_InitStruct);

    HAL_TIM_IC_Start(&TIM_Handle,TOUCHPAD_TIM_CHANNEL);
}

开发板上电之后,电容按键默认已经充满了电,要想测得电容按键的充电时间 就必须先把电容按键的电放掉, 方法为让接电容按键的IO输出低电平即可,这个放电的时间一般都是us级别,我们可以稍微延时以下即可。放电完毕之后, 再把连接电容按键的IO配置为输入,然后通过输入捕获的方法测量电容按键的充电时间,这个充电时间T1是没有手指触摸的情况下的充电时间, 而且这个空载的充电时间非常稳定,因为电路板的硬件已经确定了。当有手指触摸的情况下,相当于电容变大,充电时间T2会变长, 我们只需要对比这两个时间就可以 知道电容按键是否有手指触摸。

电容按键初始化

/**
* @brief  初始化触摸按键,获得空载的时候触摸按键的充电时间
* @param  无
* @retval 0:成功,1:失败
* @note   空载值一般很稳定,由硬件电路决定,该函数只需要调用一次即可
*         而且这个空载的充电时间每个硬件都不一样,最好实际测试下
*/
uint8_t TPAD_Init(void)
{
    uint16_t temp;

    // 电容按键用到的输入捕获的IO和捕获模式参数初始化
    TPAD_TIM_Init();

    temp = TPAD_Get_Val();

    // 电容按键空载的充电时间非常稳定,不同的硬件充电时间不一样
    // 需要实际测试所得,MINI上的电容按键空载充电时间稳定在218
    // 如果你觉得单次测量不准确,你可以多次测量然后取个平均值
    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这个全局变量当中。 这个空载的充电时间不同的硬件是不一样的,需要实际测试,在调试的过程中,可把捕获到的值打印出来看看。MINI开发板上这个值稳定在218。

在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);
}

获取最大输入捕获值

static uint16_t TPAD_Get_Val(void)
{
    TPAD_Reset();
    while (__HAL_TIM_GET_FLAG(&TIM_Handle,TOUCHPAD_TIM_FLAG_CCR)
        ==RESET) {
        uint16_t count;
        count=__HAL_TIM_GET_COUNTER(&TIM_Handle);
        if (count>(TOUCHPAD_TIM_ARR-500))
            return count;//超时了,直接返回CNT的值
    };
    return HAL_TIM_ReadCapturedValue(&TIM_Handle,TOUCHPAD_TIM_CHANNEL);
}

该函数接收一个参数,用来指定获取电容按键捕获值的循环次数,函数的返回值则为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)
{
    /* 配置系统时钟为72 MHz */
    SystemClock_Config();

    /* 蜂鸣器初始化 */
    BEEP_GPIO_Config();

    /* 初始化USART1 配置模式为 115200 8-N-1 */
    DEBUG_USART_Config();

    printf ( "\r\n野火STM32输入捕获电容按键检测实验\r\n" );
    printf ( "\r\n触摸电容按键,蜂鸣器则会响\r\n" );

    /* 初始化电容按键获取未被触摸时参数 */
    TPAD_Init();

    while (1) {
        if ( TPAD_Scan(0)  ) {
            BEEP(ON);
            HAL_Delay(30);
            BEEP(OFF);
        }
    }
}

主函数初始化了蜂鸣器和串口,然后等待电容按键初始化成功,如果不成功则会一直等待。初始化成功之后, 在一个while无限循环中不断的扫描按键,当按键按下之后蜂鸣器响25ms,然后关掉。

32.2.3. 下载验证

把编译好的程序烧写到开发板,手按电容按键,蜂鸣器则会响。