Elecrow CrowPanel 5.79" E-Paper display

E-Paper display with 792*272 resolution, using ESP32-S3 as the main control, powerful performance.

Specification

Panel size168mm * 57mm
Screen size5.79 inch
Display ColorBlack and white
Resolution792(L) * 272(H) Pixel
Pixel pitch0.1755*0.1755
Viewing AngleFull Viewing Angle
MCUESP32-S3-WROOM-1-N8R8,up to 240 MHz
Flash8 MB
PSRAM8 MB
MaterialActive Matrix Electroph oretic Display (AM EPD)
Driver ChipSSD1683 * 2
Communication interface3-/4-wire SPI, default 4-wire SPI
InterfaceUART0x1, BATx1, GPIOx1, TF Card Slot x1
ButtonDial Switchx1, Menu Buttonx1, Back Buttonx1, REST Buttonx1, BOOT Buttonx1
Development EnvironmentArduino IDE、ESP IDF、MicroPython
Refresh ModePartial refresh (saves more power)
Display Voltage2.2~3.7V
Operation Temperature-0~50℃
Storage Temperature-25~70℃
Active Area47.74(H)*139.00(L)(H*L)

Code

CrowPanel.py
# This library CrowPanel E-paper display, based on SSD1683 chips
# CrowPanel 5.79"
# CrowPanel 4.20" (not tested)
# 
# Library is based on micropython frameBuffer
#
# V0.1.0 Dec 2025 Initial version
# V0.1.1 Dec 2025 Fixed initialization of screen
#
# Released under the MIT License (MIT).
# Copyright (c) 2025 Ignas Bukys
 
 
from micropython import const
from time import sleep_ms
import framebuf
from ustruct import pack
from io import BytesIO
from machine import SPI, Pin
 
__version__ = (0, 1, 1)
 
# Display colour codes
COLOR_WHITE = const(1)
COLOR_BLACK = const(0)
 
#generic class for chip
class SSD1683(framebuf.FrameBuffer):
    '''Low-level controls for E-Paper chip'''
 
    # Constants for SSD1608 driver IC
    SET_DRIVER_CONTROL      = const(0x01)
    SET_GATE_VOLTAGE        = const(0x03)
    SET_SOURCE_VOLTAGE      = const(0x04)
    SET_DISPLAY_CONTROL     = const(0x07)
    SET_NON_OVERLAP         = const(0x0B)
    SET_BOOSTER_SOFT_START  = const(0x0C)
    SET_GATE_SCAN_START     = const(0x0F)
    SET_DEEP_SLEEP          = const(0x10)
    SET_DATA_MODE           = const(0x11)
    SET_DATA_MODE_SLAVE     = const(0x91)
    SET_SW_RESET            = const(0x12)
    SET_TEMP_WRITE          = const(0x1A)
    SET_TEMP_READ           = const(0x1B)
    SET_TEMP_CONTROL        = const(0x18)
    SET_TEMP_LOAD           = const(0x1A)
    SET_MASTER_ACTIVATE     = const(0x20)
    SET_DISP_CTRL1          = const(0x21)
    SET_DISP_CTRL2          = const(0x22)
    SET_WRITE_RAM           = const(0x24)
    SET_WRITE_ALTRAM        = const(0x26)
    SET_READ_RAM            = const(0x25)
    SET_VCOM_SENSE          = const(0x2B)
    SET_VCOM_DURATION       = const(0x2C)
    SET_WRITE_VCOM          = const(0x2C)
    SET_READ_OTP            = const(0x2D)
    SET_WRITE_LUT           = const(0x32)
    SET_WRITE_DUMMY         = const(0x3A)
    SET_WRITE_GATELINE      = const(0x3B)
    SET_WRITE_BORDER        = const(0x3C)
    SET_RAMXPOS             = const(0x44)
    SET_RAMYPOS             = const(0x45)
    SET_RAMXCOUNT           = const(0x4E)
    SET_RAMYCOUNT           = const(0x4F)
    SET_WRITE_RAM_SLAVE     = const(0xA4)
    SET_WRITE_ALTRAM_SLAVE  = const(0xA6)
    SET_RAMXPOS_SLAVE       = const(0xC4)
    SET_RAMYPOS_SLAVE       = const(0xC5)
    SET_RAMXCOUNT_SLAVE     = const(0xCE)
    SET_RAMYCOUNT_SLAVE     = const(0xCF)
    SET_NOP                 = const(0xFF)
 
    # Pins for communication
    LED_PIN = 41
    RESET_PIN = 47
    BUSY_PIN = 48
    DC_PIN = 46
    MOSI_PIN = 11
    SCK_PIN = 12
    CS_PIN = 45
    SCREEN_POWER_PIN = 7
 
    # Rotation
    ROTATION_0 = const(0)
    ROTATION_90 = const(1)
    ROTATION_180 = const(2)
    ROTATION_270 = const(3)
 
 
    def __init__(self, w, h, rotation=ROTATION_0):
        self._init_spi()
        self._init_buffer(w, h, rotation)
        self.FastMode1Init()
        self.HW_RESET()
 
    def _init_spi(self):
        #Set pin 7 to high level to activate the screen power
        Pin(self.SCREEN_POWER_PIN, Pin.OUT, value=1)
 
        self.cs = Pin(self.CS_PIN, Pin.OUT)
        self.dc = Pin(self.DC_PIN, Pin.OUT)
        self.rst = Pin(self.RESET_PIN, Pin.OUT)
        self.busy = Pin(self.BUSY_PIN, Pin.IN)
 
        self.spi = SPI(1,
            baudrate=4_000_000,
            sck=Pin(self.SCK_PIN),
            mosi=Pin(self.MOSI_PIN),
            polarity=0,
            phase=0,
            firstbit=SPI.MSB)
 
        self.spi.init()
        self.cs.init(self.cs.OUT, value=1)
        self.dc.init(self.dc.OUT, value=1)
        self.rst.init(self.rst.OUT, value=1)
        self.busy.init(self.busy.IN, value=0)
 
 
    def _cmd(self, command, data=None):
        '''command and optional 1 byte of data'''
        self.cs(1)
        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):
        '''one byte of data'''
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(bytearray([data]))
        self.cs(1)
 
 
    def _data_s(self, data):
        '''data in stream of bytes'''
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(pack('B'*len(data), *data))
        self.cs(1)
 
 
    def _init_buffer(self, w, h, rotation):
        self._rotation = rotation
        size = w * h // 8
        self.buffer = bytearray(size)
 
        if self._rotation == self.ROTATION_0 or self._rotation == self.ROTATION_180:
            self.width = w
            self.height = h
        else:
            self.width = w
            self.height = h
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_HLSB)
        print('Buffer width:{}, height:{}, size:{}'.format(self.width, self.height, size))
 
 
    def _wait_until_idle(self):
        while self.busy.value() == 1:
            sleep_ms(10)
 
 
    def HW_RESET(self):
        '''Perform Hardware reset'''
        sleep_ms(10)
        self.rst(0)
        sleep_ms(10)
        self.rst(1)
        sleep_ms(10)
        self._wait_until_idle()
 
 
    def EPD_Init(self):
        '''Intialize screen by doing Hard and Soft reset. Wait till busy pin low'''
        self.HW_RESET()
        self._wait_until_idle()
        self._cmd(self.SET_SW_RESET)
        self._wait_until_idle()
 
 
    def FastMode1Init(self):
        self.EPD_Init()
 
        self._cmd(self.SET_TEMP_CONTROL, 0x80)       # Read built-in temperature sensor
 
        self._cmd(self.SET_DISP_CTRL2, 0xB1)         # Load temperature value
        self._cmd(self.SET_MASTER_ACTIVATE)
        self._wait_until_idle()
 
        self._cmd(self.SET_TEMP_WRITE, 0x64)         # Write to temperature register
        self._data(0x00)
 
        self._cmd(self.SET_DISP_CTRL2, 0x91)         # Load temperature value
        self._cmd(self.SET_MASTER_ACTIVATE)
        self._wait_until_idle()
 
        self._cmd(self.SET_WRITE_BORDER, 0x1)        # 0x3 | 0-ryškus 1-jokio 2-jokio
        self._wait_until_idle()
 
 
    def Display_Clear(self, count):
        '''Fill ram and altram of both chips with 0s and 1s'''
        self.SetRAMMP()
        self.SetRAMMA()
        self._cmd(self.SET_WRITE_RAM)
        self._data_s(b'\xFF' * count)
        self.SetRAMMA()
        self._cmd(self.SET_WRITE_ALTRAM)
        self._data_s(b'\x00' * count)
        self.SetRAMSP()
        self.SetRAMSA()
        self._cmd(self.SET_WRITE_RAM_SLAVE)
        self._data_s(b'\xFF' * count)
        self.SetRAMSA()
        self._cmd(self.SET_WRITE_ALTRAM_SLAVE)
        self._data_s(b'\x00' * count)
 
 
    def SetRAMMP(self):
        '''Data entry mode for ram primary'''
        self._cmd(self.SET_DATA_MODE, 0x02)      # Data Entry mode setting; 1 –Y decrement, X increment
        self._cmd(self.SET_RAMXPOS)              # Set Ram X- address Start / End position
        self._data(0x31)                         # XStart, POR = 00h
        self._data(0x00)
        self._cmd(self.SET_RAMYPOS)              # Set Ram Y- address  Start / End position
        self._data(0x00)
        self._data(0x00)
        self._data(0x0f)
        self._data(0x01)
 
 
    def SetRAMMA(self):
        '''Data entry mode for altram primary'''
        self._cmd(self.SET_RAMXCOUNT, 0x31)
        self._cmd(self.SET_RAMYCOUNT, 0x00)
        self._data(0x00)
 
 
    def SetRAMSP(self):
        '''Data entry mode for ram Slave'''
        self._cmd(self.SET_DATA_MODE_SLAVE, 0x03)
        self._cmd(self.SET_RAMXPOS_SLAVE)
        self._data(0x00)
        self._data(0x31)
        self._cmd(self.SET_RAMYPOS_SLAVE)
        self._data(0x00)                # Set Ram Y- address  Start / End position
        self._data(0x00)
        self._data(0x0f)                # YEnd L
        self._data(0x01)
 
 
    def SetRAMSA(self):
        '''Data entry mode for altram Slave'''
        self._cmd(self.SET_RAMXCOUNT_SLAVE, 0x00)
        self._cmd(self.SET_RAMYCOUNT_SLAVE, 0x00)
        self._data(0x00)
 
 
    def Update(self):
        self._cmd(self.SET_DISP_CTRL2, 0xF7)
        self._cmd(self.SET_MASTER_ACTIVATE)
        self._wait_until_idle()
 
 
    def PartUpdate(self):
        self._cmd(self.SET_DISP_CTRL2, 0xDC)
        self._cmd(self.SET_MASTER_ACTIVATE)
        self._wait_until_idle()
 
 
    def FastUpdate(self):
        self._cmd(self.SET_DISP_CTRL2, 0xC7)
        self._cmd(self.SET_MASTER_ACTIVATE)
        self._wait_until_idle()
 
 
    def DeepSleep(self, mode=0x01):
        '''Send device to sleep and save power
 
        To wake up call EPD_Init()
 
        Parameters
        ----------
        int: mode
            0x00- Normal
            0x01- Mode1
            0x11- Mode2 (without RAM retention)
        '''
        self._cmd(self.SET_DEEP_SLEEP, mode)
        sleep_ms(5)
 
 
    def LoadImage(self, PosX, PosY, ImgName, ImgWidth, ImgHeight):
        ''' Load image into frame buffer on predefined possition
 
        Your file must be Black and White. 
        You can use https://javl.github.io/image2cpp/
        Draw mode must be "Horizontal- 1 bit per pixel"
        framebuf.MONO_HLSB
 
        Parameters
        ----------
        int : Possition X on buffer
        int : Possition Y on buffer
        str : Image location
        int : Image width
        int : Image height
 
        Raises
        ------
        ValueError
            Sometime buffer construct method return it. I try to choose 2^X images of square dimensions
        '''
        # Create a bytearray to store the image data
        img_data = bytearray(ImgWidth * ImgHeight // 8)
 
        with open(ImgName, 'rb') as f:
            f.readinto(img_data)
 
        # Create a FrameBuffer object from the image data
        img_buf = framebuf.FrameBuffer(img_data, ImgWidth, ImgHeight, framebuf.MONO_HLSB)
        self.blit(img_buf, PosX, PosY)
 
 
class Screen_579(SSD1683):
    '''device specifics for CrowPanel 5.79" size'''
 
    # Resolution
    EPD_WIDTH = 792
    EPD_HEIGHT = 272
 
    def __init__(self):
        super().__init__(self.EPD_WIDTH, self.EPD_HEIGHT)
        self.Prepare((self.EPD_WIDTH+8) * self.EPD_HEIGHT // 8)
 
 
    def Prepare(self, count):
        '''Fill RAM and AltRAM of both chips with 0s and 1s
        for proper start'''
        self.SetRAMMP()
        self.SetRAMMA()
        self._cmd(self.SET_WRITE_RAM)
        self._data_s(b'\xFF' * count)
        self.SetRAMMA()
        self._cmd(self.SET_WRITE_ALTRAM)
        self._data_s(b'\x00' * count)
        self.SetRAMSP()
        self.SetRAMSA()
        self._cmd(self.SET_WRITE_RAM_SLAVE)
        self._data_s(b'\xFF' * count)
        self.SetRAMSA()
        self._cmd(self.SET_WRITE_ALTRAM_SLAVE)
        self._data_s(b'\x00' * count)
 
 
    def show(self, mode=1):
        '''Show buffer on screen.
 
        Parameters
        ----------
        mode : int, optional
            1- Fast;
            2- Partial;
            0- Full mode. Slowest but most clear view
 
        Raises
        ------
        ValueError
            Buffer size is not as expected according to screen dimension
        '''
        if len(self.buffer) != self.EPD_WIDTH * self.EPD_HEIGHT / 8:
            raise ValueError(f"Invalid frame buffer size. Expected {self.EPD_WIDTH * self.EPD_HEIGHT} bytes.")
 
        # prepare file-like object to work with
        bitmap_buffer = BytesIO(self.buffer)
 
        while True:
            chunk = bitmap_buffer.read(50)
            if not chunk: break
 
            self._cmd(self.SET_WRITE_RAM_SLAVE)
            self._data_s(chunk)
 
            # Simulate "partial data hidden" behavior (optional)
            bitmap_buffer.seek(-1, 1)  # Not recommended for BytesIO, can cause errors
            # on screen intersection, half of byte is invisible. On right screen- 4 MSB's, 
            # on right- 4 LSB's. That why need to share same byte between two screens
 
            # Read the next chunk (if any)
            chunk = bitmap_buffer.read(50)
            if not chunk: break
 
            self._cmd(self.SET_WRITE_RAM)
            self._data_s(chunk)
        bitmap_buffer.close()
 
        if mode == 1:
            self.FastUpdate()
        elif mode == 2:
            self.PartUpdate()
        else:
            self.Update()
 
 
class Screen_420(SSD1683):
    '''device specifics for CrowPanel 4.2" size'''
 
    # Resolution
    EPD_HEIGHT = 300
    EPD_WIDTH = 400
 
    def __init__(self):
        raise NotImplementedError
        '''probably initialization may be different if screen
        orientation differs from 5.79 screen'''
        super().__init__(self.EPD_WIDTH, self.EPD_HEIGHT)
        self.Prepare(self.EPD_WIDTH * self.EPD_HEIGHT // 8)
 
 
    def Prepare(self, count):
        '''Fill RAM and AltRAM of chip with 0s and 1s
        for proper start'''
        self.SetRAMMP()
        self.SetRAMMA()
        self._cmd(self.SET_WRITE_RAM)
        self._data_s(b'\xFF' * count)
        self.SetRAMMA()
        self._cmd(self.SET_WRITE_ALTRAM)
        self._data_s(b'\x00' * count)
 
 
    def show(self, mode=1):
        '''Show buffer on screen.
 
        Parameters
        ----------
        mode : int, optional
            1- Fast;
            2- Partial;
            0- Full mode. Slowest but most clear view
 
        Raises
        ------
        ValueError
            Buffer size is not as expected according to screen dimension
        '''
 
        if len(self.buffer) != self.EPD_WIDTH * self.EPD_HEIGHT / 8:
            raise ValueError(f"Invalid frame buffer size. Expected {self.EPD_WIDTH * self.EPD_HEIGHT} bytes.")
 
        self._cmd(self.SET_WRITE_RAM)
        self._data_s(self.buffer)
 
        if mode == 1:
            self.FastUpdate()
        elif mode == 2:
            self.PartUpdate()
        else:
            self.Update()

main.py
import time
time.sleep(1)
# short sleep to CTRL+C if something goes wrong
 
import CrowPanel as eink
 
# Instantiate a Screen
screen = eink.Screen_579()
 
# Test another font
# bassed on https://github.com/peterhinch/micropython-font-to-py/tree/master
from writer import Writer
import freesans20
wri = Writer(screen, freesans20)
 
# prepare framebuffer
screen.fill(eink.COLOR_WHITE)
 
screen.LoadImage(30, 10, 'Images/CrowPanel_64_32.bin', 64, 32)
Writer.set_textpos(screen, 130, 15)
wri.printstring('CrowPanel ESP32 5.79" E-paper Display with 272*792 Resolution', True)
 
#diagonal line, to confirm correct display between 2 screens
screen.line(50, 50, 750, 222, eink.COLOR_BLACK)
screen.text("diagonal line, to confirm correct", 280, 115, eink.COLOR_BLACK)
screen.text("display between 2 screens", 295, 135, eink.COLOR_BLACK)
 
# Draw arc
screen.text("Draw ARC from two semi-ellipses", 30, 180, eink.COLOR_BLACK)
screen.ellipse(130,170, 50, 50, eink.COLOR_BLACK, True, 3)
screen.ellipse(130,175, 50, 50, eink.COLOR_WHITE, True, 3)
 
screen.LoadImage(600, 50, 'Images/houseImg128.bin', 128, 128)
screen.text("Load BW image", 615, 50, eink.COLOR_BLACK)
 
Writer.set_textpos(screen, 280, 250)
wri.printstring('Inverted Color of another font')
 
#Load buffer to screen and display
screen.show()
  • components/crowpanel_5_79.txt
  • Last modified: 2025/01/06 20:50
  • by Ignas