第11章 电容触摸按键实验
第十一章 电容触摸按键实验
1. 硬件设计
本实验用到的硬件资源有:
-
指示灯DS0和DS1
-
定时器TIM2
-
触摸按键TPAD
前面两个之前均有介绍,我们需要通过 TIM2_CH1(PA5)采集 TPAD 的信号,所以本实验需要用跳线帽短接多功能端口(P12)的 TPAD 和 ADC,以实现 TPAD 连接到 PA5。
硬件设置(用跳线帽短接多功能端口的 ADC 和 TPAD 即可)好之后,下面我们开始软件设计。
2. 软件设计
2.1 编程大纲
-
TIM2_CH1输入捕获初始化
-
电容按键复位
-
取得捕获值并处理
-
电容按键初始化
-
电容按键扫描
-
主函数测试
2.2 代码分析
2.2.1 TIM2通道1配置输入捕获
// 定时器2通道1输入捕获配置
void TIM2_CH1_Cap_Init(u32 arr, u16 psc)
{
/* TIM2基础配置 */
TIM_IC_InitTypeDef TIM2_CH1Config;
TIM2_Handler.Instance = TIM2;
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;
TIM2_CH1Config.ICPrescaler = TIM_ICPSC_DIV1;
TIM2_CH1Config.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&TIM2_Handler, &TIM2_CH1Config, TIM_CHANNEL_1); // 配置TIM2_CH1
HAL_TIM_IC_Start(&TIM2_Handler, TIM_CHANNEL_1);
}
// TIM2底层驱动
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_Initure.Pin = GPIO_PIN_5;
GPIO_Initure.Mode = GPIO_MODE_AF_PP;
GPIO_Initure.Pull = GPIO_NOPULL;
GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_Initure.Alternate = GPIO_AF1_TIM2;
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.2 复位电容按键和定时器
// 复位电容按键:释放电容点亮,清楚定时器计数值
void TPAD_Reset(void)
{
GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin = GPIO_PIN_5;
GPIO_Initure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_Initure.Pull = GPIO_PULLDOWN;
GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 释放电容
delay_ms(5);
__HAL_TIM_CLEAR_FLAG(&TIM2_Handler, TIM_FLAG_CC1|TIM_FLAG_UPDATE); // 清除中断标志位
__HAL_TIM_SET_COUNTER(&TIM2_Handler, 0); // 清除计数器
GPIO_Initure.Mode = GPIO_MODE_AF_PP;
GPIO_Initure.Pull = GPIO_NOPULL;
GPIO_Initure.Alternate = GPIO_AF1_TIM2; // PA5复用为TIM2_CH1
HAL_GPIO_Init(GPIOA, &GPIO_Initure);
}
函数 TPAD_Reset 顾名思义,是进行一次复位操作。先设置 PA5 输出低电平,电容放电,同时清除中断标志位并且计数器值清零,然后配置 PA5 为复用功能浮空输入,利用外部上拉电阻给电容 Cs(Cs+Cx)充电,同时开启 TIM2_CH1 的输入捕获。
2.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); // 读取计数器值
}
// 读取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;
}
2.2.4 电容按键初始化
// 电容按键初始化
uint8_t tpad_init(u16 psc)
{
u16 buf[10];
u16 temp;
uint8_t i, j;
TIM2_CH1_Cap_Init(TPAD_ARR_MAX_VAL, psc-1); // 初始化定时器2通道1,设置自动重装载值和预分频值
for(i = 0; i < 10; i++)
{
buf[i] = TPAD_Get_Val(); // 读取电容触摸按键值
}
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];
}
tpad_default_val = temp/6; // 计算默认值
printf("tpad_default_val:%d\r\n",tpad_default_val);
if(tpad_default_val>(vu16)TPAD_ARR_MAX_VAL/2)
{
return 1; // 电容按键初始化失败
}
return 0; // 电容按键初始化成功
}
函数 TPAD_Init 用于初始化输入捕获,并获取默认的 TPAD 值。该函数有一个参数,用来传递分频系数,其实是为了配置 TIM2_CH1_Cap_Init 的计数周期。在该函数中连续 10 次读取TPAD 值,将这些值升序排列后取中间 6 个值再做平均(这样做的目的是尽量减少误差),并赋值给 tpad_default_val,用于后续触摸判断的标准。
2.2.5 电容按键扫描
// 触摸按键扫描
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 为门限值。
2.2.6 主函数
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "tpad.h"
int main(void)
{
u8 t=0;
HAL_Init();
Stm32_Clock_Init(336,8,2,7);
delay_init(168);
uart_init(115200);
LED_Init();
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. 小结
实际上这个实验就是运用的定时器的输入捕获功能,下面简单回顾一下:
3.1 硬件连接
- LED连接:
- 将LED1(比如连接到GPIOA的引脚5)连接到STM32的相应引脚。
- 电容按键连接:
- 使用一个电容按键(如连接到GPIOB的引脚6),并设置为输入模式。
- 定时器设置:
- 使用TIM2设置为输入捕获模式,监测电容按键的按下事件。
3.2 软件配置步骤
1. 初始化工程
- 使用STM32CubeMX创建一个新的STM32F407项目。
- 启用
GPIOA
和GPIOB
,配置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生成的代码
}
3.3 代码说明
- LED状态管理:使用
ledState
变量来跟踪LED的状态,并在输入捕获中触发时取反。 - 输入捕获回调:当TIM2接收到上升沿信号时,会调用
HAL_TIM_IC_CaptureCallback
函数,改变LED状态。 - 定时器配置:TIM2被配置为输入捕获模式,监测PB6引脚的变化。
2024.10.4 第一次修订,后期不再维护
2025.3.4 修订程序,简化内容
本文作者:hazy1k
本文链接:https://www.cnblogs.com/hazy1k/p/18446396
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步