电子产业一站式赋能平台

PCB联盟网

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

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

[复制链接]

478

主题

478

帖子

1544

积分

三级会员

Rank: 3Rank: 3

积分
1544
发表于 2024-9-19 17:50:00 | 显示全部楼层 |阅读模式
我是老温,一名热爱学习的嵌入式工程师
. L, B5 Z3 x% S1 T  G9 R关注我,一起变得更加优秀!* V$ H) z1 X, n0 _4 }( ?
/ T& v- O4 }9 P8 E; P" T8 W, W
来源 | 屋脊雀
1 i! W% x3 B% D% |2 [( k网络上配套STM32开发板有很多LCD例程,主要是TFT LCD跟OLED的。从这些例程,大家都能学会如何点亮一个LCD。但这代码都有下面这些问题:
" `& O/ j% m8 Q
  • 分层不清晰,通俗讲就是模块化太差。
  • 接口乱。只要接口不乱,分层就会好很多了。
  • 可移植性差。
  • 通用性差。为什么这样说呢?如果你已经了解了LCD的操作,请思考如下情景:
    $ I9 q- F3 J* X: R' @( V! D1、代码空间不够,只能保留9341的驱动,其他LCD驱动全部删除。能一键(一个宏定义)删除吗?删除后要改多少地方才能编译通过?6 i; K/ A9 X3 K$ b5 J+ n1 R
    2、有一个新产品,收银设备。系统有两个LCD,都是OLED,驱动IC相同,但是一个是128x64,另一个是128x32像素,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。怎么办?这些例程代码要怎么改才能支持两个屏幕?全部代码复制粘贴然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。) n: K$ E: b/ L  K; i$ o* K' B
    3、一个OLED,原来接在这些IO,后来改到别的IO,容易改吗?1 S! C: x8 \/ O' N  O/ [/ V, S+ j
    4、原来只是支持中文,现在要卖到南美,要支持多米尼加语言,好改吗?
    / @2 ^4 h* Y, C5 hLCD种类概述在讨论怎么写LCD驱动之前,我们先大概了解一下嵌入式常用LCD。概述一些跟驱动架构设计有关的概念,在此不对原理和细节做深入讨论,会有专门文章介绍,或者参考网络文档。
    1 M" N( P0 E$ WTFT lcdTFT LCD,也就是我们常说的彩屏。通常像素较高,例如常见的2.8寸,320X240像素。4.0寸的,像素800X400。这些屏通常使用并口,也就是8080或6800接口(STM32 的FSMC接口);或者是RGB接口,STM32F429等芯片支持。其他例如手机上使用的有MIPI接口。. x1 A6 U% j1 ?# R6 s: P
    总之,接口种类很多。也有一些支持SPI接口的。除非是比较小的屏幕,否则不建议使用SPI接口,速度慢,刷屏闪屏。玩STM32常用的TFT lcd屏幕驱动IC通常有:ILI9341/ILI9325等。
    : p# Z! X9 l9 O! A* |# `tft lcd:
    0 }+ q( c: D6 W" e
    . J% d3 n3 S! V) R, W+ {/ RIPS:
    : s; K+ r2 F* D0 g# I" D

    4nqs423sgba64035223722.jpg

    4nqs423sgba64035223722.jpg
    ' `7 j) i# R' |1 Y. l
    COG lcd很多人可能不知道COG LCD是什么,我觉得跟现在开发板销售方向有关系,大家都出大屏,玩酷炫界面,对于更深的技术,例如软件架构设计,都不涉及。使用单片机的产品,COG LCD其实占比非常大。COG是Chip On Glass的缩写,就是驱动芯片直接绑定在玻璃上,透明的。实物像下图:
    . }# p! c. f! x, F: [  x

    r54yd031wzu64035223822.jpg

    r54yd031wzu64035223822.jpg
    " p, N) \8 F" F4 V: E5 X) Y
    这种LCD通常像素不高,常用的有128X64,128X32。一般只支持黑白显示,也有灰度屏。/ [8 ?) R; A: p9 C5 ]
    接口通常是SPI,I2C。也有号称支持8位并口的,不过基本不会用,3根IO能解决的问题,没必要用8根吧?常用的驱动IC:STR7565。
    3 z# ^; J: X/ O% b7 EOLED lcd买过开发板的应该基本用过。新技术,大家都感觉高档,在手环等产品常用。OLED目前屏幕较小,大一点的都很贵。在控制上跟COG LCD类似,区别是两者的显示方式不一样。从我们程序角度来看,最大的差别就是,OLED LCD,不用控制背光。。。。。实物如下图:
    ( ?' e  L/ `% g9 T) f, C
    # K$ k0 b/ x. O0 w常见的是SPI跟I2C接口。常见驱动IC:SSD1615。
    $ }, i: u, J4 g9 \( m硬件场景接下来的讨论,都基于以下硬件信息:
    & Y: c2 [/ K6 _" g+ c1、有一个TFT屏幕,接在硬件的FSMC接口,什么型号屏幕?不知道。2 ^: L" A8 V( m' k. K7 c
    2、有一个COG lcd,接在几根普通IO口上,驱动IC是STR7565,128X32像素。6 Q+ ?$ L1 J9 b' j" w3 e
    3、有一个COG LCD,接在硬件SPI3跟几根IO口上,驱动IC是STR7565,128x64像素。8 Q7 r5 E) h) \% G
    4、有一个OLED LCD,接在SPI3上,使用CS2控制片选,驱动IC是SSD1315。% c: B+ y9 d$ @3 @; G9 }
    0 U# n) N4 ^; a9 Q7 C7 b1 M
    预备知识在进入讨论之前,我们先大概说一下下面几个概念,对于这些概念,如果你想深入了解,请GOOGLE。1 z& g, Y/ W8 t* M  P
    面向对象面向对象,是编程界的一个概念。什么叫面向对象呢?编程有两种要素:程序(方法),数据(属性)。例如:一个LED,我们可以点亮或者熄灭它,这叫方法。LED什么状态?亮还是灭?这就是属性。我们通常这样编程:8 t. S. _9 B0 |# w' G
    u8 ledsta = 0;3 f, t& h3 l: P
    void ledset(u8 sta)
    * n% y% g# `$ [5 [1 o* R{
    ; v# F5 D5 [  J. ]* E# d' o}
    ; n/ \* B# |. w  I, P* Q这样的编程有一个问题,假如我们有10个这样的LED,怎么写?这时我们可以引入面向对象编程,将每一个LED封装为一个对象。可以这样做:4 X' m3 N; z# ^8 v( o& O3 Z7 A& i
    /*
    + X% {5 x7 o$ M$ D; X6 u6 X, @1 k定义一个结构体,将LED这个对象的属性跟方法封装。
    ' H1 `4 o0 d' e: J( V7 I8 t! i这个结构体就是一个对象。
    2 z4 O" _* D- s, F% G( ?8 l+ V2 Z+ N但是这个不是一个真实的存在,而是一个对象的抽象。9 N: K( X# }1 E2 l& b8 B8 |  f
    */. f# c8 m3 ?! S! X) g  h" F: H
    typedef struct{
    & v8 R; w$ U' w) J1 m    u8 sta;2 d' D( u% C3 v+ ]1 V
        void (*setsta)(u8 sta);
    9 A8 K( j6 E- f. n6 f}LedObj;" ]" a8 ]6 m% e2 M4 \& r$ `
    /*  声明一个LED对象,名称叫做LED1,并且实现它的方法drv_led1_setsta*/* k8 L/ l6 q$ p. L
    void drv_led1_setsta(u8 sta)2 N0 ^! @1 g& `: `) P5 H# S% Q  S
    {) t3 W5 b  L; F  ]9 }) f6 a
    }
    1 q$ D( V/ F) M- @. U( ULedObj LED1={. P/ r# `! n3 C
            .sta = 0,. x; s2 N+ E3 j! ^, a  B: O" k% @
            .setsta = drv_led1_setsta,8 w3 U$ x. C- c: Q6 U3 ?4 M+ w/ m
        };
      h- \+ R* ]: t7 N# Y) k, I/*  声明一个LED对象,名称叫做LED2,并且实现它的方法drv_led2_setsta*/3 R- K4 r  p' J4 ]& D, K' s
    void drv_led2_setsta(u8 sta)) G# N8 ^8 N! R
    {
    ' p* Y7 B+ E+ e1 ^) o}2 U3 q% {, d* N' W1 v
    LedObj LED2={
    % N/ }2 k& r/ ~9 S& n0 F# Q        .sta = 0,
    9 b5 ^5 H: r. ^, q: n- ?0 c- [3 y        .setsta = drv_led2_setsta,
    : I# v- c+ S- n1 C4 y: i    };; R8 X$ E: w9 j3 T* A& p. Z
       
    8 _, K9 G$ N* w! [9 I% ]+ B/*  操作LED的函数,参数指定哪个led*/0 N; u0 ~+ G9 C8 _
    void ledset(LedObj *led, u8 sta)
    7 @# V; l1 b6 q) C7 F- K! T{
    : p# a1 O9 d% t3 e  w    led->setsta(sta);
    + G2 G& c8 k% A: U" i}/ O& v# C6 ~1 y
    是的,在C语言中,实现面向对象的手段就是结构体的使用。上面的代码,对于API来说,就很友好了。操作所有LED,使用同一个接口,只需告诉接口哪个LED。大家想想,前面说的LCD硬件场景。4个LCD,如果不面向对象,「显示汉字的接口是不是要实现4个」?每个屏幕一个?
    * u$ N& y. i5 r驱动与设备分离如果要深入了解驱动与设备分离,请看LINUX驱动的书籍。
    ! m- z* j) f  |# q: x什么是设备?我认为的设备就是「属性」,就是「参数」,就是「驱动程序要用到的数据和硬件接口信息」。那么驱动就是「控制这些数据和接口的代码过程」
    * M+ J! ?' s/ l通常来说,如果LCD的驱动IC相同,就用相同的驱动。有些不同的IC也可以用相同的,例如SSD1315跟STR7565,除了初始化,其他都可以用相同的驱动。例如一个COG lcd:
    ( y/ j  A9 V7 u* P; |?驱动IC是STR7565 128 * 64 像素用SPI3背光用PF5 ,命令线用PF4 ,复位脚用PF3
    0 r( T6 O! d. A' I$ a?
    上面所有的信息综合,就是一个设备。驱动就是STR7565的驱动代码。
    ' G' G! O# m- \7 g7 e8 @0 [为什么要驱动跟设备分离,因为要解决下面问题:
    , @4 q& \9 G6 q1 q& R0 F" @1 ^?有一个新产品,收银设备。系统有两个LCD,都是OLED,驱动IC相同,但是一个是128x64,另一个是128x32像素,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。' {2 h0 ]3 m  w+ I- O* L$ h
    ?
    这个问题,「两个设备用同一套程序控制」才是最好的解决办法。驱动与设备分离的手段:, q# @' P1 g7 Z2 }0 ^2 P
    ?在驱动程序接口函数的参数中增加设备参数,驱动用到的所有资源从设备参数传入。8 O+ D2 H( i$ ]8 P
    ?
    驱动如何跟设备绑定呢?通过设备的驱动IC型号。
    8 }9 W1 P" }2 `模块化我认为模块化就是将一段程序封装,提供稳定的接口给不同的驱动使用。不模块化就是,在不同的驱动中都实现这段程序。例如字库处理,在显示汉字的时候,我们要找点阵,在打印机打印汉字的时候,我们也要找点阵,你觉得程序要怎么写?把点阵处理做成一个模块,就是模块化。非模块化的典型特征就是「一根线串到底,没有任何层次感」
    % A2 V: u% w& [# M" ^- E' T$ sLCD到底是什么前面我们说了面向对象,现在要对LCD进行抽象,得出一个对象,就需要知道LCD到底是什么。问自己下面几个问题:8 O! D) S( I! y) [
  • LCD能做什么?
  • 要LCD做什么?
  • 谁想要LCD做什么?刚刚接触嵌入式的朋友可能不是很了解,可能会想不通。我们模拟一下LCD的功能操作数据流。APP想要在LCD上显示 一个汉字。
    2 G; a# N9 [' A; Z1、首先,需要一个显示汉字的接口,APP调用这个接口就可以显示汉字,假设接口叫做lcd_display_hz。" R1 O8 t7 N! @, b4 E
    2、汉字从哪来?从点阵字库来,所以在lcd_display_hz函数内就要调用一个叫做find_font的函数获取点阵。
    ; r- a) m7 A: B! K3、获取点阵后要将点阵显示到LCD上,那么我们调用一个ILL9341_dis的接口,将点阵刷新到驱动IC型号为ILI9341的LCD上。. b: ]0 S- C- s$ }2 ^" ^2 d
    4、ILI9341_dis怎么将点阵显示上去?调用一个8080_WRITE的接口。
    * C8 y! p" K  t; G好的,这个就是大概过程,我们从这个过程去抽象LCD功能接口。汉字跟LCD对象有关吗?无关。在LCD眼里,无论汉字还是图片,都是一个个点。那么前面问题的答案就是:
    8 D* @0 W$ R& x5 A1 ?' x. X- Y
  • LCD可以一个点一个点显示内容。
  • 要LCD显示汉字或图片-----就是显示一堆点
  • APP想要LCD显示图片或文字。结论就是:所有LCD对象的功能就是显示点。「那么驱动只要提供显示点的接口就可以了,显示一个点,显示一片点。」 抽象接口如下:" f. U, O0 t$ ~. A8 D
    /*0 ~, O0 Z+ G6 J8 M) V, ]2 {" m
        LCD驱动定义
    8 C3 v+ P  j& l*/& p. D" [, l2 y0 U
    typedef struct  # {3 r* N; o. @) ]3 Y& m
    {
    $ R% T0 [8 v( v) W, _    u16 id;
    # C: E. Y) f+ z% g; k3 i6 i    s32 (*init)(DevLcd *lcd);4 U3 y) I5 ^' E
        s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);) U* j5 }  U# a
        s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);9 b/ v$ s. ~* L4 q* \. m$ @; v" r
        s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);
    : y8 f$ g& `1 p' a7 r    s32 (*onoff)(DevLcd *lcd, u8 sta);3 e5 J$ ?- w0 b( ]. Y9 ?/ K
        s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);
    . P- u* |( R3 N6 k/ l9 ]" L    void (*set_dir)(DevLcd *lcd, u8 scan_dir);
    7 n/ Y4 ~: p* D5 A& {; @" L, W    void (*backlight)(DevLcd *lcd, u8 sta);& R" e2 {) J: T  T2 }
    }_lcd_drv;
    / G, v! L( M  z- m6 o上面的接口,也就是对应的驱动,包含了一个驱动id号。$ I+ t6 @, M, n: o. o5 J
  • id,驱动型号
  • 初始化
  • 画点
  • 将一片区域的点显示某种颜色
  • 将一片区域的点显示某些颜色
  • 显示开关
  • 准备刷新区域(主要彩屏直接DMA刷屏使用)
  • 设置扫描方向
  • 背光控制显示字符,划线等功能,不属于LCD驱动。应该归类到GUI层。
    - ^! }+ b) |; L! r  Y' QLCD驱动框架我们设计了如下的驱动框架:+ |$ T+ Q, I1 E! }  r: |  f

    ! X* U, O5 H6 Z# _3 \设计思路:7 Q1 Q) o. F! g; }( Y& R
    1、中间显示驱动IC驱动程序提供统一接口,接口形式如前面说的_lcd_drv结构体。
    " G2 `, I# n) g- N2、各显示IC驱动根据设备参数,调用不同的接口驱动。例如TFT就用8080驱动,其他的都用SPI驱动。SPI驱动只有一份,用IO口控制的我们也做成模拟SPI。
    % U1 ^+ J3 N1 ~4 v3、LCD驱动层做LCD管理,例如完成TFT LCD的识别。并且将所有LCD接口封装为一套接口。
    2 V( y/ k# i" g8 ~% z4、简易GUI层封装了一些显示函数,例如划线、字符显示。% X3 P7 |0 ~* g; |  o# b- Z" D' C
    5、字体点阵模块提供点阵获取与处理接口。
    $ d6 {; \3 B9 [" j由于实际没那么复杂,在例程中我们将GUI跟LCD驱动层放到一起。TFT LCD的两个驱动也放到一个文件,但是逻辑是分开的。OLED除初始化,其他接口跟COG LCD基本一样,因此这两个驱动也放在一个文件。
    , |8 Z3 _9 {' ^+ u5 b% z5 \代码分析代码分三层:" d& l3 k  X9 A0 V
    1、GUI和LCD驱动层 dev_lcd.c dev_lcd.h* J* b$ r  s  p
    2、显示驱动IC层 dev_str7565.c & dev_str7565.h dev_ILI9341.c & dev_ILI9341.h" |" h5 ^1 B1 W" `: ~' h. B' \8 K
    3、接口层 mcu_spi.c & mcu_spi.h stm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h9 R+ Q7 L6 U8 T* v, j  L% i( o
    GUI和LCD层这层主要有3个功能 :
    6 j' @, o! L% [& v, ~「1、设备管理」& P6 g2 u7 d5 a8 v8 D
    首先定义了一堆LCD参数结构体,结构体包含ID,像素。并且把这些结构体组合到一个list数组内。
    5 U# R, X/ H7 N7 |  d/*  各种LCD的规格参数*/1 J8 z0 j8 _0 x8 b; d; D
    _lcd_pra LCD_IIL9341 ={; v5 G6 t' C7 d* K: y
            .id   = 0x9341,; e! ^2 k! ~3 d
            .width = 240,   //LCD 宽度$ j9 l- |- a8 ?* `
            .height = 320,  //LCD 高度
    1 j( `/ F7 p! D6 c+ v};
    # F# N& v) L9 K# t4 `; e..., u  P6 [5 _! Z. I: s" e/ m- o
    /*各种LCD列表*/
    # e' F1 c  G5 |! R: E_lcd_pra *LcdPraList[5]=
    , e0 @1 U$ M7 r0 o7 y* U! M; y            {
    # r: B9 c5 A9 S% O8 F( D& G                &LCD_IIL9341,      
    6 l# [# f9 r$ T; O: X: L                &LCD_IIL9325,
    ) x; s" ~$ ~7 l' A+ f                &LCD_R61408,
    6 }& B; {0 @) f  `) o( D. F/ {2 O0 ]                &LCD_Cog12864," }7 J5 i1 O3 j
                    &LCD_Oled12864,
    # k6 I  b" C4 |% \. c1 z: N            };+ w" M9 s& t5 E4 ]6 A
    然后定义了所有驱动list数组,数组内容就是驱动,在对应的驱动文件内实现。
    * [+ d0 z; `7 @4 r  B8 u6 G; j/*  所有驱动列表/ a, J: k$ A' u: e4 i1 }4 Q( M' i! f
        驱动列表*/
      s9 K- D7 J( B4 Q% v3 N2 ?; ~_lcd_drv *LcdDrvList[] = {4 G& W7 [; _& h+ [
                        &TftLcdILI9341Drv,
    + d* f8 r  i/ N* N                    &TftLcdILI9325Drv,
    : d& A( Q+ g- P: d* |1 |                    &CogLcdST7565Drv,
    - h7 V& f$ I6 w6 j. d0 s3 y                    &OledLcdSSD1615rv,1 C- v8 U/ E5 M3 R3 V9 Y
    定义了设备树,即是定义了系统有多少个LCD,接在哪个接口,什么驱动IC。如果是一个完整系统,可以做成一个类似LINUX的设备树。" m' d! o# n; h: Q
    /*设备树定义*/" M# d& t/ h. z% r' h/ I
    #define DEV_LCD_C 3//系统存在3个LCD设备
    # i# r: k1 l" w" ~5 J: w2 rLcdObj LcdObjList[DEV_LCD_C]=8 f$ N8 R# h* A7 F
    {2 R% I6 {; I4 L& _- a3 L" t( t( F
        {"oledlcd", LCD_BUS_VSPI, 0X1315},
    * s3 m1 B3 r* A/ W  S" k3 @8 J    {"coglcd", LCD_BUS_SPI,  0X7565},
    ' s' r9 @0 p' P    {"tftlcd", LCD_BUS_8080, NULL},
    % ]6 `; p( l) u9 @  }};
    ; v! |% S9 X7 R「2 、接口封装」5 I$ L1 ?/ i3 G, }. R
    void dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir)3 m, K! b/ g. c; C8 ~. B; b
    s32 dev_lcd_init(void)
    ' Y6 w0 n9 r2 W" W) KDevLcd *dev_lcd_open(char *name)
    2 d7 a" L! ]8 k$ r4 S7 d6 e9 bs32 dev_lcd_close(DevLcd *dev)
    ) Y7 I3 M6 \  zs32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)4 {$ |+ [; ~5 @2 w) B: e& P
    s32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)
    3 L- ]) d; u/ d2 X9 a8 @/ cs32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta); r, Y4 Y) p  \  X  \
    s32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color)$ a* i5 `5 t# i5 {* p4 I" Q: d
    s32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)
    4 Q- B! I2 S4 q- v$ [2 bs32 dev_lcd_backlight(DevLcd *lcd, u8 sta)$ o" Y, `" V$ @. y! i
    大部分接口都是对驱动IC接口的二次封装。有区别的是初始化和打开接口。初始化,就是根据前面定义的设备树,寻找对应驱动,找到对应设备参数,并完成设备初始化。打开函数,根据传入的设备名称,查找设备,找到后返回设备句柄,后续的操作全部需要这个设备句柄。
    9 R8 f* W7 A6 p" H「3 、简易GUI层」
    3 a9 a: C% D9 T1 s; b) A目前最重要就是显示字符函数。
    " F; X1 L. T, ~: K4 `/ Ns32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)
    " I7 u2 q" @. s% w; I) V1 C其他划线画圆的函数目前只是测试,后续会完善。' r& L0 |8 c" X& p  S: _6 q, ]
    驱动IC层驱动IC层分两部分:' o  B2 D9 g- `( A
    「1 、封装LCD接口」3 l( i9 G. d$ l$ V. C. J, u  J+ \. ^
    LCD有使用8080总线的,有使用SPI总线的,有使用VSPI总线的。这些总线的函数由单独文件实现。但是,除了这些通信信号外,LCD还会有复位信号,命令数据线信号,背光信号等。我们通过函数封装,将这些信号跟通信接口一起封装为「LCD通信总线」, 也就是buslcd。BUS_8080在dev_ILI9341.c文件中封装。BUS_LCD1和BUS_lcd2在dev_str7565.c 中封装。
    5 s& G& w/ ~5 A& b' O「2 驱动实现」
    & v6 t& L0 R9 Z9 W实现_lcd_drv驱动结构体。每个驱动都实现一个,某些驱动可以共用函数。+ L4 V# j" }2 B4 E0 h6 h, t$ G
    _lcd_drv CogLcdST7565Drv = {8 |* D! o( T2 F/ S0 Y* U! V) k( u
                                .id = 0X7565,
    / k) F% m3 i8 [  f                            .init = drv_ST7565_init,1 X; X( q2 T% k9 T# a0 z; ]( n* W2 b9 V
                                .draw_point = drv_ST7565_drawpoint,
    & Q) @. A! L  k1 U4 o) g2 a                            .color_fill = drv_ST7565_color_fill,4 U# R, p/ P2 D( z) T
                                .fill = drv_ST7565_fill,
    ( L# b' y+ _; D7 c. f, m( k1 L! d! O                            .onoff = drv_ST7565_display_onoff,' W$ G  K6 l( v3 l  V
                                .prepare_display = drv_ST7565_prepare_display,
    ) N7 R7 c8 M& k. S  o                            .set_dir = drv_ST7565_scan_dir,
    $ ~( I* q6 \! }; o8 [0 H! }                            .backlight = drv_ST7565_lcd_bl8 i/ V& f) ]+ h& e# H
                                };
    3 p3 O9 y  z% i" Y- i& v+ C接口层8080层比较简单,用的是官方接口。SPI接口提供下面操作函数,可以操作SPI,也可以操作VSPI。
    : o+ i* y; j* `6 Qextern s32 mcu_spi_init(void);$ w; g$ {! a; q1 T& h
    extern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);
      U/ H' C9 `6 ], {0 S# Q$ Pextern s32 mcu_spi_close(SPI_DEV dev);
    " P# g9 Q3 {" V% C2 Textern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);9 D0 N" }( l' H4 \4 Z$ e. F5 \
    extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);$ V( M9 K% y6 f: S  v& k. {
    至于SPI为什么这样写,会有一个单独文件说明。/ U. i- ^, i( ]2 t' c4 b
    总体流程前面说的几个模块时如何联系在一起的呢?请看下面结构体:& h  p1 g* d, h
    /*  初始化的时候会根据设备数定义,* u: y: n2 X( a/ W' s
        并且匹配驱动跟参数,并初始化变量。: V7 U% d2 @- Y1 y9 Q7 W; W
        打开的时候只是获取了一个指针 */
    3 G$ T  A' p6 k, x" J4 s; Xstruct _strDevLcd
    ! d  J. M( V9 o* \/ K% @6 _' I5 g( A{
    ) S# G. i: p. z, F  p5 j$ c    s32 gd;//句柄,控制是否可以打开
    ! C" ^- f7 _: ~! g- u: s: O    LcdObj   *dev;
    8 X' q, F" W0 @; m- W# f    /* LCD参数,固定,不可变*/
    ) O1 x+ f2 Z3 ]# r    _lcd_pra *pra;# k% @% U) ^8 x  P3 Y
        /* LCD驱动 */7 E# @1 [- L9 C9 B- f7 _
        _lcd_drv *drv;
    ) W; N& @5 M6 t& a* U" @  u    /*驱动需要的变量*/
    . `- l1 |# }: _7 P- Q. ~    u8  dir;    //横屏还是竖屏控制:0,竖屏;1,横屏。! X  ?& |! D& X7 i. h
        u8  scandir;//扫描方向
    4 G6 t6 Q$ O' K8 V* P  U    u16 width;  //LCD 宽度5 e* s$ F9 q' B. O1 [
        u16 height; //LCD 高度
    ! p+ G/ A$ K# C- L4 c  D1 o& G& i    void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存* S  a( d7 g' i9 K  L( H! `
    };
    / d2 m: ?( L& S2 }3 g每一个设备都会有一个这样的结构体,这个结构体在初始化LCD时初始化。- m/ U7 N$ q4 H0 c
  • 成员dev指向设备树,从这个成员可以知道设备名称,挂在哪个LCD总线,设备ID。typedef struct& `: V9 C6 M% w. Y% Q+ B7 q! t
    {6 T6 c/ X: I! h2 d) ?  b" P
        char *name;//设备名字5 B4 X3 }$ R" n$ Y+ j9 x% x+ `' f
        LcdBusType bus;//挂在那条LCD总线上
    ; S# i! P' U5 s  m: Q! J1 y    u16 id;* S) [- ]6 C/ j- E9 {/ A
    }LcdObj;
    ; J: h! z$ [1 l) W" N% O# O
  • 成员pra指向LCD参数,可以知道LCD的规格。typedef struct
    0 X! R- i: |( h{
    9 |5 b3 c2 v4 w  o* F6 I    u16 id;
    2 n! i! D! X3 a! j: h* C1 b    u16 width;  //LCD 宽度  竖屏
    . P5 b* j1 g2 j# s, \+ Z/ K0 }    u16 height; //LCD 高度    竖屏
    4 E0 V6 Q. Y4 d0 I" s}_lcd_pra;
    5 ]- O( d6 s2 O
  • 成员drv指向驱动,所有操作通过drv实现。typedef struct  
    $ o& n& `9 d( L( c  V( y& T7 S{3 y8 i# M9 g/ q! w
        u16 id;0 Y* J# W- \/ M
        s32 (*init)(DevLcd *lcd);
    . A7 D, A) C  L. {8 Q) c. k    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);8 Z1 n# p# \- P" ~4 B" w
        s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);8 h+ c& C  D. i; H6 \
        s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);  B/ b: N2 |! L- A
        s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);
    $ k' ^3 v1 m. y% J+ o    s32 (*onoff)(DevLcd *lcd, u8 sta);) d3 ^1 |4 A( w( m, s) b4 \
        void (*set_dir)(DevLcd *lcd, u8 scan_dir);
    % ~1 c7 ]7 ~6 t+ Q1 H! s3 ]    void (*backlight)(DevLcd *lcd, u8 sta);6 w$ p: ]8 q$ h1 O* Y! ~
    }_lcd_drv;
    0 F. A6 u+ T, V; l. ^; m( E
  • 成员dir、scandir、 width、 height是驱动要使用的通用变量。因为每个LCD都有一个结构体,一套驱动程序就能控制多个设备而互不干扰。
  • 成员pri是一个私有指针,某些驱动可能需要有些比较特殊的变量,就全部用这个指针记录,通常这个指针指向一个结构体,结构体由驱动定义,并且在设备初始化时申请变量空间。目前主要用于COG LCD跟OLED LCD显示缓存。整个LCD驱动,就通过这个结构体组合在一起。
    ! W3 X) j2 _/ `( U; q1、初始化,根据设备树,找到驱动跟参数,然后初始化上面说的结构体。
    + I& E' R0 j0 `7 d; c2、要使用LCD前,调用dev_lcd_open函数。打开成功就返回一个上面的结构体指针。5 a$ k2 k8 y8 j* D
    3、显示字符,接口找到点阵后,通过上面结构体的drv,调用对应的驱动程序。
    & r  b. \0 [# L6 Y1 }4、驱动程序根据这个结构体,决定操作哪个LCD总线,并且使用这个结构体的变量。
    8 f# m5 N6 x' E6 c$ O- V用法和好处
  • 好处1请看测试程序
    6 E0 z, a+ B( r  w9 Xvoid dev_lcd_test(void)
    ) H! Z2 O6 {+ M4 M1 T  {{
    7 {# m* R; M  s    DevLcd *LcdCog;* M- y) k; D/ S' k
        DevLcd *LcdOled;8 [% @! L; A# C6 J. Z2 B) t% D. Z
        DevLcd *LcdTft;
    6 F; |# t0 x! V: J3 b    /*  打开三个设备 */5 O1 M' }) f* q/ c# \; z; k4 C- E
        LcdCog = dev_lcd_open("coglcd");; ~4 k4 ]8 [2 B* j: v
        if(LcdCog==NULL)8 U2 G0 L. l- B  r/ _
            uart_printf("open cog lcd err\r$ q3 w2 b/ _" O/ r6 D
    ");- a( n# t) t/ o3 A( [  Q( G
        LcdOled = dev_lcd_open("oledlcd");! ^6 B: k  ^) v1 f( W0 ^
        if(LcdOled==NULL)
    # \) w7 c' o8 H        uart_printf("open oled lcd err\r
    ( x  a! E' x% ]7 d");
    ) N0 j  N# n3 d6 R  ?    LcdTft = dev_lcd_open("tftlcd");- R( R: z4 H! x& ^) x0 |4 b1 f
        if(LcdTft==NULL)
    0 t( S& [+ ]- o5 v        uart_printf("open tft lcd err\r
    0 k" W0 o# O9 G");
    - \- E4 B0 @8 w5 k' ^- \    /*打开背光*/
    : ^& v* [3 `) S5 P2 y: v    dev_lcd_backlight(LcdCog, 1);# S/ P9 u3 T4 N" x, m
        dev_lcd_backlight(LcdOled, 1);/ \. ~( m6 J+ }
        dev_lcd_backlight(LcdTft, 1);
    * M' Z& l3 c6 x6 a" m! O    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
    " g) Y$ {1 O& H    dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "这是oled lcd", BLACK);8 T9 I. T) p! x4 Q, B* _
        dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);: Z* \  K% {$ F
        dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);
    " T6 F3 V5 g% ?9 O+ W; l+ q. m    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);4 b% U) n8 \8 x4 c9 U/ v
        dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是cog lcd", BLACK);* ^& Z+ I' x  J& n) g7 r2 r
        dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);2 K; U  G6 k1 o; n) _% O
        dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);! c( v$ ]2 U2 G3 ?% l' U: w
        dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);
    ) R! g; D) I/ U; d    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是tft lcd", RED);
    - T/ Y4 b% M) H2 f. m    dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);
    3 z& {0 h4 _2 t: q( s; }5 a    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "屋脊雀工作室", RED);+ P7 |+ w  h: e* @6 o# _, H" @
        while(1);
    1 F7 _6 ?$ m& V1 ~& V. f}! K$ T0 x6 q( u. a* U1 i/ s7 P5 r
    使用一个函数dev_lcd_open,可以打开3个LCD,获取LCD设备。然后调用dev_lcd_put_string就可以在不同的LCD上显示。其他所有的gui操作接口都只有一个。这样的设计对于APP层来说,就很友好。显示效果:
    7 K0 w0 D) R* d: k& h/ i2 x! n6 ?0 f; k* }% J+ m1 N' F) `
  • 好处2现在的设备树是这样定义的
    * m  |9 |5 Q4 X: W6 `! `LcdObj LcdObjList[DEV_LCD_C]=4 m' d; e8 O- ]) P2 O2 C) U
    {( P! T$ j4 H; v' f9 `9 y8 c  T
        {"oledlcd", LCD_BUS_VSPI, 0X1315},9 u# J- `- X" q! J8 z) U
        {"coglcd", LCD_BUS_SPI,  0X7565},) d- n% y+ M4 J& P# F0 D
        {"tftlcd", LCD_BUS_8080, NULL},2 F, [0 x( n' G8 X% H
    };
    - |1 u5 h& O7 W8 R: C: O6 K某天,oled lcd要接到SPI上,只需要将设备树数组里面的参数改一下,就可以了,当然,在一个接口上不能接两个设备。1 q& b# h' I/ ]' f
    LcdObj LcdObjList[DEV_LCD_C]=7 ]8 S1 a! l9 S% s6 r: ^
    {
    / ~! c8 o2 F% r' G    {"oledlcd", LCD_BUS_SPI, 0X1315},: B6 G9 n/ G6 g1 J+ Y( T
        {"tftlcd", LCD_BUS_8080, NULL},
    8 D1 a# l. f- @: |' J9 k};; [' ?# t1 K5 n$ Y3 q" k
    字库暂时不做细说,例程的字库放在SD卡中,各位移植的时候根据需要修改。具体参考font.c。% T! W% y( Q$ R% e, e" n* t
    声明代码请按照版权协议使用。当前源码只是一个能用的设计,完整性与健壮性尚未测试。后续会放到github,并且持续更新优化。最新消息请关注www.wujique.com。
    - e6 q" [; A2 h  h2 K7 N- a-END-
    9 o) I$ o( a, ^! O/ w* g+ O# H, t3 h4 I往期推荐:点击图片即可跳转阅读
    8 ~. {  D! T" B                                                        $ z; W0 d/ l( j  O
                                                                    * }  J: S$ \+ k" _5 B
                                                                           
    4 U5 X! d/ M" R! E& G& F                                                                                " j+ a3 U- [' f( U% P1 c( o4 \/ F- |

    # l" n4 O, ]0 Q6 l& ?( u( I                                                                                5 C; b* G% [6 s& X# H4 g' U. j# g
                                                                                            浅谈为何不该入行嵌入式技术开发1 E! T# X) A: v0 L! J7 v. R
                                                                                    ' V5 \* O3 R$ |. ^% p3 e% P3 E
                                                                            * S# l5 d) n* Y# @" S
                                                                   
    8 I- U, J0 p$ k7 m! m                                                       
    7 v7 V, L" ~$ m8 T4 X* N                                               
    * N4 f  [. a; k* v
    / M/ z  v9 M+ I: A- w                                                       
    # b* t/ e  Z6 f$ G1 K                                                               
    + G( U: o3 A* ^, q5 f2 l5 }                                                                        & f; V8 R/ X; W5 f+ [2 s& b9 W
                                                                                    5 j0 B! O9 S" P

    8 @0 D0 A1 \+ ]- y' i3 f                                                                               
    0 W! j* _6 G, i" b* g  X2 D% S                                                                                        在深圳搞嵌入式,从来没让人失望过!
    ; K( a- f- E' p4 e% ^                                                                                9 D. k" L, ^" a0 M
                                                                            ; u- J9 k4 N' V0 }. }+ j% e. Z( g
                                                                   
      Q. m; R5 m" N; k3 p4 j% T& T2 ^                                                        ) H& L7 J! }! ?' T" x1 i/ o
                                                    . S$ ?- M/ n- o3 D

    # S$ e1 Z7 `/ N1 g, K5 k                                                       
    4 k* t- [2 ]8 f' u, n                                                               
    : o4 \% x2 X+ H( k                                                                       
    4 a! S$ D0 J5 I6 Z% ^. C/ J                                                                               
    3 x) S0 V6 g2 e2 ]* u! Q: t4 J: T( ?. T+ B8 U" A5 u
                                                                                    ) ~0 d+ s5 x, P
                                                                                            苹果iPhone16发布了,嵌入式鸿蒙,国产化程度有多高?4 p: ^3 L! @5 x$ z. v. [
                                                                                   
    4 b- e" W1 @4 A4 b3 m  q                                                                       
    9 P* M# B+ N* h! B                                                               
    , ^4 J3 N  D- n# \! {  V                                                        / G7 J( g2 ^( f/ q3 @) [1 ?$ Q& r
                                                    我是老温,一名热爱学习的嵌入式工程师
    6 I: x0 @3 ^# n. ~关注我,一起变得更加优秀!
  • 回复

    使用道具 举报

    发表回复

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

    本版积分规则


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