ILI9341触摸驱动实现
前言
本章更新TFT-LCD触摸屏的触摸驱动。
显示驱动可跳转至STM32F4驱动ILI9341的TFT-LCD触摸屏(HAL库)(一),本章在此基础上完成触摸功能
实验硬件:STM32F407ZGT6;2.4寸TFT-LCD模块(ILI9341驱动芯片、XTP2046触摸芯片)
最终目标:使用STMF407ZGT6驱动2.4寸TFT-LCD触摸屏,并移植LVGL库
本节目标:ILI9341显示芯片驱动实现
软件:STM32CubeMX;Keil
完整项目在文章末尾github链接中
一、XPT2046介绍
XPT2046 是一款 4 线制电阻式触摸屏控制器,内含 12 位分辨率 125KHz 转换速率逐步逼近型 A/D 转换器。XPT2046 支持从 1.5V 到 5.25V 的低电压 I/O 接口。XPT2046 能通过执行两次 A/D 转换查出被按的屏幕位置,除此之外, 还可以测量加在触摸屏上的压力。内部自带 2.5V 参考电压,可以作为辅助输入、 温度测量和电池监测之用,电池监测的电压范围可以从 0V 到 6V。
二、CubeMX配置
1、GPIO配置,部分配置已于STM32F4驱动ILI9341的TFT-LCD触摸屏(HAL库)(一)配置完成
2、串口配置,由于LCD显示驱动未编写显示字符函数,本文采用串口进行调试,也可不进行配置
三、XPT2046触摸驱动实现
本文只展示部分关键代码,完整程序请至文末github链接自行获取。
- 24ccxx.h
AT24CXX主要是保存电阻式触摸屏校准的参数数据,利用模拟I2C通讯。
#include "24cxx.h"
/**
* @brief 初始化AT24CXX
* @param None
* @retval None
*/
void AT24CXX_Init(void)
{
IIC_Init();
}
/**
* @brief 在AT24CXX指定地址读出一个数据
* @param ReadAddr: 开始读数的地址
* @retval 读到的数据
*/
uint8_t AT24CXX_ReadOneByte(uint16_t ReadAddr)
{
uint8_t temp = 0;
IIC_Start();
if(EE_TYPE > AT24C16)
{
IIC_Send_Byte(AT24CXX_ADDR); // 发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr >> 8); // 发送高地址
IIC_Wait_Ack();
}
else
{
IIC_Send_Byte(AT24CXX_ADDR + ((ReadAddr/256) << 1)); // 发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr % 256); // 发送低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(AT24CXX_ADDR | 0x01); // 进入接收模式
IIC_Wait_Ack();
temp = IIC_Read_Byte(0);
IIC_Stop();
return temp;
}
/**
* @brief 在AT24CXX指定地址写入一个数据
* @param WriteAddr: 写入数据的目的地址
* @param DataToWrite: 要写入的数据
* @retval None
*/
void AT24CXX_WriteOneByte(uint16_t WriteAddr, uint8_t DataToWrite)
{
IIC_Start();
if(EE_TYPE > AT24C16)
{
IIC_Send_Byte(AT24CXX_ADDR); // 发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr >> 8); // 发送高地址
IIC_Wait_Ack();
}
else
{
IIC_Send_Byte(AT24CXX_ADDR + ((WriteAddr/256) << 1)); // 发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr % 256); // 发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); // 发送字节
IIC_Wait_Ack();
IIC_Stop();
HAL_Delay(10); // 注意:这里要等待写入完成
}
/**
* @brief 在AT24CXX里面的指定地址开始写入长度为Len的数据
* @param WriteAddr: 开始写入的地址
* @param DataToWrite: 数据数组首地址
* @param Len: 要写入数据的长度2,4
* @retval None
*/
void AT24CXX_WriteLenByte(uint16_t WriteAddr, uint32_t DataToWrite, uint8_t Len)
{
for(uint8_t t = 0; t < Len; t++)
{
AT24CXX_WriteOneByte(WriteAddr + t, (DataToWrite >> (8*t)) & 0xFF);
}
}
/**
* @brief 在AT24CXX里面的指定地址开始读出长度为Len的数据
* @param ReadAddr: 开始读出的地址
* @param Len: 要读出数据的长度2,4
* @retval 读取到的数据
*/
uint32_t AT24CXX_ReadLenByte(uint16_t ReadAddr, uint8_t Len)
{
uint32_t temp = 0;
for(uint8_t t = 0; t < Len; t++)
{
temp <<= 8;
temp += AT24CXX_ReadOneByte(ReadAddr + Len - t - 1);
}
return temp;
}
/**
* @brief 检查AT24CXX是否正常
* @param None
* @retval 0: 检测成功
* 1: 检测失败
*/
uint8_t AT24CXX_Check(void)
{
uint8_t temp;
temp = AT24CXX_ReadOneByte(255); // 避免每次开机都写AT24CXX
if(temp == 0x55)
return 0;
else // 排除第一次初始化的情况
{
AT24CXX_WriteOneByte(255, 0x55);
temp = AT24CXX_ReadOneByte(255);
if(temp == 0x55)
return 0;
}
return 1;
}
/**
* @brief 在AT24CXX里面的指定地址开始读出指定个数的数据
* @param ReadAddr: 开始读出的地址
* @param pBuffer: 数据数组首地址
* @param NumToRead: 要读出数据的个数
* @retval None
*/
void AT24CXX_Read(uint16_t ReadAddr, uint8_t *pBuffer, uint16_t NumToRead)
{
while(NumToRead)
{
*pBuffer++ = AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
/**
* @brief 在AT24CXX里面的指定地址开始写入指定个数的数据
* @param WriteAddr: 开始写入的地址
* @param pBuffer: 数据数组首地址
* @param NumToWrite: 要写入数据的个数
* @retval None
*/
void AT24CXX_Write(uint16_t WriteAddr, uint8_t *pBuffer, uint16_t NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr, *pBuffer);
WriteAddr++;
pBuffer++;
}
}
- 24ccxx.c
#include "24cxx.h"
/**
* @brief 初始化AT24CXX
* @param None
* @retval None
*/
void AT24CXX_Init(void)
{
IIC_Init();
}
/**
* @brief 在AT24CXX指定地址读出一个数据
* @param ReadAddr: 开始读数的地址
* @retval 读到的数据
*/
uint8_t AT24CXX_ReadOneByte(uint16_t ReadAddr)
{
uint8_t temp = 0;
IIC_Start();
if(EE_TYPE > AT24C16)
{
IIC_Send_Byte(AT24CXX_ADDR); // 发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr >> 8); // 发送高地址
IIC_Wait_Ack();
}
else
{
IIC_Send_Byte(AT24CXX_ADDR + ((ReadAddr/256) << 1)); // 发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr % 256); // 发送低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(AT24CXX_ADDR | 0x01); // 进入接收模式
IIC_Wait_Ack();
temp = IIC_Read_Byte(0);
IIC_Stop();
return temp;
}
/**
* @brief 在AT24CXX指定地址写入一个数据
* @param WriteAddr: 写入数据的目的地址
* @param DataToWrite: 要写入的数据
* @retval None
*/
void AT24CXX_WriteOneByte(uint16_t WriteAddr, uint8_t DataToWrite)
{
IIC_Start();
if(EE_TYPE > AT24C16)
{
IIC_Send_Byte(AT24CXX_ADDR); // 发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr >> 8); // 发送高地址
IIC_Wait_Ack();
}
else
{
IIC_Send_Byte(AT24CXX_ADDR + ((WriteAddr/256) << 1)); // 发送器件地址0XA0,写数据
}
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr % 256); // 发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); // 发送字节
IIC_Wait_Ack();
IIC_Stop();
HAL_Delay(10); // 注意:这里要等待写入完成
}
/**
* @brief 在AT24CXX里面的指定地址开始写入长度为Len的数据
* @param WriteAddr: 开始写入的地址
* @param DataToWrite: 数据数组首地址
* @param Len: 要写入数据的长度2,4
* @retval None
*/
void AT24CXX_WriteLenByte(uint16_t WriteAddr, uint32_t DataToWrite, uint8_t Len)
{
for(uint8_t t = 0; t < Len; t++)
{
AT24CXX_WriteOneByte(WriteAddr + t, (DataToWrite >> (8*t)) & 0xFF);
}
}
/**
* @brief 在AT24CXX里面的指定地址开始读出长度为Len的数据
* @param ReadAddr: 开始读出的地址
* @param Len: 要读出数据的长度2,4
* @retval 读取到的数据
*/
uint32_t AT24CXX_ReadLenByte(uint16_t ReadAddr, uint8_t Len)
{
uint32_t temp = 0;
for(uint8_t t = 0; t < Len; t++)
{
temp <<= 8;
temp += AT24CXX_ReadOneByte(ReadAddr + Len - t - 1);
}
return temp;
}
/**
* @brief 检查AT24CXX是否正常
* @param None
* @retval 0: 检测成功
* 1: 检测失败
*/
uint8_t AT24CXX_Check(void)
{
uint8_t temp;
temp = AT24CXX_ReadOneByte(255); // 避免每次开机都写AT24CXX
if(temp == 0x55)
return 0;
else // 排除第一次初始化的情况
{
AT24CXX_WriteOneByte(255, 0x55);
temp = AT24CXX_ReadOneByte(255);
if(temp == 0x55)
return 0;
}
return 1;
}
/**
* @brief 在AT24CXX里面的指定地址开始读出指定个数的数据
* @param ReadAddr: 开始读出的地址
* @param pBuffer: 数据数组首地址
* @param NumToRead: 要读出数据的个数
* @retval None
*/
void AT24CXX_Read(uint16_t ReadAddr, uint8_t *pBuffer, uint16_t NumToRead)
{
while(NumToRead)
{
*pBuffer++ = AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
/**
* @brief 在AT24CXX里面的指定地址开始写入指定个数的数据
* @param WriteAddr: 开始写入的地址
* @param pBuffer: 数据数组首地址
* @param NumToWrite: 要写入数据的个数
* @retval None
*/
void AT24CXX_Write(uint16_t WriteAddr, uint8_t *pBuffer, uint16_t NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr, *pBuffer);
WriteAddr++;
pBuffer++;
}
}
- touch.h
touch.h文件包含触摸屏控制结构体定义以及各IO口宏定义。
#ifndef __TOUCH_H__
#define __TOUCH_H__
#include <stdint.h>
#include "main.h"
#include "spi.h"
#include "gpio.h"
#include "stm32f4xx_hal.h"
// 触摸屏状态
#define TP_PRESS_DOWN 0x80 // 触屏被按下
#define TP_PRESS_LIFT 0x40 // 触屏被按下后松开
// 触摸屏控制器结构体
typedef struct {
uint8_t (*init)(void); // 初始化触摸屏控制器
uint8_t (*scan)(uint8_t); // 扫描触摸屏
void (*adjust)(void); // 触摸屏校准
uint16_t x0; // 原始坐标(第一次按下时的坐标)
uint16_t y0;
uint16_t x; // 当前坐标(此次扫描时的坐标)
uint16_t y;
uint8_t sta; // 笔的状态
float xfac; // 触摸屏校准参数
float yfac;
short xoff;
short yoff;
uint8_t touchtype; // 触摸屏类型
} TouchTypeDef;
extern TouchTypeDef tp_dev;
// 触摸屏IO定义
#define TOUCH_PEN_PIN GPIO_PIN_1 // T_PEN (PB1)
#define TOUCH_PEN_PORT GPIOB
#define TOUCH_MISO_PIN GPIO_PIN_2 // T_MISO (PB2)
#define TOUCH_MISO_PORT GPIOB
#define TOUCH_MOSI_PIN GPIO_PIN_11 // T_MOSI (PF11)
#define TOUCH_MOSI_PORT GPIOF
#define TOUCH_CLK_PIN GPIO_PIN_0 // T_SCK (PB0)
#define TOUCH_CLK_PORT GPIOB
#define TOUCH_CS_PIN GPIO_PIN_5 // T_CS (PC5)
#define TOUCH_CS_PORT GPIOC
// IO操作函数
#define PEN_READ() HAL_GPIO_ReadPin(TOUCH_PEN_PORT, TOUCH_PEN_PIN)
#define DOUT_READ() HAL_GPIO_ReadPin(TOUCH_MISO_PORT, TOUCH_MISO_PIN)
#define TDIN(n) HAL_GPIO_WritePin(TOUCH_MOSI_PORT, TOUCH_MOSI_PIN, ((n) ? GPIO_PIN_SET : GPIO_PIN_RESET))
#define TCLK(n) HAL_GPIO_WritePin(TOUCH_CLK_PORT, TOUCH_CLK_PIN, ((n) ? GPIO_PIN_SET : GPIO_PIN_RESET))
#define TCS(n) HAL_GPIO_WritePin(TOUCH_CS_PORT, TOUCH_CS_PIN, ((n) ? GPIO_PIN_SET : GPIO_PIN_RESET))
// 函数声明
void TP_Write_Byte(uint8_t num);
uint16_t TP_Read_AD(uint8_t cmd);
uint16_t TP_Read_XOY(uint8_t xy);
uint8_t TP_Read_XY(uint16_t *x, uint16_t *y);
uint8_t TP_Read_XY2(uint16_t *x, uint16_t *y);
void TP_Drow_Touch_Point(uint16_t x, uint16_t y, uint16_t color);
void TP_Draw_Big_Point(uint16_t x, uint16_t y, uint16_t color);
uint8_t TP_Scan(uint8_t tp);
void TP_Save_Adjdata(void);
uint8_t TP_Get_Adjdata(void);
void TP_Adjust(void);
uint8_t TP_Init(void);
void TP_Adj_Info_Show(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,
uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3, uint16_t fac);
void Touch_Test(void);
#endif
- touch.c
touch.c需完成模拟SPI、读取坐标值、扫描触摸状态、校准及保存校准参数至AT24C02、初始化等功能 - 读取坐标值,两次读取保证数据准确
/**
* @brief 读取触摸屏的AD值
* @param cmd: 读取命令
* @retval 读取到的AD值
*/
uint16_t TP_Read_AD(uint8_t cmd)
{
uint8_t count = 0;
uint16_t Num = 0;
TCLK(0);
TDIN(0);
TCS(0);
TP_Write_Byte(cmd);
delay_us(6);
TCLK(0);
delay_us(1);
TCLK(1);
delay_us(1);
TCLK(0);
for(count = 0; count < 16; count++)
{
Num <<= 1;
TCLK(0);
delay_us(1);
TCLK(1);
if(DOUT_READ()) Num++;
}
Num >>= 4;
TCS(1);
return(Num);
}
#define READ_TIMES 5
#define LOST_VAL 1
/**
* @brief 读取一个坐标值
* @param xy: 指令(CMD_RDX/CMD_RDY)
* @retval 读取到的值
*/
uint16_t TP_Read_XOY(uint8_t xy)
{
uint16_t i, j;
uint16_t buf[READ_TIMES];
uint16_t sum = 0;
uint16_t temp;
for(i = 0; i < READ_TIMES; i++) buf[i] = TP_Read_AD(xy);
for(i = 0; i < READ_TIMES - 1; i++)
{
for(j = i + 1; j < READ_TIMES; j++)
{
if(buf[i] > buf[j])
{
temp = buf[i];
buf[i] = buf[j];
buf[j] = temp;
}
}
}
sum = 0;
for(i = LOST_VAL; i < READ_TIMES - LOST_VAL; i++) sum += buf[i];
temp = sum / (READ_TIMES - 2 * LOST_VAL);
return temp;
}
/**
* @brief 读取x,y坐标
* @param x,y: 读取到的坐标值
* @retval 0-读取失败,1-读取成功
*/
uint8_t TP_Read_XY(uint16_t *x, uint16_t *y)
{
*x = TP_Read_XOY(CMD_RDX);
*y = TP_Read_XOY(CMD_RDY);
return 1;
}
#define ERR_RANGE 50
/**
* @brief 连续读取两次触摸值,判断是否有效
* @param x,y: 读取到的坐标值
* @retval 0-读取失败,1-读取成功
*/
uint8_t TP_Read_XY2(uint16_t *x, uint16_t *y)
{
uint16_t x1, y1;
uint16_t x2, y2;
uint8_t flag;
flag = TP_Read_XY(&x1, &y1);
if(flag == 0) return 0;
flag = TP_Read_XY(&x2, &y2);
if(flag == 0) return 0;
if(((x2 <= x1 && x1 < x2 + ERR_RANGE) || (x1 <= x2 && x2 < x1 + ERR_RANGE)) &&
((y2 <= y1 && y1 < y2 + ERR_RANGE) || (y1 <= y2 && y2 < y1 + ERR_RANGE)))
{
*x = (x1 + x2) / 2;
*y = (y1 + y2) / 2;
return 1;
}
else return 0;
}
- 扫描触摸状态
/**
* @brief 扫描触摸屏状态
* @param tp: 0-屏幕坐标; 1-物理坐标
* @retval 当前触屏状态
*/
uint8_t TP_Scan(uint8_t tp)
{
if(!PEN_READ())
{
if(tp)
{
TP_Read_XY2(&tp_dev.x, &tp_dev.y);
}
else if(TP_Read_XY2(&tp_dev.x, &tp_dev.y))
{
tp_dev.x = tp_dev.xfac * tp_dev.x + tp_dev.xoff;
tp_dev.y = tp_dev.yfac * tp_dev.y + tp_dev.yoff;
}
if((tp_dev.sta & TP_PRESS_DOWN) == 0)
{
tp_dev.sta = TP_PRESS_DOWN | TP_PRESS_LIFT;
tp_dev.x0 = tp_dev.x;
tp_dev.y0 = tp_dev.y;
}
}
else
{
if(tp_dev.sta & TP_PRESS_DOWN)
{
tp_dev.sta &= ~(1 << 7);
}
else
{
tp_dev.x0 = 0;
tp_dev.y0 = 0;
tp_dev.x = 0xffff;
tp_dev.y = 0xffff;
}
}
return tp_dev.sta & TP_PRESS_DOWN;
}
- 触摸校准
校准使用了串口打印进行调试,编写代码时可通过连接串口查看调试情况,也可更改为直接在屏幕上打印。
校准需依次点击屏幕中的校准点,程序记录每个点的触摸原始坐标(x,y)到pos_temp数组,如果比例在0.95~1.05之间,说明触摸基本均匀,校准可信,否则需要重新校准。
/**
* @brief 触摸屏校准
*/
void TP_Adjust(void)
{
uint16_t pos_temp[4][2];
uint8_t cnt = 0;
uint16_t d1, d2;
uint32_t tem1, tem2;
double fac;
uint16_t outtime = 0;
printf("开始进行校准.\r\n");
TP_Drow_Touch_Point(20, 20, RED);
printf("Please touch point1.\r\n");
while(1)
{
tp_dev.scan(1);
if((tp_dev.sta & 0xc0) == TP_PRESS_LIFT)
{
outtime = 0;
tp_dev.sta &= ~(1 << 6);
pos_temp[cnt][0] = tp_dev.x;
pos_temp[cnt][1] = tp_dev.y;
cnt++;
switch(cnt)
{
case 0:
break;
case 1:
TP_Drow_Touch_Point(20, 20, WHITE); // 清除点1
TP_Drow_Touch_Point(lcddev.width - 20, 20, RED); // 画点2
printf("Please touch point2.\r\n");
break;
case 2:
TP_Drow_Touch_Point(lcddev.width - 20, 20, WHITE); // 清除点2
TP_Drow_Touch_Point(20, lcddev.height - 20, RED); // 画点3
printf("Please touch point3.\r\n");
break;
case 3:
TP_Drow_Touch_Point(20, lcddev.height - 20, WHITE); // 清除点3
TP_Drow_Touch_Point(lcddev.width - 20, lcddev.height - 20, RED); // 画点4
printf("Please touch point4.\r\n");
break;
case 4:
// 开始计算
tem1 = abs(pos_temp[0][0] - pos_temp[1][0]);
tem2 = abs(pos_temp[0][1] - pos_temp[1][1]);
tem1 *= tem1;
tem2 *= tem2;
d1 = sqrt(tem1 + tem2);
tem1 = abs(pos_temp[2][0] - pos_temp[3][0]);
tem2 = abs(pos_temp[2][1] - pos_temp[3][1]);
tem1 *= tem1;
tem2 *= tem2;
d2 = sqrt(tem1 + tem2);
fac = (float)d1 / d2;
if(fac < 0.95 || fac > 1.05 || d1 == 0 || d2 == 0)
{
cnt = 0;
TP_Drow_Touch_Point(lcddev.width - 20, lcddev.height - 20, WHITE);
TP_Drow_Touch_Point(20, 20, RED); // 画点1
printf("Calibration failed! Please try again!\r\n");
printf("Please touch point1.\r\n");
continue;
}
// 计算结果
tp_dev.xfac = (float)(lcddev.width - 40) / (pos_temp[1][0] - pos_temp[0][0]);
tp_dev.xoff = (lcddev.width - tp_dev.xfac * (pos_temp[1][0] + pos_temp[0][0])) / 2;
tp_dev.yfac = (float)(lcddev.height - 40) / (pos_temp[2][1] - pos_temp[0][1]);
tp_dev.yoff = (lcddev.height - tp_dev.yfac * (pos_temp[2][1] + pos_temp[0][1])) / 2;
if(abs(tp_dev.xfac) > 2 || abs(tp_dev.yfac) > 2)
{
cnt = 0;
TP_Drow_Touch_Point(lcddev.width - 20, lcddev.height - 20, WHITE);
TP_Drow_Touch_Point(20, 20, RED); // 画点1
printf("Calibration failed! Please try again!\r\n");
printf("Please touch point1.\r\n");
tp_dev.touchtype = !tp_dev.touchtype;
if(tp_dev.touchtype)
{
CMD_RDX = 0X90;
CMD_RDY = 0XD0;
}
else
{
CMD_RDX = 0XD0;
CMD_RDY = 0X90;
}
continue;
}
TP_Drow_Touch_Point(lcddev.width - 20, lcddev.height - 20, WHITE);
printf("Touch Screen Adjust OK!\r\n");
HAL_Delay(1000);
TP_Save_Adjdata();
return;
}
}
HAL_Delay(10);
outtime++;
if(outtime > 1000)
{
TP_Get_Adjdata();
break;
}
}
}
- 初始化
/**
* @brief 触摸屏初始化
* @retval 0-初始化失败,1-初始化成功
*/
uint8_t TP_Init(void)
{
TP_Read_XY(&tp_dev.x, &tp_dev.y);
AT24CXX_Init();
if(TP_Get_Adjdata())
{
return 0;
}
else
{
TP_Adjust();
TP_Save_Adjdata();
}
TP_Get_Adjdata();
return 1;
}
- 测试程序
测试程序会先进行触摸校准,校准完成后可在屏幕上触摸描点
void Touch_Test(void)
{
TP_Init();
uint16_t x, y;
uint8_t touchStatus;
while (1)
{
// 使用 TP_Scan 函数检测触摸状态
touchStatus = TP_Scan(0); // 0 表示获取屏幕坐标
if (touchStatus & TP_PRESS_DOWN) // 如果触摸屏被按下
{
x = tp_dev.x;
y = tp_dev.y;
// printf("Touch detected at: x=%d, y=%d\r\n", x, y);
// 在触摸点绘制一个大点
TP_Draw_Big_Point(x, y, BLACK);
}
else
{
}
}
}