基于战舰V3的4.3寸电容触摸屏
原理简介
4.3寸电容屏可以等效为800*480个点,当我们触碰到LCD屏幕时,触摸点的电容值会发生变化,此时内置MPU可以读取到这一点的准确坐标我们访问其中的寄存器就可以获取该信息,而且电容屏不像电阻屏无需任何校准,只需使用厂家提供的初始化序列初始化一下GT9147芯片即可。
引脚简介
GT9147采用IIC驱动,由于一般器件的IIC读写时序都很简单,使用STM32的IIC固件库有点杀鸡焉用牛刀的感觉,因此这里我们使用普通GPIO口模拟IIC时序驱动GT9147。MCU与GT9147的硬件连接关系如下所示:
但是开发板上没有GT9147这4各引脚,这是因为正点原子这LCD的标注还是沿用的电阻型触摸屏的引脚名称,名称对应关系如下:
表格 1
电阻屏引脚名称 | 电容屏引脚名称 |
PEN | INT |
CS | RST |
CLK | SCL |
MOSI | SDA |
GT9147知识点
上电或复位 I2C 地址选择
GT9147的IIC从设备地址有两组,分别为0xBA/0xBB和0x28/0x29。主控在上电初始化时或通过Reset脚复位(唤醒)时,均需要设定I2C设备地址。控制Reset和INT口时序可以进行地址设定,设定方法及时序图如下:
程序详解
结构体详解
//触摸屏控制器
typedef struct
{ u8 (*init)(void); //初始化触摸屏控制器 u8 (*scan)(u8); //扫描触摸屏.0,屏幕扫描;1,物理坐标; void (*adjust)(void); //触摸屏校准 u16 x[CT_MAX_TOUCH]; //当前坐标 u16 y[CT_MAX_TOUCH]; //电容屏有最多5组坐标u8 sta; //笔的状态 //b7:按下1/松开0; //b6:0,没有按键按下;1,有按键按下. //b5:保留 //b4~b0:电容触摸屏按下的点数(0,表示未按下,1表示按下)
/触摸屏校准参数(电容屏不需要校准)// float xfac; float yfac; short xoff; short yoff;
//新增的参数,当触摸屏的左右上下完全颠倒时需要用到.
//b0:0,竖屏(适合左右为X坐标,上下为Y坐标的TP)
// 1,横屏(适合左右为Y坐标,上下为X坐标的TP)
//b1~6:保留.
//b7:0,电阻屏
// 1,电容屏 u8 touchtype;
} _m_tp_dev;
由于电容屏不用校验,因此这里的xfac,yfac,xoff,yoff变量没用。电容屏中,变量sta用到的各个位含义如下:
b6:代表此时有无触摸;
b7:代表此时有无有效的触摸;
有效触摸和触摸的区别:如果我们一直按着触摸点那么就相当于我们一直再触摸,但是我们认为不管一次触摸时长是多少,这仅仅只是一次有效触摸而已。
向寄存器写数据/指令
GT9147的写时序如下:
首先主 CPU 产生一个起始信号,然后发送地址信息及读写位信息“0”表示写操作: 0X28。在收到应答后,主 CPU 发送寄存器的 16 位地址,随后是 8 位要写入到寄存器的数据内容。
大致流程如下:
GT9147牛逼的地方就在于:GT9147寄存器的地址指针自动自加1,所以当主CPU需要对连续地址的寄存器进行写操作时,可以在一次写操作中连续写入。由于地址自增,这些数据将会填入连续地址的多个寄存器中。写操作完成,主CPU发送停止信号结束当前写操作。
//向GT9147写入一次数据
//reg:起始寄存器地址
//buf:数据缓缓存区
//len:写数据长度
//返回值:0,成功;1,失败.
u8 GT9147_WR_Reg(u16 reg,u8 *buf,u8 len)
{ u8 i; u8 ret=0; CT_IIC_Start(); CT_IIC_Send_Byte(GT_CMD_WR); //发送写命令 CT_IIC_Wait_Ack(); CT_IIC_Send_Byte(reg>>8); //发送高8位地址 CT_IIC_Wait_Ack(); CT_IIC_Send_Byte(reg&0XFF); //发送低8位地址 CT_IIC_Wait_Ack(); for(i=0; i<len; i++) { CT_IIC_Send_Byte(buf[i]); //发数据 ret=CT_IIC_Wait_Ack(); if(ret)break; } CT_IIC_Stop(); //产生一个停止条件 return ret;
}
从寄存器读数据
上图为主CPU对GT9147进行的读操作流程图。首先主CPU产生一个起始信号,然后发送设备地址信息及读写位信息“0”表示写操作:0X29。在收到应答后,主CPU发送首寄存器的16位地址信息,设置要读取的寄存器地址。在收到应答后,主CPU重新发送一次起始信号,发送读操作:0X28。收到应答后,主CPU开始读取数据。
GT9147同样支持连续的读操作,默认为连续读取数据。主CPU在每收到一个Byte数据后需发送一个应答信号表示成功接收。在接收到所需的最后一个Byte数据后,主CPU发送“非应答信号NACK”(就是啥也不用回),然后再发送停止信号结束通讯。
//从GT9147读出一次数据
//reg:起始寄存器地址
//buf:数据缓缓存区
//len:读数据长度
void GT9147_RD_Reg(u16 reg,u8 *buf,u8 len)
{ u8 i; CT_IIC_Start(); CT_IIC_Send_Byte(GT_CMD_WR); //发送写命令 CT_IIC_Wait_Ack(); CT_IIC_Send_Byte(reg>>8); //发送高8位地址 CT_IIC_Wait_Ack(); CT_IIC_Send_Byte(reg&0XFF); //发送低8位地址 CT_IIC_Wait_Ack(); CT_IIC_Start(); CT_IIC_Send_Byte(GT_CMD_RD); //发送读命令 CT_IIC_Wait_Ack(); for(i=0; i<len; i++) { buf[i]=CT_IIC_Read_Byte(i==(len-1)?0:1); //发数据 } CT_IIC_Stop();//产生一个停止条件
}
配置GT9147的属性
这段代码结合上述厂家给的初始化数据(全存在了GT9147_CFG_TBL数组中),我们首先写入序列时首先需要更新地址为0X80FF的寄存器,我们要向0X80FF寄存器填入什么样的数据:
我们要配置GT9147就必须配置地址:0X8047~0XFE的所有寄存器。这里共186个寄存器,用于配置GT9147的各个参数,这些配置一般由厂家提供给我们(一个数组),所以我们只需要将厂家给我们的配置,写入到这些寄存器里面,即可完成GT9147的配置。
我们为什么要配置0X80FF的寄存器呢?
当我们更新完0X80FF寄存器后,我们就可以修改0X8047~0X80FE这186个寄存器,然后我们只需再向0X8100寄存器写入1,这些配置就可以生效了。
//发送GT9147配置参数
//mode:0,参数不保存到flash
// 1,参数保存到flash
u8 GT9147_Send_Cfg(u8 mode)
{ u8 buf[2]; u8 i=0; buf[0]=0; buf[1]=mode; //是否写入到GT9147 FLASH? 即是否掉电保存 for(i=0; i<sizeof(GT9147_CFG_TBL); i++)buf[0]+=GT9147_CFG_TBL[i]; //计算校验和 buf[0]=(~buf[0])+1; GT9147_WR_Reg(GT_CFGS_REG,(u8*)GT9147_CFG_TBL,sizeof(GT9147_CFG_TBL));//发送寄存器配置 GT9147_WR_Reg(GT_CHECK_REG,buf,2);//写入校验和,和配置更新标记 return 0;
}
0X80FF和0X8100是两个连续地址的寄存器,因此我们将“将要填入0X80FF和0X8100寄存器的数据”装入一个buf数组中,然后利用寄存器的自增操作将buf数组中两个数据连续写入0X80FF和0X8100寄存器中。
初始化GT9147
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // PB2端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_Init(GPIOF, &GPIO_InitStructure);//PF10上拉输入
GPIO_SetBits(GPIOF,GPIO_Pin_10);//上拉 CT_IIC_Init(); //初始化电容屏的I2C总线
GT_RST=0; //复位
delay_ms(10);
GT_RST=1; //释放复位
delay_ms(10);
上述代码将GT9147的IIC地址设置为0X28(R)/0X29(W),由于将INT引脚配置为IPU模式,使得INT引脚一直为高电平,然后我们将RST引脚先置零后置一,完成了如下时序:
根据“上电或复位IIC地址选择”可知,此时的IIC地址为0X28(R)/0X29(W)。
注意:在配置与INT引脚相连的MCU引脚时,我们需要注意如下配置细节:
之后,我们还需将与INT相连的MCU引脚设置为IPD。因为我们的厂家给的初始化序列中地址为0X804D的寄存器填入的数据为0X35,该寄存器的8个位的含义如下:
其中,我们关心的是INT的触发方式,0X35转换为二进制为0X0011 0101,因此,INT配置为下降沿触发,由于我们需要在第一个沿提醒MCU接收信息,因此我们需要将与INT引脚相连的MCU引脚配置为IPD(下拉输入)。
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // PB2端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //下拉输入
GPIO_Init(GPIOF, &GPIO_InitStructure);//PF10下拉输入
GPIO_ResetBits(GPIOF,GPIO_Pin_10);//下拉
中断触发方式存在的意义如下:
首先,我们需要读取ID,看看是不是GT9147在进行下一步,ID所在寄存器如下:
这里总共由4个寄存器组成,用于保存产品ID,对于GT9147,这4个寄存器读出来就是:9,1,4,7四个字符(ASCII码格式)。因此,我们可以通过这4个寄存器的值,来判断驱动IC的型号,从而判断是OTT2001A还是GT9147,以便执行不同的初始化。
delay_ms(100);
GT9147_RD_Reg(GT_PID_REG,temp,4); //读取产品ID
控制命令寄存器(0X8040):该寄存器可以写入不同值,实现不同的控制,我们一般使用0和2这两个值,写入2,即可软复位GT9147,在硬复位之后,一般要往该寄存器写2,实行软复位。然后,写入0,即可正常读取坐标数据(并且会结束软复位)。
当软复位进行时,我们读取0X8047寄存器中的版本号,因为必须保证读出来的版本号大于0X60,GT9147的内部flash才会更新配置(才会将我们输入的指令付诸实施),0X8047寄存器如下所示:
此时,如果版本号合格,那么我们向0X8040寄存器写入0结束软复位,下一步我们开始正常读取坐标数据。
if(strcmp((char*)temp,"9147")==0) //ID==9147
{ temp[0]=0X02; GT9147_WR_Reg(GT_CTRL_REG,temp,1);//软复位GT9147 GT9147_RD_Reg(GT_CFGS_REG,temp,1);//读取GT_CFGS_REG寄存器 if(temp[0]<0X60)//默认版本比较低,需要更新flash配置 { printf("Default Ver:%d\r\n",temp[0]); GT9147_Send_Cfg(1);//更新并保存配置 } delay_ms(10); temp[0]=0X00; GT9147_WR_Reg(GT_CTRL_REG,temp,1); //结束复位 return 0;
}
扫描触摸屏
这里用到了几个重要的寄存器:
1. 状态寄存器(0X814E)
2. 由于电容屏支持最多5点触摸,第二个触摸点的信息存储在地址(16b)为0x8160 ~0x8165寄存器中,该寄存器如下所示:
表格 2
寄存器地址 | 功能 |
0x8160 | point 2 x coordinate (low byte) |
0x8161 | point 2 x coordinate (high byte) |
0x8162 | point 2 x coordinate (high byte) |
0x8163 | point 2 y coordinate (high byte) |
0x8164 | 触摸区域的大小(low byte) |
0x8165 | 触摸区域的大小(low byte) |
3. 由于电容屏支持最多5点触摸,第三个触摸点的信息存储在地址(16b)为0x8168 ~0x816D寄存器中,该寄存器如下所示:
表格 3
寄存器地址 | 功能 |
0x8168 | point 3 x coordinate (low byte) |
0x8169 | point 3 x coordinate (high byte) |
0x816A | point 3 x coordinate (high byte) |
0x816B | point 3 y coordinate (high byte) |
0x816C | 触摸区域的大小(low byte) |
0x816D | 触摸区域的大小(low byte) |
4. 由于电容屏支持最多5点触摸,第四个触摸点的信息存储在地址(16b)为0x8170~0x8175寄存器中,该寄存器如下所示:
表格 4
寄存器地址 | 功能 |
0x8170 | point 4 x coordinate (low byte) |
0x8171 | point 4 x coordinate (high byte) |
0x8172 | point 4 x coordinate (high byte) |
0x8173 | point 4 y coordinate (high byte) |
0x8174 | 触摸区域的大小(low byte) |
0x8175 | 触摸区域的大小(low byte) |
5. 由于电容屏支持最多5点触摸,第五个触摸点的信息存储在地址(16b)为0x8178~0x817D寄存器中,该寄存器如下所示:
表格 5
寄存器地址 | 功能 |
0x8178 | point 5 x coordinate (low byte) |
0x8179 | point 5 x coordinate (high byte) |
0x817A | point 5 x coordinate (high byte) |
0x817B | point 5 y coordinate (high byte) |
0x817C | 触摸区域的大小(low byte) |
0x817D | 触摸区域的大小(low byte) |
我觉得正点原子给的例程与GT9147的数据手册中给的寄存器地址不太一样,正点原子给的寄存器地址为:
#define GT_GSTID_REG 0X814E //GT9147当前检测到的触摸情况
#define GT_TP1_REG 0X8150 //第一个触摸点数据地址 (错误)
#define GT_TP2_REG 0X8158 //第二个触摸点数据地址
#define GT_TP3_REG 0X8160 //第三个触摸点数据地址
#define GT_TP4_REG 0X8168 //第四个触摸点数据地址
#define GT_TP5_REG 0X8170 //第五个触摸点数据地址
地址为0X8150的寄存器在数据手册中的说明如下:
是个保留寄存器,也就是该寄存器没被使用。因此我们根据GT9147数据手册改正正点原子的错误:
#define GT_GSTID_REG 0X814E //GT9147当前检测到的触摸情况
#define GT_TP1_REG 0X8158 //第一个触摸点数据地址
#define GT_TP2_REG 0X8160 //第二个触摸点数据地址
#define GT_TP3_REG 0X8168 //第三个触摸点数据地址
#define GT_TP4_REG 0X8170 //第四个触摸点数据地址
#define GT_TP5_REG 0x8178 //第五个触摸点数据地址
触摸屏扫描函数执行步骤如下:
1. 读取状态寄存器看看有几个触摸点,状态寄存器有个要求:
但是正点原子的代码中,读取完状态寄存器之后紧接着就将状态寄存器中的一个字节数据写为0,这颠倒了官方说明书的要求,但是也对。因为状态寄存器不能硬件置零,因此需要我们软件归零,至于读完坐标再写0还是写0之后在读取坐标,这都无所谓,只要每读一次状态寄存器都要在完全读取完触控点坐标之前向其写入0即可。代码如下:
GT9147_RD_Reg(GT_GSTID_REG,&mode,1); //读取触摸点的状态
if(mode&0X80&&((mode&0XF)<6))
{ temp=0; GT9147_WR_Reg(GT_GSTID_REG,&temp,1);//清标志
}
2. 我们需要根据从状态寄存器读取到的数据来分析是否有触摸点,如果有那有几个触摸点。我们在这里只需要了解状态寄存器中的两个位即可:
表格 6
位 | 含义 |
7 | 是否有触摸 |
3:0 | 一共有几个触摸点(最多有5个) |
具体代码如下:
if((mode&0XF)&&((mode&0XF)<6))
{ temp=0XFF<<(mode&0XF); //将点的个数转换为1的位数,匹配tp_dev.sta定义 tempsta=tp_dev.sta; //保存当前的tp_dev.sta值 tp_dev.sta=(~temp)|TP_PRES_DOWN|TP_CATH_PRES; tp_dev.x[4]=tp_dev.x[0]; //保存触点0的数据 tp_dev.y[4]=tp_dev.y[0]; for(i=0; i<5; i++) { if(tp_dev.sta&(1<<i)) //触摸有效? { GT9147_RD_Reg(GT9147_TPX_TBL[i],buf,4); //读取XY坐标值 if(tp_dev.touchtype&0X01)//横屏 { tp_dev.y[i]=((u16)buf[1]<<8)+buf[0]; tp_dev.x[i]=800-(((u16)buf[3]<<8)+buf[2]); } else { tp_dev.x[i]=((u16)buf[1]<<8)+buf[0]; tp_dev.y[i]=((u16)buf[3]<<8)+buf[2]; } } } res=1; t=0; //触发一次,则会最少连续监测10次,从而提高命中率
}
上述代码中比较难理解的是横竖屏部分:
4.3寸TFTLCD屏幕处于竖屏状态时的分辨率为x*y=480*800。此时,缓冲区buf存的变量与坐标为关系如下:
横屏时X,Y轴如下图所示:
4.3寸TFTLCD屏幕处于横屏状态时的分辨率为x*y=800*480。此时,缓冲区buf存的变量与坐标为关系如下:
竖屏时X,Y轴如下所示:
循环读取N个(由状态寄存器可以得知有几个触摸点,这里假设有N个触摸点)触摸点的坐标,代码如下:
temp=0XFF<<(mode&0XF); //将点的个数转换为1的位数,匹配tp_dev.sta定义
tempsta=tp_dev.sta; //保存当前的tp_dev.sta值
tp_dev.sta=(~temp)|TP_PRES_DOWN|TP_CATH_PRES;
for(i=0; i<5; i++)
{ if(tp_dev.sta&(1<<i)) //触摸有效? { GT9147_RD_Reg(GT9147_TPX_TBL[i],buf,4); //读取XY坐标值 if(tp_dev.touchtype&0X01)//横屏 { tp_dev.y[i]=((u16)buf[1]<<8)+buf[0]; tp_dev.x[i]=800-(((u16)buf[3]<<8)+buf[2]); } else { tp_dev.x[i]=((u16)buf[1]<<8)+buf[0]; tp_dev.y[i]=((u16)buf[3]<<8)+buf[2]; } }
}
其中,比较难理解的是tp_dev.sta变量,我们可以在touch.h文件中可以找到该变量的定义:
u8 sta; //笔的状态
//b7:按下1/松开0;
//b6:0,没有按键按下;1,有按键按下.
//b5:保留
//b4~b0:电容触摸屏按下的点数(0,表示未按下,1表示按下)
3. 然后,我们需要确定此次触摸是否为有效触摸,我们触摸不可能瞬时完成,因此我们的手指和触摸屏会有短暂的接触,短暂的接触会造成GT9147不止一次的识别到我们的触摸点,在这个短暂的接触当中,我们只认为第一次触摸为有效触摸,其他触摸均为无效触摸。
GT9147也很智能,当我们手指与屏幕短暂接触的过程中,状态寄存器的接触标志位只会置位一次(也就是说:在短暂接触过程中,当我们读出状态寄存器内的内容并且软件清空状态寄存器之后,状态寄存器的接触指示位就不再置一)。
当状态寄存器的接触标志位为1时,我们就将sta变量值的B7(第7位)置一,代码如下所示:
tp_dev.sta=(~temp)|TP_PRES_DOWN|TP_CATH_PRES;
第一次有效触摸之后(就是前面提及的无效触摸),sta变量的B6(第6位)置一,但是在上述代码中,我们只将sta变量的B7(第7位)是否置一当作“是否为有效触摸的依据“。
下端代码就是保证了一次接触只认当作一次有效触摸:
if((mode&0X8F)==0X80)//无触摸点按下
{ if(tp_dev.sta&TP_PRES_DOWN) //是否进行过有效触摸 { tp_dev.sta&=~(1<<7); //清楚有效触摸标志位 } else //是否为无效触摸,如果是,将所有变量清空 { tp_dev.x[0]=0xffff; tp_dev.y[0]=0xffff; tp_dev.sta&=0XE0; //清除点有效标记 }
}
那有效触摸标志位有什么用呢?
if(tp_dev.sta&(1<<i)) //触摸有效?
{ GT9147_RD_Reg(GT9147_TPX_TBL[i],buf,4); //读取XY坐标值 if(tp_dev.touchtype&0X01)//横屏 { tp_dev.y[i]=((u16)buf[1]<<8)+buf[0]; tp_dev.x[i]=800-(((u16)buf[3]<<8)+buf[2]); } else { tp_dev.x[i]=((u16)buf[1]<<8)+buf[0]; tp_dev.y[i]=((u16)buf[3]<<8)+buf[2]; } //printf("x[%d]:%d,y[%d]:%d\r\n",i,tp_dev.x[i],i,tp_dev.y[i]);
}
这是我们循环读取5个坐标点的代码段,我们看到如果有效触摸,我们才可以读取坐标寄存器中的数据并且赋值给触摸点坐标存储变量。
触摸屏初始化
由于4.3寸TFTLCD的显示ID为0X5510,因此我们只摘取与0X5510有关的初始化代码进行说明:
if(lcddev.id==0X5510) //4.3寸电容触摸屏
{ if(GT9147_Init()==0) //是GT9147 { tp_dev.scan=GT9147_Scan; //扫描函数指向GT9147触摸屏扫描 } else { OTT2001A_Init(); tp_dev.scan=OTT2001A_Scan; //扫描函数指向OTT2001A触摸屏扫描 } tp_dev.touchtype|=0X80; //电容屏 tp_dev.touchtype|=lcddev.dir&0X01;//横屏还是竖屏 return 0;
}
该部分函数的功能为:初始化GT9147触摸屏芯片,将GT9147的扫描函数指针赋值给tp_dex结构体中的scan函数,并且将touchtype屏幕类型标志位置为“电容屏”,根据指定方向设置横竖屏标志位。