From 749b2f672674085b43d42ca6a1fb1ee850b28ce7 Mon Sep 17 00:00:00 2001 From: Kenneth Bell Date: Wed, 20 Jul 2022 20:05:00 +0100 Subject: [PATCH] epd: add waveshare 2.9in (v1) --- README.md | 3 +- examples/waveshare-epd/epd2in9/main.go | 84 +++++++ waveshare-epd/epd2in9/epd2in9.go | 291 +++++++++++++++++++++++++ waveshare-epd/epd2in9/registers.go | 31 +++ 4 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 examples/waveshare-epd/epd2in9/main.go create mode 100644 waveshare-epd/epd2in9/epd2in9.go create mode 100644 waveshare-epd/epd2in9/registers.go diff --git a/README.md b/README.md index 45a0150..245c84a 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ func main() { ## Currently supported devices -The following 81 devices are supported. +The following 82 devices are supported. | Device Name | Interface Type | |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------| @@ -137,6 +137,7 @@ The following 81 devices are supported. | [VL53L1X time-of-flight distance sensor](https://www.st.com/resource/en/datasheet/vl53l1x.pdf) | I2C | | [Waveshare 2.13" (B & C) e-paper display](https://www.waveshare.com/w/upload/d/d3/2.13inch-e-paper-b-Specification.pdf) | SPI | | [Waveshare 2.13" e-paper display](https://www.waveshare.com/w/upload/e/e6/2.13inch_e-Paper_Datasheet.pdf) | SPI | +| [Waveshare 2.9" e-paper display (V1)](https://www.waveshare.com/w/upload/e/e6/2.9inch_e-Paper_Datasheet.pdf) | SPI | | [Waveshare 4.2" e-paper B/W display](https://www.waveshare.com/w/upload/6/6a/4.2inch-e-paper-specification.pdf) | SPI | | [WS2812 RGB LED](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) | GPIO | | [XPT2046 touch controller](http://grobotronics.com/images/datasheets/xpt2046-datasheet.pdf) | GPIO | diff --git a/examples/waveshare-epd/epd2in9/main.go b/examples/waveshare-epd/epd2in9/main.go new file mode 100644 index 0000000..bc99323 --- /dev/null +++ b/examples/waveshare-epd/epd2in9/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "machine" + + "image/color" + + "time" + + "tinygo.org/x/drivers/waveshare-epd/epd2in9" +) + +var display epd2in9.Device + +const ( + width = 128 + height = 296 +) + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 8000000, + Mode: 0, + }) + + display = epd2in9.New(machine.SPI0, machine.GPIO2, machine.GPIO3, machine.GPIO4, machine.GPIO5) + display.Configure(epd2in9.Config{ + Width: width, + LogicalWidth: width, + Height: height, + }) + + black := color.RGBA{1, 1, 1, 255} + white := color.RGBA{0, 0, 0, 255} + + display.ClearBuffer() + println("Clear the display") + display.ClearDisplay() + display.WaitUntilIdle() + println("Waiting for 2 seconds") + time.Sleep(2 * time.Second) + + // Show a checkered board + for i := int16(0); i < width/8; i++ { + for j := int16(0); j < height/8; j++ { + if (i+j)%2 == 0 { + showRect(i*8, j*8, 8, 8, black) + } + } + } + println("Show checkered board") + display.Display() + display.WaitUntilIdle() + println("Waiting for 2 seconds") + time.Sleep(2 * time.Second) + + println("Set partial lut") + display.SetLUT(false) // partial updates (faster, but with some ghosting) + println("Show smaller striped area") + for i := int16(40); i < 88; i++ { + for j := int16(83); j < 166; j++ { + if (i+j)%4 == 0 || (i+j)%4 == 1 { + display.SetPixel(i, j, black) + } else { + display.SetPixel(i, j, white) + } + } + } + + display.Display() + display.WaitUntilIdle() + + display.DeepSleep() + + println("You could remove power now") +} + +func showRect(x int16, y int16, w int16, h int16, c color.RGBA) { + for i := x; i < x+w; i++ { + for j := y; j < y+h; j++ { + display.SetPixel(i, j, c) + } + } +} diff --git a/waveshare-epd/epd2in9/epd2in9.go b/waveshare-epd/epd2in9/epd2in9.go new file mode 100644 index 0000000..1400f4d --- /dev/null +++ b/waveshare-epd/epd2in9/epd2in9.go @@ -0,0 +1,291 @@ +// Package epd2in9 implements a driver for Waveshare 2.9in black and white e-paper device. +// +// Note: this is for the V1 device (using IL3820), the V2 device uses a different chipset. +// +// Datasheets: +// https://www.waveshare.com/w/upload/e/e6/2.9inch_e-Paper_Datasheet.pdf +// https://www.smart-prototyping.com/image/data/9_Modules/EinkDisplay/GDE029A1/IL3820.pdf +// +// This implementation is essentially a copy of the 2in13 driver with the correct LUTs and +// default size for the 2.9in device. +// +package epd2in9 // import "tinygo.org/x/drivers/waveshare-epd/epd2in9" + +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 Rotation // Rotation is clock-wise +} + +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 Rotation +} + +type Rotation uint8 + +// Look up table for full updates +var lutFullUpdate = [30]uint8{ + 0x50, 0xAA, 0x55, 0xAA, 0x11, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0x1F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +} + +// Look up table for partial updates, faster but there will be some ghosting +var lutPartialUpdate = [30]uint8{ + 0x10, 0x18, 0x18, 0x08, 0x18, 0x18, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +} + +// New returns a new epd2in9 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 = 128 + } + if cfg.Width != 0 { + d.width = cfg.Width + } else { + d.width = 128 + } + if cfg.Height != 0 { + d.height = cfg.Height + } else { + d.height = 296 + } + 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(DRIVER_OUTPUT_CONTROL) + d.SendData(uint8((d.height - 1) & 0xFF)) + d.SendData(uint8(((d.height - 1) >> 8) & 0xFF)) + d.SendData(0x00) // GD = 0; SM = 0; TB = 0; + d.SendCommand(BOOSTER_SOFT_START_CONTROL) + d.SendData(0xD7) + d.SendData(0xD6) + d.SendData(0x9D) + d.SendCommand(WRITE_VCOM_REGISTER) + d.SendData(0xA8) // VCOM 7C + d.SendCommand(SET_DUMMY_LINE_PERIOD) + d.SendData(0x1A) // 4 dummy lines per gate + d.SendCommand(SET_GATE_TIME) + d.SendData(0x08) // 2us per line + d.SendCommand(DATA_ENTRY_MODE_SETTING) + d.SendData(0x03) // X increment; Y increment + + d.SetLUT(true) +} + +// 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 deepsleep +func (d *Device) DeepSleep() { + d.SendCommand(DEEP_SLEEP_MODE) + d.WaitUntilIdle() +} + +// 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 full or partial updates +func (d *Device) SetLUT(fullUpdate bool) { + d.SendCommand(WRITE_LUT_REGISTER) + if fullUpdate { + for i := 0; i < 30; i++ { + d.SendData(lutFullUpdate[i]) + } + } else { + for i := 0; i < 30; i++ { + d.SendData(lutPartialUpdate[i]) + } + } +} + +// 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 >= d.logicalWidth || y < 0 || y >= d.height { + return + } + byteIndex := (int32(x) + int32(y)*int32(d.logicalWidth)) / 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) + } +} + +// Display sends the buffer to the screen. +func (d *Device) Display() error { + d.setMemoryArea(0, 0, d.logicalWidth-1, d.height-1) + for j := int16(0); j < d.height; j++ { + d.setMemoryPointer(0, j) + d.SendCommand(WRITE_RAM) + for i := int16(0); i < d.logicalWidth/8; i++ { + d.SendData(d.buffer[i+j*(d.logicalWidth/8)]) + } + } + + d.SendCommand(DISPLAY_UPDATE_CONTROL_2) + d.SendData(0xC4) + d.SendCommand(MASTER_ACTIVATION) + d.SendCommand(TERMINATE_FRAME_READ_WRITE) + return nil +} + +// ClearDisplay erases the device SRAM +func (d *Device) ClearDisplay() { + d.setMemoryArea(0, 0, d.logicalWidth-1, d.height-1) + d.setMemoryPointer(0, 0) + d.SendCommand(WRITE_RAM) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(0xFF) + } + d.Display() +} + +// setMemoryArea sets the area of the display that will be updated +func (d *Device) setMemoryArea(x0 int16, y0 int16, x1 int16, y1 int16) { + d.SendCommand(SET_RAM_X_ADDRESS_START_END_POSITION) + d.SendData(uint8((x0 >> 3) & 0xFF)) + d.SendData(uint8((x1 >> 3) & 0xFF)) + d.SendCommand(SET_RAM_Y_ADDRESS_START_END_POSITION) + d.SendData(uint8(y0 & 0xFF)) + d.SendData(uint8((y0 >> 8) & 0xFF)) + d.SendData(uint8(y1 & 0xFF)) + d.SendData(uint8((y1 >> 8) & 0xFF)) +} + +// setMemoryPointer moves the internal pointer to the speficied coordinates +func (d *Device) setMemoryPointer(x int16, y int16) { + d.SendCommand(SET_RAM_X_ADDRESS_COUNTER) + d.SendData(uint8((x >> 3) & 0xFF)) + d.SendCommand(SET_RAM_Y_ADDRESS_COUNTER) + d.SendData(uint8(y & 0xFF)) + d.SendData(uint8((y >> 8) & 0xFF)) + d.WaitUntilIdle() +} + +// WaitUntilIdle waits until the display is ready +func (d *Device) WaitUntilIdle() { + for d.busy.Get() { + 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 == ROTATION_90 || d.rotation == ROTATION_270 { + return d.height, d.logicalWidth + } + return d.logicalWidth, d.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 d.width - y - 1, x + case ROTATION_180: + return d.width - x - 1, d.height - y - 1 + case ROTATION_270: + return y, d.height - x - 1 + } + return x, y +} diff --git a/waveshare-epd/epd2in9/registers.go b/waveshare-epd/epd2in9/registers.go new file mode 100644 index 0000000..517a92b --- /dev/null +++ b/waveshare-epd/epd2in9/registers.go @@ -0,0 +1,31 @@ +package epd2in9 + +// Registers +const ( + DRIVER_OUTPUT_CONTROL = 0x01 + BOOSTER_SOFT_START_CONTROL = 0x0C + GATE_SCAN_START_POSITION = 0x0F + DEEP_SLEEP_MODE = 0x10 + DATA_ENTRY_MODE_SETTING = 0x11 + SW_RESET = 0x12 + TEMPERATURE_SENSOR_CONTROL = 0x1A + MASTER_ACTIVATION = 0x20 + DISPLAY_UPDATE_CONTROL_1 = 0x21 + DISPLAY_UPDATE_CONTROL_2 = 0x22 + WRITE_RAM = 0x24 + WRITE_VCOM_REGISTER = 0x2C + WRITE_LUT_REGISTER = 0x32 + SET_DUMMY_LINE_PERIOD = 0x3A + SET_GATE_TIME = 0x3B + BORDER_WAVEFORM_CONTROL = 0x3C + SET_RAM_X_ADDRESS_START_END_POSITION = 0x44 + SET_RAM_Y_ADDRESS_START_END_POSITION = 0x45 + SET_RAM_X_ADDRESS_COUNTER = 0x4E + SET_RAM_Y_ADDRESS_COUNTER = 0x4F + TERMINATE_FRAME_READ_WRITE = 0xFF + + NO_ROTATION Rotation = 0 + ROTATION_90 Rotation = 1 // 90 degrees clock-wise rotation + ROTATION_180 Rotation = 2 + ROTATION_270 Rotation = 3 +)