LV16-26-LCD-03-ILI9341驱动LCD

本文主要是STM32开发——ILI9341驱动LCD的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
  • 通用
分类 网址说明
官方网站https://www.arm.com/ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.st.com/content/st_com/zh.htmlST官方网站,在这里我们可以找到STM32的相关文档
https://www.stmcu.com.cn/意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档
http://elm-chan.org/fsw/ff/00index_e.htmlFatFs文件系统官网
教程书籍《ARM Cortex-M3权威指南》ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩
《ARM Cortex-M0权威指南》
《ARM Cortex-M3与Cortex-M4权威指南》
开发论坛http://47.111.11.73/forum.php开源电子网,正点原子的资料下载及问题讨论论坛
https://www.firebbs.cn/forum.php国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT
https://www.amobbs.com/index.php阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。
http://download.100ask.net/index.html韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。
博客参考http://www.openedv.com/开源网-原子哥个人博客
http://blog.chinaaet.com/jihceng0622博主是原Freescale现NXP的现场应用工程师
cortex-m-resources这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接)
- STM32
STM32STM32 HAL库开发实战指南——基于F103系列开发板野火STM32开发教程在线文档
STM32库开发实战指南——基于野火霸道开发板野火STM32开发教程在线文档
  • SD卡
SD Association提供了SD存储卡和SDIO卡系统规范
点击查看相关文件下载
STM32F103xx英文数据手册STM32F103xC/D/E系列的英文数据手册
STM32F103xx中文数据手册STM32F103xC/D/E系列的中文数据手册
STM32F10xxx英文参考手册(RM0008)STM32F10xxx系列的英文参考手册
STM32F10xxx中文参考手册(RM0008)STM32F10xxx系列的中文参考手册
Arm Cortex-M3 处理器技术参考手册-英文版Cortex-M3技术参考手册-英文版
STM32F10xxx Cortex-M3编程手册-英文版(PM0056)STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版
SD卡相关资料——最新版本有关SD卡的一些资料可以从这里下载
SD卡相关资料——历史版本有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议
SD 2.0 协议标准完整版这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看

一、相关资料

这一部分主要看一下我们有哪些资料可以看,注意这里的资料都是放在本地的笔记对应的文件夹中:

二、2.8寸LCD模块

我使用的是正点原子的2.8寸LCD模块,后边也是以这个模块进行学习。

1. 屏幕简介

ATK-MD0280-V2.4(V2.4 是版本号,下面均以 ATK-MD0280 表示该产品)是正点原子推出的一款高性能 2.8 寸电阻触摸屏模块。该模块屏幕分辨率为 320*240, 16 位真彩显示,采用 ILI9341/ST7789 驱动,该芯片直接自带 GRAM,无需外加驱动器,因而任何单片机,都可以轻易驱动。 ATK-MD0280 模块采用电阻触摸屏, 精度高,抗干扰能力强,稳定性好,但不支持多点触摸。

image-20230503205011631

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

2. 模块参数

项目 说明
接口类型 LCD: Intel8080-16 位并口 触摸屏: SPI
颜色格式 RGB565
颜色深度 16 位
LCD 驱动器芯片 ILI9341/ST7789
电阻触摸屏触摸芯片 XPT2046 / HR2046
LCD 分辨率 320*240
触摸屏类型 电阻触摸
触摸点数 单点触摸
屏幕尺寸 2.8’
工作温度 -20℃~70℃
存储温度 -30℃~80℃
外形尺寸 51mm*82.6mm

3. 电气特性

项目 说明
电源电压 背光: 5V 其他: 3.3V
IO 口电平 3.3V
功耗 30~113mA

(1)3.3V 系统,可以直接接本模块(供电必须 5V&3.3V 双供电),如果是 5V 系统,建议串接 100R左右电阻,做限流处理。

(2)30mA 对应背光关闭时的功耗, 113mA 对应背光最亮时的功耗,此数据是模块的总功耗(包括3.3V 和 5V 电源部分),实际应用中功耗会由于电源电压的波动而略微变化。

4. 模块特点

(1)320× 240 的分辨率;

(2)自带驱动,无需外加驱动器,单片机直接使用;

(3)板载背光电路,只需要 3.3V&5V 供电即可,无需外加高压;

(4)接口简单(LCD 采用 16 位 8080 并口,触摸屏采用 SPI 接口),使用方便;

(5)PCB 尺寸为 51mm*82.6mm,并带有安装孔位,安装方便;

(6)16 位真彩显示(65536色)。

【注意】模块是3.3V供电的,不支持5V电压的MCU,如果是5V MCU,必须在信号线串接120R电阻使用。

5. 模块原理图

这个原理图是来自于模块自带的资料。

image-20230503205516509

【注意】DB1DB8,DB10DB17,总是按顺序连接MCU的FSMC_D0~D15

序号 名称 说明
1 CS LCD 片选信号(低电平有效)
2 RS 命令/数据控制信号(0,命令; 1,数据;)
3 WR 写使能信号(低电平有效)
4 RD 读使能信号(低电平有效)
5 RST 复位信号(低电平有效)
6~21 D0~D15 双向数据总线
22,26,27 GND 地线
23 BL_CTR 背光控制引脚(高电平点亮背光,低电平关闭)
24,25 VCC3.3 主电源供电引脚(3.3V)
28 VCC5 背光供电引脚(5V)
29 MISO NC,电容触摸屏未用到
30 MOSI 电容触摸屏 IIC_SDA 信号(CT_SDA)
31 PEN 电容触摸屏中断信号(CT_INT)
32 BUSY NC,电容触摸屏未用到
33 CS 电容触摸屏复位信号(CT_RST)
34 CLK 电容触摸屏 IIC_SCL 信号(CT_SCL)

6. 与STM32接线

TFTLCD 模块采用 2*17 的 2.54 公排针与外部连接(即接开发板的接口),与STM32引脚接口定义如图所示 :

image-20230503210953410

7. 裸屏引脚

我们来看一下00ATK-MD0280裸屏尺寸图(钢化玻璃TP).pdf中的LCD裸屏引脚图:

image-20230517232250346

这是裸屏的引脚排布,裸屏是接了一个ILI9341液晶控制器,我们通过控制ILI9341控制器来驱动LCD屏幕。

三、ILI9341驱动

1. 简介

ILI9341是奕力科技股份有限公司 (ilitek.com)的一款小尺寸驱动IC,我在官网没看到芯片手册,但是有些查datasheet的地方是可以查到的,这里就不写链接了。我在本地保存了一份:01ILI9341_DS.pdf

它是一个支持分辨率为240x320点阵的a-TFT LCD 的262144(26万色)色单片驱动器。这个单片驱动器包含了一个720通道的源极驱动器(source driver),一个320通道的栅极驱动器(gate driver),自带一个172800字节(320x240x18/8)的GRAM用于显示240x320分辨率的图片数据。ILI9341提供8位/9位/16位/18位的并行MCU数据总线,6位/16位/18位RGB接口数据总线以及3或4线SPI接口(serial peripheral interface),工作于1.65V-3.3V。

2. 内部框图

我们可以查看01ILI9341_DS.pdf的3. Block Diagram :

image-20230519211819014

2.1 ① 控制和信号引脚

①为控制引脚和信号引脚,支持DBI B类接口( Intel 8080接口)、 DPI接口、 MIPI DSI接口输入。我们可以根据其不同状态设置可以使芯片工作在不同的模式,如每个像素点的位数是6、16还是18位;可配置使用SPI接口、8080接口还是RGB接口与MCU进行通讯。

MCU可以通过SPI、8080接口或RGB接口与ILI9341进行通讯,从而访问它的控制寄存器(CR)、地址计数器(AC)、及GRAM。经过索引寄存器( IR)、 控制寄存器( CR)、地址寄存器( AC)、读数据寄存器( RDR)、写数据寄存器( WDR)到GRAM。最后, GRAM把显示内容传输到LCD屏幕的显示。

控制引脚IM[2:0]用于设置控制器的接口模式,如下表所示(01ILI9341_DS.pdf的6. Block Function Description )。 我使用的这个显示屏支持的接口为16 Bit的MIPI-DBIB类,也就是8080接口,使用16根数据线,支持RGB565格式,因此IM[2:0]值为001,该值由屏幕供应商生产时硬件设置,此时用到的数据引脚为DB[15:0]。

image-20230519212447766

2.2 ② GRAM

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

2.3 ③ LED控制器

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

3. 与STM32引脚对应关系

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

image-20230519214635944

我们这一节笔记用到的信号线如下表(注意我使用的模块只引出了DB[15:0]使用):

信号线 ILI9341对应的信号线 说明
LCD_DB[17:1] D[15:0] 16位双向数据线
LCD_RD RDX LCD读数据信号,低电平有效
LCD_RS D/CX 数据/命令信号,高电平时,D[15:0]表示的是数据(RGB像素数据或命令数据),低电平时D[15:0]表示控制命令
LCD_RST RESX 硬复位信号,低电平有效
LCD_WR WRX LCD写数据信号,低电平有效
LCD_CS CSX LCD片选信号,低电平有效
LCD_BL - LCD的背光信号,低电平点亮

模块对外接口采用16位并口,颜色深度为16位,格式为RGB565,关系如下图:

image-20230519225138990

4. 向ILI9341写的时序

我们可以看01ILI9341_DS.pdf 的7.1.3. Write Cycle Sequence 一节:

image-20230519215250956

(1)写命令时序由片选信号CSX拉低开始;

(2)对数据/命令选择信号线D/CX也置低电平,这表示写入的是命令地址(可理解为命令编码,如软件复位命令:0x01);

(3)以写信号WRX为低,读信号RDX为高,表示数据传输方向为写入,在WR的上升沿,使D[17:0]上的数据写入到ILI9341里面。同时,在数据线D[17:0](或D[15:0])输出命令地址。

(4)在第二个传输阶段传送的是命令的参数,所以D/CX要置高电平,表示写入的是命令数据,命令数据是某些指令带有的参数,如复位指令编码为0x01,它后面可以带一个参数,该参数表示多少秒后复位(实际的复位命令不含参数,此处只是为了先了解一下指令编码与参数的区别)。

我们看一个更详细的图:

image-20230519215852281

首先片选信号CS拉低,复位信号RES保持为高。 数据/命令切换信号D/C拉低,写信号引脚拉低,此时DB[15:0]发送的就是指令(黄色部分) ;数据/命令切换信号D/C拉高,写信号引脚拉低,此时DB[15:0]发送的就是一个像素点的颜色数据( 红、绿、蓝部分) ,其中低5位为蓝色数据、中6位为绿色数据、高5位为红色数据。

5. 从ILI9341读的时序

这里我们一般用的不多,这里简单提一下吧,我们可以看01ILI9341_DS.pdf 的7.1.4. Read Cycle Sequence 一节:

image-20230519220200874

先根据要读取的数据的类型,设置RS为高(数据)/低(命令),然后拉低片选,选中ILI9341,接着我们根据是读数据,置RD为低,然后读数据,在RD的上升沿, 读取数据线上的数据(D[15:0])。

6. 命令与寄存器

这一部分我们可以查看01ILI9341_DS.pdf 的8. Command一节,这个后边开始学习驱动LCD的代码的时候结合来看会更加容易理解。这里简单学习几个吧。学到这里,其实觉得挺奇怪的,在芯片手册中,没有看到寄存器的相关概念,也可能是我没有仔细看吧,手册中都是一些命令,但是这些命令控制的应该都是寄存器才对,所以后边这些的话,就可以看作是命令去控制的寄存器。

6.1 命令格式说明

ILI9341所有的指令都是 8 位的(高 8 位无效),且参数除了读写GRAM的时候是 16 位,其他操作参数,都是 8 位的。

6.2Read ID4 (D3h)

该指令为读ID4指令,用于读取LCD控制器的ID 。因此,同一个代码,可以根据ID的不同,执行不同的LCD驱动初始化,以兼容不同的LCD屏幕。

image-20230519225401282

6.3 Memory Access Control (36h)

该指令为存储访问控制指令,可以控制ILI9341存储器的读写方向,简单的说,就是在连续写GRAM的时候,可以控制GRAM指针的增长方向,从而控制显示方式(读GRAM也是一样)。

image-20230519225604394

6.4 . Column Address Set (2Ah)

该指令是列地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置横坐标(x坐标):

image-20230519225636754

在默认扫描方式时,该指令用于设置x坐标,该指令带有4个参数,实际上是2个坐标值:SC和EC,即列地址的起始值和结束值,SC必须小于等于EC,且0≤SC/EC≤239。一般在设置x坐标的时候,我们只需要带2个参数即可,也就是设置SC即可,因为如果EC没有变化,我们只需要设置一次即可(在初始化ILI9341的时候设置),从而提高速度。

6.5 Page Address Set (2Bh)

该指令是页地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置纵坐标(y坐标):

image-20230519225718890

在默认扫描方式时,该指令用于设置y坐标,该指令带有4个参数,实际上是2个坐标值:SP和EP,即页地址的起始值和结束值,SP必须小于等于EP,且0≤SP/EP≤319。一般在设置y坐标的时候,我们只需要带2个参数即可,也就是设置SP即可,因为如果EP没有变化,我们只需要设置一次即可(在初始化ILI9341的时候设置),从而提高速度。

6.6 Memory Write (2Ch)

该指令是写GRAM指令,在发送该指令之后,我们便可以往LCD的GRAM里面写入颜色数据了,该指令支持连续写 (地址自动递增):

image-20230519225754328

在收到指令0X2C之后,数据有效位宽变为16位,我们可以连续写入LCD GRAM值,而GRAM的地址将根据MY/MX/MV设置的扫描方向进行自增。例如:假设设置的是从左到右,从上到下的扫描方式,那么设置好起始坐标(通过SC,SP设置)后,每写入一个颜色值,GRAM地址将会自动自增1(SC++),如果碰到EC,则回到SC,同时SP++,一直到坐标:EC,EP结束,其间无需再次设置的坐标,从而大大提高写入速度。

6.7 Memory Read (2Eh)

该指令是读GRAM指令,用于读取ILI9341的显存(GRAM),同0X2C指令,该指令支持连续读 (地址自动递增):

image-20230519225832833

ILI9341在收到该指令后,第一次输出的是dummy数据(无效),第二次开始,读取到的才是有效的GRAM数据(从坐标:SC,SP开始),输出规律为:每个颜色分量占8个位,一次输出2个颜色分量。比如:第一次输出是R1G1,随后的规律为:B1R2→G2B2→R3G3→B3R4→G4B4→R5G5… 以此类推

7. 内存显示地址映射

这一部分可以参考01ILI9341_DS.pdf 的9.2. Memory to Display Address Mapping 一节,对后边理解扫描方向会有好处。其实这一部分笔记有一些是从网上看的,有一些概念看的我挺不明所以的,后来来起来按照我自己的理解,这里GRAM是一个虚拟内存,缓存我们要显示的数据,我们通过MCU向GRAM写入数据,然后LCD屏幕会有一个类似“电子枪”的东西,从GRAM中取出像素点的数据,然后按照特定的扫描方式,点亮LCD面板的真实像素点,这些真实的像素点,按照我自己的理解,我觉得可以理解成LCD的物理内存。

7.1 GRAM与LCD面板

这里的模式为Normal Display ON or Partial Mode ON, Vertical Scroll Mode OFF,也就是说下图只讨论“正常显示”,不讨论“垂直滚动显示”模式。

image-20230523221724206

可以看到GRAM内存被两个指针访问,行指针和列指针,行指针范围从0000h到0013Fh(换算成十进制就是0到319,一共320行),列指针范围为0000h到00EFh(换算成十进制就是0到239,一共240列)。也就是说,GRAM和LCD像素面板的对应关系是一种竖屏(240*320)的对应的关系。至于如何让GRAM数据显示到LCD屏上,不需要我们去考虑,只要知道这种对应关系就可以了。我们要是想要在最左上角显示一个点,可以将将点数据存储在GRAM(列指针,页指针)=(0,0)处。

当实际将GRAM中的数据刷新到LCD屏进行显示的时候,ILI9341有8种显示方式:左上角→右下角(竖屏)、左下角→右上角(竖屏)、右上角→左下角(竖屏)、右下角→左上角(竖屏)、左上角→右下角(横屏)、左下角→右上角(横屏)、右上角-→左下角(横屏)、右下角→左上角(横屏),既然GRAM与LCD像素面板的点是固定对应的,那么不同的显示模式又是怎么实现的呢?

按照我自己的理解,GRAM与LCD面板的像素点的地址是一一对应的,这个对应关系是无法被改变的,前边我们知道,要想显示一幅图像,有一个行同步信号,比如就从左上角开始显示,当显示完一行的时候,会自动进行下一行的显示,显示完一帧图像的时候,会产生一个帧同步信号,进行下一帧的显示,此时就显示到了右下角。当然这里的扫描针对的是LCD显示屏,而非GRAM,可以想象一下,我们有一个写在GRAM(0,0)位置的像素点,现在我们要开始显示,我们在LCD屏的右下角开始显示,那么读取的这一个像素的数据就会被显示到LCD屏的右下角。

7.2 MCU对GRAM的写/读方向

image-20230523222310976

从这张图可以看出,我们通过MCU向GRAM写入数据的时候,数据按照上面所示的顺序写入,B为起始点,E为结束点,按照从左到右,从上到下的顺序进行写入,写入完一行数据后,切换到下一行进行显示。

接下来就是将GRAM中的数据刷新到到LCD的像素面板(也可以称之为物理内存吧,单证目前我是这么认为的)进行显示,在物理内存中写入数据的计数器由“Memory Data Access Control(36h)”命令控制,B5、B6和B7位如下所述:

image-20230524123443366

从图中可以看出,B5位对应的是 MADCTL (Memory Access Control) 寄存器中的D5,表示交换行列,B6对应的是D6,表示列地址顺序,B7对应的是D7,表示的是行地址顺序。多以这里的B[7:5]就对应MADCTL 的D[7:5],这三位共同决定了LCD的八种扫描方式。图中的四个角有四个说明,它们都是在不关心行列交换的情况下,另外两位取不同值的时候整个LCD显示的时候扫描的起始点。比如,B6=0,B7=1,这个时候LCD开始显示的时候,起始点就是左下角的(0,319),对于虚拟地址来说就会显示GRAM中(0, 0)这个位置的数据,当然,这个值是针对竖屏240*320来讲的。

详细的GRAM中的虚拟地址到LCD中的物理地址的转换关系如下表:

image-20230524124345341

于是我们从GRAM到LCD就有8种扫描模式啦。

7.2 8种显示方向

一些说明:

(1)MV、MX、MY的控制位在命令 0x36 对应的寄存器中

(2)Image in the Memory(MPU)中描述的是在MCU的240(宽)* 320(高)* 16bit(RGB565显示方式)中显示了一个图像“F”。

(3)Image In the Driver(Frame Memory)描述的是经过虚拟地址到物理地址转换器后,实际传输到GRAM中的数据显示阵列。

(4)B、E分别描述了MCU传输的数据流的起始位置和终止位置,下边的图都是来自于01ILI9341_DS.pdf 的9.3. MCU to memory write/read direction一节。这里需要注意,从GRAM读取像素点的数据,这个顺序按照从左到右,从上到下的顺序进行,然后送到LCD显示的时候,根据MY、MX和MV三个位的不同值,从指定的位置开始,到这个位置的对角结束,按照先行后列的方式进行扫描显示

其实不论哪种显示方向,其实并没有改变GRAM物理内存与LCD显示屏的对应关系,也即是说GRAM物理内存与显示屏之间的对应关系、内存到LCD的扫描方式,是固定不变的。那么这种显示方向是怎么出现的?

实际上,显示方向说的是MCU的显示缓存MPU(或者MCU读写GRAM的数据流)与LCD显示屏的对应关系。由于GRAM地址与LCD显示屏像素点的物理地址的对应关系是不会改变的。

用户在写程序的过程中,LCD显示操作是更改MPU的内容,至于MPU到GRAM的传输是驱动程序完成的,就是前边学习的8080时序。也就是说,用户控制显示的内容,接触的是MPU(其实就是GRAM啦),而更改显示方向需要配置ILI9341的寄存器。其实,ILI9341的扫描方向的功能也可以没有,但是这个时候要想改变显示的方向之类的,需要用户自己软件进行转换,直接在GRAM中进行转换,然后刷新到LCD屏幕,其实ILI9341配置的那3位就相当于帮我们做了一个转换关系表,这样就不用我们自己去配置了。

7.2.1 Normal

image-20230523223202157

这是正常的模式,以(x,y)表示(列地址,行地址),那么MCU向(0,0)写入数据,经过虚拟地址到物理地址的转换,实际写入到GRAM的地址是(0,0),对应LCD的左上角。MCU向(239,319)写入数据,经过虚拟地址到物理地址的转换,实际写入到GRAM的地址是(239,319),对应LCD的右下角。

image-20230523224446808
B[7:5] D[7:5] MY MX MV D[7:5]十进制
000 000 0 0 0 0

7.2.2 Y-Mirror

image-20230523223509624

这是Y镜像,也就是行地址翻转的情况,以(x,y)表示(列地址,行地址),那么MCU向(0,0)写入数据,经过虚拟地址到物理地址的转换,实际写入到GRAM的地址是(0,319),对应LCD的左下角:

image-20230524130422020

MCU向(239,319)写入数据,经过虚拟地址到物理地址的转换,实际写入到GRAM的地址是(239,0),对应LCD的右上角。最终的效果就是LCD的显示实现了Y方向上的翻转。

image-20230523224720981
B[7:5] D[7:5] MY MX MV D[7:5]十进制
100 100 1 0 0 4

7.2.3 X-Mirror

image-20230523223647692
B[7:5] D[7:5] MY MX MV D[7:5]十进制
010 010 0 1 0 2

7.2.4 X/Y-Mirror

image-20230523223825411
B[7:5] D[7:5] MY MX MV D[7:5]十进制
110 110 1 1 0 6

7.2.5 X-Y Exchange

image-20230523223947867

这是XY交换,也就是行列地址交换的情况,以(x,y)表示(列地址,行地址),MCU向(0,0)写入数据,经过虚拟地址到物理地址的转换,实际写入到GRAM的地址是(0,0),对应LCD的左上角。MCU向(239,319)写入数据,经过虚拟地址到物理地址的转换,实际写入到GRAM的地址是(319,239),对应LCD的右下角。经过这样的变换,LCD就从原来的竖屏变成了横屏显示。最终的效果就是LCD的显示实现了行列的交换。

B[7:5] D[7:5] MY MX MV D[7:5]十进制
001 001 0 0 1 1

7.2.6 X-Y Exchange Y-Mirror

image-20230523224100259
B[7:5] D[7:5] MY MX MV D[7:5]十进制
101 101 1 0 1 5

7.2.7 XY Exchange X-Mirror

image-20230523224159218
B[7:5] D[7:5] MY MX MV D[7:5]十进制
011 011 0 1 1 3

7.2.8 XY Exchange XY-Mirror

image-20230523224301780
B[7:5] D[7:5] MY MX MV D[7:5]十进制
111 111 1 1 1 7

7.2.9 八种显示方向对应关系

B[7:5] D[7:5] MY MX MV D[7:5]十进制 模式说明 图片对应
000 000 0 0 0 0 Normal image-20230524133046886
001 001 0 0 1 1 X-Y Exchange image-20230524133125714
010 010 0 1 0 2 X-Mirror image-20230524133202987
011 011 0 1 1 3 XY Exchange X-Mirror image-20230524215000828
100 100 1 0 0 4 Y-Mirror image-20230524215142332
101 101 1 0 1 5 X-Y Exchange Y-Mirror image-20230524215313557
110 110 1 1 0 6 X/Y-Mirror image-20230524215421446
111 111 1 1 1 7 XY Exchange XY-Mirror image-20230524215619493

四、FSMC模拟8080时序

1. FSMC?

ILI9341的8080通讯接口时序可以由STM32使用普通I/O接口进行模拟,但这样效率太低,STM32提供了一种特别的控制方法——使用FSMC接口实现8080时序。

image-20230519221727962

我们前边学习SRAM的时候了解到STM32的FSMC外设可以用于控制扩展的外部存储器,而MCU对液晶屏的操作实际上就是把显示数据写入到显存中,与控制存储器非常类似,且8080接口的通讯时序完全可以使用FSMC外设产生,因而非常适合使用FSMC控制液晶屏。

2. NOR闪存,非复用接口

控制LCD时,适合使用FSMC的NOR\PSRAM模式,它与前面使用FSMC控制SRAM的稍有不同,控制SRAM时使用的是模式A,而控制LCD时使用的是与NOR FLASH一样的模式B,所以我们重点分析前边框图中NOR FLASH控制信号线部分。我们可以看[STM32中文参考手册](https://www.stmcu.com.cn/Designresource/detail/localization_document /710001)的19.5.1 外部存储器接口信号:

FSMC信号名称 信号方向 功能
CLK 输出 时钟信号( 用于同步访问的外部存储器)
A[25:16] 输出 地址总线
AD[15:0] 输入/输出 16位复用的,双向地址/数据总线
NEx 输出 Bank1区域内片选信号, x=1~4,每个区域大小为64MB
NOE 输出 读使能信号
NWE 输出 写使能信号
NWAIT 输入 NOR闪存要求FSMC等待的信号

NOR闪存存储器是按16位的字寻址,最大容量达64M字节(26条地址线)。在控制LCD时,使用的是类似异步、地址与数据线独立的NOR FLASH控制方式,所以实际上CLK、NWAIT、NADV引脚并没有使用到。

3. ILI9341与FSMC怎么连接?

首先我们回顾下外部 SRAM的连接,外部 SRAM 的控制一般有:地址线(如 A0A18)、数据线(如 D0D15)、写信号(WE)、读信号(OE)、片选信号(CS),如果 SRAM 支持字节控制,那么还有 UB/LB 信号。

而 TFTLCD的信号我们在前边有介绍,包括: D/C、 D0 ~ D15、 WR、 RD、 CS、 RST 和 BL 等,其中真正在操作 LCD 的时候需要用到的就只有: D/C、D0 ~ D15、 WR、 RD 和 CS。其操作时序和 SRAM的控制完全类似,唯一不同就是 TFTLCD 有 RS 信号,但是没有地址信号。

TFTLCD 通过 D/C 信号来决定传送的数据是数据还是命令,本质上可以理解为一个地址信号, 比如我们把 D/C 接在 FSMC_A0 上面,那么当 FSMC 控制器写地址 0 的时候,会使得 FSMC_A0 变为 0,对 TFTLCD 来说,就是写命令。而 FSMC 写地址 1 的时候, FSMC_A0 将会变为 1,对 TFTLCD 来说,就是写数据了。这样,就把数据和命令区分开了,他们其实就是对应 SRAM 操作的两个连续地址。当然 D/C 也可以接在其他地址线上,战舰 STM32 开发板是把 D/C 连接在 FSMC_A10 上面的。 所以两者的对应关系可以如下表(带X的表示低电平有效):

FSMC信号 功能 8080信号 功能
NEx Bank1片选信号 = CSX 控制器片选
NOE 读使能 = RXD 读信号引脚
NEW 写使能 = WRX 写信号引脚
D[15:0] 数据总线 = DB[15:0] 数据总线
A[25:0] 地址总线 D/CX 数据/命令切换信号

4. FSMC NOR/PSRAM模式B

我们重点来看一下模式B的写时序(读时序一般不怎么用,因为我们主要是向LCD写数据来进行显示用的,而且我们看一下读时序就知道其实对应关系是一样的,所以我们这里只分析写时序就可以啦,读时序是一样的)与ILI9341的写时序的对应关系:

image-20230519223940847

我们可以看到除了D/CX信号外,剩下的几个信号的时序就是一致的,看到这个图,我们就该明白为什么FSMC可以拿来模拟8080时序啦。但是还有一个问题,那就是D/CX这个信号怎么办呢?要是FSMC也可以产生高低电平来使控制D/CX那不就好了吗?

当我们使用FSMC访问特定地址时, FSMC 会产生相应的模拟 8080 时序,控制地址线输出要访问的内存地址,使用数据信号线接收或发送数据,其它片选信号 NE、读使能信号 NOE、写使能信号NWE 辅助产生完整的时序,而由于控制液晶屏的硬件连接中,使用如图下中的连接来模拟 8080 时序,所以 FSMC 产生的这些信号会被 ILI9341 接收,并且使用其中一根 FSMC_Ax 地址控制命令/数据选择引脚 RS(即 D/CX),因此,我们要是知道 STM32 访问什么地址时,对应的 FSMC_Ax 引脚会输出高电平表示传输的是数据,访问什么地址时,对应的 FSMC_Ax 引脚会输出低电平表示传输的是命令。若理解了计算过程,以后就可以根据自己制作的硬件电路来计算访问地址了。

image-20230519230605271

根据STM32对寻址空间的地址映射,地址0x6000 0000 ~0x9FFF FFFF是映射到外部存储器的,而其中的0x6000 0000 ~0x6FFF FFFF则是分配给NOR FLASH、PSRAM这类可直接寻址的器件。当FSMC外设被配置成正常工作,并且外部接了NOR FLASH时,若向0x60000000地址写入数据如0xABCD,FSMC会自动在各信号线上产生相应的电平信号,写入数据。FSMC会控制片选信号NE1选择相应的NOR 芯片,然后使用地址线A[25:0]输出0x60000000,在NWE写使能信号线上发出低电平的写使能信号,而要写入的数据信号0xABCD则从数据线D[15:0]输出,然后数据就被保存到NOR FLASH中了。

我们假设,现在将D/CX接在FSMC_A0上,由于FSMC会自动产生地址信号,当使用FSMC向0x6xxx xxx1、0x6xxx xxx3、0x6xxx xxx5…这些奇数地址写入数据时,地址最低位的值均为1,所以它会控制地址线A0(D/CX)输出高电平,那么这时通过数据线传输的信号会被理解为数值;若向0x6xxx xxx0 、0x6xxx xxx2、0x6xxx xxx4…这些偶数地址写入数据时,地址最低位的值均为0,所以它会控制地址线A0(D/CX)输出低电平,因此这时通过数据线传输的信号会被理解为命令,如下表:

地址 地址的二进制值(低四位) A0(D/CX)的电平 控制ILI9341时的意义
0x6xxx xxx1 0001 1 高电平 D 数值
0x6xxx xxx3 0011 1 高电平 D 数值
0x6xxx xxx5 0101 1 高电平 D 数值
0x6xxx xxx0 0000 0 低电平 C 命令
0x6xxx xxx2 0010 0 低电平 C 命令
0x6xxx xxx4 0100 0 低电平 C 命令

有了这个基础,我们只要配置好FSMC外设,然后在代码中利用指针变量,向不同的地址单元写入数据,就能够由FSMC模拟出的8080接口向ILI9341写入控制命令或GRAM的数据了。

注意:在实际控制时,以上地址计算方式还不完整,还需要注意HADDR内部地址与FSMC地址信号线的转换,关于这部分内容在代码讲解时再详细举例说明.

5. FSMC NOR/PSRAM模式A

后边在实际实现LCD驱动的时候,发现,模式A好像也可以使用:

  • 读时序
image-20230520191544089
  • 写时序
image-20230520191715157

对比过后,模式A好像也能用,反正不管用哪种模式,主要还是ADDSET和DATAST的确定。

6. 地址计算

我们上边知道可以吧D/CX接在FSMC_A[25:0]任意一个引脚上,接在A0的情况上边已经分析了,那要是不使用A0,我们怎么计算ILI9341的命令和数据所对应的FSMC需要发送的地址呢?

(1)我们根据开发板的原理图,我们假设使用的是 FSMC_NE1 作为 8080_CS 片选信号,所以首先可以确认地址范围,当访问0X6000 0000 ~ 0X63FF FFFF 地址时, FSMC 均会对外产生片选有效的访问时序;

(2)我们假设使用 FSMC_A16 地址线作为命令/数据选择线 D/C 信号,所以在以上地址范围内,再选择出使得 FSMC_A16 输出高电平的地址,即可控制表示数据,选择出使得 FSMC_A16 输出低电平的地址,即可控制表示命令

(3)计算高低电平对应的地址:

要使 FSMC_A16 地址线为高电平,实质是输出地址信号的第 16 位为 1 即可,使用 0X60000000~0X63FF FFFF 内的任意地址,作如下运算:

1
设置地址的第 16 位为 1: 0X6000 0000 |= (1<<16) = 0x6001 0000

要使 FSMC_A16 地址线为低电平,实质是输出地址信号的第 16 位为 0 即可,使用 0X60000000~0X63FF FFFF 内的任意地址,作如下运算

1
设置地址的第 16 位为 0: 0X6000 0000 &= ~ (1<<16) = 0x6000 0000

(4)但是,以上方法计算的地址还不完全正确,STM32 内部访问地址时使用的是内部 HADDR 总线,它是需要转换到外部存储器的内部 AHB 地址线,它是字节地址 (8 位),而存储器访问不都是按字节访问,因此接到存储器的地址线依存储器的数据宽度有所不同。

image-20230519231123433

而我们使用的是 16 位的数据访问方式,所以 HADDR 与 FSMC_A 的地址线连接关系会左移一位,如 HADDR[1] 与 FSMC_A[0] 对应、 HADDR[2] 与 FSMC_A[1] 对应。因此,当 FSMC_A0 地址线为 1 时,实际上内部地址的第 1 位为 1, FSMC_A1 地址线为 1 时,实际上内部地址的第 2 位为1。同样地,当希望 FSMC_A16 地址输出高电平或低电平时,需要重新调整计算公式:

要使 FSMC_A16 地址线为高电平,实质是访问内部 HADDR 地址的第 (16+1) 位为 1 即可,使用 0X6000 0000~0X63FF FFFF 内的任意地址,作如下运算:

1
使 FSMC_A16 地址线为高电平: 0X6000 0000 |= (1 << (16+1)) = 0x6002 0000

要使 FSMC_A16 地址线为低电平,实质是访问内部 HADDR 地址的第 (16+1) 位为 0 即可,使用 0X6000 0000~0X63FF FFFF 内的任意地址,作如下运算:

1
使 FSMC_A16 地址线为低电平: 0X6000 0000 &= ~ (1<<(16+1)) = 0x6000 0000

根据最终的计算结果,总结如下:当 STM32 访问内部的 0x6002 0000 地址时, FSMC 自动输出时序,且使得与液晶屏的数据/命令选择线 RS(即 D/CX) 相连的 FSMC_A16 输出高电平,使得液晶屏会把传输过程理解为数据传输;类似地,当 STM32 访问内部的 0X6000 0000 地址时, FSMC自动输出时序,且使得与液晶屏的数据/命令选择线 RS(即 D/CX) 相连的 FSMC_A16 输出低电平,使得液晶屏会把传输过程理解为命令传输。

(5)我们可以将上述结果封装成一个宏,方便使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/****************************************************************
2^26 =0X0400 0000 = 64MB, 每个 BANK 有 4*64MB = 256MB
64MB:FSMC_Bank1_NORSRAM1:0X6000 0000 ~ 0X63FF FFFF
64MB:FSMC_Bank1_NORSRAM2:0X6400 0000 ~ 0X67FF FFFF
64MB:FSMC_Bank1_NORSRAM3:0X6800 0000 ~ 0X6BFF FFFF
64MB:FSMC_Bank1_NORSRAM4:0X6C00 0000 ~ 0X6FFF FFFF

选择 BANK1-BORSRAM1 连接 TFT,地址范围为 0X6000 0000 ~ 0X63FF FFFF
FSMC_A16 接 LCD 的 DC(寄存器/数据选择) 脚寄存器基地址 = 0X6C00 0000
RAM 基地址 = 0X6002 0000 = 0X6000 0000+2^16*2 = 0X6000 0000 + 0x2 0000 = 0X6002 0000
当选择不同的地址线时,地址要重新计算
********************************************************/

/*************** ILI9341 显示屏的 FSMC 参数定义 ****************/
//FSMC_Bank1_NORSRAM 用于 LCD 命令操作的地址
#define FSMC_Addr_ILI9341_CMD ( ( uint32_t ) 0x60020000 )

//FSMC_Bank1_NORSRAM 用于 LCD 数据操作的地址
#define FSMC_Addr_ILI9341_DATA ( ( uint32_t ) 0x60000000 )

【注意】其实吧,通过这种方式计算得来的地址,并不是所有的地址都能用,不能用的时候注意换一个试一下,我后边写代码的时候用A10测了一下,最低的4个位不能是奇数,至少我学习这个的时候看到是这样的规律随后没有深究,可能是存在一个对齐问题吧,后边有坑了再补充。

6. 读写IL9341

FSMC连接好外部的存储器并初始化后,就可以直接通过访问地址来读写数据。FSMC访问存储器的方式与I2C EEPROM、SPI FLASH的不一样,后两种方式都需要控制I2C或SPI总线给存储器发送地址,然后获取数据;在程序里,这个地址和数据都需要分开使用不同的变量存储,并且访问时还需要使用代码控制发送读写命令。而使用FSMC外接存储器时,其存储单元是映射到STM32的内部寻址空间的;在程序里,定义一个指向这些地址的指针,然后就可以通过指针直接修改该存储单元的内容,FSMC外设会自动完成数据访问过程,读写命令之类的操作不需要程序控制。

总的来说就是,我们确定了数据的读写地址之后,我们直接向这个地址写入数据,就可以发送或者接收16bit数据啦控制命令的地址也是一样,这就是”SRAM”地址空间映射到内核的好处,我们可以直接进行访问。

6.1 写入命令

1
2
3
4
5
6
7
8
9
/**
* @brief 向ILI9341写入命令
* @param usCmd :要写入的命令(表寄存器地址)
* @retval 无
*/
__inline void ILI9341_Write_Cmd (uint16_t usCmd)
{
*(__IO uint16_t *)(FSMC_Addr_ILI9341_CMD) = usCmd; // #define __IO volatile
}

6.2 写入数据

1
2
3
4
5
6
7
8
9
10
/**
* @brief 向ILI9341写入数据
* @param usData :要写入的数据
* @retval 无
*/
__inline void ILI9341_Write_Data (uint16_t usData)
{
*(__IO uint16_t *)(FSMC_Addr_ILI9341_DATA) = usData; // #define __IO volatile
}

6.3 读取数据

1
2
3
4
5
6
7
8
9
10
/**
* @brief 从ILI9341读取数据
* @param 无
* @retval 读取到的数据
*/
__inline uint16_t ILI9341_Read_Data ( void )
{
return ( *(__IO uint16_t *)(FSMC_Addr_ILI9341_DATA) ); // #define __IO volatile

}