本帖最后由 正点原子运营 于 2025-12-26 10:00 编辑
第二十二章 MIPILCD实验
1)实验平台:正点原子DNESP32P4开发板
2)章节摘自【正点原子】ESP32-P4开发指南— V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=873309579825
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32P4.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
本章将继续学习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等参数,因此屏幕真实的水平和垂直像素数时钟如下:
水平:H_total=HPW+HB+HACTIVE+HFP
垂直:V_total=VPW+VB+VACTIVE+VFP
其中,HPW和VPW是水平和垂直同步信号宽度,HB和VB是水平和垂直后廊,HACTIVE是屏幕的水平有效像素,VACTIVE是屏幕的宽度有效像素,HFP和VFP是水平和垂直前廊。屏幕的这些参数都能在屏幕手册里面找到,比如正点原子两款5.5寸MIPI屏幕参数如表22.1.9.1所示:
表22.1.9.1 正点原子MIPI屏幕时序参数
1秒钟的像素数量就是:
Pixel_total=H_total×V_total×fps 其中fps就是屏幕帧率,一般是60帧,也就是1秒钟刷新60张图像,Pixel_total就是1秒钟要传输的总的像素点。
如果屏幕采用RGB888格式,那么1个像素就是24bit,如果是RGB565那就是16bit,这个叫色深,我们一般都使用RGB565。那么总的bit时钟就是:
Bit_total=Pixel_total×色深(比如24或16) 得到的Bit_total是总的bit时钟数,但是MIPI DSI可以配置多条数据lane,一般是2或4lane,也就是有2个或者4个通道,所以每个通道的bit时钟就是:
计算出来的Bit_clk就是MIPI DSI的时钟,但是由于MIPI DSI是双边沿采集,所以最终的DSI CLK时钟还要除以2:
我们以正点原子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功能,需要导入一下头文件:
- <font size="3">#include "esp_lcd_panel_interface.h" /* LCD面板结构体类型 */</font>
- <font size="3">#include "esp_lcd_panel_io.h" /* 驱动芯片接收/发送命令,发送颜色数据等函数 */</font>
- <font size="3">#include "esp_lcd_panel_vendor.h" /* 包含LCD外设驱动支持的几款驱动芯片 */</font>
- <font size="3">#include "esp_lcd_panel_ops.h" /* LCD设备接口函数(reset/init/del等) */</font>
- <font size="3">#include "esp_lcd_panel_commands.h" /* LCD驱动芯片的命令 */</font>
- <font size="3">#include "esp_lcd_mipi_dsi.h" /* MIPILCD的函数 */</font>
复制代码 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函数。
2,为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.cmd,
- <span style="font-style: italic;"><span style="font-style: normal;">init_cmds.data, init_cmds</span><span style="font-style: normal;">.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;
- }</span></span>
复制代码 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,如下代码所示:
- <font size="3">/**</font>
- <font size="3"> * @brief mipi_lcd初始化</font>
- <font size="3"> * [url=home.php?mod=space&uid=271674]@param[/url] 无</font>
- <font size="3"> * @retval LCD控制句柄</font>
- <font size="3"> */</font>
- <font size="3">esp_lcd_panel_handle_t mipi_lcd_init(void)</font>
- <font size="3">{</font>
- <font size="3"> mipi_dev_bsp_enable_dsi_phy_power(); /* 配置MIPI接口电压1.8V */</font>
- <font size="3"> /* 创建DSI总线 */</font>
- <font size="3"> esp_lcd_dsi_bus_handle_t mipi_dsi_bus;</font>
- <font size="3"> esp_lcd_dsi_bus_config_t bus_config = {</font>
- <font size="3"> .bus_id = 0, /* 总线ID */</font>
- <font size="3"> .num_data_lanes = MIPI_DSI_LANE_NUM, /* 2路数据信号 */</font>
- <font size="3"> .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, /* DPHY时钟源为20M */</font>
- <font size="3"> .lane_bit_rate_mbps = MIPI_DSI_LANE_BITRATE_MBPS, /* 数据通道比特率 */</font>
- <font size="3">};</font>
- <font size="3">/* 新建DSI总线 */</font>
- <font size="3"> ESP_ERROR_CHECK(esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus)); </font>
- <font size="3"> /* 配置DSI总线的DBI接口 */</font>
- <font size="3"> esp_lcd_panel_io_handle_t mipi_dbi_io;</font>
- <font size="3"> esp_lcd_dbi_io_config_t dbi_config = {</font>
- <font size="3"> .virtual_channel = 0, /* 虚拟通道(只有一个LCD连接,设置0即可) */</font>
- <font size="3"> .lcd_cmd_bits = 8, /* 根据MIPI LCD驱动IC规格设置 命令位宽度 */</font>
- <font size="3"> .lcd_param_bits = 8, /* 根据MIPI LCD驱动IC规格设置 参数位宽度 */</font>
- <font size="3"> };</font>
- <font size="3">ESP_ERROR_CHECK(esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, </font>
- <font size="3">&mipi_dbi_io));</font>
- <font size="3"> /* 创建LCD控制器驱动 */</font>
- <font size="3"> esp_lcd_panel_handle_t mipi_lcd_ctrl_panel; /* MIPI控制句柄 */</font>
- <font size="3"> esp_lcd_panel_dev_config_t lcd_dev_config = {</font>
- <font size="3"> .bits_per_pixel = 16, /* MIPILCD的像素位宽度 */</font>
- <font size="3"> .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, /* 像素数据的RGB元素顺序 */</font>
- <font size="3"> .reset_gpio_num = lcddev.ctrl.lcd_rst, /* MIPILCD屏的复位引脚 */</font>
- <font size="3"> };</font>
- <font size="3">ESP_ERROR_CHECK(mipi_lcd_new_panel(mipi_dbi_io, &lcd_dev_config, </font>
- <font size="3">&mipi_lcd_ctrl_panel));</font>
- <font size="3"> ESP_ERROR_CHECK(esp_lcd_panel_reset(mipi_lcd_ctrl_panel)); /* 复位LCD屏 */</font>
- <font size="3"> /* 读取屏幕ID */</font>
- <font size="3"> esp_lcd_panel_io_rx_param(mipi_dbi_io, 0xDA, &mipi_id[0], 1);</font>
- <font size="3"> vTaskDelay(pdMS_TO_TICKS(20));</font>
- <font size="3"> esp_lcd_panel_io_rx_param(mipi_dbi_io, 0xDB, &mipi_id[1], 1);</font>
- <font size="3"> vTaskDelay(pdMS_TO_TICKS(20));</font>
- <font size="3"> /* 不是HX8399和HX8394 */</font>
- <font size="3"> if (mipi_id[0] == 0x00 || mipi_id[1] == 0x00)</font>
- <font size="3"> {</font>
- <font size="3"> /* 读取ILI9881 ID */</font>
- <font size="3"> esp_lcd_panel_io_tx_param(mipi_dbi_io, ILI9881C_CMD_CNDBKxSEL,</font>
- <font size="3"> (uint8_t[]) {</font>
- <font size="3"> ILI9881C_CMD_BKxSEL_BYTE0, ILI9881C_CMD_BKxSEL_BYTE1,</font>
- <font size="3"> ILI9881C_CMD_BKxSEL_BYTE2_PAGE1</font>
- <font size="3"> }, 3);</font>
- <font size="3"> esp_lcd_panel_io_rx_param(mipi_dbi_io, 0x00, &mipi_id[0], 1);</font>
- <font size="3"> esp_lcd_panel_io_rx_param(mipi_dbi_io, 0x01, &mipi_id[1], 1);</font>
- <font size="3"> }</font>
- <font size="3"> mipidev.id = (uint16_t)(mipi_id[0] << 8) | mipi_id[1];</font>
- <font size="3"> ESP_LOGI(mipi_lcd_tag, "mipilcd_id:%#x ", mipidev.id); /* 打印LCD的ID */</font>
- <font size="3">ESP_ERROR_CHECK(esp_lcd_panel_init(mipi_lcd_ctrl_panel)); /* 初始化LCD屏 */</font>
- <font size="3"> ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(mipi_lcd_ctrl_panel, true)); </font>
- <font size="3"> if (mipidev.id == 0x8394) /* 5.5寸720P屏幕 */</font>
- <font size="3"> {</font>
- <font size="3"> mipidev.pwidth = 720; /* 面板宽度,单位:像素 */</font>
- <font size="3"> mipidev.pheight = 1280; /* 面板高度,单位:像素 */</font>
- <font size="3"> mipidev.hbp = 52; /* 水平后廊 */</font>
- <font size="3"> mipidev.hfp = 48; /* 水平前廊 */</font>
- <font size="3"> mipidev.hsw = 8; /* 水平同步宽度 */</font>
- <font size="3"> mipidev.vbp = 15; /* 垂直后廊 */</font>
- <font size="3"> mipidev.vfp = 16; /* 垂直前廊 */</font>
- <font size="3"> mipidev.vsw = 5; /* 垂直同步宽度 */</font>
- <font size="3"> mipidev.pclk_mhz = 60; /* 设置像素时钟 60Mhz */</font>
- <font size="3"> mipidev.dir = 0; /* 只能竖屏 */</font>
- <font size="3"> }</font>
- <font size="3"> else if (mipidev.id == 0x8399) /* 5.5寸1080P屏幕 */</font>
- <font size="3"> {</font>
- <font size="3"> mipidev.pwidth = 1080; /* 面板宽度,单位:像素 */</font>
- <font size="3"> mipidev.pheight = 1920; /* 面板高度,单位:像素 */</font>
- <font size="3"> mipidev.hbp = 22; /* 水平后廊 */</font>
- <font size="3"> mipidev.hfp = 22; /* 水平前廊 */</font>
- <font size="3"> mipidev.hsw = 20; /* 水平同步宽度 */</font>
- <font size="3"> mipidev.vbp = 9; /* 垂直后廊 */</font>
- <font size="3"> mipidev.vfp = 7; /* 垂直前廊 */</font>
- <font size="3"> mipidev.vsw = 7; /* 垂直同步宽度 */</font>
- <font size="3"> mipidev.pclk_mhz = 60; /* 设置像素时钟 60Mhz */</font>
- <font size="3"> mipidev.dir = 0; /* 只能竖屏 */</font>
- <font size="3"> }</font>
- <font size="3"> else if (mipidev.id == 0x9881) /* 10.1寸800P屏幕 */</font>
- <font size="3"> {</font>
- <font size="3"> mipidev.pwidth = 800; /* 面板宽度,单位:像素 */</font>
- <font size="3"> mipidev.pheight = 1280; /* 面板高度,单位:像素 */</font>
- <font size="3"> mipidev.hbp = 24; /* 水平后廊 */</font>
- <font size="3"> mipidev.hfp = 15; /* 水平前廊 */</font>
- <font size="3"> mipidev.hsw = 24; /* 水平同步宽度 */</font>
- <font size="3"> mipidev.vbp = 9; /* 垂直后廊 */</font>
- <font size="3"> mipidev.vfp = 7; /* 垂直前廊 */</font>
- <font size="3"> mipidev.vsw = 2; /* 垂直同步宽度 */</font>
- <font size="3"> mipidev.pclk_mhz = 60; /* 设置像素时钟 60Mhz */</font>
- <font size="3"> mipidev.dir = 0; /* 支持横/竖屏,默认为竖屏 */</font>
- <font size="3"> }</font>
- <font size="3"> lcddev.id = mipidev.id; /* LCD_ID */</font>
- <font size="3"> lcddev.width = mipidev.pwidth; /* 宽度 */</font>
- <font size="3"> lcddev.height = mipidev.pheight; /* 高度 */</font>
- <font size="3"> esp_lcd_panel_handle_t mipi_dpi_panel; /* MIPILCD控制句柄 */</font>
- <font size="3"> esp_lcd_dpi_panel_config_t dpi_config = { /* DSI数据配置 */</font>
- <font size="3"> .virtual_channel = 0, /* 虚拟通道 */</font>
- <font size="3"> .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, /* 时钟源 */</font>
- <font size="3"> .dpi_clock_freq_mhz = mipidev.pclk_mhz, /* 像素时钟频率 */</font>
- <font size="3"> .pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565,/* 颜色格式 */</font>
- <font size="3"> .num_fbs = 2, /* 帧缓冲区数量 */</font>
- <font size="3"> .video_timing = { /* 面板特定时序参数 */</font>
- <font size="3"> .h_size = mipidev.pwidth, /* 水平分辨率 */</font>
- <font size="3"> .v_size = mipidev.pheight, /* 垂直分辨率 */</font>
- <font size="3"> .hsync_back_porch = mipidev.hbp, /* 水平后廊 */</font>
- <font size="3"> .hsync_pulse_width = mipidev.hsw, /* 水平同步宽度 */</font>
- <font size="3"> .hsync_front_porch = mipidev.hfp, /* 水平前廊 */</font>
- <font size="3"> .vsync_back_porch = mipidev.vbp, /* 垂直后廊 */</font>
- <font size="3"> .vsync_pulse_width = mipidev.vsw, /* 垂直同步宽度 */</font>
- <font size="3"> .vsync_front_porch = mipidev.vfp, /* 垂直前廊 */</font>
- <font size="3"> },</font>
- <font size="3"> };</font>
- <font size="3">ESP_ERROR_CHECK(esp_lcd_new_panel_dpi(mipi_dsi_bus, &dpi_config, </font>
- <font size="3">&mipi_dpi_panel)); /* 为MIPI DSI DPI接口创建LCD控制句柄 */</font>
- <font size="3"> /* 初始化MIPILCD */</font>
- <font size="3">ESP_ERROR_CHECK(esp_lcd_panel_init(mipi_dpi_panel)); </font>
- <font size="3"> return mipi_dpi_panel;</font>
- <font size="3">}</font>
复制代码 该函数就是对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文件定义的两个重要结构体:
- <font size="3">/* MIPI LCD重要参数集 */</font>
- <font size="3">typedef struct </font>
- <font size="3">{</font>
- <font size="3"> uint16_t id; /* 720p/800p/1080p */</font>
- <font size="3"> uint32_t pwidth; /* MIPI面板的宽度,固定参数,不随显示方向改变 */</font>
- <font size="3"> uint32_t pheight; /* MIPI面板的高度,固定参数,不随显示方向改变 */</font>
- <font size="3"> uint16_t hsw; /* 水平同步宽度 */</font>
- <font size="3"> uint16_t vsw; /* 垂直同步宽度 */</font>
- <font size="3"> uint16_t hbp; /* 水平后廊 */</font>
- <font size="3"> uint16_t vbp; /* 垂直后廊 */</font>
- <font size="3"> uint16_t hfp; /* 水平前廊 */</font>
- <font size="3"> uint16_t vfp; /* 垂直前廊 */</font>
- <font size="3"> uint8_t dir; /* 0,竖屏;1,横屏; */</font>
- <font size="3"> uint32_t pclk_mhz; /* 设置像素时钟 */</font>
- <font size="3">} _mipilcd_dev; </font>
- <font size="3">/* 初始化屏幕结构体 */</font>
- <font size="3">typedef struct {</font>
- <font size="3"> esp_lcd_panel_t base; /* LCD设备的基础接口函数 */</font>
- <font size="3"> esp_lcd_panel_io_handle_t io; /* LCD设备的IO接口函数配置 */</font>
- <font size="3"> int reset_gpio_num; /* 复位管脚 */</font>
- <font size="3"> int x_gap; /* x偏移 */</font>
- <font size="3"> int y_gap; /* y偏移 */</font>
- <font size="3"> uint8_t madctl_val; /* 保存LCD CMD MADCTL寄存器的当前值 */</font>
- <font size="3"> uint8_t colmod_val; /* 保存LCD_CMD_COLMOD寄存器的当前值 */</font>
- <font size="3"> uint16_t init_cmds_size; /* 初始化序列大小 */</font>
- <font size="3"> bool reset_level; /* 复位电平 */</font>
- <font size="3">} mipi_panel_t;</font>
复制代码 _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,代码如下:
- <font size="3">/**</font>
- <font size="3"> * @brief 初始化LCD</font>
- <font size="3"> * @param 无</font>
- <font size="3"> * @retval 无</font>
- <font size="3"> */</font>
- <font size="3">void lcd_init(void)</font>
- <font size="3">{</font>
- <font size="3"> lcddev.ctrl.lcd_rst = LCD_RST_PIN; /* 复位管脚 */</font>
- <font size="3"> lcddev.ctrl.lcd_bl = LCD_BL_PIN; /* 背光管脚 */</font>
- <font size="3"> gpio_config_t gpio_init_struct = {0};</font>
- <font size="3"> gpio_init_struct.intr_type = GPIO_INTR_DISABLE; /* 失能引脚中断 */</font>
- <font size="3"> gpio_init_struct.mode = GPIO_MODE_OUTPUT; /* 输出模式 */</font>
- <font size="3"> gpio_init_struct.pull_up_en = GPIO_PULLUP_DISABLE; /* 失能上拉 */</font>
- <font size="3"> gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */</font>
- <font size="3"> gpio_init_struct.pin_bit_mask = 1ull << lcddev.ctrl.lcd_bl; /* 设置的引脚 */</font>
- <font size="3"> ESP_ERROR_CHECK(gpio_config(&gpio_init_struct)); /* 配置GPIO */</font>
- <font size="3"> LCD_BL(0); /* 背光关闭 */</font>
- <font size="3"> lcddev.lcd_panel_handle = mipi_lcd_init(); /* 初始化MIPI LCD */</font>
- <font size="3">ESP_ERROR_CHECK(esp_lcd_dpi_panel_get_frame_buffer(lcddev.lcd_panel_handle,</font>
- <font size="3">2, &lcd_buffer[0], &lcd_buffer[1])); /* 获取帧缓冲区 */</font>
- <font size="3"> </font>
- <font size="3">const esp_lcd_dpi_panel_event_callbacks_t mipi_cbs = {</font>
- <font size="3"> /* 内部缓冲区刷新完成回调函数 */</font>
- <font size="3"> .on_refresh_done = lcd_panel_refresh_done_callback, </font>
- <font size="3"> };</font>
- <font size="3">esp_lcd_dpi_panel_register_event_callbacks(lcddev.lcd_panel_handle, </font>
- <font size="3">&mipi_cbs, NULL);</font>
- <font size="3"> lcd_clear(WHITE);</font>
- <font size="3"> LCD_BL(1); /* 打开背光 */</font>
- <font size="3">}</font>
复制代码 在lcd_init函数中,首先是对LCD背光控制引脚配置,然后调用mipi_lcd_init函数初始化MIPILCD,后面直接通过esp_lcd_dpi_panel_get_frame_buffer函数接口把数据获取到。为了防止屏幕撕裂,这里还注册了刷新完成回调函数,在进行清屏函数中,需要等待一帧刷新完成,再进行下一帧刷新。最后调用lcd_clear函数清屏,拉高背光控制引脚,打开背光。
接下来,再看看这个回调函数里面的实现,如下代码所示。
- <font size="3">/**</font>
- <font size="3"> * @brief 内部缓存刷新完成回调函数</font>
- <font size="3"> * @param panel_io: LCD IO的句柄</font>
- <font size="3"> * @param edata: 事件数据类型</font>
- <font size="3"> * @param user_ctx: 传入参数</font>
- <font size="3"> * @retval 无</font>
- <font size="3"> */</font>
- <font size="3">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)</font>
- <font size="3">{</font>
- <font size="3"> refresh_done_flag = 1;</font>
- <font size="3"> return false;</font>
- <font size="3">}</font>
复制代码 当发送一帧完成之后,就回进入到回调函数中,把refresh_done_flag标志置1。
lcd.c的其他函数与RGBLCD例程中的lcd.c是一致的,请大家自行查看源码,都有详细的注释。
13_lcd例程则是有对RGBLCD和MIPILCD的兼容,大家也可以自行学习查看。
2. CMakeLists.txt文件
本例程的功能实现主要依靠LCD驱动。要在main函数中,成功调用LCD文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:
- <font size="3">set(src_dirs</font>
- <font size="3"> LED</font>
- <font size="3">LCD)</font>
- <font size="3">set(include_dirs</font>
- <font size="3"> LED</font>
- <font size="3">LCD)</font>
- <font size="3">set(requires</font>
- <font size="3"> driver</font>
- <font size="3"> esp_lcd</font>
- <font size="3"> esp_common)</font>
- <font size="3">idf_component_register( SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})</font>
- <font size="3">component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)</font>
复制代码 3. main.c驱动代码
在main.c里面编写如下代码。
- <font size="3">void app_main(void)</font>
- <font size="3">{</font>
- <font size="3"> esp_err_t ret;</font>
- <font size="3"> uint8_t x = 0;</font>
- <font size="3"> </font>
- <font size="3"> ret = nvs_flash_init(); /* 初始化NVS */</font>
- <font size="3"> if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_erase());</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_init());</font>
- <font size="3"> }</font>
- <font size="3"> led_init(); /* LED初始化 */</font>
- <font size="3"> lcd_init(); /* LCD屏初始化 */</font>
- <font size="3"> while (1)</font>
- <font size="3"> {</font>
- <font size="3"> switch (x)</font>
- <font size="3"> {</font>
- <font size="3"> case 0:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(WHITE);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 1:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(BLACK);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 2:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(BLUE);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 3:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(RED);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 4:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(MAGENTA);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 5:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(GREEN);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 6:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(CYAN);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 7:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(YELLOW);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 8:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(BRRED);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 9:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(GRAY);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 10:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(LGRAY);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> case 11:</font>
- <font size="3"> {</font>
- <font size="3"> lcd_clear(BROWN);</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> }</font>
- <font size="3"> lcd_show_string(10, 40, 240, 32, 32, "ESP32-P4", RED);</font>
- <font size="3"> lcd_show_string(10, 80, 240, 24, 24, "MIPILCD TEST", RED);</font>
- <font size="3"> lcd_show_string(10, 110, 240, 16, 16, "ATOM@ALIENTEK", RED);</font>
- <font size="3"> x++;</font>
- <font size="3"> if (x == 12)</font>
- <font size="3"> {</font>
- <font size="3"> x = 0;</font>
- <font size="3"> }</font>
- <font size="3"> LED0_TOGGLE();</font>
- <font size="3"> vTaskDelay(pdMS_TO_TICKS(500));</font>
- <font size="3"> }</font>
- <font size="3">}</font>
复制代码 app_main函数功能主要是显示一些固定的字符,字体大小包括32、24和16三种,然后不停的切换背景颜色,每500毫秒切换一次。而LED0也会不停地闪烁,指示程序已经在运行了。
22.4 下载验证
下载代码后,LED0不停地闪烁,提示程序已经在运行了。同时可以看到MIPILCD屏幕模块显示背景色不停切换,如下图所示。
图22.4.1 MIPILCD显示效果图 |