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

LCD驱动应该怎么写?–基于stm32F407  

[复制链接]
wujique 发布时间:2018-3-21 22:07
够用的硬件. `: \  P# h. A5 G
能用的代码
- U# o2 S1 T! r* E$ q# _( o使用的教程
(拷贝过来的代码有点乱,请下载附件查看文档)

网络上配套STM32开发板有很多LCD例程,主要是TFT LCD跟OLED的。从这些例程,大家都能学会如何点亮一个LCD。
; a, w9 u  \/ u+ G% x但是不知道有多少人会直接使用这些代码,至少我不用,不是不用,而是用不了。这代码都有下面这些问题:
1 分层不清晰,通俗讲就是模块化太差。
( H! N/ [* X* A/ V7 G2 接口乱。其实只要接口不乱,分层就会好很多了。
) T- y! `8 O5 f, b' I$ G4 N3 可移植性差。
, @+ l; Q' [0 M7 z4 通用性差。
为什么这样说呢?如果你已经了解了LCD的操作,请思考如下情景:
1 代码空间不够,只能保留9341的驱动,其他LCD驱动全部删除。能一键(一个宏定义)删除吗?删除后要改多少地方才能编译通过?$ \9 m! c1 M, B! `6 V, T) L
2 有一个新产品,收银设备。系统有两个LCD,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。怎么办?这些例程代码要怎么改才能支持两个屏幕?复制一套然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。
; p- O! _* |" \/ P3 一个OLED,原来接在这些IO,后来改到别的IO,容易改吗?, U0 G# X, ^0 l- y
4 原来只是支持中文,现在要卖到南美,要支持多米尼加语言,好改吗?
大家慢慢想。

( A- R& O( H2 ^& M
LCD种类概述
在讨论怎么写LCD驱动之前,我们先大概了解一下嵌入式常用LCD。只是概述一些跟驱动架构设计有关的概念。至于原理跟细节,在此不做深入讨论,会有专门文章介绍,或者参考网络文档。

! `& g& n" K* F: W3 n
TFT lcd
TFT LCD,也就是我们常说的彩屏。
. r$ t: j1 E% i通常像素较高,例如常见的2.8寸,320X240像素。4.0寸的,像素800X400。$ a( k8 K7 v( p9 `* j
这些屏通常使用并口,也就是8080或6800接口(STM32 的FSMC接口);
% n$ T# U9 \% o6 A8 d& S或者是RGB接口,STM32F429等部分较贵的芯片支持。
, ?7 C* H' x. n" K* e4 V0 e" m: T其他例如手机上使用的有MIPI接口。* ?( F6 j$ p# I; T/ L& i
也有一些支持SPI接口的,不过除非是比较小的屏幕,否则不建议使用SPI接口,速度慢,刷屏闪屏。( z: w& L( |# M9 B" q, w# a
玩STM32常用的TFT lcd屏幕驱动IC通常有:ILI9341/ILI9325等。( W" x) {0 ?  e; H5 M% ?; H( n
下图是2.8寸 TFT LCD,表面带电阻触摸屏
6 ]! B4 r% l! F' I+ }+ a2 f+ `" F/ v
下图为4.0寸 IPS LCD,表面带电容触摸屏
# t: s. Y# H! C% N0 |
9 g; ~9 I; U5 l8 q, z
COG lcd
很多人可能不知道COG LCD是什么,我觉得跟现在开发板销售方向有关系,大家都出大屏,玩酷炫界面,对于更深的技术,例如软件架构设计,都不涉及。
+ A" b4 S  W* J! e" V0 p) D; a1 d使用单片机的产品,COG LCD其实占比非常大。
) S3 ?; z& h$ }" g所谓的COG LCD,
COG是Chip On Glass的缩写,就是驱动芯片直接绑定在玻璃上,透明的。
实物像下图:
9 }$ T5 \7 ]6 {7 N0 [8 b+ f2 J" g1 {% c$ A' D+ z: K6 d
这种LCD通常像素不高,常用的有128X64,128X32。
. y' ]* {: x0 H- a. T! s一般只支持黑白显示,也有灰度屏,我没怎么用过。
* @1 B* R  [. |6 B  s* L0 p接口通常是SPI,I2C。也有号称支持8位并口的,不过基本不会用,3根IO能解决的问题,没必要用8根吧?
0 k1 e+ Y/ o+ u5 S4 @5 e常用的驱动IC:STR7565。
% V. |6 |- r& A# _0 l0 B- w
OLED lcd
买过开发板的应该基本用过。新技术,大家都感觉高档,在手环等产品常用。OLED目前屏幕较小,大一点的都很贵。
3 x& j: g8 ^& Y) q% C  l. ~1 S: l& @' B在控制上跟COG LCD类似,区别是两者的显示方式不一样。从我们程序角度来看,最大的差别就是,OLED LCD,不用控制背光。。。。。; r  v4 C/ q; N/ G# l& {2 h
实物如下图,
( p/ B& h& [/ r; ^3 y- e& S: q2 T
常见的是SPI跟I2C接口。
& r, G6 _1 f0 |" z% s常见驱动IC:STR7565。
, R8 K, Y! M0 @' c4 D. Z5 n
硬件场景
接下来的讨论,都基于以下硬件信息:+ v, w% @5 N& o( H& a- Y
1 有一个TFT屏幕,接在硬件的FSMC接口,什么型号屏幕?不知道。6 O$ l* T: b% K- Q( b( B& U
2 有一个COG lcd,接在几根IO口上,驱动IC是STR7565,128X32像素。* h8 l) J. S/ B  X# t. t$ ?
3 有一个COG LCD,接在硬件SPI3跟几根IO口上,驱动IC是STR7565,128x64像素。' `- l0 K- y; q+ Z& t. y
4 有一个OLED LCD,接在SPI3上,使用CS2控制片选,驱动IC是SSD1315。! u% _1 y% h# H
  f, g9 q9 D2 w' ]
预备知识
在进入讨论之前,我们先大概说一下下面几个概念,对于这些概念,如果你想深入了解,请GOOGLE。

5 `; L! D, o- G: _
面向对象
面向对象,是编程界的一个概念,常在C++中出现。; i( V  y7 z' o4 H% p4 c3 W
什么叫面向对象呢?
# y2 v3 b! i8 r+ z编程有两种要素:程序(方法),数据(属性)。
9 U, l6 b6 [( r0 X$ ]3 O: _例如:一个LED,我们可以点亮或者熄灭它,这叫方法。3 ]! ?4 z# F" [
LED什么状态?亮还是灭?这就是属性。
" l$ }( B/ y9 t7 B" `0 c: L# `我们通常这样编程:
* p+ g9 k/ s7 y+ b) g& B
u8 ledsta = 0;void ledset(u8 sta)
6 Q. I/ T' D+ l+ E{
& P$ ~  g- ]! y: l0 F( ?
8 }7 W! h9 D, Z  F+ u7 W2 Y
  I3 }; X- E5 {
}
/ A0 I' {0 H) {; d) G' J5 y% A* N3 d8 A
这样的编程有一个问题,假如我们有10个这样的LED,怎么写?' H2 L& B  |, i
这时我们可以引入面向对象编程,将每一个LED封装为一个对象。可以这样做:

8 n: h) z' _( i/ D  a% R* s
/*定义一个结构体,将LED这个对象的属性跟方法封装。这个结构体就是一个对象。
3 Z% |: d$ T% F2 }! b+ t但是这个不是一个真实的存在,而是一个对象的抽象。*/
) H0 r# H9 x  otypedef struct{   
8 _7 N: i- h0 f5 \6 B4 K, z) h     u8 sta;    & k) _" B- X; ?$ t* ^4 d" K
     void (*setsta)(u8 sta);3 Y3 z- s& F" U8 _( R* K$ g
}LedObj;! j8 w4 B2 ?* L2 u/ k
$ `8 V8 A! [; _) e9 x! S
7 l+ f, Q7 K) s7 @! M6 |; ]4 u. @
/*    9 p* W0 c: m9 n: Q) b( Q
声明一个LED对象,名称叫做LED1,   
% F/ o; Z; R# k3 O# X+ ^并且实现它的方法drv_led1_setsta
' I6 [! q, i* d' n8 a3 K- F9 p*/
, z4 R' o* ^/ Qvoid drv_led1_setsta(u8 sta)
4 @, X: [0 N, y3 a; Q8 ~{1 K3 ?/ X* {6 y+ y1 ]
}
1 I! }' h: ~6 _# ^) R  E% t2 T: }' I1 ]: l- |- Z
LedObj LED1={        
" ]$ }& \. [" y+ y2 m    .sta = 0,        
( H9 x; b; w: n5 f$ o    .setsta = drv_led1_setsta,   
2 {) K8 s0 }8 l( _9 d+ F+ E    };
. m$ y4 V- b$ X2 u( F2 J5 s
# A4 ]/ f9 {7 e: `

, U* m- Z8 Z$ ~7 J: ?/ _& }/*   
+ n, V; ~9 ]/ o% M0 m" T声明一个LED对象,名称叫做LED2,   
2 u  Z% k$ w! d& o+ M并且实现它的方法drv_led2_setsta*/, L& L- ?, _# f5 D
void drv_led2_setsta(u8 sta)
3 q. z' ~! [$ E3 x8 V0 q{
0 q# Y# v, _- Y& z2 a4 O}6 ?6 z1 {7 o3 w! e2 Q  O# }
LedObj LED2={       ( e0 k4 d- Z) v6 G+ m
     .sta = 0,        
% H* V5 w1 ?" }2 s/ F    .setsta = drv_led2_setsta,    5 ?$ M" J  z# F! x! f8 c/ o
    };
7 e/ M/ _/ D4 O" L; y! _/*    ; j1 m/ ]9 R: [4 M; R) E  A
操作LED的函数,参数指定哪个led*/- O+ b; y: X, C7 f' J( |% z+ P
void ledset(LedObj *led, u8 sta)3 O1 l# H7 Y. m
{   
  W( j8 V; Y4 c9 v  O1 w+ f6 `/ E     led->setsta(sta);
6 O+ F* Q% j8 g) W( x# i}
抛砖引玉,很多地方不正确,但是不想展开,大家自己搜索资料学习。

- h' i4 \) d2 g" Q9 A% b" M
是的,在C语言中,实现面向对象的手段就是结构体的使用。) i! z$ p5 b' A" I
上面的代码,对于API来说,就很友好了。4 ^% m% x( P0 y/ e
操作所有LED,使用同一个接口,只需告诉接口哪个LED。
: v* K9 x8 ?; ~) ^: m! g) v: J大家想想,前面说的LCD硬件场景。
8 R5 d6 m0 w& N% h4个LCD,如果不面向对象,显示汉字的接口是不是要实现4个?每个屏幕一个?
/ e, D8 C9 N! c# i  A
驱动与设备分离: I4 Q/ T  [4 A7 i* q
如果要深入了解驱动与设备分离,请看LINUX驱动的书籍。
什么是设备?* g4 m/ W2 v  N% l* I1 J; h
我认为的设备就是属性,就是参数,就是驱动程序要用到的数据和接口。% {  S- Y8 u& t" w3 Y9 x# w
那么驱动就是控制这些数据和接口的代码过程。通常来说,如果LCD的驱动IC相同,就用相同的驱动。有些不同的IC也可以用相同的,例如SSD1315跟STR7565,除了初始化,其他都可以用相同的驱动。
# x! B9 `1 c0 }2 a例如一个COG lcd:
驱动IC是STR7565& n8 {% M5 u8 m$ M% `; p. A
128*64像素. f( f* X! s! V) k; K) T" H
用SPI38 I  ?& w+ c7 H; ]: I! o2 V* f+ k0 P5 L
背光用PF5
0 @/ H5 }9 p( W9 J( P4 i命令线用PF4
) b  _3 Y' y& c3 e0 F, b复位脚用PF3
上面所有的信息综合,就是一个设备。9 w' M  i0 {3 q- M1 {1 P
驱动就是STR7565的驱动。
为什么要驱动跟设备分离,因为要解决下面问题:
2 有一个新产品,收银设备,系统有两个LCD,一个叫做主显示,收银员用,一个叫顾显,顾客看金额使用。怎么办?这些例程代码要怎么改才能支持两个屏幕?复制一套然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。
这个问题,两个设备用同一套程序控制才是最好的解决办法。
- I+ c6 a5 F7 R, I3 ^$ B驱动与设备分离的手段:
在驱动程序接口中增加设备参数,驱动用到的所有资源从设备参数传入。
驱动如何跟设备绑定呢?通过设备的驱动IC型号。
0 v1 e8 M6 G5 l- Q) [+ j) w# Q" q( a
模块化
9 _# u/ o9 Q/ z# x# c
我认为模块化就是将一段程序封装,提供接口,给不同的驱动使用。
: V- _, C/ U2 [' W: P不模块化就是,在不同的驱动中都实现这段程序。
+ B; y9 g0 ^0 k, }例如字库处理,在显示汉字的时候,我们要找点阵,在打印机打印汉字的时候,我们也要找点阵,你觉得程序要怎么写?1 A5 I7 N- g1 @: q3 i% S2 N
把点阵处理做成一个模块,就是模块化。5 J0 U% E4 y. l2 _! s- J
非模块化的典型特征就是一根线串到底,没有任何层次感。

4 u& J+ h7 Z4 \: @# L( r! H
LCD到底是什么
/ H7 k9 Z0 u! T  x
前面我们说了面向对象,想要对LCD进行抽象,得出一个对象,就需要知道LCD到底是什么。# G( B9 ^. i' _. r, d$ J1 T
我们问自己下面几个问题:
. D0 _' z( j9 U0 D1 LCD能做什么?
& H5 t5 |) m' G" ]1 W, Z! _7 i% G2 要LCD做什么?
( X9 ?- i" I- n4 K3 谁想要LCD做什么?
刚刚接触嵌入式的朋友可能不是很了解,可能会想不通。我们模拟一下LCD的功能操作数据流。
: M$ ~2 i7 C1 {* B6 nAPP想要像是一个汉字。
1 首先,需要一个像是汉字的接口,APP调用这个接口就可以显示汉字了。假设接口叫做lcd_display_hz。( ]6 m+ n+ |% w' N) {
2 汉字从哪来?从点阵字库来,所以在lcd_display_hz函数内就要调用一个叫做find_font的函数获取点阵。+ D$ }3 d/ M, d: I% n
3 获取点阵后要将点阵显示到LCD上,那么我们调用一个ILL9341_dis的接口,将点阵刷新到驱动IC型号为ILI9341的LCD上。
9 {9 ?' R& M9 V) Z5 C' @4 ILI9341_dis怎么将点阵显示上去?调用一个8080_WRITE的接口。
好的,这个就是大概过程,我们从这个过程去抽象LCD功能接口。1 S& N! m  O+ W* O; R7 H
汉字跟LCD对象有关吗?无关。在LCD眼里,无论汉字还是图片,都是一个个点。
( w+ B- w6 \/ f( [) u3 ?5 k9 a  U3 F, j那么前面问题的答案就是:
1 LCD可以一个点一个点显示内容。
0 S6 z- r# E, Y: P; d2 要LCD显示汉字或图片-----转化后就是显示一堆点  w5 c5 R5 Q$ w! z# `! R! `: K+ V
3 APP想要LCD显示图片或问题。
结论就是:
, {( C( w* {5 c. ?+ D! I所有LCD对象的功能就是显示点。, @" @0 }9 F* Z2 H7 p/ K
那么驱动只要提供显示点的接口就可以了,显示一个点,显示一片点。
6 ]0 z- H& v0 B) s& M' g. z抽象接口如下:

5 ~/ N  x0 F  C! O' h5 [
/*    LCD驱动定义*/% k: @9 Y) ]& c1 P6 x! z5 \
typedef struct  1 ]) l! O) _+ `( W  |
{   
( A) W" ~3 _6 ]6 k9 O' y- v1 hu16 id;   ' e7 P6 X1 V/ ?/ A! T4 \2 B
s32 (*init)(DevLcd *lcd);   
5 U7 S% P1 V0 Is32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);   
& \4 B2 M+ D3 L  M+ es32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);    2 h" c% @6 ]: k) `/ k' W
s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);    , i' b7 n  `8 J+ _: [3 X" O
s32 (*onoff)(DevLcd *lcd, u8 sta);   
  ?1 `& k  r& O& y* Z1 Vs32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);      w1 X# X" u) X
void (*set_dir)(DevLcd *lcd, u8 scan_dir);   
1 H! n0 L$ a% M/ y7 a: z4 h1 a) {void (*backlight)(DevLcd *lcd, u8 sta);
/ \3 F. `5 B: D) |& |0 L; M6 d5 B. m}_lcd_drv;
  b, h" L! `6 e; V9 }9 K& b
9 R( F1 _/ i' z9 E) g$ O
上面的接口,也就是对应的驱动,包含了一个驱动id号。
1 id,驱动型号5 J; ]' |' ^# F$ m4 [/ @2 L
2 初始化
$ A% \- E3 v) ~* _+ Z4 I3 画点
1 s2 |. p$ p+ H4 将一片区域的点显示某种颜色* h' ^2 S3 U8 H+ j# l, O9 O& O
5 将一片区域的点显示某些颜色
3 A; F& G  ~4 n" p6 显示开关3 ]0 m) A* b/ j, x' v( j. z
7 准备刷新区域(主要彩屏直接DMA刷屏使用)
2 G7 E8 @9 h6 a, N8 设置扫描方向9 B, r3 k( |  |1 j" O2 g$ [1 E
9 背光控制
8 C4 y: w/ s* A
LCD驱动框架
) Y+ N1 B( I/ b) R8 r3 A1 ~" _
我们设计了如下的驱动框架% w! B- e, q) n' c7 o( }. J2 j

5 S, r. j3 c8 W8 w" f6 y, Q- I. q+ W. F
设计思想

! t1 V& ^4 q& y7 e6 i9 J- a7 M9 ^' ~, p; u0 D

) z/ {  F" h9 K: i1 中间显示驱动IC驱动程序提供统一接口,接口形式如前面说的_lcd_drv结构体。
  g! N5 h1 q: N) z' A2 各显示IC驱动根据设备参数,调用不同的接口驱动。例如TFT就用8080驱动,其他的都用SPI驱动。6 i( |; i5 V, [) U4 Q& V
SPI驱动只有一份,用IO口控制的我们也做成模拟SPI。, i2 ^1 ^0 G2 s6 e
3 LCD驱动层做LCD管理,例如完成TFT LCD的识别。并且将所有LCD接口封装为一套接口。
  M' S! v) a2 K6 O, U% j' v4 简易GUI层封装了一些显示函数,例如划线、字符显示。# S: j; p& {3 n6 r+ R  t
5 字体点阵模块提供点阵获取与处理接口。
由于嵌入式实际没那么复杂,在例程中我们将GUI跟LCD驱动层放到一起。3 h; u1 C% p- n3 q. S( C7 r
TFT LCD的两个驱动也放到一个文件,但是逻辑是分开的。
2 d4 i1 u- {: h3 V' @. N; f$ pOLED除初始化,其他接口跟COG LCD基本一样,因此这两个驱动也放在一个文件。

( v, K1 O- W6 M( j8 G. M& K
代码分析6 P2 d# T4 O3 n  |2 B
代码分三层:
/ ?2 p. ]  K1 X# z! b" v( \2 k6 z1. GUI和LCD驱动成
) x6 }* F% L7 o: Y9 w1 ]8 Mdev_lcd.c5 B4 Y" F. ^2 y' a
dev_lcd.h
6 G1 l" n3 a( u: n( y" b- ^2. 显示驱动IC层
; x* F  z2 G) n0 ^, @" Adev_str7565.c & dev_str7565.h* X' [3 G( c; f, }1 r
dev_ILI9341.c & dev_ILI9341.h
! Y' v, A8 P( y$ Z9 S3. 接口层
1 J6 }2 {. i5 ?1 ?( ^1 ymcu_spi.c & mcu_spi.h- t+ K* h* ?0 n
stm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h
% v/ |6 i! Y; n
GUI和LCD层- y' H; ]1 h' R* K: r' C% Y
这层主要有3个功能
" x0 X4 T) X1 ^8 ]5 P7 n# L5 `
1 设备管理; {5 q4 I" W2 A5 A
首先定义了一堆LCD参数结构体,结构体包含ID,像素。  p7 H  f+ K0 Q2 Z3 y
并且把这些结构体组合到一个list数组内。
/*    各种LCD的规格参数*/
! m5 e  x- D- e  {1 W_lcd_pra LCD_IIL9341 =
- N% K# m1 v7 i. i2 U{        
# L7 |' P; }  t0 D  R, m9 h.id   = 0x9341,        " P+ e/ v1 V7 ^6 M# w4 p" D- }; t
.width = 240,   //LCD 宽度          A5 i) M6 P6 N' w
.height = 320,  //LCD 高度
9 o0 x# M+ \, ~# \3 k7 V};
2 z3 X' K% W8 C8 H6 N7 L& A4 A.... V$ j8 I, m% S6 a
/*各种LCD列表*/# m  J. M+ |0 D5 `3 n" e! C
_lcd_pra *LcdPraList[5=            
0 V% ?$ Z- h8 Z+ n$ I{                & g# U7 s% k) W, }8 o0 ^
&LCD_IIL9341,                       
- Z/ o+ s( z. C( o- F&LCD_IIL9325,               
9 p9 h+ q+ {: D7 W: F$ U& P. K# \&LCD_R61408,               
4 G2 I! Y/ ^6 }" ?&LCD_Cog12864,               
6 v9 Y7 o2 t1 e&LCD_Oled12864,            
# u6 G, W1 q; i$ r9 z' n) g};
然后定义了所有驱动list数组,数组内容就是驱动,在对应的驱动文件内实现。
/*    所有驱动列表    驱动列表*/
( S6 M- g8 a, X, |_lcd_drv *LcdDrvList[ = {                    4 B- M" {1 N3 f/ R/ @' v  Q
&TftLcdILI9341Drv,                    ' K# f  f  b/ i7 y7 B
&TftLcdILI9325Drv,                  
8 d7 u. s) [" q+ u&CogLcdST7565Drv,                    ; h; F0 b0 T# J4 g& _7 f& y& n
&OledLcdSSD1615rv,
定义了设备树,即是定义了系统有多少个LCD,接在哪个接口,什么驱动IC。6 i: D2 q% i$ e% |% O
如果是一个完整系统,可以做成一个类似LINUX的设备树。
/*设备树定义*/
, b. Z* n2 p1 B  F2 p, t' g8 ]#define DEV_LCD_C 3//系统存在3个LCD设备
2 C5 I5 s0 |1 c- H! w: w* `LcdObj LcdObjList[DEV_LCD_C={    9 n, U* Q' J! a' V; j. n. C$ @
{"oledlcd", LCD_BUS_VSPI, 0X1315},    2 v! N% X! m3 j* Z7 a+ G, _4 g: E
{"coglcd", LCD_BUS_SPI,  0X7565},   
  |* {$ {1 w3 I: U1 G1 c7 o{"tftlcd", LCD_BUS_8080, NULL},; f* R+ f' {5 O* e; N* C8 }
};3 b+ O4 d# ?* c
2 接口封装
void dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir)
6 ^6 B: d, y- q- J2 m5 x3 Ss32 dev_lcd_init(void)& d/ g2 C) b) M1 \  f4 |: G
DevLcd *dev_lcd_open(char *name)4 X+ I9 r0 M. ^: K* x
s32 dev_lcd_close(DevLcd *dev)
) a0 K3 _+ D( Z3 N- Ms32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)- Y/ f) Y5 K: R% F
s32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)
9 @& k) S( y% Z1 j6 L* hs32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta)  `0 f  S) R- p% e
s32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color)) x- o' _8 \5 E9 s" V% Q& N# ~
s32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)4 {. D# V' z1 T. Q  c3 F& s9 g
s32 dev_lcd_backlight(DevLcd *lcd, u8 sta)
大部分接口都是对驱动IC接口的二次封装。有区别的是初始化和打开接口。( N/ T2 O9 K$ u* T$ [
初始化,就是根据前面定义的设备树,寻找对应驱动,找到对应设备参数,并完成设备初始化。* s: _- _* q  }
打开函数,根据传入的设备名称,查找设备,找到后返回设备句柄,后续的操作全部需要这个设备句柄。
3 简易GUI层
0 v: d* w% g# f: i3 O5 i目前最重要就是显示字符函数。
s32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)
其他划线画圆的函数目前只是测试,后续会完善。
驱动IC层
驱动IC层分两部分
6 B: G' z; N+ a) M1 封装LCD接口' `7 c( M- }+ F
LCD有使用8080总线的,有使用SPI总线的,有使用VSPI总线的。这些总线的函数由单独文件实现。
1 x. Z& T  r# R, ?5 N  T但是,除了这些通信信号外,LCD还会有复位信号,命令数据线信号,背光信号等。1 n6 X& c/ b3 y
我们通过函数封装,将这些信号跟通信接口一起封装为LCD通信总线。6 j7 \- R3 o4 _
BUS_8080在dev_ILI9341.c文件中封装。
( L  M+ Q! x5 ]BUS_LCD1和BUS_lcd2在dev_str7565.c 中封装。
) e3 q1 h9 _7 T6 B  k1 M5 y$ n2 驱动实现- P, O6 O3 I. X, z7 E" U
实现_lcd_drv驱动结构体。每个驱动都实现一个,某些驱动可以共用函数。
_lcd_drv CogLcdST7565Drv = { + R* }3 y* V; @  S# v
                           .id = 0X7565,
; O$ D) w% z' z: t5 D( b                           .init = drv_ST7565_init,
) Q( ?. z+ h2 e8 y. {, I                           .draw_point = drv_ST7565_drawpoint,
; n1 l4 Q9 x1 z, L, u+ L$ U% }                            .color_fill = drv_ST7565_color_fill,
/ w  }7 r  w" v3 [                           .fill = drv_ST7565_fill,
0 `9 I  V# x5 n" W6 U( {( F                           .onoff = drv_ST7565_display_onoff,
: C/ D2 I+ o% ~6 w" A  z( _                            .prepare_display = drv_ST7565_prepare_display,
6 _" o( Z1 Y7 \  u' T) d, i                           .set_dir = drv_ST7565_scan_dir,* J. ^- @* R2 o5 @
                            .backlight = drv_ST7565_lcd_bl                            6 F) w- N( m$ V( N) L7 O* d+ A
};# F+ G: g; }/ u) E8 w  f% s, u

4 ^8 g3 e3 B* w3 F
8 K6 J1 k" v9 j( u8 j
接口层
$ k' d! ]) \  O+ w* C* D" r# Y
; J) j. @) L1 X7 }
8080层比较简单,用的是官方接口。
1 g2 X2 u4 _, [( `6 aSPI接口提供下面操作函数,可以操作SPI,也可以操作VSPI。
extern s32 mcu_spi_init(void);1 h/ M$ a$ _7 T, ]
extern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);- q5 q% k: Z. p* a
extern s32 mcu_spi_close(SPI_DEV dev);
! e* L4 p! m7 b5 Fextern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);
$ T$ `; F& A9 X, Y, _extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);
至于SPI为什么这样写,会有一个单独文件说明。
总体流程
前面说的几个模块时如何联系在一起的呢?
& b% o9 {; s& k2 s" O/ t请看下面结构体:
/*    初始化的时候会根据设备数定义,   
  ?. o7 L& u1 c5 X并且匹配驱动跟参数,并初始化变量。    5 D# s8 `( i! Z
打开的时候只是获取了一个指针*/! I4 j2 ?4 e8 P9 C. W. Y
struct _strDevLcd{
4 I" x; B8 ]& D) ]" l# o    s32 gd;//句柄,控制是否可以打开   
6 O% r0 X& |' {; h    LcdObj   *dev;    + o4 j' [( {& a) e' V" B
/* LCD参数,固定,不可变*/& ]; Y# a2 O9 U3 `. A  x
    _lcd_pra *pra;    9 K4 c* ~- Q1 O8 _/ k3 Y& p
/* LCD驱动 */ % ?4 ^# L  V! [/ Z
   _lcd_drv *drv;   
6 u8 [* r( f- K& P1 o3 L/*驱动需要的变量*/
, u# u- M. T) c    u8  dir;    //横屏还是竖屏控制:0,竖屏;1,横屏
; T; }& _( @" O6 ~3 }    u8  scandir;//扫描方向( R4 d) h$ {, a% O# D
    u16 width;  //LCD 宽度
; J0 i$ N  |0 `, d' Y4 l   u16 height; //LCD 高度0 [, d  n; q" I' d0 o1 E
    void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存1 K# g# u' _8 ?
};6 p# K4 ?1 C' d* H! ?/ q
每一个设备都会有一个这样的机构体,这个结构体在初始化LCD时初始化。
& L' Q$ j- `; y* D( P- 成员dev指向设备树,从这个成员可以知道设备名称,挂在哪个LCD总线,设备ID。
typedef struct{/ L8 {* y7 \0 a! E) L  E6 N
    char *name;//设备名字
/ {; b) `4 h5 s% ~1 b    LcdBusType bus;//挂在那条LCD总线上6 T8 E2 |  H2 g! Z
    u16 id;% \, z: g) Y' u( h& q
}LcdObj;
-成员pra指向LCD参数,可以知道LCD的规格。
typedef struct{
+ s/ E/ ?6 R" i% a   u16 id;   
8 U9 I" |! b! t, K* Y  w. [9 M4 `% A   u16 width;  //LCD 宽度  竖屏   
! r. M& Q- |& Y. p$ |& [, N   u16 height; //LCD 高度    竖屏
, l3 L6 I+ g+ n# q}_lcd_pra;
-成员drv指向驱动,所有操作通过drv实现。
typedef struct  {
/ A9 t/ i9 N  o  [    u16 id;. l1 t6 j* @* d- \$ F1 B
    s32 (*init)(DevLcd *lcd);
" B5 Q6 U3 i( ]* X) {2 ~3 Z0 [: k% {    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
( Y4 R( Y) h4 R' ^7 j/ ?' F: l    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);
$ R9 A" r. Y  Y9 N4 S    s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color); - ]- J# i* f  c( O+ p9 p: N
   s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);
) s* c) |8 _9 L    s32 (*onoff)(DevLcd *lcd, u8 sta);
  C0 [; {; [' Y2 }0 h) z1 G, g   void (*set_dir)(DevLcd *lcd, u8 scan_dir);
; k) y  E- T9 Q6 r   void (*backlight)(DevLcd *lcd, u8 sta);% B0 R' y9 J% x; r9 j! _3 Z1 C) \
}_lcd_drv;
  • 成员dir、scandir、 width、 height是驱动要使用的通用变量。因为每个LCD都有一个结构体,一套驱动程序就能控制多个设备而互不干扰。
  • 成员pri是一个私有指针,某些驱动可能需要有些比较特殊的变量,就全部用这个指针记录,通常这个指针指向一个结构体,结构体由驱动定义,并且在设备初始化时申请变量空间。
    7 y6 x1 g1 x# S( P% R! H0 _  J7 x* }目前主要用于COG LCD跟OLED LCD显示缓存。* `# L9 N* E% m3 R3 c
整个LCD驱动,就通过这个结构体组合在一起。
1 初始化,根据设备树,找到驱动跟参数,然后初始化上面说的结构体。
, P# k8 L4 \" X+ k2 要使用LCD前,调用dev_lcd_open函数。打开成功就返回一个上面的结构体指针。
$ o8 D/ r# ?' j3 显示字符,接口找到点阵后,通过上面结构体的drv,调用对应的驱动程序。
. f% d* K( q# U9 }0 B8 n1 I4 驱动程序根据这个结构体,决定操作哪个LCD总线,并且使用这个结构体的变量。
用法和好处
  • 好处1, f0 E+ f7 W; Q
请看测试程序
void dev_lcd_test(void)/ F2 _2 G& G* @& O3 s( i  w
{
/ E9 L7 A: [3 b: H2 v8 E    DevLcd *LcdCog; 3 u, f. {" E) U4 b. r) U
   DevLcd *LcdOled;" |; N1 P# _& o
    DevLcd *LcdTft;7 K8 t& Q7 X" T0 K6 B% y- C8 A
    /*  打开三个设备 */
* R1 l$ g. \" a0 Q% q' S   LcdCog = dev_lcd_open("coglcd"); 8 h, O' l' H2 R# D, ^$ I* p, F2 z
   if(LcdCog==NULL) ) }8 o% X* D0 N+ |$ V( y. _
       uart_printf("open cog lcd err\r\n");+ k9 \6 T& w5 u) k: y9 M
    LcdOled = dev_lcd_open("oledlcd");
4 F+ l- ^6 S  j+ H4 ~1 L    if(LcdOled==NULL) 3 X9 p6 V  b/ l4 C& n
       uart_printf("open oled lcd err\r\n");
; I! |3 `6 V  m2 c    LcdTft = dev_lcd_open("tftlcd");  Q( s. n9 \+ L% T' h$ B& w$ V* K2 R
    if(LcdTft==NULL)
9 p8 T2 b& _8 N7 K% v8 S        uart_printf("open tft lcd err\r\n");
6 q$ l) ?& [7 }; k    /*打开背光*/ ' h. N, y, q# M2 i' {: ~/ ?2 e, p0 U
   dev_lcd_backlight(LcdCog, 1);
! c/ s- T( W& y+ T4 }   dev_lcd_backlight(LcdOled, 1);( ]: {- e1 W. @- W- r1 c
    dev_lcd_backlight(LcdTft, 1);: N; Z5 ?! r0 ?! Q6 q
# P+ C5 N- ?) L7 U- ~+ V8 v; {
    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK); ) b2 W2 ^  e9 J4 ]+ \5 @# N4 y
   dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "这是oled lcd", BLACK); 7 R- F: g2 K5 V4 }% i5 b) e* A
   dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK); 3 W4 I) @  n# R0 |$ T6 N6 P4 C( ~
   dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK); / u1 q# I9 A- x
   dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK); 1 _9 `' J  H7 V
   dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是cog lcd", BLACK);( R# }/ N, z, O5 l7 v5 Q& |
    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK); 7 a; }2 R( }  ?* V
   dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK); ! O+ `7 @3 T! U2 i' H
   dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED); 3 G0 }) O* q* v2 Y3 s) q# P
   dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是tft lcd", RED); 6 ^8 f) S/ A9 G1 [; z6 U5 c
   dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);
$ Z' f/ i5 `8 b    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "屋脊雀工作室", RED);    while(1);}
使用一个函数dev_lcd_open,可以打开3个LCD,获取LCD设备。9 j2 s6 W( W. C' u# c
然后调用dev_lcd_put_string就可以在不同的LCD上显示。& u7 h5 Z- i0 v1 x! V( N  C6 e
其他所有的gui操作接口都只有一个。+ N" v+ a1 `+ @0 Q
这样的设计对于APP层来说,就很友好。
) y% }6 r# M+ m4 k
* a0 [( m3 i3 H5 b- 好处2
现在的设备树是这样定义的
LcdObj LcdObjList[DEV_LCD_C={
; W* W9 J. x+ `. T1 P' L   {"oledlcd", LCD_BUS_VSPI, 0X1315},
# n. g0 w' a3 Z9 B$ p. ?; k" l3 w/ i9 i   {"coglcd", LCD_BUS_SPI,  0X7565},0 V8 ?4 n3 g2 B( L# V; K+ X
    {"tftlcd", LCD_BUS_8080, NULL},) N4 Y" E% {, y5 a+ j8 X
};
某天,oled lcd要接到SPI上,只需要将设备树数组里面的参数改一下,就可以了,当然,在一个接口上不能接两个设备。
LcdObj LcdObjList[DEV_LCD_C={8 K2 b5 t" S& @* [
    {"oledlcd", LCD_BUS_SPI, 0X1315},
; v7 p2 O2 J3 M: s5 x. t4 E- d* J   {"tftlcd", LCD_BUS_8080, NULL},
$ Y4 w. G6 }8 Z/ S' o0 u( X};& T6 p3 |% f5 H% c0 t, w
) B  ]; ^3 M, t/ H  ~
5 M: }: D% w- D- T& `+ N, ^* i/ F  Z
字库
3 U  o; |  B9 X; B) @6 p/ w
暂时不做细说,例程的字库放在SD卡中,各位移植的时候根据需要修改。$ W: C- X0 H  c8 S/ v
具体参考font.c
声明
1 代码请按照版权协议使用。
/ T1 _1 N: S6 H/ K. q& h2 当前源码只是一个能用的设计,完整性与健壮性尚未测试。
! L( i0 Y! g0 l5 M" e3 后续会放到github,并且持续更新优化。0 z4 O* ~$ B" B6 A3 D
4 最新消息请关注www.wujique.com.
' W) F% N4 p& }' O# r

$ O: o* z+ c; Z2 n$ A0 {2 E9 L; I# N% e
6 z9 R. \9 n( E1 r0 N, P4 v, }& m9 B! X9 ^
4 f- _* C* j6 p& t$ r7 ?
( B( B5 |% j" s, n
' x+ X6 m6 `% }2 B4 V" q

# f7 n. m9 U" r7 b+ }( ]% O
! y# f) T7 o% d5 [1 N9 y9 k' M* m/ V- S

wujique.rar

下载

2.1 MB, 下载次数: 73

源码

LCD驱动应该怎么写?.pdf

下载

417.37 KB, 下载次数: 40

文档

oled lcd 0.96.pdf

下载

62.39 KB, 下载次数: 24

原理图

cog lcd 128x64.pdf

下载

39.03 KB, 下载次数: 23

原理图

字库(仅供测试).rar

下载

1.33 MB, 下载次数: 46

点阵字库

评分

参与人数 1 ST金币 +1 收起 理由
dirdirdel + 1 很给力!

查看全部评分

2 收藏 5 评论22 发布时间:2018-3-21 22:07

举报

22个回答
wujique 回答时间:2018-6-14 10:04:41
z258121131 发表于 2018-6-13 17:51; O4 |, }/ a1 Y1 b9 S: U" b8 M- M
楼主威武,不知道有没有对应的19264的
( ]1 C4 _0 s7 M% F8 Q' J, L8 S
LCD框架已经更新,你可以到我的github上下载最新的。7 d9 f9 V/ c7 o* x! U, u
在最新的里面,增加一个新的LCD非常容易。
$ o! z/ i) g# w( E+ s8 [% M* X# u19264,过段时间我会添加进去的。
wujique 回答时间:2018-6-14 10:05:25
z258121131 发表于 2018-6-13 17:51
4 l) |' d$ k. R楼主威武,不知道有没有对应的19264的

7 H  D. T% l/ I- |' {+ ]19264应该有多种,不知道你说的是哪种驱动芯片的?
z258121131 回答时间:2018-6-15 15:46:00
huangxuejia-292 发表于 2018-6-14 10:055 S* q0 G2 T: E$ Y
19264应该有多种,不知道你说的是哪种驱动芯片的?
6 x. A4 I' l7 w
ks0108/7的
wujique 回答时间:2018-3-25 11:19:12
更新资料,并且增加了对OLED(SSD1315) I2C接口的支持
~泉 回答时间:2018-6-13 14:47:07
感谢楼主
z258121131 回答时间:2018-6-13 17:51:02
楼主威武,不知道有没有对应的19264的
pkoko 回答时间:2018-6-17 10:26:08
楼主V5,学习了,非常感谢
dirdirdel 回答时间:2018-6-21 11:28:14
云盘无法打开
wujique 回答时间:2018-6-21 23:40:47
dirdirdel 发表于 2018-6-21 11:28/ v) c% f8 T; S8 V* S+ y
云盘无法打开

: F$ M9 J, P9 J, fhttps://pan.baidu.com/s/1bHUVe6X6tymktUHk_z91cA
oliverlight 回答时间:2019-1-14 15:15:10
不是keil,有什么用下错了
oliverlight 回答时间:2019-1-14 15:15:29
大家注意哈
sylar.z 回答时间:2019-1-16 09:26:01
说明很详细
bl2019 回答时间:2019-10-28 14:24:05
* ?! V' F7 L7 a0 I" K  n2 Y
学习
jxchen 回答时间:2019-11-9 11:15:28
8 b& A5 R7 D( X5 Q, u$ m8 M5 A
云盘无法打开
12下一页

所属标签

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版