Konubinix' opinionated web of thoughts

How to Play With the Wemos D32

Fleeting

Now, the same as how to play with the wemos d1

from machine import Pin
p = Pin(5, Pin.OUT)
p.value(0)
p.value(1)

trying to drive a epaper 2.9 from waveshare

using a micropython library

rdagger

mpremote mip install github:rdagger/MicroPython-2.9-inch-ePaper-Library/
mpremote mip install https://github.com/i-infra/uQR/blob/main/uQR.py
CS_PIN = 5  # CS
DC_PIN = 17  # Data/Command
RST_PIN = 16  # Reset
BUSY_PIN = 4  # Busy

from time import sleep
from machine import Pin, SPI  # type: ignore
from esp2in9bv2 import Display

spi = SPI(2, baudrate=1000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(23))
display = Display(spi, dc=Pin(DC_PIN), cs=Pin(CS_PIN), rst=Pin(RST_PIN), busy=Pin(BUSY_PIN))

display.draw_rectangle(0, 0, 63, 63)

display.present()

It gets stuck on ReadBusy. Changing the idle value from 1 to 0 made the init pass, but it gets stuck on display.present(). This is likely because the code is meant for the 3 colors version.

mcauser

mpremote mip install https://raw.githubusercontent.com/mcauser/micropython-waveshare-epaper/refs/heads/master/epaper2in9.py
CS_PIN = 5  # CS
DC_PIN = 17  # Data/Command
RST_PIN = 16  # Reset
BUSY_PIN = 4  # Busy

import epaper2in9
from machine import SPI, Pin

# SPI3 on Black STM32F407VET6
spi = SPI(2, baudrate=1000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(23))
cs = Pin(CS_PIN)
dc = Pin(DC_PIN)
rst = Pin(RST_PIN)
busy = Pin(BUSY_PIN)

e = epaper2in9.EPD(spi, cs, dc, rst, busy)
e.init()

w = 128
h = 296
x = 0
y = 0

e.clear_frame_memory(0) # black

e.display_frame()

Nothing happens

trying manually

With the help of chatgpt, claude and geminy, I could grok something that works.

import time

from machine import SPI, Pin

# Pin definitions (your wiring)
CS_PIN = 5  # CS
DC_PIN = 17  # Data/Command
RST_PIN = 16  # Reset
BUSY_PIN = 4  # Busy

# Display resolution (for Waveshare 2.9" b/w)
EPD_WIDTH = 128
EPD_HEIGHT = 296

# SPI setup (SPI2 on ESP32: sck=18, mosi=23, miso not used)
spi = SPI(2, baudrate=1000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(23))

# Control pins
cs = Pin(CS_PIN, Pin.OUT, value=1)
dc = Pin(DC_PIN, Pin.OUT, value=0)
rst = Pin(RST_PIN, Pin.OUT, value=1)
busy = Pin(BUSY_PIN, Pin.IN, Pin.PULL_UP)

current_lut_mode = "full"
x_shift = 1


def digital_write(pin, value):
    pin.value(value)


def send_command(command):
    digital_write(dc, 0)
    digital_write(cs, 0)
    spi.write(bytearray([command]))
    digital_write(cs, 1)


def send_data(data):
    digital_write(dc, 1)
    digital_write(cs, 0)
    spi.write(bytearray([data]))
    digital_write(cs, 1)


def reset():
    digital_write(rst, 1)
    time.sleep_ms(200)
    digital_write(rst, 0)
    time.sleep_ms(10)
    digital_write(rst, 1)
    time.sleep_ms(200)


def wait_until_idle():
    while busy.value() == 1:
        time.sleep_ms(100)


def init():
    print("Initializing e-paper display...")
    reset()

    # Wait a bit after reset before sending commands
    time.sleep_ms(100)

    # Check if display is responding
    print(f"Busy pin state after reset: {busy.value()}")

    wait_until_idle()

    # Software reset command (optional but recommended)
    send_command(0x12)  # Software reset
    wait_until_idle()

    send_command(0x01)  # Driver output control
    send_data(0x27)  # 296-1 = 0x127 (low byte)
    send_data(0x01)  # 296-1 = 0x127 (high byte)
    send_data(0x00)  # GD = 0; SM = 0; TB = 0;

    send_command(0x0C)  # Booster soft start control
    send_data(0xD7)
    send_data(0xD6)
    send_data(0x9D)

    send_command(0x2C)  # Write VCOM register
    send_data(0xA8)  # VCOM 7C

    send_command(0x3A)  # Set dummy line period
    send_data(0x1A)  # 4 dummy lines per gate

    send_command(0x3B)  # Set gate time
    send_data(0x08)  # 2us per line

    send_command(0x11)  # Data entry mode
    send_data(0x03)  # X increment; Y increment

    # Set RAM area
    send_command(0x44)  # Set RAM X address start/end position
    send_data(0x00 + x_shift)  # X start = 1 (shifted right by 1 to fix left offset)
    send_data(0xF + x_shift)  # X end = 16 (was 15, now 16 to compensate)

    send_command(0x45)  # Set RAM Y address start/end position
    send_data(0x00)  # Y start = 0 (low)
    send_data(0x00)  # Y start = 0 (high)
    send_data(0x27)  # Y end = 295 (low)
    send_data(0x01)  # Y end = 295 (high)

    send_command(0x4E)  # Set RAM X address counter
    send_data(0x00 + x_shift)  # Start at X=1 instead of X=0

    send_command(0x4F)  # Set RAM Y address counter
    send_data(0x00)
    send_data(0x00)

    set_lut("full")  # Default to full refresh LUT

    wait_until_idle()
    print("Display initialization complete")


def clear_black():
    print("Clearing display to black...")

    # Set RAM X address counter to start
    send_command(0x4E)
    send_data(0x00 + x_shift)  # Start at X=1

    # Set RAM Y address counter to start
    send_command(0x4F)
    send_data(0x00)
    send_data(0x00)

    send_command(0x24)  # Write RAM (black/white)

    # Send black data (0x00 = black pixels)
    for y in range(EPD_HEIGHT):
        for x in range(EPD_WIDTH // 8):
            send_data(0x00)  # All pixels black

    print("Data written to RAM, starting display refresh...")
    send_command(0x20)  # Display update control 2
    wait_until_idle()

    print("Display cleared to black")


def clear_white():
    print("Clearing display to white...")

    # Set RAM X address counter to start
    send_command(0x4E)
    send_data(0x00 + x_shift)  # Start at X=1

    # Set RAM Y address counter to start
    send_command(0x4F)
    send_data(0x00)
    send_data(0x00)

    send_command(0x24)  # Write RAM (black/white)

    # Send white data (0xFF = white pixels)
    for y in range(EPD_HEIGHT):
        for x in range(EPD_WIDTH // 8):
            send_data(0xFF)  # All pixels white

    print("Data written to RAM, starting display refresh...")
    send_command(0x20)  # Display update control 2
    wait_until_idle()

    print("Display cleared to white")


FONT_8X8 = {
    " ": [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
    "!": [0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00],
    "H": [0x66, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00],
    "e": [0x00, 0x00, 0x3C, 0x06, 0x3E, 0x66, 0x3E, 0x00],
    "l": [0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00],
    "o": [0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x00],
    "W": [0x66, 0x66, 0x66, 0x66, 0x66, 0x7E, 0x66, 0x00],
    "r": [0x00, 0x00, 0x5C, 0x66, 0x60, 0x60, 0x60, 0x00],
    "d": [0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, 0x00],
}


def draw_char(buffer, x, y, char, color=0):
    """Draw a character at position (x,y) with given color (0=black, 1=white)"""
    if char not in FONT_8X8:
        return

    char_data = FONT_8X8[char]

    for row in range(8):
        if y + row >= EPD_HEIGHT:
            break
        byte_data = char_data[row]

        for col in range(8):
            if x + col >= EPD_WIDTH:
                break

            # Check if pixel should be set
            if byte_data & (0x80 >> col):
                pixel_color = color
            else:
                pixel_color = 1 - color  # Background color

            # Calculate buffer position
            buffer_x = x + col
            buffer_y = y + row
            byte_index = (buffer_y * EPD_WIDTH + buffer_x) // 8
            bit_index = 7 - ((buffer_x) % 8)

            if byte_index < len(buffer):
                if pixel_color == 0:  # Black
                    buffer[byte_index] &= ~(1 << bit_index)
                else:  # White
                    buffer[byte_index] |= 1 << bit_index


def draw_string(buffer, x, y, text, color=0):
    """Draw a string starting at position (x,y)"""
    for i, char in enumerate(text):
        draw_char(buffer, x + i * 8, y, char, color)


def create_buffer():
    """Create a blank white buffer"""
    buffer_size = (EPD_WIDTH * EPD_HEIGHT) // 8
    return bytearray([0xFF] * buffer_size)  # 0xFF = all white


def display_buffer(buffer):
    """Send buffer data to display"""
    print("Sending buffer to display...")

    # Set RAM X address counter to start
    send_command(0x4E)
    send_data(0x00 + x_shift)  # Start at X=1

    # Set RAM Y address counter to start
    send_command(0x4F)
    send_data(0x00)
    send_data(0x00)

    send_command(0x24)  # Write RAM (black/white)

    # Send buffer data
    for byte in buffer:
        send_data(byte)

    print("Buffer sent, refreshing display...")
    if current_lut_mode == "full":
        print("full")
        send_command(0x20)
    elif current_lut_mode == "partial":
        print("partial")
        send_command(0x22)
        wait_until_idle()
        print("Display updated!")


def hello_world():
    """Display 'Hello World!' on the e-paper display"""
    print("Creating Hello World display...")

    # Create white buffer
    buffer = create_buffer()

    # Draw "Hello World!" in the center-ish area
    draw_string(buffer, 20, 100, "Hello", 0)  # Black text
    draw_string(buffer, 20, 120, "World!", 0)  # Black text

    # You can also add a border or other decorations
    # Draw a simple border (optional)
    for x in range(EPD_WIDTH):
        # Top and bottom borders
        byte_index_top = x // 8
        byte_index_bottom = ((EPD_HEIGHT - 1) * EPD_WIDTH + x) // 8
        bit_index = 7 - (x % 8)

        if byte_index_top < len(buffer):
            buffer[byte_index_top] &= ~(1 << bit_index)  # Black pixel
        if byte_index_bottom < len(buffer):
            buffer[byte_index_bottom] &= ~(1 << bit_index)  # Black pixel

    for y in range(EPD_HEIGHT):
        # Left and right borders
        if y * EPD_WIDTH // 8 < len(buffer):
            buffer[y * EPD_WIDTH // 8] &= ~0x80  # Left border
            right_byte_index = (y * EPD_WIDTH + EPD_WIDTH - 1) // 8
        if right_byte_index < len(buffer):
            buffer[right_byte_index] &= ~0x01  # Right border

    # Send to display
    display_buffer(buffer)


def test_alignment():
    """Test function to check display alignment"""
    print("Testing display alignment...")

    buffer = create_buffer()

    # Draw vertical lines every 16 pixels to test alignment
    for x in range(0, EPD_WIDTH, 16):
        for y in range(EPD_HEIGHT):
            byte_index = (y * EPD_WIDTH + x) // 8
            bit_index = 7 - (x % 8)
            if byte_index < len(buffer):
                buffer[byte_index] &= ~(1 << bit_index)  # Black pixel

    # Draw a reference pattern in the top-left corner
    draw_string(buffer, 0, 0, "0", 0)  # Should appear at true left edge
    draw_string(buffer, 8, 0, "8", 0)  # 8 pixels from left
    draw_string(buffer, 16, 0, "16", 0)  # 16 pixels from left

    display_buffer(buffer)


# fmt: off
# Full Refresh LUT data
FULL_LUT_DATA = bytearray([
    0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22,
    0x66, 0x69, 0x69, 0x59, 0x58, 0x99, 0x99, 0x88,
    0x00, 0x00, 0x00, 0x00, 0xF8, 0xB4, 0x13, 0x51,
    0x35, 0x51, 0x51, 0x19, 0x01, 0x00
])

# Partial Refresh LUT data
PARTIAL_LUT_DATA = bytearray([
    0x10, 0x18, 0x18, 0x00, 0x00, 0x02, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x12, 0x12, 0x12, 0x12,
    0x12, 0x12, 0x12, 0x12, 0x12, 0x12
])
# fmt: on


def set_lut(mode):
    """
    Sets the Look-Up Table (LUT) for the display.

    Args:
        mode (str): 'full' for full refresh or 'partial' for partial refresh.
    """
    if mode == "full":
        print("Setting full refresh LUT...")
        lut_data = FULL_LUT_DATA
    elif mode == "partial":
        print("Setting partial refresh LUT...")
        lut_data = PARTIAL_LUT_DATA
    else:
        print("Invalid mode. Use 'full' or 'partial'.")
        return

    # Write the LUT data
    send_command(0x32)
    for data in lut_data:
        send_data(data)

    wait_until_idle()
    global current_lut_mode
    current_lut_mode = mode
    print(f"LUT set to {mode} refresh.")


def main():
    init()
    clear_white()
    hello_world()
    # Uncomment below to test alignment instead:
    # test_alignment()

This gives that result.

with adruino

Using https://github.com/waveshareteam/e-Paper/Arduino/epd2in9_V2/

setup arduino for that board

installing the core for esp32

https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html

arduino-cli core list
ID          Installed Latest Name
arduino:avr 1.8.6     1.8.6  Arduino AVR Boards
arduino-cli config init --overwrite
cat<<EOF>"${HOME}/.arduino15/arduino-cli.yaml"
board_manager:
  additional_urls:
    - https://espressif.github.io/arduino-esp32/package_esp32_index.json
EOF
arduino-cli core update-index

Downloading index: package_index.tar.bz2 0 B / 84.51 KiB    0.00%
Downloading index: package_index.tar.bz2 downloaded

Downloading index: package_esp32_index.json 0 B / ?    0.00%
Downloading index: package_esp32_index.json downloaded
arduino-cli core search esp32
ID            Version          Name
arduino:esp32 2.0.18-arduino.5 Arduino ESP32 Boards
esp32:esp32   3.3.0            esp32
arduino-cli core install esp32:esp32
arduino-cli core list
ID          Installed Latest Name
arduino:avr 1.8.6     1.8.6  Arduino AVR Boards
esp32:esp32 3.3.0     3.3.0  esp32
finding the fqdn support for that board
arduino-cli board listall |gi lolin|gi 32
LOLIN C3 Mini                                      esp32:esp32:lolin_c3_mini
LOLIN C3 Pico                                      esp32:esp32:lolin_c3_pico
LOLIN D32                                          esp32:esp32:d32
LOLIN D32 PRO                                      esp32:esp32:d32_pro
LOLIN S2 Mini                                      esp32:esp32:lolin_s2_mini
LOLIN S2 PICO                                      esp32:esp32:lolin_s2_pico
LOLIN S3                                           esp32:esp32:lolin_s3
LOLIN S3 Mini                                      esp32:esp32:lolin_s3_mini
LOLIN S3 Mini Pro                                  esp32:esp32:lolin_s3_mini_pro
LOLIN S3 Pro                                       esp32:esp32:lolin_s3_pro
WEMOS LOLIN32                                      esp32:esp32:lolin32
WEMOS LOLIN32 Lite                                 esp32:esp32:lolin32-lite

It should be esp32:esp32:d32

building the code
arduino-cli compile --fqbn esp32:esp32:d32 epd2in9_V2.ino 2>&1 | ansifilter --text
/home/sam/test/next/e-Paper/Arduino/epd2in9_V2/epdpaint.cpp:27:10: fatal error: avr/pgmspace.h: No such file or directory
   27 | #include <avr/pgmspace.h>
      |          ^~~~~~~~~~~~~~~~
compilation terminated.

Used library Version Path
SPI          3.3.0   /home/sam/.arduino15/packages/esp32/hardware/esp32/3.3.0/libraries/SPI

Used platform Version Path
esp32:esp32   3.3.0   /home/sam/.arduino15/packages/esp32/hardware/esp32/3.3.0
Error during build: exit status 1

Oups, that code is meant to work only for arduino boards, not esp ones.

I missed that

The Arduino program in this package only supports the Arduino series development boards, not the ESP32, ESP8266 and other development boards that use the Arduino IDE for development. ESP32 development board use: https://www.waveshare.com/wiki/E-Paper_ESP32_Driver_Board ESP8266 development board use: https://www.waveshare.com/wiki/E-Paper_ESP8266_Driver_Board

https://github.com/waveshareteam/e-Paper/blob/master/Special%20Reminder.txt ([2025-08-26 Tue])

They actually say that we need a 15$ special board to play with the screen with esp32, and another for esp8266. That’s too bad…

Fortunately, it seems to be the almost same library moved elsewhere.

sed -i 's|avr/pgmspace|pgmspace|' *
arduino-cli compile --fqbn esp32:esp32:d32 epd2in9_V2.ino 2>&1 | ansifilter --text
Sketch uses 312767 bytes (23%) of program storage space. Maximum is 1310720 bytes.
Global variables use 22008 bytes (6%) of dynamic memory, leaving 305672 bytes for local variables. Maximum is 327680 bytes.
arduino-cli upload --port /dev/ttyUSB0 --fqbn esp32:esp32:d32 epd2in9_V2.ino

mpremote says

Image checksmets Jul 29 2019 12:21:46

rst:0x8 (TG1WDT_SYS_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0030,len:4744
load:0x40078000,len:15672
load:0x40080400,len:3152
entry 0x4008059c
E (277) esp_core_dump3x+อก: Core dump data check failed:
Calculated checksum='f86bcc05'

It’s definitely not good.

esp-idf

I could try with https://github.com/krzychb/esp-epaper-29-ws, but 8 Years old code with only 4 commits is not a good smell.

esp-home

I could also try https://esphome.io/components/display/waveshare_epaper/, that seems like the more maintained choice so far.