mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-12 19:03:42 +00:00
adafruit4650: support for Adafruite 4650 feather OLED
This commit is contained in:
committed by
Ron Evans
parent
36a12dd7a2
commit
dc2de4f9ed
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 |
@@ -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})
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
# get an md5sum).
|
# 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/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=itsybitsy-m0 ./examples/adxl345/main.go
|
||||||
tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/amg88xx
|
tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/amg88xx
|
||||||
|
|||||||
Reference in New Issue
Block a user