Skip to content

Commit dc2de4f

Browse files
thomasrichner-ovivadeadprogram
authored andcommitted
adafruit4650: support for Adafruite 4650 feather OLED
1 parent 36a12dd commit dc2de4f

File tree

5 files changed

+415
-0
lines changed

5 files changed

+415
-0
lines changed

adafruit4650/device.go

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Package adafruit4650 implements a driver for the Adafruit FeatherWing OLED - 128x64 OLED display.
2+
// The display is backed itself by a SH1107 driver chip.
3+
//
4+
// Store: https://www.adafruit.com/product/4650
5+
//
6+
// Documentation: https://learn.adafruit.com/adafruit-128x64-oled-featherwing
7+
package adafruit4650
8+
9+
import (
10+
"image/color"
11+
"time"
12+
13+
"tinygo.org/x/drivers"
14+
)
15+
16+
const DefaultAddress = 0x3c
17+
18+
const (
19+
commandSetLowColumn = 0x00
20+
commandSetHighColumn = 0x10
21+
commandSetPage = 0xb0
22+
)
23+
24+
const (
25+
width = 128
26+
height = 64
27+
)
28+
29+
// Device represents an Adafruit 4650 device
30+
type Device struct {
31+
bus drivers.I2C
32+
Address uint8
33+
buffer []byte
34+
width int16
35+
height int16
36+
}
37+
38+
// New creates a new device, not configuring anything yet.
39+
func New(bus drivers.I2C) Device {
40+
return Device{
41+
bus: bus,
42+
Address: DefaultAddress,
43+
width: width,
44+
height: height,
45+
}
46+
}
47+
48+
// Configure initializes the display with default configuration
49+
func (d *Device) Configure() error {
50+
51+
bufferSize := d.width * d.height / 8
52+
d.buffer = make([]byte, bufferSize)
53+
54+
// This sequence is an amalgamation of the datasheet, official Arduino driver, CircuitPython driver and other drivers
55+
initSequence := []byte{
56+
0xae, // display off, sleep mode
57+
//0xd5, 0x41, // set display clock divider (from original datasheet)
58+
0xd5, 0x51, // set display clock divider (from Adafruit driver)
59+
0xd9, 0x22, // pre-charge/dis-charge period mode: 2 DCLKs/2 DCLKs (POR)
60+
0x20, // memory mode
61+
0x81, 0x4f, // contrast setting = 0x4f
62+
0xad, 0x8a, // set dc/dc pump
63+
0xa0, // segment remap, flip-x
64+
0xc0, // common output scan direction
65+
0xdc, 0x00, // set display start line 0 (POR=0)
66+
0xa8, 0x3f, // multiplex ratio, height - 1 = 0x3f
67+
0xd3, 0x60, // set display offset mode = 0x60
68+
0xdb, 0x35, // VCOM deselect level = 0.770 (POR)
69+
0xa4, // entire display off, retain RAM, normal status (POR)
70+
0xa6, // normal (not reversed) display
71+
0xaf, // display on
72+
}
73+
74+
err := d.writeCommands(initSequence)
75+
if err != nil {
76+
return err
77+
}
78+
79+
// recommended in the datasheet, same in other drivers
80+
time.Sleep(100 * time.Millisecond)
81+
82+
return nil
83+
}
84+
85+
// ClearDisplay clears the image buffer as well as the actual display
86+
func (d *Device) ClearDisplay() error {
87+
d.ClearBuffer()
88+
return d.Display()
89+
}
90+
91+
// ClearBuffer clears the buffer
92+
func (d *Device) ClearBuffer() {
93+
bzero(d.buffer)
94+
}
95+
96+
// SetPixel modifies the internal buffer. Since this display has a bit-depth of 1 bit any non-zero
97+
// color component will be treated as 'on', otherwise 'off'.
98+
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
99+
if x < 0 || x >= d.width || y < 0 || y >= d.height {
100+
return
101+
}
102+
103+
// RAM layout
104+
// *-----> y
105+
// |
106+
// x| col0 col1 ... col63
107+
// v p0 a0 b0 ..
108+
// a1 b1 ..
109+
// .. .. ..
110+
// a7 b7 ..
111+
// p1 a0 b0
112+
// a1 b1
113+
//
114+
115+
//flip y - so the display orientation matches the silk screen labeling etc.
116+
y = d.height - y - 1
117+
118+
page := x / 8
119+
bytesPerPage := d.height
120+
byteIndex := y + bytesPerPage*page
121+
bit := x % 8
122+
if (c.R | c.G | c.B) != 0 {
123+
d.buffer[byteIndex] |= 1 << uint8(bit)
124+
} else {
125+
d.buffer[byteIndex] &^= 1 << uint8(bit)
126+
}
127+
}
128+
129+
// Display sends the whole buffer to the screen
130+
func (d *Device) Display() error {
131+
132+
bytesPerPage := d.height
133+
134+
pages := (d.width + 7) / 8
135+
for page := int16(0); page < pages; page++ {
136+
137+
err := d.setRAMPosition(uint8(page), 0)
138+
if err != nil {
139+
return err
140+
}
141+
142+
offset := page * bytesPerPage
143+
err = d.writeRAM(d.buffer[offset : offset+bytesPerPage])
144+
if err != nil {
145+
return err
146+
}
147+
}
148+
149+
return nil
150+
}
151+
152+
// setRAMPosition updates the device's current page and column position
153+
func (d *Device) setRAMPosition(page uint8, column uint8) error {
154+
if page > 15 {
155+
panic("page out of bounds")
156+
}
157+
if column > 127 {
158+
panic("column out of bounds")
159+
}
160+
setPage := commandSetPage | (page & 0xF)
161+
162+
lo := column & 0xF
163+
setLowColumn := commandSetLowColumn | lo
164+
165+
hi := (column >> 4) & 0x7
166+
setHighColumn := commandSetHighColumn | hi
167+
168+
cmds := []byte{
169+
setPage,
170+
setLowColumn,
171+
setHighColumn,
172+
}
173+
174+
return d.writeCommands(cmds)
175+
}
176+
177+
// Size returns the current size of the display.
178+
func (d *Device) Size() (w, h int16) {
179+
return d.width, d.height
180+
}
181+
182+
func (d *Device) writeCommands(commands []byte) error {
183+
onlyCommandsFollowing := byte(0x00)
184+
return d.bus.Tx(uint16(d.Address), append([]byte{onlyCommandsFollowing}, commands...), nil)
185+
}
186+
187+
func (d *Device) writeRAM(data []byte) error {
188+
onlyRAMFollowing := byte(0x40)
189+
return d.bus.Tx(uint16(d.Address), append([]byte{onlyRAMFollowing}, data...), nil)
190+
}
191+
192+
func bzero(buf []byte) {
193+
for i := range buf {
194+
buf[i] = 0
195+
}
196+
}

adafruit4650/device_test.go

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package adafruit4650
2+
3+
import (
4+
"bytes"
5+
_ "embed"
6+
"encoding/hex"
7+
"fmt"
8+
"image"
9+
"image/color"
10+
"image/draw"
11+
"image/png"
12+
"os"
13+
"testing"
14+
"time"
15+
"tinygo.org/x/drivers"
16+
"tinygo.org/x/tinyfont"
17+
"tinygo.org/x/tinyfont/freemono"
18+
)
19+
20+
//go:embed expected_hello_world.png
21+
var expectedHelloWorld []byte
22+
23+
// mockBus mocks a fake i2c device adafruit4650 display.
24+
// The memory layout assumes that clients set up the device in a particular way and always send complete
25+
// pages to the device buffer.
26+
type mockBus struct {
27+
img draw.Image
28+
line int
29+
addr uint8
30+
currentPage int
31+
currentColumn int
32+
}
33+
34+
func (m *mockBus) Tx(addr uint16, w, r []byte) error {
35+
if addr != uint16(m.addr) {
36+
panic("unexpected address")
37+
}
38+
if r != nil {
39+
panic("mock does not support reads")
40+
}
41+
42+
if w[0] == 0x00 {
43+
if w[1]&0xf0 == 0xb0 {
44+
m.currentPage = int(w[1] & 0x0f)
45+
46+
lo := w[2] & 0x0f
47+
hi := w[2] & 0x07
48+
m.currentColumn = int(hi<<4 | lo)
49+
}
50+
return nil
51+
}
52+
if w[0] != 0x40 {
53+
panic("unexpected first byte: " + hex.EncodeToString(w[0:1]))
54+
}
55+
56+
return m.writeRAM(w[1:])
57+
}
58+
59+
func newMock() *mockBus {
60+
61+
m := image.NewRGBA(image.Rect(0, 0, width, height))
62+
return &mockBus{img: m, addr: DefaultAddress, currentPage: -1, currentColumn: -1}
63+
}
64+
65+
func (m *mockBus) writeRAM(data []byte) error {
66+
67+
// RAM layout
68+
// *-----> y
69+
// |
70+
// x| col0 col1 ... col63
71+
// v p0 a0 b0 ..
72+
// a1 b1 ..
73+
// .. .. ..
74+
// a7 b7 ..
75+
// p1 a0 b0
76+
// a1 b1
77+
//
78+
79+
fmt.Printf("writing page %d\n", m.currentPage)
80+
// assuming entire pages will be written
81+
for x := 0; x < 8; x++ {
82+
for y := 0; y < height; y++ {
83+
84+
col := data[y]
85+
86+
c := color.Black
87+
if col&(1<<x) != 0 {
88+
c = color.White
89+
}
90+
91+
m.img.Set(x+m.currentPage*8, height-y-1, c)
92+
}
93+
}
94+
95+
return nil
96+
}
97+
98+
func (m *mockBus) toImage() *image.RGBA {
99+
100+
container := image.NewRGBA(m.img.Bounds().Inset(-1))
101+
draw.Draw(container, container.Bounds(), image.NewUniform(color.RGBA{G: 255, A: 255}), image.Point{}, draw.Over)
102+
draw.Draw(container, m.img.Bounds(), m.img, image.Point{}, draw.Over)
103+
return container
104+
}
105+
106+
func TestDevice_Display(t *testing.T) {
107+
108+
bus := newMock()
109+
dev := New(bus)
110+
111+
dev.Configure()
112+
113+
drawPlus(&dev)
114+
drawHellowWorld(&dev)
115+
116+
//when
117+
dev.Display()
118+
119+
//then
120+
actual := bus.toImage()
121+
122+
expected, err := png.Decode(bytes.NewReader(expectedHelloWorld))
123+
if err != nil {
124+
panic(err)
125+
}
126+
127+
assertEqualImages(t, actual, expected)
128+
}
129+
130+
func drawPlus(d drivers.Displayer) {
131+
for i := int16(0); i < 128; i++ {
132+
d.SetPixel(i, 32, color.RGBA{R: 1})
133+
}
134+
for i := int16(0); i < 64; i++ {
135+
d.SetPixel(64, i, color.RGBA{R: 1})
136+
}
137+
}
138+
139+
func drawHellowWorld(d drivers.Displayer) {
140+
tinyfont.WriteLine(d, &freemono.Regular9pt7b, 0, 32, "Hello World!", color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
141+
}
142+
143+
func assertEqualImages(t testing.TB, actual, expected image.Image) {
144+
145+
if actual.Bounds().Dx() != expected.Bounds().Dx() || actual.Bounds().Dy() != expected.Bounds().Dy() {
146+
f := writeImage(actual)
147+
t.Fatalf("differing size: was %v, expected %v, saved actual to %s", actual.Bounds(), expected.Bounds(), f)
148+
}
149+
150+
bb := expected.Bounds()
151+
for x := bb.Min.X; x < bb.Max.X; x++ {
152+
for y := bb.Min.Y; y < bb.Max.Y; y++ {
153+
actualBB := actual.Bounds()
154+
if actual.At(x+actualBB.Min.X, y+actualBB.Min.Y) != expected.At(x, y) {
155+
f := writeImage(actual)
156+
t.Fatalf("different pixel at %d/%d: %v != %v, saved actual at %s", x, y, actual.At(x, y), expected.At(x, y), f)
157+
}
158+
}
159+
}
160+
}
161+
162+
func writeImage(img image.Image) string {
163+
164+
fn := fmt.Sprintf("%d.png", time.Now().Unix())
165+
f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0644)
166+
if err != nil {
167+
panic(err)
168+
}
169+
defer f.Close()
170+
171+
err = png.Encode(f, img)
172+
if err != nil {
173+
panic(err)
174+
}
175+
return fn
176+
}

adafruit4650/expected_hello_world.png

449 Bytes
Loading

0 commit comments

Comments
 (0)