4、裸机校准实现:触摸点采集、滤波与坐标转换的完整流程

好,咱们进入正题。上一章讲了校准原理和数学模型,说白了就是一堆公式。这一章,我要带你亲手把这些公式变成能在裸机上跑的代码。

裸机环境,没有操作系统,没有线程调度,没有文件系统。你面对的就是一个CPU、一块LCD、一个触摸控制器。我刚开始做TouchGFX移植时,最头疼的就是这一步——明明校准算法算对了,点上去就是不准。后来才发现,问题出在采集和滤波上。

4.1 触摸点采集:从硬件寄存器到原始坐标

触摸控制器通常通过I2C或SPI接口与MCU通信。以我常用的FT5x06系列为例,它的原始坐标存放在特定的寄存器地址中。

采集流程其实很简单:

  1. 通过I2C/SPI读取触摸状态寄存器,判断是否有触摸事件
  2. 如果有触摸,读取X坐标高字节、X坐标低字节、Y坐标高字节、Y坐标低字节
  3. 组合成完整的原始坐标值
  4. 记录触摸事件的时间戳(用于后续滤波)

核心要点: 采集阶段不要做任何数据处理,只做一件事——把硬件吐出来的原始值原封不动地存下来。我在项目里见过有人直接在采集函数里做滤波,结果把有效数据也滤掉了,排查了半天。

下面是我常用的采集代码框架:

/* 触摸点数据结构 */
typedef struct {
    uint16_t x_raw;
    uint16_t y_raw;
    uint32_t timestamp;  /* 单位ms,用于滤波 */
    uint8_t  touch_count; /* 触摸点数,单点一般为1 */
} TouchPoint_t;

/* 采集函数 */
uint8_t Touch_ReadPoint(TouchPoint_t *pt) {
    uint8_t buf[6];
    uint8_t status;
    
    /* 读取状态寄存器 */
    I2C_ReadReg(TOUCH_I2C_ADDR, FT5x06_STATUS_REG, &status, 1);
    
    if ((status & 0x0F) == 0) {
        return 0;  /* 无触摸 */
    }
    
    /* 读取坐标数据 */
    I2C_ReadReg(TOUCH_I2C_ADDR, FT5x06_TOUCH1_XH, buf, 6);
    
    /* 组合坐标 */
    pt->x_raw = ((uint16_t)(buf[0] & 0x0F) << 8) | buf[1];
    pt->y_raw = ((uint16_t)(buf[2] & 0x0F) << 8) | buf[3];
    pt->touch_count = status & 0x0F;
    pt->timestamp = HAL_GetTick();
    
    return 1;
}

经验之谈: 注意看代码里的 buf[0] & 0x0F,这个掩码操作很容易被忽略。有些触摸控制器的高字节只有低4位有效,高4位是标志位。我曾经因为没做这个掩码,读出来的坐标偶尔会跳到一个离谱的值,折腾了两天才发现。

4.2 滤波处理:别让噪声毁了你的校准

原始坐标直接拿来用?不行。触摸屏的噪声比你想象的大得多。我实测过,在手指轻轻触碰时,原始坐标的抖动范围能达到±20个像素。如果不滤波,校准出来的参数全是错的。

裸机环境下,我推荐两种滤波方法:

滤波方法 适用场景 内存开销 延迟
中值滤波 突发噪声(如静电干扰) 需缓存N个采样点 N/2个采样周期
滑动平均滤波 持续噪声(如电源纹波) 需缓存N个采样点 N/2个采样周期
一阶低通滤波 实时性要求高 仅需2个变量 1个采样周期

我个人习惯在裸机校准阶段使用中值滤波+滑动平均的组合。为什么?因为校准过程是静态的——用户用手指点住某个点不动,这时候我们有充足的时间做滤波。

具体做法:

  1. 连续采集5个原始坐标点
  2. 对这5个点分别按X和Y排序,取中间值(中值滤波)
  3. 重复步骤1-2,得到3个中值滤波后的点
  4. 对3个点做算术平均(滑动平均)

代码实现:

/* 中值滤波 */
uint16_t MedianFilter(uint16_t *data, uint8_t len) {
    uint8_t i, j;
    uint16_t temp;
    uint16_t buf[10];  /* 假设len不超过10 */
    
    /* 拷贝数据 */
    for (i = 0; i < len; i++) {
        buf[i] = data[i];
    }
    
    /* 冒泡排序 */
    for (i = 0; i < len - 1; i++) {
        for (j = 0; j < len - 1 - i; j++) {
            if (buf[j] > buf[j + 1]) {
                temp = buf[j];
                buf[j] = buf[j + 1];
                buf[j + 1] = temp;
            }
        }
    }
    
    return buf[len / 2];  /* 返回中值 */
}

/* 采集并滤波 */
uint8_t Touch_ReadFiltered(TouchPoint_t *pt) {
    uint16_t x_samples[5], y_samples[5];
    uint8_t i, valid_count = 0;
    uint32_t sum_x = 0, sum_y = 0;
    
    /* 采集5个原始点 */
    for (i = 0; i < 5; i++) {
        if (Touch_ReadPoint(pt)) {
            x_samples[i] = pt->x_raw;
            y_samples[i] = pt->y_raw;
            valid_count++;
        }
        /* 适当延时,让采样点之间有间隔 */
        Delay_ms(10);
    }
    
    if (valid_count < 3) {
        return 0;  /* 有效点太少,放弃本次采集 */
    }
    
    /* 中值滤波 */
    pt->x_raw = MedianFilter(x_samples, valid_count);
    pt->y_raw = MedianFilter(y_samples, valid_count);
    
    /* 重复3次,做滑动平均 */
    for (i = 0; i < 3; i++) {
        /* 这里简化处理,实际应重新采集3组中值滤波后的点 */
        sum_x += pt->x_raw;
        sum_y += pt->y_raw;
    }
    
    pt->x_raw = sum_x / 3;
    pt->y_raw = sum_y / 3;
    
    return 1;
}

注意: 滤波不是万能的。如果你的触摸屏硬件本身就有问题(比如FPC连接松动、电源纹波过大),滤波只能缓解症状,不能根治。我曾经在一个项目里花了整整一周调滤波参数,最后发现是触摸屏的接地没做好。嗯,先检查硬件,再调软件。

4.3 坐标转换:把公式变成代码

滤波之后,我们得到了相对稳定的原始坐标。接下来就是坐标转换——把原始坐标映射到LCD的像素坐标。

还记得上一章的校准公式吗?

对于三点校准法,我们需要解这个线性方程组:

X_lcd = A * X_raw + B * Y_raw + C
Y_lcd = D * X_raw + E * Y_raw + F

其中A、B、C、D、E、F就是校准参数。在裸机环境下,这些参数是预先计算好并存储在Flash或EEPROM中的。

坐标转换的代码实现:

/* 校准参数结构体 */
typedef struct {
    float A, B, C;  /* X轴参数 */
    float D, E, F;  /* Y轴参数 */
} CalibrationParams_t;

/* 全局校准参数 */
CalibrationParams_t g_calib;

/* 坐标转换函数 */
void Touch_Transform(uint16_t raw_x, uint16_t raw_y, 
                     uint16_t *lcd_x, uint16_t *lcd_y) {
    float fx, fy;
    
    /* 应用校准公式 */
    fx = g_calib.A * raw_x + g_calib.B * raw_y + g_calib.C;
    fy = g_calib.D * raw_x + g_calib.E * raw_y + g_calib.F;
    
    /* 边界裁剪 */
    if (fx < 0) fx = 0;
    if (fx >= LCD_WIDTH) fx = LCD_WIDTH - 1;
    if (fy < 0) fy = 0;
    if (fy >= LCD_HEIGHT) fy = LCD_HEIGHT - 1;
    
    *lcd_x = (uint16_t)fx;
    *lcd_y = (uint16_t)fy;
}

一个小技巧: 如果你的MCU没有浮点运算单元(FPU),用float做乘法会很慢。我建议把校准参数放大1000倍存成整数,计算时用整数运算,最后再除以1000。这样既保证了精度,又提升了速度。

4.4 完整流程:从采集到输出

把上面所有步骤串起来,就是裸机校准的完整流程。我画了一张图,方便你理解:

裸机校准完整流程图 1. 触摸控制器初始化 2. 采集5个原始坐标点 3. 中值滤波(排序取中值) 4. 滑动平均(3次取均值) 5. 坐标转换(应用校准公式) 6. 输出LCD像素坐标 关键说明 • 采集间隔建议10-20ms • 滤波窗口大小可调 • 边界裁剪防止越界

你看,整个流程其实不复杂。但每个环节都有坑。我总结一下最容易出问题的几个地方:

  • 采集时序不对: 触摸控制器的数据就绪需要时间,读太快会读到旧数据。我一般会在状态寄存器置位后等5ms再读坐标。
  • 滤波参数不合理: 窗口太大,延迟高;窗口太小,噪声滤不干净。5点中值+3次平均是我试下来比较平衡的组合。
  • 坐标转换溢出: 校准参数算错了,转换出来的坐标可能超出LCD范围。边界裁剪一定要做,否则TouchGFX会崩溃。

最后说一句: 裸机校准的代码写完之后,一定要在屏幕上画一个十字光标,用手指点上去,看看光标是不是跟着手指走。如果光标乱跳,先检查滤波;如果光标偏了,先检查校准参数。这个调试方法我用了十年,屡试不爽。