3. TouchGFX校准框架:触摸事件处理流程与校准接口

这一章咱们来啃TouchGFX的触摸核心。说白了,就是要搞清楚:手指点下去之后,TouchGFX内部到底是怎么找到那个坐标的?校准接口又该插在哪里?

我刚开始接触TouchGFX时,也以为触摸处理就是个简单的回调函数。后来做一款7寸屏产品,发现点击总偏半个按钮的位置,才逼着自己把整个流程翻了个底朝天。嗯,今天就把这些经验掰开揉碎讲给你。

3.1 触摸事件的三层传递模型

TouchGFX的触摸处理,不是一步到位的。它分三层:

层级 组件/类 职责
硬件抽象层 TouchController 读取原始ADC/坐标数据
校准转换层 TouchCalibration(自定义) 应用偏移校正算法
事件分发层 Application / Screen 将校准后坐标派发给具体控件

为什么会这样设计?解耦。硬件驱动只管读数据,校准逻辑独立成模块,UI层只管用坐标。哪一环出问题,单独修就行,不用动整个系统。

核心原则:永远不要在TouchController里写校准算法。我见过有人把偏移补偿直接硬编码在驱动里,换一块屏就得改驱动,维护起来想哭。

3.2 默认流程:从硬件中断到屏幕响应

咱们走一遍标准流程。假设你用的是电阻屏或电容屏,TouchGFX的默认调用链是这样的:

  1. 硬件中断触发:触摸屏控制器(如XPT2046)产生INT信号。
  2. HAL层轮询:TouchGFX的HAL::touchSample()在后台任务中被周期性调用。
  3. 调用TouchController::sampleTouch():这个函数返回bool,并通过引用参数传出x、y坐标。
  4. 坐标未经校准直接传入:默认情况下,TouchGFX不内置校准,拿到的就是原始ADC值。
  5. 事件队列压入:坐标被包装成ClickEvent或DragEvent,放入消息队列。
  6. 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(),在校准层加一道转换。具体来说:

  1. 继承HAL类(或者直接修改HAL.cpp,但我不建议改库文件)。
  2. 在touchSample()中,先调用touchController->sampleTouch()拿到原始坐标。
  3. 调用自定义的calibration.correct(x, y)函数,传入引用,原地修正。
  4. 返回修正后的坐标。

个人习惯:我会把校准参数(偏移量、缩放因子、旋转角度)放在一个独立的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 整体框架图:校准数据流

下面这张图,是我每次做触摸校准培训时必画的。它把整个数据流串起来了:

TouchGFX触摸校准数据流框架图 TouchGFX 触摸校准数据流框架 物理触摸事件 TouchController::sampleTouch() TouchCalibration::correct(x, y) 偏移校正 + 缩放 + 旋转 HAL::touchSample() ClickEvent / DragEvent 校准参数来源 • 出厂预置值 • 用户校准界面 • Flash持久化存储 常见问题 • 坐标方向反了 • ADC值非线性 • 触摸屏贴歪

从这张图可以清楚看到:校准层是承上启下的关键。它夹在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保存,带校验机制。
  • 边界保护:校准后做裁剪,防止坐标越界。

掌握了这个框架,你就有了一个可扩展的校准底座。下一层,咱们就可以往里面填具体的校准算法了——比如三点校准、最小二乘法拟合,这些才是真正解决偏移的硬核内容。