文章目录
- 前言
- 一、模块介绍
-
- 1. 简介
- 2. 引脚功能
- 3. 模块通信和指令集
- 二、工作流程
- 三、程序设计
-
- 1. 写数据
- 2. 读数据
- 3. 初始化
- 4. 打开/关闭显示器
- 5. 显示函数
- 6. 示例
-
- 显示展示:
前言
TFT-LCD 由于其厚度薄、画面质量优良,信息的高速、高亮度、高对比度显示已广泛应用于图像显示系统中。虽然TFT-LCD 显示性能好,但是TFT-LCD 驱动信号非常复杂,外围控制单元需要处理的数据量非常大,分辨率为640×480的彩色TFT-LCD 为例,显示一幅彩色图像就需要处理900kbyte对于传统的单片机等处理器来说,数据量是无能为力的。由于图像数据量大,大多数图像在压缩后存储,其中jpeg (joint photographicexperts group)压缩图像的格式应用最广泛。
一、模块介绍
1. 简介
TFT-LCD 即薄膜晶体管液晶显示器,其英文全称:Thin Film Transistor-Liquid CrystalDisplay。TFT-LCD 与无源 TN-LCD、STN-LCD 不同的简单矩阵,它在液晶显示屏的每个象素上都有一个薄膜晶体管(TFT),它能有效克服非选择时的串扰,使显示液晶屏的静态特性与扫描线数无关,从而大大提高了图像质量。TFT-LCD 也叫真彩液晶显示器。
该模块具有以下特点:
- 2.4’/2.8’/3.5’/4.3’/7’ 5 屏幕大小可选。
- 320×240 的分辨率(3.5分辨率为:320480,4.3’和 7分辨率为800480)。
- 16 位真彩显示。
- 自带触摸屏,可作为控制输入。
我们以 2.8 寸的 ALIENTEK TFTLCD 例如,模块支持 65K 颜色显示,显示分辨率为 320×240,接口为 16 位的 80 并口,自带触摸屏。 模块原理图如图所示 18.1.1.2 所示:
2. 引脚功能
TFTLCD 模块采用 2*17 的 2.54 接口定义如图所示: 从图中可以看出:ALIENTEK TFTLCD 模块采用 16 位置并联与外部连接。不使用的原因 8 位置的方式是因为彩屏的数据量比较大,尤其是显示图片的时候。 8 会比较位数据线 16 当然,我们希望速度越快越好,所以我们选择 16 位的接口。
该模块的 80 并口有以下信号线:
- CS:TFTLCD 片选信号。
- WR:向 TFTLCD 写入数据。
- RD:从 TFTLCD 读取数据。
- D[15:0]:16 位双向数据线。
- RST:硬复位 TFTLCD。
- RS:命令/数据标志(0、读写命令;1、读写数据)
需要注意的是,TFTLCD模块的RST直接接到信号线STM32复位脚不受软件控制,可以节省一个 IO口。此外,我们还需要一条背光控制线来控制它TFTLCD 的背光。因此,我们总共需要它 IO口数目为 21 个。这里还需要注意的是,我们标注的DB1DB8,DB10DB17,是相对于 LCD 控制 IC 标注的,实际上大家可以把他们就等同于 D0~D这样理解起来就比较简单了。
3. 模块通信和指令集
ILI9341 液晶控制器有自己的显存,其显存总尺寸为 172800(24032018/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 当驱动器不同时,必须注意。
接下来,让我们介绍一下 ILI9341 几个重要命令:0XD3,0X36,0X2A,0X2B,0X2C,0X2E 等 6 条指令。
-
0XD3:这个是读 ID4 读取指令 LCD 控制器的 ID,如图所示: 从上表可以看出,0XD3 跟着命令 4 最后,参数 2 读出一个参数 0X93 和 0X41正好是我们的控制器 ILI9341 因此,使用的数字部分可以通过该指令来判断 LCD 什么型号的驱动器?这样,我们的代码就可以根据控制器的型号执行相应的驱动器 IC 初始同驱动器兼容的初始代码 IC 一个代码支持多个屏幕 LCD。
-
0X36:这是存储访问控制指令,可以控制 ILI9341 简单地说,存储器的读写方向是连续写作 GRAM 可以控制时间 GRAM 指针的增长方向控制显示模式(阅读) GRAM 也一样)。指令如图所示: 从上表可以看出,0X36 在指令后面,跟随一个参数,这里我们主要关注:MY、MX、MV通过这三个位置的设置,我们可以控制整个位置 ILI9341 如图所示: 这样,我们就在使用它 ILI9341 显示内容时,有很大的灵活性,如显示 BMP 图片,BMP 解码数据是从图片的左下角慢慢显示到右上角。如果设置 LCD 扫描方向是从左到右,从下到上,所以我们只需要设置一个坐标,然后继续 LCD 填充颜色数据可以大大提高显示速度。
-
0X2A:这是列地址设置指令,用于从左到右、从上到下(默认)下设置横坐标(x 坐标),如图所示: 该指令用于设置默认扫描模式 x 该指令带有坐标 4 实际上是个参数 2 个坐标值:SC 和 EC,即列地址的起始值和结束值,SC必须小于等于 EC,且 0≤SC/EC≤239。一般在设置 x 坐标时,我们只需要带 2 可以设置一个参数,即设置 SC 即可,因为如果 EC 没有变化,我们只需要设置一次即可(在初始化 ILI9341 设置时),以提高速度。
-
0X2B:与 0X2A 该指令类似于页面地址设置指令。在从左到右、从上到下的扫描(默认)下,该指令用于设置纵坐标(y 坐标)。指令如图所示: 该指令用于设置默认扫描模式 y 该指令带有坐标 4 实际上是个参数 2 个坐标值:SP 和 EP,即页地址的起始值和结束值,SP 必须小于等于 EP,且 0≤SP/EP≤319。一般在设置y 坐标时,我们只需要带 2 可以设置一个参数,即设置 SP 因为如果 EP 没有变化,我们只需要设置一次(在初始化中 ILI9341 设置时),以提高速度。
-
0X2C:该指令是写 GRAM 我们可以通常发送指令 LCD的 GRAM 里面写入颜色数据了,该指令支持连续写,指令描述如图所示: 从上表可以看出,收到指令0X2C之后,数据的有效位宽变为16位,我们可以连续写入 LCD-GRAM 值,而 GRAM 的地址将根据 MY/MX/MV 设置的扫描方向自增。例如,假设从左到右,从上到下,然后设置起始坐标(通过 SC,SP 设置)后,每次写一个颜色值,GRAM 自动增加地址 1(SC ),如果碰到 EC,则回到 SC,同时 SP ,直到坐标:EC,EP 最后,不需要再设置坐标,从而大大提高了写入速度。
-
0X2E:该指令是读 GRAM 读取指令 ILI9341 的显存(GRAM),该指令在 ILI9341 数据手册上的描述是错误的,如图所示: 该指令用于读取 GRAM,如图所示,ILI9341 收到指令后,第一次输出dummy数据,即无效数据,第二次开始,读取是有效的 GRAM 数据(坐标:SC,SP 开始),输出规则为:每种颜色分量占 8 个位,一次输出 2 个色分量。例如:第一次输出是 R1G1.后续规则如下:B1R2→G2B2→R3G3→B3R4→GB4→R5G5… 以此类推。如果我们只需要读取一个点的颜色值,那么只需要接收到参数 3 即可,如果要连续读取(利用 GRAM地址自增,方法同上),那么就按照上述规律去接收颜色数据。
二、工作流程
一般 TFTLCD 模块的使用流程如图: 任何 LCD,使用流程都可以简单的用以上流程图表示。其中硬复位和初始化序列,只需要执行一次即可。而画点流程就是:设置坐标→写 GRAM 指令→写入颜色数据,然后在 LCD 上面,我们就可以看到对应的点显示我们写入的颜色了。
读点流程为:设置坐标→读 GRAM 指令→读取颜色数据,这样就可以获取到对应点的颜色数据了。
以上只是最简单的操作,也是最常用的操作,有了这些操作,一般就可以正常使用 TFTLCD了。接下来我们将该模块(2.8 寸屏模块)用来来显示字符和数字,通过以上介绍,我们可以得出 TFTLCD 显示需要的相关设置步骤如下:
- 设置 STM32F1 与 与 TFTLCD 模块相连接的 IO 。
这一步,先将我们与 TFTLCD 模块相连的 IO 口进行初始化,以便驱动 LCD。我们可以使用FSMC。
- 初始化 TFTLCD 模块。
即初始化序列,这里我们没有硬复位 LCD,因为精英 STM32F103 的 LCD 接口,将 TFTLCD 的 RST 同 STM32F1 的 RESET 连接在一起了,只要按下开发板的 RESET 键,就会对 LCD 进行硬复位。初始化序列,就是向 LCD 控制器写入一系列的设置值(比如伽马校准),这些初始化序列一般 LCD 供应商会提供给客户,我们直接使用这些序列即可,不需要深入研究。在初始化之后,LCD 才可以正常使用。
- 通过函数将字符和数字显示到 TFTLCD 模块上。
这一步则通过图左侧的流程,即:设置坐标→写 GRAM 指令→写 GRAM 来实现,但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而达到显示字符/数字的目的,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数,就可以实现数字/字符的显示了。
三、程序设计
1. 写数据
//写寄存器函数
//regval:寄存器值
void LCD_WR_REG(u16 regval)
{
LCD->LCD_REG=regval;//写入要写的寄存器序号
}
//写LCD数据
//data:要写入的值
void LCD_WR_DATA(u16 data)
{
LCD->LCD_RAM=data;
}
//写寄存器
//LCD_Reg:寄存器地址
//LCD_RegValue:要写入的数据
void LCD_WriteReg(u16 LCD_Reg,u16 LCD_RegValue)
{
LCD->LCD_REG = LCD_Reg; //写入要写的寄存器序号
LCD->LCD_RAM = LCD_RegValue;//写入数据
}
//开始写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
}
2. 读数据
//读LCD数据
//返回值:读到的值
u16 LCD_RD_DATA(void)
{
vu16 ram; //防止被优化
ram=LCD->LCD_RAM;
return ram;
}
//读寄存器
//LCD_Reg:寄存器地址
//返回值:读到的数据
u16 LCD_ReadReg(u16 LCD_Reg)
{
LCD_WR_REG(LCD_Reg); //写入要读的寄存器序号
delay_us(5);
return LCD_RD_DATA(); //返回读到的值
}
//从ILI93xx读出的数据为GBR格式,而我们写入的时候为RGB格式。
//通过该函数转换
//c:GBR格式的颜色值
//返回值:RGB格式的颜色值
u16 LCD_BGR2RGB(u16 c)
{
u16 r,g,b,rgb;
b=(c>>0)&0x1f;
g=(c>>5)&0x3f;
r=(c>>11)&0x1f;
rgb=(b<<11)+(g<<5)+(r<<0);
return(rgb);
}
u16 LCD_ReadPoint(u16 x,u16 y)
{
u16 r=0,g=0,b=0;
if(x>=lcddev.width||y>=lcddev.height)return 0; //超过了范围,直接返回
LCD_SetCursor(x,y);
if(lcddev.id==0X9341||lcddev.id==0X6804||lcddev.id==0X5310||lcddev.id==0X1963)LCD_WR_REG(0X2E);//9341/6804/3510/1963 发送读GRAM指令
else if(lcddev.id==0X5510)LCD_WR_REG(0X2E00); //5510 发送读GRAM指令
else LCD_WR_REG(0X22); //其他IC发送读GRAM指令
if(lcddev.id==0X9320)opt_delay(2); //FOR 9320,延时2us
r=LCD_RD_DATA(); //dummy Read
if(lcddev.id==0X1963)return r; //1963直接读就可以
opt_delay(2);
r=LCD_RD_DATA(); //实际坐标颜色
if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510) //9341/NT35310/NT35510要分2次读出
{
opt_delay(2);
b=LCD_RD_DATA();
g=r&0XFF; //对于9341/5310/5510,第一次读取的是RG的值,R在前,G在后,各占8位
g<<=8;
}
if(lcddev.id==0X9325||lcddev.id==0X4535||lcddev.id==0X4531||lcddev.id==0XB505||lcddev.id==0XC505)return r; //这几种IC直接返回颜色值
else if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510)return (((r>>11)<<11)|((g>>10)<<5)|(b>>11));//ILI9341/NT35310/NT35510需要公式转换一下
else return LCD_BGR2RGB(r); //其他IC
}
3. 初始化
//初始化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; // //PORTD复用推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOE, &GPIO_InitStructure); // //PORTG12复用推挽输出 A0 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 ....... ....... }
4. 开启/关闭显示
//LCD开启显示
void LCD_DisplayOn(void)
{
if(lcddev.id==0X9341||lcddev.id==0X6804||lcddev.id==0X5310||lcddev.id==0X1963)LCD_WR_REG(0X29); //开启显示
else if(lcddev.id==0X5510)LCD_WR_REG(0X2900); //开启显示
else LCD_WriteReg(0X07,0x0173); //开启显示
}
//LCD关闭显示
void LCD_DisplayOff(void)
{
if(lcddev.id==0X9341||lcddev.id==0X6804||lcddev.id==0X5310||lcddev.id==0X1963)LCD_WR_REG(0X28); //关闭显示
else if(lcddev.id==0X5510)LCD_WR_REG(0X2800); //关闭显示
else LCD_WriteReg(0X07,0x0);//关闭显示
}
5. 显示函数
//显示数字,高位为0,还是显示
//x,y:起点坐标
//num:数值(0~999999999);
//len:长度(即要显示的位数)
//size:字体大小
//mode:
//[7]:0,不填充;1,填充0.
//[6:1]:保留
//[0]:0,非叠加显示;1,叠加显示.
void LCD_ShowxNum(u16 x,u16 y,u32 num,u8 len,u8 size,u8 mode)
{
u8 t,temp;
u8 enshow=0;
for(t=0;t<len;t++)
{
temp=(num/LCD_Pow(10,len-t-1))%10;
if(enshow==0&&t<(len-1))
{
if(temp==0)
{
if(mode&0X80)LCD_ShowChar(x+(size/2)*t,y,'0',size,mode&0X01);
else LCD_ShowChar(x+(size/2)*t,y,' ',size,mode&0X01);
continue;
}else enshow=1;
}
LCD_ShowChar(x+(size/2)*t,y,temp+'0',size,mode&0X01);
}
}
//显示字符串
//x,y:起点坐标
//width,height:区域大小
//size:字体大小
//*p:字符串起始地址
void LCD_ShowString(u16 x,u16 y,u16 width,u16 height,u8 size,u8 *p)
{
u8 x0=x;
width+=x;
height+=y;
while((*p<='~')&&(*p>=' '))//判断是不是非法字符!
{
if(x>=width){
x=x0;y+=size;}
if(y>=height)break;//退出
LCD_ShowChar(x,y,*p,size,0);
x+=size/2;
p++;
}
}
//在指定位置显示一个字符
//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;
}
}
}
}
//画矩形
//(x1,y1),(x2,y2):矩形的对角坐标
void LCD_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2)
{
LCD_DrawLine(x1,y1,x2,y1);
LCD_DrawLine(x1,y1,x1,y2);
LCD_DrawLine(x1,y2,x2,y2);
LCD_DrawLine(x2,y1,x2,y2);
}
//在指定位置画一个指定大小的圆
//(x,y):中心点
//r :半径
void LCD_Draw_Circle(u16 x0,u16 y0,u8 r)
{
int a,b;
int di;
a=0;b=r;
di=3-(r<<1); //判断下个点位置的标志
while(a<=b)
{
LCD_DrawPoint(x0+a,y0-b); //5
LCD_DrawPoint(x0+b,y0-a); //0
LCD_DrawPoint(x0+b,y0+a); //4
LCD_DrawPoint(x0+a,y0+b); //6
LCD_DrawPoint(x0-a,y0+b); //1
LCD_DrawPoint(x0-b,y0+a);
LCD_DrawPoint(x0-a,y0-b); //2
LCD_DrawPoint(x0-b<