电子产业一站式赋能平台

PCB联盟网

搜索
查看: 202|回复: 0
收起左侧

用模块化和面向对象的方式,编写单片机LCD驱动程序

[复制链接]

569

主题

569

帖子

4259

积分

四级会员

Rank: 4

积分
4259
发表于 2024-9-19 17:50:00 | 显示全部楼层 |阅读模式
我是老温,一名热爱学习的嵌入式工程师3 N* {% Z$ {& K* g% k1 |
关注我,一起变得更加优秀!( a% Q& o* S% ?' ]

7 n: W- t' U' u( w% v- I( J) u来源 | 屋脊雀5 g, K, z8 }- L% z) t5 Q$ r# ?  A% o
网络上配套STM32开发板有很多LCD例程,主要是TFT LCD跟OLED的。从这些例程,大家都能学会如何点亮一个LCD。但这代码都有下面这些问题:' y( m/ z9 C+ B2 x
  • 分层不清晰,通俗讲就是模块化太差。
  • 接口乱。只要接口不乱,分层就会好很多了。
  • 可移植性差。
  • 通用性差。为什么这样说呢?如果你已经了解了LCD的操作,请思考如下情景:; e( f. j" v4 W6 ^# N
    1、代码空间不够,只能保留9341的驱动,其他LCD驱动全部删除。能一键(一个宏定义)删除吗?删除后要改多少地方才能编译通过?; G' G% n2 J, M. D
    2、有一个新产品,收银设备。系统有两个LCD,都是OLED,驱动IC相同,但是一个是128x64,另一个是128x32像素,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。怎么办?这些例程代码要怎么改才能支持两个屏幕?全部代码复制粘贴然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。
    ! O3 S9 U2 [) y4 _# {3、一个OLED,原来接在这些IO,后来改到别的IO,容易改吗?
    8 S# \+ e) x- N/ c$ p' g4、原来只是支持中文,现在要卖到南美,要支持多米尼加语言,好改吗?9 H5 @4 K. [1 b4 m- J5 B2 u4 P
    LCD种类概述在讨论怎么写LCD驱动之前,我们先大概了解一下嵌入式常用LCD。概述一些跟驱动架构设计有关的概念,在此不对原理和细节做深入讨论,会有专门文章介绍,或者参考网络文档。
    0 F6 M# N7 K: M# \$ P) WTFT lcdTFT LCD,也就是我们常说的彩屏。通常像素较高,例如常见的2.8寸,320X240像素。4.0寸的,像素800X400。这些屏通常使用并口,也就是8080或6800接口(STM32 的FSMC接口);或者是RGB接口,STM32F429等芯片支持。其他例如手机上使用的有MIPI接口。! R* m# K; O) y7 L, m' }; G
    总之,接口种类很多。也有一些支持SPI接口的。除非是比较小的屏幕,否则不建议使用SPI接口,速度慢,刷屏闪屏。玩STM32常用的TFT lcd屏幕驱动IC通常有:ILI9341/ILI9325等。
    ; R* Z) c" t( x+ \8 Z1 a4 f- t' h0 etft lcd:( ~- g; L5 r5 b: u4 U: g
    3 U7 T/ y9 D, H) l- H7 k- j
    IPS:4 p/ p1 Q/ p3 H6 F

    4nqs423sgba64035223722.jpg

    4nqs423sgba64035223722.jpg
    * I/ j/ F: Y' t9 U1 V6 O
    COG lcd很多人可能不知道COG LCD是什么,我觉得跟现在开发板销售方向有关系,大家都出大屏,玩酷炫界面,对于更深的技术,例如软件架构设计,都不涉及。使用单片机的产品,COG LCD其实占比非常大。COG是Chip On Glass的缩写,就是驱动芯片直接绑定在玻璃上,透明的。实物像下图:
    " ?& f; Z+ [' e9 e( k

    r54yd031wzu64035223822.jpg

    r54yd031wzu64035223822.jpg

    # u, S/ m8 b8 c+ F. I  v) D# y这种LCD通常像素不高,常用的有128X64,128X32。一般只支持黑白显示,也有灰度屏。
    9 b: Y) _6 Y! B7 z. i7 k接口通常是SPI,I2C。也有号称支持8位并口的,不过基本不会用,3根IO能解决的问题,没必要用8根吧?常用的驱动IC:STR7565。" M# B- l; j" \$ Z' e/ `
    OLED lcd买过开发板的应该基本用过。新技术,大家都感觉高档,在手环等产品常用。OLED目前屏幕较小,大一点的都很贵。在控制上跟COG LCD类似,区别是两者的显示方式不一样。从我们程序角度来看,最大的差别就是,OLED LCD,不用控制背光。。。。。实物如下图:- i! j+ D+ d1 f4 e0 P
    2 }5 q3 w6 r0 t( ~7 ~4 w3 I/ D3 q
    常见的是SPI跟I2C接口。常见驱动IC:SSD1615。& f6 b1 @9 x3 r
    硬件场景接下来的讨论,都基于以下硬件信息:
    9 b6 N, F. `) M- ?9 n8 |# `! d1、有一个TFT屏幕,接在硬件的FSMC接口,什么型号屏幕?不知道。
      ?& J/ Y- M; N; I( _! y1 j2、有一个COG lcd,接在几根普通IO口上,驱动IC是STR7565,128X32像素。
    & p5 \" X# J! y/ z6 e( T! a3、有一个COG LCD,接在硬件SPI3跟几根IO口上,驱动IC是STR7565,128x64像素。; {+ d' I7 i( i$ T. ~, L
    4、有一个OLED LCD,接在SPI3上,使用CS2控制片选,驱动IC是SSD1315。% O* @) b( z: [! o
    + O3 v. S# Q# P0 o0 h7 g) n+ V1 r
    预备知识在进入讨论之前,我们先大概说一下下面几个概念,对于这些概念,如果你想深入了解,请GOOGLE。0 I" [/ J9 N, J* h7 K3 R
    面向对象面向对象,是编程界的一个概念。什么叫面向对象呢?编程有两种要素:程序(方法),数据(属性)。例如:一个LED,我们可以点亮或者熄灭它,这叫方法。LED什么状态?亮还是灭?这就是属性。我们通常这样编程:5 \2 H* m3 |. s
    u8 ledsta = 0;+ g0 {" E2 |3 v: G* c* Z: ]
    void ledset(u8 sta)) E. Q# C' ^; y" I6 v% G/ M
    {( T9 h* Q" x7 }# D, Y; u
    }! O/ n) [" O7 a: Z/ [% t0 p
    这样的编程有一个问题,假如我们有10个这样的LED,怎么写?这时我们可以引入面向对象编程,将每一个LED封装为一个对象。可以这样做:
    ! j( r$ w9 @: v3 a6 P7 F5 Y/*
    6 Z' Q! f1 m  t0 D3 Z# l定义一个结构体,将LED这个对象的属性跟方法封装。
    ! e0 d3 n; J/ ^0 `) _" c& G这个结构体就是一个对象。) b6 \  V- c( r: n
    但是这个不是一个真实的存在,而是一个对象的抽象。" @' G9 B1 ^1 I! l- B) u4 w0 w
    */
    - V6 g. j# A: t$ w$ M5 Ltypedef struct{
    1 @7 g* g: j% l! }! d' I    u8 sta;: x; b5 p" L  m/ g9 H
        void (*setsta)(u8 sta);/ k2 ^* c% r$ {, i
    }LedObj;
    ) g! H, H/ x7 t/*  声明一个LED对象,名称叫做LED1,并且实现它的方法drv_led1_setsta*/6 `7 m' _6 }! Q  h
    void drv_led1_setsta(u8 sta)
    ) O9 I2 V3 B9 _6 y0 E5 p! }/ x{
    " a0 X+ S4 @1 @}
    + T, n! {- F9 b( u8 o/ ULedObj LED1={- R. ^. b* d; r& H* n2 R
            .sta = 0,! t) c/ `4 l, ]9 n( J
            .setsta = drv_led1_setsta,
    9 M, O$ o$ T/ q$ c! q    };
    ) g. }6 k& u5 w9 _  T/*  声明一个LED对象,名称叫做LED2,并且实现它的方法drv_led2_setsta*/
    " Z' `% N, l' P7 ~void drv_led2_setsta(u8 sta)9 B% x6 C- G- H  f' B
    {
    0 @. Q" w7 n( v9 ?}" h& S" R1 H  ~, a, m& r2 ~5 ^
    LedObj LED2={& L# ^0 T4 m# M7 J; b
            .sta = 0,' }" D" [# B0 T! d1 i
            .setsta = drv_led2_setsta,9 w' f% e/ ?2 B7 {! U- X
        };" ~1 J6 U/ E5 t- T' K# p# l
        ) v' G* F; ^$ T& T& T
    /*  操作LED的函数,参数指定哪个led*/2 ]5 G( Z( X7 \; y* `) l* r* W
    void ledset(LedObj *led, u8 sta)4 n& d0 b) h3 q( i  `
    {
    $ d; d; k: H; l8 x9 C    led->setsta(sta);/ F" \/ i  H# q+ C; m
    }
    9 W' |9 H8 m4 g是的,在C语言中,实现面向对象的手段就是结构体的使用。上面的代码,对于API来说,就很友好了。操作所有LED,使用同一个接口,只需告诉接口哪个LED。大家想想,前面说的LCD硬件场景。4个LCD,如果不面向对象,「显示汉字的接口是不是要实现4个」?每个屏幕一个?
    9 B8 Q7 E0 U' o; b驱动与设备分离如果要深入了解驱动与设备分离,请看LINUX驱动的书籍。, n, I) }4 c- g
    什么是设备?我认为的设备就是「属性」,就是「参数」,就是「驱动程序要用到的数据和硬件接口信息」。那么驱动就是「控制这些数据和接口的代码过程」
    ) @/ _; z7 T+ _) t. q1 Q通常来说,如果LCD的驱动IC相同,就用相同的驱动。有些不同的IC也可以用相同的,例如SSD1315跟STR7565,除了初始化,其他都可以用相同的驱动。例如一个COG lcd:; j& h! T4 N; ~) ]
    ?驱动IC是STR7565 128 * 64 像素用SPI3背光用PF5 ,命令线用PF4 ,复位脚用PF3
    5 S& N( N( U* R+ g& a5 x6 p?
    上面所有的信息综合,就是一个设备。驱动就是STR7565的驱动代码。6 C) |1 F; I! E9 V
    为什么要驱动跟设备分离,因为要解决下面问题:. a$ n% S1 k6 @
    ?有一个新产品,收银设备。系统有两个LCD,都是OLED,驱动IC相同,但是一个是128x64,另一个是128x32像素,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。+ S% ?' }- v/ C# W% U5 ?6 j( H
    ?
    这个问题,「两个设备用同一套程序控制」才是最好的解决办法。驱动与设备分离的手段:
    % f; o5 U1 m. v. T% h, _?在驱动程序接口函数的参数中增加设备参数,驱动用到的所有资源从设备参数传入。- V0 z  Q5 A3 z5 {# Q' A9 s1 y
    ?
    驱动如何跟设备绑定呢?通过设备的驱动IC型号。# E5 T3 C4 U7 X
    模块化我认为模块化就是将一段程序封装,提供稳定的接口给不同的驱动使用。不模块化就是,在不同的驱动中都实现这段程序。例如字库处理,在显示汉字的时候,我们要找点阵,在打印机打印汉字的时候,我们也要找点阵,你觉得程序要怎么写?把点阵处理做成一个模块,就是模块化。非模块化的典型特征就是「一根线串到底,没有任何层次感」
    ( Q: d! |7 k4 J% i8 G& zLCD到底是什么前面我们说了面向对象,现在要对LCD进行抽象,得出一个对象,就需要知道LCD到底是什么。问自己下面几个问题:
    - Q2 l2 e7 l2 ~& j% h0 t9 V
  • LCD能做什么?
  • 要LCD做什么?
  • 谁想要LCD做什么?刚刚接触嵌入式的朋友可能不是很了解,可能会想不通。我们模拟一下LCD的功能操作数据流。APP想要在LCD上显示 一个汉字。
    6 J- c+ N; {. l6 @( x' P1、首先,需要一个显示汉字的接口,APP调用这个接口就可以显示汉字,假设接口叫做lcd_display_hz。
    3 X% y9 ~3 l. |4 H( {2、汉字从哪来?从点阵字库来,所以在lcd_display_hz函数内就要调用一个叫做find_font的函数获取点阵。
    8 c: X7 g) i* A+ n3、获取点阵后要将点阵显示到LCD上,那么我们调用一个ILL9341_dis的接口,将点阵刷新到驱动IC型号为ILI9341的LCD上。
    / ]- `; p: L9 l1 F# H( a: `& h4、ILI9341_dis怎么将点阵显示上去?调用一个8080_WRITE的接口。
      F6 V9 l- J8 u0 L好的,这个就是大概过程,我们从这个过程去抽象LCD功能接口。汉字跟LCD对象有关吗?无关。在LCD眼里,无论汉字还是图片,都是一个个点。那么前面问题的答案就是:
    0 a* r8 [( k' ]
  • LCD可以一个点一个点显示内容。
  • 要LCD显示汉字或图片-----就是显示一堆点
  • APP想要LCD显示图片或文字。结论就是:所有LCD对象的功能就是显示点。「那么驱动只要提供显示点的接口就可以了,显示一个点,显示一片点。」 抽象接口如下:
      g( t* `& c; j; }) S3 |/*
    4 Q* J8 i+ D/ r; f: T    LCD驱动定义
    3 d& f, _; S' P' T0 [- {*/9 f; q( a9 _1 \" P, T
    typedef struct  , p6 E% a9 B5 m
    {
    2 U: T2 }/ H" h4 J# s8 P    u16 id;
    1 u5 @$ P0 z( x4 B* w    s32 (*init)(DevLcd *lcd);
    9 q# h" P( R& l1 f# p0 i- g; _    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
    / B7 }9 C0 E$ H! y& ~' ]    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);5 {# z( O! E+ @6 q, r: E
        s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);6 y9 }7 ?- d6 T$ K7 X
        s32 (*onoff)(DevLcd *lcd, u8 sta);
    ) V/ I) S2 a. Z2 v( ]! U+ f    s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);
    2 L* a; |3 {( B    void (*set_dir)(DevLcd *lcd, u8 scan_dir);7 M3 y1 E3 g2 ?  `
        void (*backlight)(DevLcd *lcd, u8 sta);
    3 ?; O5 @( S- w  P}_lcd_drv;* ?3 U1 @% N' b' i, y5 M9 W& p5 L3 M- e
    上面的接口,也就是对应的驱动,包含了一个驱动id号。. H6 P5 e6 x1 @. [1 R0 T+ ^" Q2 L
  • id,驱动型号
  • 初始化
  • 画点
  • 将一片区域的点显示某种颜色
  • 将一片区域的点显示某些颜色
  • 显示开关
  • 准备刷新区域(主要彩屏直接DMA刷屏使用)
  • 设置扫描方向
  • 背光控制显示字符,划线等功能,不属于LCD驱动。应该归类到GUI层。( U) q0 P0 a5 j7 F( ^! y
    LCD驱动框架我们设计了如下的驱动框架:$ X7 @$ Y9 L% G8 ]7 `* ^7 s# o0 p

    $ C& p" W% g/ s& L0 Q$ O设计思路:
    $ N' t  D8 C" H5 T1、中间显示驱动IC驱动程序提供统一接口,接口形式如前面说的_lcd_drv结构体。! L. [* ]1 v9 ^6 n! @4 [: ^2 {
    2、各显示IC驱动根据设备参数,调用不同的接口驱动。例如TFT就用8080驱动,其他的都用SPI驱动。SPI驱动只有一份,用IO口控制的我们也做成模拟SPI。- ^* E5 r0 X7 v+ n1 h/ ~3 L* J
    3、LCD驱动层做LCD管理,例如完成TFT LCD的识别。并且将所有LCD接口封装为一套接口。8 ]5 V7 p3 v* R" |1 G, g$ S: p2 W
    4、简易GUI层封装了一些显示函数,例如划线、字符显示。
    ) X: o1 C3 K7 u9 w) Z( i5、字体点阵模块提供点阵获取与处理接口。
    9 |. {0 i0 w( @由于实际没那么复杂,在例程中我们将GUI跟LCD驱动层放到一起。TFT LCD的两个驱动也放到一个文件,但是逻辑是分开的。OLED除初始化,其他接口跟COG LCD基本一样,因此这两个驱动也放在一个文件。; Z0 U- p8 j+ W6 d( z7 M, Y9 B
    代码分析代码分三层:  W7 I! m3 J! \1 }8 R
    1、GUI和LCD驱动层 dev_lcd.c dev_lcd.h
    / y5 P) ^+ Q1 I9 I( t. _4 z2、显示驱动IC层 dev_str7565.c & dev_str7565.h dev_ILI9341.c & dev_ILI9341.h
    4 e7 {. h% L6 ^  P/ W3、接口层 mcu_spi.c & mcu_spi.h stm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h
    2 m' \7 S. `3 x0 c$ SGUI和LCD层这层主要有3个功能 :7 j1 R  K2 L  b, K
    「1、设备管理」' V9 _. r2 w3 {" @
    首先定义了一堆LCD参数结构体,结构体包含ID,像素。并且把这些结构体组合到一个list数组内。
    5 n9 L2 k/ [# A8 ^0 `/*  各种LCD的规格参数*/* j* h' ]+ ~) K2 ~2 i- ^- ?; `# ?# \
    _lcd_pra LCD_IIL9341 ={
    $ E) J: B& X4 M( ^4 j8 w7 b        .id   = 0x9341,1 j" q: z$ t; t7 \7 J( H
            .width = 240,   //LCD 宽度
    + T" P/ Q, q1 e: @3 R        .height = 320,  //LCD 高度
    , s3 W' p6 M& X};$ s- q& [5 t) U6 f
    ...
    7 Y% k& H4 ]5 N  q( x/*各种LCD列表*/
    9 b/ h0 ^9 `9 g& s( Z+ R_lcd_pra *LcdPraList[5]=
    + l. E7 g' t/ k6 O# K; c; z, `. O/ X            {
    % w  I( o) a" H                &LCD_IIL9341,       $ ?: I2 W! S; `) h0 n
                    &LCD_IIL9325,- \# U6 H7 \* q* k0 f6 y* W$ G
                    &LCD_R61408,
    % Y4 t' Q: ]$ q5 i5 N& ]* }* _; w                &LCD_Cog12864,
    . T' V0 w/ i' O% \" K7 Q                &LCD_Oled12864,9 s$ ^/ B/ l4 |" a; e7 s; H1 p1 ]: u
                };: d  ]! C+ a7 _% q; ^! B( v
    然后定义了所有驱动list数组,数组内容就是驱动,在对应的驱动文件内实现。) S" n" m0 R( A0 o7 j
    /*  所有驱动列表
    6 K1 K; M. I3 B5 m# n& I: Y    驱动列表*/
    1 V4 X/ ~2 W% h$ w_lcd_drv *LcdDrvList[] = {! x( ~, I$ L+ O( e$ {8 K- l  T! z
                        &TftLcdILI9341Drv,
    $ g4 `% h" u1 w% U- R  Z7 T- @                    &TftLcdILI9325Drv,) ?* J2 f$ _% M6 \' U! K7 G
                        &CogLcdST7565Drv,! c% u) ^4 j9 k: K* }; Y
                        &OledLcdSSD1615rv,7 {% Z  h' g( z
    定义了设备树,即是定义了系统有多少个LCD,接在哪个接口,什么驱动IC。如果是一个完整系统,可以做成一个类似LINUX的设备树。2 a% X9 n/ R: b) q) g4 a3 \
    /*设备树定义*/" ]- ]  ?* }& s0 ~, ?# u% F0 r
    #define DEV_LCD_C 3//系统存在3个LCD设备
    / ]5 v1 G' \6 V3 H$ p+ N8 xLcdObj LcdObjList[DEV_LCD_C]=* e  P+ _/ K5 T) y9 L; ^& ^
    {
    , U  y1 X( Y+ T    {"oledlcd", LCD_BUS_VSPI, 0X1315},
    ( j' J7 b2 q) t9 r6 W5 v1 Q    {"coglcd", LCD_BUS_SPI,  0X7565},
    7 ]3 C" c, F2 ?$ `    {"tftlcd", LCD_BUS_8080, NULL},
    ! p6 j& ]* S3 _5 W6 g};
    2 i: G3 U8 }, Q! A「2 、接口封装」
    , Y1 ]4 e, j0 F7 ^3 Gvoid dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir)
    8 I* `' n" G8 Q$ fs32 dev_lcd_init(void)
    7 |2 V& |  ^$ ~7 Q+ Z3 F) b6 ~DevLcd *dev_lcd_open(char *name)
      u9 O( e1 d# x, j  [; C/ r3 vs32 dev_lcd_close(DevLcd *dev)
    2 P) }- J7 l5 _) B* f* as32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)
    6 |/ E/ Z+ K& ^6 p5 cs32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)1 e/ _0 P2 E5 o- A3 M" ^
    s32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta)
    : D: [& Q/ H( v" W7 Q6 Us32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color)
    : ]/ F2 M, z+ f* _) gs32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)* ~# j. n! @. m2 Q
    s32 dev_lcd_backlight(DevLcd *lcd, u8 sta)
    4 N1 ]1 x/ i' X$ Y大部分接口都是对驱动IC接口的二次封装。有区别的是初始化和打开接口。初始化,就是根据前面定义的设备树,寻找对应驱动,找到对应设备参数,并完成设备初始化。打开函数,根据传入的设备名称,查找设备,找到后返回设备句柄,后续的操作全部需要这个设备句柄。$ y' ]( p- _% S  h
    「3 、简易GUI层」
    + l: i$ I2 R9 n" q( c* F, v目前最重要就是显示字符函数。
    - w4 @: Q: u. }. Gs32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)
    % \( o( F& g9 X/ l! P其他划线画圆的函数目前只是测试,后续会完善。
    - A/ E3 I1 G1 l3 w' @% H/ y0 R* h驱动IC层驱动IC层分两部分:/ w1 R& l! h" K; o0 j$ t- s
    「1 、封装LCD接口」7 a" i+ k: |$ F
    LCD有使用8080总线的,有使用SPI总线的,有使用VSPI总线的。这些总线的函数由单独文件实现。但是,除了这些通信信号外,LCD还会有复位信号,命令数据线信号,背光信号等。我们通过函数封装,将这些信号跟通信接口一起封装为「LCD通信总线」, 也就是buslcd。BUS_8080在dev_ILI9341.c文件中封装。BUS_LCD1和BUS_lcd2在dev_str7565.c 中封装。
    ! {! n! S# p8 ]) y「2 驱动实现」: M: a& c3 F% a8 r1 Y2 B
    实现_lcd_drv驱动结构体。每个驱动都实现一个,某些驱动可以共用函数。
    - q% `: _. M' z' {2 m) `_lcd_drv CogLcdST7565Drv = {, j8 |7 D8 e. T2 d8 Y( m0 w) G
                                .id = 0X7565,+ k: X) V( f- {% j, S
                                .init = drv_ST7565_init,$ I7 q- [" d- k
                                .draw_point = drv_ST7565_drawpoint,
    / H* x4 U: P  m# W                            .color_fill = drv_ST7565_color_fill,2 ]& i. O% N' u! }0 {
                                .fill = drv_ST7565_fill,
    ' i# e: e) q) f& ^: ^5 J! v                            .onoff = drv_ST7565_display_onoff,
    % l( O) X0 y& Y2 D# ^. a                            .prepare_display = drv_ST7565_prepare_display,* k1 Z5 ^1 |0 e" H$ \, o+ Q$ N
                                .set_dir = drv_ST7565_scan_dir,
    ! q- N$ e# b9 C                            .backlight = drv_ST7565_lcd_bl0 l* t1 T2 S$ Z) E
                                };6 G% Q7 c4 _  B& U
    接口层8080层比较简单,用的是官方接口。SPI接口提供下面操作函数,可以操作SPI,也可以操作VSPI。
    6 ^, E6 B0 G8 l- n) |/ J" Hextern s32 mcu_spi_init(void);' P- c/ [( w: j0 N$ N4 `3 q6 C. ^
    extern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);+ l) `1 l- w8 B
    extern s32 mcu_spi_close(SPI_DEV dev);
    ) X' r' t$ d2 ^, @; Kextern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);  i( D& Z" j% _
    extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);9 o) S/ w8 k5 [6 f. k
    至于SPI为什么这样写,会有一个单独文件说明。
    ) _- m% P7 T9 R( _0 T$ w2 S0 {& h+ X总体流程前面说的几个模块时如何联系在一起的呢?请看下面结构体:8 }7 {5 j. K: f3 c: r/ H
    /*  初始化的时候会根据设备数定义,
    ; q! g" w  R/ n7 [, Z: l    并且匹配驱动跟参数,并初始化变量。8 [+ V7 ?7 ]! j* A
        打开的时候只是获取了一个指针 */, p& B9 a0 G! L2 ~! G" a
    struct _strDevLcd/ z( t% h% ]$ K# E
    {
    # s! l1 g7 s4 B! u/ J& y: R; n    s32 gd;//句柄,控制是否可以打开
    8 K8 I2 Q9 L5 P    LcdObj   *dev;
    * e8 p0 x* S0 v2 e  R2 ?, X! B* N9 \    /* LCD参数,固定,不可变*/
    # d: r, U$ x. M: ?    _lcd_pra *pra;/ `# W* R; H/ }$ r  U1 N
        /* LCD驱动 */+ L6 n; \& C* B' D3 p7 W7 \
        _lcd_drv *drv;+ ^9 A9 Q6 G! `( a! V9 ?
        /*驱动需要的变量*/9 |+ u( l" `1 O" d
        u8  dir;    //横屏还是竖屏控制:0,竖屏;1,横屏。
    6 F. b% g& T* }* Q, |, Z    u8  scandir;//扫描方向  X, u3 k* }9 V4 ^% U: H
        u16 width;  //LCD 宽度/ y; R, w, c$ z8 [
        u16 height; //LCD 高度+ O+ ]8 l' l8 K: V
        void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存' g$ q' M7 Q  W3 _' k% F/ n. ^/ Q, O
    };1 q7 I# R8 ]7 s; J* q$ i* M
    每一个设备都会有一个这样的结构体,这个结构体在初始化LCD时初始化。
    # I) f! E' G: q9 ]- M( }1 k. w
  • 成员dev指向设备树,从这个成员可以知道设备名称,挂在哪个LCD总线,设备ID。typedef struct- C; V( F6 A5 z  \5 J7 d' ~
    {9 T+ f3 r, `0 q2 W+ Q
        char *name;//设备名字
    / u1 M1 _( }7 s7 Q. ~    LcdBusType bus;//挂在那条LCD总线上
    6 e) y7 {# Q+ }. }( i+ R$ F    u16 id;# a, j/ E8 m0 X, [0 t+ S
    }LcdObj;
    8 v- U- e0 d/ U5 X
  • 成员pra指向LCD参数,可以知道LCD的规格。typedef struct7 F, ], f; H% o& d5 F
    {
    " p% O  z# ]1 V% Y    u16 id;6 v1 w; c  @5 h% R0 o& p- ]. S  Q
        u16 width;  //LCD 宽度  竖屏1 m; }) t/ x, B
        u16 height; //LCD 高度    竖屏
    - J9 h6 [/ l- j" w6 \9 }& h; ]}_lcd_pra;6 B: f6 n. n3 c+ m; h5 Z
  • 成员drv指向驱动,所有操作通过drv实现。typedef struct  " k2 [# s/ r+ C5 x' `" L
    {8 {+ K- ^2 J8 y8 ~$ M% c
        u16 id;
      M+ X8 z" ^0 G1 S- g0 z    s32 (*init)(DevLcd *lcd);
    ' k0 s6 F/ m( i9 U) V& X    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);2 V9 g: ?( J# y2 _7 q
        s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);
    ; N# q% |) d3 t$ ~9 t! X& E7 i. |    s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);0 I( J2 ]; s$ o1 R4 r- z
        s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);+ g8 G% ~" S7 d; s8 z
        s32 (*onoff)(DevLcd *lcd, u8 sta);
    5 |3 _- J( l& @& e6 ^    void (*set_dir)(DevLcd *lcd, u8 scan_dir);
    ) @# Q* X" C+ w1 X; x  g/ O' l    void (*backlight)(DevLcd *lcd, u8 sta);4 ^' r) o# A5 ]$ y- g, c
    }_lcd_drv;, ^# W* K& o8 q, A
  • 成员dir、scandir、 width、 height是驱动要使用的通用变量。因为每个LCD都有一个结构体,一套驱动程序就能控制多个设备而互不干扰。
  • 成员pri是一个私有指针,某些驱动可能需要有些比较特殊的变量,就全部用这个指针记录,通常这个指针指向一个结构体,结构体由驱动定义,并且在设备初始化时申请变量空间。目前主要用于COG LCD跟OLED LCD显示缓存。整个LCD驱动,就通过这个结构体组合在一起。* H, Q( M$ O& a' G
    1、初始化,根据设备树,找到驱动跟参数,然后初始化上面说的结构体。
    ! Y9 T; A7 G! B  P# I, G2、要使用LCD前,调用dev_lcd_open函数。打开成功就返回一个上面的结构体指针。$ h1 k6 |! Q- V  h5 W
    3、显示字符,接口找到点阵后,通过上面结构体的drv,调用对应的驱动程序。  z3 R% M' N  Y. r) I# v1 X
    4、驱动程序根据这个结构体,决定操作哪个LCD总线,并且使用这个结构体的变量。
    ( Z* |) n  x% ?5 G0 S+ p" W用法和好处
  • 好处1请看测试程序
    6 o. s. a8 u. xvoid dev_lcd_test(void)" w' P/ x+ [0 Y' w) A
    {. b1 ?: {( B& A  F5 r) L$ Q
        DevLcd *LcdCog;
    . @( w" B1 T# ~' \* L    DevLcd *LcdOled;, s8 G) ?, ]. W" I
        DevLcd *LcdTft;$ y6 b" y+ q7 u
        /*  打开三个设备 */. T' q; y8 ^5 ^( e+ U+ M
        LcdCog = dev_lcd_open("coglcd");% Q' |0 M$ T! I/ L2 g; G. D- g$ O3 H
        if(LcdCog==NULL)1 r& }' a# V5 Z7 A; i  p
            uart_printf("open cog lcd err\r& R5 P' q% N. {+ z
    ");
    ! s: e% f" n* I5 V( h" D    LcdOled = dev_lcd_open("oledlcd");
    # p; \( P. l: b# D: s; n4 f    if(LcdOled==NULL)  I) ^+ R8 O+ m( O  w; j: s6 n
            uart_printf("open oled lcd err\r
    ; T9 E2 _: ?. I) [");
    ! W1 I4 |0 L. W/ Y. R0 ^. j* Z    LcdTft = dev_lcd_open("tftlcd");
    - a7 f( R: p5 O! O    if(LcdTft==NULL)
    2 v# S$ S5 W3 d5 _        uart_printf("open tft lcd err\r4 f& W  |) g: J1 R6 A
    ");( a0 D6 x2 ~# _# }7 j! F
        /*打开背光*/, a3 T8 h9 u$ j" B  U  H7 U' Z9 R- x
        dev_lcd_backlight(LcdCog, 1);  h- ~! N/ T# Q4 J5 g8 O
        dev_lcd_backlight(LcdOled, 1);
    0 k. b( m/ k- {    dev_lcd_backlight(LcdTft, 1);
    8 t! z/ c* i+ @% T6 C2 ]    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
    * W, j! X" }: ?9 }1 Y    dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "这是oled lcd", BLACK);
    $ c* n: j1 U1 Z9 g6 b. r    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);6 g2 b$ E+ N# e6 V* F
        dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);
    6 y4 U# V  ?" I# a. [2 u    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
    & I* Y* _1 y* L! q9 `6 f6 v    dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是cog lcd", BLACK);! Q0 T" V8 q2 F" f% o# n
        dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);
    2 z/ r! p; g( Y    dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);- y1 j3 V2 C0 M- @
        dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);& l# I: S+ U% t$ Z: t& Y
        dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是tft lcd", RED);  W7 o2 Q# [2 S) S$ d
        dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);! c+ \  r2 P* ~8 o, N: y
        dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "屋脊雀工作室", RED);
    / v' K$ `0 m; V; b' ^    while(1);2 Q8 b+ n1 O8 B: f; S1 k0 [
    }
    ( K. Y3 z9 |/ J3 {7 ~2 t( s使用一个函数dev_lcd_open,可以打开3个LCD,获取LCD设备。然后调用dev_lcd_put_string就可以在不同的LCD上显示。其他所有的gui操作接口都只有一个。这样的设计对于APP层来说,就很友好。显示效果:; q; u; `7 H/ x0 H/ b8 t* [2 o
    - D" A) w" s9 ?0 D1 ?. r
  • 好处2现在的设备树是这样定义的
    4 H( @) A1 X" G1 R2 {# X4 rLcdObj LcdObjList[DEV_LCD_C]=
    " ]# D; O- M9 C7 T8 r{6 q3 N7 n# R7 F2 r7 ]
        {"oledlcd", LCD_BUS_VSPI, 0X1315},9 b5 D' r& L% y, }, g) P  z
        {"coglcd", LCD_BUS_SPI,  0X7565},
    6 _, ^; p" V5 P+ y* l, V: U) W    {"tftlcd", LCD_BUS_8080, NULL},
    : r& [7 m5 h8 {# g9 C, _};7 e0 a; a7 y" u3 e; y% W
    某天,oled lcd要接到SPI上,只需要将设备树数组里面的参数改一下,就可以了,当然,在一个接口上不能接两个设备。
    - z2 u+ \4 H, CLcdObj LcdObjList[DEV_LCD_C]=
    # T3 O+ n' s% i) p& l/ r{
    ; x6 a; s2 [  @" Q2 R- J+ K7 O    {"oledlcd", LCD_BUS_SPI, 0X1315},3 v5 b8 h1 x8 p! ^( c" R
        {"tftlcd", LCD_BUS_8080, NULL},
    2 O3 ]! J1 W, M) s% g) X};0 ]9 O. \. v/ x( h! _
    字库暂时不做细说,例程的字库放在SD卡中,各位移植的时候根据需要修改。具体参考font.c。; o. S2 Z( u$ N2 I
    声明代码请按照版权协议使用。当前源码只是一个能用的设计,完整性与健壮性尚未测试。后续会放到github,并且持续更新优化。最新消息请关注www.wujique.com。
    6 s- D- p8 v7 g4 y1 j: m-END-% v  V$ t6 e3 p6 w/ j2 v
    往期推荐:点击图片即可跳转阅读
    * [  a1 U: f/ R$ H& g                                                        ' X$ X. U  A. h8 C/ O6 f$ x
                                                                    4 a9 E" F$ h" E. r$ \
                                                                           
    & m8 ~! w  \/ z* X0 G/ I5 N                                                                                ; U) d% _& a% x1 v) C1 f( ?
    8 A7 B  N8 S( P. M- Z& U# Z
                                                                                   
    2 w# _# J% F' G$ E5 w                                                                                        浅谈为何不该入行嵌入式技术开发; f  S- F0 E+ V4 M- Q; L
                                                                                    ! e: z; r* ?! M  C, }" x6 G
                                                                            & B) V7 y9 E% T
                                                                    $ H* D  j; S9 M$ z4 v! u1 k* h, [
                                                           
    " o; ~6 s+ t# ~# w( Z( a                                                1 |, K" g" ?; j( L6 D* ]9 X  U
    2 E* v  ]1 Z; _1 m( _
                                                           
    1 g5 ]. @' t6 z5 `4 |0 u. _                                                                * Q# W1 G4 o. K
                                                                           
    . }; B4 h' x8 ?  D, o7 P. R4 u% j8 _                                                                               
    7 Q) z9 O5 a8 _, w% A) i5 F! k% K& n' Y# b
                                                                                    * U5 y' ]# y- p, S3 U! Q$ U
                                                                                            在深圳搞嵌入式,从来没让人失望过!
    5 G; b; o6 ]+ i6 e: j7 m                                                                               
    + y/ c) h$ a3 l2 ]                                                                       
    + A1 Q2 o) h- e. I# y                                                                2 b7 \$ |1 O( ^# L' b; ~
                                                           
    : [- a% x$ Y% l2 f                                               
    # _  H5 v. u" }4 ?+ n% c+ X' A7 X) c
                                                            7 ~' i" X/ h. Z: c( a, \( P
                                                                    : E# b  p$ Y* t: l: _+ ?$ C
                                                                            9 t3 {: I; @' W6 J1 B& Y
                                                                                    1 G; s0 `9 b- \9 x3 `7 C( Z$ X
    3 T# k; B# H( n; f+ s
                                                                                    ! {7 L5 G3 `9 V; P
                                                                                            苹果iPhone16发布了,嵌入式鸿蒙,国产化程度有多高?
    8 f/ s0 C1 r* d0 \. r                                                                               
    ) G5 V% [# t+ o* a6 V3 t                                                                       
    4 O% r% n" d0 o                                                               
    6 p; K9 e' J/ Z- r) T9 p+ D; b                                                        . R  @. ^9 G( o! I2 j3 x
                                                    我是老温,一名热爱学习的嵌入式工程师
    8 l  n; ~9 U+ n+ \6 P8 v. W) h" M0 _关注我,一起变得更加优秀!
  • 回复

    使用道具 举报

    发表回复

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则


    联系客服 关注微信 下载APP 返回顶部 返回列表