3. TouchGFX校准框架:触摸事件处理流程与校准接口
这一章咱们来啃TouchGFX的触摸核心。说白了,就是要搞清楚:手指点下去之后,TouchGFX内部到底是怎么找到那个坐标的?校准接口又该插在哪里?
我刚开始接触TouchGFX时,也以为触摸处理就是个简单的回调函数。后来做一款7寸屏产品,发现点击总偏半个按钮的位置,才逼着自己把整个流程翻了个底朝天。嗯,今天就把这些经验掰开揉碎讲给你。
3.1 触摸事件的三层传递模型
TouchGFX的触摸处理,不是一步到位的。它分三层:
| 层级 | 组件/类 | 职责 |
|---|---|---|
| 硬件抽象层 | TouchController |
读取原始ADC/坐标数据 |
| 校准转换层 | TouchCalibration(自定义) |
应用偏移校正算法 |
| 事件分发层 | Application / Screen |
将校准后坐标派发给具体控件 |
为什么会这样设计?解耦。硬件驱动只管读数据,校准逻辑独立成模块,UI层只管用坐标。哪一环出问题,单独修就行,不用动整个系统。
核心原则:永远不要在TouchController里写校准算法。我见过有人把偏移补偿直接硬编码在驱动里,换一块屏就得改驱动,维护起来想哭。
3.2 默认流程:从硬件中断到屏幕响应
咱们走一遍标准流程。假设你用的是电阻屏或电容屏,TouchGFX的默认调用链是这样的:
- 硬件中断触发:触摸屏控制器(如XPT2046)产生INT信号。
- HAL层轮询:TouchGFX的
HAL::touchSample()在后台任务中被周期性调用。 - 调用
TouchController::sampleTouch():这个函数返回bool,并通过引用参数传出x、y坐标。 - 坐标未经校准直接传入:默认情况下,TouchGFX不内置校准,拿到的就是原始ADC值。
- 事件队列压入:坐标被包装成
ClickEvent或DragEvent,放入消息队列。 - Screen::handleClickEvent():事件被分发到当前活动Screen,再下发给具体控件。
你想想看,如果触摸屏本身贴歪了,或者ADC参考电压有偏差,第4步拿到的坐标就是错的。后面的分发再精确,也是错上加错。
避坑:我曾经在一个项目中,发现点击屏幕左上角,响应点却跑到了右下角。查了两天,最后发现是sampleTouch()里x和y的SPI读取顺序反了。所以,先确认原始数据方向对不对,再谈校准。
3.3 校准接口应该插在哪里?
TouchGFX官方没有提供现成的校准类。但框架留了一个很干净的切入点——HAL::touchSample()函数。
我们来看这个函数的默认实现(在TouchGFX/HAL/src/HAL.cpp中):
bool HAL::touchSample(int32_t& x, int32_t& y)
{
// 调用底层驱动
bool touched = touchController->sampleTouch(x, y);
// 注意:这里没有校准!
// 坐标x, y是原始值
return touched;
}
我的做法是:重写HAL::touchSample(),在校准层加一道转换。具体来说:
- 继承
HAL类(或者直接修改HAL.cpp,但我不建议改库文件)。 - 在
touchSample()中,先调用touchController->sampleTouch()拿到原始坐标。 - 调用自定义的
calibration.correct(x, y)函数,传入引用,原地修正。 - 返回修正后的坐标。
个人习惯:我会把校准参数(偏移量、缩放因子、旋转角度)放在一个独立的calibration_config.h头文件中。这样换屏时,只需要改这个头文件,不用动任何逻辑代码。
3.4 校准接口的抽象设计
下面是我在项目中用的一套接口,供你参考:
// calibration.h
class TouchCalibration {
public:
// 构造函数:加载校准参数
TouchCalibration(int16_t offsetX, int16_t offsetY,
float scaleX, float scaleY);
// 核心校准方法:原地修正坐标
void correct(int32_t& x, int32_t& y);
// 重置为出厂默认值
void resetToDefault();
// 保存校准参数到Flash(掉电不丢失)
bool saveToFlash(uint32_t address);
// 从Flash加载校准参数
bool loadFromFlash(uint32_t address);
private:
int16_t _offsetX;
int16_t _offsetY;
float _scaleX;
float _scaleY;
// 内部计算
int32_t apply(int32_t raw, int16_t offset, float scale);
};
这个接口的好处是:清晰、可测试、可持久化。你可以在PC上先用模拟数据验证校准算法,再烧到板子上。
3.5 整体框架图:校准数据流
下面这张图,是我每次做触摸校准培训时必画的。它把整个数据流串起来了:
从这张图可以清楚看到:校准层是承上启下的关键。它夹在TouchController和HAL之间,把原始坐标“洗”成UI能用的坐标。
3.6 实战:在TouchGFX项目中插入校准
说了这么多,咱们直接上手。假设你有一个TouchGFX项目,要在HAL::touchSample()里插入校准:
// 在 MyHAL.cpp 中重写 touchSample
#include "calibration.h"
// 全局校准对象(参数从Flash加载)
static TouchCalibration g_calib(0, 0, 1.0f, 1.0f);
bool MyHAL::touchSample(int32_t& x, int32_t& y)
{
// 1. 先拿原始数据
bool touched = touchController->sampleTouch(x, y);
if (touched)
{
// 2. 应用校准
g_calib.correct(x, y);
// 3. (可选)边界裁剪,防止越界
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x >= DISPLAY_WIDTH) x = DISPLAY_WIDTH - 1;
if (y >= DISPLAY_HEIGHT) y = DISPLAY_HEIGHT - 1;
}
return touched;
}
注意:边界裁剪这一步很容易被忽略。我遇到过校准后坐标变成负数,导致控件数组越界、系统硬fault的情况。加上裁剪,花不了几行代码,但能省很多调试时间。
3.7 校准参数的存储与加载
校准参数不能每次都手动输入。产品出厂后,用户可能自己做了校准,参数需要保存下来。我一般用外部Flash或EEPROM:
// 保存校准参数
bool TouchCalibration::saveToFlash(uint32_t address)
{
// 构造一个结构体,方便整体写入
struct CalibData {
uint32_t magic; // 校验魔数,防止读到垃圾数据
int16_t offsetX;
int16_t offsetY;
float scaleX;
float scaleY;
uint32_t checksum; // 简单校验和
};
CalibData data;
data.magic = 0xCA11B123;
data.offsetX = _offsetX;
data.offsetY = _offsetY;
data.scaleX = _scaleX;
data.scaleY = _scaleY;
data.checksum = data.magic ^ data.offsetX ^ data.offsetY
^ (uint32_t)(data.scaleX * 1000)
^ (uint32_t)(data.scaleY * 1000);
// 调用HAL的Flash写入函数
return HAL_FLASH_Write(address, (uint8_t*)&data, sizeof(data));
}
加载时,先读出来,校验魔数和checksum。如果校验失败,就用默认参数。这样即使Flash数据损坏,系统也不会死掉。
我曾经踩过的坑:第一次做Flash存储时,忘了考虑字节对齐。结构体里有float,直接memcpy到Flash地址,结果读回来全是乱码。后来加了__attribute__((packed))才解决。如果你也用GCC,记得加上这个属性。
3.8 小结:校准框架的核心要点
- 切入点明确:重写
HAL::touchSample(),不要动TouchController。 - 校准逻辑独立:封装成
TouchCalibration类,方便测试和复用。 - 参数持久化:用Flash/EEPROM保存,带校验机制。
- 边界保护:校准后做裁剪,防止坐标越界。
掌握了这个框架,你就有了一个可扩展的校准底座。下一层,咱们就可以往里面填具体的校准算法了——比如三点校准、最小二乘法拟合,这些才是真正解决偏移的硬核内容。