4、裸机校准实现:触摸点采集、滤波与坐标转换的完整流程
好,咱们进入正题。上一章讲了校准原理和数学模型,说白了就是一堆公式。这一章,我要带你亲手把这些公式变成能在裸机上跑的代码。
裸机环境,没有操作系统,没有线程调度,没有文件系统。你面对的就是一个CPU、一块LCD、一个触摸控制器。我刚开始做TouchGFX移植时,最头疼的就是这一步——明明校准算法算对了,点上去就是不准。后来才发现,问题出在采集和滤波上。
4.1 触摸点采集:从硬件寄存器到原始坐标
触摸控制器通常通过I2C或SPI接口与MCU通信。以我常用的FT5x06系列为例,它的原始坐标存放在特定的寄存器地址中。
采集流程其实很简单:
- 通过I2C/SPI读取触摸状态寄存器,判断是否有触摸事件
- 如果有触摸,读取X坐标高字节、X坐标低字节、Y坐标高字节、Y坐标低字节
- 组合成完整的原始坐标值
- 记录触摸事件的时间戳(用于后续滤波)
核心要点: 采集阶段不要做任何数据处理,只做一件事——把硬件吐出来的原始值原封不动地存下来。我在项目里见过有人直接在采集函数里做滤波,结果把有效数据也滤掉了,排查了半天。
下面是我常用的采集代码框架:
/* 触摸点数据结构 */
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个采样周期 |
我个人习惯在裸机校准阶段使用中值滤波+滑动平均的组合。为什么?因为校准过程是静态的——用户用手指点住某个点不动,这时候我们有充足的时间做滤波。
具体做法:
- 连续采集5个原始坐标点
- 对这5个点分别按X和Y排序,取中间值(中值滤波)
- 重复步骤1-2,得到3个中值滤波后的点
- 对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 完整流程:从采集到输出
把上面所有步骤串起来,就是裸机校准的完整流程。我画了一张图,方便你理解:
你看,整个流程其实不复杂。但每个环节都有坑。我总结一下最容易出问题的几个地方:
- 采集时序不对: 触摸控制器的数据就绪需要时间,读太快会读到旧数据。我一般会在状态寄存器置位后等5ms再读坐标。
- 滤波参数不合理: 窗口太大,延迟高;窗口太小,噪声滤不干净。5点中值+3次平均是我试下来比较平衡的组合。
- 坐标转换溢出: 校准参数算错了,转换出来的坐标可能超出LCD范围。边界裁剪一定要做,否则TouchGFX会崩溃。
最后说一句: 裸机校准的代码写完之后,一定要在屏幕上画一个十字光标,用手指点上去,看看光标是不是跟着手指走。如果光标乱跳,先检查滤波;如果光标偏了,先检查校准参数。这个调试方法我用了十年,屡试不爽。