ST77903 QSPI + STM32H750 + RTThread + LVGL(二)

ST77903 QSPI + STM32H750 + RTThread + LVGL(二)

这一章节我们来完成ST77903的底层驱动

前言

到手的资料,就是一份ST77903 datasheet,几页timing ppt,一块显示屏,全新的lcd驱动方式,没有任何可参考的源代码,所以我们从一穷二白开始,边调试边搭建驱动框架。
ST77903支持单线指令和4线指令,为了尽可能的效率,我们选择4线,如果是单线,会略有不同,我们这次就暂不论述了


一、宏定义

代码如下:

#define LCD_BPP             (24)

#define LCD_X_SIZE          (400U)                      /* available x pixel size */
#define LCD_Y_SIZE          (400U)		                /* available y pixle size */
#define LCD_PBYTE           ((LCD_BPP + 7) / 8)         /* bytes in pixel unit */
#define LCD_HBYTE           (LCD_X_SIZE * LCD_PBYTE)    /* bytes in horizontal line */

#define LCD_VSW             (1U)
#define LCD_HFP             (8U)
#define LCD_HBP             (8U)

#define LCD_TE_OFT          (25U)

#define FRAME_BLANKING_TIME (10U)
#define FRAME_MEM_SIZE      (LCD_HBYTE * LCD_Y_SIZE)
#define FRAME_MEM_BASE      SRAM_AXI_BASE

#if (FRAME_MEM_SIZE > SRAM_AXI_SIZE)
#error "lcdqspi frame cache size is not enough!"
#endif

static uint8_t frame_cache[LCD_Y_SIZE][LCD_HBYTE] __attribute__ ((at(FRAME_MEM_BASE)));

显示屏RGB888 - 24BIT显示,分辨率400x400
LCD需求的RGB Timing - VS, HFP, HBP
避免切屏,RAM读写动作错开,需要设定TE信号时间偏移
每帧画面之间有个BLANKING时间,留给其它线程做其它事情和更新显示内容
创建一个和显示分辨率一样大小的显存空间

二、平台底层QSPI+DMA设定

代码如下:

void HAL_QSPI_MspInit(QSPI_HandleTypeDef *hqspi)
{
    static MDMA_HandleTypeDef hmdma;

    __HAL_RCC_QSPI_CLK_ENABLE();
    __HAL_RCC_QSPI_FORCE_RESET();
    __HAL_RCC_QSPI_RELEASE_RESET();

    __HAL_RCC_MDMA_CLK_ENABLE();
    __HAL_RCC_MDMA_FORCE_RESET();
    __HAL_RCC_MDMA_RELEASE_RESET();

    HAL_NVIC_SetPriority(QUADSPI_IRQn, NVIC_PRIORITY_QSPI, 0);
    HAL_NVIC_EnableIRQ(QUADSPI_IRQn);

    hmdma.Init.Request              = MDMA_REQUEST_QUADSPI_FIFO_TH;
    hmdma.Init.TransferTriggerMode  = MDMA_BUFFER_TRANSFER;
    hmdma.Init.Priority             = MDMA_PRIORITY_HIGH;
    hmdma.Init.Endianness           = MDMA_LITTLE_ENDIANNESS_PRESERVE;

    hmdma.Init.SourceInc            = MDMA_SRC_INC_BYTE;
    hmdma.Init.DestinationInc       = MDMA_DEST_INC_DISABLE;
    hmdma.Init.SourceDataSize       = MDMA_SRC_DATASIZE_BYTE;
    hmdma.Init.DestDataSize         = MDMA_DEST_DATASIZE_BYTE;
    hmdma.Init.DataAlignment        = MDMA_DATAALIGN_PACKENABLE;
    hmdma.Init.BufferTransferLength = 16;
    hmdma.Init.SourceBurst          = MDMA_SOURCE_BURST_SINGLE;
    hmdma.Init.DestBurst            = MDMA_DEST_BURST_SINGLE;

    hmdma.Init.SourceBlockAddressOffset = 0;
    hmdma.Init.DestBlockAddressOffset   = 0;

    hmdma.Instance = MDMA_Channel1;

    __HAL_LINKDMA(hqspi, hmdma, hmdma);
    HAL_MDMA_DeInit(&hmdma);      
    HAL_MDMA_Init(&hmdma);
    HAL_NVIC_SetPriority(MDMA_IRQn, 0x00, 0);
    HAL_NVIC_EnableIRQ(MDMA_IRQn);  
}

/* QSPI initialize */
static void ll_qspi_init(void)
{
    static DMA_HandleTypeDef hdma;

    __HAL_RCC_QSPI_CLK_ENABLE();        

    __HAL_RCC_DMA1_CLK_ENABLE();  
    
    QSPI_Handler.Instance                = QUADSPI;     

    /* D1HCLK(200MHz) / (prescaler + 1) = 200/5 = 40MHZ */
    QSPI_Handler.Init.ClockPrescaler     = 4;                                                                   
    QSPI_Handler.Init.FifoThreshold      = 16;                     
    QSPI_Handler.Init.SampleShifting     = QSPI_SAMPLE_SHIFTING_NONE;
    QSPI_Handler.Init.FlashSize          = POSITION_VAL(0X800000)-1;  
    QSPI_Handler.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_5_CYCLE;
    
    /* clock keep high level in idle time cs inactive */
    QSPI_Handler.Init.ClockMode          = QSPI_CLOCK_MODE_3;        
    QSPI_Handler.Init.FlashID            = QSPI_FLASH_ID_2;              
    QSPI_Handler.Init.DualFlash          = QSPI_DUALFLASH_DISABLE;     
    HAL_QSPI_Init(&QSPI_Handler);

    /* 0xde is the first byte every transmit */
    QSPI_Cmdhandler.Instruction     = 0xde;
    /* qspi 24bit address, lcd command is set in A15~A8 */
    QSPI_Cmdhandler.Address         = 0x000000;
    QSPI_Cmdhandler.DummyCycles     = 0;                             
    QSPI_Cmdhandler.InstructionMode = QSPI_INSTRUCTION_4_LINES;     
    QSPI_Cmdhandler.AddressMode     = QSPI_ADDRESS_4_LINES;    
    QSPI_Cmdhandler.AddressSize     = QSPI_ADDRESS_24_BITS;    
    QSPI_Cmdhandler.DataMode        = QSPI_DATA_NONE;    
    QSPI_Cmdhandler.NbData          = 0;

    QSPI_Cmdhandler.SIOOMode          = QSPI_SIOO_INST_EVERY_CMD;               
    QSPI_Cmdhandler.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;   
    QSPI_Cmdhandler.DdrMode           = QSPI_DDR_MODE_DISABLE;                  
    QSPI_Cmdhandler.DdrHoldHalfCycle  = QSPI_DDR_HHC_ANALOG_DELAY;
}

QSPI DMA传输,H750的CUBE库里可以找到参考范例,注意的是配置速度是40MHz,当然也能更高,但太小的话,帧率太低,会屏抖动,传输设置为指令4线,地址4线(24BIT),默认无数据

二、初始化列表

代码如下:


#define INIT_DAT_LEN        (30)

typedef struct
{
    uint8_t cmd;
    uint8_t len;
    uint8_t dat[INIT_DAT_LEN];
}init_line_t;

/* ST77903 LCD Initialize table */
/* command - parameter length - parameter data */
/* if command is 0xff, means a delay time unit millisecond */
static const init_line_t init_table[] = {
{0xf0,  1, 0xc3},
{0xf0,  1, 0x96},
{0xf0,  1, 0xa5},
{0xe9,  1, 0x20},
{0xe7,  4, 0x80, 0x77, 0x1f, 0xcc},
{0xc1,  4, 0x77, 0x07, 0xc2, 0x07},
{0xc2,  4, 0x77, 0x07, 0xc2, 0x07},
{0xc3,  4, 0x22, 0x02, 0x22, 0x04},
{0xc4,  4, 0x22, 0x02, 0x22, 0x04},
{0xc5,  1, 0x71},
{0xe0, 14, 0x87, 0x09, 0x0c, 0x06, 0x05, 0x03, 0x29, 0x32, 0x49, 0x0f, 0x1b, 0x17, 0x2a, 0x2f},
{0xe1, 14, 0x87, 0x09, 0x0c, 0x06, 0x05, 0x03, 0x29, 0x32, 0x49, 0x0f, 0x1b, 0x17, 0x2a, 0x2f},
{0xe5, 14, 0xb2, 0xf5, 0xbd, 0x24, 0x22, 0x25, 0x10, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22},
{0xe6, 14, 0xb2, 0xf5, 0xbd, 0x24, 0x22, 0x25, 0x10, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22},
{0xec,  2, 0x40, 0x03},
{0x36,  1, 0x0c},
{0x3a,  1, 0x07},
{0xb2,  1, 0x00},
{0xb3,  1, 0x01},
{0xb4,  1, 0x00},
{0xb5,  4, 0x00, 0x08, 0x00, 0x08},
{0xb6,  2, 0xc7, 0x31},
{0xa5,  9, 0x00, 0x00, 0x00, 0x00, 0x20, 0x15, 0x2a, 0x8a, 0x02},
{0xa6,  9, 0x00, 0x00, 0x00, 0x00, 0x20, 0x15, 0x2a, 0x8a, 0x02},
{0xba,  7, 0x0a, 0x5a, 0x23, 0x10, 0x25, 0x02, 0x00},
{0xbb,  8, 0x00, 0x30, 0x00, 0x29, 0x88, 0x87, 0x18, 0x00},
{0xbc,  8, 0x00, 0x30, 0x00, 0x29, 0x88, 0x87, 0x18, 0x00},
{0xbd, 11, 0xa1, 0xb2, 0x2b, 0x1a, 0x56, 0x43, 0x34, 0x65, 0xff, 0xff, 0x0f},
{0x35,  1, 0x00},
{0x21,  0, 0x00},
{0x11,  0, 0x00},
{0xff,  1, 120},
{0x29,  0, 0x00},
{0xff,  1, 120},
#ifdef BIST_MODE
{0xb0,  1, 0xa5},
{0xcc,  9, 0x40, 0x00, 0x3f, 0x00, 0x14, 0x14, 0x20, 0x20, 0x03},
#endif
};

/* LCD initialize */
static void lcd_initialize(void)
{
    uint32_t i;
    LCD_RST_H;
    lcd_dlyms(10);
    LCD_RST_L;
    lcd_dlyms(10);
    LCD_RST_H;
    lcd_dlyms(120);

    init_line_t *init = (init_line_t *)&init_table[0];

    for (i = 0; i < sizeof(init_table)/sizeof(init_line_t); i++)
    {                           /* transmit initialize code */
        if (init->cmd == 0xff)
        {                       /* delay */
            lcd_dlyms(init->dat[0]);
        }
        else
        {                       /* command - parameter */
            lcd_transmit(init->cmd, init->len, init->dat);
        }
        init++;
    }
}

以前显示屏的初始化参数比较少,所以一行指令一行参数,但是现在的初始化越来越长,再用以前格式,既不美观,也不容易找错,所以我们用类似手机平台的代码格式
第一个字节是屏的指令,第二个是后面参数的长度,没有参数的话就填0
第一个字节如果是0XFF,表示此处需要做delay,delay长度就是后面的参数


三、数据传输

第一个关键部位到了,底层QSPI接口建好了,初始化代码填好了,我们要怎么通过QSPI传输代码给显示屏呢,我们来比较下FLASH和LCD的传输时序图
ST77903 QSPI + STM32H750 + RTThread + LVGL(二)ST77903 QSPI + STM32H750 + RTThread + LVGL(二)
ST77903 QSPI + STM32H750 + RTThread + LVGL(二)
从以上比较可知,W25Q和ST77903的写操作,完全相同
ST77903 QPI时序的指令块是0xde,地址块的A15~8是LCD真实指令,数据块是LCD指令参数或RGB数据
这样了解后,ST77903的写入就非常清楚了,甚至比W25Q还要简单非常多。。。。。

代码如下:

/* lcd transmit, write command, write display data */
static void lcd_transmit(uint32_t cmd, uint32_t len, uint8_t *dat)
{
    if (len == 0)              
    {                           /* write command, no parameter */
        QSPI_Cmdhandler.Address  = cmd << 8;                                   
        QSPI_Cmdhandler.DataMode = QSPI_DATA_NONE;    
        QSPI_Cmdhandler.NbData   = 0;
                                /* interrupt mode */
        HAL_QSPI_Command_IT(&QSPI_Handler, &QSPI_Cmdhandler);
    }
    else if (len <= INIT_DAT_LEN)
    {                           /* write command with parameter */
        QSPI_Cmdhandler.Address  = cmd << 8;  
        QSPI_Cmdhandler.DataMode = QSPI_DATA_4_LINES;    
        QSPI_Cmdhandler.NbData   = len;
                                /* interrupt mode */
        HAL_QSPI_Command_IT(&QSPI_Handler, &QSPI_Cmdhandler);        
        HAL_QSPI_Transmit_IT(&QSPI_Handler, dat);
    }
    else
    {                           /* write display data by hbyte length */
        QSPI_Cmdhandler.Address  = cmd << 8;  
        QSPI_Cmdhandler.DataMode = QSPI_DATA_4_LINES;    
        QSPI_Cmdhandler.NbData   = len;
                                /* mdma mode */
        HAL_QSPI_Command(&QSPI_Handler, &QSPI_Cmdhandler, 1000);
        HAL_QSPI_Transmit_DMA(&QSPI_Handler, dat);
    }
                                /* wait trans complete */
    rt_sem_take(&trans_sem, RT_WAITING_FOREVER);
}

LCD 0x11, 0x29等指令是无参数的,会按照len == 0来传输
LCD 0x36,0x3A等指令是带参数的,会按照带数据方式传输
LCD RGB数据是按行传输,带数据400x3=1200字节,所以按DMA方式传输

四、BIST测试模式

ST77903 QSPI + STM32H750 + RTThread + LVGL(二)
ST77903自带BIST模式,打开对应宏定义
代码如下:

//#define BIST_MODE

如果可以看到彩条画面,说明硬件连接,底层接口,指令传输都正确了,因为我们是4线指令传输,如果D1,D2,D3连接有问题,或QSPI初始化有问题,都不会看到彩条

接下来,我们可以构建怎么传输frame buffer中内容到显示屏了。。。。。

五、Frame Buffer传输

ST77903 QSPI + STM32H750 + RTThread + LVGL(二)
开始之前,我们先基本了解下帧传输的要求
1)每一帧开始发一个VS指令包0x61,时间周期大于40us
2)每一行开始发数个HS指令包0x60,时间周期大于40us
3)一个0x60跟随一整行数据1200字节,时间周期大于40us
4)一帧结束后,后面要有一个1~100ms的延时
代码如下:

/* lcd display frame thread */
static void lcdqspi_thread_entry(void * parameter)
{
    __IO uint32_t i = 0;

    /* hardware qspi initialize */
    ll_qspi_init();

    /* lcd driver st77903 initialize */
    lcd_initialize();

    /* lcd display frame with cache pixel data */
    while (1)
    {
        /* TE signal */
        rt_timer_start(&te_timer);
    
        /* vs(0x61) packet */
        for (i = 0; i < LCD_VSW; i++)
        {
            lcd_transmit(0x61, 0, NULL); 
            lcd_dlyus(40);
        }
        
        /* hbp(0x60) packet */
        for (i = 0; i < LCD_HBP; i++)
        {
            lcd_transmit(0x60, 0, NULL); 
            lcd_dlyus(40);
        }
        
        /* transmit display cache data to lcd line by line */
        
        for (i = 0; i < LCD_Y_SIZE; i++)
        {
            lcd_transmit(0x60, LCD_HBYTE, (uint8_t *)&frame_cache[i][0]);
        }

        /* hfp(0x60) packet */
        for (i = 0; i < LCD_HFP; i++)
        {
            lcd_transmit(0x60, 0, NULL); 
            lcd_dlyus(40);
        }

        /* transmit is complet, can update frame cache in blanking time */
        lcd_dlyms(FRAME_BLANKING_TIME);
    }
}

rt_timer可以调节切线位置,类似ST7789 WITH RAM的TE信号

FRAME_BLANKING可以让线程把CPU占用让出来,让CPU做些其他事情。。。。。读读sensor,睡个觉也好啊!

具体设多少,可以综合考量,短一点可以提高帧率,得到更好的效果,但是有可能UI线程来不及更新完framebuffer,又进入刷屏,造成无法消除的切线,太长的话,帧率肯定会太低,CPU是轻松了些,但是很可能造成屏抖动

硬件飞线时候,我们有飞出一个GPIO,这时可以派上用处了。。。。。
代码如下:

static void test_rgb_pattern(void)
{
     lcdqspi_wait_te();
     LCD_TE_L;
     lcdqspi_clear(0xff0000);
     LCD_TE_H;
     rt_thread_delay(1000); 
     
     lcdqspi_wait_te();
     LCD_TE_L;
     lcdqspi_clear(0x00ff00);
     LCD_TE_H;
     rt_thread_delay(1000);
    
     lcdqspi_wait_te();
     LCD_TE_L;
     lcdqspi_clear(0x0000ff);
     lcdqspi_fill_block(200, 0, 200, 399, 0xff0000);
     lcdqspi_fill_block(0, 200, 399, 200, 0x000000);
     
     LCD_TE_H;
     rt_thread_delay(1000); 
}

测试这个LCD_TE GPIO的低电平宽度,就可以知道我们实际更新一帧BUFFER需要时间长度,实测是3~4ms,所以FRAME_BLANKING_TIME差不多大于5ms比较保险,当前是设定了10ms

六、实测效果

ST77903 QSPI + STM32H750 + RTThread + LVGL(二)
使用手机相机慢动作录像,可以看到TE是否使用的不同效果
1,2未使用TE同步,看到随机切屏
3,4打开TE同步,看到固定位置切屏
5,6调整TE同步参数,切屏消失

ST77903 QSPI + STM32H750 + RTThread + LVGL(二)
以上是一帧传输的完整时序,约37ms
ST77903 QSPI + STM32H750 + RTThread + LVGL(二)
以上是一帧的开始

ST77903 QSPI + STM32H750 + RTThread + LVGL(二)
以上是一行的开始

七、驱动接口

void lcdqspi_set_pixel(uint32_t x, uint32_t y, uint32_t color)
{
    if ((x < LCD_X_SIZE)&&(y < LCD_Y_SIZE))
    {
        uint32_t xoft = x * 3;
        frame_cache[y][xoft + 2] = color & 0xff;
        frame_cache[y][xoft + 1] = (color & 0xff00) >> 8;
        frame_cache[y][xoft + 0] = (color & 0xff0000) >> 16;
    }
}

void lcdqspi_fill_block(uint32_t x0, uint32_t y0, uint32_t x1, uint32_t y1, uint32_t color)
{
    if ((x1 < LCD_X_SIZE)&&(y1 < LCD_Y_SIZE)&&(x1 >= x0)&&(y1 >= y0))
    {
        uint32_t x,y;
        for (y = y0; y <= y1; y++)
        {        
            uint32_t oft = x0 * 3;
            for (x = x0; x <= x1; x++)
            {
                frame_cache[y][oft + 2] = color & 0xff;
                frame_cache[y][oft + 1] = (color & 0xff00) >> 8;
                frame_cache[y][oft + 0] = (color & 0xff0000) >> 16;
                oft += 3;
            }
        }
    }
}

void lcdqspi_clear(uint32_t color)
{
    lcdqspi_fill_block(0, 0, LCD_X_SIZE - 1, LCD_Y_SIZE - 1, color);
}

void lcdqspi_draw_line(uint32_t x0, uint32_t x1, uint32_t y, uint32_t *pixel)
{
    if ((x1 < LCD_X_SIZE)&&(y < LCD_Y_SIZE)&&(x1 >= x0))
    {
        uint32_t  oft = x0 * 3;
        uint32_t *dat = pixel;
        for (uint32_t x = x0; x <= x1; x++)
        {
            frame_cache[y][oft + 2] = *dat & 0xff;
            frame_cache[y][oft + 1] = (*dat & 0xff00) >> 8;
            frame_cache[y][oft + 0] = (*dat & 0xff0000) >> 16;
            oft += 3;
            dat++;
        }
    }
}

void lcdqspi_set_te(uint32_t offset)
{
    te_offset = offset + 1;
    rt_timer_stop(&te_timer);
    rt_timer_control(&te_timer, RT_TIMER_CTRL_SET_TIME, (void *)&te_offset);
}

void lcdqspi_wait_te(void)
{
    /* if already done, clear flag */
    rt_completion_wait(&frame_sync, 0);
    /* wait a new flag for update sync */
    rt_completion_wait(&frame_sync, RT_WAITING_FOREVER);
}

驱动接口API,供中间层UI库调用,可以按需求修改或增加

八、测试代码


static void test_rgb_pattern(void)
{
     lcdqspi_wait_te();
     LCD_TE_L;
     lcdqspi_clear(0xff0000);
     LCD_TE_H;
     rt_thread_delay(1000); 
     
     lcdqspi_wait_te();
     LCD_TE_L;
     lcdqspi_clear(0x00ff00);
     LCD_TE_H;
     rt_thread_delay(1000);
    
     lcdqspi_wait_te();
     LCD_TE_L;
     lcdqspi_clear(0x0000ff);
     lcdqspi_fill_block(200, 0, 200, 399, 0xff0000);
     lcdqspi_fill_block(0, 200, 399, 200, 0x000000);
     
     LCD_TE_H;
     rt_thread_delay(1000); 
}

static void test_entry(void* parameter)
{  
    rt_tick_t tick0,tick1;
    lcdqspi_wait_te();
    
    while (1)
    {    
        test_rgb_pattern();
        #if 0
        LCD_TE_H;
        lcdqspi_wait_te();
        //tick0 = rt_tick_get();
        LCD_TE_L;
        lcdqspi_clear(0xff0000);
        LCD_TE_H;

        //tick1 = rt_tick_get();
       // rt_kprintf("clear frame cost - %d\r\n", (tick1 > tick0)?(tick1 - tick0):(tick0 - tick1));
        rt_thread_delay(1000); 
        
        lcdqspi_wait_te();
        
        LCD_TE_L;
        lcdqspi_clear(0x00ff00);
        LCD_TE_H;

        rt_thread_delay(1000);

        lcdqspi_wait_te();
        LCD_TE_L;
        lcdqspi_clear(0x0000ff);
        LCD_TE_H;
        rt_thread_delay(1000); 
        #endif
    }
}


static int test_init(void)
{
	rt_thread_t th1 = rt_thread_create("test", test_entry, RT_NULL, 
								4096, THREAD_TEST_PRIO, THREAD_TEST_TICK);
	if (th1 != RT_NULL)rt_thread_startup(th1);

	return 0;
}

INIT_APP_EXPORT(test_init);

总结

我们现在完成了ST77903 QSPI驱动层的设计,后续会导入LVGL,做一个实用一些的UI层的示例。

上一篇:STM32硬件CRC计算CRC8


下一篇:1334【毕设课设】基于单片机嵌入式的家用智能节水淋浴控制器的设计