27. LCD—液晶显示

本章参考资料:《STM32F10X-中文参考手册》、《STM32F103增强型系列数据手册》、库帮助文档《stm32f10x_stdperiph_lib_um.chm》。

关于开发板配套的液晶屏控制器参数可查阅《ILI9341.pdf》资料获知。

27.1. 显示器简介

显示器属于计算机的I/O设备,即输入输出设备。它是一种将特定电子信息输出到屏幕上再反射到人眼的显示工具。常见的有CRT显示器、 液晶显示器、LED点阵显示器及OLED显示器。

27.1.1. 液晶显示器

液晶显示器,简称LCD(Liquid Crystal Display),相对于上一代CRT显示器(阴极射线管显示器),LCD显示器具有功耗低、体积小、 承载的信息量大及不伤眼的优点,因而它成为了现在的主流电子显示设备,其中包括电视、电脑显示器、手机屏幕及各种嵌入式设备的显示器。 图 液晶电视及CRT电视 是液晶电视与CRT电视的外观对比,很明显液晶电视更薄,“时尚”是液晶电视给人的第一印象,而CRT 电视则感觉很“笨重”。

液晶电视及CRT电视

液晶是一种介于固体和液体之间的特殊物质,它是一种有机化合物,常态下呈液态,但是它的分子排列却和固体晶体一样非常规则,因此取名液晶。 如果给液晶施加电场,会改变它的分子排列,从而改变光线的传播方向,配合偏振光片,它就具有控制光线透过率的作用,再配合彩色滤光片, 改变加给液晶电压大小,就能改变某一颜色透光量的多少, 图 液晶屏的绿色显示结构 中的就是绿色显示结构。 利用这种原理,做出可控红、绿、蓝光输出强度的显示结构,把三种显示结构组成一个显示单位,通过控制红绿蓝的强度, 可以使该单位混合输出不同的色彩,这样的一个显示单位被称为像素。

液晶屏的绿色显示结构

注意液晶本身是不发光的,所以需要有一个背光灯提供光源,光线经过一系列处理过程才到输出,所以输出的光线强度是要比光源的强度低很多的, 比较浪费能源(当然,比CRT显示器还是节能多了)。而且这些处理过程会导致显示方向比较窄,也就是它的视角较小,从侧面看屏幕会看不清它的显示内容。 另外,输出的色彩变换时,液晶分子转动也需要消耗一定的时间,导致屏幕的响应速度低。

27.1.2. LED和OLED显示器

LED点阵显示器不存在以上液晶显示器的问题,LED点阵彩色显示器的单个像素点内包含红绿蓝三色LED灯,显示原理类似我们实验板上的LED彩灯, 通过控制红绿蓝颜色的强度进行混色,实现全彩颜色输出,多个像素点构成一个屏幕。由于每个像素点都是LED灯自发光的, 所以在户外白天也显示得非常清晰,但由于LED灯体积较大,导致屏幕的像素密度低,所以它一般只适合用于广场上的巨型显示器。 相对来说,单色的LED点阵显示器应用得更广泛,如公交车上的信息展示牌、店招等,见图 LED点阵彩屏有LED单色显示屏

LED点阵彩屏有LED单色显示屏

新一代的OLED显示器与LED点阵彩色显示器的原理类似, 但由于它采用的像素单元是“有机发光二极管”(Organic Light Emitting Diode), 所以像素密度比普通LED点阵显示器高得多,见图 OLED像素结构

OLED像素结构

OLED显示器不需要背光源、对比度高、轻薄、视角广及响应速度快等优点。待到生产工艺更加成熟时, 必将取代现在液晶显示器的地位,见图 采用OLED屏幕的电视及智能手表

采用OLED屏幕的电视及智能手表

27.1.3. 显示器的基本参数

不管是哪一种显示器,都有一定的参数用于描述它们的特性,各个参数介绍如下:

(1) 像素

像素是组成图像的最基本单元要素,显示器的像素指它成像最小的点,即前面讲解液晶原理中提到的一个显示单元。

(2) 分辨率

一些嵌入式设备的显示器常常以“行像素值x列像素值”表示屏幕的分辨率。如分辨率800x480表示该显示器的每一行有800个像素点, 每一列有480个像素点,也可理解为有800列,480行。

(3) 色彩深度

色彩深度指显示器的每个像素点能表示多少种颜色,一般用“位”(bit)来表示。如单色屏的每个像素点能表示亮或灭两种状态(即实际上能显示2种颜色), 用1个数据位就可以表示像素点的所有状态,所以它的色彩深度为1bit,其它常见的显示屏色深为16bit、24bit。

(4) 显示器尺寸

显示器的大小一般以英寸表示,如5英寸、21英寸、24英寸等,这个长度是指屏幕对角线的长度, 通过显示器的对角线长度及长宽比可确定显示器的实际长宽尺寸。

(5) 点距

点距指两个相邻像素点之间的距离,它会影响画质的细腻度及观看距离,相同尺寸的屏幕,若分辨率越高,则点距越小, 画质越细腻。如现在有些手机的屏幕分辨率比电脑显示器的还大,这是手机屏幕点距小的原因;LED点阵显示屏的点距一般都比较大,所以适合远距离观看。

27.2. 液晶控制原理

适合STM32控制的显示屏实物图 是两种适合于STM32芯片使用的显示屏,我们以它为例讲解控制液晶屏的基本原理。

适合STM32控制的显示屏实物图

这个完整的显示屏由液晶显示面板、电容触摸面板以及PCB底板构成。图中的触摸面板带有触摸控制芯片,该芯片处理触摸信号并通过引出的信号线与外部器件通讯, 触摸面板中间是透明的,它贴在液晶面板上面,一起构成屏幕的主体,触摸面板与液晶面板引出的排线连接到PCB底板上,根据实际需要, PCB底板上可能会带有“液晶控制器芯片”,图中右侧的液晶屏PCB上带有RA8875液晶控制器。因为控制液晶面板需要比较多的资源, 所以大部分低级微控制器都不能直接控制液晶面板,需要额外配套一个专用液晶控制器来处理显示过程,外部微控制器只要把它希望显示的数据直接交给液晶控制器即可。 而不带液晶控制器的PCB底板 ,只有小部分的电源管理电路,液晶面板的信号线与外部微控制器相连,直接控制。STM32F429系列的芯片不需要额外的液晶控制器, 也就是说它把专用液晶控制器的功能集成到STM32F429芯片内部了,可以理解为电脑的CPU集成显卡,它节约了额外的控制器成本。 而STM32F1系列的芯片由于没有集成液晶控制器到芯片内部,所以它只能驱动自带控制器的屏幕,可以理解为电脑的外置显卡。

总的来说,这两类屏幕的控制框图如图 两类液晶屏控制框图 所示。

两类液晶屏控制框图

27.2.1. 液晶面板的控制信号

本章我们主要讲解如何控制液晶面板,液晶面板的控制信号线即图 适合STM32控制的显示屏实物图 中液晶面板引出的FPC排线, 其说明见表 液晶面板的信号线 , 液晶面板通过这些信号线与液晶控制器通讯,使用这种通讯信号的被称为RGB接口(RGB Interface)。

液晶面板的信号线

(1) RGB信号线

RGB信号线各有8根,分别用于表示液晶屏一个像素点的红、绿、蓝颜色分量。使用红绿蓝颜色分量来表示颜色是一种通用的做法, 打开Windows系统自带的画板调色工具,可看到颜色的红绿蓝分量值,见图 颜色表示法。 常见的颜色表示会在“RGB”后面附带各个颜色分量值的数据位数,如RGB565表示红绿蓝的数据线数分别为5、6、5根, 一共为16个数据位,可表示216种颜色;而这个液晶屏的种颜色分量的数据线都有8根, 所以它支持RGB888格式,一共24位数据线,可表示的颜色为224种。

颜色表示法

(2) 同步时钟信号CLK

液晶屏与外部使用同步通讯方式,以CLK信号作为同步时钟,在同步时钟的驱动下,每个时钟传输一个像素点数据。

(3) 水平同步信号HSYNC

水平同步信号HSYNC(Horizontal Sync)用于表示液晶屏一行像素数据的传输结束, 每传输完成液晶屏的一行像素数据时,HSYNC会发生电平跳变, 如分辨率为800x480的显示屏(800列,480行),传输一帧的图像HSYNC的电平会跳变480次。

(4) 垂直同步信号VSYNC

垂直同步信号VSYNC(Vertical Sync)用于表示液晶屏一帧像素数据的传输结束,每传输完成一帧像素数据时,VSYNC会发生电平跳变。 其中“帧”是图像的单位,一幅图像称为一帧,在液晶屏中,一帧指一个完整屏液晶像素点。人们常常用“帧/秒”来表示液晶屏的刷新特性, 即液晶屏每秒可以显示多少帧图像,如液晶屏以60帧/秒的速率运行时,VSYNC每秒钟电平会跳变60次。

(5) 数据使能信号DE

数据使能信号DE(Data Enable)用于表示数据的有效性,当DE信号线为高电平时,RGB信号线表示的数据有效。

27.2.2. 液晶数据传输时序

通过上述信号线向液晶屏传输像素数据时,各信号线的时序见图 液晶时序图。图中表示的是向液晶屏传输一帧图像数据的时序,中间省略了多行及多个像素点。

液晶时序图

液晶屏显示的图像可看作一个矩形,结合图 液晶数据传输图解 来理解。液晶屏有一个显示指针,它指向将要显示的像素。 显示指针的扫描方向方向从左到右、从上到下,一个像素点一个像素点地描绘图形。这些像素点的数据通过RGB数据线传输至液晶屏, 它们在同步时钟CLK的驱动下一个一个地传输到液晶屏中,交给显示指针,传输完成一行时,水平同步信号HSYNC电平跳变一次,而传输完一帧时VSYNC电平跳变一次。

液晶数据传输图解

但是,液晶显示指针在行与行之间,帧与帧之间切换时需要延时,而且HSYNC及VSYNC信号本身也有宽度, 这些时间参数说明见表 液晶通讯中的时间参数

液晶通讯中的时间参数

在这些时间参数控制的区域,数据使能信号线“DE”都为低电平,RGB数据线的信号无效,当“DE”为高电平时,表示的数据有效,传输的数据会直接影响液晶屏的显示区域。

27.2.3. 显存

液晶屏中的每个像素点都是数据,在实际应用中需要把每个像素点的数据缓存起来,再传输给液晶屏,一般会使用SRAM或SDRAM性质的存储器, 而这些专门用于存储显示数据的存储器,则被称为显存。显存一般至少要能存储液晶屏的一帧显示数据,如分辨率为800x480的液晶屏, 使用RGB888格式显示,它的一帧显示数据大小为:3x800x480=1152000字节;若使用RGB565格式显示,一帧显示数据大小为:2x800x480=768000字节。

一般来说,外置的液晶控制器会自带显存,而像STM32F429等集成液晶控制器的芯片可使用内部SRAM或外扩SDRAM用于显存空间。

27.3. 野火3.2/2.8寸液晶屏简介

27.3.1. 3.2/2.8寸电阻触摸屏实物

上面讲解的屏幕其液晶控制器与液晶屏是完全分离的,且具有带控制器和不带控制器的版本,易于理解, 下面我们再来分析实验板标配的分辨率为320*240的3.2/2.8寸电阻触摸液晶屏,

本实验板配套的3.2寸和2.8寸屏幕的设计完全一样,采用同样的液晶控制芯片ILI9341,同样的触摸芯片XPT2046, 分辨率同为320*240,它们的区别只是液晶面板的物理尺寸不同,也就是说2.8寸显示单元的集成度更高,画面看起来细腻, 但小一点。对于控制程序来说,两个屏幕完全一样,所以本教程下面对这两个屏幕不作区分。下面以3.2寸屏幕的结构图进行讲解(2.8寸屏结构也完全一样), 见图 实验板标配的3_2寸电阻触摸屏

实验板标配的3_2寸电阻触摸屏

图中的标号部分是液晶屏幕的整体,通过引出的排针接入到实验板上可对它进行控制,它分为标号的液晶触摸面板和标号的PCB底板两部分。

标号处的液晶触摸面板由液晶屏和触摸屏组成,屏幕表面的灰色线框即为电阻触摸屏的信号线,触摸屏的下方即为液晶面板, 在它的内部包含了一个型号为ILI9341的液晶控制器芯片(由于集成度高,所以图中无法看见),该液晶控制器使用8080接口与单片机通讯, 图中液晶面板引出的FPC信号线即8080接口(RGB接口已在内部直接与ILI9341相连),且控制器中包含有显存, 单片机把要显示的数据通过引出的8080接口发送到液晶控制器,这些数据会被存储到它内部的显存中,然后液晶控制器不断把显存的内容刷新到液晶面板,显示内容。

标号处的是PCB底板,它主要包含了一个电阻触摸屏的控制器XPT2046,电阻触摸屏控制器实质上是一个ADC芯片, 通过检测电压值来计算触摸坐标。PCB底板与液晶触摸面板通过FPC排线座连接,然后引出到排针,方便与实验板的排母连接。

27.3.2. ILI9341液晶控制器简介

本液晶屏内部包含有一个液晶控制芯片ILI9341,它的内部结构非常复杂,见图 ILI9341控制器内部框图。 该芯片最主核心部分是位于中间的GRAM(Graphics RAM),它就是显存。GRAM中每个存储单元都对应着液晶面板的一个像素点。 它右侧的各种模块共同作用把GRAM存储单元的数据转化成液晶面板的控制信号,使像素点呈现特定的颜色,而像素点组合起来则成为一幅完整的图像。

框图的左上角为ILI9341的主要控制信号线和配置引脚,根据其不同状态设置可以使芯片工作在不同的模式, 如每个像素点的位数是6、16还是18位;可配置使用SPI接口、8080接口还是RGB接口与MCU进行通讯。 MCU通过SPI、8080接口或RGB接口与ILI9341进行通讯,从而访问它的控制寄存器(CR)、地址计数器(AC)、及GRAM。

在GRAM的左侧还有一个LED控制器(LED Controller)。LCD为非发光性的显示装置, 它需要借助背光源才能达到显示功能,LED控制器就是用来控制液晶屏中的LED背光源。

ILI9341控制器内部框图

27.3.3. 液晶屏的信号线及8080时序

ILI9341控制器根据自身的IM[3:0]信号线电平决定它与MCU的通讯方式,它本身支持SPI及8080通讯方式, 本示例中液晶屏的ILI9341控制器在出厂前就已经按固定配置好(内部已连接硬件电路),它被配置为通过8080接口通讯, 使用16根数据线的RGB565格式。内部硬件电路连接完,剩下的其它信号线被引出到FPC排线, 最后该排线由PCB底板引出到排针,排针再与实验板上的STM32芯片连接, 引出的排针信号线见图 液晶屏引出的信号线

液晶屏引出的信号线

这些信号线的说明见表 液晶屏引出的信号线说明

液晶屏引出的信号线说明

这些信号线即8080通讯接口,带X的表示低电平有效,STM32通过该接口与ILI9341芯片进行通讯,实现对液晶屏的控制。 通讯的内容主要包括命令和显存数据,显存数据即各个像素点的RGB565内容;命令是指对ILI9341的控制指令, MCU可通过8080接口发送命令编码控制ILI9341的工作方式,例如复位指令、设置光标指令、睡眠模式指令等等, 具体的指令在《ILI9341.pdf》数据手册均有详细说明。 写命令时序图见 图使用18条数据线的8080接口写命令时序

图使用18条数据线的8080接口写命令时序

由图可知,写命令时序由片选信号CSX拉低开始,对数据/命令选择信号线D/CX也置低电平表示写入的是命令地址(可理解为命令编码,如软件复位命令:0x01), 以写信号WRX为低,读信号RDX为高表示数据传输方向为写入,同时,在数据线D[17:0](或D[15:0])输出命令地址, 在第二个传输阶段传送的是命令的参数,所以D/CX要置高电平,表示写入的是命令数据,命令数据是某些指令带有的参数,如复位指令编码为0x01,它后面可以带一个参数, 该参数表示多少秒后复位(实际的复位命令不含参数,此处只是为了讲解指令编码与参数的区别)。

当需要把像素数据写入GRAM时,过程很类似,把片选信号CSX拉低后,再把数据/命令选择信号线D/CX置为高电平, 这时由D[17:0]传输的数据则会被ILI9341保存至它的GRAM中。

27.4. 模拟8080接口时序

ILI9341的8080通讯接口时序可以由STM32使用普通GPIO进行模拟,对于具有FSMC外设的高型号STM32(如STM32VET6/STM32ZET6等), 还能直接使用FSMC接口实现8080时序,像访问内部变量一样控制液晶屏。本开发板采用的主控芯片型号为STM32RCT6,不具备FSMC功能, 所以只能采用类似软件模拟I2C时序那样,模拟8080的时序来驱动液晶屏。若对使用FSMC驱动液晶屏感兴趣,可以参考我们F103指南者、F103霸道等开发板的教程。

27.5. 液晶显示实验

本小节讲解如何使用使用模拟8080的时序控制实验板配套的ILI9341液晶屏,见图 液晶屏实物图 ,该液晶屏的分辨率为320x240,支持RGB565格式。

学习本小节内容时,请打开配套的“固件库例程\液晶显示\程序\液晶显示”工程配合阅读。

27.5.1. 硬件设计

液晶屏实物图

液晶屏实物图 液晶屏背面的PCB电路对应图 屏幕PCB底板原理图 、 图 屏幕PCB底板的触摸部分原理图 、图 液晶屏接口 中的原理图, 分别是屏幕PCB底板原理图、触摸部分原理图、液晶排针接口线序图。

屏幕PCB底板原理图

屏幕的PCB底板引出的信号线会通过PCB底板上的FPC接口与液晶面板连接,这些信包括液晶控制相关的CS、RS等信号及DB0-DB15数据线, 其中RS引脚以高电平表示传输数据,低电平表示传输命令;另外还有引出LCD_BK引脚用于控制屏幕的背光供电,可以通过该引脚控制背光的强度, 该引脚为低电平时打开背光。图中的X+/X-/Y+/Y-引脚是液晶面板上触摸屏引出的信号线,它们会被连接到PCB底板的电阻触摸屏控制器,用于检测触摸信号, 其原理图见图 屏幕PCB底板的触摸部分原理图

屏幕PCB底板的触摸部分原理图

触摸检测的主体是型号为XPT2046的芯片,它接收触摸屏的X+/X-/Y+/Y-信号进行处理,把触摸信息使用SPI接口输出到STM32等控制器,在触摸屏章节将会详细讲解其检测原理。

液晶屏接口

液晶屏接口 表示的是PCB底板引出的排针线序,屏幕整体通过这些引出的排针与开发板或其它控制器连接。

开发板与屏幕的连接的信号说明

开发板与屏幕的连接的信号说明 是开发板上的液晶排母接口原理图, 它说明了配套屏幕接入到开发板上时的信号连接关系。标号1处的是液晶引出8080信号的16位数据线,它直接与STM32的整个GPIOB端口相连, 模拟时序时通过读写GPIO端口的数据寄存器来读写数据;标号2处的是液晶的复位及其余8080控制信号线, 这些信号线也是采用STM32的普通GPIO控制,程序驱动时将会控制这些引脚模拟出8080时序,特别地,液晶的复位引脚与STM32的NRST复位引脚, 这样的连接使得当STM32被复位的时候,同时也会触发使得液晶屏复位;标号3处的是触摸控制芯片XPT2046引出的SPI控制信号线。

以上原理图可查阅《3.2/2.8寸液晶原理图.pdf》及《MINI开发板原理图.pdf》文档获知,若您使用的液晶屏或实验板不一样,请根据实际连接的引脚修改程序。

27.5.2. 软件设计

为了使工程更加有条理,我们把LCD控制相关的代码独立分开存储,方便以后移植。在“USART—串口通讯”工程的基础上新建“bsp_ili9341_lcd.c”及“bsp_ili9341_lcd.h”文件, 这些文件也可根据您的喜好命名,它们不属于STM32标准库的内容,是由我们自己根据应用需要编写的。

27.5.2.1. 编程要点

(1) 初始化通讯使用的目标引脚及端口时钟;

(2) 编写软件模拟8080时序的读写液晶数据及发送命令的函数;

(3) 发送控制命令初始化液晶屏;

(4) 编写液晶屏的绘制像素点函数;

(5) 利用描点函数制作各种不同的液晶显示应用。

27.5.2.2. 代码分析

液晶LCD硬件相关宏定义

我们把控制液晶屏硬件相关8080接口的配置都以宏的形式定义到 “bsp_ili9341_lcd.h”文件中, 见 代码清单:液晶显示-1

代码清单:液晶显示-1硬件配置相关的宏(省略了部分数据线)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/************** ILI9341 显示屏8080通讯引脚定义 ****************/
/******控制信号线******/
//片选
#define      ILI9341_CS_CLK                RCC_APB2Periph_GPIOC
#define      ILI9341_CS_PORT               GPIOC
#define      ILI9341_CS_PIN                GPIO_Pin_4

//DC引脚
#define      ILI9341_DC_CLK                RCC_APB2Periph_GPIOC
#define      ILI9341_DC_PORT               GPIOC
#define      ILI9341_DC_PIN                GPIO_Pin_7

//写使能
#define      ILI9341_WR_CLK                RCC_APB2Periph_GPIOC
#define      ILI9341_WR_PORT               GPIOC
#define      ILI9341_WR_PIN                GPIO_Pin_6

//读使能
#define      ILI9341_RD_CLK                RCC_APB2Periph_GPIOC
#define      ILI9341_RD_PORT               GPIOC
#define      ILI9341_RD_PIN                GPIO_Pin_5

//复位引脚直接使用NRST,开发板复位的时候会使液晶复位


//背光引脚
#define      ILI9341_BK_CLK                RCC_APB2Periph_GPIOD
#define      ILI9341_BK_PORT               GPIOD
#define      ILI9341_BK_PIN                GPIO_Pin_2

/********数据信号线***************/
#define      ILI9341_DATA_CLK                RCC_APB2Periph_GPIOB
#define      ILI9341_DATA_PORT               GPIOB
#define      ILI9341_DATA_PIN                GPIO_Pin_All

/********信号线控制相关的宏***************/
#define ILI9341_CS_SET    ILI9341_CS_PORT->BSRR=ILI9341_CS_PIN   //片选端口
#define ILI9341_DC_SET    ILI9341_DC_PORT->BSRR=ILI9341_DC_PIN  //数据/命令
#define ILI9341_WR_SET    ILI9341_WR_PORT->BSRR=ILI9341_WR_PIN  //写数据
#define ILI9341_RD_SET    ILI9341_RD_PORT->BSRR=ILI9341_RD_PIN  //读数据

#define ILI9341_CS_CLR    ILI9341_CS_PORT->BRR=ILI9341_CS_PIN   //片选端口
#define ILI9341_DC_CLR    ILI9341_DC_PORT->BRR=ILI9341_DC_PIN  //数据/命令
#define ILI9341_WR_CLR    ILI9341_WR_PORT->BRR=ILI9341_WR_PIN     //写数据
#define ILI9341_RD_CLR    ILI9341_RD_PORT->BRR=ILI9341_RD_PIN     //读数据

//数据线输入输出
#define DATAOUT(x)  ILI9341_DATA_PORT->ODR=x; //数据输出
#define DATAIN      ILI9341_DATA_PORT->IDR;   //数据输入

以上代码根据硬件的连接,把与STM32与液晶屏通讯使用的引脚号都以宏封装起来。 对于CS/DC/WR/RD这些8080时序的控制引脚均定义了GPIO输出高电平和低电平的宏(ILI9341_xx_SET/CLR), 这些宏直接通过BSRR和BRR寄存器输出电平,方便模拟8080时序时使用; 而对于数据信号线的整个GPIOB端口也定义了DATAOUT和DATAIN宏用于控制整个端口输出或读取数据, 它通过向写入ODR或读取IDR寄存器实现这样的操作。

初始化 GPIO

利用上面定义引脚号的宏,编写模拟8080时序使用的GPIO引脚初始化函数, 见 代码清单:液晶显示-2

代码清单:液晶显示-2 模拟8080时序使用的GPIO初始化函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* @brief  初始化ILI9341的IO引脚
* @param  无
* @retval 无
*/
static void ILI9341_GPIO_Config ( void )
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* 使能复用IO时钟*/
    RCC_APB2PeriphClockCmd ( RCC_APB2Periph_AFIO, ENABLE );
    //开启SWD,失能JTAG (部分PB引脚用在了jtag接口,改成SWD接口就不会有干扰)
    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE);

    //复位引脚直接使用NRST,开发板复位的时候会使液晶复位

    /* 使能对应相应管脚时钟*/
    RCC_APB2PeriphClockCmd (
        /*控制信号*/
        ILI9341_CS_CLK|ILI9341_DC_CLK|ILI9341_WR_CLK|
        ILI9341_RD_CLK  |ILI9341_BK_CLK|
        /*数据信号*/
        ILI9341_DATA_CLK, ENABLE );

    /* 配置液晶相对应的数据线,PORT-D0~D15 */
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode =  GPIO_Mode_Out_PP;

    GPIO_InitStructure.GPIO_Pin = ILI9341_DATA_PIN;
    GPIO_Init ( ILI9341_DATA_PORT, &GPIO_InitStructure );

    /* 配置液晶相对应的控制线
    * 读        :LCD-RD
    * 写        :LCD-WR
    * 片选       :LCD-CS
    * 数据/命令  :LCD-DC
    */
    GPIO_InitStructure.GPIO_Pin = ILI9341_RD_PIN;
    GPIO_Init (ILI9341_RD_PORT, & GPIO_InitStructure );

    GPIO_InitStructure.GPIO_Pin = ILI9341_WR_PIN;
    GPIO_Init (ILI9341_WR_PORT, & GPIO_InitStructure );

    GPIO_InitStructure.GPIO_Pin = ILI9341_CS_PIN;
    GPIO_Init ( ILI9341_CS_PORT, & GPIO_InitStructure );

    GPIO_InitStructure.GPIO_Pin = ILI9341_DC_PIN;
    GPIO_Init ( ILI9341_DC_PORT, & GPIO_InitStructure );

    /* 配置LCD背光控制管脚BK*/
    GPIO_InitStructure.GPIO_Pin = ILI9341_BK_PIN;
    GPIO_Init ( ILI9341_BK_PORT, &GPIO_InitStructure );
}

在模拟8080时序的引脚中,只有数据引脚在读取液晶屏数据的时候才需要用作输入模式,其余时间均用作输出模式向液晶屏发送信号, 所以这段程序把所有引脚都被初始化为普通推挽输出模式, 在需要读取液晶数据的时候再用专门的函数把数据引脚更改为输入模式。

在这段初始化代码中,最值得注意的是开头的第11~13行代码,这两行代码分别使能了AFIO时钟, 以及调用重映射函数GPIO_PinRemapConfig通过输入宏参数GPIO_Remap_SWJ_JTAGDisable禁止了JTAG功能。 见 具有JTAG功能的引脚 , 由于用作数据端口中的PB3、PB4上电后的默认功能是下载接口JTAG的JTDO以及NJTRST,不能用于GPIO数据输入输出。 而又因为使用SWD下载方式不需要这两个信号也能下载程序,所以我们的初始化代码禁止JTAG,仅保留SWD下载方式, 而调用GPIO_PinRemapConfig函数时,又必须使能AFIO时钟,于是有了上述代码。

具有JTAG功能的引脚

因为程序禁止了JTAG功能,所以在使用下载器下载程序时,应把下载接口选择成SWD模式,见图 配置下载器使用SWD下载方式 , 否则下载程序时,它会提示找不到设备。非要使用JTAG下载方式的话,可以把板子的boot0引脚接到3.3V再下载,或者按着板子的复位键, 点击了MDK的下载按钮,有信息提示时再释放复位键,这两种方法的原理都是不让STM32运行程序,这样就不会禁止JTAG接口了。 一般支持JTAG接口的下载器都支持SWD模式,建议还是直接采用SWD模式方便多了,而且这样的设计能在资源紧张的芯片上省下很多引脚。

配置下载器使用SWD下载方式

模拟8080时序向液晶屏发送命令

初始化好信号引脚的模式后,即可直接模拟8080时序, 见 代码清单:液晶显示-3

代码清单:液晶显示-3模拟8080时序向液晶屏发送命令
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
* @brief  向ILI9341写入命令
* @param  usCmd :要写入的命令(表寄存器地址)
* @retval 无
*/
__inline void ILI9341_Write_Cmd ( uint16_t usCmd )
{
    ILI9341_CS_CLR;//开始片选
    ILI9341_DC_CLR;//写命令
    ILI9341_RD_SET;//禁止读
    DATAOUT(usCmd);//输出命令
    ILI9341_WR_CLR;//写入开始
    ILI9341_WR_SET;//写入结束
    ILI9341_CS_SET;//结束片选
}

这个ILI9341_Write_Cmd函数实现了前面介绍的图 使用18条数据线的8080接口写命令时序 的第1部分, 这部分时序向液晶屏发送命令(即图中的Command Address),函数接受一个输入参数usCmd作为将要发送的命令。 在发送命令时,调用控制CS、DC、RD及WR等信号线相应的宏, 模拟出图 27‑14中的信号,即在CS、DC信号为低电平、RD信号为高电平时, 数据信号线使用DATAOUT宏控制STM32的整个端口输出usCmd表示的电平信号, 准备好要发送的命令,准备好后把WR信号线设置成低电平,再切换为高电平, 最后把片选CS线拉高,结束本次通讯,完成了向液晶屏写入命令号的过程。

这个函数定义使用了“__inline”内联函数关键字修饰,可以把它的作用理解成“#define”一样的宏,程序编译时会直接在调用到这个函数的地方加入内联函数中的代码内容, 这种特性可以节省函数调用时的消耗。在此处把ILI9341_Write_Cmd定义成内联函数就是希望程序能更快地执行。

使用18条数据线的8080接口写命令时序

模拟8080时序向液晶屏发送数据

类似地,模拟8080时序向液晶屏发送数据的过程见 代码清单:液晶显示-4

代码清单:液晶显示-4模拟8080时序向液晶屏发送数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
* @brief  向ILI9341写入数据
* @param  usData :要写入的数据
* @retval 无
*/
__inline void ILI9341_Write_Data ( uint16_t usData )
{
    ILI9341_CS_CLR;//开始片选
    ILI9341_DC_SET;//写数据
    ILI9341_RD_SET;//禁止读
    DATAOUT(usData);//输出数据
    ILI9341_WR_CLR;//写入开始
    ILI9341_WR_SET;//写入结束
    ILI9341_CS_SET;//结束片选
}

这个ILI9341_Write_Data函数实现了图 ILI9341时序参数说明图 使用8080时序中的第2部分, 这部分时序向液晶屏发送参数数据(即图中的Command Data),函数接受一个输入参数usData作为将要发送的数据。 这个函数与写命令函数的唯一区别是使用DATAOUT输出参数值前,把DC信号线设置为高电平(前面发送命令时DC信号线为低电平), 当DC信号线为低电平时,液晶控制器会把数据信号线接收到的值理解为参数,在某些命令中,命令的参数就是要显示的RGB565像素数据,参数功能视具体的命令而定。

模拟8080时序从液晶屏接收数据

从液晶屏接收数据是一个相反的过程,见 代码清单:液晶显示-5

代码清单:液晶显示-5模拟8080时序从液晶屏接收数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @brief  从ILI9341读取数据
* @param  无
* @retval 读取到的数据
*/
__inline uint16_t ILI9341_Read_Data ( void )
{
    uint16_t data;
    ILI9341_DATA_PORT->CRL=0X88888888; //上拉输入
    ILI9341_DATA_PORT->CRH=0X88888888; //上拉输入
    ILI9341_DATA_PORT->ODR=0X0000;     //全部输出0

    ILI9341_DC_SET;
    ILI9341_WR_SET;

    ILI9341_CS_CLR;
    //读取数据
    ILI9341_RD_CLR;

    data=DATAIN;
    ILI9341_RD_SET;
    ILI9341_CS_SET;

    ILI9341_DATA_PORT->CRL=0X33333333; // 上拉输出
    ILI9341_DATA_PORT->CRH=0X33333333; // 上拉输出
    ILI9341_DATA_PORT->ODR=0XFFFF;    //全部输出高
    return data;
}

这个ILI9341_Read_Data函数实现了图 使用8080时序从液晶屏接收数据 中阴影表示的8080读数据部分, 它与写数据函数的主要区别是RD、WR信号线以及数据信号线的方向。

注意在STM32F1系列产品中,当GPIO引脚工作于开漏输出模式,读取输入数据寄存器IDR的值时,IDR表示的值就是GPIO引脚当前的实际电平(可参考模拟I2C例程); 而当GPIO引脚工作于本例子中的推挽输出模式时,读取输入IDR的值并不能获取到引脚的实际电平。所以在本例子中,读取数据引脚的电平时, 必须把GPIO从推挽输出模式切换到浮空输入模式(开漏输出模式也是可以的),读取数据完毕后再切换回控制液晶屏大部分时间使用的推挽输出模式, 这也就是函数中第9~11行及第24~26行代码的作用。

另一种解决方案是无论输入还是输出都统一使用GPIO的开漏输出模式,这样就不需要切换了,但使用开漏输出模式需要有外部上拉电阻的支持(跟I2C的上拉电阻类似), 否则无法正确表示高电平,只能表示高阻态。本应用中的液晶屏信号线均不带上拉电阻,所以只能在输出时使用推挽模式,而在需要读取数据时切换回输入模式。

CS、RD为低电平,WR、DC为高电平时表示读数据,液晶屏接收到这样的信号时,它会通过16根数据信号线输出参数, 这时再通过宏DATAIN读取STM32整个GPIO端口的输入数据寄存器的值赋予给data变量,在函数结束时返回。

一般来说,ILI9341控制器是在收到命令后才会输出数据,如查询控制器ID的命令或读取液晶屏像素值时。当控制器接收到命令后, 它通过8080时序输出的第1个是无效数据,即图中的“Data(invalid)”表示的内容,从第2个数据开始才是有效的输出,即图中的“Data(valid)”。

使用8080时序从液晶屏接收数据

读取液晶控制器的ID

在程序开发时,根据理论编写了模拟8080时序发送命令和收发数据的函数后,并未验证它们是否运行正常,通常的验证方法是向读取目标芯片的ID。 例如,根据ILI9341的数据手册,向它发送0xD3命令时,它会返回0x009341的数据(返回的第1个参数为无效数据), 见图 ILI9341控制器的读取ID命令 ,通过校验返回值,即可确认我们编写的8080时序是否正常。

ILI9341控制器的读取ID命令

根据该命令说明,编写的读取ID的函数见 代码清单:液晶显示-6

代码清单:液晶显示-6读取ID
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
* @brief  ILI9341读取芯片ID函数,可用于测试底层的读写函数
* @param  无
* @retval 正常时返回值为 0x9341
*/
uint16_t ILI9341_Read_ID(void)
{
    uint16_t id = 0;

    ILI9341_Write_Cmd(0xD3);
    ILI9341_Read_Data();
    ILI9341_Read_Data();
    id = ILI9341_Read_Data();
    id<<=8;
    id|=ILI9341_Read_Data();

    return id;
}

这段代码,先调用了ILI9341_Write_Cmd函数向液晶屏发送0xD3读取ID命令,然后连续调用4次ILI9341_Read_Data读取液晶屏返回的参数, 其中最后2次读取到的值被存储到变量id中,最后以函数返回值的形式返回数据。

确认硬件连接,然后初始化好控制液晶的GPIO,调用本函数,检查其返回值是否等于0x9341即可确认模拟8080时序的ILI9341_Write_Cmd和ILI9341_Read_Data函数是否正常, 由于本函数没有调用向液晶发送数据的ILI9341_Write_Data函数,所以有必要时还要用其它方式检查。

若确认底层模拟时序的函数有问题,按照调试经验,因为STM32运行速度通常高于外部设备,所以一般需要在各个信号电平切换或维持期间加入延时, 使有足够的时间让外部设备对信号进行采样。这样的调试方式不仅适用于本例子中的模拟8080时序,同样适用于模拟I2C、SPI等应用中。

当然,最标准的调试方式是使用逻辑分析仪严格地检测程序产生的模拟信号并根据外部设备的精确要求来进行调试, 如图 ILI9341时序参数说明图 及图 ILI9341的时序参数要求 中的ILI9341时序参数说明可以查询到详细的时间说明, 各个信号需要维持的最小时间以及不能超过的最大时间都有给出。

ILI9341时序参数说明图 ILI9341的时序参数要求

本工程中模拟8080时序的函数经过测试表明它们完全可以正常工作, 这是ILI9341的工作速度也比较快的原因。为了加快液晶数据的读写,本例子中就不添加不必要的延时了。

向液晶屏写入初始化配置

利用上面的发送命令及数据操作,可以向液晶屏写入一些初始化配置,见 代码清单:液晶显示-7

代码清单:液晶显示-7 向液晶屏写入初始化配置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @brief  初始化ILI9341寄存器
* @param  无
* @retval 无
*/
static void ILI9341_REG_Config ( void )
{
    /*  Power control B (CFh)  */
    DEBUG_DELAY  ();
    ILI9341_Write_Cmd ( 0xCF  );
    ILI9341_Write_Data ( 0x00  );
    ILI9341_Write_Data ( 0x81  );
    ILI9341_Write_Data ( 0x30  );

    /*  Power on sequence control (EDh) */
    DEBUG_DELAY ();
    ILI9341_Write_Cmd ( 0xED );
    ILI9341_Write_Data ( 0x64 );
    ILI9341_Write_Data ( 0x03 );
    ILI9341_Write_Data ( 0x12 );
    ILI9341_Write_Data ( 0x81 );

    /*以下省略大量配置内容*/

}

以上列出的代码中省略了大量的配置内容,本质上它们都是使用ILI9341_Write_Cmd发送代码,然后使用ILI9341_Write_Data函数发送命令对应的参数对液晶屏进行配置。

这个初始化过程中发送的代码及参数主要是配置了液晶屏的上电过程、显示屏的伽玛参数、分辨率、像素格式等内容, 这些配置主要由液晶屏生产厂家提供,本教程后面只针对常用命令进行讲解,此处不作详细说明,关于命令及参数可以查询《ILI9341数据手册》获知, 在该文档中搜索命令代码即可方便定位到相应的说明。例如,要查找代码中的0xCF命令说明,在文档中搜索“CFh”即可,见图 CFh命令的部分说明

CFh命令的部分说明

设置液晶显示窗口

根据液晶屏的要求,在发送显示数据前,需要先设置显示窗口确定后面发送的像素数据的显示区域, 见 代码清单:液晶显示-8

代码清单:液晶显示-8 设置液晶显示窗口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/********** ILI934 命令 ********************************/
#define      CMD_SetCoordinateX           0x2A       //设置X坐标
#define      CMD_SetCoordinateY           0x2B       //设置Y坐标

/**
* @brief  在ILI9341显示器上开辟一个窗口
* @param  usX :在特定扫描方向下窗口的起点X坐标
* @param  usY :在特定扫描方向下窗口的起点Y坐标
* @param  usWidth :窗口的宽度
* @param  usHeight :窗口的高度
* @retval 无
*/
void ILI9341_OpenWindow ( uint16_t usX, uint16_t usY,
uint16_t usWidth, uint16_t usHeight )
{
    ILI9341_Write_Cmd ( CMD_SetCoordinateX );          /* 设置X坐标 */
    ILI9341_Write_Data ( usX >> 8  );  /* 先高8位,然后低8位 */
    ILI9341_Write_Data ( usX & 0xff  );  /* 设置起始点和结束点*/
    ILI9341_Write_Data ( ( usX + usWidth - 1 ) >> 8  );
    ILI9341_Write_Data ( ( usX + usWidth - 1 ) & 0xff  );

    ILI9341_Write_Cmd ( CMD_SetCoordinateY );            /* 设置Y坐标*/
    ILI9341_Write_Data ( usY >> 8  );
    ILI9341_Write_Data ( usY & 0xff  );
    ILI9341_Write_Data ( ( usY + usHeight - 1 ) >> 8 );
    ILI9341_Write_Data ( ( usY + usHeight - 1) & 0xff );
}
设置显示窗口的X坐标 设置液晶显示窗口的Y坐标

代码中定义的ILI9341_OpenWindow函数实现了图 设置显示窗口的X坐标 及图 设置液晶显示窗口的Y坐标 的0x2A和0x2B命令, 它们分别用于设置显示窗口的起始及结束的X坐标和Y坐标,每个命令后包含4个8位的参数,这些参数组合后成起始坐标和结束坐标各1个用16位表示的值。

ILI9341_OpenWindow把它的四个函数输入参数X、Y起始坐标,宽度、高度转化成命令参数的格式,写入到液晶屏中,从而设置出一个显示窗口。

发送像素数据

调用上面的ILI9341_OpenWindow函数设置显示窗口后,再向液晶屏发送像素数据时,这些数据就会直接显示在它设定的窗口位置中。 发送像素数据的操作见 代码清单:液晶显示-9

代码清单:液晶显示-9 发送像素数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#define      CMD_SetPixel                 0x2C       //填充像素

/**
* @brief  在ILI9341显示器上以某一颜色填充像素点
* @param  ulAmout_Point :要填充颜色的像素点的总数目
* @param  usColor :颜色
* @retval 无
*/
static __inline void ILI9341_FillColor ( uint32_t ulAmout_Point,
uint16_t usColor )
{
    uint32_t i = 0;

    /* memory write */
    ILI9341_Write_Cmd ( CMD_SetPixel );

    for ( i = 0; i < ulAmout_Point; i ++ )
        ILI9341_Write_Data ( usColor );
}
发送像素数据

发送像素数据的命令非常简单,首先发送命令代码0x2C,然后后面紧跟着要传输的像素数据即可。按照本液晶屏的配置, 像素点的格式为RGB565,所以像素数据就是要显示的RGB565格式的颜色值。

本ILI9341_FillColor函数包含两个输入参数,分别用于设置要发送的像素数据个数ulAmout_Point及像素点的颜色值usColor, 在代码实现中它调用ILI9341_Write_Cmd发送一次命令代码,接着使用for循环调用ILI9341_Write_Data写入ulAmout_Po int个同样的颜色值。

这些颜色值会按顺序填充到前面使用ILI9341_OpenWindow函数设置的显示窗口中,例如,若设置了一个usX=10,usY=30,usWidth=50, usHeight=20的窗口,然后再连续填充50*20个颜色值为0XFFFF的像素数据,即可在(10,30)的起始坐标处显示一个宽50像素高20像素的白色矩形。

绘制单个像素点

利用前面的ILI9341_OpenWindow和ILI9341_FillColor函数,可以正式开始控制液晶屏绘制特定的图像, 而所有图像都是由多个像素点组成的,单个像素点的绘制函数见 代码清单:液晶显示-10

代码清单:液晶显示-10 绘制单个像素点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static uint16_t CurrentTextColor   = BLACK;//前景色
static uint16_t CurrentBackColor   = WHITE;//背景色

/**
* @brief  设定ILI9341的光标坐标
* @param  usX :在特定扫描方向下光标的X坐标
* @param  usY :在特定扫描方向下光标的Y坐标
* @retval 无
*/
static void ILI9341_SetCursor ( uint16_t usX, uint16_t usY )
{
    ILI9341_OpenWindow ( usX, usY, 1, 1 );
}


/**
* @brief  对ILI9341显示器的某一点以某种颜色进行填充
* @param  usX :在特定扫描方向下该点的X坐标
* @param  usY :在特定扫描方向下该点的Y坐标
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_SetPointPixel ( uint16_t usX, uint16_t usY )
{
    if ( ( usX < LCD_X_LENGTH ) && ( usY < LCD_Y_LENGTH ) ) {
        ILI9341_SetCursor ( usX, usY );

        ILI9341_FillColor ( 1, CurrentTextColor );
    }
}

ILI9341_SetPointPixel函数直接调用了ILI9341_SetCursor(实质上是ILI9341_OpenWindow函数的封装)设置单个像素点的绘制窗口, 然后调用ILI9341_FillColor填充单个像素点,而像素点的颜色由全局变量CurrentTextColor表示。

利用这个ILI9341_SetPointPixel函数,可以向液晶屏指定的XY坐标描绘单个像素点。

绘制矩形

类似地,使用ILI9341_OpenWindow和ILI9341_FillColor制作的绘制矩形操作见 代码清单:液晶显示-11

代码清单:液晶显示-11 绘制矩形
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @brief  在 ILI9341 显示器上画一个矩形
* @param  usX_Start :在特定扫描方向下矩形的起始点X坐标
* @param  usY_Start :在特定扫描方向下矩形的起始点Y坐标
* @param  usWidth:矩形的宽度(单位:像素)
* @param  usHeight:矩形的高度(单位:像素)
* @param  ucFilled :选择是否填充该矩形
*   该参数为以下值之一:
*     @arg 0 :空心矩形
*     @arg 1 :实心矩形
* @note 可使用LCD_SetBackColor、LCD_SetTextColor、LCD_SetColors函数设置颜色
* @retval 无
*/
void ILI9341_DrawRectangle ( uint16_t usX_Start, uint16_t usY_Start,
                uint16_t usWidth, uint16_t usHeight, uint8_t ucFilled )
{
    if ( ucFilled ) {
        ILI9341_OpenWindow ( usX_Start, usY_Start, usWidth, usHeight );
        ILI9341_FillColor ( usWidth * usHeight ,CurrentTextColor);
    } else {
        ILI9341_DrawLine ( usX_Start, usY_Start,
                        usX_Start + usWidth - 1, usY_Start );
        ILI9341_DrawLine ( usX_Start, usY_Start + usHeight - 1,
                    usX_Start + usWidth - 1, usY_Start + usHeight - 1 );
        ILI9341_DrawLine ( usX_Start, usY_Start, usX_Start,
                            usY_Start + usHeight - 1 );
        ILI9341_DrawLine ( usX_Start + usWidth - 1, usY_Start,
                    usX_Start + usWidth - 1, usY_Start + usHeight - 1 );
    }
}

ILI9341_DrawRectangle函数分成两部分,它根据输入参数ucFilled是否为真值决定绘制的是实心矩形还是只有边框的矩形。 绘制实心矩形时,直接使用ILI9341_OpenWindow函数根据输入参数设置显示矩形窗口,然后根据实心矩形的像素点个数调用ILI9341_FillColor即可完成绘制; 而绘制空心矩形时,实质上是绘制四条边框线,它调用ILI9341_DrawLine函数绘制,ILI9341_DrawLine函数的输入参数是用于表示直接的两个坐标点(x1,y1)与(x2,y2), 该函数内部根据数据关系,使用这两个点确定一条直线,最后调用ILI9341_SetPointPixel函数一点一点地绘制成完整的直线。

关于ILI9341_DrawLine画线函数、ILI9341_DrawCircle画圆函数等代码不再讲解,它们都是根据数学关系在特点的位置显示坐标点而已。 另外关于工程中的显示字符串的原理将在下一个章节详细说明。

设置液晶的扫描方向

控制液晶屏时,还有一个非常重要的参数,就是设置液晶屏的扫描方向,见 代码清单:液晶显示-12

代码清单:液晶显示-12 设置液晶的扫描方向
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
#define       ILI9341_LESS_PIXEL        240     //液晶屏较短方向的像素宽度
#define       ILI9341_MORE_PIXEL      320     //液晶屏较长方向的像素宽度

//根据液晶扫描方向而变化的XY像素宽度
//调用ILI9341_GramScan函数设置方向时会自动更改
uint16_t LCD_X_LENGTH = ILI9341_LESS_PIXEL;
uint16_t LCD_Y_LENGTH = ILI9341_MORE_PIXEL;

//液晶屏扫描模式,本变量主要用于方便选择触摸屏的计算参数
//参数可选值为0-7
//调用ILI9341_GramScan函数设置方向时会自动更改
//LCD刚初始化完成时会使用本默认值
uint8_t LCD_SCAN_MODE = 6;

/**
* @brief  设置ILI9341的GRAM的扫描方向
* @param  ucOption :选择GRAM的扫描方向
*     @arg 0-7 :参数可选值为0-7这八个方向
*
*  !!!其中0、3、5、6 模式适合从左至右显示文字,
*        不推荐使用其它模式显示文字 其它模式显示文字会有镜像效果
*
*  其中0、2、4、6 模式的X方向像素为240,Y方向像素为320
*  其中1、3、5、7 模式下X方向像素为320,Y方向像素为240
*
*  其中 6 模式为大部分液晶例程的默认显示方向
*  其中 3 模式为摄像头例程使用的方向
*  其中 0 模式为BMP图片显示例程使用的方向
*
* @retval 无
* @note  坐标图例:A表示向上,V表示向下,<表示向左,>表示向右
        X表示X轴,Y表示Y轴

------------------------------------------------------------
模式0:        .   模式1:    . 模式2:      . 模式3:
        A   .         A   .   A         .   A
        |   .         |   .   |         .   |
        Y   .         X   .   Y         .   X
        0   .         1   .   2         .   3
<--- X0 o   . <----Y1 o   .   o 2X--->  .   o 3Y--->
------------------------------------------------------------
模式4:        . 模式5:      . 模式6:      . 模式7:
<--- X4 o   . <--- Y5 o   .   o 6X--->  .   o 7Y--->
        4   .         5   .   6         .   7
        Y   .         X   .   Y         .   X
        |   .         |   .   |         .   |
        V   .         V   .   V         .   V
---------------------------------------------------------
                    LCD屏示例
                |-----------------|
                |     野火Logo    |
                |                 |
                |                 |
                |                 |
                |                 |
                |                 |
                |                 |
                |                 |
                |                 |
                |-----------------|
                屏幕正面(宽240,高320)

*******************************************************/
void ILI9341_GramScan ( uint8_t ucOption )
{
    //参数检查,只可输入0-7
    if (ucOption >7 )
        return;

    //根据模式更新LCD_SCAN_MODE的值,主要用于触摸屏选择计算参数
    LCD_SCAN_MODE = ucOption;

    //根据模式更新XY方向的像素宽度
    if (ucOption%2 == 0) {
        //0 2 4 6模式下X方向像素宽度为240,Y方向为320
        LCD_X_LENGTH = ILI9341_LESS_PIXEL;
        LCD_Y_LENGTH =  ILI9341_MORE_PIXEL;
    } else {
        //1 3 5 7模式下X方向像素宽度为320,Y方向为240
        LCD_X_LENGTH = ILI9341_MORE_PIXEL;
        LCD_Y_LENGTH =  ILI9341_LESS_PIXEL;
    }

    //0x36命令参数的高3位可用于设置GRAM扫描方向
    ILI9341_Write_Cmd ( 0x36 );
    ILI9341_Write_Data ( 0x08 |(ucOption<<5));//根据ucOption的值设置LCD参数,共0-7种模式
    ILI9341_Write_Cmd ( CMD_SetCoordinateX );
    ILI9341_Write_Data ( 0x00 );    /* x 起始坐标高8位 */
    ILI9341_Write_Data ( 0x00 );    /* x 起始坐标低8位 */
    ILI9341_Write_Data ( ((LCD_X_LENGTH-1)>>8)&0xFF );/* x 结束坐标高8位 */
    ILI9341_Write_Data ( (LCD_X_LENGTH-1)&0xFF );    /* x 结束坐标低8位 */

    ILI9341_Write_Cmd ( CMD_SetCoordinateY );
    ILI9341_Write_Data ( 0x00 );    /* y 起始坐标高8位 */
    ILI9341_Write_Data ( 0x00 );    /* y 起始坐标低8位 */
    ILI9341_Write_Data ( ((LCD_Y_LENGTH-1)>>8)&0xFF );/*y 结束坐标高8位 */
    ILI9341_Write_Data ( (LCD_Y_LENGTH-1)&0xFF );    /*y 结束坐标低8位 */

    /* write gram start */
    ILI9341_Write_Cmd ( CMD_SetPixel );
}

当设置了液晶显示窗口,再连续向液晶屏写入像素点时,它会一个点一个点地往液晶屏的X方向填充, 填充完一行X方向的像素点后,向Y方向下移一行,X坐标回到起始位置,再往X方向一个点一个点地填充,如此循环直至填充完整个显示窗口。

而屏幕的坐标原点和XY方向都可以根据实际需要使用0X36命令来配置的, 该命令的说明见 图 液晶扫描模式命令

液晶扫描模式命令

0X36命令参数中的MY、MX、MV这三个数据位用于配置扫描方向,因此一共有23 = 8种模式。 ILI9341_GramScan函数就是根据输入的模式设置这三个数据位,并且根据相应的模式更改XY方向的分辨率LCD_X_LENGTH和LCD_Y_LENGTH, 使得其它函数可以利用这两个全局变量获屏幕实际的XY方向分辨率信息;同时, 函数内还设置了全局变量LCD_SCAN_MODE的值用于记录当前的屏幕扫描模式,这在后面计算触摸屏坐标的时候会使用到。 设置完扫描方向后,代码中还调用设置液晶显示窗口的命令CMD_SetCoordinateX/Y(0X2A/0X2B命令)默认打开一个与屏幕大小一致的显示窗口,方便后续的显示操作。

调用ILI9341_GramScan函数设置0-7模式时,各个模式的坐标原点及XY方向如图 液晶屏的8种扫描模式 所示。

液晶屏的8种扫描模式

其中模式6最符合我们的阅读习惯,扫描方向与文字方向一致,都是从左到右,从上到下,所以本开发板中的大部分液晶程序都是默认使用模式6。

其实模式0、3、5、6的液晶扫描方向都与文字方向一致,比较适合显示文字,只要适当旋转屏幕即可,使得用屏幕四个边沿作为正面看去都有适合的文字显示模式。 而其它模式由于扫描方向与文字方向不一致,要想实现同样的效果非常麻烦,也没有实现的必要。

液晶屏全局初始化

利用前面介绍的各种函数,我们把它封装成ILI9341_Init函数,见 代码清单:液晶显示-13

代码清单:液晶显示-13 液晶屏全局初始化函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//液晶屏扫描模式,本变量主要用于方便选择触摸屏的计算参数
//参数可选值为0-7
//调用ILI9341_GramScan函数设置方向时会自动更改
//LCD刚初始化完成时会使用本默认值
uint8_t LCD_SCAN_MODE = 6;

/**
* @brief  ILI9341初始化函数,如果要用到lcd,一定要调用这个函数
* @param  无
* @retval 无
*/
void ILI9341_Init ( void )
{
    ILI9341_GPIO_Config ();
    ILI9341_FSMC_Config ();

    ILI9341_BackLed_Control ( ENABLE );      //点亮LCD背光灯
    ILI9341_Rst ();//复位液晶屏
    ILI9341_REG_Config ();//写入寄存器配置

    //设置默认扫描方向,其中 6 模式为大部分液晶例程的默认显示方向
    ILI9341_GramScan(LCD_SCAN_MODE);
}

本函数初始化GPIO、FSMC外设,然后开启液晶屏的背光,复位液晶屏,并且写入基本的液晶屏配置, 最后调用ILI9341_GramScan函数设置默认的液晶扫描方向。在需要使用液晶屏的时候,直接调用本函数即可完成初始化。

27.5.2.3. 基本液晶显示例程的main函数

本章内容中配套了两个工程进行演示,它们的液晶驱动完全一样,仅是main函数里的应用层展示不同效果时稍有区别, 先讲解基本的《液晶显示》例程,其main函数内容见 代码清单:液晶显示-14

代码清单:液晶显示-14 main函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main ( void )
{
    ILI9341_Init ();         //LCD 初始化

    USART_Config();

    printf("\r\n ********** 液晶屏英文显示程序*********** \r\n");
    printf("\r\n 本程序不支持中文,显示中文的程序请学习下一章 \r\n");

//其中0、3、5、6 模式适合从左至右显示文字,
//不推荐使用其它模式显示文字 其它模式显示文字会有镜像效果
//其中 6 模式为大部分液晶例程的默认显示方向
    ILI9341_GramScan ( 6 );
    while ( 1 ) {
        LCD_Test();
    }
}

程序中,调用了ILI9341_Init函数初始化液晶屏,然后再初始化串口。(在实际测试中,若先初始化串口再初始化液晶屏, 会导致错误,原因不明。所以在应用时,注意先初始化液晶屏再初始化串口)

初始化完成后,调用LCD_Test函数显示各种图形进行测试(如直线、矩形、圆形),见 代码清单:液晶显示-15。 具体内容请直接在工程中阅读源码。LCD_Test中还调用了文字显示函数,关于文字显示的原理在下一章再详细说明。

代码清单:液晶显示-15 液晶效果演示测试
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*用于测试各种液晶的函数*/
void LCD_Test(void)
{
    /*演示显示变量*/
    static uint8_t testCNT = 0;
    char dispBuff[100];

    testCNT++;

    LCD_SetFont(&Font8x16);
    LCD_SetColors(RED,BLACK);

    ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */
    /********显示字符串示例*******/
    ILI9341_DispStringLine_EN(LINE(0),"BH 3.2 inch LCD para:");
    ILI9341_DispStringLine_EN(LINE(1),"Image resolution:240x320 px");
    ILI9341_DispStringLine_EN(LINE(2),"ILI9341 LCD driver");
    ILI9341_DispStringLine_EN(LINE(3),"XPT2046 Touch Pad driver");

    /********显示变量示例*******/
    LCD_SetFont(&Font16x24);
    LCD_SetTextColor(GREEN);

    /*使用c标准库把变量转化成字符串*/
    sprintf(dispBuff,"Count : %d ",testCNT);
    LCD_ClearLine(LINE(4)); /* 清除单行文字 */

    /*然后显示该字符串即可,其它变量也是这样处理*/
    ILI9341_DispStringLine_EN(LINE(4),dispBuff);

    /*******显示图形示例******/
    LCD_SetFont(&Font24x32);
    /* 画直线 */

    LCD_ClearLine(LINE(4));/* 清除单行文字 */
    LCD_SetTextColor(BLUE);

    ILI9341_DispStringLine_EN(LINE(4),"Draw line:");

    LCD_SetTextColor(RED);
    ILI9341_DrawLine(50,170,210,230);
    ILI9341_DrawLine(50,200,210,240);
    /*省略部分内容*/

    ILI9341_Clear(0,16*8,LCD_X_LENGTH,LCD_Y_LENGTH-16*8); /* 清屏,显示全黑 */


    /*画矩形*/

    LCD_ClearLine(LINE(4)); /* 清除单行文字 */
    LCD_SetTextColor(BLUE);

    ILI9341_DispStringLine_EN(LINE(4),"Draw Rect:");

    LCD_SetTextColor(RED);
    ILI9341_DrawRectangle(50,200,100,30,1);
    /*省略部分内容*/

    Delay(0xFFFFFF);

    ILI9341_Clear(0,16*8,LCD_X_LENGTH,LCD_Y_LENGTH-16*8); /* 清屏,显示全黑


    /* 画圆 */
    LCD_ClearLine(LINE(4)); /* 清除单行文字 */
    LCD_SetTextColor(BLUE);

    ILI9341_DispStringLine_EN(LINE(4),"Draw Cir:");

    LCD_SetTextColor(RED);
    ILI9341_DrawCircle(100,200,20,0);
    /*省略部分内容*/
    Delay(0xFFFFFF);

ILI9341_Clear(0,16*8,LCD_X_LENGTH,LCD_Y_LENGTH-16*8); /* 清屏,显示全黑 */

}

27.5.2.4. 液晶坐标方向演示的main函数

打开《液晶坐标方向演示》的例程,可看到其main函数与上一个工程有区别, 见 代码清单:液晶显示-16

代码清单:液晶显示-16 液晶坐标方向演示例程的main函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main ( void )
{

    ILI9341_Init ();         //LCD 初始化

    USART_Config();

    printf("\r\n ********** 液晶屏显示方向说明程序*********** \r\n");
    printf("\r\n 本程序不支持中文,显示中文的程序请学习下一章 \r\n");

    while ( 1 ) {
        //展示LCD的八种方向模式
        LCD_Direction_Show();
    }
}


/*用于展示LCD的八种方向模式*/
void LCD_Direction_Show(void)
{

    uint8_t i = 0;
    char dispBuff[100];

    //轮流展示各个方向模式
    for (i=0; i<8; i++) {
        LCD_SetFont(&Font16x24);
        LCD_SetColors(RED,BLACK);

        ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH); /* 清屏,显示全黑 */

        //其中0、3、5、6 模式适合从左至右显示文字,
        //不推荐使用其它模式显示文字 其它模式显示文字会有镜像效果
        //其中 6 模式为大部分液晶例程的默认显示方向
        ILI9341_GramScan ( i );

        sprintf(dispBuff,"o%d. X --->",i);
        ILI9341_DispStringLine_EN(LINE(0),dispBuff);//沿X方向显示文字

        sprintf(dispBuff,"o%d.Y|V",i);
        ILI9341_DispString_EN_YDir(0,0,dispBuff);//沿Y方向显示文字

        Delay(0xFFFFFF);

        //显示测试
        // *  !!!其中0、3、5、6模式适合从左至右显示文字,不推荐使用其它模式显示文字
        //其它模式显示文字会有镜像效果
        LCD_Test();
    }

}

本工程的main了函数中主要调用了LCD_Direction_Show函数,该函数主要添加了液晶屏在不同扫描模式下的显示效果演示, 请直接观看程序的演示效果,了解液晶屏的各个扫描模式。注意:其中部分模式显示文字时会因为镜像效果导致无法阅读, 这是由扫描模式决定的,并不是代码错误,只要使用适当的模式即可实现正常的文字显示效果。

27.5.3. 下载验证

用USB线连接开发板,编译程序下载到实验板,并上电复位,液晶屏会显示各种内容。

27.6. 补充说明

因为芯片供应等原因,屏幕实际使用芯片请以开发板、模块资料中的说明和实物为准。

对于不同的MCU屏幕芯片以上的教程流程都适用,一般差异在于配置参数不同,同型号芯片也可能因为厂商不同的批次需要调整部分配置参数。

现程序兼体做法:

LCD初始化函数 ILI9341_REG_Config() 里先 用ILI9341_ReadID() 读取LCD控制芯片的 ID, 并使用一个变量 lcdid 来保存它,然后判断对比此时实际屏幕的型号从而执行不同的配置参数过程,配置参数一般也是由屏幕厂商提供,有需要时再微调。

另外有一个比较重要的函数是 ILI9341_GramScan(),在这个函数里面我们也根据变量 lcdid 做了不同的配置,比如配置LCD显示的颜色模式。

例程中的LCD函数仍以 ILI9341 为前缀命名。