From 67ff988e0e5ab82e30290d99c736e6eb99d9c044 Mon Sep 17 00:00:00 2001 From: Dave Cheney Date: Thu, 10 Sep 2026 11:46:17 +1000 Subject: [PATCH] waveshare-epd: add epd2in7 driver and RP2040-PiZero slideshow demo Add a driver for the Waveshare 2.7in V1 e-Paper HAT. This panel uses an IL91874-style controller, different from the IL3820 and SSD1680 controllers used by the other panels in this package. The controller also needs a GET_STATUS command before each BUSY read, or the BUSY pin never updates. Add a slideshow example for the Waveshare RP2040-PiZero. Each picture is a packed 1 bit per pixel asset file under assets/, embedded with go:embed, and the firmware cycles through the files in name order, one image every 10 seconds. The scaffold ships with four public domain pictures, dithered to 1 bit: the 1984 Macintosh hello screen, The Great Wave off Kanagawa, a 1972 Soviet space era stamp, and a guinea pig photo. Verified on real hardware: RP2040-PiZero with the 2.7in V1 HAT on the standard 40 pin header. --- .../epd2in7/assets/00-hello-mac.bin | Bin 0 -> 5812 bytes .../epd2in7/assets/01-kanagawa.bin | Bin 0 -> 5812 bytes .../epd2in7/assets/02-cccp-stamp.bin | Bin 0 -> 5812 bytes .../epd2in7/assets/03-guinea-pig.bin | Bin 0 -> 5812 bytes examples/waveshare-epd/epd2in7/main.go | 178 +++++++++ waveshare-epd/epd2in7/epd2in7.go | 372 ++++++++++++++++++ waveshare-epd/epd2in7/registers.go | 43 ++ 7 files changed, 593 insertions(+) create mode 100644 examples/waveshare-epd/epd2in7/assets/00-hello-mac.bin create mode 100644 examples/waveshare-epd/epd2in7/assets/01-kanagawa.bin create mode 100644 examples/waveshare-epd/epd2in7/assets/02-cccp-stamp.bin create mode 100644 examples/waveshare-epd/epd2in7/assets/03-guinea-pig.bin create mode 100644 examples/waveshare-epd/epd2in7/main.go create mode 100644 waveshare-epd/epd2in7/epd2in7.go create mode 100644 waveshare-epd/epd2in7/registers.go diff --git a/examples/waveshare-epd/epd2in7/assets/00-hello-mac.bin b/examples/waveshare-epd/epd2in7/assets/00-hello-mac.bin new file mode 100644 index 0000000000000000000000000000000000000000..0878c76c2dff46848f3011b52c2aa1aa69968bbd GIT binary patch literal 5812 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 B^>Y9K literal 0 HcmV?d00001 diff --git a/examples/waveshare-epd/epd2in7/main.go b/examples/waveshare-epd/epd2in7/main.go new file mode 100644 index 000000000..77e2dc57d --- /dev/null +++ b/examples/waveshare-epd/epd2in7/main.go @@ -0,0 +1,178 @@ +// Slideshow demo for the Waveshare 2.7in e-Paper HAT V1 on a Waveshare +// RP2040-PiZero. +// +// Panel version. +// +// This demo is written for the V1 panel, which uses an IL91874-style +// controller. It uses the epd2in7 driver, which sends the matching init +// sequence and 5 part LUT (VCOM, WW, BW, BB, WB). Waveshare has not sold a +// V2 or later revision of the 2.7in panel, unlike the 2.13in and 2.9in +// panels, so this driver should match any 2.7in HAT panel. +// +// V1 (IL91874-style, 176x264) works, this demo +// +// Board support. +// +// The pin numbers below are only correct for the RP2040-PiZero. Any other +// board needs its own mapping. +// +// Waveshare RP2040-PiZero tested, works +// Raspberry Pi Pico and Pico W untested, needs its own pin mapping +// because it has no 40 pin header +// Other RP2040 boards untested, needs its own pin mapping +// +// The RP2040-PiZero has a Raspberry Pi 40 pin header. Waveshare exchanges +// GPIO10 and GPIO11 on the header so that RP2040 SPI1 SCK and TX align with +// the Raspberry Pi SCLK and MOSI positions. +// Schematic: https://files.waveshare.com/wiki/RP2040-PiZero/RP2040-PiZero.pdf +// +// Header to RP2040 GPIO for the e-Paper HAT signals: +// +// pin 11 RST GPIO17 +// pin 18 BUSY GPIO24 +// pin 19 MOSI GPIO11 (SPI1 SDO) +// pin 22 DC GPIO25 +// pin 23 SCLK GPIO10 (SPI1 SCK) +// pin 24 CS GPIO8 +// +// Images. +// +// Each file in assets/ is a packed 1 bit per pixel image, generated from a +// source picture by gen.py. The firmware embeds every asset with go:embed +// and cycles through them in name order, one every slideInterval. File +// names are numbered so the slideshow order is explicit. +// +// assets/00-hello-mac.bin the original 1984 Macintosh "hello" screen +// assets/01-kanagawa.bin The Great Wave off Kanagawa, Katsushika Hokusai +// assets/02-cccp-stamp.bin a 1972 Soviet stamp, 15 years of the space era +// assets/03-guinea-pig.bin a guinea pig photo +// +// The image is drawn at Rotation270 (Rotation90 landscape, plus 180 degrees +// for how this panel sits on its connector). Other wiring may need a +// different rotation to read right way up. +// +// Build and flash: +// +// tinygo flash -target=pico ./examples/waveshare-epd/epd2in7 +package main + +import ( + "embed" + "encoding/binary" + "errors" + "image/color" + "machine" + "sort" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/waveshare-epd/epd2in7" +) + +//go:embed assets/*.bin +var assets embed.FS + +const slideInterval = 10 * time.Second + +var ( + black = color.RGBA{0, 0, 0, 255} + white = color.RGBA{255, 255, 255, 255} + + display epd2in7.Device +) + +func main() { + // Wait for the USB serial console to attach so the log is not lost. + time.Sleep(3 * time.Second) + + err := machine.SPI1.Configure(machine.SPIConfig{ + Frequency: 4000000, + SCK: machine.GPIO10, + SDO: machine.GPIO11, + SDI: machine.GPIO12, + Mode: 0, + }) + if err != nil { + println("SPI configure failed:", err.Error()) + return + } + + display = epd2in7.New(machine.SPI1, machine.GPIO8, machine.GPIO25, machine.GPIO17, machine.GPIO24) + display.Configure(epd2in7.Config{ + Rotation: drivers.Rotation270, + }) + + names, err := assetNames() + if err != nil { + println("reading assets failed:", err.Error()) + return + } + if len(names) == 0 { + println("no assets embedded") + return + } + + println("epd2in7 slideshow: init done, clearing display") + display.ClearBuffer() + display.ClearDisplay() + + for i := 0; ; i = (i + 1) % len(names) { + name := names[i] + println("epd2in7 slideshow: showing", name) + if err := showAsset(name); err != nil { + println("showing", name, "failed:", err.Error()) + continue + } + time.Sleep(slideInterval) + } +} + +// assetNames returns the embedded asset file names in sorted order, so the +// slideshow order is deterministic and reproducible. +func assetNames() ([]string, error) { + entries, err := assets.ReadDir("assets") + if err != nil { + return nil, err + } + names := make([]string, 0, len(entries)) + for _, e := range entries { + names = append(names, "assets/"+e.Name()) + } + sort.Strings(names) + return names, nil +} + +// showAsset decodes one packed 1bpp asset file and draws it to the panel. +// The file format is a 4 byte header (uint16 width, uint16 height, little +// endian) followed by packed pixel bits, MSB first, row-major, bit=1 white. +func showAsset(name string) error { + data, err := assets.ReadFile(name) + if err != nil { + return err + } + if len(data) < 4 { + return errors.New("asset too short") + } + w := int16(binary.LittleEndian.Uint16(data[0:2])) + h := int16(binary.LittleEndian.Uint16(data[2:4])) + bits := data[4:] + + want := (int(w)*int(h) + 7) / 8 + if len(bits) < want { + return errors.New("asset truncated") + } + + for y := int16(0); y < h; y++ { + for x := int16(0); x < w; x++ { + bitpos := int(y)*int(w) + int(x) + bit := bits[bitpos/8] & (0x80 >> uint(bitpos%8)) + if bit != 0 { + display.SetPixel(x, y, white) + } else { + display.SetPixel(x, y, black) + } + } + } + + return display.Display() +} diff --git a/waveshare-epd/epd2in7/epd2in7.go b/waveshare-epd/epd2in7/epd2in7.go new file mode 100644 index 000000000..39206a731 --- /dev/null +++ b/waveshare-epd/epd2in7/epd2in7.go @@ -0,0 +1,372 @@ +// Package epd2in7 implements a driver for the Waveshare 2.7in V1 black and +// white e-paper panel. This panel uses an IL91874-style controller, not the +// IL3820 (epd2in13) or SSD1680 (epd2in9v2) controllers used by other +// packages in this repository, so it needs its own init sequence and LUT. +// +// This controller only updates its BUSY pin after a GET_STATUS command, so +// WaitUntilIdle sends one before every read. Reading BUSY on its own leaves +// it stuck at the post-reset level and never returns. +// +// Datasheet: https://www.waveshare.com/wiki/2.7inch_e-Paper_HAT +package epd2in7 // import "tinygo.org/x/drivers/waveshare-epd/epd2in7" + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers" +) + +type Config struct { + Width int16 // Width is the display resolution + Height int16 + LogicalWidth int16 // LogicalWidth must be a multiple of 8 and same size or bigger than Width + Rotation drivers.Rotation +} + +type Device struct { + bus drivers.SPI + cs machine.Pin + dc machine.Pin + rst machine.Pin + busy machine.Pin + logicalWidth int16 + width int16 + height int16 + buffer []uint8 + bufferLength uint32 + rotation drivers.Rotation +} + +// New returns a new epd2in7 driver. Pass in a fully configured SPI bus. +func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) + return Device{ + bus: bus, + cs: csPin, + dc: dcPin, + rst: rstPin, + busy: busyPin, + } +} + +// Configure sets up the device. +func (d *Device) Configure(cfg Config) { + if cfg.LogicalWidth != 0 { + d.logicalWidth = cfg.LogicalWidth + } else { + d.logicalWidth = EPD_WIDTH + } + if cfg.Width != 0 { + d.width = cfg.Width + } else { + d.width = EPD_WIDTH + } + if cfg.Height != 0 { + d.height = cfg.Height + } else { + d.height = EPD_HEIGHT + } + d.rotation = cfg.Rotation + d.bufferLength = (uint32(d.logicalWidth) * uint32(d.height)) / 8 + d.buffer = make([]uint8, d.bufferLength) + for i := uint32(0); i < d.bufferLength; i++ { + d.buffer[i] = 0xFF + } + + d.cs.Low() + d.dc.Low() + d.rst.Low() + + d.Reset() + + d.SendCommand(POWER_SETTING) + d.SendData(0x03) // VDS_EN, VDG_EN + d.SendData(0x00) // VCOM_HV, VGHL_LV[1], VGHL_LV[0] + d.SendData(0x2b) // VDH + d.SendData(0x2b) // VDL + d.SendData(0x09) // VDHR + + d.SendCommand(BOOSTER_SOFT_START) + d.SendData(0x07) + d.SendData(0x07) + d.SendData(0x17) + + // Power optimization. Values come from the vendor driver and are not + // documented in the datasheet register map. + d.SendCommand(0xF8) + d.SendData(0x60) + d.SendData(0xA5) + d.SendCommand(0xF8) + d.SendData(0x89) + d.SendData(0xA5) + d.SendCommand(0xF8) + d.SendData(0x90) + d.SendData(0x00) + d.SendCommand(0xF8) + d.SendData(0x93) + d.SendData(0x2A) + d.SendCommand(0xF8) + d.SendData(0xA0) + d.SendData(0xA5) + d.SendCommand(0xF8) + d.SendData(0xA1) + d.SendData(0x00) + d.SendCommand(0xF8) + d.SendData(0x73) + d.SendData(0x41) + + d.SendCommand(PARTIAL_DISPLAY_REFRESH) + d.SendData(0x00) + + d.SendCommand(POWER_ON) + d.WaitUntilIdle() + + d.SendCommand(PANEL_SETTING) + d.SendData(0xAF) // KW-BF KWR-AF BWROTP 0f + + d.SendCommand(PLL_CONTROL) + d.SendData(0x3A) // 3A 100Hz 29 150Hz 39 200Hz 31 171Hz + + d.SendCommand(VCOM_AND_DATA_INTERVAL_SETTING) + d.SendData(0x57) + + d.SendCommand(VCM_DC_SETTING) + d.SendData(0x12) + + d.SetLUT() +} + +// Reset resets the device. +func (d *Device) Reset() { + d.rst.Low() + time.Sleep(200 * time.Millisecond) + d.rst.High() + time.Sleep(200 * time.Millisecond) +} + +// DeepSleep puts the display into deep sleep. The panel keeps the last +// image with no power, but a hardware reset is needed to wake it again. +func (d *Device) DeepSleep() { + d.SendCommand(VCOM_AND_DATA_INTERVAL_SETTING) + d.SendData(0x17) // border floating + d.SendCommand(VCM_DC_SETTING) + d.SendCommand(PANEL_SETTING) + time.Sleep(100 * time.Millisecond) + + d.SendCommand(POWER_SETTING) // VG&VS to 0V fast + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + time.Sleep(100 * time.Millisecond) + + d.SendCommand(POWER_OFF) + d.WaitUntilIdle() + d.SendCommand(DEEP_SLEEP) + d.SendData(0xA5) +} + +// SendCommand sends a command to the display. +func (d *Device) SendCommand(command uint8) { + d.sendDataCommand(true, command) +} + +// SendData sends a data byte to the display. +func (d *Device) SendData(data uint8) { + d.sendDataCommand(false, data) +} + +// sendDataCommand sends image data or a command to the screen. +func (d *Device) sendDataCommand(isCommand bool, data uint8) { + if isCommand { + d.dc.Low() + } else { + d.dc.High() + } + d.cs.Low() + d.bus.Transfer(data) + d.cs.High() +} + +// SetLUT sets the look up tables for a full update. Values come from the +// vendor EPD_2in7.c driver, section EPD_2in7_lut_vcom_dc and following. +func (d *Device) SetLUT() { + lutVcom := []uint8{ + 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, + 0x60, 0x28, 0x28, 0x00, 0x00, 0x01, + 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x12, 0x12, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + lutWW := []uint8{ + 0x40, 0x08, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x28, 0x28, 0x00, 0x00, 0x01, + 0x40, 0x14, 0x00, 0x00, 0x00, 0x01, + 0xA0, 0x12, 0x12, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + lutBW := []uint8{ + 0x40, 0x08, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x28, 0x28, 0x00, 0x00, 0x01, + 0x40, 0x14, 0x00, 0x00, 0x00, 0x01, + 0xA0, 0x12, 0x12, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + lutBB := []uint8{ + 0x80, 0x08, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x28, 0x28, 0x00, 0x00, 0x01, + 0x80, 0x14, 0x00, 0x00, 0x00, 0x01, + 0x50, 0x12, 0x12, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + lutWB := []uint8{ + 0x80, 0x08, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x28, 0x28, 0x00, 0x00, 0x01, + 0x80, 0x14, 0x00, 0x00, 0x00, 0x01, + 0x50, 0x12, 0x12, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + + d.SendCommand(LUT_FOR_VCOM) + for _, v := range lutVcom { + d.SendData(v) + } + d.SendCommand(LUT_WHITE_TO_WHITE) + for _, v := range lutWW { + d.SendData(v) + } + d.SendCommand(LUT_BLACK_TO_WHITE) + for _, v := range lutBW { + d.SendData(v) + } + d.SendCommand(LUT_WHITE_TO_BLACK) + for _, v := range lutBB { + d.SendData(v) + } + d.SendCommand(LUT_BLACK_TO_BLACK) + for _, v := range lutWB { + d.SendData(v) + } +} + +// SetPixel modifies the internal buffer at a single pixel. +// The display has 2 colors: black and white. We use RGBA(0,0,0,255) as +// black, anything else as white. +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + x, y = d.xy(x, y) + if x < 0 || x >= d.logicalWidth || y < 0 || y >= d.height { + return + } + byteIndex := (uint32(x) + uint32(y)*uint32(d.logicalWidth)) / 8 + if c.R == 0 && c.G == 0 && c.B == 0 { // black + d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) + } else { // white + d.buffer[byteIndex] |= 0x80 >> uint8(x%8) + } +} + +// Display sends the buffer to the screen and refreshes it. +func (d *Device) Display() error { + d.SendCommand(DATA_START_TRANSMISSION_2) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[i]) + } + + d.SendCommand(DISPLAY_REFRESH) + time.Sleep(100 * time.Millisecond) + d.WaitUntilIdle() + return nil +} + +// ClearDisplay erases the device SRAM and refreshes the panel to white. +func (d *Device) ClearDisplay() { + d.SendCommand(DATA_START_TRANSMISSION_1) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(0xFF) + } + d.SendCommand(DATA_START_TRANSMISSION_2) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(0xFF) + } + + d.SendCommand(DISPLAY_REFRESH) + time.Sleep(100 * time.Millisecond) + d.WaitUntilIdle() +} + +// WaitUntilIdle waits until the display is ready. This controller only +// updates the BUSY pin after a GET_STATUS command, unlike other panels in +// this repository, so send it before every read. +// Source: EPD_2in7_ReadBusy in the vendor EPD_2in7.c driver. +func (d *Device) WaitUntilIdle() { + for { + d.SendCommand(GET_STATUS) + if d.busy.Get() { + break + } + time.Sleep(100 * time.Millisecond) + } +} + +// IsBusy returns the busy status of the display. +func (d *Device) IsBusy() bool { + return d.busy.Get() +} + +// ClearBuffer sets the buffer to 0xFF (white). +func (d *Device) ClearBuffer() { + for i := uint32(0); i < d.bufferLength; i++ { + d.buffer[i] = 0xFF + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 { + return d.height, d.logicalWidth + } + return d.logicalWidth, d.height +} + +// Rotation returns the current rotation of the device. +func (d *Device) Rotation() drivers.Rotation { + return d.rotation +} + +// SetRotation changes the rotation of the device. +func (d *Device) SetRotation(rotation drivers.Rotation) error { + d.rotation = rotation + return nil +} + +// xy changes the coordinates according to the rotation. +func (d *Device) xy(x, y int16) (int16, int16) { + switch d.rotation { + case drivers.Rotation0: + return x, y + case drivers.Rotation90: + return d.width - y - 1, x + case drivers.Rotation180: + return d.width - x - 1, d.height - y - 1 + case drivers.Rotation270: + return y, d.height - x - 1 + } + return x, y +} diff --git a/waveshare-epd/epd2in7/registers.go b/waveshare-epd/epd2in7/registers.go new file mode 100644 index 000000000..20832b968 --- /dev/null +++ b/waveshare-epd/epd2in7/registers.go @@ -0,0 +1,43 @@ +package epd2in7 + +// Derived from https://github.com/waveshare/e-Paper/blob/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_2in7.c + +// Registers +const ( + // Display resolution + EPD_WIDTH = 176 + EPD_HEIGHT = 264 + + // EPD2IN7 commands + PANEL_SETTING = 0x00 + POWER_SETTING = 0x01 + POWER_OFF = 0x02 + POWER_OFF_SEQUENCE_SETTING = 0x03 + POWER_ON = 0x04 + POWER_ON_MEASURE = 0x05 + BOOSTER_SOFT_START = 0x06 + DEEP_SLEEP = 0x07 + DATA_START_TRANSMISSION_1 = 0x10 + DATA_STOP = 0x11 + DISPLAY_REFRESH = 0x12 + DATA_START_TRANSMISSION_2 = 0x13 + LUT_FOR_VCOM = 0x20 + LUT_WHITE_TO_WHITE = 0x21 + LUT_BLACK_TO_WHITE = 0x22 + LUT_WHITE_TO_BLACK = 0x23 + LUT_BLACK_TO_BLACK = 0x24 + PLL_CONTROL = 0x30 + TEMPERATURE_SENSOR_COMMAND = 0x40 + TEMPERATURE_SENSOR_SELECTION = 0x41 + TEMPERATURE_SENSOR_WRITE = 0x42 + TEMPERATURE_SENSOR_READ = 0x43 + PARTIAL_DISPLAY_REFRESH = 0x16 + VCOM_AND_DATA_INTERVAL_SETTING = 0x50 + LOW_POWER_DETECTION = 0x51 + TCON_SETTING = 0x60 + RESOLUTION_SETTING = 0x61 + GET_STATUS = 0x71 + AUTO_MEASUREMENT_VCOM = 0x80 + READ_VCOM_VALUE = 0x81 + VCM_DC_SETTING = 0x82 +)