MIPI DSI(Mobile Industry Processor Interface Display Serial Interface)是一种广泛应用于移动设备显示屏的接口标准。由MIPI联盟制定,DSI接口旨在提供高效、低功耗的显示屏数据传输解决方案。

本节来就通过学习I.MX RT1170单片机中的MIPI DSI接口例程,来学习MIPI DSI的初始化流程和相关的参数。

1 基础

1.1 MIPI基础

什么是 MIPI?

MIPI(Mobile Industry Processor Interface)是一个行业标准化组织,致力于为移动设备提供标准化接口。MIPI DSI(Display Serial Interface)是MIPI联盟制定的一个显示接口标准,主要用于连接显示屏和处理器,提供高效、低功耗的显示数据传输。

什么是 Lane?

在MIPI DSI中,“lane” 是指用于传输数据或时钟信号的通道。每个 lane 由两条信号线组成,分别为正(P)和负(N)信号线,采用差分信号传输方式。根据功能的不同,lane 可以分为时钟通道(Clock Lane)和数据通道(Data Lane)。

MIPI DSI 的 Lane 类型

1、Clock Lane(时钟通道)

  • 组成:由两个引脚组成,分别为 MIPI_DSI_CLKPMIPI_DSI_CLKN
  • 功能:传输时钟信号,用于同步数据传输。所有的数据传输都基于这个时钟信号进行同步。

2、Data Lanes(数据通道)

  • 组成:每个数据通道由两个引脚组成,例如 MIPI_DSI_DP0MIPI_DSI_DN0
  • 功能:传输显示数据。MIPI DSI 接口可以支持多条数据通道,以增加数据带宽。

数据通道数量

MIPI DSI 接口可以配置为1到4条数据通道,具体数量取决于所需的数据带宽和显示器的要求。每条数据通道都是一对差分信号线,包括正极(DP)和负极(DN)信号线。

  • 1-Lane 配置:一条数据通道(DP0 和 DN0),适用于带宽需求较低的应用。
  • 2-Lane 配置:两条数据通道(DP0, DN0 和 DP1, DN1),提供更高的数据带宽,适用于中等带宽需求的应用。
  • 4-Lane 配置:四条数据通道(DP0, DN0, DP1, DN1, DP2, DN2 和 DP3, DN3),提供最高的数据带宽,适用于高分辨率和高刷新率的显示器。

差分信号传输

MIPI DSI 使用差分信号传输(Differential Signaling)来提高抗干扰能力和信号完整性。每对正负信号线(如CLP和CLN、DP和DN)一起工作,传输相同的数据,但极性相反。这种方式有以下优点:

  • 抗干扰能力强:对外界电磁干扰不敏感。
  • 信号完整性好:在高速数据传输时保持较高的信号质量。

MIPI DSI 协议栈

MIPI DSI 协议栈分为以下几个层次:

  • 物理层(PHY):负责信号的物理传输,包括差分信号线。
  • 数据链路层(Data Link Layer):负责数据帧的封装和解封装。
  • 协议层(Protocol Layer):处理高层协议数据,如命令和显示内容。

应用场景

MIPI DSI 广泛应用于移动设备(如智能手机、平板电脑、智能手表)以及其他需要高分辨率显示屏的设备中。它提供高效、低功耗的数据传输,满足现代设备对高质量图像显示的需求。

通过理解这些基础知识,可以更好地设计和调试使用 MIPI DSI 接口的显示系统,以实现高性能和低功耗的显示解决方案。

1.2 像素时钟频率

像素时钟频率是显示控制器向显示屏发送每个像素数据的速率。它是以下几个参数的乘积:

  • Height:显示屏的垂直分辨率(像素高度)。
  • VSW(Vertical Sync Width):垂直同步脉冲宽度。
  • VFP(Vertical Front Porch):垂直前沿空白时间。
  • VBP(Vertical Back Porch):垂直后沿空白时间。
  • Width:显示屏的水平分辨率(像素宽度)。
  • HSW(Horizontal Sync Width):水平同步脉冲宽度。
  • HFP(Horizontal Front Porch):水平前沿空白时间。
  • HBP(Horizontal Back Porch):水平后沿空白时间。
  • Frame Rate:帧率(每秒显示的帧数)。

公式为:
Pixel Clock = ( Height + VSW + VFP + VBP ) × ( Width + HSW + HFP + HBP ) × Frame Rate \text{Pixel Clock} = (\text{Height} + \text{VSW} + \text{VFP} + \text{VBP}) \times (\text{Width} + \text{HSW} + \text{HFP} + \text{HBP}) \times \text{Frame Rate} Pixel Clock=(Height+VSW+VFP+VBP)×(Width+HSW+HFP+HBP)×Frame Rate

1.3 DHPY

MIPI D-PHY(Display Physical Interface)是由MIPI联盟(Mobile Industry Processor Interface Alliance)定义的一种物理层接口标准,广泛应用于移动设备的显示和摄像头模块中。它旨在提供高速和低功耗的数据传输解决方案,支持各种高分辨率显示和摄像头传感器。

D-PHY 的主要特点

高数据速率

  • 支持高速(High-Speed, HS)模式,数据速率可以达到几百Mbps到几Gbps。
  • 每条数据通道(lane)在HS模式下支持高达2.5Gbps的数据速率,多通道(通常是1到4通道)并行使用可以进一步提高总带宽。

低功耗

  • 支持低功耗(Low-Power, LP)模式,数据速率较低,但功耗显著减少。
  • LP模式通常用于传输控制信号和低速数据,数据速率通常在几Mbps到几十Mbps。

差分信号传输

  • 使用差分信号传输,提高抗干扰能力和信号完整性,适用于高噪声环境。

双向通信

  • 支持双向通信,既可以用于显示数据传输(如MIPI DSI),也可以用于摄像头数据传输(如MIPI CSI-2)。

D-PHY 的工作模式

高速模式(High-Speed Mode, HS Mode)

  • 用于高数据速率传输。
  • 差分对信号,每对信号线在高数据速率下同时传输。

低功耗模式(Low-Power Mode, LP Mode)

  • 用于低数据速率传输和控制信号传输。
  • 单端信号传输方式,用于发送低速数据和控制命令。

D-PHY 的组成部分

数据通道(Data Lanes)

  • 每条数据通道由两条差分信号线组成,用于传输数据。
  • 数据通道的数量可以是1到4条,具体数量取决于应用需求。

时钟通道(Clock Lane)

  • 一条独立的差分信号线,提供数据传输的同步时钟信号。
  • 时钟通道在HS模式下用于同步数据通道的传输。

锁相环(PLL)

  • 在HS模式下,D-PHY 使用锁相环(PLL)生成高频时钟信号,确保数据传输的时序精度。

D-PHY 的工作流程

初始化

  • 初始化时,D-PHY 模块配置为LP模式,进行基本配置和控制命令传输。

切换到HS模式

  • 当需要进行高数据速率传输时,D-PHY 从LP模式切换到HS模式,启用差分对信号传输。

数据传输

  • 在HS模式下,数据通过差分对信号进行高速传输。
  • 数据传输完成后,可以根据需要切换回LP模式,减少功耗。

2 引脚初始化

2.1 MIPI专用引脚

1. 时钟引脚(Clock Lanes)MIPI_DSI_CLKPMIPI_DSI_CLKN

  • 功能:传输时钟信号,用于数据传输的同步。
  • 特性:差分信号对,提供高抗干扰能力和信号完整性,确保数据传输时序准确。

2. 数据通道0(Data Lane 0)MIPI_DSI_DP0MIPI_DSI_DN0

  • 功能:传输显示数据。
  • 特性:差分信号对,提供稳定的数据传输,主要用于基础数据流。

3. 数据通道1(Data Lane 1)MIPI_DSI_DP1MIPI_DSI_DN1

  • 功能:传输额外的显示数据,增加数据带宽或处理不同数据流。
  • 特性:差分信号对,确保数据传输的稳定性和可靠性。

在RT1170中,下列几个引脚固定用于MIPI DSI,也没有其它复用的引脚。所以引脚不用做初始化,默认已经初始化好了。

在这里插入图片描述

2.2 其它引脚

  • 复位引脚:对于不同的MIPI屏幕厂家,屏幕一般还有一个复位引脚,在上电后需要复位一下。这个初始化为输出即可。
  • 屏幕背光:基于不同的硬件设计,屏幕背光的电源可能由一个引脚控制开启/关闭,否则如果不用屏幕的话是很耗电的

3 代码执行流程

3.1 复位显示混合模块

在RT的SDK中,第一步都是执行这个函数BOARD_ResetDisplayMix

static void BOARD_ResetDisplayMix(void)
{
    /*
     * Reset the displaymix, otherwise during debugging, the
     * debugger may not reset the display, then the behavior
     * is not right.
     */
    SRC_AssertSliceSoftwareReset(SRC, kSRC_DisplaySlice);
    while (kSRC_SliceResetInProcess == SRC_GetSliceResetState(SRC, kSRC_DisplaySlice))
    {
    }
}

3.2 外设初始化

MIPI DSI初始化的第一步是时钟配置。时钟配置是确保数据传输的同步性和正确性的基础。

3.2.1 时钟

我们要将图像显示到屏幕中的话,就需要LCD控制器,那这里我们使用的是RT1170中的LCDIFV2。所以对于这个外设,我们也要给它初始化时钟。

首先需要检查一下时钟源是否正常,下面函数用于验证显示接口时钟源是否有效,具体是验证SYSPLL2(528M)是否作为LCDIFV2像素时钟和MIPI DSI ESC时钟的源。

  • 这是RT中特殊设计的特殊配置,即必须用这个时钟源,不用过多深究。
status_t BOARD_VerifyDisplayClockSource(void)
{
    status_t status;
    uint32_t srcClkFreq;

    /*
     * In this implementation, the SYSPLL2 (528M) clock is used as the source
     * of LCDIFV2 pixel clock and MIPI DSI ESC clock. The OSC24M clock is used
     * as the MIPI DSI DPHY PLL reference clock. This function checks the clock
     * source are valid. OSC24M is always valid, so only verify the SYSPLL2.
     */
    srcClkFreq = CLOCK_GetPllFreq(kCLOCK_PllSys2);
    if (528 != (srcClkFreq / 1000000))
    {
        status = kStatus_Fail;
    }
    else
    {
        status = kStatus_Success;
    }
    return status;
}

下面初始化LCDIFV2接口的时钟频率:

static void BOARD_InitLcdifClock(void)
{
    const clock_root_config_t lcdifClockConfig = {
        .clockOff = false,
        .mux      = 4, /*!< PLL_528. */
        .div = 9,
    };
    
    CLOCK_SetRootClock(kCLOCK_Root_Lcdifv2, &lcdifClockConfig);
    mipiDsiDpiClkFreq_Hz = CLOCK_GetRootClockFreq(kCLOCK_Root_Lcdifv2);
}

根据前面的“像素时钟频率”的知识,LCDIFV2的时钟频率应该根据其规范手册计算得出:

  1. Height(垂直分辨率)**和**Width(水平分辨率)
    • 这些参数定义了显示屏的分辨率(例如,1920x1080),通常是固定的,因为它们是显示屏的物理特性。
  2. Frame Rate(帧率)
    • 帧率通常也是固定的,例如60Hz、75Hz、120Hz等,取决于显示屏的设计和应用需求。
  3. VSW(Vertical Sync Width)VFP(Vertical Front Porch)VBP(Vertical Back Porch)
    • 这些参数定义了垂直扫描过程中各种时序的细节。不同的显示屏可能有不同的要求。这些值通常由显示屏的制造商提供,并在显示屏的规格书中列出。
  4. HSW(Horizontal Sync Width)HFP(Horizontal Front Porch)HBP(Horizontal Back Porch)
    • 这些参数定义了水平扫描过程中各种时序的细节,同样不同的显示屏可能有不同的要求,这些值也通常由显示屏的制造商提供。

以注释中的RK055IQH091屏幕为例,它的参数如下:

DEMO_HSW: 6
DEMO_HFP: 12
DEMO_HBP: 24
DEMO_VSW: 2
DEMO_VFP: 16
DEMO_VBP: 14
DEMO_PANEL_WIDTH: 720
DEMO_PANEL_HEIGHT: 1280
Frame Rate: 60Hz

代入前面的公式:
Pixel Clock = ( Height + VSW + VFP + VBP ) × ( Width + HSW + HFP + HBP ) × Frame Rate \text{Pixel Clock} = (\text{Height} + \text{VSW} + \text{VFP} + \text{VBP}) \times (\text{Width} + \text{HSW} + \text{HFP} + \text{HBP}) \times \text{Frame Rate} Pixel Clock=(Height+VSW+VFP+VBP)×(Width+HSW+HFP+HBP)×Frame Rate
得到Pixel Clock为59.89MHz,所以这里用528MHz的PLL时钟9分分频后为58.67MHz。通过系统时钟分频后,不一定能获得与我们预期完全一样的时钟,但这种偏差影响其实也不大,实际上就是影响一下我们自己设置的Frame Rate

3.2.2 LCDIFV2和MIPI DSI初始化

初始化完时钟后,就需要初始化LCDIFV2和MIPI这两个片上外设的配置了,毕竟我们可以接不同参数的显示屏,所以我们就要适配不同的参数:

status_t BOARD_InitDisplayInterface(void)
{
    CLOCK_EnableClock(kCLOCK_Video_Mux);

    /* LCDIF v2 output to MIPI DSI. */
    VIDEO_MUX->VID_MUX_CTRL.SET = VIDEO_MUX_VID_MUX_CTRL_MIPI_DSI_SEL_MASK;

    /* 1. Power on and isolation off. */
    PGMC_BPC4->BPC_POWER_CTRL |= (PGMC_BPC_BPC_POWER_CTRL_PSW_ON_SOFT_MASK | PGMC_BPC_BPC_POWER_CTRL_ISO_OFF_SOFT_MASK);

    /* 2. Assert MIPI reset. */
    IOMUXC_GPR->GPR62 &=
        ~(IOMUXC_GPR_GPR62_MIPI_DSI_PCLK_SOFT_RESET_N_MASK | IOMUXC_GPR_GPR62_MIPI_DSI_ESC_SOFT_RESET_N_MASK |
          IOMUXC_GPR_GPR62_MIPI_DSI_BYTE_SOFT_RESET_N_MASK | IOMUXC_GPR_GPR62_MIPI_DSI_DPI_SOFT_RESET_N_MASK);

    /* 3. Setup clock. */
    BOARD_InitMipiDsiClock();

    /* 4. Deassert PCLK and ESC reset. */
    IOMUXC_GPR->GPR62 |=
        (IOMUXC_GPR_GPR62_MIPI_DSI_PCLK_SOFT_RESET_N_MASK | IOMUXC_GPR_GPR62_MIPI_DSI_ESC_SOFT_RESET_N_MASK);

    /* 5. Configures peripheral. */
    BOARD_SetMipiDsiConfig();

    /* 6. Deassert BYTE and DBI reset. */
    IOMUXC_GPR->GPR62 |=
        (IOMUXC_GPR_GPR62_MIPI_DSI_BYTE_SOFT_RESET_N_MASK | IOMUXC_GPR_GPR62_MIPI_DSI_DPI_SOFT_RESET_N_MASK);

    /* 7. Configure the panel. */
    return BOARD_InitLcdPanel();
}

简单说明一下上面完成的步骤,这些基本上都是RT芯片相关的操作,配置为DSI显示,然后做一些复位:

  1. 使能视频多路复用器的时钟。
  2. 配置视频多路复用器,将LCD接口输出连接到MIPI DSI。
  3. 开启电源并关闭隔离。
  4. 断言MIPI DSI模块的复位信号,确保模块处于复位状态。
  5. 初始化MIPI DSI所需的时钟。
  6. 释放部分MIPI DSI模块的复位信号。
  7. 配置MIPI DSI外围设备。
  8. 释放剩余的MIPI DSI模块复位信号。
  9. 初始化特定的LCD屏幕

这里重点说明一下BOARD_InitMipiDsiClock()BOARD_SetMipiDsiConfig()BOARD_InitLcdPanel()

3.2.2.1 初始化MIPI DSI时钟

BOARD_InitMipiDsiClock函数中完成初始化MIPI DSI的时钟。


首先我们可能有这样一个疑问:前面不是初始化过LCDIFV2时钟了吗,它不是就是MIPI DSI的时钟吗?

尽管前面已经初始化了 LCDIFV2 的时钟,但 MIPI DSI 需要特定的时钟源来满足其不同的功能需求。以下是一些关键点:

不同的时钟用途

  • LCDIFV2 像素时钟:用于驱动显示器的像素刷新频率。
  • MIPI DSI Escape 时钟(RxClkEsc 和 TxClkEsc):Escape 时钟是 MIPI DSI 协议中用于低功耗模式和控制信号传输的时钟。在低功耗模式下,数据传输速率较低,使用 Escape 时钟进行通信。
    • RxClkEsc 和 TxClkEsc 都是 Escape 时钟的一部分,分别用于接收和发送路径的低功耗模式操作。
  • MIPI DSI DPHY 参考时钟:用于 MIPI DPHY 的 PLL 操作。

频率要求不同

  • LCDIFV2 的时钟主要是像素时钟,频率计算基于显示分辨率和刷新率。
  • MIPI DSI 的 Escape 时钟和 DPHY 参考时钟有特定的频率要求,以确保数据传输和接收的稳定性和可靠性。

时钟源和分频设置不同

  • LCDIFV2 和 MIPI DSI 的时钟源可以来自同一个主时钟(如 528MHz PLL),但分频设置不同,以满足各自的频率需求。

现在我们就来看一下BOARD_InitMipiDsiClock函数中初始化MIPI DSI的这些时钟:

1、Escape时钟

首先初始化了 MIPI DSI 的 Escape 时钟:

uint32_t mipiDsiEscClkFreq_Hz;

/* RxClkEsc max 60MHz, TxClkEsc 12 to 20MHz. */
/* RxClkEsc = 528MHz / 11 = 48MHz. */
/* TxClkEsc = 528MHz / 11 / 4 = 16MHz. */
const clock_root_config_t mipiEscClockConfig = {
    .clockOff = false,
    .mux      = 4, /*!< PLL_528. */
    .div      = 11,
};

CLOCK_SetRootClock(kCLOCK_Root_Mipi_Esc, &mipiEscClockConfig);

mipiDsiEscClkFreq_Hz = CLOCK_GetRootClockFreq(kCLOCK_Root_Mipi_Esc);

2、TxClkEsc时钟

  • TxClkEsc的时钟频率范围为12MHz~20MHz(RT1170芯片中限制)

配置TxClkEsc频率为 48MHz / 3 ≈ 16MHz。

const clock_group_config_t mipiEscClockGroupConfig = {
        .clockOff = false, .resetDiv = 2, .div0 = 2, /* TX esc clock. */
};

CLOCK_SetGroupConfig(kCLOCK_Group_MipiDsi, &mipiEscClockGroupConfig);

mipiDsiTxEscClkFreq_Hz = mipiDsiEscClkFreq_Hz / 3;

所以现在可以推测RxClkEsc用的就是kCLOCK_Root_Mipi_Esc时钟,而TxClkEsc用的是由Esc时钟分频后的kCLOCK_Group_MipiDsi时钟。当然我们不能想当然,一定要在手册中找到对应的部分,如下图所示:

在这里插入图片描述

可以看到我们的猜测是对的。


TxClkEsc:12MHz or 16MHz?

上面代码中注释写的是给TxClkEsc四分频,后面mipiDsiTxEscClkFreq_Hz保存的又是三分频,时钟配置里resetDiv*div0又是4分频。应该是SDK中写错了,但这里错了代码依旧可以正常运行。


3、DPHY时钟

DPHY 参考时钟用于 MIPI DSI PHY 层的操作,确保数据传输的稳定性。下面代码设置 DPHY 参考时钟,使用 24MHz 的外部晶振(OSC 24M)作为时钟源,无分频。

/* DPHY reference clock, use OSC 24MHz clock. */
const clock_root_config_t mipiDphyRefClockConfig = {
    .clockOff = false,
    .mux      = 1, /*!< OSC_24M. */
    .div      = 1,
};

CLOCK_SetRootClock(kCLOCK_Root_Mipi_Ref, &mipiDphyRefClockConfig);

mipiDsiDphyRefClkFreq_Hz = BOARD_XTAL0_CLK_HZ;
3.2.2.2 DSI参数配置

初始化DSI需要配置一些参数,主要是屏幕特定的,不同屏幕有不同的配置。主要涉及下面几个结构体的配置:

1、dsi_config_t

/*! @brief MIPI DSI controller configuration. */
typedef struct _dsi_config
{
    uint8_t numLanes;              /*!< Number of lanes. */
    bool enableNonContinuousHsClk; /*!< In enabled, the high speed clock will enter
                                       low power mode between transmissions. */
    bool enableTxUlps;             /*!< Enable the TX ULPS. */
    bool autoInsertEoTp;           /*!< Insert an EoTp short package when switching from HS to LP. */
    uint8_t numExtraEoTp;          /*!< How many extra EoTp to send after the end of a packet. */
    uint32_t htxTo_ByteClk;        /*!< HS TX timeout count (HTX_TO) in byte clock. */
    uint32_t lrxHostTo_ByteClk;    /*!< LP RX host timeout count (LRX-H_TO) in byte clock. */
    uint32_t btaTo_ByteClk;        /*!< Bus turn around timeout count (TA_TO) in byte clock. */
} dsi_config_t;
  • numLanes:MIPI DSI 数据通道(lane)的数量。

    • 用途:指定数据传输使用的数据通道数量,通常为1到4条。
  • enableNonContinuousHsClk:启用非连续高速时钟。

    • 用途:如果启用,在两次数据传输之间,高速时钟将进入低功耗模式。可以减少功耗,但可能会略微增加延迟。
  • enableTxUlps:启用发送端的超低功耗状态(ULPS)。

    • 用途:在不传输数据时,发送端进入超低功耗状态,以节省能耗。
  • autoInsertEoTp:自动插入End-of-Transmission Packet(EoTp)包。

    • 用途:在从高速模式(HS)切换到低功耗模式(LP)时,自动插入EoTp短包,确保传输的结束信号被正确发送。
  • numExtraEoTp:在数据包结束后发送的额外EoTp包数量。

    • 用途:指定在数据包结束后发送的额外EoTp包的数量,增加传输可靠性。
  • htxTo_ByteClk:高速发送超时计数(HTX_TO),单位为字节时钟。

    • 用途:设置在高速模式下,发送数据包的超时时间,以字节时钟为单位。用于防止数据传输异常。
  • lrxHostTo_ByteClk:低功耗接收主机超时计数(LRX-H_TO),单位为字节时钟。

    • 用途:设置在低功耗模式下,接收数据包的超时时间,以字节时钟为单位。确保接收器在预期时间内接收到数据。
  • btaTo_ByteClk:总线转向超时计数(TA_TO),单位为字节时钟。

    • 用途:设置总线转向超时时间,以字节时钟为单位。在双向通信中,确保总线在预期时间内完成从发送到接收的切换。

现在来看一下代码中的配置:

dsi_config_t dsiConfig;
/*
* dsiConfig.numLanes = 4;
* dsiConfig.enableNonContinuousHsClk = false;
* dsiConfig.autoInsertEoTp = true;
* dsiConfig.numExtraEoTp = 0;
* dsiConfig.htxTo_ByteClk = 0;
* dsiConfig.lrxHostTo_ByteClk = 0;
* dsiConfig.btaTo_ByteClk = 0;
*/
DSI_GetDefaultConfig(&dsiConfig);
dsiConfig.numLanes       = DEMO_MIPI_DSI_LANE_NUM;
dsiConfig.autoInsertEoTp = true;

/* Init the DSI module. */
DSI_Init(DEMO_MIPI_DSI, &dsiConfig);

如果MIPI屏幕上有显示画面,但是画面很乱,不是预期想要的显示,可以检查一下这些参数。特别是numLanesenableNonContinuousHsClk等参数。


2、dsi_dpi_config_t

dsi_dpi_config_t 结构体用于配置MIPI DSI控制器的DPI(Display Pixel Interface)接口:

typedef struct _dsi_dpi_config
{
    uint16_t pixelPayloadSize;             /*!< Maximum number of pixels that should be sent
                                              as one DSI packet. Recommended that the line size
                                              (in pixels) is evenly divisible by this parameter. */
    dsi_dpi_color_coding_t dpiColorCoding; /*!< DPI color coding. */
    dsi_dpi_pixel_packet_t pixelPacket;    /*!< Pixel packet format. */

    dsi_dpi_video_mode_t videoMode; /*!< Video mode. */
    dsi_dpi_bllp_mode_t bllpMode;   /*!< Behavior in BLLP. */

    uint8_t polarityFlags; /*!< OR'ed value of _dsi_dpi_polarity_flag controls signal polarity. */
    uint16_t hfp;          /*!< Horizontal front porch, in dpi pixel clock. */
    uint16_t hbp;          /*!< Horizontal back porch, in dpi pixel clock. */
    uint16_t hsw;          /*!< Horizontal sync width, in dpi pixel clock. */
    uint8_t vfp;           /*!< Number of lines in vertical front porch. */
    uint8_t vbp;           /*!< Number of lines in vertical back porch. */
    uint16_t panelHeight;  /*!< Line number in vertical active area. */

    uint8_t virtualChannel; /*!< Virtual channel. */
} dsi_dpi_config_t;
  • pixelPayloadSize::每个DSI数据包中应发送的最大像素数。推荐该参数值可以整除行大小(以像素为单位),以确保有效的数据传输和对齐。

    • 用途:设置数据包的大小,以优化传输效率。
  • dpiColorCoding:DPI的颜色编码方式。

    • 用途:定义像素的颜色格式,如24位RGB、18位RGB等。
  • pixelPacket:像素数据包的格式。

    • 用途:指定每个数据包中的像素格式,如24位像素或18位像素。
  • videoMode:视频模式。

    • 用途:定义视频传输模式,如Burst模式、Non-Burst模式等。
  • bllpMode:BLLP(Blanking Long Packet)期间的行为模式。

    • 用途:定义在长消隐期间(不传输有效图像数据的时间段)的行为,如低功耗模式或空闲模式。
  • polarityFlags:信号极性控制标志的OR值。

    • 用途:设置同步信号的极性,如VSYNC和HSYNC的极性。
  • hfp(Horizontal Front Porch):水平前沿消隐区的像素时钟周期数。

    • 用途:定义从行结束到下一行开始之间的空闲时间。
  • hbp(Horizontal Back Porch):水平后沿消隐区的像素时钟周期数。

    • 用途:定义从同步脉冲结束到有效数据开始之间的空闲时间。
  • hsw(Horizontal Sync Width):水平同步脉冲的宽度,以像素时钟为单位。

    • 用途:定义水平同步信号的持续时间。
  • vfp(Vertical Front Porch):垂直前沿消隐区的行数。

    • 用途:定义从帧结束到下一帧开始之间的空闲行数。
  • vbp(Vertical Back Porch):垂直后沿消隐区的行数。

    • 用途:定义从垂直同步脉冲结束到有效数据开始之间的空闲行数。
  • panelHeight:垂直活动区域的行数。

    • 用途:定义显示面板的高度(以行数计)。
  • virtualChannel:虚拟通道号。

    • 用途:用于在MIPI DSI传输中区分不同的数据流。

相关代码如下:

const dsi_dpi_config_t dpiConfig = {
	.pixelPayloadSize = DEMO_PANEL_WIDTH,
    .dpiColorCoding   = kDSI_Dpi24Bit,
    .pixelPacket      = kDSI_PixelPacket24Bit,
    .videoMode        = kDSI_DpiBurst,
    .bllpMode         = kDSI_DpiBllpLowPower,
    .polarityFlags    = kDSI_DpiVsyncActiveLow | kDSI_DpiHsyncActiveLow,
    .hfp              = DEMO_HFP,
    .hbp              = DEMO_HBP,
    .hsw              = DEMO_HSW,
    .vfp              = DEMO_VFP,
    .vbp              = DEMO_VBP,
    .panelHeight      = DEMO_PANEL_HEIGHT,
    .virtualChannel   = 0};
DSI_SetDpiConfig(DEMO_MIPI_DSI, &dpiConfig, DEMO_MIPI_DSI_LANE_NUM, mipiDsiDpiClkFreq_Hz, mipiDsiDphyBitClkFreq_Hz);

这个函数就不展开了,就是直接根据我们定义的参数设置寄存器。同样地,如果屏幕显示不正常,需要检查一下这里面的参数,特别是pixelPayloadSizepolarityFlags

3、dsi_dphy_config_t

/*! @brief MIPI DSI D-PHY configuration. */
typedef struct _dsi_dphy_config
{
    uint32_t txHsBitClk_Hz; /*!< The generated HS TX bit clock in Hz. */

    uint8_t tClkPre_ByteClk;        /*!< TLPX + TCLK-PREPARE + TCLK-ZERO + TCLK-PRE in byte clock.
                                         Set how long the controller
                                         will wait after enabling clock lane for HS before
                                         enabling data lanes for HS. */
    uint8_t tClkPost_ByteClk;       /*!< TCLK-POST + T_CLK-TRAIL in byte clock. Set how long the controller
                                        will wait before putting clock lane into LP mode after
                                        data lanes detected in stop state. */
    uint8_t tHsExit_ByteClk;        /*!< THS-EXIT in byte clock. Set how long the controller
                                          will wait after the clock lane has been put into LP
                                          mode before enabling clock lane for HS again. */
    uint32_t tWakeup_EscClk;        /*!< Number of clk_esc clock periods to keep a clock
                                         or data lane in Mark-1 state after exiting ULPS. */
    uint8_t tHsPrepare_HalfEscClk;  /*!< THS-PREPARE in clk_esc/2. Set how long
                                      to drive the LP-00 state before HS transmissions,
                                      available values are 2, 3, 4, 5. */
    uint8_t tClkPrepare_HalfEscClk; /*!< TCLK-PREPARE in clk_esc/2. Set how long
                                     to drive the LP-00 state before HS transmissions,
                                     available values are 2, 3. */
    uint8_t tHsZero_ByteClk;        /*!< THS-ZERO in clk_byte. Set how long that controller
                                      drives data lane HS-0 state before transmit
                                      the Sync sequence. Available values are 6, 7, ..., 37. */
    uint8_t tClkZero_ByteClk;       /*!< TCLK-ZERO in clk_byte. Set how long that controller
                                      drives clock lane HS-0 state before transmit
                                      the Sync sequence. Available values are 3, 4, ..., 66. */
    uint8_t tHsTrail_ByteClk;       /*!< THS-TRAIL + 4*UI in clk_byte. Set the time
                                       of the flipped differential state after last payload
                                       data bit of HS transmission burst. Available values
                                       are 0, 1, ..., 15. */
    uint8_t tClkTrail_ByteClk;      /*!< TCLK-TRAIL + 4*UI in clk_byte. Set the time
                                       of the flipped differential state after last payload
                                       data bit of HS transmission burst. Available values
                                       are 0, 1, ..., 15. */
} dsi_dphy_config_t;
  • txHsBitClk_Hz:生成的高速发送位时钟(HS TX Bit Clock),单位为Hz。

    • 用途:配置用于高速数据传输的时钟频率。
  • tClkPre_ByteClk:TLPX + TCLK-PREPARE + TCLK-ZERO + TCLK-PRE 的时间,以字节时钟为单位。

    • 用途:设置时钟通道在高速模式启用后,数据通道启用前的等待时间。
  • tClkPost_ByteClk:TCLK-POST + TCLK-TRAIL 的时间,以字节时钟为单位。

    • 用途:设置数据通道检测到停止状态后,时钟通道进入低功耗模式前的等待时间。
  • tHsExit_ByteClk:THS-EXIT 的时间,以字节时钟为单位。

    • 用途:设置时钟通道进入低功耗模式后,重新启用高速模式前的等待时间。
  • tWakeup_EscClk:退出超低功耗状态(ULPS)后,保持时钟或数据通道在 Mark-1 状态的时间,以 Escape 时钟周期为单位。

    • 用途:确保退出ULPS后有足够的时间保持稳定状态。
  • tHsPrepare_HalfEscClk:THS-PREPARE 的时间,以 Escape 时钟的一半周期(clk_esc/2)为单位。

    • 用途:设置在进行高速传输前,驱动 LP-00 状态的时间。可选值为 2, 3, 4, 5。
  • tClkPrepare_HalfEscClk:TCLK-PREPARE 的时间,以 Escape 时钟的一半周期(clk_esc/2)为单位。

    • 用途:设置在进行高速传输前,驱动 LP-00 状态的时间。可选值为 2, 3。
  • tHsZero_ByteClk:THS-ZERO 的时间,以字节时钟为单位。

    • 用途:设置控制器在发送同步序列前,驱动数据通道 HS-0 状态的时间。可选值为 6 到 37。
  • tClkZero_ByteClk:TCLK-ZERO 的时间,以字节时钟为单位。

    • 用途:设置控制器在发送同步序列前,驱动时钟通道 HS-0 状态的时间。可选值为 3 到 66。
  • tHsTrail_ByteClk:THS-TRAIL + 4*UI 的时间,以字节时钟为单位。

    • 用途:设置在高速传输结束后,驱动翻转差分状态的时间。可选值为 0 到 15。
  • tClkTrail_ByteClk:TCLK-TRAIL + 4*UI 的时间,以字节时钟为单位。

    • 用途:设置在高速传输结束后,驱动翻转差分状态的时间。可选值为 0 到 15。

下面来看看这部分的代码:

计算基本的D-PHY位时钟频率

mipiDsiDphyBitClkFreq_Hz = mipiDsiDpiClkFreq_Hz * (24 / DEMO_MIPI_DSI_LANE_NUM);
  • mipiDsiDpiClkFreq_Hz:DPI(Display Pixel Interface)的像素时钟频率。
  • 24:每个像素使用24位来表示,即每个像素包含24个bit。
  • DEMO_MIPI_DSI_LANE_NUM:MIPI DSI数据通道的数量。

调整位时钟频率以确保足够快

#define DEMO_MIPI_DPHY_BIT_CLK_ENLARGE(origin) (((origin) / 8) * 9)
mipiDsiDphyBitClkFreq_Hz = DEMO_MIPI_DPHY_BIT_CLK_ENLARGE(mipiDsiDphyBitClkFreq_Hz);
  • 确保D-PHY位时钟频率足够快,以便在实际传输中有一定的裕量。

获取D-PHY默认配置

DSI_GetDphyDefaultConfig(&dphyConfig, mipiDsiDphyBitClkFreq_Hz, mipiDsiTxEscClkFreq_Hz);
  • DSI_GetDphyDefaultConfig 函数用于获取D-PHY的默认配置。
  • dphyConfig:用于存储D-PHY配置的结构体。
  • mipiDsiDphyBitClkFreq_Hz:之前计算和调整后的D-PHY位时钟频率。
  • mipiDsiTxEscClkFreq_Hz:Escape模式下的发送时钟频率。

初始化D-PHY

mipiDsiDphyBitClkFreq_Hz = DSI_InitDphy(DEMO_MIPI_DSI, &dphyConfig, mipiDsiDphyRefClkFreq_Hz);
  • DSI_InitDphy 函数用于初始化MIPI DSI的D-PHY。
  • DEMO_MIPI_DSI:MIPI DSI控制器实例。
  • dphyConfig:包含D-PHY配置参数的结构体。
  • mipiDsiDphyRefClkFreq_Hz:D-PHY参考时钟频率。
3.2.2.3 初始化屏幕

最后来看一下BOARD_InitLcdPanel函数:

static status_t BOARD_InitLcdPanel(void)
{
    status_t status;

    const gpio_pin_config_t pinConfig = {kGPIO_DigitalOutput, 0, kGPIO_NoIntmode};

    const display_config_t displayConfig = {
        .resolution   = FSL_VIDEO_RESOLUTION(DEMO_PANEL_WIDTH, DEMO_PANEL_HEIGHT),
        .hsw          = DEMO_HSW,
        .hfp          = DEMO_HFP,
        .hbp          = DEMO_HBP,
        .vsw          = DEMO_VSW,
        .vfp          = DEMO_VFP,
        .vbp          = DEMO_VBP,
        .controlFlags = 0,
        .dsiLanes     = DEMO_MIPI_DSI_LANE_NUM,
    };

    GPIO_PinInit(BOARD_MIPI_PANEL_POWER_GPIO, BOARD_MIPI_PANEL_POWER_PIN, &pinConfig);
    GPIO_PinInit(BOARD_MIPI_PANEL_BL_GPIO, BOARD_MIPI_PANEL_BL_PIN, &pinConfig);
    GPIO_PinInit(BOARD_MIPI_PANEL_RST_GPIO, BOARD_MIPI_PANEL_RST_PIN, &pinConfig);

    status = HX8394_Init(&hx8394Handle, &displayConfig);

    if (status == kStatus_Success)
    {
        GPIO_PinWrite(BOARD_MIPI_PANEL_BL_GPIO, BOARD_MIPI_PANEL_BL_PIN, 1);
    }

    return status;
}

这部分代码就很好理解了,初始化MIPI相关的电源、背光和复位引脚,然后调用HX8394_Init初始化这个型号的MIPI屏幕,最后打开背光引脚,LCD就可以显示图像了。

3.3 屏幕初始化

实际上这里特定分析这个HX8394_Init屏幕的初始化的通用性也不大,一般我们都能从厂商那里拿到这部分的初始化序列。但我们可以学一下通用的初始化步骤:

打开电源并复位

/* Power on. */
resource->pullPowerPin(true);
HX8394_DelayMs(1);

/* Perform reset. */
resource->pullResetPin(false);
HX8394_DelayMs(1);
resource->pullResetPin(true);
HX8394_DelayMs(50U);

初始化命令

使用MIPI_DSI_GenericWrite函数发送命令到DSI设备,长度为4字节。

uint8_t setmipi[7]                = {0xBAU, 0x60U, 0x03U, 0x68U, 0x6BU, 0xB2U, 0xC0U};
status = MIPI_DSI_GenericWrite(dsiDevice, (const uint8_t[]){0xB9U, 0xFFU, 0x83U, 0x94U}, 4);

setmipi[1] |= (config->dsiLanes - 1U);

if (kStatus_Success == status)
{
    status = MIPI_DSI_GenericWrite(dsiDevice, setmipi, 7);
}

发送其他配置命令

这些命令和上面的初始化命令一样,不用特意深究,是屏幕厂商指定的。

if (kStatus_Success == status)
{
    for (i = 0; i < ARRAY_SIZE(s_hx8394Cmds); i++)
    {
        status = MIPI_DSI_GenericWrite(dsiDevice, s_hx8394Cmds[i].cmd, (int32_t)s_hx8394Cmds[i].cmdLen);

        if (kStatus_Success != status)
        {
            break;
        }
    }
}

进入睡眠模式和开启显示

if (kStatus_Success == status)
{
    status = MIPI_DSI_DCS_EnterSleepMode(dsiDevice, false);
}

if (kStatus_Success == status)
{
    HX8394_DelayMs(120U);

    status = MIPI_DSI_DCS_SetDisplayOn(dsiDevice, true);
}

看完代码之后,我们可以深究一下这里出现的几个函数:

3.3.1 MIPI_DSI_GenericWrite

MIPI_DSI_GenericWrite 函数用于通过 MIPI DSI 协议发送数据包。该函数构建了一个传输结构体 dsi_transfer_t,并调用传输函数发送数据。

status_t MIPI_DSI_GenericWrite(mipi_dsi_device_t *device, const uint8_t *txData, int32_t txDataSize)
{
    dsi_transfer_t dsiXfer = {0};

    dsiXfer.virtualChannel = device->virtualChannel;
    dsiXfer.txDataSize     = (uint16_t)txDataSize;
    dsiXfer.txData         = txData;

    if (0 == txDataSize)
    {
        dsiXfer.txDataType = kDSI_TxDataGenShortWrNoParam;
    }
    else if (1 == txDataSize)
    {
        dsiXfer.txDataType = kDSI_TxDataGenShortWrOneParam;
    }
    else if (2 == txDataSize)
    {
        dsiXfer.txDataType = kDSI_TxDataGenShortWrTwoParam;
    }
    else
    {
        dsiXfer.txDataType = kDSI_TxDataGenLongWr;
    }

    return device->xferFunc(&dsiXfer);
}

参数与初始化

  • device:指向 MIPI DSI 设备的指针,包含设备的配置信息。
  • txData:要发送的数据。
  • txDataSize:要发送的数据大小。

数据传输类型选择

根据传输的数据大小,选择不同的传输类型:

  • kDSI_TxDataGenShortWrNoParam:无参数的短写操作。
  • kDSI_TxDataGenShortWrOneParam:一个参数的短写操作。
  • kDSI_TxDataGenShortWrTwoParam:两个参数的短写操作。
  • kDSI_TxDataGenLongWr:长写操作(超过两个字节的数据)。

传输函数调用

  • device->xferFunc(&dsiXfer):调用设备的传输函数,该函数指向 BOARD_DSI_Transfer
static status_t BOARD_DSI_Transfer(dsi_transfer_t *xfer)
{
    return DSI_TransferBlocking(DEMO_MIPI_DSI, xfer);
}

所以再来看一下DSI_TransferBlocking函数:

status_t DSI_TransferBlocking(const MIPI_DSI_Type *base, dsi_transfer_t *xfer)
{
    status_t status;
    uint32_t intFlags1Old;
    uint32_t intFlags2Old;
    uint32_t intFlags1New;
    uint32_t intFlags2New;

    DSI_HOST_APB_PKT_IF_Type *apb = base->apb;

    /* Wait for the APB state idle. */
    while (0U != (apb->PKT_STATUS & (uint32_t)kDSI_ApbNotIdle))
    {
    }

    status = DSI_PrepareApbTransfer(base, xfer);

    if (kStatus_Success == status)
    {
        DSI_SendApbPacket(base);

        /* Make sure the transfer is started. */
        while (true)
        {
            DSI_GetAndClearInterruptStatus(base, &intFlags1Old, &intFlags2Old);

            if (0U != (intFlags1Old & (uint32_t)kDSI_InterruptGroup1ApbNotIdle))
            {
                break;
            }
        }

        /* Wait for transfer finished. */
        while (true)
        {
            /* Transfer completed. */
            if (0U == (apb->PKT_STATUS & (uint32_t)kDSI_ApbNotIdle))
            {
                break;
            }

            /* Time out. */
            if (0U != (base->host->RX_ERROR_STATUS &
                       ((uint32_t)kDSI_RxErrorHtxTo | (uint32_t)kDSI_RxErrorLrxTo | (uint32_t)kDSI_RxErrorBtaTo)))
            {
                status = kStatus_Timeout;
                break;
            }
        }

        DSI_GetAndClearInterruptStatus(base, &intFlags1New, &intFlags2New);

        if (kStatus_Success == status)
        {
            status = DSI_HandleResult(base, intFlags1Old | intFlags1New, intFlags2Old | intFlags2New, xfer);
        }
    }

    return status;
}

大概总结一下函数步骤:

  • 等待APB状态空闲
  • 准备APB传输
  • 发送APB数据包
  • 确保传输开始
  • 等待传输完成
  • 清除中断状态并处理结果

MIPI DSI 协议相关

  • 数据传输类型:根据数据大小选择短写(无参数、一个参数、两个参数)或长写。MIPI DSI协议支持不同类型的数据传输格式。
  • 虚拟通道:MIPI DSI支持多通道传输,通过virtualChannel参数指定。
  • 超时和错误处理:传输过程中,检查并处理可能的超时和错误状态,确保数据传输的可靠性。
  • 中断和状态检查:使用中断和状态寄存器来确认传输的开始和结束,确保传输的完整性和同步性。

3.3.2 MIPI_DSI_DCS_EnterSleepMode

MIPI DSI 协议定义了一组标准的显示命令,称为 DCS。这些命令用于控制显示设备的各种功能,例如进入睡眠模式、退出睡眠模式、设置显示亮度等。

status_t MIPI_DSI_DCS_EnterSleepMode(mipi_dsi_device_t *device, bool enter)
{
    dsi_transfer_t dsiXfer = {0};
    uint8_t txData;

    dsiXfer.virtualChannel = device->virtualChannel;
    dsiXfer.txDataType     = kDSI_TxDataDcsShortWrNoParam;
    dsiXfer.txDataSize     = 1;
    dsiXfer.txData         = &txData;

    if (enter)
    {
        txData = (uint8_t)kMIPI_DCS_EnterSleepMode;
    }
    else
    {
        txData = (uint8_t)kMIPI_DCS_ExitSleepMode;
    }

    return device->xferFunc(&dsiXfer);
}

这里只需要设置kDSI_TxDataDcsShortWrNoParam(表示 DCS 短写命令,无参数),然后写入kMIPI_DCS_EnterSleepMode即可进入睡眠模式。

  • 同理,对于MIPI_DSI_DCS_SetDisplayOn函数来说,写入kMIPI_DCS_SetDisplayOn即可打开显示

3.4 使能中断

最后就使能LCDIFV2的中断,稍后我们分析一下这个中断中完成了什么内容。

NVIC_ClearPendingIRQ(LCDIFv2_IRQn);
NVIC_SetPriority(LCDIFv2_IRQn, 3);
EnableIRQ(LCDIFv2_IRQn);

4 图像的显示过程

现在我们把MIPI的DSI都初始化完了,我们要如何将我们想要的内容显示到LCD上呢?很明显是通过LCDIFV2实现的,在我们初始化完LCDIFV2和MIPI DSI后,这两者就关联起来了。接下来往特定的buffer写入数据然后触发某个操作后,将生成对应的MIPI时序,最终LCD可以显示对应的内容。

4.1 初始化LCDIFV2

继续往下看代码,将执行下面函数初始化LCDIFV2:

status = g_dc.ops->init(&g_dc);

实际上是调用这个函数:

status_t DC_FB_LCDIFV2_Init(const dc_fb_t *dc)
{
    status_t status = kStatus_Success;
    const dc_fb_lcdifv2_config_t *dcConfig;

    lcdifv2_display_config_t lcdifv2Config = {0};

    dc_fb_lcdifv2_handle_t *dcHandle = dc->prvData;

    if (0U == dcHandle->initTimes++)
    {
        dcConfig = (const dc_fb_lcdifv2_config_t *)(dc->config);

        LCDIFV2_DisplayGetDefaultConfig(&lcdifv2Config);

        lcdifv2Config.panelWidth    = dcConfig->width;
        lcdifv2Config.panelHeight   = dcConfig->height;
        lcdifv2Config.hsw           = (uint8_t)dcConfig->hsw;
        lcdifv2Config.hfp           = (uint8_t)dcConfig->hfp;
        lcdifv2Config.hbp           = (uint8_t)dcConfig->hbp;
        lcdifv2Config.vsw           = (uint8_t)dcConfig->vsw;
        lcdifv2Config.vfp           = (uint8_t)dcConfig->vfp;
        lcdifv2Config.vbp           = (uint8_t)dcConfig->vbp;
        lcdifv2Config.polarityFlags = dcConfig->polarityFlags;
        lcdifv2Config.lineOrder     = dcConfig->lineOrder;

        dcHandle->height  = dcConfig->height;
        dcHandle->width   = dcConfig->width;
        dcHandle->lcdifv2 = dcConfig->lcdifv2;
        dcHandle->domain  = dcConfig->domain;
		// 使能时钟,复位
        LCDIFV2_Init(dcHandle->lcdifv2);
		// 设置极性,HSW,HFP等参数
        LCDIFV2_SetDisplayConfig(dcHandle->lcdifv2, &lcdifv2Config);
		// 使能中断
        LCDIFV2_EnableInterrupts(dcHandle->lcdifv2, dcHandle->domain, (uint32_t)kLCDIFV2_VerticalBlankingInterrupt);
		// 使能Display
        LCDIFV2_EnableDisplay(dcHandle->lcdifv2, true);
    }

    return status;
}

这里主要用到dc->config中的参数,那下面那些函数肯定就是初始化这个结构体里的变量了,这个参数实际上是一个默认赋值的变量:

static const dc_fb_lcdifv2_config_t s_dcFbLcdifv2Config = {
    .lcdifv2       = DEMO_LCDIF,
    .width         = DEMO_PANEL_WIDTH,
    .height        = DEMO_PANEL_HEIGHT,
    .hsw           = DEMO_HSW,
    .hfp           = DEMO_HFP,
    .hbp           = DEMO_HBP,
    .vsw           = DEMO_VSW,
    .vfp           = DEMO_VFP,
    .vbp           = DEMO_VBP,
    .polarityFlags = DEMO_LCDIF_POL_FLAGS,
    .lineOrder     = kLCDIFV2_LineOrderRGB,
    .domain = 0,
};
  • 这里的polarityFlagslineOrder要和特定屏幕的极性和图像格式相匹配。
  • domain即LCDIFV2支持多个域,比如CM7可以用domain0,CM4可以用domain1,都可以各自产生中断,实现两个LCD应用

这里说一下这里使能了kLCDIFV2_VerticalBlankingInterrupt垂直消隐中断:在显示屏刷新过程中,从最后一行像素结束到下一帧的第一行像素开始之间的时间段(即从最后面跳到最前面)。

4.3 初始化帧缓冲区参数

在初始化LCDIFV2时,需要配置帧缓冲区的信息,这些信息包括像素格式、宽度、高度、起始位置和步幅(每行字节数)等。这些参数用于告诉LCD控制器如何处理和显示帧缓冲区中的数据。

static dc_fb_info_t fbInfo;
g_dc.ops->getLayerDefaultConfig(&g_dc, 0, &fbInfo);

fbInfo.pixelFormat = kVIDEO_PixelFormatXRGB8888;
fbInfo.width       = DEMO_BUFFER_WIDTH;    //720
fbInfo.height      = DEMO_BUFFER_HEIGHT;   //1280
fbInfo.startX      = DEMO_BUFFER_START_X;  //0
fbInfo.startY      = DEMO_BUFFER_START_Y;  //0
fbInfo.strideBytes = DEMO_BUFFER_WIDTH * sizeof(pixel_t);  //720*32位,即一行的像素个数

g_dc.ops->setLayerConfig(&g_dc, 0, &fbInfo));

像素格式需要和LCD屏幕支持的格式匹配。不同的屏幕可能支持不同的像素格式,一般也支持多种像素格式,需要根据具体屏幕的规格选择合适的格式。这里使用XRGB8888显示图像,即除了RGB之外,还有一个透明参数X,共32位。

再来看一下setLayerConfig函数:

status_t DC_FB_LCDIFV2_SetLayerConfig(const dc_fb_t *dc, uint8_t layer, dc_fb_info_t *fbInfo)
{
    assert(layer < DC_FB_LCDIFV2_MAX_LAYER);

    lcdifv2_buffer_config_t bufferConfig = {0};
    lcdifv2_pixel_format_t pixelFormat;
    LCDIFV2_Type *lcdifv2;
    status_t status;

    dc_fb_lcdifv2_handle_t *dcHandle = (dc_fb_lcdifv2_handle_t *)(dc->prvData);

    lcdifv2 = dcHandle->lcdifv2;
	// 获得LCDIFV2的XRGB888对应的像素参数
    status = DC_FB_LCDIFV2_GetPixelFormat(fbInfo->pixelFormat, &pixelFormat);
    if (kStatus_Success != status)
    {
        return status;
    }
	// 设置LCDIFV2的长,宽,开始像素的XY坐标
    LCDIFV2_SetLayerSize(lcdifv2, layer, fbInfo->width, fbInfo->height);
    LCDIFV2_SetLayerOffset(lcdifv2, layer, fbInfo->startX, fbInfo->startY);

    bufferConfig.strideBytes = fbInfo->strideBytes;
    bufferConfig.pixelFormat = pixelFormat;
    LCDIFV2_SetLayerBufferConfig(lcdifv2, layer, &bufferConfig);

    return kStatus_Success;
}

这里看一下LCDIFV2_SetLayerBufferConfig函数:

void LCDIFV2_SetLayerBufferConfig(LCDIFV2_Type *base, uint8_t layerIndex, const lcdifv2_buffer_config_t *config)
{
    assert(NULL != config);
    uint32_t reg;
	// 设置行步幅(stride)
    base->LAYER[layerIndex].CTRLDESCL3 = config->strideBytes;
	// 设置像素格式
    reg = base->LAYER[layerIndex].CTRLDESCL5;
    reg = (reg & ~(LCDIFV2_CTRLDESCL5_BPP_MASK | LCDIFV2_CTRLDESCL5_YUV_FORMAT_MASK)) | (uint32_t)config->pixelFormat;
	// 设置安全模式
    if (0U == (reg & LCDIFV2_CTRLDESCL5_AB_MODE_MASK))
    {
        reg |= LCDIFV2_CTRLDESCL5_SAFETY_EN_MASK;
    }

    base->LAYER[layerIndex].CTRLDESCL5 = reg;
}

这里layer设置为了0,实际上LCDIFV2最多支持8个layer,这8个layer可以有不同的组合方式,可以混合、可以隐藏,对我们的显示来说非常友好。当然这个例子中没用到多个layer,这里只用一个layer0就是了。

4.4 设置帧缓冲区图像

这里就手动绘制一个图案显示:

AT_NONCACHEABLE_SECTION_ALIGN(static pixel_t s_frameBuffer[DEMO_BUFFER_HEIGHT][DEMO_BUFFER_WIDTH], FRAME_BUFFER_ALIGN);

static void DEMO_InitFrameBuffer(void)
{
    uint32_t i;
    uint32_t j;

    /*
     * For compliant test, there should be 3 types of data on data lane:
     *
     * 1. ... 11110000 11110000 ...
     * 2. ... 11111000 11111000 ...
     * 3. ... 00000111 00000111 ...
     *
     * The whole frame buffer is divided into three parts, one for each
     * data pattern.
     */

    for (i = 0; i < DEMO_BUFFER_HEIGHT / 3; i++)
    {
        for (j = 0; j < DEMO_BUFFER_WIDTH; j++)
        {
            s_frameBuffer[i][j].X = 0x00U;
            s_frameBuffer[i][j].R = 0xF0U;
            s_frameBuffer[i][j].G = 0xF0U;
            s_frameBuffer[i][j].B = 0xF0U;
        }
    }

    for (; i < DEMO_BUFFER_HEIGHT * 2 / 3; i++)
    {
        for (j = 0; j < DEMO_BUFFER_WIDTH; j++)
        {
            s_frameBuffer[i][j].X = 0x00U;
            s_frameBuffer[i][j].R = 0xF8U;
            s_frameBuffer[i][j].G = 0xF8U;
            s_frameBuffer[i][j].B = 0xF8U;
        }
    }

    for (; i < DEMO_BUFFER_HEIGHT * 3 / 3; i++)
    {
        for (j = 0; j < DEMO_BUFFER_WIDTH; j++)
        {
            s_frameBuffer[i][j].X = 0x00U;
            s_frameBuffer[i][j].R = 0x07U;
            s_frameBuffer[i][j].G = 0x07U;
            s_frameBuffer[i][j].B = 0x07U;
        }
    }
}

根据初始化的帧缓冲区,图像被分为三部分,每部分显示不同的灰色阴影。

  1. 上三分之一:显示亮灰色,RGB值为 0xF0F0F0
  2. 中三分之一:显示更亮的灰色,RGB值为 0xF8F8F8
  3. 下三分之一:显示暗灰色,RGB值为 0x070707

这里主要注意一下s_frameBuffer变量,这里将这个变量设置到non-cacheable的区域。**缓存(MPU)**是视频、摄像头显示等应用的关键,这里简单讨论一下:

  • 这里图像内容明显不会变,所以实际上设置为cacheable也是没问题的
  • 如果图像内容会变,比如是从摄像头获取过来的,此时大概率用了DMA,那此时CPU不知道缓存的内容更新了,就需要将这个缓冲区设置到non-cacheable区域
    • 如果设置为cacheable,然后每次flush一下缓存的话,这样频繁flush缓存反而会导致显示效率变得很慢

4.5 显示图像

最后就设置帧缓冲区的地址然后显示图像就行了:

g_dc.ops->setFrameBuffer(&g_dc, 0, s_frameBuffer);
g_dc.ops->enableLayer(&g_dc, 0);

先来看看setFrameBuffer函数:

status_t DC_FB_LCDIFV2_SetFrameBuffer(const dc_fb_t *dc, uint8_t layer, void *frameBuffer)
{
    assert(layer < DC_FB_LCDIFV2_MAX_LAYER);
    dc_fb_lcdifv2_handle_t *dcHandle = dc->prvData;
	// 将新的帧缓冲区地址设置到指定层
    LCDIFV2_SetLayerBufferAddr(dcHandle->lcdifv2, layer, (uint32_t)(uint8_t *)frameBuffer);
    // 将新的帧缓冲区地址保存到层的非活动缓冲区字段,以便后续使用。
    dcHandle->layers[layer].inactiveBuffer = frameBuffer;

    if (dcHandle->layers[layer].enabled)
    {
    	// 触发层的影子寄存器加载。这将更新显示控制器的设置,使其使用新的帧缓冲区
        LCDIFV2_TriggerLayerShadowLoad(dcHandle->lcdifv2, layer);
        dcHandle->layers[layer].shadowLoadPending = true;
        dcHandle->layers[layer].framePending      = true;
    }
    else
    {
    }

    return kStatus_Success;
}

这里简单介绍一下**影子寄存器**的作用:

影子寄存器(Shadow Register)用于临时存储新的配置值,而不立即应用到实际的硬件寄存器中。LCDIFV2 控制器使用影子寄存器的主要目的是确保在适当的时机批量更新配置,从而避免在显示数据传输过程中出现不一致的状态。

确保数据一致性

  • 在更新显示控制器的配置时,如果直接修改硬件寄存器,可能会导致当前显示帧出现短暂的错误或不一致的显示效果。使用影子寄存器,可以在显示帧之间进行更新,确保显示过程中的数据一致性。

批量更新配置

  • 影子寄存器允许在一次更新中同时改变多个配置参数,而不会中断当前的显示操作。这对于需要同时更新多个设置(如分辨率、帧缓冲区地址、颜色格式等)的场景非常有用。

避免闪烁和撕裂

  • 通过在垂直消隐期间(VBI)或其他适当的时机触发影子寄存器加载,可以避免在显示过程中出现图像闪烁和撕裂现象。

再来看看enableLayer函数

status_t DC_FB_LCDIFV2_EnableLayer(const dc_fb_t *dc, uint8_t layer)
{
    assert(layer < DC_FB_LCDIFV2_MAX_LAYER);

    status_t status                  = kStatus_Success;
    dc_fb_lcdifv2_handle_t *dcHandle = dc->prvData;

    /* If the layer is not started. */
    if (!dcHandle->layers[layer].enabled)
    {
        LCDIFV2_SetLayerBackGroundColor(dcHandle->lcdifv2, layer, 0U);
        LCDIFV2_EnableLayer(dcHandle->lcdifv2, layer, true);
        LCDIFV2_TriggerLayerShadowLoad(dcHandle->lcdifv2, layer);
        dcHandle->layers[layer].shadowLoadPending = true;

        while (true == dcHandle->layers[layer].shadowLoadPending)
        {
#if defined(SDK_OS_FREE_RTOS)
            vTaskDelay(1);
#endif
        }

        dcHandle->layers[layer].activeBuffer = dcHandle->layers[layer].inactiveBuffer;
        dcHandle->layers[layer].enabled      = true;
    }

    return status;
}

该函数用于启用LCDIFV2控制器的指定图层(Layer)。具体步骤包括设置背景颜色、启用图层、触发影子寄存器加载,并确保影子寄存器加载完成。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐