diff --git a/sh1106/sh1106.go b/sh1106/sh1106.go new file mode 100644 index 0000000..74714ba --- /dev/null +++ b/sh1106/sh1106.go @@ -0,0 +1,484 @@ +// Copied almost verbatim from https://github.com/google/periph/tree/master/devices/ssd1306 +// The sh1106 differs from the ssd1306 only in writing to it's memory. +// We now also properly use the startCol and endCol values from calculateSubset. +// +// Copyright 2016 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 sh1106 + +import ( + "bytes" + "errors" + "fmt" + "image" + "image/color" + "image/draw" + + "periph.io/x/conn/v3" + "periph.io/x/conn/v3/display" + "periph.io/x/conn/v3/gpio" + "periph.io/x/conn/v3/i2c" + "periph.io/x/conn/v3/physic" + "periph.io/x/conn/v3/spi" + "periph.io/x/devices/v3/ssd1306/image1bit" +) + +// FrameRate determines scrolling speed. +type FrameRate byte + +// 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 + FrameRate4 FrameRate = 5 + FrameRate5 FrameRate = 0 + FrameRate25 FrameRate = 6 + FrameRate64 FrameRate = 1 + FrameRate128 FrameRate = 2 + FrameRate256 FrameRate = 3 +) + +// Orientation is used for scrolling. +type Orientation byte + +// Possible orientations for scrolling. +const ( + Left Orientation = 0x27 + Right Orientation = 0x26 + UpRight Orientation = 0x29 + UpLeft Orientation = 0x2A +) + +// DefaultOpts is the recommended default options. +var DefaultOpts = Opts{ + Addr: 0x3C, + Rotated: false, + Sequential: false, + SwapTopBottom: false, +} + +// Opts defines the options for the device. +type Opts struct { + Addr uint16 + // Rotated determines if the display is rotated by 180°. + Rotated bool + // Sequential corresponds to the Sequential/Alternative COM pin configuration + // in the OLED panel hardware. Try toggling this if half the rows appear to be + // missing on your display. + Sequential bool + // SwapTopBottom corresponds to the Left/Right remap COM pin configuration in + // the OLED panel hardware. Try toggling this if the top and bottom halves of + // your display are swapped. + SwapTopBottom bool +} + +// NewSPI returns a Dev object that communicates over SPI to a SSD1306 display +// controller. +// +// The SSD1306 can operate at up to 3.3Mhz, which is much higher than I²C. This +// permits higher refresh rates. +// +// Wiring +// +// Connect SDA to SPI_MOSI, SCK to SPI_CLK, CS to SPI_CS. +// +// 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(p spi.Port, dc gpio.PinOut, opts *Opts) (*Dev, error) { + if dc == gpio.INVALID { + return nil, errors.New("sh1106: 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 + } + c, err := p.Connect(3300*physic.KiloHertz, spi.Mode0, bits) + if err != nil { + return nil, err + } + return newDev(c, opts, true, dc) +} + +// NewI2C returns a Dev object that communicates over I²C to a SSD1306 display +// controller. +func NewI2C(i i2c.Bus, opts *Opts) (*Dev, error) { + // Maximum clock speed is 1/2.5µs = 400KHz. + return newDev(&i2c.Dev{Bus: i, Addr: opts.Addr}, opts, false, nil) +} + +// Dev is an open handle to the display controller. +type Dev struct { + // 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 +} + +func (d *Dev) String() string { + if d.spi { + return fmt.Sprintf("sh1106.Dev{%s, %s, %s}", d.c, d.dc, d.rect.Max) + } + return fmt.Sprintf("sh1106.Dev{%s, %s}", d.c, d.rect.Max) +} + +// ColorModel implements display.Drawer. +// +// It is a one bit color model, as implemented by image1bit.Bit. +func (d *Dev) ColorModel() color.Model { + return image1bit.BitModel +} + +// Bounds implements display.Drawer. Min is guaranteed to be {0, 0}. +func (d *Dev) Bounds() image.Rectangle { + return d.rect +} + +// Draw implements display.Drawer. +// +// 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. +func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) error { + var next []byte + if img, ok := src.(*image1bit.VerticalLSB); ok && r == d.rect && img.Rect == 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) + } + return d.drawInternal(next) +} + +// Write writes a buffer of pixels to the display. +// +// 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) != len(d.buffer) { + return 0, fmt.Errorf("sh1106: invalid pixel stream length; expected %d bytes, got %d bytes", len(d.buffer), len(pixels)) + } + // Write() skips d.next so it saves 1kb of RAM. + if err := d.drawInternal(pixels); err != nil { + return 0, err + } + return len(pixels), nil +} + +// 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 + // , dummy, , , , , , + return d.sendCommand([]byte{byte(o), 0x00, startPage, byte(rate), endPage - 1, 0x00, 0xFF, 0x2F}) + } + // page 29 + // , dummy, , , , , + // page 30: 0xA3 permits to set rows for scroll area. + return d.sendCommand([]byte{byte(o), 0x00, startPage, byte(rate), endPage - 1, 0x01, 0x2F}) +} + +// StopScroll stops any scrolling previously set and resets the screen. +func (d *Dev) StopScroll() error { + 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.sendCommand([]byte{0x81, level}) +} + +// 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 err +} + +// Invert the display (black on white vs white on black). +func (d *Dev) Invert(blackOnWhite bool) error { + b := []byte{0xA6} + if blackOnWhite { + b[0] = 0xA7 + } + return d.sendCommand(b) +} + +// + +// newDev is the common initialization code that is independent of the +// communication protocol (I²C or SPI) being used. +func newDev(c conn.Conn, opts *Opts, usingSPI bool, dc gpio.PinOut) (*Dev, error) { + nbPages := 64 / 8 + pageSize := 128 + d := &Dev{ + c: c, + spi: usingSPI, + dc: dc, + rect: image.Rect(0, 0, 128, 64), + buffer: make([]byte, nbPages*pageSize), + startPage: 0, + endPage: nbPages, + startCol: 0, + endCol: 128, + // Signal that the screen must be redrawn on first draw(). + scrolled: true, + } + if err := d.sendCommand(getInitCmd(opts)); err != nil { + return nil, err + } + return d, nil +} + +func getInitCmd(opts *Opts) []byte { + // Set COM output scan direction; C0 means normal; C8 means reversed + comScan := byte(0xC8) + // See page 40. + columnAddr := byte(0xA1) + if opts.Rotated { + // Change order both horizontally and vertically. + comScan = 0xC0 + columnAddr = byte(0xA0) + } + // See page 40. + hwLayout := byte(0x02) + if !opts.Sequential { + hwLayout |= 0x10 + } + if opts.SwapTopBottom { + hwLayout |= 0x20 + } + // 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. + // Page 64 has the full recommended flow. + // Page 28 lists all the commands. + 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, // + 0xDA, hwLayout, // Set COM pins hardware configuration; see page 40 + 0x81, 0xFF, // Set max contrast + 0xA4, // Set display to use GDDRAM content + 0xA6, // Set normal display (0xA7 for inverted 0=lit, 1=dark) + 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 + 0xDB, 0x40, // Set Vcomh deselect level; page 32 + 0x2E, // Deactivate scroll + 0xA8, byte(64 - 1), // Set multiplex ratio (number of lines to display) + 0x20, 0x00, // Set memory addressing mode to horizontal + 0x21, 0, uint8(128 - 1), // Set column address (Width) + 0x22, 0, uint8(64/8 - 1), // Set page address (Pages) + 0xAF, // Display on + } +} + +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 + } + // 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) + + baseLowColAddr := byte(0x00) + baseHighColAddr := byte(0x10) + basePageAddr := byte(0xB0) + pageSize := d.rect.Dx() + + // The sh1106 has 132x64 of ram, and the 128x64 pixels are positioned in the center of it, + // thus we start 2pixels in. + start := 2 + byte(startCol) + + // send data for each row + for i := startPage; i < endPage; i++ { + // Calculate the column address to write into, broken into high and low parts. + cmd := []byte{ + basePageAddr + byte(i), + baseLowColAddr + start&0xF, + baseHighColAddr | start>>4, + } + if err := d.sendCommand(cmd); err != nil { + return err + } + + if err := d.sendData(d.buffer[i*pageSize+startCol : i*pageSize+endCol]); err != nil { + return err + } + } + + return nil +} + +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("sh1106: 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(append([]byte{i2cCmd}, c...), nil) +} + +const ( + i2cCmd = 0x00 // I²C transaction has stream of command bytes + i2cData = 0x40 // I²C transaction has stream of data bytes +) + +var _ display.Drawer = &Dev{}