Skip to content

Commit fd9b1ba

Browse files
authored
Add SSD1351 OLED display driver (#146)
* ssd1351: Add SSD1351 driver
1 parent 9f23761 commit fd9b1ba

File tree

5 files changed

+375
-1
lines changed

5 files changed

+375
-1
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,7 @@ smoke-test:
131131
@md5sum ./build/test.hex
132132
tinygo build -size short -o ./build/test.hex -target=nucleo-f103rb ./examples/shiftregister/main.go
133133
@md5sum ./build/test.hex
134+
tinygo build -size short -o ./build/test.hex -target=hifive1b ./examples/ssd1351/main.go
135+
@md5sum ./build/test.hex
134136

135137
test: clean fmt-check smoke-test

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func main() {
5252

5353
## Currently supported devices
5454

55-
The following 48 devices are supported.
55+
The following 49 devices are supported.
5656

5757
| Device Name | Interface Type |
5858
|----------|-------------|
@@ -104,6 +104,7 @@ The following 48 devices are supported.
104104
| [Waveshare 2.13" (B & C) e-paper display](https://www.waveshare.com/w/upload/d/d3/2.13inch-e-paper-b-Specification.pdf) | SPI |
105105
| [Waveshare 2.13" e-paper display](https://www.waveshare.com/w/upload/e/e6/2.13inch_e-Paper_Datasheet.pdf) | SPI |
106106
| [WS2812 RGB LED](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) | GPIO |
107+
| [SSD1351 OLED display](https://download.mikroe.com/documents/datasheets/ssd1351-revision-1.3.pdf) | SPI |
107108

108109
## Contributing
109110

examples/ssd1351/main.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package ssd1351
2+
3+
import (
4+
"machine"
5+
6+
"image/color"
7+
8+
"tinygo.org/x/drivers/ssd1351"
9+
)
10+
11+
func main() {
12+
machine.SPI1.Configure(machine.SPIConfig{
13+
Frequency: 2000000,
14+
})
15+
display := ssd1351.New(machine.SPI1, machine.D18, machine.D17, machine.D16, machine.D4, machine.D19)
16+
17+
display.Configure(ssd1351.Config{
18+
Width: 96,
19+
Height: 96,
20+
ColumnOffset: 16,
21+
})
22+
23+
width, height := display.Size()
24+
25+
white := color.RGBA{255, 255, 255, 255}
26+
red := color.RGBA{255, 0, 0, 255}
27+
blue := color.RGBA{0, 0, 255, 255}
28+
green := color.RGBA{0, 255, 0, 255}
29+
30+
display.FillRectangle(0, 0, width, height/4, white)
31+
display.FillRectangle(0, height/4, width, height/4, red)
32+
display.FillRectangle(0, height/2, width, height/4, green)
33+
display.FillRectangle(0, 3*height/4, width, height/4, blue)
34+
35+
display.Display()
36+
}

ssd1351/commands.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package ssd1351
2+
3+
// Commands
4+
const (
5+
SET_COLUMN_ADDRESS = 0x15
6+
SET_ROW_ADDRESS = 0x75
7+
WRITE_RAM = 0x5C
8+
READ_RAM = 0x5D
9+
SET_REMAP_COLORDEPTH = 0xA0
10+
SET_DISPLAY_START_LINE = 0xA1
11+
SET_DISPLAY_OFFSET = 0xA2
12+
SET_DISPLAY_MODE_ALLOFF = 0xA4
13+
SET_DISPLAY_MODE_ALLON = 0xA5
14+
SET_DISPLAY_MODE_RESET = 0xA6
15+
SET_DISPLAY_MODE_INVERT = 0xA7
16+
FUNCTION_SELECTION = 0xAB
17+
SLEEP_MODE_DISPLAY_OFF = 0xAE
18+
SLEEP_MODE_DISPLAY_ON = 0xAF
19+
SET_PHASE_PERIOD = 0xB1
20+
ENHANCED_DRIVING_SCHEME = 0xB2
21+
SET_FRONT_CLOCK_DIV = 0xB3
22+
SET_SEGMENT_LOW_VOLTAGE = 0xB4
23+
SET_GPIO = 0xB5
24+
SET_SECOND_PRECHARGE_PERIOD = 0xB6
25+
GRAY_SCALE_LOOKUP = 0xB8
26+
LINEAR_LUT = 0xB9
27+
SET_PRECHARGE_VOLTAGE = 0xBB
28+
SET_VCOMH_VOLTAGE = 0xBE
29+
SET_CONTRAST = 0xC1
30+
MASTER_CONTRAST = 0xC7
31+
SET_MUX_RATIO = 0xCA
32+
NOP0 = 0xD1
33+
NOP1 = 0xE3
34+
SET_COMMAND_LOCK = 0xFD
35+
HORIZONTAL_SCROLL = 0x96
36+
STOP_MOVING = 0x9E
37+
START_MOVING = 0x9F
38+
)

ssd1351/ssd1351.go

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// Package ssd1351 implements a driver for the SSD1351 OLED color displays.
2+
//
3+
// Datasheet: https://download.mikroe.com/documents/datasheets/ssd1351-revision-1.3.pdf
4+
//
5+
package ssd1351 // import "tinygo.org/x/drivers/ssd1351"
6+
7+
import (
8+
"errors"
9+
"image/color"
10+
"machine"
11+
"time"
12+
)
13+
14+
var (
15+
errDrawingOutOfBounds = errors.New("rectangle coordinates outside display area")
16+
errBufferSizeMismatch = errors.New("buffer length does not match with rectangle size")
17+
)
18+
19+
// Device wraps an SPI connection.
20+
type Device struct {
21+
bus machine.SPI
22+
dcPin machine.Pin
23+
resetPin machine.Pin
24+
csPin machine.Pin
25+
enPin machine.Pin
26+
rwPin machine.Pin
27+
width int16
28+
height int16
29+
rowOffset int16
30+
columnOffset int16
31+
bufferLength int16
32+
}
33+
34+
// Config is the configuration for the display
35+
type Config struct {
36+
Width int16
37+
Height int16
38+
RowOffset int16
39+
ColumnOffset int16
40+
}
41+
42+
// New creates a new SSD1351 connection. The SPI wire must already be configured.
43+
func New(bus machine.SPI, resetPin, dcPin, csPin, enPin, rwPin machine.Pin) Device {
44+
return Device{
45+
bus: bus,
46+
dcPin: dcPin,
47+
resetPin: resetPin,
48+
csPin: csPin,
49+
enPin: enPin,
50+
rwPin: rwPin,
51+
}
52+
}
53+
54+
// Configure initializes the display with default configuration
55+
func (d *Device) Configure(cfg Config) {
56+
if cfg.Width == 0 {
57+
cfg.Width = 128
58+
}
59+
60+
if cfg.Height == 0 {
61+
cfg.Height = 128
62+
}
63+
64+
d.width = cfg.Width
65+
d.height = cfg.Height
66+
d.rowOffset = cfg.RowOffset
67+
d.columnOffset = cfg.ColumnOffset
68+
69+
d.bufferLength = d.width
70+
if d.height > d.width {
71+
d.bufferLength = d.height
72+
}
73+
74+
// configure GPIO pins
75+
d.dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
76+
d.resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
77+
d.csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
78+
d.enPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
79+
d.rwPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
80+
81+
// reset the device
82+
d.resetPin.High()
83+
time.Sleep(100 * time.Millisecond)
84+
d.resetPin.Low()
85+
time.Sleep(100 * time.Millisecond)
86+
d.resetPin.High()
87+
time.Sleep(200 * time.Millisecond)
88+
89+
d.rwPin.Low()
90+
d.dcPin.Low()
91+
d.enPin.High()
92+
93+
// Initialization
94+
d.Command(SET_COMMAND_LOCK)
95+
d.Data(0x12)
96+
d.Command(SET_COMMAND_LOCK)
97+
d.Data(0xB1)
98+
d.Command(SLEEP_MODE_DISPLAY_OFF)
99+
d.Command(SET_FRONT_CLOCK_DIV)
100+
d.Data(0xF1)
101+
d.Command(SET_MUX_RATIO)
102+
d.Data(0x7F)
103+
d.Command(SET_REMAP_COLORDEPTH)
104+
d.Data(0x72)
105+
d.Command(SET_COLUMN_ADDRESS)
106+
d.Data(0x00)
107+
d.Data(0x7F)
108+
d.Command(SET_ROW_ADDRESS)
109+
d.Data(0x00)
110+
d.Data(0x7F)
111+
d.Command(SET_DISPLAY_START_LINE)
112+
d.Data(0x00)
113+
d.Command(SET_DISPLAY_OFFSET)
114+
d.Data(0x00)
115+
d.Command(SET_GPIO)
116+
d.Data(0x00)
117+
d.Command(FUNCTION_SELECTION)
118+
d.Data(0x01)
119+
d.Command(SET_PHASE_PERIOD)
120+
d.Data(0x32)
121+
d.Command(SET_SEGMENT_LOW_VOLTAGE)
122+
d.Data(0xA0)
123+
d.Data(0xB5)
124+
d.Data(0x55)
125+
d.Command(SET_PRECHARGE_VOLTAGE)
126+
d.Data(0x17)
127+
d.Command(SET_VCOMH_VOLTAGE)
128+
d.Data(0x05)
129+
d.Command(SET_CONTRAST)
130+
d.Data(0xC8)
131+
d.Data(0x80)
132+
d.Data(0xC8)
133+
d.Command(MASTER_CONTRAST)
134+
d.Data(0x0F)
135+
d.Command(SET_SECOND_PRECHARGE_PERIOD)
136+
d.Data(0x01)
137+
d.Command(SET_DISPLAY_MODE_RESET)
138+
d.Command(SLEEP_MODE_DISPLAY_ON)
139+
140+
}
141+
142+
// Display does nothing, there's no buffer as it might be too big for some boards
143+
func (d *Device) Display() error {
144+
return nil
145+
}
146+
147+
// SetPixel sets a pixel in the buffer
148+
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
149+
if x < 0 || y < 0 || x >= d.width || y >= d.height {
150+
return
151+
}
152+
d.FillRectangle(x, y, 1, 1, c)
153+
}
154+
155+
// setWindow prepares the screen memory to be modified at given coordinates
156+
func (d *Device) setWindow(x, y, w, h int16) {
157+
x += d.columnOffset
158+
y += d.rowOffset
159+
d.Command(SET_COLUMN_ADDRESS)
160+
d.Tx([]byte{uint8(x), uint8(x + w - 1)}, false)
161+
d.Command(SET_ROW_ADDRESS)
162+
d.Tx([]byte{uint8(y), uint8(y + h - 1)}, false)
163+
d.Command(WRITE_RAM)
164+
}
165+
166+
// FillRectangle fills a rectangle at given coordinates with a color
167+
func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error {
168+
if x < 0 || y < 0 || width <= 0 || height <= 0 ||
169+
x >= d.width || (x+width) > d.width || y >= d.height || (y+height) > d.height {
170+
return errDrawingOutOfBounds
171+
}
172+
d.setWindow(x, y, width, height)
173+
c565 := RGBATo565(c)
174+
c1 := uint8(c565 >> 8)
175+
c2 := uint8(c565)
176+
177+
dim := int16(width * height)
178+
if d.bufferLength < dim {
179+
dim = d.bufferLength
180+
}
181+
data := make([]uint8, dim*2)
182+
183+
for i := int16(0); i < dim; i++ {
184+
data[i*2] = c1
185+
data[i*2+1] = c2
186+
}
187+
dim = int16(width * height)
188+
for dim > 0 {
189+
if dim >= d.bufferLength {
190+
d.Tx(data, false)
191+
} else {
192+
d.Tx(data[:dim*2], false)
193+
}
194+
dim -= d.bufferLength
195+
}
196+
return nil
197+
}
198+
199+
// FillRectangleWithBuffer fills a rectangle at given coordinates with a buffer
200+
func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error {
201+
if x < 0 || y < 0 || width <= 0 || height <= 0 ||
202+
x >= d.width || (x+width) > d.width || y >= d.height || (y+height) > d.height {
203+
return errDrawingOutOfBounds
204+
}
205+
dim := int16(width * height)
206+
l := int16(len(buffer))
207+
if dim != l {
208+
return errBufferSizeMismatch
209+
}
210+
211+
d.setWindow(x, y, width, height)
212+
213+
bl := dim
214+
if d.bufferLength < dim {
215+
bl = d.bufferLength
216+
}
217+
data := make([]uint8, bl*2)
218+
219+
offset := int16(0)
220+
for dim > 0 {
221+
for i := int16(0); i < bl; i++ {
222+
if offset+i < l {
223+
c565 := RGBATo565(buffer[offset+i])
224+
c1 := uint8(c565 >> 8)
225+
c2 := uint8(c565)
226+
data[i*2] = c1
227+
data[i*2+1] = c2
228+
}
229+
}
230+
if dim >= d.bufferLength {
231+
d.Tx(data, false)
232+
} else {
233+
d.Tx(data[:dim*2], false)
234+
}
235+
dim -= d.bufferLength
236+
offset += d.bufferLength
237+
}
238+
return nil
239+
}
240+
241+
// DrawFastVLine draws a vertical line faster than using SetPixel
242+
func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) {
243+
if y0 > y1 {
244+
y0, y1 = y1, y0
245+
}
246+
d.FillRectangle(x, y0, 1, y1-y0+1, c)
247+
}
248+
249+
// DrawFastHLine draws a horizontal line faster than using SetPixel
250+
func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) {
251+
if x0 > x1 {
252+
x0, x1 = x1, x0
253+
}
254+
d.FillRectangle(x0, y, x1-x0+1, y, c)
255+
}
256+
257+
// FillScreen fills the screen with a given color
258+
func (d *Device) FillScreen(c color.RGBA) {
259+
d.FillRectangle(0, 0, d.width, d.height, c)
260+
}
261+
262+
// SetContrast sets the three contrast values (A, B & C)
263+
func (d *Device) SetContrast(contrastA, contrastB, contrastC uint8) {
264+
d.Command(SET_CONTRAST)
265+
d.Tx([]byte{contrastA, contrastB, contrastC}, false)
266+
}
267+
268+
// Command sends a command byte to the display
269+
func (d *Device) Command(command uint8) {
270+
d.Tx([]byte{command}, true)
271+
}
272+
273+
// Data sends a data byte to the display
274+
func (d *Device) Data(data uint8) {
275+
d.Tx([]byte{data}, false)
276+
}
277+
278+
// Tx sends data to the display
279+
func (d *Device) Tx(data []byte, isCommand bool) {
280+
d.dcPin.Set(!isCommand)
281+
d.csPin.Low()
282+
d.bus.Tx(data, nil)
283+
d.csPin.High()
284+
}
285+
286+
// Size returns the current size of the display
287+
func (d *Device) Size() (w, h int16) {
288+
return d.width, d.height
289+
}
290+
291+
// RGBATo565 converts a color.RGBA to uint16 used in the display
292+
func RGBATo565(c color.RGBA) uint16 {
293+
r, g, b, _ := c.RGBA()
294+
return uint16((r & 0xF800) +
295+
((g & 0xFC00) >> 5) +
296+
((b & 0xF800) >> 11))
297+
}

0 commit comments

Comments
 (0)