【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#

第三十九章 触摸屏实验

本章,我们将介绍如何使用STM32F1来驱动触摸屏,正点原子战舰STM32F103本身并没有触摸屏控制器,但是它支持触摸屏,可以通过外接带触摸屏的LCD模块(比如正点原子 TFTLCD模块),来实现触摸屏控制。在本章中,我们将向大家介绍STM32控制正点原子TFTLCD模块(包括电阻触摸与电容触摸),实现触摸屏驱动,最终实现一个手写板的功能。
本章分为如下几个部分:
39.1触摸屏简介
39.2硬件设计
39.3程序设计
39.4下载验证

39.1 触摸屏简介

触摸屏是在显示屏的基础上,在屏幕或屏幕上方分布一层与屏幕大小相近的传感器形成的组合器件。触摸和显示功能由软件控制,可以独立也可以组合实现,用户可以通过侦测传感器的触点再配合相应的软件实现触摸效果。目前最常用的触摸屏有两种:电阻式触摸屏与电容式触摸屏。下面,我们来分别介绍。
39.1.1 电阻式触摸屏
正点原子2.4/2.8/3.5寸TFTLCD模块自带的触摸屏都属于电阻式触摸屏,下面简单介绍下电阻式触摸屏的原理。
电阻触摸屏的主要部分是一块与显示器表面非常贴合的电阻薄膜屏,这是一种多层的复合薄膜,具体结构如下图39.1.1.1所示。
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第1张图片

图39.1.1.1 电阻触摸屏多层结构图
表面硬涂层起保护作用,主要是一层外表面硬化处理、光滑防擦的塑料层。玻璃底层用于支撑上面的结构,主要是玻璃或者塑料平板。透明隔离点用来分离开外层ITO和内层ITO。ITO层是触摸屏关键结构,是涂有铟锡金属氧化物的导电层。还有一个结构上图没有标出,就是PET层。PET层是聚酯薄膜,处于外层ITO和表面硬涂层之间,很薄很有弹性,触摸时向下弯曲,使得ITO层接触。
当手指触摸屏幕时,两个ITO层在触摸点位置就有接触,电阻发生变化,在X和Y两个方向上产生电信号,然后送到触摸屏控制器,具体情况如下图39.1.1.2所示。触摸屏控制器侦测到这一接触并计算出X和Y方向上的AD值,简单来讲,电阻触摸屏将触摸点(X,Y)的物理位置转换为代表X坐标和Y坐标的电压值。单片机与触摸屏控制器进行通信获取到AD值,通过一定比例关系运算,获得X和Y轴坐标值。
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第2张图片

图39.1.1.2 电阻式触摸屏的触点坐标结构
电阻触摸屏的优点:精度高、价格便宜、抗干扰能力强、稳定性好。
电阻触摸屏的缺点:容易被划伤、透光性不太好、不支持多点触摸。
从以上介绍可知,触摸屏都需要一个AD转换器,一般来说是需要一个控制器的。正点原子 TFTLCD模块选择的是四线电阻式触摸屏,这种触摸屏的控制芯片有很多,包括:ADS7543、ADS7846、TSC2046、XPT2046和HR2046等。这几款芯片的驱动基本上是一样的,也就是你只要写出了XPT2046的驱动,这个驱动对其他几个芯片也是有效的。而且封装也有一样的,完全PIN-TO-PIN兼容。所以在替换起来,很方便。
正点原子TFTLCD模块自带的触摸屏控制芯片为XPT2046或HR2046。这里以XPT2046作为介绍。XPT2046是一款4导线制触摸屏控制器,使用的是SPI通信接口,内含12位分辨率125KHz转换速率逐步逼近型A/D转换器。XPT2046支持从1.5V到5.25V的低电压I/O接口。XPT2046能通过执行两次A/D转换(一次获取X位置,一次获取Y位置)查出被按的屏幕位置,除此之外,还可以测量加在触摸屏上的压力。内部自带2.5V参考电压可以作为辅助输入、温度测量和电池监测模式之用,电池监测的电压范围可以从0V到6V。XPT2046片内集成有一个温度传感器。在2.7V的典型工作状态下,关闭参考电压,功耗可小于0.75mW。
XPT2046的驱动方法也是很简单,主要看懂XPT2046通信时序图,如下图39.1.1.3所示。
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第3张图片

图39.1.1.3 XPT2046通信时序图
依照时序图,就可以很好写出这个通信代码,上图具体过程:拉低片选,选中器件发送命令字清除BUSY读取16位数据(高12位数据有效即转换的AD值)拉高片选,结束操作。这里的难点就是需要搞清楚命令字该发送什么?只要搞清楚发送什么数值,就可以获取到AD值。命令字的详情如下图39.1.1.4所示:
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第4张图片

图39.1.1.4 命令字详情图
位7,开始位,置1即可。位3,为了提供精度,MODE位清0选择12位分辨率。位2,是进行工作模式选择,为了达到最佳性能,首选差分工作模式即该位清0即可。位1-0是功耗相关的,直接清0即可。而位6-4的值要取决于工作模式,在确定了差分功能模式后,通道选择位也确定了,如图39.1.1.5所示。
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第5张图片

图39.1.1.5 差分模式输入配置图(SER/DFR=0)
从上图,就可以知道:当我们需要检测Y轴位置时,A2A1A0赋值为001;检测X轴位置时,A2A1A0赋值为101。结合前面对其他位的赋值,在X,Y方向与屏幕相同的情况下,命令字0xD0就是读取X坐标AD值,0x90就是读取Y坐标的AD值。假如X,Y方向与屏幕相反,0x90就是读取X坐标的AD值,而0xD0就是读取Y坐标的AD值。
关于这个芯片其他的功能,也可以参考芯片的datasheet。
电阻式触摸屏就介绍到这里。
39.1.2 电容式触摸屏
现在几乎所有智能手机,包括平板电脑都是采用电容屏作为触摸屏,电容屏是利用人体感应进行触点检测控制,不需要直接接触或只需要轻微接触,通过检测感应电流来定位触摸坐标。正点原子4.3/7寸TFTLCD模块自带的触摸屏采用的是电容式触摸屏,下面简单介绍下电容式触摸屏的原理。
电容式触摸屏主要分为两种:
1、表面电容式电容触摸屏。
表面电容式触摸屏技术是利用ITO(铟锡氧化物,是一种透明的导电材料)导电膜,通过电场感应方式感测屏幕表面的触摸行为进行。但是表面电容式触摸屏有一些局限性,它只能识别一个手指或者一次触摸。
2、投射式电容触摸屏。
投射电容式触摸屏是传感器利用触摸屏电极发射出静电场线。一般用于投射电容传感技术的电容类型有两种:自我电容和交互电容。
自我电容又称绝对电容,是最广为采用的一种方法,自我电容通常是指扫描电极与地构成的电容。在玻璃表面有用ITO制成的横向与纵向的扫描电极,这些电极和地之间就构成一个电容的两极。当用手或触摸笔触摸的时候就会并联一个电容到电路中去,从而使在该条扫描线上的总体的电容量有所改变。在扫描的时候,控制IC依次扫描纵向和横向电极,并根据扫描前后的电容变化来确定触摸点坐标位置。笔记本电脑触摸输入板就是采用的这种方式,笔记本电脑的输入板采用XY的传感电极阵列形成一个传感格子,当手指靠近触摸输入板时,在手指和传感电极之间产生一个小量电荷。采用特定的运算法则处理来自行、列传感器的信号来确定手指的位置。
交互电容又叫做跨越电容,它是在玻璃表面的横向和纵向的ITO电极的交叉处形成电容。交互电容的扫描方式就是扫描每个交叉处的电容变化,来判定触摸点的位置。当触摸的时候就会影响到相邻电极的耦合,从而改变交叉处的电容量,交互电容的扫面方法可以侦测到每个交叉点的电容值和触摸后电容变化,因而它需要的扫描时间与自我电容的扫描方式相比要长一些,需要扫描检测X
Y根电极。目前智能手机/平板电脑等的触摸屏,都是采用交互电容技术。
正点原子所选择的电容触摸屏,也是采用的是投射式电容屏(交互电容类型),所以后面仅以投射式电容屏作为介绍。
透射式电容触摸屏采用纵横两列电极组成感应矩阵,来感应触摸。以两个交叉的电极矩阵,即:X轴电极和Y轴电极,来检测每一格感应单元的电容变化,如图39.1.2.1所示:
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第6张图片

图39.1.2.1投射式电容屏电极矩阵示意图
示意图中的电极,实际是透明的,这里是为了方便大家理解。图中,X、Y轴的透明电极电容屏的精度、分辨率与X、Y轴的通道数有关,通道数越多,精度越高。以上就是电容触摸屏的基本原理,接下来看看电容触摸屏的优缺点:
电容触摸屏的优点:手感好、无需校准、支持多点触摸、透光性好。
电容触摸屏的缺点:成本高、精度不高、抗干扰能力差。
这里特别提醒大家电容触摸屏对工作环境的要求是比较高的,在潮湿、多尘、高低温环境下面,都是不适合使用电容屏的。
电容触摸屏一般都需要一个驱动IC来检测电容触摸,正点原子的电容触摸屏使用的是IIC接口输出触摸数据的触摸芯片。正点原子7’TFTLCD模块的电容触摸屏,采用的是15*10的驱动结构(10个感应通道,15个驱动通道),采用的是GT911/FT5206作为驱动IC。正点原子4.3’TFTLCD模块采用的驱动IC是:GT9xxx(GT9147/GT917S/GT911/GT1151/GT9271),不同型号感应通道和驱动通道数量都不一样,详看数据手册,但是这些驱动IC驱动方式都类似,这里我们以GT9147为例给大家做介绍,其他的大家参考着学习即可。
GT9147与MCU通过4根线连接:SDA、SCL、RST和INT。GT9147的IIC地址,可以是0X14或者0X5D,当复位结束后的5ms内,如果INT是高电平,则使用0X14作为地址,否则使用0X5D作为地址,具体的设置过程,请看:GT9147数据手册.pdf这个文档。本章我们使用0X14作为器件地址(不含最低位,换算成读写命令则是读:0X29,写:0X28),接下来,介绍一下GT9147的几个重要的寄存器。
1,控制命令寄存器(0X8040)
该寄存器可以写入不同值,实现不同的控制,我们一般使用0和2这两个值,写入2,即可软复位GT9147。在硬复位之后,一般要往该寄存器写2,实行软复位。然后,写入0,即可正常读取坐标数据(并且会结束软复位)。
2,配置寄存器组(0X8047~0X8100)
这里共186个寄存器,用于配置GT9147的各个参数,这些配置一般由厂家提供给我们(一个数组),所以我们只需要将厂家给我们的配置,写入到这些寄存器里面,即可完成GT9147的配置。由于GT9147可以保存配置信息(可写入内部FLASH,从而不需要每次上电都更新配置),我们有几点注意的地方提醒大家:1,0X8047寄存器用于指示配置文件版本号,程序写入的版本号,必须大于等于GT9147本地保存的版本号,才可以更新配置。2,0X80FF寄存器用于存储校验和,使得0X8047~0X80FF之间所有数据之和为0。3,0X8100用于控制是否将配置保存在本地,写0,则不保存配置,写1则保存配置。
3,产品ID寄存器(0X8140~0X8143)
这里总共由4个寄存器组成,用于保存产品ID,对于GT9147,这4个寄存器读出来就是:9,1,4,7四个字符(ASCII码格式)。因此,我们可以通过这4个寄存器的值,来判断驱动IC的型号,以便执行不同的初始化。
4,状态寄存器(0X814E)
该寄存器各位描述如表表39.1.2.1所示:
寄存器 bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
0X814E buffer状态 大点 接近有效 按键 有效触点个数
表39.1.2.1 状态寄存器各位描述
这里,我们仅关心最高位和最低4位,最高位用于表示buffer状态,如果有数据(坐标/按键),buffer就会是1,最低4位用于表示有效触点的个数,范围是:0~5,0,表示没有触摸,5表示有5点触摸。最后,该寄存器在每次读取后,如果bit7有效,则必须写0,清除这个位,否则不会输出下一次数据!!这个要特别注意!!!
5,坐标数据寄存器(共30个)
这里共分成5组(5个点),每组6个寄存器存储数据,以触点1的坐标数据寄存器组为例,如表39.1.2.2所示:
寄存器 bit7~0 寄存器 bit7~0
0X8150 触点1 x坐标低8位 0X8151 触点1 x坐标高8位
0X8152 触点1 y坐标低8位 0X8153 触点1 y坐标高8位
0X8154 触点1触摸尺寸低8位 0X8155 触点1触摸尺寸高8位
表39.1.2.2 触点1坐标寄存器组描述
我们一般只用到触点的x,y坐标,所以只需要读取0X81500X8153的数据,组合即可得到触点坐标。其他4组分别是:0X8158、0X8160、0X8168和0X8170等开头的16个寄存器组成,分别针对触点24的坐标。同样GT9147也支持寄存器地址自增,我们只需要发送寄存器组的首地址,然后连续读取即可,GT9147会自动地址自增,从而提高读取速度。
GT9147相关寄存器的介绍就介绍到这里,更详细的资料,请参考:GT9147编程指南.pdf这个文档。
GT9147只需要经过简单的初始化就可以正常使用了,初始化流程:硬复位延时10ms结束硬复位设置IIC地址延时100ms软复位更新配置(需要时)结束软复位。此时GT9147即可正常使用了。然后,我们不停的查询0X814E寄存器,判断是否有有效触点,如果有,则读取坐标数据寄存器,得到触点坐标。特别注意,如果0X814E读到的值最高位为1,就必须对该位写0,否则无法读到下一次坐标数据。
电容式触摸屏部分,就介绍到这里。
39.1.3 触摸控制原理
前面已经简单地介绍了电阻屏和电容屏的原理,并且知道了不同类型的触摸屏其实是屏幕+触摸传感器组成。那么这里就会有两组相互独立的参数:屏幕坐标和触摸坐标。要实现触摸功能,就是要把触摸点和屏幕坐标对应起来。
我们以LCD显示屏为例,我们知道屏幕的扫描方向是可以编程设定的,而触摸点,在触摸传感器安装好后,AD值的变化向方向则是固定的,我们以最常见的屏幕坐标方向:先从左到右,再从上到下扫描为例,此时,屏幕坐标和触点AD的坐标有类似的规律:从坐标原点出发,水平方向屏幕坐标增加时,AD值的X方向也增加;屏幕坐标的Y方向坐标增加,AD值的Y方向也增加;坐标减少时对应的关系也类似,可以用图39.1.3.1的示意图来表示这种关系:
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第7张图片

图39.1.3.1 屏幕坐标和触摸坐标的一种对应关系
这里再来引入两个概念,物理坐标和逻辑坐标。物理坐标指触摸屏上点的实际位置,通常以液晶上点的个数来度量。逻辑坐标指这点被触摸时A/D转换后的坐标值。仍以图39.1.3.1为例,我们假定液晶最左上角为坐标轴原点A,在液晶上任取一点B(实际人手比像素点大得多,一次按下会有多个触点,此处取十字线交叉中心),B在X方向与A相距100个点,在Y方向与A距离200个点,则这点的物理坐标B为(100,200)。如果我们触摸这一点时得到的X向A/D转换值为200,Y向A/D转换值为400,则这点的逻辑坐标B’为(200,400)。
需要特别说明的是,正点原子的电容屏的参数已经在出厂时由厂家调好,所以无需进行校准,而且可以直接读到转换后的触点坐标;对于电阻屏,请大家理解并熟记物理坐标和逻辑坐标逻辑上的对应关系,我们后面编程需要用到。
39.2 硬件设计

  1. 例程功能
    正点原子的触摸屏种类很多,并且设计了规格相对统一的接口。根据屏幕的种类不同,设置了相应的硬件ID(正点原子自编ID),可以通过软件判断触摸屏的种类。
    本章实验功能简介:开机的时候先初始化LCD,读取LCD ID,随后,根据LCD ID判断是电阻触摸屏还是电容触摸屏,如果是电阻触摸屏,则先读取24C02的数据判断触摸屏是否已经校准过,如果没有校准,则执行校准程序,校准过后再进入电阻触摸屏测试程序,如果已经校准了,就直接进入电阻触摸屏测试程序。
    如果是4.3寸电容触摸屏,则执行GT9xxx的初始化代码;如果是7寸电容触摸屏(仅支持新款7寸屏,使用SSD1963+FT5206方案),则执行FT5206的初始化代码,在初始化电容触摸屏完成后,进入电容触摸屏测试程序(电容触摸屏无需校准!!)。
    电阻触摸屏测试程序和电容触摸屏测试程序基本一样,只是电容触摸屏支持最多5点同时触摸,电阻触摸屏只支持一点触摸,其他一模一样。测试界面的右上角会有一个清空的操作区域(RST),点击这个地方就会将输入全部清除,恢复白板状态。使用电阻触摸屏的时候,可以通过按KEY0来实现强制触摸屏校准,只要按下KEY0就会进入强制校准程序。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    2 ) 独立按键
    KEY0 – PE4
    3)EEPROM AT24C02
    4)正点原子 2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
    5)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
  3. 原理图
    所有这些资源与STM32F1的连接图,在前面都已经介绍了,这里我们只针对TFTLCD模块与STM32F1的连接端口再说明一下,TFTLCD模块的触摸屏(电阻触摸屏)总共有5根线与STM32F1连接,连接电路图如图39.2.1所示:
    【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第8张图片

图39.2.1 触摸屏与STM32F1的连接图
从图中可以看出:T_SCK、T_MISO、T_MOSI、T_PEN和T_CS分别连接在STM32F1的PB1、PB2、PF9、PF10和PF11上。
如果是电容式触摸屏,我们的接口和电阻式触摸屏一样(上图右侧接口),只是没有用到五根线了,而是四根线,分别是:T_PEN(CT_INT)、T_CS(CT_RST)、T_CLK(CT_SCL)和T_MOSI(CT_SDA)。其中:CT_INT、CT_RST、CT_SCL和CT_SDA分别是GT9147/FT5206的:中断输出信号、复位信号,IIC的SCL和SDA信号。我们用查询的方式读取GT9147/FT5206的数据,对于FT5206没有用到中断信号(CT_INT),所以同STM32F1的连接,最少只需要3根线即可,不过GT9147等IC还需要用到CT_INT做IIC地址设定,所以需要4根线连接。
39.3 程序设计
39.3.1 HAL库驱动
触摸芯片我们使用到的是IIC和SPI的驱动,这部分的时序分析可以参考之前IIC/SPI的章节,我们直接使用的是软件模拟的方式,所以只需要使用HAL库的驱动的GPIO操作部分。
触摸IC驱动步骤
1)初始化通信接口与其IO(使能时钟、配置GPIO工作模式)
触摸IC用到的GPIO口,主要是PB1、PB2、PF9、PF10和PF11,因为都是用软件模拟的方式,因此在这里我们只需使能GPIOB和GPIOF时钟即可。参考代码如下:
__HAL_RCC_GPIOB_CLK_ENABLE(); /* 使能GPIOB时钟 /
__HAL_RCC_GPIOF_CLK_ENABLE(); /
使能GPIOF时钟 */
GPIO模式设置通过调用HAL_GPIO_Init函数实现,详见本例程源码。
2)编写通信协议基础读写函数
通过参考时序图,在IIC驱动或SPI驱动基础上,编写基础读写函数。读写函数均以一字节数据进行操作。
3)参考触摸IC时序图,编写触摸IC读写驱动函数
根据触摸IC的读写时序进行编写触摸IC的读写函数,详见本例程源码。
4)编写坐标获取函数(电阻触摸屏和电容触摸屏)
查阅数据手册获得命令词(电阻触摸屏)/寄存器(电容触摸屏),通过读写函数获取坐标数据,详见本例程源码。
39.3.2 程序流程图
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第9张图片

图39.3.2.1 触摸屏实验流程图
39.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。TOUCH驱动源码包括如下文件:ctiic.c、ctiic.h、ft5206.c、ft5206.h、gt9xxx.c、gt9xxx.h、touch.c和touch.h。
由于正点原子的TFTLCD的型号很多,触摸控制这部分驱动代码根据不同屏幕搭载的触摸芯片驱动而有不同,在我们的屏幕上使用的是LCD ID来帮助软件上区分。为了解决多种驱动芯片的问题,我们设计了touch.c/touch.h这两个文件统一管理各类型的驱动。不同的驱动芯片类型可以在touch.c中集中添加,并通过touch.c中的接口统一调用,不同的触摸芯片各自编写独立的.c/.h文件,需要时被touch.c调用。电阻触摸屏相关代码也在touch.c中实现。

  1. 触摸管理驱动代码
    因为需要支持的触摸驱动比较多,为了方便管理和添加新的驱动,我们用touch.c文件来统一管理这些触摸驱动,然后针对各类触摸芯片编写独立的驱动。为了方便管理触摸,我们在touch.h中定义一个用于管理触摸信息的结构体类型,具体代码如下:
/* 触摸屏控制器 */
typedef struct
{
    uint8_t (*init)(void);     	/* 初始化触摸屏控制器 */
    uint8_t (*scan)(uint8_t);  	/* 扫描触摸屏.0,屏幕扫描;1,物理坐标; */
    void (*adjust)(void);       	/* 触摸屏校准 */
uint16_t x[CT_MAX_TOUCH];  	/* 当前坐标 */
uint16_t y[CT_MAX_TOUCH];   /* 电容屏有最多10组坐标,电阻屏则用x[0],y[0]代表:此次扫
描时,触屏的坐标,用 x[9],y[9]存储第一次按下时的坐标 */

    uint16_t sta;              	/* 笔的状态
                                 	 * b15:按下1/松开0;
                                 	 * b14:0,没有按键按下;1,有按键按下.
                                 	 * b13~b10:保留
                                 	 * b9~b0:电容触摸屏按下的点数(0,表示未按下,1表示按下)
                                 	 */

    /* 5点校准触摸屏校准参数(电容屏不需要校准) */
    float xfac;                 	/* 5点校准法x方向比例因子 */
    float yfac;                 	/* 5点校准法y方向比例因子 */
    short xc;                   	/* 中心X坐标物理值(AD值) */
short yc;                   	/* 中心Y坐标物理值(AD值) */

    /* 新增的参数,当触摸屏的左右上下完全颠倒时需要用到.
     * b0:	0, 竖屏(适合左右为X坐标,上下为Y坐标的TP)
     *    	1, 横屏(适合左右为Y坐标,上下为X坐标的TP)
     * b1~6:	保留.
     * b7:	0, 电阻屏
     *    	1, 电容屏
     */
    uint8_t touchtype;
} _m_tp_dev;

extern _m_tp_dev tp_dev;        /* 触屏控制器在touch.c里面定义 */

这里我们定义了函数指针,只要把相对应的触摸芯片的函数指针赋值给它,就可以通过这个通用接口很方便调用不同芯片的函数接口。正点原子不同的触摸屏区别如下:
1、在使用4.3寸屏、10.1寸屏电容屏时,使用的是汇顶科技的GT9xxx系列触摸屏驱动IC,这是一个IIC接口的驱动芯片,我们要编写gt9xxx系列芯片的初始化程序,并编写一个坐标扫描程序,这里我们先预留这两个接口分别为gt9xxx_init()和gt9xxx_scan(),在gt9xxx.c文件中再专门实现这两个驱动,标记使用的为电容屏;
2、类似地,在使用SSD1963 7寸屏、7寸800480/1024600 RGB屏时,我们的屏幕搭载的触摸驱动芯片是ft5206/GT911,FT5206触摸IC预留这两个接口分别为ft5206_init()和ft5206_scan(),在ft5206.c文件中再专门实现这两个驱动,标记使用的为电容屏;GT911也是调用gtxxx_init()和gt9xxx_scan()接口。
3、当为其它ID时,默认为电阻屏,而电阻屏默认使用的是SPI接口的XPT2046芯片。由于电阻屏存在线性误差,所以在使用前需要进行校准,这也是为什么在前面的结构体类型中存在关于校准参数的成员。为了避免每次都要进行校准的麻烦,所以会使用AT24C02来存储校准成功后的数据。如何进行校准也会在后面进行讲解。作为电阻屏,它也有一个扫描坐标函数即tp_scan()。
(*init)(void)这个结构体函数指针,默认指向tp_init的,而在tp_init里对触摸屏进行初始化并对(*scan)(uint8_t)函数指针根据触摸芯片类型重新做了指向。在这里简单看一下touch.c的触摸屏初始化函数tp_init,其代码如下:

/**
 * @brief       	触摸屏初始化
 * @param       	无
 * @retval    	0,没有进行校准
 *             	1,进行过校准
 */
uint8_t tp_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;
    
    tp_dev.touchtype = 0;                  	/* 默认设置(电阻屏 & 竖屏) */
    tp_dev.touchtype |= lcddev.dir & 0X01; 	/* 根据LCD判定是横屏还是竖屏 */

    if (lcddev.id == 0X5510 || lcddev.id == 0X4342 || lcddev.id == 0X4384 || lcddev.id == 0X1018)  
    {   /* 电容触摸屏,4.3寸/10.1寸屏 */
        gt9xxx_init();
        tp_dev.scan = gt9xxx_scan;  /* 扫描函数指向GT9147触摸屏扫描 */
        tp_dev.touchtype |= 0X80;   /* 电容屏 */
        return 0;
    }
else if (lcddev.id == 0X1963 || lcddev.id == 0X7084 || lcddev.id == 0X7016)     
    {   /* SSD1963 7寸屏或者 7寸800*480/1024*600 RGB屏 */
        if (!ft5206_init())
{
	        tp_dev.scan = ft5206_scan;	/* 扫描函数指向FT5206触摸屏扫描 */
}
        else	     
        {
	        gt9xxx_init();
	        tp_dev.scan = gt9xxx_scan;  /* 扫描函数指向GT9147触摸屏扫描 */
}
        tp_dev.touchtype |= 0X80;   	/* 电容屏 */
        return 0;
    }
    else	     
    {
        T_PEN_GPIO_CLK_ENABLE();    	/* T_PEN脚时钟使能 */
        T_CS_GPIO_CLK_ENABLE();     	/* T_CS脚时钟使能 */
        T_MISO_GPIO_CLK_ENABLE();   	/* T_MISO脚时钟使能 */
        T_MOSI_GPIO_CLK_ENABLE();   	/* T_MOSI脚时钟使能 */
        T_CLK_GPIO_CLK_ENABLE();    	/* T_CLK脚时钟使能 */

        gpio_init_struct.Pin = T_PEN_GPIO_PIN;
        gpio_init_struct.Mode = GPIO_MODE_INPUT;            	/* 输入 */
        gpio_init_struct.Pull = GPIO_PULLUP;                 	/* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; 		/* 高速 */
        HAL_GPIO_Init(T_PEN_GPIO_PORT, &gpio_init_struct);	/* 初始化T_PEN引脚 */

        gpio_init_struct.Pin = T_MISO_GPIO_PIN;
        HAL_GPIO_Init(T_MISO_GPIO_PORT, &gpio_init_struct);	/* 初始化T_MISO引脚* /

        gpio_init_struct.Pin = T_MOSI_GPIO_PIN;
        gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;        	/* 推挽输出 */
        gpio_init_struct.Pull = GPIO_PULLUP;                 	/* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      	/* 高速 */
        HAL_GPIO_Init(T_MOSI_GPIO_PORT, &gpio_init_struct);	/* 初始化T_MOSI引脚 */

        gpio_init_struct.Pin = T_CLK_GPIO_PIN;
        HAL_GPIO_Init(T_CLK_GPIO_PORT, &gpio_init_struct);	/* 初始化T_CLK引脚 */

        gpio_init_struct.Pin = T_CS_GPIO_PIN;
        HAL_GPIO_Init(T_CS_GPIO_PORT, &gpio_init_struct); 	/* 初始化T_CS引脚 */

        tp_read_xy(&tp_dev.x[0], &tp_dev.y[0]); 				/* 第一次读取初始化 */
        at24cxx_init();           								/* 初始化24CXX */

        if (tp_get_adjust_data())
        {
            return 0;           	/* 已经校准 */
        }
        else                    	/* 未校准? */
        {
            lcd_clear(WHITE);   	/* 清屏 */
            tp_adjust();        	/* 屏幕校准 */
            tp_save_adjust_data();
        }

        tp_get_adjust_data();
    }

    return 1;
}

正点原子的电容屏在出厂时已经由厂家较对好参数了,而电阻屏由于工艺和每个屏的线性有所差异,我们需要先对其进行“校准”,我们在下一点补充说明它的实现。
通过上面的触摸初始化后,我们就可以读取相关的触点信息用于显示编程了,注意到上面还有很多个函数还没实现,比如读取坐标和校准,我们在接下来的代码中将它补充完整。
2. 电阻屏触摸函数
前面我们介绍过了电阻式触摸屏的原理,由于电阻屏的驱动代码都比较类似,我们决定把电阻屏的驱动函数直接添加在touch.c/touch.h中实现。
在touch.c的初始化函数tp_init中,对使用到的SPI接口IO进行了初始化。接下来介绍一下获取触摸点在屏幕上坐标的算法:先获取逻辑坐标(AD值),再转换成屏幕坐标。
如何获取逻辑坐标(AD值),在前面已经分析过了,所以这里我们看一下tp_read_ad()函数接口:

/**
 * @brief     	SPI读数据
 * @note      	从触摸屏IC读取adc值
 * @param      	cmd: 指令
 * @retval     	读取到的数据,ADC值(12bit)
 */
static uint16_t tp_read_ad(uint8_t cmd)
{
    uint8_t count = 0;
    uint16_t num = 0;
    T_CLK(0);           	/* 先拉低时钟 */
    T_MOSI(0);          	/* 拉低数据线 */
    T_CS(0);            	/* 选中触摸屏IC */
    tp_write_byte(cmd);	/* 发送命令字 */
    delay_us(6);        	/* ADS7846的转换时间最长为6us */
    T_CLK(0);
    delay_us(1);
    T_CLK(1);           	/* 给1个时钟,清除BUSY */
    delay_us(1);
    T_CLK(0);

    for (count = 0; count < 16; count++)    /* 读出16位数据,只有高12位有效 */
    {
        num <<= 1;
        T_CLK(0);       	/* 下降沿有效 */
        delay_us(1);
        T_CLK(1);
        if (T_MISO)num++;
    }

    num >>= 4;			/* 只有高12位有效. */
    T_CS(1);				/* 释放片选 */
    return num;
}

这里我们使用的是软件模拟SPI,遵照时序编写SPI读函数接口。而发送命令字是通过写函数tp_write_byte来实现,详看源码。
一次读取的误差会很大,我们采用平均值滤波的方法,多次读取数据并丢弃波动最大的最大和最小值,取余下的平均值。具体可以查看tp_read_xoy函数内部实现。
/* 电阻触摸驱动芯片 数据采集 滤波用参数 */

#define TP_READ_TIMES   5       /* 读取次数 */
#define TP_LOST_VAL     1       /* 丢弃值 */
/**
 * @brief   		读取一个坐标值(x或者y)
 * @note    	     连续读取TP_READ_TIMES次数据,对这些数据升序排列,
 *             	然后去掉最低和最高TP_LOST_VAL个数, 取平均值
 *            	设置时需满足: TP_READ_TIMES > 2*TP_LOST_VAL 的条件
 * @param      	cmd : 指令
 * @arg      	0XD0: 读取X轴坐标(@竖屏状态,横屏状态和Y对调.)
 * @arg      	0X90: 读取Y轴坐标(@竖屏状态,横屏状态和X对调.)
 *
 * @retval     	读取到的数据(滤波后的), ADC值(12bit)
 */
static uint16_t tp_read_xoy(uint8_t cmd)
{
    uint16_t i, j;
    uint16_t buf[TP_READ_TIMES];
    uint16_t sum = 0;
    uint16_t temp;

    for (i = 0; i < TP_READ_TIMES; i++)   		/* 先读取TP_READ_TIMES次数据 */
    {
        buf[i] = tp_read_ad(cmd);
    }

    for (i = 0; i < TP_READ_TIMES - 1; i++)   	/* 对数据进行排序 */
    {
        for (j = i + 1; j < TP_READ_TIMES; j++)
        {
            if (buf[i] > buf[j])   				/* 升序排列 */
            {
                temp = buf[i];
                buf[i] = buf[j];
                buf[j] = temp;
            }
        }
    }

    sum = 0;
    for (i = TP_LOST_VAL; i < TP_READ_TIMES - TP_LOST_VAL; i++)
    {   /* 去掉两端的丢弃值 */
        sum += buf[i];  /* 累加去掉丢弃值以后的数据. */
    }

    temp = sum / (TP_READ_TIMES - 2 * TP_LOST_VAL); /* 取平均值 */
    return temp;
}

有了前述代码,我们就可以通过tp_read_xoy(uint8_t cmd)接口调取需要的x或者y坐标的AD值了。这里我们加上横屏或者竖屏的处理代码,编写一个可以通过指针一次得到x和y的两个AD值的接口,代码如下:

/**
 * @brief     	读取x, y坐标
 * @param      	x,y: 读取到的坐标值
 * @retval   	无
 */
static void tp_read_xy(uint16_t *x, uint16_t *y)
{
    uint16_t xval, yval;

    if (tp_dev.touchtype & 0X01)    	/* X,Y方向与屏幕相反 */
    {
        xval = tp_read_xoy(0X90);   	/* 读取X轴坐标AD值, 并进行方向变换 */
        yval = tp_read_xoy(0XD0);   	/* 读取Y轴坐标AD值 */
    }
    else                    			/* X,Y方向与屏幕相同 */
    {
        xval = tp_read_xoy(0XD0);   	/* 读取X轴坐标AD值 */
        yval = tp_read_xoy(0X90);   	/* 读取Y轴坐标AD值 */
    }

    *x = xval;
    *y = yval;
}

为了进一步保证参数的精度,我们连续读两次触摸数据并取平均值作为最后的触摸参数,并对这两次滤波值平均后再传给目标存储区,由于AD的精度为12位,故该函数读取坐标的值0~4095,tp_read_xy2的代码如下:
/* 连续两次读取X,Y坐标的数据误差最大允许值 */

#define TP_ERR_RANGE    50      /* 误差范围 */
/**
 * @brief     	连续读取2次触摸IC数据, 并滤波
 *   @note      	连续2次读取触摸屏IC,且这两次的偏差不能超过ERR_RANGE,满足
 *              	条件,则认为读数正确,否则读数错误.该函数能大大提高准确度.
 *
 * @param       	x,y: 读取到的坐标值
 * @retval      	0, 失败; 1, 成功;
 */
static uint8_t tp_read_xy2(uint16_t *x, uint16_t *y)
{
    uint16_t x1, y1;
    uint16_t x2, y2;

    tp_read_xy(&x1, &y1);   /* 读取第一次数据 */
    tp_read_xy(&x2, &y2);   /* 读取第二次数据 */

/* 前后两次采样在+-TP_ERR_RANGE内 */
if (((x2 <= x1 && x1<x2+TP_ERR_RANGE)||(x1 <= x2 && x2<x1+TP_ERR_RANGE))&&
((y2 <= y1 && y1<y2+TP_ERR_RANGE)||(y1 <= y2 && y2<y1+TP_ERR_RANGE)))
    {
        *x = (x1 + x2) / 2;
        *y = (y1 + y2) / 2;
        return 1;
    }

    return 0;
}

根据以上的流程,可以得到电阻屏触摸点的比较精确的AD信息。每次触摸屏幕时会对应一组X、Y的AD值,由于坐标的AD值是在X、Y方向都是线性的,很容易想到要把触摸信息的AD值和屏幕坐标联系起来,这里需要编写一个坐标转换函数,前面在编写初始化接口时讲到的校准函数这时候就派上用场了。
从前面的知识我们就知道触摸屏的AD的XAD、YAD可以构成一个逻辑平面,LCD屏的屏幕坐标X、Y也是一个逻辑平面,由于存在误差,这两个平面并不重合,校准的作用就是要将逻辑平面映射到物理平面上,即得到触点在液晶屏上的位置坐标。校准算法的中心思想也就是要建立这样一个映射函数现有的校准算法大多是基于线性校准,即首先假定物理平面和逻辑平面之间的误差是线性误差,由旋转和偏移形成。
常用的电阻式触摸屏矫正方法有两点校准法和三点校准法。本文这里介绍的是结合了不同的电阻式触摸屏矫正法的优化算法:五点校正法。其中主要的原理是使用4点矫正法的比例运算以及三点矫正法的基准点运算。五点校正法优势在于可以更加精确的计算出X和Y方向的比例缩放系数,同时提供了中心基准点,对于一些线性电阻系数比较差的电阻式触摸屏有很好的校正功能。校正相关的变量主要有:
x[5],y[5]五点定位的物理坐标(LCD坐标)
xl[5],yl[5]五点定位的逻辑坐标(触摸AD值)
KX,KY横纵方向伸缩系数
XLC,YLC中心基点逻辑坐标
XC,YC中心基点物理坐标(数值采用LCD显示屏的物理长宽分辨率的一半)
x[5],y[5]五点定位的物理坐标是已知的,其中4点分别设置在LCD的角落,一点设置在LCD正中心,作为基准矫正点,校正关键点和距离布局如图39.3.3.1所示。
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第10张图片

图39.3.3.1 电阻屏五点校准法的参考点设定
校正步骤如下:

  1. 通过先后点击LCD的4个角落的矫正点,获取4个角落的逻辑坐标值。
  2. 计算屏幕坐标和四点间距:
    S1 = x[1]- x[0]
    S3 = x[2]- x[3]
    S2 = y[2]- y[1]
    S4 = y[3]- y[0]
    一般取点可以人为的设定S1=S3和S2=S4,以方便运算。
    计算逻辑坐标的四点“间距”,由于实际触点肯定会存在误差,所以触摸点会落在实际设定点的更大范围内,在图39.3.1中,设定点为五个点,但实际采样时触点有时会落在稍大的外圈范围,图中用红色的圆圈标注了,所以有必要设定一个误差范围:
    S1’ = xl[1]- xl[0]
    S3’ = xl[2]- xl[3]
    S2’ = yl[2]- yl[1]
    S4’ = yl[3]- yl[0]
    由于触点的误差,对于逻辑点S1’和S3’则大概率不会相等,同样的,S2’和S4’也很难取到相等的点,那么为了简化计算,我们强制以(S1’+S3’)/2的线长作一个矩形一边,以(S2’+S4’)/2为矩形另一边,这样构建的矩形在误差范围是可以接受的,也方便计算,于是得到X和Y方向的近似缩放系数:
    KX =(S1’+ S3’) / 2 / S1
    KY =(S2’+ S4’) / 2 / S2
  3. 点击LCD正中心,获取中心点的逻辑坐标,作为矫正的基准点。这里也同样的需要限制误差,之后可以得到一个中心点的AD值坐标(xl[4],yl[4]),这个点的AD值我们就作为我们对比的基准点,即xl[4]=XLC,yl[4]=YLC;
  4. 完成以上步骤则校正完成。下次点击触摸屏的时候获取的逻辑值XL和YL,便可以按下以公式转换为物理坐标:
    X = (XL - XLC) / KX + XC
    Y = (YL - YLC) / KY + YC
    最后一步的转换公式可能不好理解,大家换个角度,如果我们求到的缩放比例是正确的,在取新的触摸的时候,这个触摸点的逻辑坐标和物理坐标的转换,必然与中心点在两方向上的缩放比例相等,用中学数学直线斜率相等的情况,变换便可得到上述公式。
    通过上述得到校准参数后,在以后的使用中,我们把所有得到的物理坐标都按照这个关系式来计算,得到的就是触摸点的屏幕坐标。为了省去每次都需要校准的麻烦,我们保存这些参数到AT24Cxx的指定扇区地址,这样只要校准一次就可以重复使用这些参数了。
    根据上面的原理,我们设计的校准函数tp_adjust如下:
/**
 * @brief      	触摸屏校准代码
 * @note      	使用五点校准法(具体原理请百度)
 *              	本函数得到x轴/y轴比例因子xfac/yfac及物理中心坐标值(xc,yc)等4个参数
 *              	我们规定: 物理坐标即AD采集到的坐标值,范围是0~4095.
 *                        	 逻辑坐标即LCD屏幕的坐标, 范围为LCD屏幕的分辨率.
 *
 * @param      	无
 * @retval     	无
 */
void tp_adjust(void)
{
    uint16_t pxy[5][2]; 		/* 物理坐标缓存值 */
    uint8_t  cnt = 0;
    short s1, s2, s3, s4;  	/* 4个点的坐标差值 */
    double px, py;          	/* X,Y轴物理坐标比例,用于判定是否校准成功 */
    uint16_t outtime = 0;
    cnt = 0;

lcd_clear(WHITE);     	/* 清屏 */  
lcd_show_string(40, 40, 160, 100, 16, TP_REMIND_MSG_TBL, RED);/*显示提示信息*/
    tp_draw_touch_point(20, 20, RED);	/* 画点1 */
tp_dev.sta = 0;                      	/* 消除触发信号 */

    while (1)                          		/* 如果连续10秒钟没有按下,则自动退出 */
    {
        tp_dev.scan(1);                 	/* 扫描物理坐标 */

        if ((tp_dev.sta & 0xc000) == TP_CATH_PRES)   
        {   /* 按键按下了一次(此时按键松开了) */
            outtime = 0;
            tp_dev.sta &= ~TP_CATH_PRES;	/* 标记按键已经被处理过了. */
            pxy[cnt][0] = tp_dev.x[0]; 	/* 保存X物理坐标 */
            pxy[cnt][1] = tp_dev.y[0]; 	/* 保存Y物理坐标 */
            cnt++;

            switch (cnt)
            {
                case 1:
                    tp_draw_touch_point(20, 20, WHITE);                 	/* 清点1 */
                    tp_draw_touch_point(lcddev.width - 20, 20, RED); 	/* 画点2 */
                    break;

                case 2:
                    tp_draw_touch_point(lcddev.width-20, 20, WHITE);	/* 清点2 */
                    tp_draw_touch_point(20, lcddev.height - 20, RED);	/* 画点3 */
                    break;

                case 3:
                    tp_draw_touch_point(20, lcddev.height-20, WHITE);	/* 清点3 */
                    /* 画点4 */
                    tp_draw_touch_point(lcddev.width- 20, lcddev.height - 20, RED);
                    break;

                case 4:
                    lcd_clear(WHITE);   /* 画第五个点了, 直接清屏 */
                    /* 画点5 */
                    tp_draw_touch_point(lcddev.width / 2, lcddev.height / 2, RED);
                    break;

                case 5:     				/* 全部5个点已经得到 */
 s1=pxy[1][0]-pxy[0][0];	/*第2个点和第1个点的X轴物理坐标差值(AD值)*/
s3=pxy[3][0]-pxy[2][0];	/*第4个点和第3个点的X轴物理坐标差值(AD值)*/
s2=pxy[3][1]-pxy[1][1];	/*第4个点和第2个点的Y轴物理坐标差值(AD值)*/
s4=pxy[2][1]-pxy[0][1];	/*第3个点和第1个点的Y轴物理坐标差值(AD值)*/

                    px = (double)s1 / s3; 	/* X轴比例因子 */
                    py = (double)s2 / s4;	/* Y轴比例因子 */

                    if (px < 0)px = -px; 	/* 负数改正数 */
                    if (py < 0)py = -py;  	/* 负数改正数 */

                    if (px < 0.95 || px > 1.05 || py < 0.95 || py > 1.05 ||    
abs(s1)>4095||abs(s2)>4095||abs(s3)>4095||abs(s4)>4095|| 
abs(s1)==0 ||abs(s2)==0||abs(s3)==0||abs(s4)==0)
                    { /* 比例不合格,差值大于坐标范围或等于0,重绘校准图形 */
                       cnt = 0;
                       /* 清除点5 */
                       tp_draw_touch_point(lcddev.width/2, lcddev.height/2, WHITE);
                       tp_draw_touch_point(20, 20, RED);	/* 重新画点1 */
                       tp_adjust_info_show(pxy, px, py);	/* 显示当前信息,方便找问题 */
                       continue;
                    }

                    tp_dev.xfac = (float)(s1 + s3) / (2 * (lcddev.width - 40));
                    tp_dev.yfac = (float)(s2 + s4) / (2 * (lcddev.height - 40));
                    tp_dev.xc = pxy[4][0];		/* X轴,物理中心坐标 */
                    tp_dev.yc = pxy[4][1];		/* Y轴,物理中心坐标 */
                    lcd_clear(WHITE);			/* 清屏 */
lcd_show_string(35, 110, lcddev.width, lcddev.height, 16,
 "Touch Screen Adjust OK!", BLUE); /* 校准完成 */
                    delay_ms(1000);
                    tp_save_adjust_data();
                    lcd_clear(WHITE);	/* 清屏 */
                    return;			/* 校正完成 */
            }
        }

        delay_ms(10);
        outtime++;
        if (outtime > 1000)
        {
            tp_get_adjust_data();
            break;
        }
    }
}
注意该函数里面多次使用了lcddev.width和lcddev.height,用于坐标设置,故在程序调用前需要预先初始化LCD得 到LCD的一些屏幕信息,主要是为了兼容不同尺寸的LCD(比如320*240480*320800*480的屏都可以兼容)。
有了校准参数后,由于我们需要频繁地进行屏幕坐标和物理坐标的转换,我们为电阻屏增加一个tp_scan(uint8_t mode)用于转换,为了实际使用上更灵活,我们使这个参数支持物理坐标和屏幕坐标,设计的函数如下:
/**
 * @brief    	触摸按键扫描
 * @param       	mode: 坐标模式
 *   @arg       	0, 屏幕坐标;
 *   @arg       	1, 物理坐标(校准等特殊场合用)
 *
 * @retval      	0, 触屏无触摸; 1, 触屏有触摸;
 */
uint8_t tp_scan(uint8_t mode)
{
    if (T_PEN == 0)    	/* 有按键按下 */
    {
        if (mode)     	/* 读取物理坐标, 无需转换 */
        {
            tp_read_xy2(&tp_dev.x[0], &tp_dev.y[0]);
        }
        else if (tp_read_xy2(&tp_dev.x[0], &tp_dev.y[0]))/* 读取屏幕坐标, 需要转换*/
        {   /* 将X轴 物理坐标转换成逻辑坐标(即对应LCD屏幕上面的X坐标值) */
            tp_dev.x[0] = (signed short)(tp_dev.x[0] - tp_dev.xc) 
/ tp_dev.xfac + lcddev.width / 2;
            /* 将Y轴 物理坐标转换成逻辑坐标(即对应LCD屏幕上面的Y坐标值) */
            tp_dev.y[0] = (signed short)(tp_dev.y[0] - tp_dev.yc) 
/ tp_dev.yfac + lcddev.height / 2;
        }

        if ((tp_dev.sta & TP_PRES_DOWN) == 0)   /* 之前没有被按下 */
        {
            tp_dev.sta = TP_PRES_DOWN | TP_CATH_PRES;   /* 按键按下 */
            tp_dev.x[CT_MAX_TOUCH - 1] = tp_dev.x[0];   /* 记录第一次按下时的坐标 */
            tp_dev.y[CT_MAX_TOUCH - 1] = tp_dev.y[0];
        }
    }
    else
    {
        if (tp_dev.sta & TP_PRES_DOWN)      /* 之前是被按下的 */
        {
            tp_dev.sta &= ~TP_PRES_DOWN;    /* 标记按键松开 */
        }
        else     /* 之前就没有被按下 */
        {
            tp_dev.x[CT_MAX_TOUCH - 1] = 0;
            tp_dev.y[CT_MAX_TOUCH - 1] = 0;
            tp_dev.x[0] = 0xffff;
            tp_dev.y[0] = 0xffff;
        }
    }

    return tp_dev.sta & TP_PRES_DOWN; /* 返回当前的触屏状态 */
}

要进行电阻触摸屏的触摸扫描,只要调取tp_scan()函数,就能灵活地得到触摸坐标。电阻屏的触摸就讲到这里。
3. 电容屏触摸驱动代码
电容触摸芯片使用的是IIC接口。IIC接口部分代码,我们可以参考 myiic.c和myiic.h的代码,为了使代码独立,我们在“TOUCH”文件夹下也是采用软件模拟IIC的方式实现ctiic.c和ctiic.h,这样IO的使用更灵活,这里部分参考IIC章节的知识就可以了,这里不重复介绍了。
电容触摸芯片除了IIC接口相关引脚CT_SCL和CT_SDA,还有CT_INT和CT_RST,接口图如图39.3.3.2所示。
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第11张图片

图39.3.3.2 电容触摸芯片接口图
gt9xxx_init的实现也比较简单,实现CT_INT和CT_RST引脚初始化和调用ct_iic_init函数实现对CT_SDA和CT_SCL初始化。由于电容触摸屏在设计时是根据屏幕进行参数设计的,参数已经保存在芯片内部。所以在初始化后,就可以参考手册推荐的IIC时序从相对应的坐标数据寄存器中把对应的XY坐标数据读出来,再通过数据整理转成LCD坐标。
与电阻屏不同的是,我们是通过IIC读取状态寄存器的值并非引脚电平。而gt9xxx系列是支持中断或轮询方式得到触摸状态,本实验使用的是轮询方式:
1、按照读时序,先读取寄存器0x814E,若当前buffer(buffer status为1)数据准备好,则依据有效触点个数到相对应的坐标数据地址处进行坐标数据读取。
2、若在1中发现buffer数据(buffer status为0)未准备好,则等待1ms再进行读取。
这样,gt9xxx_scan()函数的实现如下:

/* GT9XXX 10个触摸点(最多) 对应的寄存器表 */
const uint16_t GT9XXX_TPX_TBL[10] =
{
    GT9XXX_TP1_REG,GT9XXX_TP2_REG,GT9XXX_TP3_REG,GT9XXX_TP4_REG,GT9XXX_TP5_REG,
    GT9XXX_TP6_REG,GT9XXX_TP7_REG,GT9XXX_TP8_REG,GT9XXX_TP9_REG,GT9XXX_TP10_REG,
};

/**
 * @brief    	扫描触摸屏(采用查询方式)
 * @param       	mode : 电容屏未用到次参数, 为了兼容电阻屏
 * @retval      	当前触屏状态
 *   @arg       	0, 触屏无触摸; 
 *   @arg       	1, 触屏有触摸;
 */
uint8_t gt9xxx_scan(uint8_t mode)
{
    uint8_t buf[4];
    uint8_t i = 0;
    uint8_t res = 0;
    uint16_t temp;
    uint16_t tempsta;
    static uint8_t t = 0;   /* 控制查询间隔,从而降低CPU占用率 */
    t++;

    if ((t % 10) == 0 || t < 10)    
    {   /* 空闲时,每进入10次CTP_Scan函数才检测1次,从而节省CPU使用率 */
        gt9xxx_rd_reg(GT9XXX_GSTID_REG, &mode, 1);  /* 读取触摸点的状态 */

        if ((mode & 0X80) && ((mode & 0XF) <= g_gt_tnum))
        {
            i = 0;
            gt9xxx_wr_reg(GT9XXX_GSTID_REG, &i, 1); 	/* 清标志 */
        }

        if ((mode & 0XF) && ((mode & 0XF) <= g_gt_tnum))
        {
/* 将点的个数转换为1的位数,匹配tp_dev.sta定义 */
            temp = 0XFFFF << (mode & 0XF);  
            tempsta = tp_dev.sta;           			/* 保存当前的tp_dev.sta值 */
            tp_dev.sta = (~temp) | TP_PRES_DOWN | TP_CATH_PRES;
            tp_dev.x[g_gt_tnum - 1] = tp_dev.x[0];	/* 保存触点0的数据 */
            tp_dev.y[g_gt_tnum - 1] = tp_dev.y[0];

            for (i = 0; i < g_gt_tnum; i++)
            {
                if (tp_dev.sta & (1 << i))  			/* 触摸有效? */
                {
                    gt9xxx_rd_reg(GT9XXX_TPX_TBL[i], buf, 4);   /* 读取XY坐标值 */

                    if (lcddev.id == 0X5510)            /* 4.3寸800*480 MCU屏 */
                    {
                        if (tp_dev.touchtype & 0X01)	/* 横屏 */
                        {
                            tp_dev.y[i] = ((uint16_t)buf[1] << 8) + buf[0];
                            tp_dev.x[i] = 800 - (((uint16_t)buf[3] << 8) + buf[2]);
                        }
                        else
                        {
                            tp_dev.x[i] = ((uint16_t)buf[1] << 8) + buf[0];
                            tp_dev.y[i] = ((uint16_t)buf[3] << 8) + buf[2];
                        }
                    }
                    else 	/* 其他型号 */
                    {
                        if (tp_dev.touchtype & 0X01)    /* 横屏 */
                        {
                            tp_dev.x[i] = (((uint16_t)buf[1] << 8) + buf[0]);
                            tp_dev.y[i] = (((uint16_t)buf[3] << 8) + buf[2]);
                        }
                        else
                        {
                            tp_dev.x[i]=lcddev.width-(((uint16_t)buf[3]<<8)+buf[2]);
                            tp_dev.y[i] = ((uint16_t)buf[1] << 8) + buf[0];
                        }
                    }
                    //printf("x[%d]:%d,y[%d]:%d\r\n",i,tp_dev.x[i],i,tp_dev.y[i]);
                }
            }

            res = 1;

            if (tp_dev.x[0] > lcddev.width || tp_dev.y[0] > lcddev.height)  
            {   /* 非法数据(坐标超出了) */
                if ((mode & 0XF) > 1)/*有其他点有数据,则复制第二个触点的数据到第一个触点*/
                {
                    tp_dev.x[0] = tp_dev.x[1];
                    tp_dev.y[0] = tp_dev.y[1];
                    t = 0;  /* 触发一次,则会最少连续监测10次,从而提高命中率 */
                }
                else        /* 非法数据,则忽略此次数据(还原原来的) */
                {
                    tp_dev.x[0] = tp_dev.x[g_gt_tnum - 1];
                    tp_dev.y[0] = tp_dev.y[g_gt_tnum - 1];
                    mode = 0X80;
                    tp_dev.sta = tempsta;   /* 恢复tp_dev.sta */
                }
            }
            else 
            {
                t = 0;      /* 触发一次,则会最少连续监测10次,从而提高命中率 */
            }
        }
    }

    if ((mode & 0X8F) == 0X80)  				/* 无触摸点按下 */
    {
        if (tp_dev.sta & TP_PRES_DOWN)		/* 之前是被按下的 */
        {
            tp_dev.sta &= ~TP_PRES_DOWN; 		/* 标记按键松开 */
        }
        else    /* 之前就没有被按下 */
        {
            tp_dev.x[0] = 0xffff;
            tp_dev.y[0] = 0xffff;
            tp_dev.sta &= 0XE000;       		/* 清除点有效标记 */
        }
    }

    if (t > 240)t = 10; /* 重新从10开始计数 */

    return res;
}

大家可以打开gt9xxx芯片对应的编程手册,对照时序,即可理解上述的实现过程,只是程序中为了匹配多种屏幕和横屏显示,添加了一些代码。
电容屏驱动ft5206.c/ft5206.h的驱动实现与gt9xxx的实现类似,大家参考本例程源码即可。
电容屏的触摸实验代码讲解到这里。
4. main函数和测试代码
在main.c里面编程如下代码:

void rtp_test(void)
{
    uint8_t key;
    uint8_t i = 0;

    while (1)
    {
        key = key_scan(0);
        tp_dev.scan(0);

        if (tp_dev.sta & TP_PRES_DOWN)	/* 触摸屏被按下 */
        {
            if (tp_dev.x[0] < lcddev.width && tp_dev.y[0] < lcddev.height)
            {
                if (tp_dev.x[0] > (lcddev.width - 24) && tp_dev.y[0] < 16)
                {
                    load_draw_dialog();	/* 清除 */
                }
                else 
                {
                    tp_draw_big_point(tp_dev.x[0], tp_dev.y[0], RED);  /* 画点 */
                }
            }
        }
        else 
        {
            delay_ms(10);       	/* 没有按键按下的时候 */
        }
        
        if (key == KEY0_PRES)	/* KEY0按下,则执行校准程序 */
        {
            lcd_clear(WHITE);   	/* 清屏 */
            tp_adjust();        	/* 屏幕校准 */
            tp_save_adjust_data();
            load_draw_dialog();
        }

        i++;
        if (i % 20 == 0)LED0_TOGGLE();
    }
}

/* 10个触控点的颜色(电容触摸屏用) */
const uint16_t POINT_COLOR_TBL[10] = {RED, GREEN, BLUE, BROWN, YELLOW, MAGENTA, CYAN, LIGHTBLUE, BRRED, GRAY};

void ctp_test(void)
{
    uint8_t t = 0;
    uint8_t i = 0;
    uint16_t lastpos[10][2];        /* 最后一次的数据 */
    uint8_t maxp = 5;

    if (lcddev.id == 0X1018) maxp = 10;

    while (1)
    {
        tp_dev.scan(0);

        for (t = 0; t < maxp; t++)
        {
            if ((tp_dev.sta) & (1 << t))
            {   /* 坐标在屏幕范围内 */
                if (tp_dev.x[t] < lcddev.width && tp_dev.y[t] < lcddev.height)  
                {
                    if (lastpos[t][0] == 0XFFFF)
                    {
                        lastpos[t][0] = tp_dev.x[t];
                        lastpos[t][1] = tp_dev.y[t];
                    }

                    lcd_draw_bline(lastpos[t][0], lastpos[t][1], tp_dev.x[t], 
tp_dev.y[t], 2, POINT_COLOR_TBL[t]); /* 画线 */
                    lastpos[t][0] = tp_dev.x[t];
                    lastpos[t][1] = tp_dev.y[t];

                    if (tp_dev.x[t] > (lcddev.width - 24) && tp_dev.y[t] < 20)
                    {
                        load_draw_dialog();/* 清除 */
                    }
                }
            }
            else 
            {
                lastpos[t][0] = 0XFFFF;
            }
        }

        delay_ms(5);
        i++;

        if (i % 20 == 0)LED0_TOGGLE();
    }
}

int main(void)
{
    HAL_Init();                           		/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);		/* 设置时钟, 72Mhz */
    delay_init(72);                        		/* 延时初始化 */
    usart_init(115200);                   		/* 串口初始化为115200 */
    led_init();                            		/* 初始化LED */
    lcd_init();                            		/* 初始化LCD */
    key_init();                             		/* 初始化按键 */
    tp_dev.init();                         		/* 触摸屏初始化 */

    lcd_show_string(30, 50, 200, 16, 16, "STM32F103", RED);
    lcd_show_string(30, 70, 200, 16, 16, "TOUCH TEST", RED);
    lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);

    if (tp_dev.touchtype != 0XFF)
    {   /* 电阻屏才显示 */
        lcd_show_string(30, 110, 200, 16, 16, "Press KEY0 to Adjust", RED); 
    }

    delay_ms(1500);
    load_draw_dialog();

    if (tp_dev.touchtype & 0X80)
    {
        ctp_test(); /* 电容屏测试 */
    }
    else
    {
        rtp_test(); /* 电阻屏测试 */
    }
}

上面没有把main.c全部代码列出来,只是列出重要函数,这里简单介绍一下这三个函数。
rtp_test,该函数用于电阻触摸屏的测试,该函数代码比较简单,就是扫描按键和触摸屏,如果触摸屏有按下,则在触摸屏上面划线,如果按中“RST”区域,则执行清屏。如果按键KEY0按下,则执行触摸屏校准。
ctp_test,该函数用于电容触摸屏的测试,由于我们采用tp_dev.sta来标记当前按下的触摸屏点数,所以判断是否有电容触摸屏按下,也就是判断tp_dev.sta的最低5位,如果有数据,则画线,如果没数据则忽略,且5个点画线的颜色各不一样,方便区分。另外,电容触摸屏不需要校准,所以没有校准程序。
main函数,则比较简单,初始化相关外设,然后根据触摸屏类型,去选择执行ctp_test还是rtp_test。
软件部分就介绍到这里,接下来看看下载验证。
39.4 下载验证
在代码编译成功之后,我们通过下载代码到开发板上,电阻触摸屏测试如图39.4.1所示界面:
【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第12张图片

图39.4.1 电阻触摸屏测试程序运行效果
图中我们在电阻屏上画了一些内容,右上角的RST可以用来清屏,点击该区域,即可清屏重画。另外,按KEY0可以进入校准模式,如果发现触摸屏不准,则可以按KEY0,进入校准,重新校准一下,即可正常使用。
如果是电容触摸屏,测试界面如图39.4.2所示:

【正点原子STM32连载】第三十九章 触摸屏实验 摘自【正点原子】STM32F103 战舰开发指南V1.2_第13张图片

图39.4.2 电容触摸屏测试界面
图中,同样输入了一些内容。电容屏支持多点触摸,每个点的颜色都不一样,图中的波浪线就是三点触摸画出来的,最多可以5点触摸。按右上角的RST标志,可以清屏。电容屏无需校准,所以按KEY0无效。KEY0校准仅对电阻屏有效。

你可能感兴趣的:(stm32,单片机,嵌入式硬件)