mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-16 12:53:23 +00:00
unoqmatrix: LED matrix on the Arduino Uno Q
Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"machine"
|
||||||
|
|
||||||
|
"image/color"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"tinygo.org/x/drivers/unoqmatrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var on = color.RGBA{255, 255, 255, 255}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
display := unoqmatrix.NewFromBasePin(machine.PF0)
|
||||||
|
display.ClearDisplay()
|
||||||
|
|
||||||
|
w, h := display.Size()
|
||||||
|
x := int16(0)
|
||||||
|
y := int16(0)
|
||||||
|
deltaX := int16(1)
|
||||||
|
deltaY := int16(1)
|
||||||
|
|
||||||
|
for {
|
||||||
|
pixel := display.GetPixel(x, y)
|
||||||
|
if pixel.R != 0 || pixel.G != 0 || pixel.B != 0 {
|
||||||
|
display.ClearDisplay()
|
||||||
|
x = 1 + int16(rand.Int31n(3))
|
||||||
|
y = 1 + int16(rand.Int31n(3))
|
||||||
|
deltaX = 1
|
||||||
|
deltaY = 1
|
||||||
|
if rand.Int31n(2) == 0 {
|
||||||
|
deltaX = -1
|
||||||
|
}
|
||||||
|
if rand.Int31n(2) == 0 {
|
||||||
|
deltaY = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display.SetPixel(x, y, on)
|
||||||
|
|
||||||
|
x += deltaX
|
||||||
|
y += deltaY
|
||||||
|
|
||||||
|
if x == 0 || x == w-1 {
|
||||||
|
deltaX = -deltaX
|
||||||
|
}
|
||||||
|
|
||||||
|
if y == 0 || y == h-1 {
|
||||||
|
deltaY = -deltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
display.Display()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
// Package unoqmatrix provides a driver for the UnoQMatrix LED matrix display.
|
||||||
|
//
|
||||||
|
// The UnoQMatrix is an 8x13 LED matrix display that can be controlled using a single pin.
|
||||||
|
// It uses a multiplexing technique to control the LEDs, which allows for a large number of LEDs to be controlled with fewer pins.
|
||||||
|
//
|
||||||
|
// This driver provides basic functionality to set individual pixels, clear the display, and refresh the display.
|
||||||
|
//
|
||||||
|
// Note: The UnoQMatrix does not support brightness control or color depth. Each pixel can only be turned on or off.
|
||||||
|
// Could it suppport brightness control by using PWM on the pin? To be investigated.
|
||||||
|
package unoqmatrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pin "tinygo.org/x/drivers/internal/pin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// Rotation of the LED matrix.
|
||||||
|
Rotation uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid values:
|
||||||
|
//
|
||||||
|
// 0: regular orientation, (0 degree rotation)
|
||||||
|
// 1: 90 degree rotation clockwise
|
||||||
|
// 2: 180 degree rotation clockwise
|
||||||
|
// 3: 270 degree rotation clockwise
|
||||||
|
const (
|
||||||
|
RotationNormal = 0
|
||||||
|
Rotation90 = 1
|
||||||
|
Rotation180 = 2
|
||||||
|
Rotation270 = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ledRows = 8
|
||||||
|
ledCols = 13
|
||||||
|
|
||||||
|
pixelRefreshDelay = 10 * time.Microsecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// CharlieplexPin represents a pin used for charlieplexing.
|
||||||
|
// It must be able to drive high/low (output mode) and float (high-impedance/input mode).
|
||||||
|
//
|
||||||
|
// Example construction from a machine.Pin using the pin HAL pattern:
|
||||||
|
//
|
||||||
|
// var isOutput bool
|
||||||
|
// cp := unoqmatrix.CharlieplexPin{
|
||||||
|
// Set: pin.OutputFunc(func(level bool) {
|
||||||
|
// if !isOutput {
|
||||||
|
// p.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
|
// isOutput = true
|
||||||
|
// }
|
||||||
|
// p.Set(level)
|
||||||
|
// }),
|
||||||
|
// Float: func() {
|
||||||
|
// if isOutput {
|
||||||
|
// p.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||||
|
// isOutput = false
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
type CharlieplexPin struct {
|
||||||
|
Set pin.OutputFunc // Drive pin high (true) or low (false); auto-configures to output mode.
|
||||||
|
Float func() // Put pin into high-impedance (input) mode.
|
||||||
|
}
|
||||||
|
|
||||||
|
const numPins = 11
|
||||||
|
|
||||||
|
// Device represents the UnoQMatrix LED matrix display.
|
||||||
|
type Device struct {
|
||||||
|
pins [numPins]CharlieplexPin
|
||||||
|
buffer [ledRows][ledCols]color.RGBA
|
||||||
|
rotation uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new unoqmatrix driver.
|
||||||
|
// The provided pins are the 11 charlieplex pins used to control the LED matrix.
|
||||||
|
func New(pins [numPins]CharlieplexPin) Device {
|
||||||
|
return Device{pins: pins}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure sets up the device.
|
||||||
|
func (d *Device) Configure(cfg Config) {
|
||||||
|
d.SetRotation(cfg.Rotation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRotation changes the rotation of the LED matrix.
|
||||||
|
//
|
||||||
|
// Valid values for rotation:
|
||||||
|
//
|
||||||
|
// 0: regular orientation, (0 degree rotation)
|
||||||
|
// 1: 90 degree rotation clockwise
|
||||||
|
// 2: 180 degree rotation clockwise
|
||||||
|
// 3: 270 degree rotation clockwise
|
||||||
|
func (d *Device) SetRotation(rotation uint8) {
|
||||||
|
d.rotation = rotation % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPixel sets the color of a specific pixel.
|
||||||
|
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
|
||||||
|
d.buffer[y][x] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPixel returns the color of a specific pixel.
|
||||||
|
func (d *Device) GetPixel(x int16, y int16) color.RGBA {
|
||||||
|
return d.buffer[y][x]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display sends the buffer (if any) to the screen.
|
||||||
|
// Only lights active (non-black) pixels, and resets only the 2 previously
|
||||||
|
// driven pins between LEDs instead of all 11, making each refresh cycle
|
||||||
|
// proportional to the number of lit LEDs.
|
||||||
|
func (d *Device) Display() error {
|
||||||
|
d.clearDisplay()
|
||||||
|
|
||||||
|
var lastIdx0, lastIdx1 uint8
|
||||||
|
hasLast := false
|
||||||
|
|
||||||
|
for row := 0; row < ledRows; row++ {
|
||||||
|
for col := 0; col < ledCols; col++ {
|
||||||
|
c := d.buffer[row][col]
|
||||||
|
if c.R == 0 && c.G == 0 && c.B == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := row*ledCols + col
|
||||||
|
if idx < 0 || idx >= len(pinMapping) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float only the two pins that were driving the previous LED.
|
||||||
|
if hasLast {
|
||||||
|
d.pins[lastIdx0].Float()
|
||||||
|
d.pins[lastIdx1].Float()
|
||||||
|
}
|
||||||
|
hasLast = true
|
||||||
|
|
||||||
|
idx0 := pinMapping[idx][0]
|
||||||
|
idx1 := pinMapping[idx][1]
|
||||||
|
d.pins[idx0].Set.High()
|
||||||
|
d.pins[idx1].Set.Low()
|
||||||
|
lastIdx0 = idx0
|
||||||
|
lastIdx1 = idx1
|
||||||
|
|
||||||
|
time.Sleep(pixelRefreshDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float the last driven LED.
|
||||||
|
if hasLast {
|
||||||
|
d.pins[lastIdx0].Float()
|
||||||
|
d.pins[lastIdx1].Float()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearDisplay turns off all the LEDs on the display.
|
||||||
|
func (d *Device) ClearDisplay() {
|
||||||
|
for row := 0; row < ledRows; row++ {
|
||||||
|
for col := 0; col < ledCols; col++ {
|
||||||
|
d.buffer[row][col] = color.RGBA{0, 0, 0, 255}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the current size of the display.
|
||||||
|
func (d *Device) Size() (w, h int16) {
|
||||||
|
return ledCols, ledRows
|
||||||
|
}
|
||||||
|
|
||||||
|
// pinMapping defines the mapping of LED indices to pin pairs. Each entry corresponds
|
||||||
|
// to an LED index (0-104) and contains the two pin numbers that need to be set to turn on that LED.
|
||||||
|
// based on https://github.com/arduino/ArduinoCore-zephyr/blob/main/loader/matrix.inc#L13
|
||||||
|
var pinMapping = [][2]uint8{
|
||||||
|
{0, 1}, // 0
|
||||||
|
{1, 0},
|
||||||
|
{0, 2},
|
||||||
|
{2, 0},
|
||||||
|
{1, 2},
|
||||||
|
{2, 1},
|
||||||
|
{0, 3},
|
||||||
|
{3, 0},
|
||||||
|
{1, 3},
|
||||||
|
{3, 1},
|
||||||
|
{2, 3}, // 10
|
||||||
|
{3, 2},
|
||||||
|
{0, 4},
|
||||||
|
{4, 0},
|
||||||
|
{1, 4},
|
||||||
|
{4, 1},
|
||||||
|
{2, 4},
|
||||||
|
{4, 2},
|
||||||
|
{3, 4},
|
||||||
|
{4, 3},
|
||||||
|
{0, 5}, // 20
|
||||||
|
{5, 0},
|
||||||
|
{1, 5},
|
||||||
|
{5, 1},
|
||||||
|
{2, 5},
|
||||||
|
{5, 2},
|
||||||
|
{3, 5},
|
||||||
|
{5, 3},
|
||||||
|
{4, 5},
|
||||||
|
{5, 4},
|
||||||
|
{0, 6}, // 30
|
||||||
|
{6, 0},
|
||||||
|
{1, 6},
|
||||||
|
{6, 1},
|
||||||
|
{2, 6},
|
||||||
|
{6, 2},
|
||||||
|
{3, 6},
|
||||||
|
{6, 3},
|
||||||
|
{4, 6},
|
||||||
|
{6, 4},
|
||||||
|
{5, 6}, // 40
|
||||||
|
{6, 5},
|
||||||
|
{0, 7},
|
||||||
|
{7, 0},
|
||||||
|
{1, 7},
|
||||||
|
{7, 1},
|
||||||
|
{2, 7},
|
||||||
|
{7, 2},
|
||||||
|
{3, 7},
|
||||||
|
{7, 3},
|
||||||
|
{4, 7}, // 50
|
||||||
|
{7, 4},
|
||||||
|
{5, 7},
|
||||||
|
{7, 5},
|
||||||
|
{6, 7},
|
||||||
|
{7, 6},
|
||||||
|
{0, 8},
|
||||||
|
{8, 0},
|
||||||
|
{1, 8},
|
||||||
|
{8, 1},
|
||||||
|
{2, 8}, // 60
|
||||||
|
{8, 2},
|
||||||
|
{3, 8},
|
||||||
|
{8, 3},
|
||||||
|
{4, 8},
|
||||||
|
{8, 4},
|
||||||
|
{5, 8},
|
||||||
|
{8, 5},
|
||||||
|
{6, 8},
|
||||||
|
{8, 6},
|
||||||
|
{7, 8}, // 70
|
||||||
|
{8, 7},
|
||||||
|
{0, 9},
|
||||||
|
{9, 0},
|
||||||
|
{1, 9},
|
||||||
|
{9, 1},
|
||||||
|
{2, 9},
|
||||||
|
{9, 2},
|
||||||
|
{3, 9},
|
||||||
|
{9, 3},
|
||||||
|
{4, 9}, // 80
|
||||||
|
{9, 4},
|
||||||
|
{5, 9},
|
||||||
|
{9, 5},
|
||||||
|
{6, 9},
|
||||||
|
{9, 6},
|
||||||
|
{7, 9},
|
||||||
|
{9, 7},
|
||||||
|
{8, 9},
|
||||||
|
{9, 8},
|
||||||
|
{0, 10}, // 90
|
||||||
|
{10, 0},
|
||||||
|
{1, 10},
|
||||||
|
{10, 1},
|
||||||
|
{2, 10},
|
||||||
|
{10, 2},
|
||||||
|
{3, 10},
|
||||||
|
{10, 3},
|
||||||
|
{4, 10},
|
||||||
|
{10, 4},
|
||||||
|
{5, 10}, // 100
|
||||||
|
{10, 5},
|
||||||
|
{6, 10},
|
||||||
|
{10, 6},
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearDisplay turns off all the LEDs on the display by floating all pins.
|
||||||
|
func (d *Device) clearDisplay() {
|
||||||
|
for i := range d.pins {
|
||||||
|
d.pins[i].Float()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
package unoqmatrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
pin "tinygo.org/x/drivers/internal/pin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pinState tracks the state of a mock charlieplex pin.
|
||||||
|
type pinState struct {
|
||||||
|
level bool // true=high, false=low
|
||||||
|
isOutput bool // true=output mode, false=floating (high-Z)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockPins creates 11 mock CharlieplexPins and returns them along with their observable state.
|
||||||
|
func mockPins() ([numPins]CharlieplexPin, *[numPins]pinState) {
|
||||||
|
var pins [numPins]CharlieplexPin
|
||||||
|
var states [numPins]pinState
|
||||||
|
for i := range pins {
|
||||||
|
idx := i // capture
|
||||||
|
pins[i] = CharlieplexPin{
|
||||||
|
Set: pin.OutputFunc(func(level bool) {
|
||||||
|
states[idx].isOutput = true
|
||||||
|
states[idx].level = level
|
||||||
|
}),
|
||||||
|
Float: func() {
|
||||||
|
states[idx].isOutput = false
|
||||||
|
states[idx].level = false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pins, &states
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestDevice() (Device, *[numPins]pinState) {
|
||||||
|
pins, states := mockPins()
|
||||||
|
d := New(pins)
|
||||||
|
return d, states
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
d, _ := newTestDevice()
|
||||||
|
w, h := d.Size()
|
||||||
|
if w != ledCols || h != ledRows {
|
||||||
|
t.Errorf("Size() = (%d, %d), want (%d, %d)", w, h, ledCols, ledRows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSize(t *testing.T) {
|
||||||
|
d, _ := newTestDevice()
|
||||||
|
w, h := d.Size()
|
||||||
|
if w != 13 {
|
||||||
|
t.Errorf("width = %d, want 13", w)
|
||||||
|
}
|
||||||
|
if h != 8 {
|
||||||
|
t.Errorf("height = %d, want 8", h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetGetPixel(t *testing.T) {
|
||||||
|
d, _ := newTestDevice()
|
||||||
|
c := color.RGBA{R: 255, G: 128, B: 64, A: 255}
|
||||||
|
|
||||||
|
d.SetPixel(3, 2, c)
|
||||||
|
got := d.GetPixel(3, 2)
|
||||||
|
if got != c {
|
||||||
|
t.Errorf("GetPixel(3,2) = %v, want %v", got, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset pixel should be zero-value.
|
||||||
|
got = d.GetPixel(0, 0)
|
||||||
|
if got != (color.RGBA{}) {
|
||||||
|
t.Errorf("GetPixel(0,0) = %v, want zero", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClearDisplay(t *testing.T) {
|
||||||
|
d, _ := newTestDevice()
|
||||||
|
on := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||||
|
off := color.RGBA{A: 255}
|
||||||
|
|
||||||
|
d.SetPixel(0, 0, on)
|
||||||
|
d.SetPixel(5, 3, on)
|
||||||
|
d.ClearDisplay()
|
||||||
|
|
||||||
|
for y := int16(0); y < ledRows; y++ {
|
||||||
|
for x := int16(0); x < ledCols; x++ {
|
||||||
|
got := d.GetPixel(x, y)
|
||||||
|
if got != off {
|
||||||
|
t.Errorf("after ClearDisplay, GetPixel(%d,%d) = %v, want %v", x, y, got, off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetRotation(t *testing.T) {
|
||||||
|
d, _ := newTestDevice()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input uint8
|
||||||
|
want uint8
|
||||||
|
}{
|
||||||
|
{0, 0},
|
||||||
|
{1, 1},
|
||||||
|
{2, 2},
|
||||||
|
{3, 3},
|
||||||
|
{4, 0}, // wraps
|
||||||
|
{7, 3}, // wraps
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
d.SetRotation(tt.input)
|
||||||
|
if d.rotation != tt.want {
|
||||||
|
t.Errorf("SetRotation(%d): rotation = %d, want %d", tt.input, d.rotation, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigure(t *testing.T) {
|
||||||
|
d, _ := newTestDevice()
|
||||||
|
d.Configure(Config{Rotation: 2})
|
||||||
|
if d.rotation != 2 {
|
||||||
|
t.Errorf("Configure(Rotation:2): rotation = %d, want 2", d.rotation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplayEmptyBuffer(t *testing.T) {
|
||||||
|
d, states := newTestDevice()
|
||||||
|
|
||||||
|
err := d.Display()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Display() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All pins should be floating after displaying an empty buffer.
|
||||||
|
for i, s := range states {
|
||||||
|
if s.isOutput {
|
||||||
|
t.Errorf("pin %d still in output mode after empty Display()", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplaySinglePixel(t *testing.T) {
|
||||||
|
d, states := newTestDevice()
|
||||||
|
on := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||||
|
|
||||||
|
// LED index 0 -> pinMapping[0] = {0, 1}: pin 0 high, pin 1 low.
|
||||||
|
d.SetPixel(0, 0, on)
|
||||||
|
err := d.Display()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Display() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After Display completes, all pins should be floating (last LED turned off).
|
||||||
|
for i, s := range states {
|
||||||
|
if s.isOutput {
|
||||||
|
t.Errorf("pin %d still in output mode after Display()", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplayMultiplePixels(t *testing.T) {
|
||||||
|
d, states := newTestDevice()
|
||||||
|
on := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||||
|
|
||||||
|
d.SetPixel(0, 0, on) // idx 0 -> pins {0,1}
|
||||||
|
d.SetPixel(1, 0, on) // idx 1 -> pins {1,0}
|
||||||
|
d.SetPixel(2, 0, on) // idx 2 -> pins {0,2}
|
||||||
|
|
||||||
|
err := d.Display()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Display() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All pins floating after display completes.
|
||||||
|
for i, s := range states {
|
||||||
|
if s.isOutput {
|
||||||
|
t.Errorf("pin %d still in output mode after Display()", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pinEvent records a single pin action during Display().
|
||||||
|
type pinEvent struct {
|
||||||
|
pinIdx int
|
||||||
|
action string // "high", "low", or "float"
|
||||||
|
}
|
||||||
|
|
||||||
|
// traceDevice creates a device that records every pin event for verification.
|
||||||
|
func traceDevice() (Device, *[]pinEvent) {
|
||||||
|
var pins [numPins]CharlieplexPin
|
||||||
|
events := &[]pinEvent{}
|
||||||
|
for i := range pins {
|
||||||
|
idx := i
|
||||||
|
pins[i] = CharlieplexPin{
|
||||||
|
Set: pin.OutputFunc(func(level bool) {
|
||||||
|
action := "low"
|
||||||
|
if level {
|
||||||
|
action = "high"
|
||||||
|
}
|
||||||
|
*events = append(*events, pinEvent{pinIdx: idx, action: action})
|
||||||
|
}),
|
||||||
|
Float: func() {
|
||||||
|
*events = append(*events, pinEvent{pinIdx: idx, action: "float"})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d := New(pins)
|
||||||
|
return d, events
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplayDrivesCorrectPins(t *testing.T) {
|
||||||
|
d, events := traceDevice()
|
||||||
|
on := color.RGBA{R: 255, A: 255}
|
||||||
|
|
||||||
|
// Set pixel at (0,0) -> LED index 0 -> pinMapping[0] = {0, 1}.
|
||||||
|
d.SetPixel(0, 0, on)
|
||||||
|
d.Display()
|
||||||
|
|
||||||
|
// Expected sequence:
|
||||||
|
// 1. clearDisplay: float pins 0..10
|
||||||
|
// 2. Drive LED 0: pin 0 high, pin 1 low
|
||||||
|
// 3. Cleanup: float pin 0, float pin 1
|
||||||
|
|
||||||
|
// Find the high/low events (skip initial floats from clearDisplay).
|
||||||
|
var driveEvents []pinEvent
|
||||||
|
for _, e := range *events {
|
||||||
|
if e.action == "high" || e.action == "low" {
|
||||||
|
driveEvents = append(driveEvents, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(driveEvents) != 2 {
|
||||||
|
t.Fatalf("expected 2 drive events, got %d: %v", len(driveEvents), driveEvents)
|
||||||
|
}
|
||||||
|
if driveEvents[0].pinIdx != 0 || driveEvents[0].action != "high" {
|
||||||
|
t.Errorf("first drive event = %v, want pin 0 high", driveEvents[0])
|
||||||
|
}
|
||||||
|
if driveEvents[1].pinIdx != 1 || driveEvents[1].action != "low" {
|
||||||
|
t.Errorf("second drive event = %v, want pin 1 low", driveEvents[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplaySkipsBlackPixels(t *testing.T) {
|
||||||
|
d, events := traceDevice()
|
||||||
|
on := color.RGBA{R: 255, A: 255}
|
||||||
|
|
||||||
|
// Only set one pixel in the middle of the matrix.
|
||||||
|
d.SetPixel(4, 1, on) // idx = 1*13+4 = 17 -> pinMapping[17] = {4,2}
|
||||||
|
d.Display()
|
||||||
|
|
||||||
|
var driveEvents []pinEvent
|
||||||
|
for _, e := range *events {
|
||||||
|
if e.action == "high" || e.action == "low" {
|
||||||
|
driveEvents = append(driveEvents, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should only drive one LED's worth of pin events.
|
||||||
|
if len(driveEvents) != 2 {
|
||||||
|
t.Fatalf("expected 2 drive events for 1 lit pixel, got %d", len(driveEvents))
|
||||||
|
}
|
||||||
|
if driveEvents[0].pinIdx != 4 || driveEvents[0].action != "high" {
|
||||||
|
t.Errorf("expected pin 4 high, got %v", driveEvents[0])
|
||||||
|
}
|
||||||
|
if driveEvents[1].pinIdx != 2 || driveEvents[1].action != "low" {
|
||||||
|
t.Errorf("expected pin 2 low, got %v", driveEvents[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisplayFloatsBetweenLEDs(t *testing.T) {
|
||||||
|
d, events := traceDevice()
|
||||||
|
on := color.RGBA{R: 255, A: 255}
|
||||||
|
|
||||||
|
d.SetPixel(0, 0, on) // idx 0 -> {0,1}
|
||||||
|
d.SetPixel(1, 0, on) // idx 1 -> {1,0}
|
||||||
|
d.Display()
|
||||||
|
|
||||||
|
// After the initial clearDisplay floats, the sequence for two LEDs should be:
|
||||||
|
// drive LED0 (pin0 high, pin1 low)
|
||||||
|
// float pin0, float pin1 (between LEDs)
|
||||||
|
// drive LED1 (pin1 high, pin0 low)
|
||||||
|
// float pin1, float pin0 (cleanup)
|
||||||
|
|
||||||
|
// Skip the initial 11 float events from clearDisplay.
|
||||||
|
postClear := (*events)[numPins:]
|
||||||
|
|
||||||
|
// Verify pin 0 and 1 are floated between the two LEDs.
|
||||||
|
foundFloatBetween := false
|
||||||
|
driveCount := 0
|
||||||
|
for _, e := range postClear {
|
||||||
|
if e.action == "high" || e.action == "low" {
|
||||||
|
driveCount++
|
||||||
|
}
|
||||||
|
// After the first pair of drive events, we should see floats before the next pair.
|
||||||
|
if driveCount == 2 && e.action == "float" {
|
||||||
|
foundFloatBetween = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundFloatBetween {
|
||||||
|
t.Error("expected float events between LED drives, found none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPinMappingLength(t *testing.T) {
|
||||||
|
expected := 104 // 8x13 matrix = 104 LEDs
|
||||||
|
if len(pinMapping) != expected {
|
||||||
|
t.Errorf("pinMapping has %d entries, want %d", len(pinMapping), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPinMappingIndicesInRange(t *testing.T) {
|
||||||
|
for i, pair := range pinMapping {
|
||||||
|
if pair[0] >= numPins {
|
||||||
|
t.Errorf("pinMapping[%d][0] = %d, exceeds numPins (%d)", i, pair[0], numPins)
|
||||||
|
}
|
||||||
|
if pair[1] >= numPins {
|
||||||
|
t.Errorf("pinMapping[%d][1] = %d, exceeds numPins (%d)", i, pair[1], numPins)
|
||||||
|
}
|
||||||
|
if pair[0] == pair[1] {
|
||||||
|
t.Errorf("pinMapping[%d] has same pin for both: %d", i, pair[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
//go:build baremetal
|
||||||
|
|
||||||
|
package unoqmatrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"machine"
|
||||||
|
|
||||||
|
pin "tinygo.org/x/drivers/internal/pin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewFromBasePin creates a Device from a base machine.Pin.
|
||||||
|
// It constructs 11 CharlieplexPin values from consecutive pins starting at basePin.
|
||||||
|
// Each pin lazily switches between output and input mode as needed.
|
||||||
|
func NewFromBasePin(basePin machine.Pin) Device {
|
||||||
|
var pins [numPins]CharlieplexPin
|
||||||
|
for i := range pins {
|
||||||
|
p := basePin + machine.Pin(i)
|
||||||
|
var isOutput bool
|
||||||
|
pins[i] = CharlieplexPin{
|
||||||
|
Set: pin.OutputFunc(func(level bool) {
|
||||||
|
if !isOutput {
|
||||||
|
p.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||||
|
isOutput = true
|
||||||
|
}
|
||||||
|
p.Set(level)
|
||||||
|
}),
|
||||||
|
Float: func() {
|
||||||
|
if isOutput {
|
||||||
|
p.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||||
|
isOutput = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return New(pins)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user