NodeMCU 固件 XPT2046 触摸屏模块使用指南:为 ILI9341 等廉价显示屏接入触摸输入
  • 物联网
  • 嵌入式

NodeMCU 固件为 ESP8266/ESP8285 提供了内置的xpt2046Lua 模块,用于驱动 XPT2046 电阻式触摸控制器——该控制器常见于多种廉价 TFT 显示屏上,通常与 ILI9341 显示控制器搭配使用。本文以 docs/modules/xpt2046.md 为骨架,结合 app/modules/xpt2046.c 源码,完整讲解从 SPI 初始化、模块启用、触摸校准到坐标读取与中断回调的整套实战流程,读完即可在你的 NodeMCU 项目中实现"屏幕显示 + 触摸交互"。

一、模块概览与适用场景

XPT2046 是一款 4 线制电阻式触摸屏控制器,通过 SPI 接口与主控通信,输出 12 位精度的原始坐标。在 NodeMCU 固件中,xpt2046模块将其封装为一组简洁的 Lua API,让你不必关心底层时序即可完成:

  • 触摸检测(isTouched);
  • 屏幕像素坐标换算(getPosition,基于校准值与屏参);
  • 抗抖动坐标采样(getPositionAvg);
  • 原始 ADC 值读取(getRaw,用于调试或自定义换算)。

该模块基于 spapadim/XPT2046 与 PaulStoffregen/XPT2046_Touchscreen 两个开源库移植而来(见 app/modules/xpt2046.c 的模块头注释),底层依赖 NodeMCU 的spi模块与platform_gpio接口。

启用模块

xpt2046模块默认并未编译进固件。在 app/include/user_modules.h 中可以看到其开关项默认处于注释状态:

//#define LUA_USE_MODULES_XPT2046

如需使用,请在重新编译固件前取消该行注释,使其变为:

#define LUA_USE_MODULES_XPT2046

启用后,固件编译时会将 app/modules/xpt2046.c 中的NODEMCU_MODULE(XPT2046, "xpt2046", xpt2046, NULL)注册进 Lua 运行时,Lua 侧即可直接调用xpt2046命名空间下的各函数。

二、SPI 初始化(前置条件)

所有触摸读取都通过 SPI 完成,因此必须在调用xpt2046.init()之前先调用spi.setup()。官方文档给出的标准配置如下:

spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, 8, 16, spi.FULLDUPLEX)

各参数含义(对应 app/modules/spi.c 的参数解析):

参数值说明
id1SPI 总线编号(此处为 HSPI)
modespi.MASTER主机模式,触摸屏作为从设备
cpolspi.CPOL_LOW时钟极性为低
cphaspi.CPHA_LOW时钟相位为低
databits8每次传输 8 位
clock_div16时钟分频,应不小于 16
duplex_modespi.FULLDUPLEX全双工模式(app/modules/spi.c 中SPI_FULLDUPLEX定义为1)

这里有两点必须特别注意:

  1. 全双工模式是必需的。源码 app/modules/xpt2046.c 中的transfer16()通过platform_spi_send_recv同时收发数据来产生 16 个时钟周期以读取 12 位结果,这要求 SPI 处于全双工状态。而spi.setup()的第 7 个参数duplex_mode在 app/modules/spi.c 中默认是半双工(SPI_HALFDUPLEX),所以必须显式传入spi.FULLDUPLEX。

  2. clock_div要取 16 或更高。文档明确说明"较低的分频值可能产生不准确的读数",源码 app/modules/xpt2046.c 也以注释形式确认了这一点:更高的 SPI 时钟速度会导致采样结果失真。电阻式触摸屏的 ADC 采样需要足够的转换时间,分频太低意味着单个转换周期过短。

与 ucg/u8g2 显示模块共用 SPI 的注意事项

由于ucg(以及需要 SPI 的显示方案)也要求先调用spi.setup(),而 SPI 总线的初始化参数只能设置一次,因此必须保证整个程序生命周期内spi.setup()只被调用一次,且使用spi.FULLDUPLEX。推荐顺序是:先做一次spi.setup(),再初始化显示屏(如ucg),最后初始化触摸屏。

三、xpt2046.init() —— 初始化触摸模块

在完成 SPI 配置后,调用init()传入触摸屏的片选(CS)、中断(IRQ)引脚以及屏幕的物理尺寸。

语法
xpt2046.init(cs_pin, irq_pin, height, width)
参数
参数类型说明
cs_pin整数片选信号所接的 GPIO 引脚
irq_pin整数触摸中断(PENIRQ)所接的 GPIO 引脚
height整数显示屏高度(像素)
width整数显示屏宽度(像素)
返回值

nil

源码行为

从 app/modules/xpt2046.c 可以看到init()内部完成的工作:

  1. 保存_cs_pin、_irq_pin、_height、_width到模块全局变量;
  2. 将cs_pin配置为输出模式(PLATFORM_GPIO_OUTPUT);
  3. 依据传入的宽高自动生成一份默认校准值(见下一节);
  4. 拉低 CS 做一次带断电(PD)标记的丢弃式读取,确保 ADC 进入可用状态。
示例(配合 ucg 显示 ILI9341)
-- Setup spi with `clock_div` of 16 and spi.FULLDUPLEX spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, 8, 16, spi.FULLDUPLEX) -- SETTING UP DISPLAY (using ucg module) local disp = ucg.ili9341_18x240x320_hw_spi(8, 4, 0) disp:begin(0) -- SETTING UP TOUCH xpt2046.init(2, 1, 320, 240) xpt2046.setCalibration(198, 1776, 1762, 273)

上例中ucg.ili9341_18x240x320_hw_spi(cs, dc, res)的用法与 docs/modules/ucg.md 中描述的 ILI9341 硬件 SPI 接线一致(CS、D/C 可任意选择 GPIO,RES 可选)。注意xpt2046.init的第三个、第四个参数是"高度、宽度",示例传入了(320, 240)——这里宽高顺序与getPosition返回的 x/y 语义对应,若屏幕方向与触摸方向不一致,可通过setCalibration修正,而无需颠倒传参。

四、xpt2046.setCalibration() —— 触摸校准

电阻屏的原始 ADC 值范围与屏幕像素范围并不天然对应,且不同屏幕个体存在差异,因此需要校准。setCalibration()用于设置屏幕两个对角(左上与右下)对应的原始触摸值。

语法
xpt2046.setCalibration(x1, y1, x2, y2)
参数
参数说明
x1屏幕左上角的原始 x 值
y1屏幕左上角的原始 y 值
x2屏幕右下角的原始 x 值
y2屏幕右下角的原始 y 值
返回值

nil

获取校准值的方法

文档明确给出了校准值的获取途径:使用xpt2046.getRaw()分别读取左上角与右下角按压点的原始值。典型流程是:

  1. 临时使用init()自动生成的默认校准值(或直接打印getRaw()结果);
  2. 用笔尖点按屏幕左上角,记录getRaw()返回的(x, y)作为(x1, y1);
  3. 点按屏幕右下角,记录(x2, y2);
  4. 将四值填入setCalibration()。
源码中的坐标换算原理

在 app/modules/xpt2046.c 中,校准参数被转换成线性映射系数:

_cal_dx = _width - 2*CAL_MARGIN; _cal_dy = _height - 2*CAL_MARGIN; _cal_vi1 = vi1; _cal_vj1 = vj1; _cal_dvi = vi2 - vi1; _cal_dvj = vj2 - vj1;

随后getPosition()(app/modules/xpt2046.c)使用这些系数把原始值线性映射到像素坐标:

*x = _cal_dx * (vj - _cal_vj1) / _cal_dvj + CAL_MARGIN; *y = _cal_dy * (vi - _cal_vi1) / _cal_dvi + CAL_MARGIN;

注意CAL_MARGIN在源码 app/modules/xpt2046.c 中定义为0,即不做边缘裁剪,由应用层自行决定。而init()在未手动校准时,会依据屏宽高把满量程(12 位 ADC,最大值ADC_MAX = 0x0fff,见 app/modules/xpt2046.c)按比例分配为默认校准值,因此"只调用init()不调用setCalibration()"也能得到一个线性但可能偏斜的映射,正式使用前仍建议实测校准。

五、xpt2046.isTouched() —— 触摸检测

语法
xpt2046.isTouched()
返回值

true(面板被触摸)或false(未触摸)。

实现原理

该函数直接读取 IRQ 引脚电平(app/modules/xpt2046.c):

static int isTouching() { return (platform_gpio_read(_irq_pin) == 0); }

XPT2046 的 PENIRQ 引脚在有触摸时被拉低,因此读回0即代表"正在触摸"。该函数是轮询式检测的基础,也是中断回调方案的先决条件。

示例
if (xpt2046.isTouched()) then local x, y = xpt2046.getPosition() print(x .. "-" .. y) end

六、xpt2046.getPosition() —— 获取校准后的像素坐标

getPosition()返回经校准值换算、并按照init()传入的宽高映射到屏幕的坐标,是日常交互中最常用的函数。文档特别指出,它可以直接用在 IRQ 引脚的中断回调里,从而在触摸发生时立刻拿到坐标,无需轮询。

语法
xpt2046.getPosition()
返回值

返回两个值:x与y(屏幕像素坐标)。

触摸中断回调完整示例

以下示例完整展示了 GPIO 中断 + 触摸坐标读取的组合(文档原示例):

-- Setup spi with `clock_div` of 16 and spi.FULLDUPLEX spi.setup(1, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, 8, 16, spi.FULLDUPLEX) -- SETTING UP TOUCH cs_pin = 2 -- GPIO4 irq_pin = 3 -- GPIO0 height = 240 width = 320 xpt2046.init(cs_pin, irq_pin, width, height) xpt2046.setCalibration(198, 1776, 1762, 273) gpio.mode(irq_pin, gpio.INT, gpio.PULLUP) gpio.trig(irq_pin, "down", function() print(xpt2046.getPosition()) end)

要点解读:

  • gpio.mode(irq_pin, gpio.INT, gpio.PULLUP)将 IRQ 引脚设为带内部上拉的中断输入;XPT2046 的 PENIRQ 是开漏/低有效输出,上拉是必需的。
  • gpio.trig(irq_pin, "down", ...)注册下降沿(触摸按下)回调,回调内调用getPosition()。
  • 文档示例中xpt2046.init(cs_pin, irq_pin, width, height)与第一节示例的宽高传参顺序不同(此处为(width, height)即(320, 240)),实际效果取决于你的屏幕方向约定;建议固定一种写法并配合校准值确认 x/y 映射正确。
中断期间的底层细节

源码 app/modules/xpt2046.c 的getRaw()展示了中断方案的一个关键实现:读取坐标前会先关闭 GPIO 中断(ETS_GPIO_INTR_DISABLE()),读取完成后写清除中断状态位并重新使能中断(ETS_GPIO_INTR_ENABLE()),因为 SPI 读取时序本身会产生虚假的 PENIRQ 边沿。这意味着你无需在 Lua 回调里手动屏蔽中断,底层已经处理。

此外,读取结束时会发送带断电标记(CTRL_LO_SER,即 PD=0b11 之外的序列)的控制字节来关闭 ADC——这是因为差分模式(CTRL_LO_DFR)要求 PD=0b11 才能启用 PENIRQ,若不主动关闭 ADC,后续的中断信号将失效(见 app/modules/xpt2046.c 注释)。

七、xpt2046.getPositionAvg() —— 三次采样取优平均

单次触摸读数易受噪声影响。getPositionAvg()会连续读取三次位置,然后取距离最近的两组求平均,以获得更稳定的坐标。

语法
xpt2046.getPositionAvg()
返回值

返回两个值:x与y(经平均处理的像素坐标)。

示例
local x, y = xpt2046.getPositionAvg() print(x .. "-" .. y)
实现原理

源码 app/modules/xpt2046.c 中,该函数连续调用三次getPosition(),再把三组坐标交给besttwoavg()(app/modules/xpt2046.c)处理:分别计算三组两两间的距离,选择距离最小的一对求算术平均。这样能有效剔除单次明显偏离的异常采样。

使用建议:在需要精确坐标的场景(如手写、绘图、小按钮点击)优先使用getPositionAvg();在要求低延迟的快速滑动场景,getPosition()更合适。

八、xpt2046.getRaw() —— 读取原始 ADC 值

getRaw()跳过校准换算,直接返回触摸点对应的 12 位原始值,用于调试、校准取值以及自定义坐标变换。

语法
xpt2046.getRaw()
返回值

返回两个值:rawX与rawY(原始触摸值,0~4095 范围)。

示例
local rawX, rawY = xpt2046.getRaw() print(rawX .. "-" .. rawY)
数据读取时序

从源码看,getRaw()(app/modules/xpt2046.c)的执行基于 TI 应用笔记 SBAA036 的推荐时序:

  1. 拉低 CS,发起第一个控制字节(CTRL_HI_X | CTRL_LO_DFR)开始 X 通道转换;
  2. _readLoop()(app/modules/xpt2046.c)连续发送 16 个时钟周期的控制字,直到前后两次读数一致或达到最大采样次数(255 次),返回稳定的 12 位结果;
  3. 以相同方式读取 Y 通道;
  4. 发送丢弃字节关闭 ADC 并拉高 CS,恢复 PENIRQ。

因此getRaw()已经内置了"读到稳定值"的循环,返回结果可用于校准(如第四节所述),也可以完全自定义换算逻辑。

九、硬件接线与实战建议

结合文档示例与 docs/modules/ucg.md 中 ILI9341 的 SPI 接线说明,推荐接线如下:

信号推荐 GPIO说明
SPI CLKGPIO14HSPI 时钟
SPI MOSIGPIO13主出从入(发送控制字节)
SPI MISOGPIO12主入从出(读回数据;全双工模式下必须连接)
屏 CS任选(示例用 GPIO15/GPIO8)显示控制器片选
屏 D/C任选(示例用 GPIO4)数据/命令选择
屏 RES任选可省略
触摸 CS任选(示例用 GPIO4)XPT2046 片选
触摸 IRQ任选(示例用 GPIO0)PENIRQ,需上拉,接中断输入

最后再强调三条容易踩坑的规则:

  1. spi.setup()全局只需调用一次,且必须带spi.FULLDUPLEX、clock_div >= 16;
  2. xpt2046.init()必须在spi.setup()之后调用;
  3. 正式项目务必用getRaw()实测屏幕左上、右下两角完成setCalibration(),不要依赖init()的默认校准值。

十、API 速查表

函数语法返回值
xpt2046.initxpt2046.init(cs_pin, irq_pin, height, width)nil
xpt2046.setCalibrationxpt2046.setCalibration(x1, y1, x2, y2)nil
xpt2046.isTouchedxpt2046.isTouched()true/false
xpt2046.getPositionxpt2046.getPosition()x, y
xpt2046.getPositionAvgxpt2046.getPositionAvg()x, y
xpt2046.getRawxpt2046.getRaw()rawX, rawY

全部 6 个函数均在 app/modules/xpt2046.c 的LROT_BEGIN(xpt2046, ...)模块函数表中注册,与文档列出的 API 完全一致。完整官方说明可继续查阅 docs/modules/xpt2046.md,SPI 配置细节见 docs/modules/spi.md。

赞分享
  • 物联网
  • 嵌入式
上一篇:使用colesbury/nogil项目中的urllib模块获取网络资源
下一篇:5步实现Qwen模型ONNX格式转换:终极跨平台部署指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考