一.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-->RS
GPIO_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=27ns
readWriteTiming.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个HCLK
writeTiming.FSMC_AddressHoldTime = 0x00; //地址保持时间(ADDHLD)模式A未用到
writeTiming.FSMC_DataSetupTime = 0x03; //数据保存时间为4个HCLK
writeTiming.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; SRAM
FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b; //存储器数据宽度为16bit
FSMC_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 read
lcddev.id = LCD_RD_DATA(); //读到0X00
lcddev.id = LCD_RD_DATA(); //读取0X93
lcddev.id <<= 8;
lcddev.id |= LCD_RD_DATA(); //读取0X41
if (lcddev.id != 0X9341) //不是 9341 , 尝试看看是不是 ST7789
{
LCD_WR_REG(0X04);
lcddev.id = LCD_RD_DATA(); //dummy read
lcddev.id = LCD_RD_DATA(); //读到0X85
lcddev.id = LCD_RD_DATA(); //读取0X85
lcddev.id <<= 8;
lcddev.id |= LCD_RD_DATA(); //读取0X52
if (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 read
lcddev.id = LCD_RD_DATA(); //读回0X01
lcddev.id = LCD_RD_DATA(); //读回0X53
lcddev.id <<= 8;
lcddev.id |= LCD_RD_DATA(); //这里读回0X10
if (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(); //读回0X55
lcddev.id <<= 8;
LCD_WR_REG(0xC501); //读取ID低8位
lcddev.id |= LCD_RD_DATA(); //读回0X10
delay_ms(5);
if (lcddev.id != 0X5510) //也不是NT5510,尝试看看是不是SSD1963
{
LCD_WR_REG(0XA1);
lcddev.id = LCD_RD_DATA();
lcddev.id = LCD_RD_DATA(); //读回0X57
lcddev.id <<= 8;
lcddev.id |= LCD_RD_DATA(); //读回0X61
if (lcddev.id == 0X5761)lcddev.id = 0X1963; //SSD1963读回的ID是5761H,为方便区分,我们强制设置为1963
}
}
}
}
printf(" LCD ID:%x\r\n", lcddev.id); //打印LCD ID
if (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); //画点2
break;
case 2:
TP_Drow_Touch_Point(lcddev.width-20,20,WHITE); //清除点2
TP_Drow_Touch_Point(20,lcddev.height-20,RED); //画点3
break;
case 3:
TP_Drow_Touch_Point(20,lcddev.height-20,WHITE); //清除点3
TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20,RED); //画点4
break;
case 4: //全部四个点已经得到
//对边相等
tem1=abs(pos_temp[0][0]-pos_temp[1][0]);//x1-x2
tem2=abs(pos_temp[0][1]-pos_temp[1][1]);//y1-y2
tem1*=tem1;
tem2*=tem2;
d1=sqrt(tem1+tem2);//得到1,2的距离
tem1=abs(pos_temp[2][0]-pos_temp[3][0]);//x3-x4
tem2=abs(pos_temp[2][1]-pos_temp[3][1]);//y3-y4
tem1*=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); //清除点4
TP_Drow_Touch_Point(20,20,RED); //画点1
TP_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-x3
tem2=abs(pos_temp[0][1]-pos_temp[2][1]);//y1-y3
tem1*=tem1;
tem2*=tem2;
d1=sqrt(tem1+tem2);//得到1,3的距离
tem1=abs(pos_temp[1][0]-pos_temp[3][0]);//x2-x4
tem2=abs(pos_temp[1][1]-pos_temp[3][1]);//y2-y4
tem1*=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); //清除点4
TP_Drow_Touch_Point(20,20,RED); //画点1
TP_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-x3
tem2=abs(pos_temp[1][1]-pos_temp[2][1]);//y1-y3
tem1*=tem1;
tem2*=tem2;
d1=sqrt(tem1+tem2);//得到1,4的距离
tem1=abs(pos_temp[0][0]-pos_temp[3][0]);//x2-x4
tem2=abs(pos_temp[0][1]-pos_temp[3][1]);//y2-y4
tem1*=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); //清除点4
TP_Drow_Touch_Point(20,20,RED); //画点1
TP_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;//得到xoff
tp_dev.yfac=(float)(lcddev.height-40)/(pos_temp[2][1]-pos_temp[0][1]);//得到yfac
tp_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); //清除点4
TP_Drow_Touch_Point(20,20,RED); //画点1
LCD_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(); //初始化24CXX
if(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;