From b1efe3b0c225b32456287414f30a48d24862afc5 Mon Sep 17 00:00:00 2001 From: Andrey Subbota Date: Fri, 6 Nov 2026 12:46:06 +0100 Subject: [PATCH] unor4matrix: add UNO R4 WiFi LED matrix driver Drive the UNO R4 WiFi's 12x8 charlieplexed matrix through eleven GPIO lines, with Arduino packed frames and a host-buildable GPIO adapter. Use synchronous GPIO scanning without reserving a peripheral timer or IRQ. Display scans one 12-byte image through all 96 slots and floats each active pair after its slot. Dark slots retain their delay. Configure has no Config argument because this driver currently has no configuration options. The examples use the default tasks scheduler and include Conway's Life. --- examples/unor4matrix/checkerboard/main.go | 31 ++++++ examples/unor4matrix/life/life.go | 40 +++++++ examples/unor4matrix/life/main.go | 44 ++++++++ smoketest.sh | 4 + unor4matrix/README.md | 67 +++++++++++ unor4matrix/framebuffer.go | 27 +++++ unor4matrix/mapping.go | 116 ++++++++++++++++++++ unor4matrix/matrix.go | 90 +++++++++++++++ unor4matrix/matrix_test.go | 128 ++++++++++++++++++++++ unor4matrix/matrix_unor4.go | 30 +++++ unor4matrix/pins.go | 18 +++ unor4matrix/scan_test.go | 119 ++++++++++++++++++++ 12 files changed, 714 insertions(+) create mode 100644 examples/unor4matrix/checkerboard/main.go create mode 100644 examples/unor4matrix/life/life.go create mode 100644 examples/unor4matrix/life/main.go create mode 100644 unor4matrix/README.md create mode 100644 unor4matrix/framebuffer.go create mode 100644 unor4matrix/mapping.go create mode 100644 unor4matrix/matrix.go create mode 100644 unor4matrix/matrix_test.go create mode 100644 unor4matrix/matrix_unor4.go create mode 100644 unor4matrix/pins.go create mode 100644 unor4matrix/scan_test.go diff --git a/examples/unor4matrix/checkerboard/main.go b/examples/unor4matrix/checkerboard/main.go new file mode 100644 index 000000000..009758e4c --- /dev/null +++ b/examples/unor4matrix/checkerboard/main.go @@ -0,0 +1,31 @@ +//go:build arduino_uno_r4_wifi + +package main + +import ( + "image/color" + + "tinygo.org/x/drivers/unor4matrix" +) + +func main() { + display := unor4matrix.NewOnboard() + if err := display.Configure(); err != nil { + println(err.Error()) + return + } + width, height := display.Size() + for y := int16(0); y < height; y++ { + for x := int16(0); x < width; x++ { + if (x+y)%2 == 0 { + display.SetPixel(x, y, color.RGBA{R: 255}) + } + } + } + for { + if err := display.Display(); err != nil { + println(err.Error()) + return + } + } +} diff --git a/examples/unor4matrix/life/life.go b/examples/unor4matrix/life/life.go new file mode 100644 index 000000000..d0b736de9 --- /dev/null +++ b/examples/unor4matrix/life/life.go @@ -0,0 +1,40 @@ +//go:build arduino_uno_r4_wifi + +package main + +const ( + width = 12 + height = 8 +) + +type board [height][width]bool + +func nextGeneration(current board) (next board) { + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + neighbors := 0 + for dy := -1; dy <= 1; dy++ { + for dx := -1; dx <= 1; dx++ { + if dx == 0 && dy == 0 { + continue + } + if current[(y+dy+height)%height][(x+dx+width)%width] { + neighbors++ + } + } + } + next[y][x] = neighbors == 3 || current[y][x] && neighbors == 2 + } + } + return next +} + +func seed() board { + var cells board + cells[0][1] = true + cells[1][2] = true + cells[2][0] = true + cells[2][1] = true + cells[2][2] = true + return cells +} diff --git a/examples/unor4matrix/life/main.go b/examples/unor4matrix/life/main.go new file mode 100644 index 000000000..a92b98d31 --- /dev/null +++ b/examples/unor4matrix/life/main.go @@ -0,0 +1,44 @@ +//go:build arduino_uno_r4_wifi + +package main + +import ( + "image/color" + "time" + + "tinygo.org/x/drivers/unor4matrix" +) + +const generationInterval = 300 * time.Millisecond + +func main() { + display := unor4matrix.NewOnboard() + if err := display.Configure(); err != nil { + println(err.Error()) + return + } + cells := seed() + draw := func() { + display.ClearDisplay() + for y, row := range cells { + for x, alive := range row { + if alive { + display.SetPixel(int16(x), int16(y), color.RGBA{G: 255}) + } + } + } + } + draw() + nextUpdate := time.Now().Add(generationInterval) + for { + if err := display.Display(); err != nil { + println(err.Error()) + return + } + if now := time.Now(); !now.Before(nextUpdate) { + cells = nextGeneration(cells) + draw() + nextUpdate = now.Add(generationInterval) + } + } +} diff --git a/smoketest.sh b/smoketest.sh index b9a5636ba..9fc95f586 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -194,3 +194,7 @@ tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb # network examples (comboat) tinygo build -size short -o ./build/test.hex -target=elecrow-rp2040 -stack-size 8kb ./examples/net/tlsclient/ tinygo build -size short -o ./build/test.hex -target=elecrow-rp2350 -stack-size 8kb ./examples/net/ntpclient/ + +# Arduino UNO R4 WiFi LED matrix +tinygo build -size short -o ./build/test.hex -target=arduino-uno-r4-wifi ./examples/unor4matrix/checkerboard/ +tinygo build -size short -o ./build/test.hex -target=arduino-uno-r4-wifi ./examples/unor4matrix/life/ diff --git a/unor4matrix/README.md b/unor4matrix/README.md new file mode 100644 index 000000000..18c45d2c9 --- /dev/null +++ b/unor4matrix/README.md @@ -0,0 +1,67 @@ +# Arduino UNO R4 WiFi LED matrix + +Driver for the built-in 12x8 monochrome matrix. Import +`tinygo.org/x/drivers/unor4matrix`. The driver implements `drivers.Displayer`. + +## Usage + +```go +display := unor4matrix.NewOnboard() +if err := display.Configure(); err != nil { + return +} +display.SetPixel(0, 0, color.RGBA{R: 255}) +for { + if err := display.Display(); err != nil { + return + } +} +``` + +`NewOnboard` is available with `arduino_uno_r4_wifi`. `New` accepts eleven +`CharlieplexPin` values in this order: + +```text +P0_03 P0_04 P0_11 P0_12 P0_13 P0_15 P2_04 P2_05 P2_06 P2_12 P2_13 +``` + +Set must drive the requested level. Float must initialize a high-impedance input +without pull-ups. Configure returns `ErrMissingPinFunc` if either function is +missing, otherwise it floats every line. Configure may be repeated and keeps +the image. Give the device exclusive access to its pins. + +## Refresh model + +The image occupies 12 bytes. SetPixel edits it, ClearDisplay clears it, and +LoadFrame replaces it with Arduino-format words. Words are row-major, MSB first: +word 0 bit 31 is (0,0), word 2 bit 0 is (11,7). Any nonzero RGB is on, alpha is +ignored, and writes outside the drawing area are ignored. + +Display scans all 96 slots directly from the image and floats each active pair +after its slot. Call it repeatedly to keep an image visible. Every slot sleeps +for 100 us, including dark pixels. A frame takes at least 9.6 ms plus GPIO and +scheduler overhead. Before successful Configure, Display returns ErrNotConfigured. + +An active LED stays driven during time.Sleep. Other goroutines can prolong its on-time. + +## Examples and limitations + +```sh +tinygo flash -target=arduino-uno-r4-wifi ./examples/unor4matrix/checkerboard +tinygo flash -target=arduino-uno-r4-wifi ./examples/unor4matrix/life +``` + +Life runs a glider on a wrapping 12x8 grid with 300 ms generations. Display leaves +the lines floating before Life computes the next generation. + +Use the default `tasks` scheduler. With TinyGo 0.44 and `-scheduler=none`, short +sleeps can wait for a 1 ms SysTick, reducing refresh to about 10 Hz. Display +blocks and foreground work pauses refresh. Call the driver from one application +goroutine, never from an interrupt handler. Rotation and brightness control are +unsupported. UNO R4 Minima has no built-in matrix. UNO Q uses `unoqmatrix`. + +## Sources + +- [Arduino mapping and 10 kHz timer](https://github.com/arduino/ArduinoCore-renesas/blob/424e86eff92d37f72123c2b641dd8bbf06a38b47/libraries/Arduino_LED_Matrix/src/Arduino_LED_Matrix.h). +- [UNO R4 WiFi line order](https://github.com/arduino/ArduinoCore-renesas/blob/424e86eff92d37f72123c2b641dd8bbf06a38b47/variants/UNOWIFIR4/variant.cpp). +- [RA4M1 hardware manual](https://www.renesas.com/en/document/mah/renesas-ra4m1-group-users-manual-hardware?language=en), R01UH0887EJ0110 Rev.1.10, sections 19.2.1, 19.2.3 and 19.2.5, pp. 357, 359 and 361-362. PODR holds the output level, POSR/PORR update it, and PDR enables output. diff --git a/unor4matrix/framebuffer.go b/unor4matrix/framebuffer.go new file mode 100644 index 000000000..135538210 --- /dev/null +++ b/unor4matrix/framebuffer.go @@ -0,0 +1,27 @@ +package unor4matrix + +const ( + // Width is the number of columns in the matrix. + Width = 12 + // Height is the number of rows in the matrix. + Height = 8 + + framebufferSize = (Width*Height + 7) / 8 +) + +type framebuffer struct{ pixels [framebufferSize]byte } + +func (f *framebuffer) get(index int) bool { + return f.pixels[index/8]&(byte(1)<<(index%8)) != 0 +} + +func (f *framebuffer) set(index int, on bool) { + mask := byte(1) << (index % 8) + if on { + f.pixels[index/8] |= mask + } else { + f.pixels[index/8] &^= mask + } +} + +func (f *framebuffer) clear() { f.pixels = [framebufferSize]byte{} } diff --git a/unor4matrix/mapping.go b/unor4matrix/mapping.go new file mode 100644 index 000000000..b0afaf9d8 --- /dev/null +++ b/unor4matrix/mapping.go @@ -0,0 +1,116 @@ +package unor4matrix + +// pinPair holds matrix-line indices in the line order defined by NewOnboard in matrix_unor4.go. +type pinPair struct { + anode uint8 + cathode uint8 +} + +// ledMapping is row-major wiring from ArduinoCore-renesas. +// https://github.com/arduino/ArduinoCore-renesas/blob/424e86eff92d37f72123c2b641dd8bbf06a38b47/libraries/Arduino_LED_Matrix/src/Arduino_LED_Matrix.h +var ledMapping = [...]pinPair{ + // Row y = 0. + {anode: 7, cathode: 3}, + {anode: 3, cathode: 7}, + {anode: 7, cathode: 4}, + {anode: 4, cathode: 7}, + {anode: 3, cathode: 4}, + {anode: 4, cathode: 3}, + {anode: 7, cathode: 8}, + {anode: 8, cathode: 7}, + {anode: 3, cathode: 8}, + {anode: 8, cathode: 3}, + {anode: 4, cathode: 8}, + {anode: 8, cathode: 4}, + // Row y = 1. + {anode: 7, cathode: 0}, + {anode: 0, cathode: 7}, + {anode: 3, cathode: 0}, + {anode: 0, cathode: 3}, + {anode: 4, cathode: 0}, + {anode: 0, cathode: 4}, + {anode: 8, cathode: 0}, + {anode: 0, cathode: 8}, + {anode: 7, cathode: 6}, + {anode: 6, cathode: 7}, + {anode: 3, cathode: 6}, + {anode: 6, cathode: 3}, + // Row y = 2. + {anode: 4, cathode: 6}, + {anode: 6, cathode: 4}, + {anode: 8, cathode: 6}, + {anode: 6, cathode: 8}, + {anode: 0, cathode: 6}, + {anode: 6, cathode: 0}, + {anode: 7, cathode: 5}, + {anode: 5, cathode: 7}, + {anode: 3, cathode: 5}, + {anode: 5, cathode: 3}, + {anode: 4, cathode: 5}, + {anode: 5, cathode: 4}, + // Row y = 3. + {anode: 8, cathode: 5}, + {anode: 5, cathode: 8}, + {anode: 0, cathode: 5}, + {anode: 5, cathode: 0}, + {anode: 6, cathode: 5}, + {anode: 5, cathode: 6}, + {anode: 7, cathode: 1}, + {anode: 1, cathode: 7}, + {anode: 3, cathode: 1}, + {anode: 1, cathode: 3}, + {anode: 4, cathode: 1}, + {anode: 1, cathode: 4}, + // Row y = 4. + {anode: 8, cathode: 1}, + {anode: 1, cathode: 8}, + {anode: 0, cathode: 1}, + {anode: 1, cathode: 0}, + {anode: 6, cathode: 1}, + {anode: 1, cathode: 6}, + {anode: 5, cathode: 1}, + {anode: 1, cathode: 5}, + {anode: 7, cathode: 2}, + {anode: 2, cathode: 7}, + {anode: 3, cathode: 2}, + {anode: 2, cathode: 3}, + // Row y = 5. + {anode: 4, cathode: 2}, + {anode: 2, cathode: 4}, + {anode: 8, cathode: 2}, + {anode: 2, cathode: 8}, + {anode: 0, cathode: 2}, + {anode: 2, cathode: 0}, + {anode: 6, cathode: 2}, + {anode: 2, cathode: 6}, + {anode: 5, cathode: 2}, + {anode: 2, cathode: 5}, + {anode: 1, cathode: 2}, + {anode: 2, cathode: 1}, + // Row y = 6. + {anode: 7, cathode: 10}, + {anode: 10, cathode: 7}, + {anode: 3, cathode: 10}, + {anode: 10, cathode: 3}, + {anode: 4, cathode: 10}, + {anode: 10, cathode: 4}, + {anode: 8, cathode: 10}, + {anode: 10, cathode: 8}, + {anode: 0, cathode: 10}, + {anode: 10, cathode: 0}, + {anode: 6, cathode: 10}, + {anode: 10, cathode: 6}, + // Row y = 7. + {anode: 5, cathode: 10}, + {anode: 10, cathode: 5}, + {anode: 1, cathode: 10}, + {anode: 10, cathode: 1}, + {anode: 2, cathode: 10}, + {anode: 10, cathode: 2}, + {anode: 7, cathode: 9}, + {anode: 9, cathode: 7}, + {anode: 3, cathode: 9}, + {anode: 9, cathode: 3}, + {anode: 4, cathode: 9}, + {anode: 9, cathode: 4}, +} diff --git a/unor4matrix/matrix.go b/unor4matrix/matrix.go new file mode 100644 index 000000000..686a2e024 --- /dev/null +++ b/unor4matrix/matrix.go @@ -0,0 +1,90 @@ +// Package unor4matrix drives the UNO R4 WiFi's built-in 12x8 LED matrix. +// Display scans a complete frame and blanks the lines before returning. +package unor4matrix + +import ( + "errors" + "image/color" + "time" +) + +// Device stores one drawing buffer. Use it from one application goroutine. +type Device struct { + pins [numPins]CharlieplexPin + frame framebuffer + configured bool +} + +// refreshPeriod matches the Arduino matrix driver's 10 kHz scan timer. +// https://github.com/arduino/ArduinoCore-renesas/blob/424e86eff92d37f72123c2b641dd8bbf06a38b47/libraries/Arduino_LED_Matrix/src/Arduino_LED_Matrix.h#L172 +const refreshPeriod = 100 * time.Microsecond + +// ErrNotConfigured reports Display called before successful Configure. +var ErrNotConfigured = errors.New("unor4matrix: not configured") + +// ErrMissingPinFunc reports a missing Set or Float function. +var ErrMissingPinFunc = errors.New("unor4matrix: all eleven lines require Set and Float") + +// Configure floats all lines and retains the stored image. +// The caller exclusively owns the lines and serializes display operations. +func (d *Device) Configure() error { + for _, pin := range d.pins { + if pin.Set == nil || pin.Float == nil { + return ErrMissingPinFunc + } + } + for _, pin := range d.pins { + pin.Float() + } + d.configured = true + return nil +} + +// SetPixel edits one logical pixel. Nonzero RGB is on. Alpha is ignored. +// Out-of-bounds writes are ignored. Call Display to show changes. +func (d *Device) SetPixel(x, y int16, c color.RGBA) { + if x < 0 || x >= Width || y < 0 || y >= Height { + return + } + d.frame.set(int(y)*Width+int(x), c.R != 0 || c.G != 0 || c.B != 0) +} + +// Size returns the fixed 12 by 8 drawing area. +func (d *Device) Size() (width, height int16) { return Width, Height } + +// Display scans one frame and blanks the lines. Call repeatedly to refresh. +// On RA4M1, use tasks. With -scheduler=none, refresh may fall to about 10 Hz. +func (d *Device) Display() error { + if !d.configured { + return ErrNotConfigured + } + for index, pair := range ledMapping { + on := d.frame.get(index) + if on { + d.pins[pair.cathode].Set.Low() + d.pins[pair.anode].Set.High() + } + // Yield between slots so other goroutines can run. + // They may extend the active LED's on-time. + time.Sleep(refreshPeriod) + if on { + d.pins[pair.anode].Float() + d.pins[pair.cathode].Float() + } + } + return nil +} + +// ClearDisplay clears the drawing buffer. Call Display to show the change. +func (d *Device) ClearDisplay() { d.frame.clear() } + +// LoadFrame edits the drawing buffer using Arduino row-major, MSB-first words. +// Call Display to show the image. +func (d *Device) LoadFrame(words [3]uint32) { + d.frame.clear() + for index := 0; index < Width*Height; index++ { + if words[index/32]&(uint32(1)<<(31-index%32)) != 0 { + d.frame.set(index, true) + } + } +} diff --git a/unor4matrix/matrix_test.go b/unor4matrix/matrix_test.go new file mode 100644 index 000000000..33d4d8f29 --- /dev/null +++ b/unor4matrix/matrix_test.go @@ -0,0 +1,128 @@ +package unor4matrix + +import ( + "image/color" + "testing" + + "tinygo.org/x/drivers" +) + +var _ drivers.Displayer = (*Device)(nil) + +func TestClearDisplay(t *testing.T) { + var d Device + d.LoadFrame([3]uint32{0xffffffff, 0xffffffff, 0xffffffff}) + d.ClearDisplay() + if d.frame.pixels != ([framebufferSize]byte{}) { + t.Fatalf("pixels = %v", d.frame.pixels) + } +} + +func TestLoadFrame(t *testing.T) { + allOn := [12]byte{ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + } + tests := []struct { + name string + words [3]uint32 + want [12]byte + }{ + { + name: "blank replaces existing pixels", + }, + { + name: "all pixels", + words: [3]uint32{0xffffffff, 0xffffffff, 0xffffffff}, + want: allOn, + }, + { + name: "first pixel", + words: [3]uint32{0x80000000, 0, 0}, + want: [12]byte{0: 0x01}, + }, + { + name: "last bit of first word", + words: [3]uint32{0x00000001, 0, 0}, + want: [12]byte{3: 0x80}, + }, + { + name: "first bit of second word", + words: [3]uint32{0, 0x80000000, 0}, + want: [12]byte{4: 0x01}, + }, + { + name: "last bit of second word", + words: [3]uint32{0, 0x00000001, 0}, + want: [12]byte{7: 0x80}, + }, + { + name: "first bit of third word", + words: [3]uint32{0, 0, 0x80000000}, + want: [12]byte{8: 0x01}, + }, + { + name: "last pixel", + words: [3]uint32{0, 0, 0x00000001}, + want: [12]byte{11: 0x80}, + }, + { + name: "bit order within a byte", + words: [3]uint32{0xa1000000, 0, 0}, + want: [12]byte{0: 0x85}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var d Device + d.frame.pixels = allOn + + d.LoadFrame(tt.words) + if d.frame.pixels != tt.want { + t.Fatalf("drawing buffer = %v, want %v", d.frame.pixels, tt.want) + } + }) + } +} + +func TestSizeAndColors(t *testing.T) { + var d Device + if w, h := d.Size(); w != 12 || h != 8 { + t.Fatalf("size %d,%d", w, h) + } + for _, c := range []color.RGBA{{R: 1}, {G: 1}, {B: 1}, {R: 255, A: 0}} { + d.ClearDisplay() + d.SetPixel(11, 7, c) + if d.frame.pixels != ([framebufferSize]byte{11: 0x80}) { + t.Fatalf("color %v should be on", c) + } + } + d.SetPixel(11, 7, color.RGBA{A: 255}) + if d.frame.pixels != ([framebufferSize]byte{}) { + t.Fatal("black should be off") + } + before := d.frame + for _, xy := range [][2]int16{{-1, 0}, {0, -1}, {12, 0}, {0, 8}} { + d.SetPixel(xy[0], xy[1], color.RGBA{R: 255}) + } + if d.frame != before { + t.Fatal("out-of-bounds changed drawing buffer") + } +} + +func TestMapping(t *testing.T) { + if len(ledMapping) != 96 { + t.Fatal("mapping length", len(ledMapping)) + } + seen := map[pinPair]bool{} + for i, pair := range ledMapping { + if pair.anode >= 11 || pair.cathode >= 11 || pair.anode == pair.cathode || seen[pair] { + t.Fatalf("invalid/duplicate pair %d: %v", i, pair) + } + seen[pair] = true + } + if ledMapping[0] != (pinPair{7, 3}) || ledMapping[1] != (pinPair{3, 7}) || ledMapping[95] != (pinPair{9, 4}) { + t.Fatal("known source landmarks differ") + } +} diff --git a/unor4matrix/matrix_unor4.go b/unor4matrix/matrix_unor4.go new file mode 100644 index 000000000..5b14ecd24 --- /dev/null +++ b/unor4matrix/matrix_unor4.go @@ -0,0 +1,30 @@ +//go:build arduino_uno_r4_wifi + +package unor4matrix + +import "machine" + +// NewOnboard returns a device connected to the UNO R4 WiFi's built-in matrix. +func NewOnboard() Device { + // Matrix line order from the UNO R4 WiFi variant. + // https://github.com/arduino/ArduinoCore-renesas/blob/424e86eff92d37f72123c2b641dd8bbf06a38b47/variants/UNOWIFIR4/variant.cpp + lines := [...]machine.Pin{ + machine.P0_03, machine.P0_04, machine.P0_11, + machine.P0_12, machine.P0_13, machine.P0_15, + machine.P2_04, machine.P2_05, machine.P2_06, + machine.P2_12, machine.P2_13, + } + var pins [numPins]CharlieplexPin + for i, p := range lines { + pins[i] = CharlieplexPin{ + Set: func(level bool) { + // Preload PODR before enabling PDR to avoid driving the previous level. + // RA4M1 R01UH0887EJ0110, sections 19.2.1, 19.2.3, 19.2.5, pp. 357, 359, 361-362. + p.Set(level) + p.Configure(machine.PinConfig{Mode: machine.PinOutput}) + }, + Float: func() { p.Configure(machine.PinConfig{Mode: machine.PinInput}) }, + } + } + return New(pins) +} diff --git a/unor4matrix/pins.go b/unor4matrix/pins.go new file mode 100644 index 000000000..00211fc3c --- /dev/null +++ b/unor4matrix/pins.go @@ -0,0 +1,18 @@ +package unor4matrix + +import pin "tinygo.org/x/drivers/internal/pin" + +const numPins = 11 + +// CharlieplexPin can drive a line or put it in high-impedance input mode. +// Set and Float must not block or allocate. Float must also initialize the pin. +type CharlieplexPin struct { + Set pin.OutputFunc + Float func() +} + +// New returns a device for P0_03, P0_04, P0_11, P0_12, P0_13, P0_15, +// P2_04, P2_05, P2_06, P2_12 and P2_13, in that order. +func New(pins [numPins]CharlieplexPin) Device { + return Device{pins: pins} +} diff --git a/unor4matrix/scan_test.go b/unor4matrix/scan_test.go new file mode 100644 index 000000000..4f3100c8c --- /dev/null +++ b/unor4matrix/scan_test.go @@ -0,0 +1,119 @@ +package unor4matrix + +import ( + "errors" + "image/color" + "reflect" + "testing" + "time" +) + +type event struct { + line int + action string +} + +func traceDevice() (Device, *[]event) { + var pins [numPins]CharlieplexPin + events := new([]event) + for i := range pins { + pins[i] = CharlieplexPin{ + Set: func(level bool) { + action := "low" + if level { + action = "high" + } + *events = append(*events, event{i, action}) + }, + Float: func() { *events = append(*events, event{i, "float"}) }, + } + } + return New(pins), events +} + +func TestDisplayBeforeConfigure(t *testing.T) { + d, events := traceDevice() + d.LoadFrame([3]uint32{0x80000000, 0, 1}) + if err := d.Display(); !errors.Is(err, ErrNotConfigured) { + t.Fatalf("unconfigured Display = %v", err) + } + if len(*events) != 0 { + t.Fatal("unconfigured display touched GPIO") + } +} + +func TestDisplayScansCurrentPixelsAndBlanks(t *testing.T) { + d, events := traceDevice() + d.LoadFrame([3]uint32{0x80000000, 0, 1}) + if err := d.Configure(); err != nil { + t.Fatal(err) + } + var floats []event + for i := 0; i < 11; i++ { + floats = append(floats, event{i, "float"}) + } + if !reflect.DeepEqual(*events, floats) { + t.Fatalf("configure = %v", *events) + } + *events = nil + if err := d.Display(); err != nil { + t.Fatal(err) + } + want := []event{{3, "low"}, {7, "high"}, {7, "float"}, {3, "float"}, {4, "low"}, {9, "high"}, {9, "float"}, {4, "float"}} + if !reflect.DeepEqual(*events, want) { + t.Fatalf("display = %v, want %v", *events, want) + } + d.ClearDisplay() + d.SetPixel(1, 0, color.RGBA{R: 255}) + if err := d.Configure(); err != nil { + t.Fatal(err) + } + *events = nil + if err := d.Display(); err != nil { + t.Fatal(err) + } + want = []event{{7, "low"}, {3, "high"}, {3, "float"}, {7, "float"}} + if !reflect.DeepEqual(*events, want) { + t.Fatalf("next frame = %v, want %v", *events, want) + } +} + +func TestDarkFrameKeepsAllSlotDelays(t *testing.T) { + d, events := traceDevice() + if err := d.Configure(); err != nil { + t.Fatal(err) + } + *events = nil + start := time.Now() + if err := d.Display(); err != nil { + t.Fatal(err) + } + if elapsed := time.Since(start); elapsed < 96*100*time.Microsecond { + t.Fatalf("dark frame returned too early: %v", elapsed) + } + if len(*events) != 0 { + t.Fatalf("dark frame drove GPIO: %v", *events) + } +} + +func TestIncompletePinsDoNotTouchHardware(t *testing.T) { + for _, missingSet := range []bool{true, false} { + var pins [numPins]CharlieplexPin + touched := false + for i := range pins { + pins[i] = CharlieplexPin{Set: func(bool) { touched = true }, Float: func() { touched = true }} + } + if missingSet { + pins[10].Set = nil + } else { + pins[10].Float = nil + } + d := New(pins) + if err := d.Configure(); !errors.Is(err, ErrMissingPinFunc) { + t.Fatalf("configure = %v", err) + } + if touched { + t.Fatal("invalid device touched GPIO") + } + } +}