diff --git a/devices/ssd1306/ssd1306.go b/devices/ssd1306/ssd1306.go index 798fb8c..bac0782 100644 --- a/devices/ssd1306/ssd1306.go +++ b/devices/ssd1306/ssd1306.go @@ -9,9 +9,24 @@ // Changing between protocol is likely done through resistor soldering, for // boards that support both. // -// Datasheet +// Known issue +// +// 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). +// +// Datasheets // // https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf +// +// "DM-OLED096-624": https://drive.google.com/file/d/0B5lkVYnewKTGaEVENlYwbDkxSGM/view +// +// "ssd1306": https://drive.google.com/file/d/0B5lkVYnewKTGYzhyWWp0clBMR1E/view package ssd1306 // Some have SPI enabled; @@ -104,8 +119,6 @@ func newDev(dev io.Writer, w, h int, rotated bool) (*Dev, error) { } d := &Dev{w: dev, W: w, H: h} - contrast := byte(0x7F) // (default value) - // Set COM output scan direction; C0 means normal; C8 means reversed comScan := byte(0xC8) // See page 40. @@ -115,38 +128,38 @@ func newDev(dev io.Writer, w, h int, rotated bool) (*Dev, error) { comScan = 0xC0 columnAddr = byte(0xA0) } - // Initialize the device by fully reseting all values. + // 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. - // BUG(maruel): This flow may not recover a controller in a complete - // corrupted state. Figure out a more resilient startup init code. + // Some values come from the DM-OLED096 datasheet p15. init := []byte{ + i2cCmd, 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, 0x12, // Set COM pins hardware configuration; see page 40 - 0x81, contrast, // Set contrast control + 0x81, 0xff, // Set max contrast 0xA4, // Set display to use GDDRAM content - 0xA6, // Set normal display (0xA7 for reversed bitness i.e. bit set is black) (?) - 0xD5, 0x40, // Set osc frequency and divide ratio; power on reset value is 0x3F. + 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. 0x8D, 0x14, // Enable charge pump regulator; page 62 - - // Not sure - 0xD9, 0xF1, // Set pre-charge period. - //0xDB, 0x40, // Set Vcomh deselect level; page 32 - 0x20, 0x00, // Set memory addressing mode to horizontal (can be page, horizontal or vertical) + 0xD9, 0xf1, // Set pre-charge period; from adafruit driver + 0xDB, 0x40, // Set Vcomh deselect level; page 32 + 0x20, 0x00, // Set memory addressing mode to horizontal + 0xB0, // Set page start address 0x2E, // Deactivate scroll - 0x00 | 0x00, // Set column offset (lower nibble) - 0x10 | 0x00, // Set column offset (higher nibble) + 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) 0xAF, // Display on } if _, err := d.w.Write(init); err != nil { return nil, err } + return d, nil } @@ -226,66 +239,88 @@ func (d *Dev) Write(pixels []byte) (int, error) { return 0, errors.New("ssd1306: invalid pixel stream") } - // Run as 2 big transactions to reduce downtime on the bus. Doing with one - // transaction doesn't work (?) + // 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{ - 0x21, 0x00, byte(d.W - 1), // Set column address (Width) - 0x22, 0x00, byte(d.H/8 - 1), // Set page address (Pages) + 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 _, err := d.w.Write(hdr); err != nil { return 0, err } - if _, err := d.w.Write(append([]byte{0x40}, pixels...)); err != nil { + + // Write the data. + if _, err := d.w.Write(append([]byte{i2cData}, pixels...)); err != nil { return 0, err } + return len(pixels), nil } -// Scroll scrolls the entire. +// 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. if o == Left || o == Right { // page 28 // STOP, , dummy, , , , , , - _, err := d.w.Write([]byte{0x2E, byte(o), 0x00, 0x00, byte(rate), 0x07, 0x00, 0xFF, 0x2F}) + _, err := d.w.Write([]byte{i2cCmd, 0x2E, byte(o), 0x00, 0x00, byte(rate), 0x07, 0x00, 0xFF, 0x2F}) return err } // page 29 // STOP, , dummy, , , , , // page 30: 0xA3 permits to set rows for scroll area. - _, err := d.w.Write([]byte{0x2E, byte(o), 0x00, 0x00, byte(rate), 0x07, 0x01, 0x2F}) + _, err := d.w.Write([]byte{i2cCmd, 0x2E, byte(o), 0x00, 0x00, byte(rate), 0x07, 0x01, 0x2F}) return err } // StopScroll stops any scrolling previously set. // // It will only take effect after redrawing the ram. -// -// BUG(maruel): Doesn't work. func (d *Dev) StopScroll() error { - _, err := d.w.Write([]byte{0x2E}) + _, err := d.w.Write([]byte{i2cCmd, 0x2E}) return err } // SetContrast changes the screen contrast. // -// BUG(maruel): Doesn't work. +// Note: values other than 0xff do not seem useful... func (d *Dev) SetContrast(level byte) error { - _, err := d.w.Write([]byte{0x81, level}) + _, err := d.w.Write([]byte{i2cCmd, 0x81, level}) return err } // Enable or disable the display. -// -// BUG(maruel): Doesn't work. func (d *Dev) Enable(on bool) error { b := byte(0xAE) if on { b = 0xAF } - _, err := d.w.Write([]byte{b}) + _, err := d.w.Write([]byte{i2cCmd, b}) 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 = 0xA7 + } + _, err := d.w.Write([]byte{i2cCmd, b}) + return err +} + +const ( + i2cCmd = 0x00 // i2c transaction has stream of command bytes + i2cData = 0x40 // i2c 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 a017b8c..d6067ee 100644 --- a/devices/ssd1306/ssd1306_test.go +++ b/devices/ssd1306/ssd1306_test.go @@ -27,7 +27,7 @@ func TestDrawGray(t *testing.T) { // Startup initialization. {Addr: 0x3c, Write: initline}, // Preparation to draw. - {Addr: 0x3c, Write: []byte{0x21, 0x0, 0x7f, 0x22, 0x0, 0x7}}, + {Addr: 0x3c, Write: prelude}, // Actual draw buffer. {Addr: 0x3c, Write: grayCheckboardWrite}, }, @@ -49,7 +49,7 @@ func TestDraw1D(t *testing.T) { bus := i2ctest.Playback{ Ops: []i2ctest.IO{ {Addr: 0x3c, Write: initline}, - {Addr: 0x3c, Write: []byte{0x21, 0x0, 0x7f, 0x22, 0x0, 0x7}}, + {Addr: 0x3c, Write: prelude}, {Addr: 0x3c, Write: grayCheckboardWrite}, }, } @@ -119,8 +119,14 @@ func makeGrayCheckboard(r image.Rectangle) image.Image { } var initline = []byte{ - 0xae, 0xd3, 0x00, 0x40, 0xa1, 0xc8, 0xda, 0x12, 0x81, 0x7f, 0xa4, 0xa6, 0xd5, - 0x40, 0x8d, 0x14, 0xd9, 0xf1, 0x20, 0x00, 0x2e, 0x00, 0x10, 0xa8, 0x3f, 0xaf, + 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{