由于需要做一些低功耗的东西,所以最近在尝试玩墨水屏。出于成本考虑(没钱的另一种委婉说法)从咸鱼淘到2块便宜的二手SES 2.66寸三色墨水屏,并使用micropython将其驱动起来,并用字库的方法显示中文。
买到的屏幕是图上这个样子的,带驱动小板 ,驱动小板的作用是提供给MCU标准的SPI操作接口。
墨水屏与ESP8266的连接方式采用推荐的方式,见下图。
连接没问题就可以测试了。
由于micropython没有这个屏幕的现有驱动,因此拿到手后先使用有现成驱动的arduino环境进行测试,确保屏幕和连线没有问题。
(1)直接刷现成固件测试
卖家提供了编译好的现成的固件,直接使用NodeMCU-PyFlasher刷进去,屏幕上就会有测试画面显示,这是最快的测试方法。现成固件(“2.66测试固件”)下载地址见文末。
(2)使用arduino编译固件测试
下面是arduino下驱动的方法。
arduino的安装不在此述,ESP8266开发板的安装网上有很多教程,由于网速实在很慢,所以我采用的是最懒的使用别人打好的方式安装的。使用的包(“8266_package_3.0.1_arduino.cn.exe“)下载地址见文末。
然后就是把微雪的驱动包导进去,导入方法见“墨水屏使用须知(SES2.66三色为例).docx”,下载地址见文末。导入后需要修改或替换SES2.66b的驱动文件(EPD_2in66b.cpp),文中有叙述,照做就是。
最后编译上传,就应该能看到屏幕有反应了。
由于要使用micropython来驱动屏幕,而该屏幕的资料很少,因此我必须研究下arduino下面的驱动程序,以编写相应的python驱动程序。
驱动方法还是主要参考两个部分:一是ariduino下那个EDP_2in66b.cpp,二是微雪的驱动包。
1.初始化
(1)BUSY/RESET引脚状态
由于资料不全,所以我实测了一下,SES2.66b的BUSY引脚高电平为忙,低电平为空闲,所以程序中定义:BUSY = const(1) # 0=idle, 1=busy。
RESET引脚是低电平使模块复位。并且,上电后程序拉低RESET引脚使模块复位后,BUSY引脚会一直处于高电平状态(忙),只有在后续写入POWER ON命令后,BUSY引脚才会变为低电平,这有点坑。
(2)SPI总线初始化
ESP8266只有两个SPI通道,0和1。其中0通道为内部FLASH使用,所以只能用SPI 1。
初始化时,如果不带波特率参数,SPI的总线速度会比较高,高到墨水屏不能接受。你可以测试一下这个:
>>> from machine import SPI
>>> s=SPI(1)
>>> s
HSPI(id=1, baudrate=80000000, polarity=0, phase=0)
>>>
默认波特率是80000000。所以我们需要把波特率降低些,实测10000000可以正常:
e=EPD(spi=SPI(1,baudrate=10000000),cs=Pin(15),dc=Pin(4),rst=Pin(2),busy=Pin(5))
e.init()
(3)初始化命令
直接给出代码吧。
from micropython import const
from time import sleep_ms
import ustruct
import math
# Display resolution
EPD_WIDTH = const(152)
EPD_HEIGHT = const(296)
# Display commands
PANEL_SETTING = const(0x00)
POWER_OFF = const(0x02)
POWER_ON = const(0x04)
BOOSTER_SOFT_START = const(0x06)
DEEP_SLEEP = const(0x07)
DATA_START_TRANSMISSION_1 = const(0x10)
DISPLAY_REFRESH = const(0x12)
DATA_START_TRANSMISSION_2 = const(0x13)
VCOM_AND_DATA_INTERVAL_SETTING = const(0x50)
TCON_RESOLUTION = const(0x61)
VCM_DC_SETTING_REGISTER = const(0x82)
UNKNOWN_CMD = const(0x92)
# Display orientation
ROTATE_0 = const(0)
ROTATE_90 = const(1)
ROTATE_180 = const(2)
ROTATE_270 = const(3)
#BUSY = const(0) # 0=busy, 1=idle
BUSY = const(1) # 0=idle, 1=busy
#rstPin-->low=active
#dc------>low=command
#cs------>low=active
class EPD:
def __init__(self,spi,cs,dc,rst,busy):
self.spi = spi
self.cs = cs
self.dc = dc
self.rst = rst
self.busy = busy
self.cs.init(self.cs.OUT, value=1)
self.dc.init(self.dc.OUT, value=0)
self.rst.init(self.rst.OUT, value=0)
self.busy.init(self.busy.IN)
self.width = EPD_WIDTH
self.height = EPD_HEIGHT
self.rotate = ROTATE_0
def _command(self,command,data=None):
self.dc(0)
self.cs(0)
self.spi.write(bytearray([command]))
self.cs(1)
if data is not None:
self._data(data)
def _data(self, data):
self.dc(1)
self.cs(0)
self.spi.write(data)
self.cs(1)
def init(self):
self.reset()
self._command(BOOSTER_SOFT_START,b'\x17\x17\x17')# BOOSTER_SOFT_START
self._command(POWER_ON)
self.wait_until_idle()
self._command(PANEL_SETTING, b'\xCF') # (296x160, LUT from register, B/W/R run both LU1 LU2, scan up, shift right, bootster on) KW-BF KWR-AF BWROTP 0f
self._command(VCOM_AND_DATA_INTERVAL_SETTING, b'\x77')
self._command(TCON_RESOLUTION,b'\x98\x01\x28')
self._command(VCM_DC_SETTING_REGISTER, b'\x0A')
#self._command(PLL_CONTROL, b'\x3A') # 3A 100HZ 29 150Hz 39 200HZ 31 171HZ
#self._command(POWER_SETTING, b'\x03\x00\x2b\x2b\x09') # VDS_EN VDG_EN, VCOM_HV VGHL_LV[1] VGHL_LV[0], VDH, VDL, VDHR
#self._command(BOOSTER_SOFT_START, b'\x07\x07\x17')
#self._command(POWER_OPTIMIZATION, b'\x60\xA5')
#self._command(POWER_OPTIMIZATION, b'\x89\xA5')
#self._command(POWER_OPTIMIZATION, b'\x90\x00')
#self._command(POWER_OPTIMIZATION, b'\x93\x2A')
#self._command(POWER_OPTIMIZATION, b'\x73\x41')
#self._command(VCM_DC_SETTING_REGISTER, b'\x12')
#self._command(VCOM_AND_DATA_INTERVAL_SETTING, b'\x87') # define by OTP
#self.set_lut()
#self._command(PARTIAL_DISPLAY_REFRESH, b'\x00')
#self.turnOnDisplay()
def reset(self):
self.rst(0)
sleep_ms(200)
self.rst(1)
sleep_ms(200)
def wait_until_idle(self):
while self.busy.value() == BUSY:
print("waiting for busy...")
sleep_ms(50)
def turnOnDisplay(self):
self._command(DISPLAY_REFRESH)
self.wait_until_idle()
def display_frame(self, frame_buffer_black, frame_buffer_red):
#self._command(TCON_RESOLUTION, ustruct.pack(">HH", EPD_WIDTH, EPD_HEIGHT))#写分辨率,已在init中,不用再写
if (frame_buffer_black != None):
self._command(DATA_START_TRANSMISSION_1)#写黑白
sleep_ms(2)
for i in range(0, self.width * self.height // 8):
self._data(bytearray([frame_buffer_black[i]]))
#print(i)
sleep_ms(2)
self._command(UNKNOWN_CMD);#???
if (frame_buffer_red != None):
self._command(DATA_START_TRANSMISSION_2)#写红色
sleep_ms(2)
for i in range(0, self.width * self.height // 8):
self._data(bytearray([frame_buffer_red[i]]))
sleep_ms(2)
self._command(UNKNOWN_CMD);#???
self.turnOnDisplay()
到这里,屏幕操作基本上算是有反应了,init后应该能看到屏幕在闪烁。
是的,里面有个UNKNOW_CMD(0x92),我也不知道是什么命令,C语言板本得驱动文件里有,我就抄下来了。
(4)往屏幕上显示英文字符集
我使用的是micropython下的framebuff驱动方法。该方法的思想是在内存中开辟一块与显示屏分辨率相同的内存区域,要显示东西就往该内存块操作,操作完了直接用该内存块来整屏刷新即可。这样可以避免频繁直接操作屏幕,效率比较高。
framebuff使用时,先要创建一块buff内存块,然后创建framebuff对象,该对象只是提供了一些操作方法,方便了对之前创建的buff内存块的操作,其自身不占用buff内存块,所以,要送给显示器的数据实际上是buff内存块。在framebuff没有提供的方法之外,你也可以直接操作buff内存块,更改显示内容。
但由于micropython下的framebuff方法不完善,很多操作方法都没有,字体也仅限于8*8字体,所以也只能勉强用用而已,要显示大字体和中文,就得再想办法,这也是后文想描述的内容。
整屏刷新的代码在前面初始化部分已经贴出来了,就是def display_frame部分。
要特别说一下的是,由于内存实在有限,如果同时开辟黑色和红色两块显示buffer,ESP8266会当场摆烂,所以我只开了一块黑色的buffer来测试,改到ESP32平台的话,可以两块一起开。注意display_frame方法中:
self._command(DATA_START_TRANSMISSION_1)部分是写黑白显存
self._command(DATA_START_TRANSMISSION_2)部分是写红色显存
把初始化部分文件存为epaper2in66b1.py,然后做下面的测试。
先把代码贴出来:
from epaper2in66b1 import EPD
from machine import Pin,SPI
import framebuf
import micropython,gc
# Display resolution
EPD_WIDTH = const(152)
EPD_HEIGHT = const(296)
black = const(0)
white = const(1)
# Display orientation
ROTATE_0 = const(0)
ROTATE_90 = const(1)
ROTATE_180 = const(2)
ROTATE_270 = const(3)
e=EPD(spi=SPI(1,baudrate=10000000),cs=Pin(15),dc=Pin(4),rst=Pin(2),busy=Pin(5))
e.init()
buf_black=bytearray(EPD_WIDTH *EPD_HEIGHT//8)
frb_black=framebuf.FrameBuffer(buf_black,EPD_WIDTH,EPD_HEIGHT,framebuf.MONO_HLSB)
frb_black.fill(white)
frb_black.text('Hello World',30,30,black)
e.display_frame(buf_black)
OK,运行后你会在屏幕上看到一行黑色的hello world,字体是8*8的。
(5)显示中文
micropython下,中文是个很麻烦的事情,不过经过度娘了一堆文章研究后,得到一个比较好的方法,即使用unicode字模。
大神的文章在这里:https://www.cnblogs.com/juwan/p/13198330.html
大概的原理是这样:
micropython中,均使用的是utf-8编码,包括汉字,并且你改不了。
但是有个unicode编码方法,常用汉字的代码范围在0x4E00---0x9FA5,我们可以使用程序把汉字的utf-8编码转换为unicode编码,从而得到一个汉字的16位的unicode编码。如果我们有一个unicode汉字编码顺序的字模库,比如一个16*16的字模库,每个汉字占用的字模是固定的16*16/8=32字节,那么我们就可以根据汉字的unicode码,在字模库中查找出该字的字模数据,显示在屏幕上了。好在我们有这么一个工具,图中这个。
就可以创建一个unicode字模库。我试了下,16*16的字模库大小约2M,生成后我们存为font.dzk,上传到ESP8266根目录就可以了。
下面是转码和查字模的代码:
def encode_get_utf8_size(utf):
if utf < 0x80:
return 1
if utf >= 0x80 and utf < 0xC0:
return -1
if utf >= 0xC0 and utf < 0xE0:
return 2
if utf >= 0xE0 and utf < 0xF0:
return 3
if utf >= 0xF0 and utf < 0xF8:
return 4
if utf >= 0xF8 and utf < 0xFC:
return 5
if utf >= 0xFC:
return 6
def encode_utf8_to_unicode(utf8):
utfbytes = encode_get_utf8_size(utf8[0])
if utfbytes == 1:
unic = utf8[0]
if utfbytes == 2:
b1 = utf8[0]
b2 = utf8[1]
if ((b2 & 0xE0) != 0x80):
return -1
unic = ((((b1 << 6) + (b2 & 0x3F)) & 0xFF) << 8) | (((b1 >> 2) & 0x07) & 0xFF)
if utfbytes == 3:
b1 = utf8[0]
b2 = utf8[1]
b3 = utf8[2]
if (((b2 & 0xC0) != 0x80) or ((b3 & 0xC0) != 0x80)):
return -1
unic = ((((b1 << 4) + ((b2 >> 2) & 0x0F)) & 0xFF) << 8) | (((b2 << 6) + (b3 & 0x3F)) & 0xFF)
if utfbytes == 4:
b1 = utf8[0]
b2 = utf8[1]
b3 = utf8[2]
b4 = utf8[3]
if (((b2 & 0xC0) != 0x80) or ((b3 & 0xC0) != 0x80) or ((b4 & 0xC0) != 0x80)):
return -1
unic = ((((b3 << 6) + (b4 & 0x3F)) & 0xFF) << 16) | ((((b2 << 4) + ((b3 >> 2)
& 0x0F)) & 0xFF) << 8) | ((((b1 << 2) & 0x1C) + ((b2 >> 4) & 0x03)) & 0xFF)
if utfbytes == 5:
b1 = utf8[0]
b2 = utf8[1]
b3 = utf8[2]
b4 = utf8[3]
b5 = utf8[4]
if (((b2 & 0xC0) != 0x80) or ((b3 & 0xC0) != 0x80) or ((b4 & 0xC0) != 0x80) or ((b5 & 0xC0) != 0x80)):
return -1
unic = ((((b4 << 6) + (b5 & 0x3F)) & 0xFF) << 24) | (((b3 << 4) + ((b4 >> 2) & 0x0F) & 0xFF) << 16) | ((((b2 << 2) + ((b3 >> 4) & 0x03)) & 0xFF) << 8) | (((b1 << 6)) & 0xFF)
if utfbytes == 6:
b1 = utf8[0]
b2 = utf8[1]
b3 = utf8[2]
b4 = utf8[3]
b5 = utf8[4]
b6 = utf8[5]
if (((b2 & 0xC0) != 0x80) or ((b3 & 0xC0) != 0x80) or ((b4 & 0xC0) != 0x80) or ((b5 & 0xC0) != 0x80) or ((b6 & 0xC0) != 0x80)):
return -1
unic = ((((b5 << 6) + (b6 & 0x3F)) << 24) & 0xFF) | (((b5 << 4) + ((b6 >> 2) & 0x0F) << 16) & 0xFF) | ((((b3 << 2) + ((b4 >> 4) & 0x03)) << 8) & 0xFF) | ((((b1 << 6) & 0x40) + (b2 & 0x3F)) & 0xFF)
return unic
def draw_string(img,x,y,c,string,width,high,fonts,space=0):
#draw_string(buf_black,0,20,black,2,b'中',16,16,unicode_dict)
import framebuf
i=0
pos=0
while i
前两段是大神写的utf-8转unicode代码,输出的是一个字符的unicode代码,比如“床”字的unicode编码是0x5E8A(十进制24202),然后我们就可以打开字模文件取字模了。
最后一段draw_string是我改的,目的是把汉字字模写到墨水屏的buffer里去。用到的是framebuff的blit方法。blit方法是在一块现有的buffer(墨水屏的buffer)上叠加另一块buffer(查出来的字模buffer)。
调用draw_string后再调用显示屏的display_frame,显示屏上就出现汉字了。
把该三段代码存为zhuanma.py,传到ESP8266根目录,就可以测试汉字显示了,测试代码如下:
from epaper2in66b1 import EPD
from machine import Pin,SPI
import framebuf
import micropython,gc
# Display resolution
EPD_WIDTH = const(152)
EPD_HEIGHT = const(296)
black = const(0)
white = const(1)
# Display orientation
ROTATE_0 = const(0)
ROTATE_90 = const(1)
ROTATE_180 = const(2)
ROTATE_270 = const(3)
e=EPD(spi=SPI(1,baudrate=10000000),cs=Pin(15),dc=Pin(4),rst=Pin(2),busy=Pin(5))
e.init()
buf_black=bytearray(EPD_WIDTH *EPD_HEIGHT//8)
frb_black=framebuf.FrameBuffer(buf_black,EPD_WIDTH,EPD_HEIGHT,framebuf.MONO_HLSB)
frb_black.fill(white)
frb_black.text('Hello World',30,30,black)
#以下为测试汉字显示的代码
import zhuanma
unicode_dict=open('/font.Dzk','rb')
zhuanma.draw_string(frb_black,20,80,white,b'床前明月光',16,16,unicode_dict)
#注意传给draw_string的是frameBuff对象,而不是直接的数组buffer,因为draw_string调用framBuff的blit方法,要求是frameBuff对象
e.display_frame(buf_black)
效果如下图(忽略红色,红色是之前的测试没清除掉的,代码结果只有黑色):
最后,把文中所用的一些资料和软件网盘分享出来。
字模提取是未注册板本的,字模会有斜杠,涉及版权问题,要没有斜杠的字模库请私信我。
链接: https://pan.baidu.com/s/1VQFME6P3YyuV4YWqM1-DOQ?pwd=t6v6