TFT屏的接口有SPI,8080/6800并口,RGB,MIPI等许多种,用在单片机上的一般就是前两种。SPI的好处是简单,只需要连接六七条线,缺点是速度太慢了。SPI的最高时钟频率一般是单片机主频二分频,16位彩色时写一个像素需要33~34个时钟周期(传16位数据32个周期,协议消耗增加一两个周期),分辨率低了还好,320*240的屏刷全屏需要24.6万时钟周期,48M主频时就是51ms左右,已经有点嫌慢了。好在SPI可以用DMA方式异步写屏,CPU占用率低一些。
8080并口就快多了,一般用FSMC+DMA来驱动,实测最快时平均每个像素只需要12.6个时钟周期,也可以异步写屏,但是得用100脚或者144脚的STM32才有FSMC。(DMA用了M2M方式,效率不高,如果不用DMA直接写,做循环展开,还能再稍快一点点。)有时我们主要就是驱动个屏,其他管脚用得不多,有没有办法让48脚的STM32也用上并口屏呢?得试一下。
48脚的STM32F030有完整的PA0-15, PB0-15这两组16位GPIO, 考虑到PA13和PA14需要作为SWCLK和SWDIO,还是把屏接在GPIOB吧。GPIOA出五个脚接RS/CS/RD/WR/RESET,一路PWM用作背光调节,PA9和10当串口,PA5~7作为SPI接个W25,再加个W25的CS脚,差不多了。原理图过于简单就不贴上来了。
焊好,开机,调试,显示一切正常。然而要测试刷屏速度怎么做呢?在F1/F3/F4上都可以打开DWT,然后在清屏前后各读取一次DWT->CYCCNT的值,相减即可。F0没有DWT,F030也没有32位定时器,只好用SysTick代替了。缺点是它的计数值最大只能到16777215,勉强够了。至于SysTick中断怎么办呢?用TIM16配置成每ms中断,代替一下吧。
最关键的写屏函数,把它作为回调函数传给TFT驱动,我们先写成这样:
static void fastwrite(void* buf, int count)
{
register unsigned short* ptr = buf;
while(count > 0) {
GPIOA- >BRR = LCD_WR;
GPIOB- >ODR = *ptr++;
GPIOA- >BSRR = LCD_WR;
count--;
}
}
实测结果,刷屏消耗大约1816362个时钟周期,平均每23.7个时钟周期写一个像素。这是什么情况,比SPI只快这么一点?看来是这个循环效率太低了,展开试试。怎么展开呢,试试达夫设备, 首先把前面三条端口操作做成一个宏readwrite_macro,然后如下:
static void fastwrite(void* buf, register int count)
{
register unsigned short* ptr = buf;
register int n = (count + 7) / 8;
switch(count % 8) {
case 0: do { readwrite_macro(*ptr++);
case 7: readwrite_macro(*ptr++);
case 6: readwrite_macro(*ptr++);
case 5: readwrite_macro(*ptr++);
case 4: readwrite_macro(*ptr++);
case 3: readwrite_macro(*ptr++);
case 2: readwrite_macro(*ptr++);
case 1: readwrite_macro(*ptr++);
} while(--n > 0);
}
看上去很奇怪是不是,do..while还能包在switch里?事实上它工作得很好,现在刷全屏只需要1059652个周期了,平均每13.8周期输出一个像素,已经接近FSMC的水平了。
继续优化,考虑到有时是写TFT寄存器,只需要写一两个字节的数据,对这些情况单独处理一下,可以做到798990周期刷全屏,平均每像素10.4周期,超过了FSMC的速度。
然而以上这些办法的缺点是没法做到异步写屏,如果全屏都需要刷新、刷新率较高的话,CPU负担恐怕重了点。能不能设法做到和FSMC+DMA一样的效果呢?还得再想办法。
这里的关键还是写端口的三条语句,把WR脚拉低再拉高,太浪费时间了,能不能用PWM来代替呢?正好,WR脚同时也是TIM15的1通道输出。可以把TIM15配置成PWM,写好GPIO之后PWM提供WR脚的上升沿。实测了一下,果然可行,不过速度比之前慢了不少,估计是PWM参数得好好优化。
下一步,解决了写一个数据,怎么写多个数据呢?思路是用TIM15的溢出事件触发DMA传输,把新的像素数据写到GPIOB。关键是写够了得能停下来,得再用上定时器级联功能,用TIM15的溢出事件作为子定时器(TIM3)的时钟源,计数到预定数值时,TIM3中断里把TIM15关掉——这样可以吗?不行,中断太慢了,这样肯定会写多。感觉可以用TIM3的溢出或者匹配事件再触发一次DMA传输,写TIM15的CR1寄存器,把TIM15关掉。这样应该就可以做成和FSMC+DMA一样的效果了。不过具体操作起来麻烦事还很多。