LCD驱动理论与实例分析


前言

基础篇和驱动篇使用 I.MX6U-ALPHA 开发板。
实例篇使用 RK3568 开发板和 mipi LCD 模块。

一、基础篇

1、LCD 基础知识

分辨率

1080P:LCD 屏幕上的像素数量是
19201080 个,屏幕一列 1080 个像素点,一共 1920 列。
而像素点就是一个 RGB 灯,也就是由 R(红色)、G(绿色)和 B(蓝色)这三
种颜色组成的。
在这里插入图片描述
2K:2560
1440 个像素点
4K:3840*2160 个像素点

像素格式

一般一个 R、
G、B 分别使用 8bit 的数据,一个像素点就是 8bit*3=24bit,如果再加入 8bit 的 Alpha(透明)通道的话一个像素点就是 32bit,也就是 4 个字节,这种像素格式称为 ARGB8888。
ARGB8888 格式的四个字节每个位的分配如图:
在这里插入图片描述
bit31~bit24 是 Alpha 通道,bit23~bit16 是
RED 通道,bit15~bit14 是 GREEN 通道,bit7~bit0 是 BLUE 通道。
红色对应的值为
0X00FF0000
蓝色对应的值为 0X000000FF
绿色对应的值为 0X0000FF00
黑色对应的值为 0X00000000
白色对应的值为 0X00FFFFFF
在这里插入图片描述

LCD屏幕接口

显示器上常见的 VGA、HDMI、DP 等等。
一般比较低级的处理器只支持 RGB 接口,RGB 接口的信号线如表所示:

信号线描述
R[7:0]8 根红色数据线
G[7:0]8 根绿色数据线
B[7:0]8 根蓝色数据线
DE数据使能线
VSYNC垂直同步信号线
HSYNC水平同步信号线
PCLK像素时钟信号线

RGB 接口一般有两种驱动模式:DE 模式和 HV 模式。

  • DE 模式需要用到 DE 信号线
  • HV 模式不需要用到 DE 信号线
  • DE模式下是可以不需要 HSYNC 信号线

LCD 原理图的例子:
在这里插入图片描述
J1 是一个 40 pin 的 FPC 插座,采用 RGB888
格式,还支持触摸屏和背光控制。
注:触屏接口至少需要4根 I2C 管脚,背光控制需要几个 LED灯,并且用定时器开启 PWM 功能。
这里需要再补充一个概念:LCD ID。
通过 LCD 的 RGB 接口的阴角线来设定 LCD ID,可以在驱动程序中根据 ID 值来判断使用哪些程序(不同的初始化程序,例如分辨率),这样程序的兼容性就提高了。
举个例子:
在这里插入图片描述
分别通过R、G、B 的第8根数据线来区分 ID。
实际上后来的方法是:设备树上写明分辨率。这个方法更好。

LCD 时间参数

LCD 的显示过程就是逐个像素控制颜色的过程,只是速度足够快,人看的时候就是一张张图片的显示,甚至和视频一样流畅。
假如一个 LCD 的分
辨率为 1024*600,那么其扫描如图:
在这里插入图片描述
一帧图像也是由一行一行
组成的。
HSYNC 是水平同步信号,也叫做行同步信号,当产生此信号,就表示开始显示新的
一行了。
VSYNC 是垂直同步信号,也叫做帧
同步信号,当产生此信号,就表示开始显示新的一帧图像了。
显示器的“祖先”CRT 显示器,就是90年代的老电视机,通过电子枪发射像素数据信号到 LCD 控制器。当显示完一行以后会发出 HSYNC 信号,此时电子枪就会关闭,然后迅速的移动到屏幕的
左边,当 HSYNC 信号结束以后就可以显示新的一行数据了,电子枪就会重新打开。
在 HSYNC 信号结束到电子枪重新打开之间就会一段延时,这段延时就是 HBP
显示完一行后会关闭电子枪等待 HSYNC 信号,关闭电子枪到 HSYNC 信号产生之间就有一段延时,这段延时就是 HFP
显示完一帧图像后电子枪也会关闭,然后等待 VSYNC 信号,等待就有一段延时,这段延时就是 VFP
VSYNC 信号产生,电子枪移动到左上角,VSYNC 信号结束到电子枪重新打开也有一段延时,这段延时就是 VBP

HBP、HFP、VFP、VBP 的具体数值要去查 LCD 数据手册。

LCD屏幕时序

在这里插入图片描述
HSYNC:行同步信号,高电平还是低电平有效需要看数据手册,这里是低电平有效。
HSPW:也叫 thp,表示 HSYNC 信号的持续时间。
HBP:也叫 thb,表示 HSYNC 的后肩(HSPW信号结束到 DE 信号开始的间隔时间)。
HOZVAL:也叫 thd,显示一行数据所需的时间,如果屏幕分辨率是 1024*600,那么 HOZVAL 就是1024.单位是 CLK。
HFP:也叫thf,表示 HSYNC 前肩,DE 信号结束到 HSYNC 重新开始的间隔时间。
(注意:以上时间单位全是 CLK)
HSYNC 信号发出后,需要等待 HSPW+HBP 个 CLK 时间才会接收到有效的像素数据
显示完一行数据后需要等待 HFP 个 CLK 时间才能发出下一个 HSYNC 信号,所以
显示一行需要的时间是:HSPW + HBP + HOZVAL + HFP

一帧图像就是由很多个行组成的,RGB LCD 的帧显示时序如图所示:
在这里插入图片描述
VSYNC:帧同步信号,表示开始显示新的一帧数据,需要查数据手册看是低电平有效还是高电平有效,这里是低电平有效。
VSPW:也叫 tvp,是 VSYNC 信号持续时间。
VBP:也叫 tvb,帧同步信号后肩。
LINE:也叫 tvd,显示一帧有效数据所需的时间,假如屏幕分辨率为 1024*600,
LINE 就是 600 行的时间。
VFP:也叫 tvf,帧同步信号前肩。
(注意:以上时间单位为行)

显示一帧所需要的时间就是:VSPW+VBP+LINE+VFP时间,最终的计算公式:
T = (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)

所以!配置 LCD 屏幕参数时,需要知道:
HOZVAL(屏幕有效宽度)、LINE(屏幕有效高度)、HBP、HSPW、HFP、VSPW、VBP 和 VFP
裸机开发时采用层层包装的方式,让显示调用的时间变得很直观。
驱动开发时,只需要在设备树上填充好时序参数,然后交给内核就行了。

* 时钟像素

像素时钟就是 LCD 的时钟信号。举一个例子,一款 LCD 的时序参数如下:
在这里插入图片描述
那么显示一帧图像所需要的
时钟数就是:
= (VSPW+VBP+LINE+VFP) * (HSPW + HBP + HOZVAL + HFP)
= (3 + 20 + 600 + 12) * (20 + 140 + 1024 + 160)
= 635 * 1344
= 853440。
显示一帧图像需要853440个时钟数,那么显示60帧就是:853440 * 60 = 51206400≈51.2M。
所以像素时钟就是 51.2MHz。

然后就是具体给 LCD 模块的时钟了,本例中的 LCD 是采用 eLCDIF 接口的,这个接口在其处理器上有几个时钟源可以选择:
在这里插入图片描述
时钟这部分,每个处理器都不一样,主要分配到最后,所用的时钟源给接口刚好是像素时钟的频率,也就是51.2MHZ。

显存

若采用 ARGB8888 格式,一个像素需要 4 个字节的内存。1024600 分辨率就需要 1024600*4=2457600B≈2.4MB 内存。但是 RGB LCD 内部是没有内存的,所以需要在 DDR3 中分出一段内存作为 RGB LCD 屏幕的显存,我们如果要在屏幕上显示什么图像的话直接操作这部分显存即可。

2、eLCDIF 接口

eLCDIF 是大部分低级处理自带的液晶屏幕接口。主要支持三种接口:MPU 接口、VSYNC 接口和 DOTCLK 接口。

  • MPU 接口
    MPU 接口用于在处理器和 LCD 屏幕直接传输数据和命令,这个接口用于 6080/8080 接
    口的 LCD 屏幕。
  • VSYNC 接口
    VSYNC 接口时序和 MPU 接口时序基本一样,只是多了 VSYNC 信号来作为帧同步。
  • DOTCLK 接口
    DOTCLK 接口就是用来连接 RGB LCD 接口屏幕的, 它包括 VSYNC、HSYNC、DOTCLK
    和 ENABLE(可选的)这四个信号,这样的接口通常被称为 RGB 接口。DOTCLK 接口时序如图所示:
    在这里插入图片描述
    DOTCLK 接口就是直接连接 RGB 屏幕的,本例使用 DOTCLK 接口。
    eLCDIF 要驱动起来 RGB LCD 屏幕,重点是配置好时间参数,这个通过配置相应的寄存器就可以了,所以我们接下来看一下 eLCDIF 接口的几个重要的寄存器。

LCDIF_CTRL 寄存器

此寄存器结构如图所示:
在这里插入图片描述
寄存器 LCDIF_CTRL 用到的重要位如下:
SFTRST(bit31):eLCDIF 软复位控制位,当此位为 1 的话就会强制复位 LCD。
CLKGATE(bit30):正常运行模式下,此位必须为 0!如果此位为 1 的话时钟就不会进入到
LCDIF。
BYPASS_COUNT(bit19):如果要工作在 DOTCLK 模式的话就此位必须为 1。
VSYNC_MODE(bit18):此位为 1 的话 LCDIF 工作在 VSYNC 接口模式。
DOTCLK_MODE(bit17):此位为 1 的话 LCDIF 工作在 DOTCLK 接口模式。
INPUT_DATA_SWIZZLE(bit15:14):输入数据字节交换设置,此位为 0 的话不交换字节也
就是小端模式;为 1 的话交换所有字节,也就是大端模式;为 2 的话半字交换;为 3 的话在每
个半字内进行字节交换。本章我们设置为 0,也就是不使用字节交换。
CSC_DATA_SWIZZLE(bit13:12) : CSC 数 据 字 节 交 换 设 置 , 交 换 方 式 和
INPUT_DATA_SWIZZLE 一样,本章设置为 0,不使用字节交换。
LCD_DATABUS_WIDTH(bit11:10):LCD 数据总线宽度,为 0 的话总线宽度为 16 位;为
1 的话总线宽度为 8 位;为 2 的话总线宽度为 18 位;为 3 的话总线宽度为 24 位。本章我们使用 24 位总线宽度。
WORD_LENGTH(bit9:8):输入的数据格式,也就是像素数据宽度,为 0 的话每个像素 16
位;为 1 的话每个像素 8 位;为 2 的话每个像素 18 位;为 3 的话每个像素 24 位。
MASTER(bit5):为 1 的话设置 eLCDIF 工作在主模式。
DATA_FORMAT_16_BIT(bit3):当此位为 1 并且 WORD_LENGTH 为 0 的时候像素格式
为 ARGB555,当此位为 0 并且 WORD_LENGTH 为 0 的时候像素格式为 RGB565。
DATA_FORMAT_18_BIT(bit2):只有当 WORD_LENGTH 为 2 的时候此位才有效,此位
为 0 的话低 18 位有效,像素格为 RGB666,高 14 位数据无效。当此位为 1 的话高 18 位有效,
像素格式依旧是 RGB666,但是低 14 位数据无效。
DATA_FORMAT_24_BIT(bit1):只有当 WORD_LENGTH 为 3 的时候此位才有效,为 0 的
时候表示全部的 24 位数据都有效。为 1 的话实际输入的数据有效位只有 18 位,虽然输入的是
24 位数据,但是每个颜色通道的高 2 位数据会被丢弃掉。
RUN(bit0):eLCDIF 接口运行控制位,当此位为 1 的话 eLCDIF 接口就开始传输数据,也
就是 eLCDIF 的使能位。

LCDIF_CTRL1 寄存器

此寄存器我们只用到位 BYTE_PACKING_FORMAT(bit19:16),此位用来决定在 32 位的数据中哪些字节的数据有效,默认值为 0XF,也就是所有的字节有效,当为 0 的话表示所有的字节都无效。如果显示的数据是 24 位(ARGB 格式,但是 A 通道不传输)的话就设置此位为 0X7。

寄存器 LCDIF_TRANSFER_COUNT

这个寄存器用来设置所连接的 RGB LCD 屏幕分辨率大小,此寄存器结构如图所示:
在这里插入图片描述
LCDIF_TRANSFER_COUNT分为两部分,高16位和低16位,高16位是V_COUNT,是 LCD 的垂直分辨率。低 16 位是 H_COUNT,是 LCD 的水平分辨率。如果 LCD 分辨率为 1024*600 的话,那么 V_COUNT 就是 600,H_COUNT 就是 1024。

LCDIF_VDCTRL0 寄存器

这个寄存器是 VSYNC 和 DOTCLK 模式控制寄存器 0,寄存器结构如图所示:
在这里插入图片描述
LCDIF_VDCTRL0 用到的重要位如下:
VSYNC_OEB(bit29):VSYNC 信号方向控制位,为 0 的话 VSYNC 是输出,为 1 的话
VSYNC 是输入。
ENABLE_PRESENT(bit28):EBABLE 数据线使能位,也就是 DE 数据线。为 1 的话使能
ENABLE 数据线,为 0 的话关闭 ENABLE 数据线。
VSYNC_POL(bit27):VSYNC 数据线极性设置位,为 0 的话 VSYNC 低电平有效,为 1 的
话 VSYNC 高电平有效,要根据所使用的 LCD 数据手册来设置。
HSYNC_POL(bit26):HSYNC 数据线极性设置位,为 0 的话 HSYNC 低电平有效,为 1 的
话 HSYNC 高电平有效,要根据所使用的 LCD 数据手册来设置。
DOTCLK_POL(bit25):DOTCLK 数据线(像素时钟线 CLK) 极性设置位,为 0 的话下降沿
锁存数据,上升沿捕获数据,为 1 的话相反,要根据所使用的 LCD 数据手册来设置。
ENABLE_POL(bit24):EANBLE 数据线极性设置位,为 0 的话低电平有效,为 1 的话高
电平有效。
VSYNC_PERIOD_UNIT(bit21):VSYNC 信号周期单位,为 0 的话 VSYNC 周期单位为像
素时钟。为 1 的话 VSYNC 周期单位是水平行,如果使用 DOTCLK 模式话就要设置为 1。
VSYNC_PULSE_WIDTH_UNIT(bit20) : VSYNC 信 号 脉 冲 宽 度 单 位 , 和
VSYNC_PERIOD_UNUT 一样,如果使用 DOTCLK 模式的话要设置为 1。
VSYNC_PULSE_WIDTH(bit17:0):VSPW 参数设置位。

LCDIF_VDCTRL1 寄存器

这个寄存器是 VSYNC 和 DOTCLK 模式控制寄存器 1,此寄存器只有一个功能,用来设置 VSYNC 总周期,就是:屏幕高度+VSPW+VBP+VFP。

LCDIF_VDCTRL2 寄存器

这个寄存器分为高 16 位和低 16 位两部分,高 16 位是 HSYNC_PULSE_WIDTH,用来设置 HSYNC 信号宽度,也就是 HSPW。低 16 位是 HSYNC_PERIOD,设置 HSYNC 总周期,就是:屏幕宽度+HSPW+HBP+HFP。
此寄存器结构如图所示:
在这里插入图片描述
LCDIF_VDCTRL3 用到的重要位如下:
HORIZONTAL_WAIT_CNT(bit27:16):此位用于 DOTCLK 模式,用于设置 HSYNC 信号
产生到有效数据产生之间的时间,也就是 HSPW+HBP。
VERTICAL_WAIR_CNT(bit15:0):和 HORIZONTAL_WAIT_CNT 一样,只是此位用于
VSYNC 信号,也就是 VSPW+VBP。

LCDIF_VDCTRL4 寄存器

寄存器结构如图所示:
在这里插入图片描述
LCDIF_VDCTRL4 用到的重要位如下:
SYNC_SIGNALS_ON(bit18):同步信号使能位,设置为 1 的话使能 VSYNC、HSYNC、DOTCLK 这些信号。
DOTCLK_H_VALID_DATA_CNT(bit15:0):设置 LCD 的宽度,也就是水平像素数量。

LCDIF_CUR_BUF 和 LCDIF_NEXT_BUF 寄存器

这两个寄存器分别为当前帧和下一帧缓冲区,也就是 LCD 显存。一般这两个寄存器保存同一个地址,也就是划分给 LCD 的显存首地址。

3、LCD 裸机开发流程

  1. 初始化 LCD 所使用的 IO
    首先肯定是初始化 LCD 所示使用的 IO,将其复用为 eLCDIF 接口 IO。
  2. 设置 LCD 的像素时钟
    查阅所使用的 LCD 屏幕数据手册,或者自己计算出的时钟像素,然后设置 CCM 相应的寄存器。
  3. 配置 eLCDIF 接口
    设置 LCDIF 的寄存器 CTRL、CTRL1、TRANSFER_COUNT、VDCTRL0~4、CUR_BUF 和 NEXT_BUF。根据 LCD 的数据手册设置相应的参数。
  4. 编写 API 函数
    驱动 LCD 屏幕的目的就是显示内容,所以需要编写一些基本的 API 函数,比如画点、画
    线、画圆函数,字符串显示函数等。

硬件原理分析

本试验用到的资源如下:
①、指示灯 LED0。
②、RGB LCD 接口。
③、DDR3
④、eLCDIF
RGB LCD 接口在 I.MX6U-ALPHA 开发板底板上,原理图如图所示:
在这里插入图片描述
三个 SGM3157 的目的是在未使用 RGBLCD 的时候将 LCD_DATA7、LCD_DATA15 和 LCD_DATA23 这三个线隔离开来,因为屏幕的 LCD_R7/G7/B7 这几个线用来设置 LCD 的 ID,所以这几根线上有上拉/下拉电阻。
实际不需要这么麻烦。到了驱动,ID 都是写进设备树的。

程序编写

lcd.h
#include "fsl_common.h"
#include "fsl_iomuxc.h"

/* 颜色 */
#define LCD_BLUE          0x000000FF
#define LCD_GREEN          0x0000FF00
#define LCD_RED           0x00FF0000
#define LCD_CYAN          0x0000FFFF
#define LCD_MAGENTA       0x00FF00FF
#define LCD_YELLOW          0x00FFFF00
#define LCD_LIGHTBLUE      0x008080FF
#define LCD_LIGHTGREEN      0x0080FF80
#define LCD_LIGHTRED      0x00FF8080
#define LCD_LIGHTCYAN      0x0080FFFF
#define LCD_LIGHTMAGENTA  0x00FF80FF
#define LCD_LIGHTYELLOW   0x00FFFF80
#define LCD_DARKBLUE      0x00000080
#define LCD_DARKGREEN      0x00008000
#define LCD_DARKRED       0x00800000
#define LCD_DARKCYAN      0x00008080
#define LCD_DARKMAGENTA   0x00800080
#define LCD_DARKYELLOW      0x00808000
#define LCD_WHITE          0x00FFFFFF
#define LCD_LIGHTGRAY      0x00D3D3D3
#define LCD_GRAY          0x00808080
#define LCD_DARKGRAY      0x00404040
#define LCD_BLACK          0x00000000
#define LCD_BROWN          0x00A52A2A
#define LCD_ORANGE          0x00FFA500
#define LCD_TRANSPARENT   0x00000000

/* LCD显存地址 */
#define LCD_FRAMEBUF_ADDR    (0x89000000)

/* LCD控制参数结构体 */
struct tftlcd_typedef{
    unsigned short height;        /* LCD屏幕高度 */
    unsigned short width;        /* LCD屏幕宽度 */
    unsigned char pixsize;        /* LCD每个像素所占字节大小 */
    unsigned short vspw;
    unsigned short vbpd;
    unsigned short vfpd;
    unsigned short hspw;
    unsigned short hbpd;
    unsigned short hfpd;
    unsigned int framebuffer;     /* LCD显存首地址         */
    unsigned int forecolor;        /* 前景色 */
    unsigned int backcolor;        /* 背景色 */
};

extern struct tftlcd_typedef tftlcd_dev;

/* 函数声明 */
void lcd_init(void);
void lcdgpio_init(void);
void lcdclk_init(unsigned char loopDiv, unsigned char prediv, unsigned char div);
void lcd_reset(void);
void lcd_noreset(void);
void lcd_enable(void);
void video_pllinit(unsigned char loopdivi, unsigned char postdivi);

inline void lcd_drawpoint(unsigned short x,unsigned short y,unsigned int color);
inline unsigned int lcd_readpoint(unsigned short x,unsigned short y);
void lcd_clear(unsigned int color);
void lcd_fill(unsigned    short x0, unsigned short y0, unsigned short x1, unsigned short y1, unsigned int color);
lcd.c
#include "bsp_lcd.h"
#include "bsp_gpio.h"
#include "bsp_delay.h"
#include "stdio.h"

/* 液晶屏参数结构体 */
struct tftlcd_typedef tftlcd_dev;

/*
 * @description    : 始化LCD
 * @param         : 无
 * @return         : 无
 */
void lcd_init(void)
{
    lcdgpio_init();            /* 初始化IO         */
    lcdclk_init(32, 3, 5);    /* 初始化LCD时钟 */
    lcd_reset();            /* 复位LCD      */
    delayms(10);            /* 延时10ms     */
    lcd_noreset();            /* 结束复位         */

    /* TFTLCD参数结构体初始化 */
    tftlcd_dev.height = 600;    
    tftlcd_dev.width = 1024;
    tftlcd_dev.pixsize = 4;                /* ARGB8888模式,每个像素4字节 */
    tftlcd_dev.vspw = 3;
    tftlcd_dev.vbpd = 20;
    tftlcd_dev.vfpd = 12;
    tftlcd_dev.hspw = 20;
    tftlcd_dev.hbpd = 140;
    tftlcd_dev.hfpd = 160;
    tftlcd_dev.framebuffer = LCD_FRAMEBUF_ADDR;    
    tftlcd_dev.backcolor = LCD_WHITE;    /* 背景色为白色 */
    tftlcd_dev.forecolor = LCD_BLACK;    /* 前景色为黑色 */
    
    /* 初始化ELCDIF的CTRL寄存器
     * bit [31] 0 : 停止复位
     * bit [19] 1 : 旁路计数器模式
     * bit [17] 1 : LCD工作在dotclk模式
     * bit [15:14] 00 : 输入数据不交换
     * bit [13:12] 00 : CSC不交换
     * bit [11:10] 11 : 24位总线宽度
     * bit [9:8]   11 : 24位数据宽度,也就是RGB888
     * bit [5]     1  : elcdif工作在主模式
     * bit [1]     0  : 所有的24位均有效
     */
     LCDIF->CTRL |= (1 << 19) | (1 << 17) | (0 << 14) | (0 << 12) |
                     (3 << 10) | (3 << 8) | (1 << 5) | (0 << 1);
    /*
     * 初始化ELCDIF的寄存器CTRL1
     * bit [19:16]  : 0X7 ARGB模式下,传输24位数据,A通道不用传输
     */    
     LCDIF->CTRL1 = 0X7 << 16; 

     /*
      * 初始化ELCDIF的寄存器TRANSFER_COUNT寄存器
      * bit [31:16]  : 高度
      * bit [15:0]   : 宽度
      */
    LCDIF->TRANSFER_COUNT  = (tftlcd_dev.height << 16) | (tftlcd_dev.width << 0);

    /*
     * 初始化ELCDIF的VDCTRL0寄存器
     * bit [29] 0 : VSYNC输出
     * bit [28] 1 : 使能ENABLE输出
     * bit [27] 0 : VSYNC低电平有效
     * bit [26] 0 : HSYNC低电平有效
     * bit [25] 0 : DOTCLK上升沿有效
     * bit [24] 1 : ENABLE信号高电平有效
     * bit [21] 1 : DOTCLK模式下设置为1
     * bit [20] 1 : DOTCLK模式下设置为1
     * bit [17:0] : vsw参数
     */
    LCDIF->VDCTRL0 = 0;    //先清零
    LCDIF->VDCTRL0 = (0 << 29) | (1 << 28) | (0 << 27) |
                     (0 << 26) | (0 << 25) | (1 << 24) |
                     (1 << 21) | (1 << 20) | (tftlcd_dev.vspw << 0);
    /*
     * 初始化ELCDIF的VDCTRL1寄存器
     * 设置VSYNC总周期
     */  
    LCDIF->VDCTRL1 = tftlcd_dev.height + tftlcd_dev.vspw + tftlcd_dev.vfpd + tftlcd_dev.vbpd;  //VSYNC周期

     /*
      * 初始化ELCDIF的VDCTRL2寄存器
      * 设置HSYNC周期
      * bit[31:18] :hsw
      * bit[17:0]  : HSYNC总周期
      */ 
    LCDIF->VDCTRL2 = (tftlcd_dev.hspw << 18) | (tftlcd_dev.width + tftlcd_dev.hspw + tftlcd_dev.hfpd + tftlcd_dev.hbpd);

    /*
     * 初始化ELCDIF的VDCTRL3寄存器
     * 设置HSYNC周期
     * bit[27:16] :水平等待时钟数
     * bit[15:0]  : 垂直等待时钟数
     */ 
    LCDIF->VDCTRL3 = ((tftlcd_dev.hbpd + tftlcd_dev.hspw) << 16) | (tftlcd_dev.vbpd + tftlcd_dev.vspw);

    /*
     * 初始化ELCDIF的VDCTRL4寄存器
     * 设置HSYNC周期
     * bit[18] 1 : 当使用VSHYNC、HSYNC、DOTCLK的话此为置1
     * bit[17:0]  : 宽度
     */ 
    
    LCDIF->VDCTRL4 = (1<<18) | (tftlcd_dev.width);

    /*
     * 初始化ELCDIF的CUR_BUF和NEXT_BUF寄存器
     * 设置当前显存地址和下一帧的显存地址
     */
    LCDIF->CUR_BUF = (unsigned int)tftlcd_dev.framebuffer;
    LCDIF->NEXT_BUF = (unsigned int)tftlcd_dev.framebuffer;
    lcd_enable();            /* 使能LCD */
    delayms(10);
    lcd_clear(LCD_WHITE);    /* 清屏 */
    
}

/*
 * IO引脚:     LCD_DATA00 -> LCD_B0
 *            LCD_DATA01 -> LCD_B1
 *            LCD_DATA02 -> LCD_B2
 *            LCD_DATA03 -> LCD_B3
 *            LCD_DATA04 -> LCD_B4
 *            LCD_DATA05 -> LCD_B5
 *            LCD_DATA06 -> LCD_B6
 *            LCD_DATA07 -> LCD_B7
 *
 *            LCD_DATA08 -> LCD_G0
 *            LCD_DATA09 -> LCD_G1
 *            LCD_DATA010 -> LCD_G2
 *            LCD_DATA011 -> LCD_G3
 *            LCD_DATA012 -> LCD_G4
 *            LCD_DATA012 -> LCD_G4
 *            LCD_DATA013 -> LCD_G5
 *            LCD_DATA014 -> LCD_G6
 *            LCD_DATA015 -> LCD_G7
 *
 *            LCD_DATA016 -> LCD_R0
 *            LCD_DATA017 -> LCD_R1
 *            LCD_DATA018 -> LCD_R2 
 *            LCD_DATA019 -> LCD_R3
 *            LCD_DATA020 -> LCD_R4
 *            LCD_DATA021 -> LCD_R5
 *            LCD_DATA022 -> LCD_R6
 *            LCD_DATA023 -> LCD_R7
 *
 *            LCD_CLK -> LCD_CLK
 *            LCD_VSYNC -> LCD_VSYNC
 *            LCD_HSYNC -> LCD_HSYNC
 *            LCD_DE -> LCD_DE
 *            LCD_BL -> GPIO1_IO08 
 */
 
/*
 * @description    : LCD GPIO初始化
 * @param         : 无
 * @return         : 无
 */
void lcdgpio_init(void)
{
    gpio_pin_config_t gpio_config;
    

    /* 1、IO初始化复用功能 */
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA00_LCDIF_DATA00,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA01_LCDIF_DATA01,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA02_LCDIF_DATA02,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA03_LCDIF_DATA03,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA04_LCDIF_DATA04,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA05_LCDIF_DATA05,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA06_LCDIF_DATA06,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA07_LCDIF_DATA07,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA08_LCDIF_DATA08,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA09_LCDIF_DATA09,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA10_LCDIF_DATA10,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA11_LCDIF_DATA11,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA12_LCDIF_DATA12,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA13_LCDIF_DATA13,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA14_LCDIF_DATA14,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA15_LCDIF_DATA15,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA16_LCDIF_DATA16,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA17_LCDIF_DATA17,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA18_LCDIF_DATA18,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA19_LCDIF_DATA19,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA20_LCDIF_DATA20,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA21_LCDIF_DATA21,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA22_LCDIF_DATA22,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_DATA23_LCDIF_DATA23,0);
 
    IOMUXC_SetPinMux(IOMUXC_LCD_CLK_LCDIF_CLK,0);    
    IOMUXC_SetPinMux(IOMUXC_LCD_ENABLE_LCDIF_ENABLE,0);    
    IOMUXC_SetPinMux(IOMUXC_LCD_HSYNC_LCDIF_HSYNC,0);
    IOMUXC_SetPinMux(IOMUXC_LCD_VSYNC_LCDIF_VSYNC,0);

    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO08_GPIO1_IO08,0);     /* 背光BL引脚 */

    /* 2、配置LCD IO属性    
     *bit 16:0 HYS关闭
     *bit [15:14]: 0 默认22K上拉
     *bit [13]: 0 pull功能
     *bit [12]: 0 pull/keeper使能 
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 111 驱动能力为R0/7
     *bit [0]: 1 高转换率
     */
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA00_LCDIF_DATA00,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA01_LCDIF_DATA01,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA02_LCDIF_DATA02,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA03_LCDIF_DATA03,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA04_LCDIF_DATA04,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA05_LCDIF_DATA05,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA06_LCDIF_DATA06,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA07_LCDIF_DATA07,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA08_LCDIF_DATA08,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA09_LCDIF_DATA09,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA10_LCDIF_DATA10,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA11_LCDIF_DATA11,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA12_LCDIF_DATA12,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA13_LCDIF_DATA13,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA14_LCDIF_DATA14,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA15_LCDIF_DATA15,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA16_LCDIF_DATA16,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA17_LCDIF_DATA17,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA18_LCDIF_DATA18,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA19_LCDIF_DATA19,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA20_LCDIF_DATA20,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA21_LCDIF_DATA21,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA22_LCDIF_DATA22,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_DATA23_LCDIF_DATA23,0xB9);

    IOMUXC_SetPinConfig(IOMUXC_LCD_CLK_LCDIF_CLK,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_ENABLE_LCDIF_ENABLE,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_HSYNC_LCDIF_HSYNC,0xB9);
    IOMUXC_SetPinConfig(IOMUXC_LCD_VSYNC_LCDIF_VSYNC,0xB9);

    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO08_GPIO1_IO08,0xB9);    /* 背光BL引脚         */

    /* GPIO初始化 */
    gpio_config.direction = kGPIO_DigitalOutput;            /* 输出             */
    gpio_config.outputLogic = 1;                             /* 默认关闭背光 */
    gpio_init(GPIO1, 8, &gpio_config);                        /* 背光默认打开 */
    gpio_pinwrite(GPIO1, 8, 1);                                /* 打开背光     */
}

/*
 * @description        : LCD时钟初始化, LCD时钟计算公式如下:
 *                      LCD CLK = 24 * loopDiv / prediv / div
 * @param -    loopDiv    : loopDivider值
 * @param -    loopDiv : lcdifprediv值
 * @param -    div        : lcdifdiv值
 * @return             : 无
 */
void lcdclk_init(unsigned char loopDiv, unsigned char prediv, unsigned char div)
{
    /* 先初始化video pll 
     * VIDEO PLL = OSC24M * (loopDivider + (denominator / numerator)) / postDivider
      *不使用小数分频器,因此denominator和numerator设置为0
      */
    CCM_ANALOG->PLL_VIDEO_NUM = 0;        /* 不使用小数分频器 */
    CCM_ANALOG->PLL_VIDEO_DENOM = 0;    

    /*
     * PLL_VIDEO寄存器设置
     * bit[13]:    1   使能VIDEO PLL时钟
     * bit[20:19]  2  设置postDivider为1分频
     * bit[6:0] : 32  设置loopDivider寄存器
     */
    CCM_ANALOG->PLL_VIDEO =  (2 << 19) | (1 << 13) | (loopDiv << 0); 

    /*
     * MISC2寄存器设置
     * bit[31:30]: 0  VIDEO的post-div设置,时钟源来源于postDivider,1分频
     */
    CCM_ANALOG->MISC2 &= ~(3 << 30);
    CCM_ANALOG->MISC2 = 0 << 30;

    /* LCD时钟源来源与PLL5,也就是VIDEO           PLL  */
    CCM->CSCDR2 &= ~(7 << 15);      
    CCM->CSCDR2 |= (2 << 15);            /* 设置LCDIF_PRE_CLK使用PLL5 */

    /* 设置LCDIF_PRE分频 */
    CCM->CSCDR2 &= ~(7 << 12);        
    CCM->CSCDR2 |= (prediv - 1) << 12;    /* 设置分频  */

    /* 设置LCDIF分频 */
    CCM->CBCMR &= ~(7 << 23);                    
    CCM->CBCMR |= (div - 1) << 23;                

    /* 设置LCD时钟源为LCDIF_PRE时钟 */
    CCM->CSCDR2 &= ~(7 << 9);                    /* 清除原来的设置             */
    CCM->CSCDR2 |= (0 << 9);                    /* LCDIF_PRE时钟源选择LCDIF_PRE时钟 */
}

/*
 * @description    : 复位ELCDIF接口
 * @param         : 无
 * @return         : 无
 */
void lcd_reset(void)
{
    LCDIF->CTRL  = 1<<31; /* 强制复位 */
}

/*
 * @description    : 结束复位ELCDIF接口
 * @param         : 无
 * @return         : 无
 */
void lcd_noreset(void)
{
    LCDIF->CTRL  = 0<<31; /* 取消强制复位 */
}

/*
 * @description    : 使能ELCDIF接口
 * @param         : 无
 * @return         : 无
 */
void lcd_enable(void)
{
    LCDIF->CTRL |= 1<<0; /* 使能ELCDIF */
}

/*
 * @description        : 画点函数 
 * @param - x        : x轴坐标
 * @param - y        : y轴坐标
 * @param - color    : 颜色值
 * @return             : 无
 */
inline void lcd_drawpoint(unsigned short x,unsigned short y,unsigned int color)
{ 
      *(unsigned int*)((unsigned int)tftlcd_dev.framebuffer + 
                     tftlcd_dev.pixsize * (tftlcd_dev.width * y+x))=color;
}

/*
 * @description        : 读取指定点的颜色值
 * @param - x        : x轴坐标
 * @param - y        : y轴坐标
 * @return             : 读取到的指定点的颜色值
 */
inline unsigned int lcd_readpoint(unsigned short x,unsigned short y)
{ 
    return *(unsigned int*)((unsigned int)tftlcd_dev.framebuffer + 
           tftlcd_dev.pixsize * (tftlcd_dev.width * y + x));
}

/*
 * @description        : 清屏
 * @param - color    : 颜色值
 * @return             : 读取到的指定点的颜色值
 */
void lcd_clear(unsigned int color)
{
    unsigned int num;
    unsigned int i = 0; 

    unsigned int *startaddr=(unsigned int*)tftlcd_dev.framebuffer;    //指向帧缓存首地址
    num=(unsigned int)tftlcd_dev.width * tftlcd_dev.height;            //缓冲区总长度
    for(i = 0; i < num; i++)
    {
        startaddr[i] = color;
    }        
}

/*
 * @description        : 以指定的颜色填充一块矩形
 * @param - x0        : 矩形起始点坐标X轴
 * @param - y0        : 矩形起始点坐标Y轴
 * @param - x1        : 矩形终止点坐标X轴
 * @param - y1        : 矩形终止点坐标Y轴
 * @param - color    : 要填充的颜色
 * @return             : 读取到的指定点的颜色值
 */
void lcd_fill(unsigned short x0, unsigned short y0, 
              unsigned short x1, unsigned short y1, 
              unsigned int color)
{
    unsigned short x, y;

    if(x0 < 0) x0 = 0;
    if(y0 < 0) y0 = 0;
    if(x1 >= tftlcd_dev.width) x1 = tftlcd_dev.width - 1;
    if(y1 >= tftlcd_dev.height) y1 = tftlcd_dev.height - 1;
    for(y = y0; y <= y1; y++)
    {
        for(x = x0; x <= x1; x++)
            lcd_drawpoint(x, y, color);
    }
}
lcdAPI.h
#include "bsp_lcd.h"

/* 函数声明 */
void lcd_drawline(unsigned short x1, unsigned short y1, unsigned short x2, unsigned short y2);
void lcd_draw_rectangle(unsigned short x1, unsigned short y1, unsigned short x2, unsigned short y2);
void lcd_draw_Circle(unsigned short x0,unsigned short y0,unsigned char r);
void lcd_showchar(unsigned     short x,unsigned short y,unsigned char num,unsigned char size, unsigned char mode);
unsigned int lcd_pow(unsigned char m,unsigned char n);
void lcd_shownum(unsigned short x, unsigned short y, unsigned int num, unsigned char len,unsigned char size);
void lcd_showxnum(unsigned short x, unsigned short y, unsigned int num, unsigned char len, unsigned char size, unsigned char mode);
void lcd_show_string(unsigned short x,unsigned short y,
unsigned short width, unsigned short height, unsigned char size,char *p);
lcdAPI.c
#include "bsp_lcdapi.h"
#include "font.h"        //ASCII字符编码格式,内核中会有

/*
 * @description        : 画线函数
 * @param - x1        : 线起始点坐标X轴
 * @param - y1        : 线起始点坐标Y轴
 * @param - x2        : 线终止点坐标X轴
 * @param - y2        : 线终止点坐标Y轴
 * @return             : 无
 */ 
void lcd_drawline(unsigned short x1, unsigned short y1, unsigned short x2, unsigned short y2)
{
    u16 t; 
    int xerr = 0, yerr = 0, delta_x, delta_y, distance; 
    int incx, incy, uRow, uCol; 
    delta_x = x2 - x1;                     /* 计算坐标增量 */
    delta_y = y2 - y1; 
    uRow = x1; 
    uCol = y1; 
    if(delta_x > 0)                     /* 设置单步方向 */ 
        incx = 1;
    else if(delta_x==0)                    /* 垂直线 */
        incx = 0;        
    else 
    {
        incx = -1;
        delta_x = -delta_x;
    } 
    if(delta_y>0)
        incy=1; 
    else if(delta_y == 0)                /* 水平线 */
        incy=0;
    else
    {
        incy = -1;
        delta_y = -delta_y;
    } 
    if( delta_x > delta_y)                /*选取基本增量坐标轴  */
        distance = delta_x; 
    else 
        distance = delta_y; 
    for(t = 0; t <= distance+1; t++ )    /* 画线输出 */
    {  

        lcd_drawpoint(uRow, uCol, tftlcd_dev.forecolor);/* 画点 */
        xerr += delta_x ; 
        yerr += delta_y ; 
        if(xerr > distance) 
        { 
            xerr -= distance; 
            uRow += incx; 
        } 
        if(yerr > distance) 
        { 
            yerr -= distance; 
            uCol += incy; 
        } 
    }  
}   

/*
 * @description    : 画矩形函数
 * @param - x1    : 矩形坐上角坐标X轴
 * @param - y1    : 矩形坐上角坐标Y轴
 * @param - x2    : 矩形右下角坐标X轴
 * @param - y2    : 矩形右下角坐标Y轴
 * @return         : 无
 */
void lcd_draw_rectangle(unsigned short x1, unsigned short y1, unsigned short x2, unsigned short y2)
{
    lcd_drawline(x1, y1, x2, y1);
    lcd_drawline(x1, y1, x1, y2);
    lcd_drawline(x1, y2, x2, y2);
    lcd_drawline(x2, y1, x2, y2);
}

/*
 * @description    : 在指定位置画一个指定大小的圆
 * @param - x0    : 圆心坐标X轴
 * @param - y0    : 圆心坐标Y轴
 * @param - y2    : 圆形半径
 * @return         : 无
 */
void lcd_draw_Circle(unsigned short x0,unsigned short y0,unsigned char r)
{
    int mx = x0, my = y0;
    int x = 0, y = r;

    int d = 1 - r;   
    while(y > x)        /* y>x即第一象限的第1区八分圆 */
    {
        lcd_drawpoint(x  + mx, y  + my, tftlcd_dev.forecolor);
        lcd_drawpoint(y  + mx, x  + my, tftlcd_dev.forecolor);
        lcd_drawpoint(-x + mx, y  + my, tftlcd_dev.forecolor);
        lcd_drawpoint(-y + mx, x  + my, tftlcd_dev.forecolor);

        lcd_drawpoint(-x + mx, -y + my, tftlcd_dev.forecolor);
        lcd_drawpoint(-y + mx, -x + my, tftlcd_dev.forecolor);
        lcd_drawpoint(x  + mx, -y + my, tftlcd_dev.forecolor);
        lcd_drawpoint(y  + mx, -x + my, tftlcd_dev.forecolor);
        if( d < 0)
        {
            d = d + 2 * x + 3;
        }
        else
        {
            d= d + 2 * (x - y) + 5;
            y--;
        }
        x++;
    }
}

/*
 * @description    : 在指定位置显示一个字符
 * @param - x    : 起始坐标X轴
 * @param - y    : 起始坐标Y轴
 * @param - num    : 显示字符
 * @param - size: 字体大小, 可选12/16/24/32
 * @param - mode: 叠加方式(1)还是非叠加方式(0)
 * @return         : 无
 */
void lcd_showchar(unsigned     short x, unsigned short y,
                      unsigned char num, unsigned char size, 
                      unsigned char mode)
{                                
    unsigned char  temp, t1, t;
    unsigned short y0 = y;
    unsigned char csize = (size / 8+ ((size % 8) ? 1 : 0)) * (size / 2);    /* 得到字体一个字符对应点阵集所占的字节数     */    
     num = num - ' ';      /*得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库)  */
    for(t = 0; t < csize; t++)
    {   
        if(size == 12) temp = asc2_1206[num][t];         /* 调用1206字体 */
        else if(size == 16)temp = asc2_1608[num][t];    /* 调用1608字体 */
        else if(size == 24)temp = asc2_2412[num][t];    /* 调用2412字体 */
        else if(size == 32)temp = asc2_3216[num][t];    /* 调用3216字体 */
        else return;                                    /* 没有的字库         */
        for(t1 = 0; t1 < 8; t1++)
        {                
            if(temp & 0x80)lcd_drawpoint(x, y, tftlcd_dev.forecolor);
            else if(mode==0)lcd_drawpoint(x, y, tftlcd_dev.backcolor);
            temp <<= 1;
            y++;
            if(y >= tftlcd_dev.height) return;            /* 超区域了 */    
            if((y - y0) == size)
            {
                y = y0;
                x++;
                if(x >= tftlcd_dev.width) return;        /* 超区域了 */
                break;
            }
        }       
    }                                
} 

/*
 * @description    : 计算m的n次方
 * @param - m    : 要计算的值
 * @param - n    : n次方
 * @return         : m^n次方.
 */
unsigned int lcd_pow(unsigned char m,unsigned char n)
{
    unsigned int result = 1;     
    while(n--) result *= m;    
    return result;
}

/*
 * @description    : 显示指定的数字,高位为0的话不显示
 * @param - x    : 起始坐标点X轴。
 * @param - y    : 起始坐标点Y轴。
 * @param - num    : 数值(0~999999999)。
 * @param - len : 数字位数。
 * @param - size: 字体大小
 * @return         : 无
 */
void lcd_shownum(unsigned     short x, 
                     unsigned short y, 
                     unsigned int num, 
                     unsigned char len,
                     unsigned char size)
{             
    unsigned char  t, temp;
    unsigned char  enshow = 0;                           
    for(t = 0; t < len; t++)
    {
        temp = (num / lcd_pow(10, len - t - 1)) % 10;
        if(enshow == 0 && t < (len - 1))
        {
            if(temp == 0)
            {
                lcd_showchar(x + (size / 2) * t, y, ' ', size, 0);
                continue;
            }else enshow = 1;      
        }
         lcd_showchar(x + (size / 2) * t, y, temp + '0', size, 0); 
    }
} 

/*
 * @description        : 显示指定的数字,高位为0,还是显示
 * @param - x         : 起始坐标点X轴。
 * @param - y         : 起始坐标点Y轴。
 * @param - num        : 数值(0~999999999)。
 * @param - len     : 数字位数。
 * @param - size    : 字体大小
 * @param - mode    : [7]:0,不填充;1,填充0.
 *                       [6:1]:保留
 *                      [0]:0,非叠加显示;1,叠加显示.
 * @return              : 无
 */
void lcd_showxnum(unsigned     short x, unsigned short y, 
                      unsigned int num, unsigned char len, 
                      unsigned char size, unsigned char mode)
{  
    unsigned char t, temp;
    unsigned char enshow = 0;                           
    for(t = 0; t < len; t++)
    {
        temp = (num / lcd_pow(10, len - t- 1)) % 10;
        if(enshow == 0 && t < (len - 1))
        {
            if(temp == 0)
            {
                if(mode & 0X80) lcd_showchar(x + (size / 2) * t, y, '0', size, mode & 0X01);  
                else  lcd_showchar(x + (size / 2) * t, y , ' ', size, mode & 0X01);  
                 continue;
            }else enshow=1; 
              
        }
         lcd_showchar( x + (size / 2) * t, y, temp + '0' , size , mode & 0X01); 
    }
} 

/*
 * @description        : 显示一串字符串
 * @param - x        : 起始坐标点X轴。
 * @param - y        : 起始坐标点Y轴。
 * @param - width     : 字符串显示区域长度
 * @param - height    : 字符串显示区域高度
 * @param - size    : 字体大小
 * @param - p        : 要显示的字符串首地址
 * @return             : 无
 */
void lcd_show_string(unsigned short x,unsigned short y,
                          unsigned short width,unsigned short height,
                          unsigned char size,char *p)
{         
    unsigned char x0 = x;
    width += x;
    height += y;
    while((*p <= '~') &&(*p >= ' '))        /* 判断是不是非法字符! */ 
    {       
        if(x >= width) {x = x0; y += size;}
        if(y >= height) break;                /* 退出 */
        lcd_showchar(x, y, *p , size, 0);
        x += size / 2;
        p++;
    }  
}

二、驱动篇

之前裸机开发 LCD 的步骤:
①、初始化 I.MX6U 的 eLCDIF 控制器,重点是 LCD 屏幕宽(width)、高(height)、hspw、
hbp、hfp、vspw、vbp 和 vfp 等信息。
②、初始化 LCD 像素时钟。
③、设置 RGBLCD 显存。
④、应用程序直接通过操作显存来操作 LCD,实现在 LCD 上显示字符、图片等信息。
在 Linux 中应用程序最终也是通过操作 RGB LCD 的显存来实现在 LCD 上显示字符、图片
等信息。在裸机中我们可以随意的分配显存,但是在 Linux 系统中内存的管理很严格,显存需要申请。而且有虚拟内存的存在,驱动程序设置的显存和应用程
序访问的显存必须是同一片物理内存。
所以先来看一个很重要的概念:Framebuffer

* Framebuffer

帧缓冲,简称 fb,这玩意竟然被设计成设备,在 Linux 虚拟机中可以看到/dev/fbX(X=0~n)的设备,这些设备其实都是显存。
我们要操作/读取屏幕,直接对显存下手就可以了。
帧缓冲设备操作集

/* drivers/video/fbdev/core/fbmem.c */
static const struct file_operations fb_fops = {
  .owner = THIS_MODULE,
  .read = fb_read,
  .write = fb_write,
  .unlocked_ioctl = fb_ioctl,
#ifdef CONFIG_COMPAT
  .compat_ioctl = fb_compat_ioctl,
#endif
  .mmap = fb_mmap,
  .open = fb_open,
  .release = fb_release,
#ifdef HAVE_ARCH_FB_UNMAPPED_AREA
  .get_unmapped_area = get_fb_unmapped_area,
#endif
#ifdef CONFIG_FB_DEFERRED_IO
  .fsync = fb_deferred_io_fsync,
#endif
  .llseek = default_llseek,
};

LCD 驱动简析

/* drivers/video/fbdev/mxsfb.c,mxsfb.c */
static const struct of_device_id mxsfb_dt_ids[] = {
 { .compatible = "fsl,imx23-lcdif", .data = &mxsfb_devtype[0], },
 { .compatible = "fsl,imx28-lcdif", .data = &mxsfb_devtype[1], },
 { /* sentinel */ }
};
......
struct platform_driver mxsfb_driver = {
 .probe = mxsfb_probe,
 .remove = mxsfb_remove,
 .shutdown = mxsfb_shutdown,
 .id_table = mxsfb_devtype,
 .driver = {
 .name = DRIVER_NAME,
 .of_match_table = mxsfb_dt_ids,
 .pm = &mxsfb_pm_ops,
 },
};

module_platform_driver(mxsfb_driver);

Linux 内核将所有的 Framebuffer 抽象为一个叫做 fb_info 的结构体,fb_info 结构体包含了 Framebuffer 设备的完整属性和操作集合,因此每一个 Framebuffer 设备都必须有一个 fb_info。
LCD 的驱动就是构建 fb_info,并且向系统注册 fb_info的过程!!!

/* include/linux/fb.h */
struct fb_info {
 atomic_t count;
 int node;
 int flags;
 struct mutex lock;               /* 互斥锁 */
 struct mutex mm_lock;            /* 互斥锁,用于 fb_mmap 和 smem_*域*/
 struct fb_var_screeninfo var;    /* 当前可变参数 */
 struct fb_fix_screeninfo fix;    /* 当前固定参数 */
 struct fb_monspecs monspecs;     /* 当前显示器特性 */
 struct work_struct queue;        /* 帧缓冲事件队列 */
 struct fb_pixmap pixmap;         /* 图像硬件映射 */
 struct fb_pixmap sprite;         /* 光标硬件映射 */
 struct fb_cmap cmap;             /* 当前调色板 */
 struct list_head modelist;       /* 当前模式列表 */
 struct fb_videomode *mode;       /* 当前视频模式 */

#ifdef CONFIG_FB_BACKLIGHT        /* 如果 LCD 支持背光的话 */
 /* assigned backlight device */
 /* set before framebuffer registration, 
 remove after unregister */
 struct backlight_device *bl_dev; /* 背光设备 */

 /* Backlight level curve */
 struct mutex bl_curve_mutex; 
 u8 bl_curve[FB_BACKLIGHT_LEVELS];
#endif
......
 struct fb_ops *fbops;          /* 帧缓冲操作函数集 */ 
 struct device *device;         /* 父设备 */
 struct device *dev;            /* 当前 fb 设备 */
 int class_flag;                /* 私有 sysfs 标志 */
 ......
 char __iomem *screen_base;     /* 虚拟内存基地址(屏幕显存) */
 unsigned long screen_size;     /* 虚拟内存大小(屏幕显存大小) */
 void *pseudo_palette;          /* 伪 16 位调色板 */
 ......
};

fb_info 结构体的成员变量很多,我们重点关注 var、fix、fbops、screen_base、screen_size
和 pseudo_palette。mxsfb_probe 函数的主要工作内容为:
①、申请 fb_info。
②、初始化 fb_info 结构体中的各个成员变量。
③、初始化 eLCDIF 控制器。
④、使用 register_framebuffer 函数向 Linux 内核注册初始化好的 fb_info。register_framebuffer
函数原型如下:

int register_framebuffer(struct fb_info *fb_info)

fb_info:需要上报的 fb_info。

裸机开发步骤中第一步就是配置时钟和屏幕时序,这部分之前就一直强调驱动中是放到设备树中实现的。

lcdif: lcdif@021c8000 {
 compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
 reg = <0x021c8000 0x4000>;
 interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
 clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
 <&clks IMX6UL_CLK_LCDIF_APB>,
 <&clks IMX6UL_CLK_DUMMY>;
 clock-names = "pix", "axi", "disp_axi";
 status = "disabled";
};

LCD 相关寄存器相对 lcdif 基址的偏移量

#define LCDC_CTRL 0x00
#define LCDC_CTRL1 0x10
#define LCDC_V4_CTRL2 0x20
#define LCDC_V3_TRANSFER_COUNT 0x20
#define LCDC_V4_TRANSFER_COUNT 0x30
......
#define LCDC_V4_DEBUG0 0x1d0
#define LCDC_V3_DEBUG0 0x1f0

proc 函数

static int mxsfb_probe(struct platform_device *pdev)
{
 const struct of_device_id *of_id =
 of_match_device(mxsfb_dt_ids, &pdev->dev);
 struct resource *res;
 struct mxsfb_info *host;        //指向处理器的 LCD 主控接口
 struct fb_info *fb_info;
 struct pinctrl *pinctrl;
 int irq = platform_get_irq(pdev, 0);
 int gpio, ret;
 ......
 /* 从设备树中获取 eLCDIF 接口控制器的寄存器首地址,设备树中 lcdif 节点已经
    设置了 eLCDIF 寄存器首地址为 0X021C8000,因此 res=0X021C8000 */
 res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
 if (!res) {
     dev_err(&pdev->dev, "Cannot get memory IO resource\n");
     return -ENODEV;
 }
 host = devm_kzalloc(&pdev->dev, sizeof(struct mxsfb_info), GFP_KERNEL);
 if (!host) {
     dev_err(&pdev->dev, "Failed to allocate IO resource\n");
     return -ENOMEM;
 }
 fb_info = framebuffer_alloc(sizeof(struct fb_info), &pdev->dev);//申请 fb_info
 if (!fb_info) {
     dev_err(&pdev->dev, "Failed to allocate fbdev\n");
     devm_kfree(&pdev->dev, host);
     return -ENOMEM;
 }
 /* 设置 host 的 fb_info 成员变量为 fb_info,设置 fb_info 的 par 成员变量为 host。
 通过这一步就将前面申请的 host 和 fb_info 联系在了一起 */
 host->fb_info = fb_info;
 fb_info->par = host;

 /* 申请中断,中断服务函数为 mxsfb_irq_handler */
 ret = devm_request_irq(&pdev->dev, irq, mxsfb_irq_handler, 0, dev_name(&pdev->dev), host);
 if (ret) {
     dev_err(&pdev->dev, "request_irq (%d) failed with error %d\n", irq, ret);
     ret = -ENODEV;
     goto fb_release;
 }
 /* 从设备树中获取到的寄存器首地址(res)进行内存映射,得到虚拟地址,并保存到 host 的 base 成员变量
    通过访问 host 的 base 成员即可访问 I.MX6ULL 的整个 eLCDIF 寄存器(base地址+偏移值的方法) */
 host->base = devm_ioremap_resource(&pdev->dev, res);
 if (IS_ERR(host->base)) {
     dev_err(&pdev->dev, "ioremap failed\n");
     ret = PTR_ERR(host->base);
     goto fb_release;
 }
 ......
 fb_info->pseudo_palette = devm_kzalloc(&pdev->dev, sizeof(u32) *16, GFP_KERNEL);
 if (!fb_info->pseudo_palette) {
     ret = -ENOMEM;
     goto fb_release;
 }

 INIT_LIST_HEAD(&fb_info->modelist);
 pm_runtime_enable(&host->pdev->dev);
 /* 调用 mxsfb_init_fbinfo 函数初始化 fb_info,重点是 fb_info 的 var、fix、fbops,screen_base 和
    screen_size。其中 fbops 是 Framebuffer 设备的操作集,NXP 提供的 fbops 为 mxsfb_ops */
 ret = mxsfb_init_fbinfo(host);
 if (ret != 0)
     goto fb_pm_runtime_disable;

 mxsfb_dispdrv_init(pdev, fb_info);    //NXP封装过的dispdrv_init
 if (!host->dispdrv) {
     pinctrl = devm_pinctrl_get_select_default(&pdev->dev);    //设备所用引脚设备为 default 功能
     if (IS_ERR(pinctrl)) {
         ret = PTR_ERR(pinctrl);
         goto fb_pm_runtime_disable;
     }
 }
 if (!host->enabled) {
     writel(0, host->base + LCDC_CTRL);
     mxsfb_set_par(fb_info);
     mxsfb_enable_controller(fb_info);
     pm_runtime_get_sync(&host->pdev->dev);
 }

 ret = register_framebuffer(fb_info);    // probe 最终的目的,注册 framebuffer !!!
 if (ret != 0) {
     dev_err(&pdev->dev, "Failed to register framebuffer\n");
     goto fb_destroy;
 }
 ......
 return ret;
}

NXP 提供的 fbops

static struct fb_ops mxsfb_ops = {
 .owner = THIS_MODULE,
 .fb_check_var = mxsfb_check_var,
 .fb_set_par = mxsfb_set_par,
 .fb_setcolreg = mxsfb_setcolreg,
 .fb_ioctl = mxsfb_ioctl,
 .fb_blank = mxsfb_blank,
 .fb_pan_display = mxsfb_pan_display,
 .fb_mmap = mxsfb_mmap,
 .fb_fillrect = cfb_fillrect,
 .fb_copyarea = cfb_copyarea,
 .fb_imageblit = cfb_imageblit,
};

如何驱动一块 LCD?

对于Linux系统来说,厂商已经写好了 BSP 和驱动了,我们需要按照驱动的要求去修改设备树,这部分可以直接参考厂商的设备树。只有在需要额外加业务功能的时候,才需要去驱动代码,例如:睡眠、亮度。不过这些一般都有接口给我们写参数。
修改设备树需要注意三个地方:
①、LCD 所使用的 IO 配置。
②、LCD 屏幕节点修改,修改相应的属性值,换成我们所使用的 LCD 屏幕参数。
③、LCD 背光节点信息修改,要根据实际所使用的背光 IO 来修改相应的设备节点信息。

LCD 屏幕 IO 配置

这部分是固定的,所以厂商设备中已经写好了。

/* 24 根数据线的引脚复用 */
pinctrl_lcdif_dat: lcdifdatgrp {
 fsl,pins = <
     MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x79
     MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x79
     MX6UL_PAD_LCD_DATA02__LCDIF_DATA02 0x79
     MX6UL_PAD_LCD_DATA03__LCDIF_DATA03 0x79
     MX6UL_PAD_LCD_DATA04__LCDIF_DATA04 0x79
     MX6UL_PAD_LCD_DATA05__LCDIF_DATA05 0x79
     MX6UL_PAD_LCD_DATA06__LCDIF_DATA06 0x79
     MX6UL_PAD_LCD_DATA07__LCDIF_DATA07 0x79
     MX6UL_PAD_LCD_DATA08__LCDIF_DATA08 0x79
     MX6UL_PAD_LCD_DATA09__LCDIF_DATA09 0x79
     MX6UL_PAD_LCD_DATA10__LCDIF_DATA10 0x79
     MX6UL_PAD_LCD_DATA11__LCDIF_DATA11 0x79
     MX6UL_PAD_LCD_DATA12__LCDIF_DATA12 0x79
     MX6UL_PAD_LCD_DATA13__LCDIF_DATA13 0x79
     MX6UL_PAD_LCD_DATA14__LCDIF_DATA14 0x79
     MX6UL_PAD_LCD_DATA15__LCDIF_DATA15 0x79
     MX6UL_PAD_LCD_DATA16__LCDIF_DATA16 0x79
     MX6UL_PAD_LCD_DATA17__LCDIF_DATA17 0x79
     MX6UL_PAD_LCD_DATA18__LCDIF_DATA18 0x79
     MX6UL_PAD_LCD_DATA19__LCDIF_DATA19 0x79
     MX6UL_PAD_LCD_DATA20__LCDIF_DATA20 0x79
     MX6UL_PAD_LCD_DATA21__LCDIF_DATA21 0x79
     MX6UL_PAD_LCD_DATA22__LCDIF_DATA22 0x79
     MX6UL_PAD_LCD_DATA23__LCDIF_DATA23 0x79
 >;
};
/* 4 根控制线的引脚复用 */
pinctrl_lcdif_ctrl: lcdifctrlgrp {
 fsl,pins = <
     MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x79          //时钟线
     MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x79    //LCD使能信号线
     MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x79      //行同步信号线
     MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x79      //帧同步信号线
 >;
 /* LCD 背光灯 PWM 引脚配置项 */
 pinctrl_pwm1: pwm1grp {
 fsl,pins = <
     MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
 >;
};

LCD 屏幕参数节点信息修改

厂商设备树 lcdif 节点:

&lcdif {
 pinctrl-names = "default";
 pinctrl-0 = <&pinctrl_lcdif_dat    /* 使用到的 IO */
              &pinctrl_lcdif_ctrl
              &pinctrl_lcdif_reset>;
 display = <&display0>;
 status = "okay";
 
 display0: display {             /* display 属性,指定 LCD 属性信息所在的子节点 */
     bits-per-pixel = <16>;      /* 一个像素占用几个 bit */
     bus-width = <24>;           /* 总线宽度 */

     display-timings {
         native-mode = <&timing0>; /* 时序信息 */
             timing0: timing0 { 
             clock-frequency = <9200000>;     /* LCD 像素时钟,单位 Hz */
             hactive = <480>;                 /* LCD X 轴像素个数 */
             vactive = <272>;                 /* LCD Y 轴像素个数 */
             hfront-porch = <8>;              /* LCD hfp 参数 */
             hback-porch = <4>;               /* LCD hbp 参数 */
             hsync-len = <41>;                /* LCD hspw 参数 */
             vback-porch = <2>;               /* LCD vbp 参数 */
             vfront-porch = <4>;              /* LCD vfp 参数 */
             vsync-len = <10>;                /* LCD vspw 参数 */

             hsync-active = <0>;     /* hsync 数据线极性 */
             vsync-active = <0>;     /* vsync 数据线极性 */
             de-active = <1>;        /* de 数据线极性 */
             pixelclk-active = <0>;  /* clk 数据线先极性 */
         };
     };
 };
};

修改点:
① pinctrl_lcdif_reset 引脚用于复位,不接也可以,没有用到复位引脚的可以直接删除。
② bits-per-pixel 需要把 16 改成 24,因为这里用的是 RGB888 格式。
③ bus-width 是总线宽度,也要设置成 24。
④ 时序是重点!!!LCD 商家没有直接给出的需要自己去数据手册中找,根据之前的裸机开发中的时序截图:
在这里插入图片描述
timings 节点我们应该这么改:

timing0: timing0 { 
 clock-frequency = <51200000>;     /* LCD 像素时钟,单位 Hz */
 hactive = <1024>;                 /* LCD X 轴像素个数 */
 vactive = <600>;                  /* LCD Y 轴像素个数 */
 hfront-porch = <160>;             /* LCD hfp 参数 */
 hback-porch = <140>;              /* LCD hbp 参数 */
 hsync-len = <20>;                 /* LCD hspw 参数 */
 vback-porch = <20>;               /* LCD vbp 参数 */
 vfront-porch = <12>;              /* LCD vfp 参数 */
 vsync-len = <3>;                  /* LCD vspw 参数 */

 hsync-active = <0>;               /* hsync 数据线极性 */
 vsync-active = <0>;               /* vsync 数据线极性 */
 de-active = <1>;                  /* de 数据线极性 */
 pixelclk-active = <0>;            /* clk 数据线先极性 */
};

LCD 背光节点信息

LCD 接口背光控制 IO 连接到了 I.MX6U 的 GPIO1_IO08 引脚上,GPIO1_IO08 复用为 PWM1_OUT,通过 PWM 信号来控制 LCD 屏幕背光的亮度。

pinctrl_pwm1: pwm1grp {
 fsl,pins = <MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0>; //IO 复用为 PWM1_OUT,并设置电气属性为 0x110b0
};

pinctrl_pwm1 节点就已经指定了 GPIO1_IO08 引脚。
LCD 背光要用到 PWM1,因此也要设置 PWM1 节点,在厂商设备树中可以看到:

pwm1: pwm@02080000 {
 compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
 reg = <0x02080000 0x4000>;
 interrupts = <GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>;
 clocks = <&clks IMX6UL_CLK_PWM1>,<&clks IMX6UL_CLK_PWM1>;
 clock-names = "ipg", "per";
 #pwm-cells = <2>;
};

我们要使用这个设备,所以需要对其引用:

&pwm1 {
 pinctrl-names = "default";
 pinctrl-0 = <&pinctrl_pwm1>;
 status = "okay";
};

对于 pwm1 是有专门的设备树节点和驱动的,这里属于是直接拿来用了。
然后对于背光控制又是一个驱动,只不过这个驱动针对的硬件就是上面的 pwm1,我们直接使用即可。
我们还需要一个节点来将 LCD 背光和 PWM1_OUT 连接起来。这个节点就是 backlight , backlight 节点描述可以参考
Documentation/devicetree/indings/video/backlight/pwm-backlight.txt 这个文档,此文档详细讲解了 backlight 节点该如何去创建,这里大概总结一下:
①、节点名称要为“backlight”。
②、节点的 compatible 属性值要为“pwm-backlight”,因此可以通过在 Linux 内核中搜索 pwm-backlight 来查找 PWM 背 光 控 制 驱 动 程 序 , 这个驱动程序文件为 drivers/video/backlight/pwm_bl.c,感兴趣的可以去看一下这个驱动程序。
③、pwms属性用于描述背光所使用的PWM以及PWM频率,比如本章我们要使用的pwm1,pwm 频率设置为 5KHz(NXP 官方推荐设置)。
④、brightness-levels 属性描述亮度级别,范围为 0~255,0 表示 PWM 占空比为 0%,也就是亮度最低,255 表示 100%占空比,也就是亮度最高。至于设置几级亮度,大家可以自行填写此属性。一般需要驱动工程师编写接口给上层应用控制。
⑤、default-brightness-level 属性为默认亮度级别。

backlight {
 compatible = "pwm-backlight";
 pwms = <&pwm1 0 5000000>;
 brightness-levels = <0 4 8 16 32 64 128 255>;    //8级亮度分布
 default-brightness-level = <6>;    //默认使用第 6 个亮度,也就是 128
 status = "okay";
};

brightness-levels = <0 4 8 16 32 64 128 255> 表示使用 8 级亮度分布,对应占空比为 0%、1.57%、3.13%、6.27%、12.55%、25.1%、50.19%、100%,如果嫌少可以自行添加。

至此设备树就修改完成了。
设备配置这部分参考一下就行了,不同的处理器设备树不一样,主要是要根据厂商提供的设备树原型去改动。

LCD 屏幕基本测试

  1. 编译新的设备树
make dtbs
  1. 使能 Linux logo 显示
    Linux 内核启动时可以选择显示小企鹅 logo,只要 logo 显示没问题就基本正常了。
-> Device Drivers
    -> Graphics support 
         -> Bootup logo (LOGO [=y]) 
            -> Standard black and white Linux logo
            -> Standard 16-color Linux logo 
            -> Standard 224-color Linux logo

在这里插入图片描述
三个选项分别对应黑白、16 位、24 位色彩格式的 logo,把这三个都选中编译进内核。重新编译内核,编译完成以后使用新的设备树文件和镜像启动系统,如果 LCD 驱动工作正常的
话就会在 LCD 屏幕左上角出现一个彩色的小企鹅 logo,屏幕背景色为黑色。
在这里插入图片描述
3. 设置 LCD 作为终端控制台
a. 设置 uboot 中的 bootargs
在设备树里面的chosen 节点添加,或者重启开发板,进入 Linux 命令行,重新设置 bootargs 参数的 console 内容,命令如下所示:

setenv bootargs 'console=tty1 console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.250:/home/user/linux/nfs/rootfs ip=192.168.1.251:192.168.1.250:192.168.1.1:255.255.255.0::eth0:off'

第一次设置 console=tty1,也就是设置 LCD 屏幕为控制台;
第二次又设置 console=ttymxc0,115200,也就是设置串口也作为控制台。
相当于我们打开了两个 console,一个是 LCD,一个是串口。
但是目前,LCD 也只是输出终端信息而已,并没有交互能力。
b. 修改/etc/inittab 文件
打开开发板根文件系统中的/etc/inittab 文件,在里面加入下面这一行:

tty1::askfirst:-/bin/sh

在这里插入图片描述
重启开发板,重启以后开发板 LCD 屏幕最后一
行会显示下面一行语句:
Please press Enter to activate this console.
上述提示语句说的是:按下回车键使能当前终端。
可以接上一个 USB 键盘,Linux 内核默认已经使能了 USB 键盘驱动了,因此可以直接使用 USB 键盘。
或者注册一个 KEY 按键为回车键。
向 LCD 终端打印信息可以这么做:

echo 语句 > /dev/tty1
c. LCD 背光调节

背光设备树节点设置了 8 个等级的背光调节,可以设置为 0~7。
可以通过设置背光等级来实现 LCD 背光亮度的调节,进入如下目录:

/sys/devices/platform/backlight/backlight/backlight

在这里插入图片描述
brightness 表示当前亮度等级,max_bgigntness 表示最大亮度等级。
当前这两个文件内容如图所示:
在这里插入图片描述
从图可以看出,当前屏幕亮度等级为 6,根据前面的分析可以,这个是 50%亮度。
屏幕最大亮度等级为 7。如果要修改屏幕亮度,只需要向 brightness 写入屏幕亮度等级。
例如设置屏幕亮度等级为 7:

echo 7 > brightness

如果设置 brightness 为 0 的话就会关闭 LCD背光,屏幕就会熄灭。
d. LCD 自动关闭解决方法
默认情况下 10 分钟以后 LCD 就会熄屏,需要再使用回车唤醒,这是 Linux 内核设置的。
如果要关闭 10 分钟熄屏功能,可以将 \033[9;0] 写进 blankinterval 属性。
blankinterval 属性可以在 Linux 源码中找到 drivers/tty/vt/vt.c,在此文件中找到 blankinterval 变量,如下所示:

static int vesa_blank_mode;
static int vesa_off_interval;
static int blankinterval = 10*60;    //默认是 10*60秒,也就是 10 分钟

三、实例篇:驱动 RK3568 mipi 接口LCD

先上原理图
在这里插入图片描述
这里使用的是 RK3568 处理原理图上的单路 DSI,在30个引脚的 FPC 插座上还集成 PWM、触摸屏、电源等管脚,但这些通通都不讲,只是驱动 LCD 的时候需要配置这些引脚。
相对于前面的 LCDIF 接口,mipi 算是节约了 RGB 三原色的大量引脚了。

CSI 和 DSI 的区别

CSI:串行摄像接口(用于摄像头)
DSI:串行显示接口(用于显示屏)
MIPI-DSI是一种应用于显示技术的串行接口,兼容DPI(显示像素接口,Display Pixel Interface)、DBI(显示总线接口,Display Bus Interface)和DCS(显示命令集,Display Command Set),以串行的方式发送像素信息或指令给外围,而且从外围中读取状态信息或像素信息,而且在传输的过程中享有自己独立的通信协议,包括数据包格式和纠错检错机制。

MIPI-DSI具备高速模式和低速模式两种工作模式,全部数据通道都可以用于单向的高速传输,但只有第一个数据通道才可用于低速双向传输,从属端的状态信息、像素等式通过该数据通道返回。时钟通道专用于在高速传输数据的过程中传输同步时钟信号。此外,一个主机端可允许同时与多个从属端进行通信。

MIPI DSI 接口分为 HOST 和 PHY,HOST 是 DSI 的主控制器,这部分明显是原厂 BSP 工程师配置的。我们配置的部分就是 PHY 部分。
在数据手册中就要查阅 PHY 部分,更重要的是阅读内核设备树资料中的 DSI 配置部分,这个非常重要,每一个配置选项都有详细的说明。
可以参考下面这份 firefly 的文档,基于瑞芯微原厂指导书修改的。
https://www.t-firefly.com/ueditor/php/upload/file/20171213/1513128959299913.pdf
文档截图:
在这里插入图片描述

* 设备树配置

再来看这部分的引脚的设备配置:

/* MIPI 7 */
&dsi1 {
        status = "disabled";
        //rockchip,lane-rate = <1000>;
        dsi1_panel: panel@0 {
                status = "disabled";
                compatible = "simple-panel-dsi";    //匹配驱动名为 simple-panel-dsi
                reg = <0>;
                backlight = <&backlight>;
                reset-delay-ms = <60>;
                enable-delay-ms = <60>;
                prepare-delay-ms = <60>;
                unprepare-delay-ms = <60>;
                disable-delay-ms = <60>;
                dsi,flags = <(MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_BURST |
                              MIPI_DSI_MODE_LPM | MIPI_DSI_MODE_EOT_PACKET)>;
                dsi,format = <MIPI_DSI_FMT_RGB888>;
                dsi,lanes  = <4>;
                panel-init-sequence = [
                        29 00 03 E0 AB BA
                        29 00 03 E1 BA AB
                        15 00 02 B0 00
                        29 00 05 B1 10 01 47 FF
                        29 00 07 B2 0C 0E 04 14 14 14
                        29 00 04 B3 56 D3 00
                        29 00 04 B4 22 30 04
                        15 00 02 B5 00
                        29 00 08 B6 B0 00 00 10 00 10 00
                        29 00 09 B7 0E 00 FF 08 08 FF FF 00
                        29 00 08 B8 05 12 29 49 48 00 00
                        29 00 27 B9 4D 42 38 31 33 27 2F 1B 36 35 35 53 41 49 3D 3D 33 29 26 4C 42 39 31 33 27 2F 1B 36 35 35 53 41 49 3D 3D 33 29 26
                        29 00 09 BA 00 00 00 44 24 00 00 00
                        29 00 04 BB 76 00 00
                        29 00 03 BC 00 00
                        29 00 06 BD FF 00 00 00 00
                        15 00 02 BE 00
                        29 00 11 C0 98 76 12 34 33 33 44 44 06 04 8A 04 0F 00 00 00
                        29 00 0B C1 53 94 02 85 06 04 8A 04 54 00
                        29 00 0D C2 37 09 08 89 08 10 22 21 44 BB 18 00
                        29 00 17 C3 9C 1D 1E 1F 10 12 0C 0E 05 24 24 24 24 24 24 07 24 24 24 24 24 24
                        29 00 17 C4 1C 1D 1E 1F 11 13 0D 0F 04 24 24 24 24 24 24 06 24 24 24 24 24 24
                        29 00 04 C5 E8 85 76
                        29 00 03 C6 20 20
                        29 00 17 C7 41 01 0D 11 09 15 19 4F 10 D7 CF 19 1B 1D 03 02 25 30 00 03 FF 00
                        29 00 07 C8 61 00 31 42 54 16
                        29 00 06 C9 A1 22 FF Cd 23
                        29 00 03 CA 4B 43
                        29 00 05 CC 2E 02 04 08
                        29 00 09 CD 0E 64 64 20 1E 6B 06 83
                        29 00 04 D0 27 10 80
                        29 00 05 D1 00 0D FF 0F
                        29 00 05 D2 E3 2B 38 00
                        29 00 0C D4 00 01 00 0E 04 44 08 10 00 07 00
                        15 00 02 D5 00
                        29 00 03 D6 00 00
                        29 00 05 D7 00 00 00 00
                        29 00 04 E4 08 55 03
                        29 00 09 E6 00 01 FF FF FF FF FF FF
                        29 00 04 E7 00 00 00
                        29 00 08 E8 D5 FF FF FF 00 00 00
                        15 00 02 E9 FF
                        29 00 06 F0 12 03 20 00 FF
                        29 00 1B F1 A6 C8 EA E6 E4 CC E4 BE F0 B2 AA C7 FF 66 98 E3 87 C8 99 C8 8C BE 96 91 8F FF
                        15 00 02 F3 03
                        29 00 1B F4 FF FE FC FA F8 F4 F0 E8 E0 D0 C0 A0 80 7F 5F 3F 2F 1F 17 0F 0B 07 05 03 01 00
                        29 00 1B F5 FF FE FC FA F8 F4 F0 E8 E0 D0 C0 A0 80 7F 5F 3F 2F 1F 17 0F 0B 07 05 03 01 00
                        29 00 1B F6 FF FE FC FA F8 F4 F0 E8 E0 D0 C0 A0 80 7F 5F 3F 2F 1F 17 0F 0B 07 05 03 01 00
                        29 00 08 F7 00 00 00 00 00 00 00
                        29 00 08 F8 00 00 00 00 00 00 00
                        29 00 08 F9 00 00 00 00 00 00 00
                        29 00 1A FA 00 84 12 21 48 48 21 12 84 69 69 5A A5 96 96 A5 5A B7 DE ED 7B 7B ED DE B7
                        29 00 18 FB 00 12 0F FF FF FF 00 38 40 08 70 0B 40 19 50 21 C0 27 60 2D 00 00 0F
                        29 00 03 E3 20 21
                        05 C8 01 11
                        05 14 01 29
                ];

                panel-exit-sequence = [
                        05 05 01 28
                        05 78 01 10
                ];
                disp_timings1: display-timings {
                        native-mode = <&dsi1_timing0>;
                        dsi1_timing0: timing0 {
                                screen-type = <SCREEN_MIPI>;     //单mipi SCREEN_MIPI 双mipi SCREEN_DUAL_MIPI
                                clock-frequency = <60000000>;    //像素时钟大小
                                hactive = <800>;                 // Y 轴像素个数
                                vactive = <1280>;                // X 轴像素个数
                                hfront-porch = <80>;             // hfp 参数
                                hsync-len = <20>;                // hspw 参数
                                hback-porch = <80>;              // hbp 参数
                                vfront-porch = <20>;             // vfp 参数
                                vsync-len = <4>;                 // vspw 参数
                                vback-porch = <12>;              // vbp 参数
                                hsync-active = <0>;              // hsync 数据线极性
                                vsync-active = <0>;              // vsync 数据线极性
                                de-active = <0>;                 // de 数据线极性
                                pixelclk-active = <0>;           // clk 数据线先极性
                        };
                };
                ports {
                        #address-cells = <1>;
                        #size-cells = <0>;
                        port@0 {
                                reg = <0>;
                                panel_in_dsi1: endpoint {
                                        remote-endpoint = <&dsi1_out_panel>;
                                };
                        };
                };
        };
        ports {
                #address-cells = <1>;
                #size-cells = <0>;
                port@1 {
                        reg = <1>;
                        dsi1_out_panel: endpoint {
                                remote-endpoint = <&panel_in_dsi1>;
                        };
                };
        };
};

/* 触摸屏 */
&i2c1 {
    status = "okay";
    ft5x061:ft5x06@38 {
        status = "disabled";
        compatible = "edt,edt-ft5306";
        reg = <0x38>;
        touch-gpio = <&gpio3 RK_PA5 IRQ_TYPE_EDGE_RISING>;
        interrupt-parent = <&gpio3>;
        interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
        reset-gpio = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
        touchscreen-size-x = <800>;
        touchscreen-size-y = <1280>;
        touch_type = <1>;
    };
};

#if defined(LCD_TYPE_MIPI)    //如果使用的是屏幕是 mipi 屏幕
&backlight{
    pwms = <&pwm5 0 25000 0>;
    status = "okay";
};
&dsi1{
    status = "okay";
};
&dsi1_panel
{
    status = "okay";
};
&dsi1_in_vp0 {
    status = "disabled";
};
&dsi1_in_vp1 {
    status = "okay";
};
&ft5x061                //使能触摸屏芯片
{
    status = "okay";
};
&route_dsi1{            //指定uboot 阶段的logo配置,connect属性指定的vop需与ports的一致
    status = "okay";
    connect = <&vp1_out_dsi1>;
};
&video_phy1{            //使能控制LCD的视频芯片
    status = "okay";
};
#endif

/* 下面都是移植无关的,但是需要了解 */

/* 背光 */
backlight: backlight {
        compatible = "pwm-backlight";        //匹配驱动名为 pwm-backlight
        pwms = <&pwm4 0 25000 0>;            //使用 pwm4
        brightness-levels = <                //它这里是默认支持所有亮度范围的
              0  20  20  21  21  22  22  23
             23  24  24  25  25  26  26  27
             27  28  28  29  29  30  30  31
             31  32  32  33  33  34  34  35
             35  36  36  37  37  38  38  39
             40  41  42  43  44  45  46  47
             48  49  50  51  52  53  54  55
             56  57  58  59  60  61  62  63
             64  65  66  67  68  69  70  71
             72  73  74  75  76  77  78  79
             80  81  82  83  84  85  86  87
             88  89  90  91  92  93  94  95
             96  97  98  99 100 101 102 103
            104 105 106 107 108 109 110 111
            112 113 114 115 116 117 118 119
            120 121 122 123 124 125 126 127
            128 129 130 131 132 133 134 135
            136 137 138 139 140 141 142 143
            144 145 146 147 148 149 150 151
            152 153 154 155 156 157 158 159
            160 161 162 163 164 165 166 167
            168 169 170 171 172 173 174 175
            176 177 178 179 180 181 182 183
            184 185 186 187 188 189 190 191
            192 193 194 195 196 197 198 199
            200 201 202 203 204 205 206 207
            208 209 210 211 212 213 214 215
            216 217 218 219 220 221 222 223
            224 225 226 227 228 229 230 231
            232 233 234 235 236 237 238 239
            240 241 242 243 244 245 246 247
            248 249 250 251 252 253 254 255
        >;
        default-brightness-level = <220>;    //默认使用第 220 个亮度
};

pwm4 {
    /omit-if-no-ref/
    pwm4_pins: pwm4-pins {
        rockchip,pins =
            /* pwm4 */
            <0 RK_PC3 1 &pcfg_pull_none>;    //引脚、复用、电器属性
    };
};

panel-init-sequence

panel-init-sequence = [
    ......
];

这部分用于初始化 LCD,可以在 kernel/Documentation/devicetree/bindings/display/panel/simple-panel.txt 文档中查看详细解释。初始化的部分数据是要找LCD厂商要的,然后我们再转化成处理器厂商的命令。
这部分 LCD 商家直接给了我现成的数据,但是我们可以来看怎么转化这部分数据。
参考瑞芯微官方指导文档《RockChip_LCD开发文档v1.6》
例:LCD 厂商给的数据:

U16 B0[17]={0xB0,0x00,0x0B,0x10,0x0D,0x11,0x06,0x01,0x08,0x08,0x1D,0x04,0x10,0x10,0x27,0x30,0x19};
U16 B1[17]={0xB1,0x00,0x0B,0x14,0x0C,0x11,0x05,0x03,0x08,0x08,0x20,0x04,0x13,0x10,0x28,0x30,0x19};
U16 E1[12]={0xE1,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x20,0x20};
U16 E2[14]={0xE2,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
U16 E5[17]={0xE5,0x07,0x34,0xA0,0xA0,0x05,0x34,0xA0,0xA0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
U16 E8[17]={0xE8,0x06,0x34,0xA0,0xA0,0x04,0x34,0xA0,0xA0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
U16 ED[17]={0xED,0xAA,0x54,0x0B,0xBF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFB,0xB0,0x45,0xAA};
 
Generic_Long_Write_5P(0xFF,0x77,0x01,0x00,0x00,0x10);
Generic_Long_Write_2P(0xC0,0xE9,0x03);    //display line setting //800
Generic_Long_Write_2P(0xC1,0x08,0x02);    //Porch control 
Generic_Long_Write_2P(0xC2,0x31,0x08);
Generic_Short_Write_1P(0xCC,0x10);
Generic_Long_Write_FIFO(17,B0);
Generic_Long_Write_FIFO(17,B1);
 
Generic_Long_Write_5P(0xFF,0x77,0x01,0x00,0x00,0x11);
Generic_Short_Write_1P(0xB0,0x35);    //4D
Generic_Short_Write_1P(0xB1,0x38);    //21 71 VCOM 
Generic_Short_Write_1P(0xB2,0x02);    //VGH VOLTAGE SETTING
Generic_Short_Write_1P(0xB3,0x80);
Generic_Short_Write_1P(0xB5,0x4E);    //VGL VOLTAGE SETTING
Generic_Short_Write_1P(0xB7,0x85);
Generic_Short_Write_1P(0xB8,0x20);
Generic_Short_Write_1P(0xB9,0x10);    
//Generic_Short_Write_1P(0xC0,0x90);
Generic_Short_Write_1P(0xC1,0x78);
Generic_Short_Write_1P(0xC2,0x78);
Generic_Short_Write_1P(0xD0,0x88);

Delay (100);
 

Generic_Long_Write_3P(0xE0,0x00,0x00,0x02);
Generic_Long_Write_FIFO(12,E1);
Generic_Long_Write_FIFO(14,E2);
 
Generic_Long_Write_4P(0xE3,0x00,0x00,0x33,0x00);
Generic_Long_Write_2P(0xE4,0x22,0x00);
Generic_Long_Write_FIFO(17,E5);
 
Generic_Long_Write_4P(0xE6,0x00,0x00,0x33,0x00);
Generic_Long_Write_2P(0xE7,0x22,0x00);
Generic_Long_Write_FIFO(17,E8);
 
Generic_Long_Write_7P(0xEB,0x02,0x00,0x10,0x10,0x00,0x00,0x00);
Generic_Long_Write_2P(0xEC,0x02,0x00);
Generic_Long_Write_FIFO(17,ED);

 

Generic_Long_Write_5P(0xFF,0x77,0x01,0x00,0x00,0x00);
 
Generic_Short_Write_1P(0x36,0x00);
 
DCS_Short_Write_NP(0x11);
Delay(120);

Delay(10);

 
DCS_Short_Write_NP(0x29);
Delay(20);

/* 命令类型1: Generic_xxx_Write_xP
   xxx 是 Long 或者 Short,Long ; Long 是 39,short 是 15
   x 是 一个数字,代表写入 x+1 个字节
   命令类型2: Generic_Long_Write_FIFO(n,n[]);
   n 是一个数据
   n[] 是一个数组
   命令类型3: DCS_Short_Write_NP(n);
   n 是数据
   转化格式在下面
*/

例:瑞芯微 dts 里面 panel-init-sequence 命令格式介绍:
在这里插入图片描述
格式说明:头部3个字节(16进制),分别代表 Data Type,Delay,Payload Length。
从第四个字节开始的数据代表长度为 Length 的 Payload。
第一条命令的解释如下:
39 00 04 b9 ff 83 94
data type : 0x39 (DCS Long Write)
Delay : 0x00 (0ns)
Payload Length : 0x04(4Bytes)
payload : 0xb9 0xff 0x83 0x94
最后一条命令的注释:
05 14 01 29
data type : 0x05 (DCS Short Write : no parameters)
Delay : 0x14 (20ns)
Payload Length : 0x01(4Bytes)
payload : 0x29

display-timings

时序参数设置是最重要的一部分!!!
LCD 厂商直接给了这部分数据,直接搬进设备树即可。

ports

以 ports -> port -> endpoint -> remote-endpoint 的顺序,通过remote-endpoint 属性来指定远端的设备端点。
vop 是视频对象平面,同样也有它自己的设备树。
① dsi -> ports -> port dsi0_in_vp0(endpoint) 下的remote-endpoint 为dsi与vop间建立关系的属性配置,其中dsi作为输入端,另一端接的是vop out端。
② dsi -> ports -> port -> dsi_out_panel1/dsi_out_panel2/dsi_out_panel3(endpoint)下的remote-endpoint 为dsi 与 panel 间建立关系的属性配置。此处代表有三个panel。因此 若想多屏可以在此处增加节点达到和新panel建立联系。
③ dsi->panel 为panel的配置。正常是单独创建个屏dtsi文件后,在文件中引用(&dsi0) 来进行新增屏。此处只是为了更好的分析而集成在一起(本身设备树在编译的时候也是覆盖的操作)。
④ dsi -> panel -> ports -> port -> panel1_in_dsi(endpoint) 下的remote-endpoint 为panel与dsi间的关系,即此处是连到dsi_out_panel1上。

pwm

1 秒=1000000000 纳秒

pwms = <&pwm4 0 25000>    //&pwm4 0两项代表pwmOUT0同道,25000代表初始化频率,0代表正常极性

#25000 是指的25000ns 1000000000/25000=40Khz
50000 20khz
100000 10Khz
1000000 1Khz
10000000 100Hz
50000000 200Hz

ft5x06

触摸屏节点在触摸屏驱动篇章已经讲过了,需要了解的同学可以回去看《TSP驱动理论与实例分析》。

配置 CONFIG

按照 RK 的手册,打开三个宏:

CONFIG_LCD_MIPI=y
CONFIG_MIPI_DSI=y
CONFIG_RK35_MIPI_DSI=y

检查电压

先不要接上屏,编译完代码烧录后开机。
检查原理图上各个供电管脚的电压(DVDD、IOVDD 是否为 3.3V,VDD_LCDA 是否为 5-10V,VDD_LCDK 是否为 0V),确认电压正常后,关机,上屏,结合 开机log 看能否正常开机。

参考资料
LINUX驱动-LCD驱动:https://www.freesion.com/article/1180923012/
LCD之MIPI DSI接口驱动调试流程:https://www.freesion.com/article/540743428/
LCD MIPI 调试方法及问题汇总:https://www.freesion.com/article/2421433882/
MIPI LCD 通用移植调试流程:https://www.freesion.com/article/1080767762/
RK3399 LINUX-SDK MIPI屏幕驱动及调试:https://www.freesion.com/article/3102348159/
RK3399 MIPI LCD 通用移植调试流程:https://www.freesion.com/article/9731532001/
MIPI屏 调试记录,panel-init-sequence 命令格式介绍:https://blog.csdn.net/qq_37858386/article/details/123705548
Linux MIPI DSI LCD设备驱动开发调试细节学习笔记(一):https://mp.weixin.qq.com/s?__biz=MzA3MjMzNTc4NA%3D%3D&chksm=870fe8c6b07861d0cae1f9ec03df43e9d4872f56034770592720914ef399aab7edafe6e71a1c&idx=1&mid=2649006081&scene=21&sn=584c1b8c2d93e457936685c23c415613#wechat_redirect
mipi LCD 设备树中的初始化:https://blog.csdn.net/qq_38312843/article/details/107534743
RK3568 LCD 设备树分析:https://blog.csdn.net/qq_33782617/article/details/126203159

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值