45. 电容式触摸屏
一、电容式触摸屏简介
电容式触摸屏 是利用人体感应进行触点检测控制,不需要直接接触或只需要轻微接触,通过检测感应电流来定位触摸坐标。
电容式触摸屏主要分为两种:表面电容式电容触摸屏 和 投射式电容触摸屏。
表面电容式触摸屏 技术是利用 ITO(铟锡氧化物,是一种透明的导电材料)导电膜,通过电场感应方式感测屏幕表面的触摸行为进行。但是表面电容式触摸屏有一些局限性,它只能识别一个手指或者一次触摸。
投射电容式触摸屏 是传感器利用触摸屏电极发射出静电场线。一般用于投射电容传感技术的电容类型有两种:自我电容 和 交互电容。
自我电容 又称 绝对电容,是最广为采用的一种方法,自我电容通常是指扫描电极与地构成的电容。在玻璃表面有用 ITO 制成的横向与纵向的扫描电极,这些电极和地之间就构成一个电容的两极。当用手或触摸笔触摸的时候就会并联一个电容到电路中去,从而使在该条扫描线上的总体的电容量有所改变。在扫描的时候,控制 IC 依次扫描纵向和横向电极,并根据扫描前后的电容变化来确定触摸点坐标位置。笔记本电脑触摸输入板就是采用的这种方式,笔记本电脑的输入板采用 X * Y 的传感电极阵列形成一个传感格子,当手指靠近触摸输入板时,在手指和传感电极之间产生一个小量电荷。采用特定的运算法则处理来自行、列传感器的信号来确定手指的位置。
交互电容 又叫做 跨越电容,它是在玻璃表面的横向和纵向的 ITO 电极的交叉处形成电容。交互电容的扫描方式就是扫描每个交叉处的电容变化,来判定触摸点的位置。当触摸的时候就会影响到相邻电极的耦合,从而改变交叉处的电容量,交互电容的扫面方法可以侦测到每个交叉点的电容值和触摸后电容变化,因而它需要的扫描时间与自我电容的扫描方式相比要长一些,需要扫描检测 X * Y 根电极。
投射式电容触摸屏采用纵横两列电极组成感应矩阵,来感应触摸。以两个交叉的电极矩阵,即:X 轴电极和 Y 轴电极,来检测每一格感应单元的电容变化。
电容触摸屏的优点:手感好、无需校准、支持多点触摸、透光性好。
电容触摸屏的缺点:成本高、精度不高、抗干扰能力差。容触摸屏对工作环境的要求是比较高的,在潮湿、多尘、高低温环境下面,都是不适合使用电容屏的。
电容式触摸屏 一般都需要一个 驱动 IC 来检测电容触摸。
二、触摸控制原理
电容式触摸屏基本原理和是利用充电时间检测电容大小,从而通过检测出电容值的变化来获知触摸信号。电容屏的最上层是玻璃(不会像电阻屏那样形变), 核心层部分也是由 ITO 材料构成的,这些导电材料在屏幕里构成了人眼看不见的静电网,静电网由多行 X 轴电极和多列 Y 轴电极构成,两个电极之间会形成电容。
触摸屏工作时,X 轴电极发出 AC 交流信号,而交流信号能穿过电容,即通过 Y 轴能感应出该信号,当交流电穿越时电容会有充放电过程,检测该充电时间可获知电容量。若手指触摸屏幕,会影响触摸点附近两个电极之间的耦合,从而改变两个电极之间的电容量,若检测到某电容的电容量发生了改变,即可获知该电容处有触摸动作(这就是为什么它被称为电容式触摸屏以及绝缘体触摸没有反应的原因)。
X 轴电极与 Y 轴电极在交叉处形成电容,即这两组电极构成了电容的两极,这样的结构覆盖了整个电容屏,每个电容单元在触摸屏中都有其特定的物理位置,即电容的位置就是它在触摸屏的 XY 坐标。检测触摸的坐标时,第 1 条 X 轴的电极发出激励信号,而所有 Y 轴的电极同时接收信号,通过检测充电时间可检测出各个 Y 轴与第 1 条 X 轴相交的各个互电容的大小,各个 X 轴依次发出激励信号,重复上述步骤,即可得到整个触摸屏二维平面的所有电容大小。当手指接近时,会导致局部电容改变,根据得到的触摸屏电容量变化的二维数据表,可以得知每个触摸点的坐标,因此电容触摸屏支持多点触控。
三、GT9147控制芯片
电容触摸屏一般都需要一个驱动 IC 来检测电容触摸,不同型号感应通道和驱动通道数量都不一样,详看数据手册,但是这些驱动 IC 驱动方式(一般为 I2C 通信)都类似,这里我们以 GT9147 为例给大家做介绍。
GT9147 与 MCU 通过 4 根线连接:SDA、SCL、RST 和 INT。GT9147 的 I2C 地址,可以是 0x14 或者 0x5D,当复位结束后的 5ms 内,如果 INT 是高电平,则使用 0x14 作为地址,否则使用 0x5D 作为地址。
上电复位后,GT9147 芯片需要通过外部主控芯片加载寄存器配置,设定它的工作模式,这些配置通过 I2C 信号线传输到 GT9147, 它的配置寄存器地址都由两个字节来表示。
【1】、控制命令寄存器
对该寄存器写入不同值,实现不同的控制。一般硬复位后,需要写 2 进行软复位,然后写 0 结束软复位。
【2】、产品 ID 寄存器
产品 ID 寄存器由 4 个寄存器组成,用于保存产品 ID。对于 GT9147 来说,这 4 个寄存器读出来就是 9,1,4,7 四个字符(ASCII 码格式)。
【3】、状态寄存器
Bit7: Buffer status,1 表示坐标(或按键)已经准备好,主控可以读取;0 表示未就绪,数据无效。当主控读取完坐标后,必须通过 I2C 将此标志(或整个字节)写为 0。
Bit3 ~ O:Number of touch points,屏上的坐标点个数。
【4】、坐标数据寄存器
共分为 5 组,每组 6 个寄存器存储数据,以触点 1 的坐标数据寄存器组为例。
一般只用触点的 (x, y) 坐标,所以只需要读取 0x8150 ~ 0x8153 的数据,组合即可得到触点坐标。其他 4 组分别是:0x8158、0x8160、0x8168和 0x8170 等开头的 16 个寄存器组成,分别针对触点 2 ~ 4 的坐标。设备支持寄存器地址自增,我们只需要发送寄存器组的首地址,然后连续读取即可,地址自增,从而提高读取速度。
四、原理图
五、程序源码
5.1、模式I2C驱动
#define I2C_SCL_GPIO_PORT GPIOB
#define I2C_SCL_GPIO_PIN GPIO_PIN_0
#define I2C_SCL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define I2C_SDA_GPIO_PORT GPIOF
#define I2C_SDA_GPIO_PIN GPIO_PIN_11
#define I2C_SDA_GPIO_CLK_ENABLE() __HAL_RCC_GPIOF_CLK_ENABLE()
#define I2C_SCL(x) do{ x ? \
HAL_GPIO_WritePin(I2C_SCL_GPIO_PORT,I2C_SCL_GPIO_PIN, GPIO_PIN_SET):\
HAL_GPIO_WritePin(I2C_SCL_GPIO_PORT,I2C_SCL_GPIO_PIN, GPIO_PIN_RESET);\
}while(0)
#define I2C_SDA(x) do{ x ? \
HAL_GPIO_WritePin(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN, GPIO_PIN_SET):\
HAL_GPIO_WritePin(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN, GPIO_PIN_RESET);\
}while(0)
#define I2C_READ_SDA() HAL_GPIO_ReadPin(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN)
#define I2C_DELAY() Delay_us(10)
/**
* @brief 模拟I2C初始化函数
*
*/
void I2C_Simulate_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能I2C的SCL和SDA引脚对应的GPIO时钟
I2C_SCL_GPIO_CLK_ENABLE();
I2C_SDA_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = I2C_SCL_GPIO_PIN; // I2C SCL引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出模式
GPIO_InitStruct.Pull = GPIO_PULLUP; // 使用上拉电阻
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 输出速度
HAL_GPIO_Init(I2C_SCL_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = I2C_SDA_GPIO_PIN; // I2C SCL引脚
HAL_GPIO_Init(I2C_SDA_GPIO_PORT, &GPIO_InitStruct);
// 空闲时,I2C总线SCL为高电平,I2C SDA为高电平
I2C_SCL(1);
I2C_SDA(1);
}
/**
* @brief I2C产生起始信号函数
*
* @note SCL为高电平期间,SDA从高电平往低电平跳变
*/
void I2C_Simulate_Start(void)
{
// 1、释放SDA和SCL,并延迟,空闲状态
I2C_SDA(1);
I2C_SCL(1);
I2C_DELAY();
// 2、拉低SDA,SDA产生下降沿,并延迟
I2C_SDA(0);
I2C_DELAY();
// 3、钳住SCL总线,准备发送数据/接收数据,并延时
I2C_SCL(0);
I2C_DELAY();
}
/**
* @brief I2C产生结束信号函数
*
* @note SCL为高电平期间,SDA从低电平往高电平跳变
*/
void I2C_Simulate_Stop(void)
{
// 1、SDA拉低,SCL拉高,并延迟
I2C_SDA(0);
I2C_SCL(1);
I2C_DELAY();
// 2、拉高SDA,产生上升沿,并延迟
I2C_SDA(1);
I2C_DELAY();
}
/**
* @brief 主机检测应答信号
*
* @return uint8_t 0,接收应答成功;1,接收应答失败
*/
uint8_t I2C_Simulate_WaitAck(void)
{
uint8_t waitTime = 0;
uint8_t ack = 0;
// 1、主机释放SDA数据线并延迟,此时外部器件可以拉低SDA线
I2C_SDA(1);
I2C_DELAY();
// 2、主机拉高SCL,此时从机可以返回ACK
I2C_SCL(1);
I2C_DELAY();
// 3、SCL高电平期间主机读取SDA状态,等待应答
while (I2C_READ_SDA())
{
// 4、如果超时的话,就直接产生结束信号,非应答
waitTime++;
if (waitTime > 250)
{
I2C_Simulate_Stop();
ack = 1;
break;
}
}
// 5、SCL=0,结束ACK检查
I2C_SCL(0);
I2C_DELAY();
// 6、返回是否接收到应答信号
return ack;
}
/**
* @brief 发送应答信号或非应答信号
*
* @param ack 0,发送应答信号;1,发送非应答信号
*/
void I2C_Simulate_SendAck(uint8_t ack)
{
// 1、拉低SDA,表示应答,拉高SDA,表示非应答,并延迟
I2C_SDA(ack);
I2C_DELAY();
// 2、主机拉高SCL线,并延迟,确保从机能有足够时间去接收SDA线上的电平信号
I2C_SCL(1);
I2C_DELAY();
// 3、主机拉低时钟线并延时,完成这一位数据的传送
I2C_SCL(0);
I2C_DELAY();
// 4、释放SDA线,并延迟
I2C_SDA(1);
I2C_DELAY();
}
/**
* @brief I2C读取一个字节函数
*
* @param ack 0,发送应答信号,1,发送非应答信号
* @return uint8_t
*/
uint8_t I2C_Simulate_ReadOneByte(uint8_t ack)
{
uint8_t receive = 0;
// 1、主机释放SDA
I2C_SDA(1);
for (uint8_t i = 0; i < 8; i++)
{
// 2、释放SCL,主机将在SCL高电平期间读取数据位
I2C_SCL(1);
I2C_DELAY();
// 3、读取SDA
if (I2C_READ_SDA())
{
receive |= 0x80 >> i;
}
// 4、拉低SCL,从机切换SDA线输出数据
I2C_SCL(0);
I2C_DELAY();
}
// 5、发送应答信号或非应答信号
I2C_Simulate_SendAck(ack);
// 6、返回读取的数据
return receive;
}
/**
* @brief I2C发送一个字节函数
*
* @param data 待发送的数据
*/
void I2C_Simulate_SendOneByte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
// 1、发送数据位的高位
I2C_SDA((data & 0x80) >> 7);
I2C_DELAY();
// 2、释放SCL,从机将在SCL高电平期间读取数据位
I2C_SCL(1);
I2C_DELAY();
// 3、拉低SCL
I2C_SCL(0);
// 4、数据左移一位,用于下次发送
data <<= 1;
}
// 5、发送完成,主机释放SDA线
I2C_SDA(1);
}
5.2、GT9147初始化
#define TOUCH_RESET_GPIO_PORT GPIOC
#define TOUCH_RESET_GPIO_PIN GPIO_PIN_3
#define RCC_TOUCH_RESET_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define TOUCH_INT_GPIO_PORT GPIOB
#define TOUCH_INT_GPIO_PIN GPIO_PIN_1
#define RCC_TOUCH_INT_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define TOUCH_RESETS(x) do{ x ? \
HAL_GPIO_WritePin(TOUCH_RESET_GPIO_PORT, TOUCH_RESET_GPIO_PIN, GPIO_PIN_SET):\
HAL_GPIO_WritePin(TOUCH_RESET_GPIO_PORT, TOUCH_RESET_GPIO_PIN, GPIO_PIN_RESET);\
}while(0)
#define TOUCH_PEN(x) do{ x ? \
HAL_GPIO_WritePin(TOUCH_INT_GPIO_PORT, TOUCH_INT_GPIO_PIN, GPIO_PIN_SET):\
HAL_GPIO_WritePin(TOUCH_INT_GPIO_PORT, TOUCH_INT_GPIO_PIN, GPIO_PIN_RESET);\
}while(0)
/* GT9147部分寄存器定义 */
#define GT9147_CTRL_REGISTER 0X8040 // GT9147控制寄存器
#define GT9147_CFGS_REGISTER 0X8047 // GT9147配置起始地址寄存器
#define GT9147_CHECK_REGISTER 0X80FF // GT9147校验和寄存器
#define GT9147_PID_REGISTER 0X8140 // GT9147产品ID寄存器
#define GT9147_GSTID_REGISTER 0X814E // GT9147当前检测到的触摸情况
#define GT9147_TP1_REGISTER 0X8150 // 第一个触摸点数据地址
#define GT9147_TP2_REGISTER 0X8158 // 第二个触摸点数据地址
#define GT9147_TP3_REGISTER 0X8160 // 第三个触摸点数据地址
#define GT9147_TP4_REGISTER 0X8168 // 第四个触摸点数据地址
#define GT9147_TP5_REGISTER 0X8170 // 第五个触摸点数据地址
/**
* @brief 触摸屏初始化
*
*/
void Touch_Init(void)
{
uint8_t temp[5] = {0};
GPIO_InitTypeDef GPIO_InitStruct = {0};
RCC_TOUCH_RESET_GPIO_CLK_ENABLE();
RCC_TOUCH_PEN_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = TOUCH_RESET_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(TOUCH_RESET_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = TOUCH_INT_GPIO_PIN;
HAL_GPIO_Init(TOUCH_INT_GPIO_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(TOUCH_INT_GPIO_PORT, TOUCH_INT_GPIO_PIN, GPIO_PIN_SET);
// 通过RESET引脚和INT引脚决定器件地址,0x5D
TOUCH_RESETS(0);
HAL_Delay(10);
TOUCH_RESETS(1);
HAL_Delay(10);
// INT引脚转为浮空输入
GPIO_InitStruct.Pin = TOUCH_INT_GPIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(TOUCH_INT_GPIO_PORT, &GPIO_InitStruct);
// 软件复位
temp[0] = 0x02;
GT9147_WriteRegister(GT9147_CTRL_REGISTER, temp, 1);
HAL_Delay(10);
// 正常读取坐标
temp[0] = 0x00;
GT9147_WriteRegister(GT9147_CTRL_REGISTER, temp, 1);
}
5.3、向GT9147中读写数据
#define GT9147_DEVICE_ADDRESS 0x5D
/**
* @brief GT9147写寄存器函数
*
* @param registerAddress 寄存器地址
* @param data 要写入的数据
* @param length 要写入的数据长度
*/
void GT9147_WriteRegister(uint16_t registerAddress, uint8_t *data, uint8_t length)
{
int ack = 0;
I2C_Simulate_Start(); // 发送起始信号
I2C_Simulate_SendOneByte(GT9147_DEVICE_ADDRESS << 1); // 发送写设备地址
I2C_Simulate_WaitAck(); // 等待应答
I2C_Simulate_SendOneByte(registerAddress >> 8); // 发送寄存器地址高8位
I2C_Simulate_WaitAck(); // 等待应答
I2C_Simulate_SendOneByte(registerAddress & 0xFF); // 发送寄存器地址低8位
I2C_Simulate_WaitAck(); // 等待应答
for (uint8_t i = 0; i < length; i++)
{
I2C_Simulate_SendOneByte(data[i]); // 发送数据
ack = I2C_Simulate_WaitAck(); // 等待应答
if (ack)
{
break;
}
}
I2C_Simulate_Stop(); // 发送停止信号
}
/**
* @brief 从GT9147的寄存器读取数据
*
* @param registerAddress 寄存器地址
* @param data 保存读取数据的缓冲区
* @param length 要读取数据的长度
*/
void GT9147_ReadRegister(uint8_t registerAddress, uint8_t *data, uint8_t length)
{
I2C_Simulate_Start(); // 发送起始信号
I2C_Simulate_SendOneByte(GT9147_DEVICE_ADDRESS << 1); // 发送写设备地址
I2C_Simulate_WaitAck(); // 等待应答
I2C_Simulate_SendOneByte(registerAddress >> 8); // 发送寄存器地址高8位
I2C_Simulate_WaitAck(); // 等待应答
I2C_Simulate_SendOneByte(registerAddress & 0xFF); // 发送寄存器地址低8位
I2C_Simulate_WaitAck(); // 等待应答
I2C_Simulate_Start(); // 重新发送起始信号
I2C_Simulate_SendOneByte((GT9147_DEVICE_ADDRESS << 1) | 1); // 发送读设备地址
I2C_Simulate_WaitAck(); // 等待应答
for (uint8_t i = 0; i < length; i++)
{
data[i] = I2C_Simulate_ReadOneByte((i == length - 1) ? 0 : 1); // 读数据
}
I2C_Simulate_Stop(); // 发送停止信号
}
5.4、读取触摸点位置
/**
* @brief 触摸扫描函数
*
*/
void Touch_Scan(void)
{
uint8_t mode = 0;
uint8_t i = 0;
uint8_t data[4] = {0};
uint16_t x = 0, y = 0;
GT9147_ReadRegister(GT9147_GSTID_REGISTER, &mode, 1); // 读取触摸状态
if ((mode & 0x80) && ((mode & 0x0F) <= 5))
{
// 清除标记
i = 0;
GT9147_WriteRegister(GT9147_GSTID_REGISTER, &i, 1);
}
if ((mode & 0x0F) && ((mode & 0x0F) <= 5))
{
GT9147_ReadRegister(GT9147_TP1_REGISTER, data, 4);
x = ((uint16_t)data[1] << 8) + data[0];
y = ((uint16_t)data[3] << 8) + data[2];
LCD_Printf(0, 0, 32, BLUE, WHITE, "x = %-5d", x);
LCD_Printf(0, 0, 32, BLUE, WHITE, "y = %-5d", y);
}
}