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}\) 为充电电压,R 为充电电阻,C 为电容容值,e 为自然底数,t 为充电时间。根据这个公式,我们就可以计算出 \(C_{s}\) 和 \(C_{x}\)。
当充电时间在 \(T_{cs}\) 附近,就可以认为没有触摸,而当充电时间大于 \(T_{cs} + T_{x}\) 时,就认为有触摸按下(\(T_{x}\) 为检测阀值)。
二、电容触摸按键检测过程
- TPAD 引脚设置为推挽输出,输出低电平,实现电容放电到地。
- TPAD 引脚设置为浮空输入,电容开始充电。
- 同时开启 TPAD 引脚的输入捕获功能,开始捕获高电平。
- 等待充电过程中,上升沿触发(充电到 \(V_{th}\))。
- 计算充电时间(定时器捕获/比较寄存器获取)
没有按下的时候,充电时间为 \(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;
}