你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

MiniPro STM32H750 开发指南_V1.1-TFTLCD(MCU屏)实验

[复制链接]
STMCU小助手 发布时间:2022-10-7 17:54
TFTLCD(MCU屏)实验
6 K+ O7 m2 w) c3 }7 V/ u1 t前面我们介绍了OLED模块及其显示,但是该模块只能显示单色/双色,不能显示彩色,而且尺寸也较小。本章我们将介绍正点原子的TFT LCD模块(MCU屏),该模块采用TFTLCD面板,可以显示16位色的真彩图片。在本章中,我们将使用开发板底板上的TFTLCD接口(仅支持MCU屏,本章仅介绍MCU屏的使用),来点亮TFTLCD,并实现ASCII字符和彩色的显示等功能,并在串口打印LCD控制器ID,同时在LCD上面显示。
% i/ v' Q4 Z: s$ U4 J3 C7 o# r3 _; V* w0 P3 T" M
25.1 TFTLCD和FMC简介8 U2 r' z! Q2 C. h6 u
本章我们将通过STM32H750的FMC接口来控制TFTLCD的显示,所以本节分为两个部分,分别介绍TFTLCD和FMC。% [- H, i9 W  r! {
25.1.1 TFTLCD简介
2 _) m% N7 H7 w: Z% Y液晶显示器,即Liquid Crystal Display,利用了液晶导电后透光性可变的特性,配合显示器光源、彩色滤光片和电压控制等工艺,最终可以在液晶阵列上显示彩色的图像。目前液晶显示技术以TN、STN、TFT三种技术为主,TFT-LCD即采用了TFT(Thin Film Transistor)技术的液晶显示器,也叫薄膜晶体管液晶显示器。0 ^$ k. ?# d  M- k
TFT-LCD与无源TN-LCD、STN-LCD的简单矩阵不同的是,它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。TFT式显示器具有很多优点:高响应度,高亮度,高对比度等等。TFT式屏幕的显示效果非常出色,广泛应用于手机屏幕、笔记本电脑和台式机显示器上。) r  H' j$ Z4 g2 e
由于液晶本身不会发光,加上液晶本身的特性等原因,使得液晶屏的成像角受限,我们从屏幕的的一侧可能无法看清液晶的显示内容。液晶显示器的成像角的大小也是评估一个液晶显示器优劣的指标,目前,规格较好的液晶显示器成像角一般在120°~160°之间。9 s8 t. W" l( w# O* }
正点原子TFT-LCD模块(MCU屏)有如下特点:/ o" D; A( t; A
1,2.8’/3.5’/4.3’/7’等4种大小的屏幕可选。# l' v+ M7 M% `* ?1 V
2,320×240的分辨率(3.5’分辨率为:320480,4.3’和7’分辨率为:800480)。
1 b6 O+ K4 Y! N# G3,16位真彩显示。
+ ]/ I9 P4 f" C+ P1 Z, [4,自带触摸屏,可以用来作为控制输入。
, u8 ?$ m* @9 c  Y本章,我们以正点原子2.8寸(此处的寸是代表英寸,下同)的TFT-LCD模块为例介绍,(其他尺寸的LCD可参考具体的LCD型号的资料,也比较类似),该模块支持65K色显示,显示分辨率为320×240,接口为16位的8080并口,自带触摸功能。
* W* Q4 r  y; \9 [- F该模块的外观图如图25.1.1.1所示:
$ N- ~+ x! ~; k9 F+ N
4 D! ^$ l8 P: A7 Y 671cab8a094646b28e8afdbe8583fd57.png * \, C: b; [" d/ G8 E5 [

) A8 d. i) h) m图25.1.1.1 正点原子2.8寸TFTLCD外观图
, T& B4 f, I0 p3 L0 a( W  Y模块原理图如图25.1.1.2所示:  Y0 J* N9 m, M* E' F: g

$ M9 m9 q! C$ @( z% T  s4 h6 R ef937ca30fa1423caebf6ee0f57838fd.png
0 F. G- o# K6 _1 }
7 _, z) A. x0 E7 U* C% H( x图25.1.1.2 正点原子2.8寸TFTLCD模块原理图. K( a5 s6 P, ^
TFTLCD模块采用2*17的2.54公排针与外部连接,即图中TFT_LCD部分。从图25.1.1.2可以看出,正点原子TFTLCD模块采用16位的并方式与外部连接。图25.1.1.2还列出了触摸控制的接口,但触摸控制是在显示的基础上叠加的一个控制功能,不配置也不会对显示造成影响,我们放到以后的章节再介绍触摸的用法。该模块与显示功能有关的信号线如表25.1.1.1:" g4 s. u/ H8 R

; q5 _4 C4 T! S& s$ c- w cbf7e5494a564f6dad9d11470cfba02a.png
: W' D) L* q+ y+ n9 t6 D4 \  J( M& i" s9 x$ P/ J' G
表25.1.1.1 TFT-LCD接口信号线
1 U; {( X( G! B& ~* m/ X( b/ L上述的接口线实际是对应到液晶显示控制器上的,这个芯片位于液晶屏的下方,所以我们从外观图上看不到。控制LCD显示的过程,就是按其显示驱动芯片的时序,把色彩和位置信息正确地写入对应的寄存器。
5 r; t5 u9 @6 ]+ c4 `! G25.1.2 液晶显示控制器
) S% R( K2 G; @6 i7 _7 O+ m( Q. E正点原子提供2.8/3.5/4.3/7寸等4种不同尺寸和分辨率的TFTLCD模块,其驱动芯片为:ILI9341/ST7789/NT35310/NT35510/SSD1963等(具体的型号,大家可以通过下载本章实验代码,通过串口或者LCD显示查看),这里我们仅以ILI9341控制器为例进行介绍,其他的控制基本都类似,我们就不详细阐述了。
, D' D1 ~4 D! B! c5 y" e8 ~6 z2 ]ILI9341液晶控制器自带显存,可配置支持8/9/16/18位的总线中的一种,可以通过3/4线串行协议或8080并口驱动。正点原子的TFTLCD模块上的电路配置为8080并口方式,其显存总大小为172800(24032018/8),即18位模式(26万色)下的显存量。在16位模式下,ILI9341采用RGB565格式存储颜色数据,此时ILI9341的18位显存与MCU的16位数据线以及RGB565的对应关系如图25.1.2.1所示:
( i7 [2 u3 ?' o2 b0 p; j3 B: |3 x1 j
10fbb8214fd34d179512841f3b34d24b.png - [- j. P. p! D- q8 l% P) X1 x3 M! A6 V
. V3 k+ n+ M7 x3 {
图25.1.2.1 16位数据与显存对应关系图
0 |6 R* A* w2 _4 z从图中可以看出,ILI9341在16位模式下面,18位显存的B0和B12并没有用到,对外的数据线使用DB0-DB15连接MCU的D0-D15实现16位颜色的传输(使用8080 MCU 16bit I型接口,详见9341数据手册7.1.1节)。
6 \, N/ z) f% I+ G; Q' E9 A这样MCU的16位数据,最低5位代表蓝色,中间6位为绿色,最高5位为红色。数值越大,表示该颜色越深。另外,特别注意ILI9341所有的指令都是8位的(高8位无效),且参数除了读写GRAM的时候是16位,其他操作参数,都是8位的。
* r5 }3 F4 \7 x/ }知道了屏幕的显色信息后,我们如何驱动它呢?OLED的章节我们已经描述过8080方式操作的时序,我们通过《ILI9341_DS.pdf》来加深一下在8080并口方式下如何操作这个芯片。2 D! j* b7 X, Y- e. v
以写周期为例,8080方式下的操作时序如图25.1.2.2所示。
! v1 ^( A  w9 a; C  c& t2 a1 \8 w1 x* C) q2 w" ?# Y' \& Z* f4 s
8d8203bfedf347b7b796c6f6c9fd7159.png # x8 u, j! H. @. \2 N% w  w

* G- i7 a( f! l3 n% a+ d; ]( Y图25.1.2.2 8080方式下对液晶控制器的写操作0 q0 U6 B$ l0 @& k; P0 G1 Z5 H& m  B
上图中的各个控制线与我们在表25.1.1.1提到的命名有些许差异,因为我们在原理图时往往为了方便自己记忆会对命名进行微调,为了方便读者对照,我们把图25.1.2.2中列出的引脚引脚与我们的TFTLCD模块的的对应关系再列出,如表25.1.2.1所示。
; U! J* N: N, z' X
% Y, M7 I, n- M, s* ^/ g a3fe8538a80345238c337b0fd70a7b40.png
% I7 F  J  j2 u9 I6 n. Q8 J4 x2 V$ ~8 j  ~! v4 g5 D1 H
表25.1.2.1 TFT-LCD引脚与液晶控制器的对应关系  M& J; ]' ?& d
这下我们再来分析一下图25.1.2.2所示的写操作的时序,控制液晶的主机,在整个写周期内需要控制片选CSX拉低(标注为①),之后对其它的控制线的电平才有效。在标号②表示的这个写命令周期中,D/CX被位低(参考ILI9341的引脚定义),同时把命令码通过数据线D[17:0](我们实际只用了16个引脚)按位编码。注意到③处,需要数据线在入电平拉高后再操持一段时间以便数据被正确采样。0 o* `7 s4 I3 d! D& z: v7 i0 t
图25.1.2.2中⑤表示写数据操作,与前面描述的写命令操作只有D/CX的操作不同,读者们可以尝试自己分析一下。更多的关于ILI9341的读写操作时序则参考《ILI9341_DS.pdf》。# e7 o- R  p6 B) y' j" F7 Z, U
通过前述的时序分析,我们知道了对于ILI9341来说,控制命令有命令码、数据码之分,接下来,我们介绍一下ILI9341的几个重要命令。因为ILI9341的命令很多,我们这里就不全部介绍了,有兴趣的大家可以找到ILI9341的datasheet看看。里面对这些命令有详细的介绍。我们将介绍:0xD3,0x36,0x2A,0x2B,0x2C,0x2E等6条指令。
" X! G5 k$ K# k. [' A, V* ]1 @指令0xD3,是读ID4指令,用于读取LCD控制器的ID,该指令如表25.1.2.2所示:& D  }/ @0 W( B, o6 ^

! p7 b/ \3 y2 U6 _7 a) k2 [ 52868788e4624ee197a979c2facc515b.png
& a! u/ y6 i$ a% C3 C% Z
) F) `" H7 H. k$ x$ q7 w8 B' U表25.1.2.2 0xD3指令描述
/ u! q) Q& A2 _# O从上表可以看出,0xD3指令后面跟了4个参数,最后2个参数,读出来是0x93和0x41,刚好是我们控制器ILI9341的数字部分,从而,通过该指令,即可判别所用的LCD驱动器是什么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动IC的初始化代码,从而兼容不同驱动IC的屏,使得一个代码支持多款LCD。9 u3 I6 l5 ~5 ~+ ^) B! I. @
接下来看指令:0x36,这是存储访问控制指令,可以控制ILI9341存储器的读写方向,简单的说,就是在连续写GRAM的时候,可以控制GRAM指针的增长方向,从而控制显示方式(读GRAM也是一样)。该指令如表25.1.2.3所示:
% j2 i! @8 \0 o( E$ y* A' w; Z, l
6cb1113e51564c3bbd0424a62fc7431e.png
: g" A" ~* B9 ^# ^
8 r1 L( b/ O9 v( j5 \表25.1.2.3 0x36指令描述
8 O6 ~. v8 Q+ c# q5 Q* b* V从上表可以看出,0x36指令后面,紧跟一个参数,这里主要关注:MY、MX、MV这三个位,通过这三个位的设置,我们可以控制整个ILI9341的全部扫描方向,如表25.1.2.4所示:2 @+ t+ a5 d4 |* R% U1 q" g; K
7 g" q: f5 C' R, c4 V
2477d30a4d03462c9e6f2531380d25f6.png 5 T  y- J* Y, V! e+ y, X

% R5 V2 O  u) \/ ^表25.1.2.4 MY、MX、MV设置与LCD扫描方向关系表
" m2 ~/ c3 t9 k# d2 j; t. R这样,我们在利用ILI9341显示内容的时候,就有很大灵活性了,比如显示BMP图片,BMP解码数据,就是从图片的左下角开始,慢慢显示到右上角,如果设置LCD扫描方向为从左到右,从下到上,那么我们只需要设置一次坐标,然后就不停的往LCD填充颜色数据即可,这样可以大大提高显示速度。. ~. ^! E  b7 o9 Z+ n
实验中,我们默认使用从左到右,从上到下的扫描方式。
; ^. c8 J& \5 K: E5 O# v. }接下来看指令:0x2A,这是列地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置横坐标(x坐标),该指令如表25.1.2.5所示:7 z3 `4 W& k( k8 n1 Y

( j3 B5 w& w0 c, T 3cf22442ce6e414b90d2b19cbd48b68b.png
: O' w0 y* H/ ?* |4 t" y& A5 A! J- G/ T8 g
表25.1.2.5 0x2A指令描述
/ M9 N$ z2 I+ E! L3 b, G6 @  E+ s: j$ q在默认扫描方式时,该指令用于设置x坐标,该指令带有4个参数,实际上是2个坐标值:SC和EC,即列地址的起始值和结束值,SC必须小于等于EC,且0≤SC/EC≤239。一般在设置x坐标的时候,我们只需要带2个参数即可,也就是设置SC即可,因为如果EC没有变化,我们只需要设置一次即可(在初始化ILI9341的时候设置),从而提高速度。
9 r8 Y/ U; l1 V! d# y与0X2A指令类似,指令:0X2B,是页地址设置指令,在从左到右,从上到下的扫描方式(默认)下面,该指令用于设置纵坐标(y坐标)。该指令如表25.1.2.6所示:0 u8 W. L  |6 U3 f0 A

( G: I" A% N. _ 978b9818299f45fdb9c8aa0e6c77f86d.png ! B) h( }) ^, h. \
6 U+ {7 P- l0 U" K9 I
表25.1.2.6 0X2B指令描述
: J' f& l8 v% y$ B0 v+ F( v在默认扫描方式时,该指令用于设置y坐标,该指令带有4个参数,实际上是2个坐标值:SP和EP,即页地址的起始值和结束值,SP必须小于等于EP,且0≤SP/EP≤319。一般在设置y坐标的时候,我们只需要带2个参数即可,也就是设置SP即可,因为如果EP没有变化,我们只需要设置一次即可(在初始化ILI9341的时候设置),从而提高速度。% E. B8 @# B6 w' g: j
接下来看指令:0X2C,该指令是写GRAM指令,在发送该指令之后,我们便可以往LCD的GRAM里面写入颜色数据了,该指令支持连续写,指令描述如表25.1.2.7所示。
) q) d9 }$ ]" ~" @. Z. C顺序 控制 各位描述 HEX# j0 m) \2 `+ Z4 J
RS RD WR D15~D8 D7 D6 D5 D4 D3 D2 D1 D0
8 Y. x, Y% v+ _指令 0 1 ↑ XX 0 0 1 0 1 1 0 0 2CH# c$ ~0 }2 F- O1 U" ]: V( I
参数1 1 1 ↑ D1[15:0] XX6 X: j/ }3 N) z( `
…… 1 1 ↑ D2[15:0] XX' r! a7 L) e% p% x
参数n 1 1 ↑ Dn[15:0] XX7 u! n2 ^% F" v/ ?! I  c/ p
表25.1.2.7 0X2C指令描述1 ^9 a, o9 `  f
由表25.1.2.6可知,在收到指令0X2C之后,数据有效位宽变为16位,我们可以连续写入LCD GRAM值,而GRAM的地址将根据MY/MX/MV设置的扫描方向进行自增。例如:假设设置的是从左到右,从上到下的扫描方式,那么设置好起始坐标(通过SC,SP设置)后,每写入一个颜色值,GRAM地址将会自动自增1(SC++),如果碰到EC,则回到SC,同时SP++,一直到坐标:EC,EP结束,其间无需再次设置的坐标,从而大大提高写入速度。
; e: e  f2 C0 ~9 W最后,来看看指令:0X2E,该指令是读GRAM指令,用于读取ILI9341的显存(GRAM),该指令在ILI9341的数据手册上面的描述是有误的,真实的输出情况如表25.1.2.8所示:4 I% r* B  l, n4 A
顺序 控制 各位描述 HEX
0 t+ v  o, U8 S6 J% URS RD WR D15~D11 D10~D8 D7 D6 D5 D4 D3 D2 D1 D0
# O7 A  J7 \  z/ O指令 0 1 ↑ XX 0 0 1 0 1 1 1 0 2EH& v) {7 o9 A, G" [1 h7 N8 N
参数1 1 ↑ 1 XX dummy, V/ C" ]* ?; r9 }, X. o2 ]. a! f
参数2 1 ↑ 1 R1[4:0] XX G1[5:0] XX R1G1
2 c+ Q8 _! |3 S2 Z7 U3 ^参数3 1 ↑ 1 B1[4:0] XX R2[4:0] XX B1R2
2 u/ t, C( E# d$ U! p3 _$ f( U' g参数4 1 ↑ 1 G2[5:0] XX B2[4:0] XX G2B22 y5 ^5 i9 E7 H+ X6 f& I
参数5 1 ↑ 1 R3[4:0] XX G3[5:0] XX R3G3; @' K2 V) c+ g
参数N 1 ↑ 1 按以上规律输出, J9 w& g+ w( K" C  L3 {
表25.1.2.8 0X2E指令描述* h) C8 y/ |! E# e0 l+ R. K
该指令用于读取GRAM,如表25.1.2.7所示,ILI9341在收到该指令后,第一次输出的是dummy数据,也就是无效的数据,第二次开始,读取到的才是有效的GRAM数据(从坐标:SC,SP开始),输出规律为:每个颜色分量占8个位,一次输出2个颜色分量。比如:第一次输出是R1G1,随后的规律为:B1R2G2B2R3G3B3R4G4B4R5G5…以此类推。如果我们只需要读取一个点的颜色值,那么只需要接收到参数3即可,如果要连续读取(利用GRAM地址自增,方法同上),那么就按照上述规律去接收颜色数据。
- X- S0 K( L) a% o( U# z以上,就是操作ILI9341常用的几个指令,通过这几个指令,我们便可以很好的控制ILI9341显示我们所要显示的内容了。  f$ ~% h, t' H  Q2 B) s0 V4 E
25.1.3 FMC简介. z3 t& v: B1 K" \% D. _
STM32H750xx系列芯片都带有FMC接口,即可变存储存储控制器,能够与同步或异步存储器、SDRAM存储器和NAND FLASH等连接,STM32H750的FMC接口支持包括SRAM、SDRAM、NAND FLASH、NOR FLASH和PSRAM等存储器。FMC的框图如图25.1.3.1所示:& l5 r$ W  E3 S) ]# i0 b
$ a' o9 k  I& `" C" d1 I
b82e36429e5148a0a180db1c34bf344e.png ) C9 b$ A+ E2 o* f( y

# h# I7 B/ t: y  m; H图25.1.3.1 FMC框图; g  }  m1 {! A# s
从上图我们可以看出,STM32H750的FMC将外部设备分为3类:NOR/PSRAM设备、NAND设备和SDRAM设备。他们共用地址数据总线等信号,他们具有不同的CS以区分不同的设备,比如本章我们用到的TFTLCD就是用的FMC_NE1做片选,其实就是将TFTLCD当成SRAM来控制。
4 U- I- X0 }. C) @4 [7 e图中的fmc_hclk时钟来自AHB3,例程设置为240Mhz,该时钟用于寄存器访问。而fmc_ker_ck时钟来自RCC_D1CCIPR寄存器FMCSEL[1:0]的设置,如图25.1.3.2所示:7 Y7 s1 }% E* N# n0 z5 p( d
( I5 E- w/ E; X
db0c0f77d0974a51a3ada1ba9af104dd.png % b  V* n+ S) C$ k

5 ]" |! k) z& [" J) r& q: K$ l图25.1.3.2 RCC_D1CCIPR寄存器各位描述4 @1 v* \3 T( h, r& \* c
在sys.c文件夹的sys_stm32_clock_init时钟设置初始化函数中,我们通过配置HAL_RCCEx_PeriphCLKConfig函数设置了RCC_D1CCIPR寄存器的FMCSEL[1:0]位为10,即选择pll2_r_ck作为fmc_ker_ck时钟,为220Mhz。所以,fmc_ker_ck=220Mhz。
3 I* q* e/ S( q9 R: H3 {, \另外,需要注意:图25.1.2.1中的32bit AHB总线仅用于访问FMC的寄存器,而64bit AXI总线则用于访问相关存储器。因此访问FMC寄存器和访问外部存储器,是通过不同的总线访问的。3 w7 Y  R9 P4 p1 x& t
这里我们介绍下为什么可以把TFTLCD当成SRAM设备用:首先我们了解下外部SRAM的连接,外部SRAM的控制一般有:地址线(如A0A18)、数据线(如D0D15)、写信号(WE)、读信号(OE)、片选信号(CS),如果SRAM支持字节控制,那么还有UB/LB信号。而TFTLCD的信号我们在25.1.1节有介绍,包括:RS、D0D15、WR、RD、CS、RST和BL等,其中真正在操作LCD的时候需要用到的就只有:RS、D0D15、WR、RD和CS。其操作时序和SRAM的控制完全类似,唯一不同就是TFTLCD有RS信号,但是没有地址信号。
6 b, H- C- A# ]5 e$ C+ ~5 j! STFTLCD通过RS信号来决定传送的数据是数据还是命令,本质上可以理解为一个地址信号,比如我们把RS接在A0上面,那么当FMC控制器写地址0的时候,会使得A0变为0,对TFTLCD来说,就是写命令。而FMC写地址1的时候,A0将会变为1,对TFTLCD来说,就是写数据了。这样,就把数据和命令区分开了,他们其实就是对应SRAM操作的两个连续地址。当然RS也可以接在其他地址线上,我们的开发板把RS连接在A19上面的。
2 O" W$ G- n8 \" d1 B6 c$ a+ E4 oSTM32H750的FMC支持8/16/32位数据宽度,我们这里用到的LCD是16位宽度的,所以在设置的时候,选择16位宽就OK了。我们再来看看FMC的外部设备地址映像,STM32H750的FMC将外部存储器划分为6个固定大小为256M字节的存储区域,如图25.1.3.3所示:
: J7 U: Z0 }" u: d% Q2 w
* I$ K7 V. o8 C) U) X! m" t 5fd028cb1b014a22b9974f0105d1ceee.png ; X. \& K2 X' L9 |. A

6 m- e5 w2 J3 y! f- A5 T图25.1.3.3 FMC存储块地址映像4 }8 y8 ]* F* V7 M
从上图可以看出,FMC总共管理1.5GB空间,拥有6个存储块(Bank),每个存储块256MB空间。本章,我们把TFTLCD当成SRAM设备来使用,所以用到的是存储块1。下面我们仅讨论存储块1的相关配置,其他块的配置,请参考《STM32H7xx参考手册_V3(中文版).pdf》第22章(690页)的相关介绍。
! q7 g5 {. @- O% ?2 V" ^STM32H750的FMC存储块1(Bank1)被分为4个区,每个区管理64M字节空间,每个区都有独立的寄存器对所连接的存储器进行配置。Bank1的256M字节空间由28根地址线(ADDR[27:0])寻址。2 g. y& o3 w: z! ?8 h
这里ADDR是内部AXI地址总线,其中ADDR[25:0]来自外部存储器地址FMC_A[25:0],而ADDR[26:27]对4个区进行寻址。如表25.1.3.1所示:
$ ]9 B0 z" o6 x% G" ~Bank1所选区 片选信号 地址范围 ADDR' D. g# L7 I& ~* X+ y- I3 w
[27:26] [25:0]( c; l* ?+ w" F) Q
第1区 FMC_NE1 0X6000,0000~63FF,FFFF 00 FMC_A[25:0]
3 t9 m3 V3 m7 P* f  E! B第2区 FMC_NE2 0X6400,0000~67FF,FFFF 01
7 F- ~4 H' }- Z0 s$ l第3区 FMC_NE3 0X6800,0000~6BFF,FFFF 10  i1 C& f0 o) H# V1 Z9 `
第4区 FMC_NE4 0X6C00,0000~6FFF,FFFF 11
' B$ O7 I( w1 |& }8 x( q' J, v+ O表25.1.3.1 Bank1存储区选择表8 G& i7 }+ ^( E
ADDR[25:0]位包含外部存储器的地址,由于ADDR为字节地址,而存储器按字寻址,所以,根据存储器数据宽度的不同,实际上向存储器发送的地址也有所不同,如表25.1.3.2所示:/ Z3 R: y1 r1 R, W
; I; H1 \: n+ h8 r) N
120465f22f8c4b1496e885265964df01.png
) i- j+ _4 }6 ?; N2 c; U4 E' L% Q: R9 V6 W4 y. m2 H5 f% E
表25.1.3.2 NOR/PSRAM外部存储器地址0 x) W: K3 P; m* r
因此,FMC内部ADDR与存储器寻址地址的实际对应关系就是:) r6 j7 L0 A5 X$ ?& I" R5 ?. u+ Z. V
当接的是32位宽度存储器的时候:ADDR[25:2] FMC_A [23:0]。
* }. r: K! i' y# |2 k/ `当接的是16位宽度存储器的时候:ADDR[25:1] FMC_A [24:0]。
3 A) k1 @& k" [- B- G) U当接的是8位宽度存储器的时候:ADDR[25:0] FMC_A [25:0]。* o1 P8 ]3 n5 h# L
不论外部接8位/16位/32位宽设备,FMC_A[0]永远接在外部设备地址A[0]。 这里,TFTLCD使用的是16位数据宽度,所以ADDR[0]并没有用到,只有ADDR[25:1]是有效的,对应关系变为:ADDR[25:1] FMC_A[24:0],相当于右移了一位,这里请大家特别留意。另外,ADDR[27:26]的设置,是不需要我们干预的,比如:当你选择使用Bank1的第一个区,即使用FMC_NE1来连接外部设备的时候,即对应了ADDR[27:26]=00,我们要做的就是配置对应第1区的寄存器组,来适应外部设备。STM32H750的FMC各Bank配置寄存器如表25.1.3.3所示:
4 l/ C$ N1 P2 l' K- Z5 E, X6 i, }内部控制器 存储块 管理的地址范围 支持的设备类型 配置寄存器, Y/ x/ @) d) I  N
NOR FLASH/PSRAM
/ ~2 u- r6 S- q1 U; T控制器 Bank1 0X6000,0000~
9 ]& P* ~1 k1 _5 C2 o2 z0X6FFF,FFFF SRAM/ROM
/ C+ l% T7 G6 n0 pNOR FLASH
; I9 B% ~0 _% i- {0 EPSRAM FMC_BCR1/2/3/4( |. c' }2 b; a$ q4 j  n% m
FMC_BTR1/2/2/35 a) T1 q0 J: G# G6 Y
FMC_BWTR1/2/3/4
6 L/ ~7 m& l5 T5 l# |) Q+ i) dSDRAM
# W$ S7 a: F5 x7 Y控制器 Bank2 0X7000,0000~
5 V, D- a6 |1 g6 }; N! A# A0X7FFF,FFFF SDRAM 同Bank5/Bank6
& `- l8 j; [9 u) q, v( dNAND FLASH
0 N2 n9 W0 J6 N: P# z: {控制器 Bank3 0X8000,0000~
8 `& G# W+ a4 A4 i3 k0X8FFF,FFFF NAND FLASH FMC_PCR/FMC_SR
- P9 o( h# d9 d5 K, z& G. N1 O8 S- LFMC_PMEM/FMC_PATT
) Z* Q3 }* f& J# d( z, VFMC_ECCR
1 g5 v! L& k- k1 n0 j' k6 b保留 Bank4 0X9000,0000~; p1 r0 K, W3 _" i1 x" F3 g# Y
0X9FFF,FFFF 保留 保留
( x% Q& k+ I! @+ X' V1 q4 YSDRAM* ?1 [5 ^8 Y8 X5 `6 `. c
控制器 Bank5 0XC000,0000~% {1 X7 c- C( D" V( R" p/ f
0XCFFF,FFFF SDRAM FMC_SDCR1/2
& A+ ^: Y: h- z% r4 U( OFMC_SDTR1/2
! _$ }5 |, _: x. J3 Z! r9 `FMC_SDCMR
% l* u$ u8 p. K$ R, TFMC_SDRTR7 Z) Z+ @5 [5 e, v6 p' O
FMC_SDSR9 A# J/ \1 f* j1 O* K# Z4 h
Bank6 0XD000,0000~
& W/ }7 h* f, z5 C" `# Q/ `0XDFFF,FFFF SDRAM8 Z! r0 B+ g" k8 b6 }
表25.1.3.3 FMC各Bank配置寄存器表; T2 Z2 @) M( V6 l2 o
对于NOR FLASH控制器,主要是通过FMC_BCRx、FMC_BTRx和FMC_BWTRx寄存器设置(其中x=1~4,对应4个区)。通过这3个寄存器,可以设置FMC访问外部存储器的时序参数,拓宽了可选用的外部存储器的速度范围。FMC的NOR FLASH控制器支持同步和异步突发两种访问方式。选用同步突发访问方式时,FMC将fmc_ker_ck时钟(FMC内核时钟)分频后,发送给外部存储器作为同步时钟信号FMC_CLK。此时需要的设置的时间参数有2个:* V  Q- T  m2 U( ^
  1,fmc_ker_ck与FMC_CLK的分频系数(CLKDIV),可以为2~16分频;
0 x9 A8 F" K( j% [% A. d5 V  2,同步突发访问中获得第1个数据所需要的等待延迟(DATLAT)。
: v: v: p! Y6 l$ ?# q. @9 |$ P( F对于异步突发访问方式,FMC主要设置3个时间参数:地址建立时间(ADDSET)、数据建立时间(DATAST)和地址保持时间(ADDHLD)。FMC综合了SRAM、PSRAM和NOR Flash产品的信号特点,定义了4种不同的异步时序模型。选用不同的时序模型时,需要设置不同的时序参数,如表25.1.3.4所列:! e1 D# G6 j0 ]* H  A" s

+ [1 h& v8 r$ R! l& ^" i时序模型 简单描述 时间参数
2 Y9 ^# n2 W1 ]$ }异步 Mode1 SRAM/CRAM时序 DATAST、ADDSET
) S& O  {8 J8 _4 DModeA SRAM/CRAM OE选通型时序 DATAST、ADDSET
! [+ {" |7 I* t; {; F5 u8 ?Mode2/B NOR FLASH时序 DATAST、ADDSET
8 \9 X4 R8 n4 s5 G6 e  Q- }ModeC NOR FLASH OE选通型时序 DATAST、ADDSET
) I3 h% C- c$ O  vModeD 延长地址保持时间的异步时序 DATAST、ADDSET、ADDHLD
3 E# @' a$ X/ N9 p" W2 x: Y同步突发 根据同步时钟FMC_CK读取
/ D6 y! K3 j! c! G2 l! J* q. g多个顺序单元的数据 CLKDIV、DATLAT
+ o* S$ Z5 m, Y2 d8 p' Y* [表25.1.3.4 NOR FLASH/PSRAM控制器支持的时序模型
2 b5 A5 p) R1 v) S8 L- _在实际扩展时,根据选用存储器的特征确定时序模型,从而确定各时间参数与存储器读/写周期参数指标之间的计算关系;利用该计算关系和存储芯片数据手册中给定的参数指标,可计算出FMC所需要的各时间参数,从而对时间参数寄存器进行合理的配置。
  ^. |; w) @+ ~7 H1 w本章,我们使用异步模式A(ModeA)方式来控制TFTLCD,模式A的读操作时序如图25.1.3.4所示:
2 o/ `, U, g$ Q  h; ~5 h
0 o5 c+ ]+ Y9 O 7a5ce25cd28a47ed90307387438aff43.png
2 v  J: k* L  v2 F% r  i7 q! ]8 I) y
图25.1.3.4 模式A读操作时序图3 s7 G; g3 b" [" W& y, ]$ W) u
模式A支持独立的读写时序控制,这个对我们驱动TFTLCD来说非常有用,因为TFTLCD在读的时候,一般比较慢,而在写的时候可以比较快,如果读写用一样的时序,那么只能以读的时序为基准,从而导致写的速度变慢,或者在读数据的时候,重新配置FMC的延时,在读操作完成的时候,再配置回写的时序,这样虽然也不会降低写的速度,但是频繁配置,比较麻烦。而如果有独立的读写时序控制,那么我们只要初始化的时候配置好,之后就不用再配置,既可以满足速度要求,又不需要频繁改配置。
4 q' X: w+ ]8 T- K. F% B! R+ M4 P/ v1 ~模式A的写操作时序如图25.1.3.5所示:
- T0 z3 e  [4 n
5 l# Q! E. B! P0 M4 q$ f8 N 5a87e3b389c4471692b7d6e96efdc2db.png " ^, ~: H6 M& ]3 k9 N$ }' b! l  ?

+ N: Q: K( h0 `* L4 _9 G8 {. s6 d图25.1.3.5 模式A写操作时序
6 L! N3 |4 ~6 L3 ~" I7 A( H/ J图25.1.2.4和图25.1.2.5中的ADDSET与DATAST,是通过不同的寄存器设置的,接下来我们讲解一下Bank1的几个控制寄存器。, f" ?  C: s: F" ^
25.1.4 FMC寄存器$ Y* |' A0 P9 X1 Z4 A
NOR/PSRAM控制寄存器1/2/3/4(FMC_BCR1/2/3/4)6 _! @, D! v% }3 D* n1 \9 \, {) z
SRAM/NOR闪存片选控制寄存器:FMC_BCRx(x=1~4),该寄存器描述如图25.1.4.1所示:# _+ [  m+ o0 F# u
2 N: b/ \, E, d1 @$ G! a- @% a
a2b74a7d61944f45ab2f0658792c6ebf.png
* X+ l+ r; t4 }* _' J  ^& j
1 q( ~+ l$ |* t) w/ p图25.1.4.1 FMC_BCRx寄存器各位描述(部分)8 W) F: R  _7 w, a" \  c% r
该寄存器我们在本章用到的设置有:FMCEN、EXTMOD、WREN、MWID[1:0]、MTYP[1:0]和MBKEN这几个设置,我们将逐个介绍。
0 l% @" E$ q0 p) l! v+ Q7 @9 e+ wFMCEN:FMC使能位。我们要使用FMC驱动TFTLCD就必须设置该位为1,不过只有FMC_BCR1的FMCEN位有效,FMC_BCR2~4的FMCEN位无效,统一由FMC_BCR1的FMCEN位控制。
, L: X5 m3 q$ g) k1 rEXTMOD:扩展模式使能位,也就是是否允许读写不同的时序,很明显,我们本章需要读写不同的时序,故该位需要设置为1。( S) }# ~; y  v" O+ N) N* a
WREN:写使能位。我们需要向TFTLCD写数据,故该位必须设置为1。
# O# H6 C+ \* d6 @  xMWID[1:0]:存储器数据总线宽度。00,表示8位数据模式;01表示16位数据模式;10表示32位数据模式;11保留。我们的TFTLCD是16位数据线,所以设置WMID[1:0]=01。* k2 G% ?  s# d% R+ N$ |
MTYP[1:0]:存储器类型。00表示SRAM;01表示PSRAM;10表示NOR FLASH/OneNAND FLASH;11保留。前面提到,我们把TFTLCD当成SRAM用,所以需要设置MTYP[1:0]=00。$ I2 U3 D+ }! t- p, _
MBKEN:存储块使能位。我们需要用到该存储块控制TFTLCD,所以要使能该存储块。6 G7 B7 J3 P$ Y4 c0 g7 p; S
SRAM/NOR-Flash片选时序寄存器1/2/3/4 (FMC_BTR1/2/3/4)3 g- C( D$ R' H, D5 }( @
SRAM/NOR闪存片选时序寄存器:FMC_BTRx(x=1~4),该寄存器描述如图25.1.4.2所示:( e+ b$ X1 {8 g  T- w

' e0 g3 J8 C; V4 t9 n% s b6790df2d0a540438c5cba4bb89ce1a5.png
+ H/ `7 Y; Y7 {+ Z4 X9 c! i9 C& y* s% @
图25.1.4.2 FMC_BTRx寄存器各位描述
; v3 i- \% E" }" r- J5 b# e这个寄存器包含了每个存储器块的控制信息,可以用于SRAM和NOR闪存存储器等。如果FMC_BCRx寄存器中设置了EXTMOD位,则有两个时序寄存器分别对应读(本寄存器)和写操作(FMC_BWTRx寄存器)。因为我们要求读写分开时序控制,所以EXTMOD是使能了的,也就是本寄存器是读操作时序寄存器,控制读操作的相关时序。本章我们要用到的设置有:ACCMOD、DATAST和ADDSET这三个设置。3 a, O% l: `8 n1 L
ACCMOD[1:0]:访问模式。00表示访问模式A;01表示访问模式B;10表示访问模式C;11表示访问模式D,本章我们用到模式A,故设置为00。0 A- \" h  B5 B
DATAST[7:0]:数据保持时间。0为保留设置,其他设置则代表保持时间为: DATAST个fmc_ker_ck时钟周期,最大为255个。对ILI9341来说,其实就是RD低电平持续时间,一般为355ns。而一个fmc_ker_ck时钟周期为4.5ns左右(1/220Mhz),为了兼容其他屏,我们这里设置DATAST为78,也就是78个fmc_ker_ck周期,时间大约是351ns(略超,但不影响使用)。
0 q3 ?9 G; v3 q4 s6 MADDSET[3:0]:地址建立时间。其建立时间为:ADDSET个fmc_ker_ck周期,最大为15个。对ILI9341来说,这里相当于RD高电平持续时间,为90ns,我们设置ADDSET为最大15,即15*4.3=67.5ns(略超,但不影响使用)。+ V# m8 a; q: c$ W+ u4 u6 j: V
SRAM/NOR-Flash写入时序寄存器1/2/3/4 (FMC_BWTR1/2/3/4)3 C9 @& [3 u, B! |
SRAM/NOR闪写时序寄存器:FMC_BWTRx(x=1~4),该寄存器描述如图25.1.4.3所示:3 x" a4 D3 P3 ~+ \
4 z" o% u7 i$ |, o: I! L% R
85c6f192ff53482eaae18f2fa5fca348.png
4 s2 u. O* l8 @4 i2 E3 H4 k7 N$ x/ Q
图25.1.4.3 FMC_BWTRx寄存器各位描述4 N/ e) n; l$ c  }
该寄存器在本章用作写操作时序控制寄存器,需要用到的设置同样是:ACCMOD、DATAST和ADDSET这三个设置。这三个设置的方法同FMC_BTRx一模一样,只是这里对应的是写操作的时序,ACCMOD设置同FMC_BTRx一模一样,同样是选择模式A,另外DATAST和ADDSET则对应WR的低电平和高电平持续时间,对ILI9341来说,这两个时间只需要15ns就够了,比读操作快得多。所以我们这里设置DATAST为3,即3个fmc_ker_ck周期,时间约为13.5ns。然后ADDSET设置为3,也是13.5ns。(略超,但不影响使用)" A- N. L' X) C; w
至此,我们对STM32H750的FMC介绍就差不多了,关于FMC的详细介绍,请大家参考《STM32H7xx参考手册_V7(英文版).pdf》第21章。通过以上两个小节的了解,我们可以开始写LCD的驱动代码了。注意:在MDK的寄存器定义里面,并没有定义FMC_BCRx、FMC_BTRx、FMC_BWTRx等这个单独的寄存器,而是将他们进行了一些组合。
5 \$ e/ o* k6 M  L. T) GFMC_BCRx和FMC_BTRx,组合成BTCR[8]寄存器组,他们的对应关系如下:4 U# ~" c' V8 g4 t
BTCR[0]对应FMC_BCR1,BTCR[1]对应FMC_BTR14 I5 U* O5 `) }$ n9 }
BTCR[2]对应FMC_BCR2,BTCR[3]对应FMC_BTR2
% x/ H' f0 g+ r* o$ p. RBTCR[4]对应FMC_BCR3,BTCR[5]对应FMC_BTR3
- t1 m: c& Y1 Q3 ~5 M" g" x; A, U0 UBTCR[6]对应FMC_BCR4,BTCR[7]对应FMC_BTR42 T' x7 i( D6 F: C7 j
FMC_BWTRx则组合成BWTR[7],他们的对应关系如下:
0 q, d0 P  X7 y& hBWTR[0]对应FMC_BWTR1,BWTR[2]对应FMC_BWTR2,
1 e# _- ?: w1 E8 DBWTR[4]对应FMC_BWTR3,BWTR[6]对应FMC_BWTR4," h+ ?% u1 F% V# |# T
BWTR[1]、BWTR[3]和BWTR[5]保留,没有用到。
7 y* R* e, @  ?通过对FMC相关的寄存器的描述,大家对FMC的原理有了一定的认识,如果还不熟悉的朋友,请一定要搜索网络资料理解FMC的原理。- j( G2 E) \# v9 W" C
一般TFTLCD模块的使用流程如图25.1.1.5:# c) ^% [; E/ K
; d9 `+ ^: E7 A- g, t) U
61afef6b8f454265815da45e06f93bb2.png . V: u% v0 {. }! S- h

5 {! `; `  Z4 n( k图25.1.1.5 TFTLCD使用流程
! R/ a  ]+ k8 v3 V' G任何LCD,使用流程都可以简单的用以上流程图表示。其中硬复位和初始化序列,只需要执行一次即可。而画点流程就是:设置坐标写GRAM指令写入颜色数据,然后在LCD上面,我们就可以看到对应的点显示我们写入的颜色了。读点流程为:设置坐标读GRAM指令读取颜色数据,这样就可以获取到对应点的颜色数据了。
; C1 N2 d0 o2 }- u0 J% E6 I  g  ~5 F5 p以上只是最简单的操作,也是最常用的操作,有了这些操作,一般就可以正常使用TFTLCD了。接下来我们将该模块用来来显示字符和数字,通过以上介绍,我们可以得出TFTLCD显示需要的相关设置步骤如下:
4 |# f. K2 f4 I1)设置STM32H750与TFTLCD模块相连接的IO。0 f8 H. \. _; Y0 b. h. a$ S
这一步,先将我们与TFTLCD模块相连的IO口进行初始化,以便驱动LCD,这里我们用到的是FMC。# e5 D4 ^1 m" p6 V. n
2)初始化TFTLCD模块。0 |1 `3 M/ ]) A. E: F& U
即图25.1.1.5的初始化序列,这里我们没有硬复位LCD,因为开发板的LCD接口将TFTLCD的RST同STM32H750的RESET连接在一起了,只要按下开发板的RESET键,就会对LCD进行硬复位。初始化序列,就是向LCD控制器写入一系列的设置值(比如伽马校准),这些初始化序列一般LCD供应商会提供给客户,我们直接使用这些序列即可,不需要深入研究。在初始化之后,LCD才可以正常使用。
# w) g  g* C2 ]3 i3 T3)通过函数将字符和数字显示到TFTLCD模块上。1 L& j: e" S: X* w0 O" U6 E
这一步则通过图25.1.1.5左侧的流程,即:设置坐标写GRAM指令写GRAM来实现,但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而达到显示字符/数字的目的,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数,就可以实现数字/字符的显示了。
+ y0 o' ~1 r# J25.2 硬件设计; W) ?( j- k+ k! b/ C6 ^7 h
1.例程功能/ w* O: p5 l0 ^9 j0 G) j' Z' ?+ E
使用开发板的MCU屏接口连接正点原子 TFTLCD模块(仅限MCU屏模块),实现TFTLCD模块的显示。通过把LCD模块插入底板上的TFTLCD模块接口,按下复位之后,就可以看到LCD模块不停的显示一些信息并不断切换底色。同时该实验会显示LCD驱动器的ID,并且会在串口打印(按复位一次,打印一次)。LED0闪烁用于提示程序正在运行。, N+ T. l' t; U# _
2.硬件资源8 K4 T; d; W5 s  `" F; _3 |1 z
1)RGB灯
# k6 D+ }! Z8 c  ~* ?: g1 H! hRED :LED0 - PB42 @. S6 ~) Z5 Q% j8 ]1 m3 o
2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
+ f4 M" X& t' s" T% j& l3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)' V. B; @" b, S' e
3.原理图9 ~5 b9 y( |* W6 f; c
TFTLCD模块的电路见图25.1.1.2,而开发板的LCD接口和正点原子TFTLCD模块直接可以对插,开发板上的LCD接口如图25.2.1所示:
0 C2 U, m9 Y; p0 c1 f2 m* [+ [- I& j2 g3 ~: R! R2 F0 L. s
a4f2b56b84f54cc0ad5c54440b86360c.png $ o7 t6 k# K" ^
1 T9 C* _. h, h* B% ^
图25.2.1 TFTLCD模块与开发板对接的LCD接口示意图
; O. O5 o* M% Q7 O* G- NTFTLCD模块与开发板的连接原理图如图25.2.1.2所示。. Y$ S8 `" L- g# ]; i

" K; e+ M$ P" E: c( l' |# W  A 1903643871be4f1f9b462b66cce74ea9.png
( ~5 G" i) B( a2 t
9 n+ Y6 G$ K% R8 c# |图25.2.1 TFTLCD模块与开发板的连接原理图
/ J* ]+ f) j4 X3 G; d' Z/ f5 G在硬件上,TFTLCD模块与开发板的IO口对应关系如下:& [! [! l, n. o' o6 ^+ Z4 R/ {  L- D

4 u$ Q* E( _) d8 y9 mLCD_BL(背光控制)对应PB5;
5 z" X& ^7 W5 H* CLCD_CS对应PD7,即FMC_NE1;
1 f- M$ n3 k) P$ sLCD _RS对应PE3,即FMC_A19;
6 T* M) [# ]2 a% aLCD _WR对应PD5,即FMC_NEW;
; o6 B1 Q% k8 p! ZLCD _RD对应PD4,即FMC_NOE;
5 M1 x4 ?/ [% G1 |7 ?LCD _D[15:0]则直接连接在FMC_D15~FMC_D0;% e2 J0 U! ^% ?1 D) B1 R: ?
! }& Q+ K- k. v( [& O$ I
这些线的连接,开发板的内部已经连接好了,我们只需要将TFTLCD模块插上去就好了。
* K3 h6 o9 @0 G' P4 d0 \  W/ l; C5 ^# A, ^" H
25.3 程序设计
: w3 O+ c* n4 d' `: \% @+ D25.3.1 FMC和SRAM的HAL库驱动

/ x, U9 M6 y& M9 o7 P+ v* zSRAM和FMC在HAL库中的驱动代码在stm32h7xx_ii_fmc.c/stm32h7xx_hal_sram.c以及stm32h7xx_ii_fmc.h/stm32h7xx_hal_sram.h中。) T$ w+ ~" k3 b+ q
1.HAL_SRAM_Init函数7 o, s# Q+ n* x# W
SRAM的初始化函数,其声明如下:4 Z% d+ h7 j3 c6 @0 N- ^$ M
HAL_StatusTypeDef HAL_SRAM_Init(SRAM_HandleTypeDef *hsram,
9 v- `1 t- Q6 F! |: aFMC_NORSRAM_TimingTypeDef *Timing, FMC_NORSRAM_TimingTypeDef *ExtTiming);' y+ m, Q# M" e, }: O" r5 d
函数描述:: x" Y* ~8 p( d: M* A8 Z1 ]
用于初始化 SRAM,注意这个函数不限制一定是SRAM,只要时序类似,均可使用。前面说过,这里我们把LCD当作SRAM使用,因为他们时序类似。* N6 D1 H* j3 ]
函数形参:1 [6 {# V' R- G7 D0 Q% S0 l
形参1是SRAM_HandleTypeDef结构体类型指针变量,其定义如下:
' y  y( T' {) U2 w# a# G- Q3 }( B
  1. typedef struct5 j, Q1 s9 X- r& [- F6 {
  2. {1 m& j# O* w: Z) T; d& {
  3.   FMC_NORSRAM_TypeDef              *Instance;          /* 寄存器基地址 */2 F0 R& b8 W, Y0 b! k
  4.   FMC_NORSRAM_EXTENDED_TypeDef   *Extended;          /* 扩展模式寄存器基地址 */ 5 V6 U1 C: o1 z6 [
  5.   FMC_NORSRAM_InitTypeDef         Init;                /* SRAM设备控制配置结构体 */
    7 m* Q' W/ M* B6 u$ |2 L/ i( x
  6.   HAL_LockTypeDef                   Lock;               /* SRAM锁定对象 */
    ) {3 \9 [! \, C% T
  7.   __IO HAL_SRAM_StateTypeDef      State;              /* SRAM设备访问状态 */   2 t" u2 S: \0 A6 x$ f! \
  8.   MDMA_HandleTypeDef               *hmdma;             /* 指针DMA处理配置 */ 2 V% n+ b+ [# h+ y9 O. X, w
  9. }SRAM_HandleTypeDef;
复制代码
/ F9 |' t; s3 H, m+ m6 O! {
1)Instance:指向FMC寄存器基地址。本实验我们使用异步模式A(ModeA)方式来控制TFTLCD,使用的存储块是Bank1,所以寄存器基地址Instance我们直接写FMC_Bank1_R即可,因为HAL库定义好了宏定义FMC_NORSRAM_DEVICE,也就是如果是SRAM设备,直接填写这个宏定义标识符即可。& z" G- h% z. y3 `
2)Extended:指向FMC扩展模式寄存器基地址,因为我们要配置的读写时序是不一样的,前面讲的FMC_BCRx寄存器的EXTMOD位我们会配置为1允许读写不同的时序,所以还要指定写操作时序寄存器地址,也就是通过参数Extended来指定的,这里设置为FMC_Bank1E_R即可,同样HAL库定义了FMC_NORSRAM_EXTENDED_DEVICE,直接填写这个宏定义标识符即可。
+ V7 w% f' g/ g4 p- h1 w* K3)Init:用于配置FMC外接SRAM或者相同时序设备时的基本参数,是我们接触最多的参数。
. j# q" u1 m3 @4)Lock:用于配置锁状态。* e, ~/ u$ v/ ~8 W1 G3 W
5)State:SRAM设备访问状态。
: W8 ~( |+ J& f, {' w. q  b  C6)hmdma:用于配置关联MAMA句柄。/ u& L4 M' J  [; E' O% A- z
其中成员变量Init是FMC_NORSRAM_InitTypeDef结构体指针类型,该变量才是真正用来设置SRAM控制接口参数的。下面详细了解这个结构体定义:/ J9 j' P4 j& ~! {# b: |" ~+ Y
typedef struct
/ w  c! N* Q+ {{7 U0 j1 K. J0 f8 F& \
uint32_t NSBank; /* 存储区块号 /, Z3 ^# [& \) Z. P& Q7 L0 T
uint32_t DataAddressMux; / 地址/数据复用使能 /. M4 P$ C# H/ D$ k
uint32_t MemoryType; / 存储器类型 /
6 M& B& \$ j$ t' T" h& X* Duint32_t MemoryDataWidth; / 存储器数据宽度 /7 w3 U1 ]2 S$ ?8 P3 K6 }
uint32_t BurstAccessMode; / 使能或者禁止突发模式 /
' O2 b3 W& B; J3 J5 Puint32_t WaitSignalPolarity; / 设置等待信号的极性 /  a$ _) U3 y" ~2 l7 R3 U* l3 u) k
uint32_t WaitSignalActive; / 等待状态之前或等待状态期间 /
. r. o, U+ t# w$ C! y5 Fuint32_t WriteOperation; / 存储器写使能 /7 Y( K. O, m8 e% E1 g2 K: i* H1 g
uint32_t WaitSignal; / 使能或者禁止通过等待信号来插入等待状态 /& [: q: ?; q2 G/ B7 M" j; R+ d
uint32_t ExtendedMode; / 使能或者禁止使能扩展模式 /; f8 m' {* ~' j  p3 G
uint32_t AsynchronousWait; / 用于异步传输期间,使能或者禁止等待信号 /
! _5 N; [( E) `: S* C8 Juint32_t WriteBurst; / 用于使能或者禁止异步的写突发操作 /
! D6 _5 U" W) muint32_t ContinuousClock; / 使能或者禁止FMC时钟输出到外部存储设备 /
; G& }: d# K) Z: uuint32_t WriteFifo; / 使能或者禁止写 FIFO /
5 h/ R& Z5 ?7 k' P  zuint32_t PageSize; / 设置页大小 /
' @! Z& ~; w9 V9 a! ~  }$ ^}FMC_NORSRAM_InitTypeDef;
& e" ]9 @& R6 i& k+ V  ?# KNSBank用来指定使用到的存储块区号,前面讲过,我们是使用的存储块区号1,所以选择值为FMC_NORSRAM_BANK1。: k9 s% i/ o/ I) ?/ }
DataAddressMux用来设置是否使能地址/数据复用,该变量仅对NOR/PSRAM有效,所以这里我们选择不使能地址/数据复用值FMC_DATA_ADDRESS_MUX_DISABLE即可。
$ q9 [1 D0 Q* c  ?' A/ IMemoryType用来设置存储器类型,这里我们把LCD当SRAM使用,所以设置为FMC_MEMORY_TYPE_SRAM即可。: p% D0 t* R9 v. P
MemoryDataWidth用来设置存储器数据总线宽度,可选8位还是16位,这里我们选择16位数据宽度FMC_NORSRAM_MEM_BUS_WIDTH_16。9 d! n1 X* n7 e- i% y; V
WriteOperation用来设置存储器写使能,也就是是否允许写入。毫无疑问我们会进行存储器写操作,所以这里设置为FMC_WRITE_OPERATION_ENABLE。: {* f, N& y! D7 B! Y" n
ExtendedMode用来设置是否使能扩展模式,也就是是否允许读写使用不同时序,前面讲解过本实验读写采用不同时序,所以设置值为使能值FMC_EXTENDED_MODE_ENABLE。
; e( U! s' K. A4 H: ~+ ~ContinuousClock用来设置启用/禁止FMC时钟输出到外部存储设备,这里仅当使用FMC_BCR1寄存器的时候需要启用,启用值为FMC_CONTINUOUS_CLOCK_SYNC_ASYNC。
. |4 G1 N  E7 [9 ]其他参数WriteBurst,BurstAccessMode,WaitSignalPolarity,WaitSignalActive,WaitSignal,AsynchronousWait等是用在突发访问和异步时序情况下,这里我们不做过多讲解。
, w, l6 s1 K) k" b形参2 Timing和形参3 ExtTiming都是FMC_NORSRAM_TimingTypeDef结构体类型指针变量,其定义如下:
- z0 M6 ^+ a5 n. f) h% l1 l( B7 Vtypedef struct
. M) ]4 S4 k& t. a" @4 L6 `4 Z{
3 b1 F/ j! Y9 Euint32_t AddressSetupTime; / 地址建立时间 /
5 z. |  h  [1 V1 m3 Luint32_t AddressHoldTime; / 地址保持时间 /( A* t$ e$ f- F5 B' |% z5 G/ z" f# e
uint32_t DataSetupTime; / 数据建立时间 /. n+ @2 A& E4 b/ h
uint32_t BusTurnAroundDuration; / 总线周转阶段的持续时间 /  J- L7 |% D) _
uint32_t CLKDivision; / CLK时钟输出信号的周期 /0 T7 N6 S) h" V8 n' U( i! d
uint32_t DataLatency; / 同步突发NOR FLASH的数据延迟 /
, P) T* |& r4 L3 i& Fuint32_t AccessMode; / 异步模式配置 */( k' T) Y! H8 N5 }; r' {# `
}FMC_NORSRAM_TimingTypeDef;0 Q5 n" w' t' L$ X% f% i
对于本实验,读速度比写速度慢得多,因此读写时序不一样,所以对于Timing和ExtTiming要设置了不同的值,其中Timing设置写时序参数,ExtTiming设置读时序参数。4 E& @' {  v  \8 [
下面解析一下结构体的成员变量:
2 Z' L% n  k! ~$ [AddressSetupTime用来设置地址建立时间。# V+ s- n$ A3 W. x$ Q
AddressHoldTime用来设置地址保持时间。
. J1 C& @! S2 M- DDataSetupTime用来设置数据建立时间。0 r. I+ c' ~  w% @$ q/ |# i
BusTurnAroundDuration用来配置总线周转阶段的持续时间。# x8 h$ N2 n/ y  y
CLKDivision用来配置CLK时钟输出信号的周期,以HCLK周期数表示。+ c1 ~7 E; n; i# T
DataLatency用来设置同步突发NOR FLASH的数据延迟。4 N3 w3 ~  k# U( y
AccessMode用来设置异步模式,取值范围为FMC_ACCESS_MODE_A、FMC_ACCESS_
1 F# w7 a9 o: Z0 h: t+ [MODE_B,、FMC_ACCESS_MODE_C和FMC_ACCESS_MODE_D,这里我们用是异步模式A,所以取值为FMC_ACCESS_MODE_A。
7 O3 m2 N" y) D函数返回值:
# y4 p9 O2 ~9 G) eHAL_StatusTypeDef枚举类型的值。
  d1 W7 E1 M! t, o注意事项:
' M: V5 Z( t' J- H0 ~4 ]' `9 ]5 I3 n; E和其他外设一样,HAL库也提供了SRAM的初始化MSP回调函数,函数声明如下:! Q' u, o# \* |/ G! q7 f
void HAL_SRAM_MspInit(SRAM_HandleTypeDef *hsram) ;
6 P" ?9 D! Q& `& }( x# e2. FMC_NORSRAM_Extended_Timing_Init函数% r+ ?# K  K- @0 n/ v. l
FMC_NORSRAM_Extended_Timing_Init函数是初始化扩展时序模式函数。其声明如下:/ k) ]% v+ M$ c6 M" B
HAL_StatusTypeDef FMC_NORSRAM_Extended_Timing_Init(# \; F0 t6 s4 y) ~6 Y; P
FMC_NORSRAM_EXTENDED_TypeDef *Device, FMC_NORSRAM_TimingTypeDef *Timing,
0 F0 |3 _. V6 }7 M. Fuint32_t Bank, uint32_t ExtendedMode);5 O9 V* X% J$ N
函数描述:+ h& X8 T/ R4 h/ ^: B
该函数用于初始化扩展时序模式。- h- M; ?9 n; o6 s3 B2 a
函数形参:, ?! C, j# b  o0 R0 ]
形参1是FMC_NORSRAM_EXTENDED_TypeDef结构体类型指针变量,扩展模式寄存器基地址选择。
% A  C2 B0 I2 b3 B5 o6 B% v6 q形参2是FMC_NORSRAM_TimingTypeDef结构体类型指针变量,可以是读或者写时序结构体。0 a" w$ a) w. H1 @
形参3是储存区块号。
! i" c3 h# ]/ d$ q形参4是使能或者禁止扩展模式。
) Y3 W& [9 k8 s( O1 P& [函数返回值:- E$ G( g7 P7 l5 z% Z7 t2 r
HAL_StatusTypeDef枚举类型的值。
9 M  Y) m4 B; E- C- j" l7 I注意事项:
! b8 s% x: [: w: b' `该函数我们用于重新配置写或者读时序。
! n" a1 ]. J$ ?FMC驱动LCD显示配置步骤9 t) D! r$ D7 K( J! B
1)使能FMC和相关GPIO时钟,并设置好GPIO工作模式1 H/ [7 Q# A2 I/ ?' ^6 X
我们通过FMC控制LCD,所以先需要使能FMC以及相关GPIO口的时钟,并设置好GPIO的工作模式。
  k2 b: ^  J" G. n' Q8 m( S0 r( l2)设置FMC参数, q1 f4 n( G, c1 Y
这里我们需要设置FMC的相关访问参数(数据位宽、访问时序、工作模式等),以匹配液晶驱动IC,这里我们通过HAL_SRAM_Init函数完成FMC参数配置,详见本例程源码。
% ~2 w! Q$ X, \8 L! m6 V+ F. X4 Y3)初始化LCD
  T3 W% }/ w! s, l4 s* k% y$ r1 H3 i( g由于我们例程兼容了很多种液晶驱动IC,所以先要读取对应IC的驱动型号,然后根据不同的IC型号来调用不同的初始化函数,完成对LCD的初始化。
8 i& s1 j0 ]3 G& Z: I' e- q: N: l注意:这些初始化函数里面的代码,都是由LCD厂家提供,一般不需要改动,也不需要深究,我们直接照抄即可。* ^  L3 x+ n7 U$ A$ e; F
4)实现LCD画点&读点函数" W- Z; v/ t& J, K
在初始化LCD完成以后,我们就可以控制LCD显示了,而最核心的一个函数,就是画点和读点函数,只要实现这两个函数,后续的各种LCD操作函数,都可以基于这两个函数实现。
% \) `4 H  O1 a- e) Q5)实现其他LCD操作函数
, W8 ?3 B/ g* C  G. D) q* Q& w在完成画点和读点两个最基础的LCD操作函数以后,我们就可以基于这两个函数实现各种LCD操作函数了,比如画线、画矩形、显示字符、显示字符串、显示数字等,如果不够用还可以根据自己需要来添加。详见本例程源码。1 J! C, {1 w( c. q9 U
25.3.2 程序流程图/ ]; P  F, T3 t% H- u; t
0 F  D" e+ Z' I; c2 }2 s6 _/ P9 C
图25.3.2.1 TFTLCD(MCU屏)实验程序流程图$ P- {) q  ?! ]2 n. ^+ }7 V% V6 J' M

& H% R( ?) Z- ^7 K: V25.3.3 程序解析
3 q% ]1 U+ C, b6 t( q1.LCD驱动代码
) o  r2 E9 R3 e' [这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。LCD驱动源码包括四个文件:lcd.c、lcd.h、lcd_ex.c和lcdfont.h。
. C+ t  `5 g2 W+ q/ j* k0 Elcd.c和lcd.h文件是驱动函数和引脚接口宏定义以及函数声明等。lcd_ex.c存放各个LCD驱动IC的寄存器初始化部分代码,是lcd.c文件的补充文件,起到简化lcd.c文件的作用。lcdfont.h头文件存放了4种字体大小不一样的ASCII字符集(1212、1616、2424和3232)。这个跟oledfont.h头文件一样的,只是这里多了3232的ASCII字符集,制作方法请回顾OLED实验。
6 H. J8 Y: B: |6 a6 v' g- X下面我们还是先介绍lcd.h文件,首先是LCD的引脚定义:
: z6 b( B0 ^9 v5 U' x# X2 g/*****************************************************************************/" @) p9 Z  e$ r7 k: B# E
/ LCD RST/WR/RD/BL/CS/RS 引脚 定义
0 w1 o- @$ i4 J( P( H/ ^/ O( z" y" ULCD_D0~D15,由于引脚太多,就不在这里定义了,直接在lcd_init里面修改.所以在移植的时候,除了' G4 O+ P- j# O$ ]# c- Z4 D$ {8 }
改这6个IO口, 还得改lcd_init里面的D0~D15所在的IO口.: I, s6 y4 S  b9 ^! O$ C
*/3 k" r$ o4 Q  g% q
/* RESET 和系统复位脚共用 所以这里不用定义 RESET引脚 /, [: F/ {' W- @$ `# Q) w8 _
//#define LCD_RST_GPIO_PORT GPIOX
5 R' k! U" W4 m$ u3 [  N//#define LCD_RST_GPIO_PIN GPIO_PIN_X
8 X7 J' V; V4 J6 m0 E/ O//#define LCD_RST_GPIO_CLK_ENABLE()% {: U3 T; O+ G5 r& V7 p" G
do{ __HAL_RCC_GPIOx_CLK_ENABLE(); }while(0) / 所在IO口时钟使能 */' H- h' N0 l3 T1 P+ c5 y+ ?
8 _# x* M0 C5 \' q4 M4 {
#define LCD_WR_GPIO_PORT GPIOD% C; S- Y  }( u6 R7 o
#define LCD_WR_GPIO_PIN GPIO_PIN_5) E7 J. J* A- y/ H6 l/ h- Z' e9 t
#define LCD_WR_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */: W& E+ x6 p. f. G- _

) [3 I. A) b& a( T9 e#define LCD_RD_GPIO_PORT GPIOD
, o" H/ R# k2 ^0 }% p) a#define LCD_RD_GPIO_PIN GPIO_PIN_4
0 {% G* X7 ?; l$ o, I  p9 `#define LCD_RD_GPIO_CLK_ENABLE()
/ P5 Z6 d4 J* n: e# V: cdo{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */' _' s% Q0 X* K* q" F# J/ ]1 z

, H% d! \) a7 c2 F( @#define LCD_BL_GPIO_PORT GPIOB
& n( i- d7 {6 u9 z8 l! R2 \# `8 w#define LCD_BL_GPIO_PIN GPIO_PIN_5) h, G' m3 i( `4 u+ s2 G
#define LCD_BL_GPIO_CLK_ENABLE()  D: N1 f5 x5 j; q9 G* c
do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */
1 Y7 u/ U3 X% s7 D
, a7 x: \9 J2 C/* LCD_CS(需要根据LCD_FMC_NEX设置正确的IO口) 和 LCD_RS+ j, o: C( Y4 D! n7 i# e
(需要根据LCD_FMC_AX设置正确的IO口) 引脚 定义 /. C1 P7 Q2 _9 ~7 Q+ K0 {1 [
#define LCD_CS_GPIO_PORT GPIOD
6 |/ [9 U" e! a* p#define LCD_CS_GPIO_PIN GPIO_PIN_7
. [: |+ C; ]7 n2 E, `#define LCD_CS_GPIO_CLK_ENABLE()
1 k" _4 @5 o5 N: hdo{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0) / 所在IO口时钟使能 */
8 X$ ~& K2 r7 ]- [, }; s' ^9 j; |1 v8 x- o5 d2 a  r6 [) Y8 @
#define LCD_RS_GPIO_PORT GPIOE
3 J' b( d+ V% ]3 K#define LCD_RS_GPIO_PIN GPIO_PIN_31 J# M' `% C% y- |2 V
#define LCD_RS_GPIO_CLK_ENABLE()) Y  k- e) k" _( S" W
do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* 所在IO口时钟使能 */. [0 W8 ?, Z( \

# X# s, ~) S1 h! F! d- U0 p/* FMC相关参数 定义- @+ a. c5 o" j8 ~7 _3 Y0 @1 d
2 o- V- b5 f& [; \4 @
注意: 我们默认是通过FMC块1来连接LCD, 块1有4个片选: FMC_NE1~4) T; n  [' m3 D& R/ d
修改LCD_FMC_NEX, 对应的LCD_CS_GPIO相关设置也得改. `$ d* q+ {0 N" y
修改LCD_FMC_AX , 对应的LCD_RS_GPIO相关设置也得改* n9 \/ m( ?6 N+ z4 P
/- ?5 ]/ O$ F" ]* q6 i% M$ {0 @* I: U
#define LCD_FMC_NEX 1 / 使用FMC_NE1接LCD_CS,取值范围只能是: 1~4 /
7 d8 K# M6 |- t) |+ E#define LCD_FMC_AX 19 / 使用FMC_A19接LCD_RS,取值范围是: 0 ~ 25 /
: l% v' o* x" \) {' ~/ W/*****************************************************************************/2 S( L; P) C2 ~3 v: p* n
第一部分的宏定义是LCD RST/WR/RD/BL/CS/RS 引脚定义,需要注意的是:LCD的RST引脚和系统复位脚连接在一起,所以不用单独使用一个IO口(节省一个IO口)。
0 U( @- \1 \6 F6 w& h8 o第二部分的宏定义是LCD_FMC_NEX和LCD_FMC_AX,这两个宏定义是为计算LCD的基地址LCD_BASE而定义的。其中LCD_FMC_NEX是FMC的存储区块号,取值范围只能是: 1~4,因为LCD的驱动用到块1,所以我们默认取值为1。而LCD_FMC_AX是对应LCD_RS引脚的IO口的复用功能,开发板上LCD RS引脚硬件连接PE3,PE3要复用为FMC_A19引脚。所以LCD_FMC_AX定义为19,19对应着FMC_A19,取值范围是: 0 ~ 25。
/ \) C2 C2 w9 N3 a! W" ]; |下面介绍我们在lcd.h里面定义的一个重要的结构体:
: |$ y+ {2 F0 E/ LCD重要参数集 /
; g+ O0 h- Z+ k) P6 Y! u1 T" \7 ~typedef struct0 x) r- \* L. H3 h
{  E1 b" K+ ^7 U0 t( H
uint16_t width; / LCD 宽度 /
6 b  v$ U% M* S# Z& ?uint16_t height; / LCD 高度 /
/ h& m5 t4 o! p* o, x3 x( l  _uint16_t id; / LCD ID /
$ [1 ?* x7 Q; n7 puint8_t dir; / 横屏还是竖屏控制:0,竖屏;1,横屏。 /$ E% Q* P! p$ c& M6 J* E" t
uint16_t wramcmd; / 开始写gram指令 /
# f4 P( w6 R# [( luint16_t setxcmd; / 设置x坐标指令 /& X% y0 o8 k! A8 d8 W1 H+ c4 f
uint16_t setycmd; / 设置y坐标指令 */; v8 ^, o4 s' t8 \: {1 |  J) M. o
} _lcd_dev;
/ z+ o) m% {8 l0 I$ N) E9 F/* LCD参数 /& [3 x0 B  k- \6 |( ~
extern _lcd_dev lcddev; / 管理LCD重要参数 *// Q6 `( @9 u" R

+ Q9 d  _- R$ L/ H/* LCD的画笔颜色和背景色 /
" D0 r, v6 \  C0 j* k' bextern uint32_t g_point_color; / 默认红色 /* t1 `  e' \, a$ `% Q
extern uint32_t g_back_color; / 背景颜色.默认为白色 /2 \  M0 p! n" ^+ x: L! t
该结构体用于保存一些LCD重要参数信息,比如LCD的长宽、LCD ID(驱动IC型号)、LCD横竖屏状态等,这个结构体虽然占用了十几个字节的内存,但是却可以让我们的驱动函数支持不同尺寸的LCD,同时可以实现LCD横竖屏切换等重要功能,所以还是利大于弊的。最后声明_lcd_dev结构体类型变量lcddev,lcddev在lcd.c中定义。; m# P7 d7 [: K8 B6 L. \
紧接着就是g_point_color和g_back_color变量的声明,它们也是在lcd.c中被定义。g_point_color变量用于保存LCD的画笔颜色,g_back_color则是保存LCD的背景色。# X: K& Z* d" N, `3 E& t
下面是LCD背光控制IO口的宏定义:3 z6 q7 Z$ \% l: l  q: a8 ]
/ LCD背光控制 /$ O+ K) m5 Y9 N7 R/ t  [0 J+ q
#define LCD_BL(x) do{ x ?
. w/ n1 ~) E' ?( B8 k" bHAL_GPIO_WritePin(LCD_BL_GPIO_PORT, LCD_BL_GPIO_PIN, GPIO_PIN_SET) :
5 o0 Y6 \& x3 t: \  W5 THAL_GPIO_WritePin(LCD_BL_GPIO_PORT, LCD_BL_GPIO_PIN, GPIO_PIN_RESET);
! X( r) [8 l2 w8 @* @: Z! S" `}while(0)
* q7 o! a- Y; o. A  c2 U# \我们知道TFTLCD的RS接在FMC的A19(即PE3)上面,CS接在FMC_NE1(即PD7)上,并且是16位数据总线。我们使用的是FMC存储器1的第1区,所以我们定义如下LCD操作结构体:/ c2 ^+ M% ?2 c/ P, l
/ LCD地址结构体 */' H. ^3 W+ u* S6 P% b- \: k
typedef struct
) W& H- t9 \  T9 m* o4 D8 ~! m- S/ P3 S9 ^{
, d, j( q8 x& ~# q4 lvolatile uint16_t LCD_REG;
# v9 W: M. P$ q2 J  d/ uvolatile uint16_t LCD_RAM;& T2 Y8 `" G! w' {4 T6 h, @
} LCD_TypeDef;
) [) W' \5 o( o% z1 A, ]9 O( H* Y( {& v
/* LCD_BASE的详细解算方法:
& u3 o& M0 L0 I9 ?& n- T/ k/ x$ u3 C  J
我们一般使用FMC的块1(BANK1)来驱动TFTLCD液晶屏(MCU屏), 块1地址范围总大小为256MB,- X: V$ l+ M! C* D' h2 j' z
均分成4块:
9 X+ a' o; a' n# x( [* f存储块1(FMC_NE1)地址范围: 0X6000 0000 ~ 0X63FF FFFF  N+ o7 ^1 @+ V9 h8 P# \+ y) O, G
存储块2(FMC_NE2)地址范围: 0X6400 0000 ~ 0X67FF FFFF
; j9 I9 ?5 u! z2 E/ \/ [3 B存储块3(FMC_NE3)地址范围: 0X6800 0000 ~ 0X6BFF FFFF9 r8 j" @* ~7 V+ D
存储块4(FMC_NE4)地址范围: 0X6C00 0000 ~ 0X6FFF FFFF  m) P- u3 L. y7 j) Z
我们需要根据硬件连接方式选择合适的片选(连接LCD_CS)和地址线(连接LCD_RS)
9 d+ z4 R; t; |& \# B2 {H750开发板使用FMC_NE1连接LCD_CS, FMC_A19连接LCD_RS ,16位数据线,计算方法如下:% k! T7 y( n6 r' `
首先FMC_NE1的基地址为: 0X6000 0000;
3 ], z- B* P! TNEx的基址为(x=1/2/3/4): 0X6000 0000 + (0X400 0000 * (x - 1))7 k% J$ D5 t8 J: u; I
FMC_A19对应地址值: 2^19 * 2 = 0X100000; FMC_Ay对应的地址为(y=0~25): 2^y * 2
0 l  o1 F/ K& O9 s2 VLCD->LCD_REG,对应LCD_RS = 0(LCD寄存器); LCD->LCD_RAM,对应LCD_RS = 1(LCD数据)
, W$ O2 _7 j3 L2 W: J( _2 H) e% @1 I2 r0 m则 LCD->LCD_RAM的地址为: 0X6000 0000 + 2^19 * 2 = 0X6010 00008 W! Q: M0 O/ {4 ?  G0 K
LCD->LCD_REG的地址可以为 LCD->LCD_RAM之外的任意地址.; B2 ~( e# ]3 x3 A; x+ @
由于我们使用结构体管理LCD_REG 和 LCD_RAM(REG在前,RAM在后,均为16位数据宽度)5 ~! j" E/ r/ E; P7 C, a
因此 结构体的基地址(LCD_BASE) = LCD_RAM - 2 = 0X6010 0000 -2
. V) z8 F, ]% q* }* K9 R更加通用的计算公式为((片选脚FMC_NEx)x=1/2/3/4, (RS接地址线FMC_Ay)y=0~25):8 T( k$ P% ?" |! v: C3 J( N
     LCD_BASE = (0X6000 0000 + (0X400 0000 * (x - 1))) | (2^y * 2 -2)
/ r: n4 }! Z7 H5 O% H! W$ x: X) o8 P6 ]- l# j# a0 B
     等效于(使用移位操作)4 ]7 C- I5 i& o" `
  ~' }$ Y  [1 \; b# C0 _
     LCD_BASE = (0X6000 0000 + (0X400 0000 * (x - 1))) | ((1 << y) * 2 -2)
+ W  G4 ]8 i7 C! A6 R, [( q- @# c8 j) S7 j* m
/
' y7 M0 E: ?( [- t% x) W) d#define LCD_BASE (uint32_t)((0X60000000 + (0X4000000 * (LCD_FMC_NEX - 1)))7 F: v8 |0 T( _  J4 j! V
| (((1 << LCD_FMC_AX) * 2) -2))" L1 M/ }4 @& w+ E; V0 l
#define LCD ((LCD_TypeDef ) LCD_BASE), x8 [' j! Z" G1 V/ Q
其中LCD_BASE,必须根据我们外部电路的连接来确定,即根据前面介绍的LCD_FMC_NEX和LCD_FMC_AX宏定义的值来确定。我们使用Bank1.sector1,把宏定义LCD_FMC_NEX的值代入(0X60000000 + (0X4000000 * (LCD_FMC_NEX - 1)))得到的就是存储区的基地址,即0x60000000。而FMC_A19对应地址值为(((1 << LCD_FMC_AX) * 2) -2)),代入LCD_FMC_AX的值得到0x000F FFFE,所以LCD_BASE表示的地址值为0x60000000 | 0x000F FFFE。2 q* u( h4 g' }6 c/ s3 ^- a
上面是直接代入我们给出的公式进行计算,但是很多朋友不太明白FMC_A19对应地址值怎么来的。下面我们来解析一下,以FMC_A19为例,0x000F FFFE转换成二进制就是:1111 1111 1111 1111 1110。从表25.1.2.2知道16位数据时,地址右移一位对齐,那么实际对应到地址引脚的时候,就是:[A19:A0]=0111 1111 1111 1111 1111,此时A19是0,但是如果16位地址再加1(注意:对应到8位地址是加2,即0x000F FFFE +0X02),那么:[A19:A0]=1000 0000 0000 0000 0000,这时A19就是1了,即实现了对RS的0和1的控制。) r, g9 b2 u% H8 @+ [& o
定义LCD的时候,我们将LCD_BASE这个地址强制转换为LCD_TypeDef结构体地址,那么可以得到LCD->LCD_REG的地址就是0x600F FFFE,地址右移一位对齐后对应A19的状态为0(即RS=0),而LCD->LCD_RAM的地址就是0x6010 0000(结构体地址自增),地址右移一位对齐后对应A19的状态为1(即RS=1)。; r3 L9 Y" `# C9 s  L
所以,有了这个定义,当我们要往LCD写命令/数据的时候,可以这样写:( e1 \- L+ A5 _4 ?$ v
LCD->LCD_REG = CMD; / 写命令 /
$ q: ~5 q  z% I* yLCD->LCD_RAM = DATA; / 写数据 /
3 |! S" L, b8 m/ r$ V. e  e) O而读的时候反过来操作就可以了,如下所示:
# p+ y8 X2 H- ?% o6 C0 j3 S+ eCMD = LCD->LCD_REG; / 读LCD寄存器 /
7 T8 ~& K* O. l0 ]DATA = LCD->LCD_RAM; / 读LCD数据 /
  }/ S5 H+ V. L" t& r这其中,CS、WR、RD和IO口方向都是由FMC硬件自动控制,不需要我们手动设置了。& y7 ^. W) C' P8 d3 }
最后是一些其他的宏定义,包括LCD扫描方向和颜色,以及SSD1963相关配置参数。
: a9 J' F9 _( D  s6 N# e; U+ T下面开始对lcd.c文件介绍,先看LCD初始化函数,其定义如下:
. M! _  F3 b" I2 q* C3 P5 D/7 Z! Z) n! t& W- O. m0 X/ j
! d  P+ }% b4 _8 {+ o2 v" g
@brief 初始化LCD
/ y+ E2 b' P8 i  w) R3 C: N# l3 Z4 x" Q0 g! v
@note 该初始化函数可以初始化各种型号的LCD(详见本.c文件最前面的描述)+ H$ O2 q9 r9 r' Y( S

# Q  V  W. ^6 E' W% h9 h1 O@param 无5 n9 {2 X# O6 H: }8 N: h

6 m: {- F: k+ _7 O4 u3 A% M, I@retval 无- y2 Y# j+ C$ h+ M
*/( k/ |# W& a" m3 Z2 Z2 \
void lcd_init(void)
" s) G) P. M, f" z- V9 t{
4 M. a- m; S0 iGPIO_InitTypeDef gpio_init_struct;
+ U$ P2 m/ r! H2 g3 h. P. LFMC_NORSRAM_TimingTypeDef fmc_read_handle;4 m+ D3 R5 `, [% Y
FMC_NORSRAM_TimingTypeDef fmc_write_handle;
2 C9 t8 e/ S% N: O$ E% ?8 W/ [1 `1 U# M0 y# b# P& c( ]- |
LCD_CS_GPIO_CLK_ENABLE(); /* LCD_CS脚时钟使能 /
4 i; c+ d8 H" J; O9 x. Q  Z) qLCD_WR_GPIO_CLK_ENABLE(); / LCD_WR脚时钟使能 /2 M4 h! L0 U# H, N/ u7 Z' F, [) R
LCD_RD_GPIO_CLK_ENABLE(); / LCD_RD脚时钟使能 /7 y* d! n  M) O5 o' U7 Q4 e5 `8 D
LCD_RS_GPIO_CLK_ENABLE(); / LCD_RS脚时钟使能 /
+ z9 O8 ?7 b" |' B/ BLCD_BL_GPIO_CLK_ENABLE(); / LCD_BL脚时钟使能 */
7 E; a( j6 B* m. ]- ~+ ]% t% w% m+ X1 E, w( \& I% o3 T) H
gpio_init_struct.Pin = LCD_CS_GPIO_PIN;
7 S* F4 S8 v9 A& {5 ?! Mgpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 推挽复用 /
2 ~! P( W' w# w" Q" f8 o. Ogpio_init_struct.Pull = GPIO_PULLUP; / 上拉 /3 i, x) Y1 l* q: T- ?* E6 ~
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; / 高速 /
: d- f' l3 e( x) z+ T& q- lgpio_init_struct.Alternate = GPIO_AF12_FMC; / 复用为FMC // e' n5 J" ]+ H8 w
HAL_GPIO_Init(LCD_CS_GPIO_PORT, &gpio_init_struct); / 初始化LCD_CS引脚 */
' R+ K# w' `  N
! Z) \+ T# U& R% |* Qgpio_init_struct.Pin = LCD_WR_GPIO_PIN;
! ?+ d* I9 p- x, s* ~0 z5 yHAL_GPIO_Init(LCD_WR_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_WR引脚 */6 ?  v- D1 G3 G$ X4 S

3 \: R7 k! K0 x4 j- fgpio_init_struct.Pin = LCD_RD_GPIO_PIN;" U0 K5 |- ^2 z7 ?" C- f: y
HAL_GPIO_Init(LCD_RD_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_RD引脚 */
7 Z; L; a1 F. R! X, M; H5 X) N/ w
2 e8 A6 d3 l2 B8 `" n' a# w* ^; g4 Ugpio_init_struct.Pin = LCD_RS_GPIO_PIN;- {" [/ O! E, [' P
HAL_GPIO_Init(LCD_RS_GPIO_PORT, &gpio_init_struct); /* 初始化LCD_RS引脚 */+ K' A' D* W+ I# Y/ H9 W

) {, k# Q/ @& X% C# K: v3 M5 E; Dgpio_init_struct.Pin = LCD_BL_GPIO_PIN;  W9 f* N% y- i& }
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 /. Q5 Q! x7 G& n! }3 B2 L
HAL_GPIO_Init(LCD_BL_GPIO_PORT, &gpio_init_struct);/ LCD_BL引脚模式设置 */
( Z% J6 U; I9 z* S
/ e1 x, \7 u# k: Ig_sram_handle.Instance = FMC_NORSRAM_DEVICE;! W) r7 L6 [5 j3 u; i
g_sram_handle.Extended = FMC_NORSRAM_EXTENDED_DEVICE;
! g" g1 q6 x2 w* p; h! f0 k! s: I/ E( @2 N) u
g_sram_handle.Init.NSBank = FMC_NORSRAM_BANK1; /* 使用NE1 /
9 Y0 O+ l2 x8 X. D3 K/ 不复用数据线 /3 T& ?% S: ?! D( o: \$ e" c8 b
g_sram_handle.Init.DataAddressMux = FMC_DATA_ADDRESS_MUX_DISABLE;; \% ~/ ^2 Q) G/ ?. n$ i
g_sram_handle.Init.MemoryType = FMC_MEMORY_TYPE_SRAM; / SRAM /
6 W. J, t; N; T* e$ |7 p/ 16位数据宽度 /  i. ^5 O0 K/ D
g_sram_handle.Init.MemoryDataWidth = FMC_NORSRAM_MEM_BUS_WIDTH_16;. j) x1 u! A% H/ p/ _
/ 是否使能突发访问,仅对同步突发存储器有效,此处未用到 /
1 y# E$ |+ T) M7 Gg_sram_handle.Init.BurstAccessMode = FMC_BURST_ACCESS_MODE_DISABLE;
! ?: _' F' L0 |; p9 l6 S/ 等待信号的极性,仅在突发模式访问下有用 /2 ~& g+ ]2 Z% S* Q/ G$ l
g_sram_handle.Init.WaitSignalPolarity = FMC_WAIT_SIGNAL_POLARITY_LOW;) _4 M6 h. {* {% ^
/ 存储器是在等待周期之前的一个时钟周期还是等待周期期间使能NWAIT /
  [& G) x! _; Yg_sram_handle.Init.WaitSignalActive = FMC_WAIT_TIMING_BEFORE_WS;
; Q( H# X: f$ _1 Y7 J3 C/ 存储器写使能 /
% N! S; t, h2 r3 pg_sram_handle.Init.WriteOperation = FMC_WRITE_OPERATION_ENABLE;
% U2 k9 m; s  h* K1 i& O$ _/ 等待使能位,此处未用到 /
" g. R# j! H: A9 j$ |' p2 K' Og_sram_handle.Init.WaitSignal = FMC_WAIT_SIGNAL_DISABLE;
& s1 I2 l4 \: N7 |' n/ 读写使用不同的时序 /
1 J) B7 }3 o7 d2 [# d9 z5 J" ]g_sram_handle.Init.ExtendedMode = FMC_EXTENDED_MODE_ENABLE;+ p/ h- |! j- v+ q
/ 是否使能同步传输模式下的等待信号,此处未用到 /7 Y' y- t* O" K2 h3 ^: Y
g_sram_handle.Init.AsynchronousWait = FMC_ASYNCHRONOUS_WAIT_DISABLE;8 W2 E/ F- X" W, Y" i3 P# M( x
g_sram_handle.Init.WriteBurst = FMC_WRITE_BURST_DISABLE; / 禁止突发写 */* @( W+ Y# o( H1 y
g_sram_handle.Init.ContinuousClock = FMC_CONTINUOUS_CLOCK_SYNC_ASYNC;9 D5 |8 D5 O+ W. ~1 ]: \

' X0 |9 E. z9 `7 I' P; m/* FMC读时序控制寄存器 /
' ~$ n# v- P  i9 _; ^& G/ 地址建立时间(ADDSET)为15个fmc_ker_ck 1/220M=4.5ns15=67.5ns /- t$ B8 o! t8 Z" `+ x! Z% W
fmc_read_handle.AddressSetupTime = 0x15;
" u) K7 P/ Z: W' |* V! p/ 数据保存时间(DATAST)为78个fmc_ker_ck=4.578=351ns /
& c3 N( h' Y: f6 [0 e) kfmc_read_handle.AddressHoldTime = 0x00;
  t/ L0 O1 f( X) {; B3 E/ 因为液晶驱动IC的读数据的时候,速度不能太快,尤其是个别奇葩芯片 /- r4 R4 N4 f6 k$ u* ]7 g& v
fmc_read_handle.DataSetupTime = 0x78;' i  G% G3 f# p0 h
fmc_read_handle.AccessMode = FMC_ACCESS_MODE_A; / 模式A */
) {) y, T* Z+ V) p# L/ h0 M. `) u- t+ E% {% J
/* FMC写时序控制寄存器 /( M. k( ?. u7 _+ R6 I  e
/ 地址建立时间(ADDSET)为15个fmc_ker_ck=67.5ns /. G% h9 v0 e( m6 e
fmc_write_handle.AddressSetupTime = 0x15;+ R4 e! j8 X, i9 R2 h  M/ E
/ 数据保存时间(DATAST)为15个fmc_ker_ck=67.5ns */
$ `' {& t  f# X( m, W0 _fmc_write_handle.AddressHoldTime = 0x00;
" P& f+ }7 Y/ |2 Z  ^0 G# {/*15个fmc_ker_ck(fmc_ker_ck=220Mhz),某些液晶驱动IC的写信号脉宽,最少也得50ns /
9 |" U0 ]( `7 b" Z+ q1 N2 B6 ?: Vfmc_write_handle.DataSetupTime = 0x15;
) h& e* J2 R7 ]# h& V( Y! M3 hfmc_write_handle.AccessMode = FMC_ACCESS_MODE_A; / 模式A */
5 ?6 R, A: [. N) ?HAL_SRAM_Init(&g_sram_handle, &fmc_read_handle, &fmc_write_handle);+ E# W3 T1 j. w6 l
delay_ms(50);
3 r) @; i1 n( P3 w9 O, j5 `- ~' W/ D
  1. /* 尝试9341 ID的读取 */- w! E1 k# |1 z
  2. lcd_wr_regno(0XD3);
    % _. c6 B. }6 G$ n
  3. lcddev.id = lcd_rd_data();          /* dummy read */
    % z' _4 B4 s* V% b" M3 I# z/ F
  4. lcddev.id = lcd_rd_data();  /* 读到0X00 */
    # k3 u. G7 O/ K  n
  5. lcddev.id = lcd_rd_data();  /* 读取0X93 */8 z; i- J2 o) a
  6. lcddev.id <<= 8;: w+ N! n: ~2 R( k* \5 D
  7. lcddev.id |= lcd_rd_data();        /* 读取0X41 */
    1 w5 e2 N0 s6 I; ]* a6 ]3 N3 x
  8. + ~$ Q, A9 @5 H. n3 [9 V) Z/ G
  9. if (lcddev.id != 0X9341)            /* 不是 9341 , 尝试看看是不是 ST7789 */5 G5 ]$ S! K9 d" i- f, ~: r
  10. {& M" t# s/ {1 D: e- u9 e0 [1 Y
  11.     lcd_wr_regno(0X04);1 a' v/ n( c! |; J9 D6 V3 G
  12.     lcddev.id = lcd_rd_data();              /* dummy read */
    + k- |7 _* g6 @
  13.     lcddev.id = lcd_rd_data();              /* 读到0X85 */
    ( {; j  J# g" S" F1 x
  14.     lcddev.id = lcd_rd_data();              /* 读取0X85 */
    ; r; l7 I( @% j$ H; q7 v5 o: l
  15.     lcddev.id <<= 8;4 D: Z$ G7 l) V/ [9 K7 I; ~0 A
  16.     lcddev.id |= lcd_rd_data();             /* 读取0X52 */, z3 C) a8 u7 R5 d; Y; O0 |
  17. $ @8 _5 Z( a. U
  18.     if (lcddev.id == 0X8552)                /* 将8552的ID转换成7789 */% S. z7 V( K" _- g$ j( p
  19.     {
    # _7 r$ L3 _* R* a1 K# o' d
  20.         lcddev.id = 0x7789;
    0 w* r2 n$ A6 s; e  @; q
  21.     }
    2 ?/ A. U4 X& R4 {- Q; @

  22. 1 D7 K  L# B( G
  23.     if (lcddev.id != 0x7789)                /* 也不是ST7789, 尝试是不是 NT35310 */
    + M* R- e  M2 z- L
  24.     {& N6 k6 z1 f- b8 {! V
  25.         lcd_wr_regno(0XD4);
    5 X* I8 a+ ~: A6 Z* E0 N9 [
  26.         lcddev.id = lcd_rd_data();          /* dummy read */' @$ U9 Y* ^3 a' u9 r) B
  27.         lcddev.id = lcd_rd_data();          /* 读回0X01 */
    5 \* G; y- k- i
  28.         lcddev.id = lcd_rd_data();          /* 读回0X53 */
    5 t- j$ |8 \7 r8 |$ P! F
  29.         lcddev.id <<= 8;
    4 L7 A) {# ?$ ]. R4 r, ?/ f
  30.         lcddev.id |= lcd_rd_data();         /* 这里读回0X10 *// h& U% M/ V, u* h' h# k
  31. ! ^6 ?9 c+ C  H  b# N5 I
  32.         if (lcddev.id != 0X5310)            /* 也不是NT35310,尝试看看是不是NT35510 */
    6 m! W, \5 O5 B# k  P8 c
  33.         {8 z: d4 Z$ t" G
  34.             /* 发送秘钥(厂家提供,照搬即可) */- E! t! N7 K, s$ N- X- M
  35.             lcd_write_reg(0xF000, 0x0055);$ S4 @9 [( V' r" U
  36.             lcd_write_reg(0xF001, 0x00AA);$ s" p) w+ t) B/ o( A$ D. C
  37.             lcd_write_reg(0xF002, 0x0052);9 w' f* l" Y+ o& z' F  r6 E+ @
  38.             lcd_write_reg(0xF003, 0x0008);9 e! d" k0 Q: v: X
  39.             lcd_write_reg(0xF004, 0x0001);" C. x$ L$ M# T% z% `

  40. / g8 r2 B, L5 h7 P
  41.             lcd_wr_regno(0xC500);                  /* 读取ID高8位 */
    " N1 r% F" y2 |% u. ]
  42.             lcddev.id = lcd_rd_data();        /* 读回0X55 */& k) q8 j) g! ?$ A! Y; O0 L& C: q, \
  43.             lcddev.id <<= 8;! d9 E: Q6 `2 j7 z$ A
  44. % I5 G3 ?7 f: ~2 v% Q, z
  45.             lcd_wr_regno(0xC501);                     /* 读取ID低8位 */) q/ i# M) e8 Y
  46.             lcddev.id |= lcd_rd_data();        /* 读回0X10 */
    : G4 i! C4 n4 _  |; a
  47.             delay_ms(5);
    & u8 o! D5 s$ o8 a

  48. 8 R% z, r( H: r6 r
  49.             if (lcddev.id != 0X5510)    /* 也不是NT5510,尝试看看是不是SSD1963 */$ y1 [% l) K) Q5 \2 p5 |6 y
  50.             {/ V' o$ K# k3 A! X
  51.                 lcd_wr_regno(0XA1);9 N' l# C# F/ F2 n$ j
  52.                 lcddev.id = lcd_rd_data();& q. M& F% {! q# T. K
  53.                 lcddev.id = lcd_rd_data();  /* 读回0X57 */
    ; z) D. Z" A# R/ P' I+ P
  54.                 lcddev.id <<= 8;& I' ?7 j5 d% e& T+ ]3 i, k) P9 D7 m
  55.                 lcddev.id |= lcd_rd_data(); /* 读回0X61 */' ]5 v6 Z- u, V8 f+ k8 d; \  b; {
  56. /* SSD1963读回的ID是5761H,为方便区分,我们强制设置为1963 */3 J( i- W* }' p( m
  57. if (lcddev.id == 0X5761)lcddev.id = 0X1963;  I# }0 p2 n, _( I
  58. }7 ]/ a1 f( e' B
  59. }
    # a# j, M' R0 n# e
  60. }5 |! Y# a7 s9 J
  61. }
复制代码
  1. /* 特别注意, 如果在main函数里面屏蔽串口1初始化, 则会卡死在printf. Q: _" q0 H' d' r4 p) g( F( I
  2. * 里面(卡死在f_putc函数), 所以, 必须初始化串口1, 或者屏蔽掉下面
    ' k9 n- ]& u" q# i9 N
  3. * 这行 printf 语句 !!!!!!!
    " r0 R) U5 Z0 `
  4. */: K6 \, a2 Y; G2 `
  5. printf("LCD ID:%x\r\n", lcddev.id); /* 打印LCD ID */
    ( B& Z4 n$ v( J$ x, ?2 Y; P9 u" i

  6. / l1 ^  l0 Q' }6 L6 f; [
  7. if (lcddev.id == 0X7789)
    5 h9 H/ A0 g5 U2 t# ^* T1 ~
  8. {, m1 ?$ o+ }0 u9 \: b: a, `: ?7 E* J
  9.     lcd_ex_st7789_reginit();            /* 执行ST7789初始化 */
    * K6 l9 Z: D. L0 _2 ~
  10. }- f  D8 T. ]4 D( N3 o. E7 r, Z7 t
  11. else if (lcddev.id == 0X9341)
    / ~: m0 F, w$ `! |  X$ A- q
  12. {
    * v& o  L' a# ~7 Y  Y- ?8 B. u
  13.     lcd_ex_ili9341_reginit();           /* 执行ILI9341初始化 */, R# b- Z  v7 l/ f3 ]
  14. }. g! q3 K) o( T. [$ e/ h4 H
  15. else if (lcddev.id == 0x5310)
    ' w  r' @$ d0 n: q+ M8 j
  16. {7 `5 D# x$ ?* L: B% X
  17.     lcd_ex_nt35310_reginit();           /* 执行NT35310初始化 */7 `& H4 I( k7 ]" O
  18. }) K% Q1 N1 E. t
  19. else if (lcddev.id == 0x5510)
    , a4 B% ^5 {, O& g" V# G
  20. {
    6 t# z# }" Z1 q3 `4 M
  21.     lcd_ex_nt35510_reginit();          /* 执行NT35510初始化 */( M/ L: V- A1 q; ?; k
  22. }
    7 \; k' l9 G. N9 X- K* x4 W
  23. else if (lcddev.id == 0X1963)' m  V+ \' p% H
  24. {
    # k) w! a; U: m# b; V: P
  25.     lcd_ex_ssd1963_reginit();    /* 执行SSD1963初始化 */
    6 s, N8 W- e( Q7 t$ Y! I
  26.     lcd_ssd_backlight_set(100);         /* 背光设置为最亮 */$ r. Z( h7 o! d/ j4 o9 u/ W
  27. }! W% u3 Z+ M# x
  28. # F; \- s& S* W* D7 X
  29. /* 初始化完成以后,提速 *// N; h, d. @  f" I2 z. D
  30. if (lcddev.id == 0X7789)                /* ST7789 提速 */$ G# B4 y/ v) y; i- b
  31. {$ ~# h0 o4 o8 L6 B6 O3 d, n
  32.     /* 重新配置写时序控制寄存器的时序 */, H) c5 F3 l5 D) T
  33.     fmc_write_handle.AddressSetupTime = 5;
    # ?, w, k# R) ]! k' W
  34.     fmc_write_handle.DataSetupTime = 5;" v* D$ y# p' }' P1 e4 d: P
  35.     FMC_NORSRAM_Extended_Timing_Init(g_sram_handle.Extended, ; E" w, C" W6 x' q% `2 s0 Q

  36. " `- e! R/ Z1 L7 E0 F
  37. &fmc_write_handle, g_sram_handle.Init.NSBank,/ |  w0 ~5 i6 J8 t$ w; N
  38. g_sram_handle.Init.ExtendedMode);
    9 ?; o5 B% ?+ \2 G
  39. }. _  Y0 P; p7 e' ?
  40. /* 如果是这几个IC,则设置WR时序为最快 /
      _; B' z( b  d  T; R1 F
  41. if (lcddev.id == 0X9341 || lcddev.id == 0X1963)6 {2 ?# Y5 k1 r) k3 ^$ ~0 S
  42. {5 D2 _% n/ @$ J3 f
  43. / 重新配置写时序控制寄存器的时序 /  P- O" C8 A/ x9 J0 h/ H. j
  44. fmc_write_handle.AddressSetupTime = 3;5 ], x' \% M6 P  f) q
  45. fmc_write_handle.DataSetupTime = 3;" I* B; Q: b1 \) X9 j
  46. FMC_NORSRAM_Extended_Timing_Init(g_sram_handle.Extended,2 V  b) \. @' s
  47. &fmc_write_handle, g_sram_handle.Init.NSBank,' G! e8 X& p7 T, ?
  48. g_sram_handle.Init.ExtendedMode);' b! @  v  C1 ?' R6 u7 a) p/ |
  49. }: y5 d5 ]! s" I3 b: ]
  50. / 如果是这几个IC,则设置WR时序为最快 /
    3 k( ~: b" m3 y$ _4 J
  51. if (lcddev.id == 0X5310 || lcddev.id == 0X5510)4 M; p8 U. h% t
  52. {; s: |$ g& O! o
  53. / 重新配置写时序控制寄存器的时序 /, c- a! t4 s, \( W
  54. fmc_write_handle.AddressSetupTime = 2;
    3 P9 [9 N- R! y3 r. I2 R1 V+ r' b
  55. fmc_write_handle.DataSetupTime = 2;
    7 s+ b6 }# }7 K+ e/ G$ p
  56. FMC_NORSRAM_Extended_Timing_Init(g_sram_handle.Extended,
    : p: \& ?8 m' O+ D7 n
  57. &fmc_write_handle, g_sram_handle.Init.NSBank,2 b6 Z3 e5 L3 s
  58. g_sram_handle.Init.ExtendedMode);
    4 o  H, X% T: i
  59. }
    . B7 Q  f0 e$ ]# g% l7 w2 G' U
  60. lcd_display_dir(0); / 默认为竖屏 /  T$ k' e1 m4 O( ]4 J) ?2 [
  61. LCD_BL(1); / 点亮背光 */
    # L# e1 N4 l3 t9 H8 D
  62. lcd_clear(WHITE);
    , I, N- g: }3 \& T3 r) ?
  63. }
复制代码

- |6 z7 d: x; Z# E* D该函数先对FMC相关IO进行初始化,然后使用HAL_SRAM_Init函数初始化FMC控制器,同时我们使用HAL_SRAM_MspInit回调函数来初始化相应的IO口,最后读取LCD控制器的型号,根据控制IC的型号执行不同的初始化代码,这样提高了整个程序的通用性。为了简化lcd.c的初始化程序,不同控制IC的芯片对应的初始化程序(如:lcd_ex_st7789_reginit()、lcd_ex_ili9341_reginit()等)我们放在lcd_ex.c文件中,这些初始化代码完成对LCD寄存器的初始化,由LCD厂家提供,一般是不需要做任何修改的,我们直接调用就可以了。! I+ j8 d; F9 T* x+ h" Z4 H4 W
下面是6个简单,但是很重要的函数:
/ }; F6 R- ]$ m7 T( t; F# _* c6 u0 o. b' B
  1. /**& W6 A' K( w: ~7 l; U
  2. * @brief               LCD写数据
    # x  b$ R1 {+ Y" T. z
  3. * @param               data: 要写入的数据7 `+ f$ N" H, j! J7 a* T  ?
  4. * @retval              无" {9 W3 D( r+ z( |
  5. */
    / X) ]; J9 @  g$ b
  6. void lcd_wr_data(volatile uint16_t data)
    8 R; G) ~* o. {" S/ M& M* l" `1 t
  7. {
    ! Z% Y7 O4 R4 m& }: h4 N+ P
  8.     data = data;            /* 使用-O2优化的时候,必须插入的延时 */
    5 E/ y/ G( d! e: A# y
  9.     LCD->LCD_RAM = data;
    # C. k" G8 ^, }* P
  10. }) J2 s# j5 c% Z) G3 ?; L
  11. 1 B4 \* o5 B0 X* E1 w/ `
  12. /**5 @8 {. y7 X- \
  13. * @brief               LCD写寄存器编号/地址函数
    7 ]* }9 s8 `2 |3 U* y$ W
  14. * @param               regno: 寄存器编号/地址# x- B: R4 a) E; Q9 F: g
  15. * @retval              无
    1 S* i5 N( ?. E8 b* ^- J
  16. */
    ; @( ?6 n. ~  k; `1 ?( i& u0 j
  17. void lcd_wr_regno(volatile uint16_t regno)" ~( E( R; q% |. Q1 W7 N
  18. {
    % Z3 _/ I" }9 z  y. y" u" X2 W& `5 d/ s
  19.     regno = regno;                /* 使用-O2优化的时候,必须插入的延时 */
    - R. ]" w& v, g. j, e
  20.     LCD->LCD_REG = regno;         /* 写入要写的寄存器序号 */$ B7 F6 q8 \% P& j$ v' R
  21. }5 L! \8 q' k4 m* @
  22. / [+ K& s% b' k* \$ y. ~- Z4 p6 `: p
  23. /**
    $ U3 A; G8 d/ j9 ]3 }
  24. * @brief               LCD写寄存器
    : j+ X: o" _4 @4 Q0 u& e
  25. * @param               regno:寄存器编号/地址' p& I4 e$ r+ A/ W7 |4 i3 [8 O
  26. * @param               data:要写入的数据  {: ]0 I+ L% @0 b8 l1 y& K
  27. * @retval              无' ~& U7 J( k: T; m' u) ]
  28. */
    8 A) G3 [( Y8 {  h  E
  29. void lcd_write_reg(uint16_t regno, uint16_t data)3 P. G4 w) r$ I7 G7 d5 x
  30. {
    ! ?0 d! W7 {! [/ w* U* K6 r+ o
  31.     LCD->LCD_REG = regno;   /* 写入要写的寄存器序号 */
    # }, R* N# V$ L# Y% |
  32.     LCD->LCD_RAM = data;    /* 写入数据 */" ]3 m0 O: A9 t9 v+ C
  33. }1 r* k7 k, h+ Y$ o6 y; {  L

  34. $ }" B' h$ W3 V" G8 w
  35. /**0 j, Q9 L+ s/ s
  36. * @brief             LCD延时函数,仅用于部分在mdk -O1时间优化时需要设置的地方0 [: n( b+ k' q) {
  37. * @param              t:延时的数值
    . w+ ]* l6 o/ T: ~; b
  38. * @retval              无( ?/ w! c1 ^2 ]$ v
  39. */
    " O% v# v) k" A, K
  40. static void lcd_opt_delay(uint32_t i)/ D6 f  M, D. ]" V* Z
  41. {
    + T6 _4 A% P$ {4 ]
  42.     while (i--);; h! ~3 k/ u- S: o; Y. f1 P5 `
  43. }5 w' z: n0 p+ Z# V" M. A

  44. 3 O2 f. n% ^' L7 n- L, U' u1 f
  45. /**- p8 s  `7 I" Z0 L" k! @* a. S
  46. * @brief               LCD读数据
    0 q+ U0 ]/ a' t6 V+ K& p/ u
  47. * @param               无
    4 z! A5 p( `6 g) }5 c0 K
  48. * @retval              读取到的数据
    / t& w; `" P4 R8 n2 T
  49. */) M1 c' u) W2 R& |/ P3 a+ K& L, D
  50. static uint16_t lcd_rd_data(void)3 e# b1 w/ b6 f" z& C# R; x5 f
  51. {, {) W- w, D2 _( v
  52. volatile uint16_t ram;  /* 防止被优化 */' O; k+ i* [* Q
  53.     lcd_opt_delay(2);
    . H6 W: a) E1 z8 M$ w- S" A
  54.     ram = LCD->LCD_RAM;; j: i& V  Q/ S/ l; j
  55.     return ram;% y. l6 T% e0 Y; e% [8 r7 R7 _
  56. }3 V+ ]/ p  X5 {$ J$ X- [

  57. ( |3 ~5 j3 f( w* p+ o
  58. /**7 y( i+ ^7 l- o  k8 X  {+ p; _2 h
  59. * @brief              准备写GRAM, Q- k$ @9 |8 }9 s- x" g- v
  60. * @param               无
    " Q! }1 Y7 E3 A" Z
  61. * @retval              无# k/ h5 G% C% L  ?( k" ?
  62. */4 Q" A: w6 d( f3 Q$ B5 a
  63. void lcd_write_ram_prepare(void)
    # [. [0 c! [6 `) c$ F8 G* ]
  64. {
    2 X- F4 r/ M" v7 R( y3 j0 U' p9 L
  65.     LCD->LCD_REG = lcddev.wramcmd;
    / X( ], E' ^) a* {$ p
  66. }
复制代码
& D4 [. P$ {  Z# E% S
因为FMC自动控制了WR/RD/CS等这些信号,所以这6个函数实现起来都非常简单,我们就不多说,注意,上面有几个函数,我们添加了一些对MDK –O2优化的支持,去掉的话,在-O2优化的时候会出问题。这些函数实现功能见函数前面的备注,通过这几个简单函数的组合,我们就可以对LCD进行各种操作了。
. e% _0 F8 Z2 X+ ?$ }下面要介绍的函数是坐标设置函数,该函数代码如下:
7 U% A: A9 Q& ?1 f5 c, D
/ v; t: Y/ R* X! j; b
  1. /**- T7 v/ N; d1 R* m+ C
  2. * @brief              设置光标位置(对RGB屏无效)
    $ l- T( @- w( _# e* j
  3. * @param               x,y: 坐标
    9 \! q- c/ i; a& K
  4. * @retval              无
    5 A0 {( _# {9 F4 c1 D
  5. */. h/ ~6 |; A) ^* b
  6. void lcd_set_cursor(uint16_t x, uint16_t y)
    : |! _7 M" d, K4 b
  7. {
    . e0 Z$ X' j. B) B+ w+ T
  8.     if (lcddev.id == 0X1963)
    # o$ f: X7 W# j3 m: X
  9.     {
    3 n" V* j, N' b( Z' \
  10.         if (lcddev.dir == 0)                /* 竖屏模式, x坐标需要变换 */
      U( h' f- W* |) F+ s' T
  11.         {
      b$ r$ A4 _8 X
  12.             x = lcddev.width - 1 - x;0 }  r3 h! e  A( s( B' a9 W9 _
  13.             lcd_wr_regno(lcddev.setxcmd);
    2 z, ]/ i8 m3 N
  14.             lcd_wr_data(0);5 Y; Z1 M6 X/ m8 a
  15.             lcd_wr_data(0);
    6 y' Z# T9 N1 G
  16.             lcd_wr_data(x >> 8);2 {3 c2 ~. {( C% i& v
  17.             lcd_wr_data(x & 0XFF);8 N& `! Z* B$ Z5 ~
  18.         }7 B+ y: T6 T4 l- t/ L
  19.         else                            /* 横屏模式 */8 K9 ]/ y6 [  J- y
  20.         {
    * ?. q: b3 e; t. s* B
  21.             lcd_wr_regno(lcddev.setxcmd);; a% o* r/ Z0 K0 Z
  22.             lcd_wr_data(x >> 8);
    2 S; s. m5 A4 M1 [
  23.             lcd_wr_data(x & 0XFF);) ^" o# H( W, {$ X( ?
  24.             lcd_wr_data((lcddev.width - 1) >> 8);" C# K8 x7 Y: {, C" m
  25.             lcd_wr_data((lcddev.width - 1) & 0XFF);
    6 X+ o- N& D% r" l5 s7 n9 \; t
  26.         }
    0 J. Q3 b. y- d0 P" {' u: e
  27. $ n7 v" ^$ L7 b! T3 L# @
  28.         lcd_wr_regno(lcddev.setycmd);& A2 z/ w7 x% T( e( t
  29.         lcd_wr_data(y >> 8);
    4 q& i+ ]7 h2 d' D, M1 q
  30.         lcd_wr_data(y & 0XFF);
    # |: d6 u, q; W3 l2 C  Q6 A
  31.         lcd_wr_data((lcddev.height - 1) >> 8);2 E" f7 [2 R( p, G/ b, _5 j
  32.         lcd_wr_data((lcddev.height - 1) & 0XFF);$ d, Y" k, a; n8 I: o$ @/ ~
  33. & Q$ ^# \7 `8 Z- L3 B
  34.     }
    . E% @% O! M8 N, b; X$ y
  35.     else if (lcddev.id == 0X5510)
    9 ]0 s/ o& p9 p" I: o
  36.     {" a5 S- }) {. V" E3 u. n
  37.         lcd_wr_regno(lcddev.setxcmd);1 B7 M3 S, Z3 m7 p# v. H
  38.         lcd_wr_data(x >> 8);4 `2 S- v) j$ b7 N% ?4 d- D
  39.         lcd_wr_regno(lcddev.setxcmd + 1);
    ! Z, ?9 B& R) Q; {0 }2 {1 Y( T
  40.         lcd_wr_data(x & 0XFF);
    + n$ ~& e% r$ f0 A9 C/ ]
  41.         lcd_wr_regno(lcddev.setycmd);% w5 z5 ]# Z; |  ^" e; U
  42.         lcd_wr_data(y >> 8);7 s/ m9 P0 u& n0 {  a. M: ?& d
  43.         lcd_wr_regno(lcddev.setycmd + 1);
    % W1 p" Y- [% t1 _6 y
  44.         lcd_wr_data(y & 0XFF);
    - g6 O# N: T( i- t5 i$ X2 }
  45.     }% x  W5 O5 {* F. ?$ _. I
  46.     else    /* 9341/5310/7789 等 设置坐标 */4 e+ ?7 {7 }" m2 ]- }% ]
  47.     {( g) }8 V4 r( j- t/ q% G
  48.         lcd_wr_regno(lcddev.setxcmd);
    5 i8 y$ @2 h# u+ A0 H1 t$ ?4 q
  49.         lcd_wr_data(x >> 8);
    1 i4 m* n0 M+ J- ]* q
  50.         lcd_wr_data(x & 0XFF);' X: ~4 v2 B2 i* j. i3 O
  51.         lcd_wr_regno(lcddev.setycmd);
    , g! G: O) M3 m
  52.         lcd_wr_data(y >> 8);
    + \- w0 i; e; N+ H" B$ V6 o
  53.         lcd_wr_data(y & 0XFF);
    4 X. M" l  m, C- O$ F& u
  54.     }- }% }6 J6 X7 i( V" p
  55. }- S8 B5 }9 V) S0 d
复制代码
3 W' n6 x9 j1 w. Z& S
该函数实现将LCD的当前操作点设置到指定坐标(x,y)。因为9341/5310/1963/5510等的设置有些不太一样,所以进行了区别对待。
- l1 R, B  u% r& y接下来介绍画点函数,其定义如下:
5 Y; z% }# H" ~7 @6 M% Z: e/**
( T8 q8 p' n4 L! y% G$ \7 ~
1 c4 `# @- z  Q/ b4 Y@brief 画点4 F) k$ H0 h* w4 |) |- b- A1 f2 `& R
@param x,y: 坐标" u6 Z$ _6 V/ w, U9 G, @$ o
@param color: 点的颜色(32位颜色,方便兼容LTDC)8 X4 ?( d3 G& K) ]: ~9 ~
@retval 无
6 x. _+ V( Q& P# ^) d/; I. n& c! J' i2 z5 p5 O1 C- H/ ?5 m( X
void lcd_draw_point(uint16_t x, uint16_t y, uint32_t color)
2 R+ n" x* j( q6 w# U, d2 k: O{% x5 h) A+ N4 _( T$ C; v) K
lcd_set_cursor(x, y); / 设置光标位置 /1 s+ ^) m" Q8 ^# ~
lcd_write_ram_prepare(); / 开始写入GRAM */
1 |, f3 |, Q% L9 v& C- uLCD->LCD_RAM = color;
4 V/ ~% m- u6 d8 O& F) u, Y}
' R2 F% b, P* y7 ?& n* i该函数实现比较简单,就是先设置坐标,然后往坐标写颜色。lcd_draw_point函数虽然简单,但是至关重要,其他几乎所有上层函数,都是通过调用这个函数实现的。
  s" M8 K* B% ]2 y" ~下面介绍读点函数,用于读取LCD的GRAM,这里说明一下,为什么OLED模块没做读GRAM的函数,而这里做了。因为OLED模块是单色的,所需要全部GRAM也就1K个字节,而TFTLCD模块为彩色的,点数也比OLED模块多很多,以16位色计算,一款320×240的液晶,需要320×240×2个字节来存储颜色值,也就是也需要150K字节,这对任何一款单片机来说,都不是一个小数目了。而且我们在图形叠加的时候,可以先读回原来的值,然后写入新的值,在完成叠加后,我们又恢复原来的值。这样在做一些简单菜单的时候,是很有用的。这里我们读取TFTLCD模块数据的函数为lcd_read_point,该函数直接返回读到的GRAM值。该函数使用之前要先设置读取的GRAM地址,通过lcd_set_cursor函数来实现。lcd_read_point的代码如下:1 ]4 C0 V' i" w6 A: [8 ]) Z
  1. /**
    7 P* U" M" Y- V3 S
  2. * @brief              读取个某点的颜色值
    ) O! s( q6 Z( h- l
  3. * @param               x,y:坐标
    9 ?1 E, i4 v; L3 F+ W
  4. * @retval              此点的颜色(32位颜色,方便兼容LTDC)2 b  [  n) V3 D9 ]8 b# f
  5. */
    / ?  w. @  Q# r8 V
  6. uint32_t lcd_read_point(uint16_t x, uint16_t y)
    ! t6 \* O: z! S  x' G" W/ k% T
  7. {- |7 d4 y5 x% Z* k9 ]: S
  8.     uint16_t r = 0, g = 0, b = 0;1 z' k  V5 O( |. @9 w

  9. ( m( g4 m) m1 h- f$ o+ G0 g! i+ H
  10.     if (x >= lcddev.width || y >= lcddev.height)return 0; /* 超过了范围,直接返回 */0 J  Y7 P* s1 L6 U; T
  11. ; G0 X  x) C7 P- \  L  r
  12.     lcd_set_cursor(x, y);       /* 设置坐标 */8 W- W6 t  E3 r8 W8 d9 g
  13. : I: J5 M4 i/ m4 ~8 r
  14.     if (lcddev.id == 0X5510)7 B! J6 n+ a* v4 \! @4 b% j
  15.     {; H4 `9 V, v+ o% r- y
  16.         lcd_wr_regno(0X2E00);   /* 5510 发送读GRAM指令 */
    + O% C6 S) Z5 G$ N, B. y9 L/ x$ x( D
  17.     }% V1 I% f( O" u/ a! Y
  18.     else+ L* `+ [+ q9 p# d& e0 R
  19.     {$ T1 B# `7 i& w/ u& s
  20.         lcd_wr_regno(0X2E);     /* 9341/5310/1963/7789 等发送读GRAM指令 */
    1 i8 |: O* M& w5 Z# r- {# b
  21.     }+ f* I4 M7 u" E/ Q
  22.     r = lcd_rd_data();          /* 假读(dummy read) */# K8 g* {' N& |. ]

  23. & g4 R! C4 P9 K  }, `. _0 x; `
  24.     if (lcddev.id == 0X1963)return r;   /* 1963 直接读就可以 */3 j! u8 V' p4 M
  25. . s0 F9 r0 T9 L8 v$ D3 c. \
  26.     r = lcd_rd_data();  /* 实际坐标颜色 */
    # z2 F* ~2 A0 {0 K* T% k5 G! R* D) C
  27. 0 k, L3 o# t% P  W* D- N
  28.     /* 9341/5310/5510/7789 要分2次读出 */5 r, u: z, H- l, z+ t( i
  29. b = lcd_rd_data();, |/ M; m% s8 Z9 i
  30. /* 对于 9341/5310/5510/7789, 第一次读取的是RG的值,R在前,G在后,各占8位 */
    9 O! Y0 F) X5 V
  31.     g = r & 0XFF; $ k! d2 P- g, N' ~
  32. g <<= 8;
    % x1 p2 m  j! u0 N( T
  33. /* 9341/5310/5510/7789 需要公式转换一下 */
      w; C$ n9 c# X2 n- `% M/ t
  34.     return (((r >> 11) << 11) | ((g >> 10) << 5) | (b >> 11));  2 y9 y  U5 |( ?& ?
  35. }
复制代码
8 a$ z4 n% v" M' O; k9 n; }
在lcd_read_point函数中,因为我们的代码不止支持一种LCD驱动器,所以,我们根据不同的LCD驱动器((lcddev.id)型号,执行不同的操作,以实现对各个驱动器兼容,提高函数的通用性。
1 g4 Z# C3 l0 f. |( V* c第十个要介绍的是字符显示函数lcd_show_char,该函数同前面OLED模块的字符显示函数差不多,但是这里的字符显示函数多了1个功能,就是可以以叠加方式显示,或者以非叠加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。该函数实现代码如下:( ?0 {1 [; Z$ X6 p6 B+ u
5 f7 Z: h" f0 O9 c, U
  1. /**
    % [( g  G5 [8 Q" D, I
  2. * @brief              在指定位置显示一个字符
      ~/ l$ R1 X9 z3 W  [
  3. * @param               x,y  : 坐标
    1 C! C4 l# O  m5 L' b& O
  4. * @param               chr  : 要显示的字符:" "--->"~"3 H' z0 h. r: c. l1 j+ ^! a
  5. * @param              size : 字体大小 12/16/24/32: y3 C; L0 r  W, y: p& u9 c
  6. * @param              mode : 叠加方式(1); 非叠加方式(0);
    + }4 ?6 [8 I* h: D
  7. * @retval             无
    9 }5 y5 R7 Z$ e7 \3 N- T2 v
  8. */" j! H0 d- {# M. I) t4 \
  9. void lcd_show_char(uint16_t x, uint16_t y, char chr, uint8_t size, . z* Z0 P7 \" S! G
  10. uint8_t mode, uint16_t color)  ]4 m/ [+ @* i9 L- [
  11. {; I6 c) b+ s! P0 _
  12.     uint8_t temp, t1, t;3 Y2 o7 v) K! E0 n. Y2 W9 C; K
  13.     uint16_t y0 = y;# h8 j8 ?0 M. b
  14.     uint8_t csize = 0;
    / C' n  X! U& [9 [$ Z
  15.     uint8_t *pfont = 0;
    ; U- e& N) o3 M
  16. /* 得到字体一个字符对应点阵集所占的字节数 */* c: E/ R* |/ Z- U  L  h; f
  17. csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); ) B+ s7 H9 }! G% G# g
  18. /* 得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库) */
    ) G4 Q, Z% B$ V5 s" l1 [
  19.     chr = chr - ' ';   
    / a% [# o9 V- ?: U; t0 F
  20. % T. B4 B* i& a" E" O; l
  21.     switch (size)! E, W. [" X0 U: W
  22.     {: P) y/ p+ X) F1 P! j" K
  23.         case 12:
    . O. D! I7 G* V0 ]
  24.             pfont = (uint8_t *)asc2_1206[chr];  /* 调用1206字体 */
    5 q" u8 N0 ^" g4 b1 A1 x7 a( }$ f
  25.             break;
    - C/ X: v- m, h  F6 l' D1 L

  26. # c' w0 N" C% w$ n* J& u
  27.         case 16:! m( B" k- I3 S+ d4 T2 l* K/ p$ N
  28.             pfont = (uint8_t *)asc2_1608[chr];  /* 调用1608字体 */
    ) H' l# y: \* L
  29.             break;
    ; L6 g5 q; F: C' Q( q
  30. 4 m- N) [$ ~  H! J
  31.         case 24:
    : F; m8 d+ `9 A
  32.             pfont = (uint8_t *)asc2_2412[chr];  /* 调用2412字体 */% [) B2 E( g! l$ l* g
  33.             break;
    ; N# q7 q8 Z# X6 b2 N
  34. 8 Q) Z4 M8 J4 y
  35.         case 32:
    0 S. }( E/ V8 S+ y* Z. W
  36.             pfont = (uint8_t *)asc2_3216[chr];  /* 调用3216字体 */
    1 V1 A! t2 Y6 a1 w) f# ?3 z2 A
  37.             break;9 U( v8 q7 \- M# T/ u

  38. - N# Z4 K' }- A6 Y" f
  39.         default:  X, O0 D0 \1 y" y: l/ c
  40.             return ;
    6 H& X5 g3 t1 H& F, D8 i2 d8 |
  41.     }* e: h" k* m/ A2 s8 C
  42. : e' z! _# d. I- Z6 u4 W
  43.     for (t = 0; t < csize; t++)
    9 O: Z; t  c/ O* x- c: `
  44.     {
    2 o0 L2 e6 E9 y) [
  45.         temp = pfont[t];    /* 获取字符的点阵数据 */$ u% e/ ?6 F4 d7 x! d% D- `6 z
  46.         for (t1 = 0; t1 < 8; t1++)   /* 一个字节8个点 */
    ; w% ]4 M9 V# d* v- S, |! |
  47.         {+ G$ H, g; V; X. K+ e$ b
  48.             if (temp & 0x80)        /* 有效点,需要显示 */
    1 b. Z8 g4 z( w
  49.             {
    ' x) Y% }! `9 d& Q
  50.                 lcd_draw_point(x, y, color);        /* 画点出来,要显示这个点 */2 x$ Z/ G/ h- k( c! Y+ x5 c* k
  51.             }
    & B/ w) n+ k2 J5 a7 b8 {
  52.             else if (mode == 0)     /* 无效点,不显示 */
    2 Y0 n& ~( f! z5 {
  53.             {7 A  e) U1 f. }/ g
  54. /* 画背景色,相当于这个点不显示(注意背景色由全局变量控制) */' l% a0 V" q8 e
  55.                 lcd_draw_point(x, y, g_back_color);1 y( p1 }1 {( j0 U% c$ o
  56.             }; t# W& p1 w" f+ |
  57.             temp <<= 1; /* 移位, 以便获取下一个位的状态 */7 t7 t1 j: a( i: a
  58.             y++;' `" d: r0 @3 \3 x. ]* O1 v
  59.             if (y >= lcddev.height)return;  /* 超区域了 */
    + Z: F' u& [; f; W( Z. B4 t; O
  60.             if ((y - y0) == size)   /* 显示完一列了? */
    4 }0 h8 S- g5 _. p+ u) e( |
  61.             {
    ; z0 R7 h2 t5 P# Q' t
  62.                 y = y0; /* y坐标复位 */
    # @7 a6 F* R- B  c
  63.                 x++;    /* x坐标递增 */' c* Y4 D( f# ~/ a
  64.                 if (x >= lcddev.width)return;   /* x坐标超区域了 */
    5 ^! u0 y3 j7 r3 l% H: L7 g6 Q; i
  65.                 break;7 M' a0 M) N/ w7 T
  66.             }( n! J; E& V  Q* N: o
  67.         }
    ' r. [- Z7 U; M& S6 p! u( V
  68.     }
    ) F" p5 k6 g* D6 b' p7 I3 K. n
  69. }
复制代码

0 ~$ n$ D5 b. t8 o+ j在lcd_show_char函数里面,我们用到了四个字符集点阵数据数组asc2_1206、asc2_1608、asc2_2412和asc2_3216。4 K6 k( Z! J" r( q* w: q) ?
lcd.c的函数比价多,其他的函数请大家自行查看源码,都有详细的注释。1 h8 |) U& g* R; {/ V& }% t
2. main.c代码
" R) G" j4 ]4 v3 }0 s在main.c里面编写如下代码:8 }5 C6 S4 O  X. C3 W
  U3 W0 ~' q5 M; h! {; v
  1. int main(void)& D  C# t5 y& U' B
  2. {
    4 n: g4 E6 ^, C' Y' U2 W& r
  3.     uint8_t x = 0;
    ( f; L4 k" _& D- Z; u4 k
  4. uint8_t lcd_id[12];6 M, j& h5 |2 |7 @5 k  X
  5. . k' d! C) a+ Y
  6.     sys_cache_enable();                             /* 打开L1-Cache */
      Z$ ~# Z% @6 s
  7.     HAL_Init();                                      /* 初始化HAL库 */$ }1 O' V. l& g1 X) P, _
  8.     sys_stm32_clock_init(240, 2, 2, 4);         /* 设置时钟, 480Mhz */
    $ r! k5 u  p2 P* Q; y, }/ u2 ?9 `
  9.     delay_init(480);                                /* 延时初始化 */
    3 x# t8 _: o$ b
  10.     usart_init(115200);                            /* 串口初始化为115200 */
    ! |( q. `5 _% b2 c; ^
  11.     led_init();                                      /* 初始化LED */; ]( l" \  E6 t6 `: j
  12.     mpu_memory_protection();                      /* 保护相关存储区域 */
    3 V: x1 y) |7 M, }( l" D; h
  13. lcd_init();                                      /* 初始化LCD */
    " O* E. H1 ]  O
  14. 8 }+ H* f+ t1 I9 h2 d8 ^. ?  e! e
  15.     g_point_color = RED;2 X! z0 z6 U. f& W8 ?) P# D! K
  16. sprintf((char *)lcd_id,"LCD ID:%04X",lcddev.id);/*将LCD ID打印到lcd_id数组*/  P$ [1 Q/ [9 \$ z6 t

  17. " |5 [5 P0 L! K- |$ ?
  18.     while (1)
    5 {/ t% h7 E! Y5 w6 a
  19.     {
    * g: r: w, D8 ~4 P# b; h$ g
  20.         switch (x)% P+ k2 F2 T# @) L+ W: S
  21.         {# c& w: E3 u4 k$ Y
  22.             case 0:         lcd_clear(WHITE);         break;* L0 L2 o' J3 R$ K0 `
  23.             case 1:         lcd_clear(BLACK);         break;2 A2 U8 n9 G/ J0 f6 Y& |; U
  24.             case 2:         lcd_clear(BLUE);         break;
    " }% y, F& Y9 O0 V1 i: _
  25.             case 3:         lcd_clear(RED);          break;; ^9 ]! K* \8 O5 `. Z- G8 ]
  26.             case 4:         lcd_clear(MAGENTA);        break;
    4 u7 h0 }( W' M4 I) N
  27.             case 5:         lcd_clear(GREEN);         break;
    : J3 u- c; q4 H1 h: }7 b3 i
  28.             case 6:         lcd_clear(CYAN);         break;! {6 B; L6 N: P+ F* [
  29.             case 7:         lcd_clear(YELLOW);         break;
    1 E( P* i. e, O$ `
  30.             case 8:         lcd_clear(BRRED);         break;
    / n. j* i4 D, @7 a
  31.             case 9:         lcd_clear(GRAY);         break;$ [2 g3 S! X5 `
  32.             case 10:         lcd_clear(LGRAY);         break;
    ! u, Y( T' J9 |% z3 h
  33.             case 11:         lcd_clear(BROWN);         break;* g( A. _  C8 q+ j' `8 ^' r
  34.         }6 I! E" z7 u9 S

  35.   A5 N, u& k1 j* ]: Y3 ^& p2 N
  36.         lcd_show_string(10, 40, 240, 32, 32, "STM32", RED);0 M  p. i3 Q& k( l8 e
  37.         lcd_show_string(10, 80, 240, 24, 24, "TFTLCD TEST", RED);6 a1 v+ {( z2 i! @
  38.         lcd_show_string(10, 110, 240, 16, 16, "ATOM@ALIENTEK", RED);+ X1 f* U: ?9 |2 d
  39.         lcd_show_string(10, 130, 240, 16, 16, (char*)lcd_id, RED);/* 显示LCD ID*/# x" l& D! n& |* _: ~
  40. / C. b/ M) t3 b0 ?1 _7 G
  41.         x++;. E$ w% S. }# v
  42.         if (x == 12)x = 0;9 b# c9 ^2 R9 @# H& D* W" ^) \2 [. p

  43. * l$ U' U9 e5 R; ^) d  E$ s
  44.         LED0_TOGGLE();  /* 红灯闪烁 */
    8 |; m/ U6 f9 F. H% e+ T
  45.         delay_ms(1000);) U3 {2 _4 V" j" d7 O2 D* E5 j
  46.     }* Q; {: ?& y- f$ C+ E+ S
  47. }
复制代码

6 {( R$ Z8 Y4 ?% Z! v0 X( J% W$ rmain函数功能主要是显示一些固定的字符,字体大小包括3216、2412、168和126四种,同时显示LCD驱动IC的型号,然后不停的切换背景颜色,每1s切换一次。而LED0也会不停的闪烁,指示程序已经在运行了。其中我们用到一个sprintf的函数,该函数用法同printf,只是sprintf把打印内容输出到指定的内存区间上,sprintf的详细用法,请百度学习。
4 m7 P, f5 Y8 V* p特别注意:
0 `4 Q! _7 A& Q; U: {1,MPU_Memory_Protection函数必须添加(往后的实验同样),否则会导致MCU屏显示白屏,该函数的说明,见24章。9 W" V  T1 \9 c) I
2,usart_init函数,不能去掉,因为在LCD_Init函数里面调用了printf,所以一旦去掉这个初始化,就会死机!实际上,只要你的代码有用到printf,就必须初始化串口,否则都会死机,即停在usart.c里面的fputc函数,出不来。& m1 o, A7 r- S& U; m
, J1 j; @! i# m+ Q! f2 b
25.4 下载验证
( k9 y; @' b5 N- L- x5 ]* B5 l  E下载代码后,LED0不停的闪烁,提示程序已经在运行了。同时可以看到TFTLCD模块的显示背景色不停切换,如图25.4.1所示:9 g0 d% i) j! @9 d" K/ A. A+ @7 V
) f" l8 q0 D1 D3 k1 r% ^& |
fc7ee4a7022d431aaa788296bd4f6ff5.png
8 V! h5 {( @$ |4 g: Q+ y+ A" l3 B" o! ^8 w4 G+ U, b, c  t
图25.4.1 TFTLCD显示效果图% Q% v, L7 h/ A& d7 M5 A7 @
此外,为了让大家能直观的了解LCD屏的扫描方式,我们额外编写了两个main.c文件(main1.c和main2.c,放到User文件夹中),方便大家编译下载,观察现象。
& R- t9 d" m. s4 U使用方法:关闭工程后,先把原实验中的main.c改成其他名字,然后把main1.c重命名为main.c,双击keilkill.bat清理编译的中间文件,最后打开工程重新编译下载,就可以观察实验现象。观察了main1.c,可以再观察main2.c,main2.c文件的操作方法类似。这两个main.c文件的程序非常简单,这里就不讲解,具体请看源码。
- E) Q! B4 ~6 o$ P- f$ r————————————————7 S5 v1 F- l  }3 l
版权声明:本文为CSDN博主「正点原子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。: g% n  T4 K, g4 q+ y3 {
原文链接:https://blog.csdn.net/weixin_55796564/article/details/126709942& x3 F$ I* k+ w

2 P0 m! O  `; d. {3 I3 o; Y" d4 v( p' s6 n* v& w) b: Q/ ~; q" t. }
赞 收藏 评论0 发布时间:2022-10-7 17:54

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版