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的传输时序图
从以上比较可知,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自带BIST模式,打开对应宏定义
代码如下:
//#define BIST_MODE
如果可以看到彩条画面,说明硬件连接,底层接口,指令传输都正确了,因为我们是4线指令传输,如果D1,D2,D3连接有问题,或QSPI初始化有问题,都不会看到彩条
接下来,我们可以构建怎么传输frame buffer中内容到显示屏了。。。。。
五、Frame Buffer传输
开始之前,我们先基本了解下帧传输的要求
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
六、实测效果
使用手机相机慢动作录像,可以看到TE是否使用的不同效果
1,2未使用TE同步,看到随机切屏
3,4打开TE同步,看到固定位置切屏
5,6调整TE同步参数,切屏消失
以上是一帧传输的完整时序,约37ms
以上是一帧的开始
以上是一行的开始
七、驱动接口
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层的示例。