文章目录
-
- 一、前言
- 二、硬件准备
-
- [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 | 触摸中断(可选) |
重要提示:
- TSC2046的VCC必须接3.3V,不能接5V,否则会烧毁芯片
- 触摸屏的四线(X+、X-、Y+、Y-)已经连接到TSC2046芯片上,我们不需要额外连接
- PENIRQ引脚是触摸中断输出,当有触摸动作时会拉低,我们可以使用它来触发中断,也可以通过轮询方式检测触摸状态
三、软件环境搭建
3.1 开发环境
- Keil MDK 5.38或更高版本
- STM32CubeMX 6.8.0或更高版本
- ST-Link仿真器或USB转TTL下载器
3.2 工程创建步骤
-
打开STM32CubeMX,点击"New Project"
-
在"MCU Selector"中搜索"STM32F103C8T6",选择该芯片,点击"Start Project"
-
配置系统时钟:
- 进入"Clock Configuration"页面
- 选择HSE作为时钟源
- 配置PLL倍频为9倍,系统时钟为72MHz
- 点击"Resolve Clock Issues"自动解决时钟配置问题
-
配置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):使能
-
配置GPIO引脚:
- PA4:配置为"GPIO_Output",标签命名为"TS_CS"
- PB0:配置为"GPIO_Input",上拉模式,标签命名为"TS_PENIRQ"
-
配置串口1(用于调试):
- 点击"USART1"
- 模式选择"Asynchronous"(异步模式)
- 波特率:115200
- 数据位:8位
- 停止位:1位
- 校验位:无
-
生成工程:
- 进入"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 编译工程
- 在Keil MDK中打开生成的工程
- 点击"Project" -> "Build Target"编译工程
- 确保没有错误和警告
7.2 下载程序
- 将ST-Link仿真器连接到STM32开发板
- 点击"Flash" -> "Download"下载程序到开发板
- 下载完成后,开发板会自动复位并运行程序
八、测试与验证
8.1 串口调试
- 打开串口调试助手(如XCOM、串口助手等)
- 配置串口参数:波特率115200,数据位8,停止位1,校验位无
- 复位开发板,你应该能看到串口输出"触摸屏初始化完成"
- 如果是第一次运行,会显示"未检测到校准参数,请先进行校准"
8.2 执行校准
- 按下PA0引脚连接的按键(如果没有连接按键,可以在代码中注释掉按键检测部分,直接调用TS_Calibrate()函数)
- 串口会提示你依次点击5个校准点
- 按照提示依次点击屏幕上的对应位置
- 校准完成后,串口会输出校准参数,并提示"校准参数已保存到Flash"
8.3 触摸测试
- 校准完成后,用手指触摸屏幕的不同位置
- 串口会输出触摸的原始坐标、显示坐标和压力值
- 验证显示坐标是否与你触摸的位置一致
- 测试屏幕的四个角落和中心点,确保坐标准确
九、常见问题与解决方法
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的工作原理、驱动代码编写、校准算法实现以及测试验证等全部环节。
通过本文的学习,你应该能够独立完成触摸屏的驱动开发,并将其应用到自己的项目中。触摸屏作为一种重要的人机交互接口,在嵌入式系统中有着广泛的应用,掌握触摸屏的驱动开发技术是嵌入式工程师必备的技能之一。