关于驱动中景园LCD和LVGL踩的一些坑
中景园LCD和LVGL的驱动
背景介绍
我使用的是中景园的1.3寸,240*240的LCD,主控是STM32L152系列
https://item.taobao.com/item.htm?spm=3688y.1.14.16.1916264bJ5QnqC&id=565591692266&ns=1&abbucket=2#detail
1、第一坑-驱动
开始以为驱动这个屏幕应该不难,因为有例程,结果第一坑就来了。我使用的是STM32L152系列,刚好有个PB12~PB15这对SPI2引脚引出,就把屏幕接到了这个上,但是没有使用SPI功能,只是作为普通IO口用。于是移植了例程中STM32F103系列的到STM32L152上,但是就是无法驱动,始终黑屏。
没办法,重新换管脚到PA0~PA4上,结果立马正常显示了。
目前没有找到问题,只能怀疑PB引脚驱动力不够
第二坑-LVGL无法显示
搞定驱动后就要移植LVGL。
此处是参考这个大神的教程,写的真的很棒。
https://blog.csdn.net/weixin_42111891/article/details/124989266
移植完毕后发现还是无法驱动,是花屏。
猜测是因为我的disp_flush()中填的那个驱动函数不对。
于是查看驱动代码发现
大神用的是正点原子的屏幕。所用的填充函数如下:
注意最后一个参数是u16*color是一个指针。
//在指定区域内填充指定颜色块
//(sx,sy),(ex,ey):填充矩形对角坐标,区域大小为:(ex-sx+1)*(ey-sy+1)
//color:要填充的颜色
void LCD_Color_Fill(u16 sx, u16 sy, u16 ex, u16 ey, u16 *color)
{
u16 height, width;
u16 i, j;
width = ex - sx + 1; //得到填充的宽度
height = ey - sy + 1; //高度
for (i = 0; i < height; i++)
{
LCD_SetCursor(sx, sy + i); //设置光标位置
LCD_WriteRAM_Prepare(); //开始写入GRAM
for (j = 0; j < width; j++)
{
LCD->LCD_RAM=color[i * width + j]; //写入数据
}
}
}
而中景园自带的驱动中只有这两个函数
LCD_Fill只能填单个颜色块。
void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color);
LCD_ShowPicture只能绘制8字节的图片数组。
void LCD_ShowPicture(u16 x,u16 y,u16 length,u16 width,const u8 pic[])
所以必须要重新写一个。
一开始是仿照正点原子的驱动去写,但是怎么修改都是花屏。
于是开始找资料,但是网上用这块LCD的都是ESP32,也只能看看这个资料看看他们是怎么驱动的。
于是找到了这个老哥的资料
https://www.bilibili.com/video/av847784236/
https://gitee.com/gsm-wheather-project
下载查看代码后,找到这个函数
void LCD_Fill_Colors(u16 xsta,u16 ysta,u16 xend,u16 yend,lv_color_t* color_p)
{
uint32_t x=0,y=0;
uint16_t width = (xend-xsta+1);
uint16_t height = (yend-ysta+1);
long len=width*height;
//xSemaphoreTakeRecursive(_spi_mux, portMAX_DELAY);
#if 0
LCD_Address_Set(xsta,ysta,xend,yend);//设置显示范围
for(y = 0; y <width*height; y++)
{
LCD_WR_DATA(color_p->full);
color_p++;
}
for(y = ysta; y <= yend; y++) {
for(x = xsta; x <= xend; x++) {
LCD_DrawPoint(x, y, (color_p->full));
color_p++;
}
}
#else
#define lcd_spi_dat_len 50
esp_err_t ret;
spi_transaction_t t;
int cnt=0,i=0;
int tx_mode=0;
int tx_len = lcd_spi_dat_len;
int break_cnt=0;
uint16_t buf[lcd_spi_dat_len];
memset(buf,RED,lcd_spi_dat_len);
LCD_Address_Set(xsta,ysta,xend,yend);//设置显示范围
if(len<lcd_spi_dat_len)
{
tx_len = len;
tx_mode=1;
}
while(1)
{
for(i=0;i<tx_len;i++)
{
buf[i]=SWAPBYTES(color_p[cnt*tx_len+i].full);
}
memset(&t, 0, sizeof(t));
t.length=2*8*tx_len;
t.tx_buffer=&buf[0];
t.user=(void*)0;
ret=spi_device_transmit(lcd_spi, &t);
if(tx_mode==1)
break;
cnt++;
if(len>lcd_spi_dat_len)
{
len-=tx_len;
}else
{
tx_len=len;
len-=tx_len;
if(++break_cnt>1)
break;
}
}
#endif
//xSemaphoreGiveRecursive(_spi_mux);
}
于是稍加改造,得到
void LCD_Color_Fill(u16 sx, u16 sy, u16 ex, u16 ey, lv_color_t *color)
{
u16 i,j;
u32 k=0;
uint32_t x=0,y=0;
u16 height, width;
width = ex - sx + 1; //得到填充的宽度
height = ey - sy + 1; //高度
LCD_Address_Set(sx,sy,ex,ey);
for(y = 0; y <width*height; y++)
{
LCD_WR_DATA(color->full);
color++;
}
}
注意,在LVGL中使用也要修改如下。
/*Flush the content of the internal buffer the specific area on the display
*You can use DMA or any hardware acceleration to do this operation in the background but
*'lv_disp_flush_ready()' has to be called when finished.*/
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
LCD_Color_Fill(area->x1,area->y1,area->x2,area->y2,color_p);
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
到此,LVGL和中景园的LCD可以驱动起来。但是目前还存在刷新速度很慢的问题。还需要继续解决,目前先记录。
刷新速度慢的问题
需要解释一下上面函数LCD_Color_Fill的最后一个参数lv_color_t *color的问题,关于这个lv_color_t 结构体解释如下
再LVGL中单个像素的位深有好几种定义(1,8,16,32位),可以在初始化时进行设定,
也就是一个像素占多少位。
比如8位和16位的定义如下,
typedef union {
struct {
uint8_t blue : 2;
uint8_t green : 3;
uint8_t red : 3;
} ch;
uint8_t full;
} lv_color8_t;
typedef union {
struct {
#if LV_COLOR_16_SWAP == 0
uint16_t blue : 5;
uint16_t green : 6;
uint16_t red : 5;
#else
uint16_t green_h : 3;
uint16_t red : 5;
uint16_t blue : 5;
uint16_t green_l : 3;
#endif
} ch;
uint16_t full;
} lv_color16_t;
一个lv_color16_t是一个联合体,CH部分和full部分其实是一体的,也就是一个full占16位,green_h 占full的前3位,red占接下去的5位,一共是3+5+5+3=16,那么简单来看lv_color16_t就是一个uint16_t的一个数组,即要填入的像素缓存的数组。
还需要解释一下lv_color_t 和实际的lv_color16_t对应关系
typedef LV_CONCAT3(lv_color, LV_COLOR_DEPTH, _t) lv_color_t;
上面的定义就是把lv_color_t按照LV_COLOR_DEPTH所定义的去对应相应位深的结构体。
其实就是你定义了16位,那么lv_color_t就等同于lv_color16_t。
那么我们在disp_flush函数中只要这样使用
LCD_Color_DMAFill(area->x1,area->y1,area->x2,area->y2,color_p);
需要注意的是因为DMA使用的是8位,所以使用DMA写入时需要注意高低位的先后顺序,如果写错了就容易花屏,下面的LCD_Color_DMAFill函数内部开辟了一个行的缓存用于交换高低位,同时需要注意的是,DMA写入是的数量也要乘以2。
这番操作下来,可以使得原本需要一个个写入数据可以由DMA直接写入一整个数组,效率可以大大的提升。
以下是参考程序段。
//DMA 初始化函数
void SPI1_DMA_Init(void)
{
DMA_InitTypeDef DMA_InitStructure;
// 开启 DMA1 时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置 DMA1 Channel3 (SPI1_TX)
DMA_DeInit(DMA1_Channel3);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; // SPI1 数据寄存器
DMA_InitStructure.DMA_MemoryBaseAddr = 0; // 发送缓冲区地址,稍后动态设置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 内存->外设
DMA_InitStructure.DMA_BufferSize = 0; // 传输大小,稍后动态设置
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址固定
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址自增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 普通模式(传完停)
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 不是内存到内存
DMA_Init(DMA1_Channel3, &DMA_InitStructure);
// 使能 SPI1 TX DMA 请求
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
}
//DMA传输函数
void SPI1_DMA_Send(uint8_t *pBuffer, uint16_t size)
{
// 关闭通道,重新配置+
DMA_Cmd(DMA1_Channel3, DISABLE);
DMA1_Channel3->CMAR = (uint32_t)pBuffer; // 内存首地址
DMA1_Channel3->CNDTR = size; // 数据长度
// 开启通道
DMA_Cmd(DMA1_Channel3, ENABLE);
// 等待传输完成
while(DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC3);
}
void LCD_Color_DMAFill(u16 sx, u16 sy, u16 ex, u16 ey, lv_color_t *color)
{
u16 width = ex - sx + 1;
u16 height = ey - sy + 1;
static uint16_t dma_buf[240]; // 一行缓冲,按你屏宽修改
LCD_Address_Set(sx, sy, ex, ey);
for(u16 y = 0; y < height; y++)
{
for(u16 x = 0; x < width; x++)
{
dma_buf[x] = (color[y*width + x].full >> 8) | (color[y*width + x].full << 8); // 高低字节交换,因为16为数据有写入的先后顺序,
}
SPI1_DMA_Send((uint8_t*)dma_buf, width*2); // 分行 DMA
}
}
//LVGL的显示更新函数
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
LCD_Color_DMAFill(area->x1,area->y1,area->x2,area->y2,(uint8_t*)color_p);
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
继续提高刷新速度
目前有2中方式,一种是直接按照像素一个个写入,第二种是用DMA写入,但是DMA写入原本可以传输一整块数据,但是因为上文提到的大小端存储问题导致颜色失常问题,现在提供第三种方式
1.修改LVGL16为数据交换定义
lv_conf.h文件中把LV_COLOR_16_SWAP 0改为1
/Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)/
#define LV_COLOR_16_SWAP 1
2.写入函数改写
void LCD_Color_DMAFill2(u16 sx, u16 sy, u16 ex, u16 ey, uint8_t *color)
{
u16 width = ex - sx + 1;
u16 height = ey - sy + 1;
LCD_Address_Set(sx, sy, ex, ey);
SPI1_DMA_Send(color, width*height*2); // 分行 DMA
}
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
//if(disp_flush_enabled) {
/*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
LCD_Color_DMAFill2(area->x1,area->y1,area->x2,area->y2,(uint8_t*)color_p); //填充区域函数,需要强制转换数据为uint8_t*
//}
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
这样的话是会比上面的按行刷新会更快一点。
但是如果你使用squareline产生的文件会报错,他会检查LV_COLOR_16_SWAP 是不是0,
#if LV_COLOR_16_SWAP !=0
#error “LV_COLOR_16_SWAP should be 0 to match SquareLine Studio’s settings”
#endif
你可以在工程配置里面的depth里选择16bit swap,这样会检查是不是为1,就不会报错,你直接改成1也行,但是每次生成都要重新改一下。
#if LV_COLOR_16_SWAP !=1
#error “LV_COLOR_16_SWAP should be 0 to match SquareLine Studio’s settings”
#endif
第三坑 编译警告坑
移植后编译会有报错和警告,
报错一般是找不到这个路径include “…/…/lv_conf.h”,若头文件#include "lvgl/lvgl.h"包含报错,可以添加宏定义LV_CONF_INCLUDE_SIMPLE。
警告是warning: #188-D: enumerated type mixed with another type lv_slider_set_。。。。。。。
这个需要在魔术棒的那个设置中添加–diag_suppress=188,546,68,111,含义就是消除警告编号为188,546.68,11这这类警告,并不是解决了警告,而是屏蔽。
第四坑使用DMA后花屏的坑
之前为了改善刷新速度想使用DMA进行刷新,实际填充同色时没有问题,但是使用LVGL时出现了花屏问题,原因就在于数据在内存中存放的格式问题。
首先不使用DMA刷新时,程序如下
假设有两个16为数据要传输,0x1234,0x5678,
void LCD_WR_DATA(u16 dat)
{
LCD_Writ_Bus(dat>>8);
LCD_Writ_Bus(dat);
}
可以看到SPI数据的出去的顺序为0x12,0x34,0x56,0x78
但是DMA传输时就会有问题,因为DMA我使用的是8位的宽度,那么因为单片机小端存储的特点,实际内存中存储的顺序是0x34,0x12,0x78,0x56,那么按照DMA8位宽度去取的话,会导致实际的传输顺序为0x34,0x12,0x78,0x56,造成花屏或者颜色不对。
所以必须要在传输之前把高低位手动调换过来
ESP32+TFT_eSPI+LVGL
本项目基于VSCode+Platform
首先创建一个ESP32的工程,然后在Platform主页的libraries中搜索TFT_eSPI,并把他添加到工程中,然后首先先驱动LCD屏幕确保屏幕没问题。
参考链接 https://www.jianshu.com/p/8631a10b5533
添加LVGL,也是在在Platform主页的libraries中搜索LVGL,添加到工程中。
驱动的方式可以参考下文连接,但是不用做1,2条,因为可以直接添加LGVL到工程中,我已经尝试成功。
https://blog.csdn.net/weixin_41711422/article/details/126354263
显示第一帧画面后无反应
1、主函数循环中是否有添加lv_task_handler(); // lvgl的事务处理
2、在定时器中断中要添加lv_tick_inc(1);//lvgl的1ms中断,并且要确认定时器是可以正常工作的,否则LVGL的任务不会开始调用。
简单参考程序
更多推荐



所有评论(0)