esp32-s3 espidf实现st7789-tn+电阻屏

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) 不行 可行
相关推荐
电子工匠14 分钟前
Altium + Quilter 搭建指南
嵌入式硬件·ai·pcb工艺
HUI-47419 分钟前
EG1185|SOT23‑6 高压非隔离恒压恒流控制器,极简外围高压辅助与 LED 驱动方案
嵌入式硬件·硬件工程
恒锐丰科技林技术员34 分钟前
SS6952T 单通道 H 桥电机驱动芯片:大电流电机一体化解决方案
经验分享·嵌入式硬件·硬件工程
电子元器件选型哥1 小时前
组串电流检测和MPPT电流检测,到底有什么区别?
单片机·嵌入式硬件·硬件工程师·电子元器件·器件选型
Freak嵌入式1 小时前
树莓派 Pico GPIO 深度解析:从 MCU 架构到寄存器控制,底层原理与实践指南
java·开发语言·科技·单片机·嵌入式硬件·架构
Freak嵌入式2 小时前
实战对比:用 mpremote/Putty/MobaXterm 连接树莓派 Pico REPL,谁更省心?
java·大数据·开发语言·科技·单片机·嵌入式硬件
Freak嵌入式2 小时前
告别死循环!Pico 外部中断控制 LED,比轮询高效 100 倍
java·开发语言·stm32·单片机·嵌入式硬件
自小吃多3 小时前
PCB Editor软件网络飞线常规处理笔记
笔记·嵌入式硬件
深圳市恒锐丰科技杨生3 小时前
CL4056D 1A 单节锂电池线性充电芯片|ESOP‑8/DFN2×2‑8 便携充电方案
嵌入式硬件·硬件工程