3、原始数据采集:读取触摸点坐标寄存器、数据校验与滤波、多指触控数据解析

触摸驱动调试的第一步,也是最容易出问题的一步,就是原始数据采集。

说白了,你得先把触摸屏上报的坐标读出来,读准了,后面的校准和手势识别才有意义。我见过不少工程师,上来就调滤波算法,结果发现数据源本身就是错的——坐标跳变、寄存器读错、多指数据乱套。嗯,这就像盖楼打歪了地基,后面再怎么装修也白搭。

3.1 读取触摸点坐标寄存器

不同的触控芯片,寄存器布局千差万别。但万变不离其宗,核心就三样东西:状态寄存器、坐标寄存器、点数寄存器。

我个人习惯,拿到一款新芯片,第一件事就是看 datasheet 里的寄存器映射表。先找到这三个关键地址,然后用 I2C 或 SPI 直接读回来看看。

典型寄存器读取流程:

  1. 读取状态寄存器,确认触摸事件发生(比如 bit0 为 1 表示有触摸)。
  2. 读取点数寄存器,获取当前触摸点数(单指还是多指)。
  3. 循环读取每个触摸点的 X/Y 坐标寄存器。
  4. 读取完毕,写回清除标志位,等待下一轮中断。

举个例子,某款常见电容触控芯片的寄存器定义如下:

寄存器地址 名称 描述
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 搞混了,那上层手势识别就全乱套了。

多指数据解析的核心逻辑,我总结为三步:

  1. 读取所有触摸点:根据点数寄存器,循环读取每个点的坐标和 ID。
  2. ID 匹配:将当前帧的点与上一帧的点进行匹配,确保同一根手指的 ID 不变。
  3. 数据上报:按匹配后的结果,将每个点的坐标和状态上报给上层。

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 本章知识体系

下面这张图,是我自己总结的原始数据采集全流程。你可以把它当作调试时的检查清单。

触摸原始数据采集流程 触摸原始数据采集与处理流程 中断触发 读取状态寄存器 读取触摸点数 循环读取各点坐标 数据校验 滤波处理 多指ID匹配 注意:多指触控必须做 ID 匹配,否则手势识别会混乱

这张图把整个流程串起来了。从中断触发开始,到读取状态、点数、坐标,再到校验、滤波、多指匹配,每一步都不能省。你调试的时候,可以对照这个流程,看看卡在哪一步。