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

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

[复制链接]
wujique 发布时间:2018-3-21 22:07
够用的硬件
% T, a8 x. Z3 I5 D; K9 s能用的代码
- y2 ^# a; X7 j8 s' v使用的教程
(拷贝过来的代码有点乱,请下载附件查看文档)

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

; \& x( _! R1 ^. u8 c* f. C, j( ^: J
LCD种类概述
在讨论怎么写LCD驱动之前,我们先大概了解一下嵌入式常用LCD。只是概述一些跟驱动架构设计有关的概念。至于原理跟细节,在此不做深入讨论,会有专门文章介绍,或者参考网络文档。

: |/ }& z* X5 _+ k4 R2 J
TFT lcd
TFT LCD,也就是我们常说的彩屏。
3 W1 l! [( ]6 }- E* o通常像素较高,例如常见的2.8寸,320X240像素。4.0寸的,像素800X400。
! A$ {5 F  A- O这些屏通常使用并口,也就是8080或6800接口(STM32 的FSMC接口);: J- q& e: M3 V- s% _; Y* d$ X
或者是RGB接口,STM32F429等部分较贵的芯片支持。# i* a# L. b/ V' r, a
其他例如手机上使用的有MIPI接口。" z% z. p& r  @5 ~* D6 p5 @
也有一些支持SPI接口的,不过除非是比较小的屏幕,否则不建议使用SPI接口,速度慢,刷屏闪屏。
; c4 P- X% ~/ x0 Z" t( O玩STM32常用的TFT lcd屏幕驱动IC通常有:ILI9341/ILI9325等。
% U7 ~5 S4 n9 \+ w1 G: Y% `$ Q: d下图是2.8寸 TFT LCD,表面带电阻触摸屏
' P  [& o7 c* v7 m3 u7 b/ h
7 o' R+ L0 M+ |9 G9 u- h下图为4.0寸 IPS LCD,表面带电容触摸屏' H* g9 |1 r+ Z- |) e
" A4 z# v+ }, u+ M' l, H
COG lcd
很多人可能不知道COG LCD是什么,我觉得跟现在开发板销售方向有关系,大家都出大屏,玩酷炫界面,对于更深的技术,例如软件架构设计,都不涉及。
  d) G5 b( ?1 p7 L使用单片机的产品,COG LCD其实占比非常大。
- ^1 g% Z' K& |) ~9 Q4 @2 O6 t所谓的COG LCD,
COG是Chip On Glass的缩写,就是驱动芯片直接绑定在玻璃上,透明的。
实物像下图:
& x* L! @- P) O4 g) @* C. Z. D7 }; P; b. k; Y
这种LCD通常像素不高,常用的有128X64,128X32。3 @& p& c9 k( f8 U- k9 S4 p
一般只支持黑白显示,也有灰度屏,我没怎么用过。
. V0 z. Q% C) D9 ^接口通常是SPI,I2C。也有号称支持8位并口的,不过基本不会用,3根IO能解决的问题,没必要用8根吧?; s) j, N9 q+ z
常用的驱动IC:STR7565。
, U% |) ~$ I) l
OLED lcd
买过开发板的应该基本用过。新技术,大家都感觉高档,在手环等产品常用。OLED目前屏幕较小,大一点的都很贵。, `# K# W: @9 U5 B! j! x0 \
在控制上跟COG LCD类似,区别是两者的显示方式不一样。从我们程序角度来看,最大的差别就是,OLED LCD,不用控制背光。。。。。- y. A) r7 V) z4 R- \
实物如下图,
7 ^  Q8 X0 J* w1 X0 d8 G# `& X3 R& f9 [' P" U# [
常见的是SPI跟I2C接口。
+ T! Z9 t. X3 e' c( M常见驱动IC:STR7565。
' `& p( m# Q9 Z; U! `1 `6 \  N
硬件场景
接下来的讨论,都基于以下硬件信息:
# E$ L. s/ I2 N" R' ^1 x" O1 有一个TFT屏幕,接在硬件的FSMC接口,什么型号屏幕?不知道。
- R3 _  r) h$ K% F0 c% g6 u6 Q2 有一个COG lcd,接在几根IO口上,驱动IC是STR7565,128X32像素。
+ _) E2 V! I+ |9 O' i3 有一个COG LCD,接在硬件SPI3跟几根IO口上,驱动IC是STR7565,128x64像素。# }) s  J9 T7 C8 ~; y" S4 h
4 有一个OLED LCD,接在SPI3上,使用CS2控制片选,驱动IC是SSD1315。
# ~8 K1 z: O+ X( e: N# p: [
* F* z! V5 `: N
预备知识
在进入讨论之前,我们先大概说一下下面几个概念,对于这些概念,如果你想深入了解,请GOOGLE。
& O6 z4 U9 C. I+ B3 i/ x
面向对象
面向对象,是编程界的一个概念,常在C++中出现。& ?% O4 }' n& {" s4 I
什么叫面向对象呢?
9 f- Z8 M2 L* [/ Z- F编程有两种要素:程序(方法),数据(属性)。
6 W* E$ S8 C) @例如:一个LED,我们可以点亮或者熄灭它,这叫方法。: M# N* L) M  h/ D
LED什么状态?亮还是灭?这就是属性。5 Y+ R7 T" M  `" p
我们通常这样编程:
9 Q+ p: z7 \8 A& W3 e
u8 ledsta = 0;void ledset(u8 sta)& c4 I7 @/ @" K
{
  {) B' q+ B; @) l! k& K, T! Q% Q! T& r6 l- S6 d; O9 f

. M; M, q3 n! R/ W0 k}6 c/ i, S" b0 F7 `

6 W+ h' l" F" ^- ]9 h) H
这样的编程有一个问题,假如我们有10个这样的LED,怎么写?
. [' f1 f3 T. K6 @. G这时我们可以引入面向对象编程,将每一个LED封装为一个对象。可以这样做:
: b0 ?/ Q% g) Y* @
/*定义一个结构体,将LED这个对象的属性跟方法封装。这个结构体就是一个对象。" l; s4 N/ h9 u! C6 @
但是这个不是一个真实的存在,而是一个对象的抽象。*/
- D' {; ~  c2 Q( G8 J- _typedef struct{   - l; _" ~# Z! {5 n; c7 l
     u8 sta;   
$ G3 e1 E2 m7 x) c     void (*setsta)(u8 sta);, B- z: j+ h" ~+ y
}LedObj;: Y5 \6 Y% k1 k3 M6 B& t

: g7 k. q7 y. G6 ]; y4 E

/ e: k2 [0 S; m1 ^/*    5 C* _! J; R8 B! ^8 \. c% K: n
声明一个LED对象,名称叫做LED1,   
3 @7 I3 f4 S7 M# R  t并且实现它的方法drv_led1_setsta
  j  ~3 J8 c3 J8 G6 ^*// u4 x* i# ?+ `, |3 V) V& K
void drv_led1_setsta(u8 sta)' x& H4 c$ g" A$ e# z9 l6 [
{
6 J- T* N* O! T}
( u0 S& O0 j% r! Z, S+ R1 _2 ^% Y1 f: R* ?" [5 ^
LedObj LED1={        
" K* A6 |3 J4 h. E  f9 n+ f    .sta = 0,        
9 B& P6 ~. c' c8 F4 a& ^1 x+ n    .setsta = drv_led1_setsta,    * ^/ Z3 f) r" m$ l5 H
    };' F( [! Z- N8 M% x3 V
4 p; w3 q7 g# w, q% U$ `

; R6 T; g4 u( `0 q2 C7 O& p/*   
8 R- ~: H+ H+ M( W7 B) Q2 y声明一个LED对象,名称叫做LED2,    , V  \" D$ k$ t/ [( B$ P9 G( d/ i
并且实现它的方法drv_led2_setsta*/
, D1 N/ l3 b( a7 Avoid drv_led2_setsta(u8 sta)' v* K. \% K( X7 T2 k
{3 ]( D& Y/ o0 e) ]) z8 p" v7 N! c( j
}$ Z+ X. V+ X3 |; [) x, U8 h
LedObj LED2={       . g: n# R/ z, ?1 C
     .sta = 0,        , a9 s$ T, C( ]7 _: |( ]
    .setsta = drv_led2_setsta,    & g$ H: i* \0 d  g' R
    };
3 T- _8 c- Y4 @' S8 }7 n/*    1 n/ c' X/ _* b+ q( F
操作LED的函数,参数指定哪个led*/
* S- t1 d/ G4 cvoid ledset(LedObj *led, u8 sta)' M& ~: E+ G( c8 [3 q8 k; |
{    4 v& B$ {: k, b, Z. J% @5 D1 }
     led->setsta(sta);
& E* U% t8 ^0 q3 L$ J}
抛砖引玉,很多地方不正确,但是不想展开,大家自己搜索资料学习。
0 a" ]3 y7 s3 |1 U0 R
是的,在C语言中,实现面向对象的手段就是结构体的使用。. |) O: E5 B5 H8 K  x" M
上面的代码,对于API来说,就很友好了。
6 N) f% b1 o" ]0 ]7 |操作所有LED,使用同一个接口,只需告诉接口哪个LED。: p* N6 o0 i! q0 k8 n
大家想想,前面说的LCD硬件场景。
7 I! K; E* C/ q4个LCD,如果不面向对象,显示汉字的接口是不是要实现4个?每个屏幕一个?
! L# g8 i/ S% r$ ?
驱动与设备分离0 }, y' V" j+ ]8 V2 |
如果要深入了解驱动与设备分离,请看LINUX驱动的书籍。
什么是设备?7 s. u3 q3 {; x: @2 |6 ]- u! I
我认为的设备就是属性,就是参数,就是驱动程序要用到的数据和接口。' \( M, e: j7 j. V- O
那么驱动就是控制这些数据和接口的代码过程。通常来说,如果LCD的驱动IC相同,就用相同的驱动。有些不同的IC也可以用相同的,例如SSD1315跟STR7565,除了初始化,其他都可以用相同的驱动。- ?' I" L9 r1 [0 `# O! l9 `
例如一个COG lcd:
驱动IC是STR75653 o. u3 x$ g' Q4 H
128*64像素9 g4 k& h/ r: y) ~0 h- s2 i
用SPI3
6 F* `+ w) E# @& S6 n; M. t4 K2 D) @背光用PF5* S! _' o, I. D! H
命令线用PF4' X( |) A" U- L
复位脚用PF3
上面所有的信息综合,就是一个设备。# E* ~7 Z* L9 ^0 Z4 O5 E# |
驱动就是STR7565的驱动。
为什么要驱动跟设备分离,因为要解决下面问题:
2 有一个新产品,收银设备,系统有两个LCD,一个叫做主显示,收银员用,一个叫顾显,顾客看金额使用。怎么办?这些例程代码要怎么改才能支持两个屏幕?复制一套然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。
这个问题,两个设备用同一套程序控制才是最好的解决办法。8 S  k! G! U# x: r. y
驱动与设备分离的手段:
在驱动程序接口中增加设备参数,驱动用到的所有资源从设备参数传入。
驱动如何跟设备绑定呢?通过设备的驱动IC型号。

7 U/ Z. c5 M5 w# `, J& L  Y* ?
模块化. a# n) e( R3 D: ]! s9 S2 W  o/ W& W
我认为模块化就是将一段程序封装,提供接口,给不同的驱动使用。
9 A  g0 x6 f+ a: A+ S) U不模块化就是,在不同的驱动中都实现这段程序。
& H4 U# b0 Y7 a+ P例如字库处理,在显示汉字的时候,我们要找点阵,在打印机打印汉字的时候,我们也要找点阵,你觉得程序要怎么写?
9 \. S/ S  m0 b5 k把点阵处理做成一个模块,就是模块化。
+ w/ E! T" ?. z+ [! d) \7 m3 O非模块化的典型特征就是一根线串到底,没有任何层次感。

6 J' e4 A% j+ [* n) h4 J
LCD到底是什么
8 C- {( d0 O! ]4 ?7 x
前面我们说了面向对象,想要对LCD进行抽象,得出一个对象,就需要知道LCD到底是什么。
8 A  n# o3 x5 c4 Z8 ?3 b5 e3 s我们问自己下面几个问题:. o1 B; t) w9 g6 s- O, F4 l
1 LCD能做什么?
! g: }3 t6 Y9 ?2 要LCD做什么?. r. [* \4 [0 C3 z3 O2 {0 `
3 谁想要LCD做什么?
刚刚接触嵌入式的朋友可能不是很了解,可能会想不通。我们模拟一下LCD的功能操作数据流。
& w, a; u/ l/ `% L7 k/ v' A# qAPP想要像是一个汉字。
1 首先,需要一个像是汉字的接口,APP调用这个接口就可以显示汉字了。假设接口叫做lcd_display_hz。" U/ G" I7 C6 ]( A2 Y; y
2 汉字从哪来?从点阵字库来,所以在lcd_display_hz函数内就要调用一个叫做find_font的函数获取点阵。. c# B1 A' f! w+ s1 Q' L
3 获取点阵后要将点阵显示到LCD上,那么我们调用一个ILL9341_dis的接口,将点阵刷新到驱动IC型号为ILI9341的LCD上。
6 A2 m+ j) H/ f3 u3 @' |4 ILI9341_dis怎么将点阵显示上去?调用一个8080_WRITE的接口。
好的,这个就是大概过程,我们从这个过程去抽象LCD功能接口。
8 }0 B, F% B4 q' V, {! A汉字跟LCD对象有关吗?无关。在LCD眼里,无论汉字还是图片,都是一个个点。
$ P9 ]7 q8 V- R5 j1 h+ n4 H那么前面问题的答案就是:
1 LCD可以一个点一个点显示内容。
* `/ @* D# ~. o/ S2 要LCD显示汉字或图片-----转化后就是显示一堆点
9 v1 ~) D) X# v# h/ j" ^# S3 APP想要LCD显示图片或问题。
结论就是:* P/ [) ]8 i! D9 @2 g$ D) I1 ?( y
所有LCD对象的功能就是显示点。8 u6 z! n- ^# o, \
那么驱动只要提供显示点的接口就可以了,显示一个点,显示一片点。1 P3 J: r8 T; e. O8 K0 z
抽象接口如下:

7 q0 @. ~- L/ [5 ]& @
/*    LCD驱动定义*/
, A* r2 I0 X$ v1 u, G/ b% N! }typedef struct  ( k1 W  X* q& f- P
{    1 q6 s1 \$ d4 s) m8 K% X4 b4 h
u16 id;   
  o/ T9 f- x4 | s32 (*init)(DevLcd *lcd);    " \; s" R, t8 ^; W
s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);    $ G" u7 ?( r( ?  E  c4 z
s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);   
3 H% S$ ]! T4 f4 w9 _. i3 ls32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);    6 p' T) H7 O6 m
s32 (*onoff)(DevLcd *lcd, u8 sta);   
0 b' l4 t, ?0 q* E+ y/ Q5 os32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);   
  n$ x; E, o5 }7 Z* Z. Q0 I) Zvoid (*set_dir)(DevLcd *lcd, u8 scan_dir);   
- @( n. E0 h9 l! N+ Xvoid (*backlight)(DevLcd *lcd, u8 sta);: Q  {  ?2 k' F' C
}_lcd_drv;
1 F: q5 g' [9 W, M9 \+ M
& V7 ]; J0 G& m5 q: F
上面的接口,也就是对应的驱动,包含了一个驱动id号。
1 id,驱动型号( R$ N4 p" x. ?& {# m; a
2 初始化
5 k  c+ s) s+ [- j3 画点
8 h5 Y& N9 B+ ?* c4 将一片区域的点显示某种颜色
9 ?" i8 g  s1 k4 \/ }4 V9 k3 e5 将一片区域的点显示某些颜色
& X1 [! B7 u2 a" H6 N- j" H3 ~6 显示开关
, [7 d/ d8 `" v. i7 准备刷新区域(主要彩屏直接DMA刷屏使用)
+ z' K/ `. R* o; f! J" w; b: [* l; `8 设置扫描方向
. A5 s# ?: `( B( A3 |9 背光控制
8 w/ k- n  L8 x
LCD驱动框架
. E0 b( q/ j1 k
我们设计了如下的驱动框架: s) D+ v7 n- D
$ Y$ x- P- u& J7 w) b) C" i

% i. o8 B& `' \6 t- ~
设计思想
! I% F! G8 e- E& l5 B' @. c+ y

% l8 f0 o$ f0 [0 U$ r0 S6 n1 D( X

( p( l3 t* z& T1 中间显示驱动IC驱动程序提供统一接口,接口形式如前面说的_lcd_drv结构体。9 }. }. {/ G+ y; W! F8 S. J
2 各显示IC驱动根据设备参数,调用不同的接口驱动。例如TFT就用8080驱动,其他的都用SPI驱动。  h/ L/ z2 q% o( v
SPI驱动只有一份,用IO口控制的我们也做成模拟SPI。
/ E& m$ \$ f; }0 A3 LCD驱动层做LCD管理,例如完成TFT LCD的识别。并且将所有LCD接口封装为一套接口。
: ^- j# V2 N  \* r  q4 简易GUI层封装了一些显示函数,例如划线、字符显示。/ u( e# L$ K" y& F7 S, X4 ^% u
5 字体点阵模块提供点阵获取与处理接口。
由于嵌入式实际没那么复杂,在例程中我们将GUI跟LCD驱动层放到一起。
. ~& f4 s4 J8 P0 c5 Q1 V+ H1 lTFT LCD的两个驱动也放到一个文件,但是逻辑是分开的。: m* T( S5 F* ?( N7 N5 s3 [
OLED除初始化,其他接口跟COG LCD基本一样,因此这两个驱动也放在一个文件。
3 ^5 w, w! m9 N
代码分析7 w2 n8 u, H( e+ Z' `
代码分三层:
+ O6 N% u! B) d$ M' L3 y1. GUI和LCD驱动成
3 f3 k8 M$ v1 n. F: Mdev_lcd.c' _( X$ Z0 y) m
dev_lcd.h
0 z( o+ h7 R2 _5 M, o! C0 i+ c' b( o  d2. 显示驱动IC层
7 ~5 q7 l/ A: h! d- G7 X* p! _  D" N4 ldev_str7565.c & dev_str7565.h  h+ ?& h& Z/ z2 Q
dev_ILI9341.c & dev_ILI9341.h* e9 q! y" j: c
3. 接口层, S4 Y# Z9 u0 B+ U) c, w9 j
mcu_spi.c & mcu_spi.h/ k0 J* q6 T$ F6 e2 l! X& d
stm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h
0 \- }6 w8 w8 u) ]9 T$ }$ C3 G3 S
GUI和LCD层* D! f8 W1 Z# l" z: x9 m- A4 v* ^% p9 l" b
这层主要有3个功能

3 ~$ u" J% @" Z1 设备管理9 Q- Q6 O  P3 ]7 g1 t
首先定义了一堆LCD参数结构体,结构体包含ID,像素。
4 t4 l# {5 ^6 d" G: D- ]并且把这些结构体组合到一个list数组内。
/*    各种LCD的规格参数*/
* T9 Q6 l" D2 S2 V7 _+ l6 e6 U_lcd_pra LCD_IIL9341 =
4 [) h) O/ G- b4 u$ u9 H{        * T, X: x0 ?& r8 K
.id   = 0x9341,        
2 g3 U! `: K' J! ?. l.width = 240,   //LCD 宽度        
! w. [  y( O7 K2 ~. q8 \- Z7 Y.height = 320,  //LCD 高度) N& }4 Q  U- ?; f) V- S
};
/ x: x8 m% t/ c( H$ A7 R- W...
9 C+ X9 t" q" I0 K- j/*各种LCD列表*/  {/ s! g: G9 J8 V/ v& H- B
_lcd_pra *LcdPraList[5=            $ K% k) K# f/ c) W8 Z1 K: X: y2 a7 e
{               
5 i, \6 u$ h$ F- L: {6 Y&LCD_IIL9341,                       ) t9 g! m. n5 i. f6 V: P% k
&LCD_IIL9325,                + b  R1 U5 K+ t* k0 J
&LCD_R61408,               0 p3 y7 Y  E; J7 `
&LCD_Cog12864,                ) d! E. j1 `( d
&LCD_Oled12864,            4 F& |" D5 p! p% b8 X1 n2 }; ?
};
然后定义了所有驱动list数组,数组内容就是驱动,在对应的驱动文件内实现。
/*    所有驱动列表    驱动列表*/
! ]) E: d% W: [5 H_lcd_drv *LcdDrvList[ = {                      ]* z' F( T' a2 \
&TftLcdILI9341Drv,                    . I! ]) K- s$ S% \3 f6 j$ L
&TftLcdILI9325Drv,                   ( m' @% E# _( f' N
&CogLcdST7565Drv,                    
' T; {3 H/ [$ [. w6 o&OledLcdSSD1615rv,
定义了设备树,即是定义了系统有多少个LCD,接在哪个接口,什么驱动IC。4 i+ d9 {& d$ {1 l* t- e
如果是一个完整系统,可以做成一个类似LINUX的设备树。
/*设备树定义*/
3 G9 y* ?! R& b9 c- i2 n; O, C' _#define DEV_LCD_C 3//系统存在3个LCD设备
$ I4 t: S0 Q4 o+ R6 w" mLcdObj LcdObjList[DEV_LCD_C={    0 v7 r% D( q9 _9 u* V& e( x; \
{"oledlcd", LCD_BUS_VSPI, 0X1315},   
$ A  F+ d8 F% T% L6 e{"coglcd", LCD_BUS_SPI,  0X7565},    5 k7 M! g% h2 u5 t# G4 s
{"tftlcd", LCD_BUS_8080, NULL},, I+ h2 i, R1 H! C. Z5 t! R6 q$ Z
};
" M0 p% E" H$ u9 a) e+ `
2 接口封装
void dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir), E% D  P$ y: y& G' N
s32 dev_lcd_init(void)3 Q4 O, |$ G" E. W* r% J
DevLcd *dev_lcd_open(char *name)1 Q. L  V6 l6 P! g% U$ |
s32 dev_lcd_close(DevLcd *dev)
% s8 `9 f$ a+ n$ U5 `. is32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)9 h7 }3 H& u9 J* T" d) N: b
s32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)4 c) G1 p3 J; ]
s32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta)
8 r/ s/ Q3 M/ t# E& is32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color)
( _, W5 T: x7 _" }; j5 @* is32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)) d& V+ T) P6 K( W. l
s32 dev_lcd_backlight(DevLcd *lcd, u8 sta)
大部分接口都是对驱动IC接口的二次封装。有区别的是初始化和打开接口。
& A9 ^* V- j# C: C& d初始化,就是根据前面定义的设备树,寻找对应驱动,找到对应设备参数,并完成设备初始化。* R9 k/ x# L+ n' |* A% F
打开函数,根据传入的设备名称,查找设备,找到后返回设备句柄,后续的操作全部需要这个设备句柄。
3 简易GUI层  b& C& \/ x& d) W
目前最重要就是显示字符函数。
s32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)
其他划线画圆的函数目前只是测试,后续会完善。
驱动IC层
驱动IC层分两部分
+ Q, P( C7 x9 ?" X6 t1 封装LCD接口! V: h' \  U$ M% t$ t5 g# x
LCD有使用8080总线的,有使用SPI总线的,有使用VSPI总线的。这些总线的函数由单独文件实现。3 J0 h- _# m9 L2 ]8 C
但是,除了这些通信信号外,LCD还会有复位信号,命令数据线信号,背光信号等。
$ s6 }" y0 O1 ]我们通过函数封装,将这些信号跟通信接口一起封装为LCD通信总线。
% Q3 ?3 s6 W# _; Q' G4 O6 Z, R- @BUS_8080在dev_ILI9341.c文件中封装。
6 _1 k/ e& ]! `2 `* PBUS_LCD1和BUS_lcd2在dev_str7565.c 中封装。
  r. i; M6 m# w3 c# F& }8 K. N" u2 驱动实现
- y: A! G3 O/ T5 Q, A实现_lcd_drv驱动结构体。每个驱动都实现一个,某些驱动可以共用函数。
_lcd_drv CogLcdST7565Drv = {
- G( E2 _5 j0 H# c                           .id = 0X7565,   `5 ]( }2 _0 F: D! e9 E
                           .init = drv_ST7565_init,
+ i1 s% f( @5 T& F- Y                           .draw_point = drv_ST7565_drawpoint,$ k- e- W9 T; u' v, i* D1 r
                            .color_fill = drv_ST7565_color_fill,
  b- q3 r% |, I: T1 t! o! |                           .fill = drv_ST7565_fill, # I* D& r. S4 h( J
                           .onoff = drv_ST7565_display_onoff,* i& u  S- J  j' @
                            .prepare_display = drv_ST7565_prepare_display, 8 X& Z9 v2 ]7 T- J) ]% e
                           .set_dir = drv_ST7565_scan_dir,
! T1 D' f; `: K4 Q  B( M8 w                            .backlight = drv_ST7565_lcd_bl                           
4 q% s9 h7 V0 @};
* b: z) D# a0 D; j
$ L5 b$ Z) l$ h$ x
! `; K6 Y0 k% j9 C3 ?9 M2 A
接口层
5 s/ |  B0 p& K9 D
9 _- |/ B; Y" h1 s
8080层比较简单,用的是官方接口。
' A9 f0 u; x: \6 T4 [1 oSPI接口提供下面操作函数,可以操作SPI,也可以操作VSPI。
extern s32 mcu_spi_init(void);
3 \+ C  \  f7 }( M# b+ }6 o9 Aextern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);9 C) `' d7 a$ I' M" Y* J# n
extern s32 mcu_spi_close(SPI_DEV dev);6 B. W5 h3 \) `  X
extern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);, j0 G; ~+ \8 g% h. h. O7 j
extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);
至于SPI为什么这样写,会有一个单独文件说明。
总体流程
前面说的几个模块时如何联系在一起的呢?
7 c& |( q7 A3 [9 ]7 F! f请看下面结构体:
/*    初始化的时候会根据设备数定义,    ( N: N2 }; O1 r  R" [4 N+ [, A. X# _
并且匹配驱动跟参数,并初始化变量。    7 Q# J8 z  k+ n8 o( B$ X$ a0 A
打开的时候只是获取了一个指针*/, k: @" I; ^5 V4 T( x% h- I
struct _strDevLcd{
/ m% D; y; s! i6 Z5 k; x    s32 gd;//句柄,控制是否可以打开   
9 U  s+ G7 `( J2 F( ^. u* G  x2 T' }    LcdObj   *dev;    / w; ^* x1 @% ^2 c/ w2 O
/* LCD参数,固定,不可变*/6 f/ F( q0 E. S
    _lcd_pra *pra;    * y. @5 I- D4 S1 G9 T
/* LCD驱动 */ $ G( N5 c" e0 H3 N
   _lcd_drv *drv;   
" T( ~0 i7 g2 j+ Y% `5 H5 @3 U/*驱动需要的变量*/+ U) A& s' A3 k, p; \( ^
    u8  dir;    //横屏还是竖屏控制:0,竖屏;1,横屏
$ q3 x6 C: f( T* r! K    u8  scandir;//扫描方向
/ q, w5 |, d% [$ J$ m( G" L( r    u16 width;  //LCD 宽度
% L' |8 O, S  S# v) \. I% j   u16 height; //LCD 高度2 I% f% n/ v$ {* v
    void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存" {4 R$ R0 b$ I& f! U4 f5 J
};% @2 C# G+ ^5 r9 Z+ h- v& ~
每一个设备都会有一个这样的机构体,这个结构体在初始化LCD时初始化。
! R( b7 S4 _8 E5 p7 X- 成员dev指向设备树,从这个成员可以知道设备名称,挂在哪个LCD总线,设备ID。
typedef struct{' c* C! a% x9 y( ?6 ?' a
    char *name;//设备名字
3 W4 W  t9 N3 Y" \, [    LcdBusType bus;//挂在那条LCD总线上. _8 F, i; U7 o! X- e, H1 a2 y3 t$ S
    u16 id;
  Z8 I* l9 ?+ H7 Y- b% X}LcdObj;
-成员pra指向LCD参数,可以知道LCD的规格。
typedef struct{
  Y- x, w, U9 V. S) T, r   u16 id;   
: t5 V6 ?% N% Z# w( R3 ]; e% l   u16 width;  //LCD 宽度  竖屏   3 n' h; L0 H' h9 d9 E
   u16 height; //LCD 高度    竖屏4 b+ E. z" p; F% n* I" Y
}_lcd_pra;
-成员drv指向驱动,所有操作通过drv实现。
typedef struct  {
; |  H+ O7 n7 K: W8 h4 T    u16 id;
  [7 u. j3 F" n  V# o7 z6 y    s32 (*init)(DevLcd *lcd);4 t' Y# [1 M) O7 G% V$ A4 a
    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
+ _5 u! C+ v2 K' B. y    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);! w+ E( v8 O/ A: a& y
    s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);
7 a9 L# c, _0 F: @7 }9 d   s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);
* G& p. c5 j; p3 y# A0 N) m    s32 (*onoff)(DevLcd *lcd, u8 sta); ; S% L+ H4 c" C
   void (*set_dir)(DevLcd *lcd, u8 scan_dir);   Q2 Q# d9 i( g0 ~3 i  O) d
   void (*backlight)(DevLcd *lcd, u8 sta);
8 F: s/ c1 D9 T2 M7 O}_lcd_drv;
  • 成员dir、scandir、 width、 height是驱动要使用的通用变量。因为每个LCD都有一个结构体,一套驱动程序就能控制多个设备而互不干扰。
  • 成员pri是一个私有指针,某些驱动可能需要有些比较特殊的变量,就全部用这个指针记录,通常这个指针指向一个结构体,结构体由驱动定义,并且在设备初始化时申请变量空间。0 v  C+ Y$ X% b
    目前主要用于COG LCD跟OLED LCD显示缓存。
    $ G: U9 W. K" ]9 V/ T9 r2 G
整个LCD驱动,就通过这个结构体组合在一起。
1 初始化,根据设备树,找到驱动跟参数,然后初始化上面说的结构体。6 z- @( w: Q) g) }4 m! G* Z- ~4 h2 m
2 要使用LCD前,调用dev_lcd_open函数。打开成功就返回一个上面的结构体指针。8 h* P0 z! v6 U  C) V, Q2 G
3 显示字符,接口找到点阵后,通过上面结构体的drv,调用对应的驱动程序。
# d/ v7 c% T8 g& B: L, s. U* y" ~4 驱动程序根据这个结构体,决定操作哪个LCD总线,并且使用这个结构体的变量。
用法和好处
  • 好处1
    % @: I' F; L& s& B( `5 h3 d
请看测试程序
void dev_lcd_test(void)7 I, g. `1 S; i' Z6 Z
{8 }1 Q" i! l# C3 u: W, {& a8 N
    DevLcd *LcdCog;
; a  ?2 _" {+ _  b; n8 S$ r( N   DevLcd *LcdOled;, s! `) `* p0 i+ s% h3 x
    DevLcd *LcdTft;
7 Q' \7 G: U$ k  |: F: o9 A    /*  打开三个设备 */ 5 z) }' J# V: L: S: K
   LcdCog = dev_lcd_open("coglcd"); " ]& K' k& O, x& e$ j; W7 p
   if(LcdCog==NULL)
3 l8 F( m& Z7 Z. {8 T8 ~       uart_printf("open cog lcd err\r\n");0 v" v. P1 l2 ]2 Q
    LcdOled = dev_lcd_open("oledlcd");2 V; q# ~8 T; ~: u( `8 K5 b, g
    if(LcdOled==NULL) / `6 [8 o- Z8 f5 `8 ]- L
       uart_printf("open oled lcd err\r\n");
5 f! s: t) h/ x& R/ l    LcdTft = dev_lcd_open("tftlcd");
2 d$ _3 D; V  b6 T  ~    if(LcdTft==NULL)
- D9 e5 T# d; |! N5 I5 I( ~        uart_printf("open tft lcd err\r\n");9 G' p: g7 T$ r; ^3 W6 O) O, G2 u
    /*打开背光*/ ( i# F: T5 o$ \5 K0 C# Z7 u* Y5 f
   dev_lcd_backlight(LcdCog, 1);
7 T! }' J5 P4 n& ]; X   dev_lcd_backlight(LcdOled, 1);
/ z8 l! ]. V/ w* S/ v4 S7 {8 P: p    dev_lcd_backlight(LcdTft, 1);* Z% i+ D4 b- a- {

5 T8 E+ n- A7 @# @5 s! F; q    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
: @# e) k/ W' D. Q3 l6 u4 w   dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "这是oled lcd", BLACK);
, g; J5 u4 U6 v; r+ i2 i( u   dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);
2 u9 X# F3 U. G2 w   dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);
+ M' \6 Q" W6 u  P+ s, n1 M, l   dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
+ K3 [+ ]; R; Y; \' H0 Y   dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是cog lcd", BLACK);- b, z; Y* \$ }& ]5 V
    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);
' I( Q+ h$ Y% g* A' r% \4 [   dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK); $ q3 d& R2 \4 z3 B' J. z6 Y6 Y
   dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);
: W4 t2 H5 W4 a; d   dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是tft lcd", RED); $ v+ p( z( C' ?& t; T
   dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);# i8 {$ r/ D$ ]
    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "屋脊雀工作室", RED);    while(1);}
使用一个函数dev_lcd_open,可以打开3个LCD,获取LCD设备。
: s5 ^4 Y; T% U. G然后调用dev_lcd_put_string就可以在不同的LCD上显示。) t6 q4 n$ i' P
其他所有的gui操作接口都只有一个。
# Y' Z7 o* q5 ^* h2 _+ q这样的设计对于APP层来说,就很友好。- X9 J  T1 x0 N  P6 B1 e

2 F; W# O- ~4 H& t( e5 F# W- 好处2
现在的设备树是这样定义的
LcdObj LcdObjList[DEV_LCD_C={
2 _: W+ c; z) |. e   {"oledlcd", LCD_BUS_VSPI, 0X1315},
# z) x! q9 k) k! U2 P( M4 X* m- Z   {"coglcd", LCD_BUS_SPI,  0X7565}," c) W- e1 b* F- Y) _! @' j- p1 r
    {"tftlcd", LCD_BUS_8080, NULL},$ Q+ X7 a- ~- a% Z0 V+ w. G, ?3 D
};
某天,oled lcd要接到SPI上,只需要将设备树数组里面的参数改一下,就可以了,当然,在一个接口上不能接两个设备。
LcdObj LcdObjList[DEV_LCD_C={
: g) F" |! C. f    {"oledlcd", LCD_BUS_SPI, 0X1315}, - t/ M0 u# b0 |
   {"tftlcd", LCD_BUS_8080, NULL},# t( `3 f9 {: [, B3 N
};
. O/ ~4 `3 e/ R" _9 Q2 Y4 s* G# F: \+ z3 p( r; T. v" E: S

. n* x. \7 h; `1 g9 H# E3 a/ m# d字库
9 p: H: q9 d' h/ }0 X7 U+ h. B$ E
暂时不做细说,例程的字库放在SD卡中,各位移植的时候根据需要修改。: V% U3 ^: e4 I4 Z3 c
具体参考font.c
声明
1 代码请按照版权协议使用。" m1 t( m/ _. k* o: J
2 当前源码只是一个能用的设计,完整性与健壮性尚未测试。
' _1 W8 K# I* H; j+ @. b5 P3 后续会放到github,并且持续更新优化。
' i4 v$ d4 l- f" T( X4 最新消息请关注www.wujique.com.

/ [9 H9 Z' A! Y" Y2 ]4 K9 ^" l& J: Z

2 ^! O+ l% A' O5 q3 D# O/ q2 r
3 `3 Y" t! a' a9 B  n6 s( J- k1 I- p' T6 f
# t# h- a0 Y) ^
8 |0 u6 l/ o9 V. S/ x  J, Y

! O1 R5 F( h( U: F. i1 O# f
5 p) L+ N5 O, y1 d* _3 a  ?5 Y4 j6 J4 U0 q4 b) b5 k

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:519 y' V* D# I( T: u, i0 K8 l
楼主威武,不知道有没有对应的19264的

. C9 E6 z& g  G0 M; C9 V6 f) ]% yLCD框架已经更新,你可以到我的github上下载最新的。
3 F+ Z1 x! k0 ], l* |$ s" }+ y- j: c在最新的里面,增加一个新的LCD非常容易。# X0 [$ D; H. b; @# c/ Z
19264,过段时间我会添加进去的。
wujique 回答时间:2018-6-14 10:05:25
z258121131 发表于 2018-6-13 17:517 I7 W7 l9 O+ i( V5 Q1 v
楼主威武,不知道有没有对应的19264的

6 k6 X5 ?$ g* l' H3 f% I! Z7 p- ^19264应该有多种,不知道你说的是哪种驱动芯片的?
z258121131 回答时间:2018-6-15 15:46:00
huangxuejia-292 发表于 2018-6-14 10:05' d" M5 y0 {& A+ g
19264应该有多种,不知道你说的是哪种驱动芯片的?

" q7 F; [) \/ u$ G! kks0108/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" n, g3 w+ t0 W# I, z: G
云盘无法打开
& C# g0 E: {+ {" b0 J$ C5 c7 E
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

; Q/ W( j$ T- Q0 P1 x8 l学习
jxchen 回答时间:2019-11-9 11:15:28

, Y7 A' W! L; V9 l: d$ A云盘无法打开
12下一页

所属标签

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