Skip to content

Commit

Permalink
epd2in66b: Waveshare 2.66inch E-Paper Display Module (B) for Raspberr…
Browse files Browse the repository at this point in the history
…y Pi Pico (#673)

epd2in66b: add working driver

Co-authored-by: Thomas Richner <thomas.richner@oviva.com>
  • Loading branch information
trichner and thomasrichner-oviva committed Jun 10, 2024
1 parent 7d98364 commit 4a0bcba
Show file tree
Hide file tree
Showing 6 changed files with 499 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst
# Recursively find all *_test.go files from cwd & reduce to unique dir names
HAS_TESTS = $(sort $(dir $(call rwildcard,,*_test.go)))
# Exclude anything we explicitly don't want to test for whatever reason
EXCLUDE_TESTS = image
EXCLUDE_TESTS = image waveshare-epd/epd2in66b
TESTS = $(filter-out $(addsuffix /%,$(EXCLUDE_TESTS)),$(HAS_TESTS))

unit-test:
Expand Down
84 changes: 84 additions & 0 deletions examples/waveshare-epd/epd2in66b/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"image/color"
"machine"
"time"

"tinygo.org/x/drivers"
"tinygo.org/x/drivers/waveshare-epd/epd2in66b"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freemono"
)

var (
black = color.RGBA{0, 0, 0, 0xff}
white = color.RGBA{0xff, 0xff, 0xff, 0xff}
red = color.RGBA{0xff, 0, 0, 0xff}
)

func main() {
machine.Serial.Configure(machine.UARTConfig{})
time.Sleep(2 * time.Second)

machine.SPI1.Configure(machine.SPIConfig{
Frequency: epd2in66b.Baudrate,
})

println("started")

// in case you have a Pico module, you can directly use
// dev, err := epd2in66b.NewPicoModule()

display := epd2in66b.New(machine.SPI1)

cfg := epd2in66b.Config{
DataPin: machine.GP8,
ChipSelectPin: machine.GP9,
ResetPin: machine.GP12,
BusyPin: machine.GP13,
}
err := display.Configure(cfg)
if err != nil {
panic(err)
}

err = display.Reset()
if err != nil {
panic(err)
}

println("draw checkerboard")
drawCheckerBoard(&display)

println("draw 'hello'")
tinyfont.WriteLineRotated(&display, &freemono.Bold24pt7b, 40, 10, "Hello!", white, tinyfont.ROTATION_90)
tinyfont.WriteLineRotated(&display, &freemono.Bold12pt7b, 10, 10, "tinygo rocks", white, tinyfont.ROTATION_90)
err = display.Display()
if err != nil {
panic(err)
}
}

func drawCheckerBoard(display drivers.Displayer) {
s := 8
width, height := display.Size()
for x := 0; x <= int(width)-s; x += s {
for y := 0; y <= int(height)-s; y += s {
c := red
if (x/s)%2 == (y/s)%2 {
c = black
}

showRect(display, x, y, s, s, c)
}
}
}

func showRect(display drivers.Displayer, x int, y int, w int, h int, c color.RGBA) {
for i := x; i < x+w; i++ {
for j := y; j < y+h; j++ {
display.SetPixel(int16(i), int16(j), c)
}
}
}
1 change: 1 addition & 0 deletions smoketest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl6
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
tinygo build -size short -o ./build/test.hex -target=pico ./examples/waveshare-epd/epd2in66b/main.go
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812
tinygo build -size short -o ./build/test.bin -target=m5stamp-c3 ./examples/ws2812
tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go
Expand Down
287 changes: 287 additions & 0 deletions waveshare-epd/epd2in66b/dev.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
// Package epd2in66b implements a driver for the Waveshare 2.66inch E-Paper E-Ink Display Module (B)
// for Raspberry Pi Pico, 296×152, Red / Black / White
// Datasheet: https://files.waveshare.com/upload/e/ec/2.66inch-e-paper-b-specification.pdf
package epd2in66b

import (
"image/color"
"machine"
"time"

"tinygo.org/x/drivers"
)

const (
displayWidth = 152
displayHeight = 296
)

const Baudrate = 4_000_000 // 4 MHz

type Config struct {
ResetPin machine.Pin
DataPin machine.Pin
ChipSelectPin machine.Pin
BusyPin machine.Pin
}

type Device struct {
bus drivers.SPI
cs machine.Pin
dc machine.Pin
rst machine.Pin
busy machine.Pin

blackBuffer []byte
redBuffer []byte
}

// New allocates a new device.
// The bus is expected to be configured and ready for use.
func New(bus drivers.SPI) Device {
pixelCount := displayWidth * displayHeight

bufLen := pixelCount / 8

return Device{
bus: bus,
blackBuffer: make([]byte, bufLen),
redBuffer: make([]byte, bufLen),
}
}

// Configure configures the device and its pins.
func (d *Device) Configure(c Config) error {
d.cs = c.ChipSelectPin
d.dc = c.DataPin
d.rst = c.ResetPin
d.busy = c.BusyPin

d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.busy.Configure(machine.PinConfig{Mode: machine.PinInput})

return nil
}

func (d *Device) Size() (x, y int16) {
return displayWidth, displayHeight
}

// SetPixel modifies the internal buffer in a single pixel.
// The display has 3 colors: red, black and white
//
// - white = RGBA(255,255,255, 1-255)
// - red = RGBA(1-255,0,0,1-255)
// - Anything else as black
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
if x < 0 || x >= displayWidth || y < 0 || y >= displayHeight {
return
}

bytePos, bitPos := pos(x, y, displayWidth)

if c.R == 0xff && c.G == 0xff && c.B == 0xff && c.A > 0 { // white
set(d.blackBuffer, bytePos, bitPos)
unset(d.redBuffer, bytePos, bitPos)
} else if c.R != 0 && c.G == 0 && c.B == 0 && c.A > 0 { // red-ish
set(d.blackBuffer, bytePos, bitPos)
set(d.redBuffer, bytePos, bitPos)
} else { // black or other
unset(d.blackBuffer, bytePos, bitPos)
unset(d.redBuffer, bytePos, bitPos)
}
}

func set(buf []byte, bytePos, bitPos int) {
buf[bytePos] |= 0x1 << bitPos
}

func unset(buf []byte, bytePos, bitPos int) {
buf[bytePos] &^= 0x1 << bitPos
}

func pos(x, y, stride int16) (bytePos int, bitPos int) {
p := int(x) + int(y)*int(stride)
bytePos = p / 8

// reverse bit position as it is reversed on the device's buffer
bitPos = 7 - p%8

return bytePos, bitPos
}

func (d *Device) Display() error {
// Write RAM (Black White) / RAM 0x24
// 1 == white, 0 == black
if err := d.sendCommandByte(0x24); err != nil {
return err
}

if err := d.sendData(d.blackBuffer); err != nil {
return err
}

// Write RAM (RED) / RAM 0x26)
// 0 == blank, 1 == red
if err := d.sendCommandByte(0x26); err != nil {
return err
}

if err := d.sendData(d.redBuffer); err != nil {
return err
}

return d.turnOnDisplay()
}

func (d *Device) ClearBuffer() {
fill(d.redBuffer, 0x00)
fill(d.blackBuffer, 0xff)
}

func (d *Device) turnOnDisplay() error {
// also documented as 'Master Activation'
if err := d.sendCommandByte(0x20); err != nil {
return err
}
d.WaitUntilIdle()
return nil
}

func (d *Device) Reset() error {
d.hwReset()
d.WaitUntilIdle()

// soft reset & set defaults
if err := d.sendCommandByte(0x12); err != nil {
return err
}
d.WaitUntilIdle()

// data entry mode setting
if err := d.sendCommandSequence([]byte{0x11, 0x03}); err != nil {
return err
}

if err := d.setWindow(0, displayWidth-1, 0, displayHeight-1); err != nil {
return err
}

// display update control 1 - resolution setting
if err := d.sendCommandSequence([]byte{0x21, 0x00, 0x80}); err != nil {
return err
}

if err := d.setCursor(0, 0); err != nil {
return err
}
d.WaitUntilIdle()

return nil
}

func (d *Device) setCursor(x, y uint16) error {
// Set RAM X address counter
if err := d.sendCommandSequence([]byte{0x4e, byte(x & 0x1f)}); err != nil {
return err
}

// Set RAM Y address counter
yLo := byte(y)
yHi := byte(y>>8) & 0x1
if err := d.sendCommandSequence([]byte{0x4f, yLo, yHi}); err != nil {
return err
}

return nil
}

func (d *Device) hwReset() {
d.rst.High()
time.Sleep(50 * time.Millisecond)
d.rst.Low()
time.Sleep(2 * time.Millisecond)
d.rst.High()
time.Sleep(50 * time.Millisecond)
}

func (d *Device) setWindow(xstart, xend, ystart, yend int16) error {
// set RAM X-address start / end position
d1 := byte((xstart >> 3) & 0x1f)
d2 := byte((xend >> 3) & 0x1f)
if err := d.sendCommandSequence([]byte{0x44, d1, d2}); err != nil {
return err
}

// set RAM Y-address start / end position
ystartLo := byte(ystart)
ystartHi := byte(ystart>>8) & 0x1

yendLo := byte(yend)
yendHi := byte(yend>>8) & 0x1

return d.sendCommandSequence([]byte{0x45, ystartLo, ystartHi, yendLo, yendHi})
}

func (d *Device) WaitUntilIdle() {
// give it some time to get busy
time.Sleep(50 * time.Millisecond)

for d.busy.Get() { // high = busy
time.Sleep(10 * time.Millisecond)
}

// give it some extra time
time.Sleep(50 * time.Millisecond)
}

// sendCommandSequence sends the first byte in the buffer as a 'command' and all following bytes as data
func (d *Device) sendCommandSequence(seq []byte) error {
err := d.sendCommandByte(seq[0])
if err != nil {
return err
}

for i := 1; i < len(seq); i++ {
err = d.sendDataByte(seq[i])
if err != nil {
return err
}
}

return nil
}

func (d *Device) sendCommandByte(b byte) error {
d.dc.Low()
d.cs.Low()
_, err := d.bus.Transfer(b)
d.cs.High()
return err
}

func (d *Device) sendDataByte(b byte) error {
d.dc.High()
d.cs.Low()
_, err := d.bus.Transfer(b)
d.cs.High()
return err
}

func (d *Device) sendData(b []byte) error {
d.dc.High()
d.cs.Low()
err := d.bus.Tx(b, nil)
d.cs.High()
return err
}

// fill quickly fills a slice with a given value
func fill(s []byte, b byte) {
s[0] = b
for j := 1; j < len(s); j *= 2 {
copy(s[j:], s[:j])
}
}
Loading

0 comments on commit 4a0bcba

Please sign in to comment.