Adding SSD1306 OLED display controller support

This commit is contained in:
Geoff Bourne 2017-08-31 21:09:06 -05:00
parent d3d8c0c5c6
commit 903dc72cb7
5 changed files with 725 additions and 0 deletions

View File

@ -0,0 +1,98 @@
package ssd1306
import "errors"
// Buffer abstracts "drawing" into an 8-row page space for Display calls into an SSD1306.
// A suitably-sized instance is created using NewBuffer on an SSD1306 instance.
type Buffer interface {
On(x, y int) error
Off(x, y int) error
Set(x, y int, on bool) error
FillRect(x, y int, w, h int) error
ClearRect(x, y int, w, h int) error
Cells() []byte
}
// bufferHoriz is a Buffer that operates in memory mode of SSD1306_MEMORYMODE_HORIZ
type bufferHoriz struct {
cells []byte
width uint
pages uint
}
func newBuffer(width, pages uint, memoryMode int) Buffer {
switch memoryMode {
case SSD1306_MEMORYMODE_HORIZ:
return newBufferHoriz(width, pages)
}
return nil
}
func newBufferHoriz(width, pages uint) *bufferHoriz {
return &bufferHoriz{
width: width,
pages: pages,
cells: make([]byte, width*pages),
}
}
func (b *bufferHoriz) Cells() []byte {
return b.cells
}
func (b *bufferHoriz) On(x, y int) error {
return b.Set(x, y, true)
}
func (b *bufferHoriz) Off(x, y int) error {
return b.Set(x, y, false)
}
func (b *bufferHoriz) Set(x, y int, on bool) error {
if uint(x) > b.width {
return errors.New("x cannot be greater than buffer width")
}
page := uint(y) >> 3
if page > b.pages {
return errors.New("y cannot be greater than buffer height")
}
index := uint(page*b.width) + uint(x)
cell := b.cells[index]
bit := byte(1) << (uint(y) & 0x7)
if on {
cell |= bit
} else {
cell &^= bit
}
b.cells[index] = cell
return nil
}
func (b *bufferHoriz) FillRect(x, y int, w, h int) error {
for xi := 0; xi < w; xi++ {
for yi := 0; yi < h; yi++ {
if err := b.Set(x+xi, y+yi, true); err != nil {
return err
}
}
}
return nil
}
func (b *bufferHoriz) ClearRect(x, y int, w, h int) error {
for xi := 0; xi < w; xi++ {
for yi := 0; yi < h; yi++ {
if err := b.Set(x+xi, y+yi, false); err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,126 @@
package ssd1306
import (
"reflect"
"testing"
)
func TestAllocation(t *testing.T) {
buffer := newBuffer(16, 2, memoryMode)
if buffer == nil {
t.Fatal("buffer shouldn't be nil")
}
if buffer.Cells() == nil {
t.Fatal("cells shouldn't be nil")
}
if len(buffer.Cells()) != 32 {
t.Error("wrong cell count")
}
}
var onTests = []struct {
name string
x, y int
expected []byte
}{
{name: "top left", x: 0, y: 0, expected: []byte{0x1, 0, 0, 0, 0, 0, 0, 0 /*page*/, 0, 0, 0, 0, 0, 0, 0, 0}},
{name: "left top of page 2", x: 0, y: 8, expected: []byte{0, 0, 0, 0, 0, 0, 0, 0 /*page*/, 0x1, 0, 0, 0, 0, 0, 0, 0}},
{name: "left row 2", x: 0, y: 1, expected: []byte{1 << 1, 0, 0, 0, 0, 0, 0, 0 /*page*/, 0, 0, 0, 0, 0, 0, 0, 0}},
{name: "middle ish", x: 4, y: 3, expected: []byte{0, 0, 0, 0, 1 << 3, 0, 0, 0 /*page*/, 0, 0, 0, 0, 0, 0, 0, 0}},
{name: "bottom ish", x: 6, y: 14, expected: []byte{0, 0, 0, 0, 0, 0, 0, 0 /*page*/, 0, 0, 0, 0, 0, 0, 1 << (14 - 8), 0}},
}
func TestBufferHoriz_On(t *testing.T) {
width := uint(8)
pages := uint(2) // height = 16
for _, tt := range onTests {
t.Run(tt.name, func(t *testing.T) {
buffer := newBufferHoriz(width, pages)
buffer.On(tt.x, tt.y)
if !reflect.DeepEqual(buffer.cells, tt.expected) {
t.Errorf("%s: wrong cell content, saw %v", tt.name, buffer.cells)
}
})
}
}
func TestBufferHoriz_Set(t *testing.T) {
width := uint(8)
pages := uint(2) // height = 16
for _, tt := range onTests {
t.Run(tt.name, func(t *testing.T) {
buffer := newBufferHoriz(width, pages)
buffer.Set(tt.x, tt.y, true)
if !reflect.DeepEqual(buffer.cells, tt.expected) {
t.Errorf("%s: wrong cell content, saw %v", tt.name, buffer.cells)
}
})
}
}
func TestBufferHoriz_FillRect(t *testing.T) {
width := uint(8)
pages := uint(2) // height = 16
tests := []struct {
name string
x, y int
width, height int
expected []byte
}{
{name: "all", x: 0, y: 0, width: 8, height: 16, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}},
{name: "top half", x: 0, y: 0, width: 8, height: 8, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0}},
{name: "top left", x: 0, y: 0, width: 4, height: 8, expected: []byte{0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{name: "top quarter", x: 0, y: 0, width: int(width), height: 4, expected: []byte{0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0, 0, 0, 0, 0, 0, 0, 0}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buffer := newBufferHoriz(width, pages)
buffer.FillRect(tt.x, tt.y, tt.width, tt.height)
if !reflect.DeepEqual(buffer.cells, tt.expected) {
t.Errorf("%s: wrong cell content, saw %v", tt.name, buffer.cells)
}
})
}
}
func TestBufferHoriz_Off(t *testing.T) {
width := uint(8)
pages := uint(2) // height = 16
tests := []struct {
name string
x, y int
expected []byte
}{
{name: "top left", x: 0, y: 0, expected: []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}},
{name: "bottom of upper page", x: 1, y: 7, expected: []byte{0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}},
{name: "top of second page", x: 2, y: 8, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buffer := newBufferHoriz(width, pages)
// assumes TestBufferHoriz_FillRect passes
buffer.FillRect(0, 0, 8, 16)
buffer.Off(tt.x, tt.y)
if !reflect.DeepEqual(buffer.cells, tt.expected) {
t.Errorf("%s: wrong cell content, saw %v", tt.name, buffer.cells)
}
})
}
}

View File

@ -0,0 +1,236 @@
/*
Package ssd1306 allows controlling an SSD1306 OLED controller.
This currently supports only write-only operations and a SPI connection to the controller.
Resources
This library is based on these prior implementations:
https://github.com/adafruit/Adafruit_Python_SSD1306/blob/master/Adafruit_SSD1306/SSD1306.py
https://github.com/kakaryan/i2cssd1306/blob/master/ssd1306.go
Datasheet
https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
*/
package ssd1306
import (
"github.com/golang/glog"
"github.com/kidoman/embd"
"time"
)
const (
SSD1306_I2C_ADDRESS = 0x3C
SSD1306_SETCONTRAST = 0x81
SSD1306_DISPLAYALLON_RESUME = 0xA4
SSD1306_DISPLAYALLON = 0xA5
SSD1306_NORMALDISPLAY = 0xA6
SSD1306_INVERTDISPLAY = 0xA7
SSD1306_DISPLAYOFF = 0xAE
SSD1306_DISPLAYON = 0xAF
SSD1306_SETDISPLAYOFFSET = 0xD3
SSD1306_SETCOMPINS = 0xDA
SSD1306_SETVCOMDETECT = 0xDB
SSD1306_SETDISPLAYCLOCKDIV = 0xD5
SSD1306_SETPRECHARGE = 0xD9
SSD1306_SETMULTIPLEX = 0xA8
SSD1306_SETLOWCOLUMN = 0x00
SSD1306_SETHIGHCOLUMN = 0x10
SSD1306_SETSTARTLINE = 0x40
SSD1306_MEMORYMODE = 0x20
SSD1306_MEMORYMODE_HORIZ = 0x00
SSD1306_COLUMNADDR = 0x21
SSD1306_PAGEADDR = 0x22
SSD1306_COMSCANINC = 0xC0
SSD1306_COMSCANDEC = 0xC8
SSD1306_SEGREMAP = 0xA0
SSD1306_CHARGEPUMP = 0x8D
SSD1306_EXTERNALVCC = 0x1
SSD1306_SWITCHCAPVCC = 0x2
SSD1306_ACTIVATE_SCROLL = 0x2F
SSD1306_DEACTIVATE_SCROLL = 0x2E
SSD1306_SET_VERTICAL_SCROLL_AREA = 0xA3
SSD1306_RIGHT_HORIZONTAL_SCROLL = 0x26
SSD1306_LEFT_HORIZONTAL_SCROLL = 0x27
SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL = 0x29
SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL = 0x2A
)
const (
memoryMode = SSD1306_MEMORYMODE_HORIZ
)
// SSD1306 represents an instance of an SSD1306 OLED controller.
type SSD1306 struct {
spiBus embd.SPIBus
dcPin embd.DigitalPin
resetPin embd.DigitalPin
vccState byte
width uint
height uint
pages uint
}
// NewSPI creates a new SSD1306 controller connected via the given SPIBus.
// The GPIO digital output pins that are connected to DC and Rst must also be provided.
// Finally the width x height of the OLED must be given where the width is usually 128 and height is either 32 or 64.
func NewSPI(spiBus embd.SPIBus, dcPin, resetPin embd.DigitalPin, width, height uint) (*SSD1306, error) {
controller := &SSD1306{
spiBus: spiBus,
dcPin: dcPin,
resetPin: resetPin,
vccState: SSD1306_SWITCHCAPVCC,
width: width,
height: height,
pages: height / 8,
}
err := controller.reset()
if err != nil {
glog.Errorf("ssd1306: failed to reset: %s", err)
return nil, err
}
err = controller.init()
if err != nil {
glog.Errorf("ssd1306: failed to init: %s", err)
return nil, err
}
return controller, nil
}
func (c *SSD1306) reset() error {
if err := c.resetPin.Write(embd.High); err != nil {
return err
}
time.Sleep(1 * time.Millisecond)
if err := c.resetPin.Write(embd.Low); err != nil {
return err
}
time.Sleep(10 * time.Millisecond)
if err := c.resetPin.Write(embd.High); err != nil {
return err
}
return nil
}
func (c *SSD1306) init() error {
if err := c.command(SSD1306_DISPLAYOFF); err != nil {
return err
}
if err := c.command(SSD1306_SETDISPLAYCLOCKDIV, 0x80); err != nil {
return err
}
if err := c.command(SSD1306_SETMULTIPLEX, 0x3F); err != nil {
return err
}
if err := c.command(SSD1306_SETDISPLAYOFFSET, 0x0); err != nil {
return err
}
if err := c.command(SSD1306_SETSTARTLINE | 0x0); err != nil {
return err
}
if c.vccState == SSD1306_EXTERNALVCC {
if err := c.command(SSD1306_CHARGEPUMP, 0x10); err != nil {
return err
}
} else {
if err := c.command(SSD1306_CHARGEPUMP, 0x14); err != nil {
return err
}
}
if err := c.command(SSD1306_MEMORYMODE, memoryMode); err != nil {
return err
}
if err := c.command(SSD1306_SEGREMAP | 0x1); err != nil {
return err
}
if err := c.command(SSD1306_COMSCANDEC); err != nil {
return err
}
if err := c.command(SSD1306_SETCOMPINS, 0x12); err != nil {
return err
}
if c.vccState == SSD1306_EXTERNALVCC {
if err := c.command(SSD1306_SETCONTRAST, 0x9F); err != nil {
return err
}
} else {
if err := c.command(SSD1306_SETCONTRAST, 0xCF); err != nil {
return err
}
}
if c.vccState == SSD1306_EXTERNALVCC {
if err := c.command(SSD1306_SETPRECHARGE, 0x22); err != nil {
return err
}
} else {
if err := c.command(SSD1306_SETPRECHARGE, 0xF1); err != nil {
return err
}
}
if err := c.command(SSD1306_SETVCOMDETECT, 0x40); err != nil {
return err
}
if err := c.command(SSD1306_DISPLAYALLON_RESUME); err != nil {
return err
}
if err := c.command(SSD1306_NORMALDISPLAY); err != nil {
return err
}
if err := c.command(SSD1306_DISPLAYON); err != nil {
return err
}
return nil
}
func (c *SSD1306) command(cmd ...byte) error {
c.dcPin.Write(embd.Low)
_, err := c.spiBus.Write(cmd)
return err
}
func (c *SSD1306) data(d ...byte) error {
c.dcPin.Write(embd.High)
_, err := c.spiBus.Write(d)
return err
}
// Display sends the given buffer to the controller to "rendered"
func (c *SSD1306) Display(buf Buffer) error {
if err := c.command(SSD1306_COLUMNADDR, 0, byte(c.width-1)); err != nil {
return err
}
if err := c.command(SSD1306_PAGEADDR, 0, byte(c.pages-1)); err != nil {
return err
}
return c.data(buf.Cells()...)
}
// Close turns the display off
func (c *SSD1306) Close() error {
if err := c.command(SSD1306_DISPLAYOFF); err != nil {
return err
}
return nil
}
// NewBuffer creates a buffer that is suitably configured to be used in Display calls.
func (c *SSD1306) NewBuffer() Buffer {
return newBuffer(c.width, c.pages, memoryMode)
}

View File

@ -0,0 +1,149 @@
package ssd1306
import (
"github.com/kidoman/embd"
"testing"
"time"
)
type mockSpiBus struct {
chunks [][]byte
}
func (s *mockSpiBus) Write(p []byte) (n int, err error) {
s.chunks = append(s.chunks, p)
return 0, nil
}
func (s *mockSpiBus) TransferAndReceiveData(dataBuffer []uint8) error { return nil }
func (s *mockSpiBus) ReceiveData(len int) ([]uint8, error) { return nil, nil }
func (s *mockSpiBus) TransferAndReceiveByte(data byte) (byte, error) { return 0, nil }
func (s *mockSpiBus) ReceiveByte() (byte, error) { return 0, nil }
func (s *mockSpiBus) Close() error { return nil }
type mockPin struct {
values []int
}
func (p *mockPin) Watch(edge embd.Edge, handler func(embd.DigitalPin)) error { return nil }
func (p *mockPin) StopWatching() error { return nil }
func (p *mockPin) N() int { return 0 }
func (p *mockPin) Write(val int) error {
p.values = append(p.values, val)
return nil
}
func (p *mockPin) Read() (int, error) { return 0, nil }
func (p *mockPin) TimePulse(state int) (time.Duration, error) { return 0, nil }
func (p *mockPin) SetDirection(dir embd.Direction) error { return nil }
func (p *mockPin) ActiveLow(b bool) error { return nil }
func (p *mockPin) PullUp() error { return p.Write(1) }
func (p *mockPin) PullDown() error { return p.Write(0) }
func (p *mockPin) Close() error { return nil }
func TestInit(t *testing.T) {
spiBus := &mockSpiBus{}
dcPin := &mockPin{}
resetPin := &mockPin{}
controller, err := NewSPI(spiBus, dcPin, resetPin, 128, 64)
if err != nil {
t.Fatalf("Shouldn't be an error: %s", err)
}
if controller == nil {
t.Fatal("controller shouldn't be nil")
}
if len(resetPin.values) != 3 {
t.Error("expected 3 touches to the reset pin")
}
if len(dcPin.values) != 16 {
t.Error("expected 16 touches to the dc pin")
}
if len(spiBus.chunks) != 16 {
t.Error("expected 16 commands during init")
}
i := 0
if spiBus.chunks[i][0] != SSD1306_DISPLAYOFF {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_SETDISPLAYCLOCKDIV {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_SETMULTIPLEX {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_SETDISPLAYOFFSET {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_SETSTARTLINE|0x0 {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_CHARGEPUMP {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_MEMORYMODE {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_SEGREMAP|0x1 {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_COMSCANDEC {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_SETCOMPINS {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_SETCONTRAST {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_SETPRECHARGE {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_SETVCOMDETECT {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_DISPLAYALLON_RESUME {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_NORMALDISPLAY {
t.Errorf("Wrong command in chunk %d", i)
}
i++
if spiBus.chunks[i][0] != SSD1306_DISPLAYON {
t.Errorf("Wrong command in chunk %d", i)
}
i++
}

116
samples/ssd1306.go Normal file
View File

@ -0,0 +1,116 @@
// +build ignore
// This sample runs on a monochrome 128x64 OLED graphic display using an SSD1306 controller,
// such as https://www.adafruit.com/product/938.
// It demonstrates the rectangular fill/clear and point on/off operations animated across the display.
package main
import (
"github.com/golang/glog"
"github.com/kidoman/embd"
"github.com/kidoman/embd/controller/ssd1306"
_ "github.com/kidoman/embd/host/rpi" // This loads the RPi driver
"flag"
"os"
"os/signal"
"time"
)
func main() {
flag.Parse()
glog.Info("Starting")
if err := embd.InitSPI(); err != nil {
panic(err)
}
defer embd.CloseSPI()
spiBus := embd.NewSPIBus(embd.SPIMode0, 0, 1000000, 8, 0)
defer spiBus.Close()
if err := embd.InitGPIO(); err != nil {
panic(err)
}
defer embd.CloseGPIO()
dcPin := setupPin("GPIO_23")
defer dcPin.Close()
resetPin := setupPin("GPIO_24")
defer resetPin.Close()
controller, err := ssd1306.NewSPI(spiBus, dcPin, resetPin, 128, 64)
if err != nil {
glog.Fatalf("Failed to start: %s", err)
}
defer controller.Close()
buffer := controller.NewBuffer()
first := true
chunkWidth := 32
chunkHeight := 20
zipDir := 1
zipX := 0
prevZipX := 0
var prevX, prevY int
// Setup Control-C to gracefully stop
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt)
outer:
for {
for y := 0; y+chunkHeight <= 64; y += chunkHeight {
for x := 0; x+chunkWidth <= 128; x += chunkWidth {
select {
case <-done:
break outer
default:
glog.Infof("x=%d, y=%d\n", x, y)
if !first {
buffer.ClearRect(prevX, prevY, chunkWidth, chunkHeight)
} else {
first = false
}
buffer.FillRect(x, y, chunkWidth, chunkHeight)
prevX = x
prevY = y
buffer.Off(prevZipX, 63)
buffer.On(zipX, 63)
prevZipX = zipX
zipX += zipDir
if zipX >= 128 {
zipX = 127
zipDir = -1
} else if zipX < 0 {
zipX = 0
zipDir = 1
}
controller.Display(buffer)
time.Sleep(100 * time.Millisecond)
}
}
}
}
}
func setupPin(key string) embd.DigitalPin {
p, err := embd.NewDigitalPin(key)
if err != nil {
panic(err)
}
if err := p.SetDirection(embd.Out); err != nil {
panic(err)
}
return p
}