stm32开发 -- TFTLCD相关
一.TFTLCD简介
ALIENTEK TFTLCD模块,该模块有如下特点:
1,2.4’/2.8’/3.5’/4.3’/7’ 5 种大小的屏幕可选。
2,320×240 的分辨率(3.5’分辨率为:320*480,4.3’和7’分辨率为:800*480)。
3,16位真彩显示。
4,自带触摸屏,可以用来作为控制输入。
以2.8寸的ALIENTEK TFTLCD模块为例介绍,该模块支持65K色显示,显示分辨率为320×240,接口为16位的80并口,自带触摸屏。外观如下图所示:
TFTLCD 模块采用2*17的2.54公排针与外部连接,接口定义如下图所示:
ALIENTEK提供2.8/3.5/4.3/7 寸等不同尺寸的TFTLCD模块,其驱动芯片有很多种类型, 比如有:ILI9341/ILI9325/RM68042/RM68021/ILI9320/ILI9328/LGDP4531/LGDP4535/SPFD5408 /SSD1289/1505/B505/C505/NT35310/NT35510等(具体的型号,大家可以通过下载本章实验代码, 通过串口或者LCD显示查看),以ILI9341 控制器为例进行介绍。
ILI9341 液晶控制器自带显存,其显存总大小为172800(240*320*18/8),即18位模式(26 万色)下的显存量。在16位模式下,ILI9341采用RGB565格式存储颜色数据,此时ILI9341 的18位数据线与MCU的16位数据线以及LCD GRAM的对应关系如下图所示:
从图中可以看出,ILI9341 在 16 位模式下面,数据线有用的是:D17~D13 和 D11~D1,D0和 D12 没有用到,实际上在我们 LCD 模块里面,ILI9341 的 D0 和 D12 压根就没有引出来,这样,ILI9341 的 D17~D13 和 D11~D1 对应 MCU 的 D15~D0。
这样 MCU 的 16 位数据,最低 5 位代表蓝色,中间 6 位为绿色,最高 5 位为红色。数值越大,表示该颜色越深。另外,特别注意ILI9341所有的指令都是 8 位的(高 8 位无效),且参数除了读写 GRAM 的时候是 16 位,其他操作参数,都是 8 位的,这个和 ILI9320 等驱动器不一样,必须加以注意。
TFTLCD显示需要的相关设置步骤如下:
1)设置STM32F1与TFTLCD模块相连接的IO。 这一步,先将我们与TFTLCD模块相连的IO口进行初始化,以便驱动LCD。这里用到的是FSMC。
2)初始化TFTLCD模块。 即图18.1.1.5 的初始化序列,这里我们没有硬复位LCD,因为精英STM32F103的LCD接 口,将TFTLCD的RST同STM32F1的RESET连接在一起了,只要按下开发板的RESET键, 就会对LCD进行硬复位。初始化序列,就是向LCD控制器写入一系列的设置值(比如伽马校 准),这些初始化序列一般LCD供应商会提供给客户,我们直接使用这些序列即可,不需要深 入研究。在初始化之后,LCD才可以正常使用。
3)通过函数将字符和数字显示到TFTLCD模块上。 这一步则通过图18.1.1.5左侧的流程,即:设置坐标→写GRAM指令→写GRAM来实现, 但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而 达到显示字符/数字的目的,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数, 就可以实现数字/字符的显示了。
二.FSMC 简介
FSMC,即灵活的静态存储控制器,能够与同步或异步存储器和16位PC存储器卡连接, STM32 的FSMC接口支持包括SRAM、NAND FLASH、NOR FLASH和PSRAM等存储器。 FSMC的框图如下图所示:
从上图我们可以看出,STM32的FSMC将外部设备分为3类:NOR/PSRAM设备、NAND 设备、PC卡设备。他们共用地址数据总线等信号,他们具有不同的CS以区分不同的设备,比 如本章我们用到的TFTLCD就是用的FSMC_NE4做片选,其实就是将TFTLCD当成SRAM来控制。
为什么可以把TFTLCD当成SRAM设备用:首先我们了解下外部SRAM 的连接,外部SRAM的控制一般有:地址线(如A0~A18)、数据线(如D0~D15)、写信号(WE)、 读信号(OE)、片选信号(CS),如果SRAM支持字节控制,那么还有UB/LB信号。而 TFTLCD 的信号包括:RS、D0~D15、WR、RD、CS、RST和BL等,其中真 正在操作LCD的时候需要用到的就只有:RS、D0~D15、WR、RD和CS。其操作时序和SRAM 的控制完全类似,唯一不同就是TFTLCD有RS信号,但是没有地址信号。
FSMC的地址映射:
在使用FSMC时,配置模式后,使用指针就可以访问到外设存储器的内容,不用我们去控制产生时序,FSMC自动完成,很是方便。使用FSMC外接存储器时,其存储单元是映射到STM32的内部寻址空间的;在程序里,定义一个指向这些地址的指针,然后就可以通过指针直接修改该存储单元的内容,FSMC外设会自动完成数据访问过程,读写命令之类的操作不需要程序控制(配置好工作模式的前提下)。
FSMC_NE[X]片选:
NE[X]片选就是选择存储块中的哪一个区。存储块1分为 4个区,每个区管理 64M 字节空间,每个区都有 独立的寄存器 对所连接的存储器进行配置。如下图的Bank1:
三.TFTLCD软件设计
TFTLCD的RS接在FSMC 的A10上面,CS接在FSMC_NE4上,并且是16位数据总线。即我们使用的是FSMC存储器 1 的第4区,我们定义如下LCD操作结构体(在lcd.h里面定义):
其中LCD_BASE,必须根据我们外部电路的连接来确定,我们使用 Bank1.sector4 就是从 地址 0X6C000000 开始,而 0X000007FE,则是 A10 的偏移量。我们将这个地址强制转换为LCD_TypeDef结构体地址,那么可以得到LCD->LCD_REG的地址就是0X6C00,07FE,对应 A10的状态为0(即RS=0),而LCD-> LCD_RAM的地址就是0X6C00,0800(结构体地址自增), 对应A10的状态为1(即RS=1)。
有了这个定义,当我们要往LCD写命令/数据的时候,可以这样写:LCD->LCD_REG=CMD; //写命令
LCD->LCD_RAM=DATA; //写数据读的时候反过来操作就可以了,如下所示:CMD= LCD->LCD_REG; //读LCD寄存器
DATA = LCD->LCD_RAM; //读LCD数据
其中,CS、WR、RD和IO口方向都是由FSMC控制,不需要我们手动设置了。接下来, 我们先介绍一下lcd.h里面的另一个重要结构体:
//LCD重要参数集
typedef struct
{ u16 width; //LCD 宽度 u16 height; //LCD 高度 u16 id; //LCD ID u8 dir; //横屏还是竖屏控制:0,竖屏;1,横屏。 u16 wramcmd; //开始写gram指令 u16 setxcmd; //设置x坐标指令 u16 setycmd; //设置y坐标指令
}_lcd_dev;
//LCD参数
extern _lcd_dev lcddev; //管理LCD重要参数
该结构体用于保存一些LCD重要参数信息,比如LCD的长宽、LCD ID(驱动IC型号)、 LCD横竖屏状态等,这个结构体虽然占用了10个字节的内存,但是却可以让我们的驱动函数支持不同尺寸的LCD,同时可以实现LCD横竖屏切换等重要功能,所以还是利大于弊的。有了以上了解,下面我们开始介绍lcd.c里面的一些重要函数。
先看7个简单,但是很重要的函数:
//写寄存器函数
//regval:寄存器值
void LCD_WR_REG(u16 regval)
{ LCD->LCD_REG=regval; //写入要写的寄存器序号
}
//写LCD数据
//data:要写入的值
void LCD_WR_DATA(u16 data)
{ LCD->LCD_RAM=data;
}
//读LCD数据
//返回值:读到的值u16 LCD_RD_DATA(void)
{ vu16 ram; //防止被优化 ram=LCD->LCD_RAM; return ram;
}
//写寄存器
//LCD_Reg:寄存器地址
//LCD_RegValue:要写入的数据
void LCD_WriteReg(u16 LCD_Reg, u16 LCD_RegValue)
{ LCD->LCD_REG = LCD_Reg; //写入要写的寄存器序号 LCD->LCD_RAM = LCD_RegValue; //写入数据
}
//读寄存器
//LCD_Reg:寄存器地址
//返回值:读到的数据
u16 LCD_ReadReg(u16 LCD_Reg)
{ LCD_WR_REG(LCD_Reg); //写入要读的寄存器序号 delay_us(5); return LCD_RD_DATA(); //返回读到的值
}
//开始写GRAM
void LCD_WriteRAM_Prepare(void)
{ LCD->LCD_REG=lcddev.wramcmd;
}
//LCD写GRAM
//RGB_Code:颜色值
void LCD_WriteRAM(u16 RGB_Code)
{ LCD->LCD_RAM = RGB_Code;//写十六位GRAM
}
因为FSMC自动控制了WR/RD/CS等这些信号,所以这7个函数实现起来都非常简单。
函数LCD_SetCursor实现了将LCD的当前操作点设置到指定坐标(x,y),有了该函数, 我们就可以在液晶上任意作图了。源代码如下所示:
//设置光标位置
//Xpos:横坐标
//Ypos:纵坐标
void LCD_SetCursor(u16 Xpos, u16 Ypos)
{if (lcddev.id == 0X1963){if (lcddev.dir == 0) //x坐标需要变换{Xpos = lcddev.width - 1 - Xpos;LCD_WR_REG(lcddev.setxcmd);LCD_WR_DATA(0);LCD_WR_DATA(0);LCD_WR_DATA(Xpos >> 8);LCD_WR_DATA(Xpos & 0XFF);}else{LCD_WR_REG(lcddev.setxcmd);LCD_WR_DATA(Xpos >> 8);LCD_WR_DATA(Xpos & 0XFF);LCD_WR_DATA((lcddev.width - 1) >> 8);LCD_WR_DATA((lcddev.width - 1) & 0XFF);}LCD_WR_REG(lcddev.setycmd);LCD_WR_DATA(Ypos >> 8);LCD_WR_DATA(Ypos & 0XFF);LCD_WR_DATA((lcddev.height - 1) >> 8);LCD_WR_DATA((lcddev.height - 1) & 0XFF);}else if (lcddev.id == 0X5510){LCD_WR_REG(lcddev.setxcmd);LCD_WR_DATA(Xpos >> 8);LCD_WR_REG(lcddev.setxcmd + 1);LCD_WR_DATA(Xpos & 0XFF);LCD_WR_REG(lcddev.setycmd);LCD_WR_DATA(Ypos >> 8);LCD_WR_REG(lcddev.setycmd + 1);LCD_WR_DATA(Ypos & 0XFF);}else //9341/5310/7789等设置坐标{LCD_WR_REG(lcddev.setxcmd);LCD_WR_DATA(Xpos >> 8);LCD_WR_DATA(Xpos & 0XFF);LCD_WR_REG(lcddev.setycmd);LCD_WR_DATA(Ypos >> 8);LCD_WR_DATA(Ypos & 0XFF);}
}
画点函数LCD_DrawPoint实现比较简单,就是先设置坐标,然后往坐标写颜色。LCD_DrawPoint函数虽然简单,但是至关重要,其他几乎所有上 层函数,都是通过调用这个函数实现的。
读取TFTLCD模块数据的函数为 LCD_ReadPoint,该函数直接返回读到的GRAM值。该函数使用之前要先设置读取的GRAM 地址,通过LCD_SetCursor函数来实现。LCD_ReadPoint的代码如下:
//读取个某点的颜色值
//x,y:坐标
//返回值:此点的颜色
u16 LCD_ReadPoint(u16 x, u16 y)
{u16 r, g, b;if (x >= lcddev.width || y >= lcddev.height)return 0; //超过了范围,直接返回LCD_SetCursor(x, y);if (lcddev.id == 0X5510) //5510 发送读GRAM指令{LCD_WR_REG(0X2E00);}else //其他IC(9341/5310/1963/7789)发送读GRAM指令{LCD_WR_REG(0X2E);}r = LCD_RD_DATA(); //假读if (lcddev.id == 0X1963) //对1963来说,是真读{return r; //1963直接读就可以}r = LCD_RD_DATA(); //实际坐标颜色//9341/5310/5510/7789 要分2次读出b = LCD_RD_DATA();g = r & 0XFF; //对于 9341/5310/5510/7789, 第一次读取的是RG的值,R在前,G在后,各占8位g <<= 8;return (((r >> 11) << 11) | ((g >> 10) << 5) | (b >> 11)); // 9341/5310/5510/7789 需要公式转换一下
}
字符显示函数LCD_ShowChar,该函数同OLED模块的字符显示函数差不多,但是这里的字符显示函数多了1个功能,就是可以以叠加方式显示,或者以非叠加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。该函数实现代码如下:
//在指定位置显示一个字符
//x,y:起始坐标
//num:要显示的字符:" "--->"~"
//size:字体大小 12/16/24
//mode:叠加方式(1)还是非叠加方式(0)
void LCD_ShowChar(u16 x, u16 y, u8 num, u8 size, u8 mode)
{u8 temp, t1, t;u16 y0 = y;u8 csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); //得到字体一个字符对应点阵集所占的字节数num = num - ' '; //得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库)for (t = 0; t < csize; t++){if (size == 12)temp = asc2_1206[num][t]; //调用1206字体else if (size == 16)temp = asc2_1608[num][t]; //调用1608字体else if (size == 24)temp = asc2_2412[num][t]; //调用2412字体else return; //没有的字库for (t1 = 0; t1 < 8; t1++){if (temp & 0x80)LCD_Fast_DrawPoint(x, y, POINT_COLOR);else if (mode == 0)LCD_Fast_DrawPoint(x, y, BACK_COLOR);temp <<= 1;y++;if (y >= lcddev.height)return; //超区域了if ((y - y0) == size){y = y0;x++;if (x >= lcddev.width)return; //超区域了break;}}}
}
在LCD_ShowChar函数里面,我们采用快速画点函数LCD_Fast_DrawPoint来画点显示字 符,该函数同LCD_DrawPoint一样,只是带了颜色参数,且减少了函数调用的时间。
TFTLCD模块的初始化函数LCD_Init,该函数先初始化STM32与 TFTLCD连接的IO口,并配置FSMC控制器,然后读取LCD控制器的型号,根据控制IC的 型号执行不同的初始化代码,其简化代码如下:
//初始化lcd
//该初始化函数可以初始化各种ILI93XX液晶,但是其他函数是基于ILI9320的!!!
//在其他型号的驱动芯片上没有测试!
void LCD_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure;FSMC_NORSRAMTimingInitTypeDef readWriteTiming; FSMC_NORSRAMTimingInitTypeDef writeTiming;RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC,ENABLE); //使能FSMC时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOD|RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOG,ENABLE); //使能PORTB,D,E,G以及AFIO复用功能时钟GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PB0 推挽输出 背光GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);//PORTD复用推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_14|GPIO_Pin_15; //PORTD复用推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOD, &GPIO_InitStructure); //PORTE复用推挽输出GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10|GPIO_Pin_11|GPIO_Pin_12|GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15; //PORTE复用推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOE, &GPIO_InitStructure);//PORTG12复用推挽输出 P0RTG0-->RSGPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_12; //PORTD复用推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOG, &GPIO_InitStructure); readWriteTiming.FSMC_AddressSetupTime = 0x01; //地址建立时间(ADDSET)为2个HCLK 1/36M=27nsreadWriteTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间(ADDHLD)模式A未用到readWriteTiming.FSMC_DataSetupTime = 0x0f; //数据保存时间为16个HCLK,因为液晶驱动IC的读数据的时候,速度不能太快,尤其对1289这个IC。readWriteTiming.FSMC_BusTurnAroundDuration = 0x00;readWriteTiming.FSMC_CLKDivision = 0x00;readWriteTiming.FSMC_DataLatency = 0x00;readWriteTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式A writeTiming.FSMC_AddressSetupTime = 0x00; //地址建立时间(ADDSET)为1个HCLKwriteTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间(ADDHLD)模式A未用到writeTiming.FSMC_DataSetupTime = 0x03; //数据保存时间为4个HCLKwriteTiming.FSMC_BusTurnAroundDuration = 0x00;writeTiming.FSMC_CLKDivision = 0x00;writeTiming.FSMC_DataLatency = 0x00;writeTiming.FSMC_AccessMode = FSMC_AccessMode_A; //模式A FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM4; //这里我们使用NE4,也就对应BTCR[6],[7]。FSMC_NORSRAMInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; //不复用数据地址FSMC_NORSRAMInitStructure.FSMC_MemoryType =FSMC_MemoryType_SRAM; //FSMC_MemoryType_SRAM; SRAMFSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b; //存储器数据宽度为16bitFSMC_NORSRAMInitStructure.FSMC_BurstAccessMode =FSMC_BurstAccessMode_Disable; //FSMC_BurstAccessMode_Disable;FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low;FSMC_NORSRAMInitStructure.FSMC_AsynchronousWait=FSMC_AsynchronousWait_Disable; FSMC_NORSRAMInitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState; FSMC_NORSRAMInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable; //存储器写使能FSMC_NORSRAMInitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable; FSMC_NORSRAMInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Enable; //读写使用不同的时序FSMC_NORSRAMInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable; FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct = &readWriteTiming; //读写时序FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct = &writeTiming; //写时序FSMC_NORSRAMInit(&FSMC_NORSRAMInitStructure); //初始化FSMC配置FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE); //使能BANK1 delay_ms(50); // delay 50 ms//尝试9341 ID的读取LCD_WR_REG(0XD3);lcddev.id = LCD_RD_DATA(); //dummy readlcddev.id = LCD_RD_DATA(); //读到0X00lcddev.id = LCD_RD_DATA(); //读取0X93lcddev.id <<= 8;lcddev.id |= LCD_RD_DATA(); //读取0X41if (lcddev.id != 0X9341) //不是 9341 , 尝试看看是不是 ST7789{LCD_WR_REG(0X04);lcddev.id = LCD_RD_DATA(); //dummy readlcddev.id = LCD_RD_DATA(); //读到0X85lcddev.id = LCD_RD_DATA(); //读取0X85lcddev.id <<= 8;lcddev.id |= LCD_RD_DATA(); //读取0X52if (lcddev.id == 0X8552) //将8552的ID转换成7789{lcddev.id = 0x7789;}if (lcddev.id != 0x7789) //也不是ST7789, 尝试是不是 NT35310{LCD_WR_REG(0XD4);lcddev.id = LCD_RD_DATA(); //dummy readlcddev.id = LCD_RD_DATA(); //读回0X01lcddev.id = LCD_RD_DATA(); //读回0X53lcddev.id <<= 8;lcddev.id |= LCD_RD_DATA(); //这里读回0X10if (lcddev.id != 0X5310) //也不是NT35310,尝试看看是不是NT35510{//发送秘钥(厂家提供,照搬即可)LCD_WriteReg(0xF000, 0x0055);LCD_WriteReg(0xF001, 0x00AA);LCD_WriteReg(0xF002, 0x0052);LCD_WriteReg(0xF003, 0x0008);LCD_WriteReg(0xF004, 0x0001);LCD_WR_REG(0xC500); //读取ID高8位lcddev.id = LCD_RD_DATA(); //读回0X55lcddev.id <<= 8;LCD_WR_REG(0xC501); //读取ID低8位lcddev.id |= LCD_RD_DATA(); //读回0X10delay_ms(5);if (lcddev.id != 0X5510) //也不是NT5510,尝试看看是不是SSD1963{LCD_WR_REG(0XA1);lcddev.id = LCD_RD_DATA();lcddev.id = LCD_RD_DATA(); //读回0X57lcddev.id <<= 8;lcddev.id |= LCD_RD_DATA(); //读回0X61if (lcddev.id == 0X5761)lcddev.id = 0X1963; //SSD1963读回的ID是5761H,为方便区分,我们强制设置为1963}}}}printf(" LCD ID:%x\r\n", lcddev.id); //打印LCD IDif (lcddev.id == 0X9341) //9341初始化{//.......9341初始化代码}else if (lcddev.id == 0x7789) //7789初始化{//.......7789初始化代码}else if (lcddev.id == 0x5310){//.......5310初始化代码}else if (lcddev.id == 0x5510){//.......5510初始化代码}else if (lcddev.id == 0X1963){//.......1963初始化代码}LCD_Display_Dir(0); //默认为竖屏LCD_LED = 1; //点亮背光LCD_Clear(WHITE);
}
从初始化代码可以看出,LCD初始化步骤为①~⑤在代码中标注:
①GPIO,FSMC,AFIO时钟使能。
②GPIO初始化:GPIO_Init()函数
③FSMC初始化:FSMC_NORSRAMInit()函数。
④ FSMC使能:FSMC_NORSRAMCmd()函数。
⑤ 不同的LCD驱动器的初始化代码。
四.触摸屏简介
1.电阻式触摸屏
ALIENTEK 2.4/2.8/3.5 寸 TFTLCD 模块自带的触摸屏都属于电阻式触摸屏,下面简单介绍 下电阻式触摸屏的原理。
当手指触摸屏幕时,两层导电层在触摸点位置就有了接触,电阻发生变化,在X和Y两个方向上产生 信号,然后送触摸屏控制器。控制器侦测到这一接触并计算出(X,Y)的位置,再根据获得的 位置模拟鼠标的方式运作。这就是电阻技术触摸屏的最基本的原理。
电阻触摸屏的优点:精度高、价格便宜、抗干扰能力强、稳定性好。
电阻触摸屏的缺点:容易被划伤、透光性不太好、不支持多点触摸。
ALIENTEK TFTLCD 模块自带的触摸屏控制芯片为XPT2046。XPT2046是一款4导线制触 摸屏控制器,内含12位分辨率125KHz转换速率逐步逼近型A/D转换器。XPT2046支持从1.5V 到5.25V 的低电压I/O 接口。XPT2046 能通过执行两次A/D转换查出被按的屏幕位置, 除此 之外,还可以测量加在触摸屏上的压力。内部自带2.5V参考电压可以作为辅助输入、温度测量 和电池监测模式之用,电池监测的电压范围可以从0V到6V。XPT2046片内集成有一个温度传 感器。 在2.7V的典型工作状态下,关闭参考电压,功耗可小于0.75mW。XPT2046采用微小 的封装形式:TSSOP-16,QFN-16(0.75mm厚度)和VFBGA-48。工作温度范围为-40℃~+85℃。
2.电容式触摸屏
ALIENTEK 4.3/7 寸 TFTLCD模块自带的触摸屏采用的是电容式触摸屏,电容式触摸屏主要分为两种:
表面电容式电容触摸屏:
表面电容式触摸屏技术是利用 ITO(铟锡氧化物,是一种透明的导电材料)导电膜,通过电 场感应方式感测屏幕表面的触摸行为进行。但是表面电容式触摸屏有一些局限性,它只能识别一个手指或者一次触摸。
投射式电容触摸屏:
投射电容式触摸屏是传感器利用触摸屏电极发射出静电场线。一般用于投射电容传感技术的电容类型有两种:自我电容和交互电容。
自我电容又称绝对电容,是最广为采用的一种方法,自我电容通常是指扫描电极与地构成的电容。在玻璃表面有用ITO制成的横向与纵向的扫描电极,这些电极和地之间就构成一个电容的两极。当用手或触摸笔触摸的时候就会并联一个电容到电路中去,从而使在该条扫描线上的总体的电容量有所改变。在扫描的时候,控制IC依次扫描纵向和横向电极,并根据扫描前后的电容变化来确定触摸点坐标位置。
五.触摸屏软件设计
TFTLCD模块的触摸屏(电阻触摸屏)总共有5跟线与STM32F1连接,连接电路图如下图所示:
TP_Read_XY2函数:
//连续2次读取触摸屏IC,且这两次的偏差不能超过
//ERR_RANGE,满足条件,则认为读数正确,否则读数错误.
//该函数能大大提高准确度
//x,y:读取到的坐标值
//返回值:0,失败;1,成功。
#define ERR_RANGE 50 //误差范围
u8 TP_Read_XY2(u16 *x,u16 *y)
{ u16 x1,y1; u16 x2,y2; u8 flag; flag=TP_Read_XY(&x1,&y1); if(flag==0)return(0); flag=TP_Read_XY(&x2,&y2); if(flag==0)return(0);
//前后两次采样在+- ERR_RANGE 内 if(((x2<=x1&&x1<x2+ERR_RANGE)||(x1<=x2&&x2<x1+ERR_RANGE)) &&((y2<=y1&&y1<y2+ERR_RANGE)||(y1<=y2&&y2<y1+ERR_RANGE))) { *x=(x1+x2)/2; *y=(y1+y2)/2; return 1; }else return 0;
}
该函数采用了一个非常好的办法来读取屏幕坐标值,就是连续读两次,两次读取的值之差不能超过一个特定的值(ERR_RANGE),通过这种方式,我们可以大大提高触摸屏的准确度。另 外该函数调用的TP_Read_XY函数,用于单次读取坐标值。TP_Read_XY也采用了一些软件滤波 算法。
TP_Adjust函数:
//触摸屏校准代码
//得到四个校准参数
void TP_Adjust(void)
{ u16 pos_temp[4][2];//坐标缓存值u8 cnt=0; u16 d1,d2;u32 tem1,tem2;double fac; u16 outtime=0;cnt=0; POINT_COLOR=BLUE;BACK_COLOR =WHITE;LCD_Clear(WHITE);//清屏 POINT_COLOR=RED;//红色 LCD_Clear(WHITE);//清屏 POINT_COLOR=BLACK;LCD_ShowString(40,40,160,100,16,(u8*)TP_REMIND_MSG_TBL);//显示提示信息TP_Drow_Touch_Point(20,20,RED);//画点1 tp_dev.sta=0;//消除触发信号 tp_dev.xfac=0;//xfac用来标记是否校准过,所以校准之前必须清掉!以免错误 while(1)//如果连续10秒钟没有按下,则自动退出{tp_dev.scan(1);//扫描物理坐标if((tp_dev.sta&0xc0)==TP_CATH_PRES)//按键按下了一次(此时按键松开了.){ outtime=0; tp_dev.sta&=~(1<<6);//标记按键已经被处理过了.pos_temp[cnt][0]=tp_dev.x[0];pos_temp[cnt][1]=tp_dev.y[0];cnt++; switch(cnt){ case 1: TP_Drow_Touch_Point(20,20,WHITE); //清除点1 TP_Drow_Touch_Point(lcddev.width-20,20,RED); //画点2break;case 2:TP_Drow_Touch_Point(lcddev.width-20,20,WHITE); //清除点2TP_Drow_Touch_Point(20,lcddev.height-20,RED); //画点3break;case 3:TP_Drow_Touch_Point(20,lcddev.height-20,WHITE); //清除点3TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20,RED); //画点4break;case 4: //全部四个点已经得到//对边相等tem1=abs(pos_temp[0][0]-pos_temp[1][0]);//x1-x2tem2=abs(pos_temp[0][1]-pos_temp[1][1]);//y1-y2tem1*=tem1;tem2*=tem2;d1=sqrt(tem1+tem2);//得到1,2的距离tem1=abs(pos_temp[2][0]-pos_temp[3][0]);//x3-x4tem2=abs(pos_temp[2][1]-pos_temp[3][1]);//y3-y4tem1*=tem1;tem2*=tem2;d2=sqrt(tem1+tem2);//得到3,4的距离fac=(float)d1/d2;if(fac<0.95||fac>1.05||d1==0||d2==0)//不合格{cnt=0;TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20,WHITE); //清除点4TP_Drow_Touch_Point(20,20,RED); //画点1TP_Adj_Info_Show(pos_temp[0][0],pos_temp[0][1],pos_temp[1][0],pos_temp[1][1],pos_temp[2][0],pos_temp[2][1],pos_temp[3][0],pos_temp[3][1],fac*100);//显示数据 continue;}tem1=abs(pos_temp[0][0]-pos_temp[2][0]);//x1-x3tem2=abs(pos_temp[0][1]-pos_temp[2][1]);//y1-y3tem1*=tem1;tem2*=tem2;d1=sqrt(tem1+tem2);//得到1,3的距离tem1=abs(pos_temp[1][0]-pos_temp[3][0]);//x2-x4tem2=abs(pos_temp[1][1]-pos_temp[3][1]);//y2-y4tem1*=tem1;tem2*=tem2;d2=sqrt(tem1+tem2);//得到2,4的距离fac=(float)d1/d2;if(fac<0.95||fac>1.05)//不合格{cnt=0;TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20,WHITE); //清除点4TP_Drow_Touch_Point(20,20,RED); //画点1TP_Adj_Info_Show(pos_temp[0][0],pos_temp[0][1],pos_temp[1][0],pos_temp[1][1],pos_temp[2][0],pos_temp[2][1],pos_temp[3][0],pos_temp[3][1],fac*100);//显示数据 continue;}//正确了//对角线相等tem1=abs(pos_temp[1][0]-pos_temp[2][0]);//x1-x3tem2=abs(pos_temp[1][1]-pos_temp[2][1]);//y1-y3tem1*=tem1;tem2*=tem2;d1=sqrt(tem1+tem2);//得到1,4的距离tem1=abs(pos_temp[0][0]-pos_temp[3][0]);//x2-x4tem2=abs(pos_temp[0][1]-pos_temp[3][1]);//y2-y4tem1*=tem1;tem2*=tem2;d2=sqrt(tem1+tem2);//得到2,3的距离fac=(float)d1/d2;if(fac<0.95||fac>1.05)//不合格{cnt=0;TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20,WHITE); //清除点4TP_Drow_Touch_Point(20,20,RED); //画点1TP_Adj_Info_Show(pos_temp[0][0],pos_temp[0][1],pos_temp[1][0],pos_temp[1][1],pos_temp[2][0],pos_temp[2][1],pos_temp[3][0],pos_temp[3][1],fac*100);//显示数据 continue;}//正确了//计算结果tp_dev.xfac=(float)(lcddev.width-40)/(pos_temp[1][0]-pos_temp[0][0]);//得到xfac tp_dev.xoff=(lcddev.width-tp_dev.xfac*(pos_temp[1][0]+pos_temp[0][0]))/2;//得到xofftp_dev.yfac=(float)(lcddev.height-40)/(pos_temp[2][1]-pos_temp[0][1]);//得到yfactp_dev.yoff=(lcddev.height-tp_dev.yfac*(pos_temp[2][1]+pos_temp[0][1]))/2;//得到yoff if(abs(tp_dev.xfac)>2||abs(tp_dev.yfac)>2)//触屏和预设的相反了.{cnt=0;TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20,WHITE); //清除点4TP_Drow_Touch_Point(20,20,RED); //画点1LCD_ShowString(40,26,lcddev.width,lcddev.height,16,"TP Need readjust!");tp_dev.touchtype=!tp_dev.touchtype;//修改触屏类型.if(tp_dev.touchtype)//X,Y方向与屏幕相反{CMD_RDX=0X90;CMD_RDY=0XD0; }else //X,Y方向与屏幕相同{CMD_RDX=0XD0;CMD_RDY=0X90; } continue;} POINT_COLOR=BLUE;LCD_Clear(WHITE);//清屏LCD_ShowString(35,110,lcddev.width,lcddev.height,16,"Touch Screen Adjust OK!");//校正完成delay_ms(1000);TP_Save_Adjdata(); LCD_Clear(WHITE);//清屏 return;//校正完成 }}delay_ms(10);outtime++;if(outtime>1000){TP_Get_Adjdata();break;} }
}
校正思路:可以得出下面的一个从物理坐标到像素坐标的转 换关系式: LCDx=xfac*Px+xoff; LCDy=yfac*Py+yoff;
其中(LCDx,LCDy)是在LCD上的像素坐标,(Px,Py)是从触摸屏读到的物理坐标。xfac, yfac分别是X轴方向和Y轴方向的比例因子,而xoff和yoff则是这两个方向的偏移量。 这样我们只要事先在屏幕上面显示4个点(这四个点的坐标是已知的),分别按这四个点就 可以从触摸屏读到4个物理坐标,这样就可以通过待定系数法求出xfac、yfac、xoff、yoff这四 个参数。我们保存好这四个参数,在以后的使用中,我们把所有得到的物理坐标都按照这个关 系式来计算,得到的就是准确的屏幕坐标。达到了触摸屏校准的目的。
TP_Init函数:
//触摸屏初始化
//返回值:0,没有进行校准
// 1,进行过校准
u8 TP_Init(void)
{ 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;}else if(lcddev.id==0X1963) //7寸电容触摸屏{FT5206_Init();tp_dev.scan=FT5206_Scan; //扫描函数指向GT9147触摸屏扫描 tp_dev.touchtype|=0X80; //电容屏 tp_dev.touchtype|=lcddev.dir&0X01;//横屏还是竖屏 return 0;}else{GPIO_InitTypeDef GPIO_InitStructure;//注意,时钟使能之后,对GPIO的操作才有效//所以上拉之前,必须使能时钟.才能实现真正的上拉输出RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOF, ENABLE); //使能PB,PF端口时钟GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; // PB1端口配置GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);//B1推挽输出GPIO_SetBits(GPIOB,GPIO_Pin_1);//上拉GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; // PB2端口配置GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入GPIO_Init(GPIOB, &GPIO_InitStructure);//B2上拉输入GPIO_SetBits(GPIOB,GPIO_Pin_2);//上拉 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11|GPIO_Pin_9; // F9,PF11端口配置GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOF, &GPIO_InitStructure);//PF9,PF11推挽输出GPIO_SetBits(GPIOF, GPIO_Pin_11|GPIO_Pin_9);//上拉GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // PF10端口配置GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入GPIO_Init(GPIOF, &GPIO_InitStructure);//PF10上拉输入GPIO_SetBits(GPIOF,GPIO_Pin_10);//上拉 TP_Read_XY(&tp_dev.x[0],&tp_dev.y[0]);//第一次读取初始化 AT24CXX_Init(); //初始化24CXXif(TP_Get_Adjdata())return 0;//已经校准else //未校准?{ LCD_Clear(WHITE); //清屏TP_Adjust(); //屏幕校准 } TP_Get_Adjdata(); }return 1;
}
函数根据LCD的ID(即lcddev.id)判别是电 阻屏还是电容屏,执行不同的初始化。tp_dev.scan,这个结构体函数指针,默认是指向TP_Scan 的,如果是电阻屏则用默认的即可,如果是电容屏,则指向新的扫描函数GT9147_Scan、 OTT2001A_Scan或FT5206_Scan(根据芯片ID判断到底指向那个),执行电容触摸屏的扫描函数。
结构体_m_tp_dev:
结构体_m_tp_dev,用于管理和记录触摸屏相关信息,在外部调用的时候,我们一般直接调用tp_dev 的相关成员函数/变量屏即可达到需要的效果。这样种设计简化了接口,另外管理和维护也比较 方便,定义如下:
//触摸屏控制器
typedef struct
{u8 (*init)(void); //初始化触摸屏控制器u8 (*scan)(u8); //扫描触摸屏.0,屏幕扫描;1,物理坐标; void (*adjust)(void); //触摸屏校准 u16 x[CT_MAX_TOUCH]; //当前坐标u16 y[CT_MAX_TOUCH]; //电容屏有最多5组坐标,电阻屏则用x[0],y[0]代表:此次扫描时,触屏的坐标,用//x[4],y[4]存储第一次按下时的坐标. 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;