mirror of
https://github.com/kidoman/embd
synced 2024-09-21 06:51:41 +02:00
Adding SSD1306 OLED display controller support
This commit is contained in:
parent
d3d8c0c5c6
commit
903dc72cb7
98
controller/ssd1306/buffer.go
Normal file
98
controller/ssd1306/buffer.go
Normal 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
|
||||||
|
}
|
126
controller/ssd1306/buffer_test.go
Normal file
126
controller/ssd1306/buffer_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
236
controller/ssd1306/ssd1306.go
Normal file
236
controller/ssd1306/ssd1306.go
Normal 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)
|
||||||
|
}
|
149
controller/ssd1306/ssd1306_test.go
Normal file
149
controller/ssd1306/ssd1306_test.go
Normal 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
116
samples/ssd1306.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user