最近在做一个热成像仪,需要将传感器的数据快速显示到屏幕上。屏幕是我之前试验过的使用 ILI9341 主控 240x320分辨率的LCD。对应使用 Ucg 库来驱动之。遇到的问题是显示速度太慢。起初我使用ucg.drawPixel() 通过绘制点的方式来实现绘图。编写一个简单的代码来进行测试:
#include <SPI.h>
#include "Ucglib.h"
Ucglib_ILI9341_18x240x320_HWSPI ucg(/*cd=*/ 9, /*cs=*/ 10, /*reset=*/ 8,(HardwareSerial*)&Serial);
void setup(void) {
Serial.begin(115200);
delay(3000);
ucg.begin(UCG_FONT_MODE_TRANSPARENT,&Serial);
ucg.clearScreen();
Serial.print("Starting");
}
void loop(void)
{
ucg.setColor(0xFF,0,0);
long int starttime=millis();
for (int i=0;i<320;i++)
for (int j=0;j<240;j++)
{
ucg.drawPixel(j,i);
}
Serial.println(millis()-starttime);
}
可以看到,绘制 240x320的图像需要27.366秒。
经过阅读ILI9341 的 Spec,大致了解一下显示原理:将颜色信息写入主控的 RAM 中即可驱动屏幕实现显示。DrawPixel 函数应该是有额外的开销,所以导致速度非常缓慢。查看代码,具体实现绘制点的代码在ucg_dev_ic_ili9486.c 文件中:
case UCG_MSG_DRAW_PIXEL:
if (ucg_clip_is_pixel_visible(ucg) != 0)
{
uint8_t c[3];
ucg_com_SendCmdSeq(ucg, ucg_ili9486_set_pos_seq);
c[0] = ucg->arg.pixel.rgb.color[0];
c[1] = ucg->arg.pixel.rgb.color[1];
c[2] = ucg->arg.pixel.rgb.color[2];
ucg_com_SendRepeat3Bytes(ucg, 1, c);
ucg_com_SetCSLineStatus(ucg, 1); /* disable chip */
}
return 1;
其中会将ucg_pgm_uint8_t ucg_ili9486_set_pos_seq结构体定义的数据发送出去:
const ucg_pgm_uint8_t ucg_ili9486_set_pos_seq[] =
{
UCG_CS(0), /* enable chip */
UCG_C11(0x036, 0x008),
UCG_C10(0x02a), UCG_VARX(8, 0x01, 0), UCG_VARX(0, 0x0ff, 0), UCG_A2(0x001, 0x03f), /* set x position */
UCG_C10(0x02b), UCG_VARY(8, 0x01, 0), UCG_VARY(0, 0x0ff, 0), UCG_A2(0x001, 0x0df), /* set y position */
UCG_C10(0x02c), /* write to RAM */
UCG_DATA(), /* change to data mode */
UCG_END()};
为了简单起见,直接用逻辑分析仪抓取发送的 SPI数据(实际上在整个过程中,Ucg Lib 无需使用 MISO Pin来接受屏幕的返回数据,完全用 MOSI 即可达成所有操作)。抓取结果如下:
我设定的是对 x=02,y=03 写入一个颜色为(F0,F2,F3)的点
36 08 //Memory Access Control(36)
2A 00 02 00 EF//Column Address Set( 2A)
2B 00 03 01 3F //Page Address Set (2Bh)
2C F0 F2 F3 //Memory Write(2Ch)
根据 Spec ,2A 命令后面给出 x 的位置 02, 然后 00 EF 是 239 (屏幕 x方向范围 [0,239]); 2B 命令后面给出 y 的位置 03, 然后 01 3F 是 319 (屏幕 y方向范围 [0,329])。之后的 2Ch 表示开始对 Ram 填写。顺便说一下前面结构体中UCG_C11 这样宏的意思: UCG_Cmn 中,m表示后面 command 的数量,n 表示数据的个数。因为这个屏幕通过一个C/D Pin 来切换当前的命令是命令还是数据。通过这样的定义,可以知道如何切换这个C/D Pin。
有了上面的知识,接下来改进优化速度。
第一项是减少数据量,我们知道他们需要什么数据,直接发送而不再使用 Ucg 的结构体避免额外开销:
void loop(void)
{
ucg.setColor(0xFF,0,0);
long int starttime=millis();
ucg.drawPixel(0,0);
for (int i=0;i<320;i++)
for (int j=0;j<240;j++)
{
SPI.transfer (0);//主机SPI发送
SPI.transfer (0xff);//主机SPI发送
SPI.transfer (0);//主机SPI发送
}
Serial.println(millis()-starttime);
}
这样测试下来绘制一帧花费时间 0.469秒。
第二项优化是 spi 的速度问题。示波器测量显示,上面代码默认配置为 8Mhz 的 SPI Clock,已经是 Uno 上最高的速度了。因此,对于 Uno 的板子来说已经是最快的速度了,如果用其他板子可以考虑是否达到 spi 的最高速度。
第三项是数据量的问题。前面对于每一个 Pixel 我们会发送三个Byte的 RGB 信息。实际上其中只有 18bits 是有效的
此外,还有一种16bits的模式:
这种模式下每次传输2Bytes即可。因此,如果能切换到这个模式下,那么对每个Pixel 只要2个Byte 即可,能节省33%。切换的命令是 3Ah ,我们需要将 DBI 设置为 5h 即可。
直接在ucg_dev_ic_ili9341.c 文件中添加:
const ucg_pgm_uint8_t ucg_ili9341_set_pos_seq[] =
{
UCG_CS(0), /* enable chip */
UCG_C11(ILI9341_PIXFMT, 0x55),
UCG_C11( 0x036, 0x008),
UCG_C10(0x02a), UCG_VARX(0,0x00, 0), UCG_VARX(0,0x0ff, 0), UCG_A2(0x000, 0x0ef), /* set x position */
UCG_C10(0x02b), UCG_VARY(8,0x01, 0), UCG_VARY(0,0x0ff, 0), UCG_A2(0x001, 0x03f), /* set y position */
UCG_C10(0x02c), /* write to RAM */
UCG_DATA(), /* change to data mode */
UCG_END()
};
修改代码再次试验
void loop(void)
{
ucg.setColor(0xFF,0,0);
long int starttime=millis();
ucg.drawPixel(0,0);
for (int i=0;i<320;i++)
for (int j=0;j<240;j++)
{
SPI.transfer (0);//主机SPI发送
SPI.transfer (0x0F);//主机SPI发送
//SPI.transfer (0xFF);//主机SPI发送
}
Serial.println(millis()-starttime);
}
屏幕会变成绿色,运行时间 320ms。和前面相比减小1/3的时间开销。但是总体颜色数量少了如果设计上需要丰富的颜色层次那么这种模式是不适合的。
原本我打算将这个放置在对屏幕初始化的地方,但是一直有问题,后来忽然悟道UcgLib 里面对于我这个屏幕使用的都是3byte的颜色模式。如果初始化为 2Byte , 那么很多函数是无法工作的。因此,直接修改 DrawPixel 函数这里是最简单的试验方式。
如果你使用 Teensy 3.1 那么还可以使用 ILI9341_t3 的 Library,作者针对 Teensy 做了进一步的优化,速度更快。但是我在试验中发现可能是因为初始化缺少了一些必要指令,这个库有时候无法正常显示。