3、触摸屏驱动与校准:XPT2046/FT6336驱动原理、触摸坐标读取、校准算法实现、手势识别基础
触摸屏,说白了就是中控屏的「眼睛」和「耳朵」。用户点哪里、滑哪里,全靠它来感知。我在做第一版智能家居面板时,就吃过触摸不准的亏——点「开灯」结果关掉了空调,那叫一个尴尬。所以这一章,咱们把触摸屏的驱动和校准彻底讲透。
3.1 两种主流触摸屏:电阻屏 vs 电容屏
先搞清楚你手头用的是哪种。市面上智能家居中控屏,常见两种方案:
| 类型 | 代表芯片 | 原理 | 特点 |
|---|---|---|---|
| 电阻式 | XPT2046 | 压力感应,两层导电膜接触 | 便宜、耐脏、支持手套/手写笔 |
| 电容式 | FT6336 | 人体电容感应,多点触控 | 灵敏、支持手势、透光好 |
我个人习惯:如果做低成本、户外或工业场景,选XPT2046;如果是家用中控屏,必须上FT6336——用户体验差太多了。
3.2 XPT2046 驱动原理与坐标读取
XPT2046 是一个12位的ADC芯片,通过SPI接口通信。它怎么知道触摸位置?说白了就是测电压分压比。
触摸屏的上下两层导电膜,分别接X轴和Y轴。当手指按下时,两层接触,芯片在X轴加电压,从Y轴读电压,就能算出X坐标;反过来读Y坐标。
核心要点: XPT2046 每次只能读一个轴,所以需要分时读取 X 和 Y。
来看一段我实际项目里用过的驱动代码(ESP-IDF框架):
// XPT2046 SPI 读取函数
uint16_t xpt2046_read_xy(uint8_t cmd) {
uint8_t buf[3] = {0};
uint16_t result = 0;
// 片选拉低
gpio_set_level(TOUCH_CS_PIN, 0);
// 发送命令(例如 0xD0 读X,0x90 读Y)
spi_transaction_t trans = {
.length = 24,
.tx_buffer = buf,
.rx_buffer = buf,
};
buf[0] = cmd;
spi_device_transmit(spi_handle, &trans);
// 片选拉高
gpio_set_level(TOUCH_CS_PIN, 1);
// 解析12位数据
result = ((buf[1] & 0x3F) << 6) | (buf[2] >> 2);
return result;
}
我的经验: 读XPT2046时,第一次读取的数据往往不准,因为ADC需要稳定时间。我习惯连续读两次,取第二次的值。或者直接加一个1ms的延时。
3.3 FT6336 驱动原理与多点触控
FT6336 就高级多了。它内部自带MCU,直接通过I2C接口给你返回触摸点坐标。你只需要读它的寄存器就行。
它的寄存器映射表长这样:
| 寄存器地址 | 功能 | 说明 |
|---|---|---|
| 0x02 | 触摸点数 | 0=无触摸,1=单点,2=两点 |
| 0x03 | 触摸1 X高字节 | 与0x04组合成12位X坐标 |
| 0x05 | 触摸1 Y高字节 | 与0x06组合成12位Y坐标 |
| 0x09 | 触摸2 X高字节 | 同上,第二个触摸点 |
| 0x0B | 触摸2 Y高字节 | 同上 |
读取流程很简单:
- 先读0x02寄存器,看有几个触摸点
- 如果有触摸,依次读对应点的X、Y坐标
- 坐标是12位的,需要把高字节和低字节拼起来
// FT6336 读取触摸点
typedef struct {
uint16_t x;
uint16_t y;
uint8_t id;
} touch_point_t;
uint8_t ft6336_read_points(touch_point_t *points) {
uint8_t num = i2c_read_byte(FT6336_ADDR, 0x02);
if (num == 0 || num > 2) return 0;
for (int i = 0; i < num; i++) {
uint8_t reg_base = (i == 0) ? 0x03 : 0x09;
uint8_t buf[4];
i2c_read_bytes(FT6336_ADDR, reg_base, buf, 4);
points[i].x = ((buf[0] & 0x0F) << 8) | buf[1];
points[i].y = ((buf[2] & 0x0F) << 8) | buf[3];
points[i].id = i;
}
return num;
}
注意: FT6336 的坐标原点在芯片左下角,而你的屏幕显示原点可能在左上角。记得做Y轴翻转:
y = SCREEN_HEIGHT - y。
3.4 触摸校准算法实现
为什么需要校准?因为触摸屏的物理坐标和屏幕的显示坐标不是一一对应的。可能有旋转、缩放、偏移。我曾经遇到一个项目,触摸屏贴歪了2毫米,结果边缘按钮死活点不准。
最常用的校准算法是「三点校准法」。原理很简单:
- 在屏幕三个角显示校准点
- 用户点击后,记录触摸坐标
- 通过解线性方程组,算出转换矩阵
转换公式:
X_screen = A * X_touch + B * Y_touch + C
Y_screen = D * X_touch + E * Y_touch + F
三个点,六个未知数,刚好解出来。代码实现:
// 三点校准参数
typedef struct {
float A, B, C;
float D, E, F;
} calib_params_t;
calib_params_t calib;
void calib_compute(touch_point_t *touch_pts, screen_point_t *screen_pts) {
// 解线性方程组,这里用克莱姆法则
// 具体推导略,网上很多现成代码
// 我习惯用浮点数计算,精度够用
float div = ...; // 分母
calib.A = ...;
calib.B = ...;
calib.C = ...;
calib.D = ...;
calib.E = ...;
calib.F = ...;
}
// 校准转换函数
void calib_transform(uint16_t tx, uint16_t ty, uint16_t *sx, uint16_t *sy) {
*sx = (uint16_t)(calib.A * tx + calib.B * ty + calib.C);
*sy = (uint16_t)(calib.D * tx + calib.E * ty + calib.F);
}
避坑指南: 我曾经把校准参数直接存到代码里,结果换了一块屏就全废了。正确的做法是:第一次上电时进入校准模式,把参数保存到NVS(非易失存储)中。以后每次启动直接读取。
3.5 手势识别基础
有了触摸坐标,手势识别就是水到渠成的事。手势的本质,就是触摸点的运动轨迹。
我把它分为三个层次:
- 单点手势: 点击、长按、滑动
- 多点手势: 双指缩放、旋转
- 复杂手势: 画圈、字母手势
最简单的滑动识别:
// 滑动方向判断
typedef enum {
SWIPE_NONE,
SWIPE_UP,
SWIPE_DOWN,
SWIPE_LEFT,
SWIPE_RIGHT
} swipe_dir_t;
swipe_dir_t detect_swipe(touch_point_t start, touch_point_t end) {
int16_t dx = end.x - start.x;
int16_t dy = end.y - start.y;
// 滑动距离太短,忽略
if (abs(dx) < 20 && abs(dy) < 20) return SWIPE_NONE;
// 判断主方向
if (abs(dx) > abs(dy)) {
return (dx > 0) ? SWIPE_RIGHT : SWIPE_LEFT;
} else {
return (dy > 0) ? SWIPE_DOWN : SWIPE_UP;
}
}
双指缩放更简单:记录两个手指的初始距离和当前距离,比值就是缩放因子。
我的建议: 手势识别不要做得太复杂。中控屏不是手机,用户不会在上面画字母。做好「点击」「滑动」「长按」三个手势,就覆盖了90%的交互场景。
3.6 实战中的坑与经验
最后分享几个我踩过的坑:
- 触摸抖动: 手指按下瞬间,坐标会跳变。加一个「去抖」——连续读三次,取中间值。
- 边缘不灵敏: 电容屏的边缘灵敏度天生差。可以在驱动层做「边缘补偿」,把边缘区域的坐标映射到更靠内的位置。
- 休眠唤醒: FT6336 有休眠模式,唤醒后需要重新初始化。我遇到过唤醒后读不到触摸,后来发现是I2C时钟没恢复。
- 校准参数丢失: 如果NVS被擦除,校准参数就没了。我加了一个「校准标志位」,如果标志位不对,强制进入校准模式。
嗯,这一章内容不少。触摸驱动看似简单,但真正做好、做稳,需要花不少心思。下一章我们开始讲GUI框架的选择与移植,到时候你会看到触摸和显示是怎么配合的。
公众号:蓝海资料掘金营,微信deep3321