
单色液晶控制器通常的写显示RAM方式是一次写8个像素,容易实现MCU主存到控制器的位图映射。但是支持灰度的液晶控制器不一定有这样的操作方式,于是只用黑白二色的显示时,也不得不每个像素都要写灰阶编码到控制器。但是在MCU主存中每个像素用8-bit甚至16-bit来表示,开销就大多了,很多时候是不必要的。如果用二值的位图存放显示内容,在显示驱动软件中转换,就可能实现和单色液晶在图形库上的兼容性。

例如ST7529液晶控制器的显存数据是5-bit灰度,采用并行接口(8080模式)驱动时,有16-bit表示3个像素的办法和3个8-bit表示3个像素的办法。这个控制器是给CSTN液晶设计的,所以总是要RGB 3个像素一起写,作为FSTN的驱动显得有些别扭——列方向的坐标只能以3像素为单位。如果能忍受这一点,用8-bit数据模式,每次写显存操作就是更新一个像素,按列优先模式能实现逐条扫描线的数据写入。

现在考虑类似ST7529这种控制器的位图映射软件驱动。在MCU的SRAM中开辟一块连续空间作为显示区域内容的位图存储(只显示两种颜色,每个像素1 bit),向控制器写像素数据时每次根据位图中的一个bit,决定写控制器的数据是两种颜色代码中哪一个。如果控制器是用MCU的内存控制器(如STM32的FMC)连接的,写操作就对应到一条STR指令;如果不能用FMC,就要用一组GPIO输出并行数据,另一单独GPIO产生写脉冲。
基本的显示代码:

在写每个像素数据的时候,要进行位运算测试内存中数据的某位是1还是0. 按照低地址数据在前,每个字中LSB在前的顺序访问整个位图。取数据的时候一次取32 bit的效率要高于8 bit. 内层循环就是逐位处理。在Cortex-m4上,以上代码的实现效率约为12.2 时钟周期每像素。
这样的代码足够简洁了。用位运算是因为不能直接寻址SRAM的某一位…… 但是真的不能吗?Cortex-m3/m4有bit-banding的功能,虽然我以前没觉得有什么用,SRAM是处于bit-band区域中的。也就是,在SRAM中存储了位图,就有某一段地址是每个32-bit映射到位图中一个bit的。按连续地址访问就可以遍历位图中每个像素。于是显示代码就只需要一个循环了:

针对ST7529,如果输出是全黑和全白两种颜色,数据接口上有效位是全0或者全1,还可以把条件判断也省去,修改成这样:

代码中直接输出数据 -p[n] , 因为p[n]为1则写数据就成了 0xFF,是满足需要的。这样又能少用指令了。实际测试的执行时间减少到 8~9 时钟周期每像素,有一个浮动可能是CPU流水线的关系。看一下编译的结果:

标出的部分就是循环主体,一共7条指令,显得没有任何多余操作,实际执行时间变化可能是总线的缘故。
到这里,好象已经优化到头了,不绕弯。
回头看,从原理上呢,根据每个像素判断一下要写什么数据是没错的,但是如果写的数据和上次一样其实可以不用更新接口上的并行数据,所以可以少一步操作?然而要增加这个条件判断其实是又绕弯了,因为测试、保存前次结果和条件分支会消耗更多的周期。实际测试也是平均执行时间到了 10.4 时钟周期每像素。 程序如下:

虽然上面这个尝试改进失败,减少不必要的操作的思路是有价值的。实际的显示驱动就是写连续一串(个数不确定)的前景色像素,再写连续一串背景色像素,交替进行的。假如SRAM存储的不是位图,而是按顺序排列的两种颜色各自连续的像素个数的序列,则显示代码有可能执行更快。
但是现在SRAM存储的是位图,只能在此前提下讨论。那么,从位图扫描的角度,统计连续的1个的个数,再统计后面连续的0的个数,再统计后面连续1的个数……如此下去也可以,只不过效率是个问题。不妨对比以下:

这段程序将"1"像素和"0"像素分组输出,包含了测试统计和连续写脉冲的过程,属于是绕了弯路的做法,最后的执行时间大约是 15.5 时钟周期每像素. 比最基本的方法还要慢,也是可想而知的。
如果不用bit-banding呢,像最基本的方法那样每次先取一个字,那么程序还会可预期地多耗费点时间:

但之所以要这么改写,是我想尝试一下能否快速地找出连续的1或0的个数——Cortex-m3/m4有CLZ (Count Leading Zeros)指令。在一个32-bit字之内,用这条指令直接得到从最高位开始往下有多少个连续的0. 它能省去一个循环的位测试。
还是要尝试的,下面的代码看起来过于复杂了。可能还有可优化的地方。

调试查错过后,上面这段代码在我用的测试位图(文本字符为主)上达到了平均约 8 时钟周期每像素的效率,追上了前面用bit-band的最快的代码。不枉这份努力啊。这种方式,执行时间与显示内容是关联的,一般图形界面的话像素颜色连续出现的时候多,所以应该是适用的。
到了这个地步,觉得还有更快的可能吗?其实使用CLZ指令得以提升效率的原因是减少了循环次数,上面这个程序仍然有循环:除了不可避免的从SRAM中取数据之外,连续产生多少次写脉冲是用循环来实现。而后者还有优化的可能:
不用GPIO翻转的方式,用硬件自动产生N个脉冲。STM32的TIM1/TIM8等定时器的PWM能做到,或者用一个定时器作为另一个输出PWM的定时器的门控。我暂时还没有实验,好象用的板子GPIO连接缺少条件。 如果用了FMC接口的话可以用借用这个思路,用DMA内存到内存的方式快速写。 循环展开,这要费一些代码空间了。在上面的程序中,连续的写脉冲一般不会太长。比如说,在32个以内就完全展开循环:

用这个 wr_pulses() 函数代替前一段代码中的产生WR脉冲的循环,实现部分的循环展开,之后…… 执行速度提升到了 6 时钟周期每像素的水平。
当然,要求刷屏刷得快简单地把时钟频率提上去就是了,是否要纠结这种优化是MCU玩家自己决定,本文只是假期时候的一点研究分享。关键点:一是bit-band的使用,二是CLZ指令的使用。这两个特性都得要m3/m4起才有,m0是没有的(现在国产m4也很便宜了嘛)。
· END ·

扫码添加小助手回复“进群”
和电子工程师们面对面交流经验