电子产业一站式赋能平台

PCB联盟网

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

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

[复制链接]

478

主题

478

帖子

1544

积分

三级会员

Rank: 3Rank: 3

积分
1544
发表于 2024-9-19 17:50:00 | 显示全部楼层 |阅读模式
我是老温,一名热爱学习的嵌入式工程师. U, B. d; `+ N" J2 O5 n" F
关注我,一起变得更加优秀!  e/ I7 t. Y( v. y

% ~/ P8 V# s8 r% j# K5 R& {来源 | 屋脊雀
8 f- U8 }% g$ o! o, k网络上配套STM32开发板有很多LCD例程,主要是TFT LCD跟OLED的。从这些例程,大家都能学会如何点亮一个LCD。但这代码都有下面这些问题:
3 g7 A2 Z0 F- W' K
  • 分层不清晰,通俗讲就是模块化太差。
  • 接口乱。只要接口不乱,分层就会好很多了。
  • 可移植性差。
  • 通用性差。为什么这样说呢?如果你已经了解了LCD的操作,请思考如下情景:; y% v, n$ u4 P9 E# L
    1、代码空间不够,只能保留9341的驱动,其他LCD驱动全部删除。能一键(一个宏定义)删除吗?删除后要改多少地方才能编译通过?9 q1 |$ F" o/ M% e
    2、有一个新产品,收银设备。系统有两个LCD,都是OLED,驱动IC相同,但是一个是128x64,另一个是128x32像素,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。怎么办?这些例程代码要怎么改才能支持两个屏幕?全部代码复制粘贴然后改函数名称?这样确实能完成任务,只不过程序从此就进入恶性循环了。  m8 W# ]$ B( L2 ^3 _: g
    3、一个OLED,原来接在这些IO,后来改到别的IO,容易改吗?
    1 W+ M  E: P3 D# H. U/ F# l4、原来只是支持中文,现在要卖到南美,要支持多米尼加语言,好改吗?3 M7 ~8 o7 I0 Z+ g* L3 F3 |
    LCD种类概述在讨论怎么写LCD驱动之前,我们先大概了解一下嵌入式常用LCD。概述一些跟驱动架构设计有关的概念,在此不对原理和细节做深入讨论,会有专门文章介绍,或者参考网络文档。
    1 k* s$ w% X. Q( C  y7 Y  ~/ B! Z: ?TFT lcdTFT LCD,也就是我们常说的彩屏。通常像素较高,例如常见的2.8寸,320X240像素。4.0寸的,像素800X400。这些屏通常使用并口,也就是8080或6800接口(STM32 的FSMC接口);或者是RGB接口,STM32F429等芯片支持。其他例如手机上使用的有MIPI接口。
    1 R2 S7 [( B# w  [总之,接口种类很多。也有一些支持SPI接口的。除非是比较小的屏幕,否则不建议使用SPI接口,速度慢,刷屏闪屏。玩STM32常用的TFT lcd屏幕驱动IC通常有:ILI9341/ILI9325等。
    ' i8 J3 A; o' g7 T5 ztft lcd:
    ( x* i# f9 M4 P4 L' P5 P% D# |/ ^1 P1 ~" d8 r( K
    IPS:  `5 x% [$ |4 d: r* J; x. Q

    4nqs423sgba64035223722.jpg

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

    r54yd031wzu64035223822.jpg

    r54yd031wzu64035223822.jpg

    # n( I/ h" q) l* s这种LCD通常像素不高,常用的有128X64,128X32。一般只支持黑白显示,也有灰度屏。
    . X& q8 _9 I- `  O接口通常是SPI,I2C。也有号称支持8位并口的,不过基本不会用,3根IO能解决的问题,没必要用8根吧?常用的驱动IC:STR7565。3 f9 ]- b# O& y& [; M1 Y3 M) A6 S
    OLED lcd买过开发板的应该基本用过。新技术,大家都感觉高档,在手环等产品常用。OLED目前屏幕较小,大一点的都很贵。在控制上跟COG LCD类似,区别是两者的显示方式不一样。从我们程序角度来看,最大的差别就是,OLED LCD,不用控制背光。。。。。实物如下图:
    & J- U5 `- `2 D- b) K
    ( @$ a1 R8 a0 J) w" s, S5 o3 }常见的是SPI跟I2C接口。常见驱动IC:SSD1615。( B' A* b% H3 v
    硬件场景接下来的讨论,都基于以下硬件信息:) O8 X1 [/ t# u! m, D; g
    1、有一个TFT屏幕,接在硬件的FSMC接口,什么型号屏幕?不知道。" d9 }8 f3 S. L0 |
    2、有一个COG lcd,接在几根普通IO口上,驱动IC是STR7565,128X32像素。- [1 b. z0 |8 n$ l+ h8 X3 y
    3、有一个COG LCD,接在硬件SPI3跟几根IO口上,驱动IC是STR7565,128x64像素。! B6 d% _9 s% f3 G' C% m
    4、有一个OLED LCD,接在SPI3上,使用CS2控制片选,驱动IC是SSD1315。# E5 R' F8 P6 Q  e+ a
    ( |+ _& l- X1 Q: a8 Y# ]4 Y
    预备知识在进入讨论之前,我们先大概说一下下面几个概念,对于这些概念,如果你想深入了解,请GOOGLE。
    ) x% l" E" t1 w2 Q面向对象面向对象,是编程界的一个概念。什么叫面向对象呢?编程有两种要素:程序(方法),数据(属性)。例如:一个LED,我们可以点亮或者熄灭它,这叫方法。LED什么状态?亮还是灭?这就是属性。我们通常这样编程:
    5 Q/ n1 T2 N* {% a# q8 Pu8 ledsta = 0;
    + R* c& K. L: c* M6 w* [9 H# Qvoid ledset(u8 sta)) i; Q* M2 Z) T  ~- S5 ~
    {
    , c  k( X* I* A* U, L. y}
    7 R9 [! Y2 O$ |1 K这样的编程有一个问题,假如我们有10个这样的LED,怎么写?这时我们可以引入面向对象编程,将每一个LED封装为一个对象。可以这样做:( x1 l) f. ~  g5 @) K; _. v
    /*
    ( R( M# h0 o' D& b0 g定义一个结构体,将LED这个对象的属性跟方法封装。
    0 ]+ u$ `( E( ~- x这个结构体就是一个对象。
    ( V( [" {( [: x4 |1 a% I但是这个不是一个真实的存在,而是一个对象的抽象。
    7 c0 a0 I& Y* \0 l' B8 [1 @  G*/0 y3 K. T3 F" |- s: D6 o$ B$ r
    typedef struct{
    0 L$ c7 e. q( t! Q; v" L* ~4 I! _    u8 sta;* c" ~% D2 b( O* F# S
        void (*setsta)(u8 sta);3 J/ a, C6 v! t5 C! ?& o) \
    }LedObj;. E* U: @/ L6 W6 t
    /*  声明一个LED对象,名称叫做LED1,并且实现它的方法drv_led1_setsta*/
    ! J8 C3 j5 u2 A* c& [0 Vvoid drv_led1_setsta(u8 sta)- v% F- C. M1 z
    {
    5 h% n, ?4 r/ z. }. |/ f* t}
    ' m7 z; g! _6 {+ \- HLedObj LED1={
    + Z( @. g* R7 I. |        .sta = 0,
    ! z( E5 [! ?1 z6 k5 W        .setsta = drv_led1_setsta,
    7 g" ]; h4 }, o" n6 w& W# h1 W9 m    };" D  `. J1 T. @* Z
    /*  声明一个LED对象,名称叫做LED2,并且实现它的方法drv_led2_setsta*/5 B& D7 C9 K5 e( T6 `
    void drv_led2_setsta(u8 sta)! Y- Z$ y# J8 l
    {, S/ W% q9 g3 H$ q5 O
    }
    ; h) x6 u- P" {) c6 m+ ]) a% zLedObj LED2={
    8 V1 h/ x2 i5 A. z9 \3 D# R# R        .sta = 0,: `* @# ]/ h1 [! w; N5 g0 ~+ R  Q% A
            .setsta = drv_led2_setsta,4 }, ?) Z' x" w
        };- J# z0 @* K3 @9 g5 E1 B
        6 I4 h- }  ^4 k7 s- I% l
    /*  操作LED的函数,参数指定哪个led*/" o$ l2 f8 H0 s% }
    void ledset(LedObj *led, u8 sta)
    6 y5 Q) K5 S; h, J{8 z8 S3 |3 I+ i+ B$ [
        led->setsta(sta);
    . n4 L$ x- Q+ d4 w' C}
    - f  Z( Y: z. I' k& B是的,在C语言中,实现面向对象的手段就是结构体的使用。上面的代码,对于API来说,就很友好了。操作所有LED,使用同一个接口,只需告诉接口哪个LED。大家想想,前面说的LCD硬件场景。4个LCD,如果不面向对象,「显示汉字的接口是不是要实现4个」?每个屏幕一个?
    + u! [- q; H+ P) k* V驱动与设备分离如果要深入了解驱动与设备分离,请看LINUX驱动的书籍。& O4 J$ y; r! c( }0 Y7 y! A0 V6 D
    什么是设备?我认为的设备就是「属性」,就是「参数」,就是「驱动程序要用到的数据和硬件接口信息」。那么驱动就是「控制这些数据和接口的代码过程」+ j4 l; K) ]1 O2 M: b& {
    通常来说,如果LCD的驱动IC相同,就用相同的驱动。有些不同的IC也可以用相同的,例如SSD1315跟STR7565,除了初始化,其他都可以用相同的驱动。例如一个COG lcd:
    5 a# ]9 W1 }+ t?驱动IC是STR7565 128 * 64 像素用SPI3背光用PF5 ,命令线用PF4 ,复位脚用PF3
    ! R$ [  x- }0 ?! d) D: Y! l! U4 v?
    上面所有的信息综合,就是一个设备。驱动就是STR7565的驱动代码。- s( c) w0 Q2 y  `7 o
    为什么要驱动跟设备分离,因为要解决下面问题:" n( \- _4 n7 |, l. o
    ?有一个新产品,收银设备。系统有两个LCD,都是OLED,驱动IC相同,但是一个是128x64,另一个是128x32像素,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。& G- s& n7 \) i' {: y* @* R
    ?
    这个问题,「两个设备用同一套程序控制」才是最好的解决办法。驱动与设备分离的手段:
      O& I& U) ^2 u5 V?在驱动程序接口函数的参数中增加设备参数,驱动用到的所有资源从设备参数传入。
    # g1 Y, C. b6 p4 J1 C?
    驱动如何跟设备绑定呢?通过设备的驱动IC型号。
    / w& C/ I( E' L' s% E; U3 V模块化我认为模块化就是将一段程序封装,提供稳定的接口给不同的驱动使用。不模块化就是,在不同的驱动中都实现这段程序。例如字库处理,在显示汉字的时候,我们要找点阵,在打印机打印汉字的时候,我们也要找点阵,你觉得程序要怎么写?把点阵处理做成一个模块,就是模块化。非模块化的典型特征就是「一根线串到底,没有任何层次感」" J9 B6 T% s, p: o9 d
    LCD到底是什么前面我们说了面向对象,现在要对LCD进行抽象,得出一个对象,就需要知道LCD到底是什么。问自己下面几个问题:
    $ \( J# s" }* A9 L& V6 S4 L
  • LCD能做什么?
  • 要LCD做什么?
  • 谁想要LCD做什么?刚刚接触嵌入式的朋友可能不是很了解,可能会想不通。我们模拟一下LCD的功能操作数据流。APP想要在LCD上显示 一个汉字。
    + T: B- d' e7 a. f1、首先,需要一个显示汉字的接口,APP调用这个接口就可以显示汉字,假设接口叫做lcd_display_hz。: Z9 [- ?& x  M0 q9 `! M1 v& Z' ~: H" @* d
    2、汉字从哪来?从点阵字库来,所以在lcd_display_hz函数内就要调用一个叫做find_font的函数获取点阵。6 H2 Q8 U+ ~8 u) C( A# X$ T$ X. ]1 b8 _
    3、获取点阵后要将点阵显示到LCD上,那么我们调用一个ILL9341_dis的接口,将点阵刷新到驱动IC型号为ILI9341的LCD上。
    8 }% @# K% e/ g; P8 X' [! w3 f* Y1 S4、ILI9341_dis怎么将点阵显示上去?调用一个8080_WRITE的接口。; T8 h7 Y# C' S# W- ^
    好的,这个就是大概过程,我们从这个过程去抽象LCD功能接口。汉字跟LCD对象有关吗?无关。在LCD眼里,无论汉字还是图片,都是一个个点。那么前面问题的答案就是:# D$ l! q8 {6 b6 h2 K/ q6 w3 i
  • LCD可以一个点一个点显示内容。
  • 要LCD显示汉字或图片-----就是显示一堆点
  • APP想要LCD显示图片或文字。结论就是:所有LCD对象的功能就是显示点。「那么驱动只要提供显示点的接口就可以了,显示一个点,显示一片点。」 抽象接口如下:
    / K' z' @8 T6 R! q/*2 l4 f2 M; V! b5 i
        LCD驱动定义% F: K  X) z, d# E1 a* @/ w, _6 @
    */
    - M4 R8 Y3 F$ e! V: ftypedef struct  
    " I# _# ]& V' n- q{* g# R3 b2 z/ [
        u16 id;
    6 L2 C1 [, D8 o8 ]5 G- Y6 @2 y' l    s32 (*init)(DevLcd *lcd);6 W8 ]3 t! s9 H* {" M6 z5 u& n
        s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);( b3 d* q! J2 m6 y
        s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);9 S* c+ Z; @& t/ B+ o  Y
        s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);
    $ ]9 `/ I3 d, k- h* i+ z- c( J7 V    s32 (*onoff)(DevLcd *lcd, u8 sta);
    2 ~# ^/ z9 C5 \  T- c  d( ?5 P    s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);; \* A! j0 z* I1 J7 r$ U3 e7 P
        void (*set_dir)(DevLcd *lcd, u8 scan_dir);
    2 x: d- J$ n+ e    void (*backlight)(DevLcd *lcd, u8 sta);7 ]. u2 O* o" }  j" r( r8 g& I5 [
    }_lcd_drv;: D9 I) b; |- w5 r9 R( g2 U/ j
    上面的接口,也就是对应的驱动,包含了一个驱动id号。
    ' n& P- P5 R) j/ A  a. F
  • id,驱动型号
  • 初始化
  • 画点
  • 将一片区域的点显示某种颜色
  • 将一片区域的点显示某些颜色
  • 显示开关
  • 准备刷新区域(主要彩屏直接DMA刷屏使用)
  • 设置扫描方向
  • 背光控制显示字符,划线等功能,不属于LCD驱动。应该归类到GUI层。
    ) |. Z8 \8 h# v4 E- z3 oLCD驱动框架我们设计了如下的驱动框架:8 y  S" e2 `0 H- T7 v, ~, ?

    0 n- S0 e/ R9 I+ j% O6 T设计思路:* S2 n) {0 ]0 M3 R2 T2 M
    1、中间显示驱动IC驱动程序提供统一接口,接口形式如前面说的_lcd_drv结构体。9 A! ^1 Z' p$ x
    2、各显示IC驱动根据设备参数,调用不同的接口驱动。例如TFT就用8080驱动,其他的都用SPI驱动。SPI驱动只有一份,用IO口控制的我们也做成模拟SPI。
    6 h) ?$ V( y! W7 [3、LCD驱动层做LCD管理,例如完成TFT LCD的识别。并且将所有LCD接口封装为一套接口。2 V$ T/ _- y  z2 X+ d
    4、简易GUI层封装了一些显示函数,例如划线、字符显示。
    5 L) D5 I0 Q( a0 u5、字体点阵模块提供点阵获取与处理接口。, z0 |& I. \$ }) t: ]( K
    由于实际没那么复杂,在例程中我们将GUI跟LCD驱动层放到一起。TFT LCD的两个驱动也放到一个文件,但是逻辑是分开的。OLED除初始化,其他接口跟COG LCD基本一样,因此这两个驱动也放在一个文件。
    4 h& {2 {1 `: k  |1 y3 Y3 }1 G代码分析代码分三层:
    0 v5 a5 Z) a6 Y1、GUI和LCD驱动层 dev_lcd.c dev_lcd.h
    ( z4 T  X+ `7 U) i% x2、显示驱动IC层 dev_str7565.c & dev_str7565.h dev_ILI9341.c & dev_ILI9341.h
    , r! E9 u4 {! D$ T  U3、接口层 mcu_spi.c & mcu_spi.h stm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h
    $ S4 i. x6 j( I: J' V0 KGUI和LCD层这层主要有3个功能 :
      F5 `) @1 v, s8 X# S「1、设备管理」# O: h: U) W" _/ e# q: C
    首先定义了一堆LCD参数结构体,结构体包含ID,像素。并且把这些结构体组合到一个list数组内。0 ]% e0 p8 S* B( j
    /*  各种LCD的规格参数*/
    ; {% {0 _1 f; t# T$ I( X_lcd_pra LCD_IIL9341 ={" A+ X, a* y- r( e$ _2 N/ ^
            .id   = 0x9341,
    $ z/ \* }9 ^0 _* u        .width = 240,   //LCD 宽度5 n- h" q7 N" _  J0 }, _
            .height = 320,  //LCD 高度. B* @; j/ a# ?6 ]$ ~# S
    };
    $ o: g' Z: o0 ^6 p( p...
    - O' k5 t* m, P/*各种LCD列表*/
    - D9 q  ?) Z! d5 B! _" ?; P_lcd_pra *LcdPraList[5]=
    9 m% s5 l+ V$ [$ `. C            {
    ' n$ g& `) T0 H- K8 B7 M                &LCD_IIL9341,      
    ; o4 u( Y+ [" {' B! q, r                &LCD_IIL9325,
    ) m& Z8 d+ X' j1 O5 ^                &LCD_R61408,% @+ F9 D8 l5 A+ k( c
                    &LCD_Cog12864,, l0 I2 V* W, h# t4 D+ `" I$ i. K
                    &LCD_Oled12864,( X4 w! H; @" G! l/ w
                };
    % S6 p4 |$ O6 d' j3 O: Z然后定义了所有驱动list数组,数组内容就是驱动,在对应的驱动文件内实现。
    6 I& x1 |0 f$ H0 X) `7 L/*  所有驱动列表/ `& }. s0 s( B4 ^# r6 k  ^2 w8 R
        驱动列表*/
    + V# @: j# V+ @0 B! z6 O  l  D0 R7 o( w/ P_lcd_drv *LcdDrvList[] = {
    2 f* o0 f9 V9 x                    &TftLcdILI9341Drv,
    $ u& \& k) {( D8 W+ s1 W& m                    &TftLcdILI9325Drv,% {" T) F1 \8 s
                        &CogLcdST7565Drv,
    % b& ^% m% J, K7 e! e/ C1 D& t                    &OledLcdSSD1615rv,8 I* f2 s2 C# I4 G/ Q9 H: J. |
    定义了设备树,即是定义了系统有多少个LCD,接在哪个接口,什么驱动IC。如果是一个完整系统,可以做成一个类似LINUX的设备树。
    / w2 W( w% {1 m# \/*设备树定义*/
    % k1 |+ y8 I- A$ s# n: ^#define DEV_LCD_C 3//系统存在3个LCD设备
    + `5 u5 V/ S) j& C, pLcdObj LcdObjList[DEV_LCD_C]=
    * w, D# _& s  h8 H{
    1 s; g2 o9 K$ ^1 V. H+ A  v& W    {"oledlcd", LCD_BUS_VSPI, 0X1315}," |% a2 s* ~: t2 g
        {"coglcd", LCD_BUS_SPI,  0X7565},& x- Z) L6 r0 {% V2 V: t# n
        {"tftlcd", LCD_BUS_8080, NULL},
      N* d) q4 X: e4 U& S( Z5 h};8 J" x$ ~4 d7 v
    「2 、接口封装」+ i! A/ f- _8 }0 i& n4 O( F1 @( A
    void dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir)
    5 A! U. ^' v* k7 H1 As32 dev_lcd_init(void)
    . H1 v! k0 z1 M! I$ f. d1 G- n& BDevLcd *dev_lcd_open(char *name)
    0 g! L' E, t, js32 dev_lcd_close(DevLcd *dev)
    % M3 E. i0 T! u$ ]3 g; d' ls32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)
    5 P0 O  R5 D+ S6 t4 k, ~" b, ws32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)/ E+ n+ L' v% c4 j5 Z
    s32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta)8 \! b- [" p  U
    s32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color)2 q4 k* f1 k0 ?  N
    s32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)( ?" M8 \7 v2 o. U  \
    s32 dev_lcd_backlight(DevLcd *lcd, u8 sta)$ G! Y$ r9 S6 Z- \; p8 @, |2 a! A& D
    大部分接口都是对驱动IC接口的二次封装。有区别的是初始化和打开接口。初始化,就是根据前面定义的设备树,寻找对应驱动,找到对应设备参数,并完成设备初始化。打开函数,根据传入的设备名称,查找设备,找到后返回设备句柄,后续的操作全部需要这个设备句柄。9 ^+ s4 [. M3 M! e2 B, S! {
    「3 、简易GUI层」
    % C" t9 v8 ?) d2 q" A目前最重要就是显示字符函数。
    : ?6 N4 c5 O/ t, O. rs32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)
    7 D1 H5 F& R, S  t6 w其他划线画圆的函数目前只是测试,后续会完善。0 W+ @1 g4 n# _' X, {1 @
    驱动IC层驱动IC层分两部分:
    7 E* n, j: Z4 g2 M4 N「1 、封装LCD接口」/ f# T& N6 U6 f2 O
    LCD有使用8080总线的,有使用SPI总线的,有使用VSPI总线的。这些总线的函数由单独文件实现。但是,除了这些通信信号外,LCD还会有复位信号,命令数据线信号,背光信号等。我们通过函数封装,将这些信号跟通信接口一起封装为「LCD通信总线」, 也就是buslcd。BUS_8080在dev_ILI9341.c文件中封装。BUS_LCD1和BUS_lcd2在dev_str7565.c 中封装。; J  j6 ]8 |. U
    「2 驱动实现」
    3 D5 G2 P& p2 O! S实现_lcd_drv驱动结构体。每个驱动都实现一个,某些驱动可以共用函数。8 r. Z$ q0 ~5 Q% p
    _lcd_drv CogLcdST7565Drv = {
    + [$ w  U/ H8 y6 p5 @  ]                            .id = 0X7565,
    7 ~  |  a& c; X6 n& f+ F/ v6 Y) u                            .init = drv_ST7565_init,5 f( N" R- |; w1 r5 w
                                .draw_point = drv_ST7565_drawpoint,
    - a. w7 I1 r4 V, Y7 p                            .color_fill = drv_ST7565_color_fill,# _4 f3 Y7 b' }5 b$ G, U
                                .fill = drv_ST7565_fill,/ i7 V) r% {4 E4 p& }5 ~8 \
                                .onoff = drv_ST7565_display_onoff,
    ! a$ ?! |6 u1 P9 R4 G                            .prepare_display = drv_ST7565_prepare_display,
    : l3 d4 n* Z! y& ^9 l6 p+ ]                            .set_dir = drv_ST7565_scan_dir,
    . @$ ?! g. {  A$ x9 y6 F- K* i1 C; F                            .backlight = drv_ST7565_lcd_bl
    3 `) p) y9 W/ E2 I% X                            };& z+ ^. P0 x- ^  N, |& S# K8 e
    接口层8080层比较简单,用的是官方接口。SPI接口提供下面操作函数,可以操作SPI,也可以操作VSPI。5 h! N5 [( e+ d, B
    extern s32 mcu_spi_init(void);1 L, z  o6 q  v
    extern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);
    ' M2 I0 }9 m* |) Jextern s32 mcu_spi_close(SPI_DEV dev);
    ! K3 P! q  Q. jextern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);2 f8 M) e. @% m
    extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);
    4 b. @2 x6 \; A# e+ a" m: E至于SPI为什么这样写,会有一个单独文件说明。
    ( r0 _" ?+ n: \* S. J# ^/ E# X! A' {' O总体流程前面说的几个模块时如何联系在一起的呢?请看下面结构体:
    2 x! c: w' Z7 y- u6 g8 n/*  初始化的时候会根据设备数定义,
    / v5 Q1 u/ A1 L% M    并且匹配驱动跟参数,并初始化变量。
    - A- |4 D3 ^! h2 X    打开的时候只是获取了一个指针 */' I1 B7 _+ A5 j+ i0 P2 V
    struct _strDevLcd5 e, u8 G7 ]2 N' t- X0 ~4 X
    {
    + A% `5 L4 p! a% Z9 L    s32 gd;//句柄,控制是否可以打开% w$ I, [) v8 c" y/ K4 L/ K4 Q0 ?
        LcdObj   *dev;
    $ ~, d0 W) L' G' y    /* LCD参数,固定,不可变*/
    $ q. x2 I& S1 z7 P8 o8 Y% o    _lcd_pra *pra;
    ! G+ u: N& ]7 y2 z: r; V    /* LCD驱动 */. H- ^: D" x* y" O/ E! m2 w
        _lcd_drv *drv;
    1 E2 f( s& D5 S9 D' t    /*驱动需要的变量*/
    , Z2 G: h" ]2 I. |! y- p1 d    u8  dir;    //横屏还是竖屏控制:0,竖屏;1,横屏。- p* k' Z, w2 ^4 A. ~8 H
        u8  scandir;//扫描方向
    $ f* h7 ]1 L6 _  {; @3 [    u16 width;  //LCD 宽度
    - [" ]3 |3 D, [4 k7 W    u16 height; //LCD 高度! P3 t4 p: _2 C; a9 m
        void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存
    ' V3 E% K& J" d9 K, K% W6 B! `' Z};& \$ y! k  q7 m
    每一个设备都会有一个这样的结构体,这个结构体在初始化LCD时初始化。9 x- t. C+ M5 Y2 {1 z$ L( n+ r* S
  • 成员dev指向设备树,从这个成员可以知道设备名称,挂在哪个LCD总线,设备ID。typedef struct  K3 n, ^) d' [6 M0 m
    {# e, W) k$ K+ D8 o
        char *name;//设备名字9 \5 O4 D& i0 z& }" w5 p/ M/ X
        LcdBusType bus;//挂在那条LCD总线上
    0 U: E/ p  c" Y    u16 id;
    2 G" E' |+ x1 U$ H0 e7 u- F}LcdObj;7 g1 ]  x: K: h; M, i
  • 成员pra指向LCD参数,可以知道LCD的规格。typedef struct
    ! u9 N9 |5 C# \% j5 W0 r6 _{
    0 L: C) N( k  ^    u16 id;
    ( y6 r' T8 G# E' x! I5 ?# b: {  G    u16 width;  //LCD 宽度  竖屏
    - l3 g7 H) ~- F/ R    u16 height; //LCD 高度    竖屏
    , C( O& d/ X! Y2 U7 E}_lcd_pra;
    6 M, E3 p9 S7 |8 @( }
  • 成员drv指向驱动,所有操作通过drv实现。typedef struct  
    " X" F8 q& J9 Y/ c( S{" n0 v; b. ~7 _' [
        u16 id;
    0 O* M* z) E% y3 D3 o7 [( Y    s32 (*init)(DevLcd *lcd);
    % V* |& e  U9 g7 o3 P    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
    ( r- w1 a8 }' c, u    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);
    - _. r- |+ l) Y6 {3 f    s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);( |' r5 `2 v' n$ z
        s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);
    & l, T! e; [& Q' w- }    s32 (*onoff)(DevLcd *lcd, u8 sta);" k- Q. c" n9 l; x9 U
        void (*set_dir)(DevLcd *lcd, u8 scan_dir);! J, o# F! u4 i0 w, b$ A$ r6 t
        void (*backlight)(DevLcd *lcd, u8 sta);
    # |4 {( }! f, ^: H* B+ H  S}_lcd_drv;$ g3 y  ]) q/ t7 C, J8 q8 |
  • 成员dir、scandir、 width、 height是驱动要使用的通用变量。因为每个LCD都有一个结构体,一套驱动程序就能控制多个设备而互不干扰。
  • 成员pri是一个私有指针,某些驱动可能需要有些比较特殊的变量,就全部用这个指针记录,通常这个指针指向一个结构体,结构体由驱动定义,并且在设备初始化时申请变量空间。目前主要用于COG LCD跟OLED LCD显示缓存。整个LCD驱动,就通过这个结构体组合在一起。% ~% B4 o, z% C3 Q9 J! C
    1、初始化,根据设备树,找到驱动跟参数,然后初始化上面说的结构体。: V  g0 ~5 M. u+ Q- N- T
    2、要使用LCD前,调用dev_lcd_open函数。打开成功就返回一个上面的结构体指针。; ^' e  S% C) M/ u' z
    3、显示字符,接口找到点阵后,通过上面结构体的drv,调用对应的驱动程序。0 h  M/ u4 f- S! |- L: B
    4、驱动程序根据这个结构体,决定操作哪个LCD总线,并且使用这个结构体的变量。& x, J5 X2 [4 l  V8 Q2 u
    用法和好处
  • 好处1请看测试程序2 f, q( b" r9 {- {! G
    void dev_lcd_test(void)
    1 ?$ B" ^8 v; l1 z5 i* _/ r{
      n$ O  U( A7 h' R1 U    DevLcd *LcdCog;' J( J- V! R0 V& _
        DevLcd *LcdOled;
    # a! l+ ]. n# b) n% }    DevLcd *LcdTft;/ J$ J: X7 J* c  s4 w! P
        /*  打开三个设备 */8 l/ s! z; ]) h
        LcdCog = dev_lcd_open("coglcd");
    & A0 L; g5 o: x0 J( j6 L% u9 l    if(LcdCog==NULL)5 u: o, |% u! ]# {$ f( [7 W
            uart_printf("open cog lcd err\r
      W' H+ `' S% M) N2 f; y) o2 N");+ _" t  O+ K- s, [3 m$ F
        LcdOled = dev_lcd_open("oledlcd");& |9 B4 \7 F. w1 Z1 @
        if(LcdOled==NULL)
    ' v' T5 j# [! o- Y        uart_printf("open oled lcd err\r
    6 v+ c+ D1 ~& k/ @* g");
    , u* j; u1 u2 O6 ]+ C    LcdTft = dev_lcd_open("tftlcd");
    6 U6 F0 h! @+ e# [; i+ n    if(LcdTft==NULL)
    4 r& r1 E( n/ A& [/ X        uart_printf("open tft lcd err\r  H! O% U" s- C$ q5 y6 E7 @
    ");
    " E  F; D' q  \/ X1 P) N8 F. C( S8 u  O    /*打开背光*/4 n9 }5 N7 Y6 ]- n0 H3 a) C
        dev_lcd_backlight(LcdCog, 1);2 F' D9 G% n9 c& }) K9 u' X# L
        dev_lcd_backlight(LcdOled, 1);: R/ m) X2 d0 Z2 Y- F
        dev_lcd_backlight(LcdTft, 1);
    % P6 _. q% s/ ~2 |    dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
    ! e0 h0 A# q$ v  k* y" u1 s    dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "这是oled lcd", BLACK);) b3 V+ J2 {. c6 d# w: P
        dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);8 |3 \7 q+ C3 L
        dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);8 y5 F; L9 K4 I/ u7 F
        dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);2 H1 P; S; }* a+ m2 E" x1 L
        dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是cog lcd", BLACK);" [( u  q: Y8 C* ?" a9 s
        dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);0 T- {5 M/ _1 n. M; W/ b3 m
        dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);
    2 T: `) U2 L5 |/ ]0 R    dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);
    ! F$ q1 M2 S$ v7 G+ R% ~0 W    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是tft lcd", RED);
    5 h! y# h, n( k% O$ z1 g8 P3 W8 @    dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);' b1 @% {, A) T6 J2 o
        dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "屋脊雀工作室", RED);5 n- Z, r7 J  |
        while(1);
    2 q/ N- M; x6 I0 Y1 S% m- X4 x}/ Q. R" \& o3 I& ^  s% w
    使用一个函数dev_lcd_open,可以打开3个LCD,获取LCD设备。然后调用dev_lcd_put_string就可以在不同的LCD上显示。其他所有的gui操作接口都只有一个。这样的设计对于APP层来说,就很友好。显示效果:: ?" W+ O; h& D$ g8 Q, F) v
    ( Y, U7 ]4 u8 w1 U4 a+ x; o# e
  • 好处2现在的设备树是这样定义的
    / X4 E* ^7 K& X! ?LcdObj LcdObjList[DEV_LCD_C]=
    " M1 a; Y4 i7 f; Z6 T7 C: C( c{4 |2 v- }; e" c0 I7 K, G
        {"oledlcd", LCD_BUS_VSPI, 0X1315},
    # l% d9 l# a* L$ |    {"coglcd", LCD_BUS_SPI,  0X7565},! c  R, p$ j) V7 T# v
        {"tftlcd", LCD_BUS_8080, NULL},7 J9 g# H) X# I
    };
    ' B  \  S- P3 H) L2 }某天,oled lcd要接到SPI上,只需要将设备树数组里面的参数改一下,就可以了,当然,在一个接口上不能接两个设备。
    : a6 p0 x% x& h/ {5 ?$ S8 a7 ZLcdObj LcdObjList[DEV_LCD_C]=6 c& p' c9 l3 ~
    {. K" S  m5 N. t  O0 q+ m. F
        {"oledlcd", LCD_BUS_SPI, 0X1315},# Q" r7 k+ u& b
        {"tftlcd", LCD_BUS_8080, NULL},5 N8 T4 w) h. d) p: N; e0 B3 W- L
    };
    9 Q2 t5 @- `1 d8 a2 Z3 c字库暂时不做细说,例程的字库放在SD卡中,各位移植的时候根据需要修改。具体参考font.c。1 @7 |, w# n8 r, E, ?1 i3 R" X
    声明代码请按照版权协议使用。当前源码只是一个能用的设计,完整性与健壮性尚未测试。后续会放到github,并且持续更新优化。最新消息请关注www.wujique.com。1 _7 k4 _* k; M
    -END-! B, Y! K& S7 v3 v5 H; T5 g
    往期推荐:点击图片即可跳转阅读
    + r8 Q: L- o. ]                                                        + I5 e4 k7 W" \
                                                                   
    : v  K; T4 c6 I& I: A. Y, d1 x                                                                       
      o' J, _7 c1 L" h9 M3 r                                                                                3 U9 L* N; s$ i

    ! |  r9 \8 H) H* G, \# H) T                                                                               
    ) K/ ?/ j! I4 A5 ^1 t! g* n- |                                                                                        浅谈为何不该入行嵌入式技术开发
    ( f8 ]! V! f1 X  I                                                                                ! L& S* W3 r% ~& r
                                                                            . G) o. P& g; S$ u
                                                                    0 h8 [0 W+ h* n/ \, E) i9 x' |
                                                           
    0 M7 t4 K# f- }' H% [) s                                                1 m6 B# K( `6 B& F8 t' W. H3 g
    0 m, P0 y0 k9 d0 `$ ~5 e
                                                            6 M, L( _7 [" E$ N/ T' G
                                                                   
    4 M; N5 V6 I! L0 S% H) I; D# `                                                                        , f( v( o4 f; Y/ q# O$ k- w7 K
                                                                                    + N: c6 ]4 ^2 b* j6 _) v
    * Y7 W3 X1 ?0 S9 A/ C) W& \- D
                                                                                    5 X9 h; \/ q. u* N9 x# k& `
                                                                                            在深圳搞嵌入式,从来没让人失望过!0 T" d* }0 Z' {0 z2 n
                                                                                   
    - r1 Y3 d# x1 |6 @. q                                                                        . z8 O  u+ ^; F
                                                                    ! }& v8 G( d" U# M  F0 Z
                                                            9 J$ W1 N2 A) e/ D7 D/ q  r% `
                                                    ' I1 N5 E( \- F1 A8 M% c. |

    2 j! Y$ _5 a" ?* B( p                                                       
    : i" x6 b  Z0 Z8 E3 A& |; ?                                                               
    . Y8 Z( T( S/ A) T                                                                        % [7 l9 o- _0 j2 ^+ ~) J8 V
                                                                                   
    0 V) d: {# k$ e( d9 Z7 r4 D% o  D0 {6 _; w4 e) H9 J
                                                                                    4 L) `% e6 _% @0 [$ y  A4 v
                                                                                            苹果iPhone16发布了,嵌入式鸿蒙,国产化程度有多高?9 ^- r. F0 W/ N1 A9 y5 N7 r# O
                                                                                    ) x! d2 J$ X) X; {; I  ^5 R
                                                                            3 U) A* [0 l' u2 w
                                                                   
    : o: C. f# g7 z! U, m                                                       
    # W8 z' l5 L5 N) L$ @+ l                                                我是老温,一名热爱学习的嵌入式工程师
    0 x2 {7 X5 s4 \' t$ s: W2 W0 G关注我,一起变得更加优秀!
  • 回复

    使用道具 举报

    发表回复

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

    本版积分规则


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