Max7219 LED Driver (#79)

pull/81/head
gsexton 2 years ago committed by GitHub
parent 9ab07ba9a6
commit c0037af197
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,38 @@
# Max7219 7-Segment/Matrix LED Driver
## Introduction
The Maxim 7219 LED driver is an SPI chip that simplifies driving 7-Segment LED
displays. It's also commonly used to create 8x8 LED matrixes. The datasheet
is available at:
https://www.analog.com/media/en/technical-documentation/data-sheets/MAX7219-MAX7221.pdf
You can find 8 digit, 7-segment LED boards on Ebay for $1-2. You can find
inexpensive matrixes in various sizes from 1 - 8 segments as well.
For matrixes, the driver provides a basic CP437 font that can be used to
display characters on 8x8 matrixes. If desired, you can supply your own glyph
set, or special characters for your specific application.
## Driver Functions
This driver provides simplified handling for using either 7-segment numeric
displays, or 8x8 matrixes.
It provides methods for scrolling data across either one or multiple displays.
For example, you can pass an IP address of "10.100.10.11" to the scroll function
and it will automatically scroll the display a desired number of times. The
scroll feature also works with matrixes, and scrolls characters one LED column
at a time for a smooth, even display.
## Notes About Daisy-Chaining
The Max7219 is specifically designed to handle larger displays by daisy chaining
units together. Say you want to write to digit 8 on the 3rd unit chained
together. You would make one SPI write. The first write would be the register
number (8), and the data value. Next, you would write a NOOP. The first record
would then be shifted to the second device. Finally, you write another NOOP. The
first record is shifted from the second device to the 3rd device, the first NOOP is
shifted to the second device. When the ChipSelect line goes low, each unit applies
the last data it received.

@ -0,0 +1,76 @@
// Copyright 2024 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package max7219_test
import (
"fmt"
"log"
"time"
"periph.io/x/conn/v3/spi/spireg"
"periph.io/x/devices/v3/max7219"
"periph.io/x/host/v3"
)
// basic test program. To do a numeric display, set matrix to false and
// matrixUnits to 1.
func Example() {
// basic test program. To do a numeric display, set matrix to false and
// matrixUnits to 1.
matrix := false
matrixUnits := 1
if _, err := host.Init(); err != nil {
log.Fatal(err)
}
s, err := spireg.Open("")
if err != nil {
log.Fatal(err)
}
defer s.Close()
dev, err := max7219.NewSPI(s, matrixUnits, 8)
if err != nil {
log.Fatal(err)
}
_ = dev.TestDisplay(true)
time.Sleep(time.Second * 1)
_ = dev.TestDisplay(false)
_ = dev.SetIntensity(1)
_ = dev.Clear()
if matrix {
dev.SetGlyphs(max7219.CP437Glyphs, true)
dev.SetDecode(max7219.DecodeNone)
dev.ScrollChars([]byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"), 1, 100*time.Millisecond)
} else {
dev.SetDecode(max7219.DecodeB)
}
for i := -128; i < 128; i++ {
dev.WriteInt(i)
time.Sleep(100 * time.Millisecond)
}
var tData []byte
// Continuously display a clock
for {
t := time.Now()
if matrix {
// Assumes a 4 unit matrix
tData = []byte(fmt.Sprintf("%2d%02d", t.Hour(), t.Minute()))
} else {
// 8 digit 7-segment LED display
tData = []byte(t.Format(time.TimeOnly))
tData[2] = max7219.ClearDigit
tData[5] = max7219.ClearDigit
}
_ = dev.Write(tData)
// Try to get the iteration exactly on time. FWIW, on a Pi Zero this loop
// executes in ~ 2-3ms.
dNext := time.Duration(1000-(t.UnixMilli()%1000)) * time.Millisecond
time.Sleep(dNext)
}
}

@ -0,0 +1,284 @@
// Copyright 2024 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package max7219
// A really basic 8x8 raster font in CP437. Used to display characters on matrix
// type displays.
//
// Refer to: https://www.ascii-codes.com/
var CP437Glyphs = [][]byte{
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00
{0x7e, 0x81, 0xa5, 0x81, 0x9d, 0xb9, 0x81, 0x7e}, // 0x01
{0x7e, 0xff, 0xdb, 0xff, 0xe3, 0xc7, 0xff, 0x7e}, // 0x02
{0x6c, 0xfe, 0xfe, 0xfe, 0x7c, 0x38, 0x10, 0x00}, // 0x03
{0x10, 0x38, 0x7c, 0xfe, 0x7c, 0x38, 0x10, 0x00}, // 0x04
{0x38, 0x7c, 0x38, 0xfe, 0xfe, 0x10, 0x10, 0x7c}, // 0x05
{0x00, 0x18, 0x3c, 0x7e, 0xff, 0x7e, 0x18, 0x7e}, // 0x06
{0x00, 0x00, 0x18, 0x3c, 0x3c, 0x18, 0x00, 0x00}, // 0x07
{0xff, 0xff, 0xe7, 0xc3, 0xc3, 0xe7, 0xff, 0xff}, // 0x08
{0x00, 0x3c, 0x66, 0x42, 0x42, 0x66, 0x3c, 0x00}, // 0x09
{0xff, 0xc3, 0x99, 0xbd, 0xbd, 0x99, 0xc3, 0xff}, // 0x0a
{0x0f, 0x07, 0x0f, 0x7d, 0xcc, 0xcc, 0xcc, 0x78}, // 0x0b
{0x3c, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x7e, 0x18}, // 0x0c
{0x3f, 0x33, 0x3f, 0x30, 0x30, 0x70, 0xf0, 0xe0}, // 0x0d
{0x7f, 0x63, 0x7f, 0x63, 0x63, 0x67, 0xe6, 0xc0}, // 0x0e
{0x99, 0x5a, 0x3c, 0xe7, 0xe7, 0x3c, 0x5a, 0x99}, // 0x0f
{0x80, 0xe0, 0xf8, 0xfe, 0xf8, 0xe0, 0x80, 0x00}, // 0x10
{0x02, 0x0e, 0x3e, 0xfe, 0x3e, 0x0e, 0x02, 0x00}, // 0x11
{0x18, 0x3c, 0x7e, 0x18, 0x18, 0x7e, 0x3c, 0x18}, // 0x12
{0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x66, 0x00}, // 0x13
{0x7f, 0xdb, 0xdb, 0x7b, 0x1b, 0x1b, 0x1b, 0x00}, // 0x14
{0x3f, 0x60, 0x7c, 0x66, 0x66, 0x3e, 0x06, 0xfc}, // 0x15
{0x00, 0x00, 0x00, 0x00, 0x7e, 0x7e, 0x7e, 0x00}, // 0x16
{0x18, 0x3c, 0x7e, 0x18, 0x7e, 0x3c, 0x18, 0xff}, // 0x17
{0x18, 0x3c, 0x7e, 0x18, 0x18, 0x18, 0x18, 0x00}, // 0x18
{0x18, 0x18, 0x18, 0x18, 0x7e, 0x3c, 0x18, 0x00}, // 0x19
{0x00, 0x18, 0x0c, 0xfe, 0x0c, 0x18, 0x00, 0x00}, // 0x1a
{0x00, 0x30, 0x60, 0xfe, 0x60, 0x30, 0x00, 0x00}, // 0x1b
{0x00, 0x00, 0xc0, 0xc0, 0xc0, 0xfe, 0x00, 0x00}, // 0x1c
{0x00, 0x24, 0x66, 0xff, 0x66, 0x24, 0x00, 0x00}, // 0x1d
{0x00, 0x18, 0x3c, 0x7e, 0xff, 0xff, 0x00, 0x00}, // 0x1e
{0x00, 0xff, 0xff, 0x7e, 0x3c, 0x18, 0x00, 0x00}, // 0x1f
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x20
{0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00}, // 0x21
{0x6c, 0x6c, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x22
{0x6c, 0x6c, 0xfe, 0x6c, 0xfe, 0x6c, 0x6c, 0x00}, // 0x23
{0x18, 0x7e, 0xc0, 0x7c, 0x06, 0xfc, 0x18, 0x00}, // 0x24
{0x00, 0xc6, 0xcc, 0x18, 0x30, 0x66, 0xc6, 0x00}, // 0x25
{0x38, 0x6c, 0x38, 0x76, 0xdc, 0xcc, 0x76, 0x00}, // 0x26
{0x30, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x27
{0x0c, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0c, 0x00}, // 0x28
{0x30, 0x18, 0x0c, 0x0c, 0x0c, 0x18, 0x30, 0x00}, // 0x29
{0x00, 0x66, 0x3c, 0xff, 0x3c, 0x66, 0x00, 0x00}, // 0x2a
{0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00}, // 0x2b
{0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30}, // 0x2c
{0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00}, // 0x2d
{0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00}, // 0x2e
{0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x80, 0x00}, // 0x2f
{0x7c, 0xce, 0xde, 0xf6, 0xe6, 0xc6, 0x7c, 0x00}, // 0x30
{0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00}, // 0x31
{0x7c, 0xc6, 0x06, 0x7c, 0xc0, 0xc0, 0xfe, 0x00}, // 0x32
{0xfc, 0x06, 0x06, 0x3c, 0x06, 0x06, 0xfc, 0x00}, // 0x33
{0x0c, 0xcc, 0xcc, 0xcc, 0xfe, 0x0c, 0x0c, 0x00}, // 0x34
{0xfe, 0xc0, 0xfc, 0x06, 0x06, 0xc6, 0x7c, 0x00}, // 0x35
{0x7c, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0x7c, 0x00}, // 0x36
{0xfe, 0x06, 0x06, 0x0c, 0x18, 0x30, 0x30, 0x00}, // 0x37
{0x7c, 0xc6, 0xc6, 0x7c, 0xc6, 0xc6, 0x7c, 0x00}, // 0x38
{0x7c, 0xc6, 0xc6, 0x7e, 0x06, 0x06, 0x7c, 0x00}, // 0x39
{0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00}, // 0x3a
{0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x30}, // 0x3b
{0x0c, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0c, 0x00}, // 0x3c
{0x00, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x00, 0x00}, // 0x3d
{0x30, 0x18, 0x0c, 0x06, 0x0c, 0x18, 0x30, 0x00}, // 0x3e
{0x3c, 0x66, 0x0c, 0x18, 0x18, 0x00, 0x18, 0x00}, // 0x3f
{0x7c, 0xc6, 0xde, 0xde, 0xde, 0xc0, 0x7e, 0x00}, // 0x40
{0x38, 0x6c, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0x00}, // 0x41
{0xfc, 0xc6, 0xc6, 0xfc, 0xc6, 0xc6, 0xfc, 0x00}, // 0x42
{0x7c, 0xc6, 0xc0, 0xc0, 0xc0, 0xc6, 0x7c, 0x00}, // 0x43
{0xf8, 0xcc, 0xc6, 0xc6, 0xc6, 0xcc, 0xf8, 0x00}, // 0x44
{0xfe, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xfe, 0x00}, // 0x45
{0xfe, 0xc0, 0xc0, 0xf8, 0xc0, 0xc0, 0xc0, 0x00}, // 0x46
{0x7c, 0xc6, 0xc0, 0xc0, 0xce, 0xc6, 0x7c, 0x00}, // 0x47
{0xc6, 0xc6, 0xc6, 0xfe, 0xc6, 0xc6, 0xc6, 0x00}, // 0x48
{0x7e, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7e, 0x00}, // 0x49
{0x06, 0x06, 0x06, 0x06, 0x06, 0xc6, 0x7c, 0x00}, // 0x4a
{0xc6, 0xcc, 0xd8, 0xf0, 0xd8, 0xcc, 0xc6, 0x00}, // 0x4b
{0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xfe, 0x00}, // 0x4c
{0xc6, 0xee, 0xfe, 0xfe, 0xd6, 0xc6, 0xc6, 0x00}, // 0x4d
{0xc6, 0xe6, 0xf6, 0xde, 0xce, 0xc6, 0xc6, 0x00}, // 0x4e
{0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, // 0x4f
{0xfc, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0, 0xc0, 0x00}, // 0x50
{0x7c, 0xc6, 0xc6, 0xc6, 0xd6, 0xde, 0x7c, 0x06}, // 0x51
{0xfc, 0xc6, 0xc6, 0xfc, 0xd8, 0xcc, 0xc6, 0x00}, // 0x52
{0x7c, 0xc6, 0xc0, 0x7c, 0x06, 0xc6, 0x7c, 0x00}, // 0x53
{0xff, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00}, // 0x54
{0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0xfe, 0x00}, // 0x55
{0xc6, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x38, 0x00}, // 0x56
{0xc6, 0xc6, 0xc6, 0xc6, 0xd6, 0xfe, 0x6c, 0x00}, // 0x57
{0xc6, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0xc6, 0x00}, // 0x58
{0xc6, 0xc6, 0xc6, 0x7c, 0x18, 0x30, 0xe0, 0x00}, // 0x59
{0xfe, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xfe, 0x00}, // 0x5a
{0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00}, // 0x5b
{0xc0, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x02, 0x00}, // 0x5c
{0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00}, // 0x5d
{0x10, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00}, // 0x5e
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff}, // 0x5f
{0x18, 0x18, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x60
{0x00, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0x7e, 0x00}, // 0x61
{0xc0, 0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xfc, 0x00}, // 0x62
{0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xc6, 0x7c, 0x00}, // 0x63
{0x06, 0x06, 0x06, 0x7e, 0xc6, 0xc6, 0x7e, 0x00}, // 0x64
{0x00, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0x7c, 0x00}, // 0x65
{0x1c, 0x36, 0x30, 0x78, 0x30, 0x30, 0x78, 0x00}, // 0x66
{0x00, 0x00, 0x7e, 0xc6, 0xc6, 0x7e, 0x06, 0xfc}, // 0x67
{0xc0, 0xc0, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00}, // 0x68
{0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 0x69
{0x06, 0x00, 0x06, 0x06, 0x06, 0x06, 0xc6, 0x7c}, // 0x6a
{0xc0, 0xc0, 0xcc, 0xd8, 0xf8, 0xcc, 0xc6, 0x00}, // 0x6b
{0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 0x6c
{0x00, 0x00, 0xcc, 0xfe, 0xfe, 0xd6, 0xd6, 0x00}, // 0x6d
{0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0xc6, 0x00}, // 0x6e
{0x00, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, // 0x6f
{0x00, 0x00, 0xfc, 0xc6, 0xc6, 0xfc, 0xc0, 0xc0}, // 0x70
{0x00, 0x00, 0x7e, 0xc6, 0xc6, 0x7e, 0x06, 0x06}, // 0x71
{0x00, 0x00, 0xfc, 0xc6, 0xc0, 0xc0, 0xc0, 0x00}, // 0x72
{0x00, 0x00, 0x7e, 0xc0, 0x7c, 0x06, 0xfc, 0x00}, // 0x73
{0x18, 0x18, 0x7e, 0x18, 0x18, 0x18, 0x0e, 0x00}, // 0x74
{0x00, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00}, // 0x75
{0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x7c, 0x38, 0x00}, // 0x76
{0x00, 0x00, 0xc6, 0xc6, 0xd6, 0xfe, 0x6c, 0x00}, // 0x77
{0x00, 0x00, 0xc6, 0x6c, 0x38, 0x6c, 0xc6, 0x00}, // 0x78
{0x00, 0x00, 0xc6, 0xc6, 0xc6, 0x7e, 0x06, 0xfc}, // 0x79
{0x00, 0x00, 0xfe, 0x0c, 0x38, 0x60, 0xfe, 0x00}, // 0x7a
{0x0e, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0e, 0x00}, // 0x7b
{0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // 0x7c
{0x70, 0x18, 0x18, 0x0e, 0x18, 0x18, 0x70, 0x00}, // 0x7d
{0x76, 0xdc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x7e
{0x00, 0x10, 0x38, 0x6c, 0xc6, 0xc6, 0xfe, 0x00}, // 0x7f
{0x7c, 0xc6, 0xc0, 0xc0, 0xc0, 0xd6, 0x7c, 0x30}, // 0x80
{0xc6, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00}, // 0x81
{0x0e, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0x7c, 0x00}, // 0x82
{0x7e, 0x81, 0x3c, 0x06, 0x7e, 0xc6, 0x7e, 0x00}, // 0x83
{0x66, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0x7e, 0x00}, // 0x84
{0xe0, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0x7e, 0x00}, // 0x85
{0x18, 0x18, 0x7c, 0x06, 0x7e, 0xc6, 0x7e, 0x00}, // 0x86
{0x00, 0x00, 0x7c, 0xc6, 0xc0, 0xd6, 0x7c, 0x30}, // 0x87
{0x7e, 0x81, 0x7c, 0xc6, 0xfe, 0xc0, 0x7c, 0x00}, // 0x88
{0x66, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0x7c, 0x00}, // 0x89
{0xe0, 0x00, 0x7c, 0xc6, 0xfe, 0xc0, 0x7c, 0x00}, // 0x8a
{0x66, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 0x8b
{0x7c, 0x82, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 0x8c
{0x70, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 0x8d
{0xc6, 0x10, 0x7c, 0xc6, 0xfe, 0xc6, 0xc6, 0x00}, // 0x8e
{0x38, 0x38, 0x00, 0x7c, 0xc6, 0xfe, 0xc6, 0x00}, // 0x8f
{0x0e, 0x00, 0xfe, 0xc0, 0xf8, 0xc0, 0xfe, 0x00}, // 0x90
{0x00, 0x00, 0x7f, 0x0c, 0x7f, 0xcc, 0x7f, 0x00}, // 0x91
{0x3f, 0x6c, 0xcc, 0xff, 0xcc, 0xcc, 0xcf, 0x00}, // 0x92
{0x7c, 0x82, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, // 0x93
{0x66, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, // 0x94
{0xe0, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, // 0x95
{0x7c, 0x82, 0x00, 0xc6, 0xc6, 0xc6, 0x7e, 0x00}, // 0x96
{0xe0, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00}, // 0x97
{0x66, 0x00, 0x66, 0x66, 0x66, 0x3e, 0x06, 0x7c}, // 0x98
{0xc6, 0x7c, 0xc6, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, // 0x99
{0xc6, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0xfe, 0x00}, // 0x9a
{0x18, 0x18, 0x7e, 0xd8, 0xd8, 0xd8, 0x7e, 0x18}, // 0x9b
{0x38, 0x6c, 0x60, 0xf0, 0x60, 0x66, 0xfc, 0x00}, // 0x9c
{0x66, 0x66, 0x3c, 0x18, 0x7e, 0x18, 0x7e, 0x18}, // 0x9d
{0xf8, 0xcc, 0xcc, 0xfa, 0xc6, 0xcf, 0xc6, 0xc3}, // 0x9e
{0x0e, 0x1b, 0x18, 0x3c, 0x18, 0x18, 0xd8, 0x70}, // 0x9f
{0x0e, 0x00, 0x7c, 0x06, 0x7e, 0xc6, 0x7e, 0x00}, // 0xa0
{0x1c, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 0xa1
{0x0e, 0x00, 0x7c, 0xc6, 0xc6, 0xc6, 0x7c, 0x00}, // 0xa2
{0x0e, 0x00, 0xc6, 0xc6, 0xc6, 0xc6, 0x7e, 0x00}, // 0xa3
{0x00, 0xfe, 0x00, 0xfc, 0xc6, 0xc6, 0xc6, 0x00}, // 0xa4
{0xfe, 0x00, 0xc6, 0xe6, 0xf6, 0xde, 0xce, 0x00}, // 0xa5
{0x3c, 0x6c, 0x6c, 0x3e, 0x00, 0x7e, 0x00, 0x00}, // 0xa6
{0x3c, 0x66, 0x66, 0x3c, 0x00, 0x7e, 0x00, 0x00}, // 0xa7
{0x18, 0x00, 0x18, 0x18, 0x30, 0x66, 0x3c, 0x00}, // 0xa8
{0x00, 0x00, 0x00, 0xfc, 0xc0, 0xc0, 0x00, 0x00}, // 0xa9
{0x00, 0x00, 0x00, 0xfc, 0x0c, 0x0c, 0x00, 0x00}, // 0xaa
{0xc6, 0xcc, 0xd8, 0x3f, 0x63, 0xcf, 0x8c, 0x0f}, // 0xab
{0xc3, 0xc6, 0xcc, 0xdb, 0x37, 0x6d, 0xcf, 0x03}, // 0xac
{0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00}, // 0xad
{0x00, 0x33, 0x66, 0xcc, 0x66, 0x33, 0x00, 0x00}, // 0xae
{0x00, 0xcc, 0x66, 0x33, 0x66, 0xcc, 0x00, 0x00}, // 0xaf
{0x22, 0x88, 0x22, 0x88, 0x22, 0x88, 0x22, 0x88}, // 0xb0
{0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa}, // 0xb1
{0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77, 0xdd, 0x77}, // 0xb2
{0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18}, // 0xb3
{0x18, 0x18, 0x18, 0x18, 0xf8, 0x18, 0x18, 0x18}, // 0xb4
{0x18, 0x18, 0xf8, 0x18, 0xf8, 0x18, 0x18, 0x18}, // 0xb5
{0x36, 0x36, 0x36, 0x36, 0xf6, 0x36, 0x36, 0x36}, // 0xb6
{0x00, 0x00, 0x00, 0x00, 0xfe, 0x36, 0x36, 0x36}, // 0xb7
{0x00, 0x00, 0xf8, 0x18, 0xf8, 0x18, 0x18, 0x18}, // 0xb8
{0x36, 0x36, 0xf6, 0x06, 0xf6, 0x36, 0x36, 0x36}, // 0xb9
{0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36}, // 0xba
{0x00, 0x00, 0xfe, 0x06, 0xf6, 0x36, 0x36, 0x36}, // 0xbb
{0x36, 0x36, 0xf6, 0x06, 0xfe, 0x00, 0x00, 0x00}, // 0xbc
{0x36, 0x36, 0x36, 0x36, 0xfe, 0x00, 0x00, 0x00}, // 0xbd
{0x18, 0x18, 0xf8, 0x18, 0xf8, 0x00, 0x00, 0x00}, // 0xbe
{0x00, 0x00, 0x00, 0x00, 0xf8, 0x18, 0x18, 0x18}, // 0xbf
{0x18, 0x18, 0x18, 0x18, 0x1f, 0x00, 0x00, 0x00}, // 0xc0
{0x18, 0x18, 0x18, 0x18, 0xff, 0x00, 0x00, 0x00}, // 0xc1
{0x00, 0x00, 0x00, 0x00, 0xff, 0x18, 0x18, 0x18}, // 0xc2
{0x18, 0x18, 0x18, 0x18, 0x1f, 0x18, 0x18, 0x18}, // 0xc3
{0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00}, // 0xc4
{0x18, 0x18, 0x18, 0x18, 0xff, 0x18, 0x18, 0x18}, // 0xc5
{0x18, 0x18, 0x1f, 0x18, 0x1f, 0x18, 0x18, 0x18}, // 0xc6
{0x36, 0x36, 0x36, 0x36, 0x37, 0x36, 0x36, 0x36}, // 0xc7
{0x36, 0x36, 0x37, 0x30, 0x3f, 0x00, 0x00, 0x00}, // 0xc8
{0x00, 0x00, 0x3f, 0x30, 0x37, 0x36, 0x36, 0x36}, // 0xc9
{0x36, 0x36, 0xf7, 0x00, 0xff, 0x00, 0x00, 0x00}, // 0xca
{0x00, 0x00, 0xff, 0x00, 0xf7, 0x36, 0x36, 0x36}, // 0xcb
{0x36, 0x36, 0x37, 0x30, 0x37, 0x36, 0x36, 0x36}, // 0xcc
{0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00}, // 0xcd
{0x36, 0x36, 0xf7, 0x00, 0xf7, 0x36, 0x36, 0x36}, // 0xce
{0x18, 0x18, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00}, // 0xcf
{0x36, 0x36, 0x36, 0x36, 0xff, 0x00, 0x00, 0x00}, // 0xd0
{0x00, 0x00, 0xff, 0x00, 0xff, 0x18, 0x18, 0x18}, // 0xd1
{0x00, 0x00, 0x00, 0x00, 0xff, 0x36, 0x36, 0x36}, // 0xd2
{0x36, 0x36, 0x36, 0x36, 0x3f, 0x00, 0x00, 0x00}, // 0xd3
{0x18, 0x18, 0x1f, 0x18, 0x1f, 0x00, 0x00, 0x00}, // 0xd4
{0x00, 0x00, 0x1f, 0x18, 0x1f, 0x18, 0x18, 0x18}, // 0xd5
{0x00, 0x00, 0x00, 0x00, 0x3f, 0x36, 0x36, 0x36}, // 0xd6
{0x36, 0x36, 0x36, 0x36, 0xff, 0x36, 0x36, 0x36}, // 0xd7
{0x18, 0x18, 0xff, 0x18, 0xff, 0x18, 0x18, 0x18}, // 0xd8
{0x18, 0x18, 0x18, 0x18, 0xf8, 0x00, 0x00, 0x00}, // 0xd9
{0x00, 0x00, 0x00, 0x00, 0x1f, 0x18, 0x18, 0x18}, // 0xda
{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, // 0xdb
{0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff}, // 0xdc
{0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0}, // 0xdd
{0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f}, // 0xde
{0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00}, // 0xdf
{0x00, 0x00, 0x76, 0xdc, 0xc8, 0xdc, 0x76, 0x00}, // 0xe0
{0x38, 0x6c, 0x6c, 0x78, 0x6c, 0x66, 0x6c, 0x60}, // 0xe1
{0x00, 0xfe, 0xc6, 0xc0, 0xc0, 0xc0, 0xc0, 0x00}, // 0xe2
{0x00, 0x00, 0xfe, 0x6c, 0x6c, 0x6c, 0x6c, 0x00}, // 0xe3
{0xfe, 0x60, 0x30, 0x18, 0x30, 0x60, 0xfe, 0x00}, // 0xe4
{0x00, 0x00, 0x7e, 0xd8, 0xd8, 0xd8, 0x70, 0x00}, // 0xe5
{0x00, 0x66, 0x66, 0x66, 0x66, 0x7c, 0x60, 0xc0}, // 0xe6
{0x00, 0x76, 0xdc, 0x18, 0x18, 0x18, 0x18, 0x00}, // 0xe7
{0x7e, 0x18, 0x3c, 0x66, 0x66, 0x3c, 0x18, 0x7e}, // 0xe8
{0x3c, 0x66, 0xc3, 0xff, 0xc3, 0x66, 0x3c, 0x00}, // 0xe9
{0x3c, 0x66, 0xc3, 0xc3, 0x66, 0x66, 0xe7, 0x00}, // 0xea
{0x0e, 0x18, 0x0c, 0x7e, 0xc6, 0xc6, 0x7c, 0x00}, // 0xeb
{0x00, 0x00, 0x7e, 0xdb, 0xdb, 0x7e, 0x00, 0x00}, // 0xec
{0x06, 0x0c, 0x7e, 0xdb, 0xdb, 0x7e, 0x60, 0xc0}, // 0xed
{0x38, 0x60, 0xc0, 0xf8, 0xc0, 0x60, 0x38, 0x00}, // 0xee
{0x78, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x00}, // 0xef
{0x00, 0x7e, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x00}, // 0xf0
{0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x7e, 0x00}, // 0xf1
{0x60, 0x30, 0x18, 0x30, 0x60, 0x00, 0xfc, 0x00}, // 0xf2
{0x18, 0x30, 0x60, 0x30, 0x18, 0x00, 0xfc, 0x00}, // 0xf3
{0x0e, 0x1b, 0x1b, 0x18, 0x18, 0x18, 0x18, 0x18}, // 0xf4
{0x18, 0x18, 0x18, 0x18, 0x18, 0xd8, 0xd8, 0x70}, // 0xf5
{0x18, 0x18, 0x00, 0x7e, 0x00, 0x18, 0x18, 0x00}, // 0xf6
{0x00, 0x76, 0xdc, 0x00, 0x76, 0xdc, 0x00, 0x00}, // 0xf7
{0x38, 0x6c, 0x6c, 0x38, 0x00, 0x00, 0x00, 0x00}, // 0xf8
{0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00}, // 0xf9
{0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00}, // 0xfa
{0x0f, 0x0c, 0x0c, 0x0c, 0xec, 0x6c, 0x3c, 0x1c}, // 0xfb
{0x78, 0x6c, 0x6c, 0x6c, 0x6c, 0x00, 0x00, 0x00}, // 0xfc
{0x7c, 0x0c, 0x7c, 0x60, 0x7c, 0x00, 0x00, 0x00}, // 0xfd
{0x00, 0x00, 0x3c, 0x3c, 0x3c, 0x3c, 0x00, 0x00}, // 0xfe
{0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}} // 0xff
// reverseGlyphs swaps the endianness of the raster bits to work with the 7219.
// Returns the a new set with the byte values reversed.
func reverseGlyphs(digits [][]byte) [][]byte {
nibbles := [16]byte{0x0, 0x8, 0x4, 0xc, 0x2, 0xa, 0x6, 0xe, 0x1, 0x9, 0x5, 0xc, 0x3, 0xb, 0x7, 0xf}
result := make([][]byte, len(digits))
for i := 0; i < len(digits); i++ {
newChar := make([]byte, 8)
for rasterLine := range len(digits[i]) {
x := digits[i][rasterLine]
y := nibbles[(x>>4)&0xf] | nibbles[x&0xf]<<4
newChar[rasterLine] = y
}
result[i] = newChar
}
return result
}

@ -0,0 +1,440 @@
// Copyright 2024 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
//
// The max7219 package provides a simple interface for displaying data on
// numeric 7-segment displays, or on matrix displays. It simplifies writes
// and provides useful features like scrolling characters on either type
// of display unit.
package max7219
import (
"errors"
"fmt"
"log"
"time"
"periph.io/x/conn/v3/physic"
"periph.io/x/conn/v3/spi"
)
// DecodeMode is the mode for handling data. Refer to the datasheet for
// more information.
type DecodeMode byte
const (
_REGISTER_NOOP byte = 0x0
_REGISTER_DECODE_MODE byte = 0x9
_REGISTER_INTENSITY byte = 0xa
_REGISTER_SCAN_LIMIT byte = 0xb
_REGISTER_SHUTDOWN byte = 0xc
_REGISTER_DISPLAY_TEST byte = 0xf
// Value to write to a Code B Font decoded register to blank out the
// digit.
ClearDigit byte = 0x0f
// Value to write for a minus sign symbol
MinusSign byte = 0x0a
// To turn the decimal point on for a digit display, OR the value of the
// digit with DecimalPoint
DecimalPoint byte = 0x80
// DecodeB is used for numeric segment displays. E.G. given a binary 0,
// it would turn on the appropriate segments to display the character 0.
DecodeB DecodeMode = 0xff
// DecodeNone is RAW mode, or not decoded. For each byte, bits that are
// one are turned on in the matrix, and bits that are 0 turn off the
// led at that row/column.
DecodeNone DecodeMode = 0
)
// Type for a Maxim MAX7219/MAX7221 device.
type Dev struct {
conn spi.Conn
// decode mode for all data registers
decode DecodeMode
// units is the number of 7219 units daisy-chained together.
units int
// The number of digits (or the NxN matrix size) in this display.
digits byte
// charset is the character set for matrix display use. The first index
// is the code point (e.g. 0x20=32=space. The second index is the 8 byte
// raster values for each line in the matrix.
glyphs [][]byte
}
// emptyBytes creates a slice of empty bytes (digit values or byte values)
// for the display.
func (d *Dev) emptyBytes() []byte {
b := make([]byte, d.digits)
if d.decode == DecodeB {
for ix := range b {
b[ix] = ClearDigit
}
}
return b
}
// init puts the display in the default mode. The default is to
// clear the display, set intensity to middle, and set Decode
// to DecodeB for a single unit, or DecodeNone for multi-unit.
func (d *Dev) init() {
var initCommands = [][]byte{
{_REGISTER_DISPLAY_TEST, 0x0},
{_REGISTER_SHUTDOWN, 0x00},
{_REGISTER_INTENSITY, 0x08},
{_REGISTER_SCAN_LIMIT, d.digits - 1},
{_REGISTER_SHUTDOWN, 0x01}}
for _, cmd := range initCommands {
err := d.sendCommand(cmd[0], cmd[1])
if err != nil {
log.Println(err)
break
}
}
if d.units == 1 {
_ = d.SetDecode(DecodeB)
} else {
_ = d.SetDecode(DecodeNone)
}
_ = d.Clear()
}
// sendCommand writes to a data register or command register.
// Data registers are 1-8, and command registers are > 8.
// If multiple units are daisychained together, the command
// is repeated and sent to all units.
func (d *Dev) sendCommand(register, data byte) error {
w := make([]byte, d.units*2)
for ix := range d.units {
w[ix*2] = register
w[ix*2+1] = data
}
return d.conn.Tx(w, nil)
}
// NewSPI creates a new Max7219 using the specified spi.Port. units is the number
// of Max7219 chips daisy-chained together. numDigits is the number of digits
// displayed.
func NewSPI(p spi.Port, units, numDigits int) (*Dev, error) {
if units <= 0 {
return nil, errors.New("max7219: invalid value for number of cascaded units")
}
if numDigits <= 0 || numDigits > 8 {
return nil, errors.New("max7219: invalid value for number of digits")
}
// It works in Mode0, Mode2 and Mode3.
c, err := p.Connect(10*physic.MegaHertz, spi.Mode0, 8)
if err != nil {
return nil, fmt.Errorf("max7219: %v", err)
}
d := &Dev{conn: c, digits: byte(numDigits), units: units, glyphs: nil}
d.init()
return d, nil
}
// Clear erases the content of all display segments or matrix LEDs.
func (d *Dev) Clear() error {
empty := d.emptyBytes()
if d.units > 1 {
w := make([][]byte, d.units)
for ix := range d.units {
w[ix] = empty
}
return d.WriteCascadedUnits(w)
} else {
return d.Write(empty)
}
}
// shiftBytes shifts an array of raster characters left one LED Column (bit).
// Used to continuously scroll a display of glyphs.
func shiftBytes(bytes [][]byte) {
// At least this is how my matrix is wired :)
const rightMostBit byte = 0x80
const leftMostBit byte = 0x01
var rasterLimit = len(bytes[0])
// Save the left most character so when we shift the rightmost
// character, we can put the leftmost bit of the first character into it.
saveReg := make([]byte, rasterLimit)
copy(saveReg, bytes[0])
var nextByte, curByte byte
var limit = len(bytes) - 1
for char := 0; char <= limit; char++ {
for rasterLine := 0; rasterLine < rasterLimit; rasterLine++ {
curByte = bytes[char][rasterLine] >> 1
if char == limit {
nextByte = saveReg[rasterLine]
} else {
nextByte = bytes[char+1][rasterLine]
}
if nextByte&leftMostBit > 0 {
curByte |= rightMostBit
}
bytes[char][rasterLine] = curByte
}
}
}
// ScrollChars takes a character array and scrolls it from right-to-left, one
// led column at a time. This can be used to scroll a matrix display of glyphs,
// or digits on a seven-segment display. If the length of data is less than
// the number of display units, it writes that directly without scrolling.
func (d *Dev) ScrollChars(data []byte, scrollCount int, updateInterval time.Duration) {
if d.decode == DecodeNone {
// This is a matrix
// Create a temporary copy of the data to modify - We don't want to munge
// our glyph set.
bytes := make([][]byte, len(data))
for ix, val := range data {
newVals := make([]byte, 8)
copy(newVals, d.glyphs[val])
bytes[ix] = newVals
}
_ = d.WriteCascadedUnits(bytes)
for shifts := scrollCount * len(data) * int(d.digits); shifts > 0; shifts-- {
time.Sleep(updateInterval)
shiftBytes(bytes)
_ = d.WriteCascadedUnits(bytes)
}
} else {
// This is a seven segment display.
data = convertBytes(data)
if len(data) <= int(d.digits)*d.units {
_ = d.Write(data)
time.Sleep(time.Duration(scrollCount*len(data)) * updateInterval)
return
}
displayData := make([]byte, 0)
displayData = append(displayData, data...)
displayData = append(displayData, byte(ClearDigit))
displayData = append(displayData, data...)
var pos int
for shifts := scrollCount * len(data); shifts > 0; shifts-- {
_ = d.Write(displayData[pos : pos+int(d.digits)*d.units])
pos = pos + 1
if pos >= (len(data) + 1) {
pos = 0
}
time.Sleep(updateInterval)
}
}
}
// SetGlyphs allows you to set the character set for use by the matrix display.
// If the endianness of the charset doesn't match that used by the max7219,
// pass true for reverse and it will change the endianness of the raster
// values.
//
// You only have to supply glyphs for values you intend to write. So if you're
// just writing digits, you just need 0-9 and whatever punctuation marks you
// need.
func (d *Dev) SetGlyphs(glyphs [][]byte, reverse bool) {
if reverse {
d.glyphs = reverseGlyphs(glyphs)
} else {
d.glyphs = glyphs
}
}
// SetDecode tells the Max7219 whether values should be decoded for a 7 segment
// display, or if they should be interpreted literally. Refer to the datasheet
// for more detailed information.
func (d *Dev) SetDecode(mode DecodeMode) error {
d.decode = mode
return d.sendCommand(_REGISTER_DECODE_MODE, byte(mode))
}
// SetIntensity controls the brightness of the display. The allowed range for
// intensity is from 0-15. Keep in mind that the brighter display, the more
// current drawn.
func (d *Dev) SetIntensity(intensity byte) error {
return d.sendCommand(_REGISTER_INTENSITY, intensity&0x0f)
}
// TestDisplay turns on the 7219 display mode which set all segments (or LEDs) on,
// and the intensity to maximum. If you're using multiple units, you should be
// aware of the current draw, and limit how long you leave this on.
func (d *Dev) TestDisplay(on bool) error {
if on {
return d.sendCommand(_REGISTER_DISPLAY_TEST, 1)
} else {
return d.sendCommand(_REGISTER_DISPLAY_TEST, 0)
}
}
// writeChars sends characters as glyphs out to the device(s). It stacks them
// into a two-dimensional slice and then writes them out.
func (d *Dev) writeChars(bytes []byte) error {
w := make([][]byte, d.units)
for ix := range d.units {
x := make([]byte, 8)
copy(x, d.glyphs[0x20])
w[ix] = x
}
charPos := len(bytes) - 1
for ix := d.units - 1; ix >= 0 && charPos >= 0; ix-- {
w[ix] = d.glyphs[bytes[charPos]]
charPos = charPos - 1
}
return d.WriteCascadedUnits(w)
}
// convertBytes converts ascii characters into their appropriate CodeB
// representations. Refer to the datasheet.
func convertBytes(bytes []byte) []byte {
newBytes := make([]byte, 0, len(bytes))
for ix, c := range bytes {
if c >= '0' && c <= '9' {
newBytes = append(newBytes, c-'0')
} else {
switch c {
case ' ':
newBytes = append(newBytes, ClearDigit)
case '-':
newBytes = append(newBytes, MinusSign)
case '.':
if ix > 0 {
// A decimal point is OR'd onto the previous
// digit to turn it on.
newBytes[len(newBytes)-1] |= DecimalPoint
}
case 'E':
newBytes = append(newBytes, 0xb)
case 'H':
newBytes = append(newBytes, 0xc)
case 'L':
newBytes = append(newBytes, 0xd)
case 'P':
newBytes = append(newBytes, 0xe)
default:
newBytes = append(newBytes, c)
}
}
}
return newBytes
}
// Write sends data to the display unit.
//
// If decode is DecodeNone, then it's assumed we're writing to a matrix. If
// a glyph set has been set, then bytes are treated as offsets into the
// character table.
//
// If decode is DecodeB, then any ASCII characters are converted into their
// supported CodeB values and written. If units are cascaded, this method
// automatically handles re-formatting the data, and writing it to the cascaded
// 7219 units.
func (d *Dev) Write(bytes []byte) error {
if d.decode == DecodeNone {
return d.writeChars(bytes)
}
bytes = convertBytes(bytes)
if d.units == 1 {
// single-unit numeric display.
var digit = d.digits
w := make([]byte, 2)
for _, val := range bytes {
w[0] = digit
w[1] = val
err := d.conn.Tx(w, nil)
if err != nil {
return err
}
digit -= 1
}
} else {
// A multi-unit display.
writeData := make([][]byte, d.units)
for padding := 0; padding < d.units; padding++ {
writeData[padding] = make([]byte, d.digits)
for ix := 0; ix < int(d.digits); ix++ {
writeData[padding][ix] = ClearDigit
}
}
unit := d.units - 1
digit := d.digits
for char := len(bytes) - 1; char >= 0 && unit >= 0; char-- {
c := bytes[char]
writeData[unit][digit-1] = c
if digit == 1 {
digit = d.digits
unit -= 1
} else {
digit -= 1
}
}
return d.WriteCascadedUnits(writeData)
}
return nil
}
// WriteInt provide a convenience method that displays the specified integer
// value on the display. It will work for either matrixes (with a glyph set)
// or numeric displays.
func (d *Dev) WriteInt(value int) error {
digits := d.units
if d.decode != DecodeNone {
digits *= int(d.digits)
}
return d.Write([]byte(fmt.Sprintf("%*d", digits, value)))
}
// WriteCascadedUnits writes a 2D array of raster characters to
// a a set of cascaded max7219 devices. For example, a 4 unit
// 8*8 matrix, or two eight digit 7-segment LEDs. This handles
// the complexities of how data is shifted from one 7219
// to the next in a chain.
func (d *Dev) WriteCascadedUnits(bytes [][]byte) error {
matrixCount := len(bytes)
for rasterLine := 0; rasterLine < int(d.digits); rasterLine++ {
w := make([]byte, 0)
for matrix := matrixCount - 1; matrix >= 0; matrix-- {
w = append(w, byte(rasterLine+1))
w = append(w, bytes[matrix][int(d.digits-1)-rasterLine])
}
err := d.conn.Tx(w, nil)
if err != nil {
return err
}
}
return nil
}
// WriteCascadedUnit writes data to a single display unit in
// a set of cascaded 7219 chips. offset is the 0 based number of
// the unit to write to. You could use this to update one segment
// of a cascaded matrix. Imagine rolling a digit upwards to bring
// in a new one...
func (d *Dev) WriteCascadedUnit(offset int, data []byte) error {
for i := byte(0); i < d.digits; i++ {
w := make([]byte, 0)
for matrix := d.units - 1; matrix >= 0; matrix-- {
if matrix == offset {
w = append(w, byte(i+1))
w = append(w, data[(d.digits-1)-i])
} else {
w = append(w, _REGISTER_NOOP)
w = append(w, 0)
}
}
err := d.conn.Tx(w, nil)
if err != nil {
return err
}
}
return nil
}

@ -0,0 +1,234 @@
// Copyright 2024 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package max7219
import (
"fmt"
"testing"
"time"
"periph.io/x/conn/v3/conntest"
"periph.io/x/conn/v3/spi/spitest"
)
func TestReverseGlyphs(t *testing.T) {
testVals := [][]byte{{0x01, 0xaa}, {0x80, 0x55}}
expected := [][]byte{{0x80, 0x55}, {0x01, 0xaa}}
testVals = reverseGlyphs(testVals)
for outer := range len(expected) {
for inner := range len(expected[0]) {
if testVals[outer][inner] != expected[outer][inner] {
t.Errorf("testVals[%d][%d] expected 0x%x found: 0x%x", outer, inner, expected[outer][inner], testVals[outer][inner])
}
}
}
}
func TestConvertBytes(t *testing.T) {
testStr := "-0.123456789EHLP "
expected := []byte{MinusSign, 0 | DecimalPoint, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xb, 0xc, 0xd, 0xe, 0xf}
converted := convertBytes([]byte(testStr))
if len(converted) != len(expected) {
t.Error("converted bytes length not as expected")
}
for ix := range len(expected) {
if converted[ix] != expected[ix] {
t.Errorf("error . expected 0x%x received 0x%x", expected[ix], converted[ix])
}
}
}
func TestGlyphs(t *testing.T) {
// Verify our glyphs look OK.
if len(CP437Glyphs) != 256 {
t.Errorf("CP437 glphys not expected length. Got: %d", len(CP437Glyphs))
}
for ix := range len(CP437Glyphs) {
if len(CP437Glyphs[ix]) != 8 {
t.Errorf("Invalid glyph 0x%x found. Length: %d", ix, len(CP437Glyphs[ix]))
}
}
}
func verifyOperations(found, expected []conntest.IO) error {
if len(found) != len(expected) {
return fmt.Errorf("invalid length. found length: %d expected length: %d", len(found), len(expected))
}
for outer := range len(expected) {
for inner := range len(found[outer].W) {
if expected[outer].W[inner] != found[outer].W[inner] {
return fmt.Errorf("data not as expected. found[%d][%d]=0x%x expected 0x%x",
outer,
inner,
found[outer].W[inner],
expected[outer].W[inner])
}
}
}
return nil
}
func TestInit(t *testing.T) {
pb := &spitest.Playback{Playback: conntest.Playback{DontPanic: true, Count: 1}}
defer pb.Close()
record := &spitest.Record{}
_, err := NewSPI(record, 1, 8)
if err != nil {
t.Error(err)
}
expected := []conntest.IO{
{W: []uint8{0xf, 0x0}}, // Disable self-test
{W: []uint8{0xc, 0x0}}, // Shutdown - Enter Shutdown Mode
{W: []uint8{0xa, 0x8}}, // Intensity
{W: []uint8{0xb, 0x7}}, // Scan Limit
{W: []uint8{0xc, 0x1}}, // Shutdown - Resume Normal Mode
{W: []uint8{0x9, 0xff}}, // Decode Mode
{W: []uint8{0x8, 0xf}}, // Clear digits 1-8
{W: []uint8{0x7, 0xf}},
{W: []uint8{0x6, 0xf}},
{W: []uint8{0x5, 0xf}},
{W: []uint8{0x4, 0xf}},
{W: []uint8{0x3, 0xf}},
{W: []uint8{0x2, 0xf}},
{W: []uint8{0x1, 0xf}}}
err = verifyOperations(record.Ops, expected)
if err != nil {
t.Error(err)
}
}
func TestWrite(t *testing.T) {
pb := &spitest.Playback{Playback: conntest.Playback{DontPanic: true, Count: 1}}
defer pb.Close()
record := &spitest.Record{}
dev, err := NewSPI(record, 1, 8)
record.Ops = make([]conntest.IO, 0)
dev.Write([]byte("12345678"))
if err != nil {
t.Error(err)
}
expected := []conntest.IO{
{W: []uint8{0x8, 0x1}},
{W: []uint8{0x7, 0x2}},
{W: []uint8{0x6, 0x3}},
{W: []uint8{0x5, 0x4}},
{W: []uint8{0x4, 0x5}},
{W: []uint8{0x3, 0x6}},
{W: []uint8{0x2, 0x7}},
{W: []uint8{0x1, 0x8}}}
err = verifyOperations(record.Ops, expected)
if err != nil {
t.Error(err)
}
record.Ops = make([]conntest.IO, 0)
dev.WriteInt(12345678)
if err != nil {
t.Error(err)
}
err = verifyOperations(record.Ops, expected)
if err != nil {
t.Error(err)
}
}
func TestScroll(t *testing.T) {
pb := &spitest.Playback{Playback: conntest.Playback{DontPanic: true, Count: 1}}
defer pb.Close()
record := &spitest.Record{}
dev, err := NewSPI(record, 1, 1)
record.Ops = make([]conntest.IO, 0)
dev.ScrollChars([]byte("12"), 2, time.Millisecond)
if err != nil {
t.Error(err)
}
expected := []conntest.IO{
{W: []uint8{0x1, 0x1}}, // Write 1st digit to position 1
{W: []uint8{0x1, 0x2}}, // Write 2nd digit to position 1
{W: []uint8{0x1, 0xf}}, // Write blank
{W: []uint8{0x1, 0x1}}} // Write 1st digit to position 1
err = verifyOperations(record.Ops, expected)
if err != nil {
t.Error(err)
}
}
func TestScrollGlyphs(t *testing.T) {
pb := &spitest.Playback{Playback: conntest.Playback{DontPanic: true, Count: 1}}
defer pb.Close()
record := &spitest.Record{}
dev, err := NewSPI(record, 4, 8) // Simulate a 4 unit matrix
dev.SetDecode(DecodeNone)
dev.SetGlyphs(CP437Glyphs, true)
record.Ops = make([]conntest.IO, 0)
dev.ScrollChars([]byte("123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"), 3, time.Millisecond)
if err != nil {
t.Error(err)
}
// This generates a lot of data. Just make sure the operation count matches
// what we expected.
expectedOps := 6728
if len(record.Ops) != expectedOps {
t.Errorf("expected %d operations, received %d", expectedOps, len(record.Ops))
}
}
func TestCascadedWrite(t *testing.T) {
// ops := make([]conntest.IO, 0)
pb := &spitest.Playback{Playback: conntest.Playback{DontPanic: true, Count: 1}}
defer pb.Close()
record := &spitest.Record{}
dev, err := NewSPI(record, 2, 4)
dev.SetDecode(DecodeB)
record.Ops = make([]conntest.IO, 0)
// imaginarily write 8 characters to two 4 digit displays.
dev.Write([]byte("12345678"))
if err != nil {
t.Error(err)
}
expected := []conntest.IO{
{W: []uint8{0x1, 0x8, 0x1, 0x4}}, // Unit 1 digit 8, Unit 0 digit 4
{W: []uint8{0x2, 0x7, 0x2, 0x3}}, // Unit 1 digit 7, unit 0 digit 3
{W: []uint8{0x3, 0x6, 0x3, 0x2}}, // unit 1 digit 6, unit 0 digit 2
{W: []uint8{0x4, 0x5, 0x4, 0x1}}} // unit 1 digit 5, unit 0 digit 1
err = verifyOperations(record.Ops, expected)
if err != nil {
t.Error(err)
}
}
func TestCommand(t *testing.T) {
// Verify a command is replicated #units times.
pb := &spitest.Playback{Playback: conntest.Playback{DontPanic: true, Count: 1}}
defer pb.Close()
record := &spitest.Record{}
dev, _ := NewSPI(record, 4, 8)
record.Ops = make([]conntest.IO, 0)
err := dev.SetIntensity(0x0b)
if err != nil {
t.Error(err)
}
expected := []conntest.IO{
{W: []uint8{0xa, 0xb, 0xa, 0xb, 0xa, 0xb, 0xa, 0xb}}} // Set intensity register and the value replicated units times.
err = verifyOperations(record.Ops, expected)
if err != nil {
t.Error(err)
}
}
Loading…
Cancel
Save