28. 电阻触摸屏—触摸画板

本章参考资料:《STM32F10X-中文参考手册》、《STM32F103增强型系列数据手册》。

关于开发板配套的触摸面板配套的触摸控制芯片资料可查阅《XPT2046》数据手册获知。

在前面我们学习了如何使用FSMC外设控制液晶屏并用它显示各种图形及文字,利用液晶屏,STM32的系统具有了高级信息输出功能,然而我们还希望有用户友好的输入设备,触摸屏是不二之选,目前大部分电子设备都使用触摸屏配合液晶显示器组成人机交互系统。

28.1. 触摸屏简介

触摸屏又称触控面板,它是一种把触摸位置转化成坐标数据的输入设备,根据触摸屏的检测原理,主要分为电阻式触摸屏和电容式触摸屏。相对来说,电阻屏造价便宜,能适应较恶劣的环境,但它只支持单点触控(一次只能检测面板上的一个触摸位置),触摸时需要一定的压力,使用久了容易造成表面磨损,影响寿命;而电容屏具有支持多点触控、检测精度高的特点,电容屏通过与导电物体产生的电容效应来检测触摸动作,只能感应导电物体的触摸,湿度较大或屏幕表面有水珠时会影响电容屏的检测效果。

图 28‑1单电阻屏、电阻液晶屏(带触摸控制芯片) 图 28‑2单电容屏、电容液晶屏(带触摸控制芯片)

图28_1图28_2 分别是带电阻触摸屏及电容触摸屏的两种屏幕,从外观上并没有明显 的区别,区分电阻屏与电容屏最直接的方法就是使用绝缘物体点击屏幕,因为电阻屏通过压 力能正常检测触摸动作,而该绝缘物体无法影响电容屏所检测的信号,因而无法检测到触 摸动作。目前电容式触摸屏被大部分应用在智能手机、平板电脑等电子设备中,而在汽车导航、工控机等设备中电阻式触摸屏仍占主流。

28.1.1. 电阻式触摸屏检测原理

电阻式的触摸屏结构见 图28_3。它主要由表面硬涂层、两个ITO层、间隔点以及玻璃底层构成,这些结构层 都是透明的,整个触摸屏覆盖在液晶面板上,透过触摸屏可看到液晶面板。表面涂层起到保护作用,玻璃底层 起承载的作用,而两个ITO层是触摸屏的关键结构,它们是涂有铟锡金属氧化物的导电层。两个ITO层之间使 用间隔点使两层分开,当触摸屏表面受到压力时,表面弯曲使得上层ITO与下层ITO接触,在触点处连通电路。

图 28‑3 电阻式触摸屏结构

两个ITO涂层的两端分别引出X-、X+、Y-、Y+四个电极,见 图28_4 ,这是 电阻屏最常见的四线结构,通过这些电极,外部电路向这两个涂层可以施加匀强电场或检测电压。

图 28‑4 XY的ITO层结构

当触摸屏被按下时,两个ITO层相互接触,从触点处把ITO层分为两个电阻,且由于ITO层均匀导电,两个电阻的大 小与触点离两电极的距离成比例关系,利用这个特性,可通过以下过程来检测坐标,这也正是电阻触摸屏名称的 由来,见 图28_5

  • 计算X坐标时,在X+电极施加驱动电压Vref,X-极接地,所以X+与X-处形成了匀强电场,而触点 处的电压通过Y+电极采集得到,由于ITO层均匀导电,触点电压与Vref之比等于触点X坐标与屏宽度之比,从而:

\[x = \frac{V_{Y +}}{V_{\text{ref}}} \times Width\]
  • 计算Y坐标时,在Y+电极施加驱动电压Vref,Y-极接地,所以Y+与Y-处形成了匀 强电场,而触点处的电压通过X+电极采集得到,由于ITO层均匀导电,触点电压与Vref之比等于触点Y坐标与屏高度之比,从而:

\[y = \frac{V_{Y +}}{V_{\text{ref}}} \times Height\]
图 28‑5 触摸检测等效电路

28.1.2. 电阻触摸屏控制芯片

为了方便检测触摸的坐标,一些芯片厂商制作了电阻屏专用的控制芯片,控制上述采集过程、采集电压,外部微控制器直接与触摸控制芯片通讯直接获得触点的电压或坐标。如图 28‑1中我们生产的这款3.2寸电阻触摸屏就是采用XPT2046芯片作为触摸控制芯片,XPT2046芯片控制4线电阻触摸屏,STM32与XPT2046采用SPI通讯获取采集得的电压,然后转换成坐标。

XPT2046是专用在四线电阻屏的触摸屏控制器,STM32可通过SPI接口向它写入控制字,由它测得X、Y方向的触点电压返回给STM32。见 图28_6

图 28‑6 TSC2046与电阻屏的连接图

图 28‑6 TSC2046与电阻屏的连接图

图中,电阻屏两层阻性材料的两端分别接入到XPT2046的X+、X-和Y+、Y-。当要测量X坐 标时,STM32通过SPI接口写命令到XPT2046,使它通过内部的模拟开关使X+、X-接 通电源,于是在电阻屏的X方向上产生一个匀强电场;把Y+、Y-连接到XPT2046的ADC。当电 阻屏被触摸时,上、下两层的阻性材料接触,在PENIRQ引脚产生一个中断信号,通知 STM32。该触点的电压由Y+或Y-(此时的Y+Y-电阻很小,可忽略)引入到ADC进行测量,STM32读取该电 压,进行软件转换,就可以测得触点X方向的坐标。同理可以测得Y方向的坐标。

XPT2046输出的ADC电压值是12位的,这也是它型号中2046名称的来源。

28.1.3. 电容式触摸屏检测原理

与电阻式触摸屏不同,电容式触摸屏不需要通过压力使触点变形,再通过触点处电压值来检测坐标,它的基本原理和前面定 时器章节中介绍的电容按键类似,都是利用充电时间检测电容大小,从而通过检测出电容值的变化来获知触摸信号。见 图28_7,电容屏的最上层是玻璃(不会像电阻屏那样形变),核心层部分也是由ITO材料构成的,这些导电材料在屏幕 里构成了人眼看不见的静电网,静电网由多行X轴电极和多列Y轴电极构成,两个电极之间会形成电容。触摸屏 工作时,X轴电极发出AC交流信号,而交流信号能穿过电容,即通过Y轴能感应出该信号,当交流电穿越时电容会有 充放电过程,检测该充电时间可获知电容量。若手指触摸屏幕,会影响触摸点附近两个电极之间的耦合,从而改变 两个电极之间的电容量,若检测到某电容的电容量发生了改变,即可获知该电容处有触摸动作(这就是为什么它被称 为电容式触摸屏以及绝缘体触摸没有反应的原因)。

图 28‑7 电容触摸屏基本原理

电容屏ITO层的结构见 图28_8,这是比较常见的形式,电极由多个菱形导体组成,生产时使用蚀刻工艺在ITO层生成这样的结构。

图 28‑8 电容触摸屏的ITO层结构

X轴电极与Y轴电极在交叉处形成电容,即这两组电极构成了电容的两极,这样的结构覆盖了整个电容屏,每个电容单元在触摸屏中都有其特定的物理位置,即电容的位置就是它在触摸屏的XY坐标。检测触摸的坐标时,第1条X轴的电极发出激励信号,而所有Y轴的电极同时接收信号,通过检测充电时间可检测出各个Y轴与第1条X轴相交的各个互电容的大小,各个X轴依次发出激励信号,重复上述步骤,即可得到整个触摸屏二维平面的所有电容大小。当手指接近时,会导致局部电容改变,根据得到的触摸屏电容量变化的二维数据表,可以得知每个触摸点的坐标,因此电容触摸屏支持多点触控。

其实电容触摸屏可看作是多个电容按键组合而成,就像机械按键中独立按键和矩阵按键的关系一样,甚至电容触摸屏的坐标扫描方式与矩阵按键都是很相似的。

在野火的F4系列产品中有使用到电容触摸屏,感兴趣可以去了解一下。

28.2. 电阻触摸屏—触摸画板实验

本小节讲解如何驱动电阻触摸屏,并利用触摸屏制作一个简易的触摸画板应用。

学习本小节内容时,请打开配套的“电阻触摸屏—触摸画板”工程配合阅读。

28.2.1. 硬件设计

图 28‑9 液晶屏实物图

图28_9 液晶屏背面的PCB电路对应 图28_10图28_11图28_12 中的原理图,分别是屏幕PCB底板原理图、触摸部分原理图、液晶排针接口线序图。

图 28‑10屏幕PCB底板原理图(截图于《3.2寸液晶原理图.pdf》)

屏幕的PCB底板引出的信号线会通过PCB底板上的FPC接口与液晶面板连接,这些信包括液晶控制相 关的CS、RS等信号及DB0-DB15数据线,其中RS引脚以高电平表示传输数据,低电平表示传输命 令;另外还有引出LCD_BK引脚用于控制屏幕的背光供电,可以通过该引脚控制背光的强度,该 引脚为低电平时打开背光。图中的X+/X-/Y+/Y-引脚是液晶面板上触摸屏引出的信号线,它们会被 连接到PCB底板的电阻触摸屏控制器,用于检测触摸信号,其原理图见 图28_11

图 28‑11 屏幕PCB底板的触摸部分原理图(截图于《3.2寸液晶原理图.pdf》)

触摸检测的主体是型号为XPT2046的芯片,它接收触摸屏的X+/X-/Y+/Y-信号进行处理,把触摸信息使用SPI 接口输出到STM32等控制器,注意,由于控制XPT2046芯片的并不是STM32专用的硬件SPI接口,所以在编写 程序时,需要使用软件模拟SPI时序与触摸芯片进行通讯。

图 28‑12 液晶屏接口(截图于《3.2寸液晶原理图.pdf》)

图28_12 表示的是PCB底板引出的排针线序,屏幕整体通过这些引出的排针与开发板或其它控制器连接。

图 28‑13 开发板与屏幕的连接的信号说明(截图于《指南者开发板原理图.pdf》)

图28_13 是指南者开发板上的液晶排母接口原理图,它说明了配套的3.2寸屏幕接入 到开发板上时的信号连接关系。其中请着重关注图中液晶屏LCD_CS及LCD_RS(即DC引脚) 与FSMC存储区选择引脚FSMC_NE及地址信号FSMC_A的编号,它们会决定STM32要使用什么内存地址来控制与液晶屏的通讯。

以上原理图可查阅《3.2寸液晶原理图.pdf》及《指南者开发板原理图.pdf》文档获知,若您使用的液晶屏或实验板不一样,请根据实际连接的引脚修改程序。

28.2.2. 软件设计

本工程中的把触摸屏相关的控制代码都存储到了“bsp_xpt2046_lcd.c”及“bsp_xpt2046_lcd.h”文件中,这些文件也可根据您的喜好命名,它们不属于STM32HAL库的内容,是由我们自己根据应用需要编写的。

28.2.2.1. 编程要点

  1. 编写软件模拟SPI协议的驱动;

  2. 编写触摸芯片的控制驱动,如发送命令字,获取触摸坐标等;

  3. 编写触摸校正程序;

  4. 编写测试程序检验驱动。

28.2.2.2. 代码分析

28.2.2.2.1. 触摸屏硬件相关宏定义

根据触摸屏与STM32芯片的硬件连接,我们把触摸屏硬件相关的配置都以宏的形式定义到 “bsp_xpt2046_lcd.h”文件中,见 代码清单28_1

代码清单 28‑1 触摸屏硬件配置相关的宏(bsp_xpt2046_lcd.h文件)
 /* XPT2046 触摸屏触摸信号指示引脚定义(不使用中断)*/
 #define    XPT2046_PENIRQ_GPIO_PORT   GPIOE
 #define    XPT2046_PENIRQ_GPIO_PIN    GPIO_PIN_4

 //触屏信号有效电平
 #define   XPT2046_PENIRQ_ActiveLevel       0
 #define   XPT2046_PENIRQ_Read()        HAL_GPIO_ReadPin (
                             XPT2046_PENIRQ_GPIO_PORT,
                             XPT2046_PENIRQ_GPIO_PIN )

 /*XPT2046 触摸屏模拟SPI引脚定义*/

 #define    XPT2046_SPI_CS_PIN             GPIO_PIN_13
 #define    XPT2046_SPI_CS_PORT            GPIOD

 #define    XPT2046_SPI_CLK_PIN            GPIO_PIN_0
 #define    XPT2046_SPI_CLK_PORT           GPIOE

 #define    XPT2046_SPI_MOSI_PIN           GPIO_PIN_2
 #define    XPT2046_SPI_MOSI_PORT          GPIOE

 #define    XPT2046_SPI_MISO_PIN           GPIO_PIN_3
 #define    XPT2046_SPI_MISO_PORT          GPIOE

 /* 直接操作寄存器的方法控制IO */
 #define digitalHi(p,i)      {p->BSRR=i;}  //设置为高电平
 #define digitalLo(p,i)      {p->BSRR=(uint32_t)i << 16;}//输出低电平

 #define   XPT2046_CS_ENABLE()      digitalHi( XPT2046_SPI_CS_PORT,
                             XPT2046_SPI_CS_PIN )
 #define   XPT2046_CS_DISABLE()     digitalLo ( XPT2046_SPI_CS_PORT,
                             XPT2046_SPI_CS_PIN )

 #define   XPT2046_CLK_HIGH()       digitalHi ( XPT2046_SPI_CLK_PORT,
                             XPT2046_SPI_CLK_PIN )
 #define   XPT2046_CLK_LOW()        digitalLo ( XPT2046_SPI_CLK_PORT,
                         XPT2046_SPI_CLK_PIN )

 #define   XPT2046_MOSI_1()         digitalHi ( XPT2046_SPI_MOSI_PORT,
                         XPT2046_SPI_MOSI_PIN )
 #define   XPT2046_MOSI_0()         digitalLo ( XPT2046_SPI_MOSI_PORT,
                         XPT2046_SPI_MOSI_PIN )

 #define   XPT2046_MISO()           HAL_GPIO_ReadPin (
                         XPT2046_SPI_MISO_PORT, XPT2046_SPI_MISO_PIN )

以上代码根据硬件的连接,把STM32与触摸屏通讯使用的引脚号和控制CS/CLK/MOSI引脚输出高低电平的操作、读取MISO引脚电平状态的操作都使用宏封装了起来,以便后面制作模拟SPI时序的驱动。另外,本驱动中XPT2046的PENIRQ触摸信号并没有使用中断检测,而是使用普通的引脚电平轮询获取状态的。

28.2.2.2.2. 初始化触摸屏控制引脚

利用上面的宏,编写触摸屏控制引脚的初始化函数,见 代码清单28_2

代码清单 28‑2 触摸屏控制引脚的GPIO初始化函数(bsp_xpt2046_lcd.c文件)
 void XPT2046_Init ( void )
 {
     GPIO_InitTypeDef  GPIO_InitStructure;
     /* 开启GPIO时钟 */
     __HAL_RCC_GPIOF_CLK_ENABLE();
     __HAL_RCC_GPIOG_CLK_ENABLE();

     /* 模拟SPI GPIO初始化 */
     GPIO_InitStructure.Pin=XPT2046_SPI_CLK_PIN;
     GPIO_InitStructure.Speed=GPIO_SPEED_FREQ_HIGH ;
     GPIO_InitStructure.Mode=GPIO_MODE_OUTPUT_PP;
     HAL_GPIO_Init(XPT2046_SPI_CLK_PORT, &GPIO_InitStructure);

     GPIO_InitStructure.Pin = XPT2046_SPI_MOSI_PIN;
     HAL_GPIO_Init(XPT2046_SPI_MOSI_PORT, &GPIO_InitStructure);

     GPIO_InitStructure.Pin = XPT2046_SPI_CS_PIN;
     GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH ;
     GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
     HAL_GPIO_Init(XPT2046_SPI_CS_PORT, &GPIO_InitStructure);

     GPIO_InitStructure.Pin = XPT2046_SPI_MISO_PIN;
     GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH ;
     GPIO_InitStructure.Mode = GPIO_MODE_INPUT;  //上拉输入
     GPIO_InitStructure.Pull = GPIO_PULLUP;
     HAL_GPIO_Init(XPT2046_SPI_MISO_PORT, &GPIO_InitStructure);

     /* 拉低片选,选择XPT2046 */
     XPT2046_CS_DISABLE();

     //触摸屏触摸信号指示引脚,不使用中断
     GPIO_InitStructure.Pin = XPT2046_PENIRQ_GPIO_PIN;
     GPIO_InitStructure.Mode = GPIO_MODE_INPUT;  //上拉输入
     GPIO_InitStructure.Pull = GPIO_PULLUP;
     HAL_GPIO_Init(XPT2046_PENIRQ_GPIO_PORT, &GPIO_InitStructure);
 }

以上函数直接初始化了触摸屏用到的SPI信号线,由于使用软件模拟SPI协议的方式,所以它把MISO设置为输入,其余的MOSI、CLK、CS引脚均配置为普通的推挽输出模式,而PENIRQ作为触摸信号的输入检测,它也被设置成输入模式。

28.2.2.2.3. 模拟SPI协议的读写时序

初始化完引脚,即可编写SPI协议的模拟时序,见 代码清单28_3

代码清单 28‑3 模拟SPI的读写时序(bsp_xpt2046_lcd.c文件)
 /**
 * @brief  用于 XPT2046 的简单微秒级延时函数
 * @param  nCount :延时计数值,单位为微妙
 * @retval 无
 */
 static void XPT2046_DelayUS ( __IO uint32_t ulCount )
 {
     uint32_t i;
     for ( i = 0; i < ulCount; i ++ ) {
         uint8_t uc = 12;     //设置值为12,大约延1微秒
         while ( uc -- );     //延1微秒
     }
 }

 /**
 * @brief  XPT2046 的写入命令
 * @param  ucCmd :命令
 *   该参数为以下值之一:
 *     @arg 0x90 :通道Y+的选择控制字
 *     @arg 0xd0 :通道X+的选择控制字
 * @retval 无
 */
 static void XPT2046_WriteCMD ( uint8_t ucCmd )
 {
     uint8_t i;
     XPT2046_MOSI_0();
     XPT2046_CLK_LOW();
     for ( i = 0; i < 8; i ++ ) {
         if ( ( ucCmd >> ( 7 - i ) ) & 0x01 ) {
             XPT2046_MOSI_1()
         } else {
             XPT2046_MOSI_0();
         }
         XPT2046_DelayUS ( 5 );
         XPT2046_CLK_HIGH();
         XPT2046_DelayUS ( 5 );
         XPT2046_CLK_LOW();
     }
 }

 /**
 * @brief  XPT2046 的读取命令
 * @param  无
 * @retval 读取到的数据
 */
 static uint16_t XPT2046_ReadCMD ( void )
 {
     uint8_t i;
     uint16_t usBuf=0, usTemp;
     XPT2046_MOSI_0();
     XPT2046_CLK_HIGH();
     for ( i=0; i<12; i++ ) {
         XPT2046_CLK_LOW();
         usTemp = XPT2046_MISO();
         usBuf |= usTemp << ( 11 - i );
         XPT2046_CLK_HIGH();
     }
     return usBuf;
 }

SPI协议的读写时序都比较简单,只要驱动好一个时钟信号传输一个数据位即可,发送数据时使用MOSI引脚输出电平,读取数据时从MISO引脚获取状态。

代码中的XPT2046_WriteCMD函数主要在后面用于发送控制触摸芯片的命令代码,发送不同的命令可以控制触摸芯片检测X坐标或Y坐标的触摸信号,该命令代码一般为8个数据位;而XPT2046_ReadCMD函数主要在后面用于读取触摸芯片输出的ADC电压值,这些ADC电压值一般为12个数据位。

28.2.2.2.4. 采集触摸原始数据

利用XPT2046_WriteCMD及XPT2046_ReadCMD函数,可控制触摸屏检测并获取触摸的原始ADC数据,见 代码清单28_4

代码清单 28‑4采集触摸原始数据(bsp_xpt2046_lcd.c文件)
 #define  XPT2046_CHANNEL_X    0x90            //通道Y+的选择控制字
 #define  XPT2046_CHANNEL_Y    0xd0            //通道X+的选择控制字

 /**
 * @brief  对 XPT2046 选择一个模拟通道后,启动ADC,并返回ADC采样结果
 * @param  ucChannel
 *   该参数为以下值之一:
 *     @arg 0x90 :通道Y+的选择控制字
 *     @arg 0xd0 :通道X+的选择控制字
 * @retval 该通道的ADC采样结果
 */
 static uint16_t XPT2046_ReadAdc ( uint8_t ucChannel )
 {
     XPT2046_WriteCMD ( ucChannel );
     return  XPT2046_ReadCMD ();
 }


 /**
 * @brief  读取 XPT2046 的X通道和Y通道的AD值(12 bit,最大是4096)
 * @param  sX_Ad :存放X通道AD值的地址
 * @param  sY_Ad :存放Y通道AD值的地址
 * @retval 无
 */
 static void XPT2046_ReadAdc_XY ( int16_t * sX_Ad, int16_t * sY_Ad )
 {
     int16_t sX_Ad_Temp, sY_Ad_Temp;

     sX_Ad_Temp = XPT2046_ReadAdc ( XPT2046_CHANNEL_X );
     XPT2046_DelayUS ( 1 );
     sY_Ad_Temp = XPT2046_ReadAdc ( XPT2046_CHANNEL_Y );

     * sX_Ad = sX_Ad_Temp;
     * sY_Ad = sY_Ad_Temp;
 }

根据触摸芯片的要求,发送命令代码XPT2046_CHANNEL_X(0x90)后,电阻屏的X方向会通电,然后触摸屏使用Y通道检测得电压,获取到触摸点X方向的ADC原始值;发送命令代码XPT2046_CHANNEL_Y(0xd0)后,电阻屏的Y方向会通电,然后触摸屏使用X通道检测得电压,获取到触摸点Y方向的ADC原始值。把该过程封装起来,即可得到XPT2046_ReadAdc及XPT2046_ReadAdc_XY函数,实际应用中通常直接调用XPT2046_ReadAdc_XY函数以检测两个方向的触摸数据。

28.2.2.2.5. 多次采样求平均值

为了使得采样更精确,工程中使用 代码清单28_5 中的函数来采集最终使用的数据。

代码清单 28‑5 多次采样求平均值(bsp_xpt2046_lcd.c文件)
 static uint8_t XPT2046_ReadAdc_Smooth_XY ( strType_XPT2046_Coordinate
                                             * pScreenCoordinate )
 {
     uint8_t ucCount = 0, i;

     int16_t sAD_X, sAD_Y;
     int16_t sBufferArray [ 2 ] [ 10 ] = { { 0 },{ 0 } };//坐标X和Y进行多次采样

     //存储采样中的最小值、最大值
     int32_t lX_Min, lX_Max, lY_Min, lY_Max;

     /* 循环采样10次 */
     do {
         XPT2046_ReadAdc_XY ( & sAD_X, & sAD_Y );

         sBufferArray [ 0 ] [ ucCount ] = sAD_X;
         sBufferArray [ 1 ] [ ucCount ] = sAD_Y;

         ucCount ++;

     } while ( ( XPT2046_PENIRQ_Read() == XPT2046_PENIRQ_ActiveLevel )
             && ( ucCount < 10 ) );
             //用户点击触摸屏时即TP_INT_IN信号为低并且 ucCount<10

     /*如果触笔弹起*/
     if ( XPT2046_PENIRQ_Read() != XPT2046_PENIRQ_ActiveLevel )
         ucXPT2046_TouchFlag = 0;      //中断标志复位

     /*如果成功采样10个样本*/
     if ( ucCount ==10 ) {
         lX_Max = lX_Min = sBufferArray [ 0 ] [ 0 ];
         lY_Max = lY_Min = sBufferArray [ 1 ] [ 0 ];

         for ( i = 1; i < 10; i ++ ) {
             if ( sBufferArray[ 0 ] [ i ] < lX_Min )
                 lX_Min = sBufferArray [ 0 ] [ i ];

             else if ( sBufferArray [ 0 ] [ i ] > lX_Max )
                 lX_Max = sBufferArray [ 0 ] [ i ];

         }

         for ( i = 1; i < 10; i ++ ) {
             if ( sBufferArray [ 1 ] [ i ] < lY_Min )
                 lY_Min = sBufferArray [ 1 ] [ i ];

             else if ( sBufferArray [ 1 ] [ i ] > lY_Max )
                 lY_Max = sBufferArray [ 1 ] [ i ];
         }

         /*去除最小值和最大值之后求平均值*/
         pScreenCoordinate ->x =  ( sBufferArray [ 0 ] [ 0 ] +
                                 sBufferArray [ 0 ] [ 1 ] +
                                 sBufferArray [ 0 ] [ 2 ] +
                                 sBufferArray [ 0 ] [ 3 ] +
                                 sBufferArray [ 0 ] [ 4 ] +
                                 sBufferArray [ 0 ] [ 5 ] +
                                 sBufferArray [ 0 ] [ 6 ] +
                                 sBufferArray [ 0 ] [ 7 ] +
                                 sBufferArray [ 0 ] [ 8 ] +
                                 sBufferArray [ 0 ] [ 9 ] - lX_Min-
                                 lX_Max ) >> 3;

         pScreenCoordinate ->y =  ( sBufferArray [ 1 ] [ 0 ] +
                                 sBufferArray [ 1 ] [ 1 ] +
                                 sBufferArray [ 1 ] [ 2 ] +
                                 sBufferArray [ 1 ] [ 3 ] +
                                 sBufferArray [ 1 ] [ 4 ] +
                                 sBufferArray [ 1 ] [ 5 ] +
                                 sBufferArray [ 1 ] [ 6 ] +
                                 sBufferArray [ 1 ] [ 7 ] +
                                 sBufferArray [ 1 ] [ 8 ] +
                                 sBufferArray [ 1 ] [ 9 ] - lY_Min-
                                 lY_Max ) >> 3;
         return 1;
     }
     return 0;
 }

本函数有一个输入参数strType_XPT2046_Coordinate类型的结构体,它主要包含x/y/pre_x/pre_y四个结构体成员,其中x/y是用来存储最新的触摸参数值的,而pre_x/pre_y用于存储上一次的触摸点。本函数中仅使用了x/y结构体成员值,且使用它存储的是触摸屏的原始触摸数据,即ADC值。

代码中对X、Y坐标各采样10次,然后去除极大极小值后再取平均,计算结果即存储在结构体中的x/y成员值中。

28.2.2.2.6. 根据原始数据计算坐标值

由XPT2046_ReadAdc_Smooth_XY 函数得到触摸原始数据后,再使用 代码清单28_6 中的XPT2046_Get_TouchedPoint即可计算出对应的触摸坐标。

代码清单 28‑6 根据原始数据计算坐标值(bsp_xpt2046_lcd.c文件)
 typedef struct {       //校准系数结构体(最终使用)
     float dX_X,
         dX_Y,
         dX,
         dY_X,
         dY_Y,
         dY;
 } strType_XPT2046_TouchPara;

 //默认触摸参数,不同的屏幕稍有差异,可重新调用触摸校准函数获取
 strType_XPT2046_TouchPara strXPT2046_TouchPara[] = {
 -0.006464,   -0.073259,  280.358032,    0.074878,    0.002052,   -6.545977,//扫描方式0
 0.086314,    0.001891,  -12.836658,   -0.003722,   -0.065799,  254.715714,//扫描方式1
 0.002782,    0.061522,  -11.595689,    0.083393,    0.005159,  -15.650089,//扫描方式2
 0.089743,   -0.000289,  -20.612209,   -0.001374,    0.064451,  -16.054003,//扫描方式3
 0.000767,   -0.068258,  250.891769,   -0.085559,   -0.000195,  334.747650,//扫描方式4
 -0.084744,    0.000047,  323.163147,   -0.002109,   -0.066371,  260.985809,//扫描方式5
 -0.001848,    0.066984,  -12.807136,   -0.084858,   -0.000805,  333.395386,//扫描方式6
 -0.085470,   -0.000876,  334.023163,   -0.003390,    0.064725,   -6.211169,//扫描方式7
 };

 //液晶屏扫描模式,本变量主要用于方便选择触摸屏的计算参数
 //参数可选值为0-7
 //调用ILI9341_GramScan函数设置方向时会自动更改
 //LCD刚初始化完成时会使用本默认值
 uint8_t LCD_SCAN_MODE = 6;

 /**
 * @brief  获取 XPT2046 触摸点(校准后)的坐标
 * @param  pDisplayCoordinate :该指针存放获取到的触摸点坐标
 * @param  pTouchPara:坐标校准系数
 * @retval 获取情况
 *   该返回值为以下值之一:
 *     @arg 1 :获取成功
 *     @arg 0 :获取失败
 */
 uint8_t XPT2046_Get_TouchedPoint ( strType_XPT2046_Coordinate *
 pDisplayCoordinate,
                         strType_XPT2046_TouchPara * pTouchPara )
 {
     uint8_t ucRet = 1;           //若正常,则返回0
     strType_XPT2046_Coordinate strScreenCoordinate;

     if ( XPT2046_ReadAdc_Smooth_XY ( & strScreenCoordinate ) ) {
         pDisplayCoordinate ->x = ( ( pTouchPara[LCD_SCAN_MODE].dX_X *
                                     strScreenCoordinate.x ) +
             ( pTouchPara[LCD_SCAN_MODE].dX_Y * strScreenCoordinate.y )

                                 pTouchPara[LCD_SCAN_MODE].dX );

         pDisplayCoordinate ->y = ( ( pTouchPara[LCD_SCAN_MODE].dY_X *
                                     strScreenCoordinate.x ) +
             ( pTouchPara[LCD_SCAN_MODE].dY_Y * strScreenCoordinate.y ) +
                                 pTouchPara[LCD_SCAN_MODE].dY );
     } else ucRet = 0;          //如果获取的触点信息有误,则返回0

     return ucRet;
 }

在实际应用中,并不会使用前面介绍触摸原理时讲解的直接按比例运算把触摸原始数据物理坐标转换 成与液晶屏像素对应的XY逻辑坐标(如触摸屏输出的原始数据范围为0-2045,液晶屏的像素XY坐 标为0-239及0-319),那种直接转换的方式误差比较大,所以通常会采用“多点触摸校正法”来转换坐标,使用 这种方式时,在应用前需要校正屏幕。校正时,使用液晶屏在特定的位置显示几个点要求用户点击,根据触摸校准 算法的数学关系把逻辑坐标与物理坐标转换公式的各个系数计算出来。

这些触摸转换系数,在我们上述代码中使用strType_XPT2046_TouchPara类型来存储,一共有6个系数。利用这个数据类型,代码中定义了一个数组strXPT2046_TouchPara,它存储了液晶屏在8个扫描方向时使用的转换系数,这些系数是我编写代码时使用某个液晶屏测试出来的,作为默认转换系数,不同的液晶屏这些转换系数稍有差异,若在实际使用中你感觉触摸不准确,可以使用校准函数XPT2046_Touch_Calibrate来重新计算自己屏幕的转换系数。

而本代码中列出的XPT2046_Get_TouchedPoin本函数可利用两用的转换系数计算出当前的触摸逻辑坐标。它有两个输入参数,一个参数pDisplayCoordinate用于存储计算后得到的触摸逻辑坐标,作为计算输出,这坐标与液晶屏对应;而参数pTouchPara即为校准系数,作为计算输入。在函数的内部,它先调用XPT2046_ReadAdc_Smooth_XY 检测触摸点的原始数据物理坐标,然后代入公式中计算输出逻辑坐标。

28.2.2.2.7. 触摸校正

触摸校正函数XPT2046_Touch_Calibrate的代码涉及到的都是数学函数映射关系的运算,比较复杂,此处作原理讲解,在 工程应用需要校正时,可采用 代码清单28_7 中的Calibrate_or_Get_TouchParaWithFlash函数。

代码清单 28‑7 校正并存储转换系数到SPI FLASH(bsp_xpt2046_lcd.c文件)
 //触摸参数写到FLASH里的标志
 #define   FLASH_TOUCH_PARA_FLAG_VALUE   0xA5

 //触摸标志写到FLASH里的地址
 #define   FLASH_TOUCH_PARA_FLAG_ADDR    (1*1024)

 //触摸参数写到FLASH里的地址
 #define   FLASH_TOUCH_PARA_ADDR     (2*1024)

 /**
 * @brief  从FLASH中获取 或 重新校正触摸参数(校正后会写入到SPI FLASH中)
 * @note   若FLASH中从未写入过触摸参数,
 *           会触发校正程序校正LCD_Mode指定模式的触摸参数,此时其它模式写入默认值
 *
 *         若FLASH中已有触摸参数,且不强制重新校正
 *           会直接使用FLASH里的触摸参数值
 *
 *         每次校正时只会更新指定的LCD_Mode模式的触摸参数,其它模式的不变
 * @note  本函数调用后会把液晶模式设置为LCD_Mode
 *
 * @param  LCD_Mode:要校正触摸参数的液晶模式
 * @param  forceCal:是否强制重新校正参数,可以为以下值:
 *   @arg 1:强制重新校正
 *   @arg 0:只有当FLASH中不存在触摸参数标志时才重新校正
 * @retval 无
 */
 void Calibrate_or_Get_TouchParaWithFlash(uint8_t LCD_Mode,
 uint8_t forceCal)
 {
     uint8_t para_flag=0;

     //初始化FLASH
     SPI_FLASH_Init();

     //读取触摸参数标志
     SPI_FLASH_BufferRead(&para_flag,FLASH_TOUCH_PARA_FLAG_ADDR,1);

     //若不存在标志或florceCal=1时,重新校正参数
     if (para_flag != FLASH_TOUCH_PARA_FLAG_VALUE | forceCal ==1) {
         //若标志存在,说明原本FLASH内有触摸参数,
         //先读回所有LCD模式的参数值,以便稍后强制更新时只更新指定LCD模式的参数,其它模式的不变
     if (  para_flag == FLASH_TOUCH_PARA_FLAG_VALUE && forceCal == 1) {
 SPI_FLASH_BufferRead((uint8_t *)&strXPT2046_TouchPara,FLASH_TOUCH_PARA_ADDR,4*6*8);
         }

         //等待触摸屏校正完毕,更新指定LCD模式的触摸参数值
         while ( ! XPT2046_Touch_Calibrate (LCD_Mode) );

         //擦除扇区
         SPI_FLASH_SectorErase(0);

         //设置触摸参数标志
         para_flag = FLASH_TOUCH_PARA_FLAG_VALUE;
         //写入触摸参数标志
         SPI_FLASH_BufferWrite(&para_flag,FLASH_TOUCH_PARA_FLAG_ADDR,1);
         //写入最新的触摸参数
 SPI_FLASH_BufferWrite((uint8_t*)&strXPT2046_TouchPara,FLASH_TOUCH_PARA_ADDR,4*6*8);
     } else {  //若标志存在且不强制校正,则直接从FLASH中读取
 SPI_FLASH_BufferRead((uint8_t *)&strXPT2046_TouchPara,FLASH_TOUCH_PARA_ADDR,4*6*8);
     }
 }

本函数实际上对触摸校正函数XPT2046_Touch_Calibrate做了封装,加入了把转换系数存储在外部SPI FLASH的功能,以便下次板子重新上电也能使用上一次的校正得到的系数。在调用时,使用LCD_Mode参数选择要校正的液晶 扫描模式,使用forceCal选择是否要强制校正;该函数调用后会触发XPT2046_Touch_Calibrate的校正函数,在屏幕上显示 几个触点提示用户点击,若校正成功,则会把转换系数写入到外部的SPI FLASH空间中并加入记录标志,下次再调用本函 数的时候,若不是使用forceCal设置成强制校正是不会触发重新校正的过程的。

28.2.2.2.8. 触摸检测状态机

前面介绍的函数都是获取坐标相关的,然而那些函数并不需要长期调用,只有当检测到触摸信号的时候,再去检测坐标即可,检测 触摸信号可以使用 代码清单28_8 中的触摸状态机检测。

代码清单 28‑8 触摸检测状态机(bsp_xpt2046_lcd.c文件)
 /******触摸状态机相关******/

 //触屏信号有效电平
 #define  XPT2046_PENIRQ_ActiveLevel  0
 #define  XPT2046_PENIRQ_Read()   GPIO_ReadInputDataBit ( XPT2046_PENIRQ_GPIO_PORT,
                                                         XPT2046_PENIRQ_GPIO_PIN )

 typedef enum {
     XPT2046_STATE_RELEASE  = 0, //触摸释放
     XPT2046_STATE_WAITING,      //触摸按下
     XPT2046_STATE_PRESSED,      //触摸按下
 } enumTouchState  ;

 #define TOUCH_PRESSED         1
 #define TOUCH_NOT_PRESSED     0

 //触摸消抖阈值
 #define DURIATION_TIME        2

 /**
 * @brief  触摸屏检测状态机
 * @retval 触摸状态
 *   该返回值为以下值之一:
 *     @arg TOUCH_PRESSED :触摸按下
 *     @arg TOUCH_NOT_PRESSED :无触摸
 */
 uint8_t XPT2046_TouchDetect(void)
 {
     static enumTouchState touch_state = XPT2046_STATE_RELEASE;
     static uint32_t i;
     uint8_t detectResult = TOUCH_NOT_PRESSED;

     switch (touch_state) {
     case XPT2046_STATE_RELEASE:
         if (XPT2046_PENIRQ_Read() == XPT2046_PENIRQ_ActiveLevel) {
             //第一次出现触摸信号
             touch_state = XPT2046_STATE_WAITING;
             detectResult =TOUCH_NOT_PRESSED;
         } else {  //无触摸
             touch_state = XPT2046_STATE_RELEASE;
             detectResult =TOUCH_NOT_PRESSED;
         }
         break;

     case XPT2046_STATE_WAITING:
         if (XPT2046_PENIRQ_Read() == XPT2046_PENIRQ_ActiveLevel) {
             i++;
             //等待时间大于阈值则认为触摸被按下
             //消抖时间 = DURIATION_TIME * 本函数被调用的时间间隔
 //如在定时器中调用,每10ms调用一次,则消抖时间为:DURIATION_TIME*10ms
             if (i > DURIATION_TIME) {
                 i=0;
                 touch_state = XPT2046_STATE_PRESSED;
                 detectResult = TOUCH_PRESSED;
             } else {      //等待时间累加
                 touch_state = XPT2046_STATE_WAITING;
                 detectResult =   TOUCH_NOT_PRESSED;
             }
         } else {  //等待时间值未达到阈值就为无效电平,当成抖动处理
             i = 0;
             touch_state = XPT2046_STATE_RELEASE;
             detectResult = TOUCH_NOT_PRESSED;
         }
         break;

     case XPT2046_STATE_PRESSED:
         if (XPT2046_PENIRQ_Read() == XPT2046_PENIRQ_ActiveLevel) {
             //触摸持续按下
             touch_state = XPT2046_STATE_PRESSED;
             detectResult = TOUCH_PRESSED;
         } else {  //触摸释放
             touch_state = XPT2046_STATE_RELEASE;
             detectResult = TOUCH_NOT_PRESSED;
         }
         break;

     default:
         touch_state = XPT2046_STATE_RELEASE;
         detectResult = TOUCH_NOT_PRESSED;
         break;
     }
     return detectResult;
 }

当触摸屏有触点按下时,PENIRQ引脚会输出低电平,直到没有触摸的时候,它才会输出高电平;而且STM32的中断只支持边沿触发(上升沿或下降沿),不支持电平触发,在触摸屏上存在类似机械按键的信号抖动,所以如果使用中断的方式来检测触摸状态并不适合,难以辨别触摸按下及释放的情况。

状态机编程是一种非常高效的编程方式,它非常适合应用在涉及状态转换的过程控制中,上述代码采用状态机的编程方式对触摸状态进行检测,主 要涉及触摸的按下、消抖及释放这三种状态转换。在应用时,本函数需要在循环体里调用,或定时调用(如每隔10ms调用一次),其状态 转换关系见 图28_14

图 28‑14触摸检测状态转换图

图 28‑14触摸检测状态转换图

在代码中,通过使用XPT2046_PENIRQ_Read函数获取当前PENIRQ引脚的电平,再根据当前的状态决定是否转换进入下一个状态,若经过消抖处理后进入“触摸确认按下/持续按下(XPT2046_STATE_PRESSED)”状态时,函数会返回TOUCH_PRESSED表示触摸被按下,其余状态返回TOUCH_NOT_PRESSED表示触摸无按下或释放状态。代码中的触摸消抖等待状态中,实质是通过延时、多次检测PENIRQ引脚的电平达到消抖的目的,若XPT2046_TouchDetect函数每隔10ms被调用一次,那么消抖的延时值则为DURIATION_TIME*10毫秒,可以根据实际情况适当调整该消抖阈值。

28.2.2.2.9. 触摸坐标获取及处理

XPT2046_TouchDetect函数只是检测了触摸是否被按下的状态态,当触摸被按下时,还要调用前面介绍的XPT2046_Get_TouchedPoint函数获取 触摸点的坐标,然后再处理,为便于使用,我们把这方面的操作封装到 代码清单28_9 中的XPT2046_TouchEvenHandler函数中。

代码清单 28‑9 触摸坐标获取及处理(bsp_xpt2046_lcd.c文件)
 typedef struct {        //液晶坐标结构体
     /*负数值表示无新数据*/
     int16_t x;      //记录最新的触摸参数值
     int16_t y;

     /*用于记录连续触摸时(长按)的上一次触摸位置*/
     int16_t pre_x;
     int16_t pre_y;

 } strType_XPT2046_Coordinate;

 /**
 * @brief   检测到触摸中断时调用的处理函数,通过它调用tp_down 和tp_up汇报触摸点
 * @note    本函数需要在while循环里被调用,也可使用定时器定时调用
 *
 *例如,可以每隔5ms调用一次,消抖阈值宏DURIATION_TIME可设置为2,这样每秒最多可以
 检测100个点。
 *           可在XPT2046_TouchDown及XPT2046_TouchUp函数中编写自己的触摸应用
 * @param   none
 * @retval  none
 */
 void XPT2046_TouchEvenHandler(void )
 {
     static strType_XPT2046_Coordinate cinfo= {-1,-1,-1,-1};

     if (XPT2046_TouchDetect() == TOUCH_PRESSED) {
         LED_GREEN;

         //获取触摸坐标
         XPT2046_Get_TouchedPoint(&cinfo,strXPT2046_TouchPara);

         //输出调试信息到串口
         XPT2046_DEBUG("x=%d,y=%d",cinfo.x,cinfo.y);

         //调用触摸被按下时的处理函数,可在该函数编写自己的触摸按下处理过程
         XPT2046_TouchDown(&cinfo);

         /*更新触摸信息到pre xy*/
         cinfo.pre_x = cinfo.x;
         cinfo.pre_y = cinfo.y;
     } else {
         LED_BLUE;

         //调用触摸被释放时的处理函数,可在该函数编写自己的触摸释放处理过程
         XPT2046_TouchUp(&cinfo);

         /*触笔释放,把 xy 重置为负*/
         cinfo.x = -1;
         cinfo.y = -1;
         cinfo.pre_x = -1;
         cinfo.pre_y = -1;
     }
 }

 /**
 * @brief   触摸屏被按下的时候会调用本函数
 * @param  touch包含触摸坐标的结构体
 * @note  请在本函数中编写自己的触摸按下处理应用
 * @retval 无
 */
 void XPT2046_TouchDown(strType_XPT2046_Coordinate * touch)
 {
     //若为负值表示之前已处理过
     if (touch->pre_x == -1 && touch->pre_x == -1)
         return;

     /***在此处编写自己的触摸按下处理应用***/

     /*处理触摸画板的选择按钮*/
     Touch_Button_Down(touch->x,touch->y);

     /*处理描绘轨迹*/
     Draw_Trail(touch->pre_x,touch->pre_y,touch->x,touch->y,&brush);

     /***在上面编写自己的触摸按下处理应用***/

 }

 /**
 * @brief   触摸屏释放的时候会调用本函数
 * @param  touch包含触摸坐标的结构体
 * @note  请在本函数中编写自己的触摸释放处理应用
 * @retval 无
 */
 void XPT2046_TouchUp(strType_XPT2046_Coordinate * touch)
 {
     //若为负值表示之前已处理过
     if (touch->pre_x == -1 && touch->pre_x == -1)
         return;

     /***在此处编写自己的触摸释放处理应用***/

     /*处理触摸画板的选择按钮*/
     Touch_Button_Up(touch->pre_x,touch->pre_y);

     /***在上面编写自己的触摸释放处理应用***/
 }

由于XPT2046_TouchEvenHandler函数带有XPT2046_TouchDetect状态机检测,所以它需要被循环或定时调用,以实现状态转换。当确认有触摸按下时,它会调用XPT2046_Get_TouchedPoint获取当前触摸坐标,并使用XPT2046_TouchDown函数根据触摸坐标进行处理,当触摸释放时,调用XPT2046_TouchUp函数处理释放坐标。

XPT2046_TouchDown和XPT2046_TouchUp函数是一个接口,用户可以根据自己的应用编写相应的触摸处理程序,把触摸按下和释放的处理加入到上述函数即可。在本工程中加入了触摸画板的按钮处理(Touch_Button_Down/Up)和绘制触摸笔迹(Draw_Trail)的操作,关于触摸画板应用的内容在“palette.c”及“palette.h”文件中,这些都是与STM32无关上层应用,感兴趣的读者可在工程中阅读,本教程就不讲解这些内容了。。

28.2.2.3. main函数

完成了触摸屏的驱动,就可以应用了,以下我们来看工程的主体main函数, 见 代码清单28_14

代码清单 28‑14 main函数
 int main(void)
 {
     /* 系统时钟初始化成72 MHz */
     SystemClock_Config();
     /* LED 端口初始化 */
     LED_GPIO_Config();
     /* 初始化串口 */
     DEBUG_USART_Config();

     ILI9341_Init ();             //LCD 初始化
     XPT2046_Init();

     printf("\r\n ********** 触摸画板程序 *********** \r\n");
     printf("\r\n 若汉字显示不正常,请阅读工程中的readme.txt文件说明,根据要求给FLASH重刷字模数据\r\n ");

     //其中0、3、5、6 模式适合从左至右显示文字,
     //不推荐使用其它模式显示文字其它模式显示文字会有镜像效果
     //其中 6 模式为大部分液晶例程的默认显示方向
     ILI9341_GramScan ( 3  );

     //绘制触摸画板界面
     Palette_Init(LCD_SCAN_MODE);

     while ( 1 ) {
         //触摸检测函数,本函数至少10ms调用一次
         XPT2046_TouchEvenHandler();
     }
 }

main函数中使用XPT2046_Init初始化触摸屏相关的引脚,然后调用Calibrate_or_Get_TouchParaWithFlash进行触摸校正;关于触摸画板应用程序的初始化,都包含在Palette_Init函数中,它会绘制触摸画板的按钮和白板界面;在main函数的while循环里调用了XPT2046_TouchEvenHandler函数,以实现状态机检测和对触摸进行处理,当有触摸按下和释放时,都通过其内部调用的XPT2046_TouchDown和XPT2046_TouchUp函数完成画板相关的操作。

28.2.3. 下载验证

编译程序下载到实验板,并上电复位,液晶屏会显示出触摸画板的界面,点击屏幕可以在该界面画出简单的图形。