diff --git a/Makefile b/Makefile index fd5e4bc5b..4808532b8 100644 --- a/Makefile +++ b/Makefile @@ -119,6 +119,8 @@ smoke-test: @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13x/main.go @md5sum ./build/test.hex + tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd4in2/main.go + @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/wifinina/ntpclient/main.go @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/wifinina/udpstation/main.go diff --git a/README.md b/README.md index 89cb029a2..0bd1625f4 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ func main() { ## Currently supported devices -The following 52 devices are supported. +The following 53 devices are supported. | Device Name | Interface Type | |----------|-------------| @@ -107,6 +107,7 @@ The following 52 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 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 | ## Contributing diff --git a/examples/waveshare-epd/epd4in2/main.go b/examples/waveshare-epd/epd4in2/main.go new file mode 100644 index 000000000..a78b47431 --- /dev/null +++ b/examples/waveshare-epd/epd4in2/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "machine" + + "image/color" + + "time" + + "tinygo.org/x/drivers/waveshare-epd/epd4in2" +) + +var display epd4in2.Device + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 8000000, + Mode: 0, + }) + + display = epd4in2.New(machine.SPI0, machine.P6, machine.P7, machine.P8, machine.P9) + display.Configure(epd4in2.Config{}) + + black := color.RGBA{1, 1, 1, 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 < 16; i++ { + for j := int16(0); j < 25; j++ { + if (i+j)%2 == 0 { + showRect(i*8, j*10, 8, 10, black) + } + } + } + println("Show checkered board") + display.Display() + display.WaitUntilIdle() + println("Waiting for 2 seconds") + time.Sleep(2 * time.Second) + + 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/epd4in2/epd4in2.go b/waveshare-epd/epd4in2/epd4in2.go new file mode 100644 index 000000000..e13ffee44 --- /dev/null +++ b/waveshare-epd/epd4in2/epd4in2.go @@ -0,0 +1,355 @@ +// Package epd4in2 implements a driver for Waveshare 4.2in 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/epd4in2/epd4in2.cpp +// +// Datasheet: https://www.waveshare.com/wiki/4.2inch_e-Paper_Module +// +package epd4in2 + +import ( + "image/color" + "machine" + "time" +) + +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 machine.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 + +// New returns a new epd4in2 driver. Pass in a fully configured SPI bus. +func New(bus machine.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(0xff) // VDHR + d.SendCommand(BOOSTER_SOFT_START) + d.SendData(0x17) + d.SendData(0x17) + d.SendData(0x17) //07 0f 17 1f 27 2F 37 2f + d.SendCommand(POWER_ON) + d.WaitUntilIdle() + d.SendCommand(PANEL_SETTING) + d.SendData(0xbf) // KW-BF KWR-AF BWROTP 0f + d.SendData(0x0b) + d.SendCommand(PLL_CONTROL) + d.SendData(0x3c) // 3A 100HZ 29 150Hz 39 200HZ 31 171HZ +} + +// 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(VCOM_AND_DATA_INTERVAL_SETTING) + d.SendData(0x17) //border floating + d.SendCommand(VCM_DC_SETTING) //VCOM to 0V + 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) //power off + d.WaitUntilIdle() + d.SendCommand(DEEP_SLEEP) //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 full or partial updates +func (d *Device) SetLUT() { + lut_vcom0 := []uint8{ + 0x00, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x00, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, // 44 bytes, unlike the others + } + lut_ww := []uint8{ + 0x40, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x40, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + lut_bw := []uint8{ + 0x40, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x40, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0xA0, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + lut_bb := []uint8{ + 0x80, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x80, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + lut_wb := []uint8{ + 0x80, 0x17, 0x00, 0x00, 0x00, 0x02, + 0x90, 0x17, 0x17, 0x00, 0x00, 0x02, + 0x80, 0x0A, 0x01, 0x00, 0x00, 0x01, + 0x50, 0x0E, 0x0E, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + + d.SendCommand(LUT_FOR_VCOM) //vcom + for count := 0; count < 44; count++ { + d.SendData(lut_vcom0[count]) + } + + d.SendCommand(LUT_WHITE_TO_WHITE) //ww -- + for count := 0; count < 42; count++ { + d.SendData(lut_ww[count]) + } + + d.SendCommand(LUT_BLACK_TO_WHITE) //bw r + for count := 0; count < 42; count++ { + d.SendData(lut_bw[count]) + } + + d.SendCommand(LUT_WHITE_TO_BLACK) //wb w + for count := 0; count < 42; count++ { + d.SendData(lut_bb[count]) + } + + d.SendCommand(LUT_BLACK_TO_BLACK) //bb b + for count := 0; count < 42; count++ { + d.SendData(lut_wb[count]) + } +} + +// 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 := (uint32(x) + uint32(y)*uint32(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.SendCommand(RESOLUTION_SETTING) + d.SendData(uint8(d.height >> 8)) + d.SendData(uint8(d.logicalWidth & 0xff)) + d.SendData(uint8(d.height >> 8)) + d.SendData(uint8(d.height & 0xff)) + + d.SendCommand(VCM_DC_SETTING) + d.SendData(0x12) + + d.SendCommand(VCOM_AND_DATA_INTERVAL_SETTING) + d.SendCommand(0x97) //VBDF 17|D7 VBDW 97 VBDB 57 VBDF F7 VBDW 77 VBDB 37 VBDR B7 + + d.SendCommand(DATA_START_TRANSMISSION_1) + var i int16 + for i = 0; i < d.logicalWidth/8*d.height; i++ { + d.SendData(0xFF) // bit set: white, bit reset: black + } + time.Sleep(2 * time.Millisecond) + d.SendCommand(DATA_START_TRANSMISSION_2) + for i = 0; i < d.logicalWidth/8*d.height; i++ { + d.SendData(d.buffer[i]) + } + time.Sleep(2 * time.Millisecond) + + d.SetLUT() + + d.SendCommand(DISPLAY_REFRESH) + time.Sleep(100 * time.Millisecond) + d.WaitUntilIdle() + + return nil +} + +// ClearDisplay erases the device SRAM +func (d *Device) ClearDisplay() { + d.SendCommand(RESOLUTION_SETTING) + d.SendData(uint8(d.height >> 8)) + d.SendData(uint8(d.logicalWidth & 0xff)) + d.SendData(uint8(d.height >> 8)) + d.SendData(uint8(d.height & 0xff)) + + d.SendCommand(DATA_START_TRANSMISSION_1) + time.Sleep(2 * time.Millisecond) + var i int16 + for i = 0; i < d.logicalWidth/8*d.height; i++ { + d.SendData(0xFF) + } + time.Sleep(2 * time.Millisecond) + d.SendCommand(DATA_START_TRANSMISSION_2) + time.Sleep(2 * time.Millisecond) + for i = 0; i < d.logicalWidth/8*d.height; i++ { + d.SendData(0xFF) + } + time.Sleep(2 * time.Millisecond) + + d.SetLUT() + d.SendCommand(DISPLAY_REFRESH) + time.Sleep(100 * time.Millisecond) + 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/epd4in2/registers.go b/waveshare-epd/epd4in2/registers.go new file mode 100644 index 000000000..484b5c2be --- /dev/null +++ b/waveshare-epd/epd4in2/registers.go @@ -0,0 +1,55 @@ +package epd4in2 + +// Derived from https://github.com/waveshare/e-Paper/blob/master/Arduino/epd4in2/epd4in2.h + +// Registers +const ( + // Display resolution + EPD_WIDTH = 400 + EPD_HEIGHT = 300 + + // EPD4IN2 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 + 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 // 90 degrees clock-wise rotation + ROTATION_180 Rotation = 2 + ROTATION_270 Rotation = 3 +)