使用micropython(ESP8266、ESP32)驱动SES 2.66寸墨水屏显示中文

由于需要做一些低功耗的东西,所以最近在尝试玩墨水屏。出于成本考虑(没钱的另一种委婉说法)从咸鱼淘到2块便宜的二手SES 2.66寸三色墨水屏,并使用micropython将其驱动起来,并用字库的方法显示中文。

一.屏幕的驱动 

1.硬件连线

SES 2.66墨水屏
SES 2.66墨水屏带驱动小板

买到的屏幕是图上这个样子的,带驱动小板 ,驱动小板的作用是提供给MCU标准的SPI操作接口。

墨水屏与ESP8266的连接方式采用推荐的方式,见下图。

 连接没问题就可以测试了。

2.屏幕测试

由于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驱动

 由于要使用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<len(string):
        utfbytes=encode_get_utf8_size(string[i])
        print(i,string[i],utfbytes,string[i:i+utfbytes])
        tmp=encode_utf8_to_unicode(string[i:i+utfbytes])
        print(type(tmp),tmp)
        i+=utfbytes
        pos+=1
        fonts.seek(tmp*int(high*width/8))
        #print(len(fonts.read(int(high*width/8))))
        #img.draw_font(x+(pos*s*(width+sapce)),y,width,high,fonts.read(int(high*width/8)),color=c)
        blitbuf=bytearray(int(width*high/8))
        zimo=fonts.read(int(high*width/8))
        for aa in range(int(width*high/8)):
            blitbuf[aa]=zimo[aa]
        print(blitbuf)
        blitfrmbuf=framebuf.FrameBuffer(blitbuf,width,high,framebuf.MONO_HLSB)
        img.blit(blitfrmbuf,x+(int(i/3)-1)*width,y)
        print("i=",i,"pos=",x+(i/3-1)*width)
        


 前两段是大神写的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

物联沃分享整理
物联沃-IOTWORD物联网 » 使用micropython(ESP8266、ESP32)驱动SES 2.66寸墨水屏显示中文

发表评论