第11章 电容触摸按键实验
第十一章 电容触摸按键实验
1. 硬件设计
本实验用到的硬件资源有:
-
指示灯DS0和DS1
-
定时器TIM2
-
触摸按键TPAD
前面两个之前均有介绍,我们需要通过 TIM2_CH1(PA5)采集 TPAD 的信号,所以本实验需要用跳线帽短接多功能端口(P12)的 TPAD 和 ADC,以实现 TPAD 连接到 PA5。
硬件设置(用跳线帽短接多功能端口的 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 毫秒,以确保传感器有时间稳定下来。
- 这个循环执行 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
,这是触摸传感器的默认值。
- 计算的总和除以 6,得到
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,表示初始化成功。
流程:
- 初始化定时器。
- 读取触摸传感器的值 10 次。
- 对这些值进行排序。
- 计算中间 6 个值的平均值。
- 检查这个平均值是否合理。
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. 小结
实际上这个实验就是运用的定时器的输入捕获功能,下面简单回顾一下:
硬件连接
- LED连接:
- 将LED1(比如连接到GPIOA的引脚5)连接到STM32的相应引脚。
- 电容按键连接:
- 使用一个电容按键(如连接到GPIOB的引脚6),并设置为输入模式。
- 定时器设置:
- 使用TIM2设置为输入捕获模式,监测电容按键的按下事件。
软件配置步骤
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生成的代码
}
代码说明
- LED状态管理:使用
ledState
变量来跟踪LED的状态,并在输入捕获中触发时取反。 - 输入捕获回调:当TIM2接收到上升沿信号时,会调用
HAL_TIM_IC_CaptureCallback
函数,改变LED状态。 - 定时器配置:TIM2被配置为输入捕获模式,监测PB6引脚的变化。
2024.10.4 第一次修订,后期不再维护