How to Play With the Wemos D32
Fleeting- plugged the board
- take the firmware from https://micropython.org/download/ESP32_GENERIC/
- esptool.py erase_flash
- esptool.py –baud 460800 write_flash 0x1000 ESP32_*.bin
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 ( )
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.