diff --git a/devices/ssd1306/ssd1306.go b/devices/ssd1306/ssd1306.go index cdc6a89..1744be6 100644 --- a/devices/ssd1306/ssd1306.go +++ b/devices/ssd1306/ssd1306.go @@ -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, , dummy, , , , , , - return d.c.Tx([]byte{i2cCmd, 0x2E, byte(o), 0x00, 0x00, byte(rate), 0x07, 0x00, 0xFF, 0x2F}, nil) + // , dummy, , , , , , + return d.sendCommand([]byte{byte(o), 0x00, startPage, byte(rate), endPage - 1, 0x00, 0xFF, 0x2F}) } // page 29 - // STOP, , dummy, , , , , + // , dummy, , , , , // 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{} diff --git a/devices/ssd1306/ssd1306_test.go b/devices/ssd1306/ssd1306_test.go index bad9f47..4be8576 100644 --- a/devices/ssd1306/ssd1306_test.go +++ b/devices/ssd1306/ssd1306_test.go @@ -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 } diff --git a/devices/ssd1306/ssd1306smoketest/README.md b/devices/ssd1306/ssd1306smoketest/README.md new file mode 100644 index 0000000..395ee89 --- /dev/null +++ b/devices/ssd1306/ssd1306smoketest/README.md @@ -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. diff --git a/devices/ssd1306/ssd1306smoketest/bunny.go b/devices/ssd1306/ssd1306smoketest/bunny.go new file mode 100644 index 0000000..401a305 --- /dev/null +++ b/devices/ssd1306/ssd1306smoketest/bunny.go @@ -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;") diff --git a/devices/ssd1306/ssd1306smoketest/ssd1306smoketest.go b/devices/ssd1306/ssd1306smoketest/ssd1306smoketest.go new file mode 100644 index 0000000..01ada55 --- /dev/null +++ b/devices/ssd1306/ssd1306smoketest/ssd1306smoketest.go @@ -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) +}