mirror of https://github.com/periph/devices
Max7219 LED Driver (#79)
parent
9ab07ba9a6
commit
c0037af197
@ -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…
Reference in New Issue