3. 原始触摸数据采集:从物理信号到可用坐标

触摸屏能响应你的手指,靠的是底层控制器不断上报原始数据。但控制器吐出来的东西,说白了就是一堆带噪声的电压值或电容变化量。直接拿这些值去驱动光标?那光标会像喝醉了一样满屏乱跳。

我刚开始做触摸驱动时,就吃过这个亏。当时觉得「数据读回来不就能用了吗」,结果光标抖得用户根本点不准按钮。后来才明白——原始数据必须经过采集、滤波、归一化这三关,才能交给上层用。

核心流程: 物理触摸 → 控制器寄存器读取 → 数据滤波去抖 → 坐标归一化 → 可用坐标

原始触摸数据采集与处理流程 物理触摸 手指/笔接触 SPI/I2C 读取 控制器寄存器 滤波与去抖 中值/均值/限幅 坐标归一化 映射到屏幕 ⚠ 常见问题 • 读取时序不对 → 数据错位或 CRC 错误 • 未做滤波 → 光标抖动,点击误触发 • 归一化参数错误 → 触摸位置与显示位置偏移

3.1 通过 SPI/I2C 读取触摸控制器寄存器

触摸控制器和主控芯片之间,最常用的就是 SPI 或 I2C。选哪个?看你的项目需求。

通信协议 引脚数 速度 适合场景
I2C 2 线 (SCL, SDA) 100kHz~1MHz 引脚紧张、布线简单
SPI 4 线 (SCLK, MOSI, MISO, CS) 10MHz~50MHz 高刷新率、多触摸点

我个人习惯用 I2C 做原型验证,因为接线少、调试方便。但量产项目如果触摸点数超过 5 个,我会换成 SPI——数据量一大,I2C 的速率瓶颈就暴露了。

读取流程

  1. 初始化通信接口:配置 GPIO 为对应功能,设置时钟频率。
  2. 发送读命令:向控制器写入寄存器地址,通常需要先写地址再读数据。
  3. 读取数据包:根据控制器数据手册,读取固定长度的字节。
  4. 解析数据:提取触摸状态、X/Y 坐标、压力值等。

我的经验: 很多控制器支持「批量读取」模式。一次读取多个寄存器,比单次读取效率高得多。我曾经在 GT911 上把读取时间从 2ms 降到了 0.5ms,就靠这个。

// I2C 读取触摸控制器示例 (以 FT5x06 为例)
uint8_t buf[8];
uint8_t reg_addr = 0x02;  // 触摸状态寄存器地址

i2c_write(dev_addr, &reg_addr, 1);   // 发送寄存器地址
i2c_read(dev_addr, buf, 8);          // 读取 8 字节数据

// 解析坐标
uint8_t touch_points = buf[0] & 0x0F; // 触摸点数
uint16_t x = ((buf[1] & 0x0F) << 8) | buf[2];
uint16_t y = ((buf[3] & 0x0F) << 8) | buf[4];

注意: 不同控制器的数据格式差异很大。有的用 12 位坐标,有的用 16 位。一定要仔细看数据手册的寄存器映射表。我曾经因为字节序搞反,浪费了整整一天。

3.2 数据滤波与去抖

原始数据读回来了,但你会发现——手指明明没动,坐标却在几个像素之间来回跳。这就是噪声。触摸控制器的 ADC 采样、电源纹波、甚至手指的微小颤动都会引入抖动。

怎么处理?我常用的方法有三种,各有优劣。

滤波方法 原理 优点 缺点
限幅滤波 当前值与上次值差值超过阈值则丢弃 简单、响应快 无法消除周期性噪声
中值滤波 取 N 次采样值的中间值 抗脉冲噪声好 延迟较大
滑动平均 取最近 N 次采样值的平均值 平滑效果好 有相位滞后

我推荐的组合方案

实际项目中,我很少只用一种滤波。我的惯用套路是:

  • 先限幅:剔除明显异常的跳变(比如一次跳了 100 像素以上)。
  • 再滑动平均:取最近 4~8 次数据做平均,平滑微小抖动。
// 限幅 + 滑动平均 组合滤波
#define MAX_DELTA  50    // 最大允许跳变
#define FILTER_N   4     // 滑动窗口大小

static int16_t filter_buf[FILTER_N];
static uint8_t filter_idx = 0;

int16_t touch_filter(int16_t raw_value, int16_t last_value) {
    // 1. 限幅
    if (abs(raw_value - last_value) > MAX_DELTA) {
        return last_value;  // 丢弃异常值
    }

    // 2. 滑动平均
    filter_buf[filter_idx] = raw_value;
    filter_idx = (filter_idx + 1) % FILTER_N;

    int32_t sum = 0;
    for (int i = 0; i < FILTER_N; i++) {
        sum += filter_buf[i];
    }
    return sum / FILTER_N;
}

避坑指南: 滤波窗口不是越大越好。窗口太大,触摸响应会变慢,用户会感觉「卡顿」。我一般把窗口控制在 4~8 次,采样率 100Hz 时延迟约 40~80ms,人眼基本无感。

3.3 坐标归一化处理

滤波后的坐标还是「原始坐标」,范围取决于触摸控制器的分辨率。比如某款控制器输出 0~4095,但你的屏幕分辨率是 480x320。直接拿 4095 去驱动 480 像素?那触摸点会跑到屏幕外面去。

归一化,就是把原始坐标映射到屏幕坐标空间。

归一化公式

screen_x = (raw_x - x_min) * screen_width  / (x_max - x_min)
screen_y = (raw_y - y_min) * screen_height / (y_max - y_min)

其中 x_min、x_max 是触摸屏有效区域的原始坐标范围。这个范围怎么得到?

  • 方法一:读数据手册——控制器厂商通常会给出。
  • 方法二:实际标定——在屏幕四角点按,记录坐标值。

我个人更推荐方法二。因为硬件装配有公差,数据手册的值往往不准。我在一个项目里就遇到过——手册写 X 范围 0~4095,实际测下来只有 50~4000,边缘差了将近 2mm。

// 坐标归一化实现
typedef struct {
    int16_t x_min, x_max;
    int16_t y_min, y_max;
    int16_t screen_width;
    int16_t screen_height;
} touch_calib_t;

void touch_normalize(int16_t raw_x, int16_t raw_y,
                     int16_t *screen_x, int16_t *screen_y,
                     touch_calib_t *calib) {
    // 防止除零
    if (calib->x_max == calib->x_min) return;
    if (calib->y_max == calib->y_min) return;

    // 限幅
    if (raw_x < calib->x_min) raw_x = calib->x_min;
    if (raw_x > calib->x_max) raw_x = calib->x_max;
    if (raw_y < calib->y_min) raw_y = calib->y_min;
    if (raw_y > calib->y_max) raw_y = calib->y_max;

    *screen_x = (raw_x - calib->x_min) * calib->screen_width  / (calib->x_max - calib->x_min);
    *screen_y = (raw_y - calib->y_min) * calib->screen_height / (calib->y_max - calib->y_min);
}

小技巧: 归一化时用整数运算,避免浮点数。嵌入式 MCU 没有 FPU 的话,浮点运算会拖慢速度。上面的代码全部用整数除法,效率高很多。

3.4 完整数据采集流程示例

把上面三步串起来,就是一个完整的触摸数据采集循环。嗯,这里我贴一个实际项目里用过的伪代码框架。

void touch_task(void) {
    static int16_t last_x = 0, last_y = 0;
    int16_t raw_x, raw_y;
    int16_t filtered_x, filtered_y;
    int16_t screen_x, screen_y;

    // 1. 读取原始数据
    if (touch_read(&raw_x, &raw_y) != 0) {
        return;  // 读取失败
    }

    // 2. 滤波去抖
    filtered_x = touch_filter(raw_x, last_x);
    filtered_y = touch_filter(raw_y, last_y);

    // 3. 坐标归一化
    touch_normalize(filtered_x, filtered_y,
                    &screen_x, &screen_y,
                    &g_calib);

    // 4. 更新 LVGL 输入
    lv_indev_set_point(g_indev, screen_x, screen_y);

    last_x = filtered_x;
    last_y = filtered_y;
}

重要提醒: 这个任务要放在定时器或 RTOS 任务里周期性执行。采样率建议 50~100Hz。太快了 MCU 扛不住,太慢了用户觉得卡。我一般用 80Hz,兼顾流畅度和性能。

3.5 本章小结

原始触摸数据采集,说白了就是三件事:

  • 读出来——通过 SPI/I2C 从控制器寄存器拿数据。
  • 洗干净——用限幅+滑动平均去掉噪声和抖动。
  • 对齐——把原始坐标映射到屏幕像素坐标。

这三步做好了,触摸数据才能交给 LVGL 用。否则后面做再多校准算法,都是白搭。我在好几个项目里都验证过——滤波和归一化做扎实了,触摸体验直接上一个台阶。