diff --git a/adafruit4650/device.go b/adafruit4650/device.go new file mode 100644 index 0000000..82fc78b --- /dev/null +++ b/adafruit4650/device.go @@ -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 + } +} diff --git a/adafruit4650/device_test.go b/adafruit4650/device_test.go new file mode 100644 index 0000000..9cf56f9 --- /dev/null +++ b/adafruit4650/device_test.go @@ -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<