触摸控制
触摸功能与屏幕显示类型相互独立:无论 LCD、AMOLED 还是墨水屏,均可叠加触摸层。触摸芯片通常通过 I2C 与主控通信,驱动方式在不同屏幕上是通用的,本章将触摸作为独立主题介绍。
1. 触摸屏原理
按感应方式,触摸屏分为两类:
- 电阻式:触摸层由两片导电薄膜构成,按压使两层接触,通过测量接触点的分压计算坐标。任何物体按压均可触发(可戴手套、用笔尖),成本低;但需要一定按压力度、只支持单点、透光率略差,且薄膜坐标与屏幕坐标存在偏差,需要校准。
- 电容式:表面为钢化玻璃,内层布有电极阵列。手指靠近时改变局部电容,触摸 IC 扫描电极阵列计算出坐标。轻触即可、支持多点触控、透光率好,是目前的主流方案;缺点是依赖导电物体,戴普通手套无效。
微雪带触摸的开发板基本采用电容方案(型号带 "Touch" 字样),坐标由触摸 IC 计算完成,主控直接读取即可,无需校准。
2. 常见触摸芯片
电容触摸 IC 独立于显示驱动 IC,通常通过 I2C 与主控通信,并配有 INT(触摸中断输出)和 RST(复位)两根辅助线。常见型号:
| 触摸芯片 | 触点数 | 常见场景 |
|---|---|---|
| CST816 系列 | 单点 + 手势 | 1.3~2 英寸小屏(手表类) |
| CST9220 | 5 点 | ≤2.2 英寸小屏(手表 / 手环) |
| FT3168 / FT6236 | 2 点 | 1.8~3.5 英寸 |
| GT911 | 5 点 | 3.5~7 英寸大屏 |
3. 读取方式:轮询与中断
无论用哪种触摸 IC,主控读取触摸的方式都是两种:
- 轮询:在主循环中周期性读取触摸寄存器。实现简单,但无触摸时仍会产生无效的 I2C 通信。
- 中断:触摸发生时 INT 引脚产生信号,主控在中断服务函数中置标志位,主循环检测到标志后再读坐标。响应及时且省总线带宽,也是微雪示例常用的方式。
下一节的示例采用中断方式。
4. Arduino + SensorLib 触摸示例
本节以 微雪 ESP32-S3-Touch-AMOLED-2.16 为例,其触摸 IC 为 CST9220(芯片最多支持 5 触点,详见 产品页)。触摸读取使用开源库 SensorLib,其 CST92xx 驱动当前最多上报 2 个触点,因此本节示例按 2 点处理;屏幕绘制沿用 AMOLED 章节 的 Arduino_GFX,以便在触点处画方块、直观展示触摸效果。
4.1 准备工作
- 安装 Arduino IDE 并添加 ESP32 支持(参考 Arduino IDE 开发环境搭建教程)。
- 在库管理器(工具 > 管理库)中搜索并安装
SensorLib和GFX Library for Arduino:前者读取触摸坐标,后者驱动屏幕绘制。本节代码使用 SensorLib v0.4.1 引入的TouchDrv.hpp,请确认版本不低于 v0.4.1。其他安装方式参考 Arduino 库管理教程。 - 在开发板选择器中搜索并选择
Waveshare ESP32-S3-Touch-AMOLED-2.16(搜不到时将esp32 by Espressif Systems更新至 v3.3.10 及以上)。 - 将 工具 > USB CDC On Boot 设为 Enabled。ESP32-S3 通过原生 USB 输出串口,未开启时串口监视器收不到
Serial内容。
4.2 确认引脚
ESP32-S3-Touch-AMOLED-2.16 的触摸 IC 与屏幕均已直连主控,触摸相关引脚如下(屏幕 QSPI 引脚见 AMOLED 章节):
| 信号 | GPIO |
|---|---|
| SDA | GPIO15 |
| SCL | GPIO14 |
| TP_RST | GPIO40 |
| TP_INT | GPIO11 |
4.3 示例代码
下面的示例在中断中读取触摸坐标,一边通过串口打印,一边在触点处画一个方块光标:
#include <Wire.h>
#include <TouchDrv.hpp>
#include <Arduino_GFX_Library.h>
// 触摸引脚(CST9220)
#define IIC_SDA 15
#define IIC_SCL 14
#define TP_RST 40
#define TP_INT 11
// 屏幕引脚(CO5300,QSPI,详见 AMOLED 章节)
#define LCD_SDIO0 4
#define LCD_SDIO1 5
#define LCD_SDIO2 6
#define LCD_SDIO3 7
#define LCD_SCLK 38
#define LCD_CS 12
#define LCD_RESET 39
#define LCD_WIDTH 480
#define LCD_HEIGHT 480
TouchDrvCST92xx touch;
int16_t x[2], y[2]; // 数组长度需 ≥ 实际读取的最大触点数(SensorLib 的 CST92xx 驱动最多上报 2 点)
volatile bool touchPending = false;
Arduino_DataBus *bus = new Arduino_ESP32QSPI(
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
Arduino_CO5300 *gfx = new Arduino_CO5300(
bus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
// 光标边长(CO5300 要求绘制窗口偶数对齐,故取偶数)
#define CURSOR 20
// 触摸中断:只置标志,读坐标留给主循环(I2C 读取耗时,不宜在中断中执行)
void IRAM_ATTR onTouchInterrupt() {
touchPending = true;
}
void setup() {
Serial.begin(115200);
// 1. 初始化屏幕:清黑、拉满亮度,方向沿用 AMOLED 章节的初始化参数
gfx->begin();
bus->writeC8D8(0x36, 0xA0);
gfx->fillScreen(RGB565_BLACK);
gfx->setBrightness(255);
// 2. 指定复位、中断引脚并初始化触摸 IC
touch.setPins(TP_RST, TP_INT);
if (!touch.begin(Wire, CST92XX_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) {
Serial.println("Touch initialization failed");
while (true) {
delay(1000);
}
}
// 3. 坐标变换:分辨率与交换/镜像参数需与屏幕显示方向一致
touch.setMaxCoordinates(LCD_WIDTH, LCD_HEIGHT);
touch.setSwapXY(true);
touch.setMirrorXY(true, false);
// 4. 配置中断引脚,下降沿触发
pinMode(TP_INT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(TP_INT), onTouchInterrupt, FALLING);
}
void loop() {
// 5. 无触摸事件时直接返回
if (!touchPending) {
return;
}
noInterrupts();
touchPending = false;
interrupts();
// 6. 读取所有触点:串口打印坐标,并在触点处画一个方块光标
uint8_t touched = touch.getPoint(x, y, touch.getSupportTouchPoint());
for (uint8_t i = 0; i < touched; ++i) {
Serial.printf("Touch %u: x=%d, y=%d\n", i, x[i], y[i]);
// 起点与边长均取偶数,满足 CO5300 绘制窗口的偶数对齐要求
int16_t px = (x[i] - CURSOR / 2) & ~1;
int16_t py = (y[i] - CURSOR / 2) & ~1;
gfx->fillRect(px, py, CURSOR, CURSOR, RGB565_CYAN);
}
}
烧录后打开串口监视器,触摸屏幕即可看到坐标输出,同时触点处会出现一个青色方块。几个关键点:
- 中断里只置标志位:INT 触发后仅设置
touchPending,真正的 I2C 读取放到主循环。I2C 读取相对耗时,不应在中断上下文中执行。 - 画方块即对齐自检:方块应正好落在手指下方。若方块相对手指镜像或偏移,说明触摸坐标变换与显示方向不一致,按坐标与显示的对齐调整
setSwapXY/setMirrorXY。 - 绘制窗口按偶数对齐:CO5300 只可靠支持偶数对齐的矩形整块写入,所以这里用
fillRect画方块(起点和边长取偶数)而非fillCircle。这条约束的来龙去脉见 AMOLED 章节。 - 数组长度要够:
x、y的长度需不小于驱动上报的最大触点数(SensorLib 的 CST92xx 驱动为 2)。
使用其他开发板时,按其触摸 IC 选择对应的 SensorLib 驱动类,并按产品页修改引脚、I2C 地址和坐标变换参数;外接 I2C 设备时注意避开板载设备的地址。ESP32-S3-Touch-AMOLED-2.16 的库文件和完整示例见 资源与文档。
4.4 常见问题
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 串口监视器无输出 | 未开启 USB CDC On Boot | 工具 > USB CDC On Boot 设为 Enabled 后重新烧录 |
| 初始化失败(串口打印 failed) | I2C 地址或引脚不符 | 核对 SDA/SCL/RST/INT 引脚与 I2C 地址是否与产品页一致 |
| 无坐标输出、屏幕也无方块 | INT 引脚未接或触发沿不对;库版本过低 | 确认 TP_INT 接线正确、触发沿为 FALLING,并检查 SensorLib 版本 |
| 方块位置与手指镜像或偏移 | 坐标变换与显示方向不一致 | 调整 setSwapXY / setMirrorXY(见坐标与显示的对齐) |
| 触点较多时数组越界 | x、y 数组长度不足 | 按驱动上报的最大触点数增大数组 |
5. 坐标与显示的对齐
触摸 IC 输出的坐标基于面板的原生方向。当显示进行了旋转(如将竖屏内容横向显示),触摸坐标必须做同样的变换,否则触摸位置会与显示响应错位。SensorLib 用三个接口完成这一变换,对应上一节示例中的调用:
setMaxCoordinates(w, h):告知面板原生分辨率;setSwapXY(true):交换 X、Y 轴,用于 90° / 270° 旋转;setMirrorXY(mirrorX, mirrorY):按需翻转某一轴,处理镜像与 180° 旋转。
不同面板的原点方向和轴向不尽相同,参数需与实际显示方向匹配。可靠的调试做法:先设好 setMaxCoordinates、其余变换暂不开启,依次触摸屏幕四个角并打印坐标,确认原点和轴向后再决定是否交换或镜像。
电阻屏则还需校准(采集已知点的原始 ADC 值反算映射系数),电容屏无此步骤,本教程不展开。
6. 对接 GUI 框架
GUI 框架将触摸抽象为输入设备(Input Device),只需提供一个上报坐标和按下状态的回调函数。以 LVGL v8 为例:
void my_touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
int32_t x, y;
if (读取到触摸(&x, &y)) { // 即前面示例的读坐标逻辑
data->state = LV_INDEV_STATE_PR; // 按下
data->point.x = x;
data->point.y = y;
} else {
data->state = LV_INDEV_STATE_REL; // 释放
}
}
注册该回调后,LVGL 会周期性调用它,控件的点击、拖动、滑动手势都由框架自动处理。图形库的选型见 GUI 框架章节,完整的注册流程见 Arduino 教程第 12 节 LVGL 图形界面开发。