From 374d3f6bec46e30113d4ed03b096ed7152f26b03 Mon Sep 17 00:00:00 2001 From: Daniel Esteban Date: Fri, 17 May 2019 07:55:57 +0200 Subject: [PATCH] Support for Waveshare 2.13" (B & C versions) e-paper display (#56) * Support for Waveshare 2.13" (B) e-paper display --- .circleci/config.yml | 1 + README.md | 1 + examples/waveshare-epd/epd2in13x/main.go | 46 ++++ waveshare-epd/epd2in13x/epd2in13x.go | 304 +++++++++++++++++++++++ waveshare-epd/epd2in13x/registers.go | 46 ++++ 5 files changed, 398 insertions(+) create mode 100644 examples/waveshare-epd/epd2in13x/main.go create mode 100644 waveshare-epd/epd2in13x/epd2in13x.go create mode 100644 waveshare-epd/epd2in13x/registers.go diff --git a/.circleci/config.yml b/.circleci/config.yml index f6d4521..c4debe4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,4 +38,5 @@ jobs: - run: tinygo build -size short -o test.elf -target=microbit ./examples/ssd1306/spi_128x64/main.go - run: tinygo build -size short -o test.elf -target=circuitplay-express ./examples/thermistor/main.go - run: tinygo build -size short -o test.elf -target=itsybitsy-m0 ./examples/vl53l1x/main.go + - run: tinygo build -size short -o test.elf -target=microbit ./examples/waveshare-epd/epd2in13x/main.go - run: tinygo build -size short -o test.elf -target=circuitplay-express ./examples/ws2812/main.go diff --git a/README.md b/README.md index d1ac128..4a80f67 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ func main() { | [SSD1306 OLED display](https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf) | I2C / SPI | | [Thermistor](https://www.farnell.com/datasheets/33552.pdf) | ADC | | [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 | | [WS2812 RGB LED](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) | GPIO | ## Contributing diff --git a/examples/waveshare-epd/epd2in13x/main.go b/examples/waveshare-epd/epd2in13x/main.go new file mode 100644 index 0000000..d8d0721 --- /dev/null +++ b/examples/waveshare-epd/epd2in13x/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "machine" + + "image/color" + + "github.com/tinygo-org/drivers/waveshare-epd/epd2in13x" +) + +var display epd2in13x.Device + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 8000000, + Mode: 0, + }) + + display = epd2in13x.New(machine.SPI0, machine.P6, machine.P7, machine.P8, machine.P9) + display.Configure(epd2in13x.Config{}) + + white := color.RGBA{0, 0, 0, 255} + colored := color.RGBA{255, 0, 0, 255} + black := color.RGBA{1, 1, 1, 255} + + display.ClearBuffer() + display.ClearDisplay() + + // Show a checkered board + for i := int16(0); i < 27; i++ { + showRect((i%3)*35, i*8, 35, 8, colored) + showRect(((i+1)%3)*35, i*8, 35, 8, black) + showRect(((i+2)%3)*35, i*8, 35, 8, white) + } + display.Display() + display.WaitUntilIdle() + 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/epd2in13x/epd2in13x.go b/waveshare-epd/epd2in13x/epd2in13x.go new file mode 100644 index 0000000..5cfaff0 --- /dev/null +++ b/waveshare-epd/epd2in13x/epd2in13x.go @@ -0,0 +1,304 @@ +// Package epd2in13x implements a driver for Waveshare 2.13in (B & C versions) tri-color e-paper device. +// +// Datasheet: https://www.waveshare.com/w/upload/d/d3/2.13inch-e-paper-b-Specification.pdf +package epd2in13x + +import ( + "errors" + "image/color" + "machine" + "time" +) + +type Config struct { + Width int16 + Height int16 + NumColors uint8 +} + +type Device struct { + bus machine.SPI + cs machine.GPIO + dc machine.GPIO + rst machine.GPIO + busy machine.GPIO + width int16 + height int16 + buffer [][]uint8 + bufferLength uint32 +} + +type Color uint8 + +// New returns a new epd2in13x driver. Pass in a fully configured SPI bus. +func New(bus machine.SPI, csPin uint8, dcPin uint8, rstPin uint8, busyPin uint8) Device { + pinCs := machine.GPIO{csPin} + pinCs.Configure(machine.GPIOConfig{Mode: machine.GPIO_OUTPUT}) + pinDc := machine.GPIO{dcPin} + pinDc.Configure(machine.GPIOConfig{Mode: machine.GPIO_OUTPUT}) + pinRst := machine.GPIO{rstPin} + pinRst.Configure(machine.GPIOConfig{Mode: machine.GPIO_OUTPUT}) + pinBusy := machine.GPIO{busyPin} + pinBusy.Configure(machine.GPIOConfig{Mode: machine.GPIO_INPUT}) + return Device{ + bus: bus, + cs: pinCs, + dc: pinDc, + rst: pinRst, + busy: pinBusy, + } +} + +// Configure sets up the device. +func (d *Device) Configure(cfg Config) { + if cfg.Width != 0 { + d.width = cfg.Width + } else { + d.width = 104 + } + if cfg.Height != 0 { + d.height = cfg.Height + } else { + d.height = 212 + } + if cfg.NumColors == 0 { + cfg.NumColors = 3 + } else if cfg.NumColors == 1 { + cfg.NumColors = 2 + } + d.bufferLength = (uint32(d.width) * uint32(d.height)) / 8 + d.buffer = make([][]uint8, cfg.NumColors-1) + for i := range d.buffer { + d.buffer[i] = make([]uint8, d.bufferLength) + } + for i := range d.buffer { + for j := uint32(0); j < d.bufferLength; j++ { + d.buffer[i][j] = 0xFF + } + } + + d.cs.Low() + d.dc.Low() + d.rst.Low() + + d.Reset() + + d.SendCommand(BOOSTER_SOFT_START) + d.SendData(0x17) + d.SendData(0x17) + d.SendData(0x17) + d.SendCommand(POWER_ON) + d.WaitUntilIdle() + d.SendCommand(PANEL_SETTING) + d.SendData(0x8F) + d.SendCommand(VCOM_AND_DATA_INTERVAL_SETTING) + d.SendData(0x37) + d.SendCommand(RESOLUTION_SETTING) + d.SendData(uint8(d.width)) + d.SendData(0x00) + d.SendData(uint8(d.height)) +} + +// 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(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() +} + +// SetPixel modifies the internal buffer in a single pixel. +// The display have 3 colors: black, white and a third color that could be red or yellow +// We use RGBA(0,0,0, 255) as white (transparent) +// RGBA(1-255,0,0,255) as colored (red or yellow) +// Anything else as black +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + if x < 0 || x >= d.width || y < 0 || y >= d.height { + return + } + if c.R != 0 && c.G == 0 && c.B == 0 { // COLORED + d.SetEPDPixel(x, y, COLORED) + } else if c.G != 0 || c.B != 0 { // BLACK + d.SetEPDPixel(x, y, BLACK) + } else { // WHITE / EMPTY + d.SetEPDPixel(x, y, WHITE) + } +} + +// SetEPDPixel modifies the internal buffer in a single pixel. +func (d *Device) SetEPDPixel(x int16, y int16, c Color) { + if x < 0 || x >= d.width || y < 0 || y >= d.height { + return + } + byteIndex := (x + y*d.width) / 8 + if c == WHITE { + d.buffer[BLACK-1][byteIndex] |= 0x80 >> uint8(x%8) + d.buffer[COLORED-1][byteIndex] |= 0x80 >> uint8(x%8) + } else if c == COLORED { + d.buffer[BLACK-1][byteIndex] |= 0x80 >> uint8(x%8) + d.buffer[COLORED-1][byteIndex] &^= 0x80 >> uint8(x%8) + } else { // BLACK + d.buffer[COLORED-1][byteIndex] |= 0x80 >> uint8(x%8) + d.buffer[BLACK-1][byteIndex] &^= 0x80 >> uint8(x%8) + } +} + +// Display sends the buffer (if any) to the screen. +func (d *Device) Display() error { + d.SendCommand(DATA_START_TRANSMISSION_1) // black + time.Sleep(2 * time.Millisecond) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[BLACK-1][i]) + } + time.Sleep(2 * time.Millisecond) + d.SendCommand(DATA_START_TRANSMISSION_2) // red + time.Sleep(2 * time.Millisecond) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[COLORED-1][i]) + } + time.Sleep(2 * time.Millisecond) + d.SendCommand(DISPLAY_REFRESH) + return nil +} + +// SetDisplayRect sends a rectangle of data at specific coordinates to the device SRAM directly +func (d *Device) SetDisplayRect(buffer [][]uint8, x int16, y int16, w int16, h int16) error { + if w%8 != 0 { + return errors.New("rectangle width needs to be a multiple of 8") + } + for i := range buffer { + if int16(len(buffer[i])) < (w/8)*h { + return errors.New("buffer has the wrong size") + } + } + d.SendCommand(PARTIAL_IN) + d.SendCommand(PARTIAL_WINDOW) + d.SendData(uint8(x) & 0xF8) + d.SendData(((uint8(x) & 0xF8) + uint8(w) - 1) | 0x07) + d.SendData(uint8(y) >> 8) + d.SendData(uint8(y) & 0xFF) + d.SendData(uint8(y+h-1) >> 8) + d.SendData(uint8(y+h-1) & 0xFF) + d.SendData(0x01) + time.Sleep(2 * time.Millisecond) + d.SendCommand(DATA_START_TRANSMISSION_1) + for i := int16(0); i < (w/8)*h; i++ { + d.SendData(buffer[BLACK-1][i]) + } + time.Sleep(2 * time.Millisecond) + if len(buffer) > 1 { + d.SendCommand(DATA_START_TRANSMISSION_2) + for i := int16(0); i < (w/8)*h; i++ { + d.SendData(buffer[COLORED-1][i]) + } + time.Sleep(2 * time.Millisecond) + } + d.SendCommand(PARTIAL_OUT) + return nil +} + +// SetDisplayRectColor sends a rectangle of data at specific coordinates to the device SRAM directly +func (d *Device) SetDisplayRectColor(buffer []uint8, x int16, y int16, w int16, h int16, c Color) error { + if w%8 != 0 { + return errors.New("rectangle width needs to be a multiple of 8") + } + if int16(len(buffer)) < (w/8)*h { + return errors.New("buffer has the wrong size") + } + if c == WHITE { + return errors.New("wrong color") + } + d.SendCommand(PARTIAL_IN) + d.SendCommand(PARTIAL_WINDOW) + d.SendData(uint8(x) & 0xF8) + d.SendData(((uint8(x) & 0xF8) + uint8(w) - 1) | 0x07) + d.SendData(uint8(y) >> 8) + d.SendData(uint8(y) & 0xFF) + d.SendData(uint8(y+h-1) >> 8) + d.SendData(uint8(y+h-1) & 0xFF) + d.SendData(0x01) + time.Sleep(2 * time.Millisecond) + if c == COLORED { + d.SendCommand(DATA_START_TRANSMISSION_2) + } else { + d.SendCommand(DATA_START_TRANSMISSION_1) + } + for i := int16(0); i < (w/8)*h; i++ { + d.SendData(buffer[i]) + } + time.Sleep(2 * time.Millisecond) + d.SendCommand(PARTIAL_OUT) + return nil +} + +// ClearDisplay erases the device SRAM +func (d *Device) ClearDisplay() { + d.SendCommand(DATA_START_TRANSMISSION_1) // black + time.Sleep(2 * time.Millisecond) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(0xFF) + } + time.Sleep(2 * time.Millisecond) + d.SendCommand(DATA_START_TRANSMISSION_2) // red + time.Sleep(2 * time.Millisecond) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(0xFF) + } + time.Sleep(2 * time.Millisecond) +} + +// 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 := uint8(0); i < uint8(len(d.buffer)); i++ { + for j := uint32(0); j < d.bufferLength; j++ { + d.buffer[i][j] = 0xFF + } + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + return d.width, d.height +} diff --git a/waveshare-epd/epd2in13x/registers.go b/waveshare-epd/epd2in13x/registers.go new file mode 100644 index 0000000..8577ae2 --- /dev/null +++ b/waveshare-epd/epd2in13x/registers.go @@ -0,0 +1,46 @@ +package epd2in13x + +// Registers +const ( + WHITE Color = 0 + BLACK Color = 1 + COLORED Color = 2 // In some board it's red in others yellow + + 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 + VCOM_LUT = 0x20 + W2W_LUT = 0x21 + B2W_LUT = 0x22 + W2B_LUT = 0x23 + B2B_LUT = 0x24 + PLL_CONTROL = 0x30 + TEMPERATURE_SENSOR_CALIBRATION = 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 + GET_STATUS = 0x71 + AUTO_MEASURE_VCOM = 0x80 + READ_VCOM_VALUE = 0x81 + VCM_DC_SETTING = 0x82 + PARTIAL_WINDOW = 0x90 + PARTIAL_IN = 0x91 + PARTIAL_OUT = 0x92 + PROGRAM_MODE = 0xA0 + ACTIVE_PROGRAM = 0xA1 + READ_OTP_DATA = 0xA2 + POWER_SAVING = 0xE3 +)