LCD
简介
LCD device是一个rt_device,内部是一个简单的LCD框架用于注册不同屏幕驱动。本章节主要介绍LCD device的使用及框架,以及如何注册一个新屏幕到该框架。
内部结构
LCD驱动有3层:
- rt_device_graphic层 - 对上层提供统一调用接口 - 内部支持多个驱动查找功能,方便兼容多个屏(根据比较注册的ID和ReadID函数返回的值决定) 
- 内部包含3个framebuffer机制,让渲染和送屏可以同步进行,支持压缩。 
 
- 具体驱动的逻辑层 - 具体各个屏驱动的接口、频率、TE等配置,以及初始化代码、送屏命令,睡眠,开关指令 
 
- 屏物理接口的抽象层 - 对大部分接口提供统一的操作函数 详见LCDC 
 

备注
SDK内实现了2个rt_device_graphic实例,rt_device name:
- lcd(drv_lcd.c) - 正常物理LCD屏 
 
- ram_lcd(drv_ram_lcd.c) - 将输出写到SRAM(打开后将从系统堆内存分配一个LCD buffer) 
- ram_lcd的使用跟正常屏幕一样(屏幕尺寸固定) 
 
上层使用LCD device的示例
在屏幕中央绘制一个100*100的RGB565格式的红色区域,每刷新一次红色加深一点,32次后从头循环。
#define RGB565_FB_WIDTH  100
#define RGB565_FB_HEIGHT  100
static uint16_t rgb565_frambuffer[RGB565_FB_WIDTH * RGB565_FB_HEIGHT];
static struct rt_semaphore lcd_sema;
static rt_err_t lcd_flush_done(rt_device_t dev, void *buffer)
{
    rt_sem_release(&lcd_sema);
    return RT_EOK;
}
void lcd_flush_task(void *parameter)
{
    rt_err_t ret;
    uint8_t color = 0;
    struct rt_device_graphic_info info;
    uint16_t framebuffer_color_format = RTGRAPHIC_PIXEL_FORMAT_RGB565;
    /* LCD Device Init */
    rt_device_t lcd_device = rt_device_find("lcd");
    RT_ASSERT(lcd_device != RT_NULL);
    rt_device_set_tx_complete(lcd_device, lcd_flush_done);
    if (rt_device_open(lcd_device, RT_DEVICE_OFLAG_RDWR) == RT_EOK)
    {
        if (rt_device_control(lcd_device, RTGRAPHIC_CTRL_GET_INFO, &info) == RT_EOK)
        {
            rt_kprintf("Lcd info w:%d, h%d, bits_per_pixel %d\r\n", info.width, info.height, info.bits_per_pixel);
        }
    }
    rt_device_control(lcd_device, RTGRAPHIC_CTRL_SET_BUF_FORMAT, &framebuffer_color_format);
    rt_sem_init(&lcd_sema, "lv_lcd", 1, RT_IPC_FLAG_FIFO);
    while (1)
    {
        rt_err_t err;
        int32_t dx, dy;
        /*Draw framebuffer at center of screen*/
        dx = (info.width - RGB565_FB_WIDTH) / 2;
        dy = (info.height - RGB565_FB_HEIGHT) / 2;
         /* Fill RGB565 framebuffer with red color*/
        color = (color + 1) % 0x1F;
        for(uint32_t i = 0; i < RGB565_FB_HEIGHT; i++)
            for(uint32_t j = 0; j < RGB565_FB_WIDTH; j++)
                {
                    rgb565_frambuffer[(i * RGB565_FB_WIDTH) + j] = ((uint16_t )color) << 11;
                }
        //Wait lcd_flush_done to release lcd_sema
        err = rt_sem_take(&lcd_sema, rt_tick_from_millisecond(1000));
        RT_ASSERT(RT_EOK == err);
		/*
			设置绘图操作的有效区域,以下所有坐标的坐标系原点是屏幕左上角
		*/
        rt_graphix_ops(lcd_device)->set_window(dx, dy, dx + RGB565_FB_WIDTH - 1, dy + RGB565_FB_HEIGHT - 1);
		/*
			将一个buffer画到LCD设备上,异步函数,完成后调用lcd_flush_done
			
			关于{x0,y0,x1,y1}区域:
			   1. 该区域表示整个buffer在屏幕上占据的区域,并不是用于剪切该buffer的
			   2. 如果只更新buffer中的某一部分,需配合set_window函数使用
        */
        rt_graphix_ops(lcd_device)->draw_rect_async((const char *)&rgb565_frambuffer, dx, dy, dx + RGB565_FB_WIDTH - 1, dy + RGB565_FB_HEIGHT - 1);
    }
}
增加一个新屏幕的流程
1. 选择example\rt_driver下对应板子的工程
- 这个工程里面有一个简单的绘制矩形区域的示例(见前面 “_rt_device_graphic_层接口的使用示例”) 
- 如果选择用_watch_demo_工程,需要 把tp线程关闭再调屏幕 ,防止TP不通阻塞UI送屏线程(关闭办法: - drv_touch.c- touch_open函数,去掉_rt_thread_startup(touch_thread);_ )
2. 将新驱动添加到编译工程里面
- 添加新屏幕代码到目录_customer\peripherals_内 - 可以从其他已有的驱动复制一份代码,然后将名字、ID、对应的命令(绝大部分都一样不需要改)改成自己的 
- 注意修改内部的Kconfig文件的depend宏 
- 屏驱注册的宏说明: 
 - LCD_DRIVER_EXPORT( rm69090, //屏驱的名称,字符串 RM69090_ID, //屏驱的ID,用于和RM69090_ReadID的返回值做比较 &lcdc_int_cfg, //屏驱的初始化配置(包括接口、频率、输出颜色格式等) &RM69090_drv, //屏驱的对外API接口实现 RM69090_LCD_PIXEL_WIDTH, // IC的横向最大分辨率(注意不是玻璃的分辨率) RM69090_LCD_PIXEL_HEIGHT, // IC的垂直最大分辨率(注意不是玻璃的分辨率) 2 //刷新时的像素对齐(不需要则写1) ); 
备注
玻璃的分辨率固定在宏LCD_DRIVER_EXPORT的定义内,见宏LCD_HOR_RES_MAX和LCD_VER_RES_MAX
- 在_customer\peripherals\Kconfig_内,为新加的驱动添加一个隐藏开选项,比如: - config LCD_USING_RM69090 bool default n 
- 在板级的配置中添加屏幕模组的开关,模组开关内选上前面添加的隐藏开关以及使用的接口开关(接口开关用于修改pinmux),比如_customer\boards\ec-lb551XXX\Kconfig_内,添加模组开关: - config LCD_USING_ED_LB55SPI17801_QADSPI_LB551 bool "1.78 rect QAD-SPI LCD(ED-LB55SPI17801)" select TSC_USING_FT3168 if BSP_USING_TOUCHD select LCD_USING_RM69090 select BSP_LCDC_USING_QADSPI if LCD_USING_ED_LB55SPI17801_QADSPI_LB551 config LCD_RM69090_VSYNC_ENABLE bool "Enable LCD VSYNC (TE signal)" def_bool n endif 
备注
触控的添加流程类似屏,并在此处模组开关内一并选择
- 还是在上面文件中,为新添加的屏幕定义分辨率、DPI值(注意此处是模组玻璃的分辨率,不是屏幕IC能支持的最大分辨率) - config LCD_HOR_RES_MAX int default 454 if LCD_USING_ED_LB55SPI17201_QADSPI_LB551 default 400 if LCD_USING_ED_LB55_77903_QADSPI_LB551 default 240 if LCD_USING_ED_LB55_387A_JDI_LB551 default 368 if LCD_USING_ED_LB55SPI17801_QADSPI_LB551 <-------- 新添加的行 config LCD_VER_RES_MAX int default 454 if LCD_USING_ED_LB55SPI17201_QADSPI_LB551 default 400 if LCD_USING_ED_LB55_77903_QADSPI_LB551 default 240 if LCD_USING_ED_LB55_387A_JDI_LB551 default 448 if LCD_USING_ED_LB55SPI17801_QADSPI_LB551 <-------- 新添加的行 config LCD_DPI int default 315 if LCD_USING_ED_LB55SPI17201_QADSPI_LB551 default 315 if LCD_USING_ED_LB55_77903_QADSPI_LB551 default 315 if LCD_USING_ED_LB55_387A_JDI_LB551 default 315 if LCD_USING_ED_LB55SPI17801_QADSPI_LB551 <-------- 新添加的行 
- 若用scons 编译,则需要进入工程的menuconfig选择菜单,然后选择前面新增的屏幕模组,最终生成_.config_和_rtconfig.h_ 
- 若用Keil编译,也可以直接添加源代码进入(但还是建议和scons编译添加方法一样,这样下次重新生成Keil工程后会自动加入) 
3. 检查新增LCD用到的pin,以及reset pin 的pinmux是否正确
- 如前所述,在模组的配置时会选择一个接口的宏,SDK内部有对不同的LCDC接口宏做pin mux处理 
- reset pin由于是独立的GPIO控制,需要确认 - BPS_LCD_Reset函数控制的pin是否正确
4. 修改新屏幕的接口、频率、输出颜色格式
- 见下面示例,修改 - LCD_DRIVER_EXPORT宏内- init_cfg结构体(输出频率跟配置的可能会有偏差,请以实际输出的为准,因为HAL层实现输出频率=HCLK/divider, HCLK在console输入”sysinfo”可以查看,divider为2~255的整数)
- TE在调试初期建议关闭,防止LCDCD因为等不到TE信号而不送数(我们的TE信号是LCDC自动处理,不需要软件参与) 
备注
我们的TE信号是LCDC自动处理,不需要软件参与,所以没有TE软件中断上来。只要TE pin mux和极性正确配置,启动LCDC送数后(HAL_LCDC_SendLayerData2Reg_IT),收到了TE脉冲LCDC就会启动送数
5 使用任意GPIO作为TE信号(可选)
大部分情况下,只要定义相应的管脚为TE功能,LCDC就可以自动处理TE信号,但在某些特殊情况下,TE信号无法从期望的通路来时,需要改成普通GPIO来实现,此时可以通过软件GPIO中断办法实现:
- 按照正常的配置将TE打开、设置好TE的延迟 
- 定义宏“ - LCD_USE_GPIO_TE”为一个普通GPIO, 它将会在中断上升沿主动去人为制造一个TE信号(翻转TE极性),从而触发LCDC送数据
备注
因为正常TE通路不正常,所以第一步的设置无法工作,只能靠第二步的人为制造信号去触发送数,从而实现正常TE处理
6. 修改新屏幕驱动的初始化代码
- 一般是先初始化LCDC,配置接口、频率等. 调用API - HAL_LCDC_Init. 
- 然后是reset LCD , 通过drv_io.c内实现的 - BPS_LCD_Reset函数去控制GPIO 复位屏幕。
- 然后就是屏厂给的初始化代码 
7. 修改read id函数
- drv_lcd.c`` 将利用该函数返回值和LCD_DRIVER_EXPORT`注册的ID比较, 相同则认为该驱动可用,才会去调用。
8. QAD-SPI LCD扩展命令修改
- QAD-SPI LCD 一般会把标准8bit命令扩展成32bit,需要修改扩展方法。 可以参考rm69330.c, 一般有这几个函数需要修改: RM69330_WriteMultiplePixels, RM69330_WriteReg,RM69330_ReadData。 
客户新增屏幕驱动代码示例(部分)
以下示例代码展示了RM69330如何注册到drv_lcd.c(rt_device_graphic层)以及接口配置、函数回调等. 具体每个函数的实现请参考SDK代码,此处不赘述。
//RM69330 chip IDs
#define RM69330_ID                  0x8000
//RM69330 resulution
#define  RM69330_LCD_PIXEL_WIDTH    (454)
#define  RM69330_LCD_PIXEL_HEIGHT   (454)
//Interface, Frequency, Output color format, TE
static LCDC_InitTypeDef lcdc_int_cfg_qspi =
{
    .lcd_itf = LCDC_INTF_SPI_DCX_4DATA, //QAD-SPI mode
    .freq = 48000000,
    .color_mode = LCDC_PIXEL_FORMAT_RGB565,
    .cfg = {
        .spi = {
            .dummy_clock = 0, //0: QAD-SPI/SPI3   1:SPI4
            .syn_mode = HAL_LCDC_SYNC_VER,
            .vsyn_polarity = 0,
            .vsyn_delay_us = 1000,
            .hsyn_num = 0,
        },
    },
};
//Function callbacks
static const LCD_DrvOpsDef RM69330_drv =
{
    RM69330_Init,
    RM69330_ReadID,
    RM69330_DisplayOn,
    RM69330_DisplayOff,
    RM69330_SetRegion,
    RM69330_WritePixel,
    RM69330_WriteMultiplePixels,
    RM69330_ReadPixel,
    RM69330_SetColorMode,
    RM69330_SetBrightness
};
//Regist to drv_lcd.c
LCD_DRIVER_EXPORT(rm69330, RM69330_ID, &lcdc_int_cfg_qspi,
    &RM69330_drv,
    RM69330_LCD_PIXEL_WIDTH,
    RM69330_LCD_PIXEL_HEIGHT,2);
备注
如前面所述,`drv_lcd.c``初始化时将比较 LCD_DRIVER_EXPORT注册的RM69330_ID 和 RM69330_ReadID返回的ID,如果相同才会调用。
同时兼容多个屏幕模组
假设要兼容2各模组:
- 模组一为LB55SPI17801(屏幕IC是RM69090,触控IC是FT3168), 
- 模组二为LB55BILI8688E(屏幕IC是ILI8688E,触控IC是CST918), 
跟前面添加屏幕一样,往工程对应的Kconfig文件添加一项同时选择2款模组的屏驱和触控驱动即可(注意:屏驱的ReadID函数要能分别2款IC,同理触控的probe函数也要能区分不同的IC)。
Kconfig文件示例如下:
config LCD_USING_ED_LB55SPI17801_LB55BILI8688E_QADSPI_LB551
    bool "1.78 rect QAD-SPI LCD(ED-LB55SPI17801 and ED-LB55BILI8688E)"
    select TSC_USING_FT3168 if BSP_USING_TOUCHD
    select TSC_USING_CST918 if BSP_USING_TOUCHD
    select LCD_USING_RM69090
    select LCD_USING_ILI8688E
    select BSP_LCDC_USING_QADSPI
    if LCD_USING_ED_LB55SPI17801_LB55BILI8688E_QADSPI_LB551
        config LCD_RM69090_VSYNC_ENABLE
            bool
            def_bool n
        config LCD_ILI8688E_VSYNC_ENABLE
            bool
            def_bool y
    endif
备注
其他的屏幕分辨率配置,DPI配置就共用LCD_USING_ED_LB55SPI17801_LB55BILI8688E_QADSPI_LB551宏去设置即可,既然兼容这些参数应该都是一样的
DSI屏幕调试
建议先调低速模式 因为低速可以绕过因为硬件导致的问题,并且好用示波器分析。 低速模式调通后读ID,读ID 能检查屏幕上电是否正常。 低速模式读ID,刷屏都正常后,再改用高速模式刷屏
DSI低速模式操作流程
- 降低LCDC送数的速度 - 需要将系统时钟降到48M,在 - drv_io.c内,将_HAL_RCC_HCPU_ClockSelect(RCC_CLK_MOD_SYS, XXX); XXX就是系统时钟频率,改成_RCC_SYSCLK_HXT48(晶体时钟48MHz)
- 将所有命令都改成LP模式(低速模式)发送,并且打开LCD acknowledge,方便用逻分仪或示波器抓取波形分析(在前面的 - LCDC_InitTypeDef那个结构体内配置,如下:)
 - .LPCmd = { .LPGenShortWriteNoP = DSI_LP_GSW0P_ENABLE, .LPGenShortWriteOneP = DSI_LP_GSW1P_ENABLE, .LPGenShortWriteTwoP = DSI_LP_GSW2P_ENABLE, .LPGenShortReadNoP = DSI_LP_GSR0P_ENABLE, .LPGenShortReadOneP = DSI_LP_GSR1P_ENABLE, .LPGenShortReadTwoP = DSI_LP_GSR2P_ENABLE, .LPGenLongWrite = DSI_LP_GLW_ENABLE, .LPDcsShortWriteNoP = DSI_LP_DSW0P_ENABLE, .LPDcsShortWriteOneP = DSI_LP_DSW1P_ENABLE, .LPDcsShortReadNoP = DSI_LP_DSR0P_ENABLE, .LPDcsLongWrite = DSI_LP_DLW_ENABLE, //DSI_LP_DLW_DISABLE, ENABLE - LP mode, DISABLE - high speed mode .LPMaxReadPacket = DSI_LP_MRDP_ENABLE, .AcknowledgeRequest = DSI_ACKNOWLEDGE_ENABLE, //Enable LCD error reports }, - 备注 - _init->cfg.dsi.LPCmd.LPDcsLongWrite_调成`DSI_LP_DLW_ENABLE`后, _HAL_LCDC_Init_内部会主动将DBI的送数频率降到最低(48 * 16 / 126 = 6Mbps 左右,48为系统时钟,16为DBI带宽,126为最大分频系数)
- 调整DSI LP 模式的频率到屏幕能支持的范围(一般在6~20Mbps), 如下配置时LP频率 = 480 / 8 / 4 = 15Mbps(其中480为freq, 8 为固定值, 4为TXEscapeCkdiv) - static LCDC_InitTypeDef lcdc_int_cfg_dsi = { .lcd_itf = LCDC_INTF_DSI, .freq = DSI_FREQ_480MHZ, .color_mode = LCDC_PIXEL_FORMAT_RGB888, .cfg = { .dsi = { .Init = { .AutomaticClockLaneControl = DSI_AUTO_CLK_LANE_CTRL_ENABLE, .NumberOfLanes = RM69090_DSI_DATALANES, .TXEscapeCkdiv = 0x4, }, ... } ... } ... } 
- 将drv_lcd 层的刷屏超时时间加长,否则默认的500ms在低速下容易超时,调整宏 - MAX_LCD_DRAW_TIME的值即可
- 使能 - bf0_hal_dsi.c里面的log(需要覆盖_HAL_DBG_printf_), 可以通过log检查通信过程发生的错误- #define DSI_LOG_D(...) HAL_DBG_printf(__VA_ARGS__) #define DSI_LOG_E(...) HAL_DBG_printf(__VA_ARGS__) 
- 如果有条件可以用逻分仪或示波器抓取DATALANE0上P,N两个引脚的波形,解析各个命令、颜色格式等是否是期望的数据 
备注
读ID时,屏幕在bus turnaround之后有没有ack,若没有可能上电或者reset异常
DSI 高速模式配置
- 低速模式可以读ID和刷期望的颜色后,可以调成高速模式,一般来说就通了。 - 高速模式要把AcknowledgeRequest改成disable,否则容易引起发数fifo溢出, 
- 另外注意有的屏幕不同的颜色格式对应不一样的最高频率。 - 在前面的LCDC_InitTypeDef那个结构体内配置,如下: .LPCmd = { ... .LPDcsLongWrite = DSI_LP_DLW_DISABLE, //高速模式主要就是这里把长包改成高速模式 ... .AcknowledgeRequest = DSI_ACKNOWLEDGE_DISABLE, //高速模式下需要把Ack包关掉 } 
 
DSI 颜色,TE功能配置
- DSI颜色格式 参考 
- DSI TE功能 DSI TE有2条通路可选:通过DSI链路或者通过LCDC_TE的功能管脚 如果走LCDC_TE管脚,需要同时指定一个物理管脚的pinmux 为 LCDC_TE功能(可以是LCDCx_SPI_TE/LCDCx_8080_TE) - static LCDC_InitTypeDef lcdc_int_cfg_dsi = { .lcd_itf = LCDC_INTF_DSI, .freq = DSI_FREQ_480MHZ, .color_mode = LCDC_PIXEL_FORMAT_RGB888, .cfg = { .dsi = { ... .CmdCfg = { ... .TearingEffectSource = DSI_TE_EXTERNAL, <<<---- DSI_TE_EXTERNAL 代表走LCDC_TE管脚, DSI_TE_DSILINK代表走DSI链路 - 备注 - 在55x系列芯片上,不支持DSI使用LCDC_TE的功能管脚,请参考上面“使用任意GPIO作为TE信号”章节的办法。 
