Files
drivers/waveshare-epd/epd2in9/epd2in9.go
T
2025-07-06 11:41:03 +02:00

288 lines
7.3 KiB
Go

// 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"
"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 drivers.Pin
dc drivers.Pin
rst drivers.Pin
busy drivers.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 and pins.
// Busy pin should input, the rest should be output.
func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin drivers.Pin) Device {
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
}