mirror of https://github.com/periph/devices
ssd1306: implement SPI and transparent differential update.
- Increase test coverage to 100%. - Add ssd1306smoketest. - Complete SPI 4-wire implementation. Differential update reduces the amount of data sent over the I²C/SPI bus at the (small) cost of slightly more CPU usage. On many platform, I²C default transfert speed is only 100KHz so when only a small section of the display is updated, this can lead to significant performance improvement. For now only page based differential update is used, as column based offset corrupt the display. This still results in large savings.pull/1/head
parent
722b0f8824
commit
daad425c63
@ -0,0 +1,7 @@
|
||||
# 'ssd1306' smoke test
|
||||
|
||||
Verifies that two SSD1306, one over I²C, one over SPI, can display the same
|
||||
output.
|
||||
|
||||
It can also be leveraged to record the I/O to write playback unit tests. It is a
|
||||
good example to reuse to write other device driver unit test.
|
||||
@ -0,0 +1,7 @@
|
||||
// Copyright 2017 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 ssd1306smoketest
|
||||
|
||||
var bunny = []byte("GIF87a7\x00@\x00\x80\x01\x00\x00\x00\x00\xff\xff\xff,\x00\x00\x00\x007\x00@\x00\x00\x02\xfe\x84\x8f\xa9\xcb\x16\x1f\u0682\xf4\xc9\xeb*\xc4@k>y\x9bT\x1d\x1ehR\x88\u068cJ\t\xb2\x89\xfbZ\xadL\u04b3]G\x17\x1e\xe2\x05\x194 p\xe7\xcb$;\u0095\x10\x96Z\"\x93P\x9d\xd3ilF\xb1\xb6\xe36Z\xb42\xa921\x18\x1c\x96\x8e\xb9d5\x92\xcd\u02f6G^.\x1c\xfbV\u01b5\xc1\xe5G\x89\xa7\xd7e\xd6\xe3\x87\xe3\x82X\xc6w\xf3\xa4\u0606\xa6\x87\x12y\x068\xe6S\xb5\xf8\x93H78wI\xa8\xb9'\xba6I*iW\xb9\u01b9z:4\xf9gY\xdaJI\n\xcb\x02*y+\xda\xf9\x99yZG\xd49\xdb\xe7\x16\xe3\x9b{\x1c:\u0733\x9c\xda\fl\\\xfc\x9b\x8c\x01\x1d\xdd\xfc\xb5l}\xfdL\u0371\x9d#\u074a\xab\x8cM;->K\x1e\xac\x96\u0337^8\x15\xde\ue747\u02ae\x05\x1exO\\\x98n\u007f\xce\u0554z\xf0\xd0\xe9+\x88,\xc2Aw\xdbtQ[\u05f0\u047fXDRT\xd3\x01\xabO0`Kb\x8e\x9c`\xe4FDH/\x1f\xf1M\xf4Wl\x03\xae\x8c\xa1@\xb1\x9aWR\x90\x8aE/[\x8a\x14\xe1\fO\xc1\x9b8s\xc29iJ\x90@b@}\x86\x839\xaeIG\x97\xf5\x00\x06\xe5X\x0e\x1d\xb3+\x17\x9bf\xbb\xca\xc4\xe60\x8cJi\xb2Lhh\x17O\x84D\u01ce5'\xd3,\x14\xb4ld\xa2-\x00\x00;")
|
||||
@ -0,0 +1,495 @@
|
||||
// Copyright 2017 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 ssd1306smoketest is leveraged by periph-smoketest to verify that two
|
||||
// SSD1306, one over I²C, one over SPI, can display the same output.
|
||||
package ssd1306smoketest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/math/fixed"
|
||||
|
||||
"periph.io/x/periph/conn/gpio"
|
||||
"periph.io/x/periph/conn/gpio/gpioreg"
|
||||
"periph.io/x/periph/conn/i2c"
|
||||
"periph.io/x/periph/conn/i2c/i2creg"
|
||||
"periph.io/x/periph/conn/i2c/i2ctest"
|
||||
"periph.io/x/periph/conn/spi"
|
||||
"periph.io/x/periph/conn/spi/spireg"
|
||||
"periph.io/x/periph/conn/spi/spitest"
|
||||
"periph.io/x/periph/devices/ssd1306"
|
||||
"periph.io/x/periph/devices/ssd1306/image1bit"
|
||||
)
|
||||
|
||||
// SmokeTest is imported by periph-smoketest.
|
||||
type SmokeTest struct {
|
||||
delay time.Duration
|
||||
devices []*ssd1306.Dev
|
||||
timings []time.Duration
|
||||
}
|
||||
|
||||
func (s *SmokeTest) String() string {
|
||||
return s.Name()
|
||||
}
|
||||
|
||||
// Name implements the SmokeTest interface.
|
||||
func (s *SmokeTest) Name() string {
|
||||
return "ssd1306"
|
||||
}
|
||||
|
||||
// Description implements the SmokeTest interface.
|
||||
func (s *SmokeTest) Description() string {
|
||||
return "Tests SSD1306 over I²C and SPI by displaying multiple patterns that exercises all code paths"
|
||||
}
|
||||
|
||||
// Run implements the SmokeTest interface.
|
||||
func (s *SmokeTest) Run(args []string) (err error) {
|
||||
s.delay = 2 * time.Second
|
||||
|
||||
f := flag.NewFlagSet("buses", flag.ExitOnError)
|
||||
i2cName := f.String("i2c", "", "I²C bus to use")
|
||||
spiName := f.String("spi", "", "SPI bus to use")
|
||||
dcName := f.String("dc", "", "DC pin to use in 4-wire SPI mode")
|
||||
|
||||
w := f.Int("w", 128, "Display width")
|
||||
h := f.Int("h", 64, "Display height")
|
||||
rotated := f.Bool("rotated", false, "Rotate the displays by 180°")
|
||||
|
||||
record := f.Bool("record", false, "record operation (for playback unit testing)")
|
||||
f.Parse(args)
|
||||
|
||||
i2cBus, err2 := i2creg.Open(*i2cName)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
defer func() {
|
||||
if err2 := i2cBus.Close(); err == nil {
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
|
||||
spiBus, err2 := spireg.Open(*spiName)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
defer func() {
|
||||
if err2 := spiBus.Close(); err == nil {
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
|
||||
var dc gpio.PinOut
|
||||
if len(*dcName) != 0 {
|
||||
dc = gpioreg.ByName(*dcName)
|
||||
}
|
||||
if !*record {
|
||||
return s.run(i2cBus, spiBus, dc, *w, *h, *rotated)
|
||||
}
|
||||
|
||||
i2cRecorder := i2ctest.Record{Bus: i2cBus}
|
||||
spiRecorder := spitest.Record{Conn: spiBus}
|
||||
err = s.run(&i2cRecorder, &spiRecorder, dc, *w, *h, *rotated)
|
||||
if len(i2cRecorder.Ops) != 0 {
|
||||
fmt.Printf("I²C recorder Addr: 0x%02X\n", i2cRecorder.Ops[0].Addr)
|
||||
} else {
|
||||
fmt.Print("I²C recorder\n")
|
||||
}
|
||||
for _, op := range i2cRecorder.Ops {
|
||||
fmt.Print(" Write: ")
|
||||
for i, b := range op.Write {
|
||||
if i != 0 {
|
||||
fmt.Print(", ")
|
||||
}
|
||||
fmt.Printf("0x%02X", b)
|
||||
}
|
||||
fmt.Print("\n Read: ")
|
||||
for i, b := range op.Read {
|
||||
if i != 0 {
|
||||
fmt.Print(", ")
|
||||
}
|
||||
fmt.Printf("0x%02X", b)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
}
|
||||
fmt.Print("\nSPI recorder\n")
|
||||
for _, op := range spiRecorder.Ops {
|
||||
fmt.Print(" Write: ")
|
||||
if len(op.Read) != 0 {
|
||||
// Read data.
|
||||
fmt.Printf("0x%02X\n Read: ", op.Write[0])
|
||||
// first byte is dummy.
|
||||
for i, b := range op.Read[1:] {
|
||||
if i != 0 {
|
||||
fmt.Print(", ")
|
||||
}
|
||||
fmt.Printf("0x%02X", b)
|
||||
}
|
||||
} else {
|
||||
// Write-only command.
|
||||
for i, b := range op.Write {
|
||||
if i != 0 {
|
||||
fmt.Print(", ")
|
||||
}
|
||||
fmt.Printf("0x%02X", b)
|
||||
}
|
||||
fmt.Print("\n Read: ")
|
||||
}
|
||||
fmt.Print("\n")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SmokeTest) run(i2cBus i2c.Bus, spiBus spi.ConnCloser, dc gpio.PinOut, w, h int, rotated bool) (err error) {
|
||||
s.timings = make([]time.Duration, 2)
|
||||
start := time.Now()
|
||||
i2cDev, err2 := ssd1306.NewI2C(i2cBus, w, h, rotated)
|
||||
s.timings[0] = time.Since(start)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
start = time.Now()
|
||||
spiDev, err2 := ssd1306.NewSPI(spiBus, dc, w, h, rotated)
|
||||
s.timings[1] = time.Since(start)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
|
||||
s.devices = []*ssd1306.Dev{i2cDev, spiDev}
|
||||
fmt.Printf("%s: Devices: %s, %s\n", s, s.devices[0], s.devices[1])
|
||||
s.printStr("NewXXX() durations")
|
||||
|
||||
// Preparations.
|
||||
imgBunnyNRGBA, err := gif.Decode(bytes.NewReader(bunny))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Right format but not the right size.
|
||||
imgBunny1bit := image1bit.NewVerticalLSB(imgBunnyNRGBA.Bounds())
|
||||
draw.Src.Draw(imgBunny1bit, imgBunnyNRGBA.Bounds(), imgBunnyNRGBA, image.Point{})
|
||||
// Right format, right size
|
||||
imgBunny1bitLarge := image1bit.NewVerticalLSB(i2cDev.Bounds())
|
||||
center := imgBunny1bit.Bounds()
|
||||
draw.Src.Draw(imgBunny1bitLarge, center.Add(image.Point{X: (w - center.Dx()) / 2}), imgBunny1bit, image.Point{})
|
||||
imgClear := make([]byte, w*h/8)
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if _, err := d.Write(imgClear); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.printStr("Clear")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
d.Draw(d.Bounds(), imgBunnyNRGBA, image.Point{})
|
||||
if err := d.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Bunny NRGBA")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
d.Draw(d.Bounds(), imgBunny1bitLarge, image.Point{})
|
||||
if err := d.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Bunny image1bit exact frame")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
d.Draw(d.Bounds(), imgBunny1bit, image.Point{})
|
||||
if err := d.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Bunny image1bit partial draw")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Scroll(ssd1306.Left, ssd1306.FrameRate2, 0, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Scroll left: rate = 2")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Scroll(ssd1306.Right, ssd1306.FrameRate25, 0, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Scroll right: rate = 25")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Scroll(ssd1306.UpLeft, ssd1306.FrameRate5, 0, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Scroll up left: rate = 5")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Scroll(ssd1306.UpRight, ssd1306.FrameRate128, 0, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Scroll up right: rate = 128")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Scroll(ssd1306.Left, ssd1306.FrameRate2, 0, 16); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Split scroll top 16 pixels")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Scroll(ssd1306.Right, ssd1306.FrameRate2, 16, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Split scroll 16-64 pixels")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.StopScroll(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Stop scroll")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
d.Draw(d.Bounds(), imgBunny1bitLarge, image.Point{})
|
||||
if err := d.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Redraw")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.SetContrast(0); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Contrast min")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.SetContrast(0xFF); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Contrast max")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Invert(true); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Invert")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Invert(false); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Restore")
|
||||
|
||||
imgStripes := broadStripes(w, h)
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if _, err := d.Write(imgStripes); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("broad stripes: testing raw Write()")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Halt(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Off")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Invert(false); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("On")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if _, err := d.Write(imgClear); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.printStr("Clear")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if _, err := d.Write(imgClear); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.printStr("Clear (redundant)")
|
||||
|
||||
imgPattern := binaryPattern(w, h)
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if _, err := d.Write(imgPattern); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Fill display with binary 0..255 pattern")
|
||||
|
||||
imgPattern[w+h/2] ^= 0x10
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if _, err := d.Write(imgPattern); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Update a single pixel on second band")
|
||||
|
||||
bmp := image1bit.NewVerticalLSB(i2cDev.Bounds())
|
||||
copy(bmp.Pix, imgPattern)
|
||||
drawText(bmp, "periph.io", 1)
|
||||
drawText(bmp, "is awesome!", 0)
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
d.Draw(d.Bounds(), bmp, image.Point{})
|
||||
if err := d.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Draw text")
|
||||
|
||||
for i, d := range s.devices {
|
||||
start := time.Now()
|
||||
if err := d.Halt(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.timings[i] = time.Since(start)
|
||||
}
|
||||
s.step("Halt")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SmokeTest) printStr(str string) {
|
||||
fmt.Printf("%s: %-50s:", s, str)
|
||||
for i, t := range s.timings {
|
||||
if i != 0 {
|
||||
fmt.Print(",")
|
||||
}
|
||||
fmt.Printf(" %s", round(t))
|
||||
}
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
func (s *SmokeTest) step(str string) {
|
||||
s.printStr(str)
|
||||
time.Sleep(s.delay)
|
||||
}
|
||||
|
||||
// broadStripes() returns an image using a raw array. Each byte corresponds to 8
|
||||
// vertical pixels, and then the array scans horizontally and down.
|
||||
func broadStripes(w, h int) []byte {
|
||||
img := make([]byte, w*h/8)
|
||||
for y := 0; y < 8; y++ {
|
||||
// Horizontal stripes.
|
||||
for x := 0; x < 64; x++ {
|
||||
img[x+128*y] = byte((y & 1) * 0xff)
|
||||
}
|
||||
// Vertical stripes.
|
||||
for x := 64; x < 128; x++ {
|
||||
img[x+128*y] = byte(((x / 8) & 1) * 0xff)
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func binaryPattern(w, h int) []byte {
|
||||
img := make([]byte, w*h/8)
|
||||
for i := 0; i < len(img); i++ {
|
||||
img[i] = 0
|
||||
}
|
||||
for i := 0; i < 256; i++ {
|
||||
offset := i % w
|
||||
band := ((i / w) * w) * 2
|
||||
img[band+offset] = byte(i)
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// round returns the duration rounded in µs.
|
||||
func round(d time.Duration) string {
|
||||
µs := (d + time.Microsecond/2) / time.Microsecond
|
||||
ms := µs / 1000
|
||||
µs %= 1000
|
||||
return fmt.Sprintf("%3d.%03dms", ms, µs)
|
||||
}
|
||||
|
||||
// drawText draws text at the bottom right of img.
|
||||
func drawText(img draw.Image, text string, lastToBottom int) {
|
||||
f := basicfont.Face7x13
|
||||
advance := font.MeasureString(f, text).Ceil()
|
||||
bounds := img.Bounds()
|
||||
if advance > bounds.Dx() {
|
||||
advance = 0
|
||||
} else {
|
||||
advance = bounds.Dx() - advance
|
||||
}
|
||||
drawer := font.Drawer{
|
||||
Dst: img,
|
||||
Src: &image.Uniform{image1bit.On},
|
||||
Face: f,
|
||||
Dot: fixed.P(advance, bounds.Dy()-1-f.Descent-lastToBottom*f.Height),
|
||||
}
|
||||
drawer.DrawString(text)
|
||||
}
|
||||
Loading…
Reference in New Issue