最近15元淘了一块电子墨水屏,合宙家的产品。看评论是可以用微雪的代码驱动,于是找了微雪的代码,顺便翻了下读取BMP的代码,做了个小DEMO玩玩。手头几个ESP32都用了,只好翻了个旧的8266去写代码,可惜内存小了点,做更复杂的合成有点不够。好在只是DEMO下,就无所谓了。

准备了几个素材,打算做3个背景轮换,试了下FrameBuffer绘图叠加的功能。绘制原理如下

python通过ESP8266发送数据给arduino python控制esp8266_ESP8266

最终绘制的效果

python通过ESP8266发送数据给arduino python控制esp8266_ESP8266_02

python通过ESP8266发送数据给arduino python控制esp8266_SPI_03

 

相关代码如下 

 main.py

'''
ESP8266 控制电子墨水屏
元件:
    ESP8266+ CP2012
    200*200 waveshare compatiable E-Ink screen SPI/I2C
功能:
    每10秒将sprite叠加到背景的随机位置,背景从3张图片中随机选择

@Author Jim

微雪示例接线:
VCC  VCC		3.3V Power input
GND  GND		Power ground
SCK  GPIO14(D5)	SPI CLK, Clock Signal Input
DIN  GPIO13(D7)	SPI MOSI, Data Input
CS   GPIO15(D8)	Chip Select, active-low
BUSY GPIO5(D1)	Busy Output Pin (means busy)
RST  GPIO2(D4)	Reset, active-low
DC   GPIO4(D2)	Reset, Data/Demand, low level means to demand, high level means data
'''

from machine import Pin, SPI, SoftSPI
from struct import unpack
from epaper1in54 import EPD, EPD_WIDTH, EPD_HEIGHT
import framebuf
import gc
import random
from machine import Timer


class Global:
    epaper = EPD(SPI(1, baudrate=80000000, polarity=0, phase=0), cs=Pin(15), dc=Pin(4), rst=Pin(2), busy=Pin(5))  # 硬SPI,esp8266会自动调用GPIO14,GPIO15,GPIO16,GPIO17.用户只需 将引脚对应连接 #sck(D0)=6 mosi(D1)=7 miso=unused D4=GPIO2=dc D4=GPIO2=rst D1=GPIO5=BUSY

    layerBuffer = bytearray(EPD_WIDTH * EPD_HEIGHT // 8)  # 主绘图层
    layerFrame = framebuf.FrameBuffer(layerBuffer, EPD_WIDTH, EPD_HEIGHT, framebuf.MONO_HLSB)

    spriteBuffer = bytearray(96 * 88 // 8)  # 叠加层
    spriteFrame = framebuf.FrameBuffer(spriteBuffer, 96, 88, framebuf.MONO_HLSB)

    maskBuffer = bytearray(96 * 88 // 8)  # 叠加层
    maskFrame = framebuf.FrameBuffer(maskBuffer, 96, 88, framebuf.MONO_HLSB)

    timer = Timer(-1)

def load_bmp(file_path, frameBuf=None):
    with open(file_path, "rb") as file:
        file.seek(14)
        data = file.read(4)
        DIB_len = unpack("<I", data[0:4])[0]
        data = file.read(DIB_len - 4)  # 文件头
        (
            DIB_w,  # 保证宽度为4字节对齐,即32的倍数
            DIB_h,
            DIB_planes_num,
            DIB_depth,
            DIB_comp,
            DIB_raw_size,
            DIB_hres,
            DIB_vres,
        ) = unpack("<iiHHIIii", data[0:28])
        if DIB_depth > 1:  # 只支持黑白
            print('only support mono color')
            return bytearray(b'')
        plt_size = 2 ** DIB_depth * 4
        file.seek(plt_size, 1)  # 跳过调色板
        print('width=%d, height=%d, data=%d' % (DIB_w, DIB_h, DIB_raw_size))

        size_width = (DIB_w * DIB_depth + 7) // 8
        row_size = (size_width + 3) // 4 * 4  # 对齐后的宽度
        if frameBuf == None:  # 直接返回数据
            data = bytearray(size_width * DIB_h)
        pos = (DIB_h - 1) * size_width
        for j in range(DIB_h):
            row_data = file.read(row_size)
            for x in range(row_size):
                if x < size_width:
                    if frameBuf == None:  # 直接填充返回数据
                        data[pos + x] = row_data[x]
                    else:  # 绘制到framebuf
                        for n in range(8):
                            point = (row_data[x] & 0xff) >> (7 - n) & 0x1
                            frameBuf.pixel(x * 8 + n, DIB_h - j - 1, point)
            pos -= size_width
        gc.collect()
        return data


def randrange(n): # 坑爹,官网的没有编译其他随机函数,于是乎DIY个
    return int(random.getrandbits(8) % n)

def main_loop(timer):
    n = randrange(3)
    load_bmp(f'bg{n}.bmp', Global.layerFrame)

    x = randrange(200 - 96)
    y = randrange(200 - 88)

    Global.layerFrame.blit(Global.maskFrame, x, y, 0)
    Global.layerFrame.blit(Global.spriteFrame, x, y, 1)

    Global.epaper.set_frame_memory(Global.layerBuffer, 0, 0, 200, 200)  # 绘制背景
    Global.epaper.display_frame()

    print(f'paint bg{n}')

if __name__ == "__main__":
    load_bmp('sprite.bmp', Global.spriteFrame)
    load_bmp('mask.bmp', Global.maskFrame)

    Global.epaper.init()
    Global.epaper.clear_frame_memory(0xFF)
    random.seed(88)

    main_loop(None)
    Global.timer.init(period=10000, mode=Timer.PERIODIC, callback=main_loop)

微雪1.54''驱动 epaper1in54.py

"""
MicroPython Waveshare 1.54" Black/White GDEH0154D27 e-paper display driver
https://github.com/mcauser/micropython-waveshare-epaper
MIT License
Copyright (c) 2017 Waveshare
Copyright (c) 2018 Mike Causer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

from micropython import const
from time import sleep_ms
import ustruct

# Display resolution
EPD_WIDTH  = const(200)
EPD_HEIGHT = const(200)

# Display commands
DRIVER_OUTPUT_CONTROL                = const(0x01)
BOOSTER_SOFT_START_CONTROL           = const(0x0C)
#GATE_SCAN_START_POSITION             = const(0x0F)
DEEP_SLEEP_MODE                      = const(0x10)
DATA_ENTRY_MODE_SETTING              = const(0x11)
#SW_RESET                             = const(0x12)
#TEMPERATURE_SENSOR_CONTROL           = const(0x1A)
MASTER_ACTIVATION                    = const(0x20)
#DISPLAY_UPDATE_CONTROL_1             = const(0x21)
DISPLAY_UPDATE_CONTROL_2             = const(0x22)
WRITE_RAM                            = const(0x24)
WRITE_VCOM_REGISTER                  = const(0x2C)
WRITE_LUT_REGISTER                   = const(0x32)
SET_DUMMY_LINE_PERIOD                = const(0x3A)
SET_GATE_TIME                        = const(0x3B) # not in datasheet
#BORDER_WAVEFORM_CONTROL              = const(0x3C)
SET_RAM_X_ADDRESS_START_END_POSITION = const(0x44)
SET_RAM_Y_ADDRESS_START_END_POSITION = const(0x45)
SET_RAM_X_ADDRESS_COUNTER            = const(0x4E)
SET_RAM_Y_ADDRESS_COUNTER            = const(0x4F)
TERMINATE_FRAME_READ_WRITE           = const(0xFF) # aka NOOP

BUSY = const(1)  # 1=busy, 0=idle

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

    LUT_FULL_UPDATE    = bytearray(b'\x02\x02\x01\x11\x12\x12\x22\x22\x66\x69\x69\x59\x58\x99\x99\x88\x00\x00\x00\x00\xF8\xB4\x13\x51\x35\x51\x51\x19\x01\x00')
    LUT_PARTIAL_UPDATE = bytearray(b'\x10\x18\x18\x08\x18\x18\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x14\x44\x12\x00\x00\x00\x00\x00\x00')

    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(DRIVER_OUTPUT_CONTROL)
        self._data(bytearray([(EPD_HEIGHT - 1) & 0xFF]))
        self._data(bytearray([((EPD_HEIGHT - 1) >> 8) & 0xFF]))
        self._data(bytearray([0x00])) # GD = 0 SM = 0 TB = 0
        self._command(BOOSTER_SOFT_START_CONTROL, b'\xD7\xD6\x9D')
        self._command(WRITE_VCOM_REGISTER, b'\xA8') # VCOM 7C
        self._command(SET_DUMMY_LINE_PERIOD, b'\x1A') # 4 dummy lines per gate
        self._command(SET_GATE_TIME, b'\x08') # 2us per line
        self._command(DATA_ENTRY_MODE_SETTING, b'\x03') # X increment Y increment
        self.set_lut(self.LUT_FULL_UPDATE)

    def wait_until_idle(self):
        while self.busy.value() == BUSY:
            sleep_ms(100)

    def reset(self):
        self.rst(0)
        sleep_ms(200)
        self.rst(1)
        sleep_ms(200)

    def set_lut(self, lut):
        self._command(WRITE_LUT_REGISTER, lut)

    # put an image in the frame memory
    def set_frame_memory(self, image, x, y, w, h):
        # x point must be the multiple of 8 or the last 3 bits will be ignored
        x = x & 0xF8
        w = w & 0xF8

        if (x + w >= self.width):
            x_end = self.width - 1
        else:
            x_end = x + w - 1

        if (y + h >= self.height):
            y_end = self.height - 1
        else:
            y_end = y + h - 1

        self.set_memory_area(x, y, x_end, y_end)
        self.set_memory_pointer(x, y)
        self._command(WRITE_RAM, image)

    # replace the frame memory with the specified color
    def clear_frame_memory(self, color):
        self.set_memory_area(0, 0, self.width - 1, self.height - 1)
        self.set_memory_pointer(0, 0)
        self._command(WRITE_RAM)
        # send the color data
        for i in range(0, self.width // 8 * self.height):
            self._data(bytearray([color]))

    # draw the current frame memory and switch to the next memory area
    def display_frame(self):
        self._command(DISPLAY_UPDATE_CONTROL_2, b'\xC4')
        self._command(MASTER_ACTIVATION)
        self._command(TERMINATE_FRAME_READ_WRITE)
        self.wait_until_idle()

    # specify the memory area for data R/W
    def set_memory_area(self, x_start, y_start, x_end, y_end):
        self._command(SET_RAM_X_ADDRESS_START_END_POSITION)
        # x point must be the multiple of 8 or the last 3 bits will be ignored
        self._data(bytearray([(x_start >> 3) & 0xFF]))
        self._data(bytearray([(x_end >> 3) & 0xFF]))
        self._command(SET_RAM_Y_ADDRESS_START_END_POSITION, ustruct.pack("<HH", y_start, y_end))

    # specify the start point for data R/W
    def set_memory_pointer(self, x, y):
        self._command(SET_RAM_X_ADDRESS_COUNTER)
        # x point must be the multiple of 8 or the last 3 bits will be ignored
        self._data(bytearray([(x >> 3) & 0xFF]))
        self._command(SET_RAM_Y_ADDRESS_COUNTER, ustruct.pack("<H", y))
        self.wait_until_idle()

    # to wake call reset() or init()
    def sleep(self):
        self._command(DEEP_SLEEP_MODE, b'\x01') # enter deep sleep A0=1, A0=0 power on
        self.wait_until_idle()

相关项目代码下载 <这里>

最近又改了点,买了个水凝膜贴上通透了不少,还加了中文显示

python通过ESP8266发送数据给arduino python控制esp8266_LuatOS_04


 


电子墨水屏

电子墨水屏表面附着很多体积很小的“微胶囊”,封装了带有负电的黑色颗粒和带有正电的白色颗粒,通过改变电荷使不同颜色的颗粒有序排列,从而呈现出黑白分明的可视化效果。

python通过ESP8266发送数据给arduino python控制esp8266_电子墨水屏_05

相比于传统屏幕,电子墨水屏最大的特点就是省电节能,但是缺点就是颜色不够鲜艳,尽管能够显示彩色,并且屏幕可能刷新率和响应率都不如传统屏幕,所以一般都是作为副屏来使用

典型使用:
1.电子书
2.时钟(以分钟为单位刷新)
3.少量的即时通知
4.电子价签

墨水屏刷新原理

像素与字节的关系

对于黑白图片,我们可以规定,如果如果是黑色我们定义成0,如果是白色就定义成1,那么有了表示颜色的方式:
白色:□,对应1
黑色:■:对应0

  • 一个点在图形上一般称之为像素点(pixel),而颜色不是1就是0,也就是1个位就可以标识颜色:1Pixel = 1bit,那么一个字节里面就包含了8个像素点。
  • 以16个像素点为例,我们假设前8个像素点为黑,后8个像素点为白色,那么可以这么认为,像素点1-16,对应这0位到15位,0表示黑色,1表示白色:

python通过ESP8266发送数据给arduino python控制esp8266_MicroPython_06

对于计算机而言,它的数据存储方式是高位在前,低位在后,且一个字节只有8个位,因此会有一点改变:

这样只需要2个字节即可表示16个像素点了。