《ESP32-P4开发指南—V1.0》第二十二章 MIPILCD实验

第二十二章 MIPILCD实验


       本章将继续学习ESP_IDF的LCD外设驱动,主要学习MIPI接口屏幕的驱动方法。本章用到的是ESP32-P4的MIPI_DSI接口驱动MIPILCD屏幕,实现和MIPILCD屏之间的通信,实现ASCII字符、图形和彩色的显示。

       本章分为如下几个小节:

       22.1 MIPI介绍和ESP32-P4的MIPI_DSI介绍

       22.2 硬件设计

       22.3 程序设计

       22.4 下载验证


        22.1 MIPI介绍和ESP32-P4的MIPI_DSI介绍


       22.1.1 MIPI介绍

       MIPI即移动产业处理器接口,是Mobile Industry Processor Interface缩写,2003年由ARM、Nokia、ST和TI等公司成立的一个联盟,所以大家在看MIPI相关文档的时候常看到MIPI Aliance。MIPI官方网址为https://www.mipi.org/。当前MIPI联盟已经有很多成员, 比如大家熟知的ARM、ST、高通、TI、苹果、海思等,基本上囊括了主流的移动处理器生产厂家,如图22.1.1.1所示:


图22.1.1.1 MIPI联盟成员(部分)


       MIPI联盟主要是为移动处理器定制标准接口和规范,开发的接口广泛应用于处理器、相机、显示屏、基带调制解调器等设备。比如:

       ·MIPI DSI(显示屏接口)

       ·MIPI CSI(摄像头接口)

       ·MIPI I3C

       ·MIPI RFFE(射频前端控制接口)

       ·MIPI SPMI(系统电源管理接口)

       ·等等。

       MIPI通过定义一套协议和标准,满足各个子系统之间互联,确保不同公司开发的产品可以兼容连接,减少协议标准。图22.1.1.2就是MIPI相关协议在移动处理器中的使用:


图22.1.1.2 移动处理器中MIPI的使用场合


       从图22.1.1.2可以看出,MIPI下的各种协议、接口在移动处理器中使用非常广泛,典型的就是MIPI DSI和MIPI CSI。

       MIPI主要包括四个方面,如图22.1.1.3所示:


图22.1.1.3 MIPI框架


       从图22.1.1.3可以看出,MIPI主要有四个方向的协议:

       ①、Multimedia,多媒体。

       ②、Control&Data,控制和数据。

       ③、Chip-to-Chip Inter Process Communications,

       ④、Debug&Trace,调试和追踪

       我们依次来简单看一下这四部分都有哪些内容。


       1、Multimedia

       Multimedia部分框图如图22.1.1.4所示:


图22.1.1.4 Multimedia部分框图


       Multimedia就是多媒体部分,分为如下几部分:

       ·摄像头,应用层有CCS,协议层主要有CSI-2、CSI-3,物理层有A-PHY、C-PHY、D-PHY和M-PHY。

       ·屏幕,应用层有DCS,协议层主要有DSI,物理层有A-PHY、C-PHY、D-PHY。

       ·触摸,应用层有TCS,协议层是I3C。

       ·存储,UFS协议,这个是目前手机以及平板上最常用的存储协议,物理层为M-PHY。

       ·音频,协议层有SLIMIbus和SoundWire。

       多媒体部分我们用的最多就是DSI和CSI,DSI应用于屏幕,CSI用于摄像头。对应的物理层协议有A-PHY、C-PHY、D-PHY和M-PHY。

       D-PHY:目前用的最多的接口,不管是摄像头还是屏幕,D-PHY接口为1/2/4lane(lane可以理解为通道,也就是1/2/3/4通道,每个通道2条差分线),外加一对时钟线,数据线和时钟线都是差分线,为电流驱动型,不同版本的D-PHY速度不同,比如ESP32-P4用的V1.1版本的D-PHY双lane最高可到3Gbps。D-PHY最多10根线,有专门的时钟线来进行同步。

       C-PHY:随着屏幕和摄像头的分辨率以及帧率越来越高,D-PHY的带宽越来越不够用。C-PHY应运而生,C-PHY接口是1/2/3 Trio,每个Trio有3根线,最高9根线,没有专用的时钟线了。C-PHY目前在高端旗舰手机芯片中可能会用到,本教程不讲解C-PHY。

       A-PHY:主要为汽车自动驾驶而生,目前汽车自动驾驶发展非常迅猛, ADAS(高级驾驶员辅助系统)摄像头于车载娱乐屏幕越来越多,分辨率也越来越高,而且车载摄像头和娱乐屏幕分布比较分散,到主控的距离一般比较长。但是C-PHY和D-PHY的距离太短,最多不超过15CM,显然不适合用在当今高度智能化的车载领域。A-PHY于2020年9月发布,用于长距离、超高速的汽车应用中,比如ADAS、自动驾驶系统 (ADS)、车载信息娱乐系统 (IVI) 和其他环绕传感器。

       M-PHY:目前主要用在USF存储中。


       2、Control&Data

       Control&Data部分框图如图22.1.1.5所示:


图22.1.1.5 Control&Data


       图22.1.1.5中主要是RF、电源管理以及I3C通信接口相关的协议。


       3、Chip-to-Chip Inter Process Communications

       Chip-to-Chip Inter Process Communications框图如图22.1.1.6所示:


图22.1.1.6 Chip-to-Chip Inter Process Communications框图


       4、Debug&Trace

       Debug&Trace框图如图22.1.1.7所示:


图22.1.1.7 Debug&Trace框图


       关于MIPI联盟就介绍到这里,感兴趣的可以去MIPI联盟官网上去看一下。


       22.1.2 MIPI DSI概述


       22.1.2.1 MIPI DSI协议综述

       本章我们是来学习如何驱动MIPI接口屏幕,所以需要学习的就是MIPI DSI。DSI全称是Display Serial Interface,是主控和显示模组之间的串行连接接口,图22.1.2.1.1展示了主控和屏幕之间的连接方式:


图22.1.2.1.1 DSI主控与屏幕之间接口示意图


       MIPI DSI接口分为数据线和时钟线,均为差分信号。数据线可选择1/2/3/4 lanes,时钟线有一对,最多10根线。MIPI DSI以串行的方式发送指令和数据给屏幕,也可以读取屏幕中的信息。如果屏幕的分辨率和帧率越高,需要的带宽就越大,就需要更多的数据线来传输图像数据,但是ESP32-P4只支持使用2lanes来驱动MIPI屏幕。对于MIPI DSI接口而言,最常用的就是2 lanes和4 lanes。


       22.1.2.2 MIPI DSI分层

       和网络协议栈一样,MIPI DSI也是分层的,如图22.1.2.2.1所示:


图22.1.2.2.1 MIPI DSI协议分层


       从图22.1.2.2.1可以看出,MIPI DSI一共有四层,从上往下依次为:

       ·应用层

       ·协议层

       ·通道管理层

       ·物理层

       1、应用层

       应用层处理更高层次的编码,将要显示的数据打包进数据流中,下层会处理并发送应用层的数据流。发送端将命令以及数据编码成MIPI DSI规格的格式,接收端则将接收到的数据还原为原始的数据。

       2、协议层

       协议层主要是打包数据,在原始的数据上添加ECC和校验和等东西。应用层传递下来的数据会打包成两种格式的数据:长数据包和短数据包,关于长短数据包后面会有详细讲解。发送端将原始数据打包好,添加包头和包尾,然后将打包好的数据发送给下层。接收端介绍到下层传来的数据包以后执行相反的操作,去除包头和包尾,然后使用ECC进行校验接收到的数据,如果没问题就将解包后的原始数据交给应用层。

       3、链路层

       链路层负责如何将数据分配到具体的通道上,若MIPI DSI可以支持1/2/3/4 Lane,采用几通道取决于你的实际应用,如果带宽需求低,那么2 Lane就够了,带宽高的话就要4 Lane。协议层下来的数据包都是串行的,如果只有1 Lane的话,那就直接使用这1 Lane将数据串行的发送出去,如果是2/4 Lane的话数据该如何发送呢?如图22.1.2.2.2所示:


图22.1.2.2.2 发送端通道管理层处理示意图


       图22.1.3.2左侧是1 Lane的时候数据如何在数据线上传输,由于只有1 Lane,所以上层传递下来的数据就只能按照Byte0~ByteN这样的顺序传输。

       图22.1.3.2右侧是4 lane的时候数据传输方式,由于采用了4通道,那么上层传递下来的数据就要平均分配给4个通道。分配 方法也很简单,每个通道一个字节,比如Byte0是Lane0,Byte1是Lane1,Byte2是Lane2,Byte3是Lane3,如此反复循环。

       如果要发送的数据和通道数不是整数倍数,那么先发送完的数据通道就进入EoT(End of Transmission)模式。我们来看一个2 Lane整数倍传输和非整数倍传输的图,如图22.1.2.2.3所示:


图22.1.2.2.3 整数倍和非整数倍数据传输方式


       图22.1.2.2.3中上部分就是整数倍传输,2条通道一起结束,进入EoT模式。下图是非整数倍传输,其中Lane 1先传输完,所以Lane 1先进入EoT模式。同理,3/4 Lane也一样。

       在接收端执行相反的操作,将Lane上的数据整理打包成串行数据上报给上层,如图22.1.2.2.4所示:


图22.1.2.2.4 接收端通道管理层处理示意图


       4、物理层

       物理层就是最 底层了,完成MIPI DSI数据在具体电路上的发送与接收,与物理层紧密相关的就是D-PHY。物理层规定了MIPI DSI的整个电气属性,信号传输的时候电压等,关于物理层后面会详细讲解。


       22.1.3 MIPI DSI物理层和D-PHY

       MIPI DSI的物理层也叫PHY层,前面说了MIPI有C-PHY、D-PHY等,只是在MIPI DSI领域D-PHY用的最多,所以这里可以简单的认为MIPI DSI物理层说的就是D-PHY,本文后面统一用D-PHY来表示MIPI DSI的物理层。

       D-PHY是一个源同步、高速、低功耗、低开销的PHY,特别适合移动领域。D-PHY主要用于主处理器的摄像头和显示器外设,比如MIPI摄像头和屏幕。D-PHY提供了一个同步通信接口来连接主机和外设,在实际使用中提供一对时钟线以及一对或多对信号线。时钟线是单向的,由主控产生,发送给设备。数据线根据实际配置,可以有1~4 Lane,只有Data0这一组数据线可以是单向也可以是双向的,其他组的数据线都是单向的。

       数据链路分为High-Speed模式和Low-Power模式,也就是图22.2.1.1中的HS和LP。HS模式用来传输高速数据,比如屏幕像素数据。LP模式用来传输低速的异步信号,一般是配置指令,屏幕的配置参数就是用LP模式传输的。HS模式下每个数据通道速率为80~1500Mbps, LP模式下最高10Mbps。


       22.1.3.1 什么是Lane

       我们前面已经提了很多次“Lane”这个词,英文直译过来就是:航道、跑道。在这里就是在主控与外设直接传输数据的通道,MIPI DSI包括一个时钟Lane和多个数据Lane,每条Lane使用2根差分线来连接主控和外设。收发端都有对应的Lane模块来处理相关的数据,一个完整的Lane模块如图22.1.3.1.1所示:


图22.1.3.1.1 通用的Lane模块


       从图22.1.3.1.1可以看出,一个通用的Lane模块,包括一个高速收发器和一个低速收发器,其中高速收发器有HS-TX、HS-RX,低速收发器有LP-RX和LP-TX,以及一个低速竞争检测器LP-CD。实际的Lane模块是在图22.1.3.1.1中简化而来的,比如对于高速单向数据通道,可能只有HS-TX或者HS-RX。


       22.1.3.2 D-PHY信号电平

       Lane分为HS和LP两种模式,其中HS采用低压差分信号,传输速度高,但是功耗大,信号电压幅度100mv~300mV,中心电平200mV。LP模式下采用单端驱动,功耗小,速率低(<10Mbps),信号电压幅度0~1.2V。在LP模式下只使用Lane0(也就是数据通道0),不需要时钟信号,通信过程的时钟信号通过Lane0两个差分线异或得到,而且是双向通信。

       HS和LP模式下的信号电平如图22.1.3.2.1所示:


图22.1.3.2.1 HS和LP模式信号电平


       图22.1.3.2.1中蓝色实线是LP模式下的信号波形示例,电压为0~1.2V。绿色虚线是LP模式下信号的高低电平门限。红色实线是HS模式下的信号波形示例,中心电平200mV。


       22.1.3.3 通道状态

       上面说了HS模式下是单向差分信号,主控发送(HS_TX),外设接收(HS_RX)。而LP是双向单端信号,接收和发送端都有LP_TX和LP_RX,注意只有Lane0能做LP。

       由于HS采用差分信号,所以只有两种状态:

       HS-0:高速模式下Dp信号低电平,Dn信号高电平的时候。

       HS-1:高速模式下Dp信号高电平,Dn信号低电平的时候。

       LP模式下有两根独立的信号线驱动,所以有4个状态:

       LP-00:后面的“00”对应两根信号线的电平状态,第1个0表示Dp为低电平,第2个0表示Dn为低电平。如果是高电平,那么就为1。因此LP模式剩下的三个状态就是LP-01、LP-10和LP-11。

       这6种状态对应的功能如图22.1.3.3.1所示:


图22.1.3.3.1 6种状态


       通过图22.1.3.3.1种这6个状态的转换,D-PHY就能工作在不同的工作模式。


       22.1.3.4 数据Lane三种工作模式

       D-PHY协议规定了,通过Lane的不同状态转换有三种工作模式:控制模式、高速模式(Burst Mode)和Escape模式。控制模式和Escape模式都属于LP,高速模式属于HS。正常情况下,数据Lane工作在控制模式或者高速模式下。


       1、高速模式(HS Burst Mode)

       高速模式用于传输实际的屏幕像素数据,采用突发(Burst)传输方式。为了帮助接收端同步,需要在数据头尾添加一些序列,接收端在接收到数据以后要把头尾去掉。高速数据传输起始于STOP状态(LP-11),并且也终止于STOP状态(LP-11)。在高速模式下传输数据的时候, Lane始终工作于HS模式,提供DDR时钟,也就是双边沿时钟,在时钟频率不变的情况下,传输速率提高一倍,这样可以有效利用带宽。

       当数据传输请求发出以后,数据Lane退出STOP模式进入到高速模式,顺序是:LP-11→LP-01→LP-00。然后发出一个SoT序列(Start-of-Transmission),SoT后面跟着的就是实际的负载数据。当负载数据传输结束以后会紧跟一个EoT序列(End-of-Transmission)序列,数据线直接进入到STOP模式。图22.1.3.4.1是一个基础的高速传输结构示意图:


图22.1.3.4.1 基础的高速数据传说结构体


       图22.1.3.4.1只是展示了一条Lane,负载数据前面是一个SoT,传输完成以后紧跟一个EoT,中间就是实际的负载数据。

       一个完整的高速模式数据传输时序如图22.1.3.4.2所示:


图22.1.3.4.2高速数据传输时序


       图22.1.3.4.2中左侧蓝色部分是进入HS模式,要从LP-11→LP01→LP-00, 数据线进入到HS模式,也就是中间红色部分,传输实际的数据。传输完成以后重新进入到LP-11(STOP)模式,也就是右边的蓝色部分。


       2、Escape模式

       Escape是运行在LP状态下的一个特殊模式,在此模式下可以实现一些特殊的功能,我们给屏幕发送配置信息就需要运行在Escape模式下。数据线进入Escape模式的方式为:LP-11→LP-10→LP-00→LP-01→LP-00。退出Escape模式的方式为:LP-00→LP-10→LP-11,也就是最后会进入到STOP模式,进入和退出Escape模式的时序如图22.1.3.4.3所示:


图22.1.3.4.3 Escape模式进入和退出


       对于数据Lanes,进入Escape模式以后,应该紧接着发送一个8bit的命令来表示接下来要做的操作,命令如图22.1.3.4.4所示:


图22.1.3.4.4 Escape模式命令


       从图22.1.3.4.4可以看出,有三个可选的命令:LPDT(Low-Power Data Transmissio)、ULPS(Ultra-Low Power State)和Remote-Trigger(这里没有写错,因为这个命令大部分做复位操作,所以有些资料将这个命令也叫做Remote-Trigger)。

       1)、LPDT命令

       图22.1.3.4.4中第一个就是LPDT命令,命令序列为11100001,注意低bit先发送,所以对应的十六进制就是0X87(0X10000111)!LPDT直译过来就是低功耗数据传输,我们在初始化MIPI屏幕的时候发送的初始化序列就需要用LPDT命令,后面会给大家看逻辑分析仪采集到的实际数据。LPDT命令序列后面紧跟着就是要发送的数据,分为长包和短包两种,长短包结构后面会详细讲解。

       2)、ULPS命令

       ULPS命令是让Lane进入超低功耗模式。

       3)、Remote-Trigger命令

       注意,这里大家可能会疑惑,有的资料叫做Remote-Trigger,有的叫做Reset-Trigger,其实都是一个东西。因为本质是Remote Application,但是做的是Reset的工作,所以就产生了两种叫法,目前此命令就是用于远程复位。

       Escape模式下发送这三个命令的时序图如图22.1.3.4.5所示:


图22.1.3.4.5 Escape模式下各命令时序图


       22.1.4 video和command模式

       在MIPI DSI的链路层有两种模式:video(视频)和command(命令)模式,这个属于HOST端,也就是主控端,比如ESP32-P4的DSI HOST接口。video和command通常离不开HS和LP模式,但是video和command属于Host范畴,HS和LP属于D-PHY范畴。


       22.1.4.1 command模式

       command模式一般是针对那些含有buffer的MCU屏幕,比如STM32单片机驱动MCU屏的时候就是command模式。当画面有变化的时候,DSI Host端将数据发给屏幕,主控只有在画面需要更改的时候发送像素数据,画面不变化的时候屏幕驱动芯片从自己内部buffer里面提取数据显示,command模式下需要双向数据接口。一般此种模式的屏幕尺寸和分辨率不大,一般用在单片机等低端领域。command模式如图22.1.4.1.1所示:


图22.1.4.1.1 command模式示意图


       22.1.4.2 video模式

       video模式没有framebuffer,需要主控一直发送数据给屏幕,和我们使用过的RGB接口屏幕类似。但是MIPI DSI没有专用的信号线发送同步信息,比如VSYNC、HSYNC等,所以这些控制信号和RGB图像数据以报文的形式在MIPI数据线上传输。基本上我们说的“MIPI屏”就是工作在video模式下,包括我们使用的RK3568,其工作模式就是video。video模式如图22.1.4.2.1所示:


图22.1.4.2.1 video模式示意图


       22.1.5 DPI格式

       DPI接口全称Display Pixel Interface,就是我们常说的RGB接口,在21.1.1章节有做介绍,大家可以自行回顾一下RGB接口涉及到的时序参数。

       关于DPI这两种格式的详细协议内容,请参考《MIPI Alliance Standard for Display Pixel Interface(DPI-2).pdf》这份文档。

       MIPI DSI接口的屏幕里面传输的是DPI格式的数据。DPI格式的数据时序,主要就是要关注一些时序参数,比如thpw、thb、thfp、tvpw、tvb和tvfp。这些参数都是为了锁定有效的像素数据,都可以从MIPI LCD屏幕数据手册中找到。


       22.1.6 video模式下三种传输方式

       对于video模式下的数据传输有三种时序模式:

       ●Non-Burst Mode with Sync Pulses:外设可以准确的重建原始的视频时序,包括同步脉冲宽度。

       ●Non-Burst Mode with Sync Events:和上面的模式类似,但是不需要精准的重建同步脉冲宽度,取而代之的是发送一个“Sync event”包。

       ●Burst Mode:此模式下发送RGB数据包的时间被压缩,这样可以在发送一行数据以后尽快进入到LP模式,以节省功耗。


       22.1.6.1 Non-Burst Mode with Sync Pluses

       此模式下外设可以准确的重建原始视频的时序、同步信号。通过在DSI接口上发送DPI时序,可以精确的匹配DPI像素传输速率以及时序宽度等,比如同步脉冲。所以此模式下每一个Sync Start(HSA)都要有一个对应的Sync End(HSE),此模式下的时序图如图22.1.6.1.1所示:


图22.1.6.1.1 Non-Burst Mode with Sync Pluses模式时序图


       Non-Burst Mode with Sync Pluses模式需要精准控制同步时序宽度,所以有Sync Start和Sync End,比如图22.1.6.1.1中的VSS和VSE、HSS和HSE信号,也就是水平同步开始和结束信号。


       22.1.6.2 Non-Burst Mode with Sync Events

       此模式和上一小节讲解的Non-Burst Mode with Sync Pluses模式类似,只是此模式下不需要精准的控制同步时序的宽度,所以此模式只有Sync Start,时序如图22.1.6.2.1所示:


图22.1.6.2.1 Non-Burst Mode with Sync Events模式时序图


       从图22.1.6.2.1可以看出此模式下只有Sync Start,没有Sync End,比如只有VSS和HSS,而没有VSE和HSE。


       22.1.6.3 Burst Mode

       Burst Mode是MIPI DSI最常用的模式,相比于Non-Burst Mode with Sync Events模式,Burst Mode可以快速的完成一帧或一行图像像素的传输,这样可以是数据线有更多的时间处于LP模式以节省功耗。此模式下时序图如图22.1.6.3.1所示:


图22.1.6.3.1 Burst Mode模式时序图


       图22.1.6.3.1中VACT Lines中,RGB图像像素数据会尽快传递完成,然后进入到BLLP以节省功耗。


       22.1.7 长短数据包


       22.1.7.1 数据包概述

       在MIPI DSI的数据传输中,不管是并行数据、信号事件还是命令,都需要按照协议打包成数据包。按照规定的协议添加头尾等信息,然后通过数据Lane将打包好的数据发送出去。

       如一次传输只发送一个数据包,那么在传输多个数据包就会花费大量的开销在LPS和HS切换上,这样会严重的浪费带宽。为此,MIPI DSI协议允许在一次传输中可以串行的发送多个数据包,这样就可以大幅的提高带宽利用率,这对于像外设初始化这种操作非常有益,比如我们在初始化屏幕的时候会发送大量的初始化命令。

       数据包第一个字节是DI(Data Identifier),用来指定当前数据包的含义,数据包 一共有两种数据包:

       ● 短数据包:固定 4个字节,包括ECC。短包一般用于Command模式下发送命令和参数,但是在实际使用中,Video模式下也用短包发送命令参数信息。其他的一些短包发送一些事件,比如H Sync和V Sync边沿。

       ● 长数据包:通过2个字节的 WC(Word Count)域来指定负载长度,负载长度范围:0~216-1个字节,也就是0~65535个字节。因此一个长数据包最多有65541个字节:

       1个字节的DI+2个字节的WC+1个字节的ECC+65535个字节的负载+2个字节的校验和=1+2+1+65535+2=65541。

       我们在22.1.3.4小节讲解Escape模式的时候说过,进入Escape模式以后紧跟着一个8bit的命令来表示接下来要做的操作,我们一般使用LPDT命令+具体的配置参数来完成向MIPI屏幕发送初始化参数的操作。但是并不是直接在LPDT命令后面跟着发送想要的时序参数就行了,而是要按照上面说的MIPI DSI格式将时序参数打包成长短数据包发送出去。


       22.1.7.2 数据包字节排序策略

       数据包都是以字节形式通过接口传输,同一个字节LSB先传输,MSB后传输,也就是低bit先发,高bit后发。如果某个域有多个字节,那么低字节(LS Byte)先发,高字节(MS Byte)后发,比如WC字段有2个字节,那么低字节的就先发,高字节的后发。但是,对于负载段就不用按照这个规定来,而是按照字节顺序发送。

       图22.1.7.2.1就是一个长包数据的发送方式:


图22.1.7.2.1 长包数据发送格式


       22.1.7.3 长数据包

       长数据包结构如图22.1.7.3.1所示:


图22.1.7.3.1长数据包结构


       图22.1.7.3.1中绿色的部分就是长包结构,长包有3部分:32-bit的PH(包头)、用于自定义的负载数据、16-bit的包尾(PF)。PH有3部分:8-bit的DI,16-bit的WC以及8-bit的ECC。PF只有1部分:16-bit的校验和,因此长包数据长度范围是6~65541个字节。


       22.1.7.4 短数据包

       短包结构如图22.1.7.4.1所示:


图22.1.7.4.1 短数据包结构


       图22.1.7.4.1就是短数据包结构,只有一个PH(包头),PH分为3部分:和长数据包一样,第1个就是8-bit的DI域;接下来是2个字节的数据负载域,也就是用户要实际发送的内容;最后是一个8-bit的ECC域,可以实现1bit纠错,2比特错误检测。

       我们重点来看一下DI域,因为长短数据包第第一部分就是DI域,而且是含义是相同的。DI域由两部分组成:虚拟通道和负载数据类型,结构如图22.1.7.4.2所示:


图22.1.7.4.2 DI域接口


       其中bit7:6这两位指定虚拟通道,在本章节我们不研究这个虚拟通道。bit5:0这6位就是指定后面要发送的负载数据类型,也就是那些负载数据是做啥的。注意,这个数据类型才是我们要学习的重点,后面小节会详细讲解MIPI DSI协议所支持的数据类型。


       22.1.8 指令类型

       在上一小节讲解长短数据包的时候说过,DI域包含了后面负载数据的数据类型,MIPI DSI协议已经定义好了这些类型,这里只介绍最常用的。这里叫说数据类型,有些资料也叫做指令,有两种指令集:Generic指令和DSC指令,这两个的区别在于Generic指令是不区分index和parameter的,而DSC会默认把data0作为index,然后计算parameter数目。关于DCS相关的详细请参考《Specification for Display Command Set (DCS).pdf》。

       指令分为主机发向外设,以及外设发向主机两种,我们要初始化屏幕肯定用的是主机发向外设的,其指令集合如图22.1.8.1所示:


图22.1.8.1 主控向外设发送的命令


       图22.1.8.1中有DCS指令,也有Generic指令。每个指令是长数据包还是短数据包,在最后一列都注明了。图中这些指令的详细含义请参考《MIPI DSI Specification_v1-3.pdf》的“8.8 Processor-to-Peripheral Transactions – Detailed Format Description”小节。

       我们稍后在具体初始化MIPI屏幕的时候就要用到上面这些指令。


       22.1.9 MIPI DSI时钟计算

       屏幕传输一帧图形数据的时候要用到的理论像素时钟不是1280*720,因为还涉及到thpw、thb、thfp、tvpw、tvb和tvfp等参数,因此屏幕真实的水平和垂直像素数时钟如下:

       水平:

       垂直:

       其中,HPW和VPW是水平和垂直同步信号宽度,HB和VB是水平和垂直后廊,HACTIVE是屏幕的水平有效像素,VACTIVE是屏幕的宽度有效像素,HFP和VFP是水平和垂直前廊。屏幕的这些参数都能在屏幕手册里面找到,比如正点原子两款5.5寸MIPI屏幕参数如表22.1.9.1所示:


表22.1.9.1 正点原子MIPI屏幕时序参数


       1秒钟的像素数量就是:

 

       其中fps就是屏幕帧率,一般是60帧,也就是1秒钟刷新60张图像,Pixel_total就是1秒钟要传输的总的像素点。

       如果屏幕采用RGB888格式,那么1个像素就是24bit,如果是RGB565那就是16bit,这个叫色深,我们一般都使用RGB565。那么总的bit时钟就是:

       得到的Bit_total是总的bit时钟数,但是MIPI DSI可以配置多条数据lane,一般是2或4lane,也就是有2个或者4个通道,所以每个通道的bit时钟就是:

       计算出来的Bit_clk就是MIPI DSI的时钟,但是由于MIPI DSI是双边沿采集,所以最终的DSI CLK时钟还要除以2:

       总结一下DSI CLK时钟的计算公式如下:

       我们以正点原子720*1280这款5.5寸MIPI屏幕为例,采用RGB565格式,帧率为60fps,使用2lane传输数据,那么此屏幕的DSI时钟为:

       Dsi_clk = (8+52+720+48)×(6+15+1280+16)×60×16/2/2

              = 828×1317×60×16/2/2

                = 261,714,240 Hz

          ≈261M Hz

       261M就是我们用示波器测出来的频率,注意这个261M只是理论值,实际值要高!因为要考虑到开销,后面会讲。

       我们一般说的MIPI DSI时钟要在这个时间测量到的频率上乘以2,因为双边沿采集嘛,所以MIPI DSI速率就是261714240*2=523,428,480 Hz≈523M。

       注意重点:

       后面在程序中设置MIPI DSI速率时,不能直接设置前面计算出来的理论MIPI DSI速率,在前面学习长短包的时候就知道,实际的MIPI DSI通信中还有其他的开销。假如直接将MIPI DSI的速率设置成523M,那么实际的屏幕帧率肯定到不了60fps。但是我们也没必要研究具体用了多少开销,实际精准的速率是多少,这样太耗费时间了,难度也很大。一般都是在理论速率上加上一些余量,参考ESP32-P4驱动其他MIPI屏幕,那么实际设置的MIPI DSI速率大于750即可。


       22.1.10 ESP32-P4的MIPI DSI介绍

       ESP32-P4带有一个MIPI DSI接口,用于连接MIPI接口的显示屏,具有如下特性:

       ● 符合MIPI DSI协议。

       ● 使用DPHY v1.1版本。

       ● 2-lane x 1.5 Gbps。

       ● 输入格式支持RGB888、RGB666、RGB565、YUV422。

       ● 输出格式支持RGB888、RGB666、RGB565。

       ● 使用video mode输出视频流

       ● 支持输出固定图像pattern

       MIPI DSI接口使用专用数字管脚,管脚序号为34~40。

       关于ESP32-P4的MIPI DSI外设就介绍到这里,具体内容可以自行查阅ESP32-P4参考手册。


        22.2 硬件设计


       22.2.1 例程功能

       使用ESP32-P4底板的MIPI屏幕FPC座实现MIPILCD模块的显示。通过把正点原子的MIPI屏幕模块插入底板上的MIPI屏幕FPC座,按下复位之后,就可以看到MIPILCD模块不停地显示一些信息并不断切换底色。LED0闪烁用于提示程序正在运行。


       22.2.2 硬件资源


       1)LED灯

              LED 0 - IO51


       2)MIPILCD

              DSI_D0_P(固定引脚)  DSI_D0_N(固定引脚)

              DSI_D1_P(固定引脚)  DSI_D1_N(固定引脚)

              DSI_CK_P(固定引脚)  DSI_CK_N(固定引脚)

              LCD_RST  - IO52

              CT_RST   - IO45   CT_INT - IO21

              IIC_SCL  - IO32   IIC_SDA - IO33


       22.2.3 原理图

       MIPILCD原理图,如下图所示。


图22.2.3.1 MIPILCD原理图


       从上图可知,正点原子ESP32-P4开发板的MIPI屏幕接口采用2Lane,其中LCD_RST是屏幕的复位引脚,通过一个电阻分压电路得到MIPI_RST_1V8,因为MIPI屏幕要求复位引脚电平为1.8V。


        22.3 程序设计


       22.3.1 LCD的IDF驱动

       LCD外设驱动位于ESP-IDF下的components/esp_lcd目录下。要使用esp_lcd功能,需要导入一下头文件:

#include "esp_lcd_panel_interface.h" /* LCD面板结构体类型 */
#include "esp_lcd_panel_io.h"   /* 驱动芯片接收/发送命令,发送颜色数据等函数 */
#include "esp_lcd_panel_vendor.h"  /* 包含LCD外设驱动支持的几款驱动芯片 */
#include "esp_lcd_panel_ops.h"   /* LCD设备接口函数(reset/init/del等) */
#include "esp_lcd_panel_commands.h" /* LCD驱动芯片的命令 */
#include "esp_lcd_mipi_dsi.h"   /* MIPILCD的函数 */

       MIPILCD 驱动流程可大致分为三个部分:初始化接口设备、移植驱动组件和初始化 LCD 设备。

       接下来,作者就按照这三个部分分别介绍用到的函数。

       初始化接口设备

       初始化接口设备需要先初始化总线,再创建接口设备。


       1, 初始化MIPI_DSI总线函数esp_lcd_new_dis_bus

       该函数用于创建DSI总线,并对D-PHY进行初始化设置,其函数原型如下:

esp_err_t esp_lcd_new_dsi_bus(const esp_lcd_dsi_bus_config_t *bus_config, esp_lcd_dsi_bus_handle_t *ret_bus)

       函数形参:


表22.3.1.1 esp_lcd_new_dsi_bus函数形参描述


       函数返回值:

       ESP_OK表示MIPI_DSI总线初始化成功。

       ESP_ERR_INVALID_ARG表示错误参数。

       ESP_ERR_NO_MEM表示内存不足。

       ESP_ERR_NOT_FOUND表示没有空闲的DSI总线。

       ESP_FAIL表示创建MIPI_DSI发生其他错误。

       bus_config为指向DSI总线配置结构体的指针,esp_lcd_dsi_bus_config_t结构体中包含很多成员,如下代码所示。

typedef struct {
    int bus_id;                            /* 指定要使用的DSI主机 */
    uint8_t num_data_lanes;                   /* 要使用的数据通道数 */
    mipi_dsi_phy_clock_source_t phy_clk_src;  /* DPHY时钟源 */
    uint32_t lane_bit_rate_mbps;              /* 数据通道的比特率(Mbps) */
} esp_lcd_dsi_bus_config_t;

       esp_lcd_dsi_bus_config_t结构体的bus_id注意要从0开始编号;num_data_lanes要根据芯片支持的数量进行设置;DPHY时钟源是可选XTAL、F160M和F240M,都有对应的宏选择MIPI_DSI_DPI_CLK_SRC_XTAL、MIPI_DSI_DPI_CLK_SRC_PLL_F160M和MIPI_DSI_DPI_CLK_SRC_PLL_F240M,直接设置默认MIPI_DSI_DPI_CLK_SRC_DEFAULT即可,即选择F240M;而lane_bit_rate_mbps要根据dsi时钟计算进行设置。

       ret_bus为指向esp_lcd_dsi_bus_handle_t结构体的指针,esp_lcd_dis_bus_handle_t结构体可以不需要了解。

       创建接口设备esp_lcd_new_panel_io_dpi

       该函数用于创建DBI接口LCD IO设备。DBI接口用于控制IO层,使用该接口可读写LCD设备内部的配置寄存器,其函数原型如下:

esp_err_t esp_lcd_new_panel_io_dbi(esp_lcd_dsi_bus_handle_t bus, const esp_lcd_dbi_io_config_t *io_config, esp_lcd_panel_io_handle_t *ret_io)

       函数形参:


表22.3.1.2 esp_lcd_new_panel_io_dpi函数形参描述


       函数返回值:

       ESP_OK表示创建接口设备成功。

       ESP_ERR_INVALID_ARG表示错误参数。

       ESP_ERR_NO_MEM表示内存不足。

       ESP_FAIL表示其他错误。

       bus为DSI总线句柄结构体,调用 esp_lcd_new_dsi_bus函数会创建DSI总线句柄结构体。

       io_config为指向DBI接口的LCD IO设备配置结构体的指针,esp_lcd_dbi_io_config_t结构体中包含很多成员,如下代码所示。

typedef struct {
    uint8_t virtual_channel;  /* 设置虚拟通道号  */
    int lcd_cmd_bits;         /* 设置LCD控制芯片可识别的命令位宽  */
    int lcd_param_bits;       /* 设置LCD控制芯片可识别的参数位宽  */
} esp_lcd_dbi_io_config_t;

       这里需要注意:① virtual_channel虚拟通道是一种逻辑通道,用于从不同来源多路复用数据。如果只连接了一个LCD,则将此值设置为0。② 根据LCD驱动芯片的实际情况,配置命令和参数位宽,对于正点原子的MIPI屏这两个参数位宽都是8位。

       ret_io为指向LCD接口句柄结构体指针,esp_lcd_panel_io_handle_t实际是esp_lcd_panel_io_t,该结构体中包含一些函数接口,用于给LCD驱动芯片发送命令和图像数据,如下代码所示。

struct esp_lcd_panel_io_t {
    esp_err_t (*rx_param)(esp_lcd_panel_io_t *io, int lcd_cmd, void *param, size_t param_size);        /* 发送单个LCD命令并接收响应参数 */
esp_err_t (*tx_param)(esp_lcd_panel_io_t *io, int lcd_cmd, const void *param, size_t param_size);      /* 发送单个LCD命令及配套参数 */
esp_err_t (*tx_color)(esp_lcd_panel_io_t *io, int lcd_cmd, const void *color, size_t color_size);      /* 发送单次LCD刷屏命令和图像数据 */
esp_err_t (*del)(esp_lcd_panel_io_t *io); /* 卸载LCD IO设备句柄 */
esp_err_t (*register_event_callbacks)(esp_lcd_panel_io_t *io, const esp_lcd_panel_io_callbacks_t *cbs, void *user_ctx); /* 注册LCD IO设备回调 */
};

       在esp_lcd_new_panel_io_dbi函数的内部就会将DBI底层接口与ret_io绑定起来,后续直接访问ret_io即可调用到DBI的底层接口,比如panel_io_dbi_rx_param、panel_io_ dbi _tx_param和panel_io_dbi_del。

       移植驱动组件

       移植MIPI LCD驱动组件的基本原理包含以下三点:

       1、 基于数据类型为esp_lcd_panel_io_handle_t的接口设备句柄发送指定格式的命令及参数

       2、 实现并创建一个LCD设备,然后通过注册回调函数的方式实现结构体esp_lcd_panel_t中的各项功能

       3、 实现一个函数用于提供数据类型为esp_lcd_panel_handle_t的LCD设备句柄,使得应用程序能够利用LCD通用API来操作LCD设备

       第一点已经在前面创建接口设备时完成了,可以调用DBI底层接口。而第二和第三点需要用到esp_lcd_new_panel_xxx函数去实现。xxx对应的是LCD驱动芯片,由于正点原子的MIPI屏幕有三款,虽然说乐鑫已经对两款做了支持,但是我们为了兼容所有MIPI屏幕,还是自己编写MIPI LCD的驱动组件。想要知道乐鑫已经对哪些MIPI屏做了兼容可以通过组件仓库进行搜索,如下图所示。


图22.3.1.1 查看乐鑫支持MIPI LCD驱动芯片


       在前面SPILCD章节中,直接使用乐鑫提供的st7789文件很方便就能驱动起来。RGBLCD章节更简单,由于不需要SPI接口配置,直接跳过该步骤。而在本章节,就得介绍一下怎么对屏幕驱动,知道要编写哪些函数,函数该怎么编写,以及怎么样与上层接口对应起来,了解这种方法之后,后续大家就可以兼容自己的屏幕。

       接下来,我们了解一下LCD驱动组件需要要实现的函数接口以及我们要实现的函数,如下两表所示。


表22.3.1.3 LCD通用API函数


表22.3.1.4 MIPILCD函数列表


       表22.3.1.1罗列的是通用API接口,而表22.3.1.2罗列的是我们在mipi_lcd.c文件中实现的函数接口。通过两表,大家大概也猜到一些函数里面的实现了,就是发送不同命令实现不同功能。draw_bitmap函数没有实现,后续在其他文件中实现。

       首先介绍一下mipi_lcd_new_panel函数。


       1, 为MIPI LCD驱动芯片创建LCD面板mipi_lcd_new_panel。

       该函数用于为MIPI LCD驱动芯片创建LCD面板并对LCD设备配置,其函数原型如下:

esp_err_t mipi_lcd_new_panel(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel)

       函数形参:


表22.3.1.5 mipi_lcd_new_panel函数形参描述


       函数实现:

esp_err_t mipi_lcd_new_panel(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel)
{
    esp_err_t ret = ESP_OK;
 
    ESP_RETURN_ON_FALSE( io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, mipi_lcd_tag, "invalid argument");
 
    mipi_panel_t *mipi_lcd = (mipi_panel_t *)calloc(1, sizeof(mipi_panel_t));
ESP_RETURN_ON_FALSE( mipi_lcd, ESP_ERR_NO_MEM, mipi_lcd_tag, 
"no mem for mipi_lcd panel");
 
    if (panel_dev_config->reset_gpio_num >= 0)  /* 配置LCD复位引脚 */
    {
        gpio_config_t io_conf = {
            .mode         = GPIO_MODE_OUTPUT,
            .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num,
        };
        ESP_GOTO_ON_ERROR( gpio_config(&io_conf), err, mipi_lcd_tag, 
"configure GPIO for RST line failed");
    }
 
    switch (panel_dev_config->rgb_ele_order)   /* 颜色顺序RGB/BGR */
    {
        case LCD_RGB_ELEMENT_ORDER_RGB:
            mipi_lcd->madctl_val = 0;
            break;
        case LCD_RGB_ELEMENT_ORDER_BGR:
            mipi_lcd->madctl_val |= LCD_CMD_BGR_BIT;
            break;
        default:
            ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, mipi_lcd_tag, "unsupported rgb element order");
            break;
    }
 
    switch (panel_dev_config->bits_per_pixel)       /* 像素格式 */
    {
        case 16:    /* RGB565 */
            mipi_lcd->colmod_val = 0x55;
            break;
        case 18:    /* RGB666 */
            mipi_lcd->colmod_val = 0x66;
            break;
        case 24:    /* RGB888 */
            mipi_lcd->colmod_val = 0x77;
            break;
        default:
            ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, mipi_lcd_tag, "unsupported pixel width");
            break;
    }
 
    mipi_lcd->io                = io;
    mipi_lcd->reset_gpio_num    = panel_dev_config->reset_gpio_num;
    mipi_lcd->reset_level       = panel_dev_config->flags.reset_active_high;
    mipi_lcd->base.reset        = mipi_lcd_panelreset;  
    mipi_lcd->base.init         = mipi_lcd_panelinit;         
    mipi_lcd->base.del          = mipi_lcd_paneldel;         
    mipi_lcd->base.mirror       = mipi_lcd_panelmirror;       
    mipi_lcd->base.swap_xy      = mipi_lcd_panelswap_xy;      
    mipi_lcd->base.set_gap      = mipi_lcd_panelset_gap;      
    mipi_lcd->base.invert_color = mipi_lcd_panelinvert_color; 
    mipi_lcd->base.disp_on_off  = mipi_lcd_paneldisp_on_off;  
    mipi_lcd->base.disp_sleep   = mipi_lcd_panelsleep;        
 
    *ret_panel = &mipi_lcd->base;
 
    return ESP_OK;
 
err:
    if (mipi_lcd)
    {
        mipi_lcd_paneldel(&mipi_lcd->base);
    }
 
    return ret;
}

        简单来说,就做了两件事情:

       ① 根据传参panel_dev_config对LCD做配置,比如说复位引脚配置和像素格式。

       ② 将LCD的配置信息和一些函数接口传递给esp_lcd_panel_handle_t结构体类型指针变量ret_panel。

       函数返回值:

       ESP_OK表示创建成功。

       其他表示异常。

       io为LCD接口句柄结构体esp_lcd_panel_io_t,该结构体在前面已经介绍了,这里不再展开。

       panel_dev_config为指向LCD设备配置结构体指针,esp_lcd_panel_dev_config_t结构体中包含几个成员,如下代码所示。

typedef struct {
    int reset_gpio_num;       /* 连接LCD复位信号的引脚 */
    union {
        esp_lcd_color_space_t color_space;    /* RGB色彩空间,rgb_ele_order代替 */
        lcd_color_rgb_endian_t rgb_endian;    /* 设置数据端序,rgb_ele_order代替 */
        lcd_rgb_element_order_t rgb_ele_order; /* 像素色彩的元素顺序(RGB/BGR) */
    };
    lcd_rgb_data_endian_t data_endian;         /* 设置>1字节的颜色数据的数据端序 */
    uint32_t bits_per_pixel;              /* 色彩格式的位数 */
    struct {
        uint32_t reset_active_high: 1;   /* 复位引脚有效电平 */
    } flags;                           
    void *vendor_config;       /* 用于替换驱动组件的初始化序列 */
} esp_lcd_panel_dev_config_t;

       esp_lcd_panel_dev_config_t结构体需要注意以下几点:

       ① 当发生颜色显示不对,比如想显示红色,最终是蓝色,这是像素色彩的元素顺序问题,通过调整rgb_ele_order的赋值。

       ② 颜色显示异常,并非①中描述的情况,除了白色、黑色显示出来,这是由于颜色数据的发送顺序错误,通过调整data_endian的赋值。

       ③ 若手上的屏幕驱动起来,色彩有点偏差,可通过vendor_config把厂家提供的初始化序列填充进去。

       ④ 若不是通过芯片的IO作为复位引脚,reset_gpio_num直接赋值GPIO_NUM_NC即可,硬件复位就得自己去实现。

       ret_panel为指向LCD设备句柄结构体指针,esp_lcd_panel_handle_t其实是esp_lcd_panel_t,如下代码所示。

struct esp_lcd_panel_t {
esp_err_t (*reset)(esp_lcd_panel_t *panel); /* LCD屏幕复位 */
esp_err_t (*init)(esp_lcd_panel_t *panel);  /* LCD屏幕初始化 */
esp_err_t (*del)(esp_lcd_panel_t *panel);  /* 卸载LCD屏幕 */
esp_err_t (*draw_bitmap)(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);  /* LCD屏幕绘画函数 */
/* LCD屏幕镜像X轴和Y轴 */
esp_err_t (*mirror)(esp_lcd_panel_t *panel, bool x_axis, bool y_axis);
/* LCD屏幕交换X轴和Y轴 */
esp_err_t (*swap_xy)(esp_lcd_panel_t *panel, bool swap_axes);
/* LCD屏幕设置画图的起始坐标 */
esp_err_t (*set_gap)(esp_lcd_panel_t *panel, int x_gap, int y_gap);
/* LCD屏幕像素颜色数据按位取反(0xF0F0->0x0F0F),即反显功能 */
esp_err_t (*invert_color)(esp_lcd_panel_t *panel, bool invert_color_data);
/* LCD屏幕显示开关 */
esp_err_t (*disp_on_off)(esp_lcd_panel_t *panel, bool on_off);
/* LCD屏幕休眠开关 */
    esp_err_t (*disp_sleep)(esp_lcd_panel_t *panel, bool sleep);
void *user_data;    /* 用户数据,用于存储外部自定义数据 */
};

       esp_lcd_panel_handle_t结构体对应的就是表22.3.1.1的内容了。

       接下来,就要介绍一下我们对应实现的函数接口。

       第一个介绍的是mipi_lcd_panelinit函数,如下代码所示。

static esp_err_t mipi_lcd_panelinit(esp_lcd_panel_t *panel)
{
    mipi_panel_t *mipi_lcd = __containerof(panel, mipi_panel_t, base);
    esp_lcd_panel_io_handle_t io = mipi_lcd->io;
    
    const mipi_lcd_init_cmd_t *init_cmds = {0};
    uint16_t init_cmds_size = 0;
    bool mirror_x = true;
    bool mirror_y = false;
 
    if (mipidev.id == 0x8399)       /* 5寸,720P */
    {
        init_cmds = vendor_specific_init_code_default_1080p;
        init_cmds_size = sizeof(vendor_specific_init_code_default_1080p) / 
sizeof(mipi_lcd_init_cmd_t);
    }
    else if (mipidev.id == 0x8394)  /* 5寸,1080p */
    {
        init_cmds = vendor_specific_init_code_default_720p;
        init_cmds_size = sizeof(vendor_specific_init_code_default_720p) / 
sizeof(mipi_lcd_init_cmd_t);
    }
    else if (mipidev.id == 0x9881)  /* 10.1寸,800p */
    {
        init_cmds = vendor_specific_init_code_default_800p;
        init_cmds_size = sizeof(vendor_specific_init_code_default_800p) / 
sizeof(mipi_lcd_init_cmd_t);
 
        /* 返回 命令页 1 */
        ESP_RETURN_ON_ERROR( esp_lcd_panel_io_tx_param(io, ILI9881C_CMD_CNDBKxSEL, (uint8_t[]) {
           ILI9881C_CMD_BKxSEL_BYTE0, ILI9881C_CMD_BKxSEL_BYTE1, ILI9881C_CMD_BKxSEL_BYTE2_PAGE1
}, 3), mipi_lcd_tag, "send command failed");
        /* 设置2 lane */
        ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, ILI9881C_PAD_CONTROL, 
(uint8_t[]) {
                ILI9881C_DSI_2_LANE,
             }, 1), mipi_lcd_tag, "send command failed");
        /* 返回 命令页 0 */
        ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, 
ILI9881C_CMD_CNDBKxSEL, (uint8_t[]) {
         ILI9881C_CMD_BKxSEL_BYTE0, ILI9881C_CMD_BKxSEL_BYTE1,
             ILI9881C_CMD_BKxSEL_BYTE2_PAGE0
          }, 3), mipi_lcd_tag, "send command failed");
        mirror_x = false;
    }
 
    /* 发送初始化序列 */
    for (int i = 0; i < init_cmds_size; i++)
    {
        ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd,
init_cmds[i].data, init_cmds[i].data_bytes),mipi_lcd_tag,"send command failed");
    }
 
    vTaskDelay(pdMS_TO_TICKS(120));
 
    /* 退出睡眠 */
ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, NULL, 0), 
mipi_lcd_tag, "io tx param failed");
    
    /* 根据MIPI屏放置位置调整显示方向(也可调用mipi_lcd_panelmirror函数设置) */
    if (mirror_x)
    {
        mipi_lcd->madctl_val |= HX_F_SS_PANEL;       /* 扫描方向水平翻转 */
    }
    else
    {
        mipi_lcd->madctl_val &= ~HX_F_SS_PANEL;      /* 扫描方向水平不翻转 */
    }
 
    if (mirror_y)
    {
        mipi_lcd->madctl_val |= HX_F_GS_PANEL;       /* 扫描方向垂直翻转 */
    }
    else
    {
        mipi_lcd->madctl_val &= ~HX_F_GS_PANEL;      /* 扫描方向垂直不翻转 */
    }
    ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io,LCD_CMD_MADCTL, (uint8_t[])
    {
        mipi_lcd->madctl_val,
    }, 1), mipi_lcd_tag, "send command failed");    /* 配置MIPILCD的显示 */
 
    ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io,LCD_CMD_COLMOD, (uint8_t[])
    {
        mipi_lcd->colmod_val,
    }, 1), mipi_lcd_tag, "send command failed");    /* 配置像素格式 */
    vTaskDelay(pdMS_TO_TICKS(120));
 
return ESP_OK;
}

       mipi_lcd_panelinit函数主要就是通过发送厂家提供的初始化序列来初始化LCD设备,其次设置LCD的扫描方向以及配置像素格式。

       在这里说明一下container_of函数,该函数的功能就是根据结构体中的已知成员变量的地址,来寻求该结构体的首地址,如下图所示。


图22.3.1.2 container_of函数功能


       通过container_of函数能把mipi_lcd结构体(自定义的结构体类型)中的base成员地址获取到,即结构体的首地址。通过mipi_lcd结构体便可以通过其成员,对LCD进行配置。

       mipi_lcd结构体类型,如下代码所示。

/* 初始化屏幕结构体 */
typedef struct {
    esp_lcd_panel_t base;           /* LCD设备的基础接口函数 */
    esp_lcd_panel_io_handle_t io;   /* LCD设备的IO接口函数配置 */
    int reset_gpio_num;             /* 复位管脚 */
    int x_gap;                      /* x偏移 */
    int y_gap;                      /* y偏移 */
    uint8_t madctl_val;             /* 保存LCD CMD MADCTL寄存器的当前值 */
    uint8_t colmod_val;             /* 保存LCD_CMD_COLMOD寄存器的当前值 */
    uint16_t init_cmds_size;        /* 初始化序列大小 */
    bool reset_level;               /* 复位电平 */
} mipi_panel_t;

       第二个介绍的是mipi_lcd_paneldisp_on_off函数,如下代码所示。

static esp_err_t mipi_lcd_paneldisp_on_off(esp_lcd_panel_t *panel, bool on_off)
{
    mipi_panel_t *mipi_lcd = __containerof(panel, mipi_panel_t, base);
    esp_lcd_panel_io_handle_t io = mipi_lcd->io;
    int command = 0;
 
    if (on_off)
    {
        command = LCD_CMD_DISPON;   /* 打开显示命令 */
    }
    else
    {
        command = LCD_CMD_DISPOFF;  /* 关闭显示命令 */
    }
ESP_RETURN_ON_ERROR( esp_lcd_panel_io_tx_param(io, command, NULL, 0), 
mipi_lcd_tag, "send command failed");
    
    return ESP_OK;
}

       该函数的功能是LCD显示开关,通过传参进行判断,打开显示命令LCD_CMD_DISPON,关闭显示命令LCD_CMD_DISPOFF,最终,通过esp_lcd_panel_io_tx_param函数接口发送出去。

       其他函数接口跟这里实现的代码逻辑是比较相似的,所以也不再一个个罗列说明,大家自行查看mipi_lcd.c文件即可。

       初始化LCD设备

       通过前面两个步骤的操作,算是完成了准备工作,接下来,就得调用LCD通用API接口对LCD进行初始化,如下代码所示。

ESP_ERROR_CHECK(esp_lcd_panel_reset(mipi_lcd_ctrl_panel));  /* 复位LCD */
ESP_ERROR_CHECK(esp_lcd_panel_init(mipi_lcd_ctrl_panel));   /* 初始化LCD */
/* 打开LCD */
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(mipi_lcd_ctrl_panel, true));

       此时仍然无法通过esp_lcd_panel_draw_bitmap函数向LCD绘画,因为MIPI LCD具有高分辨率,而LCD控制器中没有GRAM。因此需要维护LCD帧buffer,并通过MIPI DSI DPI接口将其刷新到LCD屏幕上,所以在这里需要对DPI进行配置。


       1, 为MIPI DSI DPI接口创建LCD面板函数esp_lcd_new_panel_dpi

       该函数用于创建DPI接口,并创建LCD面板,其函数原型如下:

esp_err_t esp_lcd_new_panel_dpi(esp_lcd_dsi_bus_handle_t bus, const esp_lcd_dpi_panel_config_t *panel_config, esp_lcd_panel_handle_t *ret_panel)

       函数形参:


表22.3.1.6 esp_lcd_new_panel_dpi函数形参描述


       函数返回值:

       ESP_OK表示MIPI_DSI数据面板创建成功。

       ESP_ERR_INVALID_ARG表示错误参数。

       ESP_ERR_NO_MEM表示内存不足。

       ESP_ERR_NOT_SUPPORTED表示不支持。

       ESP_FAIL表示发生其他错误。

       bus为DSI总线句柄结构体。

       panel_config为指向DSI数据面板配置结构体指针,esp_lcd_dpi_panel_config_t 结构体如下代码所示。

typedef struct {
    uint8_t virtual_channel;                   /* 设置虚拟通道号 */
    mipi_dsi_dpi_clock_source_t dpi_clk_src;   /* 设置DPI接口的时钟源 */
    uint32_t dpi_clock_freq_mhz;               /* 设置DPI时钟频率 */
    lcd_color_rgb_pixel_format_t pixel_format; /* 设置像素数据的像素格式 */
    uint8_t num_fbs;                           /* 整屏大小的帧缓存区数量 */
    esp_lcd_video_timing_t video_timing;       /* LCD面板的特定时序参数 */
 
    struct extra_flags {
        uint32_t use_dma2d: 1;      /* 是否启用DMA2D */
    } flags;                   
} esp_lcd_dpi_panel_config_t;

       这里需要注意几点:

       ① 与DBI接口类似,DPI 接口也需要设置虚拟通道。如果只连接了一个LCD,则将此值设置为0。

       ② DPI时钟源dpi_clk_src有三个,分别是XTAL、F160M和F240M,都有对应的宏选择MIPI_DSI_DPI_CLK_SRC_XTAL、MIPI_DSI_DPI_CLK_SRC_PLL_F160M和MIPI_DSI_DPI_CLK_SRC_PLL_F240M,直接设置默认MIPI_DSI_DPI_CLK_SRC_DEFAULT即可,即选择F240M;

       ③ 在设置DPI时钟频率时需要注意:像素时钟频率越高,刷新率越高,但如果DMA带宽不足或LCD控制器芯片不支持高像素时钟频率,则可能会导致闪烁。

       ④ 设置像素数据格式pixel_format有三种选择,分别是LCD_COLOR_PIXEL_FORMAT_RGB565、LCD_COLOR_PIXEL_FORMAT_RGB666、LCD_COLOR_PIXEL_FORMAT_RGB888,为了兼容性,这里选择了RGB565。MIPI LCD通常使用RGB888来获得最佳色彩深度。

       ⑤ LCD面板的特定时序参数video_timing,主要是看屏幕的规格书,在前面22.1.9小节已经有说明了,配置时对号入座即可。

       ret_panel为指向esp_lcd_panel_handle_t结构体的指针,esp_lcd_panel_handle_t结构体在前面介绍mipi_lcd_new_panel函数时,已经介绍过了,这里不再展开。

       最后,再调用一下esp_lcd_panel_init函数初始化一下,这时,MIPILCD完全驱动好,可以使用esp_lcd_panel_draw_bitmap函数进行绘画了。


       22.3.2 程序流程图


图22.3.2.1 MIPILCD实验程序流程图


       22.3.3 程序解析

       在12_mipilcd例程中,作者在12_mipilcd\components\BSP路径下新建LCD文件夹,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。


       1. LCD驱动代码

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。LCD驱动源码包括五个文件:mipi_lcd.c、mipi_lcd.h、lcd.c、lcd.h和lcdfont.h。

       mipi_lcd.c文件存放的是MIPILCD的驱动函数,而mipi_lcd.h存放的是LCD驱动芯片的命令宏、DSI总线配置宏以及自定义用于管理LCD的结构体类型,以及函数声明。lcd.c文件主要lcd的一些绘图函数,而lcd.h则存放的是引脚接口宏定义以及函数声明。lcdfont.h存放的是4种字体大小不一样的ASCII字符集(12*12、16*16、24*24和32*32)。lcd.c和lcd.h两个文件在11_rgblcd例程和12_mipilcd例程中存在差别,都是单独为某种类型屏幕进行驱动,而在13_lcd例程中对这两种类型屏幕做了兼容。

       在前面22.3.1小节中,已经提及了不少mipi_lcd.c文件里面的函数,这里主要给大家介绍一下MIPILCD的初始化函数mipi_lcd_init,如下代码所示:

/**
 * @brief    mipi_lcd初始化
 * @param    无
 * @retval     LCD控制句柄
 */
esp_lcd_panel_handle_t mipi_lcd_init(void)
{
    mipi_dev_bsp_enable_dsi_phy_power();    /* 配置MIPI接口电压1.8V */
 
    /* 创建DSI总线 */
    esp_lcd_dsi_bus_handle_t mipi_dsi_bus;
    esp_lcd_dsi_bus_config_t bus_config = {
        .bus_id = 0,                                    /* 总线ID */
        .num_data_lanes = MIPI_DSI_LANE_NUM,              /* 2路数据信号 */
        .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT,       /* DPHY时钟源为20M */
        .lane_bit_rate_mbps = MIPI_DSI_LANE_BITRATE_MBPS, /* 数据通道比特率 */
};
/* 新建DSI总线 */
    ESP_ERROR_CHECK(esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus));   
 
    /* 配置DSI总线的DBI接口 */
    esp_lcd_panel_io_handle_t mipi_dbi_io;
    esp_lcd_dbi_io_config_t dbi_config = {
        .virtual_channel = 0,    /* 虚拟通道(只有一个LCD连接,设置0即可) */
        .lcd_cmd_bits    = 8,      /* 根据MIPI LCD驱动IC规格设置 命令位宽度 */
        .lcd_param_bits  = 8,     /* 根据MIPI LCD驱动IC规格设置 参数位宽度 */
    };
ESP_ERROR_CHECK(esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, 
&mipi_dbi_io));
 
    /* 创建LCD控制器驱动 */
    esp_lcd_panel_handle_t mipi_lcd_ctrl_panel;    /* MIPI控制句柄 */
    esp_lcd_panel_dev_config_t lcd_dev_config = {
        .bits_per_pixel = 16,         /* MIPILCD的像素位宽度 */
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,   /* 像素数据的RGB元素顺序 */
        .reset_gpio_num = lcddev.ctrl.lcd_rst,       /* MIPILCD屏的复位引脚 */
    };
ESP_ERROR_CHECK(mipi_lcd_new_panel(mipi_dbi_io, &lcd_dev_config, 
&mipi_lcd_ctrl_panel));
 
    ESP_ERROR_CHECK(esp_lcd_panel_reset(mipi_lcd_ctrl_panel));  /* 复位LCD屏 */
 
    /* 读取屏幕ID */
    esp_lcd_panel_io_rx_param(mipi_dbi_io, 0xDA, &mipi_id[0], 1);
    vTaskDelay(pdMS_TO_TICKS(20));
    esp_lcd_panel_io_rx_param(mipi_dbi_io, 0xDB, &mipi_id[1], 1);
    vTaskDelay(pdMS_TO_TICKS(20));
    /* 不是HX8399和HX8394 */
    if (mipi_id[0] == 0x00 || mipi_id[1] == 0x00)
    {
        /* 读取ILI9881 ID */
        esp_lcd_panel_io_tx_param(mipi_dbi_io, ILI9881C_CMD_CNDBKxSEL,
        (uint8_t[]) {
       ILI9881C_CMD_BKxSEL_BYTE0, ILI9881C_CMD_BKxSEL_BYTE1,
     ILI9881C_CMD_BKxSEL_BYTE2_PAGE1
        }, 3);
        esp_lcd_panel_io_rx_param(mipi_dbi_io, 0x00, &mipi_id[0], 1);
        esp_lcd_panel_io_rx_param(mipi_dbi_io, 0x01, &mipi_id[1], 1);
    }
 
    mipidev.id = (uint16_t)(mipi_id[0] << 8) | mipi_id[1];
    ESP_LOGI(mipi_lcd_tag, "mipilcd_id:%#x ", mipidev.id);  /* 打印LCD的ID */
 
ESP_ERROR_CHECK(esp_lcd_panel_init(mipi_lcd_ctrl_panel)); /* 初始化LCD屏 */
    ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(mipi_lcd_ctrl_panel, true));  
 
    if (mipidev.id == 0x8394)     /* 5.5寸720P屏幕 */
    {
        mipidev.pwidth = 720;    /* 面板宽度,单位:像素 */
        mipidev.pheight = 1280;     /* 面板高度,单位:像素 */
        mipidev.hbp = 52;          /* 水平后廊 */
        mipidev.hfp = 48;         /* 水平前廊 */
        mipidev.hsw = 8;            /* 水平同步宽度 */
        mipidev.vbp = 15;           /* 垂直后廊 */
        mipidev.vfp = 16;          /* 垂直前廊 */
        mipidev.vsw = 5;          /* 垂直同步宽度 */
        mipidev.pclk_mhz = 60;      /* 设置像素时钟 60Mhz */
        mipidev.dir = 0;         /* 只能竖屏 */
    }
    else if (mipidev.id == 0x8399)  /* 5.5寸1080P屏幕 */
    {
        mipidev.pwidth = 1080;         /* 面板宽度,单位:像素 */
        mipidev.pheight = 1920;       /* 面板高度,单位:像素 */
        mipidev.hbp = 22;            /* 水平后廊 */
        mipidev.hfp = 22;              /* 水平前廊 */
        mipidev.hsw = 20;              /* 水平同步宽度 */
        mipidev.vbp = 9;               /* 垂直后廊 */
        mipidev.vfp = 7;               /* 垂直前廊 */
        mipidev.vsw = 7;               /* 垂直同步宽度 */
        mipidev.pclk_mhz = 60;       /* 设置像素时钟 60Mhz */
        mipidev.dir = 0;              /* 只能竖屏 */
    }
    else if (mipidev.id == 0x9881)  /* 10.1寸800P屏幕 */
    {
        mipidev.pwidth = 800;       /* 面板宽度,单位:像素 */
        mipidev.pheight = 1280;        /* 面板高度,单位:像素 */
        mipidev.hbp = 24;              /* 水平后廊 */
        mipidev.hfp = 15;              /* 水平前廊 */
        mipidev.hsw = 24;              /* 水平同步宽度 */
        mipidev.vbp = 9;               /* 垂直后廊 */
        mipidev.vfp = 7;               /* 垂直前廊 */
        mipidev.vsw = 2;              /* 垂直同步宽度 */
        mipidev.pclk_mhz = 60;         /* 设置像素时钟 60Mhz */
        mipidev.dir = 0;              /* 支持横/竖屏,默认为竖屏 */
    }
 
    lcddev.id = mipidev.id;                                 /* LCD_ID */
    lcddev.width = mipidev.pwidth;                          /* 宽度 */
    lcddev.height = mipidev.pheight;                        /* 高度 */
 
    esp_lcd_panel_handle_t mipi_dpi_panel;                  /* MIPILCD控制句柄 */
    esp_lcd_dpi_panel_config_t dpi_config = {               /* DSI数据配置 */
        .virtual_channel    = 0,                            /* 虚拟通道 */
        .dpi_clk_src        = MIPI_DSI_DPI_CLK_SRC_DEFAULT, /* 时钟源 */
        .dpi_clock_freq_mhz = mipidev.pclk_mhz,             /* 像素时钟频率 */
        .pixel_format       = LCD_COLOR_PIXEL_FORMAT_RGB565,/* 颜色格式 */
        .num_fbs            = 2,                            /* 帧缓冲区数量 */
        .video_timing       = {                             /* 面板特定时序参数 */
            .h_size            = mipidev.pwidth,            /* 水平分辨率 */
            .v_size            = mipidev.pheight,           /* 垂直分辨率 */
            .hsync_back_porch  = mipidev.hbp,               /* 水平后廊 */
            .hsync_pulse_width = mipidev.hsw,               /* 水平同步宽度 */
            .hsync_front_porch = mipidev.hfp,               /* 水平前廊 */
            .vsync_back_porch  = mipidev.vbp,               /* 垂直后廊 */
            .vsync_pulse_width = mipidev.vsw,               /* 垂直同步宽度 */
            .vsync_front_porch = mipidev.vfp,               /* 垂直前廊 */
        },
    };
ESP_ERROR_CHECK(esp_lcd_new_panel_dpi(mipi_dsi_bus, &dpi_config, 
&mipi_dpi_panel)); /* 为MIPI DSI DPI接口创建LCD控制句柄 */
    /* 初始化MIPILCD */
ESP_ERROR_CHECK(esp_lcd_panel_init(mipi_dpi_panel));                                    
    return mipi_dpi_panel;
}

       该函数就是对MIPILCD进行初始化,整个过程就围绕着22.3.1小节中描述的驱动流程实现。通过esp_lcd_new_dsi_bus函数和esp_lcd_new_panel_io_dbi函数初始化DBI接口设备;通过mipi_lcd_new_panel函数创建MIPI设备;后续通过LCD通用API接口对LCD进行配置,比如esp_lcd_panel_reset复位LCD屏,esp_lcd_panel_init初始化LCD屏,esp_lcd_panel_disp_on_off打开显示;最终还得通过esp_lcd_new_panel_dpi函数为DPI接口创建LCD面板,申请到对应的缓冲区即GRAM,后面还需要重新调用esp_lcd_panel_init再次初始化LCD屏。函数的返回值是esp_lcd_panel_handle_t类型的句柄,便于后续对LCD屏幕进行操作。函数中还涉及到配置DSI总线的电压,通过调用mipi_dev_bsp_enable_dsi_phy_power函数设置LDO_VO3输出1.8V给到MIPI_DSI总线。

       下面介绍在mipi_lcd.h文件定义的两个重要结构体:

/* MIPI LCD重要参数集 */
typedef struct  
{
    uint16_t id;                    /* 720p/800p/1080p */
    uint32_t pwidth;                /* MIPI面板的宽度,固定参数,不随显示方向改变 */
    uint32_t pheight;               /* MIPI面板的高度,固定参数,不随显示方向改变 */
    uint16_t hsw;                   /* 水平同步宽度 */
    uint16_t vsw;                   /* 垂直同步宽度 */
    uint16_t hbp;                   /* 水平后廊 */
    uint16_t vbp;                   /* 垂直后廊 */
    uint16_t hfp;                   /* 水平前廊 */
    uint16_t vfp;                   /* 垂直前廊  */
    uint8_t dir;                    /* 0,竖屏;1,横屏; */
    uint32_t pclk_mhz;              /* 设置像素时钟 */
} _mipilcd_dev; 
 
/* 初始化屏幕结构体 */
typedef struct {
    esp_lcd_panel_t base;           /* LCD设备的基础接口函数 */
    esp_lcd_panel_io_handle_t io;   /* LCD设备的IO接口函数配置 */
    int reset_gpio_num;             /* 复位管脚 */
    int x_gap;                      /* x偏移 */
    int y_gap;                      /* y偏移 */
    uint8_t madctl_val;             /* 保存LCD CMD MADCTL寄存器的当前值 */
    uint8_t colmod_val;             /* 保存LCD_CMD_COLMOD寄存器的当前值 */
    uint16_t init_cmds_size;        /* 初始化序列大小 */
    bool reset_level;               /* 复位电平 */
} mipi_panel_t;

       _mipilcd_dev结构体用于保存一些MIPILCD重要参数信息,比如MIPILCD的ID、MIPILCD的长宽、MIPILCD的时序参数等。最后声明_mipilcd_dev结构体类型变量mipilcd_dev,mipilcd_dev在mipi_lcd.c中定义。

       _mipi_panel_t结构体用来保存MIPILCD相关的一些重要信息,比如LCD设备的基础接口结构体、LCD设备的IO接口结构体、复位引脚、初始化序列等。

       在lcd.h文件中也定义了一个重要参数结构体_lcd_dev,在RGBLCD实验章节中介绍过,这里就不再展开。

       下面我们再解析lcd.c的程序,看一下初始化函数lcd_init,代码如下:

/**
 * @brief      初始化LCD
 * @param      无
 * @retval      无
 */
void lcd_init(void)
{
    lcddev.ctrl.lcd_rst = LCD_RST_PIN;                          /* 复位管脚 */
    lcddev.ctrl.lcd_bl = LCD_BL_PIN;                            /* 背光管脚 */
 
    gpio_config_t gpio_init_struct = {0};
    gpio_init_struct.intr_type    = GPIO_INTR_DISABLE;          /* 失能引脚中断 */
    gpio_init_struct.mode         = GPIO_MODE_OUTPUT;           /* 输出模式 */
    gpio_init_struct.pull_up_en   = GPIO_PULLUP_DISABLE;        /* 失能上拉 */
    gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;      /* 失能下拉 */
    gpio_init_struct.pin_bit_mask = 1ull << lcddev.ctrl.lcd_bl; /* 设置的引脚 */
    ESP_ERROR_CHECK(gpio_config(&gpio_init_struct));            /* 配置GPIO */
 
    LCD_BL(0);      /* 背光关闭 */
 
    lcddev.lcd_panel_handle = mipi_lcd_init();     /* 初始化MIPI LCD */
ESP_ERROR_CHECK(esp_lcd_dpi_panel_get_frame_buffer(lcddev.lcd_panel_handle,
2, &lcd_buffer[0], &lcd_buffer[1]));  /* 获取帧缓冲区 */
    
const esp_lcd_dpi_panel_event_callbacks_t mipi_cbs = {
/* 内部缓冲区刷新完成回调函数 */
        .on_refresh_done = lcd_panel_refresh_done_callback,     
    };
esp_lcd_dpi_panel_register_event_callbacks(lcddev.lcd_panel_handle, 
&mipi_cbs, NULL);
    lcd_clear(WHITE);
    LCD_BL(1);      /* 打开背光 */
}

       在lcd_init函数中,首先是对LCD背光控制引脚配置,然后调用mipi_lcd_init函数初始化MIPILCD,后面直接通过esp_lcd_dpi_panel_get_frame_buffer函数接口把数据获取到。为了防止屏幕撕裂,这里还注册了刷新完成回调函数,在进行清屏函数中,需要等待一帧刷新完成,再进行下一帧刷新。最后调用lcd_clear函数清屏,拉高背光控制引脚,打开背光。

       接下来,再看看这个回调函数里面的实现,如下代码所示。

/**
 * @brief     内部缓存刷新完成回调函数
 * @param    panel_io: LCD IO的句柄
 * @param      edata: 事件数据类型
 * @param     user_ctx: 传入参数
 * @retval    无
 */
IRAM_ATTR static bool lcd_panel_refresh_done_callback(esp_lcd_panel_handle_t panel_io, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx)
{
    refresh_done_flag = 1;
    return false;
}

       当发送一帧完成之后,就回进入到回调函数中,把refresh_done_flag标志置1。

       lcd.c的其他函数与RGBLCD例程中的lcd.c是一致的,请大家自行查看源码,都有详细的注释。

       13_lcd例程则是有对RGBLCD和MIPILCD的兼容,大家也可以自行学习查看。


       2. CMakeLists.txt文件

       本例程的功能实现主要依靠LCD驱动。要在main函数中,成功调用LCD文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:

set(src_dirs
            LED
LCD)
 
set(include_dirs
            LED
LCD)
 
set(requires
           driver
esp_lcd
esp_common)
 
idf_component_register( SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
 
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)


       3. main.c驱动代码

       在main.c里面编写如下代码。

void app_main(void)
{
    esp_err_t ret;
    uint8_t x = 0;
    
    ret = nvs_flash_init();     /* 初始化NVS */
    if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ESP_ERROR_CHECK(nvs_flash_init());
    }
 
    led_init();        /* LED初始化 */
    lcd_init();        /* LCD屏初始化 */
 
    while (1)
    {
        switch (x)
        {
            case 0:
            {
                lcd_clear(WHITE);
                break;
            }
            case 1:
            {
                lcd_clear(BLACK);
                break;
            }
            case 2:
            {
                lcd_clear(BLUE);
                break;
            }
            case 3:
            {
                lcd_clear(RED);
                break;
            }
            case 4:
            {
                lcd_clear(MAGENTA);
                break;
            }
            case 5:
            {
                lcd_clear(GREEN);
                break;
            }
            case 6:
            {
                lcd_clear(CYAN);
                break;
            }
            case 7:
            {
                lcd_clear(YELLOW);
                break;
            }
            case 8:
            {
                lcd_clear(BRRED);
                break;
            }
            case 9:
            {
                lcd_clear(GRAY);
                break;
            }
            case 10:
            {
                lcd_clear(LGRAY);
                break;
            }
            case 11:
            {
                lcd_clear(BROWN);
                break;
            }
        }
 
        lcd_show_string(10, 40,  240, 32, 32, "ESP32-P4", RED);
        lcd_show_string(10, 80,  240, 24, 24, "MIPILCD TEST", RED);
        lcd_show_string(10, 110, 240, 16, 16, "ATOM@ALIENTEK", RED);
 
        x++;
 
        if (x == 12)
        {
            x = 0;
        }
 
        LED0_TOGGLE();
 
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

       app_main函数功能主要是显示一些固定的字符,字体大小包括32、24和16三种,然后不停的切换背景颜色,每500毫秒切换一次。而LED0也会不停地闪烁,指示程序已经在运行了。


        22.4 下载验证

       下载代码后,LED0不停地闪烁,提示程序已经在运行了。同时可以看到MIPILCD屏幕模块显示背景色不停切换,如下图所示。


图22.4.1 MIPILCD显示效果图


请使用浏览器的分享功能分享到微信等