From fd9b1ba89b58be6d70df1973ad4b1746fbd3f360 Mon Sep 17 00:00:00 2001 From: Yannis Huber <32066446+yannishuber@users.noreply.github.com> Date: Fri, 24 Apr 2020 07:30:50 +0200 Subject: [PATCH] Add SSD1351 OLED display driver (#146) * ssd1351: Add SSD1351 driver --- Makefile | 2 + README.md | 3 +- examples/ssd1351/main.go | 36 +++++ ssd1351/commands.go | 38 +++++ ssd1351/ssd1351.go | 297 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 examples/ssd1351/main.go create mode 100644 ssd1351/commands.go create mode 100644 ssd1351/ssd1351.go diff --git a/Makefile b/Makefile index 4ac96bc..9fc48c8 100644 --- a/Makefile +++ b/Makefile @@ -131,5 +131,7 @@ smoke-test: @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=nucleo-f103rb ./examples/shiftregister/main.go @md5sum ./build/test.hex + tinygo build -size short -o ./build/test.hex -target=hifive1b ./examples/ssd1351/main.go + @md5sum ./build/test.hex test: clean fmt-check smoke-test diff --git a/README.md b/README.md index 6dd693c..e5864e7 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ func main() { ## Currently supported devices -The following 48 devices are supported. +The following 49 devices are supported. | Device Name | Interface Type | |----------|-------------| @@ -104,6 +104,7 @@ The following 48 devices are supported. | [Waveshare 2.13" (B & C) e-paper display](https://www.waveshare.com/w/upload/d/d3/2.13inch-e-paper-b-Specification.pdf) | SPI | | [Waveshare 2.13" e-paper display](https://www.waveshare.com/w/upload/e/e6/2.13inch_e-Paper_Datasheet.pdf) | SPI | | [WS2812 RGB LED](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) | GPIO | +| [SSD1351 OLED display](https://download.mikroe.com/documents/datasheets/ssd1351-revision-1.3.pdf) | SPI | ## Contributing diff --git a/examples/ssd1351/main.go b/examples/ssd1351/main.go new file mode 100644 index 0000000..00099cc --- /dev/null +++ b/examples/ssd1351/main.go @@ -0,0 +1,36 @@ +package ssd1351 + +import ( + "machine" + + "image/color" + + "tinygo.org/x/drivers/ssd1351" +) + +func main() { + machine.SPI1.Configure(machine.SPIConfig{ + Frequency: 2000000, + }) + display := ssd1351.New(machine.SPI1, machine.D18, machine.D17, machine.D16, machine.D4, machine.D19) + + display.Configure(ssd1351.Config{ + Width: 96, + Height: 96, + ColumnOffset: 16, + }) + + width, height := display.Size() + + white := color.RGBA{255, 255, 255, 255} + red := color.RGBA{255, 0, 0, 255} + blue := color.RGBA{0, 0, 255, 255} + green := color.RGBA{0, 255, 0, 255} + + display.FillRectangle(0, 0, width, height/4, white) + display.FillRectangle(0, height/4, width, height/4, red) + display.FillRectangle(0, height/2, width, height/4, green) + display.FillRectangle(0, 3*height/4, width, height/4, blue) + + display.Display() +} diff --git a/ssd1351/commands.go b/ssd1351/commands.go new file mode 100644 index 0000000..32ebf37 --- /dev/null +++ b/ssd1351/commands.go @@ -0,0 +1,38 @@ +package ssd1351 + +// Commands +const ( + SET_COLUMN_ADDRESS = 0x15 + SET_ROW_ADDRESS = 0x75 + WRITE_RAM = 0x5C + READ_RAM = 0x5D + SET_REMAP_COLORDEPTH = 0xA0 + SET_DISPLAY_START_LINE = 0xA1 + SET_DISPLAY_OFFSET = 0xA2 + SET_DISPLAY_MODE_ALLOFF = 0xA4 + SET_DISPLAY_MODE_ALLON = 0xA5 + SET_DISPLAY_MODE_RESET = 0xA6 + SET_DISPLAY_MODE_INVERT = 0xA7 + FUNCTION_SELECTION = 0xAB + SLEEP_MODE_DISPLAY_OFF = 0xAE + SLEEP_MODE_DISPLAY_ON = 0xAF + SET_PHASE_PERIOD = 0xB1 + ENHANCED_DRIVING_SCHEME = 0xB2 + SET_FRONT_CLOCK_DIV = 0xB3 + SET_SEGMENT_LOW_VOLTAGE = 0xB4 + SET_GPIO = 0xB5 + SET_SECOND_PRECHARGE_PERIOD = 0xB6 + GRAY_SCALE_LOOKUP = 0xB8 + LINEAR_LUT = 0xB9 + SET_PRECHARGE_VOLTAGE = 0xBB + SET_VCOMH_VOLTAGE = 0xBE + SET_CONTRAST = 0xC1 + MASTER_CONTRAST = 0xC7 + SET_MUX_RATIO = 0xCA + NOP0 = 0xD1 + NOP1 = 0xE3 + SET_COMMAND_LOCK = 0xFD + HORIZONTAL_SCROLL = 0x96 + STOP_MOVING = 0x9E + START_MOVING = 0x9F +) diff --git a/ssd1351/ssd1351.go b/ssd1351/ssd1351.go new file mode 100644 index 0000000..2349613 --- /dev/null +++ b/ssd1351/ssd1351.go @@ -0,0 +1,297 @@ +// 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" + "machine" + "time" +) + +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 machine.SPI + dcPin machine.Pin + resetPin machine.Pin + csPin machine.Pin + enPin machine.Pin + rwPin machine.Pin + width int16 + height int16 + rowOffset int16 + columnOffset int16 + bufferLength int16 +} + +// 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 machine.SPI, resetPin, dcPin, csPin, enPin, rwPin machine.Pin) Device { + return Device{ + bus: bus, + dcPin: dcPin, + resetPin: resetPin, + csPin: csPin, + enPin: enPin, + rwPin: 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 + d.dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.enPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rwPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + // 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(0x72) + 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, y, 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.Set(!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)) +}