3、原始数据采集:读取触摸点坐标寄存器、数据校验与滤波、多指触控数据解析
触摸驱动调试的第一步,也是最容易出问题的一步,就是原始数据采集。
说白了,你得先把触摸屏上报的坐标读出来,读准了,后面的校准和手势识别才有意义。我见过不少工程师,上来就调滤波算法,结果发现数据源本身就是错的——坐标跳变、寄存器读错、多指数据乱套。嗯,这就像盖楼打歪了地基,后面再怎么装修也白搭。
3.1 读取触摸点坐标寄存器
不同的触控芯片,寄存器布局千差万别。但万变不离其宗,核心就三样东西:状态寄存器、坐标寄存器、点数寄存器。
我个人习惯,拿到一款新芯片,第一件事就是看 datasheet 里的寄存器映射表。先找到这三个关键地址,然后用 I2C 或 SPI 直接读回来看看。
典型寄存器读取流程:
- 读取状态寄存器,确认触摸事件发生(比如 bit0 为 1 表示有触摸)。
- 读取点数寄存器,获取当前触摸点数(单指还是多指)。
- 循环读取每个触摸点的 X/Y 坐标寄存器。
- 读取完毕,写回清除标志位,等待下一轮中断。
举个例子,某款常见电容触控芯片的寄存器定义如下:
| 寄存器地址 | 名称 | 描述 |
|---|---|---|
| 0x02 | Status | bit0: 触摸状态;bit1: 按键状态 |
| 0x03 | Touch Points | 当前触摸点数(1~5) |
| 0x04-0x05 | Point 1 X | 第一个触摸点的 X 坐标(16位) |
| 0x06-0x07 | Point 1 Y | 第一个触摸点的 Y 坐标(16位) |
| 0x08-0x09 | Point 2 X | 第二个触摸点的 X 坐标 |
| 0x0A-0x0B | Point 2 Y | 第二个触摸点的 Y 坐标 |
读取代码其实不复杂,但要注意字节序。大部分触控芯片是大端模式,高字节在前。我曾经踩过这个坑,读出来的坐标死活不对,后来发现是高低字节搞反了。
// 示例:读取单点触摸坐标(I2C 接口)
uint8_t buf[4];
uint16_t x, y;
// 从 0x04 地址连续读取 4 个字节
i2c_read(dev_addr, 0x04, buf, 4);
// 注意字节序:高字节在前
x = (buf[0] << 8) | buf[1];
y = (buf[2] << 8) | buf[3];
// 坐标有效性检查
if (x > MAX_X_RES || y > MAX_Y_RES) {
// 数据异常,丢弃
return -1;
}
小技巧:调试阶段,我建议把原始寄存器值全部打印出来,包括状态位和点数。很多时候,芯片上报了 5 个点,但实际只有 2 根手指——多出来的都是噪声或基线漂移导致的伪点。
3.2 数据校验与滤波
读回来的坐标,你敢直接用吗?反正我不敢。
触摸屏的原始数据,受电源噪声、手指抖动、屏幕干扰等因素影响,经常会出现毛刺和跳变。你想想看,如果用户明明按着一个点不动,坐标却在 ±10 像素范围内乱跳,那体验得多糟糕。
数据校验,我一般分两步走:
- 范围校验:坐标是否在有效分辨率内?比如 X 最大是 1024,读出来 2048,那肯定是错的。
- 连续性校验:相邻两次采样的坐标变化是否合理?如果上一帧在左上角,这一帧突然跑到右下角,大概率是异常。
滤波方面,我个人最常用的是限幅滤波和滑动平均滤波的组合。
限幅滤波 + 滑动平均 示例:
#define MAX_DELTA 30 // 最大允许的像素跳变
#define FILTER_LEN 5 // 滑动窗口长度
static int16_t filter_buf[FILTER_LEN];
static uint8_t filter_idx = 0;
int16_t touch_filter(int16_t new_val, int16_t last_val) {
int32_t sum = 0;
// 1. 限幅:如果跳变太大,直接丢弃
if (abs(new_val - last_val) > MAX_DELTA) {
return last_val; // 保持上一帧的值
}
// 2. 滑动平均
filter_buf[filter_idx] = new_val;
filter_idx = (filter_idx + 1) % FILTER_LEN;
for (int i = 0; i < FILTER_LEN; i++) {
sum += filter_buf[i];
}
return (int16_t)(sum / FILTER_LEN);
}
为什么这么组合?限幅滤波能干掉明显的野点,滑动平均能平滑掉高频抖动。但要注意,滑动窗口不能太大,否则延迟会很明显。我一般控制在 3~5 个点,既平滑又不跟手。
避坑指南:我曾经在一个项目中,滤波窗口设了 10 个点,结果用户快速滑动时,坐标延迟了将近 100ms,体验极差。后来改成 3 点滑动平均 + 限幅,效果好了很多。记住,滤波不是越强越好,要在平滑和响应之间找平衡。
3.3 多指触控数据解析
多指触控,才是真正考验驱动功底的地方。
单指的时候,你只需要读一个点的坐标。但多指时,你要处理点 ID 管理、点迹跟踪、新旧帧匹配这些问题。你想想看,如果两根手指交叉滑动,驱动把手指 A 和手指 B 的 ID 搞混了,那上层手势识别就全乱套了。
多指数据解析的核心逻辑,我总结为三步:
- 读取所有触摸点:根据点数寄存器,循环读取每个点的坐标和 ID。
- ID 匹配:将当前帧的点与上一帧的点进行匹配,确保同一根手指的 ID 不变。
- 数据上报:按匹配后的结果,将每个点的坐标和状态上报给上层。
ID 匹配的算法,我一般用最近邻匹配:计算当前帧每个点与上一帧所有点的欧氏距离,距离最小的认为是同一根手指。
// 多指 ID 匹配伪代码
typedef struct {
uint8_t id;
uint16_t x;
uint16_t y;
uint8_t status; // 0: 释放, 1: 按下, 2: 移动
} touch_point_t;
void match_points(touch_point_t *cur, int cur_cnt,
touch_point_t *prev, int prev_cnt) {
for (int i = 0; i < cur_cnt; i++) {
int min_dist = 9999;
int match_idx = -1;
for (int j = 0; j < prev_cnt; j++) {
int dist = abs(cur[i].x - prev[j].x) +
abs(cur[i].y - prev[j].y);
if (dist < min_dist) {
min_dist = dist;
match_idx = j;
}
}
if (match_idx != -1 && min_dist < MAX_MATCH_DIST) {
cur[i].id = prev[match_idx].id; // 继承上一帧的 ID
cur[i].status = 2; // 移动
} else {
cur[i].id = get_new_id(); // 新手指出现
cur[i].status = 1; // 按下
}
}
}
经验之谈:多指调试时,我习惯在串口打印每个点的 ID 和坐标,然后用手指在屏幕上画圈。如果 ID 不乱跳,说明匹配算法没问题。如果 ID 频繁切换,那就要检查匹配阈值是不是设得太宽松了。
另外,多指触控还有一个容易忽略的点:手指抬起事件。有些芯片在手指抬起后,不会立即清除该点的寄存器,而是把坐标设为 0xFFFF 或某个特殊值。驱动里必须判断这种“幽灵点”,否则上层会一直认为有手指按着。
避坑指南:我曾经遇到一个 case,手指明明已经拿开了,但屏幕上还有一个点不动。查了半天,发现是驱动没有处理芯片上报的“无效坐标”标志位。后来加了一行判断:如果 X 和 Y 都等于 0xFFFF,直接跳过该点。问题解决。
3.4 本章知识体系
下面这张图,是我自己总结的原始数据采集全流程。你可以把它当作调试时的检查清单。
这张图把整个流程串起来了。从中断触发开始,到读取状态、点数、坐标,再到校验、滤波、多指匹配,每一步都不能省。你调试的时候,可以对照这个流程,看看卡在哪一步。