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

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

[复制链接]
wujique 发布时间:2018-3-21 22:07
够用的硬件
3 V$ `% _1 S. `! _. A# J能用的代码
+ W6 r( G& Z" |0 g" r2 o" ?使用的教程
(拷贝过来的代码有点乱,请下载附件查看文档)

网络上配套STM32开发板有很多LCD例程,主要是TFT LCD跟OLED的。从这些例程,大家都能学会如何点亮一个LCD。0 i4 W" K% L* V" F9 F
但是不知道有多少人会直接使用这些代码,至少我不用,不是不用,而是用不了。这代码都有下面这些问题:
1 分层不清晰,通俗讲就是模块化太差。
% R9 l8 M9 N. X0 p$ A# z2 接口乱。其实只要接口不乱,分层就会好很多了。. a; c3 z5 S9 g; T2 \- ~
3 可移植性差。7 X0 A( [2 W: }! R  Q
4 通用性差。
为什么这样说呢?如果你已经了解了LCD的操作,请思考如下情景:
1 代码空间不够,只能保留9341的驱动,其他LCD驱动全部删除。能一键(一个宏定义)删除吗?删除后要改多少地方才能编译通过?
( U0 `1 @; V% m* i2 有一个新产品,收银设备。系统有两个LCD,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。怎么办?这些例程代码要怎么改才能支持两个屏幕?复制一套然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。
9 t% t2 e! y# G# d3 一个OLED,原来接在这些IO,后来改到别的IO,容易改吗?: P* ~1 M1 e+ G, v$ R& ^/ v
4 原来只是支持中文,现在要卖到南美,要支持多米尼加语言,好改吗?
大家慢慢想。

/ @: F6 o" _8 B' N- L3 f* ?: w2 k
LCD种类概述
在讨论怎么写LCD驱动之前,我们先大概了解一下嵌入式常用LCD。只是概述一些跟驱动架构设计有关的概念。至于原理跟细节,在此不做深入讨论,会有专门文章介绍,或者参考网络文档。

: w, @9 U" y  F6 [
TFT lcd
TFT LCD,也就是我们常说的彩屏。. t4 `! e3 y  ]( N1 U$ G+ V
通常像素较高,例如常见的2.8寸,320X240像素。4.0寸的,像素800X400。
$ e4 w6 T' e2 t! T. V这些屏通常使用并口,也就是8080或6800接口(STM32 的FSMC接口);
( [) ?4 j0 @. T8 U4 E& {) u4 ]或者是RGB接口,STM32F429等部分较贵的芯片支持。7 |* J/ }" y1 M' t  E
其他例如手机上使用的有MIPI接口。
& V9 n% \! H+ r7 ^也有一些支持SPI接口的,不过除非是比较小的屏幕,否则不建议使用SPI接口,速度慢,刷屏闪屏。0 e# @, |/ P( x
玩STM32常用的TFT lcd屏幕驱动IC通常有:ILI9341/ILI9325等。6 \3 R0 M) A& S( p: d$ @' L0 w
下图是2.8寸 TFT LCD,表面带电阻触摸屏
1 {( {  ]! m5 p1 r2 k6 c) |; S1 d  f: y+ A3 b, U0 C  l- I
下图为4.0寸 IPS LCD,表面带电容触摸屏- h6 A0 n+ s: I$ C

$ b; f: I2 M. _( H; _
COG lcd
很多人可能不知道COG LCD是什么,我觉得跟现在开发板销售方向有关系,大家都出大屏,玩酷炫界面,对于更深的技术,例如软件架构设计,都不涉及。
* e+ @4 E) Q0 @, p  c使用单片机的产品,COG LCD其实占比非常大。/ {- G- o! T+ B9 y
所谓的COG LCD,
COG是Chip On Glass的缩写,就是驱动芯片直接绑定在玻璃上,透明的。
实物像下图:
7 ~8 M" I4 {( }: J* z) ?" Z( Y4 B- ]1 V  @
这种LCD通常像素不高,常用的有128X64,128X32。& i% S# t9 B- H* }6 j4 c: r( \; X
一般只支持黑白显示,也有灰度屏,我没怎么用过。& a: T, m2 ^8 B7 H7 X8 p* u" M
接口通常是SPI,I2C。也有号称支持8位并口的,不过基本不会用,3根IO能解决的问题,没必要用8根吧?( E. `3 j+ z6 \1 Z7 S9 }
常用的驱动IC:STR7565。
5 T% j9 [% Q4 B  N% Z
OLED lcd
买过开发板的应该基本用过。新技术,大家都感觉高档,在手环等产品常用。OLED目前屏幕较小,大一点的都很贵。3 I4 Z4 |$ Y  H; w3 X: M- T, B
在控制上跟COG LCD类似,区别是两者的显示方式不一样。从我们程序角度来看,最大的差别就是,OLED LCD,不用控制背光。。。。。; t! W8 w) y7 D3 E
实物如下图,2 z9 H3 {& Z, C0 J+ P

! K5 e8 B; N9 c8 {常见的是SPI跟I2C接口。
; c0 ~( k" ~" l3 a- `常见驱动IC:STR7565。

; s$ F3 o% m4 r8 f& T4 d
硬件场景
接下来的讨论,都基于以下硬件信息:1 r4 w8 d1 L, [1 f* ~7 e
1 有一个TFT屏幕,接在硬件的FSMC接口,什么型号屏幕?不知道。' ?7 _; I. k) v7 u
2 有一个COG lcd,接在几根IO口上,驱动IC是STR7565,128X32像素。
  C8 {) v; m' ~# @" a: e3 有一个COG LCD,接在硬件SPI3跟几根IO口上,驱动IC是STR7565,128x64像素。6 x# M. d! W' F$ a, ?" S8 b  ~
4 有一个OLED LCD,接在SPI3上,使用CS2控制片选,驱动IC是SSD1315。8 R( Y  [# Y5 y  H
% L2 a% F% h3 m* q
预备知识
在进入讨论之前,我们先大概说一下下面几个概念,对于这些概念,如果你想深入了解,请GOOGLE。
* a+ m( d# {( j
面向对象
面向对象,是编程界的一个概念,常在C++中出现。
- Q' F1 l, M' E1 i什么叫面向对象呢?
# e: w$ y3 _1 O7 v( h4 {/ \# h编程有两种要素:程序(方法),数据(属性)。
% H" ^1 ?( A/ `' q, d1 {4 q. f例如:一个LED,我们可以点亮或者熄灭它,这叫方法。
6 P2 \2 l3 x8 |! RLED什么状态?亮还是灭?这就是属性。) x+ B8 R) M; p& s& R6 ?; L
我们通常这样编程:
/ t8 L0 X) T0 n' O
u8 ledsta = 0;void ledset(u8 sta)
9 r1 m: L; Q0 r2 V2 \' b( K{
& b- U3 p; S, |* W4 S6 b8 Q
. V. m0 f4 l; U$ s8 Z# k

$ C) q1 i6 t! M8 J" C. p}
6 X/ Y" H) V8 v) h! x2 \& C0 d! u% ?0 N; ]7 n7 z$ v4 l
这样的编程有一个问题,假如我们有10个这样的LED,怎么写?
9 W. r) O: i: c, Z+ C这时我们可以引入面向对象编程,将每一个LED封装为一个对象。可以这样做:
) F* I; O' n6 j* _/ u5 `
/*定义一个结构体,将LED这个对象的属性跟方法封装。这个结构体就是一个对象。! c; t9 A: d, k( _
但是这个不是一个真实的存在,而是一个对象的抽象。*/
; G& Z6 n' o; k' a5 L  z: H5 vtypedef struct{   : w6 g* E+ j5 n) g+ E+ a
     u8 sta;    8 t% O) C" M/ w5 `0 l. w. Y& D
     void (*setsta)(u8 sta);
6 s' d% m* U6 c! B' g6 Y) M}LedObj;
* |: ?0 z0 r6 A$ _' g6 [( s% Q6 u% Z/ w2 x  m9 I
: v& Q; W& j7 g% i- q- s, R. z
/*   
" e% K1 e6 _! T* b9 X声明一个LED对象,名称叫做LED1,   
1 b3 s! v( A4 s- k并且实现它的方法drv_led1_setsta
: [' s$ ]2 w+ b# t/ Y*/2 c. L' j% D4 a2 ~
void drv_led1_setsta(u8 sta)7 u) z+ V7 n- g- @3 F8 h/ l
{- H) @! D( B% P. u/ F; o2 _) z) K; e
}
6 X+ u. U9 p- x) V+ Y
9 A/ T. l8 _0 B, h+ s4 ^5 n! ULedObj LED1={        
/ M7 R" Q1 F7 G: t6 Z" M/ f' b    .sta = 0,        . a) s$ m' B6 O$ J0 e! a3 l
    .setsta = drv_led1_setsta,    ! J- a% @  Q5 R- {1 h  h
    };1 J5 I+ ^: a$ w  Y& ]3 S3 j* C
  u+ H1 h2 w6 i
- y4 h5 A* I% X! T: z$ W5 H& _
/*    + J, L! j8 s- ?6 l  B( H4 V
声明一个LED对象,名称叫做LED2,   
- K& A5 ^7 S* ^+ `2 V4 @并且实现它的方法drv_led2_setsta*/
0 {  R/ g0 V! B1 i8 uvoid drv_led2_setsta(u8 sta)) N2 `: t; e8 I& q# b$ N. W/ k
{
) ?# f+ n' h5 v! M3 X! u}
8 n2 G! E. N# Y0 \  ~" ^LedObj LED2={       5 _$ E* H3 ]# B1 V
     .sta = 0,        / Y6 p$ V5 x( ?( S1 q
    .setsta = drv_led2_setsta,   
9 M6 k8 `' r" k( A. j, L" i9 |1 |& _- i& h    };- R2 g5 @* x5 U# L. U) w7 K
/*    / w3 \0 ~8 [8 v% w
操作LED的函数,参数指定哪个led*// r- e1 Q& o$ `) w
void ledset(LedObj *led, u8 sta)
/ ?" M& r+ _& I: A8 \. D{   
  x6 o: z- ], h1 W+ x1 c; N* {% k     led->setsta(sta);
) v% b. J# t0 ~) Y5 G- f}
抛砖引玉,很多地方不正确,但是不想展开,大家自己搜索资料学习。

; N  E7 I- [7 p- H, j( ?
是的,在C语言中,实现面向对象的手段就是结构体的使用。
# O6 N+ |$ L' h0 c上面的代码,对于API来说,就很友好了。# X% o( f: @& b, M5 K
操作所有LED,使用同一个接口,只需告诉接口哪个LED。, o! `* L& w$ S7 c" t
大家想想,前面说的LCD硬件场景。2 l! I! ?, @: C# s2 I9 u  w
4个LCD,如果不面向对象,显示汉字的接口是不是要实现4个?每个屏幕一个?
5 ?/ u+ O! G, p0 G) S; s$ v
驱动与设备分离8 y# W6 m, y2 U5 W. G0 n& t
如果要深入了解驱动与设备分离,请看LINUX驱动的书籍。
什么是设备?
6 L" t. |! {. s, U8 o5 H3 \& M我认为的设备就是属性,就是参数,就是驱动程序要用到的数据和接口。5 @9 _3 G" S) g' o) b/ c/ F
那么驱动就是控制这些数据和接口的代码过程。通常来说,如果LCD的驱动IC相同,就用相同的驱动。有些不同的IC也可以用相同的,例如SSD1315跟STR7565,除了初始化,其他都可以用相同的驱动。2 O: x- w# h& r7 D0 X2 f5 @/ q
例如一个COG lcd:
驱动IC是STR7565
( [( V( ]8 T# O8 T9 @128*64像素% R3 W0 W7 y5 f9 R
用SPI3" u7 o0 T. G4 E$ Q# \
背光用PF5' m2 Q; k8 R' j5 J- d  D' l
命令线用PF48 T9 ~6 w3 y$ G7 H8 `' j+ a
复位脚用PF3
上面所有的信息综合,就是一个设备。' G+ i% `8 Z$ ~' d8 T/ _% f
驱动就是STR7565的驱动。
为什么要驱动跟设备分离,因为要解决下面问题:
2 有一个新产品,收银设备,系统有两个LCD,一个叫做主显示,收银员用,一个叫顾显,顾客看金额使用。怎么办?这些例程代码要怎么改才能支持两个屏幕?复制一套然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。
这个问题,两个设备用同一套程序控制才是最好的解决办法。9 P- ]- X" C3 m
驱动与设备分离的手段:
在驱动程序接口中增加设备参数,驱动用到的所有资源从设备参数传入。
驱动如何跟设备绑定呢?通过设备的驱动IC型号。

" ^" L! o" K; X' C" ~3 ?- E
模块化* _& z* Y% i# p' b  |* C. g/ Y+ Y
我认为模块化就是将一段程序封装,提供接口,给不同的驱动使用。6 |1 s8 ]$ q* ^3 L
不模块化就是,在不同的驱动中都实现这段程序。5 e. n3 R" }) u/ [; E* K
例如字库处理,在显示汉字的时候,我们要找点阵,在打印机打印汉字的时候,我们也要找点阵,你觉得程序要怎么写?& c* i4 _% N, o
把点阵处理做成一个模块,就是模块化。6 O' K6 c3 i- x0 d+ N
非模块化的典型特征就是一根线串到底,没有任何层次感。

+ }0 ^, \4 U/ F
LCD到底是什么
( \8 |' n0 i$ N% Q+ F1 \9 L
前面我们说了面向对象,想要对LCD进行抽象,得出一个对象,就需要知道LCD到底是什么。
' T6 a. D! j; [6 M& f我们问自己下面几个问题:
) R6 j7 r" x2 ]" \8 j" [1 LCD能做什么?7 o# N5 c+ p' z9 R, d1 J& w
2 要LCD做什么?
+ S* M! Y( ]# {0 b2 y3 谁想要LCD做什么?
刚刚接触嵌入式的朋友可能不是很了解,可能会想不通。我们模拟一下LCD的功能操作数据流。
6 ^2 V! a3 L) |! ]( X- K$ aAPP想要像是一个汉字。
1 首先,需要一个像是汉字的接口,APP调用这个接口就可以显示汉字了。假设接口叫做lcd_display_hz。/ k6 Y0 N6 h7 F5 V% G8 z+ ]" `
2 汉字从哪来?从点阵字库来,所以在lcd_display_hz函数内就要调用一个叫做find_font的函数获取点阵。9 ~# j5 t) b8 b7 z7 K- }/ w7 p/ b
3 获取点阵后要将点阵显示到LCD上,那么我们调用一个ILL9341_dis的接口,将点阵刷新到驱动IC型号为ILI9341的LCD上。
0 n  [+ @3 P  Z4 ILI9341_dis怎么将点阵显示上去?调用一个8080_WRITE的接口。
好的,这个就是大概过程,我们从这个过程去抽象LCD功能接口。
  T8 F& H2 |9 ^. S2 ]; i+ z汉字跟LCD对象有关吗?无关。在LCD眼里,无论汉字还是图片,都是一个个点。
; R& |6 W% B( i  u那么前面问题的答案就是:
1 LCD可以一个点一个点显示内容。- D7 b% Q, r4 }1 c" ^: ^
2 要LCD显示汉字或图片-----转化后就是显示一堆点: Q- ~/ M! k" L3 M8 F
3 APP想要LCD显示图片或问题。
结论就是:
1 k& C7 `) `2 T7 k3 Z所有LCD对象的功能就是显示点。
3 i% {8 l: s/ c0 m那么驱动只要提供显示点的接口就可以了,显示一个点,显示一片点。9 V6 ?1 h$ g# v5 B
抽象接口如下:

3 x& t+ V% M3 [/ r# }/ S8 u7 Z
/*    LCD驱动定义*/. S- R! \' z9 [% `+ y
typedef struct  7 C3 o( R7 J5 Y8 N. E
{   
& V( w' A1 E& ?" s9 s  }# wu16 id;   
+ n2 O/ m. E' |6 S2 W2 I+ } s32 (*init)(DevLcd *lcd);   
6 q" c, T8 i! X' Js32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);   
( c( M+ D) l. z' Us32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);    7 x8 R  `6 t  m$ J# a
s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);   
% s: t6 S! b4 `! rs32 (*onoff)(DevLcd *lcd, u8 sta);   
/ x0 z+ v0 O& C" js32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);   
. M* {4 v3 \( F/ f& wvoid (*set_dir)(DevLcd *lcd, u8 scan_dir);   
. f. u8 ~0 l5 |+ m! k0 o$ gvoid (*backlight)(DevLcd *lcd, u8 sta);# b  l( q) S% k
}_lcd_drv;
7 w# J5 o& z- ~0 H' ~( Y! e2 E( W8 h8 H. e& F' Z$ q+ f
上面的接口,也就是对应的驱动,包含了一个驱动id号。
1 id,驱动型号" J/ E# q5 L9 I$ }4 n& h; _
2 初始化
8 L9 V6 j3 ]3 P) W3 画点
1 E  @( S2 K5 S  g2 }+ g+ y* E4 将一片区域的点显示某种颜色
: T. w1 V% K) N1 ~- J5 将一片区域的点显示某些颜色* a1 ]9 f. P+ R! B* @& E9 w
6 显示开关" k2 e0 e+ p; T% b& s& I$ g
7 准备刷新区域(主要彩屏直接DMA刷屏使用)+ }5 Z: h  U. x3 k* W4 U
8 设置扫描方向" _6 p- O6 r1 G
9 背光控制

0 ~) Y6 r6 e4 c. D
LCD驱动框架
% X& _) x, _$ J  Y- S' q
我们设计了如下的驱动框架9 N- d0 ~; A2 D9 E6 ]. R

$ D# o/ _4 @' s+ `1 Q7 a  J3 K/ U5 j9 M+ V0 C+ k2 S
设计思想
0 e' F7 W/ p: R

7 Q- c+ u% N% i1 }, A# S5 i1 _

' z+ G- f! L: p5 P4 f. [- F8 `1 中间显示驱动IC驱动程序提供统一接口,接口形式如前面说的_lcd_drv结构体。
- l7 s. _9 ?8 H5 q2 各显示IC驱动根据设备参数,调用不同的接口驱动。例如TFT就用8080驱动,其他的都用SPI驱动。
; f) w9 ~1 A6 d. ASPI驱动只有一份,用IO口控制的我们也做成模拟SPI。
9 o7 o9 N1 f8 A( J# v% |3 LCD驱动层做LCD管理,例如完成TFT LCD的识别。并且将所有LCD接口封装为一套接口。" n$ P3 C* g) ~9 T1 c6 c# {: `$ O
4 简易GUI层封装了一些显示函数,例如划线、字符显示。/ j# n1 i, Z( u: i3 J8 r+ M
5 字体点阵模块提供点阵获取与处理接口。
由于嵌入式实际没那么复杂,在例程中我们将GUI跟LCD驱动层放到一起。
, s! h. ~6 L* R$ ]( v5 M  u  |TFT LCD的两个驱动也放到一个文件,但是逻辑是分开的。6 m$ P% s: e6 K- R
OLED除初始化,其他接口跟COG LCD基本一样,因此这两个驱动也放在一个文件。
' P! |4 I8 u/ V  G' j
代码分析1 A2 B% m1 k" S! b/ {
代码分三层:
$ N' n1 v* y0 E! t! O1. GUI和LCD驱动成
6 n+ d( c" w4 ndev_lcd.c
3 H, q+ T9 C" `1 }( r' adev_lcd.h  R0 x7 [( K+ o$ y' O) K. S
2. 显示驱动IC层
# h7 A8 p( z, [$ q  k* Q( w$ Bdev_str7565.c & dev_str7565.h
5 T+ ]# y- @- \& j4 }. S8 Y5 ?dev_ILI9341.c & dev_ILI9341.h
: Q1 O. g& `* i4 Y0 I1 P9 {2 i1 L: p  N3. 接口层
! l0 p5 h' ]2 @! L8 mmcu_spi.c & mcu_spi.h- b1 ?5 M  F1 t0 n* C% i
stm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h

3 g. m7 h# H7 q5 d0 p9 m7 \
GUI和LCD层8 [2 e- z+ l6 D4 Z. Z& n
这层主要有3个功能

+ _  @' C. c/ g" D' |. O$ j1 设备管理0 v0 K+ O" ~+ N2 K1 W+ w4 F6 c: ]
首先定义了一堆LCD参数结构体,结构体包含ID,像素。
7 U: |. c2 |) c/ ^' @9 t并且把这些结构体组合到一个list数组内。
/*    各种LCD的规格参数*/
; Y% f2 b0 H( t_lcd_pra LCD_IIL9341 =
; a" j8 z1 `& w' M! q# h6 C" c8 s* f3 j{        0 k* [% z( o' f- U- {
.id   = 0x9341,        $ p+ S; G# x$ \# W
.width = 240,   //LCD 宽度        " z1 F5 C2 s) r) F8 E7 f
.height = 320,  //LCD 高度
0 ]1 A0 k* x. v/ a( M};) M/ k5 d. V% ^# _# E3 z
...! Q, T3 q3 n; h6 F! R% v8 t/ u
/*各种LCD列表*/
5 t7 @3 y8 g+ M! \! p: [_lcd_pra *LcdPraList[5=            
' X* R; J9 V) e0 `3 W, L2 @0 H{               
. z; R- X  d% J- k6 G9 P&LCD_IIL9341,                       $ `' o( q8 I& V( e
&LCD_IIL9325,                + r3 M' M( ^  e) `, E: _
&LCD_R61408,               
1 Z6 l- H6 i1 ?+ w3 v1 h" x&LCD_Cog12864,               
, `; E1 w9 a; f/ z& z4 B9 v&LCD_Oled12864,            
2 E; A9 m4 T* E7 Y};
然后定义了所有驱动list数组,数组内容就是驱动,在对应的驱动文件内实现。
/*    所有驱动列表    驱动列表*/7 c+ [' ]9 e- t# v) ?
_lcd_drv *LcdDrvList[ = {                    
3 ~7 v; E  s# L/ t4 p' f$ C- @&TftLcdILI9341Drv,                    6 B% q" v9 Z9 T$ d/ R5 f2 r: l
&TftLcdILI9325Drv,                   & @9 X2 n+ b3 U% o2 J# c" f
&CogLcdST7565Drv,                    
3 U  W* B4 W4 T- J2 e&OledLcdSSD1615rv,
定义了设备树,即是定义了系统有多少个LCD,接在哪个接口,什么驱动IC。4 V( x& k# l' T5 j5 J  _
如果是一个完整系统,可以做成一个类似LINUX的设备树。
/*设备树定义*/
& Z% u. ~6 C- e- K: Z! B6 a3 n#define DEV_LCD_C 3//系统存在3个LCD设备  S. R4 k3 E# k2 u& }* \
LcdObj LcdObjList[DEV_LCD_C={    9 M. ^( U9 W" L8 z2 t# P4 n5 y
{"oledlcd", LCD_BUS_VSPI, 0X1315},   
. a; ?/ a* K4 y{"coglcd", LCD_BUS_SPI,  0X7565},    # U8 P" q% r" c$ O( K3 o
{"tftlcd", LCD_BUS_8080, NULL},
, ]# w6 ]' i1 Q5 u3 m7 ?1 [" M};
. D+ \$ D) V" B" I- `. D/ }
2 接口封装
void dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir)
9 D* v$ q' w( H/ ]: ]( is32 dev_lcd_init(void)
- r1 j- U3 n* R3 ^7 @3 Z8 X  hDevLcd *dev_lcd_open(char *name)
; w2 ?$ g' T; X  A; x- `, }: T+ ~4 Qs32 dev_lcd_close(DevLcd *dev)7 H9 O6 V6 Y/ p3 C
s32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)
0 w. L$ ~' D$ @. Ws32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)
. e1 J2 y0 p0 vs32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta)$ D- X: r+ B6 d
s32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color)* [! i) f2 d( l- [8 U
s32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)
4 A1 G; d6 r0 q/ Is32 dev_lcd_backlight(DevLcd *lcd, u8 sta)
大部分接口都是对驱动IC接口的二次封装。有区别的是初始化和打开接口。7 F0 K/ p  v0 f$ L
初始化,就是根据前面定义的设备树,寻找对应驱动,找到对应设备参数,并完成设备初始化。1 ?% ?$ S: e/ e" |
打开函数,根据传入的设备名称,查找设备,找到后返回设备句柄,后续的操作全部需要这个设备句柄。
3 简易GUI层
0 L- [4 p, C) i. D+ G3 ]+ Q2 q目前最重要就是显示字符函数。
s32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)
其他划线画圆的函数目前只是测试,后续会完善。
驱动IC层
驱动IC层分两部分% Y- W1 p9 z* P
1 封装LCD接口' v( I6 W* e/ u5 I. f/ ^
LCD有使用8080总线的,有使用SPI总线的,有使用VSPI总线的。这些总线的函数由单独文件实现。
0 o6 u' }0 m! @  Y+ A9 o; v但是,除了这些通信信号外,LCD还会有复位信号,命令数据线信号,背光信号等。% S  i) ?0 E5 w# n6 y
我们通过函数封装,将这些信号跟通信接口一起封装为LCD通信总线。
' u. c4 a* v) X9 _& aBUS_8080在dev_ILI9341.c文件中封装。& \* s5 u2 i0 v8 {
BUS_LCD1和BUS_lcd2在dev_str7565.c 中封装。
. x  Y) j/ {; F8 @, h2 驱动实现
& b% ^8 V. N* }实现_lcd_drv驱动结构体。每个驱动都实现一个,某些驱动可以共用函数。
_lcd_drv CogLcdST7565Drv = {
  |8 i  f$ H' t3 J' q& C3 G, t                           .id = 0X7565, 5 t5 a8 x7 s* P* @/ |( m7 k
                           .init = drv_ST7565_init, , y9 b+ {$ u& o  f# r
                           .draw_point = drv_ST7565_drawpoint,6 D$ v6 _& X" r* }- F5 R
                            .color_fill = drv_ST7565_color_fill, - f/ M  m- V$ l# k0 Y0 w2 Y' o7 X
                           .fill = drv_ST7565_fill,   j  f" o5 `$ H
                           .onoff = drv_ST7565_display_onoff,
5 l" D3 |0 @# p/ F                            .prepare_display = drv_ST7565_prepare_display, + W5 E& ^- a( j' ~* e: y7 ^) t
                           .set_dir = drv_ST7565_scan_dir,
0 `% L7 ]  Y8 L: s5 u) a8 @* U' U                            .backlight = drv_ST7565_lcd_bl                           
$ a5 {8 g% R& j- E9 p" ^1 K9 m};
" W# g4 O: M& O& U# n7 ~) o3 I$ A  b+ \+ ?7 d
. l/ ^% R+ M8 H( v0 A3 x  ?
接口层
( n' e! \. A% s! j+ }: F& w, Q4 B& X0 V4 `0 ~4 ~( }/ Y! U& @) K7 ~
8080层比较简单,用的是官方接口。
0 R/ G0 g0 k; u$ X+ SSPI接口提供下面操作函数,可以操作SPI,也可以操作VSPI。
extern s32 mcu_spi_init(void);, p1 p: C6 ~2 [" E. P  U- v% ~7 F. {
extern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);
$ N' J+ z: T: u% D: J. R" `extern s32 mcu_spi_close(SPI_DEV dev);
  m: i9 _* E. H. }extern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);, G/ C9 G4 L: `6 p! m8 W6 t- W
extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);
至于SPI为什么这样写,会有一个单独文件说明。
总体流程
前面说的几个模块时如何联系在一起的呢?7 ?( @  Q3 @* k& e  M' A8 p8 g& ~
请看下面结构体:
/*    初始化的时候会根据设备数定义,    ; ?5 P7 S% k4 l8 Q
并且匹配驱动跟参数,并初始化变量。    & W9 M6 ]+ {/ @
打开的时候只是获取了一个指针*/
% L) ?# c: e- w) e6 o* Z% [struct _strDevLcd{
- k5 q/ t: e. s, r/ Z0 a    s32 gd;//句柄,控制是否可以打开   
( F0 o0 R" w. Z+ f& e  B$ D    LcdObj   *dev;    6 H1 f4 K: A# M! `' N  |$ d- M6 H
/* LCD参数,固定,不可变*/' u5 H' D8 _! |" }- r
    _lcd_pra *pra;    ' v7 H) U8 \$ ~- T6 L
/* LCD驱动 */
7 s2 L3 {% L# o3 P! U3 n: A. Z   _lcd_drv *drv;    1 {4 z, @7 \* o3 N; [7 s
/*驱动需要的变量*/& I3 K8 M# a6 N" s
    u8  dir;    //横屏还是竖屏控制:0,竖屏;1,横屏。* B( H4 X4 f8 R8 D9 j3 f6 I2 k
    u8  scandir;//扫描方向
: N" {: w; U$ v  B" \( s    u16 width;  //LCD 宽度
& u0 y; \6 ~2 e' P' g7 v$ ~$ O   u16 height; //LCD 高度8 a) B# {+ U& I* w7 `4 N
    void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存
; V( y+ C+ D; Y( b" b- ~: r$ g};
3 `* u0 u, O; t' j5 a7 _
每一个设备都会有一个这样的机构体,这个结构体在初始化LCD时初始化。
1 g: ~8 {! w7 A& ]2 G% U- 成员dev指向设备树,从这个成员可以知道设备名称,挂在哪个LCD总线,设备ID。
typedef struct{8 A+ n% J' M# @+ M+ ?
    char *name;//设备名字0 g7 y8 F4 |# t0 q
    LcdBusType bus;//挂在那条LCD总线上
8 U& H5 o6 y0 K$ `    u16 id;
- o# `# d2 u$ J& c7 G- ?}LcdObj;
-成员pra指向LCD参数,可以知道LCD的规格。
typedef struct{ 0 K7 r" A. N* ?0 p+ W
   u16 id;    , A$ U5 t; E& K$ g$ ~1 f
   u16 width;  //LCD 宽度  竖屏   
! d" O2 G4 x4 m0 M4 k1 {   u16 height; //LCD 高度    竖屏
/ s0 D) x0 ~" `0 T}_lcd_pra;
-成员drv指向驱动,所有操作通过drv实现。
typedef struct  {
2 I3 S( d" g4 n) ]7 C( v9 ]' t/ q    u16 id;
0 g, {5 J, B& B- j$ i" c  h- Z! K    s32 (*init)(DevLcd *lcd);+ e- r! x$ q' j  D& L) h
    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);# J2 y0 M- h% m' l/ C( D
    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);
7 c, P4 @+ @6 }2 \    s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color); : R7 w; k( z9 l
   s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);9 R$ r! N' C3 Q& g! k' b8 ]2 v
    s32 (*onoff)(DevLcd *lcd, u8 sta); - T( o: y6 b# O
   void (*set_dir)(DevLcd *lcd, u8 scan_dir); 1 n' |  @' S. W
   void (*backlight)(DevLcd *lcd, u8 sta);  N- |% S7 z) f9 d- H+ O$ R& k
}_lcd_drv;
  • 成员dir、scandir、 width、 height是驱动要使用的通用变量。因为每个LCD都有一个结构体,一套驱动程序就能控制多个设备而互不干扰。
  • 成员pri是一个私有指针,某些驱动可能需要有些比较特殊的变量,就全部用这个指针记录,通常这个指针指向一个结构体,结构体由驱动定义,并且在设备初始化时申请变量空间。
    " s' B4 r1 m. R1 ^/ ~目前主要用于COG LCD跟OLED LCD显示缓存。( ~$ P) s% Q& m- a; b
整个LCD驱动,就通过这个结构体组合在一起。
1 初始化,根据设备树,找到驱动跟参数,然后初始化上面说的结构体。
! o( _' d. |" W  U  J3 h2 要使用LCD前,调用dev_lcd_open函数。打开成功就返回一个上面的结构体指针。
, ^4 u9 `, h# _( q# w0 G! S3 显示字符,接口找到点阵后,通过上面结构体的drv,调用对应的驱动程序。' O8 ?" d! m3 h, V  a$ H* F) {% I
4 驱动程序根据这个结构体,决定操作哪个LCD总线,并且使用这个结构体的变量。
用法和好处
  • 好处1
    - H) K( W: {2 P
请看测试程序
void dev_lcd_test(void)$ G9 O- K7 x# C
{
9 G/ Z5 ]$ d6 w. D    DevLcd *LcdCog;
0 p% u5 ?: ?" u; J  s: ]* V   DevLcd *LcdOled;+ K/ M5 V1 F8 q, W0 h6 h) p; Q5 I; Q6 A
    DevLcd *LcdTft;+ i1 D7 i1 I6 h# Y- U- p
    /*  打开三个设备 */
& W4 p5 @: T* t, j# J* `   LcdCog = dev_lcd_open("coglcd");
& E5 P1 s5 e7 Y. E   if(LcdCog==NULL) , c) q: o7 A) Z# A: l8 c
       uart_printf("open cog lcd err\r\n");& W! P5 E! j) ]$ e
    LcdOled = dev_lcd_open("oledlcd");( r$ H5 S& d& B
    if(LcdOled==NULL) & F& `0 N: a5 Q9 q
       uart_printf("open oled lcd err\r\n");
# A9 ^. F& V; a% [1 \4 X) @& n  v    LcdTft = dev_lcd_open("tftlcd");6 G( i9 V9 e* J0 F
    if(LcdTft==NULL)
* f% Z1 C) e4 y. t, a        uart_printf("open tft lcd err\r\n");
' @: z2 L( _: }; E% P8 z' E    /*打开背光*/ * b( ~( l/ B" U+ n3 C3 e
   dev_lcd_backlight(LcdCog, 1);
: t/ d1 F' K$ L; ^   dev_lcd_backlight(LcdOled, 1);
& ]* b+ ?& m3 }+ {    dev_lcd_backlight(LcdTft, 1);: n! J- q. f: R& a
; _5 ^0 W0 T. p7 e. o5 Y
    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK); + {8 V  @$ A+ B3 x1 f/ m
   dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "这是oled lcd", BLACK); & R" {4 J; `* v; O6 h
   dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK); 0 r1 o. J/ R; Z1 B2 _9 T& g
   dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK); 6 p$ r0 d$ b# f3 g. t/ }! s# v9 M
   dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK); 8 J+ N9 j7 h0 w9 G0 M
   dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是cog lcd", BLACK);
9 f! h0 l) L  X9 B8 C    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK); 5 z" \+ i6 `1 b" q
   dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);
6 u3 o+ q1 J4 {" |# h   dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);
( }- m7 T1 E; N6 o   dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是tft lcd", RED);
$ L0 i$ G/ d- c8 `" D   dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);
! `- u5 \! J( o+ q# Y* G2 A    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "屋脊雀工作室", RED);    while(1);}
使用一个函数dev_lcd_open,可以打开3个LCD,获取LCD设备。
& ~( f4 q! I3 P; T7 y6 y然后调用dev_lcd_put_string就可以在不同的LCD上显示。0 J. T' j; I% L& @
其他所有的gui操作接口都只有一个。
4 s' k& y% b2 l这样的设计对于APP层来说,就很友好。' D( o' z4 }& f; U5 o

6 Z7 T: J$ ^. ]0 G- 好处2
现在的设备树是这样定义的
LcdObj LcdObjList[DEV_LCD_C={
* g8 I* f" g0 C" v% f   {"oledlcd", LCD_BUS_VSPI, 0X1315}, 4 Z0 b# ^# ?" s8 S* l: ]# ?" X
   {"coglcd", LCD_BUS_SPI,  0X7565},2 G! x; B& b( J
    {"tftlcd", LCD_BUS_8080, NULL},# Z- s9 N( Q; l% x8 K
};
某天,oled lcd要接到SPI上,只需要将设备树数组里面的参数改一下,就可以了,当然,在一个接口上不能接两个设备。
LcdObj LcdObjList[DEV_LCD_C={
, S8 K# Z6 a. `1 [) ~* R$ u    {"oledlcd", LCD_BUS_SPI, 0X1315},
- J4 V$ C, p* A# {4 m4 h   {"tftlcd", LCD_BUS_8080, NULL},
' F& L* T* x+ J, J7 s- g( x6 A1 w};
( g2 A9 M0 n2 _. q# q7 S0 j7 O" }, X: c3 n: w) W+ F' C  C2 X- A; _. `

! v" g2 [" g7 M% V* z3 E. h字库
9 E7 o+ p% R$ k1 I( b0 T6 F
暂时不做细说,例程的字库放在SD卡中,各位移植的时候根据需要修改。; v  w8 {6 ^7 e! x1 Z
具体参考font.c
声明
1 代码请按照版权协议使用。* q5 K# M0 \8 m* i
2 当前源码只是一个能用的设计,完整性与健壮性尚未测试。$ k% L$ P9 h. D- h  Q" V  {- X
3 后续会放到github,并且持续更新优化。7 n. O" j  M9 G: e  S
4 最新消息请关注www.wujique.com.
, r5 j9 H  V  W- d$ m; s

" G5 C( N$ D* g, g4 g. x  [
1 {4 E9 U3 g9 U, z5 C5 E( m
4 J5 I3 d( [( e0 _. G, Q
* w6 h( P* K& r
# }. X' O4 T4 s" f# |8 R- U" N2 d* }. B1 P& c0 {& x$ f$ a

3 `8 P+ ^( D% M. w9 E, M# y7 _) d
3 E" B0 Y$ v" K; X' M' b
  t2 @* x/ h4 W

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+ h5 {/ _2 d6 u' N
楼主威武,不知道有没有对应的19264的

1 D4 v9 \1 d) h3 k# ~9 X! XLCD框架已经更新,你可以到我的github上下载最新的。0 b, k4 i- v) Y# I+ `6 I0 I
在最新的里面,增加一个新的LCD非常容易。
+ X9 Z' n' j) F19264,过段时间我会添加进去的。
wujique 回答时间:2018-6-14 10:05:25
z258121131 发表于 2018-6-13 17:51
* Y8 v- G7 {( K# K: V. ?楼主威武,不知道有没有对应的19264的

8 @; ~# w& ^4 y: G' E19264应该有多种,不知道你说的是哪种驱动芯片的?
z258121131 回答时间:2018-6-15 15:46:00
huangxuejia-292 发表于 2018-6-14 10:05
; s% H9 C" _6 c# v1 V19264应该有多种,不知道你说的是哪种驱动芯片的?
1 u1 y% f+ d. i3 t9 w: E& y- C
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
6 Z4 q% W, ]5 P& y+ N& W: E# K云盘无法打开
0 Y& Y/ A; l$ O
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
赞
1 h3 ]& i! ]- C' q+ Z5 }学习
jxchen 回答时间:2019-11-9 11:15:28

; o6 w" C5 F) ?" f云盘无法打开
12下一页

所属标签

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