电子产业一站式赋能平台

PCB联盟网

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

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

[复制链接]

569

主题

569

帖子

4259

积分

四级会员

Rank: 4

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

    / y  f' K, `3 sIPS:5 l9 A: [6 c4 Z% m

    4nqs423sgba64035223722.jpg

    4nqs423sgba64035223722.jpg

    2 f" b0 z) j" \8 H. [COG lcd很多人可能不知道COG LCD是什么,我觉得跟现在开发板销售方向有关系,大家都出大屏,玩酷炫界面,对于更深的技术,例如软件架构设计,都不涉及。使用单片机的产品,COG LCD其实占比非常大。COG是Chip On Glass的缩写,就是驱动芯片直接绑定在玻璃上,透明的。实物像下图:
    ) q2 U1 q9 K4 q' M2 c* W

    r54yd031wzu64035223822.jpg

    r54yd031wzu64035223822.jpg

    3 }2 w6 ?: k4 `8 {这种LCD通常像素不高,常用的有128X64,128X32。一般只支持黑白显示,也有灰度屏。
    & p3 Q! e. P6 x/ W. X5 [接口通常是SPI,I2C。也有号称支持8位并口的,不过基本不会用,3根IO能解决的问题,没必要用8根吧?常用的驱动IC:STR7565。  K/ s# L: s! Q
    OLED lcd买过开发板的应该基本用过。新技术,大家都感觉高档,在手环等产品常用。OLED目前屏幕较小,大一点的都很贵。在控制上跟COG LCD类似,区别是两者的显示方式不一样。从我们程序角度来看,最大的差别就是,OLED LCD,不用控制背光。。。。。实物如下图:; j% A4 n' Z; V8 h& U+ a/ W
    7 O7 {# D# ?# C2 m$ G
    常见的是SPI跟I2C接口。常见驱动IC:SSD1615。& n4 w% N$ [1 U4 R
    硬件场景接下来的讨论,都基于以下硬件信息:
    , g7 [/ R+ p- ~" c1、有一个TFT屏幕,接在硬件的FSMC接口,什么型号屏幕?不知道。
    2 a2 H) ^( @" E* o# @% K2、有一个COG lcd,接在几根普通IO口上,驱动IC是STR7565,128X32像素。
    / x; v1 |9 K; Y" r/ i3、有一个COG LCD,接在硬件SPI3跟几根IO口上,驱动IC是STR7565,128x64像素。1 F6 v3 u/ D, x$ h, z  g- _2 d
    4、有一个OLED LCD,接在SPI3上,使用CS2控制片选,驱动IC是SSD1315。
    / o/ g$ Q$ w( W9 n1 j4 d' f
    , c+ H# X  o! ]5 j# H6 M* u预备知识在进入讨论之前,我们先大概说一下下面几个概念,对于这些概念,如果你想深入了解,请GOOGLE。4 T7 |& j! b& d8 u+ h  |
    面向对象面向对象,是编程界的一个概念。什么叫面向对象呢?编程有两种要素:程序(方法),数据(属性)。例如:一个LED,我们可以点亮或者熄灭它,这叫方法。LED什么状态?亮还是灭?这就是属性。我们通常这样编程:4 K2 P& x* P' X+ l7 ^1 w
    u8 ledsta = 0;
    6 N. m7 h6 n, R( @void ledset(u8 sta)
    7 ]" X# v6 f8 c, H8 F{$ g) T/ x4 s0 k7 l7 P
    }
    : b0 h' Z: X2 K6 k; H- K& W这样的编程有一个问题,假如我们有10个这样的LED,怎么写?这时我们可以引入面向对象编程,将每一个LED封装为一个对象。可以这样做:
    " E% X: U1 ]% |& n" y7 j* O9 F/*
    1 `' ?( h: Q+ ?. {  R6 }* h定义一个结构体,将LED这个对象的属性跟方法封装。0 l* P: F. T; j7 u* M& E: O5 k
    这个结构体就是一个对象。/ M+ k) i7 i. I3 ]3 {
    但是这个不是一个真实的存在,而是一个对象的抽象。
    # Z) m% H: `- c3 l/ b*/5 u! c0 n3 k$ D& H3 r
    typedef struct{
    : G. j6 ~- D! c" i2 f    u8 sta;
    " [- k6 M2 G9 G& u    void (*setsta)(u8 sta);+ L/ P5 P+ C5 ~) _, J. h
    }LedObj;+ G0 l0 Q3 V9 w) u3 h
    /*  声明一个LED对象,名称叫做LED1,并且实现它的方法drv_led1_setsta*/
    0 N  c9 C$ T/ X% E: vvoid drv_led1_setsta(u8 sta); s- n5 }+ c1 U* O" }
    {- G' t; G8 V8 ?  W. I* h  e6 x
    }
    $ Y8 E. A/ l! s3 }  LLedObj LED1={
    ; v* A0 @  b. _+ u9 Y        .sta = 0,- ~& w, m7 O2 v
            .setsta = drv_led1_setsta,* A- l/ _0 G8 R4 f1 f" }- ]  j* i
        };  g( p% R0 R' _4 @
    /*  声明一个LED对象,名称叫做LED2,并且实现它的方法drv_led2_setsta*/; d+ |* a% h' L- ]
    void drv_led2_setsta(u8 sta)
    ' _! Q6 l3 B; l/ [{
    7 {! x: C# }% y0 x) p5 G, r) b}
    . p+ g# M6 q( T7 S3 C* |' w! N2 |LedObj LED2={
    5 B: T: m5 P- y5 f- n        .sta = 0," y7 A* O$ `% j, B4 m5 G; ?
            .setsta = drv_led2_setsta,7 u3 |- {  b0 W" K1 O2 C
        };1 x1 C6 G( \0 d1 l  e- q7 H3 d& G
       
    + z+ O' i- a. [  {6 b/*  操作LED的函数,参数指定哪个led*/9 K$ W/ l3 O% |+ ~
    void ledset(LedObj *led, u8 sta)) T9 Y3 A  H+ g, Z. @) ]4 @/ I
    {* r; z# ?' V3 y
        led->setsta(sta);
    + p9 }+ V' d' W6 \* G}
    ; E9 A5 m9 d9 y2 g9 ?是的,在C语言中,实现面向对象的手段就是结构体的使用。上面的代码,对于API来说,就很友好了。操作所有LED,使用同一个接口,只需告诉接口哪个LED。大家想想,前面说的LCD硬件场景。4个LCD,如果不面向对象,「显示汉字的接口是不是要实现4个」?每个屏幕一个?) o6 l5 c+ n: h5 x
    驱动与设备分离如果要深入了解驱动与设备分离,请看LINUX驱动的书籍。9 m+ k8 |' z: a7 P3 J3 E  e  t, A3 {
    什么是设备?我认为的设备就是「属性」,就是「参数」,就是「驱动程序要用到的数据和硬件接口信息」。那么驱动就是「控制这些数据和接口的代码过程」
    + G& N& c& F5 v/ X7 r9 e通常来说,如果LCD的驱动IC相同,就用相同的驱动。有些不同的IC也可以用相同的,例如SSD1315跟STR7565,除了初始化,其他都可以用相同的驱动。例如一个COG lcd:+ @2 I2 u5 d- b/ }9 K$ K3 W
    ?驱动IC是STR7565 128 * 64 像素用SPI3背光用PF5 ,命令线用PF4 ,复位脚用PF35 t9 s. S' R8 I7 I; r
    ?
    上面所有的信息综合,就是一个设备。驱动就是STR7565的驱动代码。( T, n9 L0 I' V& |# D
    为什么要驱动跟设备分离,因为要解决下面问题:
    ( d/ z. \+ p5 J' F- Z& L" L" E?有一个新产品,收银设备。系统有两个LCD,都是OLED,驱动IC相同,但是一个是128x64,另一个是128x32像素,一个叫做主显示,收银员用;一个叫顾显,顾客看金额。( d9 s  q/ r- o. n- r+ s
    ?
    这个问题,「两个设备用同一套程序控制」才是最好的解决办法。驱动与设备分离的手段:$ P: K+ c+ ?* q4 N) A' P( e" h
    ?在驱动程序接口函数的参数中增加设备参数,驱动用到的所有资源从设备参数传入。7 P, h* {# z2 v) X
    ?
    驱动如何跟设备绑定呢?通过设备的驱动IC型号。9 a  ~' L6 k( i5 d2 u
    模块化我认为模块化就是将一段程序封装,提供稳定的接口给不同的驱动使用。不模块化就是,在不同的驱动中都实现这段程序。例如字库处理,在显示汉字的时候,我们要找点阵,在打印机打印汉字的时候,我们也要找点阵,你觉得程序要怎么写?把点阵处理做成一个模块,就是模块化。非模块化的典型特征就是「一根线串到底,没有任何层次感」
    3 d( w/ D* ~" G6 ?LCD到底是什么前面我们说了面向对象,现在要对LCD进行抽象,得出一个对象,就需要知道LCD到底是什么。问自己下面几个问题:
    " A) D$ Q' T& N, j# H
  • LCD能做什么?
  • 要LCD做什么?
  • 谁想要LCD做什么?刚刚接触嵌入式的朋友可能不是很了解,可能会想不通。我们模拟一下LCD的功能操作数据流。APP想要在LCD上显示 一个汉字。
    % W0 V. T2 C, Z0 R# U1、首先,需要一个显示汉字的接口,APP调用这个接口就可以显示汉字,假设接口叫做lcd_display_hz。, C( i% k7 S. E8 N
    2、汉字从哪来?从点阵字库来,所以在lcd_display_hz函数内就要调用一个叫做find_font的函数获取点阵。1 v# G0 g. j4 k0 |/ g
    3、获取点阵后要将点阵显示到LCD上,那么我们调用一个ILL9341_dis的接口,将点阵刷新到驱动IC型号为ILI9341的LCD上。/ p  R2 {  l" P6 t$ o
    4、ILI9341_dis怎么将点阵显示上去?调用一个8080_WRITE的接口。
    0 {+ ^1 W- z' C5 q. e6 e+ f好的,这个就是大概过程,我们从这个过程去抽象LCD功能接口。汉字跟LCD对象有关吗?无关。在LCD眼里,无论汉字还是图片,都是一个个点。那么前面问题的答案就是:
    * e$ S6 E+ q* \# B: u; E$ E. Z
  • LCD可以一个点一个点显示内容。
  • 要LCD显示汉字或图片-----就是显示一堆点
  • APP想要LCD显示图片或文字。结论就是:所有LCD对象的功能就是显示点。「那么驱动只要提供显示点的接口就可以了,显示一个点,显示一片点。」 抽象接口如下:
    * e  @0 c: ]/ w4 U* M7 x/*) }- P0 N) O" b
        LCD驱动定义
    : h+ Z2 i( Z" o! B  r$ O*/
    3 z3 t$ o3 Q0 v8 _6 g5 g- Gtypedef struct  
    3 ]8 i9 o2 A/ e( H{
    : m4 T* J6 ^9 o6 v$ m    u16 id;
    * p' E# E- o# W2 d" {    s32 (*init)(DevLcd *lcd);
    ) {2 L! f! }7 y. ]+ p+ w; g# f- N    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
    ! K$ }+ A" U5 X2 X2 R    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);7 h' c& u2 l$ p, a" k- A
        s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);( G+ h6 G5 p2 v, z/ b1 F9 u
        s32 (*onoff)(DevLcd *lcd, u8 sta);4 m# f0 z" d2 Q2 |( P. z
        s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);# ?% |7 h4 D  a* R, I& N
        void (*set_dir)(DevLcd *lcd, u8 scan_dir);
    8 u" F* m' k) ]$ c$ ?    void (*backlight)(DevLcd *lcd, u8 sta);
    $ i) z$ R$ M. w' _  A}_lcd_drv;
    ( ?6 l. q" C& J/ V$ {上面的接口,也就是对应的驱动,包含了一个驱动id号。+ n: r/ B; A4 Y* [6 m
  • id,驱动型号
  • 初始化
  • 画点
  • 将一片区域的点显示某种颜色
  • 将一片区域的点显示某些颜色
  • 显示开关
  • 准备刷新区域(主要彩屏直接DMA刷屏使用)
  • 设置扫描方向
  • 背光控制显示字符,划线等功能,不属于LCD驱动。应该归类到GUI层。4 q" W% {; ]8 U" I- t
    LCD驱动框架我们设计了如下的驱动框架:0 F5 C# ]' b6 j* f0 Y' t8 y
    9 y$ T% s5 I7 R  d5 P/ i. v
    设计思路:* H! A0 i/ X; I8 M* o1 ?; q" N
    1、中间显示驱动IC驱动程序提供统一接口,接口形式如前面说的_lcd_drv结构体。
    5 X; T- t' B  ?' o6 B3 j& r- @2、各显示IC驱动根据设备参数,调用不同的接口驱动。例如TFT就用8080驱动,其他的都用SPI驱动。SPI驱动只有一份,用IO口控制的我们也做成模拟SPI。3 P. \4 u5 j- u& k5 z
    3、LCD驱动层做LCD管理,例如完成TFT LCD的识别。并且将所有LCD接口封装为一套接口。
    % I  K+ W$ N% Q" Z2 n7 o( b4、简易GUI层封装了一些显示函数,例如划线、字符显示。( j& {1 E0 \6 ~# d* S- Z7 \
    5、字体点阵模块提供点阵获取与处理接口。
    ! ?! z4 Z5 w. P7 N1 R4 p. k& W2 J由于实际没那么复杂,在例程中我们将GUI跟LCD驱动层放到一起。TFT LCD的两个驱动也放到一个文件,但是逻辑是分开的。OLED除初始化,其他接口跟COG LCD基本一样,因此这两个驱动也放在一个文件。
    ; f! e+ e- Z4 f, n代码分析代码分三层:+ E7 c+ Q) h& {1 s- {5 y4 v
    1、GUI和LCD驱动层 dev_lcd.c dev_lcd.h8 B% D% u. A; a" O. A! h
    2、显示驱动IC层 dev_str7565.c & dev_str7565.h dev_ILI9341.c & dev_ILI9341.h8 ?. I1 {& h; ~* z1 V; u! J9 f& _
    3、接口层 mcu_spi.c & mcu_spi.h stm324xg_eval_fsmc_sram.c & stm324xg_eval_fsmc_sram.h
    3 [& H) R5 x  P$ bGUI和LCD层这层主要有3个功能 :
    ; ^+ K! E! v5 u$ F% t* G) M「1、设备管理」* V" l& J4 @) L( B$ D1 v, X  }* m
    首先定义了一堆LCD参数结构体,结构体包含ID,像素。并且把这些结构体组合到一个list数组内。+ c' d; e6 U8 X6 w! E  |
    /*  各种LCD的规格参数*/: Y7 K2 ^: [# P# r' }, n- Z
    _lcd_pra LCD_IIL9341 ={
    ( @6 s' i) n# w' S* M! x/ s        .id   = 0x9341,+ k# X9 h  ^# ^, ~. a- c! @; @$ U
            .width = 240,   //LCD 宽度
    4 O8 m7 |7 a: A7 ^( M5 Y        .height = 320,  //LCD 高度
    4 n( i8 K) {/ B% Y};0 U* W* i! i2 W0 T
    ...! Y7 P7 c6 L4 }2 M& y2 L
    /*各种LCD列表*/
    ; b& L6 x4 \+ @. g) }_lcd_pra *LcdPraList[5]=$ Z* b+ s" ^7 T: \3 @5 j5 W
                {8 o% |8 H2 ^% W5 M# S$ v  C& e
                    &LCD_IIL9341,       ( b  m( Q" K* y  u; e
                    &LCD_IIL9325,
    ) l7 }) q) O/ j9 n* q                &LCD_R61408,
    & k( E2 X- K% {4 z/ ?) J                &LCD_Cog12864,
    # j5 t2 q. U" ?" n: B                &LCD_Oled12864,: x- y# e$ B: y7 h% i
                };* B. H, ^( x4 d4 I5 M- y1 c  A
    然后定义了所有驱动list数组,数组内容就是驱动,在对应的驱动文件内实现。3 P( f) U% P! w2 ~* {; v% ^: t* T: d
    /*  所有驱动列表
    ' `5 a: f2 D  R& K2 [$ D: H5 x    驱动列表*/
    - o& e4 m. v0 j2 Q- `& x- v_lcd_drv *LcdDrvList[] = {
    , e( C8 A6 ^6 g- {                    &TftLcdILI9341Drv,
    8 E% A7 u$ K  ~( j, S                    &TftLcdILI9325Drv,9 Q( `6 g2 h6 ?  j* d( ]5 U. i
                        &CogLcdST7565Drv,
    ; M4 x9 ^! I& V( @4 |! v6 v3 @                    &OledLcdSSD1615rv,
    ( N+ l6 B8 t( F6 w9 ?定义了设备树,即是定义了系统有多少个LCD,接在哪个接口,什么驱动IC。如果是一个完整系统,可以做成一个类似LINUX的设备树。! V0 d2 M# N6 V2 H1 V  N
    /*设备树定义*/
    4 l8 ^$ D. g0 h9 u1 u5 y#define DEV_LCD_C 3//系统存在3个LCD设备1 Y- L3 ~9 v. ]- L1 i% t' t2 _4 ]
    LcdObj LcdObjList[DEV_LCD_C]=
    * ^# @) H/ x' F9 x7 }, y{/ f; q6 H' |  l$ r0 X1 E4 K  p/ L
        {"oledlcd", LCD_BUS_VSPI, 0X1315},
    ' a* M6 j8 f" q    {"coglcd", LCD_BUS_SPI,  0X7565},4 ]! k" O; |- @( i" d
        {"tftlcd", LCD_BUS_8080, NULL},# @2 p) c- w: u- d$ {* U; W
    };
    * g1 n4 H& t5 ]% w) x「2 、接口封装」+ K( o/ k. o( \- l# H+ g7 r
    void dev_lcd_setdir(DevLcd *obj, u8 dir, u8 scan_dir)
    8 F$ J  b, Q6 [9 C: W% O' P. |) [$ |s32 dev_lcd_init(void), ^' V: |- y$ ~& A) p  J
    DevLcd *dev_lcd_open(char *name)
    3 m3 ?7 E$ e5 u( r/ S/ g" ss32 dev_lcd_close(DevLcd *dev)
    " r5 V6 ]; A2 K! m4 _5 ps32 dev_lcd_drawpoint(DevLcd *lcd, u16 x, u16 y, u16 color)
    ( v8 q* U- E+ ^0 `( e" _; M" \9 js32 dev_lcd_prepare_display(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey)
    / }9 h& Z9 e. s9 Cs32 dev_lcd_display_onoff(DevLcd *lcd, u8 sta)* `6 p+ }+ I) K
    s32 dev_lcd_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color). V  v2 Y5 m8 o, E
    s32 dev_lcd_color_fill(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 color)
    4 w# Y! m9 t# Q: T$ [s32 dev_lcd_backlight(DevLcd *lcd, u8 sta)& m6 x1 m% Q4 n" z
    大部分接口都是对驱动IC接口的二次封装。有区别的是初始化和打开接口。初始化,就是根据前面定义的设备树,寻找对应驱动,找到对应设备参数,并完成设备初始化。打开函数,根据传入的设备名称,查找设备,找到后返回设备句柄,后续的操作全部需要这个设备句柄。
    . ~& q3 A& ^, d' u「3 、简易GUI层」; Q8 z' r' r6 i& J0 z( |, z* {
    目前最重要就是显示字符函数。
    3 D& M6 M0 e& H) W1 {! s5 ^s32 dev_lcd_put_string(DevLcd *lcd, FontType font, int x, int y, char *s, unsigned colidx)
    + s' G1 s! n6 n+ i5 I* G0 V. S& Y( g其他划线画圆的函数目前只是测试,后续会完善。
    + ^+ e) B8 \7 @0 X- F) J& U驱动IC层驱动IC层分两部分:
    2 t1 Q' r2 u% C! W( B「1 、封装LCD接口」
    9 {) y$ n0 h; b$ S# ~$ yLCD有使用8080总线的,有使用SPI总线的,有使用VSPI总线的。这些总线的函数由单独文件实现。但是,除了这些通信信号外,LCD还会有复位信号,命令数据线信号,背光信号等。我们通过函数封装,将这些信号跟通信接口一起封装为「LCD通信总线」, 也就是buslcd。BUS_8080在dev_ILI9341.c文件中封装。BUS_LCD1和BUS_lcd2在dev_str7565.c 中封装。- D+ w! M/ t( r& f
    「2 驱动实现」
    ; m- E. f' ?5 U, P实现_lcd_drv驱动结构体。每个驱动都实现一个,某些驱动可以共用函数。
    2 C; ^3 d. a& `) J' ]' K_lcd_drv CogLcdST7565Drv = {7 M- l( u0 s8 i- J+ w" ~. m* ?. U
                                .id = 0X7565,) e/ L6 w% C' C  J
                                .init = drv_ST7565_init,
    ! _% G' F" f6 t                            .draw_point = drv_ST7565_drawpoint,
    ; d+ f6 w, Z# U                            .color_fill = drv_ST7565_color_fill,& U" b0 P5 H9 _
                                .fill = drv_ST7565_fill,  K& `/ f4 z8 D# `6 g( J
                                .onoff = drv_ST7565_display_onoff,
    # |; q6 o% ^# g, V1 b! w                            .prepare_display = drv_ST7565_prepare_display,
      D4 ]3 G* q' _/ U. x                            .set_dir = drv_ST7565_scan_dir,( P2 N' b# N, }% C0 ]
                                .backlight = drv_ST7565_lcd_bl8 G) P8 Y' ^! J
                                };6 q6 R, ~5 q0 K6 J7 P, g
    接口层8080层比较简单,用的是官方接口。SPI接口提供下面操作函数,可以操作SPI,也可以操作VSPI。
    ! y. P6 R5 u# s! C6 T8 qextern s32 mcu_spi_init(void);
    : t/ W+ j  j. M7 u/ G) hextern s32 mcu_spi_open(SPI_DEV dev, SPI_MODE mode, u16 pre);
    2 D# a9 h. U! b0 ]( @+ Fextern s32 mcu_spi_close(SPI_DEV dev);( Q! o4 m% [5 }, a3 ]% q
    extern s32 mcu_spi_transfer(SPI_DEV dev, u8 *snd, u8 *rsv, s32 len);# }+ r$ e2 H3 s" q# P6 g% n2 F
    extern s32 mcu_spi_cs(SPI_DEV dev, u8 sta);# ]8 D) r! {; R
    至于SPI为什么这样写,会有一个单独文件说明。
    % ^& c/ v5 L1 @1 k: R总体流程前面说的几个模块时如何联系在一起的呢?请看下面结构体:
    ) V7 h2 [7 u; K7 S* i/*  初始化的时候会根据设备数定义,4 Q9 X6 i; G1 W! p
        并且匹配驱动跟参数,并初始化变量。/ I0 R. n6 w2 c1 ?) E
        打开的时候只是获取了一个指针 */8 }! H! g" g% Z/ g
    struct _strDevLcd: {# @" t! T/ p" p
    {
    ' Y6 Y; ~' {4 i" ?* ^8 W, Q    s32 gd;//句柄,控制是否可以打开
    ; B* X1 P/ G  g! L# c7 I    LcdObj   *dev;" e( R" |" A  |0 e. e7 V
        /* LCD参数,固定,不可变*/
    , }% W9 a+ z7 m8 O    _lcd_pra *pra;
    - H9 y/ i) T0 _* p+ n    /* LCD驱动 */- ]1 P: q8 j2 Z; g. o
        _lcd_drv *drv;  V" v5 C. c$ `5 g  z
        /*驱动需要的变量*/
    / M3 v1 \, \, S! O. \# [* e1 i. j+ C- |    u8  dir;    //横屏还是竖屏控制:0,竖屏;1,横屏。
    * f) N( q; ?$ O    u8  scandir;//扫描方向
    + W% h' ^4 O0 \% \' d; D( L: t    u16 width;  //LCD 宽度
    ' d, q1 ~- S* `3 w3 {8 l" E    u16 height; //LCD 高度# e# _& c; |, q) X+ B' A& ~
        void *pri;//私有数据,黑白屏跟OLED屏在初始化的时候会开辟显存
    + l3 \: l  x) k4 x};
    / K, v7 E* ~) C每一个设备都会有一个这样的结构体,这个结构体在初始化LCD时初始化。9 v& q4 {* H' d# V$ D: [
  • 成员dev指向设备树,从这个成员可以知道设备名称,挂在哪个LCD总线,设备ID。typedef struct2 Y" j) n" ~0 X1 n, V: i( T
    {
    ; o, x: m* q5 J& k1 q/ l4 n    char *name;//设备名字7 K1 W( G# z, j( p9 S
        LcdBusType bus;//挂在那条LCD总线上( b; O! n$ q! q0 g
        u16 id;/ k- x+ P" q6 S
    }LcdObj;$ p% U9 L# r, D6 H% `& D; ^, s( ~, W4 p. m
  • 成员pra指向LCD参数,可以知道LCD的规格。typedef struct
    ! X; b9 H' m# c{
    ( z2 m: ^$ ?8 g. ?    u16 id;
    0 h1 ~, d# d1 N3 B$ l" i8 R! G    u16 width;  //LCD 宽度  竖屏
    1 p2 U3 A+ M1 K/ _    u16 height; //LCD 高度    竖屏1 Y8 e# Y3 |4 k- D  G2 g2 j5 \: q
    }_lcd_pra;
    6 c( w  o2 n8 O! J
  • 成员drv指向驱动,所有操作通过drv实现。typedef struct  7 M/ v; L7 }( W. J1 N6 R' s% R
    {
    - ]0 f+ E( X' D- e/ o, z    u16 id;9 e  k( I3 _+ t7 B' `. C2 W
        s32 (*init)(DevLcd *lcd);
    ! [$ Y$ a8 Q3 ^( N8 [- u    s32 (*draw_point)(DevLcd *lcd, u16 x, u16 y, u16 color);
    ! R8 b0 e5 @! o/ [# r- X    s32 (*color_fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey, u16 color);
    8 H8 h- p& H5 n6 M    s32 (*fill)(DevLcd *lcd, u16 sx,u16 ex,u16 sy,u16 ey,u16 *color);
    ; I# ^/ J1 F7 i( U; B/ X    s32 (*prepare_display)(DevLcd *lcd, u16 sx, u16 ex, u16 sy, u16 ey);8 n  @# J) C& A- O' \
        s32 (*onoff)(DevLcd *lcd, u8 sta);
    2 i# a$ v; x  h    void (*set_dir)(DevLcd *lcd, u8 scan_dir);) w6 j; g- z5 J; p6 \+ l
        void (*backlight)(DevLcd *lcd, u8 sta);
    6 ~7 P, \- I( M' h  [: R0 r}_lcd_drv;/ a1 t1 H0 y  Y( o9 |( K6 q4 C" o
  • 成员dir、scandir、 width、 height是驱动要使用的通用变量。因为每个LCD都有一个结构体,一套驱动程序就能控制多个设备而互不干扰。
  • 成员pri是一个私有指针,某些驱动可能需要有些比较特殊的变量,就全部用这个指针记录,通常这个指针指向一个结构体,结构体由驱动定义,并且在设备初始化时申请变量空间。目前主要用于COG LCD跟OLED LCD显示缓存。整个LCD驱动,就通过这个结构体组合在一起。
    # Z+ k. v4 l/ }& ?- V0 l1、初始化,根据设备树,找到驱动跟参数,然后初始化上面说的结构体。
    0 {) n6 S# X7 I! h6 H& O! O/ o( O2、要使用LCD前,调用dev_lcd_open函数。打开成功就返回一个上面的结构体指针。
    * a$ `) z: H3 h% `2 O3、显示字符,接口找到点阵后,通过上面结构体的drv,调用对应的驱动程序。% D0 B# ^" d; G, @' n/ ^
    4、驱动程序根据这个结构体,决定操作哪个LCD总线,并且使用这个结构体的变量。: w4 {' t" I2 n9 {! d. S7 S, m& J
    用法和好处
  • 好处1请看测试程序) e$ r4 w$ O3 t  z
    void dev_lcd_test(void)
    ( \# y! y. h" C- O3 ^{
    7 f& A, M; _9 S( e+ H) a( [    DevLcd *LcdCog;
    7 w0 j/ ?+ e4 _, e1 P# H    DevLcd *LcdOled;
    ' N- f$ |6 F% M; d& c  M+ D* l    DevLcd *LcdTft;
    % L' P5 G& {( \' p3 W  V8 w% c    /*  打开三个设备 */
    ' f9 t& u7 Q' P    LcdCog = dev_lcd_open("coglcd");8 f* ]( a5 L8 T1 g3 z2 h% T2 O/ L
        if(LcdCog==NULL)
    & k, p! ~4 a; p0 E: q$ g        uart_printf("open cog lcd err\r
    . g; D  t5 ^  w8 m" Q0 |");
    ' P/ {  j& f) v- I+ Y5 V, p2 l7 b8 `8 j    LcdOled = dev_lcd_open("oledlcd");% m6 m/ p, }9 t9 w1 {7 r
        if(LcdOled==NULL)
    1 H2 j) q2 @' l        uart_printf("open oled lcd err\r
    . r, k% Q; y3 l# h' x& A9 h& r: ]");' y' P9 M3 H( E8 R
        LcdTft = dev_lcd_open("tftlcd");
    ! }4 Y8 J1 p0 ]( d0 k0 @    if(LcdTft==NULL); u. a- S. W4 m! o+ i7 k
            uart_printf("open tft lcd err\r
    5 j- m. s! |6 G' l7 y6 C");  v2 m; I. n" e$ K( |
        /*打开背光*/  M) z" @, l1 T" \2 i/ @
        dev_lcd_backlight(LcdCog, 1);3 Q7 M8 @* o% s8 Y+ }
        dev_lcd_backlight(LcdOled, 1);. E+ o, R8 _, t  s7 K6 J
        dev_lcd_backlight(LcdTft, 1);6 m* b8 e1 @! Q) _: ~9 X8 ]
        dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);
    0 T+ K; e- X( a* F1 N    dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 13, "这是oled lcd", BLACK);9 r1 @1 Y! V* i' P: [4 |' l5 [7 J
        dev_lcd_put_string(LcdOled, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);# ~1 H7 r9 y$ M) Z* |3 P% n1 w4 |
        dev_lcd_put_string(LcdOled, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);
    9 f2 a5 ?! n7 k; Z2 s    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,1, "ABC-abc,", BLACK);; Y* R  Y; Z- |4 n8 J5 j
        dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 13, "这是cog lcd", BLACK);
    3 ~- _$ q0 w, n& ^! G" e% B! j  c    dev_lcd_put_string(LcdCog, FONT_SONGTI_1212, 10,30, "www.wujique.com", BLACK);1 K% n& f2 ^/ ~0 B! P! E/ x7 |) V- T5 v! L
        dev_lcd_put_string(LcdCog, FONT_SIYUAN_1616, 1, 47, "屋脊雀工作室", BLACK);
    6 \* ?3 r* ~: d  ^$ v% B    dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,30, "ABC-abc,", RED);
    # V5 d# M8 `, f% I    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,60, "这是tft lcd", RED);0 m4 [% j7 w# C4 d2 h
        dev_lcd_put_string(LcdTft, FONT_SONGTI_1212, 20,100, "www.wujique.com", RED);
    6 u3 g% V& C" @! {    dev_lcd_put_string(LcdTft, FONT_SIYUAN_1616, 20,150, "屋脊雀工作室", RED);
    , x4 B5 S8 k( P' \    while(1);
    ! U# R: E  o+ p  r6 k7 I5 n}: f) c/ \+ x% {; l1 l1 g
    使用一个函数dev_lcd_open,可以打开3个LCD,获取LCD设备。然后调用dev_lcd_put_string就可以在不同的LCD上显示。其他所有的gui操作接口都只有一个。这样的设计对于APP层来说,就很友好。显示效果:
    4 C" t9 |5 D( r3 X& p/ u) O
    8 C0 H- r; E2 b& ?4 T" D
  • 好处2现在的设备树是这样定义的
    ! b4 s* H' k$ o& Q8 `LcdObj LcdObjList[DEV_LCD_C]=
    ) Y/ }; G6 G& |% z; {. H7 i5 i& O{
    # u8 m6 ]% h# ?$ G# m/ l) H, E    {"oledlcd", LCD_BUS_VSPI, 0X1315},
    9 ]$ j) r3 ^3 }; I, r3 {8 c# f( l    {"coglcd", LCD_BUS_SPI,  0X7565},
      ]2 l, J8 ~5 t# j. Q6 Q    {"tftlcd", LCD_BUS_8080, NULL},: J5 o! E7 I6 v0 H. C" U9 O
    };! A. I  @! I7 k( f/ G2 L
    某天,oled lcd要接到SPI上,只需要将设备树数组里面的参数改一下,就可以了,当然,在一个接口上不能接两个设备。
    ' U3 e! m( I$ Q; S( i9 ?, l. F8 O6 xLcdObj LcdObjList[DEV_LCD_C]=
    ; x% U/ N! m* _' t. r( j% S{
    4 W$ t. C2 p/ m- N- Q    {"oledlcd", LCD_BUS_SPI, 0X1315},
      M3 C, o# J  e    {"tftlcd", LCD_BUS_8080, NULL}," R' j0 _  W4 g/ f7 }# I0 M
    };
    & F  x5 z; h0 G' k$ {  S字库暂时不做细说,例程的字库放在SD卡中,各位移植的时候根据需要修改。具体参考font.c。0 }: B+ [* g& Y9 n
    声明代码请按照版权协议使用。当前源码只是一个能用的设计,完整性与健壮性尚未测试。后续会放到github,并且持续更新优化。最新消息请关注www.wujique.com。
    $ x, x9 c4 S- ]; m-END-9 W3 F- \: A# m( Q4 y0 j
    往期推荐:点击图片即可跳转阅读; I4 w; g2 l! K: }6 g0 X/ S
                                                            * O+ ]9 L% M2 T6 p) r/ v: g- M8 }8 L
                                                                   
    ! M3 V6 B! Z% Q                                                                        / e! o  ]4 x4 m; s. d/ Y' A
                                                                                    3 w+ i$ Z8 j; p/ z4 R

    ; Z4 s0 y$ G$ v1 u                                                                               
    % e, `( Z4 H; ]" N                                                                                        浅谈为何不该入行嵌入式技术开发
      ~. H( J+ B5 _: `* N                                                                                % V  \; U5 u/ U  @) s
                                                                           
    7 C, J# L. j. ]% h9 |% h7 F! n! A. g  u                                                               
    1 r9 o* U# c( [/ ]' F                                                       
    ) X' I2 N, X2 `& B8 V                                               
    2 _3 ?6 U: H# R9 C# Y( j* |* C  X- _& H* M
                                                              h9 C! O/ S  G5 l1 ?
                                                                    5 \/ z/ z/ Q% Y) R8 _3 X
                                                                           
    6 O& o2 _/ R7 `                                                                                ( `: B! s. q1 D! D& N  `$ h
    , |! V# y9 y2 h+ y+ m
                                                                                   
    . v3 c) g9 l: c0 j( K$ I                                                                                        在深圳搞嵌入式,从来没让人失望过!
    & |; ~7 h4 x8 E5 R. j* G) D                                                                                ! h. _1 q/ y0 [& A% ]( D
                                                                           
    " V" t7 Y1 y3 b0 W                                                                2 o! e8 L, h% @6 E0 r; T; K
                                                            * w9 `* \& H2 @4 Y5 J/ Z6 p
                                                    6 X1 J" j; B' S5 ?

    ) V: T, l. F% U3 ~( [; [/ ~5 u                                                       
    ! m$ i4 Q' v& ~& b                                                               
    ; ]  E/ a& r" D2 ^! H' n* o                                                                       
    ; l& V) d  R9 H' s  `$ Z1 X  z                                                                                , @) P4 E+ W4 t' @( ~
    - `7 U+ b" I2 \( n
                                                                                   
    8 R# w. I7 n7 h' ]2 W4 \                                                                                        苹果iPhone16发布了,嵌入式鸿蒙,国产化程度有多高?
    ( o+ F2 ]9 r  y% D6 H9 k" D                                                                               
    / Q* O; L4 u" U# ?4 H! D                                                                       
    : R' f% r5 b5 T; A                                                               
    8 l7 ?$ x9 _6 {                                                        7 ~- _% X- @6 V6 b1 ?
                                                    我是老温,一名热爱学习的嵌入式工程师" ^! K3 @0 x. `" X1 N; |
    关注我,一起变得更加优秀!
  • 回复

    使用道具 举报

    发表回复

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

    本版积分规则


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