diff --git a/max7219/README.md b/max7219/README.md new file mode 100644 index 0000000..4ddd4e8 --- /dev/null +++ b/max7219/README.md @@ -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. diff --git a/max7219/example_test.go b/max7219/example_test.go new file mode 100644 index 0000000..f2885b8 --- /dev/null +++ b/max7219/example_test.go @@ -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) + } +} diff --git a/max7219/glyphs.go b/max7219/glyphs.go new file mode 100644 index 0000000..7b63976 --- /dev/null +++ b/max7219/glyphs.go @@ -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 +} diff --git a/max7219/max7219.go b/max7219/max7219.go new file mode 100644 index 0000000..db9aba7 --- /dev/null +++ b/max7219/max7219.go @@ -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 +} diff --git a/max7219/max7219_test.go b/max7219/max7219_test.go new file mode 100644 index 0000000..89a92ac --- /dev/null +++ b/max7219/max7219_test.go @@ -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) + } +}