电子产业一站式赋能平台

PCB联盟网

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

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

[复制链接]

569

主题

569

帖子

4259

积分

四级会员

Rank: 4

积分
4259
发表于 2024-9-19 17:50:00 | 显示全部楼层 |阅读模式
我是老温,一名热爱学习的嵌入式工程师
' N4 x, S# J" f+ Q5 ^3 l关注我,一起变得更加优秀!/ l- F3 ?& U  u: m/ e

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

    gx2aotjbcot64023325705.png

    gx2aotjbcot64023325705.png
    + q. h5 x0 A1 a0 B& \$ H8 V1 ?
    IPS:
    , [  j6 V; e- ?2 P

    vfslbzhcgwr64023325805.jpg

    vfslbzhcgwr64023325805.jpg
    5 p4 v+ Q5 A6 _3 j7 b! {
    COG lcd很多人可能不知道COG LCD是什么,我觉得跟现在开发板销售方向有关系,大家都出大屏,玩酷炫界面,对于更深的技术,例如软件架构设计,都不涉及。使用单片机的产品,COG LCD其实占比非常大。COG是Chip On Glass的缩写,就是驱动芯片直接绑定在玻璃上,透明的。实物像下图:+ S7 N; }, R2 X* k' o

    vr0phcske3x64023325905.jpg

    vr0phcske3x64023325905.jpg
    : X* K" o2 E* q7 O: d
    这种LCD通常像素不高,常用的有128X64,128X32。一般只支持黑白显示,也有灰度屏。/ H5 g2 ]% |' h9 T. K$ H4 V
    接口通常是SPI,I2C。也有号称支持8位并口的,不过基本不会用,3根IO能解决的问题,没必要用8根吧?常用的驱动IC:STR7565。& ~7 C( K; r& \  s
    OLED lcd买过开发板的应该基本用过。新技术,大家都感觉高档,在手环等产品常用。OLED目前屏幕较小,大一点的都很贵。在控制上跟COG LCD类似,区别是两者的显示方式不一样。从我们程序角度来看,最大的差别就是,OLED LCD,不用控制背光。。。。。实物如下图:
    ! |! y" W3 E0 r1 N- b

    2hkur0o353c64023326006.png

    2hkur0o353c64023326006.png

    . S4 j, |1 |! C4 i- N6 U/ S3 x常见的是SPI跟I2C接口。常见驱动IC:SSD1615。8 R4 r3 U/ a0 f6 c' F+ b4 e
    硬件场景接下来的讨论,都基于以下硬件信息:" D; u2 b1 }+ L7 L" b0 \0 z$ ?
    1、有一个TFT屏幕,接在硬件的FSMC接口,什么型号屏幕?不知道。5 C- h; y2 w0 z# N7 V
    2、有一个COG lcd,接在几根普通IO口上,驱动IC是STR7565,128X32像素。
    9 T: @" c. S, j3、有一个COG LCD,接在硬件SPI3跟几根IO口上,驱动IC是STR7565,128x64像素。' }5 a9 y! W3 ~$ X
    4、有一个OLED LCD,接在SPI3上,使用CS2控制片选,驱动IC是SSD1315。) W5 D! M) Z; Z6 m/ s

    qd1ugincpbr64023326106.jpg

    qd1ugincpbr64023326106.jpg

    ( c0 `+ U; M- p" K/ T' S* x2 @预备知识在进入讨论之前,我们先大概说一下下面几个概念,对于这些概念,如果你想深入了解,请GOOGLE。
    ) |; V: T* d$ c/ ~1 C3 G5 c面向对象面向对象,是编程界的一个概念。什么叫面向对象呢?编程有两种要素:程序(方法),数据(属性)。例如:一个LED,我们可以点亮或者熄灭它,这叫方法。LED什么状态?亮还是灭?这就是属性。我们通常这样编程:3 R0 X2 @% T/ {) V7 @5 x, D' F
    u8 ledsta = 0;
    8 }* b  I& `$ i" s) qvoid ledset(u8 sta)) a, L4 I5 [; A1 E7 z
    {
    * j) x+ R  T8 p4 y/ m}
    6 V+ s+ i5 n; a: E( u8 c; F这样的编程有一个问题,假如我们有10个这样的LED,怎么写?这时我们可以引入面向对象编程,将每一个LED封装为一个对象。可以这样做:
    - Y8 s3 ?) n" ~7 J- B! c1 K/*1 h& M3 V) P/ W
    定义一个结构体,将LED这个对象的属性跟方法封装。
    * S8 B! s2 o! {& d4 Q6 _, R这个结构体就是一个对象。8 w3 d# v  @+ x
    但是这个不是一个真实的存在,而是一个对象的抽象。
    9 R+ z; I  B1 O; N* n" k2 u*/+ S. r1 C# |% |; p& |& z. u
    typedef struct{
    , S2 v$ n  _: u' S. l9 D, ~    u8 sta;* Y+ Q/ `& u% B3 ~% U+ C; |5 S
        void (*setsta)(u8 sta);. g; k  f' U# M, [# I
    }LedObj;
    : F5 W: ]+ D* }" E+ i  X9 U  B1 A8 H/*  声明一个LED对象,名称叫做LED1,并且实现它的方法drv_led1_setsta*/- c. I! D1 S" ^# M8 p, F
    void drv_led1_setsta(u8 sta). n/ J; [. L1 q, a+ C
    {
    ( A  v/ U1 v6 w3 s4 d( [$ ], q' U}, b& c/ h+ X  P& O* L( P
    LedObj LED1={
    . j2 A( j+ E$ [+ ?. w9 r3 H        .sta = 0,1 j/ ^$ U8 t% z8 C( e, z# v
            .setsta = drv_led1_setsta,5 q, O$ s- L0 B- k/ ]+ a
        };
    7 P( Y8 `7 h% r1 n4 ?/*  声明一个LED对象,名称叫做LED2,并且实现它的方法drv_led2_setsta*/
    1 v/ E0 [% y* _void drv_led2_setsta(u8 sta)
    + Y/ }1 i2 V: q% @  t{
    6 a* g. [! E( j, }4 h}) c, I+ g3 W: F$ Z! F9 V
    LedObj LED2={) K0 n7 s& h1 t1 r! ^' J8 P
            .sta = 0,2 M& g4 w$ T# @6 y5 {0 [
            .setsta = drv_led2_setsta,
    + o- s0 x% R& [! t  Y" g7 A    };
    + ]; Y+ Z; o7 k- h   
    , T& X% X8 i' H/*  操作LED的函数,参数指定哪个led*/
    9 S( n* ?+ U" r/ {, a5 z, F& S& ]- B5 Vvoid ledset(LedObj *led, u8 sta)
    ( ?% m8 i) [0 u: C; X/ X9 _{( S! \1 W  T- V
        led->setsta(sta);& f8 Q3 W. j6 P( j8 E. C9 M8 x& x
    }* u7 |% ^" b& t; g; h2 j' l: J
    是的,在C语言中,实现面向对象的手段就是结构体的使用。上面的代码,对于API来说,就很友好了。操作所有LED,使用同一个接口,只需告诉接口哪个LED。大家想想,前面说的LCD硬件场景。4个LCD,如果不面向对象,「显示汉字的接口是不是要实现4个」?每个屏幕一个?
    ; A) u6 b* x4 T驱动与设备分离如果要深入了解驱动与设备分离,请看LINUX驱动的书籍。
    ' @+ E; [5 b) j7 N, Y什么是设备?我认为的设备就是「属性」,就是「参数」,就是「驱动程序要用到的数据和硬件接口信息」。那么驱动就是「控制这些数据和接口的代码过程」2 ^; J+ m" b7 ^! ^& z  I) ~: m# W
    通常来说,如果LCD的驱动IC相同,就用相同的驱动。有些不同的IC也可以用相同的,例如SSD1315跟STR7565,除了初始化,其他都可以用相同的驱动。例如一个COG lcd:
    0 Q7 k. S3 y7 L8 ~5 N) o/ N( U9 G?驱动IC是STR7565 128 * 64 像素用SPI3背光用PF5 ,命令线用PF4 ,复位脚用PF3
    & V% a; R- v/ v# |% j) N?
    上面所有的信息综合,就是一个设备。驱动就是STR7565的驱动代码。
    ; _. Q) G# R6 z# y+ H2 H- ^为什么要驱动跟设备分离,因为要解决下面问题:8 y8 _1 a9 |. H( I/ u! ?. k# J+ D9 m
    ?有一个新产品,收银设备。系统有两个LCD,都是OLED,驱动IC相同,但是一个是128x64,另一个是128x32像素,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。) g. A0 \, R% A; Y+ l3 k
    ?
    这个问题,「两个设备用同一套程序控制」才是最好的解决办法。驱动与设备分离的手段:
    ! i% R3 V; i; ^0 s% P; p" Y3 O3 c?在驱动程序接口函数的参数中增加设备参数,驱动用到的所有资源从设备参数传入。
    4 }: C) a0 t  c?
    驱动如何跟设备绑定呢?通过设备的驱动IC型号。
    , D# z. H2 p# [( N% c模块化我认为模块化就是将一段程序封装,提供稳定的接口给不同的驱动使用。不模块化就是,在不同的驱动中都实现这段程序。例如字库处理,在显示汉字的时候,我们要找点阵,在打印机打印汉字的时候,我们也要找点阵,你觉得程序要怎么写?把点阵处理做成一个模块,就是模块化。非模块化的典型特征就是「一根线串到底,没有任何层次感」
    7 a1 o' c8 x& }) @7 B0 DLCD到底是什么前面我们说了面向对象,现在要对LCD进行抽象,得出一个对象,就需要知道LCD到底是什么。问自己下面几个问题:
    / t' w7 s8 v: a! A2 d; ]4 i
  • LCD能做什么?
  • 要LCD做什么?
  • 谁想要LCD做什么?刚刚接触嵌入式的朋友可能不是很了解,可能会想不通。我们模拟一下LCD的功能操作数据流。APP想要在LCD上显示 一个汉字。9 e, n6 h: {" Z/ C  ^0 H
    1、首先,需要一个显示汉字的接口,APP调用这个接口就可以显示汉字,假设接口叫做lcd_display_hz。+ T' b; d& w1 w9 e5 J  t
    2、汉字从哪来?从点阵字库来,所以在lcd_display_hz函数内就要调用一个叫做find_font的函数获取点阵。
    + X2 k" s& X6 k7 J0 H$ K$ d3、获取点阵后要将点阵显示到LCD上,那么我们调用一个ILL9341_dis的接口,将点阵刷新到驱动IC型号为ILI9341的LCD上。; ?/ ?5 Z0 K& O( t6 T  D+ V
    4、ILI9341_dis怎么将点阵显示上去?调用一个8080_WRITE的接口。
    6 b5 S& r2 f% a* C9 x/ I好的,这个就是大概过程,我们从这个过程去抽象LCD功能接口。汉字跟LCD对象有关吗?无关。在LCD眼里,无论汉字还是图片,都是一个个点。那么前面问题的答案就是:1 T' h" b7 @0 @  c
  • LCD可以一个点一个点显示内容。
  • 要LCD显示汉字或图片-----就是显示一堆点
  • APP想要LCD显示图片或文字。结论就是:所有LCD对象的功能就是显示点。「那么驱动只要提供显示点的接口就可以了,显示一个点,显示一片点。」 抽象接口如下:# V% o+ G* N2 F
    /*/ F/ c: C" S4 Y  p
        LCD驱动定义
    0 f, U" V  t. P+ N*/0 Q' F( S; q1 I0 ^
    typedef struct  
    " G- m: e" x- h8 W{
    4 M6 f+ Y4 A, t" n' w( z  c    u16 id;8 H1 E7 B8 F- G: R
        s32 (*init)(DevLcd *lcd);
    ) ~, L, D9 S) d; S    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);- w1 d9 y$ j9 c5 S% a2 H( ?0 U
        s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);0 v. ^$ `$ o+ n
        s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);3 O, M7 Y2 q' {
        s32 (*onoff)(DevLcd *lcd, u8 sta);
    9 {; z+ z4 @) t% o; p' G    s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);' H6 d# ]' U+ f% ~
        void (*set_dir)(DevLcd *lcd, u8 scan_dir);8 O% J$ |$ c, x( M( C
        void (*backlight)(DevLcd *lcd, u8 sta);/ e0 m' C9 Z8 y3 ^, z
    }_lcd_drv;$ t3 U7 ]( s3 b9 I' d6 B! y
    上面的接口,也就是对应的驱动,包含了一个驱动id号。* p" ]7 f; O% q2 g. {
  • id,驱动型号
  • 初始化
  • 画点
  • 将一片区域的点显示某种颜色
  • 将一片区域的点显示某些颜色
  • 显示开关
  • 准备刷新区域(主要彩屏直接DMA刷屏使用)
  • 设置扫描方向
  • 背光控制显示字符,划线等功能,不属于LCD驱动。应该归类到GUI层。
    " [- _+ s5 e7 `+ Y6 z6 p5 p" v; D- _; X  ^LCD驱动框架我们设计了如下的驱动框架:! H& P9 [, X5 w% {7 ^+ J# |

    efnzvdtm2qh64023326206.jpg

    efnzvdtm2qh64023326206.jpg

    . ?5 @8 g; j) W设计思路:
    ' Q+ Z% H& \3 _1、中间显示驱动IC驱动程序提供统一接口,接口形式如前面说的_lcd_drv结构体。( R2 \% v  q1 M/ C7 {
    2、各显示IC驱动根据设备参数,调用不同的接口驱动。例如TFT就用8080驱动,其他的都用SPI驱动。SPI驱动只有一份,用IO口控制的我们也做成模拟SPI。' L8 N0 [9 ]* \8 ^
    3、LCD驱动层做LCD管理,例如完成TFT LCD的识别。并且将所有LCD接口封装为一套接口。
      W, Y* b; O) K4、简易GUI层封装了一些显示函数,例如划线、字符显示。2 f& J9 P" v: x; P# [# p- u0 f
    5、字体点阵模块提供点阵获取与处理接口。, |# j4 x% D& D* q1 e
    由于实际没那么复杂,在例程中我们将GUI跟LCD驱动层放到一起。TFT LCD的两个驱动也放到一个文件,但是逻辑是分开的。OLED除初始化,其他接口跟COG LCD基本一样,因此这两个驱动也放在一个文件。
    4 f! m. t; P6 M6 c+ l: n7 n2 ]代码分析代码分三层:" i* N9 b: u0 L4 M# |( R
    1、GUI和LCD驱动层 dev_lcd.c dev_lcd.h
    : `8 H1 h0 N& U7 Q2、显示驱动IC层 dev_str7565.c & dev_str7565.h dev_ILI9341.c & dev_ILI9341.h
    8 }. n+ I( j  c3 `" H1 c/ h1 [. b. }3、接口层 mcu_spi.c & mcu_spi.h stm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h8 U- S+ V0 h: @; l4 b0 G
    GUI和LCD层这层主要有3个功能 :' D/ H+ `& |$ O. t) T
    「1、设备管理」
    ! j5 M, o% h. U' L5 U6 `首先定义了一堆LCD参数结构体,结构体包含ID,像素。并且把这些结构体组合到一个list数组内。+ |( t( u3 t$ S2 Q  Q. C$ `
    /*  各种LCD的规格参数*/8 K4 }7 T9 o0 L# `
    _lcd_pra LCD_IIL9341 ={
    & n" S( h# x# Y! S8 X, Q! v6 s# q        .id   = 0x9341,
    $ i" p& V( i# x! {# u2 s- H9 [        .width = 240,   //LCD 宽度
    " V! M/ a2 `1 H2 U0 V        .height = 320,  //LCD 高度, a3 p& m2 d# B# F/ k
    };. |! x" @9 i0 k; u7 k5 s
    ...4 l( A3 y# z7 j2 t% f  B
    /*各种LCD列表*/' w9 t' g& R( ?+ |9 K
    _lcd_pra *LcdPraList[5]=( |% p4 T& e3 }
                {4 D# Q4 F# Q2 G9 r$ o( z/ j
                    &LCD_IIL9341,      
    % h) f* X4 P6 X- c, U& F( a                &LCD_IIL9325,
    + D  F2 R/ r4 M* M                &LCD_R61408,
    . F1 `& f1 L. Z- p  ?                &LCD_Cog12864,, g  N0 y) t: x/ {
                    &LCD_Oled12864,: O4 q/ l; K; u( P" O* E3 ^
                };
    5 z0 W0 T# @+ p# z然后定义了所有驱动list数组,数组内容就是驱动,在对应的驱动文件内实现。
    2 B. C; D. o. E4 R8 G$ F/*  所有驱动列表8 c# s$ x# N6 F7 ~8 e
        驱动列表*/5 B! m' _- @' a( L( i5 M
    _lcd_drv *LcdDrvList[] = {
    1 A1 r' [; h5 j  [1 l                    &TftLcdILI9341Drv,
    2 j0 h( V( r$ `0 \9 i& H  ?7 ?                    &TftLcdILI9325Drv,
    7 ^, D& B7 ?! y" V                    &CogLcdST7565Drv,
    6 S4 _; E, H9 A; ?6 P                    &OledLcdSSD1615rv," G0 d8 o; H3 {+ b* f7 L; e" |
    定义了设备树,即是定义了系统有多少个LCD,接在哪个接口,什么驱动IC。如果是一个完整系统,可以做成一个类似LINUX的设备树。
    5 G2 k( M7 |4 ], u# N  r- q/ q/*设备树定义*/6 P4 L4 N- T; p/ ~) V/ L% R3 J
    #define DEV_LCD_C 3//系统存在3个LCD设备) e2 q7 H! y9 W$ U
    LcdObj LcdObjList[DEV_LCD_C]=
    & j+ e, X0 X, t0 l( G6 X7 l{
    & @3 L' |2 h4 y5 E; {3 n    {"oledlcd", LCD_BUS_VSPI, 0X1315},: E/ n& E- a( Y. P% ]# k
        {"coglcd", LCD_BUS_SPI,  0X7565},7 X" b  v0 E; C" }, t" l
        {"tftlcd", LCD_BUS_8080, NULL},
    # t; ^* J: x2 S: f};
    4 J! k0 ]4 F, {2 O9 ^「2 、接口封装」
    : \+ F9 V9 U3 X4 V6 `7 X" j, d; Svoid dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir)2 n1 [6 a6 F! E# O- ^
    s32 dev_lcd_init(void)  l8 w) p# B& X) s: v( ~: S
    DevLcd *dev_lcd_open(char *name)4 h$ q( H7 p, E
    s32 dev_lcd_close(DevLcd *dev)
    $ n+ V1 x6 b# l/ m) U4 Ds32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)8 K# o" l. Z) D) f
    s32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)
    / Z: P# Z, l# Js32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta)
    ) k1 ?( H6 G& F+ x6 F# xs32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color)# L" r! V+ Y6 U1 ?7 [
    s32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)$ O! s7 c" ]; \( y6 z1 `$ R
    s32 dev_lcd_backlight(DevLcd *lcd, u8 sta)
    * m( @' G8 b- v" k+ Q- @大部分接口都是对驱动IC接口的二次封装。有区别的是初始化和打开接口。初始化,就是根据前面定义的设备树,寻找对应驱动,找到对应设备参数,并完成设备初始化。打开函数,根据传入的设备名称,查找设备,找到后返回设备句柄,后续的操作全部需要这个设备句柄。
    $ Y5 Q2 s6 ], S' g6 {! e) b* O「3 、简易GUI层」
    / [/ D: y# W$ q6 W& X# q目前最重要就是显示字符函数。  S6 U' V0 z6 n- A! @
    s32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)4 m7 A, n0 r" X7 v
    其他划线画圆的函数目前只是测试,后续会完善。& j6 L9 z+ P4 m
    驱动IC层驱动IC层分两部分:
    9 q! S* ]9 B& o/ w6 \% q2 M8 b8 T1 \8 u2 M「1 、封装LCD接口」
    5 m- L, k1 u7 e% `0 w, n$ T+ ~9 QLCD有使用8080总线的,有使用SPI总线的,有使用VSPI总线的。这些总线的函数由单独文件实现。但是,除了这些通信信号外,LCD还会有复位信号,命令数据线信号,背光信号等。我们通过函数封装,将这些信号跟通信接口一起封装为「LCD通信总线」, 也就是buslcd。BUS_8080在dev_ILI9341.c文件中封装。BUS_LCD1和BUS_lcd2在dev_str7565.c 中封装。
    . d$ k2 B; X8 g- q0 o6 M「2 驱动实现」) t" w2 q" B& k* C
    实现_lcd_drv驱动结构体。每个驱动都实现一个,某些驱动可以共用函数。& Q4 B3 S% K$ A0 M) w8 D
    _lcd_drv CogLcdST7565Drv = {/ p  r1 ~* N. p
                                .id = 0X7565,0 M; p8 f' Y2 K1 e  r
                                .init = drv_ST7565_init,# |- ]! \0 K9 ?! e
                                .draw_point = drv_ST7565_drawpoint,
    6 R7 b; A& K3 C( p8 ]( o3 s: I                            .color_fill = drv_ST7565_color_fill,
    ! v6 `) o4 s% A! |                            .fill = drv_ST7565_fill,
    9 V2 F- c8 a# V  r) o# f                            .onoff = drv_ST7565_display_onoff,) H$ ^  X) s% `, ~  B4 `) J" |, w
                                .prepare_display = drv_ST7565_prepare_display,- ~) y  B9 p& H0 ~  P' Z% w
                                .set_dir = drv_ST7565_scan_dir,
      P& g: W" [( e6 C3 E                            .backlight = drv_ST7565_lcd_bl
      Z) a8 h8 ~% w                            };
    2 ]# w2 A  U$ h( }, K+ d接口层8080层比较简单,用的是官方接口。SPI接口提供下面操作函数,可以操作SPI,也可以操作VSPI。5 d- y( y7 h/ A5 d* h& x
    extern s32 mcu_spi_init(void);
    8 E( q$ E% O: U" G: m, Dextern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);4 H1 N  K9 O0 N: X1 a2 M
    extern s32 mcu_spi_close(SPI_DEV dev);
    9 w3 A$ g+ X+ f; R3 V3 j9 Lextern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);
    , x/ n0 |+ a+ N( ~* h1 _extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);
    : k" y7 P. V+ v0 Z! k# |' a至于SPI为什么这样写,会有一个单独文件说明。7 B8 j8 n( W3 e
    总体流程前面说的几个模块时如何联系在一起的呢?请看下面结构体:
    ) ^% z. r2 t0 U6 `+ L- y7 f) _/*  初始化的时候会根据设备数定义,2 _; j# y2 W  ?( ?4 S
        并且匹配驱动跟参数,并初始化变量。- b; T* ~/ \7 K" W0 T
        打开的时候只是获取了一个指针 */* y2 e7 U! ]/ H3 X% I3 z9 m
    struct _strDevLcd$ G3 ~3 j) B% h& T% }
    {
    4 P; ^+ U' I$ A9 H( ~7 d! Q    s32 gd;//句柄,控制是否可以打开
    ' W  H* Y! x( K4 [1 }  a" z    LcdObj   *dev;8 E9 w3 H, H% _0 t; X( l
        /* LCD参数,固定,不可变*/
    % B1 l8 X6 W  v    _lcd_pra *pra;; w5 c, n4 x9 Q0 d) u9 _9 c6 {
        /* LCD驱动 */
    6 f% S# Q+ W+ J# k    _lcd_drv *drv;) s/ A' d. c1 c; M
        /*驱动需要的变量*/$ e7 _5 B. |& b& X4 Z( ?
        u8  dir;    //横屏还是竖屏控制:0,竖屏;1,横屏。
    1 k- z- q% J& v' Q9 y    u8  scandir;//扫描方向! d% {7 v& g' [, K; i. S/ W
        u16 width;  //LCD 宽度9 T( y! H( N( c, g3 Q5 e
        u16 height; //LCD 高度
    2 h8 A) X; E& k* C7 H* k8 z4 K9 `! O    void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存
    / _, g/ V! m, f# N4 }7 r% }};
    $ j% [5 o0 X5 X9 \# ~每一个设备都会有一个这样的结构体,这个结构体在初始化LCD时初始化。
    : ~8 m# o! @  _$ E& c
  • 成员dev指向设备树,从这个成员可以知道设备名称,挂在哪个LCD总线,设备ID。typedef struct% u% _3 F1 V" A0 ^+ U" u
    {, B/ n' b' a4 J' n# ?7 J0 U1 u
        char *name;//设备名字4 R. `. Q5 u5 W2 Y! g' B. ~
        LcdBusType bus;//挂在那条LCD总线上
    , B- y4 F+ X5 [8 [/ H5 d4 E& D    u16 id;" s  |% Y- @1 e
    }LcdObj;1 Q# |8 K5 M: J/ k" `
  • 成员pra指向LCD参数,可以知道LCD的规格。typedef struct8 N& i9 p3 f2 m' x! P
    {
    " h2 A) d* d8 B" y" s+ [3 o& v    u16 id;
    " L! |6 {) y& S! P    u16 width;  //LCD 宽度  竖屏
    ! |- }9 x/ _# H6 P2 R; y4 i    u16 height; //LCD 高度    竖屏- w5 g2 {+ [0 d5 t- C
    }_lcd_pra;
    $ |) Y# B' V3 B$ R
  • 成员drv指向驱动,所有操作通过drv实现。typedef struct  
    ) Q( V. S! Q" Z( Z# z: P{* e$ _2 O& \( i# [0 e: `% U" n
        u16 id;
    ; I6 G; f9 j7 ^9 e' E    s32 (*init)(DevLcd *lcd);
    $ h9 m& c0 V: h! _6 n0 ^    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
    7 y' T! t# d) g2 U- m    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);; }' D2 ?% t; x5 b2 P" z
        s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);# }- z( U3 }: T. m# _- y- j
        s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);
    % M" W  Y& ^$ D  Y. f    s32 (*onoff)(DevLcd *lcd, u8 sta);8 q) k2 [" g) G8 q6 n8 {: R
        void (*set_dir)(DevLcd *lcd, u8 scan_dir);8 e/ ?7 q2 J' @3 e
        void (*backlight)(DevLcd *lcd, u8 sta);$ @) v& b# c% b8 Z" l
    }_lcd_drv;
    & }! a. e. ~/ ^, U) X1 q5 T
  • 成员dir、scandir、 width、 height是驱动要使用的通用变量。因为每个LCD都有一个结构体,一套驱动程序就能控制多个设备而互不干扰。
  • 成员pri是一个私有指针,某些驱动可能需要有些比较特殊的变量,就全部用这个指针记录,通常这个指针指向一个结构体,结构体由驱动定义,并且在设备初始化时申请变量空间。目前主要用于COG LCD跟OLED LCD显示缓存。整个LCD驱动,就通过这个结构体组合在一起。
    & F3 q: P7 M8 K! e- x  x% t/ R1、初始化,根据设备树,找到驱动跟参数,然后初始化上面说的结构体。1 _# K, A$ g' _: D
    2、要使用LCD前,调用dev_lcd_open函数。打开成功就返回一个上面的结构体指针。
    0 q$ s% Y5 W' B: G- x; I5 O3、显示字符,接口找到点阵后,通过上面结构体的drv,调用对应的驱动程序。
    ( d/ X/ E6 i, h0 m4、驱动程序根据这个结构体,决定操作哪个LCD总线,并且使用这个结构体的变量。: D2 E" ^' u" r1 j
    用法和好处
  • 好处1请看测试程序, c$ v7 }) x: A/ |  J- f
    void dev_lcd_test(void): z. b5 N4 p9 z, @7 [# y* C9 x
    {
    ' m0 l1 ~$ {6 N  ?" a' m4 F/ u' }    DevLcd *LcdCog;
    6 w- q& [6 U! r  P  Z9 Z    DevLcd *LcdOled;7 d8 h' `2 U* A* g
        DevLcd *LcdTft;2 `% H+ a' J0 v* b4 v9 y. I
        /*  打开三个设备 */  K5 X' i  a' {1 ]8 t
        LcdCog = dev_lcd_open("coglcd");
    3 A! L3 K' M% P3 [6 e  }5 f  B    if(LcdCog==NULL)
    . d% q. d4 I! i9 S( m& S5 m        uart_printf("open cog lcd err\r8 ]! s* B$ i4 F( H+ t1 w% T$ w
    ");5 g3 \$ T5 v& T1 E
        LcdOled = dev_lcd_open("oledlcd");/ H3 }' t6 |; D% }% B4 K
        if(LcdOled==NULL)
    - d* k2 O$ G- @; m1 M3 E4 m        uart_printf("open oled lcd err\r
    ; h2 b& n$ x! B");' \7 D2 e! b1 k
        LcdTft = dev_lcd_open("tftlcd");
    ; f) h& j: z2 g    if(LcdTft==NULL)
    ; q+ K" P8 _  {/ c        uart_printf("open tft lcd err\r
    $ Q5 ?3 a" E; x* A5 Z1 t3 v3 ~5 D% ?");
    " l/ S+ Y8 a3 L, o2 u    /*打开背光*/
    ( e5 ~# ]2 f* I5 ^& ~7 u- a4 \3 P    dev_lcd_backlight(LcdCog, 1);
    % |+ s: x' [+ N% j3 R( t. h- u    dev_lcd_backlight(LcdOled, 1);
    4 [  |0 L5 ~: w2 ^    dev_lcd_backlight(LcdTft, 1);
    , y* s+ I2 r9 b- d2 g8 q/ d    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
    ( X4 L5 [6 p: n" X. {    dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "这是oled lcd", BLACK);# R4 F( o/ V! m) Y, ~) w* G
        dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);
    ! e* S$ }/ T" p2 d2 b6 i3 Q* h4 t    dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);) N3 u3 v- V5 ?" u% L( A& \) f
        dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);, x4 D4 G6 j( L1 ]/ }* d
        dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是cog lcd", BLACK);( W5 O3 K: S; Z- D' o1 _/ y
        dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);6 r# y7 a* ^+ l
        dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);6 I  O  S% v' E0 d' h5 u9 ~
        dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);
    2 M# V; w/ o9 w    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是tft lcd", RED);5 A7 v1 s4 D. k* b2 a+ u
        dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);4 t! O, z+ M$ n8 c5 ~( E1 s3 j* ~( l* n
        dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "屋脊雀工作室", RED);
    + e5 X) `: d% E% m* r1 i7 E    while(1);
    : w( P# V& ]( I2 h% l8 t- e2 w+ m}
    * y; n, L3 U; R& \; G5 B2 ?; t使用一个函数dev_lcd_open,可以打开3个LCD,获取LCD设备。然后调用dev_lcd_put_string就可以在不同的LCD上显示。其他所有的gui操作接口都只有一个。这样的设计对于APP层来说,就很友好。显示效果:
    7 {9 W0 e8 `( o8 q$ u

    xim3fzlxcju64023326306.jpg

    xim3fzlxcju64023326306.jpg

    & E9 v. A  G; X
  • 好处2现在的设备树是这样定义的  e& `+ `& Q9 G6 W5 [3 v
    LcdObj LcdObjList[DEV_LCD_C]=
    4 M5 H. H$ s; H8 ~: N% {! y{
    1 G7 _$ r% m" b' @& `% j    {"oledlcd", LCD_BUS_VSPI, 0X1315},
    ; R! V( b: X8 M# j) f7 b# p  f    {"coglcd", LCD_BUS_SPI,  0X7565},5 o$ W3 a+ h& L, K! t6 a, e1 L4 v
        {"tftlcd", LCD_BUS_8080, NULL},
    2 C* N  w4 J; Z% X+ t};
    / i9 O& g5 k4 y& Q3 c0 K7 i某天,oled lcd要接到SPI上,只需要将设备树数组里面的参数改一下,就可以了,当然,在一个接口上不能接两个设备。, G9 u+ m/ ~* }' o
    LcdObj LcdObjList[DEV_LCD_C]=5 J  `7 t9 U* @
    {& J$ I9 n/ ~4 l7 x. O$ [  E
        {"oledlcd", LCD_BUS_SPI, 0X1315},
    # [% g3 Z- s1 d7 S    {"tftlcd", LCD_BUS_8080, NULL},
    . c3 e+ h5 a6 H, ?. a9 O  T: c};+ I( Q! d# W4 S% y/ I
    字库暂时不做细说,例程的字库放在SD卡中,各位移植的时候根据需要修改。具体参考font.c。
    - X/ U, q- U6 V$ W& _! e; l, t声明代码请按照版权协议使用。当前源码只是一个能用的设计,完整性与健壮性尚未测试。后续会放到github,并且持续更新优化。最新消息请关注www.wujique.com。
    ( L! n6 q4 W- Q; w7 n0 D  V-END-
    7 C  B) g  L$ h8 y& x6 Y% x# n9 ^往期推荐:点击图片即可跳转阅读3 i$ Z4 w& V0 u4 ^$ O9 S% m
                                                           
    3 E2 t9 P) Y) d                                                               
    , \7 Q6 j; E% L4 {) C3 `                                                                        , r2 v) o8 g5 p1 [  N( x* I# m
                                                                                    / G' {% d9 K' P$ M1 ]

    q1sbxil0mga64023326406.jpg

    q1sbxil0mga64023326406.jpg

    - u& T5 R0 w* F2 {+ b                                                                                * j. z1 d0 C3 B! w+ D
                                                                                            浅谈为何不该入行嵌入式技术开发
    2 n1 j6 S! @) F7 e) j! v                                                                               
    & R5 O& O# a/ n; H* P                                                                        : S" n& j- z4 \/ i3 }' d
                                                                    - E9 _" l( W& @8 u2 F8 L6 U2 b
                                                           
    ; `' B5 J# s; u( d6 _                                               
    2 i" q9 `5 j0 L
    ' O0 q  z$ n3 s! W7 G0 A                                                       
    - j6 {5 p/ [: n$ M" {7 I% C. M                                                                % f! k% Q9 B; i( e0 A2 ?4 V
                                                                           
    4 p. K! t+ T' A7 K                                                                               
    7 ^6 r- F. d* m

    nzecwdmixdw64023326506.jpg

    nzecwdmixdw64023326506.jpg

    ( p* s; E/ z# l( T) Z" L                                                                                / T, P! r9 U" r, B/ F4 W
                                                                                            在深圳搞嵌入式,从来没让人失望过!  p" a* r. m) l
                                                                                   
    7 b. L& M9 h% m* n( y                                                                          X+ S1 j. v( q3 ~/ o& C! x2 F9 F
                                                                    $ H' [0 s9 e& J5 Q$ D3 X
                                                            / D& P' |8 h( k  B! n, N- |
                                                   
    ; P* I7 X! X* n+ X0 o" N3 O- w" Q& V5 i& i  i% k
                                                           
    4 {( L: {6 Z! r0 W2 Q' Y                                                               
    6 x' g' m4 L) ~+ c                                                                        9 ]2 C( m( U' X
                                                                                    % L, J- L- G& i8 k; g

    lv5rjot3gme64023326606.jpg

    lv5rjot3gme64023326606.jpg

    1 K; q; i( Z( Z2 J1 q                                                                               
    8 t/ O* a' y5 y: \* F                                                                                        苹果iPhone16发布了,嵌入式鸿蒙,国产化程度有多高?
    6 [. }/ v. ~/ t) n1 f                                                                               
    * h2 p% k- J% e3 A9 r, e# u7 _4 D                                                                       
      l9 M: O4 O  o" s" t                                                                6 e% Y) r: A8 u& }. j. ?- _
                                                            # |( Q5 u* p4 f/ Y( @) d
                                                    我是老温,一名热爱学习的嵌入式工程师
    ! E* I- X0 J; s" \2 o关注我,一起变得更加优秀!
  • 回复

    使用道具 举报

    发表回复

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

    本版积分规则


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