From ce80e7582f5e514ccb6710213a7afcbcea77d080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Bia=C5=82o=C5=84?= Date: Wed, 23 Oct 2024 13:43:30 +0200 Subject: [PATCH] epd1in54: add Waveshare 1.54inch B/W e-Paper display (#704) epd1in54: add Waveshare 1.54inch B/W e-Paper display --- examples/waveshare-epd/epd1in54/main.go | 36 ++ smoketest.sh | 1 + waveshare-epd/epd1in54/epd1in54.go | 424 ++++++++++++++++++++++++ waveshare-epd/epd1in54/registers.go | 52 +++ 4 files changed, 513 insertions(+) create mode 100644 examples/waveshare-epd/epd1in54/main.go create mode 100644 waveshare-epd/epd1in54/epd1in54.go create mode 100644 waveshare-epd/epd1in54/registers.go diff --git a/examples/waveshare-epd/epd1in54/main.go b/examples/waveshare-epd/epd1in54/main.go new file mode 100644 index 0000000..11b34cf --- /dev/null +++ b/examples/waveshare-epd/epd1in54/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "image/color" + "machine" + + "tinygo.org/x/drivers/waveshare-epd/epd1in54" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/gophers" +) + +var ( + spi0 = machine.SPI0 + cs = machine.D10 + dc = machine.D9 + rst = machine.D6 + busy = machine.D5 + + black = color.RGBA{R: 1, G: 1, B: 1, A: 255} +) + +func main() { + display := epd1in54.New(spi0, cs, dc, rst, busy) + + display.LDirInit(epd1in54.Config{}) + display.Clear() + display.ClearBuffer() + + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 150, 0, "A B C", black, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 100, 0, "D E F", black, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 50, 0, "G H I", black, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 0, 0, "J K L", black, tinyfont.ROTATION_90) + + display.Display() + display.Sleep() +} diff --git a/smoketest.sh b/smoketest.sh index 1237d85..211e92f 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -77,6 +77,7 @@ tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/r tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/resistive/pyportal_touchpaint/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl53l1x/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl6180x/main.go +tinygo build -size short -o ./build/test.hex -target=feather-nrf52840-sense ./examples/waveshare-epd/epd1in54/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd4in2/main.go diff --git a/waveshare-epd/epd1in54/epd1in54.go b/waveshare-epd/epd1in54/epd1in54.go new file mode 100644 index 0000000..4cbda2a --- /dev/null +++ b/waveshare-epd/epd1in54/epd1in54.go @@ -0,0 +1,424 @@ +// Package epd1in54 implements a driver for Waveshare 1.54in black and white e-paper device. +// +// Derived from: +// +// https://github.com/tinygo-org/drivers/tree/master/waveshare-epd +// https://github.com/waveshare/e-Paper/blob/master/Arduino/epd1in54_V2/epd1in54_V2.cpp +// +// Datasheet: https://www.waveshare.com/w/upload/e/e5/1.54inch_e-paper_V2_Datasheet.pdf +package epd1in54 + +import ( + "image/color" + "machine" + "time" +) + +type Config struct { + Width int16 + Height int16 + LogicalWidth int16 + Rotation Rotation +} + +type Device struct { + bus machine.SPI + cs machine.Pin + dc machine.Pin + rst machine.Pin + busy machine.Pin + + buffer []uint8 + rotation Rotation +} + +type Rotation uint8 + +var fullRefresh = [159]uint8{ + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x8, 0x1, 0x0, 0x8, 0x1, 0x0, 0x2, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x22, 0x17, 0x41, 0x0, 0x32, 0x20, +} + +var partialRefresh = [159]uint8{ + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x02, 0x17, 0x41, 0xB0, 0x32, 0x28, +} + +// New returns a new epd1in54 driver. Pass in a fully configured SPI bus. +func New(bus machine.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { + return Device{ + buffer: make([]uint8, (uint32(Width)*uint32(Height))/8), + bus: bus, + cs: csPin, + dc: dcPin, + rst: rstPin, + busy: busyPin, + } +} + +func (d *Device) LDirInit(cfg Config) { + d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.busy.Configure(machine.PinConfig{Mode: machine.PinInput}) + + d.bus.Configure(machine.SPIConfig{ + Frequency: 2000000, + Mode: 0, + LSBFirst: false, + }) + + d.Reset() + d.WaitUntilIdle() + + d.SendCommand(0x12) + d.WaitUntilIdle() + + d.SendCommand(0x01) + d.SendData(0xC7) + d.SendData(0x00) + d.SendData(0x00) + + d.SendCommand(0x11) + d.SendData(0x03) + + d.SendCommand(0x44) + /* x point must be the multiple of 8 or the last 3 bits will be ignored */ + d.SendData((0 >> 3) & 0xFF) + d.SendData((199 >> 3) & 0xFF) + + d.SendCommand(0x45) + d.SendData(0 & 0xFF) + d.SendData((0 >> 8) & 0xFF) + d.SendData(199 & 0xFF) + d.SendData((199 >> 8) & 0xFF) + + d.SendCommand(0x3C) + d.SendData(0x01) + + d.SendCommand(0x18) + d.SendData(0x80) + + d.SendCommand(0x22) + d.SendData(0xB1) + + d.SendCommand(0x20) + + d.SendCommand(0x4E) + d.SendData(0x00) + + d.SendCommand(0x4F) + d.SendData(0xC7) + d.SendData(0x00) + + d.WaitUntilIdle() + d.setLUT(fullRefresh) +} + +func (d *Device) HDirInit(cfg Config) { + d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.busy.Configure(machine.PinConfig{Mode: machine.PinInput}) + + d.bus.Configure(machine.SPIConfig{ + Frequency: 2000000, + Mode: 0, + LSBFirst: false, + }) + + d.Reset() + d.WaitUntilIdle() + + d.SendCommand(0x12) + d.WaitUntilIdle() + + d.SendCommand(0x01) + d.SendData(0xC7) + d.SendData(0x00) + d.SendData(0x01) + + d.SendCommand(0x11) + d.SendData(0x01) + + d.SendCommand(0x44) + d.SendData(0x00) + d.SendData(0x18) + + d.SendCommand(0x45) + d.SendData(0xC7) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + + d.SendCommand(0x3C) + d.SendData(0x01) + + d.SendCommand(0x18) + d.SendData(0x80) + + d.SendCommand(0x22) + d.SendData(0xB1) + + d.SendCommand(0x20) + + d.SendCommand(0x4E) + d.SendData(0x00) + + d.SendCommand(0x4F) + d.SendData(0xC7) + d.SendData(0x00) + + d.WaitUntilIdle() + d.setLUT(fullRefresh) +} + +func (d *Device) setLUT(lut [159]uint8) { + d.SendCommand(0x32) + for i := 0; i < 153; i++ { + d.SendData(lut[i]) + } + d.WaitUntilIdle() + + d.SendCommand(0x3F) + d.SendData(lut[153]) + + d.SendCommand(0x03) + d.SendData(lut[154]) + + d.SendCommand(0x04) + d.SendData(lut[155]) + d.SendData(lut[156]) + d.SendData(lut[157]) + + d.SendCommand(0x2C) + d.SendData(lut[158]) +} + +// Reset resets the display. +func (d *Device) Reset() { + d.rst.High() + time.Sleep(20 * time.Millisecond) + d.rst.Low() + time.Sleep(5 * time.Millisecond) + d.rst.High() + time.Sleep(20 * time.Millisecond) +} + +// 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() +} + +// SetPixel modifies the internal buffer in a single pixel. +// The display have 2 colors: black and white +// We use RGBA(0,0,0, 255) as white (transparent) +// Anything else as black +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + x, y = d.xy(x, y) + if x < 0 || x >= Width || y < 0 || y >= Height { + return + } + byteIndex := (uint32(x) + uint32(y)*uint32(Width)) / 8 + if c.R == 0 && c.G == 0 && c.B == 0 { // TRANSPARENT / WHITE + d.buffer[byteIndex] |= 0x80 >> uint8(x%8) + } else { // WHITE / EMPTY + d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) + } +} + +func (d *Device) DisplayImage(image []uint8) { + var w, h int + if Width%8 == 0 { + w = int(Width / 8) + } else { + w = int(Width/8 + 1) + } + h = int(Height) + + d.SendCommand(0x24) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(image[i+j*w]) + } + } + + d.SendCommand(0x26) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(image[i+j*w]) + } + } + + d.displayFrame() +} + +func (d *Device) Display() error { + var w, h int + if Width%8 == 0 { + w = int(Width / 8) + } else { + w = int(Width/8 + 1) + } + h = int(Height) + + d.SendCommand(0x24) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + x := i + j*w + d.SendData(d.buffer[x]) + } + } + + d.SendCommand(0x26) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + x := i + j*w + d.SendData(d.buffer[x]) + } + } + + d.displayFrame() + + return nil +} + +func (d *Device) displayFrame() { + d.SendCommand(0x22) + d.SendData(0xC7) + d.SendCommand(0x20) + d.WaitUntilIdle() +} + +func (d *Device) Clear() { + var w, h int + if Width%8 == 0 { + w = int(Width / 8) + } else { + w = int(Width/8 + 1) + } + h = int(Height) + + d.SendCommand(0x24) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(0xff) + } + } + + d.SendCommand(0x26) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(0xff) + } + } + + d.displayFrame() +} + +// WaitUntilIdle waits until the display is ready +func (d *Device) WaitUntilIdle() { + for d.busy.Get() { + time.Sleep(100 * time.Millisecond) + } + time.Sleep(200 * 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 := 0; i < len(d.buffer); i++ { + d.buffer[i] = 0xFF + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 { + return Height, Width + } + return Width, Height +} + +// SetRotation changes the rotation (clock-wise) of the device +func (d *Device) SetRotation(rotation Rotation) { + d.rotation = rotation +} + +// xy chages the coordinates according to the rotation +func (d *Device) xy(x, y int16) (int16, int16) { + switch d.rotation { + case NO_ROTATION: + return x, y + case ROTATION_90: + return Width - y - 1, x + case ROTATION_180: + return Width - x - 1, Height - y - 1 + case ROTATION_270: + return y, Height - x - 1 + } + return x, y +} + +func (d *Device) Sleep() { + d.SendCommand(0x10) + d.SendData(0x01) + time.Sleep(200 * time.Millisecond) + + d.rst.Low() +} diff --git a/waveshare-epd/epd1in54/registers.go b/waveshare-epd/epd1in54/registers.go new file mode 100644 index 0000000..ba09c8d --- /dev/null +++ b/waveshare-epd/epd1in54/registers.go @@ -0,0 +1,52 @@ +package epd1in54 + +// Derived from https://github.com/waveshare/e-Paper/blob/master/Arduino/epd4in2/epd4in2.h + +const ( + Width = 200 + Height = 200 + + 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 + VCOM_AND_DATA_INTERVAL_SETTING = 0x50 + LOW_POWER_DETECTION = 0x51 + TCON_SETTING = 0x60 + RESOLUTION_SETTING = 0x61 + GSST_SETTING = 0x65 + GET_STATUS = 0x71 + AUTO_MEASUREMENT_VCOM = 0x80 + READ_VCOM_VALUE = 0x81 + VCM_DC_SETTING = 0x82 + PARTIAL_WINDOW = 0x90 + PARTIAL_IN = 0x91 + PARTIAL_OUT = 0x92 + PROGRAM_MODE = 0xA0 + ACTIVE_PROGRAMMING = 0xA1 + READ_OTP = 0xA2 + POWER_SAVING = 0xE3 + + NO_ROTATION Rotation = 0 + ROTATION_90 Rotation = 1 + ROTATION_180 Rotation = 2 + ROTATION_270 Rotation = 3 +)