机械按键靠物理通断,触摸按键靠的是手指靠近改变的那点电容。触摸按键的可靠性,软件滤波算法很关键。
从原始读数开始
触摸传感芯片(或 MCU 内置 TSC)定期给出一个表征电容大小的数值。手指按下时,数值相对"未按"基准上升(或下降,取决于电极设计)。
// 周期性读取每个通道的原始电容计数#define CH_NUM 4uint16_t raw[CH_NUM]; // 原始读数uint16_t baseline[CH_NUM]; // 当前基线(无触摸时的估计值)voidscan_raw(void){for (int i = 0; i < CH_NUM; i++) { raw[i] = tsc_read_channel(i); // 读取电容转后的计数 }}裸读数抖动很大,不能拿来直接判键。第一步是建立"无触摸时它大概是多少"的估计——也就是基线。
基线跟踪:跟着环境慢变
基线必须动态更新,否则温度一变整体读数平移,固定阈值就失效了。常规做法是:在"判定为未触摸"的窗口内,用指数滑动平均缓慢追踪。
// 仅当认为未触摸时更新基线,避免把手指值吸进去voidupdate_baseline(int ch){if (!is_touched(ch)) {// 系数越小,基线跟得越慢,抗抖动越强 baseline[ch] = baseline[ch] - (baseline[ch] >> 4) + (raw[ch] >> 4); }}>> 4 即 1/16 的权重,基线每秒只挪一小步,既能跟上温漂,又不会被一次触摸拽偏。若权重太大(如 1/2),基线会"追"上手指值,导致松手后误判一直按下。
差分阈值:用变化量判键
判键看的是"当前值与基线的差",而不是绝对值。手指带来的差值是稳定且显著的。
#define TOUCH_THRESHOLD 30 // 差值超过此值判为触摸#define RELEASE_THRESHOLD 15 // 回落到此值以下判为松开(带回差)intis_touched(int ch){uint16_t delta = raw[ch] - baseline[ch];return delta > TOUCH_THRESHOLD;}引入回差(hysteresis):按下阈值(30)高于松开阈值(15),两者之间的区域保持稳定,避免临界抖动时按键反复跳变。这点和机械按键的软件消抖是同一思路,只是对象从电平变为了电容差值。
温漂与环境补偿
温度变化时,整条曲线平移,单靠基线慢追踪还不够快。可以在基线更新里加约束:限制单步最大变化量,过滤掉突变干扰;同时用"长期最大值"和"长期最小值"动态收紧判定窗口。
// 限制基线单步跳变,滤除异常读数int16_t diff = (int16_t)raw[ch] - (int16_t)baseline[ch];if (diff > -MAX_STEP && diff < MAX_STEP) { baseline[ch] += diff >> 4; // 正常慢追踪}// 否则本次 raw 异常(如上电瞬态),忽略不更新MAX_STEP 拦住上电、强干扰等造成的离群值,防止基线被一次性污染。配合周期性的"全通道环境重校准"(设备空闲时强制重算基线),可应对季节级温漂。
消抖状态机
和机械按键类似,触摸也有"假触发"和"抖动"。但触摸的消抖不是延时等待,而是确认状态稳定的持续时间。用一个小状态机把"疑似→确认"分开:
// 每个通道维护一个计时器,确认稳定才切换有效状态voiddebounce_ch(int ch){if (raw[ch] - baseline[ch] > TOUCH_THRESHOLD) {if (++touch_timer[ch] >= DEBOUNCE_TICKS) key_state[ch] = 1; // 确认按下 } elseif (raw[ch] - baseline[ch] < RELEASE_THRESHOLD) {if (++release_timer[ch] >= DEBOUNCE_TICKS) { key_state[ch] = 0; // 确认松开 touch_timer[ch] = 0; } } else {// 回差区内,两个计时器清零,保持当前状态 touch_timer[ch] = 0; release_timer[ch] = 0; }}区分"疑似"和"确认"后,单次尖峰干扰不会触发按键,只有稳定超过 DEBOUNCE_TICKS 的触摸才算数。
多键扫描与防水
多键共用一个激励源时,要在扫描周期里轮流采集各通道,避免相互耦合。防水(水膜误触)靠"相邻键差值比对":水膜会让一片电极整体抬升,而真实手指是单点突出,据此可剔除水误触。
完整链路
触摸按键的软件链路是:原始读数 → 基线慢追踪(限幅抗污染)→ 差分+回差判疑似 → 状态机去抖确认 → 多键/防水后处理。把基线和回差调好,比追求硬件灵敏度更重要。
// 主循环里周期调用,典型扫描周期 10~20msvoidtouch_task(void){ scan_raw();for (int i = 0; i < CH_NUM; i++) { update_baseline(i); // 先更新基线 debounce_ch(i); // 再判定与去抖 }// key_state[] 即为稳定的按键有效状态,供上层读取}
夜雨聆风