针对常见的LCD屏幕,比如IL9341、ST7789、NT5510等,这里以ST7789S为例,分享一下LCD刷新速度的改进方案。
一、硬件方式
使用DMA方式传递数据,再使用信号量进行通知。
常见的SPI、I2C接口,都用对应的DMA(内存到外设)方式可以选择,对于8080接口(即MCU的FMC接口)可以使用DMA(内存到内存)方式实现传输。对于RGB接口,则可以使用DMA2D。
二、软件方式
这是着重要说的,正点原子等开发板的例程的显示效率是比较低的。
1)更高效快速的显示单字符
比如下面的 LCD_ShowChar 显示单个字符函数,每画一个像素点,都需要写2次寄存器和4次数据。而 LCD_Fill 函数是用来填充区域的,只需要设定光标位置,然后按照顺序写入数据即可。
假设 显示字符大小为24*28,则用例程的LCD_ShowChar函数至少需要2*672次寄存器和4*672次数据,而用 LCD_Fill 函数只需要3次寄存器+4次数据+672次数据,效率提升了(672*6 - 679)/ 672*6 = 83% !!!
那么我们就可以基于此改造一下 LCD_ShowChar 函数。对于画线、画矩形、画圆这种空心图形,只能单点逐个操作,我们沿用这种方式,对于画字符、实现矩形、实心圆这种实心图像,我们应该用这种方式来操作。
//快速画点
//x,y:坐标
//color:颜色
void LCD_Fast_DrawPoint(u16 x,u16 y,u32 color)
{
if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0x7789)
{
LCD_WR_REG(lcddev.setxcmd);
LCD_WR_DATA(x>>8);LCD_WR_DATA(x&0XFF);
LCD_WR_REG(lcddev.setycmd);
LCD_WR_DATA(y>>8);LCD_WR_DATA(y&0XFF);
}else if(lcddev.id==0X5510)
{
LCD_WR_REG(lcddev.setxcmd);LCD_WR_DATA(x>>8);
LCD_WR_REG(lcddev.setxcmd+1);LCD_WR_DATA(x&0XFF);
LCD_WR_REG(lcddev.setycmd);LCD_WR_DATA(y>>8);
LCD_WR_REG(lcddev.setycmd+1);LCD_WR_DATA(y&0XFF);
}else if(lcddev.id==0X1963)
{
if(lcddev.dir==0)x=lcddev.width-1-x;
LCD_WR_REG(lcddev.setxcmd);
LCD_WR_DATA(x>>8);LCD_WR_DATA(x&0XFF);
LCD_WR_DATA(x>>8);LCD_WR_DATA(x&0XFF);
LCD_WR_REG(lcddev.setycmd);
LCD_WR_DATA(y>>8);LCD_WR_DATA(y&0XFF);
LCD_WR_DATA(y>>8);LCD_WR_DATA(y&0XFF);
}
LCD->LCD_REG=lcddev.wramcmd;
LCD->LCD_RAM=color;
}
//在指定位置显示一个字符
//x,y:起始坐标
//num:要显示的字符:" "--->"~"
//size:字体大小 12/16/24/32
//mode:叠加方式(1)还是非叠加方式(0)
void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode)
{
u8 temp,t1,t;
u16 y0=y;
u8 csize=(size/8+((size%8)?1:0))*(size/2); //得到字体一个字符对应点阵集所占的字节数
num=num-' ';//得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库)
for(t=0;t<csize;t++)
{
if(size==12)temp=asc2_1206[num][t]; //调用1206字体
else if(size==16)temp=asc2_1608[num][t]; //调用1608字体
else if(size==24)temp=asc2_2412[num][t]; //调用2412字体
else if(size==32)temp=asc2_3216[num][t]; //调用3216字体
else return; //没有的字库
for(t1=0;t1<8;t1++)
{
if(temp&0x80)LCD_Fast_DrawPoint(x,y,POINT_COLOR);
else if(mode==0)LCD_Fast_DrawPoint(x,y,BACK_COLOR);
temp<<=1;
y++;
if(y>=lcddev.height)return; //超区域了
if((y-y0)==size)
{
y=y0;
x++;
if(x>=lcddev.width)return; //超区域了
break;
}
}
}
}
//设置光标位置(对RGB屏无效)
//Xpos:横坐标
//Ypos:纵坐标
void LCD_SetCursor(u16 Xpos, u16 Ypos)
{
if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0x7789)
{
LCD_WR_REG(lcddev.setxcmd);
LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF);
LCD_WR_REG(lcddev.setycmd);
LCD_WR_DATA(Ypos>>8);LCD_WR_DATA(Ypos&0XFF);
}else if(lcddev.id==0X1963)
{
if(lcddev.dir==0)//x坐标需要变换
{
Xpos=lcddev.width-1-Xpos;
LCD_WR_REG(lcddev.setxcmd);
LCD_WR_DATA(0);LCD_WR_DATA(0);
LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF);
}else
{
LCD_WR_REG(lcddev.setxcmd);
LCD_WR_DATA(Xpos>>8);LCD_WR_DATA(Xpos&0XFF);
LCD_WR_DATA((lcddev.width-1)>>8);LCD_WR_DATA((lcddev.width-1)&0XFF);
}
LCD_WR_REG(lcddev.setycmd);
LCD_WR_DATA(Ypos>>8);LCD_WR_DATA(Ypos&0XFF);
LCD_WR_DATA((lcddev.height-1)>>8);LCD_WR_DATA((lcddev.height-1)&0XFF);
}else if(lcddev.id==0X5510)
{
LCD_WR_REG(lcddev.setxcmd);LCD_WR_DATA(Xpos>>8);
LCD_WR_REG(lcddev.setxcmd+1);LCD_WR_DATA(Xpos&0XFF);
LCD_WR_REG(lcddev.setycmd);LCD_WR_DATA(Ypos>>8);
LCD_WR_REG(lcddev.setycmd+1);LCD_WR_DATA(Ypos&0XFF);
}
}
//开始写GRAM
void LCD_WriteRAM_Prepare(void)
{
LCD->LCD_REG=lcddev.wramcmd;
}
//在指定区域内填充单个颜色
//(sx,sy),(ex,ey):填充矩形对角坐标,区域大小为:(ex-sx+1)*(ey-sy+1)
//color:要填充的颜色
void LCD_Fill(u16 sx,u16 sy,u16 ex,u16 ey,u32 color)
{
u16 i,j;
u16 xlen=0;
xlen=ex-sx+1;
for(i=sy;i<=ey;i++)
{
LCD_SetCursor(sx,i); //设置光标位置
LCD_WriteRAM_Prepare(); //开始写入GRAM
for(j=0;j<xlen;j++)LCD->LCD_RAM=color; //显示颜色
}
}
修改如下:
新增注释:这里着重说明下,以这种方式显示字符,要保证LCD设置的扫描方式和字模方向一致的!!!比如LCD扫描方向是从左到右再从从上到下的逐行式扫描,那么取字模时也要按照这个方向来,否则显示的就是错的。
//用于显示标准的95个Ascii字符 从空格到~
//入参:x,y为起始坐标,ch为字符,color为画点颜色,back为背景色
void LCD_ShowEnglish(uint16_t x, uint16_t y, const char ch, uint16_t color, uint16_t back)
{
uint8_t n, bit, num, temp;
num = ch - ' ';//获取字符索引
LCD_SetCursor(x, y, x+FONT_WIDTH_HALF, y+FONT_HEIGHT);//设置光标位置
for(n = 0;n < FONT_BYTE_NUM_HALF; n++)
{
temp = font_28[num*FONT_BYTE_NUM_HALF + n];
for( bit = 0; bit < 8; bit++)
{
if(temp&0x80) //写入像素点数据
LCD_WR_DATA(color);
else
LCD_WR_DATA(back);
temp<<=1;
}
}
}
//用于显示标准的95个Ascii字符 从空格到~
//入参:x,y为起始坐标,ch为字符,color为画点颜色,back为背景色
void LCD_ShowEnglish(uint16_t x, uint16_t y, const char ch, uint16_t color, uint16_t back)
{
uint8_t n, bit, num, temp;
num = ch - ' ';//获取字符索引
LCD_SetCursor(x, y, x+FONT_WIDTH_HALF, y+FONT_HEIGHT);//设置光标位置
for(n = 0;n < FONT_BYTE_NUM_HALF; n++)
{
temp = font_28[num*FONT_BYTE_NUM_HALF + n];
for( bit = 0; bit < 8; bit++)
{
if(temp&0x80) //写入像素点数据
LCD_WR_DATA(color);
else
LCD_WR_DATA(back);
temp<<=1;
}
}
}
2)高效显示整块区域
也是基于 LCD_Fill 改造下,
比如我在前面分享的STM32示波器中,对于波形数据的显示可以用这种方法,
用宽300*高200的区域显示波形图,最直接的方案是每两个点之间画一条线,最终拟合出一条线出来,但是这种方法不仅效率低,而且重复刷新时需要消隐上一次的画线。用填充区域的方式,就不会有这种问题,但是这会带来内存的问题。
显示一个300*200的区域,每个像素点16bit,则完整直接的显示需要117.1875KB。
肯定会面临内存不够的问题,
1)假如颜色只有2种,则可以用1bit标识颜色,这样就只需要73KB
2)假如颜色有多种,则可以用多个bit标识颜色进行映射
3)假如内存依然不够,则可以拆分显示,比如将300*200的区域拆分成4个75*200的子块,每次刷新1个子块
//在指定区域内填充颜色
//(sx,sy),(ex,ey):填充矩形对角坐标,区域大小为:(ex-sx+1)*(ey-sy+1)
//color:要填充的颜色buf|方向是从左到右从上到下
void LCD_FillColor_Buf(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t *color)
{
uint16_t i,j;
LCD_SetCursor(x1,y1,x2,y2);
for(j = 0; j < (y2-y1+1); j++)
for(i = 0; i < (x2-x1+1); i++)
LCD_WR_DATA( *color++ );
}
3)DMA方式改进
以上都是基于ST7789S+STM32芯片进行测试的,还没有用到DMA方式。
寄存器我一般选择不用DMA,直接写入,
但是对于写数据可以改成DMA方式,然后等待信号量通知传输完成。
比如
for(j = 0; j < (y2-y1+1); j++)
for(i = 0; i < (x2-x1+1); i++)
LCD_WR_DATA( *color++ );
实测显示整屏240*320需要21ms,这点时间完全可以用DMA剩下来,去做其他事。
具体实现方法可以去Cubemx 选择XCube display的例程参考下。