3. 输入事件处理:触摸屏原理、坐标校准与事件队列
嵌入式GUI里,输入事件处理这块,说白了就是「人怎么跟机器说话」。触摸屏、按键、编码器,这些都是嘴巴。而事件队列,就是翻译官和传话筒。我做了这么多年嵌入式,见过太多因为输入处理没做好,导致整个界面卡死、误触、漂移的案例。今天咱们就把这块彻底拆开看看。
3.1 触摸屏原理:电阻与电容
触摸屏分两大类:电阻式和电容式。两者原理完全不同,应用场景也天差地别。
3.1.1 电阻触摸屏
电阻屏的结构很简单:两层透明导电薄膜,中间用微小的绝缘点隔开。你用手指或笔一按,上层薄膜就压下去,跟下层接触了。控制器检测到接触点的电压,就能算出坐标。
它的工作方式是这样的:
- X轴测量:在左右两端加电压,从上下端读取分压值
- Y轴测量:在上下两端加电压,从左右端读取分压值
- 两个值组合起来,就是触摸点的坐标
3.1.2 电容触摸屏
电容屏的原理就优雅多了。它利用人体电场和屏幕表面的电极阵列形成耦合电容。手指靠近时,该点的电容值发生变化,控制器检测到变化量,就能定位。
电容屏有两种主流方案:
| 类型 | 原理 | 典型应用 |
|---|---|---|
| 表面电容式 | 四角加电压,检测电流变化 | 早期ATM机、工控屏 |
| 投射电容式 | 行列电极矩阵,检测互电容变化 | 智能手机、平板、现代GUI |
投射电容式是目前的主流。它支持多点触控,透光率高,手感顺滑。但有个致命弱点:只能感应导电物体。你戴个厚手套,或者用笔尖去戳,它根本不搭理你。
3.2 触摸坐标校准
触摸屏和显示屏是两个独立的物理设备。触摸屏采集的是模拟电压值,显示屏显示的是像素坐标。两者之间需要建立映射关系。这个映射过程,就是校准。
为什么需要校准?因为触摸屏和显示屏的坐标系可能不重合:
- 触摸屏的零点可能在左上角,也可能在左下角
- 触摸屏的X轴和显示屏的X轴可能方向相反
- 触摸屏的物理尺寸和显示屏的分辨率不成比例
- 触摸屏本身有非线性误差
3.2.1 三点校准法
最常用的校准方法是三点校准。原理很简单:在屏幕上显示三个已知坐标的点,让用户去点。记录触摸屏返回的原始值,然后解一个线性方程组。
校准公式是这样的:
// 显示屏坐标 = A * 触摸屏原始值 + B
// 对于每个轴,需要两个参数
Xd = A * Xt + B
Yd = C * Yt + D
三个点可以解出四个参数。但实际中,我们通常用最小二乘法拟合,用更多的点来提高精度。
3.2.2 校准流程
- 在屏幕四个角附近显示校准点(通常用十字准星)
- 等待用户点击,记录触摸原始值
- 用最小二乘法计算映射参数
- 将参数保存到非易失性存储(如EEPROM、Flash)
- 每次触摸事件发生时,用参数实时转换坐标
嗯,这里要注意:校准点的顺序和位置会影响校准精度。我习惯把校准点放在屏幕边缘内侧10%的位置,太靠边容易点偏,太靠中间又覆盖不了全屏。
3.3 按键与编码器输入
触摸屏虽然方便,但在某些场景下,物理按键和编码器反而更可靠。比如盲操作、戴手套、或者需要精确调节的场景。
3.3.1 物理按键
按键处理的核心是「去抖」。机械按键在按下和释放的瞬间,会产生多次抖动。如果不处理,一次按下可能被识别成几十次。
去抖有两种方式:
- 硬件去抖:用RC滤波电路或施密特触发器
- 软件去抖:采样后延时10-20ms,再确认电平状态
我个人习惯用软件去抖。因为硬件去抖会增加BOM成本,而且RC参数一旦固定,很难适应不同按键的机械特性。软件去抖灵活,改个延时值就行。
// 软件去抖示例
uint8_t debounce(uint8_t pin) {
uint8_t state = digitalRead(pin);
delay(10); // 等待10ms
uint8_t new_state = digitalRead(pin);
if (state == new_state) {
return state; // 稳定状态
} else {
return previous_state; // 抖动,返回之前的状态
}
}
3.3.2 旋转编码器
旋转编码器输出两路相位差90度的脉冲信号(A相和B相)。通过检测两路信号的相位关系,可以判断旋转方向。
判断方向的逻辑:
- A相上升沿时,B相为高电平 → 顺时针
- A相上升沿时,B相为低电平 → 逆时针
编码器的处理比按键复杂一些。因为它需要实时检测边沿变化,通常用外部中断来实现。
3.4 事件队列机制
好了,现在触摸屏、按键、编码器都能产生输入了。但问题来了:这些输入是异步的,随时可能发生。而GUI的绘制和逻辑处理是同步的,需要按顺序执行。怎么协调?
答案就是事件队列。
3.4.1 事件队列的结构
事件队列本质上是一个FIFO(先进先出)缓冲区。所有输入设备产生的事件,都先放进队列里。GUI主循环从队列头部取出事件,依次处理。
一个典型的事件结构体:
typedef struct {
uint8_t type; // 事件类型:触摸、按键、编码器
uint8_t id; // 设备ID:触摸屏0、按键1、编码器2
uint16_t x; // 触摸X坐标(如果是触摸事件)
uint16_t y; // 触摸Y坐标(如果是触摸事件)
uint8_t value; // 按键值或编码器增量
uint32_t timestamp; // 事件发生的时间戳
} Event_t;
3.4.2 队列的实现方式
嵌入式系统资源有限,事件队列通常用环形缓冲区实现。这样不需要动态内存分配,也不会产生碎片。
#define EVENT_QUEUE_SIZE 32
typedef struct {
Event_t buffer[EVENT_QUEUE_SIZE];
uint8_t head; // 写入位置
uint8_t tail; // 读取位置
uint8_t count; // 当前事件数
} EventQueue_t;
// 入队
uint8_t enqueue(EventQueue_t *q, Event_t *e) {
if (q->count >= EVENT_QUEUE_SIZE) {
return 0; // 队列满,丢弃
}
q->buffer[q->head] = *e;
q->head = (q->head + 1) % EVENT_QUEUE_SIZE;
q->count++;
return 1;
}
// 出队
uint8_t dequeue(EventQueue_t *q, Event_t *e) {
if (q->count == 0) {
return 0; // 队列空
}
*e = q->buffer[q->tail];
q->tail = (q->tail + 1) % EVENT_QUEUE_SIZE;
q->count--;
return 1;
}
3.4.3 事件处理的流程
整个输入事件处理的流程,可以用下面这张图来概括:
3.4.4 队列的优先级问题
你想想看,如果触摸屏事件和按键事件同时发生,谁先处理?
一般来说,事件队列是公平的,谁先来谁先处理。但在某些场景下,我们需要优先级:
- 紧急事件:比如「关机」按键,应该插队到最前面
- 普通事件:触摸、编码器,按顺序处理
- 低优先级事件:比如系统空闲时的自检事件,可以延后处理
实现优先级队列也不复杂。可以用多个队列,每个优先级一个。或者用一个队列,但事件结构体里加一个优先级字段,出队时按优先级排序。
3.5 本章小结
输入事件处理这块,核心就三件事:
- 采集:从触摸屏、按键、编码器拿到原始信号
- 转换:校准坐标、去抖、判断方向
- 排队:用事件队列统一管理,按顺序处理
这三件事做好了,你的GUI就成功了一半。剩下的,就是怎么把这些事件翻译成界面上的动作了。嗯,那是后面章节的内容,咱们到时候再细聊。