初学者指南:正点原子Ministm32TFTLCD显示实验详解

本章我们将介绍ALIENTEK 2.8寸TFT LCD模块,该模块采用TFTLCD面板,可以显示16位色的真彩图片。在本章中,我们将使用MiniSTM32开发板上的LCD接口,来点亮TFTLCD,并实现ASCII字符和彩色的显示等功能,并在串口打印LCD控制器ID,同时在LCD上面显示。本章分为如下几个部分:

1 TFTLCD简介

2 硬件设计

3 软件设计

一、TFTLCD简介

本章我们将通过STM32的普通IO口模拟8080总线来控制TFTLCD的显示。TFT-LCD即薄膜晶体管液晶显示器。其英文全称为:Thin Film Transistor-Liquid Crystal Display。TFT-LCD与无源TN-LCD、STN-LCD的简单矩阵不同,它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。TFT-LCD也被叫做真彩液晶显示器。

上一章介绍了OLED模块,本章,我们给大家介绍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并口,自带触摸屏。

本章我们将通过STM32的普通IO口模拟8080总线来控制TFTLCD的显示。TFT-LCD即薄膜晶体管液晶显示器。其英文全称为:Thin Film Transistor-Liquid Crystal Display。TFT-LCD

与无源TN-LCD、STN-LCD的简单矩阵不同,它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。TFT-LCD也被叫做真彩液晶显示器。

上一章介绍了OLED模块,本章,我们给大家介绍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 TFTLCD 模块采用 16 位的并方式与外部连接,之所以 不采用 8 位的方式,是因为彩屏的数据量比较大,尤其在显示图片的时候,如果用 8 位数据线, 就会比 16 位方式慢一倍以上,我们当然希望速度越快越好,所以我们选择 16 位的接口。图 16.1.3还列出了触摸屏芯片的接口,关于触摸屏本章我们不多介绍,后面的章节会有详细的介绍。该 模块的 80 并口有如下一些信号线: CS:TFTLCD 片选信号。 WR:向 TFTLCD 写入数据。 RD:从 TFTLCD 读取数据。 D[15:0]:16 位双向数据线。 RST:硬复位 TFTLCD。 RS:命令/数据标志(0,读写命令;1,读写数据)。 80 并口在上一节我们已经有详细的介绍了,这里我们就不再介绍,需要说明的是,TFTLCD

模块的 RST 信号线是直接接到 STM32 的复位脚上,并不由软件控制,这样可以省下来一个 IO

口。另外我们还需要一个背光控制线来控制 TFTLCD 的背光。所以,我们总共需要的 IO 口数 目为 21 个。这里还需要注意,我们标注的 DB1~DB8,DB10~DB17,是相对于 LCD 控制 IC 标 注的,实际上大家可以把他们就等同于 D0~D15(按从小到大顺序),这样理解起来简单点。 ALIENTEK 提供 2.8/3.5/4.3/7 寸等不同尺寸的 TFTLCD 模块,其驱动芯片有很多种类型, 比如有:ILI9341/ILI9325/RM68042/RM68021/ILI9320/ILI9328/LGDP4531/LGDP4535/SPFD5408 /SSD1289/1505/B505/C505/NT35310/NT35510/SSD1963 等(具体的型号,大家可以通过下载本章 实验代码,通过串口或者 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 等驱动器不一 样,必须加以注意。

接下来,我们介绍一下 ILI9341 的几个重要命令,因为 ILI9341 的命令很多,我们这里就 不全部介绍了,有兴趣的大家可以找到 ILI9341 的 datasheet 看看。里面对这些命令有详细的介 绍。我们将介绍:0XD3,0X36,0X2A,0X2B,0X2C,0X2E 等 6 条指令。 首先来看指令:0XD3,这个是读 ID4 指令,用于读取 LCD 控制器的 ID,该指令如下表所示:

顺序                        控制                                     各位描述                            HEX                                                       RS RD WR            D15~D8 D7 D6 D5 D4 D3 D2 D1 D0

指令                  0      1    ↑                   XX       1    1   0   1   0    0   1   1      D3H

参数 1               1      ↑    1                   XX       X    X  X   X  X    X   X   X       X

参数 2               1      ↑    1                   XX       0    0   0   0   0    0   0    0      00H

参数3                1      ↑    1                   XX       1    0   0   1   0    0   1    1      93H

参数4                1      ↑    1                   XX       0    1   0   0   0    0   0    1      41H

从上表可以看出,0XD3 指令后面跟了 4 个参数,最后 2 个参数,读出来是 0X93 和 0X41, 刚好是我们控制器 ILI9341 的数字部分,从而,通过该指令,即可判别所用的 LCD 驱动器是什 么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动 IC 的初始化代码,从而 兼容不同驱动 IC 的屏,使得一个代码支持多款 LCD。 接下来看指令:0X36,这是存储访问控制指令,可以控制 ILI9341 存储器的读写方向,简 单的说,就是在连续写 GRAM 的时候,可以控制 GRAM 指针的增长方向,从而控制显示方式(读 GRAM 也是一样)。

接下来我们将该模块用来来显示字符和数字,通过以上介绍,我们可以得出 TFTLCD 显示 需要的相关设置步骤如下:

1)设置 STM32 与 TFTLCD 模块相连接的 IO。

这一步,先将我们与 TFTLCD 模块相连的 IO 口进行初始化,以便驱动 LCD。这里需要根 据连接电路以及 TFTLCD 模块的设置来确定。

2)初始化 TFTLCD 模块。

即图 16.1.4 的初始化序列,这里我们没有硬复位 LCD,因为 MiniSTM32 开发板的 LCD 接 口,将 TFTLCD 的 RST 同 STM32 的 RESET 连接在一起了,只要按下开发板的 RESET 键,就 会对 LCD 进行硬复位。初始化序列,就是向 LCD 控制器写入一系列的设置值(比如伽马校准), 这些初始化序列一般 LCD 供应商会提供给客户,我们直接使用这些序列即可,不需要深入研究。 在初始化之后,LCD 才可以正常使用。

3)通过函数将字符和数字显示到 TFTLCD 模块上。

这一步则通过图 16.1.4 左侧的流程,即:设置坐标→写 GRAM 指令→写 GRAM 来实现, 但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而 达到显示字符/数字的目标,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数, 就可以实现数字/字符的显示了。

二、硬件设计

本实验用到的硬件资源有:

1) 指示灯 DS0

2) TFTLCD 模块

TFTLCD 模块的电路在前面已有详细说明了,这里我们介绍 TFTLCD 模块与 ALIETEKMiniSTM32 开发板的连接,MiniSTM32 开发板底板的 LCD 接口和 ALIENTEK TFTLCD 模块直 接可以对(靠右插!),连接如下图所示:

上图中圈出来的部分就是连接 TFTLCD 模块的接口,板上的接口比液晶模块的插针要 多 2 个口,液晶模块在这里是靠右插的。多出的 2 个口是给 OLED 用的,所以 OLED 模块在接 这里的时候,是靠左插的,这个要注意。在硬件上,TFTLCD 模块与 MiniSTM32 开发板的 IO

口对应关系如下: LCD_LED 对应 PC10; LCD_CS 对应 PC9; LCD _RS 对应 PC8; LCD _WR 对应 PC7; LCD _RD 对应 PC6; LCD _D[17:1]对应 PB[15:0];

这些线的连接,MiniSTM32 开发板的内部已经连接好了,我们只需要将 TFTLCD 模块插 上去就好了。

三、软件设计

大家打开液晶显示实验工程会发现,我们在工程中添加了 lcd.c 文件和对应的头文件 lcd.h。 lcd.c 里面代码比较多,我们这里就不贴出来了,只针对几个重要的函数进行讲解。完整版 的代码见光盘→4,程序源码→标准例程-库函数版本→实验 11 TFTLCD 显示实验的 lcd.c 文件。

首先,我们介绍一下 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 横竖屏状态等,这个结构体虽然占用了 14 个字节的内存,但是却可以让我们的驱动函数 支持不同尺寸的 LCD,同时可以实现 LCD 横竖屏切换等重要功能,所以还是利大于弊的。有 了以上了解,下面我们开始介绍 ILI93xx.c 里面的一些重要函数。

第一个是 LCD_WR_DATA 函数,该函数在 lcd.h 里面,通过宏定义的方式申明。该函数通 过 80 并口向 LCD 模块写入一个 16 位的数据,使用频率是最高的,这里我们采用了宏定义的方 式,以提高速度。其代码如下

//写数据函数 
#define LCD_WR_DATA(data){\ 
LCD_RS_SET;\ 
LCD_CS_CLR;\ 
DATAOUT(data);\ 
LCD_WR_CLR;\ 
LCD_WR_SET;\ 
LCD_CS_SET;\ 
} 

上面函数中的‘\’是 C 语言中的一个转义字符,用来连接上下文,因为宏定义只能是一个 串,而当你的串过长(超过一行的时候),就需要换行了,此时就必须通过反斜杠来连接上下文。 这里的‘\’正是起这个作用。在上面的函数中,LCD_RS_SET/ LCD_CS_CLR/ LCD_WR_CLR/ LCD_WR_SET/ LCD_CS_SET 等是操作 RS/CS/WR 的宏定义,均是采用 STM32 的快速 IO 控制 寄存器实现的,从而提高速度。

第二个是:LCD_WR_DATAX 函数,该函数在 ILI93xx.c 里面定义,功能和 LCD_WR_DATA

一模一样,该函数代码如下:

//写数据函数 
//可以替代 LCD_WR_DATAX 宏,拿时间换空间. 
//data:寄存器值 
void LCD_WR_DATAX(u16 data) 
{ 
 LCD_RS_SET; 
 LCD_CS_CLR; 
 DATAOUT(data); 
 LCD_WR_CLR; 
 LCD_WR_SET; 
 LCD_CS_SET; 
} 

我们知道,宏定义函数的好处就是速度快(直接嵌到被调用函数里面去了),坏处就是占空 间大。在 LCD_Init 函数里面,有很多地方要写数据,如果全部用宏定义的 LCD_WR_DATA 函 数,那么就会占用非常大的 flash,所以我们这里另外实现一个函数:LCD_WR_DATAX,专门 给 LCD_Init 函数调用,从而大大减少 flash 占用量。

第三个是 LCD_WR_REG 函数,该函数是通过 8080 并口向 LCD 模块写入寄存器命令,因 为该函数使用频率不是很高,我们不采用宏定义来做(宏定义占用 FLASH 较多),通过 LCD_RS

来标记是写入命令(LCD_RS=0)还是数据(LCD_RS=1)。该函数代码如下:

//写寄存器函数 
//data:寄存器值 
void LCD_WR_REG(u16 data) 
{ 
 LCD_RS_CLR;//写地址 
 LCD_CS_CLR; 
 DATAOUT(data); 
 LCD_WR_CLR; 
 LCD_WR_SET; 
 LCD_CS_SET; 
} 

既然有写寄存器命令函数,那就有读寄存器数据函数。接下来介绍 LCD_RD_DATA 函数, 该函数用来读取 LCD 控制器的寄存器数据(非 GRAM 数据),该函数代码如下:

//读 LCD 寄存器数据 
//返回值:读到的值 
u16 LCD_RD_DATA(void) 
{ 
 u16 t; 
 GPIOB->CRL=0X88888888; //PB0-7 上拉输入 
 GPIOB->CRH=0X88888888; //PB8-15 上拉输入 
 GPIOB->ODR=0X0000; //全部输出 0 
 LCD_RS_SET; 
 LCD_CS_CLR; 
 LCD_RD_CLR; //读取数据(读寄存器时,并不需要读 2 次) 
 if(lcddev.id==0X8989)delay_us(2);//FOR 8989,延时 2us 
 t=DATAIN; 
 LCD_RD_SET; 
 LCD_CS_SET; 
 GPIOB->CRL=0X33333333; //PB0-7 上拉输出 
 GPIOB->CRH=0X33333333; //PB8-15 上拉输出 
 GPIOB->ODR=0XFFFF; //全部输出高 
 return t; 
} 

以上 4 个函数,用于实现 LCD 基本的读写操作,接下来,我们介绍 2 个 LCD 寄存器操作 的函数,LCD_WriteReg 和 LCD_ReadReg,这两个函数代码如下:

//LCD_Reg:寄存器编号 
//LCD_RegValue:要写入的值 
void LCD_WriteReg(u16 LCD_Reg,u16 LCD_RegValue) 
{ 
 LCD_WR_REG(LCD_Reg); 
 LCD_WR_DATA(LCD_RegValue); 
} 
//读寄存器 
//LCD_Reg:寄存器编号 
//返回值:读到的值 
u16 LCD_ReadReg(u16 LCD_Reg) 
{ 
 LCD_WR_REG(LCD_Reg); //写入要读的寄存器号 
 return LCD_RD_DATA(); 
} 

这两个函数函数十分简单,LCD_WriteReg 用于向 LCD 指定寄存器写入指定数据,而LCD_ReadReg 则用于读取指定寄存器的数据,这两个函数,都只带一个参数/返回值,所以, 在有多个参数操作(读取/写入)的时候,就不适合用这两个函数了,得另外实现。

第七个要介绍的函数是坐标设置函数,该函数代码如下:

//设置光标位置 
//Xpos:横坐标 
//Ypos:纵坐标 
void LCD_SetCursor(u16 Xpos, u16 Ypos) 
{ 
 if(lcddev.id==0X9341||lcddev.id==0X5310) 
 { 
 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); 
 }else if(lcddev.id==0X6804) 
 { 
 if(lcddev.dir==1)Xpos=lcddev.width-1-Xpos;//横屏时处理 
 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); 
 }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 
 { 
 if(lcddev.dir==1)Xpos=lcddev.width-1-Xpos;//横屏其实就是调转 x,y 坐标 
 LCD_WriteReg(lcddev.setxcmd, Xpos); 
 LCD_WriteReg(lcddev.setycmd, Ypos); 
 } 
} 

该函数实现将 LCD 的当前操作点设置到指定坐标(x,y)。因为不同 LCD 的设置方式不一定 完全一样,所以代码里面有好几个判断,对不同的驱动 IC 进行不同的设置。

接下来我们介绍第八个函数:画点函数。该函数实现代码如下:

//画点 
//x,y:坐标 
//POINT_COLOR:此点的颜色 
void LCD_DrawPoint(u16 x,u16 y) 
{ 
 LCD_SetCursor(x,y); //设置光标位置 
 LCD_WriteRAM_Prepare(); //开始写入 GRAM 
 LCD_WR_DATA(POINT_COLOR); 
} 

该函数实现比较简单,就是先设置坐标,然后往坐标写颜色。其中 POINT_COLOR 是我们 定义的一个全局变量,用于存放画笔颜色,顺带介绍一下另外一个全局变量:BACK_COLOR, 该变量代表 LCD 的背景色。LCD_DrawPoint 函数虽然简单,但是至关重要,其他几乎所有上 层函数,都是通过调用这个函数实现的。

有了画点,当然还需要有读点的函数,第九个介绍的函数就是读点函数,用于读取 LCD

的 GRAM,这里说明一下,为什么 OLED 模块没做读 GRAM 的函数,而这里做了。因为 OLED

模块是单色的,所需要全部 GRAM 也就 1K 个字节,而 TFTLCD 模块为彩色的,点数也比 OLED

模块多很多,以 16 位色计算,一款 320×240 的液晶,需要 320×240×2 个字节来存储颜色值, 也就是也需要 150K 字节,这对任何一款单片机来说,都不是一个小数目了。而且我们在图形 叠加的时候,可以先读回原来的值,然后写入新的值,在完成叠加后,我们又恢复原来的值。 这样在做一些简单菜单的时候,是很有用的。这里我们读取 TFTLCD 模块数据的函数为

LCD_ReadPoint,该函数直接返回读到的 GRAM 值。该函数使用之前要先设置读取的 GRAM

地址,通过 LCD_SetCursor 函数来实现。LCD_ReadPoint 的代码如下:

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==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 指令 
 GPIOB->CRL=0X88888888; //PB0-7 上拉输入 
 GPIOB->CRH=0X88888888; //PB8-15 上拉输入 
 GPIOB->ODR=0XFFFF; //全部输出高 
 LCD_RS_SET; 
 LCD_CS_CLR; 
 LCD_RD_CLR; //读取数据(读 GRAM 时,第一次为假读) 
 opt_delay(2); //延时 
 r=DATAIN; //实际坐标颜色 
 LCD_RD_SET; 
 if(lcddev.id==0X1963) 
 { 
 LCD_CS_SET; 
 GPIOB->CRL=0X33333333; //PB0-7 上拉输出 
 GPIOB->CRH=0X33333333; //PB8-15 上拉输出 
 GPIOB->ODR=0XFFFF; //全部输出高 
 return r; //1963 直接读就可以 
 } 
 LCD_RD_CLR; //dummy READ 
 opt_delay(2); //延时 
 r=DATAIN; //实际坐标颜色 
 LCD_RD_SET; 
 if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510)//这几个 IC 要分 2 次读出 
 { 
 LCD_RD_CLR; 
 opt_delay(2);//延时 
 b=DATAIN;//读取蓝色值 
 LCD_RD_SET; 
 g=r&0XFF;//对于 9341,第一次读取的是 RG 的值,R 在前,G 在后,各占 8 位 
 g<<=8; 
 }else if(lcddev.id==0X6804) 
 { 
 LCD_RD_CLR; 
 LCD_RD_SET; 
 r=DATAIN;//6804 第二次读取的才是真实值
} 
 LCD_CS_SET; 
 GPIOB->CRL=0X33333333; //PB0-7 上拉输出 
 GPIOB->CRH=0X33333333; //PB8-15 上拉输出 
 GPIOB->ODR=0XFFFF; //全部输出高 
 if(lcddev.id==0X9325||lcddev.id==0X4535||lcddev.id==0X4531||lcddev.id==0X8989|| 
lcddev.id==0XB505)return r; //这几种 IC 直接返回颜色值 
 else if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510) 
return (((r>>11)<<11)|((g>>10)<<5)|(b>>11));//这几个 IC 需要公式转换一下 
 else return LCD_BGR2RGB(r); //其他 IC 
} 

在 LCD_ReadPoint 函数中,因为我们的代码不止支持一种 LCD 驱动器,所以,我们根据 不同的 LCD 驱动器((lcddev.id)型号,执行不同的操作,以实现对各个驱动器兼容,提高函数 的通用性。

第十个要介绍的是字符显示函数 LCD_ShowChar,该函数同前面 OLED 模块的字符显示函 数差不多,但是这里的字符显示函数多了一个功能,就是可以以叠加方式显示,或者以非叠加 方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。 该函数实现代码如下:

//在指定位置显示一个字符 
//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-' ';//得到偏移后的值 
 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(x>=lcddev.width)return; //超区域了 
if((y-y0)==size) 
 { 
 y=y0; x++; 
 if(x>=lcddev.width)return; //超区域了 
 break; 
 } 
 } 
 } 
} 

在 LCD_ShowChar 函数里面,我们采用快速画点函数 LCD_Fast_DrawPoint 来画点显示字 符,该函数同 LCD_DrawPoint 一样,只是带了颜色参数,且减少了函数调用的时间,详见本例 程源码。该代码中我们用到了三个字符集点阵数据数组 asc2_2412、asc2_1206 和 asc2_1608, 这几个字符集的点阵数据的提取方式,同十五章介绍的方法是一模一样的。详细请参考第十五 章。

最后,我们再介绍一下 TFTLCD 模块的初始化函数 LCD_Init,该函数先初始化 STM32 与

TFTLCD 连接的 IO 口,然后读取 LCD 控制器的型号,根据控制 IC 的型号执行不同的初始化 代码,其简化代码如下:

//该初始化函数可以初始化各种 ALIENTEK 出品的 LCD 液晶屏 
//本函数占用较大 flash,可根据自己的实际情况,删掉未用到的LCD初始化代码.以节省空间. 
void LCD_Init(void) 
{ 
 GPIO_InitTypeDef GPIO_InitStructure; 
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC| 
RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO, ENABLE); 
//使能 PORTB,C 时钟以及 AFIO 时钟 
 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE); //开启 SWD 
 
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_9| 
GPIO_Pin_8|GPIO_Pin_7|GPIO_Pin_6; // //PORTC6~10 复用推挽输出 
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 
 GPIO_Init(GPIOC, &GPIO_InitStructure); //GPIOC 
 
GPIO_SetBits(GPIOC,GPIO_Pin_10|GPIO_Pin_9|GPIO_Pin_8|GPIO_Pin_7|GPIO_Pin_6); 
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; // PORTB 推挽输出 
 GPIO_Init(GPIOB, &GPIO_InitStructure); //GPIOB 
 GPIO_SetBits(GPIOB,GPIO_Pin_All); delay_ms(50); // delay 50 ms 
 LCD_WriteReg(0x0000,0x0001); //可以去掉 
 delay_ms(50); // delay 50 ms 
 lcddev.id = LCD_ReadReg(0x0000); 
 if(lcddev.id<0XFF||lcddev.id==0XFFFF||lcddev.id==0X9300)//读到 ID 不正确 
 { 
 //尝试 9341 ID 的读取 
LCD_WR_REG(0XD3); 
 LCD_RD_DATA(); //dummy read 
 LCD_RD_DATA(); //读到 0X00 
 lcddev.id=LCD_RD_DATA(); //读取 93 
 lcddev.id<<=8; 
 lcddev.id|=LCD_RD_DATA(); //读取 41 
 if(lcddev.id!=0X9341) //非 9341,尝试是不是 6804 
 { 
 LCD_WR_REG(0XBF); 
 LCD_RD_DATA(); //dummy read 
 LCD_RD_DATA(); //读回 0X01 
 LCD_RD_DATA(); //读回 0XD0 
 lcddev.id=LCD_RD_DATA();//这里读回 0X68 
 lcddev.id<<=8; 
 lcddev.id|=LCD_RD_DATA();//这里读回 0X04 
 if(lcddev.id!=0X6804) //也不是 6804,尝试看看是不是 NT35310 
 { 
 LCD_WR_REG(0XD4); 
 LCD_RD_DATA(); //dummy read 
 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_WR_REG(0XDA00); 
 LCD_RD_DATA(); //读回 0X00 
 LCD_WR_REG(0XDB00); 
 lcddev.id=LCD_RD_DATA();//读回 0X80 
 lcddev.id<<=8; 
 LCD_WR_REG(0XDC00); 
 lcddev.id|=LCD_RD_DATA();//读回 0X00 
 if(lcddev.id==0x8000)lcddev.id=0x5510;//NT35510 读回的 ID 是 
//8000H,为方便区分,我们强制设置为 5510 
 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==0xXXXX) //其他 LCD 初始化代码 
 { 
 ……//其他 LCD 驱动 IC,初始化代码 
 } 
 LCD_Display_Dir(0); //默认为竖屏显示 
 LCD_LED=1; //点亮背光 
 LCD_Clear(WHITE); 
} 

该函数先对 STM32 与 LCD 连接的相关 IO 进行初始化,之后读取 LCD 控制器型号(LCD ID), 根据读到的 LCD ID,对不同的驱动器执行不同的初始化代码,其中 else if(lcddev.id==0xXXXX), 是省略写法,实际上代码里面有十几个这种 else if 结构,从而可以支持十多款不同的驱动 IC 执 行初始化操作,这样大大提高了整个程序的通用性。大家在以后的学习中应该多使用这样的方 式,以提高程序的通用性、兼容性。

特别注意:本函数使用了 printf 来打印 LCD ID,所以,如果你在主函数里面没有初始化串 口,那么将导致程序死在 printf 里面!!如果不想用 printf,那么请注释掉它。

介绍完 lcd.c 文件后,接下来我们看看 lcd.h 文件内容:

#ifndef __LCD_H 
#define __LCD_H 
#include "sys.h" 
#include "stdlib.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 的画笔颜色和背景色 
extern u16 POINT_COLOR;//默认红色 
extern u16 BACK_COLOR; //背景颜色.默认为白色 
//LCD 端口定义,使用快速 IO 控制 
#define LCD_LED PCout(10) //LCD 背光 PC10 
#define LCD_CS_SET GPIOC->BSRR=1<<9 //片选端口 PC9 
#define LCD_RS_SET GPIOC->BSRR=1<<8 //数据/命令 PC8 
#define LCD_WR_SET GPIOC->BSRR=1<<7 //写数据 PC7 
#define LCD_RD_SET GPIOC->BSRR=1<<6 //读数据 PC6 
#define LCD_CS_CLR GPIOC->BRR=1<<9 //片选端口 PC9 
#define LCD_RS_CLR GPIOC->BRR=1<<8 //数据/命令 PC8 
#define LCD_WR_CLR GPIOC->BRR=1<<7 //写数据 PC7 
#define LCD_RD_CLR GPIOC->BRR=1<<6 //读数据 PC6 
//PB0~15,作为数据线 
#define DATAOUT(x) GPIOB->ODR=x; //数据输出 
#define DATAIN GPIOB->IDR; //数据输入 
// 
//扫描方向定义 
#define L2R_U2D 0 //从左到右,从上到下 
#define L2R_D2U 1 //从左到右,从下到上 
#define R2L_U2D 2 //从右到左,从上到下 
#define R2L_D2U 3 //从右到左,从下到上 
#define U2D_L2R 4 //从上到下,从左到右 
#define U2D_R2L 5 //从上到下,从右到左 
#define D2U_L2R 6 //从下到上,从左到右 
#define D2U_R2L 7 //从下到上,从右到左 
#define DFT_SCAN_DIR L2R_U2D //默认的扫描方向 
//画笔颜色 
#define WHITE 0xFFFF 

……//省略部分 
#define LBBLUE 0X2B12 //浅棕蓝色(选择条目的反色) 
void LCD_Init(void); //初始化 

……//省略部分函数定义 
void LCD_Set_Window(u16 sx,u16 sy,u16 width,u16 height);//设置窗口 
//SSD1963 驱动 LCD 面板参数 
//LCD 分辨率设置 
#define SSD_HOR_RESOLUTION 800 //LCD 水平分辨率 
#define SSD_VER_RESOLUTION 480 //LCD 垂直分辨率 
//LCD 驱动参数设置 
#define SSD_HOR_PULSE_WIDTH 1 //水平脉宽 
#define SSD_HOR_BACK_PORCH 210 //水平前廊 
#define SSD_HOR_FRONT_PORCH 45 //水平后廊 
#define SSD_VER_PULSE_WIDTH 1 //垂直脉宽 
#define SSD_VER_BACK_PORCH 34 //垂直前廊 
#define SSD_VER_FRONT_PORCH 10 //垂直前廊 
//如下几个参数,自动计算 
#define SSD_HT (SSD_HOR_RESOLUTION+SSD_HOR_PULSE_WIDTH+ 
SSD_HOR_BACK_PORCH+SSD_HOR_FRONT_PORCH) 
#define SSD_HPS (SSD_HOR_PULSE_WIDTH+SSD_HOR_BACK_PORCH) 
#define SSD_VT (SSD_VER_PULSE_WIDTH+SSD_VER_BACK_PORCH+ 
SSD_VER_FRONT_PORCH+SSD_VER_RESOLUTION) 
#define SSD_VSP (SSD_VER_PULSE_WIDTH+SSD_VER_BACK_PORCH) 
#endif 

代码里里面的_lcd_dev 结构体,在前面有已有介绍,其他的相对就比较简单了。另外这段 代码对颜色和驱动器的寄存器进行了很多宏定义,限于篇幅考虑,我们没有完全贴出来,省略 了其中绝大部分。此部分我们就不多说了。接下来,看看主函数代码:

int main(void) 
{ 
 u8 x=0; 
 u8 lcd_id[12]; //存放 LCD ID 字符串 
 delay_init(); //延时函数初始化 
 uart_init(9600); //串口初始化为 9600 
 LED_Init(); //初始化与 LED 连接的硬件接口 
 LCD_Init(); 
 POINT_COLOR=RED; sprintf((char*)lcd_id,"LCD ID:%04X",lcddev.id); 
//将 LCD ID 打印到 lcd_id 数组。 
 while(1) 
 { 
 switch(x) 
 { 
 case 0:LCD_Clear(WHITE);break; 
 case 1:LCD_Clear(BLACK);break; 
 case 2:LCD_Clear(BLUE);break; 
 case 3:LCD_Clear(RED);break; 
 case 4:LCD_Clear(MAGENTA);break; 
 case 5:LCD_Clear(GREEN);break; 
 case 6:LCD_Clear(CYAN);break; 
 case 7:LCD_Clear(YELLOW);break; 
 case 8:LCD_Clear(BRRED);break; 
 case 9:LCD_Clear(GRAY);break; 
 case 10:LCD_Clear(LGRAY);break; 
 case 11:LCD_Clear(BROWN);break; 
 } 
 POINT_COLOR=RED; 
 LCD_ShowString(30,40,200,24,24,"Mini STM32 ^_^"); 
 LCD_ShowString(30,70,200,16,16,"TFTLCD TEST") ; 
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); 
 LCD_ShowString(30,110,200,16,16,lcd_id); //显示 LCD ID 
 LCD_ShowString(30,130,200,12,12,"2014/3/7"); 
 x++; 
 if(x==12)x=0; 
 LED0=!LED0; 
 delay_ms(1000); 
 } 
} 

该部分代码将显示一些固定的字符,字体大小包括 24*12、16*8 和 12*6 等三种,同时显示LCD 驱动 IC 的型号,然后不停的切换背景颜色,每 1s 切换一次。而 LED0 也会不停的闪烁, 指示程序已经在运行了。其中我们用到一个 sprintf 的函数,该函数用法同 printf,只是 sprintf把打印内容输出到指定的内存区间上,sprintf 的详细用法,请百度。

在编译通过之后,我们开始下载验证代码。

物联沃分享整理
物联沃-IOTWORD物联网 » 初学者指南:正点原子Ministm32TFTLCD显示实验详解

发表评论