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 000000000..0878c76c2 Binary files /dev/null and b/examples/waveshare-epd/epd2in7/assets/00-hello-mac.bin differ diff --git a/examples/waveshare-epd/epd2in7/assets/01-kanagawa.bin b/examples/waveshare-epd/epd2in7/assets/01-kanagawa.bin new file mode 100644 index 000000000..4ba34ab8a Binary files /dev/null and b/examples/waveshare-epd/epd2in7/assets/01-kanagawa.bin differ diff --git a/examples/waveshare-epd/epd2in7/assets/02-cccp-stamp.bin b/examples/waveshare-epd/epd2in7/assets/02-cccp-stamp.bin new file mode 100644 index 000000000..21d990e98 Binary files /dev/null and b/examples/waveshare-epd/epd2in7/assets/02-cccp-stamp.bin differ diff --git a/examples/waveshare-epd/epd2in7/assets/03-guinea-pig.bin b/examples/waveshare-epd/epd2in7/assets/03-guinea-pig.bin new file mode 100644 index 000000000..b0ae87df0 Binary files /dev/null and b/examples/waveshare-epd/epd2in7/assets/03-guinea-pig.bin differ 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 +)