27. LTDC/DMA2D—液晶显示

本章参考资料:《STM32F4xx参考手册2》、《STM32F4xx规格书》、库帮助文档《stm32f4xx_dsp_stdperiph_lib_um.chm》。

关于开发板配套的液晶屏参数可查阅《5.0寸液晶屏数据手册》配套资料获知。

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底板 ,只有小部分的电源管理电路,液晶面板的信号线与外部微控制器相连,直接控制。STM32F429系列的芯片不需要额外的液晶控制器, 也就是说它把专用液晶控制器的功能集成到STM32F429芯片内部了,节约了额外的控制器成本。

27.2.1. 液晶面板的控制信号

本章我们主要讲解控制液晶面板(不带控制器),液晶面板的控制信号线见表 液晶面板的信号线

液晶面板的信号线

(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. 显存

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

27.3. LTDC液晶控制器简介

STM32F429系列芯片内部自带一个LTDC液晶控制器,使用SDRAM的部分空间作为显存,可直接控制液晶面板,无需额外增加液晶控制器芯片。 STM32的LTDC液晶控制器最高支持800x600分辨率的屏幕;可支持多种颜色格式,包括RGB888、RGB565、ARGB8888和ARGB1555等(其中的“A”是指透明像素); 支持2层显示数据混合,利用这个特性,可高效地做出背景和前景分离的显示效果,如以视频为背景,在前景显示弹幕。

27.3.1. 图像数据混合

LTDC外设支持2层数据混合,混合前使用2层数据源,分别为前景层和背景层,见图 图像的分层与混合 。 在输出时,实际上液晶屏只能显示一层图像,所以LTDC在输出数据到液晶屏前需要把2层图像混合成一层,跟Photoshop软件的分层合成图片过程类似。 混合时,直接用前景层中的不透明像素替换相同位置的背景像素;而前景层中透明像素的位置,则使用背景的像素数据,即显示背景层的像素。

图像的分层与混合

如果想使用图像混合功能,前景层必须使用包含透明的像素格式,如ARGB1555或ARGB8888。其中ARGB1555使用1个数据位表示透明元素,它只能表示像素是透明或不透明, 当最高位(即“A”位)为1时,表示这是一个不透明的像素,具体颜色值为RGB位表示的颜色,而当最高位为0时,表示这是一个完全透明的像素,RGB位的数据无效; 而ARGB8888的像素格式使用8个数据位表示透明元素,它使用高8位表示“透明度”(即代表“A”的8个数据位),若A的值为“0xFF”,则表示这个像素完全不透明, 若A的值为“0x00”则表示这个像素完全透明,介于它们之间的值表示其RGB颜色不同程度的透明度,即混合后背景像 素根据这个值按比例来表示。

注意液晶屏本身是没有透明度概念的,如24位液晶屏的像素数据格式是RGB888,RGB颜色各有对应的8根数据线,不存在用于表示透明度的数据线, 所以实际上ARGB只是针对内部分层数据处理的格式,最终经过混合运算得出直接颜色数据RGB888才能交给液晶屏显示。

27.3.2. LTDC结构框图剖析

LTDC控制器框图 是LTDC控制器的结构框图,它主要包含信号线、图像处理单元、寄存器及时钟信号。

LTDC控制器框图

27.3.2.1. LTDC信号线

LTDC的控制信号线与液晶显示面板的数据线一一对应,包含有HSYNC、VSYNC、DE、CLK及RGB数据线各8根。设计硬件时把液晶面板与STM32对应的这些引脚连接起来即可, 查阅《STM32F4xx规格书》可知LTDC信号线对应的引脚,见表 LTDC引脚表

LTDC引脚表

27.3.2.2. 图像处理单元

LTDC框图标号表示的是图像处理单元,它通过“AHB接口”获取显存中的数据,然后按分层把数据分别发送到两个“层FIFO”缓存,每个FIFO可缓存64x32位的数据, 接着从缓存中获取数据交给“PFC”(像素格式转换器),它把数据从像素格式转换成字(ARGB8888)的格式,再经过“混合单元”把两层数据合并起来, 最终混合得到的是单层要显示的数据,通过信号线输出到液晶面板。这部分结构与DMA2D的很类似,我们在下一小节详细讲解。

在输出前混合单元的数据还经过一个“抖动单元”,它的作用是当像素数据格式的色深大于液晶面板实际色深时,对像素数据颜色进行舍入操作, 如向18位显示器上显示24位数据时,抖动单元把像素数据的低6位与阈值比较,若大于阈值,则向数据的第7位进1,否则直接舍掉低6位。

27.3.2.3. 配置和状态寄存器

框图中标号表示的是LTDC的控制逻辑,它包含了LTDC的各种配置和状态寄存器。如配置与液晶面板通讯时信号线的有效电平、各种时间参数、 有效数据宽度、像素格式及显存址等等,LTDC外设根据这些配置控制数据输出,使用AHB接口从显存地址中搬运数据到液晶面板。 还有一系列用于指示当前显示状态和位置的状态寄存器,通过读取这些寄存器可以了解LTDC的工作状态。

27.3.2.4. 时钟信号

LTDC外设使用3种时钟信号,包括AHB时钟、APB2时钟及像素时钟LCD_CLK。AHB时钟用于驱动数据从存储器存储到FIFO,APB2时钟用于驱动LTDC的寄存器。 而LCD_CLK用于生成与液晶面板通讯的同步时钟,见图 LCD_CLK时钟来源 ,它的来源是HSE(高速外部晶振),经过“/M”分频因子分频输出到“PLLSAI”分频器, 信号由“PLLSAI”中的倍频因子N倍频得到“PLLSAIN”时钟、然后由“/R”因子分频得到“PLLCDCLK”时钟,再经过“DIV”因子得到“LCD-TFT clock”, “LCD-TFTclock”即通讯中的同步时钟LCD_CLK,它使用LCD_CLK引脚输出。

LCD_CLK时钟来源

27.4. DMA2D图形加速器简介

在实际使用LTDC控制器控制液晶屏时,使LTDC正常工作后,往配置好的显存地址写入要显示的像素数据,LTDC就会把这些数据从显存搬运到液晶面板进行显示, 而显示数据的容量非常大,所以我们希望能用DMA来操作,针对这个需求,STM32专门定制了DMA2D外设,它可用于快速绘制矩形、直线、分层数据混合、 数据复制以及进行图像数据格式转换,可以把它理解为图形专用的DMA。

27.4.1. DMA2D结构框图剖析

DMA2D结构框图 是DMA2D的结构框图,它与前面LTDC结构里的图像处理单元很类似,主要为分层FIFO、PFC及彩色混合器。

DMA2D结构框图

27.4.1.1. FG FIFO与BG FIFO

FG FIFO(Foreground FIFO)与BG FIFO(Backgroun FIFO)是两个64x32位大小的缓冲区,它们用于缓存从AHB总线获取的像素数据,分别专用于缓冲前景层和背景层的数据源。

AHB总线的数据源一般是SDRAM,也就是说在LTDC外设中配置的前景层及背景层数据源地址一般指向SDRAM的存储空间,使用SDRAM的部分空间作为显存。

27.4.1.2. FG PFC与BG PFC

FG PFC(FG Pixel Format Convertor)与BG PFC(BG Pixel Format Convertor)是两个像素格式转换器,分别用于前景层和背景层的像素格式转换, 不管从FIFO的数据源格式如何,都把它转化成字的格式(即32位),ARGB8888。

图中的“ɑ”表示Alpha,即透明度,经过PFC,透明度会被扩展成8位的格式。

图中的“CLUT”表示颜色查找表(Color Lookup Table),颜色查找表是一种间接的颜色表示方式,它使用一个256x32位的空间缓存256种颜色, 颜色的格式是ARGB8888或RGB888。见图 使用颜色查找表显示图像的过程 ,利用颜色查找表,实际的图像只使用这256种颜色,而图像的每个像素使用8位的数据来表示, 该数据并不是直接的RGB颜色数据,而是指向颜色查找表的地址偏移,即表示这个像素点应该显示颜色查找表中的哪一种颜色。在图像大小不变的情况下, 利用颜色查找表可以扩展颜色显示的能力,其特点是用8位的数据表示了一个24或32位的颜色,但整个图像颜色的种类局限于颜色表中的256种。 DMA2D的颜色查找表可以由CPU自动加载或编程手动加载。

使用颜色查找表显示图像的过程

27.4.1.3. 混合器

FIFO中的数据源经过PFC像素格式转换器后,前景层和背景层的图像都输入到混合器中运算,运算公式见图 混合公式

混合公式

从公式可以了解到混合器的运算主要是使用前景和背景的透明度作为因子,对像素RGB颜色值进行加权运算。经过混合器后,两层数据合成为一层ARGB8888格式的图像。

27.4.1.4. OUT PFC

OUT PFC是输出像素格式转换器,它把混合器转换得到的图像转换成目标格式,如ARGB8888、RGB888、RGB565、ARGB1555或ARGB4444, 具体的格式可根据需要在输出PFC控制寄存器DMA2D_OPFCCR中选择。

STM32F429芯片使用LTDC、DMA2D及RAM存储器,构成了一个完整的液晶控制器。LTDC负责不断刷新液晶屏,DMA2D用于图像数据搬运、 混合及格式转换,RAM存储器作为显存。其中显存可以使用STM32芯片内部的SRAM或外扩SDRAM/SRAM,只要容量足够大即可(至少要能存储一帧图像数据)。

27.5. LTDC初始化结构体

控制LTDC涉及到非常多的寄存器,利用LTDC初始化结构体可以减轻开发和维护的工作量,LTDC初始化结构体见 代码清单:DMA2D-1

代码清单:DMA2D-1 LTDC初始化结构体LTDC_InitTypeDef
 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  LTDC Init structure definition
*/
typedef struct
{
    uint32_t LTDC_HSPolarity;             /*配置行同步信号HSYNC的极性 */
    uint32_t LTDC_VSPolarity;             /*配置垂直同步信号VSYNC的极性 */
    uint32_t LTDC_DEPolarity;             /*配置数据使能信号DE的极性*/
    uint32_t LTDC_PCPolarity;             /*配置像素时钟信号CLK的极性 */
    uint32_t LTDC_HorizontalSync;         /*配置行同步信号HSYNC的宽度(HSW-1) */

    uint32_t LTDC_VerticalSync;          /*配置垂直同步信号VSYNC的宽度(VSW-1) */

    uint32_t LTDC_AccumulatedHBP;         /*配置(HSW+HBP-1)的值*/
    uint32_t LTDC_AccumulatedVBP;         /*配置(VSW+VBP-1)的值*/
    uint32_t LTDC_AccumulatedActiveW;     /*配置(HSW+HBP+有效宽度-1)的值*/
    uint32_t LTDC_AccumulatedActiveH;     /*配置(VSW+VBP+有效高度-1)的值*/
    uint32_t LTDC_TotalWidth;             /*配置(HSW+HBP+有效宽度+HFP-1)的值 */

    uint32_t LTDC_TotalHeigh;             /*配置(VSW+VBP+有效高度+VFP-1)的值 */

    uint32_t LTDC_BackgroundRedValue;     /*配置背景的红色值*/
    uint32_t LTDC_BackgroundGreenValue;   /*配置背景的绿色值*/
    uint32_t LTDC_BackgroundBlueValue;   /*配置背景的蓝色值*/
} LTDC_InitTypeDef;

这个结构体大部分成员都是用于定义LTDC的时序参数的,包括信号有效电平及各种时间参数的宽度,配合“液晶数据传输时序”中的说明更易理解。 各个成员介绍如下,括号中的是STM32标准库定义的宏:

(1) LTDC_HSPolarity

本成员用于设置行同步信号HSYNC的极性,即HSYNC有效时的电平,该成员的值可设置为高电平(LTDC_HSPolarity_AH)或低电平(LTDC_HSPolarity_AL)。

(2) LTDC_VSPolarity

本成员用于设置垂直同步信号VSYNC的极性,可设置为高电平(LTDC_VSPolarity_AH)或低电平(LTDC_VSPolarity_AL)。

(3) LTDC_DEPolarity

本成员用于设置数据使能信号DE的极性,可设置为高电平(LTDC_DEPolarity_AH)或低电平(LTDC_DEPolarity_AL)。

(4) LTDC_PCPolarity

本成员用于设置像素时钟信号CLK的极性,可设置为上升沿(LTDC_PCPolarity_IPC)或下降沿(LTDC_PCPolarity_IIPC),表示RGB数据信号在CLK的哪个时刻被采集。

(5) LTDC_HorizontalSync

本成员设置行同步信号HSYNC的宽度HSW,它以像素时钟CLK的周期为单位,实际写入该参数时应写入(HSW-1),参数范围为0x000- 0xFFF。

(6) LTDC_VerticalSync

本成员设置垂直同步信号VSYNC的宽度VSW,它以“行”为位,实际写入该参数时应写入(VSW-1) ,参数范围为0x000- 0x7FF。

(7) LTDC_AccumulatedHBP

本成员用于配置“水平同步像素HSW”加“水平后沿像素HBP”的累加值,实际写入该参数时应写入(HSW+HBP-1) ,参数范围为0x000- 0xFFF。

(8) LTDC_AccumulatedVBP

本成员用于配置“垂直同步行VSW”加“垂直后沿行VBP”的累加值,实际写入该参数时应写入(VSW+VBP-1) ,参数范围为0x000- 0x7FF。

(9) LTDC_AccumulatedActiveW

本成员用于配置“水平同步像素HSW”加“水平后沿像素HBP”加“有效像素”的累加值,实际写入该参数时应写入(HSW+HBP+有效宽度-1) ,参数范围为0x000- 0xFFF。

(10) LTDC_AccumulatedActiveH

本成员用于配置“垂直同步行VSW”加“垂直后沿行VBP”加“有效行”的累加值,实际写入该参数时应写入(VSW+VBP+有效高度-1) ,参数范围为0x000- 0x7FF。

(11) LTDC_TotalWidth

本成员用于配置“水平同步像素HSW”加“水平后沿像素HBP”加“有效像素”加“水平前沿像素HFP”的累加值,即总宽度,实际写入该参数时应写入(HSW+HBP+有效宽度+HFP-1) ,参数范围为0x000- 0xFFF。

(12) LTDC_TotalHeigh

本成员用于配置“垂直同步行VSW”加“垂直后沿行VBP”加“有效行”加“垂直前沿行VFP”的累加值,即总高度,实际写入该参数时应写入(HSW+HBP+有效高度+VFP-1),参数范围为0x000- 0x7FF。

(13) LTDC_BackgroundRedValue/ GreenValue/ BlueValue

这三个结构体成员用于配置背景的颜色值,见图 两层与背景混合 ,这里说的背景层与前面提到的“前景层/背景层”概念有点区别, 它们对应下图中的“第2层/第1层”,而在这两层之外,还有一个最终的背景层,当第1第2层都透明时,这个背景层就会被显示,而这个背景层是一个纯色的矩形, 它的颜色值就是由这三个结构体成员配置的,各成员的参数范围为0x00- 0xFF。

两层与背景混合

对这些LTDC初始化结构体成员赋值后,调用库函数LTDC_Init可把这些参数写入到LTDC的各个配置寄存器,LTDC外设根据这些配置控制时序。

27.6. LTDC层级初始化结构体

LTDC初始化结构体只是配置好了与液晶屏通讯的基本时序,还有像素格式、显存地址等诸多参数需要使用LTDC层级初始化结构体完成,见 代码清单:DMA2D-2

代码清单:DMA2D-2 LTDC层级初始化结构体LTDC_Layer_InitTypeDef
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @brief  LTDC Layer structure definition
*/
typedef struct
{
    uint32_t LTDC_HorizontalStart;            /*配置窗口的行起始位置 */
    uint32_t LTDC_HorizontalStop;             /*配置窗口的行结束位置 */
    uint32_t LTDC_VerticalStart;             /*配置窗口的垂直起始位置 */
    uint32_t LTDC_VerticalStop;               /*配置窗口的垂直束位置 */
    uint32_t LTDC_PixelFormat;                /*配置当前层的像素格式*/
    uint32_t LTDC_ConstantAlpha;              /*配置当前层的透明度Alpha常量值*/
    uint32_t LTDC_DefaultColorBlue;           /*配置当前层的默认蓝色值 */
    uint32_t LTDC_DefaultColorGreen;           /*配置当前层的默认绿色值 */
    uint32_t LTDC_DefaultColorRed;             /*配置当前层的默认红色值 */
    uint32_t LTDC_DefaultColorAlpha;          /*配置当前层的默认透明值 */
    uint32_t LTDC_BlendingFactor_1;           /*配置混合因子BlendingFactor1 */
    uint32_t LTDC_BlendingFactor_2;            /*配置混合因子BlendingFactor2 */
    uint32_t LTDC_CFBStartAdress;             /*配置当前层的显存起始位置*/
    uint32_t LTDC_CFBLineLength;              /*配置当前层的行数据长度 */
    uint32_t LTDC_CFBPitch;        /*配置从某行的起始到下一行像素起始处的增量*/
    uint32_t LTDC_CFBLineNumber;              /* 配置当前层的行数*/
} LTDC_Layer_InitTypeDef;

LTDC_Layer_InitTypeDef各个结构体成员的功能介绍如下:

(1) LTDC_ HorizontalStart /HorizontalStop/ VerticalStart/ VerticalStop

这些成员用于确定该层显示窗口的边界,分别表示行起始、行结束、垂直起始及垂直结束的位置,见图 配置可层的显示窗口 。 注意这些参数包含同步HSW/VSW、后沿大小HBP/VBP和有效数据区域的内部时序发生器的配置,表 各个窗口成员值 的是各个窗口配置成员应写入的数值。

配置可层的显示窗口 各个窗口成员值

(2) LTDC_PixelFormat

本成员用于设置该层数据的像素格式,可以设置为LTDC_Pixelformat_ARGB8888/ RGB888/ RGB565/ ARGB1555/ ARGB4444/ L8/ AL44/ AL88格式。

(3) LTDC_ConstantAlpha

本成员用于设置该层恒定的透明度常量Alpha,称为恒定Alpha,参数范围为0x00-0xFF,在图层混合时,可根据后面的BlendingFactor成员的配置, 选择是只使用这个恒定Alpha进行混合运算还是把像素本身的Alpha值也加入到运算中。

(4) LTDC_DefaultColorBlue/Green/Red/Alpha

这些成员用于配置该层的默认颜色值,分别为蓝、绿、红及透明分量,该颜色在定义的层窗口外或在层禁止时使用。

(5) LTDC_BlendingFactor_1/2

本成员用于设置混合系数 BF1 和 BF2。每一层实际显示的颜色都需要使用透明度参与运算,计算出不包含透明度的直接RGB颜色值, 然后才传输给液晶屏(因为液晶屏本身没有透明的概念)。混合的计算公式为:

BC = BF1 x C + BF2 x Cs,

公式中的参数见表 混合公式参数说明表

混合公式参数说明表

本结构体成员可以设置BF1/BF2参数使用CA配置(LTDC_BlendingFactor1/2_CA)还是PAxCA配置(LTDC_BlendingFactor1/2_PAxCA)。 配置成CA表示混合系数中只包含恒定的Alpha值,即像素本身的Alpha不会影响混合效果,若配置成PAxCA,则混合系数中包含有像素本身的Alpha值, 即把像素本身的Alpha加入到混合运算中。其中的恒定Alpha值即前面“LTDC_ConstantAlpha”结构体配置参数的透明度百分比:(配置的Alpha值/0xFF)。

图两层与背景混合

图两层与背景混合 ,数据源混合时,由下至上,如果使用了2层,则先将第1层与LTDC背景混合,随后再使用该混合颜色与第2层混合得到最终结果。 例如,当只使用第1层数据源时,且BF1及BF2都配置为使用恒定Alpha,该Alpha值在LTDC_ConstantAlpha结构体成员值中被配置为240(0xF0)。 因此,恒定Alpha值为240/255=0.94。若当前层颜色C=128,背景色Cs=48,那么第1层与背景色的混合结果为:

BC=恒定Alpha x C + (1- 恒定Alpha) x Cs=0.94 x Cs +(1-0.94)x 48=123

(6) LTDC_CFBStartAdress

本成员用于设置该层的显存首地址,该层的像素数据保存在从这个地址开始的存储空间内。

(7) LTDC_CFBLineLength

本成员用于设置当前层的行数据长度,即每行的有效像素点个数x每个像素的字节数,实际配置该参数时应写入值(行有效像素个数x每个像素的字节数+3), 每个像素的字节数跟像素格式有关,如RGB565为2字节,RGB888为3字节,ARGB8888为4字节。

(8) LTDC_CFBPitch

本成员用于设置从某行的有效像素起始位置到下一行起始位置处的数据增量,无特殊情况的话,它一般就直接等于行的有效像素个数x每个像素的字节数。

(9) LTDC_CFBLineNumber

本成员用于设置当前层的显示行数。

配置完LTDC_Layer_InitTypeDef层级初始化结构体后,调用库函数LTDC_LayerInit可把这些配置写入到LTDC的层级控制寄存器中,完成初始化。 初始化完成后LTDC会不断把显存空间的数据传输到液晶屏进行显示,我们可以直接修改或使用DMA2D修改显存中的数据,从而改变显示的内容。

27.7. DMA2D初始化结构体

在实际显示时,我们常常采用DMA2D描绘直线和矩形,这个时候会用到DMA2D结构体,见 代码清单:DMA2D-3

代码清单:DMA2D-3 DMA2D初始化结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/**
* @brief  DMA2D Init structure definition
*/
typedef struct
{
    uint32_t DMA2D_Mode;                           /*配置DMA2D的传输模式*/
    uint32_t DMA2D_CMode;                          /*配置DMA2D的颜色模式 */
    uint32_t DMA2D_OutputBlue;                     /*配置输出图像的蓝色分量*/
    uint32_t DMA2D_OutputGreen;                    /*配置输出图像的绿色分量*/
    uint32_t DMA2D_OutputRed;                      /*配置输出图像的红色分量*/
    uint32_t DMA2D_OutputAlpha;                   /*配置输出图像的透明度分量 */

    uint32_t DMA2D_OutputMemoryAdd;               /*配置显存地址*/
    uint32_t DMA2D_OutputOffset;                   /*配置输出地址偏移*/
    uint32_t DMA2D_NumberOfLine;                   /*配置要传输多少行 */
    uint32_t DMA2D_PixelPerLine;                   /*配置每行有多少个像素 */
} DMA2D_InitTypeDef;

DMA2D初始化结构体中的各个成员介绍如下:

(1) DMA2D_Mode

本成员用于配置DMA2D的工作模式,它可以被设置为表 DMA2D的工作模式 中的值。

DMA2D的工作模式

这几种工作模式主要区分数据的来源、是否使能PFC以及是否使能混合器。使用DMA2D时,可把数据从某个位置搬运到显存,该位置可以是DMA2D本身的寄存器, 也可以是设置好的DMA2D前景地址、背景地址(即从存储器到存储器)。若使能了PFC,则存储器中的数据源会经过转换再传输到显存。 若使能了混合器,DMA2D会把两个数据源中的数据混合后再输出到显存。

若使用存储器到存储器模式,需要调用库函数DMA2D_FGConfig,使用初始化结构体DMA2D_FG_InitTypeDef配置数据源的格式、地址等参数。 (背景层使用函数DMA2D_BGConfig和结构体DMA2D_BG_InitTypeDef)

(2) DMA2D_CMode

本成员用于配置DMA2D的输出PFC颜色格式,即它将要传输给显存的格式。

(3) DMA2D_OutputBlue/ Green/ Red/ Alpha

这几个成员用于配置DMA2D的寄存器颜色值,若DMA2D工作在“寄存器到存储器”(DMA2D_R2M)模式时,这个颜色值作为数据源, 被DMA2D复制到显存空间,即目标空间都会被填入这一种色彩。

(4) DMA2D_OutputMemoryAdd

本成员用于配置DMA2D的输出FIFO的地址, DMA2D的数据会被搬运到该空间,一般把它设置为本次传输显示位置的起始地址。

(5) DMA2D_OutputOffset

本成员用于配置行偏移(以像素为单位),行偏移会被添加到各行的结尾,用于确定下一行的起始地址。如表 数据传输示例 中的黄色格子表示行偏移, 绿色格子表示要显示的数据。左表中显示的是一条垂直的线,且线的宽度为1像素,所以行偏移的值=7-1=6,即“行偏移的值=行宽度-线的宽度”, 右表中的线宽度为2像素,行偏移的值=7-2=5。

数据传输示例

(6) DMA2D_NumberOfLine

本成员用于配置DMA2D一共要传输多少行数据,如表 数据传输示例 中一共有5行数据。

(7) DMA2D_PixelPerLine

本成员用于配置每行有多少个像素点,如表 数据传输示例 左侧表示每行有1个像素点,右侧表示每行有2个像素点。

配置完这些结构体成员,调用库函数DMA2D_Init即可把这些参数写入到DMA2D的控制寄存器中,然后再调用DMA2D_StartTransfer函数开启数据传输及转换。

27.8. LTDC/DMA2D—液晶显示实验

本小节讲解如何使用LTDC及DMA2D外设控制型号为“STD800480”的5寸液晶屏,见图 液晶屏实物图 ,该液晶屏的分辨率为800x480,支持RGB888格式。

学习本小节内容时,请打开配套的“LTDC/DMA2D—液晶显示英文”工程配合阅读。

27.8.1. 硬件设计

液晶屏实物图

液晶屏实物图 液晶屏背面的PCB电路对应图 升压电路原理图 、图 电容屏接口 、图 液晶屏接口 、图 排针接口 中的原理图, 分别是升压电路、触摸屏接口、液晶屏接口及排针接口。升压电路把输入的5V电源升压为20V,输出到液晶屏的背光灯中; 触摸屏及液晶屏接口通过FPC插座把两个屏的排线连接到PCB电路板上,这些FPC插座与信号引出到屏幕右侧的排针处,方便整个屏幕与外部器件相连。

升压电路原理图

升压电路中的BK引脚可外接PWM信号,控制液晶屏的背光强度,BK为高电平时输出电压。

电容屏接口

电容触摸屏使用I2C通讯,它的排线接口包含了I2C的通讯引脚SCL、SDA,还包含控制触摸屏芯片复位的RSTN信号以及触摸中断信号INT。

液晶屏接口

关于这部分液晶屏的排线接口说明见图 液晶排线接口

液晶排线接口 排针接口

以上是我们STM32F429实验板使用的5寸屏原理图,它通过屏幕上的排针接入到实验板的液晶排母接口,与STM32芯片的引脚相连,连接见图 屏幕与实验板的引脚连接

屏幕与实验板的引脚连接

由于液晶屏的部分引脚与实验板的CAN芯片信号引脚相同,所以使用液晶屏的时候不能使用CAN通讯。

以上原理图可查阅《LCD5.0-黑白原理图》及《野火F429开发板黑白原理图》文档获知,若您使用的液晶屏或实验板不一样,请根据实际连接的引脚修改程序。

27.8.2. 软件设计

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

27.8.2.1. 编程要点

(1) 初始化LTDC时钟、DMA2D时钟、GPIO时钟;

(2) 初始化SDRAM,以便用作显存;

(3) 根据液晶屏的参数配置LTDC外设的通讯时序;

(4) 配置LTDC层级控制参数,配置显存地址;

(5) 初始化DMA2D,使用DMA2D辅助显示;

(6) 编写测试程序,控制液晶输出。

27.8.2.2. 代码分析

LTDC硬件相关宏定义

我们把LTDC控制液晶屏硬件相关的配置都以宏的形式定义到 “bsp_lcd.h”文件中,见 代码清单:DMA2D-4

代码清单:DMA2D-4 LTDC硬件配置相关的宏(省略了部分数据线)
 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
/*部分液晶信号线的引脚复用编号是AF9*/
#define GPIO_AF_LTDC_AF9          ((uint8_t)0x09)

//红色数据线
#define LTDC_R0_GPIO_PORT           GPIOH
#define LTDC_R0_GPIO_CLK            RCC_AHB1Periph_GPIOH
#define LTDC_R0_GPIO_PIN            GPIO_Pin_2
#define LTDC_R0_PINSOURCE           GPIO_PinSource2
#define LTDC_R0_AF                  GPIO_AF_LTDC    //使用LTDC复用编号

#define LTDC_R3_GPIO_PORT           GPIOB
#define LTDC_R3_GPIO_CLK            RCC_AHB1Periph_GPIOB
#define LTDC_R3_GPIO_PIN            GPIO_Pin_0
#define LTDC_R3_PINSOURCE           GPIO_PinSource0
#define LTDC_R3_AF                  GPIO_AF_LTDC_AF9 //使用AF9复用编号
/*此处省略R1、R2、R4-R7*/
//绿色数据线
#define LTDC_G0_GPIO_PORT           GPIOE
#define LTDC_G0_GPIO_CLK            RCC_AHB1Periph_GPIOE
#define LTDC_G0_GPIO_PIN            GPIO_Pin_5
#define LTDC_G0_PINSOURCE           GPIO_PinSource5
#define LTDC_G0_AF                  GPIO_AF_LTDC
/*此处省略G1-G7*/
//蓝色数据线
#define LTDC_B0_GPIO_PORT           GPIOE
#define LTDC_B0_GPIO_CLK            RCC_AHB1Periph_GPIOE
#define LTDC_B0_GPIO_PIN            GPIO_Pin_4
#define LTDC_B0_PINSOURCE           GPIO_PinSource4
#define LTDC_B0_AF                  GPIO_AF_LTDC
/*此处省略B1-B7*/

//控制信号线
/*像素时钟CLK*/
#define LTDC_CLK_GPIO_PORT        GPIOG
#define LTDC_CLK_GPIO_CLK         RCC_AHB1Periph_GPIOG
#define LTDC_CLK_GPIO_PIN         GPIO_Pin_7
#define LTDC_CLK_PINSOURCE        GPIO_PinSource7
#define LTDC_CLK_AF               GPIO_AF_LTDC
/*水平同步信号HSYNC*/
#define LTDC_HSYNC_GPIO_PORT      GPIOI
#define LTDC_HSYNC_GPIO_CLK       RCC_AHB1Periph_GPIOI
#define LTDC_HSYNC_GPIO_PIN       GPIO_Pin_10
#define LTDC_HSYNC_PINSOURCE      GPIO_PinSource10
#define LTDC_HSYNC_AF             GPIO_AF_LTDC
/*垂直同步信号VSYNC*/
#define LTDC_VSYNC_GPIO_PORT      GPIOI
#define LTDC_VSYNC_GPIO_CLK       RCC_AHB1Periph_GPIOI
#define LTDC_VSYNC_GPIO_PIN       GPIO_Pin_9
#define LTDC_VSYNC_PINSOURCE      GPIO_PinSource9
#define LTDC_VSYNC_AF             GPIO_AF_LTDC
/*数据使能信号DE*/
#define LTDC_DE_GPIO_PORT         GPIOF
#define LTDC_DE_GPIO_CLK          RCC_AHB1Periph_GPIOF
#define LTDC_DE_GPIO_PIN          GPIO_Pin_10
#define LTDC_DE_PINSOURCE         GPIO_PinSource10
#define LTDC_DE_AF                GPIO_AF_LTDC
/*液晶屏使能信号DISP,高电平使能*/
#define LTDC_DISP_GPIO_PORT        GPIOD
#define LTDC_DISP_GPIO_CLK         RCC_AHB1Periph_GPIOD
#define LTDC_DISP_GPIO_PIN         GPIO_Pin_4
/*液晶屏背光信号,高电平使能*/
#define LTDC_BL_GPIO_PORT         GPIOD
#define LTDC_BL_GPIO_CLK          RCC_AHB1Periph_GPIOD
#define LTDC_BL_GPIO_PIN          GPIO_Pin_7

以上代码根据硬件的连接,把与LTDC与液晶屏通讯使用的引脚号、引脚源以及复用功能映射都以宏封装起来。其中部分LTDC信号的复用功能映射比较特殊, 如用作R3信号线的PB0,它的复用功能映射值为AF9,而大部分LTDC的信号线都是AF14,见图 LTDC的复用功能映射 ,在编写宏的时候要注意区分。

LTDC的复用功能映射

初始化LTDC的 GPIO

利用上面的宏,编写LTDC的GPIO引脚初始化函数,见 代码清单:DMA2D-5

代码清单:DMA2D-5 LTDC的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
54
55
56
57
58
59
60
61
/**
* @brief  初始化控制LCD的IO
* @param  无
* @retval 无
*/
static void LCD_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    /* 使能LCD使用到的引脚时钟 */
    //红色数据线 /*此处省略部分信号线......*/
    RCC_AHB1PeriphClockCmd(LTDC_R0_GPIO_CLK |
                            //控制信号线
    LTDC_CLK_GPIO_CLK | LTDC_HSYNC_GPIO_CLK |LTDC_VSYNC_GPIO_CLK|
        LTDC_DE_GPIO_CLK  | LTDC_BL_GPIO_CLK  |LTDC_DISP_GPIO_CLK ,ENABLE);

    /* GPIO配置 */

    /* 红色数据线 */
    GPIO_InitStruct.GPIO_Pin = LTDC_R0_GPIO_PIN;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;

    GPIO_Init(LTDC_R0_GPIO_PORT, &GPIO_InitStruct);
    GPIO_PinAFConfig(LTDC_R0_GPIO_PORT, LTDC_R0_PINSOURCE, LTDC_R0_AF);
    /*此处省略部分数据信号线......*/
    //控制信号线
    GPIO_InitStruct.GPIO_Pin = LTDC_CLK_GPIO_PIN;
    GPIO_Init(LTDC_CLK_GPIO_PORT, &GPIO_InitStruct);
    GPIO_PinAFConfig(LTDC_CLK_GPIO_PORT, LTDC_CLK_PINSOURCE, LTDC_CLK_AF);

    GPIO_InitStruct.GPIO_Pin = LTDC_HSYNC_GPIO_PIN;
    GPIO_Init(LTDC_HSYNC_GPIO_PORT, &GPIO_InitStruct);
    GPIO_PinAFConfig(LTDC_HSYNC_GPIO_PORT, LTDC_HSYNC_PINSOURCE,LTDC_HSYNC_AF);

    GPIO_InitStruct.GPIO_Pin = LTDC_VSYNC_GPIO_PIN;
    GPIO_Init(LTDC_VSYNC_GPIO_PORT, &GPIO_InitStruct);
    GPIO_PinAFConfig(LTDC_VSYNC_GPIO_PORT, LTDC_VSYNC_PINSOURCE,LTDC_VSYNC_AF);

    GPIO_InitStruct.GPIO_Pin = LTDC_DE_GPIO_PIN;
    GPIO_Init(LTDC_DE_GPIO_PORT, &GPIO_InitStruct);
    GPIO_PinAFConfig(LTDC_DE_GPIO_PORT, LTDC_DE_PINSOURCE, LTDC_DE_AF);

    //背光BL 及液晶使能信号DISP
    GPIO_InitStruct.GPIO_Pin = LTDC_DISP_GPIO_PIN;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;

    GPIO_Init(LTDC_DISP_GPIO_PORT, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin = LTDC_BL_GPIO_PIN;
    GPIO_Init(LTDC_BL_GPIO_PORT, &GPIO_InitStruct);

    //拉高使能lcd
    GPIO_SetBits(LTDC_DISP_GPIO_PORT,LTDC_DISP_GPIO_PIN);
    GPIO_SetBits(LTDC_BL_GPIO_PORT,LTDC_BL_GPIO_PIN);
}

与所有使用到GPIO的外设一样,都要先把使用到的GPIO引脚模式初始化,以上代码把LTDC的信号线全都初始化为LCD复用功能, 而背光BL及液晶使能DISP信号则被初始化成普通的推挽输出模式,并且在初始化完毕后直接控制它们开启背光及使能液晶屏。

配置LTDC的模式

接下来需要配置LTDC的工作模式,这个函数的主体是根据液晶屏的硬件特性,设置LTDC与液晶屏通讯的时序参数及信号有效极性。 代码清单:DMA2D-6

代码清单:DMA2D-6 配置LTDC的模式
 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
/*根据液晶数据手册的参数配置*/
#define HBP  46     //HSYNC后的无效像素
#define VBP  23     //VSYNC后的无效行数

#define HSW   1     //HSYNC宽度
#define VSW   1     //VSYNC宽度

#define HFP  16     //HSYNC前的无效像素
#define VFP   7     //VSYNC前的无效行数
/* LCD Size (Width and Height) */
#define  LCD_PIXEL_WIDTH    ((uint16_t)800)
#define  LCD_PIXEL_HEIGHT   ((uint16_t)480)

/**
* @brief LCD参数配置
* @note  这个函数用于配置LTDC外设:
*/
void LCD_Init(void)
{
    LTDC_InitTypeDef       LTDC_InitStruct;

    /* 使能LTDC外设时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_LTDC, ENABLE);
    /* 使能DMA2D时钟 */
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2D, ENABLE);
    /* 初始化LCD的控制引脚 */
    LCD_GPIO_Config();
    /* 初始化SDRAM,以便使用SDRAM作显存 */
    SDRAM_Init();

    /* 配置 PLLSAI 分频器,它的输出作为像素同步时钟CLK*/
    /* PLLSAI_VCO 输入时钟 = HSE_VALUE/PLL_M = 1 Mhz */
    /* PLLSAI_VCO 输出时钟 = PLLSAI_VCO输入 * PLLSAI_N = 420 Mhz */
    /* PLLLCDCLK = PLLSAI_VCO 输出/PLLSAI_R = 420/6  Mhz */
    /* LTDC 时钟频率 = PLLLCDCLK / RCC_PLLSAI = 420/6/8 = 8.75 Mhz */
    /* LTDC时钟太高会导花屏,若对刷屏速度要求不高,降低时钟频率可减少花屏现象*/
    /* 以下函数三个参数分别为:PLLSAIN,PLLSAIQ,PLLSAIR,其中PLLSAIQ与LTDC无关

    RCC_PLLSAIConfig(420, 7, 6);
    RCC_LTDCCLKDivConfig(RCC_PLLSAIDivR_Div8); //这个函数的参数值为DIV

    /* 使能 PLLSAI 时钟 */
    RCC_PLLSAICmd(ENABLE);
    /* 等待 PLLSAI 初始化完成 */
    while (RCC_GetFlagStatus(RCC_FLAG_PLLSAIRDY)== RESET);

    /* LTDC配置*********************************************************/
    /*信号极性配置*/
    /* 行同步信号极性 */
    LTDC_InitStruct.LTDC_HSPolarity = LTDC_HSPolarity_AL;
    /* 垂直同步信号极性 */
    LTDC_InitStruct.LTDC_VSPolarity = LTDC_VSPolarity_AL;
    /* 数据使能信号极性 */
    LTDC_InitStruct.LTDC_DEPolarity = LTDC_DEPolarity_AL;
    /* 像素同步时钟极性 */
    LTDC_InitStruct.LTDC_PCPolarity = LTDC_PCPolarity_IPC;

    /* 配置LCD背景颜色 */
    LTDC_InitStruct.LTDC_BackgroundRedValue = 0;
    LTDC_InitStruct.LTDC_BackgroundGreenValue = 0;
    LTDC_InitStruct.LTDC_BackgroundBlueValue = 0;

    /* 时间参数配置 */
    /* 配置行同步信号宽度(HSW-1) */
    LTDC_InitStruct.LTDC_HorizontalSync =HSW-1;
    /* 配置垂直同步信号宽度(VSW-1) */
    LTDC_InitStruct.LTDC_VerticalSync = VSW-1;
    /* 配置(HSW+HBP-1) */
    LTDC_InitStruct.LTDC_AccumulatedHBP =HSW+HBP-1;
    /* 配置(VSW+VBP-1) */
    LTDC_InitStruct.LTDC_AccumulatedVBP = VSW+VBP-1;
    /* 配置(HSW+HBP+有效像素宽度-1) */
    LTDC_InitStruct.LTDC_AccumulatedActiveW = HSW+HBP+LCD_PIXEL_WIDTH-1;
    /* 配置(VSW+VBP+有效像素高度-1) */
    LTDC_InitStruct.LTDC_AccumulatedActiveH = VSW+VBP+LCD_PIXEL_HEIGHT-1;
    /* 配置总宽度(HSW+HBP+有效像素宽度+HFP-1) */
    LTDC_InitStruct.LTDC_TotalWidth =HSW+ HBP+LCD_PIXEL_WIDTH  + HFP-1;
    /* 配置总高度(VSW+VBP+有效像素高度+VFP-1) */
    LTDC_InitStruct.LTDC_TotalHeigh =VSW+ VBP+LCD_PIXEL_HEIGHT  + VFP-1;

    LTDC_Init(&LTDC_InitStruct);
    LTDC_Cmd(ENABLE);
}

该函数的执行流程如下:

(1) 初始化GPIO引脚以及LTDC、DMA2D时钟

函数开头调用了前面定义的LCD_GPIO_Config函数对液晶屏用到的GPIO进行初始化,并且使用库函数RCC_APB2PeriphClockCmd及RCC_AHB1PeriphClockCmd使能LTDC和DMA2D外设的时钟。

(2) 初始化SDRAM

接下来调用前面章节讲解的SDRAM_Init函数初始化FMC外设控制SDRAM,以便使用SDRAM的存储空间作为显存。

(3) 设置像素同步时钟

在“LTDC结构框图的时钟信号”小节讲解到,LTDC与液晶屏通讯的像素同步时钟CLK是由PLLSAI分频器控制输出的, 它的时钟源为外部高速晶振HSE经过分频因子M分频后的时钟,按照默认设置,一般分频因子M会把HSE分频得到1MHz的时钟,如HSE晶振频率为25MHz时, 把M设置为25,HSE晶振频率为8MHz时,把M设置为8,然后调用SystemInit函数初始化系统时钟。经过M分频得到的1MHz时钟输入到PLLSAI分频器后, 使用倍频因子“N”倍频,然后再经过“R”因子分频,得到PLLCDCLK时钟,再由“DIV”因子分频得到LTDC通讯的同步时钟LCD_CLK。

即:fLCD_CLK=fHSE/M x N/R/DIV

由于M把HSE时钟分频为1MHz的时钟,所以上式等价于:

fLCD_CLK=1xN/R/DIV

利用库函数RCC_PLLSAIConfig及RCC_LTDCCLKDivConfig函数可以配置PLLSAI分频器的这些参数,其中库函数RCC_PLLSAIConfig的三个输入参数分别是倍频因子N、 分频因子Q和分频因子R,其中“Q”因子是作用于SAI接口的分频时钟,与LTDC无关,RCC_LTDCCLKDivConfig函数的输入参数为分频因子“DIV”。 在配置完这些分频参数后,需要调用库函数RCC_PLLSAICmd使能PLLSAI的时钟并且检测标志位等待时钟初始化完成。

在上面的代码中调用函数设置N=420,R=6,DIV=8,计算得LCD_CLK的时钟频率为8.75MHz,这个时钟频率是我们根据实测效果选定的,若使用的是16位数据格式, 可把时钟频率设置为24MHz,若只使用单层液晶屏数据源,则可配置为34MHz。然而根据液晶屏的数据手册查询可知它支持最大的同步时钟为50MHz, 典型速率为33.3Mhz,见图 液晶屏数据手册标注的时间参数 ,由此说明传输速率主要受限于STM32一方。LTDC外设需要从SDRAM显存读取数据, 这会消耗一定的时间,所以使用32位像素格式的数据要比使用16位像素格式的慢,如若只使用单层数据源,还可以进一步减少一半的数据量,所以更快。

(4) 配置信号极性

接下来根据液晶屏的时序要求,配置LTDC与液晶屏通讯时的信号极性,见图 液晶屏时序中的有效电平 。 在程序中配置的HSYNC、VSYNC、DE有效信号极性均为低电平,同步时钟信号极性配置为上升沿。其中DE信号的极性跟液晶屏时序图的要求不一样, 文档中DE的有效电平为高电平,而实际测试中把设置为DE低电平有效时屏幕才能正常工作,我们以实际测试为准。

液晶屏时序中的有效电平

(5) 配置时间参数

液晶屏通讯中还有时间参数的要求,接下来的程序我们根据液晶屏手册给出的时间参数,配置HSW、VSW、HBP、HFP、VBP、VFP、有效像素宽度及有效行数。 这些参数都根据图 液晶屏数据手册标注的时间参数 的说明以宏定义在程序中给出。

液晶屏数据手册标注的时间参数

(6) 写入参数到寄存器并使能外设

经过上面步骤,赋值完了初始化结构体,接下来调用库函数LTDC_Init把各种参数写入到LTDC的控制寄存器中,然后调用库函数LTDC_Cmd使能LTDC。

配置LTDC的层级初始化

在上面配置完成STM32的LTDC外设基本工作模式后,还需要针对液晶屏的各个数据源层进行初始化,才能正常工作,代码清单:DMA2D-7

代码清单:DMA2D-7 LTDC的层级初始化
 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
/* LCD Size (Width and Height) */
#define  LCD_PIXEL_WIDTH    ((uint16_t)800)
#define  LCD_PIXEL_HEIGHT   ((uint16_t)480)

#define LCD_FRAME_BUFFER       ((uint32_t)0xD0000000)       //第一层首地址
#define BUFFER_OFFSET          ((uint32_t)800*480*3)     //一层液晶的数据量
#define LCD_PIXCELS            ((uint32_t)800*480)
/**
* @brief 初始化LTD的 层 参数
*           - 设置显存空间
*           - 设置分辨率
* @param  None
* @retval None
*/
void LCD_LayerInit(void)
{
    LTDC_Layer_InitTypeDef LTDC_Layer_InitStruct;

    /* 层窗口配置 */
    /* 配置本层的窗口边界,注意这些参数是包含HBP HSW VBP VSW的 */
    //一行的第一个起始像素,该值应为 (LTDC_InitStruct.LTDC_AccumulatedHBP+1)的值
    LTDC_Layer_InitStruct.LTDC_HorizontalStart = HBP + HSW;
    //一行的最后一个像素,该值应为 (LTDC_InitStruct.LTDC_AccumulatedActiveW)的值
    LTDC_Layer_InitStruct.LTDC_HorizontalStop = HSW+HBP+LCD_PIXEL_WIDTH-1;
    //一列的第一个起始像素,该值应为 (LTDC_InitStruct.LTDC_AccumulatedVBP+1)的值
    LTDC_Layer_InitStruct.LTDC_VerticalStart =  VBP + VSW;
    //一列的最后一个像素,该值应为 (LTDC_InitStruct.LTDC_AccumulatedActiveH)的值
    LTDC_Layer_InitStruct.LTDC_VerticalStop = VSW+VBP+LCD_PIXEL_HEIGHT-1;

    /* 像素格式配置*/
    LTDC_Layer_InitStruct.LTDC_PixelFormat = LTDC_Pixelformat_RGB888;

    /* 默认背景颜色,该颜色在定义的层窗口外或在层禁止时使用。 */
    LTDC_Layer_InitStruct.LTDC_DefaultColorBlue = 0;
    LTDC_Layer_InitStruct.LTDC_DefaultColorGreen = 0;
    LTDC_Layer_InitStruct.LTDC_DefaultColorRed = 0;
    LTDC_Layer_InitStruct.LTDC_DefaultColorAlpha = 0;
    /* 恒定Alpha值配置,0-255 */
    LTDC_Layer_InitStruct.LTDC_ConstantAlpha = 255;
    /* 配置混合因子 CA表示使用恒定Alpha值,PAxCA表示使用像素Alpha x 恒定Alpha值 */
    LTDC_Layer_InitStruct.LTDC_BlendingFactor_1 = LTDC_BlendingFactor1_CA;
    LTDC_Layer_InitStruct.LTDC_BlendingFactor_2 = LTDC_BlendingFactor2_CA;

    /* 该成员应写入(一行像素数据占用的字节数+3)
    Line Lenth = 行有效像素个数 x 每个像素的字节数 + 3
    行有效像素个数 = LCD_PIXEL_WIDTH
    每个像素的字节数 = 2(RGB565/RGB1555)/ 3 (RGB888)/ 4(ARGB8888)
    */
    LTDC_Layer_InitStruct.LTDC_CFBLineLength = ((LCD_PIXEL_WIDTH * 3) + 3);
    /* 从某行的起始位置到下一行起始位置处的像素增量
    Pitch = 行有效像素个数 x 每个像素的字节数 */
    LTDC_Layer_InitStruct.LTDC_CFBPitch = (LCD_PIXEL_WIDTH * 3);

    /* 配置有效的行数 */
    LTDC_Layer_InitStruct.LTDC_CFBLineNumber = LCD_PIXEL_HEIGHT;
    /* 配置本层的显存首地址 */
    LTDC_Layer_InitStruct.LTDC_CFBStartAdress = LCD_FRAME_BUFFER;
    /* 以上面的配置初始化第 1 层*/
    LTDC_LayerInit(LTDC_Layer1, &LTDC_Layer_InitStruct);

    /*配置第 2 层,若没有重写某个成员的值,则该成员使用跟第1层一样的配置 */
    /* 配置本层的显存首地址,这里配置它紧挨在第1层的后面*/
    LTDC_Layer_InitStruct.LTDC_CFBStartAdress = LCD_FRAME_BUFFER+ BUFFER_OFFSET;

    /* 配置混合因子,使用像素Alpha参与混合 */
    LTDC_Layer_InitStruct.LTDC_BlendingFactor_1 = LTDC_BlendingFactor1_PAxCA;
    LTDC_Layer_InitStruct.LTDC_BlendingFactor_2 = LTDC_BlendingFactor2_PAxCA;
    /* 初始化第2层 */
    LTDC_LayerInit(LTDC_Layer2, &LTDC_Layer_InitStruct);

    /* 立即重载配置 */
    LTDC_ReloadConfig(LTDC_IMReload);
    /*使能前景及背景层 */
    LTDC_LayerCmd(LTDC_Layer1, ENABLE);
    LTDC_LayerCmd(LTDC_Layer2, ENABLE);

    /* 立即重载配置 */
    LTDC_ReloadConfig(LTDC_IMReload);
}

LTDC的层级初始化函数执行流程如下:

(1) 配置窗口边界

每层窗口都需要配置有效显示窗口,使用LTDC_HorizontalStart/ HorizontalStop/ LTDC_VerticalStart/ LTDC_VerticalStop成员来确定这个窗口的左右上下边界,各个成员应写入的值与前面LTDC初始化结构体中某些参数类似,注意某些成员要求加1或减1。

(2) 配置像素的格式

LTDC_PixelFormat成员用于配置本层像素的格式,在这个实验中我们把这层设置为RGB888格式,两层数据源的像素可以配置成不同的格式,层与层之间是独立的。

(3) 配置默认背景颜色

在定义的层窗口外或在层禁止时,该层会使用默认颜色作为数据源,默认颜色使用LTDC_DefaultColorBlue/Green/Red/Alpha成员来配置,本实验中我们把默认颜色配置成透明了。

(4) 配置第1层的恒定Alpha与混合因子

前面提到两层数据源混合时可根据混合因子设置只使用恒定Alpha运算,还是把像素的Alpha也加入到运算中。对于第1层数据源,我们不希望LTDC的默认背景层参与到混合运算中, 而希望第1层直接作为背景(因为第1层的数据每个像素点都是可控的,而背景层所有像素点都是同一个颜色)。因此我们把恒定Alpha值(LTDC_ConstantAlpha)设置为255, 即完全不透明,混合因子BF1/BF2参数(LTDC_BlendingFactor_1/2)都配置成LTDC_BlendingFactor1/2_CA,即只使用恒定Alpha值运算, 这样配置的结果是第1层的数据颜色直接等于它像素本身的RGB值,不受像素中的Alpha值及背景影响。

(5) 配置层的数据量

通过参数LTDC_CFBLineLength及LTDC_CFBLineNumber可设定层的数据量,层的数据量跟显示窗口大小及像素格式有关,由于我们把这层的像素格式设置成了RGB888, 所以每个像素点的大小为3字节,LTDC_CFBLineLength参数写入值:行有效像素个数x3+3。而LTDC_CFBLineNumber参数直接写入有效行数(LCD_PIXEL_HEIGHT)。 还有一个参数LTDC_CFBPitch它用于配置上一行起始像素与下一行起始像素的数据增量,我们直接写入:行有效像素个数x3 即可。

(6) 配置显存首地址

每一层都有独立的显存空间,向LTDC_CFBStartAdress参数赋值可设置该层的显存首地址,我们把第1层的显存首地址直接设置成宏LCD_FRAME_BUFFER, 该宏表示的地址为0xD0000000,即SDRAM的首地址,从该地址开始,BUFFER_OFFSET个字节的空间都用作这一层的显存空间(BUFFER_OFFSET宏的值为800x480x3: 行有效像素宽度x行数x每个字节的数据量),向这些空间写入的数据会被解释成像素数据,LTDC会把这些数据传输到液晶屏上,所以我们要控制液晶屏的输出, 只要修改这些空间的数据即可,包括变量操作、指针操作、DMA操作以及DMA2D操作等一切可修改SDRAM内容的操作都支持。

实际设置中不需要刻意设置成SDRAM首地址,只要能保证该地址后面的数据空间足够存储该层的一帧数据即可。

(7) 向寄存器写入配置参数

赋值完后,调用库函数LTDC_LayerInit可把这些参数写入到LTDC的层控制寄存器,根据函数的第一个参数LTDC_Layer1/2来决定配置的是第1层还是第2层。

(8) 配置第2层控制参数

要想有混合效果,还需要使用第2层数据源,它与第1层的配置大致是一样的,主要区别是显存首地址和混合因子。在程序中我们把第2层的显存首地址设置成紧挨着第1层显存空间的结尾。 而混合因子都配置成PAxCA以便它的透明像素能参与运算,实现透明效果,但实际上我们并没有修改第2层像素数据的格式,它依然使用RGB888格式, 由于像素本身并没有Alpha通道的数据,所以是没有透明混合效果的。

正常使用时可把第2层配置成ARGB8888或ARGB1555格式,才能正常使用两层数据混合的功能。本程序没有配置透明格式主要是因为各种描绘函数(如画点、画线等)是要根据像素格式进行修改的。 两层使用不同的像素格式那么就要有两套同样功能的函数,容易造成混乱,而ARGB8888的数据量太大,所以我们把两层的像素格式都设置成了RGB888。

如果想了解使用透明像素格式如何使用,可把工程里的“bsp_lcd.h”文件的宏“LCD_RGB888”值修改为0,这样“bsp_lcd.c”文件会使用两层都配置为ARGB1555的格式及控制函数:

/*把这个宏设置成非0值 液晶屏使用RGB888色彩,若为0则使用ARGB1555色彩*/
#define LCD_RGB_888  1

(9) 重载LTDC配置并使能数据层

把两层的参数都写入到寄存器后,使用库函数LTDC_ReloadConfig让LTDC外设立即重新加载这些配置,并使用库函数LTDC_LayerCmd使能两层的数据源。 至此,LTDC配置就完成,可以向显存空间写入数据进行显示了。

辅助显示的全局变量及函数

为方便显示操作,我们定义了一些全局变量及函数来辅助修改显存内容,这些函数都是我们自己定义的,不是STM32标准库提供的内容。见 代码清单:DMA2D-8

代码清单:DMA2D-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
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
/*字体格式*/
typedef struct _tFont
{
    const uint16_t *table;    /*指向字模数据的指针*/
    uint16_t Width;           /*字模的像素宽度*/
    uint16_t Height;          /*字模的像素高度*/
} sFONT;
/*这些可选的字体格式定义在fonts.c文件*/
extern sFONT Font16x24;
extern sFONT Font12x12;
extern sFONT Font8x12;
extern sFONT Font8x8;

/**
* @brief  LCD Layer
*/
#define LCD_BACKGROUND_LAYER     0x0000
#define LCD_FOREGROUND_LAYER     0x0001

#define LCD_FRAME_BUFFER       ((uint32_t)0xD0000000)       //第一层首地址
#define BUFFER_OFFSET          ((uint32_t)800*480*3)     //一层液晶的数据量

/*用于存储当前选择的字体格式*/
static sFONT *LCD_Currentfonts;
/* 用于存储当前字体颜色和字体背景颜色的变量*/
static uint32_t CurrentTextColor   = 0x000000;
static uint32_t CurrentBackColor   = 0xFFFFFF;
/* 用于存储层对应的显存空间 和 当前选择的层*/
static uint32_t CurrentFrameBuffer = LCD_FRAME_BUFFER;
static uint32_t CurrentLayer = LCD_BACKGROUND_LAYER;

/**
* @brief  设置字体格式(英文)
* @param  fonts: 选择要设置的字体格式
* @retval None
*/
void LCD_SetFont(sFONT *fonts)
{
    LCD_Currentfonts = fonts;
}
/**
* @brief  选择要控制的层.
* @param  Layerx: 选择要操作前景层(第2层)还是背景层(第1层)
* @retval None
*/
void LCD_SetLayer(uint32_t Layerx)
{
    if (Layerx == LCD_BACKGROUND_LAYER)
    {
        CurrentFrameBuffer = LCD_FRAME_BUFFER;
        CurrentLayer = LCD_BACKGROUND_LAYER;
    }
    else
    {
        CurrentFrameBuffer = LCD_FRAME_BUFFER + BUFFER_OFFSET;
        CurrentLayer = LCD_FOREGROUND_LAYER;
    }
}

/**
* @brief  设置字体的颜色及字体的背景颜色
* @param  TextColor: 字体颜色
* @param  BackColor: 字体的背景颜色
* @retval None
*/
void LCD_SetColors(uint32_t TextColor, uint32_t BackColor)
{
    CurrentTextColor = TextColor;
    CurrentBackColor = BackColor;
}

/**
* @brief  设置字体颜色
* @param  Color: 字体颜色
* @retval None
*/
void LCD_SetTextColor(uint32_t Color)
{
    CurrentTextColor = Color;
}

/**
* @brief  设置字体的背景颜色
* @param  Color: 字体的背景颜色
* @retval None
*/
void LCD_SetBackColor(uint32_t Color)
{
    CurrentBackColor = Color;
}

(1) 切换字体大小格式

液晶显示中,文字内容占据了很大部分,显示文字需要有“字模数据”配合。关于字模的知识我们在下一章节讲解,在这里只简单介绍一下基本概念。字模是一个个像素点阵方块, 如上述代码中的sFont结构体,包含了指向字模数据的指针以及每个字模的像素宽度、高度,即字体的大小。本实验的工程中提供了像素格式为16x24、12x12、8x12、8x8的英文字模。 为了方便选择字模,定义了全局指针变量LCD_Currentfonts用来存储当前选择的字模格式,实际显示时根据该指针指向的字模格式来显示文字, 可以使用下面的LCD_SetFont函数切换指针指向的字模格式,该函数的可输入参数为: Font16x24/ Font12x12/ Font8x12/ Font8x8。

(2) 切换字体颜色和字体背景颜色

很多时候我们还希望文字能以不同的色彩显示,为此定义了全局变量CurrentTextColor和CurrentBackColor用于设定要显示字体的颜色和字体背景颜色,如:字体为红色和字体背景为蓝色

使用函数LCD_SetColors、LCD_SetTextColor以及LCD_SetBackColor可以方便修改这两个全局变量的值。若液晶的像素格式支持透明, 可把字体背景设置为透明值,实现弹幕显示的效果(文字浮在图片之上,透过文字可看到背景图片)。

(3) 切换当前操作的液晶层

由于显示的数据源有两层,在写入数据时需要区分到底要写入哪个显存空间,为此,我们定义了全局变量CurrentLayer和CurrentFrameBuffer用于存储要操作的液晶层及该层的显存首地址。 使用函数LCD_SetLayer可切换要操作的层及显存地址。

绘制像素点

有了以上知识准备,就可以开始向液晶屏绘制像素点了,见 代码清单:DMA2D-9

代码清单:DMA2D-9 绘制像素点
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
* @brief  显示一个像素点
* @param  x: 像素点的x坐标
* @param  y: 像素点的y坐标
* @retval None
*/
void PutPixel(int16_t x, int16_t y)
{
    if (x < 0 || x > LCD_PIXEL_WIDTH || y < 0 || y > LCD_PIXEL_HEIGHT)
    {
        return;
    }
    {
        /*RGB888*/
        uint32_t  Xaddress =0;
        Xaddress =  CurrentFrameBuffer + 3*(LCD_PIXEL_WIDTH*y + x);
    *(__IO uint16_t*) Xaddress= (0x00FFFF & CurrentTextColor);          //GB
    *(__IO uint8_t*)( Xaddress+2)= (0xFF0000 & CurrentTextColor) >> 16; //R
    }
}

这个绘制像素点的函数可输入x,y两个参数,用于指示要绘制像素点的坐标。得到输入参数后它首先进行参数检查,若坐标超出液晶显示范围则直接退出函数, 不进行操作。坐标检查通过后根据坐标计算该像素所在的显存地址, 液晶屏中的每个像素点都有对应的显存空间,像素点的坐标与显存地址有固定的映射关系,见表 显存存储像素数据的方式

显存存储像素数据的方式

当像素格式为RGB888时,每个像素占据3个字节,各个像素点按顺序排列。而且RGB通道的数据各占一个字节空间,蓝色数据存储在低位的地址, 红色数据存储右高位地址。据此可以得出像素点显存地址与像素点坐标存在以下映射关系:

像素点的显存基地址= 当前层显存首地址 + 每个像素点的字节数*(每行像素个数*坐标y+坐标x)

而像素点内的RGB颜色分量地址如下:

蓝色分量地址 = 像素点显存基地址

绿色分量地址 = 像素点显存基地址+1

红色分量地址 = 像素点显存基地址+2

利用这些映射关系,绘制点函数代入存储了当前要操作的层显存首地址的全局变量CurrentFrameBuffer计算出像素点的显存基地址Xaddress, 再利用指针操作把当前字体颜色CurrentTextColor中的RGB颜色分量分别存储到对应的位置。由于LTDC工作后会一直刷新显存的数据到液晶屏, 所以在下一次LTDC刷新的时候,被修改的显存数据就会显示到液晶屏上了。

掌握了绘制任意像素点颜色的操作后,就能随心所欲地控制液晶屏了,其它复杂的显示操作如绘制直线、矩形、圆形、文字、图片以及视频都是一样的, 本质上都是操纵一个个像素点而已。如直线由点构成,矩形由直线构成,它们的区别只是点与点之间几何关系的差异,对液晶屏来说并没有什么特别。

使用DMA2D绘制直线和矩形

利用上面的像素点绘制方式可以实现所有液晶操作,但直接使用指针访问内存空间效率并不高,在某些场合下可使用DMA2D搬运内存数据, 加速传输。绘制纯色直线和矩形的时候十分适合,代码清单:DMA2D-10

代码清单:DMA2D-10 使用DMA2D绘制直线
 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
/**
* @brief  LCD Direction
*/
#define LCD_DIR_HORIZONTAL       0x0000
#define LCD_DIR_VERTICAL         0x0001
/**
* @brief 显示一条直线
* @param Xpos: 直线起点的x坐标
* @param Ypos: 直线起点的y坐标
* @param Length: 直线的长度
* @param Direction: 直线的方向,可输入LCD_DIR_HORIZONTAL(水平方向)
                                        LCD_DIR_VERTICAL(垂直方向).
* @retval None
*/
void LCD_DrawLine(uint16_t Xpos, uint16_t Ypos, uint16_t Length,
                uint8_t Direction)
{
    DMA2D_InitTypeDef      DMA2D_InitStruct;

    uint32_t  Xaddress = 0;
    uint16_t Red_Value = 0, Green_Value = 0, Blue_Value = 0;

    /*计算目标地址*/
    Xaddress = CurrentFrameBuffer + 3*(LCD_PIXEL_WIDTH*Ypos + Xpos);

    /*提取颜色分量*/
    Red_Value = (0xFF0000 & CurrentTextColor) >>16;
    Blue_Value = 0x0000FF & CurrentTextColor;
    Green_Value = (0x00FF00 & CurrentTextColor)>>8 ;

    /* 配置DMA2D */
    DMA2D_DeInit();
    DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M;
    DMA2D_InitStruct.DMA2D_CMode = DMA2D_RGB888;
    DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value;
    DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value;
    DMA2D_InitStruct.DMA2D_OutputRed = Red_Value;
    DMA2D_InitStruct.DMA2D_OutputAlpha = 0x0F;
    DMA2D_InitStruct.DMA2D_OutputMemoryAdd = Xaddress;

    /*水平方向*/
    if (Direction == LCD_DIR_HORIZONTAL)
    {
        DMA2D_InitStruct.DMA2D_OutputOffset = 0;
        DMA2D_InitStruct.DMA2D_NumberOfLine = 1;
        DMA2D_InitStruct.DMA2D_PixelPerLine = Length;
    }
    else /*垂直方向*/
    {
        DMA2D_InitStruct.DMA2D_OutputOffset = LCD_PIXEL_WIDTH - 1;
        DMA2D_InitStruct.DMA2D_NumberOfLine = Length;
        DMA2D_InitStruct.DMA2D_PixelPerLine = 1;
    }
    DMA2D_Init(&DMA2D_InitStruct);
    /*开始DMA2D传输 */
    DMA2D_StartTransfer();
    /*等待传输结束 */
    while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC)== RESET);
}

这个绘制直线的函数输入参数为直线起始像素点的坐标,直线长度,以及直线的方向(它只能描绘水平直线或垂直直线), 函数主要利用了前面介绍的DMA2D初始化结构体,执行流程介绍如下:

(1) 计算起始像素点的显存位置

与绘制单个像素点一样,使用DMA2D绘制也需要知道像素点对应的显存地址。利用直线起始像素点的坐标计算出直线在显存的基本位置Xaddress。

(2) 提取直线颜色的RGB分量

使用DMA2D绘制纯色数据时,需要向它的寄存器写入RGB通道的数据,所以我们先把直线颜色CurrentTextColor的RGB分量提取到RED/ Green / Blue _Value变量值中。

(3) 配置DMA2D传输模式像素格式、颜色分量及偏移地址。

接下来开始向DMA2D初始化结构体赋值,在赋值前先调用了库函数DMA2D_DeInit,以便关闭DMA2D,因为它只有在非工作状态下才能重新写入配置。 配置时把DMA2D的模式设置成了DMA2D_R2M,以寄存器中的颜色作为数据源,即DMA2D_OutputGreen/Blue/Red/Alpha中的值, 我们向这些参数写入上面提取得到的颜色分量。DMA2D输出地址设置为上面计算得的Xaddress。

(4) 配置DMA2D的输出偏移、行数及每行的像素点个数

直线方向不同时,对DMA2D_OutputOffset(行偏移)、DMA2D_NumberOfLine(行的数量)及DMA2D_PixelPerLine(每行的像素宽度)这几个参数的配置是不一样的。 在显示垂直线的时候才需要行偏移,而在显示水平线的时候,由于水平线宽度只有一个像素点,只占据一行,像素点,不需要换行,所以行偏移设置为任意值都不影响。 行偏移的概念比较抽象,请参考前面解释“DMA2D初始化结构体”的内容理解。

(5) 写入参数到寄存器并传输

配置完DMA2D的参数后,调用库函数DMA2D_Init把参数写入到寄存器中,然后调用DMA2D_StartTransfer开始传输,然后检测标志位等待传输结束。

使用DMA2D绘制矩形

与绘制直线很类似,利用DMA2D绘制纯色矩形的方法见 代码清单:DMA2D-11

代码清单:DMA2D-11 使用DMA2D绘制矩形
 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
/**
* @brief  绘制实心矩形
* @param  Xpos: 起始X坐标
* @param  Ypos: 起始Y坐标
* @param  Height: 矩形高
* @param  Width: 矩形宽
* @retval None
*/
void LCD_DrawFullRect(uint16_t Xpos, uint16_t Ypos, uint16_t Width,
                        uint16_t Height)
{
    DMA2D_InitTypeDef      DMA2D_InitStruct;

    uint32_t  Xaddress = 0;
    uint16_t Red_Value = 0, Green_Value = 0, Blue_Value = 0;

    Red_Value = (0xFF0000 & CurrentTextColor)>>16 ;
    Blue_Value = 0x0000FF & CurrentTextColor;
    Green_Value = (0x00FF00 & CurrentTextColor)>>8;

    Xaddress = CurrentFrameBuffer + 3*(LCD_PIXEL_WIDTH*Ypos + Xpos);

    /* 配置DMA2D DMA2D */
    DMA2D_DeInit();
    DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M;
    DMA2D_InitStruct.DMA2D_CMode = DMA2D_RGB888;
    DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value;
    DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value;
    DMA2D_InitStruct.DMA2D_OutputRed = Red_Value;
    DMA2D_InitStruct.DMA2D_OutputAlpha = 0x0F;
    DMA2D_InitStruct.DMA2D_OutputMemoryAdd = Xaddress;
    DMA2D_InitStruct.DMA2D_OutputOffset = (LCD_PIXEL_WIDTH - Width);
    DMA2D_InitStruct.DMA2D_NumberOfLine = Height;
    DMA2D_InitStruct.DMA2D_PixelPerLine = Width;
    DMA2D_Init(&DMA2D_InitStruct);
    /* 开始DMA2D传输 */
    DMA2D_StartTransfer();

    /* 等待传输结束 */
    while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC)== RESET);
}

对于DMA2D来说,绘制矩形实际上就是绘制一条很粗的直线,与绘制直线的主要区别是行偏移、行数以及每行的像素个数。

27.8.2.3. main函数

最后我们来编写main函数,使用液晶屏显示图像,见 代码清单:DMA2D-12

代码清单:DMA2D-12 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
/**
* @brief  主函数
* @param  无
* @retval 无
*/
int main(void)
{
    /* LED 端口初始化 */
    LED_GPIO_Config();

    /*初始化液晶屏*/
    LCD_Init();
    LCD_LayerInit();
    LTDC_Cmd(ENABLE);

    /*把背景层刷黑色*/
    LCD_SetLayer(LCD_BACKGROUND_LAYER);
    LCD_Clear(LCD_COLOR_BLACK);

    /*初始化后默认使用前景层*/
    LCD_SetLayer(LCD_FOREGROUND_LAYER);
    /*默认设置不透明 ,该函数参数为不透明度,范围 0-0xff ,0为全透明,0xff为不透明

    LCD_SetTransparency(0xFF);
    LCD_Clear(LCD_COLOR_BLACK);
    /*经过LCD_SetLayer(LCD_FOREGROUND_LAYER)函数后,
    以下液晶操作都在前景层刷新,除非重新调用过LCD_SetLayer函数设置背景层*/

    LED_BLUE;

    Delay(0xfff);

    while (1)
    {
        LCD_Test();
    }
}

上电后,调用了LCD_Init、LCD_LayerInit函数初始化LTDC外设,然后使用LCD_SetLayer函数切换到背景层,使用LCD_Clear函数把背景层都刷成黑色, LCD_Clear实质是一个使用DMA2D显示矩形的函数,只是它默认矩形的宽和高直接设置成液晶屏的分辨率,把整个屏幕都刷成同一种颜色。 刷完背景层的颜色后再调用LCD_SetLayer切换到前景层,然后在前景层绘制图形。中间还有一个LCD_SetTransparency函数,它用于设置当前层的透明度, 设置后整一层的像素包含该透明值,由于整层透明并没有什么用(一般应用是某些像素点透明看到背景,而其它像素点不透明),我们把前景层设置为完全不透明。

初始化完成后,我们调用LCD_Test函数显示各种图形进行测试(如直线、矩形、圆形),具体内容请直接在工程中阅读源码,这里不展开讲解。 LCD_Test中还调用了文字显示函数,其原理在下一章节详细说明。

27.8.3. 下载验证

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