普中stm32开发板tftlcd显示图片_「正点原子STM32Mini板资料连载」第十六章 TFTLCD 显示实验...

论坛 期权论坛 脚本     
匿名技术用户   2021-1-5 02:33   29   0

1)实验平台:正点原子STM32mini开发板

2)摘自《正点原子STM32 不完全手册(HAL 库版)关注官方微信号公众号,获取更多资料:正点原子

8cd64af0370d1fcbf523d559b78979dc.png

第十六章 TFTLCD 显示实验

上一章我们介绍了 OLED 模块及其显示,但是该模块只能显示单色/双色,不能显示彩色,

而且尺寸也较小。本章我们将介绍 ALIENTEK 2.8 寸 TFT LCD 模块,该模块采用 TFTLCD 面

板,可以显示 16 位色的真彩图片。在本章中,我们将使用 MiniSTM32 开发板上的 LCD 接口,

来点亮 TFTLCD,并实现 ASCII 字符和彩色的显示等功能,并在串口打印 LCD 控制器 ID,同

时在 LCD 上面显示。本章分为如下几个部分:

16.1 TFTLCD 简介

16.2 硬件设计

16.3 软件设计

16.4 下载验证

16.1 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 并口,自带触摸屏。

该模块的外观图如图 16.1.1 所示:

639a556f8e96924d6917bb878bc0a1ac.png

图 16.1.1 ALIENTEK 2.8 寸 TFTLCD 外观图

模块原理图如图 16.1.2 所示:

3c4e1c44fbe95f16e4f52c666a5357b3.png

图 16.1.2 ALIENTEK 2.8 寸 TFTLCD 模块原理图

TFTLCD 模块采用 2*17 的 2.54 公排针与外部连接,接口定义如图 16.1.3 所示:

9f83f9f4cde2d028008ee7c0983c93cf.png

图 16.1.3 ALIENTEK 2.8 寸 TFTLCD 模块接口图

从图 16.1.3 可以看出,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 的对应关系如图 16.1.4 所示:

2ea44dac54bf4d4ee63608c934274a13.png

图 16.1.4 16 位数据与显存对应关系图

从图中可以看出,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,该指令如表 16.1.1

所示:

61c76d2232f25809b4bf919a67b12780.png

表 16.1.1 0XD3 指令描述

从上表可以看出,0XD3 指令后面跟了 4 个参数,最后 2 个参数,读出来是 0X93 和 0X41,

刚好是我们控制器 ILI9341 的数字部分,从而,通过该指令,即可判别所用的 LCD 驱动器是什

么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动 IC 的初始化代码,从而

兼容不同驱动 IC 的屏,使得一个代码支持多款 LCD。

接下来看指令:0X36,这是存储访问控制指令,可以控制 ILI9341 存储器的读写方向,简

单的说,就是在连续写 GRAM 的时候,可以控制 GRAM 指针的增长方向,从而控制显示方式

(读 GRAM 也是一样)。该指令如表 16.1.2 所示:

a045708eb49dce1958f589a4e6d3c4da.png

表 16.1.2 0X36 指令描述

从上表可以看出,0X36 指令后面,紧跟一个参数,这里我们主要关注:MY、MX、MV

这三个位,通过这三个位的设置,我们可以控制整个 ILI9341 的全部扫描方向,如表 16.1.3 所

示:

b91f677d9753d94a1ca37215523add7b.png

表 16.1.3 MY、MX、MV 设置与 LCD 扫描方向关系表

这样,我们在利用 ILI9341 显示内容的时候,就有很大灵活性了,比如显示 BMP 图片,

BMP 解码数据,就是从图片的左下角开始,慢慢显示到右上角,如果设置 LCD 扫描方向为从

左到右,从下到上,那么我们只需要设置一次坐标,然后就不停的往 LCD 填充颜色数据即可,

这样可以大大提高显示速度。

接下来看指令:0X2A,这是列地址设置指令,在从左到右,从上到下的扫描方式(默认)

下面,该指令用于设置横坐标(x 坐标),该指令如表 16.1.4 所示:

c0140131dd6d7f080619cbac4dd41e00.png

表 16.1.4 0X2A 指令描述

在默认扫描方式时,该指令用于设置 x 坐标,该指令带有 4 个参数,实际上是 2 个坐标值:

SC 和 EC,即列地址的起始值和结束值,SC 必须小于等于 EC,且 0≤SC/EC≤239。一般在设

置 x 坐标的时候,我们只需要带 2 个参数即可,也就是设置 SC 即可,因为如果 EC 没有变化,

我们只需要设置一次即可(在初始化 ILI9341 的时候设置),从而提高速度。

与 0X2A 指令类似,指令:0X2B,是页地址设置指令,在从左到右,从上到下的扫描方式

(默认)下面,该指令用于设置纵坐标(y 坐标)。该指令如表 16.1.5 所示:

c92f8443784bd6d7743ed7804fecd915.png

表 16.1.5 0X2B 指令描述

在默认扫描方式时,该指令用于设置 y 坐标,该指令带有 4 个参数,实际上是 2 个坐标值:

SP 和 EP,即页地址的起始值和结束值,SP 必须小于等于 EP,且 0≤SP/EP≤319。一般在设置

y 坐标的时候,我们只需要带 2 个参数即可,也就是设置 SP 即可,因为如果 EP 没有变化,我

们只需要设置一次即可(在初始化 ILI9341 的时候设置),从而提高速度。

接下来看指令:0X2C,该指令是写 GRAM 指令,在发送该指令之后,我们便可以往 LCD

的 GRAM 里面写入颜色数据了,该指令支持连续写,指令描述如表 16.1.6 所示:

d288d778bbb4b6641b572877861bff8d.png

表 16.1.6 0X2C 指令描述

从上表可知,在收到指令 0X2C 之后,数据有效位宽变为 16 位,我们可以连续写入 LCD

GRAM 值,而 GRAM 的地址将根据 MY/MX/MV 设置的扫描方向进行自增。例如:假设设置

的是从左到右,从上到下的扫描方式,那么设置好起始坐标(通过 SC,SP 设置)后,每写入

一个颜色值,GRAM 地址将会自动自增 1(SC++),如果碰到 EC,则回到 SC,同时 SP++,一

直到坐标:EC,EP 结束,其间无需再次设置的坐标,从而大大提高写入速度。

最后,来看看指令:0X2E,该指令是读 GRAM 指令,用于读取 ILI9341 的显存(GRAM),

该指令在 ILI9341 的数据手册上面的描述是有误的,真实的输出情况如表 16.1.7 所示:

323cc522c23d54c1e70729e5073a8bd5.png

表 16.1.7 0X2E 指令描述

该指令用于读取GRAM,如表16.1.7所示,ILI9341在收到该指令后,第一次输出的是dummy

数据,也就是无效的数据,第二次开始,读取到的才是有效的 GRAM 数据(从坐标:SC,SP

开始),输出规律为:每个颜色分量占 8 个位,一次输出 2 个颜色分量。比如:第一次输出是

R1G1,随后的规律为:B1R2→G2B2→R3G3→B3R4→G4B4→R5G5... 以此类推。如果我们只

需要读取一个点的颜色值,那么只需要接收到参数 3 即可,如果要连续读取(利用 GRAM 地址

自增,方法同上),那么就按照上述规律去接收颜色数据。

以上,就是操作 ILI9341 常用的几个指令,通过这几个指令,我们便可以很好的控制 ILI9341

显示我们所要显示的内容了。

一般 TFTLCD 模块的使用流程如图 16.1.4:

765bd60ee102fc4c67f99e9f730cbece.png

图 16.1.4 TFTLCD 使用流程

任何 LCD,使用流程都可以简单的用以上流程图表示。其中硬复位和初始化序列,只需要

执行一次即可。而画点流程就是:设置坐标→写 GRAM 指令→写入颜色数据,然后在 LCD 上

面,我们就可以看到对应的点显示我们写入的颜色了。读点流程为:设置坐标→读 GRAM 指令

→读取颜色数据,这样就可以获取到对应点的颜色数据了。

以上只是最简单的操作,也是最常用的操作,有了这些操作,一般就可以正常使用 TFTLCD

了。接下来我们将该模块用来来显示字符和数字,通过以上介绍,我们可以得出 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 来实现,

但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而

达到显示字符/数字的目标,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数,

就可以实现数字/字符的显示了。

16.2 硬件设计

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

1) 指示灯 DS0

2) TFTLCD 模块

TFTLCD 模块的电路在前面已有详细说明了,这里我们介绍 TFTLCD 模块与 ALIETEK

MiniSTM32 开发板的连接,MiniSTM32 开发板底板的 LCD 接口和 ALIENTEK TFTLCD 模块直

接可以对插(靠右插!),连接如图 16.2.1 所示:

a72d9a68986a56d85c365ea0ca388f74.png

图 16.2.1 TFTLCD 与开发板连接示意图

图 16.2.1 中圈出来的部分就是连接 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 模块插

上去就好了。

16.3 软件设计

软件设计我们依旧在之前的工程上面增加,不过没用到 OLED,所以先去掉 oled.c(注意,

此时 HARDWARE 组仅剩:led.c),然后在 HARDWARE 文件夹下新建一个 LCD 的文件夹。然

后打开 USER 文件夹下的工程,新建一个 lcd.c 的文件和 lcd.h 的头文件,保存在 LCD 文件夹下,

并将 LCD 文件夹加入头文件包含路径。在 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 横竖屏切换等重要功能,所以还是利大于弊的。有

了以上了解,下面我们开始介绍 lcd.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 的代码如下:

//读取个某点的颜色值//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==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

{

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 的型号执行不同的初始化

代码,其简化代码如下:

//初始化 lcd

//该初始化函数可以初始化各种 ALIENTEK 出品的 LCD 液晶屏

//本函数占用较大 flash,用户可以根据自己的实际情况,删掉未用到的 LCD 初始化代码.以节

省空间.

void LCD_Init(void)

{

GPIO_InitTypeDef GPIO_Initure;

__HAL_RCC_GPIOB_CLK_ENABLE();

//开启 GPIOB 时钟

__HAL_RCC_GPIOC_CLK_ENABLE();

//开启 GPIOC 时钟

//PC6,7,8,9,10

GPIO_Initure.Pin=GPIO_PIN_6|GPIO_PIN_7|GPIO_PIN_8|

GPIO_PIN_9|GPIO_PIN_10;

GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出

GPIO_Initure.Pull=GPIO_PULLUP; //上拉

GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH; //高速

HAL_GPIO_Init(GPIOC,&GPIO_Initure);

//PB0~15

GPIO_Initure.Pin=GPIO_PIN_All;

//PB 推挽输出

HAL_GPIO_Init(GPIOB,&GPIO_Initure);

__HAL_AFIO_REMAP_SWJ_DISABLE();

//禁止 JTAG

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 不正确,

//新增 lcddev.id==0X9300 判断,因为 9341 在未被复位的情况下会被读成 9300

{

//尝试 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",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,并将该代码加入到 HARDWARE 组下。在介绍完了 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 结构体,在前面有已有介绍,其他的相对就比较简单了。另外这段

代码对颜色和驱动器的寄存器进行了很多宏定义,限于篇幅考虑,我们没有完全贴出来,省略

了其中绝大部分。此部分我们就不多说了。接下来,我们在 test.c 里面修改 main 函数如下:

int main(void)

{

u8 x=0;

u8 lcd_id[12];

//存放 LCD ID 字符串

HAL_Init();

//初始化 HAL 库

Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M

delay_init(72);

//初始化延时函数

uart_init(115200);

//初始化串口

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,"2019/11/15");

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 的详细用法,请百度。

另外特别注意:uart_init 函数,不能去掉,因为在 LCD_Init 函数里面调用了 printf,所以一

旦你去掉这个初始化,就会死机了。实际上,只要你的代码有用到 printf,就必须初始化串口,

否则都会死机,即停在 usart.c 里面的 fputc 函数,出不来。

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

16.4 下载验证

将程序下载到 MiniSTM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时

可以看到 TFTLCD 模块的显示如图 16.4.1 所示:

c93a5b12559e2b8b7f2d8b24b067c175.png

图 16.4.1 TFTLCD 显示效果图

我们可以看到屏幕的背景是不停切换的,同时 DS0 不停的闪烁,证明我们的代码被正确的

执行了,达到了我们预期的目的。另外,本例程除了不支持 CPLD 方案的 7 寸屏模块,其余所

有的 ALIENTEK TFTLCD 模块都可以支持,直接插上去即可使用。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:7942463
帖子:1588486
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP