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
Marc-Antoine Ruel 9 years ago
parent 722b0f8824
commit daad425c63

@ -2,26 +2,27 @@
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
// Package ssd1306 controls a 128x64 monochrome OLED display via a ssd1306
// controler.
// Package ssd1306 controls a 128x64 monochrome OLED display via a SSD1306
// controller.
//
// The SSD1306 is a write-only device. It can be driven on either I²C or SPI.
// Changing between protocol is likely done through resistor soldering, for
// boards that support both.
// The driver does differential updates: it only sends modified pixels for the
// smallest rectangle, to economize bus bandwidth. This is especially important
// when using I²C as the bus default speed (often 100kHz) is slow enough to
// saturate the bus at less than 10 frames per second.
//
// Known issue
// The SSD1306 is a write-only device. It can be driven on either I²C or SPI
// with 4 wires. Changing between protocol is likely done through resistor
// soldering, for boards that support both.
//
// The SPI version of this driver is not functional. To interface with the ssd1306
// in 3-wire SPI mode each byte must be transmitted using 9 bits where the 9th bit
// discriminates between command & data. To interface using 4-wire SPI a separate
// gpio is needed to drive a c/d input. Neither of these two mechanisms have been
// implemented yet.
// For more info, see
// https://drive.google.com/file/d/0B5lkVYnewKTGYzhyWWp0clBMR1E/view
// pages 17-18 (8.1.3, 8.1.4).
// Some boards expose a RES / Reset pin. If present, it must be normally be
// High. When set to Low (Ground), it enables the reset circuitry. It can be
// used externally to this driver, if used, the driver must be reinstantiated.
//
// Datasheets
//
// Product page:
// http://www.solomon-systech.com/en/product/display-ic/oled-driver-controller/ssd1306/
//
// https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
//
// "DM-OLED096-624": https://drive.google.com/file/d/0B5lkVYnewKTGaEVENlYwbDkxSGM/view
@ -34,13 +35,15 @@ package ssd1306
// https://learn.adafruit.com/ssd1306-oled-displays-with-raspberry-pi-and-beaglebone-black?view=all
import (
"bytes"
"errors"
"fmt"
"image"
"image/color"
"log"
"image/draw"
"periph.io/x/periph/conn"
"periph.io/x/periph/conn/gpio"
"periph.io/x/periph/conn/i2c"
"periph.io/x/periph/conn/spi"
"periph.io/x/periph/devices"
@ -50,7 +53,8 @@ import (
// FrameRate determines scrolling speed.
type FrameRate byte
// Possible frame rates.
// Possible frame rates. The value determines the number of refreshes between
// movement. The lower value, the higher speed.
const (
FrameRate2 FrameRate = 7
FrameRate3 FrameRate = 4
@ -73,49 +77,109 @@ const (
UpLeft Orientation = 0x2A
)
// Dev is an open handle to the display controler.
// Dev is an open handle to the display controller.
type Dev struct {
c conn.Conn
W int
H int
// Communication
c conn.Conn
dc gpio.PinOut
spi bool
// Display size controlled by the SSD1306.
rect image.Rectangle
// Mutable
// See page 25 for the GDDRAM pages structure.
// Narrow screen will waste the end of each page.
// Short screen will ignore the lower pages.
// There is 8 pages, each covering an horizontal band of 8 pixels high (1
// byte) for 128 bytes.
// 8*128 = 1024 bytes total for 128x64 display.
buffer []byte
// next is lazy initialized on first Draw(). Write() skips this buffer.
next *image1bit.VerticalLSB
startPage, endPage int
startCol, endCol int
scrolled bool
halted bool
err error
}
// NewSPI returns a Dev object that communicates over SPI to SSD1306 display
// controler.
// NewSPI returns a Dev object that communicates over SPI to a SSD1306 display
// controller.
//
// If rotated, turns the display by 180°
// If rotated is true, turns the display by 180°
//
// The SSD1306 can operate at up to 3.3Mhz, which is much higher than I²C. This
// permits higher refresh rates.
//
// Wiring
//
// It's up to the caller to use the RES (reset) pin if desired. Simpler
// connection is to connect RES and DC to ground, CS to 3.3v, SDA to MOSI, SCK
// to SCLK.
// Connect SDA to MOSI, SCK to SCLK, CS to CS.
//
func NewSPI(s spi.Conn, w, h int, rotated bool) (*Dev, error) {
if err := s.DevParams(3300000, spi.Mode3, 8); err != nil {
// In 3-wire SPI mode, pass nil for 'dc'. In 4-wire SPI mode, pass a GPIO pin
// to use.
//
// The RES (reset) pin can be used outside of this driver but is not supported
// natively. In case of external reset via the RES pin, this device drive must
// be reinstantiated.
func NewSPI(s spi.Conn, dc gpio.PinOut, w, h int, rotated bool) (*Dev, error) {
if dc == gpio.INVALID {
return nil, errors.New("ssd1306: use nil for dc to use 3-wire mode, do not use gpio.INVALID")
}
bits := 8
if dc == nil {
// 3-wire SPI uses 9 bits per word.
bits = 9
} else if err := dc.Out(gpio.Low); err != nil {
return nil, err
}
return newDev(s, w, h, rotated)
if err := s.DevParams(3300000, spi.Mode0, bits); err != nil {
return nil, err
}
return newDev(s, w, h, rotated, true, dc)
}
// NewI2C returns a Dev object that communicates over I²C to SSD1306 display
// controler.
// NewI2C returns a Dev object that communicates over I²C to a SSD1306 display
// controller.
//
// If rotated, turns the display by 180°
func NewI2C(i i2c.Bus, w, h int, rotated bool) (*Dev, error) {
// Maximum clock speed is 1/2.5µs = 400KHz.
return newDev(&i2c.Dev{Bus: i, Addr: 0x3C}, w, h, rotated)
return newDev(&i2c.Dev{Bus: i, Addr: 0x3C}, w, h, rotated, false, nil)
}
// newDev is the common initialization code that is independent of the bus
// being used.
func newDev(c conn.Conn, w, h int, rotated bool) (*Dev, error) {
func newDev(c conn.Conn, w, h int, rotated, usingSPI bool, dc gpio.PinOut) (*Dev, error) {
if w < 8 || w > 128 || w&7 != 0 {
return nil, fmt.Errorf("ssd1306: invalid width %d", w)
}
if h < 8 || h > 64 || h&7 != 0 {
return nil, fmt.Errorf("ssd1306: invalid height %d", h)
}
d := &Dev{c: c, W: w, H: h}
nbPages := h / 8
pageSize := w
d := &Dev{
c: c,
spi: usingSPI,
dc: dc,
rect: image.Rect(0, 0, int(w), int(h)),
buffer: make([]byte, nbPages*pageSize),
startPage: 0,
endPage: nbPages,
startCol: 0,
endCol: w,
// Signal that the screen must be redrawn on first draw().
scrolled: true,
}
if err := d.sendCommand(getInitCmd(w, h, rotated)); err != nil {
return nil, err
}
return d, nil
}
func getInitCmd(w, h int, rotated bool) []byte {
// Set COM output scan direction; C0 means normal; C8 means reversed
comScan := byte(0xC8)
// See page 40.
@ -125,193 +189,302 @@ func newDev(c conn.Conn, w, h int, rotated bool) (*Dev, error) {
comScan = 0xC0
columnAddr = byte(0xA0)
}
// Set the max frequency. The problem with I²C is that it creates visible
// tear down. On SPI at high speed this is not visible. Page 23 pictures how
// to avoid tear down. For now default to max frequency.
freq := byte(0xF0)
// Initialize the device by fully resetting all values.
// https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
// Page 64 has the full recommended flow.
// Page 28 lists all the commands.
// Some values come from the DM-OLED096 datasheet p15.
init := []byte{
i2cCmd,
return []byte{
0xAE, // Display off
0xD3, 0x00, // Set display offset; 0
0x40, // Start display start line; 0
columnAddr, // Set segment remap; RESET is column 127.
comScan,
comScan, //
0xDA, 0x12, // Set COM pins hardware configuration; see page 40
0x81, 0xff, // Set max contrast
0x81, 0xFF, // Set max contrast
0xA4, // Set display to use GDDRAM content
0xA6, // Set normal display (0xA7 for inverted 0=lit, 1=dark)
0xD5, 0x80, // Set osc frequency and divide ratio; power on reset value is 0x3F.
0xD5, freq, // Set osc frequency and divide ratio; power on reset value is 0x80.
0x8D, 0x14, // Enable charge pump regulator; page 62
0xD9, 0xf1, // Set pre-charge period; from adafruit driver
0xD9, 0xF1, // Set pre-charge period; from adafruit driver
0xDB, 0x40, // Set Vcomh deselect level; page 32
0x2E, // Deactivate scroll
0xA8, byte(h - 1), // Set multiplex ratio (number of lines to display)
0x20, 0x00, // Set memory addressing mode to horizontal
0xB0, // Set page start address
0x2E, // Deactivate scroll
0x00, // Set column offset (lower nibble)
0x10, // Set column offset (higher nibble)
0xA8, byte(d.H - 1), // Set multiplex ratio (number of lines to display)
0x21, 0, uint8(w - 1), // Set column address (Width)
0x22, 0, uint8(h/8 - 1), // Set page address (Pages)
0xAF, // Display on
}
if err := d.c.Tx(init, nil); err != nil {
return nil, err
}
}
return d, nil
func (d *Dev) String() string {
if d.spi {
return fmt.Sprintf("ssd1360.Dev{%s, %s, %s}", d.c, d.dc, d.rect.Max)
}
return fmt.Sprintf("ssd1360.Dev{%s, %s}", d.c, d.rect.Max)
}
// ColorModel implements devices.Display. It is a one bit color model.
// ColorModel implements devices.Display.
//
// It is a one bit color model, as implemented by image1bit.Bit.
func (d *Dev) ColorModel() color.Model {
return color.NRGBAModel
return image1bit.BitModel
}
// Bounds implements devices.Display. Min is guaranteed to be {0, 0}.
func (d *Dev) Bounds() image.Rectangle {
return image.Rectangle{Max: image.Point{X: d.W, Y: d.H}}
}
func colorToBit(c color.Color) byte {
r, g, b, a := c.RGBA()
if (r|g|b) >= 0x8000 && a >= 0x4000 {
return 1
}
return 0
return d.rect
}
// Draw implements devices.Display.
//
// BUG(maruel): It discards any failure. Change devices.Display interface?
// BUG(maruel): Support r.Min.Y and r.Max.Y not divisible by 8.
// BUG(maruel): Support sp.Y not divisible by 8.
// It draws synchronously, once this function returns, the display is updated.
// It means that on slow bus (I²C), it may be preferable to defer Draw() calls
// to a background goroutine.
//
// It discards any failure.
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
r = r.Intersect(d.Bounds())
srcR := src.Bounds()
srcR.Min = srcR.Min.Add(sp)
if dX := r.Dx(); dX < srcR.Dx() {
srcR.Max.X = srcR.Min.X + dX
}
if dY := r.Dy(); dY < srcR.Dy() {
srcR.Max.Y = srcR.Min.Y + dY
}
// Take 8 lines at a time.
deltaX := r.Min.X - srcR.Min.X
deltaY := r.Min.Y - srcR.Min.Y
var pixels []byte
if img, ok := src.(*image1bit.VerticalLSB); ok {
if srcR.Min.X == 0 && srcR.Dx() == d.W && srcR.Min.Y == 0 && srcR.Dy() == d.H {
// Fast path.
pixels = img.Pix
}
}
if pixels == nil {
pixels = make([]byte, d.W*d.H/8)
for sY := srcR.Min.Y; sY < srcR.Max.Y; sY += 8 {
rY := ((sY + deltaY) / 8) * d.W
for sX := srcR.Min.X; sX < srcR.Max.X; sX++ {
rX := sX + deltaX
c0 := colorToBit(src.At(sX, sY))
c1 := colorToBit(src.At(sX, sY+1)) << 1
c2 := colorToBit(src.At(sX, sY+2)) << 2
c3 := colorToBit(src.At(sX, sY+3)) << 3
c4 := colorToBit(src.At(sX, sY+4)) << 4
c5 := colorToBit(src.At(sX, sY+5)) << 5
c6 := colorToBit(src.At(sX, sY+6)) << 6
c7 := colorToBit(src.At(sX, sY+7)) << 7
pixels[rX+rY] = c0 | c1 | c2 | c3 | c4 | c5 | c6 | c7
}
var next []byte
if img, ok := src.(*image1bit.VerticalLSB); ok && r == d.Bounds() && src.Bounds() == d.rect && sp.X == 0 && sp.Y == 0 {
// Exact size, full frame, image1bit encoding: fast path!
next = img.Pix
} else {
// Double buffering.
if d.next == nil {
d.next = image1bit.NewVerticalLSB(d.rect)
}
next = d.next.Pix
draw.Src.Draw(d.next, r, src, sp)
}
if _, err := d.Write(pixels); err != nil {
log.Printf("ssd1306: Draw failed: %v", err)
}
d.err = d.drawInternal(next)
}
// Err returns the last error that occurred
func (d *Dev) Err() error {
return d.err
}
// Write writes a buffer of pixels to the display.
//
// The format is unsual as each byte represent 8 vertical pixels at a time. So
// the memory is effectively horizontal bands of 8 pixels high.
// The format is unsual as each byte represent 8 vertical pixels at a time. The
// format is horizontal bands of 8 pixels high.
//
// This function accepts the content of image1bit.VerticalLSB.Pix.
func (d *Dev) Write(pixels []byte) (int, error) {
if len(pixels) != d.H*d.W/8 {
return 0, errors.New("ssd1306: invalid pixel stream")
}
// Run as 2 big transactions to reduce downtime on the bus.
// First tx is commands, second is data.
// The following commands should not be needed, but then if the ssd1306 gets out of sync
// for some reason the display ends up messed-up. Given the small overhead compared to
// sending all the data might as well reset things a bit.
hdr := []byte{
i2cCmd,
0xB0, // Set page start addr just in case
0x00, 0x10, // Set column start addr, lower & upper nibble
0x20, 0x00, // Ensure addressing mode is horizontal
0x21, 0x00, byte(d.W - 1), // Set start/end column
0x22, 0x00, byte(d.H/8 - 1), // Set start/end page
if len(pixels) != len(d.buffer) {
return 0, fmt.Errorf("ssd1306: invalid pixel stream length; expected %d bytes, got %d bytes", len(d.buffer), len(pixels))
}
if err := d.c.Tx(hdr, nil); err != nil {
// Write() skips d.next so it saves 1kb of RAM.
if err := d.drawInternal(pixels); err != nil {
return 0, err
}
// Write the data.
if err := d.c.Tx(append([]byte{i2cData}, pixels...), nil); err != nil {
return 0, err
}
return len(pixels), nil
}
// Scroll scrolls the entire screen.
func (d *Dev) Scroll(o Orientation, rate FrameRate) error {
// TODO(maruel): Allow to specify page.
// TODO(maruel): Allow to specify offset.
// Scroll scrolls an horizontal band.
//
// Only one scrolling operation can happen at a time.
//
// Both startLine and endLine must be multiples of 8.
//
// Use -1 for endLine to extend to the bottom of the display.
func (d *Dev) Scroll(o Orientation, rate FrameRate, startLine, endLine int) error {
h := d.rect.Dy()
if endLine == -1 {
endLine = h
}
if startLine >= endLine {
return fmt.Errorf("startLine (%d) must be lower than endLine (%d)", startLine, endLine)
}
if startLine&7 != 0 || startLine < 0 || startLine >= h {
return fmt.Errorf("invalid startLine %d", startLine)
}
if endLine&7 != 0 || endLine < 0 || endLine > h {
return fmt.Errorf("invalid endLine %d", endLine)
}
startPage := uint8(startLine / 8)
endPage := uint8(endLine / 8)
d.scrolled = true
if o == Left || o == Right {
// page 28
// STOP, <op>, dummy, <start page>, <rate>, <end page>, <dummy>, <dummy>, <ENABLE>
return d.c.Tx([]byte{i2cCmd, 0x2E, byte(o), 0x00, 0x00, byte(rate), 0x07, 0x00, 0xFF, 0x2F}, nil)
// <op>, dummy, <start page>, <rate>, <end page>, <dummy>, <dummy>, <ENABLE>
return d.sendCommand([]byte{byte(o), 0x00, startPage, byte(rate), endPage - 1, 0x00, 0xFF, 0x2F})
}
// page 29
// STOP, <op>, dummy, <start page>, <rate>, <end page>, <offset>, <ENABLE>
// <op>, dummy, <start page>, <rate>, <end page>, <offset>, <ENABLE>
// page 30: 0xA3 permits to set rows for scroll area.
return d.c.Tx([]byte{i2cCmd, 0x2E, byte(o), 0x00, 0x00, byte(rate), 0x07, 0x01, 0x2F}, nil)
return d.sendCommand([]byte{byte(o), 0x00, startPage, byte(rate), endPage - 1, 0x01, 0x2F})
}
// StopScroll stops any scrolling previously set.
//
// It will only take effect after redrawing the ram.
// StopScroll stops any scrolling previously set and resets the screen.
func (d *Dev) StopScroll() error {
return d.c.Tx([]byte{i2cCmd, 0x2E}, nil)
return d.sendCommand([]byte{0x2E})
}
// SetContrast changes the screen contrast.
//
// Note: values other than 0xff do not seem useful...
func (d *Dev) SetContrast(level byte) error {
return d.c.Tx([]byte{i2cCmd, 0x81, level}, nil)
return d.sendCommand([]byte{0x81, level})
}
// Enable or disable the display.
func (d *Dev) Enable(on bool) error {
b := byte(0xAE)
if on {
b = 0xAF
// Halt turns off the display.
//
// Sending any other command afterward reenables the display.
func (d *Dev) Halt() error {
d.halted = false
err := d.sendCommand([]byte{0xAE})
if err == nil {
d.halted = true
}
return d.c.Tx([]byte{i2cCmd, b}, nil)
return err
}
// Invert the display (black on white vs white on black).
func (d *Dev) Invert(blackOnWhite bool) error {
b := byte(0xA6)
b := []byte{0xA6}
if blackOnWhite {
b = 0xA7
b[0] = 0xA7
}
return d.sendCommand(b)
}
//
func (d *Dev) calculateSubset(next []byte) (int, int, int, int, bool) {
w := d.rect.Dx()
h := d.rect.Dy()
startPage := 0
endPage := h / 8
startCol := 0
endCol := w
if d.scrolled {
// Painting disable scrolling but if scrolling was enabled, this requires a
// full screen redraw.
d.scrolled = false
} else {
// Calculate the smallest square that need to be sent.
pageSize := w
// Top.
for ; startPage < endPage; startPage++ {
x := pageSize * startPage
y := pageSize * (startPage + 1)
if !bytes.Equal(d.buffer[x:y], next[x:y]) {
break
}
}
// Bottom.
for ; endPage > startPage; endPage-- {
x := pageSize * (endPage - 1)
y := pageSize * endPage
if !bytes.Equal(d.buffer[x:y], next[x:y]) {
break
}
}
if startPage == endPage {
// Early exit, the image is exactly the same.
return 0, 0, 0, 0, true
}
// TODO(maruel): This currently corrupts the screen. Likely a small error
// in the way the commands are sent.
/*
// Left.
for ; startCol < endCol; startCol++ {
for i := startPage; i < endPage; i++ {
x := i*pageSize + startCol
if d.buffer[x] != next[x] {
goto breakLeft
}
}
}
breakLeft:
// Right.
for ; endCol > startCol; endCol-- {
for i := startPage; i < endPage; i++ {
x := i*pageSize + endCol - 1
if d.buffer[x] != next[x] {
goto breakRight
}
}
}
breakRight:
*/
}
return startPage, endPage, startCol, endCol, false
}
// drawInternal sends image data to the controller.
func (d *Dev) drawInternal(next []byte) error {
startPage, endPage, startCol, endCol, skip := d.calculateSubset(next)
if skip {
return nil
}
copy(d.buffer, next)
if d.startPage != startPage || d.endPage != endPage || d.startCol != startCol || d.endCol != endCol {
d.startPage = startPage
d.endPage = endPage
d.startCol = startCol
d.endCol = endCol
cmd := []byte{
0x21, uint8(d.startCol), uint8(d.endCol - 1), // Set column address (Width)
0x22, uint8(d.startPage), uint8(d.endPage - 1), // Set page address (Pages)
}
if err := d.sendCommand(cmd); err != nil {
return err
}
}
// Write the subset of the data as needed.
pageSize := d.rect.Dx()
return d.sendData(d.buffer[startPage*pageSize+startCol : (endPage-1)*pageSize+endCol])
}
func (d *Dev) sendData(c []byte) error {
if d.halted {
// Transparently enable the display.
if err := d.sendCommand(nil); err != nil {
return err
}
}
if d.spi {
// 4-wire SPI.
if err := d.dc.Out(gpio.High); err != nil {
return err
}
return d.c.Tx(c, nil)
}
return d.c.Tx(append([]byte{i2cData}, c...), nil)
}
func (d *Dev) sendCommand(c []byte) error {
if d.halted {
// Transparently enable the display.
c = append([]byte{0xAF}, c...)
d.halted = false
}
if d.spi {
if d.dc == nil {
// 3-wire SPI.
return errors.New("ssd1306: 3-wire SPI mode is not yet implemented")
}
// 4-wire SPI.
if err := d.dc.Out(gpio.Low); err != nil {
return err
}
return d.c.Tx(c, nil)
}
return d.c.Tx([]byte{i2cCmd, b}, nil)
return d.c.Tx(append([]byte{i2cCmd}, c...), nil)
}
const (
i2cCmd = 0x00 // i2c transaction has stream of command bytes
i2cData = 0x40 // i2c transaction has stream of data bytes
i2cCmd = 0x00 // I²C transaction has stream of command bytes
i2cData = 0x40 // I²C transaction has stream of data bytes
)
var _ devices.Display = &Dev{}

@ -5,6 +5,7 @@
package ssd1306
import (
"errors"
"image"
"image/color"
"log"
@ -14,58 +15,381 @@ import (
"golang.org/x/image/font/basicfont"
"golang.org/x/image/math/fixed"
"periph.io/x/periph/conn/conntest"
"periph.io/x/periph/conn/gpio"
"periph.io/x/periph/conn/gpio/gpiotest"
"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/spitest"
"periph.io/x/periph/devices/ssd1306/image1bit"
)
func TestDrawGray(t *testing.T) {
// ssd1306 is a write-only device. This is simply a playback test to exercise
// the code a little.
func Example() {
bus, err := i2creg.Open("")
if err != nil {
log.Fatalf("failed to open I²C: %v", err)
}
defer bus.Close()
dev, err := NewI2C(bus, 128, 64, false)
if err != nil {
log.Fatalf("failed to initialize ssd1306: %v", err)
}
// Draw on it.
f := basicfont.Face7x13
img := image1bit.NewVerticalLSB(dev.Bounds())
drawer := font.Drawer{
Dst: img,
Src: &image.Uniform{image1bit.On},
Face: f,
Dot: fixed.P(0, img.Bounds().Dy()-1-f.Descent),
}
drawer.DrawString("Hello from periph!")
dev.Draw(dev.Bounds(), img, image.Point{})
if err := dev.Err(); err != nil {
log.Fatal(err)
}
}
//
func TestNewI2C_fail(t *testing.T) {
bus := i2ctest.Playback{DontPanic: true}
if d, err := NewI2C(&bus, 0, 64, false); d != nil || err == nil {
t.Fatal(d, err)
}
if d, err := NewI2C(&bus, 64, 0, false); d != nil || err == nil {
t.Fatal(d, err)
}
if d, err := NewI2C(&bus, 64, 64, true); d != nil || err == nil {
t.Fatal(d, err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_ColorModel(t *testing.T) {
bus := getI2CPlayback()
dev, err := NewI2C(bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
if c := dev.ColorModel(); c != image1bit.BitModel {
t.Fatal(c)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_String(t *testing.T) {
bus := getI2CPlayback()
dev, err := NewI2C(bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
expected := "ssd1360.Dev{playback(60), (128,64)}"
if s := dev.String(); s != expected {
t.Fatalf("%q != %q", expected, s)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_Draw_VerticalLSD_fast(t *testing.T) {
// Exercise the fast path.
buf := make([]byte, 1025)
buf[0] = i2cData
buf[23] = 1
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
// Startup initialization.
{Addr: 0x3c, Write: initCmdI2C()},
// Actual draw buffer.
{Addr: 0x3c, Write: buf},
},
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
img := image1bit.NewVerticalLSB(dev.Bounds())
img.Pix[22] = 1
dev.Draw(dev.Bounds(), img, image.Point{})
if err := dev.Err(); err != nil {
t.Fatal(err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_Halt_Write(t *testing.T) {
// Exercise the fast path.
buf := make([]byte, 1025)
buf[0] = i2cData
buf[23] = 1
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
// Startup initialization.
{Addr: 0x3c, Write: initCmdI2C()},
// Halt()
{Addr: 0x3c, Write: []byte{0x0, 0xae}},
// transparent resume
{Addr: 0x3c, Write: []byte{0x0, 0xaf}},
// Actual draw buffer.
{Addr: 0x3c, Write: buf},
},
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
if err := dev.Halt(); err != nil {
t.Fatal(err)
}
pix := make([]byte, 1024)
pix[22] = 1
if n, err := dev.Write(pix); n != len(pix) || err != nil {
t.Fatal(n, err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_Halt_resume_fail(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
// Startup initialization.
{Addr: 0x3c, Write: initCmdI2C()},
// Halt()
{Addr: 0x3c, Write: []byte{0x0, 0xae}},
},
DontPanic: true,
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
if err := dev.Halt(); err != nil {
t.Fatal(err)
}
if n, err := dev.Write(make([]byte, 1024)); n != 0 || !conntest.IsErr(err) {
t.Fatalf("expected conntest error: %v", err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_Write_invalid_size(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
// Startup initialization.
{Addr: 0x3c, Write: initCmdI2C()},
},
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
if n, err := dev.Write([]byte{1}); n != 0 || err == nil {
t.Fatal("expected failure")
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_Write_fail(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
// Startup initialization.
{Addr: 0x3c, Write: initCmdI2C()},
},
DontPanic: true,
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
if n, err := dev.Write(make([]byte, 1024)); n != 0 || !conntest.IsErr(err) {
t.Fatalf("expected conntest error: %v", err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_Draw_fail(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
// Startup initialization.
{Addr: 0x3c, Write: initCmdI2C()},
},
DontPanic: true,
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{})
if err := dev.Err(); !conntest.IsErr(err) {
t.Fatalf("expected conntest error: %v", err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_DrawGray(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
// Startup initialization.
{Addr: 0x3c, Write: initline},
// Preparation to draw.
{Addr: 0x3c, Write: prelude},
{Addr: 0x3c, Write: initCmdI2C()},
// Actual draw buffer.
{Addr: 0x3c, Write: grayCheckboardWrite},
{Addr: 0x3c, Write: append([]byte{i2cData}, grayCheckboard()...)},
},
}
// To record, use the following instead:
//bus := i2ctest.Record{}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{0, 0})
if err := dev.Err(); err != nil {
t.Fatal(err)
}
// No-op (skip path).
dev.Draw(dev.Bounds(), makeGrayCheckboard(dev.Bounds()), image.Point{0, 0})
if err := dev.Err(); err != nil {
t.Fatal(err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
//pretty.Printf("%# v\n", bus)
}
func TestDraw1D(t *testing.T) {
func TestI2C_Scroll(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x3c, Write: initline},
{Addr: 0x3c, Write: prelude},
{Addr: 0x3c, Write: grayCheckboardWrite},
{Addr: 0x3c, Write: initCmdI2C()},
// Scroll Left.
{Addr: 0x3c, Write: []byte{0x0, 0x27, 0x0, 0x0, 0x6, 0x7, 0x0, 0xff, 0x2f}},
// Scroll UpRight.
{Addr: 0x3c, Write: []byte{0x0, 0x29, 0x0, 0x0, 0x6, 0x0, 0x1, 0x2f}},
// StopScroll.
{Addr: 0x3c, Write: []byte{0x0, 0x2e}},
},
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
bounds := dev.Bounds()
gray := makeGrayCheckboard(bounds)
img := image1bit.NewVerticalLSB(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
img.Set(x, y, gray.At(x, y))
}
if dev.Scroll(Left, FrameRate25, 1, 8) == nil {
t.Fatal("invalid start")
}
if dev.Scroll(Left, FrameRate25, 8, 0) == nil {
t.Fatal("reversed start and end")
}
if dev.Scroll(Left, FrameRate25, 0, 9) == nil {
t.Fatal("invalid end")
}
if err := dev.Scroll(Left, FrameRate25, 0, -1); err != nil {
t.Fatal(err)
}
if err := dev.Scroll(UpRight, FrameRate25, 0, 8); err != nil {
t.Fatal(err)
}
if err := dev.StopScroll(); err != nil {
t.Fatal(err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_SetContrast(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x3c, Write: initCmdI2C()},
{Addr: 0x3c, Write: []byte{0x0, 0x81, 0x0}},
{Addr: 0x3c, Write: []byte{0x0, 0x81, 0x7f}},
{Addr: 0x3c, Write: []byte{0x0, 0x81, 0xff}},
},
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
if err := dev.SetContrast(0); err != nil {
t.Fatal(err)
}
if err := dev.SetContrast(127); err != nil {
t.Fatal(err)
}
if err := dev.SetContrast(255); err != nil {
t.Fatal(err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_Invert_Halt_resume(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x3c, Write: initCmdI2C()},
// Invert(true)
{Addr: 0x3c, Write: []byte{0x0, 0xa7}},
// Halt()
{Addr: 0x3c, Write: []byte{0x0, 0xae}},
// transparent resume + Invert(false)
{Addr: 0x3c, Write: []byte{0x0, 0xaf, 0xa6}},
},
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
if err := dev.Invert(true); err != nil {
t.Fatal(err)
}
if err := dev.Halt(); err != nil {
t.Fatal(err)
}
if err := dev.Invert(false); err != nil {
t.Fatal(err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestI2C_Halt(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x3c, Write: initCmdI2C()},
// Halt()
{Addr: 0x3c, Write: []byte{0x0, 0xae}},
// transparent resume + StopScroll()
{Addr: 0x3c, Write: []byte{0x0, 0xaf, 0x2e}},
},
DontPanic: true,
}
dev, err := NewI2C(&bus, 128, 64, false)
if err != nil {
t.Fatal(err)
}
if err := dev.Halt(); err != nil {
t.Fatal(err)
}
if err := dev.StopScroll(); err != nil {
t.Fatal(err)
}
dev.Draw(dev.Bounds(), img, image.Point{0, 0})
if err := bus.Close(); err != nil {
t.Fatal(err)
}
@ -73,32 +397,169 @@ func TestDraw1D(t *testing.T) {
//
func Example() {
bus, err := i2creg.Open("")
func TestNewSPI_fail(t *testing.T) {
if d, err := NewSPI(&spitest.Playback{}, nil, 0, 64, false); d != nil || err == nil {
t.Fatal(d, err)
}
if d, err := NewSPI(&configFail{}, nil, 64, 64, false); d != nil || err == nil {
t.Fatal(d, err)
}
if d, err := NewSPI(&spitest.Playback{}, gpio.INVALID, 64, 64, false); d != nil || err == nil {
t.Fatal(d, err)
}
if d, err := NewSPI(&spitest.Playback{}, &failPin{fail: true}, 64, 64, false); d != nil || err == nil {
t.Fatal(d, err)
}
}
func TestSPI_3wire(t *testing.T) {
// Not supported yet.
if dev, err := NewSPI(&spitest.Playback{}, nil, 128, 64, false); dev != nil || err == nil {
t.Fatal("SPI 3-wire is not supported")
}
}
func TestSPI_4wire_String(t *testing.T) {
bus := spitest.Playback{
Playback: conntest.Playback{
Ops: []conntest.IO{{Write: getInitCmd(128, 64, false)}},
},
}
dev, err := NewSPI(&bus, &gpiotest.Pin{N: "pin1", Num: 42}, 128, 64, false)
if err != nil {
log.Fatalf("failed to open I²C: %v", err)
t.Fatal(err)
}
defer bus.Close()
dev, err := NewI2C(bus, 128, 64, false)
expected := "ssd1360.Dev{playback, pin1(42), (128,64)}"
if s := dev.String(); s != expected {
t.Fatalf("%q != %q", expected, s)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestSPI_4wire_Write_differential(t *testing.T) {
buf1 := make([]byte, 1024)
buf1[130] = 1
buf2 := make([]byte, 128)
buf2[130-128] = 1
buf2[131-128] = 2
bus := spitest.Playback{
Playback: conntest.Playback{
Ops: []conntest.IO{
{Write: getInitCmd(128, 64, false)},
{Write: buf1},
// Reset to write only to the first page.
{Write: []byte{0x21, 0x0, 0x7f, 0x22, 0x1, 0x1}},
{Write: buf2},
},
},
}
dev, err := NewSPI(&bus, &gpiotest.Pin{N: "pin1", Num: 42}, 128, 64, false)
if err != nil {
log.Fatalf("failed to initialize ssd1306: %v", err)
t.Fatal(err)
}
pix := make([]byte, 1024)
pix[130] = 1
if n, err := dev.Write(pix); n != len(pix) || err != nil {
t.Fatal(n, err)
}
pix[131] = 2
if n, err := dev.Write(pix); n != len(pix) || err != nil {
t.Fatal(n, err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
// Draw on it.
f := basicfont.Face7x13
img := image1bit.NewVerticalLSB(dev.Bounds())
drawer := font.Drawer{
Dst: img,
Src: &image.Uniform{image1bit.On},
Face: f,
Dot: fixed.P(0, img.Bounds().Dy()-1-f.Descent),
func TestSPI_4wire_Write_differential_fail(t *testing.T) {
buf1 := make([]byte, 1024)
buf1[130] = 1
bus := spitest.Playback{
Playback: conntest.Playback{
Ops: []conntest.IO{
{Write: getInitCmd(128, 64, false)},
{Write: buf1},
},
DontPanic: true,
},
}
dev, err := NewSPI(&bus, &gpiotest.Pin{N: "pin1", Num: 42}, 128, 64, false)
if err != nil {
t.Fatal(err)
}
pix := make([]byte, 1024)
pix[130] = 1
if n, err := dev.Write(pix); n != len(pix) || err != nil {
t.Fatal(n, err)
}
pix[131] = 2
if n, err := dev.Write(pix); n != 0 || !conntest.IsErr(err) {
t.Fatalf("expected conntest error: %v", err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
}
func TestSPI_4wire_gpio_fail(t *testing.T) {
bus := spitest.Playback{
Playback: conntest.Playback{
Ops: []conntest.IO{
{Write: getInitCmd(128, 64, false)},
},
},
}
pin := &failPin{fail: false}
dev, err := NewSPI(&bus, pin, 128, 64, false)
if err != nil {
t.Fatal(err)
}
// GPIO suddenly fail.
pin.fail = true
if n, err := dev.Write(make([]byte, 1024)); n != 0 || err == nil || err.Error() != "injected error" {
t.Fatalf("expected gpio error: %v", err)
}
if err := dev.Halt(); err == nil || err.Error() != "injected error" {
t.Fatalf("expected gpio error: %v", err)
}
if err := bus.Close(); err != nil {
t.Fatal(err)
}
drawer.DrawString("Hello from periph!")
dev.Draw(dev.Bounds(), img, image.Point{})
}
//
func initCmdI2C() []byte {
return append([]byte{0}, getInitCmd(128, 64, false)...)
}
var preludeI2C = []byte{
0x0, 0x21, 0x0, 0x7f, 0x22, 0x0, 0x7,
}
func getI2CPlayback() *i2ctest.Playback {
return &i2ctest.Playback{
Ops: []i2ctest.IO{
// Startup initialization.
{Addr: 0x3c, Write: initCmdI2C()},
},
}
}
func grayCheckboard() []byte {
buf := make([]byte, 1024)
for i := range buf {
if i&1 == 0 {
buf[i] = 0xaa
} else {
buf[i] = 0x55
}
}
return buf
}
func makeGrayCheckboard(r image.Rectangle) image.Image {
img := image.NewGray(r)
c := color.Gray{255}
@ -112,81 +573,22 @@ func makeGrayCheckboard(r image.Rectangle) image.Image {
return img
}
var initline = []byte{
0x00,
0xae, 0xd3, 0x00, 0x40, 0xa1, 0xc8, 0xda, 0x12, 0x81, 0xff, 0xa4, 0xa6, 0xd5,
0x80, 0x8d, 0x14, 0xd9, 0xf1, 0xdb, 0x40, 0x20, 0x00, 0xb0, 0x2e, 0x00, 0x10,
0xa8, 0x3f, 0xaf,
}
var prelude = []byte{
0x0, 0xb0, 0x0, 0x10, 0x20, 0x0, 0x21, 0x0, 0x7f, 0x22, 0x0, 0x7,
}
var grayCheckboardWrite = []byte{
0x40,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55, 0xaa, 0x55,
type configFail struct {
spitest.Record
}
func (c *configFail) DevParams(maxHz int64, mode spi.Mode, bits int) error {
return errors.New("injected error")
}
type failPin struct {
gpiotest.Pin
fail bool
}
func (f *failPin) Out(l gpio.Level) error {
if f.fail {
return errors.New("injected error")
}
return nil
}

@ -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…
Cancel
Save