1. 课程导入

各位做嵌入式GUI的同行,咱们今天聊聊触摸响应。这玩意儿看着简单——手指一点,屏幕动了。但真要把响应做到丝滑,底层原理必须吃透。我最早做触摸驱动时也踩过不少坑,今天把这些经验掰开揉碎讲给你。

1.1 触摸屏工作原理

市面上主流的触摸屏就两种:电阻式和电容式。别看现在电容屏满大街都是,电阻屏在某些工业场景里依然活得挺好。

电阻式触摸屏

电阻屏的原理说白了就是「压感导通」。两层导电薄膜中间隔着微小间隙,手指一压,上下层接触,控制器通过分压计算出坐标。

关键特性:

  • 支持任何物体触摸(手指、手套、笔尖)
  • 成本低,抗干扰强
  • 透光率差(约70-80%),需要校准
  • 不支持多点触控

我记得有个工控项目,现场环境油污多、操作工戴手套,电容屏根本没法用。最后换了四线电阻屏,虽然响应慢了点,但稳定可靠。嗯,这里要注意:电阻屏用久了,薄膜会老化,坐标漂移是家常便饭。我习惯在驱动里加定期校准逻辑,每1000次触摸自动触发一次。

电容式触摸屏

电容屏靠的是「人体耦合电容」。手指靠近时,电极间的电容发生变化,控制器检测这个变化量定位坐标。现在手机平板清一色用这个。

关键特性:

  • 支持多点触控(2点、5点、10点)
  • 透光率高(>90%),无需校准
  • 只能感应导电物体(裸指、电容笔)
  • 受湿度和噪声影响大

你想想看,为什么冬天戴手套点不了手机?因为普通手套不导电,电容屏感应不到。我做过一个户外设备,冬天操作员戴厚手套,最后只能改方案——加了个「手套模式」,提高检测灵敏度,但代价是误触率上升。

对比项 电阻式 电容式
触发方式 物理按压 电容感应
多点触控 不支持 支持
透光率 70-80% >90%
耐用性 易磨损 较好
成本 低 中高
典型场景 工控、医疗 消费电子

1.2 触摸事件模型

搞清楚了物理层,咱们看看软件层怎么处理。触摸事件模型,说白了就是「物理信号→坐标数据→GUI事件」这条链路。

我习惯把触摸事件分成三个层级:

  1. 物理层:ADC采样、电容检测,输出原始坐标
  2. 驱动层:滤波、去抖、坐标转换,输出标准化事件
  3. 应用层:命中检测、手势识别,输出GUI回调

我的经验:很多新手只关注应用层,觉得驱动层用现成的就行。但实际项目中,80%的触摸响应问题都出在驱动层——滤波参数不对、坐标转换有误差、中断处理不及时。我建议你至少把驱动层的代码通读一遍。

举个例子,一个典型的触摸事件结构体长这样:

typedef struct {
    uint16_t x;              // X坐标
    uint16_t y;              // Y坐标
    uint8_t  pressure;       // 压力值(电阻屏用)
    uint8_t  touch_id;       // 触摸点ID(多点触控用)
    touch_event_t event;     // 事件类型:按下/移动/抬起
    uint32_t timestamp;      // 时间戳(ms)
} touch_point_t;

为什么要有时间戳?我遇到过一个问题:两个触摸事件间隔太短,GUI还没来得及处理上一个,下一个就来了。加上时间戳后,可以在应用层做事件合并,避免重复处理。

1.3 中断与轮询机制对比

这是触摸响应里最核心的设计决策。用中断还是轮询?没有绝对答案,看场景。

轮询机制

轮询就是CPU定时去读触摸状态。简单粗暴,但效率低。

轮询的典型实现:

void touch_polling(void) {
    while(1) {
        if(touch_read(&point) == TOUCH_OK) {
            gui_handle_touch(&point);
        }
        delay_ms(10);  // 10ms轮询一次
    }
}

轮询的好处是代码简单,没有中断嵌套问题。但坏处也很明显——CPU被占着,啥也干不了。我见过一个项目,用轮询跑触摸,结果UI动画一卡一卡的,因为CPU大部分时间都在等触摸。

避坑指南:我曾经在一个低功耗项目里用轮询,结果发现设备待机电流下不去。后来一查,轮询线程每10ms唤醒一次,MCU根本没法深度睡眠。如果你做电池供电设备,轮询基本是死路。

中断机制

中断是触摸屏主动通知CPU:「嘿,有人摸我!」。CPU不用一直盯着,效率高得多。

中断的典型实现:

void touch_irq_handler(void) {
    // 注意:中断里只做最轻量的事
    touch_point_t point;
    if(touch_read(&point) == TOUCH_OK) {
        // 把数据扔到队列里,让任务去处理
        xQueueSend(touch_queue, &point, 0);
    }
}

void gui_task(void *param) {
    touch_point_t point;
    while(1) {
        if(xQueueReceive(touch_queue, &point, portMAX_DELAY)) {
            gui_handle_touch(&point);
        }
    }
}

中断的响应速度比轮询快得多。但要注意:中断服务程序里不能做耗时操作。我习惯的做法是「中断收数据,任务做处理」——中断里只把坐标塞进队列,真正的GUI处理交给任务线程。

对比项 轮询 中断
CPU占用 高(持续占用) 低(事件触发)
响应延迟 取决于轮询周期 微秒级
代码复杂度 低 中(需处理临界区)
功耗 高 低
适用场景 简单UI、低要求 高性能、低功耗

1.4 触摸响应核心流程

把上面这些串起来,一个完整的触摸响应流程是这样的:

触摸响应核心流程 物理触摸 驱动层:滤波 → 去抖 → 坐标转换 轮询:定时读取 中断:事件触发 应用层:命中检测 → 事件分发 GUI响应更新 关键决策点 中断 vs 轮询 滤波参数选择

我的建议:新项目直接上中断+任务队列的方案。虽然代码量比轮询多一倍,但后续扩展性好。如果你做的是超低成本的MCU(比如几毛钱的8位机),没有RTOS,那轮询可能是唯一选择。但记住——轮询周期别超过20ms,否则用户能感觉到延迟。