a photo of Whexy

Wenxuan

CyberSecurity Researcher at Northwestern University

使用 MicroPython 在树莓派 Pico 上驱动微雪墨水屏显示器

Whexy /
February 09, 2025

墨水屏(又称 "电子墨水")显示器非常适合低功耗和日光下可读的应用。在这篇文章中,我们将展示如何将 800 × 480 墨水屏连接到树莓派 Pico,然后通过 MicroPython 代码显示图像。

硬件设置

我们假设:

  • 一个具有 SPI 接口的微雪风格 7.5″ 墨水屏模块(分辨率 800×480)。
  • 一个运行 MicroPython 的树莓派 Pico 2。

我们需要使用 Pico 的 SPI 接口。Pico 2 有两个 SPI 接口:SPI0 和 SPI1。我们将使用 SPI0。请参见下面的引脚图。

Pico 2 Pinout

Pico 2 Pinout

以下是我的引脚连接。墨水屏通常有以下信号:

墨水屏引脚描述Pico SPI0 示例引脚
VCC3.3 V 电源3V3 (例如 引脚 36)
GND地线GND (例如 引脚 38)
DINMOSI (SPI TX)SPI0 TX / GP7 (引脚 10)
CLKSCLK (SPI CLK)SPI0 SCK / GP6 (引脚 9)
CSSPI CSSPI0 CSn / GP5 (引脚 7)
DC数据 / 命令GP8 (引脚 11)
RST复位SPI0 RST / GP9 (引脚 12)
BUSY忙信号GP10 (引脚 14)
👉

GPIO 说明

对于 DC、RST、BUSY,你实际上可以使用任何你想要的 GPIO。我只是随机选择了 GP8、GP9、GP10。当然,如果你想使用其他 GPIO,你需要相应地修改代码。

Pico 上的 MicroPython 脚本

将以下脚本(例如 epaper_800x480.py)通过 Thonny 或任何其他方法保存到你的树莓派 Pico 上。它定义了一个驱动类,初始化显示器,并可以显示图像。

import machine
import time

# -------------------------------------------------------------------------
# 1) Display resolution for 7.5" e-paper (800 x 480)
# -------------------------------------------------------------------------
EPD_WIDTH  = 800
EPD_HEIGHT = 480

# -------------------------------------------------------------------------
# 2) Configure the Pico pins and SPI0
# -------------------------------------------------------------------------
#   GP6  = SCK
#   GP7  = MOSI
#   GP4  = MISO (not used but must assign a pin)
#   GP5  = CS
#   GP8  = DC
#   GP9  = RST
#   GP10 = BUSY
spi = machine.SPI(
    0,
    baudrate=2_000_000,
    polarity=0,
    phase=0,
    sck=machine.Pin(6),
    mosi=machine.Pin(7),
    miso=machine.Pin(4)
)

cs_pin   = machine.Pin(5,  machine.Pin.OUT, value=1)
dc_pin   = machine.Pin(8,  machine.Pin.OUT, value=0)
rst_pin  = machine.Pin(9,  machine.Pin.OUT, value=1)
busy_pin = machine.Pin(10, machine.Pin.IN)

def delay_ms(ms):
    time.sleep_ms(ms)

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

def digital_read(pin):
    return pin.value()

def spi_write_block(data_block):
    spi.write(data_block)

# -------------------------------------------------------------------------
# 3) E‐Paper Driver Class
# -------------------------------------------------------------------------
class EPD_800x480:
    def __init__(self):
        self.width  = EPD_WIDTH
        self.height = EPD_HEIGHT
        self.reset_pin = rst_pin
        self.dc_pin    = dc_pin
        self.busy_pin  = busy_pin
        self.cs_pin    = cs_pin

    def hardware_reset(self):
        digital_write(self.reset_pin, 1)
        delay_ms(200)
        digital_write(self.reset_pin, 0)
        delay_ms(2)
        digital_write(self.reset_pin, 1)
        delay_ms(200)

    def send_command(self, cmd):
        digital_write(self.dc_pin, 0)  # Command
        digital_write(self.cs_pin, 0)
        spi_write_block(bytes([cmd]))
        digital_write(self.cs_pin, 1)

    def send_data(self, data):
        digital_write(self.dc_pin, 1)  # Data
        digital_write(self.cs_pin, 0)
        spi_write_block(bytes([data]))
        digital_write(self.cs_pin, 1)

    def send_data_block(self, data_block):
        digital_write(self.dc_pin, 1)
        digital_write(self.cs_pin, 0)
        spi_write_block(data_block)
        digital_write(self.cs_pin, 1)

    def read_busy(self):
        while digital_read(self.busy_pin) == 0:
            delay_ms(20)

    def turn_on_display(self):
        # Refresh
        self.send_command(0x12)
        delay_ms(100)
        self.read_busy()

    def init(self):
        """Initialize the display (power on, set registers, etc.).
           Make sure to match your e‐paper's datasheet or Waveshare example code!
        """
        self.hardware_reset()

        # Example sequence (replace with your display's official sequence):
        #  - POWER SETTING
        #  - PANEL SETTING
        #  - TRES (resolution)
        #  - etc.
        # This is a placeholder; adapt to your hardware:
        self.send_command(0x01)  # POWER SETTING
        self.send_data(0x07)
        self.send_data(0x07)
        self.send_data(0x3f)
        self.send_data(0x3f)

        self.send_command(0x04)  # POWER ON
        delay_ms(100)
        self.read_busy()

        self.send_command(0x00)  # PANEL SETTING
        self.send_data(0x1F)

        # TRES: set resolution 800x480
        self.send_command(0x61)
        self.send_data(0x03)  # 800 >> 8
        self.send_data(0x20)  # 800 & 0xFF
        self.send_data(0x01)  # 480 >> 8
        self.send_data(0xE0)  # 480 & 0xFF

        self.send_command(0x15)
        self.send_data(0x00)

        self.send_command(0x50)
        self.send_data(0x10)
        self.send_data(0x07)

        self.send_command(0x60)
        self.send_data(0x22)

    # -- CHUNKED SENDING to save memory --
    def _send_zeros_in_chunks(self, total_size, chunk_size=512):
        zero_chunk = b'\x00' * chunk_size
        sent = 0
        while sent < total_size:
            remain = total_size - sent
            if remain >= chunk_size:
                self.send_data_block(zero_chunk)
                sent += chunk_size
            else:
                self.send_data_block(b'\x00' * remain)
                sent += remain

    def _send_inverted_data_in_chunks(self, data, chunk_size=512):
        idx = 0
        length = len(data)
        while idx < length:
            end = min(idx + chunk_size, length)
            slice_data = data[idx:end]
            inverted = bytearray(len(slice_data))
            for i, b in enumerate(slice_data):
                inverted[i] = ~b & 0xFF
            self.send_data_block(inverted)
            idx = end

    def display(self, black_buffer):
        """black_buffer is a bytes/bytearray of 48,000 bytes (800x480 // 8).
           We'll invert each byte on the fly, because the hardware often
           expects 1=white and 0=black.
        """
        size = (self.width * self.height) // 8

        # 1) Send old data (all white) in chunks
        self.send_command(0x10)
        self._send_zeros_in_chunks(size)

        # 2) Send new data (inverted)
        self.send_command(0x13)
        self._send_inverted_data_in_chunks(black_buffer)

        self.turn_on_display()

    def clear(self):
        """Clear the screen to white."""
        size = (self.width * self.height) // 8
        self.send_command(0x10)
        self._send_zeros_in_chunks(size)
        self.send_command(0x13)
        self._send_zeros_in_chunks(size)
        self.turn_on_display()

    def sleep(self):
        """Deep sleep / power off the display."""
        self.send_command(0x02)  # POWER OFF
        self.read_busy()
        self.send_command(0x07)  # DEEP_SLEEP
        self.send_data(0xA5)
        delay_ms(2000)

# -------------------------------------------------------------------------
# EXAMPLE USAGE
# -------------------------------------------------------------------------
def main():
    epd = EPD_800x480()
    epd.init()

    # Clear the screen
    epd.clear()

    # Suppose you uploaded an 800x480 BIN file to the Pico (flash),
    # containing 48,000 bytes of 1-bit data:
    with open("image_800x480.bin", "rb") as f:
        image_data = f.read()

    # Display it
    epd.display(image_data)

    # Wait a few seconds, then sleep
    delay_ms(5000)
    epd.sleep()

if __name__ == "__main__":
    main()

准备图像

你需要一个 800×480 像素的 1 位(黑白)图像。在你的桌面计算机上(常规 Python),你可以使用 Pillow 转换任何彩色图像:

#!/usr/bin/env python3
import argparse
from PIL import Image

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--input",  "-i", required=True, help="Input image (png/jpg)")
    parser.add_argument("--output", "-o", required=True, help="Output bin file")
    args = parser.parse_args()

    # 1) Open and resize/crop your image to 800x480 if needed
    img = Image.open(args.input).convert("RGB")
    img = img.resize((800, 480))
    # 2) Convert to 1-bit
    bw_img = img.convert("1")  # uses dithering or pass dither=Image.NONE

    pixels = bw_img.load()
    width, height = bw_img.size
    buf = bytearray(width*height//8)

    idx = 0
    for y in range(height):
        for x_block in range(0, width, 8):
            b = 0
            for bitpos in range(8):
                px_val = pixels[x_block+bitpos, y]
                # px_val is 0 (black) or 255 (white) typically
                if px_val >= 128:
                    # White => bit=1
                    b |= (1 << (7 - bitpos))
            buf[idx] = b
            idx += 1

    with open(args.output, "wb") as f:
        f.write(buf)
    print(f"Saved {len(buf)} bytes to {args.output}")

if __name__ == "__main__":
    main()

运行方式:

python image_to_bin.py --input myphoto.png --output image_800x480.bin

将 image_800x480.bin 复制到 Pico(例如,使用 Thonny 的 视图 → 文件)。

在 Pico 上运行

  1. 打开 Thonny(或你喜欢的 MicroPython IDE)。
  2. epaper_800x480.py 复制到你的桌面。
  3. 同时将 image_800x480.bin 复制到 Pico 上。
  4. 运行 epaper_800x480.py。它应该初始化显示器,清除它,然后显示你转换的图像!

如果一切正常工作,你将在墨水屏上看到你的黑白图像。

常见问题 / 技巧

  • 内存错误:如果你看到 MemoryError: memory allocation failed,这通常意味着你试图创建一个巨大的数组。上面代码中的分块方法避免了这个问题。
  • 颜色反转:如果你的显示器颜色反转了(白色是黑色,黑色是白色),看看是否需要移除或修改 ~b & 0xFF 逻辑。
  • 局部更新:一些墨水屏支持局部刷新。你需要更高级的序列来进行局部更新。
  • 缓慢:墨水屏刷新可能需要 2-3 秒或更长时间,特别是在大型显示器上。这是正常的。

结论

就是这样!你已经成功地用 MicroPython 在树莓派 Pico 上驱动了一个大型 7.5″、800×480 墨水屏。关键是将数据分小块发送以节省内存,并在硬件需要时即时反转位。

墨水屏提供了一种华丽的、低功耗的方式来显示静态图像、标牌或仪表板。既然你有了基础知识,你可以扩展到绘制文本、形状或局部更新。

E‐paper display

E‐paper display

祝你在墨水屏和 Pico 的编程中玩得愉快!

© LICENSED UNDER CC BY-NC-SA 4.0