消费电子触控按键编程:实现高抗干扰的电容按键检测与手势识别
电容按键抗干扰不是“阈值调大点”就完事。真实环境里有温漂、湿手、水滴、LED PWM耦合、电源纹波、ESD,原始计数值一直在飘。 正确做法是:采样→滤波→基线跟踪→Delta判定→去抖状态机→滑条重心法→手势时序。算法不碰绝对电容值,只认“相对基线的变化量”。
一、采样与滤波
触摸芯片(自电容/互电容)出来的是Raw Count,先过一遍IIR低通压高频毛刺:
#define IIR_A 8 // 越小越平滑
static int16_t filt[N_CH];
void cap_scan_filter(uint16_t raw[], int n){
for(int i=0;i<n;i++){
filt[i] = filt[i] + ((int16_t)raw[i]-filt[i])*IIR_A/16;
}
}
滤波系数别太重,否则响应迟滞;LED调光、电机启动时噪声是周期性的,可加多次平均或跳频扫描。
二、基线跟踪:慢跟、触摸冻结
基线是“无触摸时稳态参考”。温度湿度慢变要跟,但手指按下去不能跟。
static int16_t baseline[N_CH];
static uint8_t touch[N_CH];
void baseline_update(int ch, int16_t v){
int16_t d = v - baseline[ch];
if(touch[ch]) return; // 触摸中冻结基线
if(d > NOISE_TH){ // 超噪声门限,疑似干扰,不跟
return;
}
if(d>0) baseline[ch]++; // 水桶式单步跟随
else if(d<0) baseline[ch]--;
}
要点:
基线更新只在“非触摸”状态做
突变超过噪声阈值当干扰丢弃,别学进去
单步递增最稳,别一下拉到当前值
上电前几百ms先采空板基线再进业务
三、Delta判定+去抖状态机
判定用基线+触摸阈值,释放用滞回阈值,避免边界抖动:
#define TOUCH_TH 15
#define HYST 6
#define DEBOUNCE 3
static uint8_t cnt[N_CH];
uint8_t key_judge(int ch, int16_t v){
int16_t delta = v - baseline[ch];
if(!touch[ch]){
if(delta > TOUCH_TH){
if(++cnt[ch] >= DEBOUNCE){
touch[ch]=1; cnt[ch]=0;
}
}else cnt[ch]=0;
}else{
if(delta < TOUCH_TH-HYST){
if(++cnt[ch] >= DEBOUNCE){
touch[ch]=0; cnt[ch]=0;
}
}else cnt[ch]=0;
}
return touch[ch];
}
连续N次采样才算按下/抬起,抖动脉冲被滤掉。 长按别在主循环计时,用按下时刻戳算。
四、多键防水滴误触
水滴覆盖会同时抬高相邻几个通道基线,看起来像“多键同按”。加一条规则:
int active_cnt=0;
for(int i=0;i<N_CH;i++) if(touch[i]) active_cnt++;
if(active_cnt>=3 && is_adjacent_group()){ // 大片相邻激活
// 判为水膜,全部丢弃
for(int i=0;i<N_CH;i++) touch[i]=0;
}
硬件上加Driven Shield/Guard Ring,软件再判“广域漂移”,家电面板基本压得住。
五、滑条位置:重心法
滑条不是“哪个电极最大”,是多个通道加权出连续位置:
int slider_pos(int16_t v[], int n){
int32_t num=0, den=0;
for(int i=0;i<n;i++){
int16_t s = v[i]-baseline[i];
if(s<SLIDER_TH) continue;
num += s*i;
den += s;
}
if(den==0) return -1;
return num/den; // 0~n-1 连续位置
}
输出再做一次滑动平均,跳点会少很多。 电极尺寸/走线长度不一致要存增益补偿系数,否则滑到中间会卡。
六、手势识别:时序状态机
滑条位置出来后,按时间窗判手势:
typedef struct {
int last_pos;
uint32_t last_move_ms;
uint32_t press_ms;
uint8_t got_first;
} gesture_t;
void gesture_proc(gesture_t*g, int pos, uint32_t now, uint8_t pressed){
if(pressed){
if(g->press_ms==0) g->press_ms=now;
if(pos>=0 && g->got_first){
int dx = pos-g->last_pos;
if(abs(dx)>=2 && now-g->last_move_ms<400){
emit(dx>0?GEST_RIGHT:GEST_LEFT);
}
g->last_pos=pos; g->last_move_ms=now;
}
if(pos>=0) g->got_first=1;
}else{
if(g->press_ms && now-g->press_ms>1000)
emit(GEST_LONGPRESS);
g->press_ms=0; g->got_first=0;
}
}
规则:
位移阈值过滤手颤
时间窗限死滑动判定(300~500ms)
抬起时算按压时长→长按
双击记两次press事件间隔<300ms
七、工程落坑
主循环轮询扫描→响应慢、耗电高,用定时中断/芯片INT唤醒
基线在触摸中继续跟→手按久了松不开
只有单阈值无滞回→边界来回抖
LED PWM走线并行传感器→周期误触
水滴多键不屏蔽→湿手狂触发
滑条取max通道→位置跳变
手势不设时间窗→轻微颤动判成滑动
电源不独立LDO→开关电源纹波进基线
传感器走线过长→天线效应捡RF
八、落地顺序
定时扫描各通道Raw
IIR滤波压毛刺
非触摸态慢更新基线
Delta+滞回+去抖出按键
邻键广域激活判水膜
滑条重心法出位置
位置时序状态机出手势
事件上报应用层,按键/滑条/手势分开回调
电容触控稳不稳,代码层就看三件事:基线敢不敢在触摸时冻结、阈值有没有滞回、滑条用不用重心法。硬件布局(屏蔽电极、走线远离PWM、独立LDO)决定上限,固件算法决定下限。 只比“raw>常数”的方案,LED一调光、手一湿就废;把基线跟踪+去抖状态机+重心法滑条搭起来,家电面板、门锁、耳机仓基本都能过常规抗扰测试。





