项目使用正点原子STM32F767阿波罗开发板+IPS TFT-LCD屏幕(非正点原子屏幕)进行开发,应用层移植轻量级GUI库LVGL实现列表按键点击,切换,显示当前内部工作电压功能。重点在于对于RGB屏幕驱动普适性的学习和LVGL开发的理解。希望我实习期间的项目学习过程能对大家有所帮助。
一.RGB LCD屏幕开发所需基础知识
1.颜色格式及帧缓冲区大小
与接线较少的MCU屏幕不同,RGB屏幕采用并口连接通常像素点颜色数据传输采用RGB888,RGB565,RGB666,ARGB8888等格式进行传输。这意味着开发过程中如果不使用专用LCD驱动IC,在MCU选择过程中应选择引脚数足够的MCU进行开发。
另外,对于屏幕开发,我们在MCU选型过程中还应注意所开发屏幕显示过程中需要占用的显存,并选择足够RAM的MCU或外扩SDRAM。其中RGB LCD的占用显存是这样计算出来的:
以一块分辨率为240*320的屏幕为例:屏幕在显示过程中经过设定可选择为从左到右,从上到下的扫描方式(或者通过改变驱动选择其他方式),LCD控制器将从一块专门的帧缓冲区(frame buffer)中将各像素点的颜色数据取出并在屏幕上呈现。每一个像素都由R(红色)G(绿色)B(蓝色)三种颜色的数据组合而成。我们之前提到的RGB565/RGB888等即为各颜色在一个像素点里所占的位数(bit)大小,项目中使用的屏幕为RGB565格式,即为红色数据占5bits,绿色占6bits,蓝色占5bits。
5bits+6bits+5bits=16bits=2bytes 即为RGB565接口格式屏幕每个像素显示需要占用2字节(bytes)大小空间。分辨率240*320意思就是一帧完整的图像显示需要显示240*320个像素点。一般项目中需要较为流畅的显示,项目中为帧缓冲区预留了一帧图像大小的显存,所占用的RAM=2bytes*240*320=153.6KB
2.屏幕刷新率
我们常说的屏幕刷新率,60Hz,144Hz等等意思显而易见,结尾屏幕在1s内显示了多少帧图像。在屏幕厂家的datasheet没有描述时,我们该如何计算呢?这就要去看datasheet中的时钟频率了。
项目中的屏幕的时钟频率为6.49Mhz,意为在1s中有6490000次时钟脉冲,每次对应一次像素数据的传输,datasheet中的如图两个数据前者为一次行扫描所需时钟数,后者为一次帧扫描所需行扫描数
它们的乘积即为显示一帧图像的时钟数,用6490000/(320*338)=60Hz即为屏幕的刷新率。
二.方案及硬件连接
在实际开发中如不外加LCD驱动IC,使用MCU直接驱动的方式需要尽量选择内部带有LCD控制器的MCU,模拟时序的方式既不方便又不能很快发现问题。项目使用的STM32F767开发板内部带有专用LCD控制器LTDC和图形加速器DMA2D。很方便进行开发。以下为本项目的硬件连接,其它屏幕开发时需注意接口对应的MCU LCD控制器引脚,并且注意为背光引脚供电。
三.代码部分注意事项
我使用CUBEMX+HAL的方式直接进行开发,详细代码及配置可自行下载。下面我来讲述一下在开发过程中需要注意的点。
1.LTDC屏幕参数配置
在配置cubemx的ltdc时需要严格对应datasheet中的lcd显示参数的值,包括vd,vbl,hd,hbl等。
2.LCD驱动编写
利用DMA2D外设,编写LCD控制器绘制矩形函数,并且注意函数参数的设定之后要与LVGL的API接口相对应。实际上我们使用LVGL库时,LCD的驱动代码只需要写这一个即可,如果想自己写类似于画圆、画点做的函数、画线也可以加进去。
void LTDC_Color_Fill(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey,uint16_t *color)
{ uint32_t timeout=0;
uint32_t psx,psy,pex,pey;
uint16_t offline;
uint32_t addr;
psx=sx;psy=sy;
pex=ex;pey=ey;
offline=240-(pex-psx+1);
RCC->AHB1ENR|=1<<23; //使能DM2D时钟
DMA2D->CR=0<<16; //存储器到存储器模式
DMA2D->FGPFCCR=LCD.ColorMode; //设置颜色格式
DMA2D->FGOR=0; //前景层行偏移为0
DMA2D->OOR=offline; //设置行偏移
DMA2D->CR&=~(1<<0); //先停止DMA2D
DMA2D->FGMAR=(uint32_t)color; //源地址
DMA2D->OMAR=LCD.LayerMemoryAdd + LCD.BytesPerPixel*(LCD_Width * sy + sx); //输出存储器地址
DMA2D->NLR=(pey-psy+1)|((pex-psx+1)<<16); //设定行数寄存器
DMA2D->CR|=1<<0; //启动DMA2D
while((DMA2D->ISR&(1<<1))==0) //等待传输完成
{
timeout++;
if(timeout>0X1FFFFF)break; //超时退出
}
DMA2D->IFCR|=1<<1; //清除传输完成标志
}
3.LVGL相关API
(1)lv_port_disp.c
在配置中需要注意刷新的方式,我选择了一次刷新一整屏,实际调试时才不会出现卡顿,在其它屏幕开发时需要根据实际而定。
在此驱动与我们的LCD驱动需要对应的API为如下函数,需要于我们自己驱动的绘制矩形函数相对应,建议使用DMA2D绘制。
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
这里的函数里只需加入之前写好的绘制矩形函数并添加函数lv_disp_flush_ready(disp_drv);
LTDC_Color_Fill(area->x1,area->y1,(area->x2)-(area->x1),(area->y1)-(area->y2),(uint16_t*)color_p);
这里的area结构体中的(x1,y1),(x2,y2)分别对应绘制矩形区域的左上角和右下角,写入参数时需严格对应自己之前编写的参数值。
(2)lv_port_indev.c
在调试中因为屏幕非触屏且开发板仅有4个按键,我在相关api中选择了以按键对应坐标点的方式对应屏幕位置,在其它开发调试中可针对实际需要完善方案.
static const lv_point_t btn_points[4] = {
{10, 10}, /*Button 0 -> x:10; y:10*/
{80, 70}, /*Button 1 -> x:80; y:70*/
{180,10}, /*button 2 -> x:180; y:10*/
{80,100} /*button 3 -> x:80; y:100*/
};
lv_indev_set_button_points(indev_button, btn_points);
4.LVGL编程
在LVGL编程中要善用c++思想父类子类的关系,我认为这也是LVGL开发的关键之处。还需要将相关事件和功能底层函数对应起来,实现屏幕显示和具体功能的对应。
5.LVGL心跳时钟配置
了解过LVGL后大家应该知道,在LVGL的实际运行时,我们需要为其配置心跳始终才能正常运行,在这里我们使用定时器中断来为LVGL配置心跳。并将lv_tick_inc()函数在定时器的中断回调函数中调用。
在初始化过程中我们需要注意的是,如果使用的是CUBEMX进行的定时器配置,我们在初始化函数中需要启动定时器的计时。只进行初始化是没有用的。
HAL_TIM_Base_Start_IT(&htim3);
四.调试结果
使用官方demo提供和的背景图片,对LVGL的基本功能进行了简单的运用并和外设驱动实际联系起来。