起因

2007年,苹果公司推出了第一代的iPhone,后来这款产品完全改变了人们对于智能手机的理解和认识,智能手机以及衍生的后PC产品功能不断强化,正在一步步取代着原本PC才能做的事情。而就是这样一个背景下,亚马逊公司却反其道而行,推出了一款功能无比单一的产品:Kindle。Kindle不但软件上设计成只能用来看书,就连硬件上也选用了一块基本只能用来看书的屏幕:黑白EPD屏幕。这种屏幕只能显示黑白颜色(或者灰阶),响应速度也非常慢(大约400ms-1s),而且还不能主动发光,必须要借助环境光才能显示……然而这种屏幕却也有一些非常重要的优点,比如显示效果非常接近纸张,不刺眼,只有在刷新时耗电等等。几年后,国产厂家也进军了这一领域,把这类使用EPD屏幕的电纸书的价格做到了千元以下。当时我就买了一台,着实是被这种显示屏的效果给吸引住了。当时我就想着要是能自己用单片机驱动起来玩一玩就好了。无奈当时自己技术差,屏幕也贵,没能顺利实施。最近发现大尺寸(6英寸,型号ED060SC4)的E-Ink屏幕价格已经降到了50以内,于是决定开始研究下它的驱动,也顺便做个最简单的应用:台历。

初步研究

首先,为了各位方便阅读,先来区分几个名词,首先是EPD,EPD并非是E-Paper Display(电子纸显示器)的缩写,而应该是Electrophoretic Display即电泳显示器的缩写。E-Ink则是PVI公司的注册商标,用于指代他们旗下的EPD产品。但是并非只有PVI公司生产电子纸,天马、龙亭、友达、佳显和LG等公司都在生产类似且兼容的EPD产品,所以最好称他们为EPD而非E-Ink。电子纸这个概念就比较笼统了,EPD是一种电子纸,但是也有很多基于其它技术的电子纸,比如说Ch-LCD、PN-LCD、HR-TFT LCD等等,比如Pebble所使用的HR-TFT LCD也被宣传为电子纸,但实际上并非EPD。

其实在去年3月的《无线电》上就已经刊登了关于小尺寸EPD驱动的文章,当时看了下感觉挺简单,无非就是和驱动一般的串行液晶一样,通过SPI接口把指令和数据发送到EPD控制器就可以。但是仔细看了下发现对于大尺寸的屏幕却完全不是这样。这类大尺寸的EPD面板通常都没有集成控制器!而一般的做法是在一个应用控制器外通过总线扩展一个独立的EPD控制IC来完成对它的控制,通常使用PVI或者是EPSON的控制器。这类控制器价格昂贵不说,还都是使用BGA封装的,控制器本身还需要外扩SDRAM或者DDR SDRAM才能使用,相当麻烦,所以鲜有个人爱好者去尝试驱动这种屏幕。然而这个昂贵且复杂的控制器到底隐藏了什么细节呢?我们首先从EPD的驱动原理讲起。

首先,电泳显示,简单讲就是带电粒子在电场中的运动。具体实现的话主要有两种,最早出现的是让二氧化钛粒子分散在烃油中来实现的技术,这种技术原理上最为简单,但是寿命短且难以加工,因此没能顺利商业化。现在常见的是另一种被称为微胶囊的技术。这种技术在上个世纪90年代被几个MIT的本科学生提出,并在1997年成立了E-Ink公司来商业化这种技术,2年后与菲利普公司合作继续开发,2005年被菲利普出售给PVI。微胶囊技术最根本的就是把带电的白色颗粒和染色的油封装成一个个微胶囊,然后把微胶囊排列在电极之间而非像前一种技术一样直接把粒子分散在电极之间。随后在粒子的正面和反面排列出矩阵式的电极,然后通过给电极通电的方法来吸引粒子改变胶囊表面的颜色,这样电极交叉就能形成一个个像素,实现图像的显示。目前实际生产中的做法是使用了和现有TFT LCD上一样的TFT技术来提高多路驱动的对比度,并且只在胶囊的下方排列了电极。

简单的原理就是这样,说白了就是通电改变像素状态,但是这个其实不讲也知道,所以并没有什么实质性的进展。不过,我倒是找到了一份大连佳显公司公开的驱动文档(其实那个2英寸的SPI EPD屏幕也是这家公司开发的),文档中描述了直接用单片机驱动无控制器电子纸屏幕的方法。

首先如同LCD屏幕一样,EPD屏幕也是用扫描方式驱动,扫描分为Gate驱动器和Source驱动器,也就是行和列驱动器,结构如图:

驱动器当然是以COG方式集成在面板上的,控制器只需要按照一定的时序发送扫描信号给驱动器即可。像素数据通过数据总线移入Source驱动器,锁存后Gate打开这一行进行驱动,同时继续移入下一行的数据,锁存后Gate打开下一行进行驱动,这样逐行完成600行的扫描,为1帧。到这里其实都是和液晶一样的,无控制器的LCD驱动就是这样,维持一定的帧率不停刷新就能获得清晰的图像。于是,和液晶不同的地方来了,不同于LCD每个像素只要送显示数据(对于黑白液晶是0或1表示亮或暗,对于彩色液晶直接送RGB数据),EPD对于每个像素,要发送的是“操作”。每个像素有3种操作,分别是不驱动,驱动黑和驱动白。因此每次发送数据的时候一个像素占2个bit,但是切记,每像素2bit并非表示2bpp的色深,而是指可以有多种操作。此外,EPD因为响应速度慢,需要多帧叠加才能完成显示。这也就是之前为什么叫“驱动黑”和“驱动白”而并非直接叫“黑”和“白”。而为了确定每一帧应该送的数据,还有一个波形表用来根据先前的像素数据和新的像素数据来确定一个特定帧中应该送的数据:

另外手册中还列出了一些注意事项,如上图中可以看见的,EPD在刷新的时候需要同时知道上一帧的数据和当前要显示的数据一共两帧的数据才能完成查表显示数据,因此需要有足够的RAM来缓存至少两帧的内容。另外用作灰阶显示时一帧的时间需要在85ms以内,不同的帧时间需要不同的波形表。另外还警告了不要在帧时长比较长的情况下连接EDP屏,可能造成损坏。

由此可见,专用的EPD控制器主要的要点就是驱动的时序和波形表。然而这个波形表的问题怎么解决呢?我一开始想到的是逆向,不过后来证明完全没有这个必要。

另外在网上还能找到一个老外做的EPD驱动实验,用的也是ED060SC4这款屏幕,而且他用了更差的处理器(STM32L1),为了解决RAM不够的问题使用了分块刷新,不过只实现了单黑白的显示(没有灰度)。

电路设计

没啥意思,略。

基本验证

板子拿到手第一件事就是焊接验证。我首先焊接了高压部分测试升压是否正常,结果发现原理图中有数个双二极管、三极管画反,翻过来焊接解决后,正常输出所需电压,于是就可以开始写程序了。

程序无非也就是实现Gate和Source的驱动时序,首先还是看手册当中给出的时序图:

上一张图是Source时序而下一张是Gate时序,注意对应两者之间的关系,以下是写好的代码,注意屏幕可能有个体差异,建议先用0xAA(白)和0x55(黑)测试,时序不正确有可能会导致一些奇怪的效果(比如只能刷白不能刷黑、只能刷线不能正确描点等等)。

void EPD_Vclock (void)
{
  unsigned char i;

  for (i=0;i<2;i++)
  {
    EPD_CKV_L();
    DelayCycle(20);
    EPD_CKV_H();
    DelayCycle(20);
  }
}
inline void EPD_Hclock(void)
{
  EPD_CL_H();
  EPD_CL_L();
}
void EPD_Start_Scan(void)
{ 
  EPD_SPV_H();
  EPD_Vclock ();
  EPD_SPV_L();
  EPD_Vclock ();
  EPD_SPV_H();
  EPD_Vclock ();
}
void EPD_Send_Row_Data(u8 *pArray)  
{
  unsigned char i;
  unsigned short a;

  a = GPIOC->IDR & 0xFF00;

  EPD_LE_H(); 
  EPD_Hclock();
  EPD_Hclock();

  EPD_LE_L();
  EPD_Hclock();
  EPD_Hclock();

  EPD_OE_H();

  EPD_SPH_L();                                          

  for (i=0;i<200;i++)
  {
    GPIOC->ODR = a|pArray[i];
    EPD_Hclock();
  }

  EPD_SPH_H();

  EPD_Hclock();
  EPD_Hclock();

  EPD_CKV_L();
  EPD_OE_L();

  EPD_Hclock();
  EPD_Hclock();

  EPD_CKV_H();     
}
void EPD_Frame(void)
{
  unsigned short line,frame;

  for(frame=0; frame<FRAME_INIT_LEN; frame++)
  {
    EPD_Start_Scan();
    for(line=0; line<600; line++)
    {
      EPD_Send_Row_Data( EPD_Buf + line*200 );
    }
    EPD_Send_Row_Data( EPD_Buf );	
  }
}

这里建议先把EPD装到成品的电子书上先正常刷一帧图,然后接上自己的板子测试是否能够正确刷黑白。下图是焊接了一半的PCB,因为临时修改设计所以飞了许多线。

显示灰度

如前面所讲,EPD的显示原理是通过对平行导电板加电使得微胶囊中的颗粒会以电泳的方式迁移到带有与自己相反电荷的薄板上来显示图像。电泳运动是需要时间的,可想而知,在时间没到的时候(也就是没有完全显示到位),就会显示成灰色。EPD的灰度显示就是这样实现的。

落实到具体的操作中应该是怎样的呢?假设完全转换的时间是t,那么假设要在画面中同时显示4级灰度,那么第1级(白色)的像素导通时间应该为0,而第4级(黑色)的像素导通时间应该为t,中间两级分别为(1/3)t和(2/3)t。然而这个时间怎么控制呢?有一种方法,比如让每帧的时间刚好等于(1/3)t,那么对于白色像素,每帧都不通电,随后是1帧通电2帧不通电、2帧通电1帧不通电和全都通电。以上其实说明了EPD面板驱动的一个重要特点:对时序敏感。帧率将直接决定可以实现的灰度等级数(因为灰度越多,帧率就需要越高)。这里也就印证了之前佳显文档中描述的4级灰度需要保证帧时长在85ms之内的说法。

此时便出现了一个问题,STM32的速度其实做不到16级灰度。比如我现在的STM32,跑120MHz的时候数据频率只有5.5MHz,对应到帧率也就是45Hz,差不多就是只能实现4~8级灰度,当然优化到位后可能可以达到85Hz以实现16级灰度,不过,我还有一个别的解决办法。

这个解决方法,是用慢来解决快的问题。原来不是说帧率不够吗,但是帧率不够的根本原因还是希望能将每一帧的时间缩短,比如从(1/3)t缩短到(1/15)t,以实现更高级别的灰度。但是如果帧率上不去,其实还可以在帧内的时序上动手脚。先前的驱动时序是,在STM32向EPD写入下一行的数据的时候,当前行的数据是被输出的,用来驱动屏幕,当下一行写入完成,则让Source锁存下一行数据,然后让Gate驱动下一行。这里其实可以做一点修改,就是在写入下一行数据的时候,关闭Gate的驱动,等写入完成了,再让Gate打开当前行的驱动,延迟自己想要的行时间后关闭当前行的驱动,再锁存下一行的数据。这样的做法目的就是在STM32写入数据的时候,不要驱动屏幕。这样虽然总的帧时长变长了,但是实际一帧中驱动屏幕的时间变短了,因此目的就达到了。

另外以上讲的都是理论情况,实际情况下(1/15)t时长的帧驱动后的效果和t时长的帧驱动的效果是不一样的,还是需要具体调试。最终为了改善灰度还原,不断尝试后我还对不同的帧使用了不同的帧时长来获得最好的效果,如下是16级灰度的效果:

到这里,还记不记得之前有一个波形表的概念?其实这里就是相当于实现了一个线性波形表,驱动帧数为15帧。不过这个波形表假定了开始状态是确定的(全白),因此如果原有图像不是全白,就必须先反复黑白黑白的刷新达到全白状态才能开始显示。如果有完整的波形表的话,就可以从任意状态直接到达目标显示效果了。

功能实现

其实一旦屏幕驱动完成了,剩下的这些功能实现都是简单的问题,没有什么新意,比较时钟一类的东西是大部分爱好者入门就做的东西吧。硬件上就设计了一片DS3231 RTC芯片,没有使用STM32内置的RTC是为了提高稳定性和精度,无论如何,计时还是它的本职工作。

先讲下功能设计,我目前的设计当中,整个屏幕被分为两个部分,上530行用于显示一张静态图片,下70行用于显示时间和这个月的月历,图片存储在SD卡当中,每天自动更换一张。当然可能有人觉得这样做本末倒置应该把时间放上面显示得很大才对,不过这都是次要的,毕竟这些代码是很容易修改的,我写这篇文章的主要目的也不是在于做一个万年历而是在于分享无控制器EPD电子纸的驱动方法,台历只是一个应用演示。

这里顺便插播一个小知识,关于EPD的刷新模式。其实各位如果用过Kindle或者是其它的电纸书阅读器,应该知道这种屏幕有两种刷新模式,一种叫全部刷新,一种叫局部刷新。两种刷新模式最直观的区别是全部刷新屏幕需要闪一下,而局部刷新屏幕会留下残影。其实我个人认为这种叫法不太合适,但是这里还是沿用这个说法。另外因为EPD在扫描的时候支持不对像素做操作,所以还可以实现分块刷新,即只对一个部分进行操作且不影响别的区域。我这里受限于RAM容量和处理器速度,并不能支持局部刷新,因此所有刷新都是全部刷新。但是使用了分块刷新,也就是屏幕上半部分和下半部分是单独刷新的,这样就避免了在显示时间的时候整个屏幕都要闪一下。不过其实全部刷新在我这里的实现当中也得分两种,一种是已知前一种状态的刷新,一种是未知。已知情况下的做法是从原有灰度先刷到黑色(比如原来是白色就驱动3帧到黑色,第二级灰度就驱动2帧到黑色这样,实际上帧数做了一定微调),然后刷到白色完成清空操作,而未知情况下则是无论原来如何都刷4帧到完全黑色,再4帧到完全白色,这样重复两到四轮完成清空。实际效果上,未知状态的要比已知状态的慢得多。

另外关于响应速度,EPD屏幕在不同灰度模式下的响应速度也不太一样,纯黑白模式下大约是300ms的响应时间,4级灰度600ms,16级灰度则需要1s。我这次制作综合效果和代码简易程度,图片部分使用16级灰度,只能执行不知原有情况的慢速全刷,日历部分使用4级灰度,执行已知原有情况的快速全刷。

代码也就不贴了,自己上GitHub翻吧。

成品

总结

这次使用STM32实现了对于无控制器EPD屏幕的驱动,解决了数据传输速度不够时高灰度等级的调制问题,并且完成了一个简易的功能性Demo,也算是了结了我个人的一个心愿。关于这个项目的原理图、源码等信息可以在我的github上找到。另外特别感谢@ntzyz对本项目的贡献。全文完。