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

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

[复制链接]
wujique 发布时间:2018-3-21 22:07
够用的硬件! G, A% |. u$ R( l1 g  w, o9 M
能用的代码
% [8 b) Z. G' u; q* E% V使用的教程
(拷贝过来的代码有点乱,请下载附件查看文档)

网络上配套STM32开发板有很多LCD例程,主要是TFT LCD跟OLED的。从这些例程,大家都能学会如何点亮一个LCD。1 ~6 }& g/ m, L/ z
但是不知道有多少人会直接使用这些代码,至少我不用,不是不用,而是用不了。这代码都有下面这些问题:
1 分层不清晰,通俗讲就是模块化太差。: G! C6 @% p; P8 @: [
2 接口乱。其实只要接口不乱,分层就会好很多了。  k: H: M- A6 t: K! o3 }6 a& _
3 可移植性差。+ E" X5 v, a4 c  w. k
4 通用性差。
为什么这样说呢?如果你已经了解了LCD的操作,请思考如下情景:
1 代码空间不够,只能保留9341的驱动,其他LCD驱动全部删除。能一键(一个宏定义)删除吗?删除后要改多少地方才能编译通过?
, o/ W1 j  W  ]! _2 有一个新产品,收银设备。系统有两个LCD,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。怎么办?这些例程代码要怎么改才能支持两个屏幕?复制一套然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。
6 p8 Y" v+ U: C. T5 e3 一个OLED,原来接在这些IO,后来改到别的IO,容易改吗?( }7 J$ L* v) O5 K
4 原来只是支持中文,现在要卖到南美,要支持多米尼加语言,好改吗?
大家慢慢想。
) P" I' |' K4 z- _- p
LCD种类概述
在讨论怎么写LCD驱动之前,我们先大概了解一下嵌入式常用LCD。只是概述一些跟驱动架构设计有关的概念。至于原理跟细节,在此不做深入讨论,会有专门文章介绍,或者参考网络文档。

* g, d+ m+ }! v$ p2 F
TFT lcd
TFT LCD,也就是我们常说的彩屏。+ r3 V. c/ \0 ~; i& t$ U# N" g& F
通常像素较高,例如常见的2.8寸,320X240像素。4.0寸的,像素800X400。1 Y1 i& y. ?# M4 j- ?
这些屏通常使用并口,也就是8080或6800接口(STM32 的FSMC接口);
, F, q' u" b; s  @: f或者是RGB接口,STM32F429等部分较贵的芯片支持。/ X( s! o& R7 G- ]. S
其他例如手机上使用的有MIPI接口。
! M7 P" w( g$ k$ x/ q+ q! T. s也有一些支持SPI接口的,不过除非是比较小的屏幕,否则不建议使用SPI接口,速度慢,刷屏闪屏。
7 u  C* S' L1 Q8 o4 c+ Z2 W0 ]玩STM32常用的TFT lcd屏幕驱动IC通常有:ILI9341/ILI9325等。2 i1 n7 c+ n: c
下图是2.8寸 TFT LCD,表面带电阻触摸屏
$ ?3 @4 e. m* E3 v
" ?$ a1 Z$ S7 U0 ?下图为4.0寸 IPS LCD,表面带电容触摸屏% s2 N  f# X. v  o% Q

* z( @" z; e' }3 M* O( p
COG lcd
很多人可能不知道COG LCD是什么,我觉得跟现在开发板销售方向有关系,大家都出大屏,玩酷炫界面,对于更深的技术,例如软件架构设计,都不涉及。
. R( `8 `' e5 U  W5 o' V1 x7 Q使用单片机的产品,COG LCD其实占比非常大。: |+ S2 H8 [1 s# |
所谓的COG LCD,
COG是Chip On Glass的缩写,就是驱动芯片直接绑定在玻璃上,透明的。
实物像下图:7 \* P# ~9 U- e
0 \1 E: t! u9 d5 x3 u4 `
这种LCD通常像素不高,常用的有128X64,128X32。
2 J7 l3 R. z$ `* F一般只支持黑白显示,也有灰度屏,我没怎么用过。, w0 N1 M$ X# m5 L4 ^( F5 o
接口通常是SPI,I2C。也有号称支持8位并口的,不过基本不会用,3根IO能解决的问题,没必要用8根吧?& q' ~. m  C* v  m
常用的驱动IC:STR7565。
$ ?+ k. p- n; y
OLED lcd
买过开发板的应该基本用过。新技术,大家都感觉高档,在手环等产品常用。OLED目前屏幕较小,大一点的都很贵。  F% z* G; v1 G$ d( J+ \5 C7 x
在控制上跟COG LCD类似,区别是两者的显示方式不一样。从我们程序角度来看,最大的差别就是,OLED LCD,不用控制背光。。。。。" j0 C: z% a" x
实物如下图,
4 v* V* v% \3 ~( u& F8 Y
" Z8 E5 `% Y" \1 E5 I常见的是SPI跟I2C接口。
# L- _# _- g' Z+ d# Q) e: d常见驱动IC:STR7565。
) G. O% L( ]' I0 e8 c
硬件场景
接下来的讨论,都基于以下硬件信息:* U- Z7 d5 \% ]  t( {/ t
1 有一个TFT屏幕,接在硬件的FSMC接口,什么型号屏幕?不知道。! [- c1 M( y0 b8 G' |0 d
2 有一个COG lcd,接在几根IO口上,驱动IC是STR7565,128X32像素。* T& m& W  {: y7 I1 N  l
3 有一个COG LCD,接在硬件SPI3跟几根IO口上,驱动IC是STR7565,128x64像素。" `0 ^. f' [5 `; }2 w) K
4 有一个OLED LCD,接在SPI3上,使用CS2控制片选,驱动IC是SSD1315。5 s. O, x  Z& _! Y! n! P( p$ I% G
1 T: z' ^2 Y. @' O/ c# @! P  s
预备知识
在进入讨论之前,我们先大概说一下下面几个概念,对于这些概念,如果你想深入了解,请GOOGLE。

# v  y" }- M( g; I# g
面向对象
面向对象,是编程界的一个概念,常在C++中出现。% I  N2 J+ a4 S) ]
什么叫面向对象呢?" b7 d4 Q  @8 t/ L( U4 n5 W7 d' `
编程有两种要素:程序(方法),数据(属性)。" T9 e) i8 c6 v% f
例如:一个LED,我们可以点亮或者熄灭它,这叫方法。0 w3 Z5 G) v$ O
LED什么状态?亮还是灭?这就是属性。# B* v4 w% \* k3 s* U+ p
我们通常这样编程:
" H8 N. `9 \( q9 [! M: ?* N; \5 O9 v# N
u8 ledsta = 0;void ledset(u8 sta)
$ k1 k% W7 b$ X3 f' X{
5 l+ p& V2 H1 `1 i% O
. M& i) ^* l# `9 K/ V
2 A; i1 {) R8 _
}
8 F, c" `8 J% o( U) x- B% |
; \( d/ O$ i( V. B2 T! g! f
这样的编程有一个问题,假如我们有10个这样的LED,怎么写?5 J3 j$ g% M0 V/ H' ?- E) B+ u+ `
这时我们可以引入面向对象编程,将每一个LED封装为一个对象。可以这样做:
9 f; l7 z+ p, C
/*定义一个结构体,将LED这个对象的属性跟方法封装。这个结构体就是一个对象。, @- T2 }) W& A- P  c; \
但是这个不是一个真实的存在,而是一个对象的抽象。*/- l# X2 l. m/ y9 T0 a
typedef struct{   + P6 R* d' a8 Y2 T( \
     u8 sta;    1 Q% u. x* `/ M. Q+ S: o- l2 \2 ~
     void (*setsta)(u8 sta);$ t" z9 M9 M8 b% [: }" k
}LedObj;
+ u8 @' _- x% y5 I7 K# R" Y
+ g, x" B9 Z: A9 u$ u. A
4 X" j& j  E# l0 E& r
/*   
1 n* T+ v% l* ?  F声明一个LED对象,名称叫做LED1,    8 u+ t" u- t' C
并且实现它的方法drv_led1_setsta
8 A/ J/ [5 i1 ?, I3 _2 p3 e2 \*/% k- h% v; Z8 M+ F8 L2 s* B
void drv_led1_setsta(u8 sta). C9 q! ]' |( V6 j
{
, e: |) W8 t0 {' K5 k, s7 _8 |}" W8 b" j! Q. S

8 u7 L0 W) }: t5 l5 K+ `LedObj LED1={        
7 b8 N& M0 Z) Q& J: _' E, ?    .sta = 0,        ) P+ O& x/ j1 R/ G
    .setsta = drv_led1_setsta,   
3 X( z% W/ m7 s7 {    };
% j4 x% G' z2 ?
7 |  d/ u0 p7 }# [+ b: d+ t& \% o* p5 ]

' R# m  s: h! e4 K) o* C/*    # a3 \' |3 ^: r3 T& l8 [, F4 _- p1 Q
声明一个LED对象,名称叫做LED2,   
3 o! \( d; }( }- g4 d) h; e: @并且实现它的方法drv_led2_setsta*/
$ B: Q5 t' j- C5 {# `/ ^( zvoid drv_led2_setsta(u8 sta)
/ C( v# U, }7 c& E! _. F% o{
* f4 J3 D) p7 Y/ b. d5 s- k}
) F& f& @$ @" mLedObj LED2={      
$ w& ]1 o9 E) P7 v     .sta = 0,        8 b7 \) A0 X6 H) f
    .setsta = drv_led2_setsta,   
6 i; d# y* u& Y  M3 x    };2 n3 L" s! E4 U* h: n/ h
/*   
! ?$ C) t$ }' q5 Y/ h操作LED的函数,参数指定哪个led*/
; C4 X9 [6 m( kvoid ledset(LedObj *led, u8 sta)4 @/ W- n  C. w" B; I8 S% V5 Q
{   
+ W. V5 T" k, U; L     led->setsta(sta);& d" ?$ R! s4 U7 ]2 S( K
}
抛砖引玉,很多地方不正确,但是不想展开,大家自己搜索资料学习。
* ]  c" I1 C' u# l. U+ Y0 g
是的,在C语言中,实现面向对象的手段就是结构体的使用。' k; `+ f7 H) f1 j* V
上面的代码,对于API来说,就很友好了。
' b) p( N. _3 W# W3 t1 a/ e操作所有LED,使用同一个接口,只需告诉接口哪个LED。% |$ {( G0 V1 e4 H1 Y
大家想想,前面说的LCD硬件场景。) [9 a  I1 ?- R- E7 e( R$ i, V3 x7 Z
4个LCD,如果不面向对象,显示汉字的接口是不是要实现4个?每个屏幕一个?

$ B$ ]' u6 S' T/ ~. V. i
驱动与设备分离3 k0 ^- \1 |; {8 c- {
如果要深入了解驱动与设备分离,请看LINUX驱动的书籍。
什么是设备?
  N+ A- S, C3 q- q我认为的设备就是属性,就是参数,就是驱动程序要用到的数据和接口。
; f8 I$ w) K+ K5 P- N, {那么驱动就是控制这些数据和接口的代码过程。通常来说,如果LCD的驱动IC相同,就用相同的驱动。有些不同的IC也可以用相同的,例如SSD1315跟STR7565,除了初始化,其他都可以用相同的驱动。
6 ^/ y; @) @! z1 N* d- b( a例如一个COG lcd:
驱动IC是STR7565
- v$ M! ]9 [) l% E3 a128*64像素
3 S6 C0 \5 J! {7 k用SPI3
, _# O. L6 R7 a4 Y( V5 C+ b7 e背光用PF5
) @7 c) F3 a: ?4 [1 f- u命令线用PF4
2 A- R: v7 l* c1 V复位脚用PF3
上面所有的信息综合,就是一个设备。1 b( A7 G% u4 T- r- v
驱动就是STR7565的驱动。
为什么要驱动跟设备分离,因为要解决下面问题:
2 有一个新产品,收银设备,系统有两个LCD,一个叫做主显示,收银员用,一个叫顾显,顾客看金额使用。怎么办?这些例程代码要怎么改才能支持两个屏幕?复制一套然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。
这个问题,两个设备用同一套程序控制才是最好的解决办法。
! e; I. ~" S2 g/ k: i驱动与设备分离的手段:
在驱动程序接口中增加设备参数,驱动用到的所有资源从设备参数传入。
驱动如何跟设备绑定呢?通过设备的驱动IC型号。
0 ?* C+ h% f- \0 g  F
模块化" y' @, v$ {4 c" q0 S% a
我认为模块化就是将一段程序封装,提供接口,给不同的驱动使用。
4 b7 ^( r/ H; G  T& s6 X% j不模块化就是,在不同的驱动中都实现这段程序。
$ j1 ^9 V0 E/ A* o- C例如字库处理,在显示汉字的时候,我们要找点阵,在打印机打印汉字的时候,我们也要找点阵,你觉得程序要怎么写?
- l# [) @" h2 V" L0 w$ w把点阵处理做成一个模块,就是模块化。
5 ^+ L$ q* K0 E- V3 [+ s非模块化的典型特征就是一根线串到底,没有任何层次感。
, V8 ]. y' }, _' \7 Z6 H( n7 E: _3 ^% W
LCD到底是什么  X" H9 k7 W% T% `
前面我们说了面向对象,想要对LCD进行抽象,得出一个对象,就需要知道LCD到底是什么。
  t8 S9 m# ^2 `我们问自己下面几个问题:
& s; M# I5 }0 P1 LCD能做什么?9 u" ?- i$ H6 S4 h& A9 H
2 要LCD做什么?
9 d) k5 D& y; N: q1 H9 k! H3 谁想要LCD做什么?
刚刚接触嵌入式的朋友可能不是很了解,可能会想不通。我们模拟一下LCD的功能操作数据流。% h' s" G4 L3 I7 }, I
APP想要像是一个汉字。
1 首先,需要一个像是汉字的接口,APP调用这个接口就可以显示汉字了。假设接口叫做lcd_display_hz。
+ X+ H9 U4 b% U: b) b2 C2 汉字从哪来?从点阵字库来,所以在lcd_display_hz函数内就要调用一个叫做find_font的函数获取点阵。: P! t$ H+ T, @
3 获取点阵后要将点阵显示到LCD上,那么我们调用一个ILL9341_dis的接口,将点阵刷新到驱动IC型号为ILI9341的LCD上。  @/ b1 U: \4 z. X% G3 O7 V
4 ILI9341_dis怎么将点阵显示上去?调用一个8080_WRITE的接口。
好的,这个就是大概过程,我们从这个过程去抽象LCD功能接口。
9 Q& {0 n7 I) X6 O% c( H汉字跟LCD对象有关吗?无关。在LCD眼里,无论汉字还是图片,都是一个个点。: f0 Z; w+ t0 s" `/ Y1 o9 N
那么前面问题的答案就是:
1 LCD可以一个点一个点显示内容。! y7 q6 t  E4 Q/ P/ h, B
2 要LCD显示汉字或图片-----转化后就是显示一堆点, L% I8 U! |+ z8 R! S9 U
3 APP想要LCD显示图片或问题。
结论就是:: n' }+ j" s/ _/ d. E- l. R
所有LCD对象的功能就是显示点。
6 B8 X! n; Q# w0 [& L那么驱动只要提供显示点的接口就可以了,显示一个点,显示一片点。4 Z9 e9 ?+ f" t# f; i7 T) `
抽象接口如下:

+ o, x5 j2 c8 ]6 ~2 I0 q
/*    LCD驱动定义*/
; d0 h. X8 J$ z0 H. z/ R; i% y6 ktypedef struct  , I& G- K. F3 q: T& }3 x& [8 k9 @
{   
; x: f3 A0 m+ @3 j- R; [3 Ou16 id;   + T5 \+ j1 E5 n# a5 R# w
s32 (*init)(DevLcd *lcd);   
, l- W4 X6 C1 ^- D, F5 Ks32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);    : x8 V* W! C- T1 e/ B& q1 z
s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);   
+ @/ ?9 A) k. P1 ns32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);    , Y. b. n( c, F" K1 Z1 P2 x
s32 (*onoff)(DevLcd *lcd, u8 sta);   
7 [1 h1 Q/ e2 B/ Rs32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);    % n; V+ n8 `" |( L' _0 u+ v4 B
void (*set_dir)(DevLcd *lcd, u8 scan_dir);   
7 f" i! M& V# ?( Z% W; p  F( nvoid (*backlight)(DevLcd *lcd, u8 sta);
. H7 o" r0 i2 L5 H. m& }}_lcd_drv;
" c  [7 O" [- c+ z" c7 v
. W4 ~' E+ o3 N8 K: D- k
上面的接口,也就是对应的驱动,包含了一个驱动id号。
1 id,驱动型号
  o- Y9 N- v8 R8 e" R2 初始化+ Q7 y0 S/ `9 S; A$ Z
3 画点
& z. S) z3 H9 f# Y& L4 将一片区域的点显示某种颜色
2 l% d1 w4 G: E, A1 o5 将一片区域的点显示某些颜色
' H% @8 s' I. K& y+ ?; S1 a6 显示开关+ {& C# M3 I* ]
7 准备刷新区域(主要彩屏直接DMA刷屏使用)" `8 J' w1 _: C4 s3 V
8 设置扫描方向
8 V+ D7 o& i/ t, u& Q4 e8 D4 h9 背光控制
3 Y1 ]/ S5 V2 L3 W  o9 W) m
LCD驱动框架
$ D5 g) d( _* Y  @
我们设计了如下的驱动框架
* N+ {; G- C7 U3 ~, b! Y1 s; V
6 u5 n2 A( q4 ?5 E
! _; d. Q( M  |0 k& @
设计思想

" ?# ~, h2 k' {. }: C- ]) p  z1 J$ G" F6 f
& t# Z1 h, w' b% F
1 中间显示驱动IC驱动程序提供统一接口,接口形式如前面说的_lcd_drv结构体。
' X! _0 Q& R$ M2 各显示IC驱动根据设备参数,调用不同的接口驱动。例如TFT就用8080驱动,其他的都用SPI驱动。
1 ?0 a! j9 }9 M# SSPI驱动只有一份,用IO口控制的我们也做成模拟SPI。* n: I* C$ @2 g- x! ^
3 LCD驱动层做LCD管理,例如完成TFT LCD的识别。并且将所有LCD接口封装为一套接口。/ C" K# I  R4 v9 J6 l4 y
4 简易GUI层封装了一些显示函数,例如划线、字符显示。: s1 ~/ e/ x1 r0 `
5 字体点阵模块提供点阵获取与处理接口。
由于嵌入式实际没那么复杂,在例程中我们将GUI跟LCD驱动层放到一起。
. `# h- L+ A& L5 b: |$ ?) MTFT LCD的两个驱动也放到一个文件,但是逻辑是分开的。3 m, x6 Z" h6 T& T
OLED除初始化,其他接口跟COG LCD基本一样,因此这两个驱动也放在一个文件。

4 \8 A- j" o, z% \
代码分析: C# z4 X0 O! ^5 q
代码分三层:
5 E$ \- Y5 }. k/ Y1. GUI和LCD驱动成3 p4 ^9 U9 M) `( e: B3 Z& R
dev_lcd.c* ?& o! ^& i0 a8 f% m9 X1 k
dev_lcd.h* y9 s' {7 `3 N4 i( `& v; g  A
2. 显示驱动IC层
9 C* V3 g: a) c$ O& o8 G1 k+ v3 zdev_str7565.c & dev_str7565.h' v! W/ i4 F* m. \% M9 G* e& V
dev_ILI9341.c & dev_ILI9341.h5 Y- g# w8 A0 F4 @3 z! Y6 ]+ G
3. 接口层. e& F4 N, j8 `, R
mcu_spi.c & mcu_spi.h
, b, Y8 i6 ~3 x3 c( E8 I% ?' h0 lstm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h
3 c9 s& s3 Z: ?" v0 H+ g9 ?
GUI和LCD层
3 ?0 R$ J% A' I+ f/ y6 u# H! _, C; L
这层主要有3个功能
- G* L5 v1 x4 J- B& X9 L. s
1 设备管理
$ s! n$ ?/ n" U首先定义了一堆LCD参数结构体,结构体包含ID,像素。3 `' v1 J6 _4 k$ M; ^4 |" B
并且把这些结构体组合到一个list数组内。
/*    各种LCD的规格参数*/+ a2 }6 ~" s- P3 p- f" Y
_lcd_pra LCD_IIL9341 =
2 |7 q. q5 {( }  v) V( a1 K6 K{        0 W  `8 K& h! v* Y" A9 f
.id   = 0x9341,        , O6 q# `* }) m; R6 w8 X8 q, `
.width = 240,   //LCD 宽度        
; {* V# X, c* V# P.height = 320,  //LCD 高度. G4 J8 l; C0 E8 p  O2 M
};
. V; w% L4 z' A: A2 g- k...
* L0 p0 m' K' w5 G: A4 O/*各种LCD列表*/0 q& x9 T" h& L0 Z3 x  q3 i6 g
_lcd_pra *LcdPraList[5=            . j/ u8 o7 v1 }0 t8 \9 A
{                7 _8 p. i+ i- }5 H. n/ _! o
&LCD_IIL9341,                       ) |. ^; l) }; |; F$ I
&LCD_IIL9325,               
7 a+ d% S: J2 G1 U6 {3 g&LCD_R61408,               
$ H- E# r0 y$ c8 j7 l' n6 G4 ~&LCD_Cog12864,                + g# p& S+ l, Q1 Q
&LCD_Oled12864,            ! M. b  k. U$ q1 p) G
};
然后定义了所有驱动list数组,数组内容就是驱动,在对应的驱动文件内实现。
/*    所有驱动列表    驱动列表*/
6 ]1 J7 q. g2 a6 l_lcd_drv *LcdDrvList[ = {                    
5 ]! i! [( W3 t$ W1 p/ b6 J&TftLcdILI9341Drv,                    ; T( k3 A) P6 l% m8 K9 p* I( F
&TftLcdILI9325Drv,                  
! s: u7 @+ ?0 Q* I# t6 J, l&CogLcdST7565Drv,                    
: \7 ]" u2 f. X6 l7 j( x&OledLcdSSD1615rv,
定义了设备树,即是定义了系统有多少个LCD,接在哪个接口,什么驱动IC。
* e: d% F5 \$ |( V% j- I( d如果是一个完整系统,可以做成一个类似LINUX的设备树。
/*设备树定义*/
$ C! G& q0 q3 `3 K# s) I# j' J#define DEV_LCD_C 3//系统存在3个LCD设备
5 \5 Q% l  }+ I. j1 O2 J9 p3 JLcdObj LcdObjList[DEV_LCD_C={    ' N( M# R/ Q- ^8 @  [8 ]9 R
{"oledlcd", LCD_BUS_VSPI, 0X1315},   
5 R/ q5 ~" o; t; |# ~+ N{"coglcd", LCD_BUS_SPI,  0X7565},    % F" _4 F: d5 \' y6 c
{"tftlcd", LCD_BUS_8080, NULL},
3 ]+ `: R, e( N* R2 e$ }$ B9 `# K/ c};
: }, t& v' ?4 l: ~
2 接口封装
void dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir)
; j- p* A2 l+ P4 n5 Ps32 dev_lcd_init(void)$ B4 h" S' I( E0 E. S* n
DevLcd *dev_lcd_open(char *name)9 R+ ~( z( j  U' ?" z0 a- J
s32 dev_lcd_close(DevLcd *dev)4 r8 Y9 r/ U* |
s32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)
7 ]0 ]8 X) V0 v2 S5 J8 ks32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)* w4 P- y1 R/ K! D  e1 c" g
s32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta)9 \5 V1 i9 ?  e
s32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color)6 c, U/ U1 X* t- ^4 o) Y% h2 N
s32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)
+ w+ q. ~. n5 I$ ?+ Qs32 dev_lcd_backlight(DevLcd *lcd, u8 sta)
大部分接口都是对驱动IC接口的二次封装。有区别的是初始化和打开接口。& a' k. C. Z& s- S+ H: P2 e
初始化,就是根据前面定义的设备树,寻找对应驱动,找到对应设备参数,并完成设备初始化。
. c, `/ N  P- F' _3 I打开函数,根据传入的设备名称,查找设备,找到后返回设备句柄,后续的操作全部需要这个设备句柄。
3 简易GUI层, z6 P) R, _; @+ _
目前最重要就是显示字符函数。
s32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)
其他划线画圆的函数目前只是测试,后续会完善。
驱动IC层
驱动IC层分两部分
9 a" h2 c. m! o: H1 封装LCD接口% F& |" h6 x: j- m6 i5 H: D3 Q5 @
LCD有使用8080总线的,有使用SPI总线的,有使用VSPI总线的。这些总线的函数由单独文件实现。
" c* M- n. [8 i+ V  W. g但是,除了这些通信信号外,LCD还会有复位信号,命令数据线信号,背光信号等。6 F. M/ m  b, K; r
我们通过函数封装,将这些信号跟通信接口一起封装为LCD通信总线。0 B  r- j6 s7 w
BUS_8080在dev_ILI9341.c文件中封装。
% [: _! X& S- z/ e! S- U& }# A3 d- vBUS_LCD1和BUS_lcd2在dev_str7565.c 中封装。3 J; Y6 U8 A! N# t. ~
2 驱动实现* v( o! v$ _9 B4 P9 s. F
实现_lcd_drv驱动结构体。每个驱动都实现一个,某些驱动可以共用函数。
_lcd_drv CogLcdST7565Drv = {
! G2 B7 p" {& p: Q8 Q& m' Z  j                           .id = 0X7565,
3 b" S$ a; d2 t                           .init = drv_ST7565_init,
5 L( y4 H9 h, C                           .draw_point = drv_ST7565_drawpoint,* s+ O- S6 G9 d5 s/ h, x
                            .color_fill = drv_ST7565_color_fill,
) r% K) ^& h; d  R                           .fill = drv_ST7565_fill, " h. ?+ P3 v. X; B
                           .onoff = drv_ST7565_display_onoff,1 i$ e- y0 t8 f3 O; W& a
                            .prepare_display = drv_ST7565_prepare_display, 5 H$ G9 m1 U$ B$ R, {! V/ F$ }
                           .set_dir = drv_ST7565_scan_dir,
- X/ V4 H' b  y; F/ L6 [. v                            .backlight = drv_ST7565_lcd_bl                           
0 R3 j0 ?. U7 M9 u4 C. S};
# m+ B& z3 `- b+ L: r! A
; |0 B2 h) K  H! l
/ x, p5 F! v( Y
接口层
5 x# O% t0 ]/ i) h1 E) `/ [8 m7 x# e' u2 k& }  O
8080层比较简单,用的是官方接口。! r/ f$ j4 t% y& Z+ z$ Y9 l; _
SPI接口提供下面操作函数,可以操作SPI,也可以操作VSPI。
extern s32 mcu_spi_init(void);$ G) S1 i5 M0 i
extern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);
! ^8 b( L. e& e9 n/ eextern s32 mcu_spi_close(SPI_DEV dev);' r6 I% B# v6 z( E
extern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);( J  t+ W# S5 b8 D( ~3 j
extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);
至于SPI为什么这样写,会有一个单独文件说明。
总体流程
前面说的几个模块时如何联系在一起的呢?( b0 G4 I1 g: c  ?# R
请看下面结构体:
/*    初始化的时候会根据设备数定义,      f: f5 H" c7 X" h$ X. e6 L0 w
并且匹配驱动跟参数,并初始化变量。   
6 h4 L3 X6 |8 U) U( {' @打开的时候只是获取了一个指针*/
1 F) @7 `! S5 {: B$ M) _struct _strDevLcd{( _( y  o- K' G
    s32 gd;//句柄,控制是否可以打开    3 E, D# s/ D% `; L9 a; A
    LcdObj   *dev;   
% o6 }1 x* V/ h( z" x/* LCD参数,固定,不可变*/* z: H6 U7 L. Q8 ]/ Z9 \
    _lcd_pra *pra;   
* a3 j; P6 p7 ?+ ^2 F1 y/* LCD驱动 */ ! n( P' ?4 G" e
   _lcd_drv *drv;    ; }7 g, Z; D( H
/*驱动需要的变量*/
0 o5 Q) E$ s. C( }. p* L    u8  dir;    //横屏还是竖屏控制:0,竖屏;1,横屏。7 ]9 B$ p& \: I3 b; X3 d3 }! J
    u8  scandir;//扫描方向
  b5 t, \; I( p& K" s    u16 width;  //LCD 宽度
) u  W! m: `- w+ R! ^   u16 height; //LCD 高度! ]5 N2 N3 b6 l3 Z7 Z
    void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存
0 p2 v4 M% k4 A% F2 c4 D! r};# K0 }8 Y- A. }; m& F" ^
每一个设备都会有一个这样的机构体,这个结构体在初始化LCD时初始化。* p3 X4 [* A. S  V0 s
- 成员dev指向设备树,从这个成员可以知道设备名称,挂在哪个LCD总线,设备ID。
typedef struct{
' F- Z0 q! T! {6 d- x/ K9 U    char *name;//设备名字' p; Q6 \7 G: R, m4 G4 @8 Y. n% l
    LcdBusType bus;//挂在那条LCD总线上
9 K% X9 k! g; C5 k. L    u16 id;, T0 o- Z. w$ b" Z0 }
}LcdObj;
-成员pra指向LCD参数,可以知道LCD的规格。
typedef struct{
/ A1 {9 N1 n0 w* M& d8 H   u16 id;    4 o1 k: q% T+ y# y: h0 }' ^; ^4 H6 X
   u16 width;  //LCD 宽度  竖屏   
% ~7 E9 Q7 d$ [& h   u16 height; //LCD 高度    竖屏
/ W& x+ p6 J8 B2 |; K( d7 E' G}_lcd_pra;
-成员drv指向驱动,所有操作通过drv实现。
typedef struct  {7 C+ ]9 Y% b! a" }
    u16 id;
9 ^" [6 A. \3 s+ g2 n* I' g    s32 (*init)(DevLcd *lcd);
# v2 f) M2 g3 h" D9 G    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
' T2 v) X, D" j7 H! d    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);
! c* D: G" ^7 f+ w( g- ^7 s7 E    s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color); 8 h- F* c" g+ P
   s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);* l0 n4 g% S7 @, I4 W4 F) Q, \) [
    s32 (*onoff)(DevLcd *lcd, u8 sta); . y  `7 U* A6 b: @8 }+ F9 ]
   void (*set_dir)(DevLcd *lcd, u8 scan_dir); ) I1 Z. P1 i; Q) A3 c& x0 L/ K4 M
   void (*backlight)(DevLcd *lcd, u8 sta);
/ h) r' d/ W! D9 Y) o}_lcd_drv;
  • 成员dir、scandir、 width、 height是驱动要使用的通用变量。因为每个LCD都有一个结构体,一套驱动程序就能控制多个设备而互不干扰。
  • 成员pri是一个私有指针,某些驱动可能需要有些比较特殊的变量,就全部用这个指针记录,通常这个指针指向一个结构体,结构体由驱动定义,并且在设备初始化时申请变量空间。& g1 [4 H! U- t; B9 m+ [
    目前主要用于COG LCD跟OLED LCD显示缓存。% o7 A- U# U7 h. q4 s# a) H
整个LCD驱动,就通过这个结构体组合在一起。
1 初始化,根据设备树,找到驱动跟参数,然后初始化上面说的结构体。. m; d; A8 h* S( L
2 要使用LCD前,调用dev_lcd_open函数。打开成功就返回一个上面的结构体指针。/ @( c: u+ C1 w: p
3 显示字符,接口找到点阵后,通过上面结构体的drv,调用对应的驱动程序。" w/ A1 N- @0 P+ G: z* Z$ k+ V
4 驱动程序根据这个结构体,决定操作哪个LCD总线,并且使用这个结构体的变量。
用法和好处
  • 好处1
    ' i2 ^0 T& e/ Q4 T1 ?+ z
请看测试程序
void dev_lcd_test(void)
* O+ x0 l9 p! f* x8 \  Y; ]; ~1 B{
0 Z" ]- _0 K4 Q! L    DevLcd *LcdCog; - b5 b) i, L, m& l. X" S# l% B# \
   DevLcd *LcdOled;
: }! v' N9 v+ M1 \1 x8 |    DevLcd *LcdTft;
/ D) i+ P+ }; Y1 m4 s' g    /*  打开三个设备 */ ; ^# ?' B, q/ p) V$ u9 A0 A
   LcdCog = dev_lcd_open("coglcd"); 0 R9 ~: T4 r0 C* H- b
   if(LcdCog==NULL) 8 I: Z$ r3 J) W* T% c! G: M
       uart_printf("open cog lcd err\r\n");
/ H) B, K6 O" s: L. Y, S. T8 g    LcdOled = dev_lcd_open("oledlcd");
# y8 O/ ]+ |  x. Q- g3 u    if(LcdOled==NULL)
/ j% ~0 ^  \2 ?. w       uart_printf("open oled lcd err\r\n");
/ d/ p& x* Y! R. x6 Z    LcdTft = dev_lcd_open("tftlcd");$ q4 x+ [6 m7 e- H& ?4 j% @5 x
    if(LcdTft==NULL)' u& k) _, h3 a4 C5 H: h! |
        uart_printf("open tft lcd err\r\n");
8 n+ O1 b( M2 K0 P    /*打开背光*/
2 f3 j# T# Y1 j5 f: A5 p& F1 p   dev_lcd_backlight(LcdCog, 1);
. a, ], @" V+ n! K1 X' V+ ?   dev_lcd_backlight(LcdOled, 1);1 A) o. _9 l8 u+ x
    dev_lcd_backlight(LcdTft, 1);- }. a* I6 K; _7 l+ D8 R
3 h7 L: l2 w+ _$ W
    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
* ^, p8 |! z3 c; p   dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "这是oled lcd", BLACK);
, o5 y" N6 y9 ?. I; w" {. T& A   dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK); : }! i9 p! N" c1 D: l$ \8 g
   dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);
+ X1 u9 L8 ?$ N8 v: {1 {# _   dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
& B+ r& _' M. _   dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是cog lcd", BLACK);
. s6 P" D- F% K; Z! l( F* p* z4 v    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK); ' m; s/ E! o6 A$ Q5 ?& L
   dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK); 9 I- ~& M4 J- }
   dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);
2 U- b, X3 Y( a   dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是tft lcd", RED); - ]8 S0 o% a2 b  k
   dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);
; g: H, f$ i- I) t    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "屋脊雀工作室", RED);    while(1);}
使用一个函数dev_lcd_open,可以打开3个LCD,获取LCD设备。% I( t8 `$ \# Y' d1 L* x2 m  M5 }
然后调用dev_lcd_put_string就可以在不同的LCD上显示。/ B# I: |5 O* g
其他所有的gui操作接口都只有一个。+ Z  K- G+ N! X; R# ^
这样的设计对于APP层来说,就很友好。
1 ]9 J$ W; t7 w+ b6 h1 [4 ]; Z6 R) N$ k# u7 e& r  t
- 好处2
现在的设备树是这样定义的
LcdObj LcdObjList[DEV_LCD_C={ : ^, w" s$ M. B
   {"oledlcd", LCD_BUS_VSPI, 0X1315},
8 _* l8 h7 d) o' p/ |5 ^  ?9 k   {"coglcd", LCD_BUS_SPI,  0X7565},
8 q8 L: p( m' p0 f8 K: b5 X    {"tftlcd", LCD_BUS_8080, NULL},! N: S# M2 Y3 [3 z  R& \
};
某天,oled lcd要接到SPI上,只需要将设备树数组里面的参数改一下,就可以了,当然,在一个接口上不能接两个设备。
LcdObj LcdObjList[DEV_LCD_C={. k2 ~: @) T: L% A
    {"oledlcd", LCD_BUS_SPI, 0X1315}, 4 o0 ~% P5 T, E  _/ g- b7 F
   {"tftlcd", LCD_BUS_8080, NULL},  j3 Q0 p+ v" h2 V
};  K' ?# Y# Y2 @; _, c( I" o

! Y' Y) q" X3 Z

9 W9 }. m$ m; ]  \! f8 U, H, B字库
2 b7 K0 B) b" U7 I! P: R6 F
暂时不做细说,例程的字库放在SD卡中,各位移植的时候根据需要修改。, b; U' Y' j5 B+ ^
具体参考font.c
声明
1 代码请按照版权协议使用。; ~6 {% U) Y9 k4 }6 D' I) h# |( a
2 当前源码只是一个能用的设计,完整性与健壮性尚未测试。
' M6 n7 Q+ z! p: ~% M4 U) C3 后续会放到github,并且持续更新优化。3 X2 S1 D7 Y4 y: J
4 最新消息请关注www.wujique.com.
' [. X5 x/ S# V# d; G( C

3 j+ X3 V; f, ~( t2 y1 P+ a; h7 e) t1 [: N+ K

% b, A* w  V# Q. ^2 o: C( m# N1 Y
6 I: W& [3 m5 R% B
) n7 E5 n: @. M( S2 ~" ~. n$ s; _7 e
+ C* r* E- M+ E5 }" ?
2 q4 X0 u% k+ l) ^
/ u5 Z/ `: W; l, R) E* [0 h8 J4 h$ l) 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:517 }& h, K2 D6 |# I- O
楼主威武,不知道有没有对应的19264的
$ i3 K7 v! H+ J7 q: \6 K* b% z& ]( d
LCD框架已经更新,你可以到我的github上下载最新的。
' U, w+ `* v  g" x. I9 x在最新的里面,增加一个新的LCD非常容易。
+ F! r( v8 X6 j19264,过段时间我会添加进去的。
wujique 回答时间:2018-6-14 10:05:25
z258121131 发表于 2018-6-13 17:51
  e' r% E# Z8 U/ _- [楼主威武,不知道有没有对应的19264的
. B/ o8 v7 \3 N& C
19264应该有多种,不知道你说的是哪种驱动芯片的?
z258121131 回答时间:2018-6-15 15:46:00
huangxuejia-292 发表于 2018-6-14 10:05+ \3 U4 `7 F, V: j
19264应该有多种,不知道你说的是哪种驱动芯片的?

/ f7 u# n# q4 C$ M8 B# R" L4 k+ s9 Mks0108/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+ T5 Q! O% ^8 x2 G
云盘无法打开
0 A  X6 N; u) [: i# f2 r. o2 ^6 r
https://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
赞
% q9 g! K2 d- D/ u4 b学习
jxchen 回答时间:2019-11-9 11:15:28

, m& x- r. k3 `( x. C云盘无法打开
12下一页

所属标签

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