mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-09-01 20:39:04 +00:00
gdew0154m09: add e-paper display driver
This commit is contained in:
committed by
Ron Evans
parent
be556a971c
commit
119afb933c
@@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"device/esp"
|
||||||
|
"image/color"
|
||||||
|
"machine"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tinygo.org/x/drivers/gdew0154m09"
|
||||||
|
"tinygo.org/x/tinyfont"
|
||||||
|
"tinygo.org/x/tinyfont/freemono"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Hold the M5Stack CoreInk power rail on.
|
||||||
|
machine.GPIO12.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
|
machine.GPIO12.High()
|
||||||
|
|
||||||
|
cs := machine.IO9
|
||||||
|
dc := machine.GPIO15
|
||||||
|
rst := machine.IO0
|
||||||
|
busy := machine.IO4
|
||||||
|
cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
|
dc.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
|
rst.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
|
busy.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||||
|
cs.High()
|
||||||
|
dc.High()
|
||||||
|
rst.High()
|
||||||
|
|
||||||
|
// Enable and reset the ESP32 VSPI peripheral before configuring SPI3.
|
||||||
|
esp.DPORT.SetPERIP_RST_EN_SPI3_RST(1)
|
||||||
|
esp.DPORT.SetPERIP_CLK_EN_SPI3_CLK_EN(1)
|
||||||
|
esp.DPORT.SetPERIP_RST_EN_SPI3_RST(0)
|
||||||
|
if err := machine.SPI3.Configure(machine.SPIConfig{
|
||||||
|
Frequency: gdew0154m09.Baudrate,
|
||||||
|
SCK: machine.IO18,
|
||||||
|
SDO: machine.IO23,
|
||||||
|
SDI: machine.NoPin,
|
||||||
|
Mode: gdew0154m09.SPIMode,
|
||||||
|
}); err != nil {
|
||||||
|
println("could not configure SPI:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
display := gdew0154m09.New(machine.SPI3, cs, dc, rst, busy)
|
||||||
|
if err := display.Configure(gdew0154m09.DefaultConfig); err != nil {
|
||||||
|
println("could not configure display:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
black := color.RGBA{A: 0xff}
|
||||||
|
tinyfont.WriteLine(display, &freemono.Bold18pt7b, 15, 90, "TinyGo", black)
|
||||||
|
for x := int16(0); x < gdew0154m09.Width; x++ {
|
||||||
|
display.SetPixel(x, 0, black)
|
||||||
|
display.SetPixel(x, gdew0154m09.Height-1, black)
|
||||||
|
}
|
||||||
|
for y := int16(0); y < gdew0154m09.Height; y++ {
|
||||||
|
display.SetPixel(0, y, black)
|
||||||
|
display.SetPixel(gdew0154m09.Width-1, y, black)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := display.Display(); err != nil {
|
||||||
|
println("could not refresh display:", err.Error())
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Hour)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
// Package gdew0154m09 provides a driver for the Good Display GDEW0154M09
|
||||||
|
// 1.54-inch 200x200 monochrome e-paper panel used by the M5Stack CoreInk.
|
||||||
|
//
|
||||||
|
// The initialization sequence is based on the MIT-licensed M5Stack
|
||||||
|
// M5Core-Ink library:
|
||||||
|
// https://github.com/m5stack/M5Core-Ink
|
||||||
|
package gdew0154m09 // import "tinygo.org/x/drivers/gdew0154m09"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image/color"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tinygo.org/x/drivers"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Width and Height are the physical panel dimensions in pixels.
|
||||||
|
Width = 200
|
||||||
|
Height = 200
|
||||||
|
|
||||||
|
// Baudrate and SPIMode are the recommended SPI bus configuration.
|
||||||
|
Baudrate = 10_000_000
|
||||||
|
SPIMode = 3
|
||||||
|
|
||||||
|
frameBufferSize = Width * Height / 8
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBusyTimeout = errors.New("gdew0154m09: busy timeout")
|
||||||
|
ErrInvalidBusyTimeout = errors.New("gdew0154m09: busy timeout must not be negative")
|
||||||
|
)
|
||||||
|
|
||||||
|
// OutputPin is the interface required from an output pin. Pins must be
|
||||||
|
// configured by the caller before they are passed to New.
|
||||||
|
type OutputPin interface {
|
||||||
|
Set(level bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputPin is the interface required from an input pin. The pin must be
|
||||||
|
// configured by the caller before it is passed to New.
|
||||||
|
type InputPin interface {
|
||||||
|
Get() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config contains the display configuration.
|
||||||
|
type Config struct {
|
||||||
|
// BusyTimeout is the maximum time to wait for a panel operation. A zero
|
||||||
|
// duration selects the default timeout.
|
||||||
|
BusyTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig contains the recommended display configuration.
|
||||||
|
var DefaultConfig = Config{
|
||||||
|
BusyTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device is a GDEW0154M09 display connected over SPI.
|
||||||
|
type Device struct {
|
||||||
|
bus drivers.SPI
|
||||||
|
cs OutputPin
|
||||||
|
dc OutputPin
|
||||||
|
rst OutputPin
|
||||||
|
busy InputPin
|
||||||
|
|
||||||
|
buffer [frameBufferSize]byte
|
||||||
|
previous [frameBufferSize]byte
|
||||||
|
tx [3]byte
|
||||||
|
busyTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ drivers.Displayer = (*Device)(nil)
|
||||||
|
|
||||||
|
// New returns a new GDEW0154M09 driver. The SPI bus and pins must already be
|
||||||
|
// configured. New performs no I/O.
|
||||||
|
func New(bus drivers.SPI, cs, dc, rst OutputPin, busy InputPin) *Device {
|
||||||
|
d := &Device{
|
||||||
|
bus: bus,
|
||||||
|
cs: cs,
|
||||||
|
dc: dc,
|
||||||
|
rst: rst,
|
||||||
|
busy: busy,
|
||||||
|
busyTimeout: DefaultConfig.BusyTimeout,
|
||||||
|
}
|
||||||
|
d.ClearBuffer()
|
||||||
|
fill(d.previous[:], 0xff)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure resets and initializes the display controller. It also clears the
|
||||||
|
// current and previous framebuffers to white without refreshing the panel.
|
||||||
|
func (d *Device) Configure(config Config) error {
|
||||||
|
if config.BusyTimeout < 0 {
|
||||||
|
return ErrInvalidBusyTimeout
|
||||||
|
}
|
||||||
|
if config.BusyTimeout == 0 {
|
||||||
|
config.BusyTimeout = DefaultConfig.BusyTimeout
|
||||||
|
}
|
||||||
|
d.busyTimeout = config.BusyTimeout
|
||||||
|
|
||||||
|
d.hardwareReset()
|
||||||
|
if err := d.waitUntilIdle(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.sendCommand2(commandPanelSetting, 0xdf, 0x0e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand1(commandFITIInternalCode, 0x55); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand1(commandUnknownAA, 0x0f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand1(commandUnknownE9, 0x02); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand1(commandBoosterSoftStart, 0x11); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand1(commandPowerSequence, 0x0a); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand3(commandResolutionSetting, 0xc8, 0x00, 0xc8); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand1(commandTCONSetting, 0x00); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand1(commandVCOMDataInterval, defaultVCOMDataInterval); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand1(commandPowerSaving, 0x00); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand(commandPowerOn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
if err := d.waitUntilIdle(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.ClearBuffer()
|
||||||
|
fill(d.previous[:], 0xff)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the dimensions of the display in pixels.
|
||||||
|
func (d *Device) Size() (width, height int16) {
|
||||||
|
return Width, Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPixel updates one pixel in the framebuffer. Transparent and light colors
|
||||||
|
// are rendered white; opaque dark colors are rendered black.
|
||||||
|
func (d *Device) SetPixel(x, y int16, c color.RGBA) {
|
||||||
|
if x < 0 || x >= Width || y < 0 || y >= Height {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
index := int(y)*(Width/8) + int(x)/8
|
||||||
|
mask := byte(0x80 >> uint(x%8))
|
||||||
|
brightness := uint16(c.R) + uint16(c.G) + uint16(c.B)
|
||||||
|
if c.A == 0 || brightness >= 3*128 {
|
||||||
|
d.buffer[index] |= mask
|
||||||
|
} else {
|
||||||
|
d.buffer[index] &^= mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearBuffer sets every pixel in the framebuffer to white without refreshing
|
||||||
|
// the panel.
|
||||||
|
func (d *Device) ClearBuffer() {
|
||||||
|
fill(d.buffer[:], 0xff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display sends the previous and current framebuffers to the panel and starts
|
||||||
|
// a full refresh. It blocks until the refresh completes or the busy timeout is
|
||||||
|
// reached.
|
||||||
|
func (d *Device) Display() error {
|
||||||
|
if err := d.sendCommand(commandDataStartOld); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendData(d.previous[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
|
||||||
|
if err := d.sendCommand(commandDataStartNew); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendData(d.buffer[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Millisecond)
|
||||||
|
|
||||||
|
if err := d.sendCommand(commandDisplayRefresh); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.waitUntilIdle(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy(d.previous[:], d.buffer[:])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearDisplay clears the framebuffer and refreshes the panel.
|
||||||
|
func (d *Device) ClearDisplay() error {
|
||||||
|
d.ClearBuffer()
|
||||||
|
return d.Display()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepSleep powers off the panel and enters deep sleep. Configure must be
|
||||||
|
// called to wake and reinitialize the controller.
|
||||||
|
func (d *Device) DeepSleep() error {
|
||||||
|
if err := d.sendCommand1(commandVCOMDataInterval, deepSleepVCOMDataInterval); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.waitUntilIdle(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand(commandPowerOff); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return d.sendCommand1(commandDeepSleep, deepSleepCheckCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBusy reports whether the active-low busy signal is asserted.
|
||||||
|
func (d *Device) IsBusy() bool {
|
||||||
|
return !d.busy.Get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) hardwareReset() {
|
||||||
|
d.rst.Set(true)
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
d.rst.Set(false)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
d.rst.Set(true)
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) waitUntilIdle() error {
|
||||||
|
deadline := time.Now().Add(d.busyTimeout)
|
||||||
|
for d.IsBusy() {
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return ErrBusyTimeout
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) sendCommandWithData(command byte, data []byte) error {
|
||||||
|
if err := d.sendCommand(command); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return d.sendData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) sendCommand1(command, data byte) error {
|
||||||
|
d.tx[0] = data
|
||||||
|
return d.sendCommandWithData(command, d.tx[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) sendCommand2(command, data0, data1 byte) error {
|
||||||
|
d.tx[0] = data0
|
||||||
|
d.tx[1] = data1
|
||||||
|
return d.sendCommandWithData(command, d.tx[:2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) sendCommand3(command, data0, data1, data2 byte) error {
|
||||||
|
d.tx[0] = data0
|
||||||
|
d.tx[1] = data1
|
||||||
|
d.tx[2] = data2
|
||||||
|
return d.sendCommandWithData(command, d.tx[:3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) sendCommand(command byte) error {
|
||||||
|
d.dc.Set(false)
|
||||||
|
d.cs.Set(false)
|
||||||
|
_, err := d.bus.Transfer(command)
|
||||||
|
d.cs.Set(true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Device) sendData(data []byte) error {
|
||||||
|
d.dc.Set(true)
|
||||||
|
d.cs.Set(false)
|
||||||
|
err := d.bus.Tx(data, nil)
|
||||||
|
d.cs.Set(true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func fill(buffer []byte, value byte) {
|
||||||
|
for index := range buffer {
|
||||||
|
buffer[index] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
package gdew0154m09
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"image/color"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeOutputPin struct {
|
||||||
|
high bool
|
||||||
|
changes []bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *fakeOutputPin) Set(level bool) {
|
||||||
|
p.high = level
|
||||||
|
p.changes = append(p.changes, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeInputPin struct {
|
||||||
|
high bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *fakeInputPin) Get() bool {
|
||||||
|
return p.high
|
||||||
|
}
|
||||||
|
|
||||||
|
type spiOperation struct {
|
||||||
|
command bool
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeSPI struct {
|
||||||
|
t *testing.T
|
||||||
|
cs *fakeOutputPin
|
||||||
|
dc *fakeOutputPin
|
||||||
|
operations []spiOperation
|
||||||
|
failAt int
|
||||||
|
failErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeSPI) Transfer(value byte) (byte, error) {
|
||||||
|
s.t.Helper()
|
||||||
|
if s.cs.high {
|
||||||
|
s.t.Error("chip select was high during command transfer")
|
||||||
|
}
|
||||||
|
if s.dc.high {
|
||||||
|
s.t.Error("data/command pin was high during command transfer")
|
||||||
|
}
|
||||||
|
s.operations = append(s.operations, spiOperation{command: true, data: []byte{value}})
|
||||||
|
if len(s.operations) == s.failAt {
|
||||||
|
return 0, s.failErr
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fakeSPI) Tx(write, read []byte) error {
|
||||||
|
s.t.Helper()
|
||||||
|
if s.cs.high {
|
||||||
|
s.t.Error("chip select was high during data transfer")
|
||||||
|
}
|
||||||
|
if !s.dc.high {
|
||||||
|
s.t.Error("data/command pin was low during data transfer")
|
||||||
|
}
|
||||||
|
data := append([]byte(nil), write...)
|
||||||
|
s.operations = append(s.operations, spiOperation{data: data})
|
||||||
|
if len(s.operations) == s.failAt {
|
||||||
|
return s.failErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestDevice(t *testing.T) (*Device, *fakeSPI, *fakeOutputPin, *fakeInputPin) {
|
||||||
|
t.Helper()
|
||||||
|
cs := &fakeOutputPin{high: true}
|
||||||
|
dc := &fakeOutputPin{high: true}
|
||||||
|
rst := &fakeOutputPin{high: true}
|
||||||
|
busy := &fakeInputPin{high: true}
|
||||||
|
bus := &fakeSPI{t: t, cs: cs, dc: dc}
|
||||||
|
return New(bus, cs, dc, rst, busy), bus, rst, busy
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewStartsWithWhiteBuffers(t *testing.T) {
|
||||||
|
device, _, _, _ := newTestDevice(t)
|
||||||
|
for index := range device.buffer {
|
||||||
|
if device.buffer[index] != 0xff || device.previous[index] != 0xff {
|
||||||
|
t.Fatalf("pixel byte %d was not initialized white", index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSize(t *testing.T) {
|
||||||
|
device, _, _, _ := newTestDevice(t)
|
||||||
|
width, height := device.Size()
|
||||||
|
if width != Width || height != Height {
|
||||||
|
t.Fatalf("size = %dx%d, want %dx%d", width, height, Width, Height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetPixel(t *testing.T) {
|
||||||
|
device, _, _, _ := newTestDevice(t)
|
||||||
|
black := color.RGBA{A: 0xff}
|
||||||
|
white := color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
|
||||||
|
|
||||||
|
device.SetPixel(0, 0, black)
|
||||||
|
device.SetPixel(199, 199, black)
|
||||||
|
if device.buffer[0] != 0x7f {
|
||||||
|
t.Fatalf("first byte = %#x, want 0x7f", device.buffer[0])
|
||||||
|
}
|
||||||
|
if device.buffer[len(device.buffer)-1] != 0xfe {
|
||||||
|
t.Fatalf("last byte = %#x, want 0xfe", device.buffer[len(device.buffer)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
device.SetPixel(0, 0, white)
|
||||||
|
if device.buffer[0] != 0xff {
|
||||||
|
t.Fatalf("first byte after white pixel = %#x, want 0xff", device.buffer[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetPixelIgnoresOutOfBoundsCoordinates(t *testing.T) {
|
||||||
|
device, _, _, _ := newTestDevice(t)
|
||||||
|
device.SetPixel(-1, 0, color.RGBA{A: 0xff})
|
||||||
|
device.SetPixel(0, -1, color.RGBA{A: 0xff})
|
||||||
|
device.SetPixel(Width, 0, color.RGBA{A: 0xff})
|
||||||
|
device.SetPixel(0, Height, color.RGBA{A: 0xff})
|
||||||
|
for index, value := range device.buffer {
|
||||||
|
if value != 0xff {
|
||||||
|
t.Fatalf("byte %d changed to %#x", index, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigure(t *testing.T) {
|
||||||
|
device, bus, rst, _ := newTestDevice(t)
|
||||||
|
if err := device.Configure(Config{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if device.busyTimeout != DefaultConfig.BusyTimeout {
|
||||||
|
t.Fatalf("busy timeout = %v, want %v", device.busyTimeout, DefaultConfig.BusyTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantReset := []bool{true, false, true}
|
||||||
|
if !equalBools(rst.changes, wantReset) {
|
||||||
|
t.Fatalf("reset changes = %v, want %v", rst.changes, wantReset)
|
||||||
|
}
|
||||||
|
wantCommands := []byte{0x00, 0x4d, 0xaa, 0xe9, 0xb6, 0xf3, 0x61, 0x60, 0x50, 0xe3, 0x04}
|
||||||
|
if got := commandBytes(bus.operations); !bytes.Equal(got, wantCommands) {
|
||||||
|
t.Fatalf("commands = %#v, want %#v", got, wantCommands)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantData := [][]byte{
|
||||||
|
{0xdf, 0x0e}, {0x55}, {0x0f}, {0x02}, {0x11}, {0x0a},
|
||||||
|
{0xc8, 0x00, 0xc8}, {0x00}, {0xd7}, {0x00},
|
||||||
|
}
|
||||||
|
if got := dataTransfers(bus.operations); !equalByteSlices(got, wantData) {
|
||||||
|
t.Fatalf("data transfers = %#v, want %#v", got, wantData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigurePropagatesSPIErrors(t *testing.T) {
|
||||||
|
wantErr := errors.New("SPI failure")
|
||||||
|
for _, operation := range []int{1, 2} {
|
||||||
|
t.Run(operationName(operation), func(t *testing.T) {
|
||||||
|
device, bus, _, _ := newTestDevice(t)
|
||||||
|
bus.failAt = operation
|
||||||
|
bus.failErr = wantErr
|
||||||
|
if err := device.Configure(DefaultConfig); !errors.Is(err, wantErr) {
|
||||||
|
t.Fatalf("error = %v, want %v", err, wantErr)
|
||||||
|
}
|
||||||
|
if !bus.cs.high {
|
||||||
|
t.Error("chip select was not released after SPI error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigureRejectsNegativeTimeout(t *testing.T) {
|
||||||
|
device, bus, rst, _ := newTestDevice(t)
|
||||||
|
err := device.Configure(Config{BusyTimeout: -time.Millisecond})
|
||||||
|
if !errors.Is(err, ErrInvalidBusyTimeout) {
|
||||||
|
t.Fatalf("error = %v, want %v", err, ErrInvalidBusyTimeout)
|
||||||
|
}
|
||||||
|
if len(bus.operations) != 0 || len(rst.changes) != 0 {
|
||||||
|
t.Fatal("invalid configuration performed hardware I/O")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplaySendsOldAndNewFrames(t *testing.T) {
|
||||||
|
device, bus, _, _ := newTestDevice(t)
|
||||||
|
device.SetPixel(0, 0, color.RGBA{A: 0xff})
|
||||||
|
if err := device.Display(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bus.operations) != 5 {
|
||||||
|
t.Fatalf("operation count = %d, want 5", len(bus.operations))
|
||||||
|
}
|
||||||
|
assertCommand(t, bus.operations[0], commandDataStartOld)
|
||||||
|
if len(bus.operations[1].data) != frameBufferSize || bus.operations[1].data[0] != 0xff {
|
||||||
|
t.Fatal("old frame was not initially white")
|
||||||
|
}
|
||||||
|
assertCommand(t, bus.operations[2], commandDataStartNew)
|
||||||
|
if len(bus.operations[3].data) != frameBufferSize || bus.operations[3].data[0] != 0x7f {
|
||||||
|
t.Fatal("new frame did not contain the black pixel")
|
||||||
|
}
|
||||||
|
assertCommand(t, bus.operations[4], commandDisplayRefresh)
|
||||||
|
|
||||||
|
bus.operations = nil
|
||||||
|
device.SetPixel(0, 0, color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
|
||||||
|
if err := device.Display(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if bus.operations[1].data[0] != 0x7f {
|
||||||
|
t.Fatal("second refresh did not send the first frame as old data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplayPropagatesSPIErrors(t *testing.T) {
|
||||||
|
wantErr := errors.New("SPI failure")
|
||||||
|
for operation := 1; operation <= 5; operation++ {
|
||||||
|
t.Run(operationName(operation), func(t *testing.T) {
|
||||||
|
device, bus, _, _ := newTestDevice(t)
|
||||||
|
bus.failAt = operation
|
||||||
|
bus.failErr = wantErr
|
||||||
|
if err := device.Display(); !errors.Is(err, wantErr) {
|
||||||
|
t.Fatalf("error = %v, want %v", err, wantErr)
|
||||||
|
}
|
||||||
|
if !bus.cs.high {
|
||||||
|
t.Error("chip select was not released after SPI error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBusyTimeout(t *testing.T) {
|
||||||
|
device, _, _, busy := newTestDevice(t)
|
||||||
|
busy.high = false
|
||||||
|
device.busyTimeout = time.Millisecond
|
||||||
|
if err := device.waitUntilIdle(); !errors.Is(err, ErrBusyTimeout) {
|
||||||
|
t.Fatalf("error = %v, want %v", err, ErrBusyTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeepSleep(t *testing.T) {
|
||||||
|
device, bus, _, _ := newTestDevice(t)
|
||||||
|
if err := device.DeepSleep(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantCommands := []byte{commandVCOMDataInterval, commandPowerOff, commandDeepSleep}
|
||||||
|
if got := commandBytes(bus.operations); !bytes.Equal(got, wantCommands) {
|
||||||
|
t.Fatalf("commands = %#v, want %#v", got, wantCommands)
|
||||||
|
}
|
||||||
|
wantData := [][]byte{{deepSleepVCOMDataInterval}, {deepSleepCheckCode}}
|
||||||
|
if got := dataTransfers(bus.operations); !equalByteSlices(got, wantData) {
|
||||||
|
t.Fatalf("data transfers = %#v, want %#v", got, wantData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertCommand(t *testing.T, operation spiOperation, command byte) {
|
||||||
|
t.Helper()
|
||||||
|
if !operation.command || len(operation.data) != 1 || operation.data[0] != command {
|
||||||
|
t.Fatalf("operation = %#v, want command %#x", operation, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandBytes(operations []spiOperation) []byte {
|
||||||
|
var commands []byte
|
||||||
|
for _, operation := range operations {
|
||||||
|
if operation.command {
|
||||||
|
commands = append(commands, operation.data[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataTransfers(operations []spiOperation) [][]byte {
|
||||||
|
var transfers [][]byte
|
||||||
|
for _, operation := range operations {
|
||||||
|
if !operation.command {
|
||||||
|
transfers = append(transfers, operation.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return transfers
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalBools(left, right []bool) bool {
|
||||||
|
if len(left) != len(right) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index := range left {
|
||||||
|
if left[index] != right[index] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalByteSlices(left, right [][]byte) bool {
|
||||||
|
if len(left) != len(right) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index := range left {
|
||||||
|
if !bytes.Equal(left[index], right[index]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func operationName(operation int) string {
|
||||||
|
return "operation-" + string(rune('0'+operation))
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package gdew0154m09
|
||||||
|
|
||||||
|
const (
|
||||||
|
commandPanelSetting = 0x00
|
||||||
|
commandPowerOff = 0x02
|
||||||
|
commandPowerOn = 0x04
|
||||||
|
commandDeepSleep = 0x07
|
||||||
|
commandDisplayRefresh = 0x12
|
||||||
|
commandDataStartOld = 0x10
|
||||||
|
commandDataStartNew = 0x13
|
||||||
|
commandVCOMDataInterval = 0x50
|
||||||
|
commandResolutionSetting = 0x61
|
||||||
|
commandTCONSetting = 0x60
|
||||||
|
commandPowerSaving = 0xe3
|
||||||
|
commandPowerSequence = 0xf3
|
||||||
|
commandBoosterSoftStart = 0xb6
|
||||||
|
commandUnknownAA = 0xaa
|
||||||
|
commandUnknownE9 = 0xe9
|
||||||
|
commandFITIInternalCode = 0x4d
|
||||||
|
deepSleepCheckCode = 0xa5
|
||||||
|
defaultVCOMDataInterval = 0xd7
|
||||||
|
deepSleepVCOMDataInterval = 0xf7
|
||||||
|
)
|
||||||
@@ -29,6 +29,7 @@ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/easyste
|
|||||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/flash/console/spi
|
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/flash/console/spi
|
||||||
tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/flash/console/qspi
|
tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/flash/console/qspi
|
||||||
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/gc9a01/main.go
|
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/gc9a01/main.go
|
||||||
|
tinygo build -size short -o ./build/test.bin -target=esp32-coreboard-v2 ./examples/gdew0154m09/main.go
|
||||||
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/gps/i2c/main.go
|
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/gps/i2c/main.go
|
||||||
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/gps/uart/main.go
|
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/gps/uart/main.go
|
||||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/hcsr04/main.go
|
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/hcsr04/main.go
|
||||||
|
|||||||
Reference in New Issue
Block a user