1. XPT2046简介
XPT2046是一款4线电阻式触摸屏控制器,包含12位125 kHz采样SAR型a /D转换器。
XPT2046可以通过执行两个A/D转换来检测按下的屏幕位置。
…
下面简单的了解下吧
内部逻辑电路
引脚描述
基础应用电路
读写时序
2. XPT2046驱动
XPT2046 用的是SPI驱动,不过这里使用的是模拟的spi,原因有三:
- 1. XPT2046 不支持太高的SPI速率
- 2. ESP32上没有足够的SPI了,一个60MHzSPI用来驱动LCD屏、一个16MHzSPI驱动MAX6675和flash
- 3. 模拟SPI好处理特殊时序
先定义引脚、寄存器和数据结构吧
#define TCLK 2
#define TCS 0
#define TDIN 4
#define TDO 16
#define TIRQ 17
#define ESP_INTR_FLAG_DEFAULT 0
#define CMD_RDX 0X90
#define CMD_RDY 0XD0
typedef struct _coord_t_{
float xRate; //x轴比例系数
float yRate;
int16_t xOffset;//触摸屏与显示屏x轴偏移
int16_t yOffset;
int16_t xCoord; //x轴坐标
int16_t yCoord; //y轴坐标
int16_t xIndev; //触摸屏x轴数据
int16_t yIndex; //触摸屏y轴数据
bool isPressed;//检测触摸是否被按下
}coord_TypeDef;
enum{
CMD_GPIO = 0,
CMD_TMEP,
};
typedef struct _cmt_t_{
uint32_t cmdNum;
uint32_t cmdVal;
uint8_t cmdType;
}cmdTypedef;
触摸驱动
/**
*************************************************************************************************
* @file : slim_touch.c
* @author : slim
* @version : V0.0.1
* @date : 2021.08.29
* @brief : touch pad moduler driver
*************************************************************************************************
*/
#include "slim_touch.h"
#include <math.h>
#include "esp_task_wdt.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/timers.h"
extern xQueueHandle evt_queue;
coord_TypeDef g_Coord;
//===========================================================================
// 静态函数声明
static void _gpio_init(void);
static void IRAM_ATTR _gpio_isr_handler(void* arg);
static void _send_cmd(uint8_t dat);
static uint16_t _read_coord(uint8_t reg);
static uint16_t _tp_read_xoy(uint8_t xy_reg);
static bool _tp_read_xy(uint16_t *x, uint16_t *y);
static bool _tp_read_xy2(uint16_t *x, uint16_t *y);
static void _detect_TP_Press_TimerCallback(TimerHandle_t xTimer);
//============================================================================
// simulate spi 初始化、读、写
//============================================================================
static void _gpio_init(void){
esp_err_t ret = ESP_OK;
gpio_config_t io_conf;
gpio_config_t irq_conf;
#if 0 //写法不一样而已
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pin_bit_mask = BIT64(TCLK) | BIT64(TCS) | BIT64(TDIN);
io_conf.pull_down_en = GPIO_PULLUP_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
ret = gpio_config(&io_conf);
#else
gpio_pad_select_gpio(TCLK);
gpio_pad_pullup(TCLK);
gpio_set_direction(TCLK, GPIO_MODE_OUTPUT);
gpio_pad_select_gpio(TCS);
gpio_pad_pullup(TCS);
gpio_set_direction(TCS, GPIO_MODE_OUTPUT);
gpio_pad_select_gpio(TDIN);
gpio_pad_pullup(TDIN);
gpio_set_direction(TDIN, GPIO_MODE_OUTPUT);
#endif
#if 1
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = BIT64(TDO);
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
ret = gpio_config(&io_conf);
#else
gpio_pad_select_gpio(TDO);
gpio_set_direction(TDO, GPIO_MODE_INPUT);
#endif
irq_conf.intr_type = GPIO_INTR_NEGEDGE;
irq_conf.mode = GPIO_MODE_INPUT;
irq_conf.pin_bit_mask = BIT64(TIRQ);
irq_conf.pull_up_en = GPIO_PULLUP_ENABLE;
irq_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
ret = gpio_config(&irq_conf);
//install gpio isr service
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
//hook isr handler for specific gpio pin
gpio_isr_handler_add(TIRQ, _gpio_isr_handler, (void*) TIRQ);
//remove isr handler for gpio number.
gpio_isr_handler_remove(TIRQ);
//hook isr handler for specific gpio pin again
gpio_isr_handler_add(TIRQ, _gpio_isr_handler, (void*) TIRQ);
printf("Minimum free heap size: %d bytes\n", esp_get_minimum_free_heap_size());
assert(ret == ESP_OK);
}
static void IRAM_ATTR _gpio_isr_handler(void* arg){
uint32_t gpio_num = (uint32_t) arg;
cmdTypedef cmdBody;
cmdBody.cmdType = CMD_GPIO;
cmdBody.cmdNum = gpio_num;
cmdBody.cmdVal = gpio_get_level(gpio_num);
xQueueSendFromISR(evt_queue, &cmdBody, NULL);
}
static void _send_cmd(uint8_t dat){
for(uint8_t i=0;i<8;i++){
if(dat & 0x80){
gpio_set_level(TDIN, 1);
}else{
gpio_set_level(TDIN, 0);
}
dat <<= 1;
gpio_set_level(TCLK, 0);
gpio_set_level(TCLK, 1);
}
}
//============================================================================
// 获取坐标、坐标处理
//============================================================================
static uint16_t _read_coord(uint8_t reg){
uint16_t tmp = 0;
gpio_set_level(TCLK, 0);
gpio_set_level(TDIN, 0);
gpio_set_level(TCS, 0);
_send_cmd(reg);
ets_delay_us(6); //busy
gpio_set_level(TCLK, 0);
ets_delay_us(1);
gpio_set_level(TCLK, 1);// 给1个时钟,清除BUSY
gpio_set_level(TCLK, 0);
for(uint8_t i=0;i<16;i++){
tmp <<= 1;
gpio_set_level(TCLK, 0); // 下降沿有效
gpio_set_level(TCLK, 1);
if(gpio_get_level(TDO)){
tmp++;
}
}
tmp >>= 4;
gpio_set_level(TCS, 1);
return tmp;
}
/*
* 输入:寄存器
* 输出:坐标值 / 0:错误值
*/
#define LOST_CNT 1 //丢弃值
#define BUFF_SIZE 10
static uint16_t _tp_read_value(uint8_t xy_reg){
uint16_t tmp = 0;
uint16_t buff[BUFF_SIZE] = {0};
uint32_t sum = 0;
uint8_t errCnt = 0;
for(uint8_t i=0;i<BUFF_SIZE;){
tmp = _read_coord(xy_reg);
if((0xFFF != tmp) && (0 != tmp)){
// 过滤掉错误的值
buff[i] = tmp;
i++;
}else{
// 防止阻塞
errCnt++;
}
if(errCnt > 100){
return 0;
}
}
//升序排序
for(uint8_t i=0;i<BUFF_SIZE-1;i++){
for(uint8_t j=i+1;j<BUFF_SIZE;j++){
if(buff[i] > buff[j]){
tmp = buff[i];
buff[i] = buff[j];
buff[j] = tmp;
}
}
}
//掐头去尾 —— 去掉一个最高值和最低值
for(uint8_t i=LOST_CNT;i<BUFF_SIZE-LOST_CNT;i++){
sum += buff[i];
}
tmp = sum/(BUFF_SIZE-2*LOST_CNT);
return tmp;
}
/*
* 输入:x/y 坐标地址
* 输出:true:获取正确坐标 / false:获取坐标失败
*/
static bool _tp_read_coord(uint16_t *x, uint16_t *y){
uint16_t xTmp = _tp_read_value(CMD_RDX);
uint16_t yTmp = _tp_read_value(CMD_RDY);
*x = xTmp;
*y = yTmp;
if(0 == xTmp*yTmp){
return false;
}
return true;
}
//============================================================================
// TouchPad 外部接口
//============================================================================
void TouchPad_Init(void){
memset(&g_Coord, 0, sizeof(coord_TypeDef));
if(true){
//未校准,使用默认校准系数
g_Coord.xOffset = 200; //x轴偏移200
g_Coord.yOffset = 280; //y轴偏移280
g_Coord.xRate = 7.71; //x轴比例系数7.71
g_Coord.yRate = 10.94; //y轴比例系数10.94
}else
{
//已校准,从flash中读取校准系数
}
_gpio_init();
}
uint32_t TP_test(void){
uint32_t tmp = 0;
gpio_isr_handler_remove(TIRQ);
tmp = _tp_read_value(CMD_RDX) << 16 | _tp_read_value(CMD_RDY);
gpio_isr_handler_add(TIRQ, _gpio_isr_handler, (void*) TIRQ);
return tmp;
}
bool TP_Update_Coord(void){
bool sta = true;
uint16_t x = 0;
uint16_t y = 0;
gpio_isr_handler_remove(TIRQ);
sta = _tp_read_coord(&x, &y);
if(true == sta){
g_Coord.xCoord = (int16_t)((x - g_Coord.xOffset)/g_Coord.xRate);
g_Coord.yCoord = (int16_t)((y - g_Coord.yOffset)/g_Coord.yRate);
g_Coord.xIndev = x;
g_Coord.yIndex = y;
}
gpio_isr_handler_add(TIRQ, _gpio_isr_handler, (void*) TIRQ);
return sta;
}
/*
* brief: 检测触摸屏释放按下或释放,按下后定时器检测IRQ来检测是否释放按压
*
*/
TimerHandle_t Tmr_Press_Monitor = NULL;
void TP_Press_Handler(void){
if(0 == gpio_get_level(TIRQ)){
// 被按下
g_Coord.isPressed = true;
if(NULL == Tmr_Press_Monitor){
Tmr_Press_Monitor = xTimerCreate("Detect TP_Press timer", // 名字
pdMS_TO_TICKS(100UL), // 重载需要时间
pdTRUE, // 重载模式
0,
_detect_TP_Press_TimerCallback); // 回调函数
if(Tmr_Press_Monitor != NULL){
xTimerStart(Tmr_Press_Monitor, 0); // 启动定时器
}
}
}else{
//被释放
g_Coord.isPressed = false;
if(NULL != Tmr_Press_Monitor){
xTimerStop(Tmr_Press_Monitor, 0);
xTimerDelete(Tmr_Press_Monitor, 50);
Tmr_Press_Monitor = NULL;
}
}
}
static void _detect_TP_Press_TimerCallback(TimerHandle_t xTimer){
if(1 == gpio_get_level(TIRQ)){
// 触摸屏释放
g_Coord.isPressed = false;
if(NULL != Tmr_Press_Monitor){
xTimerStop(Tmr_Press_Monitor, 0);
xTimerDelete(Tmr_Press_Monitor, 50);
Tmr_Press_Monitor = NULL;
}
}
}
3. XPT2046触摸手动校准
XPT2046 获取出来的按压点阵坐标与LCD上点阵的坐标不是相同的,需要将触摸点阵坐标转化为LCD的点阵坐标,才能更好的取控制LCD上面的控件
实现方式
- 1. 确定好LCD的xy坐标方向是否与触摸的xy坐标方向是否一致,如果不一致,则交换
CMD_RDX
与CMD_RDY
命令 - 2. 在LCD最左侧点击触摸屏获取触摸的x轴偏移量X1,在LCD最上侧点击触摸屏获取触摸屏y轴偏移量Y1
- 3. 在LCD最右侧点击触摸屏获取触摸的x轴偏移量X2,在LCD最下侧点击触摸屏获取触摸屏y轴偏移量Y2
- 4. x轴相对偏移
abs(X2-X1)
与y轴相对偏移abs(Y2-Y1)
分别与LCD的显示宽度和显示高度的比值,即为x轴与y轴上的比例 - 5. 有了x轴、y轴的偏移值跟比例值,就可以把获取到的触摸屏坐标换算成为对应的LCD坐标,
g_Coord.xCoord = (int16_t)((x - g_Coord.xOffset)/g_Coord.xRate);
与g_Coord.yCoord = (int16_t)((y - g_Coord.yOffset)/g_Coord.yRate);
4. LVGL移植触摸驱动
将写好的XPT2046驱动移植到 LittleVGL上,才能做到与LCD进行交互
- 1. 使能
"lv_port_indev.c
和"lv_port_indev.h"
文件 - 2. 初始化触摸屏
void lv_port_indev_init(void){
static lv_indev_drv_t indev_drv;
touchpad_init();
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read;
indev_touchpad = lv_indev_drv_register(&indev_drv);
}
static void touchpad_init(void){
TouchPad_Init();
}
- 3. 点击触摸屏时触发读坐标回调
其实这里只是获取对应的数据,点击触摸触发获取坐标,在事件处理线程中处理了,并将获取结果保存到对应的结构数据中
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data){
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
if(touchpad_is_pressed()) {
touchpad_get_xy(&last_x, &last_y);
data->state = LV_INDEV_STATE_PR;
} else {
data->state = LV_INDEV_STATE_REL;
}
data->point.x = last_x;
data->point.y = last_y;
}
static bool touchpad_is_pressed(void){
if(true == g_Coord.isPressed){
return true;
}
return false;
}
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y){
(*x) = (lv_coord_t)(g_Coord.xCoord);
(*y) = (lv_coord_t)(g_Coord.yCoord);
}
//main.c
static void Thread_Event(void *arg){
cmdTypedef cmdEvt;
while(1){
if(xQueueReceive(evt_queue, &cmdEvt, 0xffff)) {
if(CMD_GPIO == cmdEvt.cmdType){
switch (cmdEvt.cmdNum){
case TIRQ:
if(0 == cmdEvt.cmdVal){ //press down the screen
TP_Update_Coord(); //更新按压点坐标数据
TP_Press_Handler();
}
break;
default:
break;
}
}
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
5. LVGL写个触摸校准界面吧
使用校准界面去校准,使校准流程更加方便及可视化
- 1. 在LCD四个角上创建四个按钮控件
- 2. 分别多次点击四个按钮,获取对应的触摸屏原始坐标数据
- 3. 将获取的四个触摸屏点阵坐标用上面提到的公式或关系计算出比例值和偏移值即可
- 4. 将更新后的校准数据保存到flash中,以便后续使用
校准触摸页面用到的数据结构
#define ADJUST_TIME 10 //校准次数
typedef struct {
lv_obj_t *cont;
lv_obj_t *adjBtn[4];
lv_obj_t *adjBtnLabel[4];
lv_obj_t *adjCntLabel[4];
lv_obj_t *adjLabel;
}adjObj_TypeDef;
typedef struct{
int16_t adj_coord_x[4][ADJUST_TIME];
int16_t adj_coord_y[4][ADJUST_TIME];
int32_t adj_sum_x[4];
int32_t adj_sum_y[4];
bool fullCnt[4];
}adjBuff_TypeDef;
校准触摸页面及流程
//===========================================================================
// 触摸屏校准
//===========================================================================
static void adj_coord_handler(adjBuff_TypeDef buff){
//x1 = x3 / x2 = x4
//y1 = y2 / y3 = y4
int32_t tmpX1=0, tmpX2=0;
int32_t tmpY1=0, tmpY2=0;
for(uint8_t i=0;i<4;i++){
for(uint8_t j=0;j<ADJUST_TIME;j++){
buff.adj_sum_x[i] += buff.adj_coord_x[i][j];
buff.adj_sum_y[i] += buff.adj_coord_y[i][j];
}
}
tmpX1 = (buff.adj_sum_x[0] + buff.adj_sum_x[2])/(2*ADJUST_TIME);
tmpX2 = (buff.adj_sum_x[1] + buff.adj_sum_x[3])/(2*ADJUST_TIME);
tmpY1 = (buff.adj_sum_y[0] + buff.adj_sum_y[1])/(2*ADJUST_TIME);
tmpY2 = (buff.adj_sum_y[2] + buff.adj_sum_y[3])/(2*ADJUST_TIME);
g_Coord.xRate = abs(tmpX1-tmpX2)/(480.0-25.0*2); //触摸点x相对距离 / x轴两按钮中心点距离
g_Coord.yRate = abs(tmpY1-tmpY2)/(320.0-25.0*2); //触摸点y相对距离 / y轴两按钮中心点距离
g_Coord.xOffset = g_Coord.xRate*(((tmpX1>tmpX2?tmpX2:tmpX1)/g_Coord.xRate)-25); //输入的x轴偏移量
g_Coord.yOffset = g_Coord.yRate*(((tmpY1>tmpY2?tmpY2:tmpY1)/g_Coord.yRate)-25); //输入的y轴偏移量
printf("=============================================\n");
printf("X:[%d - %d] \t Y:[%d - %d]\n", tmpX1, tmpX2, tmpY1, tmpY2);
printf("Offset[X:%d Y:%d]\n",g_Coord.xOffset, g_Coord.yOffset);
printf("Rate[X:%.3f Y:%.3f]\n", g_Coord.xRate, g_Coord.yRate);
printf("=============================================\n");
//保存校准数据到flash中
//lfs_save(g_Coord);
}
static void adj_event_cb(lv_event_t *event){
static uint8_t cnt = 0;
static uint8_t adjCnt[4] = {0};
char str[20] = "";
static lv_style_t style_btn_full;
static lv_style_t style_label_normal;
static lv_style_t style_label_error;
uint8_t fullFlag = 0;
if(NULL == adjBuff){
adjBuff = (adjBuff_TypeDef*)malloc(sizeof(adjBuff_TypeDef));
if(NULL == adjBuff){
printf("malloc adjBuff failed!!!\n");
return;
}
memset(adjBuff, 0, sizeof(adjBuff_TypeDef)); //数据清零
}
lv_style_init(&style_btn_full);
lv_style_set_radius(&style_btn_full, 0);
lv_style_set_bg_color(&style_btn_full, lv_color_make(0xFF, 0, 0));
lv_obj_t *currObj = lv_event_get_user_data(event);
lv_style_init(&style_label_normal);
lv_style_set_text_font(&style_label_normal, &lv_font_montserrat_32);
lv_style_set_text_color(&style_label_normal, lv_color_make(0xFF, 0xFF, 0xFF));
lv_style_init(&style_label_error);
lv_style_set_text_font(&style_label_error, &lv_font_montserrat_32);
lv_style_set_text_color(&style_label_error, lv_color_make(0xFF, 0, 0));
if(LV_EVENT_RELEASED == lv_event_get_code(event)){
if(0 != g_Coord.xIndev*g_Coord.yIndex){
sprintf(str, "[%d, %d]", g_Coord.xCoord, g_Coord.yCoord);
// sprintf(str, "[%d, %d]", g_Coord.xIndev, g_Coord.yIndex);
lv_label_set_text(adjStruct.adjLabel, str);
for(uint8_t i=0;i<4;i++){
// 判断按下哪个按键,处理相应的 label 控件显示
if(adjStruct.adjBtn[i] == currObj){
if(++adjCnt[i] > ADJUST_TIME){
// 已经获取足够校准次数的坐标按钮变成红色
lv_obj_add_style(adjStruct.adjBtn[i], &style_btn_full, LV_PART_MAIN);
lv_obj_add_style(adjStruct.adjLabel, &style_label_error, LV_PART_MAIN);
adjBuff->fullCnt[i] = true;
}else{
sprintf(str, "Btn_%d ( %d )", i+1, adjCnt[i]);
lv_label_set_text(adjStruct.adjCntLabel[i], str);
lv_obj_add_style(adjStruct.adjLabel, &style_label_normal, LV_PART_MAIN);
adjBuff->adj_coord_x[i][adjCnt[i]-1] = g_Coord.xIndev;
adjBuff->adj_coord_y[i][adjCnt[i]-1] = g_Coord.yIndex;
}
}
fullFlag += adjBuff->fullCnt[i];
if(fullFlag == 4){
// 已经获取足够的数据来计算校准信息
adj_coord_handler(*adjBuff);
free(adjBuff);
adjBuff = NULL;
}
}
}else{
lv_label_set_text(adjStruct.adjLabel, "- - -");
}
}
if(NULL == adjBuff){ //已经释放资源了
//删除校准页面并释放资源
TP_Adjust_Del();
slim_index();
}
}
static void TP_Adjust_Create(void){
lv_obj_t *scr = lv_scr_act(); //获取屏幕对象
char str[15] = "";
static lv_style_t style_cont;
static lv_style_t style_txt;
uint8_t btnAlign[4] = {LV_ALIGN_TOP_LEFT, LV_ALIGN_TOP_RIGHT, LV_ALIGN_BOTTOM_LEFT, LV_ALIGN_BOTTOM_RIGHT};
lv_style_init(&style_cont);
lv_style_set_radius(&style_cont, 0);
lv_style_set_bg_color(&style_cont, lv_color_make(0, 0, 0));
lv_style_init(&style_txt);
lv_style_set_text_font(&style_txt, &lv_font_montserrat_32);
adjStruct.cont = lv_obj_create(scr); //cont 作为校准界面中所有控件的父对象
lv_obj_add_style(adjStruct.cont, &style_cont, LV_PART_MAIN);
lv_obj_set_size(adjStruct.cont, LCD_H, LCD_W);
lv_obj_align(adjStruct.cont, LV_ALIGN_CENTER, 0, 0);
for(uint8_t i=0;i<4;i++){ //按钮控件
adjStruct.adjBtn[i] = lv_btn_create(adjStruct.cont);
lv_obj_set_size(adjStruct.adjBtn[i], 30, 30);
lv_obj_align(adjStruct.adjBtn[i], btnAlign[i], 0, 0);
lv_obj_add_event_cb(adjStruct.adjBtn[i], adj_event_cb, LV_EVENT_RELEASED, (void*)adjStruct.adjBtn[i]);
}
for(uint8_t i=0;i<4;i++){ //标签控件
adjStruct.adjBtnLabel[i] = lv_label_create(adjStruct.cont);
sprintf(str, "%d", i+1);
lv_label_set_text(adjStruct.adjBtnLabel[i], str);
lv_obj_align_to(adjStruct.adjBtnLabel[i], adjStruct.adjBtn[i], LV_ALIGN_CENTER, 0, 0);
adjStruct.adjCntLabel[i] = lv_label_create(adjStruct.cont);
sprintf(str, "Btn_%d --", i+1);
lv_label_set_text(adjStruct.adjCntLabel[i], str);
lv_obj_align_to(adjStruct.adjCntLabel[i], adjStruct.cont, LV_ALIGN_TOP_MID, 0, 20+22*i);
}
adjStruct.adjLabel = lv_label_create(adjStruct.cont);
lv_obj_align(adjStruct.adjLabel, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(adjStruct.adjLabel, "- - -");
lv_obj_add_style(adjStruct.adjLabel, &style_txt, LV_PART_MAIN);
}
static void TP_Adjust_Del(void){
lv_obj_del(adjStruct.cont);
adjStruct.cont = NULL;
}
触摸驱动和触摸校准就搞定了