mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-05 23:43:41 +00:00
epd2in66b: Waveshare 2.66inch E-Paper Display Module (B) for Raspberry Pi Pico (#673)
epd2in66b: add working driver Co-authored-by: Thomas Richner <thomas.richner@oviva.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//go:build pico
|
||||
|
||||
package epd2in66b
|
||||
|
||||
import (
|
||||
"machine"
|
||||
)
|
||||
|
||||
// DefaultConfig contains the default config for the https://www.waveshare.com/wiki/Pico-ePaper-2.66 module
|
||||
var DefaultConfig = Config{
|
||||
DataPin: machine.GP8,
|
||||
ChipSelectPin: machine.GP9,
|
||||
ResetPin: machine.GP12,
|
||||
BusyPin: machine.GP13,
|
||||
}
|
||||
|
||||
// NewPicoModule allocates a new device backed by the https://www.waveshare.com/wiki/Pico-ePaper-2.66 module
|
||||
// This will also configure the SPI1 bus and configure the device with the DefaultConfig
|
||||
func NewPicoModule() (Device, error) {
|
||||
spi := machine.SPI1
|
||||
|
||||
if err := spi.Configure(machine.SPIConfig{
|
||||
Frequency: Baudrate,
|
||||
}); err != nil {
|
||||
return Device{}, err
|
||||
}
|
||||
|
||||
dev := New(spi)
|
||||
if err := dev.Configure(DefaultConfig); err != nil {
|
||||
return dev, err
|
||||
}
|
||||
|
||||
return dev, nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package epd2in66b
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/tinyfont"
|
||||
"tinygo.org/x/tinyfont/freemono"
|
||||
)
|
||||
|
||||
type mockBus struct{}
|
||||
|
||||
func (m *mockBus) Tx(w, r []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockBus) Transfer(b byte) (byte, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func TestBufferDrawing(t *testing.T) {
|
||||
dev := New(&mockBus{})
|
||||
|
||||
tinyfont.WriteLine(&dev, &freemono.Bold9pt7b, 10, 40, "Hello World!", color.RGBA{0xff, 0xff, 0xff, 0xff})
|
||||
|
||||
red := color.RGBA{0xff, 0, 0, 0xff}
|
||||
black := color.RGBA{0xff, 0xff, 0xff, 0xff}
|
||||
showRect(&dev, 10, 10, 10, 10, black)
|
||||
showRect(&dev, 10, 20, 10, 10, red)
|
||||
|
||||
img := toImage(&dev)
|
||||
writeImage(img)
|
||||
}
|
||||
|
||||
func toImage(dev *Device) *image.RGBA {
|
||||
red := color.RGBA{0xff, 0, 0, 0xff}
|
||||
|
||||
xMax, yMax := dev.Size()
|
||||
r := image.Rect(0, 0, int(xMax), int(yMax))
|
||||
container := image.NewRGBA(r)
|
||||
draw.Draw(container, container.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Over)
|
||||
|
||||
for x := 0; x < int(xMax); x++ {
|
||||
for y := 0; y < int(yMax); y++ {
|
||||
|
||||
bytePos, bitPos := pos(int16(x), int16(y), displayWidth)
|
||||
|
||||
if isSet(dev.redBuffer, bytePos, bitPos) {
|
||||
container.Set(x, y, red)
|
||||
} else if isSet(dev.blackBuffer, bytePos, bitPos) {
|
||||
container.Set(x, y, color.Black)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
func isSet(buf []byte, bytePos, bitPos int) bool {
|
||||
return (buf[bytePos])&(0x1<<bitPos) != 0
|
||||
}
|
||||
|
||||
func showRect(display drivers.Displayer, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeImage(img image.Image) string {
|
||||
fn := fmt.Sprintf("%d.png", time.Now().Unix())
|
||||
f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0o644)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
err = png.Encode(f, img)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fn
|
||||
}
|
||||
Reference in New Issue
Block a user