// Package ssd1351 implements a driver for the SSD1351 OLED color displays. // // Datasheet: https://download.mikroe.com/documents/datasheets/ssd1351-revision-1.3.pdf package ssd1351 // import "tinygo.org/x/drivers/ssd1351" import ( "errors" "image/color" "time" "tinygo.org/x/drivers" "tinygo.org/x/drivers/internal/legacy" "tinygo.org/x/drivers/internal/pin" ) var ( errDrawingOutOfBounds = errors.New("rectangle coordinates outside display area") errBufferSizeMismatch = errors.New("buffer length does not match with rectangle size") ) // Device wraps an SPI connection. type Device struct { bus drivers.SPI dcPin pin.OutputFunc resetPin pin.OutputFunc csPin pin.OutputFunc enPin pin.OutputFunc rwPin pin.OutputFunc width int16 height int16 rowOffset int16 columnOffset int16 bufferLength int16 configurePins func() } // Config is the configuration for the display type Config struct { Width int16 Height int16 RowOffset int16 ColumnOffset int16 } // New creates a new SSD1351 connection. The SPI wire must already be configured. func New(bus drivers.SPI, resetPin, dcPin, csPin, enPin, rwPin pin.Output) Device { return Device{ bus: bus, dcPin: dcPin.Set, resetPin: resetPin.Set, csPin: csPin.Set, enPin: enPin.Set, rwPin: rwPin.Set, configurePins: func() { legacy.ConfigurePinOut(dcPin) legacy.ConfigurePinOut(resetPin) legacy.ConfigurePinOut(csPin) legacy.ConfigurePinOut(enPin) legacy.ConfigurePinOut(rwPin) }, } } // Configure initializes the display with default configuration func (d *Device) Configure(cfg Config) { if cfg.Width == 0 { cfg.Width = 128 } if cfg.Height == 0 { cfg.Height = 128 } d.width = cfg.Width d.height = cfg.Height d.rowOffset = cfg.RowOffset d.columnOffset = cfg.ColumnOffset d.bufferLength = d.width if d.height > d.width { d.bufferLength = d.height } // configure GPIO pins (on baremetal targets only, for backwards compatibility) d.configurePins() // reset the device d.resetPin.High() time.Sleep(100 * time.Millisecond) d.resetPin.Low() time.Sleep(100 * time.Millisecond) d.resetPin.High() time.Sleep(200 * time.Millisecond) d.rwPin.Low() d.dcPin.Low() d.enPin.High() // Initialization d.Command(SET_COMMAND_LOCK) d.Data(0x12) d.Command(SET_COMMAND_LOCK) d.Data(0xB1) d.Command(SLEEP_MODE_DISPLAY_OFF) d.Command(SET_FRONT_CLOCK_DIV) d.Data(0xF1) d.Command(SET_MUX_RATIO) d.Data(0x7F) d.Command(SET_REMAP_COLORDEPTH) d.Data(0x62) d.Command(SET_COLUMN_ADDRESS) d.Data(0x00) d.Data(0x7F) d.Command(SET_ROW_ADDRESS) d.Data(0x00) d.Data(0x7F) d.Command(SET_DISPLAY_START_LINE) d.Data(0x00) d.Command(SET_DISPLAY_OFFSET) d.Data(0x00) d.Command(SET_GPIO) d.Data(0x00) d.Command(FUNCTION_SELECTION) d.Data(0x01) d.Command(SET_PHASE_PERIOD) d.Data(0x32) d.Command(SET_SEGMENT_LOW_VOLTAGE) d.Data(0xA0) d.Data(0xB5) d.Data(0x55) d.Command(SET_PRECHARGE_VOLTAGE) d.Data(0x17) d.Command(SET_VCOMH_VOLTAGE) d.Data(0x05) d.Command(SET_CONTRAST) d.Data(0xC8) d.Data(0x80) d.Data(0xC8) d.Command(MASTER_CONTRAST) d.Data(0x0F) d.Command(SET_SECOND_PRECHARGE_PERIOD) d.Data(0x01) d.Command(SET_DISPLAY_MODE_RESET) d.Command(SLEEP_MODE_DISPLAY_ON) } // Display does nothing, there's no buffer as it might be too big for some boards func (d *Device) Display() error { return nil } // SetPixel sets a pixel in the buffer func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { if x < 0 || y < 0 || x >= d.width || y >= d.height { return } d.FillRectangle(x, y, 1, 1, c) } // setWindow prepares the screen memory to be modified at given coordinates func (d *Device) setWindow(x, y, w, h int16) { x += d.columnOffset y += d.rowOffset d.Command(SET_COLUMN_ADDRESS) d.Tx([]byte{uint8(x), uint8(x + w - 1)}, false) d.Command(SET_ROW_ADDRESS) d.Tx([]byte{uint8(y), uint8(y + h - 1)}, false) d.Command(WRITE_RAM) } // FillRectangle fills a rectangle at given coordinates with a color func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { if x < 0 || y < 0 || width <= 0 || height <= 0 || x >= d.width || (x+width) > d.width || y >= d.height || (y+height) > d.height { return errDrawingOutOfBounds } d.setWindow(x, y, width, height) c565 := RGBATo565(c) c1 := uint8(c565 >> 8) c2 := uint8(c565) dim := int16(width * height) if d.bufferLength < dim { dim = d.bufferLength } data := make([]uint8, dim*2) for i := int16(0); i < dim; i++ { data[i*2] = c1 data[i*2+1] = c2 } dim = int16(width * height) for dim > 0 { if dim >= d.bufferLength { d.Tx(data, false) } else { d.Tx(data[:dim*2], false) } dim -= d.bufferLength } return nil } // FillRectangleWithBuffer fills a rectangle at given coordinates with a buffer func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { if x < 0 || y < 0 || width <= 0 || height <= 0 || x >= d.width || (x+width) > d.width || y >= d.height || (y+height) > d.height { return errDrawingOutOfBounds } dim := int16(width * height) l := int16(len(buffer)) if dim != l { return errBufferSizeMismatch } d.setWindow(x, y, width, height) bl := dim if d.bufferLength < dim { bl = d.bufferLength } data := make([]uint8, bl*2) offset := int16(0) for dim > 0 { for i := int16(0); i < bl; i++ { if offset+i < l { c565 := RGBATo565(buffer[offset+i]) c1 := uint8(c565 >> 8) c2 := uint8(c565) data[i*2] = c1 data[i*2+1] = c2 } } if dim >= d.bufferLength { d.Tx(data, false) } else { d.Tx(data[:dim*2], false) } dim -= d.bufferLength offset += d.bufferLength } return nil } // DrawFastVLine draws a vertical line faster than using SetPixel func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { if y0 > y1 { y0, y1 = y1, y0 } d.FillRectangle(x, y0, 1, y1-y0+1, c) } // DrawFastHLine draws a horizontal line faster than using SetPixel func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { if x0 > x1 { x0, x1 = x1, x0 } d.FillRectangle(x0, y, x1-x0+1, 1, c) } // FillScreen fills the screen with a given color func (d *Device) FillScreen(c color.RGBA) { d.FillRectangle(0, 0, d.width, d.height, c) } // SetContrast sets the three contrast values (A, B & C) func (d *Device) SetContrast(contrastA, contrastB, contrastC uint8) { d.Command(SET_CONTRAST) d.Tx([]byte{contrastA, contrastB, contrastC}, false) } // Command sends a command byte to the display func (d *Device) Command(command uint8) { d.Tx([]byte{command}, true) } // Data sends a data byte to the display func (d *Device) Data(data uint8) { d.Tx([]byte{data}, false) } // Tx sends data to the display func (d *Device) Tx(data []byte, isCommand bool) { d.dcPin(!isCommand) d.csPin.Low() d.bus.Tx(data, nil) d.csPin.High() } // Size returns the current size of the display func (d *Device) Size() (w, h int16) { return d.width, d.height } // RGBATo565 converts a color.RGBA to uint16 used in the display func RGBATo565(c color.RGBA) uint16 { r, g, b, _ := c.RGBA() return uint16((r & 0xF800) + ((g & 0xFC00) >> 5) + ((b & 0xF800) >> 11)) }