stm32/gpio-in/ttp223-touch-key.md
触摸按键(TTP223 x4)
触摸按键(TTP223 x4)
#一、元信息
| 字段 | 值 |
|---|---|
| 芯片/平台 | STM32F103C8T6(HAL) |
| 接口类型 | GPIO 输入(内部上拉) |
| 验证状态 | ✅ 已验证 |
| 验证环境 | Demo00 工程 / Keil MDK(AC5)/ 2026-08-18;OLED 菜单工程 + 步进电机工程(FreeRTOS 1ms 扫描 & 直接轮询双采样)/ 2026-08-20 |
| 驱动方式 | HAL(逻辑参照洋桃1号官方 1-2-22 范例:直接电平轮询) |
#二、接线表
| 信号 | MCU 引脚 | 模块引脚 | 备注 |
|---|---|---|---|
| KEY_A | PA0 | TTP223-1 OUT | 触摸 = 输出低电平 |
| KEY_B | PA1 | TTP223-2 OUT | 触摸 = 输出低电平 |
| KEY_C | PA2 | TTP223-3 OUT | 触摸 = 输出低电平 |
| KEY_D | PA3 | TTP223-4 OUT | 触摸 = 输出低电平 |
| VCC | 3.3V | TTP223 VCC | 2.0~5.5V |
| GND | GND | TTP223 GND | 必须共地 |
#三、踩坑清单
- 直接用官方"电平轮询"最可靠(本工程实测结论):洋桃1号官方 1-2-22 范例就是
while(1){ if(!GPIO_ReadInputDataBit(...)){ ... } },无消抖、无边沿触发,TTP223 自带硬件消抖。HAL 等价写法if (HAL_GPIO_ReadPin(GPIOA, KEY_A_Pin) == GPIO_PIN_RESET)。之前自写的"按下边沿触发 + 释放消抖"逻辑复杂,反而成为排查干扰源。 - 继电器吸合瞬间的电磁干扰会让触摸误触发(2026-08-18 实测):现象——继电器吸合后"按 B/D 关不掉",按了 LED 只变暗不熄灭(A/C 被干扰误判为按下,relay_on 每轮循环覆盖 relay_off 的结果,LED 在高/低之间高速切换看起来变暗)。解法:主循环对每个按键连续两次采样(间隔 5ms)确认再动作,过滤瞬态干扰(代码见工程 main.c 主循环)。
- 输出极性必须核对:TTP223 默认"触摸输出低电平"(
KEY_ACTIVE_LEVEL = GPIO_PIN_RESET)。部分模块有跳线可切高低有效,极性搞反 = 永远"按了没反应"或"一直触发"。 - printf 卡死导致"触摸没反应":程序里用
printf而 Keil 未勾选 MicroLIB,独立运行(无调试器)时 printf 走 semihosting 触发 BKPT 卡死,主循环不执行 → 按键轮询根本没跑。排查按键无响应先确认主循环在跑(可用点亮 LED 验证)。 - 烧录旧 HEX 导致"改了没效果":改完代码必须重新 F7 编译并确认 0 Error、HEX 时间戳/大小更新后再烧。曾出现"HEX 编译时间(20:15)早于源码修改时间(20:17)"导致烧的是旧代码的案例。
- 上电瞬间输出不确定:TTP223 上电有约 0.5s 内部校准,期间输出可能抖动,若首次扫描过早可能误判一次按键。
- 抗干扰:触摸走线远离继电器/电机等大电流线;手指带水、走线过长会导致误触发或没触发。
- MCU 引脚开内部上拉(
GPIO_PULLUP),防止浮空误触发;TTP223 输出低有效时上拉不影响电平判定。 - 电源不稳会放大按键异常:继电器线圈吸合若把 3.3V 拉垮,TTP223 供电不足会输出抖动/误触发。先保证供电充足,再谈软件消抖。
- 别写复杂状态机(边沿触发 + 释放锁存),会卡状态(2026-08-20 实测):步进电机固件原
key_scan_hold()用"按下边沿触发 + 释放需连续 2 次确认解锁"的状态机,电机线圈电磁干扰下会偶发"按了没反应 / 松手不停 / 乱转"。解法:回到直接电平轮询 + 双采样(两次采样间隔 2ms 取与),无锁存状态,从机制上消除卡状态。 - printf 阻塞会拉长扫描周期,长按按循环次数计时会失真:115200 波特率每字符约 87µs,若主循环内按住时每轮都
printf(约 1.5ms),循环周期从 2ms 被拉到 ~4ms;而"长按阈值"若按循环次数累加(如key_hold_cnt >= 250),实际按住要 ~1 秒才响应,非常迟钝。解法:printf 只在状态变化时打一次,或改用HAL_GetTick()真实毫秒计时。 - 固件不含 OLED 驱动 → 屏幕黑属正常,别误判板子没工作:不同固件外设不同,烧步进电机固件后 OLED 屏幕必然不亮。判断固件是否运行应看串口启动信息/电机动作/GPIO 电平,不要以屏幕亮不亮为准。
#四、驱动代码
(Demo00 最终版,2026-08-18 实测:触摸 A/B/C/D 正确控制继电器1/2 与 LED1/2)
#4.1 main.h(引脚/宏定义)
c/* CubeMX 生成区 */
#define KEY_A_Pin GPIO_PIN_0
#define KEY_A_GPIO_Port GPIOA
#define KEY_B_Pin GPIO_PIN_1
#define KEY_B_GPIO_Port GPIOA
#define KEY_C_Pin GPIO_PIN_2
#define KEY_C_GPIO_Port GPIOA
#define KEY_D_Pin GPIO_PIN_3
#define KEY_D_GPIO_Port GPIOA
/* USER CODE BEGIN Private defines */
#define KEY_ACTIVE_LEVEL 0 /* 低电平有效(触摸=低) */
/* USER CODE END Private defines */
#4.2 key.h
c#ifndef KEY_H
#define KEY_H
#include "main.h"
void key_init(void); /* GPIO 输入上拉初始化(CubeMX 的 MX_GPIO_Init 已含) */
#endif /* KEY_H */
#4.3 key.c
c#include "key.h"
void key_init(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef g = {0};
g.Mode = GPIO_MODE_INPUT;
g.Pull = GPIO_PULLUP;
g.Speed = GPIO_SPEED_FREQ_LOW;
g.Pin = KEY_A_Pin | KEY_B_Pin | KEY_C_Pin | KEY_D_Pin;
HAL_GPIO_Init(GPIOA, &g);
}
注:官方范例并未单独写按键驱动文件,直接在 main 主循环读电平。CubeMX 生成的
gpio.c::MX_GPIO_Init()已经把 PA0-PA3 配成上拉输入,工程里无需再调key_init()。
#五、调用示例
(✅ 已验证:直接电平轮询,无消抖)
c/* main() 初始化顺序 */
HAL_Init();
SystemClock_Config(); /* 外部 8MHz 晶振 ×9 = 72MHz */
MX_GPIO_Init(); /* 已配置 PA0-PA3 上拉输入 */
MX_USART1_UART_Init();
relay_init(); /* 继电器/指示灯初始化 */
/* 主循环:官方 1-2-22 风格,触摸低电平有效,按下执行 */
while (1)
{
if (HAL_GPIO_ReadPin(KEY_A_GPIO_Port, KEY_A_Pin) == GPIO_PIN_RESET)
relay_on(RELAY_ID_1); /* 触摸A → 继电器1吸合 */
if (HAL_GPIO_ReadPin(KEY_B_GPIO_Port, KEY_B_Pin) == GPIO_PIN_RESET)
relay_off(RELAY_ID_1); /* 触摸B → 继电器1断开 */
if (HAL_GPIO_ReadPin(KEY_C_GPIO_Port, KEY_C_Pin) == GPIO_PIN_RESET)
relay_on(RELAY_ID_2); /* 触摸C → 继电器2吸合 */
if (HAL_GPIO_ReadPin(KEY_D_GPIO_Port, KEY_D_Pin) == GPIO_PIN_RESET)
relay_off(RELAY_ID_2); /* 触摸D → 继电器2断开 */
}
#六、调用示例(步进电机版:即按即转、松手即停,2026-08-20)
(✅ 方案已应用在 Demo00 步进电机固件;电机本体当时单独调试中)
c/* main() 初始化顺序 */
key_init(); /* PA0-PA3 上拉输入(CubeMX 或手动) */
step_motor_init(); /* PB3/PB4/PB8/PB9 推挽输出(需先释放 JTAG) */
uint8_t key_val;
SM_Dir_t cur_dir = SM_STOP;
for (;;)
{
/* 第 1 次采样 */
uint8_t s1 = 0;
if (HAL_GPIO_ReadPin(GPIOA, KEY_A_PIN) == GPIO_PIN_RESET) s1 |= KEY_A;
if (HAL_GPIO_ReadPin(GPIOA, KEY_B_PIN) == GPIO_PIN_RESET) s1 |= KEY_B;
if (HAL_GPIO_ReadPin(GPIOA, KEY_C_PIN) == GPIO_PIN_RESET) s1 |= KEY_C;
if (HAL_GPIO_ReadPin(GPIOA, KEY_D_PIN) == GPIO_PIN_RESET) s1 |= KEY_D;
step_motor_tick(); /* 非阻塞步进(HAL_GetTick 真实计时) */
HAL_Delay(2); /* 两次采样间隔 */
/* 第 2 次采样:双采样确认(s1 & s2),过滤电机/继电器电磁瞬态干扰 */
uint8_t s2 = 0;
if (HAL_GPIO_ReadPin(GPIOA, KEY_A_PIN) == GPIO_PIN_RESET) s2 |= KEY_A;
if (HAL_GPIO_ReadPin(GPIOA, KEY_B_PIN) == GPIO_PIN_RESET) s2 |= KEY_B;
if (HAL_GPIO_ReadPin(GPIOA, KEY_C_PIN) == GPIO_PIN_RESET) s2 |= KEY_C;
if (HAL_GPIO_ReadPin(GPIOA, KEY_D_PIN) == GPIO_PIN_RESET) s2 |= KEY_D;
key_val = s1 & s2;
/* 即按即转、松手即停 */
SM_Dir_t dir = SM_STOP;
if (key_val & KEY_A) dir = SM_CW_4; /* A=顺时针快转 */
else if (key_val & KEY_B) dir = SM_CCW_4; /* B=逆时针快转 */
else if (key_val & KEY_C) dir = SM_CW_8; /* C=顺时针慢转 */
else if (key_val & KEY_D) dir = SM_CCW_8; /* D=逆时针慢转 */
if (dir != cur_dir) /* 只在状态变化时动作 + 打印,避免 printf 阻塞扫描周期 */
{
cur_dir = dir;
if (dir == SM_STOP) step_motor_stop();
else step_motor_set_dir(dir);
}
HAL_Delay(2); /* 扫描周期 */
}
#七、参考库/链接
- 洋桃1号官方范例(本地):
D:\学习资料\【用户资料】洋桃1号开发板(2)\B5-教学视频示例程序\1-2-22-按键控制继电器程序\(Hardware/TOUCH_KEY/touch_key.c、User/main.c直接轮询写法) - TTP223 数据手册:搜索 "TTP223 datasheet PDF"(官方 Tontek)