第55章 电阻触摸屏-触摸画板
第五十五章 电阻触摸屏-触摸画板
1. 硬件设计
1.1 液晶实物图
1.2 屏幕PCB底版
屏幕的PCB底板引出的信号线会通过PCB底板上的FPC接口与液晶面板连接,这些信包括液晶控制相关的CS、RS等信号及DB0-DB15数据线, 其中RS引脚以高电平表示传输数据,低电平表示传输命令;另外还有引出LCD_BK引脚用于控制屏幕的背光供电,可以通过该引脚控制背光的强度, 该引脚为低电平时打开背光。图中的X+/X-/Y+/Y-引脚是液晶面板上触摸屏引出的信号线,它们会被连接到PCB底板的电阻触摸屏控制器, 用于检测触摸信号
1.3 屏幕PCB底版的触摸部分原理图
触摸检测的主体是型号为XPT2046的芯片,它接收触摸屏的X+/X-/Y+/Y-信号进行处理,把触摸信息使用SPI接口输出到STM32等控制器, 注意,由于控制XPT2046芯片的并不是STM32专用的硬件SPI接口,所以在编写程序时,需要使用软件模拟SPI时序与触摸芯片进行通讯。
1.4 液晶屏接口
图表示的是PCB底板引出的排针线序,屏幕整体通过这些引出的排针与开发板或其它控制器连接。
1.5 开发板与屏幕的连接的信号说明
图是霸道开发板上的液晶排母接口原理图, 它说明了配套的3.2寸屏幕接入到开发板上时的信号连接关系。 其中请着重关注图中液晶屏LCD_CS及LCD_RS(即DC引脚)与FSMC存储区选择引脚FSMC_NE及地址信号FSMC_A的编号, 它们会决定STM32要使用什么内存地址来控制与液晶屏的通讯。
2. 软件设计
2.1 编程大纲
-
LCD显示驱动
-
XPT2046初始化
-
配置XPT2046
-
画板配置
-
主函数测试
2.2 代码分析
2.2.1 触摸屏硬件相关宏定义
根据触摸屏与STM32芯片的硬件连接,我们把触摸屏硬件相关的配置都以宏的形式定义到 “bsp_xpt2046_lcd.h”文件中
/* 中断引脚 */
#define XPT2046_PENIRQ_GPIO_CLK RCC_APB2Periph_GPIOE
#define XPT2046_PENIRQ_GPIO_PORT GPIOE
#define XPT2046_PENIRQ_GPIO_PIN GPIO_Pin_4
/* 电平触发 */
#define XPT2046_PENIRQ_ActiveLevel 0
/* 读取触摸屏中断引脚 */
#define XPT2046_PENIRQ_Read() GPIO_ReadInputDataBit(XPT2046_PENIRQ_GPIO_PORT, XPT2046_PENIRQ_GPIO_PIN)
/* SPI 接口 */
#define XPT2046_SPI_GPIO_CLK (RCC_APB2Periph_GPIOE| RCC_APB2Periph_GPIOD)
#define XPT2046_SPI_CS_PIN GPIO_Pin_13
#define XPT2046_SPI_CS_PORT GPIOD
#define XPT2046_SPI_CLK_PIN GPIO_Pin_0
#define XPT2046_SPI_CLK_PORT GPIOE
#define XPT2046_SPI_MOSI_PIN GPIO_Pin_2
#define XPT2046_SPI_MOSI_PORT GPIOE
#define XPT2046_SPI_MISO_PIN GPIO_Pin_3
#define XPT2046_SPI_MISO_PORT GPIOE
/* CS引脚控制 */
#define XPT2046_CS_ENABLE() GPIO_SetBits(XPT2046_SPI_CS_PORT, XPT2046_SPI_CS_PIN)
#define XPT2046_CS_DISABLE() GPIO_ResetBits(XPT2046_SPI_CS_PORT, XPT2046_SPI_CS_PIN)
/* 时钟引脚控制 */
#define XPT2046_CLK_HIGH() GPIO_SetBits(XPT2046_SPI_CLK_PORT, XPT2046_SPI_CLK_PIN)
#define XPT2046_CLK_LOW() GPIO_ResetBits(XPT2046_SPI_CLK_PORT, XPT2046_SPI_CLK_PIN)
/* 读写数据引脚控制 */
#define XPT2046_MOSI_1() GPIO_SetBits(XPT2046_SPI_MOSI_PORT, XPT2046_SPI_MOSI_PIN)
#define XPT2046_MOSI_0() GPIO_ResetBits(XPT2046_SPI_MOSI_PORT, XPT2046_SPI_MOSI_PIN)
#define XPT2046_MISO() GPIO_ReadInputDataBit(XPT2046_SPI_MISO_PORT, XPT2046_SPI_MISO_PIN)
以上代码根据硬件的连接,把STM32与触摸屏通讯使用的引脚号和控制CS/CLK/MOSI引脚输出高低电平的操作、读取MISO引脚电平状态的操作都使用宏封装了起来, 以便后面制作模拟SPI时序的驱动。另外,本驱动中XPT2046的PENIRQ触摸信号并没有使用中断检测,而是使用普通的引脚电平轮询获取状态的。
2.2.2 初始化触摸屏控制引脚
利用上面的宏,编写触摸屏控制引脚的初始化函数
void XPT2046_Init(void) // 初始化XPT2046
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(XPT2046_SPI_GPIO_CLK|XPT2046_PENIRQ_GPIO_CLK, ENABLE);
/* 模拟SPI GPIO初始化 */
GPIO_InitStructure.GPIO_Pin = XPT2046_SPI_CLK_PIN; // 时钟引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz ;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(XPT2046_SPI_CLK_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = XPT2046_SPI_MOSI_PIN; // 串行数据输入引脚
GPIO_Init(XPT2046_SPI_MOSI_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = XPT2046_SPI_MISO_PIN; // 串行数据输出引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz ;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(XPT2046_SPI_MISO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = XPT2046_SPI_CS_PIN; // 片选引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz ;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(XPT2046_SPI_CS_PORT, &GPIO_InitStructure);
/* 拉低片选,选择XPT2046 */
XPT2046_CS_DISABLE();
// 触摸屏触摸信号指示引脚,不使用中断
GPIO_InitStructure.GPIO_Pin = XPT2046_PENIRQ_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(XPT2046_PENIRQ_GPIO_PORT, &GPIO_InitStructure);
}
以上函数直接初始化了触摸屏用到的SPI信号线,由于使用软件模拟SPI协议的方式,所以它把MISO设置为输入,其余的MOSI、CLK、CS引脚均配置为普通的推挽输出模式, 而PENIRQ作为触摸信号的输入检测,它也被设置成输入模式。
2.2.3 模拟SPI协议的读写时序
初始化完引脚,即可编写SPI协议的模拟时序
// 向XPT2046发送命令,函数参数:0x90 :通道Y+的选择控制字,0xd0 :通道X+的选择控制字
static void XPT2046_WriteCMD(uint8_t ucCmd)
{
uint8_t i;
XPT2046_MOSI_0(); // 拉低MOSI
XPT2046_CLK_LOW(); // 拉低时钟
for(i = 0; i < 8; i ++) // 发送命令
{
((ucCmd >> (7 - i)) & 0x01 ) ? XPT2046_MOSI_1() : XPT2046_MOSI_0();
XPT2046_DelayUS(5);
XPT2046_CLK_HIGH();
XPT2046_DelayUS(5);
XPT2046_CLK_LOW();
}
}
// 读取XPT2046的命令
static uint16_t XPT2046_ReadCMD ( void )
{
uint8_t i;
uint16_t usBuf=0, usTemp;
XPT2046_MOSI_0();
XPT2046_CLK_HIGH();
for(i = 0; i < 12; i++)
{
XPT2046_CLK_LOW();
usTemp = XPT2046_MISO();
usBuf |= usTemp << ( 11 - i );
XPT2046_CLK_HIGH();
}
return usBuf;
}
SPI协议的读写时序都比较简单,只要驱动好一个时钟信号传输一个数据位即可,发送数据时使用MOSI引脚输出电平,读取数据时从MISO引脚获取状态。
代码中的XPT2046_WriteCMD函数主要在后面用于发送控制触摸芯片的命令代码,发送不同的命令可以控制触摸芯片检测X坐标或Y坐标的触摸信号, 该命令代码一般为8个数据位;而XPT2046_ReadCMD函数主要在后面用于读取触摸芯片输出的ADC电压值,这些ADC电压值一般为12个数据位。
2.2.4 采集触摸原始数据
利用XPT2046_WriteCMD及XPT2046_ReadCMD函数,可控制触摸屏检测并获取触摸的原始ADC数据
#define XPT2046_CHANNEL_X 0x90 // 通道Y+的选择控制字
#define XPT2046_CHANNEL_Y 0xd0 // 通道X+的选择控制字
/*
对 XPT2046 选择一个模拟通道后,启动ADC,并返回ADC采样结果
函数参数:ucChannel
该参数为以下值之一:
0x90 :通道Y+的选择控制字
0xd0 :通道X+的选择控制字
返回结果为该通道的ADC采样结果
*/
static uint16_t XPT2046_ReadAdc(uint8_t ucChannel)
{
XPT2046_WriteCMD(ucChannel);
return XPT2046_ReadCMD ();
}
/*
读取 XPT2046 的X通道和Y通道的AD值(12 bit,最大是4096)
sX_Ad :存放X通道AD值的地址
sY_Ad :存放Y通道AD值的地址
*/
static void XPT2046_ReadAdc_XY(int16_t *sX_Ad, int16_t *sY_Ad)
{
int16_t sX_Ad_Temp, sY_Ad_Temp;
sX_Ad_Temp = XPT2046_ReadAdc(XPT2046_CHANNEL_X);
XPT2046_DelayUS(1);
sY_Ad_Temp = XPT2046_ReadAdc(XPT2046_CHANNEL_Y);
*sX_Ad = sX_Ad_Temp;
*sY_Ad = sY_Ad_Temp;
}
根据触摸芯片的要求,发送命令代码XPT2046_CHANNEL_X(0x90)后,电阻屏的X方向会通电,然后触摸屏使用Y通道检测得电压,获取到触摸点X方向的ADC原始值; 发送命令代码XPT2046_CHANNEL_Y(0xd0)后,电阻屏的Y方向会通电,然后触摸屏使用X通道检测得电压,获取到触摸点Y方向的ADC原始值。把该过程封装起来, 即可得到XPT2046_ReadAdc及XPT2046_ReadAdc_XY函数,实际应用中通常直接调用XPT2046_ReadAdc_XY函数以检测两个方向的触摸数据。
2.2.5 多次采用求平均值
为了使得采样更精确,工程中使用函数来采集最终使用的数据。
typedef struct { // 液晶坐标结构体
// 负数值表示无新数据
int16_t x; // 记录最新的触摸参数值
int16_t y;
// 用于记录连续触摸时(长按)的上一次触摸位置
int16_t pre_x;
int16_t pre_y;
} strType_XPT2046_Coordinate;
/*
触摸 XPT2046 屏幕时获取一组坐标的AD值,并对该坐标进行滤波
返回滤波之后的坐标AD值
*/
static uint8_t XPT2046_ReadAdc_Smooth_XY(strType_XPT2046_Coordinate *pScreenCoordinate)
{
uint8_t ucCount = 0, i;
int16_t sAD_X, sAD_Y;
// 坐标X和Y进行多次采样
int16_t sBufferArray[2][10] = {{0},{0}};
// 存储采样中的最小值、最大值
int32_t lX_Min, lX_Max, lY_Min, lY_Max;
// 循环采样10次
do {
XPT2046_ReadAdc_XY(&sAD_X, &sAD_Y);
sBufferArray[0][ucCount] = sAD_X;
sBufferArray[1][ucCount] = sAD_Y;
ucCount ++;
} while((XPT2046_PENIRQ_Read() == XPT2046_PENIRQ_ActiveLevel ) && (ucCount < 10));
// 用户点击触摸屏时即TP_INT_IN信号为低 并且 ucCount<10
// 如果触笔弹起
if(XPT2046_PENIRQ_Read() != XPT2046_PENIRQ_ActiveLevel)
ucXPT2046_TouchFlag = 0; // 中断标志复位
// 如果成功采样10个样本
if(ucCount ==10)
{
lX_Max = lX_Min = sBufferArray[0][0];
lY_Max = lY_Min = sBufferArray[1][0];
for(i = 1; i < 10; i++)
{
if(sBufferArray[0][i] < lX_Min)
lX_Min = sBufferArray[0][i];
else if(sBufferArray[0][i] > lX_Max)
lX_Max = sBufferArray[0][i];
}
for(i = 1; i < 10; i++)
{
if(sBufferArray[1][i] < lY_Min)
lY_Min = sBufferArray[1][i];
else if(sBufferArray[1][i] > lY_Max)
lY_Max = sBufferArray[1][i];
}
// 去除最小值和最大值之后求平均值
pScreenCoordinate ->x = (sBufferArray [ 0 ] [ 0 ] +
sBufferArray [ 0 ] [ 1 ] +
sBufferArray [ 0 ] [ 2 ] +
sBufferArray [ 0 ] [ 3 ] +
sBufferArray [ 0 ] [ 4 ] +
sBufferArray [ 0 ] [ 5 ] +
sBufferArray [ 0 ] [ 6 ] +
sBufferArray [ 0 ] [ 7 ] +
sBufferArray [ 0 ] [ 8 ] +
sBufferArray [ 0 ] [ 9 ] - lX_Min-lX_Max ) >> 3;
pScreenCoordinate ->y = ( sBufferArray [ 1 ] [ 0 ] +
sBufferArray [ 1 ] [ 1 ] +
sBufferArray [ 1 ] [ 2 ] +
sBufferArray [ 1 ] [ 3 ] +
sBufferArray [ 1 ] [ 4 ] +
sBufferArray [ 1 ] [ 5 ] +
sBufferArray [ 1 ] [ 6 ] +
sBufferArray [ 1 ] [ 7 ] +
sBufferArray [ 1 ] [ 8 ] +
sBufferArray [ 1 ] [ 9 ] - lY_Min-lY_Max ) >> 3;
return 1;
}
return 0;
}
本函数有一个输入参数strType_XPT2046_Coordinate类型的结构体,它主要包含x/y/pre_x/pre_y四个结构体成员, 其中x/y是用来存储最新的触摸参数值的,而pre_x/pre_y用于存储上一次的触摸点。本函数中仅使用了x/y结构体成员值, 且使用它存储的是触摸屏的原始触摸数据,即ADC值。
代码中对X、Y坐标各采样10次,然后去除极大极小值后再取平均,计算结果即存储在结构体中的x/y成员值中。
2.2.6 根据原始数据计算坐标值
typedef struct { // 校准系数结构体(最终使用)
float dX_X,
dX_Y,
dX,
dY_X,
dY_Y,
dY;
} strType_XPT2046_TouchPara;
// 默认触摸参数,不同的屏幕稍有差异,可重新调用触摸校准函数获取
strType_XPT2046_TouchPara strXPT2046_TouchPara[] = {
-0.006464, -0.073259, 280.358032, 0.074878, 0.002052, -6.545977,//扫描方式0
0.086314, 0.001891, -12.836658, -0.003722, -0.065799, 254.715714,//扫描方式1
0.002782, 0.061522, -11.595689, 0.083393, 0.005159, -15.650089,//扫描方式2
0.089743, -0.000289, -20.612209, -0.001374, 0.064451, -16.054003,//扫描方式3
0.000767, -0.068258, 250.891769, -0.085559, -0.000195, 334.747650,//扫描方式4
-0.084744, 0.000047, 323.163147, -0.002109, -0.066371, 260.985809,//扫描方式5
-0.001848, 0.066984, -12.807136, -0.084858, -0.000805, 333.395386,//扫描方式6
-0.085470, -0.000876, 334.023163, -0.003390, 0.064725, -6.211169,//扫描方式7
};
// 液晶屏扫描模式,本变量主要用于方便选择触摸屏的计算参数
// 参数可选值为0-7
// 调用ILI9341_GramScan函数设置方向时会自动更改
// LCD刚初始化完成时会使用本默认值
uint8_t LCD_SCAN_MODE = 6;
/*
获取 XPT2046 触摸点(校准后)的坐标
pDisplayCoordinate :该指针存放获取到的触摸点坐标
pTouchPara:坐标校准系数
获取情况
该返回值为以下值之一:
1 :获取成功
0 :获取失败
*/
uint8_t XPT2046_Get_TouchedPoint ( strType_XPT2046_Coordinate *
pDisplayCoordinate, strType_XPT2046_TouchPara * pTouchPara )
{
uint8_t ucRet = 1; //若正常,则返回0
strType_XPT2046_Coordinate strScreenCoordinate;
if ( XPT2046_ReadAdc_Smooth_XY ( & strScreenCoordinate ) ) {
pDisplayCoordinate ->x = ( ( pTouchPara[LCD_SCAN_MODE].dX_X *
strScreenCoordinate.x ) +
( pTouchPara[LCD_SCAN_MODE].dX_Y * strScreenCoordinate.y )
pTouchPara[LCD_SCAN_MODE].dX );
pDisplayCoordinate ->y = ( ( pTouchPara[LCD_SCAN_MODE].dY_X *
strScreenCoordinate.x ) +
( pTouchPara[LCD_SCAN_MODE].dY_Y * strScreenCoordinate.y ) +
pTouchPara[LCD_SCAN_MODE].dY );
} else ucRet = 0; //如果获取的触点信息有误,则返回0
return ucRet;
}
在实际应用中,并不会使用前面介绍触摸原理时讲解的直接按比例运算把 触摸原始数据物理坐标转换成与液晶屏像素对应的XY逻辑坐标(如触摸屏输出的原始数据范围为0-2045, 液晶屏的像素XY坐标为0-239及0-319),那种直接转换的方式误差比较大,所以通常会采用“多点触摸校正法”来转换坐标, 使用这种方式时,在应用前需要校正屏幕。校正时,使用液晶屏在特定的位置显示几个点要求用户点击, 根据触摸校准算法的数学关系把逻辑坐标与物理坐标转换公式的各个系数计算出来。
这些触摸转换系数,在我们上述代码中使用strType_XPT2046_TouchPara类型来存储,一共有6个系数。利用这个数据类型, 代码中定义了一个数组strXPT2046_TouchPara,它存储了液晶屏在8个扫描方向时使用的转换系数,这些系数是我编写代码时使用某个液晶屏测试出来的, 作为默认转换系数,不同的液晶屏这些转换系数稍有差异,若在实际使用中你感觉触摸不准确, 可以使用校准函数XPT2046_Touch_Calibrate来重新计算自己屏幕的转换系数。
而本代码中列出的XPT2046_Get_TouchedPoint本函数可利用两用的转换系数计算出当前的触摸逻辑坐标。它有两个输入参数, 一个参数pDisplayCoordinate用于存储计算后得到的触摸逻辑坐标,作为计算输出,这坐标与液晶屏对应;而参数pTouchPara即为校准系数, 作为计算输入。在函数的内部,它先调用XPT2046_ReadAdc_Smooth_XY 检测触摸点的原始数据物理坐标,然后代入公式中计算输出逻辑坐标。
2.2.7 触摸校正
触摸校正函数XPT2046_Touch_Calibrate的代码涉及到的都是数学函数映射关系的运算,比较复杂,此处作原理讲解,在工程应用需要校正时,可采用下面代码中的Calibrate_or_Get_TouchParaWithFlash函数。
// 触摸参数写到FLASH里的标志
#define FLASH_TOUCH_PARA_FLAG_VALUE 0xA5
// 触摸标志写到FLASH里的地址
#define FLASH_TOUCH_PARA_FLAG_ADDR (1*1024)
// 触摸参数写到FLASH里的地址
#define FLASH_TOUCH_PARA_ADDR (2*1024)
/*
从FLASH中获取 或 重新校正触摸参数(校正后会写入到SPI FLASH中)
若FLASH中从未写入过触摸参数,
会触发校正程序校正LCD_Mode指定模式的触摸参数,此时其它模式写入默认值
若FLASH中已有触摸参数,且不强制重新校正
会直接使用FLASH里的触摸参数值
每次校正时只会更新指定的LCD_Mode模式的触摸参数,其它模式的不变
本函数调用后会把液晶模式设置为LCD_Mode
LCD_Mode:要校正触摸参数的液晶模式
forceCal:是否强制重新校正参数,可以为以下值:
1:强制重新校正
0:只有当FLASH中不存在触摸参数标志时才重新校正
*/
void Calibrate_or_Get_TouchParaWithFlash(uint8_t LCD_Mode,
uint8_t forceCal)
{
uint8_t para_flag = 0;
// 初始化FLASH
SPI_FLASH_Init();
// 读取触摸参数标志
SPI_FLASH_BufferRead(¶_flag,FLASH_TOUCH_PARA_FLAG_ADDR,1);
//若不存在标志或florceCal=1时,重新校正参数
if (para_flag != FLASH_TOUCH_PARA_FLAG_VALUE | forceCal ==1) {
//若标志存在,说明原本FLASH内有触摸参数,
//先读回所有LCD模式的参数值,以便稍后强制更新时只更新指定LCD模式的参数,其它模式的不变
if ( para_flag == FLASH_TOUCH_PARA_FLAG_VALUE && forceCal == 1) {
SPI_FLASH_BufferRead((uint8_t *)&strXPT2046_TouchPara,FLASH_TOUCH_PARA_ADDR,4*6*8);
}
//等待触摸屏校正完毕,更新指定LCD模式的触摸参数值
while ( ! XPT2046_Touch_Calibrate (LCD_Mode) );
//擦除扇区
SPI_FLASH_SectorErase(0);
//设置触摸参数标志
para_flag = FLASH_TOUCH_PARA_FLAG_VALUE;
//写入触摸参数标志
SPI_FLASH_BufferWrite(¶_flag,FLASH_TOUCH_PARA_FLAG_ADDR,1);
//写入最新的触摸参数
SPI_FLASH_BufferWrite((uint8_t*)&strXPT2046_TouchPara,FLASH_TOUCH_PARA_ADDR,4*6*8);
} else { //若标志存在且不强制校正,则直接从FLASH中读取
SPI_FLASH_BufferRead((uint8_t *)&strXPT2046_TouchPara,FLASH_TOUCH_PARA_ADDR,4*6*8);
}
}
本函数实际上对触摸校正函数XPT2046_Touch_Calibrate做了封装,加入了把转换系数存储在外部SPI FLASH的功能, 以便下次板子重新上电也能使用上一次的校正得到的系数。在调用时,使用LCD_Mode参数选择要校正的液晶扫描模式, 使用forceCal选择是否要强制校正;该函数调用后会触发XPT2046_Touch_Calibrate的校正函数,在屏幕上显示几个触点提示用户点击, 若校正成功,则会把转换系数写入到外部的SPI FLASH空间中并加入记录标志,下次再调用本函数的时候,若不是使用forceCal设置成强制校正是不会触发重新校正的过程的。
2.2.8 触摸检测状态机
前面介绍的函数都是获取坐标相关的,然而那些函数并不需要长期调用,只有当检测到触摸信号的时候,再去检测坐标即可。
/******触摸状态机相关******/
// 触屏信号有效电平
#define XPT2046_PENIRQ_ActiveLevel 0
#define XPT2046_PENIRQ_Read() GPIO_ReadInputDataBit(XPT2046_PENIRQ_GPIO_PORT, XPT2046_PENIRQ_GPIO_PIN)
typedef enum
{
XPT2046_STATE_RELEASE = 0, //触摸释放
XPT2046_STATE_WAITING, //触摸按下
XPT2046_STATE_PRESSED, //触摸按下
} enumTouchState;
#define TOUCH_PRESSED 1
#define TOUCH_NOT_PRESSED 0
// 触摸消抖阈值
#define DURIATION_TIME 2
/*
触摸屏检测状态机
触摸状态
该返回值为以下值之一:
TOUCH_PRESSED :触摸按下
TOUCH_NOT_PRESSED :无触摸
*/
uint8_t XPT2046_TouchDetect(void)
{
static enumTouchState touch_state = XPT2046_STATE_RELEASE;
static uint32_t i;
uint8_t detectResult = TOUCH_NOT_PRESSED;
switch (touch_state) {
case XPT2046_STATE_RELEASE:
if (XPT2046_PENIRQ_Read() == XPT2046_PENIRQ_ActiveLevel) {
//第一次出现触摸信号
touch_state = XPT2046_STATE_WAITING;
detectResult =TOUCH_NOT_PRESSED;
} else { //无触摸
touch_state = XPT2046_STATE_RELEASE;
detectResult =TOUCH_NOT_PRESSED;
}
break;
case XPT2046_STATE_WAITING:
if (XPT2046_PENIRQ_Read() == XPT2046_PENIRQ_ActiveLevel) {
i++;
//等待时间大于阈值则认为触摸被按下
//消抖时间 = DURIATION_TIME * 本函数被调用的时间间隔
//如在定时器中调用,每10ms调用一次,则消抖时间为:DURIATION_TIME*10ms
if (i > DURIATION_TIME) {
i=0;
touch_state = XPT2046_STATE_PRESSED;
detectResult = TOUCH_PRESSED;
} else { //等待时间累加
touch_state = XPT2046_STATE_WAITING;
detectResult = TOUCH_NOT_PRESSED;
}
} else { //等待时间值未达到阈值就为无效电平,当成抖动处理
i = 0;
touch_state = XPT2046_STATE_RELEASE;
detectResult = TOUCH_NOT_PRESSED;
}
break;
case XPT2046_STATE_PRESSED:
if (XPT2046_PENIRQ_Read() == XPT2046_PENIRQ_ActiveLevel) {
//触摸持续按下
touch_state = XPT2046_STATE_PRESSED;
detectResult = TOUCH_PRESSED;
} else { //触摸释放
touch_state = XPT2046_STATE_RELEASE;
detectResult = TOUCH_NOT_PRESSED;
}
break;
default:
touch_state = XPT2046_STATE_RELEASE;
detectResult = TOUCH_NOT_PRESSED;
break;
}
return detectResult;
}
当触摸屏有触点按下时,PENIRQ引脚会输出低电平,直到没有触摸的时候,它才会输出高电平;而且STM32的中断只支持边沿触发(上升沿或下降沿), 不支持电平触发,在触摸屏上存在类似机械按键的信号抖动,所以如果使用中断的方式来检测触摸状态并不适合,难以辨别触摸按下及释放的情况。
状态机编程是一种非常高效的编程方式,它非常适合应用在涉及状态转换的过程控制中,上述代码采用状态机的编程方式对触摸状态进行检测, 主要涉及触摸的按下、消抖及释放这三种状态转换。在应用时,本函数需要在循环体里调用,或定时调用(如每隔10ms调用一次), 其状态转换关系见图
在代码中,通过使用XPT2046_PENIRQ_Read函数获取当前PENIRQ引脚的电平,再根据当前的状态决定是否转换进入下一个状态, 若经过消抖处理后进入“触摸确认按下/持续按下(XPT2046_STATE_PRESSED)”状态时,函数会返回TOUCH_PRESSED表示触摸被按下, 其余状态返回TOUCH_NOT_PRESSED表示触摸无按下或释放状态。代码中的触摸消抖等待状态中,实质是通过延时、 多次检测PENIRQ引脚的电平达到消抖的目的,若XPT2046_TouchDetect函数每隔10ms被调用一次, 那么消抖的延时值则为DURIATION_TIME*10毫秒,可以根据实际情况适当调整该消抖阈值。
2.2.9 触摸坐标获取及处理
XPT2046_TouchDetect函数只是检测了触摸是否被按下的状态,当触摸被按下时, 还要调用前面介绍的XPT2046_Get_TouchedPoint函数获取触摸点的坐标,然后再处理,为便于使用, 我们把这方面的操作封装到下面代码中的XPT2046_TouchEvenHandler函数中。
typedef struct { //液晶坐标结构体
// 负数值表示无新数据
int16_t x; // 记录最新的触摸参数值
int16_t y;
// 用于记录连续触摸时(长按)的上一次触摸位置
int16_t pre_x;
int16_t pre_y;
}strType_XPT2046_Coordinate;
/*
检测到触摸中断时调用的处理函数,通过它调用tp_down 和tp_up汇报触摸点
本函数需要在while循环里被调用,也可使用定时器定时调用
例如,可以每隔5ms调用一次,消抖阈值宏DURIATION_TIME可设置为2,这样每秒最多可以检测100个点。
可在XPT2046_TouchDown及XPT2046_TouchUp函数中编写自己的触摸应用
*/
void XPT2046_TouchEvenHandler(void )
{
static strType_XPT2046_Coordinate cinfo= {-1,-1,-1,-1};
if (XPT2046_TouchDetect() == TOUCH_PRESSED) {
LED_GREEN;
//获取触摸坐标
XPT2046_Get_TouchedPoint(&cinfo,strXPT2046_TouchPara);
//输出调试信息到串口
XPT2046_DEBUG("x=%d,y=%d",cinfo.x,cinfo.y);
//调用触摸被按下时的处理函数,可在该函数编写自己的触摸按下处理过程
XPT2046_TouchDown(&cinfo);
// 更新触摸信息到pre xy
cinfo.pre_x = cinfo.x;
cinfo.pre_y = cinfo.y;
}
else
{
LED_BLUE;
// 调用触摸被释放时的处理函数,可在该函数编写自己的触摸释放处理过程
XPT2046_TouchUp(&cinfo);
// 触笔释放,把 xy 重置为负
cinfo.x = -1;
cinfo.y = -1;
cinfo.pre_x = -1;
cinfo.pre_y = -1;
}
}
/*
触摸屏被按下的时候会调用本函数
touch包含触摸坐标的结构体
请在本函数中编写自己的触摸按下处理应用
*/
void XPT2046_TouchDown(strType_XPT2046_Coordinate * touch)
{
// 若为负值表示之前已处理过
if (touch->pre_x == -1 && touch->pre_x == -1)
return;
/***在此处编写自己的触摸按下处理应用***/
/*处理触摸画板的选择按钮*/
Touch_Button_Down(touch->x,touch->y);
/*处理描绘轨迹*/
Draw_Trail(touch->pre_x,touch->pre_y,touch->x,touch->y,&brush);
/***在上面编写自己的触摸按下处理应用***/
}
/*
触摸屏释放的时候会调用本函数
touch包含触摸坐标的结构体
*/
void XPT2046_TouchUp(strType_XPT2046_Coordinate * touch)
{
//若为负值表示之前已处理过
if (touch->pre_x == -1 && touch->pre_x == -1)
return;
/***在此处编写自己的触摸释放处理应用***/
/*处理触摸画板的选择按钮*/
Touch_Button_Up(touch->pre_x,touch->pre_y);
/***在上面编写自己的触摸释放处理应用***/
}
2.2.10 main函数
完成了触摸屏的驱动,就可以应用了,以下我们来看工程的主体main函数
int main(void)
{
// LCD 初始化
ILI9341_Init();
// 触摸屏初始化
XPT2046_Init();
// 从FLASH里获取校正参数,若FLASH无参数,则使用模式3进行校正
Calibrate_or_Get_TouchParaWithFlash(3,0);
// USART config
USART_Config();
LED_GPIO_Config();
printf("\r\n ********** 触摸画板程序 *********** \r\n");
printf("\r\n 若汉字显示不正常,请阅读工程中的readme.txt文件说明,根据要求给FLASH重刷字模数据\r\n");
//其中0、3、5、6 模式适合从左至右显示文字,
//不推荐使用其它模式显示文字 其它模式显示文字会有镜像效果
//其中 6 模式为大部分液晶例程的默认显示方向
ILI9341_GramScan(3);
// 绘制触摸画板界面
Palette_Init(LCD_SCAN_MODE);
while(1)
{
//触摸检测函数,本函数至少10ms调用一次
XPT2046_TouchEvenHandler();
}
}
main函数中使用XPT2046_Init初始化触摸屏相关的引脚,然后调用Calibrate_or_Get_TouchParaWithFlash进行触摸校正; 关于触摸画板应用程序的初始化,都包含在Palette_Init函数中,它会绘制触摸画板的按钮和白板界面; 在main函数的while循环里调用了XPT2046_TouchEvenHandler函数,以实现状态机检测和对触摸进行处理,当有触摸按下和释放时, 都通过其内部调用的XPT2046_TouchDown和XPT2046_TouchUp函数完成画板相关的操作。
2024.9.29 第一次修订,后期不再维护
2025.2.11 简化代码
本文作者:hazy1k
本文链接:https://www.cnblogs.com/hazy1k/p/18440768
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步