mirror of https://github.com/periph/devices
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
13 KiB
Go
441 lines
13 KiB
Go
// 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
|
|
}
|