stm32/gpio-in/ttp223-touch-key.md

触摸按键(TTP223 x4)

知识库 · STM32

触摸按键(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_APA0TTP223-1 OUT触摸 = 输出低电平
KEY_BPA1TTP223-2 OUT触摸 = 输出低电平
KEY_CPA2TTP223-3 OUT触摸 = 输出低电平
KEY_DPA3TTP223-4 OUT触摸 = 输出低电平
VCC3.3VTTP223 VCC2.0~5.5V
GNDGNDTTP223 GND必须共地

#三、踩坑清单

  1. 直接用官方"电平轮询"最可靠(本工程实测结论):洋桃1号官方 1-2-22 范例就是 while(1){ if(!GPIO_ReadInputDataBit(...)){ ... } }无消抖、无边沿触发,TTP223 自带硬件消抖。HAL 等价写法 if (HAL_GPIO_ReadPin(GPIOA, KEY_A_Pin) == GPIO_PIN_RESET)。之前自写的"按下边沿触发 + 释放消抖"逻辑复杂,反而成为排查干扰源。
  2. 继电器吸合瞬间的电磁干扰会让触摸误触发(2026-08-18 实测):现象——继电器吸合后"按 B/D 关不掉",按了 LED 只变暗不熄灭(A/C 被干扰误判为按下,relay_on 每轮循环覆盖 relay_off 的结果,LED 在高/低之间高速切换看起来变暗)。解法:主循环对每个按键连续两次采样(间隔 5ms)确认再动作,过滤瞬态干扰(代码见工程 main.c 主循环)。
  3. 输出极性必须核对:TTP223 默认"触摸输出低电平"(KEY_ACTIVE_LEVEL = GPIO_PIN_RESET)。部分模块有跳线可切高低有效,极性搞反 = 永远"按了没反应"或"一直触发"。
  4. printf 卡死导致"触摸没反应":程序里用 printf 而 Keil 未勾选 MicroLIB,独立运行(无调试器)时 printf 走 semihosting 触发 BKPT 卡死,主循环不执行 → 按键轮询根本没跑。排查按键无响应先确认主循环在跑(可用点亮 LED 验证)。
  5. 烧录旧 HEX 导致"改了没效果":改完代码必须重新 F7 编译并确认 0 Error、HEX 时间戳/大小更新后再烧。曾出现"HEX 编译时间(20:15)早于源码修改时间(20:17)"导致烧的是旧代码的案例。
  6. 上电瞬间输出不确定:TTP223 上电有约 0.5s 内部校准,期间输出可能抖动,若首次扫描过早可能误判一次按键。
  7. 抗干扰:触摸走线远离继电器/电机等大电流线;手指带水、走线过长会导致误触发或没触发。
  8. MCU 引脚开内部上拉GPIO_PULLUP),防止浮空误触发;TTP223 输出低有效时上拉不影响电平判定。
  9. 电源不稳会放大按键异常:继电器线圈吸合若把 3.3V 拉垮,TTP223 供电不足会输出抖动/误触发。先保证供电充足,再谈软件消抖。
  10. 别写复杂状态机(边沿触发 + 释放锁存),会卡状态(2026-08-20 实测):步进电机固件原 key_scan_hold() 用"按下边沿触发 + 释放需连续 2 次确认解锁"的状态机,电机线圈电磁干扰下会偶发"按了没反应 / 松手不停 / 乱转"。解法:回到直接电平轮询 + 双采样(两次采样间隔 2ms 取与),无锁存状态,从机制上消除卡状态。
  11. printf 阻塞会拉长扫描周期,长按按循环次数计时会失真:115200 波特率每字符约 87µs,若主循环内按住时每轮都 printf(约 1.5ms),循环周期从 2ms 被拉到 ~4ms;而"长按阈值"若按循环次数累加(如 key_hold_cnt >= 250),实际按住要 ~1 秒才响应,非常迟钝。解法:printf 只在状态变化时打一次,或改用 HAL_GetTick() 真实毫秒计时。
  12. 固件不含 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.cUser/main.c 直接轮询写法)
  • TTP223 数据手册:搜索 "TTP223 datasheet PDF"(官方 Tontek)