1源码轮询版
cpp
/**
* ============================================================================
* 工程:ESP32-S3 + ST7789 LCD (SPI2) + XPT2046 电阻触摸 (SPI3)
* 版本:轮询版 v1.1(不依赖中断)
* 环境:ESP-IDF v5.2
*
* 功能:
* 1. ST7789 开机显示黑底 + 红块
* 2. 触摸任务每 20ms 轮询 XPT2046,用 Z1 压力值判断按下(T_IRQ 仅辅助)
* 3. 按下时 5 次采样 -> 中位值滤波 -> 校准映射 -> 画白色方块(手写板)
*
* 按下检测策略(双保险):
* - 主判断:Z1 压力 ADC 值 >= 阈值 (不依赖 IRQ 接线)
* - 辅判断:T_IRQ 引脚为低电平 (接了就生效,没接不影响)
*
* 架构分层(自顶向下):
* app_main
* ├── lcd_init() LCD 初始化(SPI 总线 + panel_io + st7789)
* └── xpt2046_init() 触摸初始化(SPI 总线 + 设备 + IRQ 脚)
* └── touch_task() 轮询循环
* ├── xpt2046_read() 底层 SPI 读 ADC
* ├── median5() 滤波
* ├── touch_map() 坐标映射
* └── lcd_fill() LCD 画图
* ============================================================================
*/
/* ############################################################################
* # 移植配置区(换板子只改这里) #
* ############################################################################
*
* 【芯片引脚注意】
* - ESP32-S3 :下方默认引脚全部可用
* - ESP32-C3 :GPIO11~13 被 Flash 占用,触摸线必须换脚(如 2/3/4/5)
* - ESP32 :GPIO6~11 被 Flash 占用,同样要避开
*/
/* ---------------- LCD(ST7789 @ SPI2) ---------------- */
#define LCD_HOST SPI2_HOST /* LCD 独占一条 SPI 总线 */
#define PIN_NUM_SCLK 6 /* SPI 时钟 */
#define PIN_NUM_MOSI 7 /* 主出 -> 屏幕SDA */
#define PIN_NUM_MISO (-1) /* 屏幕无 MISO,-1 禁用 */
#define PIN_NUM_CS 5 /* 片选 */
#define PIN_NUM_DC 4 /* 0=命令 1=数据 */
#define PIN_NUM_RST 8 /* 复位(模块无则改 -1) */
#define PIN_NUM_BK 9 /* 背光,高电平亮 */
#define LCD_WIDTH 240 /* 逻辑宽(竖屏) */
#define LCD_HEIGHT 320 /* 逻辑高(竖屏) */
#define LCD_CMD_BITS 8 /* ★ 必填!为 0 时 esp_lcd
init 报 INVALID_ARG */
#define LCD_PARAM_BITS 8
#define LCD_PCLK_HZ (40 * 1000 * 1000) /* 花屏降 26M/20M */
#define FILL_ROWS 8 /* 每次填充行数:越大越快越占RAM */
/* ---------------- 触摸(XPT2046 @ SPI3) ---------------- */
#define TOUCH_HOST SPI3_HOST /* 触摸独立总线 */
#define T_PIN_CLK 13
#define T_PIN_SDI 12 /* 模块T_SDI = MOSI(进芯片) */
#define T_PIN_SDO 11 /* 模块T_SDO = MISO(出芯片) */
#define T_PIN_CS 10
#define T_PIN_IRQ 14 /* 按下为低;没接也能工作 */
#define TOUCH_CLK_HZ (1 * 1000 * 1000) /* XPT2046 上限 2.5M,1M 稳 */
#define TOUCH_POLL_MS 20 /* 空闲轮询周期 */
#define TOUCH_DRAW_MS 30 /* 按住时的画点周期 */
#define TOUCH_SAMPLES 5 /* 每次采样次数(奇数,取中位) */
/* ---------------- 触摸校准 ----------------
* 校准三步法:
* 1. 开 TOUCH_DEBUG_RAW=1,按屏幕四角记录 RAW 极值,填入下面 4 个宏
* 2. 方向不对调 SWAP / INVERT 开关(每次只改一个)
* 3. 方向对但整体偏 OFFSET 像素:画点偏上填正数,偏下填负数
*/
#define RAW_X_MIN 300 /* X 最小 RAW(屏幕最左/最右) */
#define RAW_X_MAX 3800
#define RAW_Y_MIN 300 /* Y 最小 RAW(屏幕最顶/最底) */
#define RAW_Y_MAX 3800
#define TOUCH_SWAP_XY 1 /* 1 = X/Y 轴互换 */
#define TOUCH_INVERT_X 0 /* 1 = X 方向镜像 */
#define TOUCH_INVERT_Y 0 /* 1 = Y 方向镜像 */
#define TOUCH_VALID_MIN 100 /* RAW 低于此 = 无效读数 */
#define TOUCH_Z_THRESHOLD 20 /* Z1 压力 < 此值 = 未按下 */
/* ---------------- 像素偏移补偿 ---------------- */
#define TOUCH_X_OFFSET 0 /* 画点偏左填 +N */
#define TOUCH_Y_OFFSET 20 /* 画点偏上填 +N */
/* ---------------- 调试开关 ----------------
* 1 = 无视按压状态,每 200ms 打印 X/Y/Z1/IRQ 原始值:
* - 数值随手指变化 => 硬件正常,改回 0
* - 恒 0 或恒 4095 => T_SDI/T_SDO 接反,互换 GPIO11/12
* - Z1 按压不变 => 检查 T_CLK / T_CS 接线
*/
#define TOUCH_DEBUG_RAW 0
/* ---------------- 颜色(RGB565) ---------------- */
#define COLOR_RED 0xF800
#define COLOR_GREEN 0x07E0
#define COLOR_BLUE 0x001F
#define COLOR_WHITE 0xFFFF
#define COLOR_BLACK 0x0000
/* ############################################################################
* # 以下为实现部分,一般不用改 #
* ############################################################################ */
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
/* XPT2046 控制字节 [S][A2A1A0][MODE][SER/DFR][PD1PD0]:
* 差分模式、12 位分辨率、供电常开 */
#define XPT2046_CMD_X 0x90 /* 读 X 位置 */
#define XPT2046_CMD_Y 0xD0 /* 读 Y 位置 */
#define XPT2046_CMD_Z1 0xB0 /* 读 Z1 压力(按下检测用) */
/* ======================== 模块状态 ======================== */
static esp_lcd_panel_handle_t s_lcd; /* LCD 面板句柄 */
static spi_device_handle_t s_touch_dev; /* 触摸 SPI 设备句柄 */
/* 填充行缓冲:静态分配(约 3.8KB),避免运行时堆分配 */
static uint16_t s_fill_buf[LCD_WIDTH * FILL_ROWS];
/* ============================================================
* LCD:填充矩形 [x1,x2) x [y1,y2)(开区间,自动裁剪+防越界)
* 分块送 FILL_ROWS 行,单块大小受 max_transfer_sz 约束
* ============================================================ */
static void lcd_fill(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
if (x2 > LCD_WIDTH) x2 = LCD_WIDTH; /* 裁剪到屏内 */
if (y2 > LCD_HEIGHT) y2 = LCD_HEIGHT;
if (x2 <= x1 || y2 <= y1) return; /* 空区域直接返回 */
/* 整块行缓冲预刷色,循环复用 */
for (uint32_t i = 0; i < (uint32_t)(x2 - x1) * FILL_ROWS; i++) {
s_fill_buf[i] = color;
}
for (uint16_t y = y1; y < y2; ) {
uint16_t rows = (y2 - y > FILL_ROWS) ? FILL_ROWS : (y2 - y);
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(s_lcd, x1, y, x2, y + rows, s_fill_buf));
y += rows;
}
}
/* ============================================================
* XPT2046:单次 12 位 ADC 读取
* 设备配了 command_bits=8,硬件每笔事务自动先发命令字节,
* 随后 16 个时钟收应答:[busy][12位数据][3位填充],右移3取12位
* ============================================================ */
static uint16_t xpt2046_read(uint8_t cmd)
{
static const uint8_t zeros[2] = {0}; /* MOSI 占位(只读操作) */
uint8_t rx[2] = {0};
spi_transaction_t t = {
.cmd = cmd, /* 硬件自动发出的命令字节 */
.length = 16, /* 收 16 位 */
.tx_buffer = zeros,
.rx_buffer = rx,
};
ESP_ERROR_CHECK(spi_device_polling_transmit(s_touch_dev, &t));
return (((uint16_t)rx[0] << 8) | rx[1]) >> 3 & 0x0FFF;
}
/* ============================================================
* 滤波:插入排序后取中位值
* 电阻屏单次读数偶发尖刺,中位值滤波可无损失地剔除
* ============================================================ */
static uint16_t median5(uint16_t *a)
{
for (int i = 1; i < TOUCH_SAMPLES; i++) {
uint16_t key = a[i];
int j = i - 1;
while (j >= 0 && a[j] > key) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
return a[TOUCH_SAMPLES / 2];
}
/* ============================================================
* 坐标映射:RAW ADC -> 屏幕像素
* 处理顺序:轴交换 -> 镜像 -> 线性映射(钳位)-> 像素偏移补偿
* ============================================================ */
static void touch_map(uint16_t rx, uint16_t ry, uint16_t *sx, uint16_t *sy)
{
#if TOUCH_SWAP_XY
uint16_t t = rx; rx = ry; ry = t;
#endif
#if TOUCH_INVERT_X
rx = RAW_X_MIN + RAW_X_MAX - rx;
#endif
#if TOUCH_INVERT_Y
ry = RAW_Y_MIN + RAW_Y_MAX - ry;
#endif
/* 线性映射,(uint32_t) 中间量防乘法溢出 */
*sx = (uint16_t)(rx <= RAW_X_MIN ? 0 :
rx >= RAW_X_MAX ? LCD_WIDTH - 1 :
(uint32_t)(rx - RAW_X_MIN) * (LCD_WIDTH - 1) / (RAW_X_MAX - RAW_X_MIN));
*sy = (uint16_t)(ry <= RAW_Y_MIN ? 0 :
ry >= RAW_Y_MAX ? LCD_HEIGHT - 1 :
(uint32_t)(ry - RAW_Y_MIN) * (LCD_HEIGHT - 1) / (RAW_Y_MAX - RAW_Y_MIN));
/* 叠加像素偏移补偿(有符号运算,双向可调),最后钳位 */
int32_t ox = (int32_t)*sx + TOUCH_X_OFFSET;
int32_t oy = (int32_t)*sy + TOUCH_Y_OFFSET;
*sx = (uint16_t)(ox < 0 ? 0 : ox >= LCD_WIDTH ? LCD_WIDTH - 1 : ox);
*sy = (uint16_t)(oy < 0 ? 0 : oy >= LCD_HEIGHT ? LCD_HEIGHT - 1 : oy);
}
/* ============================================================
* 触摸任务(轮询)
* 循环:
* 20ms 空闲轮询 -> Z1/IRQ 判断按下 -> 5次采样滤波 ->
* 映射画点 -> 30ms 限速 -> 抬笔后回到空闲轮询
* ============================================================ */
static void touch_task(void *arg)
{
#if TOUCH_DEBUG_RAW
/* 调试模式:无条件周期打印原始值,用于验证 SPI 链路 */
for (;;) {
uint16_t dx = xpt2046_read(XPT2046_CMD_X);
uint16_t dy = xpt2046_read(XPT2046_CMD_Y);
uint16_t dz = xpt2046_read(XPT2046_CMD_Z1);
printf("DBG X=%4u Y=%4u Z1=%4u IRQ=%d\n",
(unsigned)dx, (unsigned)dy, (unsigned)dz,
gpio_get_level(T_PIN_IRQ));
vTaskDelay(pdMS_TO_TICKS(200));
}
#else
uint16_t xs[TOUCH_SAMPLES], ys[TOUCH_SAMPLES];
for (;;) {
vTaskDelay(pdMS_TO_TICKS(TOUCH_POLL_MS));
/* ---- 1. 按下检测:Z1 压力为主,T_IRQ 电平为辅 ----
* Z1 是触摸膜上下层压合程度的 ADC 反映:
* 未按下 ~0,按压时几百到几千(力越大越大) */
bool pressed = (xpt2046_read(XPT2046_CMD_Z1) >= TOUCH_Z_THRESHOLD) ||
(gpio_get_level(T_PIN_IRQ) == 0);
if (!pressed) {
continue; /* 未按下,回到轮询 */
}
/* ---- 2. 采样 + 中位值滤波 ---- */
for (int i = 0; i < TOUCH_SAMPLES; i++) {
xs[i] = xpt2046_read(XPT2046_CMD_X);
ys[i] = xpt2046_read(XPT2046_CMD_Y);
}
uint16_t rx = median5(xs);
uint16_t ry = median5(ys);
/* 刚接触/边缘抖动时 RAW 会异常小,丢弃本次 */
if (rx < TOUCH_VALID_MIN || ry < TOUCH_VALID_MIN) {
continue;
}
/* ---- 3. 映射 + 画点 ---- */
uint16_t sx, sy;
touch_map(rx, ry, &sx, &sy);
printf("RAW(%4u,%4u) -> LCD(%3u,%3u)\n",
(unsigned)rx, (unsigned)ry, (unsigned)sx, (unsigned)sy);
lcd_fill(sx, sy, sx + 4, sy + 4, COLOR_BLACK); /* 5x5 黑点 */
vTaskDelay(pdMS_TO_TICKS(TOUCH_DRAW_MS)); /* 按住时限速 */
}
#endif
}
/* ============================================================
* XPT2046 初始化:SPI3 总线 + 设备 + IRQ 输入脚
* ============================================================ */
static void xpt2046_init(void)
{
/* SPI 总线:触摸数据量小(每笔 3 字节),禁 DMA 省资源 */
spi_bus_config_t bus = {
.mosi_io_num = T_PIN_SDI,
.miso_io_num = T_PIN_SDO,
.sclk_io_num = T_PIN_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
ESP_ERROR_CHECK(spi_bus_initialize(TOUCH_HOST, &bus, SPI_DMA_DISABLED));
/* XPT2046 设备:
* - mode 0(CPOL=0 CPHA=0),CS 低有效
* - command_bits=8:省去每笔事务手工拼命令 */
spi_device_interface_config_t dev = {
.clock_speed_hz = TOUCH_CLK_HZ,
.mode = 0,
.spics_io_num = T_PIN_CS,
.command_bits = 8,
.queue_size = 1,
};
ESP_ERROR_CHECK(spi_bus_add_device(TOUCH_HOST, &dev, &s_touch_dev));
/* T_IRQ:普通输入 + 上拉(未按下保持高)。轮询版不上中断,
* 接错/悬空都不影响功能(Z1 兜底) */
gpio_config_t io = {
.pin_bit_mask = 1ULL << T_PIN_IRQ,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
};
ESP_ERROR_CHECK(gpio_config(&io));
}
/* ============================================================
* LCD 初始化:SPI2 总线 -> panel_io -> st7789 -> 上电显示
* ============================================================ */
static void lcd_init(void)
{
/* SPI 总线:DMA 自动分配通道;传输上限 = 单块填充缓冲大小 */
spi_bus_config_t bus = {
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = PIN_NUM_MISO,
.sclk_io_num = PIN_NUM_SCLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = LCD_WIDTH * FILL_ROWS * sizeof(uint16_t),
};
ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, &bus, SPI_DMA_CH_AUTO));
/* Panel IO 层:自动打包"命令+参数"并控制 DC 脚 */
esp_lcd_panel_io_spi_config_t io_cfg = {
.cs_gpio_num = PIN_NUM_CS,
.dc_gpio_num = PIN_NUM_DC,
.spi_mode = 0,
.pclk_hz = LCD_PCLK_HZ,
.trans_queue_depth = 8,
.lcd_cmd_bits = LCD_CMD_BITS, /* ★ 不填会 init 失败 */
.lcd_param_bits = LCD_PARAM_BITS,
};
esp_lcd_panel_io_handle_t io = NULL;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST,
&io_cfg, &io));
/* ST7789 面板 */
esp_lcd_panel_dev_config_t panel_cfg = {
.reset_gpio_num = PIN_NUM_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, /* 红蓝反了改 BGR */
.bits_per_pixel = 16, /* RGB565 */
};
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io, &panel_cfg, &s_lcd));
/* 复位 -> 初始化寄存器 -> 开显示。
* invert_color:IPS 屏一般 true,TN 屏 false(按你的屏实测为 false) */
ESP_ERROR_CHECK(esp_lcd_panel_reset(s_lcd));
ESP_ERROR_CHECK(esp_lcd_panel_init(s_lcd));
esp_lcd_panel_invert_color(s_lcd, false);
esp_lcd_panel_disp_on_off(s_lcd, true);
/* 方向/横竖屏需要时打开(改了显示方向,触摸 SWAP/INVERT 要跟调):
* esp_lcd_panel_swap_xy(s_lcd, true);
* esp_lcd_panel_mirror(s_lcd, true, true); */
/* 背光 */
gpio_set_direction(PIN_NUM_BK, GPIO_MODE_OUTPUT);
gpio_set_level(PIN_NUM_BK, 1);
}
/* ============================================================
* 主入口:LCD -> 开机画面 -> 触摸任务
* ============================================================ */
void app_main(void)
{
lcd_init();
lcd_fill(0, 0, LCD_WIDTH, LCD_HEIGHT, COLOR_WHITE); /* 清屏 */
lcd_fill(20, 20, 140, 100, COLOR_RED); /* 测试红块 */
xpt2046_init();
xTaskCreate(touch_task, "touch", 4096, NULL, 5, NULL);
printf("demo start (polling touch), touch LCD to draw\n");
}
2中断实现触摸
cpp
/**
* ============================================================================
* 工程:ESP32-S3 + ST7789 LCD (SPI2) + XPT2046 触摸 (SPI3)
* 版本:中断版 v2.0(T_IRQ 下降沿唤醒 + Z1 压力兜底)
* 环境:ESP-IDF v5.2
*
* 与轮询版的区别:
* - 轮询版:任务每 20ms 醒一次查状态(简单,但空闲也占一点 CPU)
* - 中断版:任务平时深度休眠(0 CPU),T_IRQ 按下瞬间拉低触发
* GPIO 下降沿中断,中断里给任务发通知将其唤醒
*
* 中断工作流程:
* 手指按下 → T_IRQ 拉低 → 下降沿中断 touch_pen_isr()
* → vTaskNotifyGiveFromISR() 唤醒触摸任务
* 触摸任务被唤醒 → 消抖 → 读 Z1 确认真按下 → 采样滤波 → 画点
* → 循环采样直到 T_IRQ 变高(抬笔)→ 回到休眠等下一次中断
*
* ★ 注意事项:
* - s_touch_task 句柄必须先创建任务、再挂中断,否则 ISR 引用空句柄崩溃
* - touch_pen_isr 必须 IRAM_ATTR 且不能调 printf 等非 IRAM 安全函数
* - T_IRQ 没接线时此方案失效(回退用轮询版)
* ============================================================================
*/
/* ############################################################################
* # 移植配置区(换板子只改这里) #
* ############################################################################
*/
/* ---------------- LCD(ST7789 @ SPI2) ---------------- */
#define LCD_HOST SPI2_HOST
#define PIN_NUM_SCLK 6
#define PIN_NUM_MOSI 7
#define PIN_NUM_MISO (-1)
#define PIN_NUM_CS 5
#define PIN_NUM_DC 4
#define PIN_NUM_RST 8
#define PIN_NUM_BK 9
#define LCD_WIDTH 240
#define LCD_HEIGHT 320
#define LCD_CMD_BITS 8 /* ★ 必填,否则 init 报错 */
#define LCD_PARAM_BITS 8
#define LCD_PCLK_HZ (40 * 1000 * 1000)
#define FILL_ROWS 8
/* ---------------- 触摸(XPT2046 @ SPI3) ---------------- */
#define TOUCH_HOST SPI3_HOST
#define T_PIN_CLK 13
#define T_PIN_SDI 12 /* MOSI */
#define T_PIN_SDO 11 /* MISO */
#define T_PIN_CS 10
#define T_PIN_IRQ 14 /* ★ 中断版必须接!按下为低 */
#define TOUCH_CLK_HZ (1 * 1000 * 1000)
#define TOUCH_DEBOUNCE_MS 10 /* 中断唤醒后的消抖延时 */
#define TOUCH_DRAW_MS 30 /* 按住时的画点周期 */
#define TOUCH_SAMPLES 5 /* 每次采样次数(奇数) */
/* ---------------- 触摸校准 ---------------- */
#define RAW_X_MIN 300
#define RAW_X_MAX 3800
#define RAW_Y_MIN 300
#define RAW_Y_MAX 3800
#define TOUCH_SWAP_XY 1
#define TOUCH_INVERT_X 0
#define TOUCH_INVERT_Y 0
#define TOUCH_VALID_MIN 100
#define TOUCH_Z_THRESHOLD 20 /* Z1 兜底阈值 */
/* ---------------- 像素偏移补偿 ---------------- */
#define TOUCH_X_OFFSET 0
#define TOUCH_Y_OFFSET 20
/* ---------------- 颜色定义(bgr565) ---------------- */
#define COLOR_RED 0x001F
#define COLOR_WHITE 0x0000
#define COLOR_BLACK 0xFFFF
/* ############################################################################
* # 以下为实现部分,一般不用改 #
* ############################################################################ */
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
/* XPT2046 控制字节 */
#define XPT2046_CMD_X 0x90
#define XPT2046_CMD_Y 0xD0
#define XPT2046_CMD_Z1 0xB0
/* ======================== 模块状态 ======================== */
static esp_lcd_panel_handle_t s_lcd = NULL;
static spi_device_handle_t s_touch_dev = NULL;
static TaskHandle_t s_touch_task = NULL; /* ★ 中断通知的目标任务 */
static uint16_t s_fill_buf[LCD_WIDTH * FILL_ROWS];
/* ============================================================
* LCD:填充矩形 [x1,x2) x [y1,y2)(开区间,自动裁剪)
* ============================================================ */
static void lcd_fill(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
if (x2 > LCD_WIDTH) x2 = LCD_WIDTH;
if (y2 > LCD_HEIGHT) y2 = LCD_HEIGHT;
if (x2 <= x1 || y2 <= y1) return;
for (uint32_t i = 0; i < (uint32_t)(x2 - x1) * FILL_ROWS; i++) {
s_fill_buf[i] = color;
}
for (uint16_t y = y1; y < y2; ) {
uint16_t rows = (y2 - y > FILL_ROWS) ? FILL_ROWS : (y2 - y);
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(s_lcd, x1, y, x2, y + rows, s_fill_buf));
y += rows;
}
}
/* ============================================================
* XPT2046:单次 12 位 ADC 读取
* ============================================================ */
static uint16_t xpt2046_read(uint8_t cmd)
{
static const uint8_t zeros[2] = {0};
uint8_t rx[2] = {0};
spi_transaction_t t = {
.cmd = cmd,
.length = 16,
.tx_buffer = zeros,
.rx_buffer = rx,
};
ESP_ERROR_CHECK(spi_device_polling_transmit(s_touch_dev, &t));
return (((uint16_t)rx[0] << 8) | rx[1]) >> 3 & 0x0FFF;
}
/* ============================================================
* 滤波:插入排序取中位值
* ============================================================ */
static uint16_t median5(uint16_t *a)
{
for (int i = 1; i < TOUCH_SAMPLES; i++) {
uint16_t key = a[i];
int j = i - 1;
while (j >= 0 && a[j] > key) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
return a[TOUCH_SAMPLES / 2];
}
/* ============================================================
* 坐标映射:轴交换 -> 镜像 -> 线性映射 -> 偏移补偿
* ============================================================ */
static void touch_map(uint16_t rx, uint16_t ry, uint16_t *sx, uint16_t *sy)
{
#if TOUCH_SWAP_XY
uint16_t t = rx; rx = ry; ry = t;
#endif
#if TOUCH_INVERT_X
rx = RAW_X_MIN + RAW_X_MAX - rx;
#endif
#if TOUCH_INVERT_Y
ry = RAW_Y_MIN + RAW_Y_MAX - ry;
#endif
*sx = (uint16_t)(rx <= RAW_X_MIN ? 0 :
rx >= RAW_X_MAX ? LCD_WIDTH - 1 :
(uint32_t)(rx - RAW_X_MIN) * (LCD_WIDTH - 1) / (RAW_X_MAX - RAW_X_MIN));
*sy = (uint16_t)(ry <= RAW_Y_MIN ? 0 :
ry >= RAW_Y_MAX ? LCD_HEIGHT - 1 :
(uint32_t)(ry - RAW_Y_MIN) * (LCD_HEIGHT - 1) / (RAW_Y_MAX - RAW_Y_MIN));
int32_t ox = (int32_t)*sx + TOUCH_X_OFFSET;
int32_t oy = (int32_t)*sy + TOUCH_Y_OFFSET;
*sx = (uint16_t)(ox < 0 ? 0 : ox >= LCD_WIDTH ? LCD_WIDTH - 1 : ox);
*sy = (uint16_t)(oy < 0 ? 0 : oy >= LCD_HEIGHT ? LCD_HEIGHT - 1 : oy);
}
/* ============================================================
* ★ T_IRQ 下降沿中断服务函数
* 规则:
* - IRAM_ATTR:中断可能发生在 flash 缓存被禁用期间(如写 flash),
* ISR 代码和引用的静态数据必须在 IRAM 中
* - 只做最少的事:发通知唤醒任务,实际工作全部丢给任务
* - 不允许 printf / ESP_ERROR_CHECK / SPI 传输(耗时且不 IRAM 安全)
* ============================================================ */
static void IRAM_ATTR touch_pen_isr(void *arg)
{
BaseType_t higher_wakeup = pdFALSE;
/* 给触摸任务发通知(计数+1),唤醒 ulTaskNotifyTake 的休眠 */
vTaskNotifyGiveFromISR(s_touch_task, &higher_wakeup);
/* 中断唤醒了更高优先级任务时立即切换(本例任务优先级5,通常需要) */
if (higher_wakeup) {
portYIELD_FROM_ISR();
}
}
/* ============================================================
* 触摸任务(中断驱动)
* 状态机:
* [休眠] --中断通知--> [消抖] --Z1确认--> [按住循环采样画点]
* --抬笔--> [休眠]
* ============================================================ */
static void touch_task(void *arg)
{
uint16_t xs[TOUCH_SAMPLES], ys[TOUCH_SAMPLES];
for (;;) {
/* ---- 1. 深度休眠等中断(计数模式:取走通知并清零) ----
* portMAX_DELAY = 永久等待,不超时;空闲时 0 CPU */
(void)ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
/* ---- 2. 消抖:等抖动过去,也等首次采样稳定 ---- */
vTaskDelay(pdMS_TO_TICKS(TOUCH_DEBOUNCE_MS));
/* ---- 3. 确认真按下(防 EMI 误触发 / 毛刺) ----
* Z1 压力值作二次确认;若确认为误触发则回到休眠 */
if (xpt2046_read(XPT2046_CMD_Z1) < TOUCH_Z_THRESHOLD) {
continue;
}
/* ---- 4. 按住期间循环采样画点,直到抬笔 ---- */
do {
for (int i = 0; i < TOUCH_SAMPLES; i++) {
xs[i] = xpt2046_read(XPT2046_CMD_X);
ys[i] = xpt2046_read(XPT2046_CMD_Y);
}
uint16_t rx = median5(xs);
uint16_t ry = median5(ys);
if (rx >= TOUCH_VALID_MIN && ry >= TOUCH_VALID_MIN) {
uint16_t sx, sy;
touch_map(rx, ry, &sx, &sy);
printf("RAW(%4u,%4u) -> LCD(%3u,%3u)\n",
(unsigned)rx, (unsigned)ry, (unsigned)sx, (unsigned)sy);
lcd_fill(sx, sy, sx + 4, sy + 4, COLOR_BLACK); /* 白底画黑点 */
}
vTaskDelay(pdMS_TO_TICKS(TOUCH_DRAW_MS));
} while (gpio_get_level(T_PIN_IRQ) == 0); /* IRQ=0 还按着;变高=抬笔 */
/* 抬笔,回到循环开头继续休眠等下一次中断 */
}
}
/* ============================================================
* XPT2046 初始化:SPI3 总线 + 设备 + IRQ 下降沿中断
* ============================================================ */
static void xpt2046_init(void)
{
/* SPI 总线 */
spi_bus_config_t bus = {
.mosi_io_num = T_PIN_SDI,
.miso_io_num = T_PIN_SDO,
.sclk_io_num = T_PIN_CLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
ESP_ERROR_CHECK(spi_bus_initialize(TOUCH_HOST, &bus, SPI_DMA_DISABLED));
/* XPT2046 设备 */
spi_device_interface_config_t dev = {
.clock_speed_hz = TOUCH_CLK_HZ,
.mode = 0,
.spics_io_num = T_PIN_CS,
.command_bits = 8,
.queue_size = 1,
};
ESP_ERROR_CHECK(spi_bus_add_device(TOUCH_HOST, &dev, &s_touch_dev));
/* IRQ 脚:输入 + 上拉 + 下降沿中断(按下瞬间拉低触发) */
gpio_config_t io = {
.pin_bit_mask = 1ULL << T_PIN_IRQ,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_NEGEDGE,
};
ESP_ERROR_CHECK(gpio_config(&io));
}
/* ============================================================
* LCD 初始化(与你验证过的版本一致:BGR + invert)
* ============================================================ */
static void lcd_init(void)
{
spi_bus_config_t bus = {
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = PIN_NUM_MISO,
.sclk_io_num = PIN_NUM_SCLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = LCD_WIDTH * FILL_ROWS * sizeof(uint16_t),
};
ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, &bus, SPI_DMA_CH_AUTO));
esp_lcd_panel_io_spi_config_t io_cfg = {
.cs_gpio_num = PIN_NUM_CS,
.dc_gpio_num = PIN_NUM_DC,
.spi_mode = 0,
.pclk_hz = LCD_PCLK_HZ,
.trans_queue_depth = 8,
.lcd_cmd_bits = LCD_CMD_BITS,
.lcd_param_bits = LCD_PARAM_BITS,
};
esp_lcd_panel_io_handle_t io = NULL;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST,
&io_cfg, &io));
esp_lcd_panel_dev_config_t panel_cfg = {
.reset_gpio_num = PIN_NUM_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, /* 你的屏实测 BGR */
.bits_per_pixel = 16,
};
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io, &panel_cfg, &s_lcd));
ESP_ERROR_CHECK(esp_lcd_panel_reset(s_lcd));
ESP_ERROR_CHECK(esp_lcd_panel_init(s_lcd));
esp_lcd_panel_invert_color(s_lcd, true); /* 你的屏实测需要反相 */
esp_lcd_panel_disp_on_off(s_lcd, true);
gpio_set_direction(PIN_NUM_BK, GPIO_MODE_OUTPUT);
gpio_set_level(PIN_NUM_BK, 1);
}
/* ============================================================
* 主入口
* ★ 顺序很重要:先建触摸任务(拿到句柄)→ 再挂中断
* 若顺序反了,中断可能在句柄未初始化时触发导致崩溃
* ============================================================ */
void app_main(void)
{
lcd_init();
lcd_fill(0, 0, LCD_WIDTH, LCD_HEIGHT, COLOR_WHITE); /* 白底 */
lcd_fill(20, 20, 140, 100, COLOR_RED); /* 红测试块 */
/* 1. 触摸外设初始化(含 IRQ 脚配置,但中断还没挂) */
xpt2046_init();
/* 2. 先创建任务,s_touch_task 句柄就绪后中断才能安全通知它 */
xTaskCreate(touch_task, "touch", 4096, NULL, 5, &s_touch_task);
/* 3. 安装 GPIO 中断服务(整个程序只需一次),再挂本引脚的 ISR */
ESP_ERROR_CHECK(gpio_install_isr_service(0)); /* 0 = 默认标志 */
ESP_ERROR_CHECK(gpio_isr_handler_add(T_PIN_IRQ, touch_pen_isr, NULL));
printf("demo start (irq touch), touch LCD to draw\n");
}
中断版 vs 轮询版对比
| 轮询版 | 中断版(本版) | |
|---|---|---|
| 空闲 CPU | 每 20ms 醒一次(<1%) | 0%(深度休眠) |
| 响应延迟 | 最多 20ms | 按下瞬间(<1ms) |
| 依赖 T_IRQ 接线 | 不依赖(Z1 兜底) | 必须接好(Z1 只做消抖确认) |
| 代码复杂度 | 简单 | 需理解 ISR/通知机制 |
| 低功耗场景(Light-sleep) | 不行 | 可行 |