欢迎光临
我们一直在努力

触摸屏交互实战:STM32F746驱动电阻屏,控件与事件处理

文章目录

    • 一、开发环境与硬件准备
      • 1.1 硬件清单
      • 1.2 软件环境
    • 二、原理与整体流程
      • 2.1 电阻屏工作原理
      • 2.2 整体开发流程图
    • 三、硬件接线(STM32F746 ↔ XPT2046)
    • 四、STM32CubeMX配置步骤
      • 4.1 新建工程
      • 4.2 核心配置
        • 4.2.1 SPI配置
        • 4.2.2 GPIO配置
        • 4.2.3 时钟配置
        • 4.2.4 串口配置(用于调试)
      • 4.3 生成代码
    • 五、底层驱动开发
      • 5.1 XPT2046驱动代码(touchscreen.c/h)
        • 文件名:touchscreen.h
        • 文件名:touchscreen.c
      • 5.2 LCD驱动适配(基础显示功能)
        • 文件名:lcd.h(简化版,需适配实际屏)
        • 文件名:lcd.c(核心函数示例)
      • 5.3 ASCII字库文件(font.h)
        • 文件名:font.h
    • 六、控件系统开发
      • 6.1 控件类型与事件定义
        • 文件名:gui_widget.h
      • 6.2 控件实现代码
        • 文件名:gui_widget.c
    • 七、事件处理与业务逻辑
      • 7.1 主函数集成(main.c)
        • 文件名:main.c
    • 八、校准与测试
      • 8.1 触摸屏校准步骤
      • 8.2 测试步骤
      • 8.3 常见问题排查
    • 九、扩展功能建议
      • 总结

一、开发环境与硬件准备

1.1 硬件清单

  • 主控芯片:STM32F746ZG(核心板/开发板)
  • 电阻屏:4线式电阻触摸屏(配套STM32F746开发板)
  • 触摸屏控制器:XPT2046(主流电阻屏控制芯片)
  • 外设:SPI接口(用于驱动XPT2046)、GPIO(中断/片选)
  • 辅助工具:USB-TTL模块、杜邦线、电源、STM32烧录器

1.2 软件环境

  • IDE:STM32CubeIDE 1.15.0(兼容主流版本)
  • 固件库:STM32CubeF7 v1.17.0
  • 调试工具:ST-Link V2、串口调试助手
  • 字体库:ASCII字库(用于控件显示,文末附简易字库代码)

二、原理与整体流程

2.1 电阻屏工作原理

4线式电阻屏通过检测X/Y轴的电压分压值计算触摸坐标:

  • 触摸时,上下两层ITO膜接触,控制器通过切换电极电压采集X/Y轴模拟量;
  • XPT2046将模拟量转换为12位数字量,通过SPI传输给STM32;
  • STM32对原始坐标进行校准,转换为屏幕像素坐标;
  • 结合控件映射关系,判断触摸事件并执行对应逻辑。
  • 2.2 整体开发流程图

    #mermaid-svg-tvXu4MfO3Jd1oc27{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tvXu4MfO3Jd1oc27 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tvXu4MfO3Jd1oc27 .error-icon{fill:#552222;}#mermaid-svg-tvXu4MfO3Jd1oc27 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tvXu4MfO3Jd1oc27 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tvXu4MfO3Jd1oc27 .marker.cross{stroke:#333333;}#mermaid-svg-tvXu4MfO3Jd1oc27 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tvXu4MfO3Jd1oc27 p{margin:0;}#mermaid-svg-tvXu4MfO3Jd1oc27 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-tvXu4MfO3Jd1oc27 .cluster-label text{fill:#333;}#mermaid-svg-tvXu4MfO3Jd1oc27 .cluster-label span{color:#333;}#mermaid-svg-tvXu4MfO3Jd1oc27 .cluster-label span p{background-color:transparent;}#mermaid-svg-tvXu4MfO3Jd1oc27 .label text,#mermaid-svg-tvXu4MfO3Jd1oc27 span{fill:#333;color:#333;}#mermaid-svg-tvXu4MfO3Jd1oc27 .node rect,#mermaid-svg-tvXu4MfO3Jd1oc27 .node circle,#mermaid-svg-tvXu4MfO3Jd1oc27 .node ellipse,#mermaid-svg-tvXu4MfO3Jd1oc27 .node polygon,#mermaid-svg-tvXu4MfO3Jd1oc27 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tvXu4MfO3Jd1oc27 .rough-node .label text,#mermaid-svg-tvXu4MfO3Jd1oc27 .node .label text,#mermaid-svg-tvXu4MfO3Jd1oc27 .image-shape .label,#mermaid-svg-tvXu4MfO3Jd1oc27 .icon-shape .label{text-anchor:middle;}#mermaid-svg-tvXu4MfO3Jd1oc27 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tvXu4MfO3Jd1oc27 .rough-node .label,#mermaid-svg-tvXu4MfO3Jd1oc27 .node .label,#mermaid-svg-tvXu4MfO3Jd1oc27 .image-shape .label,#mermaid-svg-tvXu4MfO3Jd1oc27 .icon-shape .label{text-align:center;}#mermaid-svg-tvXu4MfO3Jd1oc27 .node.clickable{cursor:pointer;}#mermaid-svg-tvXu4MfO3Jd1oc27 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tvXu4MfO3Jd1oc27 .arrowheadPath{fill:#333333;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tvXu4MfO3Jd1oc27 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tvXu4MfO3Jd1oc27 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tvXu4MfO3Jd1oc27 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tvXu4MfO3Jd1oc27 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tvXu4MfO3Jd1oc27 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tvXu4MfO3Jd1oc27 .cluster text{fill:#333;}#mermaid-svg-tvXu4MfO3Jd1oc27 .cluster span{color:#333;}#mermaid-svg-tvXu4MfO3Jd1oc27 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tvXu4MfO3Jd1oc27 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tvXu4MfO3Jd1oc27 rect.text{fill:none;stroke-width:0;}#mermaid-svg-tvXu4MfO3Jd1oc27 .icon-shape,#mermaid-svg-tvXu4MfO3Jd1oc27 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tvXu4MfO3Jd1oc27 .icon-shape p,#mermaid-svg-tvXu4MfO3Jd1oc27 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tvXu4MfO3Jd1oc27 .icon-shape rect,#mermaid-svg-tvXu4MfO3Jd1oc27 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tvXu4MfO3Jd1oc27 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tvXu4MfO3Jd1oc27 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tvXu4MfO3Jd1oc27 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-tvXu4MfO3Jd1oc27 .node>*{fill:#2c3e50!important;stroke:#3498db!important;stroke-width:2px!important;color:#ffffff!important;font-size:14px!important;font-family:Arial!important;}#mermaid-svg-tvXu4MfO3Jd1oc27 .node span{fill:#2c3e50!important;stroke:#3498db!important;stroke-width:2px!important;color:#ffffff!important;font-size:14px!important;font-family:Arial!important;}#mermaid-svg-tvXu4MfO3Jd1oc27 .node tspan{fill:#ffffff!important;}

    硬件接线

    CubeMX配置

    底层驱动开发

    坐标校准

    控件系统实现

    事件处理逻辑

    联调测试

    三、硬件接线(STM32F746 ↔ XPT2046)

    STM32F746引脚XPT2046引脚功能说明
    PB12 CS 片选(低有效)
    PB13 SCK SPI时钟
    PB14 MISO 数据输入
    PB15 MOSI 数据输出
    PA0 PENIRQ 触摸中断(低有效)
    3.3V VCC 电源
    GND GND

    四、STM32CubeMX配置步骤

    4.1 新建工程

  • 打开STM32CubeIDE,新建STM32 Project,选择芯片型号STM32F746ZG;
  • 配置工程名称(如STM32F746_TouchScreen),选择保存路径,点击Finish。
  • 4.2 核心配置

    4.2.1 SPI配置
    • 外设选择:SPI2(对应PB12-PB15);
    • 模式:Master(主机模式);
    • 时钟极性(CPOL):High;
    • 时钟相位(CPHA):2 Edge;
    • 数据大小:8位;
    • 波特率:2MHz(XPT2046最大支持3MHz);
    • NSS:Software(软件片选)。
    4.2.2 GPIO配置
    • PA0:Input(下拉模式),命名为TOUCH_INT(触摸中断引脚);
    • PB12:Output(推挽输出),命名为TOUCH_CS(片选引脚),默认高电平(未选中)。
    4.2.3 时钟配置
    • 外部晶振:25MHz;
    • 系统时钟:216MHz(STM32F746最大主频);
    • SPI2时钟源:APB1(42MHz)。
    4.2.4 串口配置(用于调试)
    • USART1:异步模式,波特率115200,8位数据位,1位停止位,无校验;
    • 引脚:PA9(TX)、PA10(RX)。

    4.3 生成代码

  • 点击Project → Generate Code;
  • 选择生成HAL库代码,保留默认设置,完成工程生成。
  • 五、底层驱动开发

    5.1 XPT2046驱动代码(touchscreen.c/h)

    文件名:touchscreen.h

    #ifndef TOUCHSCREEN_H
    #define TOUCHSCREEN_H

    #include "main.h"
    #include "spi.h"

    // 屏幕分辨率(根据实际屏调整,示例为800×480)
    #define SCREEN_WIDTH 800
    #define SCREEN_HEIGHT 480

    // XPT2046 命令定义
    #define XPT2046_CMD_X 0x90 // 读取X坐标
    #define XPT2046_CMD_Y 0xD0 // 读取Y坐标
    #define XPT2046_CMD_Z1 0xB0 // 读取压力1
    #define XPT2046_CMD_Z2 0xC0 // 读取压力2

    // 触摸状态枚举
    typedef enum {
    TOUCH_STATE_RELEASED = 0, // 未触摸
    TOUCH_STATE_PRESSED // 触摸中
    } Touch_StateTypeDef;

    // 触摸坐标结构体
    typedef struct {
    uint16_t x; // X坐标(像素)
    uint16_t y; // Y坐标(像素)
    uint16_t adc_x; // X原始ADC值
    uint16_t adc_y; // Y原始ADC值
    Touch_StateTypeDef state; // 触摸状态
    } Touch_CoordinateTypeDef;

    // 校准参数结构体
    typedef struct {
    float x_a; // X轴校准系数a (x = a*adc_x + b)
    float x_b; // X轴校准系数b
    float y_a; // Y轴校准系数a (y = a*adc_y + b)
    float y_b; // Y轴校准系数b
    } Touch_CalibrateTypeDef;

    // 函数声明
    void TOUCH_Init(void); // 触摸屏初始化
    Touch_StateTypeDef TOUCH_GetState(void); // 获取触摸状态
    uint16_t TOUCH_ReadADC(uint8_t cmd); // 读取ADC值
    void TOUCH_ReadRawCoordinate(Touch_CoordinateTypeDef *coord); // 读取原始坐标
    void TOUCH_Calibrate(Touch_CalibrateTypeDef *calib, uint16_t adc_x, uint16_t adc_y, uint16_t real_x, uint16_t real_y); // 单点校准
    void TOUCH_ConvertToPixel(Touch_CoordinateTypeDef *coord, Touch_CalibrateTypeDef *calib); // 转换为像素坐标

    #endif /* TOUCHSCREEN_H */

    文件名:touchscreen.c

    #include "touchscreen.h"

    // 全局校准参数(默认值,需实际校准后更新)
    static Touch_CalibrateTypeDef touch_calib = {
    .x_a = 0.190, // 示例系数,需根据实际屏调整
    .x_b = 10,
    .y_a = 0.115,
    .y_b = 5
    };

    /**
    * @brief 触摸屏初始化
    * @param 无
    * @retval 无
    */

    void TOUCH_Init(void)
    {
    // 设置片选引脚初始状态为高(未选中)
    HAL_GPIO_WritePin(TOUCH_CS_GPIO_Port, TOUCH_CS_Pin, GPIO_PIN_SET);
    }

    /**
    * @brief 获取触摸状态
    * @param 无
    * @retval TOUCH_STATE_RELEASED/TOUCH_STATE_PRESSED
    */

    Touch_StateTypeDef TOUCH_GetState(void)
    {
    // PENIRQ引脚低电平表示有触摸
    if(HAL_GPIO_ReadPin(TOUCH_INT_GPIO_Port, TOUCH_INT_Pin) == GPIO_PIN_RESET)
    {
    return TOUCH_STATE_PRESSED;
    }
    else
    {
    return TOUCH_STATE_RELEASED;
    }
    }

    /**
    * @brief 读取XPT2046 ADC值
    * @param cmd:读取命令(X/Y/Z)
    * @retval 12位ADC值
    */

    uint16_t TOUCH_ReadADC(uint8_t cmd)
    {
    uint8_t tx_data[2] = {0};
    uint8_t rx_data[2] = {0};
    uint16_t adc_value = 0;

    // 片选拉低,选中XPT2046
    HAL_GPIO_WritePin(TOUCH_CS_GPIO_Port, TOUCH_CS_Pin, GPIO_PIN_RESET);
    HAL_Delay(1); // 短延时确保稳定

    // 发送读取命令
    tx_data[0] = cmd;
    HAL_SPI_TransmitReceive(&hspi2, tx_data, rx_data, 2, 100);

    // 片选拉高,取消选中
    HAL_GPIO_WritePin(TOUCH_CS_GPIO_Port, TOUCH_CS_Pin, GPIO_PIN_SET);

    // 提取12位ADC值(前4位无效,中间12位有效)
    adc_value = ((rx_data[0] & 0x0F) << 8) | rx_data[1];

    return adc_value;
    }

    /**
    * @brief 读取原始ADC坐标
    * @param coord:坐标结构体指针
    * @retval 无
    */

    void TOUCH_ReadRawCoordinate(Touch_CoordinateTypeDef *coord)
    {
    if(coord == NULL) return;

    // 更新触摸状态
    coord->state = TOUCH_GetState();

    if(coord->state == TOUCH_STATE_PRESSED)
    {
    // 多次读取取平均值,减少抖动
    uint32_t adc_x_sum = 0, adc_y_sum = 0;
    for(uint8_t i=0; i<5; i++)
    {
    adc_x_sum += TOUCH_ReadADC(XPT2046_CMD_X);
    adc_y_sum += TOUCH_ReadADC(XPT2046_CMD_Y);
    }
    coord->adc_x = adc_x_sum / 5;
    coord->adc_y = adc_y_sum / 5;
    }
    else
    {
    coord->adc_x = 0;
    coord->adc_y = 0;
    }
    }

    /**
    * @brief 单点校准(最小二乘法基础版)
    * @param calib:校准参数指针
    * @param adc_x:校准点ADC X值
    * @param adc_y:校准点ADC Y值
    * @param real_x:校准点实际像素X值
    * @param real_y:校准点实际像素Y值
    * @retval 无
    */

    void TOUCH_Calibrate(Touch_CalibrateTypeDef *calib, uint16_t adc_x, uint16_t adc_y, uint16_t real_x, uint16_t real_y)
    {
    if(calib == NULL) return;

    // 简化版单点校准公式(实际建议用4点校准)
    calib->x_a = (float)(real_x) / (float)(adc_x);
    calib->x_b = 0;
    calib->y_a = (float)(real_y) / (float)(adc_y);
    calib->y_b = 0;

    // 保存校准参数(可扩展写入Flash,避免掉电丢失)
    }

    /**
    * @brief 将ADC坐标转换为屏幕像素坐标
    * @param coord:坐标结构体指针
    * @param calib:校准参数指针
    * @retval 无
    */

    void TOUCH_ConvertToPixel(Touch_CoordinateTypeDef *coord, Touch_CalibrateTypeDef *calib)
    {
    if(coord == NULL || calib == NULL) return;

    if(coord->state == TOUCH_STATE_PRESSED)
    {
    // 应用校准公式计算像素坐标
    coord->x = (uint16_t)(calib->x_a * coord->adc_x + calib->x_b);
    coord->y = (uint16_t)(calib->y_a * coord->adc_y + calib->y_b);

    // 边界限制,防止坐标超出屏幕
    if(coord->x > SCREEN_WIDTH) coord->x = SCREEN_WIDTH;
    if(coord->x < 0) coord->x = 0;
    if(coord->y > SCREEN_HEIGHT) coord->y = SCREEN_HEIGHT;
    if(coord->y < 0) coord->y = 0;
    }
    else
    {
    coord->x = 0;
    coord->y = 0;
    }
    }

    5.2 LCD驱动适配(基础显示功能)

    文件名:lcd.h(简化版,需适配实际屏)

    #ifndef LCD_H
    #define LCD_H

    #include "main.h"

    // LCD基本操作函数
    void LCD_Init(void); // LCD初始化
    void LCD_Clear(uint16_t color); // 清屏
    void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color); // 画点
    void LCD_ShowString(uint16_t x, uint16_t y, uint8_t *str, uint16_t color); // 显示字符串
    uint16_t LCD_GetWidth(void); // 获取屏幕宽度
    uint16_t LCD_GetHeight(void); // 获取屏幕高度

    #endif /* LCD_H */

    文件名:lcd.c(核心函数示例)

    #include "lcd.h"
    #include "font.h" // ASCII字库(文末附)

    // 屏幕参数
    static uint16_t lcd_width = 800;
    static uint16_t lcd_height = 480;

    /**
    * @brief LCD初始化(需根据实际屏驱动修改)
    * @param 无
    * @retval 无
    */

    void LCD_Init(void)
    {
    // 实际项目中需添加LCD硬件初始化代码(如FSMC驱动RGB屏)
    // 此处仅为占位,需适配STM32F746的LCD-TFT控制器
    HAL_Delay(100);
    LCD_Clear(0x0000); // 清屏为黑色
    }

    /**
    * @brief LCD清屏
    * @param color:清屏颜色(RGB565格式)
    * @retval 无
    */

    void LCD_Clear(uint16_t color)
    {
    // 实际项目中添加清屏代码
    // 示例:遍历所有像素点并填充颜色
    for(uint16_t y=0; y<lcd_height; y++)
    {
    for(uint16_t x=0; x<lcd_width; x++)
    {
    LCD_DrawPoint(x, y, color);
    }
    }
    }

    /**
    * @brief LCD画点
    * @param x:X坐标
    * @param y:Y坐标
    * @param color:点颜色
    * @retval 无
    */

    void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color)
    {
    // 实际项目中添加画点底层驱动
    // 需对接LCD的显存地址(如0xD0000000)
    if(x >= lcd_width || y >= lcd_height) return;
    }

    /**
    * @brief 显示ASCII字符串
    * @param x:起始X坐标
    * @param y:起始Y坐标
    * @param str:字符串指针
    * @param color:字体颜色
    * @retval 无
    */

    void LCD_ShowString(uint16_t x, uint16_t y, uint8_t *str, uint16_t color)
    {
    uint8_t ch = 0;
    uint16_t x0 = x;

    while(*str != '\\0')
    {
    ch = *str 32; // 偏移量(ASCII码从32开始)
    if(x > (lcd_width 8)) // 超出屏幕宽度换行
    {
    x = x0;
    y += 16;
    }
    if(y > (lcd_height 16)) // 超出屏幕高度退出
    {
    break;
    }

    // 绘制单个字符(8×16点阵)
    for(uint8_t i=0; i<16; i++)
    {
    uint8_t line = ascii_font[ch][i]; // 从字库取点阵数据
    for(uint8_t j=0; j<8; j++)
    {
    if(line & (0x80 >> j))
    {
    LCD_DrawPoint(x+j, y+i, color);
    }
    }
    }

    x += 8; // 字符宽度8像素
    str++;
    }
    }

    /**
    * @brief 获取屏幕宽度
    * @param 无
    * @retval 屏幕宽度
    */

    uint16_t LCD_GetWidth(void)
    {
    return lcd_width;
    }

    /**
    * @brief 获取屏幕高度
    * @param 无
    * @retval 屏幕高度
    */

    uint16_t LCD_GetHeight(void)
    {
    return lcd_height;
    }

    5.3 ASCII字库文件(font.h)

    文件名:font.h

    #ifndef FONT_H
    #define FONT_H

    // 8×16 ASCII字库(仅包含可见字符,32-127)
    const unsigned char ascii_font[96][16] = {
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // 空格(32)
    {0x00,0x00,0x7C,0x12,0x11,0x12,0x7C,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // !(33)
    // 省略中间字库数据(实际使用需补全完整8×16 ASCII字库)
    {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00} // ~(126)
    };

    #endif /* FONT_H */

    六、控件系统开发

    6.1 控件类型与事件定义

    文件名:gui_widget.h

    #ifndef GUI_WIDGET_H
    #define GUI_WIDGET_H

    #include "main.h"
    #include "touchscreen.h"
    #include "lcd.h"

    // 控件类型枚举
    typedef enum {
    WIDGET_TYPE_BUTTON = 0, // 按钮控件
    WIDGET_TYPE_LABEL, // 标签控件
    WIDGET_TYPE_SLIDER // 滑动条控件
    } Widget_TypeDef;

    // 事件类型枚举
    typedef enum {
    EVENT_NONE = 0, // 无事件
    EVENT_CLICK, // 单击事件
    EVENT_PRESS, // 长按事件
    EVENT_RELEASE, // 释放事件
    EVENT_SLIDE // 滑动事件
    } Event_TypeDef;

    // 控件状态枚举
    typedef enum {
    WIDGET_STATE_NORMAL = 0, // 正常状态
    WIDGET_STATE_PRESSED, // 按下状态
    WIDGET_STATE_DISABLED // 禁用状态
    } Widget_StateTypeDef;

    // 事件回调函数类型
    typedef void (*Widget_EventCallback)(void *widget, Event_TypeDef event);

    // 按钮控件结构体
    typedef struct {
    uint16_t x; // 左上角X坐标
    uint16_t y; // 左上角Y坐标
    uint16_t width; // 宽度
    uint16_t height; // 高度
    uint8_t *text; // 显示文本
    uint16_t normal_color; // 正常状态背景色
    uint16_t press_color; // 按下状态背景色
    uint16_t text_color; // 文本颜色
    Widget_StateTypeDef state; // 控件状态
    Widget_EventCallback callback; // 事件回调函数
    } Button_WidgetTypeDef;

    // 全局控件管理函数
    void GUI_DrawButton(Button_WidgetTypeDef *btn); // 绘制按钮
    Event_TypeDef GUI_CheckWidgetEvent(Button_WidgetTypeDef *btn, Touch_CoordinateTypeDef *coord); // 检查控件事件
    void GUI_HandleWidgetEvent(Button_WidgetTypeDef *btn, Event_TypeDef event); // 处理控件事件

    #endif /* GUI_WIDGET_H */

    6.2 控件实现代码

    文件名:gui_widget.c

    #include "gui_widget.h"

    /**
    * @brief 绘制按钮控件
    * @param btn:按钮结构体指针
    * @retval 无
    */

    void GUI_DrawButton(Button_WidgetTypeDef *btn)
    {
    if(btn == NULL) return;

    uint16_t bg_color = 0;

    // 根据状态选择背景色
    if(btn->state == WIDGET_STATE_PRESSED)
    {
    bg_color = btn->press_color;
    }
    else
    {
    bg_color = btn->normal_color;
    }

    // 绘制按钮背景(填充矩形)
    for(uint16_t y=btn->y; y<btn->y+btn->height; y++)
    {
    for(uint16_t x=btn->x; x<btn->x+btn->width; x++)
    {
    LCD_DrawPoint(x, y, bg_color);
    }
    }

    // 绘制按钮文本(居中显示)
    uint16_t text_x = btn->x + (btn->width (strlen((char*)btn->text) * 8)) / 2;
    uint16_t text_y = btn->y + (btn->height 16) / 2;
    LCD_ShowString(text_x, text_y, btn->text, btn->text_color);
    }

    /**
    * @brief 检查按钮控件事件
    * @param btn:按钮结构体指针
    * @param coord:触摸坐标结构体指针
    * @retval 事件类型
    */

    Event_TypeDef GUI_CheckWidgetEvent(Button_WidgetTypeDef *btn, Touch_CoordinateTypeDef *coord)
    {
    if(btn == NULL || coord == NULL) return EVENT_NONE;

    static uint32_t press_start_time = 0;
    Event_TypeDef event = EVENT_NONE;

    // 判断坐标是否在按钮区域内
    if(coord->x >= btn->x && coord->x <= btn->x + btn->width &&
    coord->y >= btn->y && coord->y <= btn->y + btn->height)
    {
    if(coord->state == TOUCH_STATE_PRESSED)
    {
    btn->state = WIDGET_STATE_PRESSED;
    if(press_start_time == 0)
    {
    press_start_time = HAL_GetTick(); // 记录按下起始时间
    }
    // 长按判断(超过500ms)
    if(HAL_GetTick() press_start_time >= 500)
    {
    event = EVENT_PRESS;
    }
    }
    else
    {
    if(press_start_time != 0)
    {
    // 短按(小于500ms)
    if(HAL_GetTick() press_start_time < 500)
    {
    event = EVENT_CLICK;
    }
    press_start_time = 0;
    }
    btn->state = WIDGET_STATE_NORMAL;
    event = EVENT_RELEASE;
    }
    }
    else
    {
    btn->state = WIDGET_STATE_NORMAL;
    press_start_time = 0;
    }

    return event;
    }

    /**
    * @brief 处理控件事件
    * @param btn:按钮结构体指针
    * @param event:事件类型
    * @retval 无
    */

    void GUI_HandleWidgetEvent(Button_WidgetTypeDef *btn, Event_TypeDef event)
    {
    if(btn == NULL || btn->callback == NULL) return;

    // 调用回调函数处理事件
    btn->callback((void*)btn, event);
    }

    七、事件处理与业务逻辑

    7.1 主函数集成(main.c)

    文件名:main.c

    /* Includes ——————————————————————*/
    #include "main.h"
    #include "spi.h"
    #include "usart.h"
    #include "gpio.h"
    #include "touchscreen.h"
    #include "lcd.h"
    #include "gui_widget.h"

    /* Private typedef ———————————————————–*/
    /* Private define ————————————————————*/
    // 按钮定义
    #define BTN1_X 100
    #define BTN1_Y 100
    #define BTN1_WIDTH 200
    #define BTN1_HEIGHT 80

    #define BTN2_X 100
    #define BTN2_Y 200
    #define BTN2_WIDTH 200
    #define BTN2_HEIGHT 80

    /* Private macro ————————————————————-*/
    /* Private variables ———————————————————*/
    // 触摸坐标实例
    static Touch_CoordinateTypeDef touch_coord;
    // 按钮实例
    static Button_WidgetTypeDef btn1 = {
    .x = BTN1_X,
    .y = BTN1_Y,
    .width = BTN1_WIDTH,
    .height = BTN1_HEIGHT,
    .text = (uint8_t*)"Button 1",
    .normal_color = 0x00FF, // 蓝色(RGB565)
    .press_color = 0x0080, // 深蓝色
    .text_color = 0xFFFF, // 白色
    .state = WIDGET_STATE_NORMAL,
    .callback = NULL
    };

    static Button_WidgetTypeDef btn2 = {
    .x = BTN2_X,
    .y = BTN2_Y,
    .width = BTN2_WIDTH,
    .height = BTN2_HEIGHT,
    .text = (uint8_t*)"Button 2",
    .normal_color = 0xFF00, // 红色
    .press_color = 0x8000, // 深红色
    .text_color = 0xFFFF, // 白色
    .state = WIDGET_STATE_NORMAL,
    .callback = NULL
    };

    /* Private function prototypes ———————————————–*/
    void SystemClock_Config(void);
    static void MX_GPIO_Init(void);
    static void MX_SPI2_Init(void);
    static void MX_USART1_UART_Init(void);
    void Button1_Callback(void *widget, Event_TypeDef event); // 按钮1回调
    void Button2_Callback(void *widget, Event_TypeDef event); // 按钮2回调

    /* Private user code ———————————————————*/
    /**
    * @brief The application entry point.
    * @retval int
    */

    int main(void)
    {
    /* MCU Configuration——————————————————–*/

    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();

    /* Configure the system clock */
    SystemClock_Config();

    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_SPI2_Init();
    MX_USART1_UART_Init();

    /* USER CODE BEGIN 2 */
    // 初始化外设
    LCD_Init();
    TOUCH_Init();

    // 设置按钮回调函数
    btn1.callback = Button1_Callback;
    btn2.callback = Button2_Callback;

    // 初始绘制按钮
    GUI_DrawButton(&btn1);
    GUI_DrawButton(&btn2);

    /* USER CODE END 2 */

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
    // 读取触摸坐标
    TOUCH_ReadRawCoordinate(&touch_coord);
    // 转换为像素坐标
    TOUCH_ConvertToPixel(&touch_coord, &touch_calib);

    // 检查按钮1事件
    Event_TypeDef event1 = GUI_CheckWidgetEvent(&btn1, &touch_coord);
    if(event1 != EVENT_NONE)
    {
    GUI_HandleWidgetEvent(&btn1, event1);
    GUI_DrawButton(&btn1); // 重绘按钮更新状态
    }

    // 检查按钮2事件
    Event_TypeDef event2 = GUI_CheckWidgetEvent(&btn2, &touch_coord);
    if(event2 != EVENT_NONE)
    {
    GUI_HandleWidgetEvent(&btn2, event2);
    GUI_DrawButton(&btn2); // 重绘按钮更新状态
    }

    HAL_Delay(10); // 消抖延时
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
    }

    /**
    * @brief 按钮1事件回调函数
    * @param widget:控件指针
    * @param event:事件类型
    * @retval 无
    */

    void Button1_Callback(void *widget, Event_TypeDef event)
    {
    Button_WidgetTypeDef *btn = (Button_WidgetTypeDef*)widget;
    switch(event)
    {
    case EVENT_CLICK:
    HAL_UART_Transmit(&huart1, (uint8_t*)"Button1 Click\\r\\n", 14, 100);
    LCD_ShowString(400, 120, (uint8_t*)"Btn1 Clicked!", 0x07E0); // 绿色文字
    break;
    case EVENT_PRESS:
    HAL_UART_Transmit(&huart1, (uint8_t*)"Button1 Press\\r\\n", 14, 100);
    LCD_ShowString(400, 120, (uint8_t*)"Btn1 Pressed!", 0xFFE0); // 黄色文字
    break;
    case EVENT_RELEASE:
    LCD_ShowString(400, 120, (uint8_t*)" ", 0x0000); // 清空提示
    break;
    default:
    break;
    }
    }

    /**
    * @brief 按钮2事件回调函数
    * @param widget:控件指针
    * @param event:事件类型
    * @retval 无
    */

    void Button2_Callback(void *widget, Event_TypeDef event)
    {
    Button_WidgetTypeDef *btn = (Button_WidgetTypeDef*)widget;
    switch(event)
    {
    case EVENT_CLICK:
    HAL_UART_Transmit(&huart1, (uint8_t*)"Button2 Click\\r\\n", 14, 100);
    LCD_ShowString(400, 220, (uint8_t*)"Btn2 Clicked!", 0x07E0);
    break;
    case EVENT_PRESS:
    HAL_UART_Transmit(&huart1, (uint8_t*)"Button2 Press\\r\\n", 14, 100);
    LCD_ShowString(400, 220, (uint8_t*)"Btn2 Pressed!", 0xFFE0);
    break;
    case EVENT_RELEASE:
    LCD_ShowString(400, 220, (uint8_t*)" ", 0x0000);
    break;
    default:
    break;
    }
    }

    /* 以下为自动生成的系统时钟、外设初始化代码,需保留 */
    /**
    * @brief System Clock Configuration
    * @retval None
    */

    void SystemClock_Config(void)
    {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    /** Configure LSE Drive Capability
    */

    HAL_PWR_EnableBkUpAccess();

    /** Configure the main internal regulator output voltage
    */

    __HAL_RCC_PWR_CLK_ENABLE();
    __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

    /** Initializes the RCC Oscillators according to the specified parameters
    * in the RCC_OscInitTypeDef structure.
    */

    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLM = 25;
    RCC_OscInitStruct.PLL.PLLN = 432;
    RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
    RCC_OscInitStruct.PLL.PLLQ = 9;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
    {
    Error_Handler();
    }

    /** Activate the Over-Drive mode
    */

    if (HAL_PWREx_EnableOverDrive() != HAL_OK)
    {
    Error_Handler();
    }

    /** Initializes the CPU, AHB and APB buses clocks
    */

    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
    |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_7) != HAL_OK)
    {
    Error_Handler();
    }
    }

    /**
    * @brief SPI2 Initialization Function
    * @param None
    * @retval None
    */

    static void MX_SPI2_Init(void)
    {
    hspi2.Instance = SPI2;
    hspi2.Init.Mode = SPI_MODE_MASTER;
    hspi2.Init.Direction = SPI_DIRECTION_2LINES;
    hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;
    hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;
    hspi2.Init.NSS = SPI_NSS_SOFT;
    hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_21; // 2MHz
    hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
    hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi2.Init.CRCPolynomial = 10;
    if (HAL_SPI_Init(&hspi2) != HAL_OK)
    {
    Error_Handler();
    }
    }

    /**
    * @brief USART1 Initialization Function
    * @param None
    * @retval None
    */

    static void MX_USART1_UART_Init(void)
    {
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;
    if (HAL_UART_Init(&huart1) != HAL_OK)
    {
    Error_Handler();
    }
    }

    /**
    * @brief GPIO Initialization Function
    * @param None
    * @retval None
    */

    static void MX_GPIO_Init(void)
    {
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    /* GPIO Ports Clock Enable */
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();

    /*Configure GPIO pin Output Level */
    HAL_GPIO_WritePin(TOUCH_CS_GPIO_Port, TOUCH_CS_Pin, GPIO_PIN_SET);

    /*Configure GPIO pin : TOUCH_INT_Pin */
    GPIO_InitStruct.Pin = TOUCH_INT_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLDOWN;
    HAL_GPIO_Init(TOUCH_INT_GPIO_Port, &GPIO_InitStruct);

    /*Configure GPIO pin : TOUCH_CS_Pin */
    GPIO_InitStruct.Pin = TOUCH_CS_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(TOUCH_CS_GPIO_Port, &GPIO_InitStruct);
    }

    /**
    * @brief This function is executed in case of error occurrence.
    * @retval None
    */

    void Error_Handler(void)
    {
    __disable_irq();
    while (1)
    {
    }
    }

    #ifdef USE_FULL_ASSERT
    /**
    * @brief Reports the name of the source file and the source line number
    * where the assert_param error has occurred.
    * @param file: pointer to the source file name
    * @param line: assert_param error line source number
    * @retval None
    */

    void assert_failed(uint8_t *file, uint32_t line)
    {
    }
    #endif /* USE_FULL_ASSERT */

    八、校准与测试

    8.1 触摸屏校准步骤

  • 在LCD上显示校准点(如屏幕四个角);
  • 用手指依次点击校准点,记录每个点的ADC值和实际像素值;
  • 调用TOUCH_Calibrate函数更新校准系数;
  • 可选:将校准系数写入STM32 Flash,避免掉电丢失。
  • 8.2 测试步骤

  • 将代码烧录到STM32F746开发板;
  • 连接串口调试助手(波特率115200);
  • 触摸按钮1/2,观察:
    • LCD上按钮状态变化(按下/释放);
    • 串口输出对应事件信息(Click/Press);
    • 提示文字显示/清空。
  • 8.3 常见问题排查

  • 无触摸响应:检查SPI接线、片选/中断引脚电平、XPT2046供电;
  • 坐标偏移:重新校准,调整校准系数;
  • 按钮无事件:检查控件坐标范围、回调函数绑定;
  • 显示异常:检查LCD驱动、字库数据、颜色格式(RGB565)。
  • 九、扩展功能建议

  • 增加更多控件:滑动条、复选框、文本框;
  • 实现多触点支持(需硬件适配);
  • 添加触摸手势识别(滑动、双击);
  • 优化抗干扰:增加滤波算法、消抖逻辑;
  • 移植GUI库:如emWin、TouchGFX(适合复杂界面)。
  • 总结

  • 整个开发流程核心分为硬件接线→CubeMX配置→底层驱动→控件开发→事件处理→测试校准,其中XPT2046的SPI通信和坐标校准是电阻屏驱动的关键;
  • 控件系统通过封装按钮绘制、事件检测、回调函数,实现了触摸交互的解耦,零基础小白可直接复用代码,仅需调整屏幕分辨率、校准系数和控件坐标;
  • 实际落地时需重点适配LCD底层驱动(如FSMC/TFT控制器)和完善字库数据,确保显示功能正常。
  • 赞(0)
    未经允许不得转载:171主机测评 » 触摸屏交互实战:STM32F746驱动电阻屏,控件与事件处理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址