4. 原始数据采集:触摸点坐标的ADC采样原理、滤波算法
好,咱们进入正题。
触摸屏的原始数据采集,说白了就是怎么把手指按下去的位置,变成CPU能认的数字。这一步要是做不好,后面再牛的算法也白搭。我见过不少项目,滤波算法写得天花乱坠,结果ADC采样本身就有问题——嗯,这就是典型的“地基没打好”。
4.1 触摸点坐标的ADC采样原理
电阻式触摸屏的原理其实很简单。你想想看,它就像两块导电薄膜叠在一起。你按下去,上下两层接触了。这时候,我在X方向加一个电压,Y方向就能读到一个分压值。反过来,Y方向加电压,X方向读数。两个读数一组合,就是坐标。
具体到电路上,我们一般用MCU自带的ADC来采样。我个人习惯用12位的ADC,分辨率4096,对于大多数HMI应用足够了。采样的时候要注意几个关键点:
- 采样时序:触摸按下后,不能立刻采样。薄膜接触有个稳定过程,我一般等5-10ms再开始采。
- 参考电压:ADC的参考电压必须稳定。我曾经在一个项目里直接用LDO输出做参考,结果LDO纹波太大,坐标跳得跟心电图似的。
- 采样速率:工业HMI一般100Hz左右就够了。太快了反而容易引入噪声。
核心要点:ADC采到的不是坐标,是电压值。坐标 = 电压值 / 参考电压 × 屏幕分辨率。这个换算公式一定要写对,我见过有人把分子分母搞反了,结果触摸点满屏乱飞。
4.2 均值滤波
均值滤波是最基础的,也是我最早用的方法。说白了就是连续采N次,然后求个平均值。
// 均值滤波示例
#define FILTER_N 8
uint16_t average_filter(uint16_t *buf, uint8_t len)
{
uint32_t sum = 0;
for(uint8_t i = 0; i < len; i++)
{
sum += buf[i];
}
return (uint16_t)(sum / len);
}
这个算法简单吧?但坑也不少。N取太大,响应变慢;N取太小,滤波效果差。我一般取8或16。还有,均值滤波对脉冲噪声基本无效——你想想看,一个异常大值和一堆正常值平均,结果还是偏大。
我的经验:均值滤波适合用在噪声比较平稳的场合。比如电源纹波引起的随机抖动,效果不错。但如果是偶尔的尖峰干扰,就别指望它了。
4.3 中值滤波
中值滤波就聪明多了。它把采样值排序,取中间那个。这样一来,哪怕有一个采样值离谱地高或低,只要数量不多,中间值基本不受影响。
// 中值滤波示例
#define MEDIAN_N 5
uint16_t median_filter(uint16_t *buf, uint8_t len)
{
// 冒泡排序(数据量小,够用)
for(uint8_t i = 0; i < len - 1; i++)
{
for(uint8_t j = 0; j < len - 1 - i; j++)
{
if(buf[j] > buf[j + 1])
{
uint16_t temp = buf[j];
buf[j] = buf[j + 1];
buf[j + 1] = temp;
}
}
}
return buf[len / 2];
}
中值滤波的缺点也很明显——慢。排序需要时间,而且N越大越慢。我一般取3或5,再大就不划算了。另外,中值滤波对连续多个异常值也无能为力。比如连续3个采样都是干扰,中值滤波也救不了。
注意:中值滤波会改变信号的时序特性。如果你需要精确的时间信息,比如手势识别,中值滤波可能会引入延迟。我曾经在一个手势识别项目里用中值滤波,结果滑动轨迹变得“一卡一卡”的,后来换成了卡尔曼滤波才解决。
4.4 卡尔曼滤波
卡尔曼滤波,听起来高大上,其实没那么神秘。它本质上是一个“预测+修正”的过程。我预测下一个触摸点大概在哪,然后用实际测量值来修正这个预测。
对于触摸屏这种一维系统(X和Y分开处理),卡尔曼滤波可以简化成几个公式:
// 一维卡尔曼滤波简化实现
typedef struct {
float x; // 状态值(当前估计的坐标)
float p; // 估计误差协方差
float q; // 过程噪声协方差
float r; // 测量噪声协方差
float k; // 卡尔曼增益
} kalman_t;
void kalman_init(kalman_t *kf, float init_x, float init_p, float q, float r)
{
kf->x = init_x;
kf->p = init_p;
kf->q = q;
kf->r = r;
kf->k = 0.0f;
}
float kalman_update(kalman_t *kf, float measurement)
{
// 预测
kf->p = kf->p + kf->q;
// 计算卡尔曼增益
kf->k = kf->p / (kf->p + kf->r);
// 更新状态
kf->x = kf->x + kf->k * (measurement - kf->x);
// 更新误差协方差
kf->p = (1.0f - kf->k) * kf->p;
return kf->x;
}
这里有两个关键参数:q和r。q代表你对模型的信任程度,r代表你对测量值的信任程度。q越大,滤波越“灵活”,响应越快;r越大,滤波越“平滑”,但延迟也大。
调参心得:我一般先设q=0.01,r=1.0,然后根据实际效果微调。如果触摸点抖动厉害,就增大r;如果响应太慢,就增大q。记住,没有万能参数,每个项目都得现场调。
4.5 三种滤波算法的对比
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 均值滤波 | 简单、快速、资源占用少 | 对脉冲噪声无效、响应慢 | 噪声平稳的工业环境 |
| 中值滤波 | 抗脉冲噪声能力强 | 计算量大、有延迟 | 有偶发干扰的场合 |
| 卡尔曼滤波 | 平滑度高、可调性强 | 需要调参、浮点运算 | 高精度、手势识别等 |
我的建议:别一上来就用卡尔曼滤波。先试试均值滤波,不行再换中值滤波,最后才上卡尔曼。我曾经在一个项目里,均值滤波加中值滤波组合使用,效果就很好,根本没必要上卡尔曼。记住,最简单的方案往往是最可靠的。
4.6 实战中的避坑指南
最后,分享几个我踩过的坑:
- 采样频率别太高:我曾经把采样频率设到1kHz,结果CPU忙得连界面刷新都卡了。工业HMI 100Hz足够。
- 注意ADC的建立时间:切换通道后,ADC需要时间稳定。我一般加一个虚拟采样,丢弃第一次结果。
- 滤波算法要放在中断里还是主循环里?:我个人习惯放在主循环里。中断里只做采样和存数据,滤波和坐标计算放到主循环。这样不会阻塞中断响应。
- 别忘了去抖:触摸按下和释放也有抖动。我一般加一个20ms的去抖计时器,确认状态稳定后再处理。
嗯,关于原始数据采集,就讲这么多。下一章咱们聊聊坐标转换和校准算法——那才是真正让触摸屏“指哪打哪”的关键。