adafruit4650: support for Adafruite 4650 feather OLED

This commit is contained in:
Thomas Richner
2023-09-22 23:30:04 +02:00
committed by Ron Evans
parent 36a12dd7a2
commit dc2de4f9ed
5 changed files with 415 additions and 0 deletions
+196
View File
@@ -0,0 +1,196 @@
// Package adafruit4650 implements a driver for the Adafruit FeatherWing OLED - 128x64 OLED display.
// The display is backed itself by a SH1107 driver chip.
//
// Store: https://www.adafruit.com/product/4650
//
// Documentation: https://learn.adafruit.com/adafruit-128x64-oled-featherwing
package adafruit4650
import (
"image/color"
"time"
"tinygo.org/x/drivers"
)
const DefaultAddress = 0x3c
const (
commandSetLowColumn = 0x00
commandSetHighColumn = 0x10
commandSetPage = 0xb0
)
const (
width = 128
height = 64
)
// Device represents an Adafruit 4650 device
type Device struct {
bus drivers.I2C
Address uint8
buffer []byte
width int16
height int16
}
// New creates a new device, not configuring anything yet.
func New(bus drivers.I2C) Device {
return Device{
bus: bus,
Address: DefaultAddress,
width: width,
height: height,
}
}
// Configure initializes the display with default configuration
func (d *Device) Configure() error {
bufferSize := d.width * d.height / 8
d.buffer = make([]byte, bufferSize)
// This sequence is an amalgamation of the datasheet, official Arduino driver, CircuitPython driver and other drivers
initSequence := []byte{
0xae, // display off, sleep mode
//0xd5, 0x41, // set display clock divider (from original datasheet)
0xd5, 0x51, // set display clock divider (from Adafruit driver)
0xd9, 0x22, // pre-charge/dis-charge period mode: 2 DCLKs/2 DCLKs (POR)
0x20, // memory mode
0x81, 0x4f, // contrast setting = 0x4f
0xad, 0x8a, // set dc/dc pump
0xa0, // segment remap, flip-x
0xc0, // common output scan direction
0xdc, 0x00, // set display start line 0 (POR=0)
0xa8, 0x3f, // multiplex ratio, height - 1 = 0x3f
0xd3, 0x60, // set display offset mode = 0x60
0xdb, 0x35, // VCOM deselect level = 0.770 (POR)
0xa4, // entire display off, retain RAM, normal status (POR)
0xa6, // normal (not reversed) display
0xaf, // display on
}
err := d.writeCommands(initSequence)
if err != nil {
return err
}
// recommended in the datasheet, same in other drivers
time.Sleep(100 * time.Millisecond)
return nil
}
// ClearDisplay clears the image buffer as well as the actual display
func (d *Device) ClearDisplay() error {
d.ClearBuffer()
return d.Display()
}
// ClearBuffer clears the buffer
func (d *Device) ClearBuffer() {
bzero(d.buffer)
}
// SetPixel modifies the internal buffer. Since this display has a bit-depth of 1 bit any non-zero
// color component will be treated as 'on', otherwise 'off'.
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
if x < 0 || x >= d.width || y < 0 || y >= d.height {
return
}
// RAM layout
// *-----> y
// |
// x| col0 col1 ... col63
// v p0 a0 b0 ..
// a1 b1 ..
// .. .. ..
// a7 b7 ..
// p1 a0 b0
// a1 b1
//
//flip y - so the display orientation matches the silk screen labeling etc.
y = d.height - y - 1
page := x / 8
bytesPerPage := d.height
byteIndex := y + bytesPerPage*page
bit := x % 8
if (c.R | c.G | c.B) != 0 {
d.buffer[byteIndex] |= 1 << uint8(bit)
} else {
d.buffer[byteIndex] &^= 1 << uint8(bit)
}
}
// Display sends the whole buffer to the screen
func (d *Device) Display() error {
bytesPerPage := d.height
pages := (d.width + 7) / 8
for page := int16(0); page < pages; page++ {
err := d.setRAMPosition(uint8(page), 0)
if err != nil {
return err
}
offset := page * bytesPerPage
err = d.writeRAM(d.buffer[offset : offset+bytesPerPage])
if err != nil {
return err
}
}
return nil
}
// setRAMPosition updates the device's current page and column position
func (d *Device) setRAMPosition(page uint8, column uint8) error {
if page > 15 {
panic("page out of bounds")
}
if column > 127 {
panic("column out of bounds")
}
setPage := commandSetPage | (page & 0xF)
lo := column & 0xF
setLowColumn := commandSetLowColumn | lo
hi := (column >> 4) & 0x7
setHighColumn := commandSetHighColumn | hi
cmds := []byte{
setPage,
setLowColumn,
setHighColumn,
}
return d.writeCommands(cmds)
}
// Size returns the current size of the display.
func (d *Device) Size() (w, h int16) {
return d.width, d.height
}
func (d *Device) writeCommands(commands []byte) error {
onlyCommandsFollowing := byte(0x00)
return d.bus.Tx(uint16(d.Address), append([]byte{onlyCommandsFollowing}, commands...), nil)
}
func (d *Device) writeRAM(data []byte) error {
onlyRAMFollowing := byte(0x40)
return d.bus.Tx(uint16(d.Address), append([]byte{onlyRAMFollowing}, data...), nil)
}
func bzero(buf []byte) {
for i := range buf {
buf[i] = 0
}
}
+176
View File
@@ -0,0 +1,176 @@
package adafruit4650
import (
"bytes"
_ "embed"
"encoding/hex"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
"testing"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freemono"
)
//go:embed expected_hello_world.png
var expectedHelloWorld []byte
// mockBus mocks a fake i2c device adafruit4650 display.
// The memory layout assumes that clients set up the device in a particular way and always send complete
// pages to the device buffer.
type mockBus struct {
img draw.Image
line int
addr uint8
currentPage int
currentColumn int
}
func (m *mockBus) Tx(addr uint16, w, r []byte) error {
if addr != uint16(m.addr) {
panic("unexpected address")
}
if r != nil {
panic("mock does not support reads")
}
if w[0] == 0x00 {
if w[1]&0xf0 == 0xb0 {
m.currentPage = int(w[1] & 0x0f)
lo := w[2] & 0x0f
hi := w[2] & 0x07
m.currentColumn = int(hi<<4 | lo)
}
return nil
}
if w[0] != 0x40 {
panic("unexpected first byte: " + hex.EncodeToString(w[0:1]))
}
return m.writeRAM(w[1:])
}
func newMock() *mockBus {
m := image.NewRGBA(image.Rect(0, 0, width, height))
return &mockBus{img: m, addr: DefaultAddress, currentPage: -1, currentColumn: -1}
}
func (m *mockBus) writeRAM(data []byte) error {
// RAM layout
// *-----> y
// |
// x| col0 col1 ... col63
// v p0 a0 b0 ..
// a1 b1 ..
// .. .. ..
// a7 b7 ..
// p1 a0 b0
// a1 b1
//
fmt.Printf("writing page %d\n", m.currentPage)
// assuming entire pages will be written
for x := 0; x < 8; x++ {
for y := 0; y < height; y++ {
col := data[y]
c := color.Black
if col&(1<<x) != 0 {
c = color.White
}
m.img.Set(x+m.currentPage*8, height-y-1, c)
}
}
return nil
}
func (m *mockBus) toImage() *image.RGBA {
container := image.NewRGBA(m.img.Bounds().Inset(-1))
draw.Draw(container, container.Bounds(), image.NewUniform(color.RGBA{G: 255, A: 255}), image.Point{}, draw.Over)
draw.Draw(container, m.img.Bounds(), m.img, image.Point{}, draw.Over)
return container
}
func TestDevice_Display(t *testing.T) {
bus := newMock()
dev := New(bus)
dev.Configure()
drawPlus(&dev)
drawHellowWorld(&dev)
//when
dev.Display()
//then
actual := bus.toImage()
expected, err := png.Decode(bytes.NewReader(expectedHelloWorld))
if err != nil {
panic(err)
}
assertEqualImages(t, actual, expected)
}
func drawPlus(d drivers.Displayer) {
for i := int16(0); i < 128; i++ {
d.SetPixel(i, 32, color.RGBA{R: 1})
}
for i := int16(0); i < 64; i++ {
d.SetPixel(64, i, color.RGBA{R: 1})
}
}
func drawHellowWorld(d drivers.Displayer) {
tinyfont.WriteLine(d, &freemono.Regular9pt7b, 0, 32, "Hello World!", color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
}
func assertEqualImages(t testing.TB, actual, expected image.Image) {
if actual.Bounds().Dx() != expected.Bounds().Dx() || actual.Bounds().Dy() != expected.Bounds().Dy() {
f := writeImage(actual)
t.Fatalf("differing size: was %v, expected %v, saved actual to %s", actual.Bounds(), expected.Bounds(), f)
}
bb := expected.Bounds()
for x := bb.Min.X; x < bb.Max.X; x++ {
for y := bb.Min.Y; y < bb.Max.Y; y++ {
actualBB := actual.Bounds()
if actual.At(x+actualBB.Min.X, y+actualBB.Min.Y) != expected.At(x, y) {
f := writeImage(actual)
t.Fatalf("different pixel at %d/%d: %v != %v, saved actual at %s", x, y, actual.At(x, y), expected.At(x, y), f)
}
}
}
}
func writeImage(img image.Image) string {
fn := fmt.Sprintf("%d.png", time.Now().Unix())
f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
defer f.Close()
err = png.Encode(f, img)
if err != nil {
panic(err)
}
return fn
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

+42
View File
@@ -0,0 +1,42 @@
package main
import (
"image/color"
"machine"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/adafruit4650"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freemono"
)
func main() {
machine.I2C0.Configure(machine.I2CConfig{})
dev := adafruit4650.New(machine.I2C0)
err := dev.Configure()
if err != nil {
panic(err)
}
drawPlus(&dev)
drawHelloWorld(&dev)
err = dev.Display()
if err != nil {
panic(err)
}
}
func drawPlus(d drivers.Displayer) {
for i := int16(0); i < 128; i++ {
d.SetPixel(i, 32, color.RGBA{R: 1})
}
for i := int16(0); i < 64; i++ {
d.SetPixel(64, i, color.RGBA{R: 1})
}
}
func drawHelloWorld(d drivers.Displayer) {
tinyfont.WriteLine(d, &freemono.Regular9pt7b, 0, 32, "Hello World!", color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
}
+1
View File
@@ -5,6 +5,7 @@
# get an md5sum).
tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/adafruit4650
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adt7410/main.go
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adxl345/main.go
tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/amg88xx