我们板子用的时电容式触摸屏
我们用的触摸芯片是GT911(I2C)
INT:触摸时会产生中断,初始化时配置设备地址
我们配置的设备地址为0x28/0x29
一定一定要注意读取触摸芯片的周期不要大于10ms!!!!
可以检测按下的坐标,按下的面积
下面我们用软件模拟I2C的方式驱动触摸芯片
touch_drv.c
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include "gd32f30x.h"
#include "delay.h"
#include "touch_drv.h"
#include "systick.h"
#define GET_I2C_SDA() gpio_input_bit_get(GPIOF, GPIO_PIN_9) // 读取SDA端口
#define SET_I2C_SCL() gpio_bit_set(GPIOF, GPIO_PIN_10) // 时钟线SCL输出高电平
#define CLR_I2C_SCL() gpio_bit_reset(GPIOF, GPIO_PIN_10) // 时钟线SCL输出低电平
#define SET_I2C_SDA() gpio_bit_set(GPIOF, GPIO_PIN_9) // 数据线SDA输出高电平
#define CLR_I2C_SDA() gpio_bit_reset(GPIOF, GPIO_PIN_9) // 数据线SDA输出低电平
static void GpioInit(void)
{
rcu_periph_clock_enable(RCU_GPIOF);
gpio_init(GPIOF, GPIO_MODE_OUT_OD, GPIO_OSPEED_10MHZ, GPIO_PIN_9 | GPIO_PIN_10); // SDA SCL
rcu_periph_clock_enable(RCU_GPIOB);
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_10MHZ, GPIO_PIN_9); // RST
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_10MHZ, GPIO_PIN_12); // INT
}
/**
*******************************************************************
* @function 产生IIC起始时序,准备发送或接收数据前必须由起始序列开始
* @param
* @return
* @brief SCL为高电平时,SDA由高电平向低电平跳变,开始传输数据
* 生成下图所示的波形图,即为起始时序
* 1 2 3 4
* __________
* SCL : __/ \_____
* ________
* SDA : \___________
*******************************************************************
*/
static void I2CStart(void)
{
SET_I2C_SDA(); // 1#数据线SDA输出高电平
SET_I2C_SCL(); // 2#时钟线SCL输出高电平
DelayNus(4); // 延时4us
CLR_I2C_SDA(); // 3#数据线SDA输出低电平
DelayNus(4); // 延时4us
CLR_I2C_SCL(); // 4#时钟线SCL输出低电平,保持I2C的时钟线SCL为低电平,准备发送或接收数据
DelayNus(4); // 延时4us
}
/**
*******************************************************************
* @function 产生IIC停止时序
* @param
* @return
* @brief SCL为高电平时,SDA由低电平向高电平跳变,结束传输数据
* 生成下图所示的波形图,即为停止时序
* 1 2 3 4
* _______________
* SCL : ______/
* __ ____________
* SDA: \______/
*******************************************************************
*/
static void I2CStop(void)
{
CLR_I2C_SDA(); //2#数据线SDA输出低电平
DelayNus(4); //延时4us
SET_I2C_SCL(); //3#时钟线SCL输出高电平
DelayNus(4);
SET_I2C_SDA(); //4#数据线SDA输出高电平,发送I2C总线结束信号
}
/**
*******************************************************************
* @function 发送一字节,数据从高位开始发送出去
* @param byte
* @return
* @brief 下面是具体的时序图
* 1 2 3 4
* ______
* SCL: ________/ \______
* ______________________
* SDA: \\\___________________
*******************************************************************
*/
static void I2CSendByte(uint8_t byte)
{
for (uint8_t i = 0; i < 8; i++) // 循环8次,从高到低取出字节的8个位
{
if ((byte & 0x80)) // 2#取出字节最高位,并判断为‘0’还是‘1’,从而做出相应的操作
{
SET_I2C_SDA(); // 数据线SDA输出高电平,数据位为‘1’
}
else
{
CLR_I2C_SDA(); // 数据线SDA输出低电平,数据位为‘0’
}
byte <<= 1; // 左移一位,次高位移到最高位
DelayNus(4); // 延时4us
SET_I2C_SCL(); // 3#时钟线SCL输出高电平
DelayNus(4); // 延时4us
CLR_I2C_SCL(); // 4#时钟线SCL输出低电平
DelayNus(4); // 延时4us
}
}
/**
*******************************************************************
* @function 读取一字节数据
* @param
* @return 读取的字节
* @brief 下面是具体的时序图
* ______
* SCL: ______/ \___
* ____________________
* SDA: \\\\______________\\\
*******************************************************************
*/
static uint8_t I2CReadByte(void)
{
uint8_t byte = 0; // byte用来存放接收的数据
SET_I2C_SDA(); // 释放SDA
for (uint8_t i = 0; i < 8; i++) // 循环8次,从高到低读取字节的8个位
{
SET_I2C_SCL(); // 时钟线SCL输出高电平
DelayNus(4); // 延时4us
byte <<= 1; // 左移一位,空出新的最低位
if (GET_I2C_SDA()) // 读取数据线SDA的数据位
{
byte++; // 在SCL的上升沿后,数据已经稳定,因此可以取该数据,存入最低位
}
CLR_I2C_SCL(); // 时钟线SCL输出低电平
DelayNus(4); // 延时4us
}
return byte; // 返回读取到的数据
}
/**
*******************************************************************
* @function 等待接收端的应答信号
* @param
* @return 1,接收应答失败;0,接收应答成功
* @brief 当SDA拉低后,表示接收到ACK信号,然后,拉低SCL,
* 此处表示发送端收到接收端的ACK
* _______|____
* SCL: | \_________
* _______|
* SDA: \_____________
*******************************************************************
*/
static bool I2CWaitAck(void)
{
uint8_t errTimes = 0;
SET_I2C_SDA(); // 释放SDA总线,很重要
DelayNus(4); // 延时4us
SET_I2C_SCL(); // 时钟线SCL输出高电平
DelayNus(4); // 延时4us
while (GET_I2C_SDA()) // 读回来的数据如果是高电平,即接收端没有应答
{
errTimes++; // 计数器加1
if (errTimes > 250) // 如果超过250次,则判断为接收端出现故障,因此发送结束信号
{
I2CStop(); // 产生一个停止信号
return false; // 返回值为1,表示没有收到应答信号
}
}
CLR_I2C_SCL(); // 表示已收到应答信号,时钟线SCL输出低电平
DelayNus(4); // 延时4us
return true; // 返回值为0,表示接收应答成功
}
/**
*******************************************************************
* @function 发送应答信号
* @param
* @return
* @brief 下面是具体的时序图
* 1 2 3 4 5
* ______
* SCL: ________/ \____________
* __ ______
* SDA: \___________________/
*******************************************************************
*/
static void I2CSendAck(void)
{
CLR_I2C_SDA(); // 2#数据线SDA输出低电平
DelayNus(4); // 延时4us
SET_I2C_SCL(); // 3#时钟线SCL输出高电平,在SCL上升沿前就要把SDA拉低,为应答信号
DelayNus(4); // 延时4us
CLR_I2C_SCL(); // 4#时钟线SCL输出低电平
DelayNus(4); // 延时4us
SET_I2C_SDA(); // 5#数据线SDA输出高电平,释放SDA总线,很重要
}
/**
*******************************************************************
* @function 发送非应答信号
* @param
* @return
* @brief 下面是具体的时序图
* 1 2 3 4
* ______
* SCL: ________/ \______
* __ ___________________
* SDA: __/
*******************************************************************
*/
static void I2CSendNack(void)
{
SET_I2C_SDA(); // 2#数据线SDA输出高电平
DelayNus(4); // 延时4us
SET_I2C_SCL(); // 3#时钟线SCL输出高电平,在SCL上升沿前就要把SDA拉高,为非应答信号
DelayNus(4); // 延时4us
CLR_I2C_SCL(); // 4#时钟线SCL输出低电平
DelayNus(4); // 延时4us
}
#define TOUCH_I2C_WR 0 // 写控制bit
#define TOUCH_I2C_RD 1 // 读控制bit
/* GT911 部分寄存器定义 */
#define TOUCH_DEV_ADDR 0x28 //设备地址
#define GT911_CTRL_REG 0x8040 // GT911控制寄存器
#define GT911_CFGS_REG 0x8050 // GT911配置起始地址寄存器
#define GT911_PID_REG 0x8140 // GT911产品ID寄存器
#define GT911_STATUS_REG 0x814E // GT911当前检测到的触摸情况
#define GT911_TP1_REG 0x8150 // 第一个触摸点数据地址
#define GT911_TP2_REG 0x8158 // 第二个触摸点数据地址
#define GT911_TP3_REG 0x8160 // 第三个触摸点数据地址
#define GT911_TP4_REG 0x8168 // 第四个触摸点数据地址
#define GT911_TP5_REG 0x8170 // 第五个触摸点数据地址
#define DETECT_INTERVAL_TIME 20 // GT911检测间隔时间要求10ms以上
/**
*******************************************************************
* @function 配置触摸芯片的设备地址 0x28/0x29
* @param
* @return
*******************************************************************
*/
static void ConfigDevAddr(void)
{
gpio_bit_reset(GPIOB, GPIO_PIN_9); // RST拉低,复位GT911
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_12); // INT设为输出
gpio_bit_reset(GPIOB, GPIO_PIN_12);
DelayNms(1); // 延时1毫秒
gpio_bit_set(GPIOB, GPIO_PIN_12); // 拉高INT
DelayNms(1); // 延时1毫秒
gpio_bit_set(GPIOB, GPIO_PIN_9); // RST拉高,释放复位状态
DelayNms(10); // 延时10毫秒
gpio_init(GPIOB, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_12); // INT设为输入
}
/**
*******************************************************************
* @function 向触摸芯片的寄存器写入数据
* @param reg,寄存器地址
* @param pBuffer,数组首地址
* @param numToWrite,要写入的数据个数,不大于256
* @return 写入是否成功
*******************************************************************
*/
static bool WriteTouchReg(uint16_t reg, uint8_t *pBuffer, uint8_t numToWrite)
{
I2CStart();
I2CSendByte(TOUCH_DEV_ADDR | TOUCH_I2C_WR);
if (!I2CWaitAck())
{
goto i2c_err; // 器件无应答
}
I2CSendByte((uint8_t)(reg >> 8) & 0xFF);
if (!I2CWaitAck())
{
goto i2c_err; // 器件无应答
}
I2CSendByte((uint8_t)reg & 0xFF);
if (!I2CWaitAck())
{
goto i2c_err; // 器件无应答
}
for (uint8_t i = 0; i < numToWrite; i++)
{
I2CSendByte(pBuffer[i]);
if (!I2CWaitAck())
{
goto i2c_err; // 器件无应答
}
}
I2CStop();
return true;
i2c_err: // 命令执行失败后,要发送停止信号,避免影响I2C总线上其他设备
I2CStop();
return false;
}
/**
*******************************************************************
* @function 指定地址开始读出指定个数的数据
* @param readAddr,读取地址,0~255
* @param pBuffer,数组首地址
* @param numToRead,要读出的数据个数,不大于256
* @return
*******************************************************************
*/
static bool ReadTouchReg(uint16_t reg, uint8_t *pBuffer, uint8_t numToRead)
{
I2CStart(); // 发送起始信号
I2CSendByte(TOUCH_DEV_ADDR | TOUCH_I2C_WR); // 发送器件地址和读写模式
if (!I2CWaitAck()) // 等待应答
{
goto i2c_err;
}
I2CSendByte((uint8_t)(reg >> 8) & 0xFF);
if (!I2CWaitAck())
{
goto i2c_err; // 器件无应答
}
I2CSendByte((uint8_t)reg & 0xFF);
if (!I2CWaitAck())
{
goto i2c_err; // 器件无应答
}
I2CStart(); // 发送起始信号
I2CSendByte(TOUCH_DEV_ADDR | TOUCH_I2C_RD); // 发送器件地址和读写模式
if (!I2CWaitAck()) // 等待应答
{
goto i2c_err;
}
numToRead--;
while(numToRead--) // 数据未读完
{
*pBuffer++ = I2CReadByte(); // 逐字节读出存放到数据数组
I2CSendAck();
}
*pBuffer = I2CReadByte(); // 最后一个字节发送非应答
I2CSendNack();
I2CStop();
return true;
i2c_err:
I2CStop();
return false;
}
void TouchDrvInit(void)
{
GpioInit();
ConfigDevAddr();
uint8_t id[5];
if (ReadTouchReg(GT911_PID_REG, id, 4))
{
id[4] = '\0';
printf("Touch ID: %s\n", id);
return;
}
printf("Touch init error\n");
}
#if 0
void TouchScan(TouchInfo_t *touchInfo)
{
uint8_t statRegVal;
uint8_t buff[6];
if (!ReadTouchReg(GT911_STATUS_REG, &statRegVal, 1))
{
printf("read GT911_STATUS_REG error\n");
touchInfo->state = UP;
return;
}
if ((statRegVal & 0x80) == 0) // 读取最高位查看是否有按下
{
printf("It has not been touched\n");
touchInfo->state = UP;
return;
}
uint8_t touchNums = statRegVal & 0x0F;
printf("It has been touched, touch nums = %d\n", touchNums);
statRegVal = 0;
WriteTouchReg(GT911_STATUS_REG, &statRegVal, 1); // 清除数据标志位
if (touchNums == 0 || touchNums > TOUCH_POINT_MAX) //如果触摸点为0或>5个
{
touchInfo->state = UP;
return;
}
ReadTouchReg(GT911_TP1_REG, buff, 6); // 读出触摸点x y坐标和面积
touchInfo->point.x = (uint16_t)(buff[1] << 8) | buff[0];
touchInfo->point.y = (uint16_t)(buff[3] << 8) | buff[2];
touchInfo->point.size = (uint16_t)(buff[5] << 8) | buff[4];
// printf("point[%d].x = %d, point[%d].y = %d, point[%d].size = %d\n", \
// 0, touchInfo->point.x, 0, touchInfo->point.y, 0, touchInfo->point.size);
touchInfo->state = DOWN;
return;
}
#endif
/* 扫描电容触摸芯片 GT911,输出最新触摸点信息
* 入口:touchInfo 结构体指针,用于返回坐标/面积/状态
* 特性:带防抖间隔、自动清标志、只处理第 1 指 */
void TouchScan(TouchInfo_t *touchInfo)
{
/* 静态变量:保存上一次触摸状态与系统时间,用于防抖 */
static TouchInfo_t lastTouchInfo = {UP};
static uint64_t lastSysTime = 0;
/* 若距离上次调用时间不足 DETECT_INTERVAL_TIME ms,直接返回旧状态 */
if ((GetSysRunTime() - lastSysTime) < DETECT_INTERVAL_TIME) // 判断两次调用接口函数的时间间隔是否小于DETECT_INTERVAL_TIME ms
{
*touchInfo = lastTouchInfo; //小于触摸芯片的时间间隔返回上一次的状态
return;
}
lastSysTime = GetSysRunTime(); // 记录本次扫描时刻
uint8_t statRegVal;
uint8_t buff[6];
/* 读取状态寄存器,失败则标记为抬起并返回 */
if (!ReadTouchReg(GT911_STATUS_REG, &statRegVal, 1))
{
printf("read GT911_STATUS_REG error\n");
touchInfo->state = UP;
lastTouchInfo = *touchInfo;
return;
}
/* 状态寄存器最高位为0表示无触摸,直接返回 UP */
if ((statRegVal & 0x80) == 0) // 读取最高位查看是否有按下
{
//printf("It has not been touched\n");
touchInfo->state = UP;
lastTouchInfo = *touchInfo;
return;
}
/* 低4位表示当前触摸点数,只做日志打印 */
uint8_t touchNums = statRegVal & 0x0F;
printf("touch nums = %d\n", touchNums);
/* 清状态寄存器(写0清标志),准备下一次中断/查询 */
statRegVal = 0;
WriteTouchReg(GT911_STATUS_REG, &statRegVal, 1);//清除数据标志位
/* 触摸数为0或超过最大支持点数,视为无效,返回 UP */
if (touchNums == 0 || touchNums > TOUCH_POINT_MAX)
{
touchInfo->state = UP;
lastTouchInfo = *touchInfo;
return;
}
/* 读取第1个触摸点寄存器(X/Y/面积共6字节) */
ReadTouchReg(GT911_TP1_REG, buff, 6); // 读出触摸点x y坐标和面积
/* 组合出16位坐标与面积 */
touchInfo->point.x = (uint16_t)(buff[1] << 8) | buff[0];
touchInfo->point.y = (uint16_t)(buff[3] << 8) | buff[2];
touchInfo->point.size = (uint16_t)(buff[5] << 8) | buff[4];
//printf("point[%d].x = %d, point[%d].y = %d, point[%d].size = %d\n", \
0, touchInfo->point.x, 0, touchInfo->point.y, 0, touchInfo->point.size);
/* 标记为按下,并缓存本次结果供下次防抖使用 */
touchInfo->state = DOWN;
lastTouchInfo = *touchInfo;
return;
}
.h
#ifndef _TOUCH_DRV_H_
#define _TOUCH_DRV_H_
#include <stdint.h>
#define TOUCH_POINT_MAX 5
/* 触摸点坐标数据结构 */
typedef struct
{
uint16_t x; // 触摸点X坐标
uint16_t y; // 触摸点Y坐标
uint16_t size; // 触摸点大小
} TouchPoint_t;
#define UP 0
#define DOWN 1
/* 触摸信息结构体 */
typedef struct
{
uint8_t state; // 1:按下 or 0:未按下
TouchPoint_t point;
} TouchInfo_t; //触摸信息结构体
void TouchDrvInit(void);
void TouchScan(TouchInfo_t *touchInfo);
#endif
我们要实现画布的功能
在人机交互文件hmi_app.c里实现
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "rtc_drv.h"
#include "sensor_drv.h"
#include "led_drv.h"
#include "key_drv.h"
#include "touch_drv.h"
#include "lcd_drv.h"
#include "store_app.h"
/**
*******************************************************************
* @function 绘制实心圆
* @param x0、y0:圆心坐标
* @param r:半径
* @param color:颜色
* @return
*******************************************************************
*/
static void DrawSolidCircle(uint16_t x0,uint16_t y0, uint16_t r, uint16_t color)
{
int a = 0;
int b = r;
int di = 3 - (r << 1); // 判断下个点位置的标志
while (a <= b)
{
int i = a, p = b;
while (i > 0)
{
LcdDrawPoint(x0+b,y0-i, color);
LcdDrawPoint(x0-i,y0+b, color);
i--;
}
while ( p > 0)
{
LcdDrawPoint(x0 - a, y0 - p, color);
LcdDrawPoint(x0 - p, y0 - a, color);
LcdDrawPoint(x0 + a, y0 - p, color);
LcdDrawPoint(x0 - p, y0 + a, color);
LcdDrawPoint(x0 + a, y0 + p, color);
LcdDrawPoint(x0 + p, y0 + a, color);
p--;
}
a++;
/* Bresenham算法画圆 */
if (di < 0)
{
di += 4 * a + 6;
}
else
{
di += 10 + 4 * (a - b);
b--;
}
}
LcdDrawPoint(x0, y0, color); // 圆心坐标
}
/**
*******************************************************************
* @function 绘制直线
* @param x0、y0:起点坐标
* @param x1、y1:终点坐标
* @param size:直线粗细
* @param color:颜色
* @return
*******************************************************************
*/
static void DrawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t size, uint16_t color)
{
uint16_t t;
int xerr = 0, yerr = 0, delta_x, delta_y, distance;
int incx, incy, uRow, uCol;
delta_x = x1 - x0; // 计算坐标增量
delta_y = y1 - y0;
uRow = x0;
uCol = y0;
// 设置单步方向
if (delta_x > 0)
{
incx=1;
}
// 垂直线
else if (delta_x == 0)
{
incx=0;
}
else
{
incx = -1;
delta_x = -delta_x;
}
if (delta_y > 0)
{
incy = 1;
}
// 水平线
else if (delta_y == 0)
{
incy = 0;
}
else
{
incy = -1;
delta_y = -delta_y;
}
// 选取基本增量坐标轴
if (delta_x > delta_y)
{
distance = delta_x;
}
else
{
distance = delta_y;
}
// 画线输出
for (t = 0; t <= distance + 1; t++)
{
DrawSolidCircle(uRow, uCol, size, color);
xerr += delta_x ;
yerr += delta_y ;
if (xerr > distance)
{
xerr -= distance;
uRow += incx;
}
if (yerr > distance)
{
yerr -= distance;
uCol += incy;
}
}
}
/**
****************************************************************
* @brief 触摸画板任务:手指/笔触移动时实时绘制轨迹
* @note 1. 首次按下画实心圆作为起点
* 2. 后续移动画线段形成连续轨迹
* 3. 触点面积映射为笔刷粗细
****************************************************************
*/
void CanvasTask(void)
{
/* 静态变量:线条是否已开始、上一帧坐标 */
static bool s_lineFlag = false; // 标记线条是否已开始绘制
static TouchPoint_t s_arrLastPoint; // 上一个点的坐标
/* 当前帧触摸信息清零并扫描 */
TouchInfo_t touchInfoNow;
memset(&touchInfoNow, 0, sizeof(touchInfoNow));
TouchScan(&touchInfoNow);
/* 手指抬起:重置标志,退出本次任务 */
if (touchInfoNow.state == UP)
{
s_lineFlag = false;
return;
}
uint16_t x0, y0, x1, y1, size;
/* 提取起点、终点、笔刷半径 */
x0 = s_arrLastPoint.x;
y0 = s_arrLastPoint.y;
x1 = touchInfoNow.point.x;
y1 = touchInfoNow.point.y;
size = touchInfoNow.point.size;
/* 将触摸面积映射到 1~15 像素笔刷:过大则缩小,过小则保底 */
size = size / 5;
if (0 == size)
{
size = 1;
}
else if (size > 15)
{
size = 15;
}
/* 首次触点:画实心圆作为“落笔”起点 */
if (!s_lineFlag)
{
DrawSolidCircle(x1, y1, size, YELLOW); // 线条第一个点用画点方式
s_lineFlag = true; // 标记线条已经开始绘制
}
else
{
/* 非首次:从前一坐标到当前坐标画线段,形成连续轨迹 */
DrawLine(x0, y0, x1, y1, size, YELLOW); // 后边的用画线方式
}
/* 保存当前坐标,供下一帧画线使用 */
s_arrLastPoint.x = touchInfoNow.point.x;
s_arrLastPoint.y = touchInfoNow.point.y;
}
/**
***********************************************************
* @brief 人机交互任务处理函数
* @param
* @return
***********************************************************
*/
void HmiTask(void)
{
CanvasTask();
// TouchInfo_t touchInfoNow;
// memset(&touchInfoNow, 0, sizeof(touchInfoNow));
// TouchScan(&touchInfoNow);
uint8_t keyVal;
keyVal = GetKeyVal();
switch (keyVal)
{
case KEY1_SHORT_PRESS:
TurnOnLed(LED1);
if (SetModbusParam(2))
{
printf("SetModbusParam sucess\n");
}
else
{
printf("SetModbusParam fail\n");
}
break;
case KEY1_LONG_PRESS:
TurnOffLed(LED1);
break;
case KEY2_SHORT_PRESS:
TurnOnLed(LED2);
break;
case KEY2_LONG_PRESS:
TurnOffLed(LED2);
break;
case KEY3_SHORT_PRESS:
TurnOnLed(LED3);
break;
case KEY3_LONG_PRESS:
TurnOffLed(LED3);
break;
default:
break;
}
}