简介:基于MSP430F425微控制器实现滑动按键、滑动触摸与触摸长按三种交互方式的嵌入式工程资源,面向电子设计竞赛、低功耗触摸面板开发以及MSP430初学者和进阶开发者。压缩包共12个文件,体积仅15KB,包含main.c源码、IAR工程文件(.eww/.ewp)、调试配置文件(.ewd/.dbgdt)、批处理脚本(.bat)等,构成一个便于参考的IAR工程,可对照学习项目结构与编译环境。内容覆盖电容式触摸感应原理、定时器与ADC周期性采样、连续电容值比较与滑动方向速度识别、多节点网格轨迹跟踪、长按计时阈值设置,以及中断服务程序和抗干扰设计等关键技术点,贴合实际产品开发中的低功耗与稳定性需求。已有246人学习下载,适合希望快速掌握MSP430触摸方案、阅读工程源码并借鉴代码框架的开发者。
1. 先别急着改界面:CMAJ2 里三个触摸交互共用一套事件流
打开 CMAJ2.rar 这类固件包,真正决定交互手感的东西不在 UI 绘制函数里,而在最底层那个触摸事件上报入口。滑动按键、滑动触摸、触摸长按看着是三个独立功能,落到代码里其实是同一份坐标流在不同时间窗里的三种判决。在按下、移动、抬起三态之间加一个小的中转状态,比分别写三个模块更稳,也更利于在后续调参时把误触规则收敛到一处。这一步对嵌入式 UI 尤其关键:中断和调度抖动会让触摸事件乱序,手势层必须自己做“先判断、再触发”的仲裁,而不是直接响应每个坐标。
2. 滑动触摸与滑动按键共用的底层事件流模型
触摸屏驱动做的事情比很多人想象中少。电容屏控制器一般只给出被触发点的 X、Y 坐标和一个 pressed 状态,有些芯片把 pressed 换成“触点数量”,数量大于 0 就相当于按下。至于这个点是从哪根手指来的、是不是噪声,驱动层并不负责。手势层要把这些原始采样整理成可判别的序列事件,才有资格谈滑动和长按。
2.1 从触摸面板拿到的事件里必须保留字段
我先给一个最小结构体,它也是后续所有手势代码的输入格式:
typedef struct { uint16_t x; uint16_t y; uint8_t pressed; /* 1 = 手指按下,0 = 手指抬起 */ uint32_t t_ms; /* 单调递增的毫秒时间戳 */ } touch_point_t;这个结构体只保存一帧数据。为了让状态机感知“按下”“抬起”这两个边沿,还要额外做一次去重过滤。很多开发者在驱动里用pressed == 1直接触发按压逻辑,结果同一帧被循环读到两次,长按计时就被冲掉了。常见做法是把上一次 pressed 状态存下来,对比后再决定返回什么事件:
#define TOUCH_PRESSED 1 #define TOUCH_RELEASED 2 #define TOUCH_MOVED 3 static uint8_t last_pressed; int touch_scan_filter(touch_point_t *now) { int type; if (now->pressed && !last_pressed) type = TOUCH_PRESSED; else if (!now->pressed && last_pressed) type = TOUCH_RELEASED; else type = TOUCH_MOVED; last_pressed = now->pressed; return type; }这段过滤的逻辑并不复杂,关键是让手势层只消费一次边沿事件。每次扫描拿到TOUCH_PRESSED时记录起点,拿到TOUCH_RELEASED时结算终点,中间一直返回TOUCH_MOVED。这样长按的“保持次数”才准确,滑动触摸的“路径长度”也不会因为重复的按下事件而多加一截。
2.2 滑动触摸、滑动按键、触摸长按的判定差异
三种手势的差异,压缩到两个维度上就是位移距离和保持时间。一句话版本:滑动触摸看运动速度,滑动按键看方向锁定,触摸长按看“既没动又按得久”。我一般把这些条件提前写成一张对照表,调参时才不会互相打架:
| 交互名称 | 触发依据 | 常用边界值 | 常见误判 |
|---|---|---|---|
| 滑动触摸 | 单次位移超过阈值,且方向夹角可控 | 15~30 px,方向夹角小于 30° | 手指从上向下拖动时被误判为左右滑动 |
| 滑动按键 | 起点命中控件,随后沿固定轴移动 | 30~50 px,方向按轴锁定 | 一次短滑被拆成“点击 + 滑动”两次事件 |
| 触摸长按 | 保持时间超过阈值,位移始终锁定在范围内 | 500~800 ms,位移小于 8 px | 慢速拖动在超时后被误判成长按 |
从这个表可以看出,长按是最脆弱的:它要求时间足够长、位移足够小。而滑动触摸正好相反,位移是决定性条件,时间反而不重要。所以两套判定不能放在同一个 if 里做“谁先到谁赢”,需要给长按一个短暂的仲裁窗口。这个窗口的具体长度取多大,取决于轮询周期和手抖幅度,放到第 4 章展开。
3. 实现滑动按键和滑动触摸的状态机:方向锁与事件回调
如果同一个工程里既要页面左右滑动,又要滑块控件跟随手指,还希望列表上下滚动时不被长按打断,就得把决策集中到手势状态机里。逐层写回调最终会变成“一堆 if 判断当前坐标落在哪个控件”,难以维护,也没有办法统一处理优先级。
3.1 用四态状态机同时承载两种滑动语义
状态机只需要四个状态:空闲、按住未定、滑动中、长按中。这里的“按住未定”是核心,它承担从按下到判定成功之间的所有中间过程。定义如下:
#define ST_IDLE 0 #define ST_HOLDING 1 #define ST_SWIPING 2 #define ST_LONGPRESS 3 typedef struct { uint8_t state; uint16_t x0, y0; uint16_t x_last, y_last; uint32_t press_t_ms; uint8_t dir; } gesture_ctx_t;有了这个上下文结构,每个扫描周期把触摸事件喂给一个统一的处理函数。下面的代码是简化版,但已经把滑动、长按和点击的互斥关系写清楚了:
int gesture_run(gesture_ctx_t *g, touch_point_t *ev) { int dx = (int)ev->x - g->x0; int dy = (int)ev->y - g->y0; int dist2 = dx * dx + dy * dy; switch (g->state) { case ST_IDLE: if (ev->pressed) { g->x0 = g->x_last = ev->x; g->y0 = g->y_last = ev->y; g->press_t_ms = ev->t_ms; g->state = ST_HOLDING; } return GESTURE_NONE; case ST_HOLDING: if (!ev->pressed) { g->state = ST_IDLE; if (ev->t_ms - g->press_t_ms < SHORT_CLICK_MS) return GESTURE_CLICK; return GESTURE_NONE; } if (ev->t_ms - g->press_t_ms > LONGPRESS_MS && dist2 < LONGPRESS_DEADZONE2) { g->state = ST_LONGPRESS; return GESTURE_LONGPRESS_START; } if (dist2 > SWIPE_DIST2) { g->state = ST_SWIPING; g->dir = (abs(dx) > abs(dy)) ? (dx > 0 ? DIR_RIGHT : DIR_LEFT) : (dy > 0 ? DIR_DOWN : DIR_UP); return GESTURE_SWIPE_START; } break; case ST_SWIPING: if (!ev->pressed) { g->state = ST_IDLE; return GESTURE_SWIPE_END; } if (g->dir == DIR_RIGHT || g->dir == DIR_LEFT) return GESTURE_SWIPE_DELTA_X; return GESTURE_SWIPE_DELTA_Y; case ST_LONGPRESS: if (!ev->pressed) { g->state = ST_IDLE; return GESTURE_LONGPRESS_END; } if (ev->t_ms - g->press_t_ms >= LONGPRESS_REPEAT_MS) { g->press_t_ms += LONGPRESS_REPEAT_MS; return GESTURE_LONGPRESS_REPEAT; } break; } return GESTURE_NONE; }这段代码使用平方距离来避免开方,SWIPE_DIST2是滑动阈值平方,LONGPRESS_DEADZONE2是长按允许位移的平方。注意ST_HOLDING分支里长按和滑动是两个并列条件,但滑动条件的位移判定优先在前面的时间判断之后。这意味着如果手指快速移动并停留在某处超过长按时间,只要位移已经先行超过滑动阈值,状态就已经切到ST_SWIPING,不会再触发长按。这是一种“位移优先于时间”的仲裁策略。
注意:不要在
ST_HOLDING分支中先判断时间再判断位移。顺序写反的后果是:慢速拖动在 600ms 后触发了长按,但拖动手势本身应该归滑动。
3.2 滑动按键和页面滑动的差异在“命中检测”而非指令集
状态机给出的GESTURE_SWIPE_START和GESTURE_SWIPE_DELTA_X是通用事件。所谓“滑动按键”,我的理解是:它是一个可交互控件,不是一条全屏手势。所以要区分“滑动触摸”和“滑动按键”,是在回调层判断触摸起点是否命中某个控件,而不是为控件单开一条事件通道。
举个例子,一个音量滑块只接受水平拖动。起点落在滑轨范围内就锁定这个控件,之后所有 delta 都去改音量值;起点不落在范围内,则让同样的GESTURE_SWIPE_DELTA_X去驱动页面切换。处理代码如下:
static int slider_locked; void on_gesture_event(gesture_ctx_t *g, int event, int delta) { static int last_value; switch (event) { case GESTURE_SWIPE_START: if (touch_hit_slider(g->x0, g->y0)) { slider_locked = 1; last_value = volume_get(); } break; case GESTURE_SWIPE_DELTA_X: if (slider_locked) { volume_set(last_value + delta / 4); } break; case GESTURE_SWIPE_END: if (slider_locked) { slider_locked = 0; slider_commit(); } break; } }这里的关键在于,手势层已经通过g->dir锁定了方向,所以回调里不需要再比较横向位移和纵向位移谁大。很多误触来自回调层二次判断方向:第一次判断时上下抖动恰好把dir锁成纵向,第二次却用新的坐标去算,方向就不一致了。正确做法是,事件到达时直接用状态机给出的 delta,而不是把原始坐标再算一遍。
3.3 滑动触摸的参数默认值及调整方向
参数表对所有传感器平台都适用,只是单位要换成你实际使用的像素密度。默认值以 240×320 分辨率的 3.5 寸屏为准:
| 参数 | 建议值 | 调整说明 |
|---|---|---|
| SWIPE_DIST | 24 px | 小于该值当点击或噪声处理 |
| LONGPRESS_DEADZONE | 8 px | 应小于 SWIPE_DIST 的一半 |
| SHORT_CLICK_MS | 200 ms | 与系统按压识别时间一致 |
| LONGPRESS_MS | 600 ms | 老人机场景可提到 800 ms |
实际踩坑最多的是SWIPE_DIST和LONGPRESS_DEADZONE的比例。如果两者都写成 20 px,慢速拖动就会在“未滑动”与“已滑动”之间反复横跳。我的经验是,长按死区保持滑动阈值的 1/3,给手指抖动留出余量。
4. 触摸长按的仲裁窗口:让长按和滑动互不占用
触摸长按的实现难点从来不是用delay()等 600ms,而是如何保证在这一段时间内不被移动事件打断,又能在手指真正滑动时及时取消。
4.1 用定时器轮询取代对移动事件的依赖
很多触摸控制器的上报频率在 60~120Hz,读到的坐标会有帧间抖动。如果只依赖TOUCH_MOVED事件来检测位移,会碰到一种情况:手指完全没动,但驱动报告了几个像素的偏移。这时长按会被误判为滑动。更稳的做法是在应用层开一个固定周期任务,周期性检查当前位置与按下的距离:
#define LONGPRESS_CHECK_MS 10 void touch_loop_task(gesture_ctx_t *g) { touch_point_t ev; uint32_t last_check = 0; while (1) { if (panel_fetch_touch(&ev) == 0) { vTaskDelay(5); continue; } gesture_run(g, &ev); if ((ev.t_ms - last_check >= LONGPRESS_CHECK_MS) && (g->state == ST_HOLDING)) { int dx = (int)ev.x - g->x0; int dy = (int)ev.y - g->y0; if (dx * dx + dy * dy < LONGPRESS_DEADZONE2 && (ev.t_ms - g->press_t_ms) >= LONGPRESS_MS) { g->state = ST_LONGPRESS; notify_longpress_start(); } last_check = ev.t_ms; } vTaskDelay(5); } }gesture_run中本身已经包含了长按触发条件,这里再检查一次是为了覆盖那些“没有产生移动事件”的帧。比如手指按住不动时,驱动可能只在按下瞬间上报一次坐标,之后没有任何新事件,如果不靠定时器轮询,长按永远不会被确认。
提示:长按检测的节拍建议取目标长按时间的 1/20 到 1/10。长按 600ms,就用 10ms 检查一次;长按 200ms,检查周期要缩到 5ms 以内。
4.2 长按开始后的行为:单次触发与重复触发
状态机里ST_LONGPRESS分支返回两种事件:GESTURE_LONGPRESS_START和GESTURE_LONGPRESS_REPEAT。具体业务里,长按按钮需要重复触发(比如连续减音量),而长按菜单项只需要触发一次。可以在上下文里加一个标志位来控制:
#define LONGPRESS_MODE_ONCE 0 #define LONGPRESS_MODE_REPEAT 1 uint8_t longpress_mode; /* 回调里判断 */ case GESTURE_LONGPRESS_START: execute_longpress_action(); break; case GESTURE_LONGPRESS_REPEAT: if (longpress_mode == LONGPRESS_MODE_REPEAT) execute_longpress_action(); break;重复触发的间隔由LONGPRESS_REPEAT_MS控制,这个值不能设太短,否则长按 1 秒会触发几十次回调。常用间隔是 200ms,相当于每秒触发 5 次,对参数调整类操作已经是快速刷新频率。需要注意,ST_LONGPRESS分支虽然把press_t_ms累加了,但抬起事件仍然会正常返回GESTURE_LONGPRESS_END,不要在里面忘记复位状态。
4.3 慢速滑动的仲裁规则
状态机中加入了“慢速拖动”这种边界情况。用户以极慢速度上下滑动列表,位移最终超过了滑动阈值,但耗时也超过了长按时间。此时不应触发长按,因为手指的意图是滚动。仲裁表如下:
| 条件 | 归属事件 | 判定原则 |
|---|---|---|
| 位移 < 长按死区,时间 > 长按阈值 | 触摸长按 | 既没动又按得久 |
| 位移 > 滑动阈值,时间 < 长按阈值 | 滑动触摸 | 速度优先 |
| 位移 > 滑动阈值,时间 > 长按阈值 | 滑动触摸 | 位移一旦突破就放弃长按 |
| 位移在死区和滑动阈值之间 | 继续观望 | 等下一个检查周期 |
这个表格对应的实现逻辑就是状态机里ST_HOLDING分支的顺序:先检查长按时间,再检查滑动距离。如果位移已经超过SWIPE_DIST,即使时间也达到长按阈值,也要在代码上把它当作普通可开始滑动的移动事件,因为状态已经不在ST_HOLDING,长按条件不会再被满足。
5. 用串口打点和可视化调试滑动与长按阈值
最后落地时,先不要急着调节阈值。把触摸坐标、位移和时间戳通过串口输出,然后用脚本离线回放,是最快的校准方式。我习惯在触摸循环里加一行调试输出:
printf("%u,%u,%u,%d,%d,%d\r\n", ev.t_ms, ev.x, ev.y, (int)(ev.x - g->x0), (int)(ev.y - g->y0), g->state);六个字段分别是时间戳、当前坐标、相对起点横向位移、纵向位移、手势状态。用串口助手保存成 CSV 后,导入 Python 或 Excel,把 x 轴设为时间,y 轴设为位移值,能看到一条非常直观的曲线。
校准步骤按三步走:第一步,记录 10 次“手抖不动”的位移数据,取最大值作为噪声基线;第二步,将SWIPE_DIST设到噪声基线的 3 倍以上,例如基线是 6px,滑动阈值取 20px;第三步,长按死区取滑动阈值的 1/3,约 7px,这个值刚好在基线附近但低于滑动阈值。按下后,观察那根位移曲线是否越过了长按死区线,如果越过了但没达到滑动阈值,说明死区太紧,应该放宽 2~3px。
另一种定位问题是“长按和滑动互相抢占”。此时在日志里同时打印两个临界值:ev.t_ms - g->press_t_ms和dist2。触发长按前,位移曲线如果只是短暂越过死区又回落到 0,可以给长按状态增加一个 debounce 计数,也就是连续三次检查都满足位移条件,才真正切换到长按。对于 10ms 轮询周期来说,这相当于额外增加 20~30ms 的确认时间,对识别结果影响很小。
最后一个实用技巧是方向锁定时的“轴投影”。滑动按键回调里使用的 delta 不要直接用原始坐标差,而是只保留锁定轴的分量,另一轴丢弃。比如dir锁定为横向时,dx直接用于滑块更新,dy置零;否则纵向漂移会叠加到水平位置,滑块就会出现肉眼可见的回跳。在 3.3 节参数表的基础上,这套逻辑足以覆盖多数 CMAJ2 类工程包里的触摸交互需求。
本文还有配套的精品资源,点击获取