5.2. LCD Driver

5.2.1. Overview

本文主要介绍AC792N SDK 的LCD驱动框架。目前LCD驱动已支持MIPI屏、RGB屏、MCU屏和SPI屏。

LCD Driver在SDK中的作用,是将各种类型屏幕显示接口抽象出一套统一的接口,供给上层应用调用。

image1

在AC792N芯片里,上述显示接口,DSI推MIPI屏、DPI推RGB/MCU屏、PAP(Parallel Active Port)推MCU屏、SPI可推SPI屏。图片中的DMM(Display Memory Management)模块负责从memory读取显示数据,再把显示数据提供给显示接口(仅DSI、DPI显示接口支持联动DMM)。

LCD Driver使用了device设备接口层,也就是说,上层应用需要通过dev_open()dev_ioctl()等接口来对LCD进行操作。开发者可以查看app/common/lcd/lcd_driver/lcd_driver.c,并根据实际屏幕驱动需求进行修改。

一般来说,开发者只需要添加相应屏幕的配置文件,如lcd_480x480_st7701s.c,即可驱动屏幕,不需要修改lcd_driver.c

目前驱动不支持多屏显示

5.2.2. LCD驱动接口

5.2.2.1. Device接口

1.打开设备

操作:使用dev_open()打开设备。接口会遍历注册的屏幕配置,并根据屏幕的配置信息初始化显示接口等。

参数:arg0-设备名;arg1-NULL。

返回值:打开的设备的句柄。后续其他操作可通过该句柄对该设备进行操作。

注意:无。

lcd_hdl = dev_open("lcd", NULL);

2.关闭设备

操作:使用dev_close()关闭设备。。

参数:lcd设备open时获得的句柄。

返回值:固定为0。

注意:无。

dev_close(lcd_hdl);

3.IOCTL控制

LCD支持的IOCTL命令:

#define IOCTL_LCD_RGB_SET_ISR_CB                _IOW('F', 1, sizeof(void *(*)(void)))
#define IOCTL_LCD_RGB_WAIT_FB_SWAP_FINISH       _IOW('F', 2, sizeof(int))
#define IOCTL_LCD_RGB_START_DISPLAY             _IOW('F', 3, sizeof(void *))
#define IOCTL_LCD_RGB_GET_LCD_HANDLE            _IOR('F', 4, sizeof(struct lcd_dev_drive **))
#define IOCTL_LCD_RGB_SET_LINE_ISR_CB           _IOW('F', 5, sizeof(int (*)(int)))
#define IOCTL_LCD_RGB_WAIT_LINE_FINISH          _IOW('F', 6, sizeof(int))
  • IOCTL_LCD_RGB_SET_ISR_CB

功能:设置中断回调函数。仅MIPI和RGB屏支持。

参数:回调函数地址。

返回值:固定为0。

dev_ioctl(lcd_hdl, IOCTL_LCD_RGB_SET_ISR_CB, (u32)lcd_isr_cb);
  • IOCTL_LCD_RGB_WAIT_FB_SWAP_FINISH

功能:等待FB交换BUF地址介绍。

参数:图像buf地址。

返回值:固定为0。

dev_ioctl(lcd_hdl, IOCTL_LCD_RGB_WAIT_FB_SWAP_FINISH, 0);
  • IOCTL_LCD_RGB_START_DISPLAY

功能:启动显示传入的图像buf。

参数:图像buf的地址。

返回值:固定为0。

dev_ioctl(lcd_hdl, IOCTL_LCD_RGB_START_DISPLAY, (u32)display_buf);
  • IOCTL_LCD_RGB_GET_LCD_HANDLE

功能:获取屏幕配置数据句柄

参数:存放句柄的指针变量的地址。

返回值:固定为0。

struct lcd_dev_drive *lcd;
dev_ioctl(lcd_hdl, IOCTL_LCD_RGB_GET_LCD_HANDLE, (u32)&lcd);
  • IOCTL_LCD_RGB_SET_LINE_ISR_CB

功能:等待行数据发送中断回调函数。

参数:回调函数地址。

返回值:固定为0。

dev_ioctl(lcd_hdl, IOCTL_LCD_RGB_SET_LINE_ISR_CB, (u32)line_isr_cb);
  • IOCTL_LCD_RGB_WAIT_LINE_FINISH

功能:等待行数据发送完成。

参数:无。

返回值:固定为0。

dev_ioctl(lcd_hdl, IOCTL_LCD_RGB_WAIT_LINE_FINISH, (u32)0);

5.2.2.2. 其他接口

void WriteCOM(u8 cmd);                    ///< 发送1 byte命令
void WriteDAT_8(u8 dat);                  ///< 发送1 byte数据
void WriteDAT_one_page(u8 *dat, int len); ///< 写一帧图像数据

MCU屏和SPI屏,通过WriteCOM()WriteDAT_8()接口来发送init_code。

MCU屏和SPI屏,通过调用WriteDAT_one_page()来向屏幕发送图像数据。

5.2.3. 添加LCD屏驱方法

添加屏幕驱动,需要修改三个位置:

1. 添加屏幕配置。将屏幕类型、IO分配、init_code、屏厂提供的配置(如HFP)等信息,添加到屏驱配置文件中。可参考已支持的屏驱配置,比如开发板标配的屏幕配置文件lcd_480x800_st7701s.c。开发者需要实现如下结构体配置信息。

struct lcd_dev_drive {
    const char *logo;                          ///< LCD logo。例如"st7701s"
    enum LCD_IF type;                          ///< LCD 类型。例如 LCD_MIPI、LCD_RGB等
    int (*init)(void *_data);                  ///< 初始化接口。
    int (*draw)(void *_data);                  ///< 显示一帧图像的接口
    int (*uninit)(void *_data);                ///< 反初始化接口
    void (*bl_ctrl)(void *_data, u8 onoff);    ///< 背光控制接口
    union lcd_dev_info *dev;                   ///< LCD配置信息结构体的地址。需根据type类型选择配置项
};

2. 修改板级配置。参考《屏接口说明》章节进行IO分配,并修改板级配置文件(比如board_develop_AC7926A.h)里LCD的部分的IO。

// board.h
#define TCFG_LCD_BL_VALUE           1              ///< 背光点亮时的IO电平,0-低电平,1-高电平。
#define TCFG_LCD_RESET_IO           IO_PORTA_15    ///< 复位脚。
#define TCFG_LCD_BL_IO              -1             ///< 背光控制脚。
#define TCFG_LCD_RS_IO              IO_PORTB_01    ///< 指令/数据选择脚,MCU屏和SPI屏特有。
#define TCFG_LCD_CS_IO              IO_PORTB_00    ///< 片选信号脚。
#define TCFG_LCD_SPI_INTERFACE      "spi2"         ///< 分配给屏的SPI口,SPI屏特有。
#define TCFG_LCD_TE_ENABLE          1              ///< TE(Tearing effect)放撕裂功能使能。MCU屏和SPI屏特有。
#define TCFG_LCD_TE_IO              IO_PORTA_14    ///< TE信号脚。
#define TCFG_LCD_SDA_IO             IO_PORTC_00    ///< SDA脚。部分屏幕需要单独的init code发送口,如lcd_480x800.c
#define TCFG_LCD_SCL_IO             IO_PORTC_01    ///< SCL脚。同上。

// 在board.c,注册lcd配置
LCD_PLATFORM_DATA_BEGIN(lcd_data)
    .lcd_name               = TCFG_LCD_DEVICE_NAME,
    .lcd_io                 = {
        .backlight          = TCFG_LCD_BL_IO,
        .backlight_value    = TCFG_LCD_BL_VALUE,
        .lcd_reset          = TCFG_LCD_RESET_IO,
        .lcd_cs             = TCFG_LCD_CS_IO,
        .lcd_rs             = TCFG_LCD_RS_IO,
    },
LCD_PLATFORM_DATA_END()

另外,MCU屏使用到了PAP接口(配置参考如下),SPI屏使用到了SPI接口(配置参考专门的SPI文档),因此需要对相应接口参数进行配置。

// board.h

#define TCFG_PAP_ENABLE                     1                    ///< PAP接口使能。

#define TCFG_PAP_DATA_WIDTH                 PAP_DATAWIDTH_8BIT   ///< PAP接口位宽,可选8bit或16bit。

#define TCFG_PAP_DATA_ENDIAN                PAP_ENDIAN_BE        ///< 大小端选择

#define TCFG_PAP_WRITE_ACTIVE_LEVEL         PAP_WR_LEVEL_LOW     ///< WR写有效电平

#define TCFG_PAP_READ_ACTIVE_LEVEL          PAP_RD_LEVEL_LOW     ///< RD读有效电平

#define TCFG_PAP_READ_ENABLE                0                    ///< 读使能

#define TCFG_PAP_WE_IO                      IO_PORTA_12          ///< WR脚

#define TCFG_PAP_RE_IO                      -1                   ///< RD脚



// board.c

PAP_PLATFORM_DATA_BEGIN(pap_data)

    .datawidth              = TCFG_PAP_DATA_WIDTH,

    .endian                 = TCFG_PAP_DATA_ENDIAN,

    .pwe                    = TCFG_PAP_WRITE_ACTIVE_LEVEL,

    .pre                    = TCFG_PAP_READ_ACTIVE_LEVEL,

    .rd_en                  = TCFG_PAP_READ_ENABLE,

    .we_port                = TCFG_PAP_WE_IO,

    .re_port                = TCFG_PAP_RE_IO,

    .port_sel               = PAP_PORT_A,                       ///< PAP出口选择。可选A或B端口

    .timing_setup           = PAP_TS_0_CLK,                     ///< TS时序配置

    .timing_hold            = PAP_TH_0_CLK,                     ///< TH时序配置

    .timing_width           = PAP_TW_1_CLK,                     ///< TW时序配置

PAP_PLATFORM_DATA_END();



//timing_setup(ts) timing_hold(th) timing_width(tw):数据传输配置

/*

写时钟信号:          __________               _________

pwe=H: ____________|          |_____________|         |__________

pwe=L: ____________            _____________           __________

                   |__________|             |_________|

数据信号:    ________________________                       ______

data:  _____|                        |_____________________|

            |       |         |      |

ts,tw,th:   |       |         |      |

            |<--ts->|<--tw--->|<-th->|

            |       |         |      |

ts:0-3,ts个lsb系统时钟宽度

tw:0-15,0-->16个lsb系统时钟宽度,1-15:tw个lsb系统时钟宽度

th:0-3,th个lsb系统时钟宽度

*/
  1. 添加分辨率配置。在lcd_config.h中添加相应屏幕分辨率。

// 在lcd_config.h
// MIPI LCD
#if TCFG_LCD_MIPI_ST7701S_480x800
#define LCD_W 480
#define LCD_H 800
#endif

5.2.4. 屏幕配置文件说明

int (*init)(void *_data);                  ///< 初始化接口
int (*draw)(void *_data);                  ///< 显示一帧图像的接口
int (*uninit)(void *_data);                ///< 反初始化接口
void (*bl_ctrl)(void *_data, u8 onoff);    ///< 背光控制接口
union lcd_dev_info *dev;                   ///< LCD配置信息结构体的地址。需根据type类型选择配置项

开发者在添加新屏驱时,一般只需要在SDK中找到同一类型屏幕的配置文件,稍加修改即可。下面对struct lcd_dev_drive部分参数进行说明。

init函数:初始化屏幕的接口。AC792N SDK在这个接口里将屏厂提供的init_code通过显示接口或者专用的通讯接口发送给屏幕初始化(MIPI屏使用DSI接口的command模式发送DCS命令集)。一般情况下,AC792N 的init_code发送,RGB屏使用单独的CLK+DAT两线串行接口发送,MCU屏使用pap接口发送,SPI屏则直接用spi接口发送。

draw函数:显示一帧图像。AC792N SDK中只有MCU屏(LCD_MCU类型)和SPI屏需要实现此接口。

uninit函数:顾名思义。

bl_ctrl函数:背光控制接口。

LCD配置信息结构体

  • MIPI屏

struct mipi_dev {
    struct imd_dmm_info info;                           ///< DMM配置
    struct pll4_info pll4;                              ///< MIPI PLL时钟配置
    struct te_mode_ctrl te_mode;                        ///< TE模式配置
    struct dsi_lane_mapping lane_mapping;               ///< 使用的硬件lane接口配置。使用多少lane,是否对调P/N等配置
    struct dsi_video_timing video_timing;               ///< DIS video时序配置。前肩、后肩等参数
    struct dsi_timing timing;                           ///< DSI时序设置

    u8 dsi_open_flag;                                   ///< DSI已经打开标志
    unsigned int reset_gpio;                            ///< 复位IO
    void (*lcd_reset)(unsigned int reset_gpio);         ///< 复位函数
    void (*lcd_reset_release)(unsigned int reset_gpio); ///< 复位释放函数

    const u8 *cmd_list;                                 ///< DCS初始化命令集
    u16 cmd_list_item;                                  ///< DCS初始化命令集占用空间大小

    u8 debug_mode;                                      ///< 调试模式。使能时,DSI驱动会将DCS命令集打印出
};
  • RGB屏

struct imd_dev {
    struct imd_dmm_info info;       ///< DMM配置
    struct te_mode_ctrl te_mode;    ///< TE模式配置

    enum LCD_LSB data_out_mode;     ///< 输出数据大小端。仅输出RGB565支持
    enum NCYCLE ncycle;             ///< 每像素时钟数。如数据格式RGB888,接口位宽8bit,cycle = 24bit / 8bit =3 cycle
    enum PHASE raw_odd_phase;       ///< 奇行相位
    enum PHASE raw_even_phase;      ///< 偶行相位
    enum RAW_MODE raw_mode;         ///< RAW模式选择
    enum LCD_PORT data_width;       ///< 数据位宽
    enum LCD_GROUP lcd_group;       ///< 输出端口选择
    u8 avout_mode;                              ///< 保留
    u8 show_buf_mode;               ///< 显示buf模式

    u8 set_io_hd;                   ///< IO强驱配置。所dat、hsy、vsy、clk、den脚都会根据该配置设置强驱档位。
    u8 dclk_set;                    ///< dclk使能以及极性配置
    u8 sync0_set;                   ///< 配置为DE/HSYNC/VSYNC
    u8 sync1_set;                   ///< 配置为DE/HSYNC/VSYNC
    u8 sync2_set;                   ///< 配置为DE/HSYNC/VSYNC
    u8 sync3_set;                   ///< Reversed
    u8 dpi_open_flag;               ///< dpi打开标志
    u32 clk_cfg;                    ///< dpi clk时钟配置。dpi模块的时钟配置
    u32 dclk_cfg;                   ///< dclk时钟配置。dpi输出的dclk的时钟源和时钟分频配置
    struct imd_dpi_timing timing;   ///< 时序参数
};
  • MCU屏

MCU屏可以用两种.type去推,分别是LCD_MCU_SINGLE_FRAMELCD_MCU。它们有如下区别:

  • LCD_MCU_SINGLE_FRAME:发送init_code和cmd时使用PAP接口发送,发送图像数据时,使用DPI接口(的EMI模式)发送。该模式下因为使用了DPI,需要用到struct imd_dev参数配置。需要注意,LCD的IO分配必须使用MCU接口中的PORT-A才能用该模式。
    (可能有人会疑惑,PAP和DPI是两个独立的接口,怎么可以这样用?可以看下《屏接口说明》章节的IO分配,PAP接口的PORT-ARGB接口的PORT-A的信号线出线是一样的。)
  • LCD_MCU:init_code、cmd和图像数据全部都通过pap接口来发送。在需要大小端数据转换情况下,lcd_driver会需要malloc多一块buf用于存放转换后的数据,内存空间有点浪费。

LCD_MCU_SINGLE_FRAME模式推屏的优点是可以使用DMM模块,这样,DMM模块的一些特性就可以用上,例如大小端转换颜色增强等。

建议AC792N推MCU屏使用LCD_MCU_SINGLE_FRAME模式。由于LCD_MCU_SINGLE_FRAME模式使用到了IMD的配置参数,相对复杂,因此调屏过程可以先用LCD_MCU模式,将init_code和PAP接口等调通后,再改为LCD_MCU_SINGLE_FRAME模式来调整IMD参数。

  • SPI屏

struct spi_dev {
    struct imd_dmm_info info;       ///< DMM配置
    struct te_mode_ctrl te_mode;    ///< TE模式配置
    enum LCD_LSB data_out_mode;     ///< 输出数据大小端。选择MODE_BE时,将进行大小端数据转换。
    char *interface_name;           ///< 使用的SPI口名。比如"SPI2"
    u8 spi_open_flag;               ///< SPI LCD已经打开标志
};

注意:SPI屏参数如果配置了大小端转换(即.data_out_mode = MODE_BE),lcd_driver将会malloc一块buf空间用于存放转换后的图像数据,空间占用多一些。

5.2.5. 屏幕旋转配置

AC792N支持旋转ROTATE_90ROTATE_180ROTATE_270度。配置方式:

1. 修改配置文件旋转角度

驱动会根据.rotate的值,来旋转图像地址空间。

// lcd_xxx.c
REGISTER_IMD_DEVICE_BEGIN(lcd_mcu_dev) = {
    .info = {
// ...
        .len                          = LEN_256,
        .interlaced_mode = INTERLACED_NONE,
        .rotate          = ROTATE_0,                 ///< 旋转
// ...
REGISTER_IMD_DEVICE_END()

2. 调整分辨率宽/高

如果旋转角度选择了ROTATE_90ROTATE_270LCD_WLCD_H需要对调。该配置影响LVGL生成的图像分辨率。

// lcd_config.h
/* #define LCD_W 320 */
/* #define LCD_H 480 */
#define LCD_W 480
#define LCD_H 320

5.2.6. TE(Tearing effect)使用说明

MCU/SPI屏显示为避免出现撕裂问题(Tearing effect),lcd driver需要检测到帧TE信号后才开始推数据给屏幕,并且推屏速率需要遵循2倍关系:

  1. 不能让读屏指针赶上写屏指针。写屏ram的数据时间(W)应该小于等于2倍MCU/SPI屏的自刷新时间(R)。

  2. 不能让写屏指针赶上读屏指针。写屏ram的数据时间(W)应该大于MCU/SPI屏的自刷新时间(R)。即lcd_te_int_time < lcd_push_data_time <= 2 * lcd_te_int_time

可调整接口推屏速率。推屏接口的时钟频率决定了推数据速率,各个接口的时钟频率调整位置:

  1. SPI接口——修改波特率和LSB时钟;

  2. PAP接口——修改TS、TH、TW时钟以及LSB时钟;(PAP的WR速率 = LSB / (TS + TH + TW))

  3. DPI接口——修改.dclk_cfg的时钟源和分频值;

当然,也可以调整MCU/SPI屏刷新帧率,需要咨询屏厂怎么修改。

5.2.6.1. 开启TE功能

在board.h中配置宏来使能TE功能:

#define TCFG_LCD_TE_ENABLE                  1               ///< 非0开启TE

#define TCFG_LCD_TE_IO                      IO_PORTA_14     ///< 配置TE脚

开启TE功能后,驱动会在lcd初始化时配置TCFG_LCD_TE_IO的引脚为外部中断脚,以判断TE信号的到来。UI刷新图像给驱动,lcd驱动会等待TE信号来之后立马开始推一帧图像。

5.2.6.2. TE调试

为了方便调试TE,使得推屏速率需要遵循2倍关系,驱动提供一个调试方式,可配置LCD_TE_DEBUG_EN为1来使能。

#define LCD_TE_DEBUG_EN         0          ///< 开启TE调试。通过打印判断推屏速率是否在合适范围。



// TE速率判断逻辑

if (lcd_te_int_time > lcd_push_data_time) {

    // lcd推数据太快

    printf("TE issue! lcd push data is too fast. diff_time = %d\n", lcd_te_int_time - lcd_push_data_time);

} else if (lcd_te_int_time * 2 < lcd_push_data_time) {

    // lcd推数据太慢

    printf("TE issue! lcd push data is too slow. diff_time = %d\n", lcd_push_data_time - lcd_te_int_time * 2);

} else {

    // lcd推数据速率刚刚好

    printf("TE normal! diff_time = %d\n", lcd_te_int_time * 2 - lcd_push_data_time);

}

其中,lcd_te_int_time为通过TE中断获得的TE信号时间间隔。lcd_push_data_time为lcd驱动开始推数据给屏幕到结束的时间。

开发者需要通过调整推屏速率或者屏自刷新帧率来实现2倍关系,当log里出现“TE normal!”的打印时,即调整完成,调整完成后即可关闭调试功能(如果2倍关系处于临界状态,受关闭TE调试代码的影响推屏速率有稍微变化,可能又出现撕裂条纹,这时需要再微调一下)。

5.2.7. 注意点

  1. 由于lcd driverspi/pap都使用了device设备这一套接口,如果屏幕使用了相应的外设,那么就对设备注册有顺序要求。SDK开机启动流程中会通过devices_init()对板级board_xxx.c里注册的设备进行基本的初始化操作,设备的初始化顺序与板级里设备注册顺序有关,注册得越靠前,越早初始化。LCD驱动里,SPI屏需要依赖spi接口通信,MCU屏需要pap接口,因此,当驱动的屏幕为SPI屏/MCU屏,spi/pap设备注册应早于lcd设备。参考如下顺序:

    REGISTER_DEVICES(device_table) = {
        { "pap",   &pap_dev_ops, (void *)&pap_data  }, ///< MUC屏使用了pap,pap设备需要放在lcd前面注册
        { "spi2",  &spi_dev_ops, (void *)&spi2_data }, ///< SPI屏使用了spi2,spi2设备需要放在lcd前面注册
        { "lcd",   &lcd_dev_ops, (void *)&lcd_data  },
    };