STM32实战:基于STM32F103的触摸屏(TSC2046)驱动与校准

文章目录

    • 一、前言
    • 二、硬件准备
      • [2.1 所需材料](#2.1 所需材料)
      • [2.2 TSC2046芯片介绍](#2.2 TSC2046芯片介绍)
      • [2.3 硬件连接](#2.3 硬件连接)
    • 三、软件环境搭建
      • [3.1 开发环境](#3.1 开发环境)
      • [3.2 工程创建步骤](#3.2 工程创建步骤)
    • 四、TSC2046工作原理
      • [4.1 四线电阻式触摸屏原理](#4.1 四线电阻式触摸屏原理)
      • [4.2 TSC2046工作流程](#4.2 TSC2046工作流程)
      • [4.3 控制字节格式](#4.3 控制字节格式)
    • 五、驱动代码编写
      • [5.1 创建触摸屏驱动文件](#5.1 创建触摸屏驱动文件)
      • [5.2 编写头文件tsc2046.h](#5.2 编写头文件tsc2046.h)
      • [5.3 编写源文件tsc2046.c](#5.3 编写源文件tsc2046.c)
      • [5.4 修改main.c文件](#5.4 修改main.c文件)
    • 六、校准算法详解
      • [6.1 为什么需要校准](#6.1 为什么需要校准)
      • [6.2 四点校准法](#6.2 四点校准法)
      • [6.3 校准参数计算](#6.3 校准参数计算)
    • 七、编译与下载
      • [7.1 编译工程](#7.1 编译工程)
      • [7.2 下载程序](#7.2 下载程序)
    • 八、测试与验证
      • [8.1 串口调试](#8.1 串口调试)
      • [8.2 执行校准](#8.2 执行校准)
      • [8.3 触摸测试](#8.3 触摸测试)
    • 九、常见问题与解决方法
      • [9.1 没有触摸响应](#9.1 没有触摸响应)
      • [9.2 触摸坐标不准确](#9.2 触摸坐标不准确)
      • [9.3 触摸抖动严重](#9.3 触摸抖动严重)
      • [9.4 校准参数丢失](#9.4 校准参数丢失)
    • 十、功能扩展
      • [10.1 触摸手势识别](#10.1 触摸手势识别)
      • [10.2 与LCD显示结合](#10.2 与LCD显示结合)
      • [10.3 多点触摸](#10.3 多点触摸)
    • 十一、总结

一、前言

在嵌入式系统开发中,触摸屏作为一种直观的人机交互接口,被广泛应用于各种智能设备中。TSC2046是一款非常经典的四线电阻式触摸屏控制器,它通过SPI接口与微控制器通信,具有精度高、成本低、接口简单等优点。

本文将从零开始,详细讲解如何在STM32F103单片机上实现TSC2046触摸屏的驱动与校准。教程将涵盖硬件连接、SPI通信原理、驱动代码编写、校准算法实现以及实际测试等全部环节。所有代码都经过实际验证,零基础的小白也可以按照步骤一步步操作,最终实现一个可以正常使用的触摸屏功能。

二、硬件准备

2.1 所需材料

  • STM32F103C8T6最小系统板(或其他STM32F103系列开发板)
  • 2.4寸/2.8寸/3.5寸四线电阻式触摸屏(带TSC2046控制器)
  • 杜邦线若干
  • USB转TTL模块(用于程序下载和串口调试)
  • 5V电源适配器或USB线供电

2.2 TSC2046芯片介绍

TSC2046是TI公司生产的一款四线电阻式触摸屏控制器,主要特性如下:

  • 支持12位A/D转换,分辨率可达4096×4096
  • 内置2.5V参考电压源
  • 支持SPI串行接口通信,最高时钟频率可达2MHz
  • 低功耗设计,待机电流仅为1μA
  • 内置温度传感器
  • 工作电压范围:2.7V~5.5V

2.3 硬件连接

TSC2046与STM32F103通过SPI接口连接,同时还需要一个片选信号和一个中断信号(可选)。以下是推荐的引脚连接方式:

TSC2046引脚 STM32F103引脚 功能说明
VCC 3.3V 电源输入
GND GND
CS PA4 片选信号
SCK PA5 SPI时钟
MISO PA6 SPI数据输入
MOSI PA7 SPI数据输出
PENIRQ PB0 触摸中断(可选)

重要提示

  1. TSC2046的VCC必须接3.3V,不能接5V,否则会烧毁芯片
  2. 触摸屏的四线(X+、X-、Y+、Y-)已经连接到TSC2046芯片上,我们不需要额外连接
  3. PENIRQ引脚是触摸中断输出,当有触摸动作时会拉低,我们可以使用它来触发中断,也可以通过轮询方式检测触摸状态

三、软件环境搭建

3.1 开发环境

  • Keil MDK 5.38或更高版本
  • STM32CubeMX 6.8.0或更高版本
  • ST-Link仿真器或USB转TTL下载器

3.2 工程创建步骤

  1. 打开STM32CubeMX,点击"New Project"

  2. 在"MCU Selector"中搜索"STM32F103C8T6",选择该芯片,点击"Start Project"

  3. 配置系统时钟:

    • 进入"Clock Configuration"页面
    • 选择HSE作为时钟源
    • 配置PLL倍频为9倍,系统时钟为72MHz
    • 点击"Resolve Clock Issues"自动解决时钟配置问题
  4. 配置SPI1接口:

    • 进入"Pinout & Configuration"页面
    • 在左侧"Categories"中找到"Connectivity",点击"SPI1"
    • 模式选择"Full-Duplex Master"(全双工主模式)
    • 硬件NSS信号选择"Disable"(我们使用软件片选)
    • 配置SPI参数:
      • 数据大小:8位
      • 时钟极性(CPOL):低电平
      • 时钟相位(CPHA):第1个边沿
      • 波特率预分频器:64(此时SPI时钟为72MHz/64=1.125MHz,在TSC2046支持范围内)
      • 高位在前(MSB First):使能
  5. 配置GPIO引脚:

    • PA4:配置为"GPIO_Output",标签命名为"TS_CS"
    • PB0:配置为"GPIO_Input",上拉模式,标签命名为"TS_PENIRQ"
  6. 配置串口1(用于调试):

    • 点击"USART1"
    • 模式选择"Asynchronous"(异步模式)
    • 波特率:115200
    • 数据位:8位
    • 停止位:1位
    • 校验位:无
  7. 生成工程:

    • 进入"Project Manager"页面
    • 输入工程名称和保存路径
    • 工具链/IDE选择"MDK-ARM V5"
    • 点击"Generate Code"生成工程

四、TSC2046工作原理

4.1 四线电阻式触摸屏原理

四线电阻式触摸屏由两层透明的电阻薄膜组成,中间有微小的绝缘点隔开。当手指触摸屏幕时,两层薄膜在触摸点处接触,形成电阻回路。

  • X方向测量:在X+和X-两端施加电压,Y+作为测量端,测量触摸点的X坐标
  • Y方向测量:在Y+和Y-两端施加电压,X+作为测量端,测量触摸点的Y坐标

4.2 TSC2046工作流程

TSC2046的工作流程可以用以下Mermaid流程图表示:


初始化SPI接口和GPIO
拉低CS片选信号
发送控制字节
等待转换完成
读取12位转换结果
拉高CS片选信号
是否需要更多测量?
处理测量数据
计算触摸坐标
应用校准参数
输出最终坐标

4.3 控制字节格式

TSC2046通过一个8位的控制字节来配置转换模式和通道选择,控制字节的格式如下:

位7 位6 位5 位4 位3 位2 位1 位0
S A2 A1 A0 MODE SER/ PD1 PD0
DFR
  • S:起始位,必须为1
  • A2-A0:通道选择位
    • 001:X方向测量
    • 101:Y方向测量
    • 011:Z1方向测量(触摸压力)
    • 100:Z2方向测量(触摸压力)
    • 000:温度测量
  • MODE:分辨率选择
    • 0:12位分辨率
    • 1:8位分辨率
  • SER/DFR:单端/差分模式选择
    • 0:差分模式(推荐用于触摸屏测量)
    • 1:单端模式
  • PD1-PD0:电源模式选择
    • 00:掉电模式,转换完成后进入低功耗
    • 11:始终开启电源

常用的控制字节:

  • X方向测量:0x90(二进制:10010000)
  • Y方向测量:0xD0(二进制:11010000)

五、驱动代码编写

5.1 创建触摸屏驱动文件

在工程中创建两个新文件:

  • tsc2046.h:头文件,包含函数声明和宏定义
  • tsc2046.c:源文件,包含函数实现

5.2 编写头文件tsc2046.h

c 复制代码
#ifndef __TSC2046_H
#define __TSC2046_H

#include "stm32f1xx_hal.h"
#include "spi.h"
#include "gpio.h"
#include "usart.h"
#include <stdio.h>

/* 触摸屏参数定义 */
#define TS_SCREEN_WIDTH    240    // 屏幕宽度(像素)
#define TS_SCREEN_HEIGHT   320    // 屏幕高度(像素)
#define TS_MAX_SAMPLES     10     // 每次采样次数
#define TS_FILTER_THRESHOLD 50    // 滤波阈值

/* 控制字节定义 */
#define TS_CMD_X           0x90   // X方向测量命令
#define TS_CMD_Y           0xD0   // Y方向测量命令
#define TS_CMD_Z1          0xB0   // Z1方向测量命令
#define TS_CMD_Z2          0xC0   // Z2方向测量命令

/* 片选信号操作宏 */
#define TS_CS_LOW()        HAL_GPIO_WritePin(TS_CS_GPIO_Port, TS_CS_Pin, GPIO_PIN_RESET)
#define TS_CS_HIGH()       HAL_GPIO_WritePin(TS_CS_GPIO_Port, TS_CS_Pin, GPIO_PIN_SET)

/* 触摸状态结构体 */
typedef struct
{
    uint16_t x;            // 原始X坐标
    uint16_t y;            // 原始Y坐标
    uint16_t x_display;    // 校准后的显示X坐标
    uint16_t y_display;    // 校准后的显示Y坐标
    uint8_t  pressed;      // 触摸状态:1-按下,0-释放
    uint16_t pressure;     // 触摸压力
} TS_TouchTypeDef;

/* 校准参数结构体 */
typedef struct
{
    float kx;              // X方向比例系数
    float ky;              // Y方向比例系数
    int16_t bx;            // X方向偏移量
    int16_t by;            // Y方向偏移量
    uint8_t calibrated;    // 校准标志:1-已校准,0-未校准
} TS_CalibrationTypeDef;

/* 函数声明 */
void TS_Init(void);
uint8_t TS_IsPressed(void);
uint16_t TS_ReadAD(uint8_t cmd);
void TS_ReadRaw(uint16_t *x, uint16_t *y);
void TS_ReadFiltered(uint16_t *x, uint16_t *y);
void TS_Calibrate(void);
void TS_GetPoint(TS_TouchTypeDef *touch);
void TS_SaveCalibration(void);
void TS_LoadCalibration(void);

#endif /* __TSC2046_H */

5.3 编写源文件tsc2046.c

c 复制代码
#include "tsc2046.h"

/* 全局变量定义 */
TS_CalibrationTypeDef ts_calibration = {0};
TS_TouchTypeDef ts_touch = {0};

/* 校准点坐标(屏幕坐标) */
const uint16_t ts_cal_points[5][2] = {
    {20, 20},                  // 左上角
    {TS_SCREEN_WIDTH-20, 20},  // 右上角
    {TS_SCREEN_WIDTH-20, TS_SCREEN_HEIGHT-20},  // 右下角
    {20, TS_SCREEN_HEIGHT-20},  // 左下角
    {TS_SCREEN_WIDTH/2, TS_SCREEN_HEIGHT/2}     // 中心点
};

/* 校准点原始坐标 */
uint16_t ts_cal_raw_points[5][2] = {0};

/**
 * @brief  触摸屏初始化函数
 * @param  无
 * @retval 无
 */
void TS_Init(void)
{
    /* 初始化SPI接口(已由CubeMX生成) */
    /* 初始化GPIO引脚(已由CubeMX生成) */
    
    /* 初始时拉高片选信号 */
    TS_CS_HIGH();
    
    /* 加载校准参数 */
    TS_LoadCalibration();
    
    printf("触摸屏初始化完成\r\n");
    if(ts_calibration.calibrated)
    {
        printf("已加载校准参数\r\n");
        printf("kx = %.4f, ky = %.4f\r\n", ts_calibration.kx, ts_calibration.ky);
        printf("bx = %d, by = %d\r\n", ts_calibration.bx, ts_calibration.by);
    }
    else
    {
        printf("未检测到校准参数,请先进行校准\r\n");
    }
}

/**
 * @brief  检测是否有触摸动作
 * @param  无
 * @retval 1-有触摸,0-无触摸
 */
uint8_t TS_IsPressed(void)
{
    /* PENIRQ引脚低电平表示有触摸 */
    if(HAL_GPIO_ReadPin(TS_PENIRQ_GPIO_Port, TS_PENIRQ_Pin) == GPIO_PIN_RESET)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

/**
 * @brief  读取TSC2046的AD转换结果
 * @param  cmd: 控制字节命令
 * @retval 12位AD转换结果
 */
uint16_t TS_ReadAD(uint8_t cmd)
{
    uint8_t tx_buf[3] = {0};
    uint8_t rx_buf[3] = {0};
    uint16_t ad_value = 0;
    
    /* 拉低片选信号 */
    TS_CS_LOW();
    
    /* 发送控制字节 */
    tx_buf[0] = cmd;
    
    /* 发送3个字节,同时接收3个字节 */
    /* 第1个字节:发送命令,接收无效数据 */
    /* 第2个字节:发送0,接收高8位结果 */
    /* 第3个字节:发送0,接收低4位结果 */
    HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, 3, 100);
    
    /* 组合12位结果 */
    ad_value = ((uint16_t)rx_buf[1] << 4) | ((uint16_t)rx_buf[2] >> 4);
    
    /* 拉高片选信号 */
    TS_CS_HIGH();
    
    return ad_value;
}

/**
 * @brief  读取一次原始坐标
 * @param  x: 存储原始X坐标的指针
 * @param  y: 存储原始Y坐标的指针
 * @retval 无
 */
void TS_ReadRaw(uint16_t *x, uint16_t *y)
{
    *x = TS_ReadAD(TS_CMD_X);
    *y = TS_ReadAD(TS_CMD_Y);
}

/**
 * @brief  读取经过滤波的坐标
 * @param  x: 存储滤波后X坐标的指针
 * @param  y: 存储滤波后Y坐标的指针
 * @retval 无
 */
void TS_ReadFiltered(uint16_t *x, uint16_t *y)
{
    uint16_t x_buf[TS_MAX_SAMPLES] = {0};
    uint16_t y_buf[TS_MAX_SAMPLES] = {0};
    uint32_t x_sum = 0, y_sum = 0;
    uint16_t x_max, x_min, y_max, y_min;
    uint8_t i, j;
    
    /* 连续采样TS_MAX_SAMPLES次 */
    for(i = 0; i < TS_MAX_SAMPLES; i++)
    {
        TS_ReadRaw(&x_buf[i], &y_buf[i]);
    }
    
    /* 冒泡排序 */
    for(i = 0; i < TS_MAX_SAMPLES - 1; i++)
    {
        for(j = 0; j < TS_MAX_SAMPLES - i - 1; j++)
        {
            if(x_buf[j] > x_buf[j+1])
            {
                uint16_t temp = x_buf[j];
                x_buf[j] = x_buf[j+1];
                x_buf[j+1] = temp;
            }
            if(y_buf[j] > y_buf[j+1])
            {
                uint16_t temp = y_buf[j];
                y_buf[j] = y_buf[j+1];
                y_buf[j+1] = temp;
            }
        }
    }
    
    /* 去掉最大值和最小值 */
    x_max = x_buf[TS_MAX_SAMPLES - 1];
    x_min = x_buf[0];
    y_max = y_buf[TS_MAX_SAMPLES - 1];
    y_min = y_buf[0];
    
    /* 计算剩余值的平均值 */
    for(i = 1; i < TS_MAX_SAMPLES - 1; i++)
    {
        x_sum += x_buf[i];
        y_sum += y_buf[i];
    }
    
    *x = x_sum / (TS_MAX_SAMPLES - 2);
    *y = y_sum / (TS_MAX_SAMPLES - 2);
}

/**
 * @brief  触摸屏校准函数
 * @param  无
 * @retval 无
 */
void TS_Calibrate(void)
{
    uint8_t i;
    uint16_t x_raw, y_raw;
    float x1, y1, x2, y2, x3, y3, x4, y4;
    
    printf("开始触摸屏校准\r\n");
    printf("请依次点击屏幕上显示的5个校准点\r\n");
    
    /* 依次采集5个校准点的原始坐标 */
    for(i = 0; i < 5; i++)
    {
        printf("请点击第%d个校准点:(%d, %d)\r\n", i+1, ts_cal_points[i][0], ts_cal_points[i][1]);
        
        /* 等待触摸按下 */
        while(!TS_IsPressed());
        HAL_Delay(20);  // 消抖
        
        /* 读取滤波后的坐标 */
        TS_ReadFiltered(&x_raw, &y_raw);
        ts_cal_raw_points[i][0] = x_raw;
        ts_cal_raw_points[i][1] = y_raw;
        
        printf("第%d个校准点原始坐标:(%d, %d)\r\n", i+1, x_raw, y_raw);
        
        /* 等待触摸释放 */
        while(TS_IsPressed());
        HAL_Delay(20);  // 消抖
    }
    
    /* 计算校准参数 */
    /* 使用四点校准法 */
    x1 = ts_cal_raw_points[0][0];  // 左上角原始X
    y1 = ts_cal_raw_points[0][1];  // 左上角原始Y
    x2 = ts_cal_raw_points[1][0];  // 右上角原始X
    y2 = ts_cal_raw_points[1][1];  // 右上角原始Y
    x3 = ts_cal_raw_points[2][0];  // 右下角原始X
    y3 = ts_cal_raw_points[2][1];  // 右下角原始Y
    x4 = ts_cal_raw_points[3][0];  // 左下角原始X
    y4 = ts_cal_raw_points[3][1];  // 左下角原始Y
    
    /* 计算比例系数 */
    ts_calibration.kx = (float)(TS_SCREEN_WIDTH - 40) / ((x2 + x3) / 2 - (x1 + x4) / 2);
    ts_calibration.ky = (float)(TS_SCREEN_HEIGHT - 40) / ((y3 + y4) / 2 - (y1 + y2) / 2);
    
    /* 计算偏移量 */
    ts_calibration.bx = 20 - ts_calibration.kx * (x1 + x4) / 2;
    ts_calibration.by = 20 - ts_calibration.ky * (y1 + y2) / 2;
    
    /* 设置校准标志 */
    ts_calibration.calibrated = 1;
    
    printf("校准完成\r\n");
    printf("校准参数:\r\n");
    printf("kx = %.4f\r\n", ts_calibration.kx);
    printf("ky = %.4f\r\n", ts_calibration.ky);
    printf("bx = %d\r\n", ts_calibration.bx);
    printf("by = %d\r\n", ts_calibration.by);
    
    /* 保存校准参数 */
    TS_SaveCalibration();
}

/**
 * @brief  获取触摸点信息
 * @param  touch: 存储触摸信息的结构体指针
 * @retval 无
 */
void TS_GetPoint(TS_TouchTypeDef *touch)
{
    uint16_t x_raw, y_raw;
    
    /* 检测触摸状态 */
    touch->pressed = TS_IsPressed();
    
    if(touch->pressed)
    {
        /* 读取滤波后的原始坐标 */
        TS_ReadFiltered(&x_raw, &y_raw);
        touch->x = x_raw;
        touch->y = y_raw;
        
        /* 如果已经校准,计算显示坐标 */
        if(ts_calibration.calibrated)
        {
            touch->x_display = (int16_t)(ts_calibration.kx * x_raw + ts_calibration.bx);
            touch->y_display = (int16_t)(ts_calibration.ky * y_raw + ts_calibration.by);
            
            /* 坐标范围限制 */
            if(touch->x_display < 0) touch->x_display = 0;
            if(touch->x_display >= TS_SCREEN_WIDTH) touch->x_display = TS_SCREEN_WIDTH - 1;
            if(touch->y_display < 0) touch->y_display = 0;
            if(touch->y_display >= TS_SCREEN_HEIGHT) touch->y_display = TS_SCREEN_HEIGHT - 1;
        }
        else
        {
            touch->x_display = 0;
            touch->y_display = 0;
        }
        
        /* 计算触摸压力(可选) */
        uint16_t z1 = TS_ReadAD(TS_CMD_Z1);
        uint16_t z2 = TS_ReadAD(TS_CMD_Z2);
        touch->pressure = z2 - z1;
    }
    else
    {
        touch->x = 0;
        touch->y = 0;
        touch->x_display = 0;
        touch->y_display = 0;
        touch->pressure = 0;
    }
}

/**
 * @brief  保存校准参数到Flash
 * @param  无
 * @retval 无
 */
void TS_SaveCalibration(void)
{
    /* 这里使用STM32的Flash最后一页来保存校准参数 */
    /* STM32F103C8T6的Flash大小为64KB,最后一页地址为0x0800FC00 */
    uint32_t flash_addr = 0x0800FC00;
    uint32_t data[4] = {0};
    
    /* 将校准参数转换为32位数据 */
    data[0] = *(uint32_t*)&ts_calibration.kx;
    data[1] = *(uint32_t*)&ts_calibration.ky;
    data[2] = (uint32_t)ts_calibration.bx & 0xFFFF | ((uint32_t)ts_calibration.by << 16);
    data[3] = ts_calibration.calibrated;
    
    /* 解锁Flash */
    HAL_FLASH_Unlock();
    
    /* 擦除Flash页 */
    FLASH_EraseInitTypeDef erase_init;
    uint32_t page_error;
    
    erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
    erase_init.PageAddress = flash_addr;
    erase_init.NbPages = 1;
    
    HAL_FLASHEx_Erase(&erase_init, &page_error);
    
    /* 写入数据 */
    for(uint8_t i = 0; i < 4; i++)
    {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, flash_addr + i*4, data[i]);
    }
    
    /* 锁定Flash */
    HAL_FLASH_Lock();
    
    printf("校准参数已保存到Flash\r\n");
}

/**
 * @brief  从Flash加载校准参数
 * @param  无
 * @retval 无
 */
void TS_LoadCalibration(void)
{
    uint32_t flash_addr = 0x0800FC00;
    uint32_t data[4] = {0};
    
    /* 读取Flash数据 */
    for(uint8_t i = 0; i < 4; i++)
    {
        data[i] = *(uint32_t*)(flash_addr + i*4);
    }
    
    /* 检查是否有有效的校准参数 */
    if(data[3] == 1)
    {
        /* 恢复校准参数 */
        ts_calibration.kx = *(float*)&data[0];
        ts_calibration.ky = *(float*)&data[1];
        ts_calibration.bx = (int16_t)(data[2] & 0xFFFF);
        ts_calibration.by = (int16_t)(data[2] >> 16);
        ts_calibration.calibrated = 1;
    }
    else
    {
        /* 没有有效的校准参数 */
        ts_calibration.calibrated = 0;
    }
}

5.4 修改main.c文件

c 复制代码
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "spi.h"
#include "gpio.h"
#include "usart.h"
#include "tsc2046.h"

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);

/* 重定向printf函数到串口1 */
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100);
    return ch;
}

int main(void)
{
    /* 初始化HAL库 */
    HAL_Init();
    
    /* 配置系统时钟 */
    SystemClock_Config();
    
    /* 初始化所有外设 */
    MX_GPIO_Init();
    MX_SPI1_Init();
    MX_USART1_UART_Init();
    
    /* 初始化触摸屏 */
    TS_Init();
    
    /* 触摸信息结构体 */
    TS_TouchTypeDef touch;
    
    /* 主循环 */
    while (1)
    {
        /* 获取触摸点信息 */
        TS_GetPoint(&touch);
        
        /* 如果有触摸动作 */
        if(touch.pressed)
        {
            printf("触摸按下:原始坐标(%d, %d),显示坐标(%d, %d),压力:%d\r\n",
                   touch.x, touch.y, touch.x_display, touch.y_display, touch.pressure);
            
            /* 等待触摸释放 */
            while(touch.pressed)
            {
                TS_GetPoint(&touch);
                HAL_Delay(10);
            }
            
            printf("触摸释放\r\n");
        }
        
        /* 检测按键(用于触发校准) */
        /* 这里假设使用PA0作为校准按键,按下低电平 */
        if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
        {
            HAL_Delay(20);  // 消抖
            if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
            {
                /* 等待按键释放 */
                while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET);
                HAL_Delay(20);
                
                /* 开始校准 */
                TS_Calibrate();
            }
        }
        
        HAL_Delay(10);
    }
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    /** 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.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != 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_DIV2;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

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

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
    /* User can add his own implementation to report the HAL error return state */
    __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)
{
    /* User can add his own implementation to report the file name and line number,
       ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
}
#endif /* USE_FULL_ASSERT */

六、校准算法详解

6.1 为什么需要校准

由于触摸屏的安装误差、电阻薄膜的不均匀性以及AD转换的误差,原始坐标与实际屏幕坐标之间存在一定的偏差。为了获得准确的触摸位置,我们需要对触摸屏进行校准。

校准的本质是建立原始坐标(Xraw, Yraw)与显示坐标(Xdisplay, Ydisplay)之间的数学关系。对于大多数四线电阻式触摸屏,我们可以使用线性校准模型:

复制代码
Xdisplay = kx * Xraw + bx
Ydisplay = ky * Yraw + by

其中:

  • kx, ky:比例系数
  • bx, by:偏移量

6.2 四点校准法

本文使用的是四点校准法,这是一种简单而有效的校准方法。我们在屏幕的四个角落各取一个校准点,然后通过这四个点的原始坐标和显示坐标来计算校准参数。

校准流程的Mermaid流程图如下:
开始校准
显示第1个校准点左上角
等待用户点击
读取原始坐标并保存
显示第2个校准点(右上角)
等待用户点击
读取原始坐标并保存
显示第3个校准点(右下角)
等待用户点击
读取原始坐标并保存
显示第4个校准点(左下角)
等待用户点击
读取原始坐标并保存
计算校准参数kx, ky, bx, by
保存校准参数到Flash
校准完成

6.3 校准参数计算

假设四个校准点的显示坐标为:

  • P1(20, 20):左上角
  • P2(W-20, 20):右上角
  • P3(W-20, H-20):右下角
  • P4(20, H-20):左下角

对应的原始坐标为:

  • P1'(x1, y1)
  • P2'(x2, y2)
  • P3'(x3, y3)
  • P4'(x4, y4)

其中W是屏幕宽度,H是屏幕高度。

我们可以通过以下公式计算校准参数:

复制代码
kx = (W - 40) / [(x2 + x3)/2 - (x1 + x4)/2]
ky = (H - 40) / [(y3 + y4)/2 - (y1 + y2)/2]
bx = 20 - kx * (x1 + x4)/2
by = 20 - ky * (y1 + y2)/2

这种方法通过取平均值来减少单点测量误差,提高校准精度。

七、编译与下载

7.1 编译工程

  1. 在Keil MDK中打开生成的工程
  2. 点击"Project" -> "Build Target"编译工程
  3. 确保没有错误和警告

7.2 下载程序

  1. 将ST-Link仿真器连接到STM32开发板
  2. 点击"Flash" -> "Download"下载程序到开发板
  3. 下载完成后,开发板会自动复位并运行程序

八、测试与验证

8.1 串口调试

  1. 打开串口调试助手(如XCOM、串口助手等)
  2. 配置串口参数:波特率115200,数据位8,停止位1,校验位无
  3. 复位开发板,你应该能看到串口输出"触摸屏初始化完成"
  4. 如果是第一次运行,会显示"未检测到校准参数,请先进行校准"

8.2 执行校准

  1. 按下PA0引脚连接的按键(如果没有连接按键,可以在代码中注释掉按键检测部分,直接调用TS_Calibrate()函数)
  2. 串口会提示你依次点击5个校准点
  3. 按照提示依次点击屏幕上的对应位置
  4. 校准完成后,串口会输出校准参数,并提示"校准参数已保存到Flash"

8.3 触摸测试

  1. 校准完成后,用手指触摸屏幕的不同位置
  2. 串口会输出触摸的原始坐标、显示坐标和压力值
  3. 验证显示坐标是否与你触摸的位置一致
  4. 测试屏幕的四个角落和中心点,确保坐标准确

九、常见问题与解决方法

9.1 没有触摸响应

  • 检查硬件连接是否正确,特别是VCC和GND
  • 检查TSC2046的VCC是否接的是3.3V,而不是5V
  • 检查SPI接口配置是否正确(CPOL=0, CPHA=0, MSB First)
  • 检查片选信号是否正确拉低

9.2 触摸坐标不准确

  • 重新进行校准,确保点击校准点时准确无误
  • 增加采样次数,提高滤波效果
  • 检查屏幕尺寸定义是否正确(TS_SCREEN_WIDTH和TS_SCREEN_HEIGHT)
  • 检查校准参数是否正确保存和加载

9.3 触摸抖动严重

  • 增加消抖时间(HAL_Delay(20))
  • 增加采样次数(TS_MAX_SAMPLES)
  • 调整滤波阈值(TS_FILTER_THRESHOLD)
  • 检查触摸屏是否安装牢固,没有松动

9.4 校准参数丢失

  • 检查Flash写入和读取函数是否正确
  • 确保Flash地址没有被其他程序使用
  • 可以将校准参数硬编码到代码中,作为默认值

十、功能扩展

10.1 触摸手势识别

在基本触摸功能的基础上,我们可以添加手势识别功能,如:

  • 单击
  • 双击
  • 长按
  • 滑动(上下左右)
  • 缩放

10.2 与LCD显示结合

将触摸屏与LCD显示屏结合,可以实现完整的人机交互界面。你可以在LCD上显示按钮、菜单、文本等内容,然后通过触摸屏进行操作。

10.3 多点触摸

TSC2046本身不支持多点触摸,但你可以使用支持多点触摸的控制器,如FT5206、GT911等,实现多点触摸功能。

十一、总结

本文详细介绍了如何在STM32F103单片机上实现TSC2046触摸屏的驱动与校准。我们从硬件准备、软件环境搭建开始,逐步讲解了TSC2046的工作原理、驱动代码编写、校准算法实现以及测试验证等全部环节。

通过本文的学习,你应该能够独立完成触摸屏的驱动开发,并将其应用到自己的项目中。触摸屏作为一种重要的人机交互接口,在嵌入式系统中有着广泛的应用,掌握触摸屏的驱动开发技术是嵌入式工程师必备的技能之一。

相关推荐
集和诚JHCTECH1 小时前
边缘计算 + 机器视觉 | BRAV-7821让农产品智能分拣真正落地
人工智能·嵌入式硬件·边缘计算
国科安芯1 小时前
抗辐射 MCU 赋能商业航天电源系统:基于 AS32S601 的高可靠能量管理控制器设计与辐照验证
stm32·单片机·嵌入式硬件·mcu·risc-v·空间计算
The Shio1 小时前
OptiByte 操练场:面向 IoT/嵌入式的协议可视化调试工具
网络·嵌入式硬件·物联网·c#·.net·业界资讯·iot
大志出奇迹3 小时前
传输协议为大端,STM32为小端,数据传输的字节序问题
c语言·stm32·单片机·mcu·算法·rtos
踏着七彩祥云的小丑3 小时前
嵌入式测试学习第 8 天:万用表使用:测电压、电阻、通断、二极管档
单片机·嵌入式硬件
magic_now4 小时前
U-Boot双阶段启动机制深度解析:init_sequence_f[] 与 init_sequence_r[]
linux·嵌入式硬件
济6175 小时前
FreeRTOS日志任务设计----LogTask 日志任务
单片机·嵌入式·freertos
振南的单片机世界5 小时前
PWM模拟电压:数字信号“平均”一下,就能变成模拟量
stm32·单片机·嵌入式硬件
blevoice5 小时前
杰理AC6966B-QFN32蓝牙音频进阶:获取手机歌曲信息——让音箱“报歌名”其实不难
嵌入式硬件·智能手机·音视频·jl杰理蓝牙音频芯片·杰理ac696n开发板·ac6966b蓝牙音响芯片