From ad2b76be8b9b3e756b16262ad45f76875ad4d1a4 Mon Sep 17 00:00:00 2001 From: Daniel Esteban Date: Sun, 5 May 2019 11:45:26 +0200 Subject: [PATCH] support for SSD1306 displays using I2C or SPI, several screen sizes (#49) supported too --- .circleci/config.yml | 2 + README.md | 1 + examples/ssd1306/i2c_128x32/main.go | 51 +++++ examples/ssd1306/spi_128x64/main.go | 48 +++++ ssd1306/registers.go | 41 ++++ ssd1306/ssd1306.go | 302 ++++++++++++++++++++++++++++ 6 files changed, 445 insertions(+) create mode 100644 examples/ssd1306/i2c_128x32/main.go create mode 100644 examples/ssd1306/spi_128x64/main.go create mode 100644 ssd1306/registers.go create mode 100644 ssd1306/ssd1306.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 65f95bc..bbd0139 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,6 +32,8 @@ jobs: - run: tinygo build -size short -o test.elf -target=itsybitsy-m0 ./examples/mpu6050/main.go - run: tinygo build -size short -o test.elf -target=microbit ./examples/pcd8544/setbuffer/main.go - run: tinygo build -size short -o test.elf -target=microbit ./examples/pcd8544/setpixel/main.go + - run: tinygo build -size short -o test.elf -target=microbit ./examples/ssd1306/i2c_128x32/main.go + - run: tinygo build -size short -o test.elf -target=microbit ./examples/ssd1306/spi_128x64/main.go - run: tinygo build -size short -o test.elf -target=circuitplay-express ./examples/thermistor/main.go - run: tinygo build -size short -o test.elf -target=itsybitsy-m0 ./examples/vl53l1x/main.go - run: tinygo build -size short -o test.elf -target=circuitplay-express ./examples/ws2812/main.go diff --git a/README.md b/README.md index 9cd8813..93a8699 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ func main() { | [MMA8653 accelerometer](https://www.nxp.com/docs/en/data-sheet/MMA8653FC.pdf) | I2C | | [MPU6050 accelerometer/gyroscope](https://store.invensense.com/datasheets/invensense/MPU-6050_DataSheet_V3%204.pdf) | I2C | | [PCD8544 display](http://eia.udg.edu/~forest/PCD8544_1.pdf) | SPI | +| [SSD1306 OLED display](https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf) | I2C / SPI | | [Thermistor](https://www.farnell.com/datasheets/33552.pdf) | ADC | | [VL53L1X time-of-flight distance sensor](https://www.st.com/resource/en/datasheet/vl53l1x.pdf) | I2C | | [WS2812 RGB LED](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) | GPIO | diff --git a/examples/ssd1306/i2c_128x32/main.go b/examples/ssd1306/i2c_128x32/main.go new file mode 100644 index 0000000..3e79a61 --- /dev/null +++ b/examples/ssd1306/i2c_128x32/main.go @@ -0,0 +1,51 @@ +package i2c_128x32 + +import ( + "machine" + + "image/color" + "time" + + "github.com/tinygo-org/drivers/ssd1306" +) + +func main() { + machine.I2C0.Configure(machine.I2CConfig{ + Frequency: machine.TWI_FREQ_400KHZ, + }) + + display := ssd1306.NewI2C(machine.I2C0) + display.Configure(ssd1306.Config{ + Address: ssd1306.Address_128_32, + Width: 128, + Height: 32, + }) + + display.ClearDisplay() + + x := int16(0) + y := int16(0) + deltaX := int16(1) + deltaY := int16(1) + for { + pixel := display.GetPixel(x, y) + c := color.RGBA{255, 255, 255, 255} + if pixel { + c = color.RGBA{0, 0, 0, 255} + } + display.SetPixel(x, y, c) + display.Display() + + x += deltaX + y += deltaY + + if x == 0 || x == 127 { + deltaX = -deltaX + } + + if y == 0 || y == 31 { + deltaY = -deltaY + } + time.Sleep(1 * time.Millisecond) + } +} diff --git a/examples/ssd1306/spi_128x64/main.go b/examples/ssd1306/spi_128x64/main.go new file mode 100644 index 0000000..1eca945 --- /dev/null +++ b/examples/ssd1306/spi_128x64/main.go @@ -0,0 +1,48 @@ +package i2c_128x64 + +import ( + "image/color" + "machine" + "time" + + "github.com/tinygo-org/drivers/ssd1306" +) + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 8000000, + }) + display := ssd1306.NewSPI(machine.SPI0, machine.P8, machine.P7, machine.P9) + display.Configure(ssd1306.Config{ + Width: 128, + Height: 64, + }) + + display.ClearDisplay() + + x := int16(64) + y := int16(32) + deltaX := int16(1) + deltaY := int16(1) + for { + pixel := display.GetPixel(x, y) + c := color.RGBA{255, 255, 255, 255} + if pixel { + c = color.RGBA{0, 0, 0, 255} + } + display.SetPixel(x, y, c) + display.Display() + + x += deltaX + y += deltaY + + if x == 0 || x == 127 { + deltaX = -deltaX + } + + if y == 0 || y == 63 { + deltaY = -deltaY + } + time.Sleep(1 * time.Millisecond) + } +} diff --git a/ssd1306/registers.go b/ssd1306/registers.go new file mode 100644 index 0000000..63590b8 --- /dev/null +++ b/ssd1306/registers.go @@ -0,0 +1,41 @@ +package ssd1306 + +// Registers +const ( + Address = 0x3D + Address_128_32 = 0x3C + + SETCONTRAST = 0x81 + DISPLAYALLON_RESUME = 0xA4 + DISPLAYALLON = 0xA5 + NORMALDISPLAY = 0xA6 + INVERTDISPLAY = 0xA7 + DISPLAYOFF = 0xAE + DISPLAYON = 0xAF + SETDISPLAYOFFSET = 0xD3 + SETCOMPINS = 0xDA + SETVCOMDETECT = 0xDB + SETDISPLAYCLOCKDIV = 0xD5 + SETPRECHARGE = 0xD9 + SETMULTIPLEX = 0xA8 + SETLOWCOLUMN = 0x00 + SETHIGHCOLUMN = 0x10 + SETSTARTLINE = 0x40 + MEMORYMODE = 0x20 + COLUMNADDR = 0x21 + PAGEADDR = 0x22 + COMSCANINC = 0xC0 + COMSCANDEC = 0xC8 + SEGREMAP = 0xA0 + CHARGEPUMP = 0x8D + ACTIVATE_SCROLL = 0x2F + DEACTIVATE_SCROLL = 0x2E + SET_VERTICAL_SCROLL_AREA = 0xA3 + RIGHT_HORIZONTAL_SCROLL = 0x26 + LEFT_HORIZONTAL_SCROLL = 0x27 + VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL = 0x29 + VERTICAL_AND_LEFT_HORIZONTAL_SCROLL = 0x2A + + EXTERNALVCC VccMode = 0x1 + SWITCHCAPVCC VccMode = 0x2 +) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go new file mode 100644 index 0000000..854fc5a --- /dev/null +++ b/ssd1306/ssd1306.go @@ -0,0 +1,302 @@ +// Package ssd1306 implements a driver for the SSD1306 led matrix controller, it comes in various colors and screen sizes. +// +// Datasheet: https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf +package ssd1306 + +import ( + "errors" + "image/color" + "machine" + "time" +) + +// Device wraps an SPI connection. +type Device struct { + bus Buser + buffer []byte + width int16 + height int16 + bufferSize int16 + vccState VccMode +} + +// Config is the configuration for the display +type Config struct { + Width int16 + Height int16 + VccState VccMode + Address uint16 +} + +type I2CBus struct { + wire machine.I2C + Address uint16 +} + +type SPIBus struct { + wire machine.SPI + dcPin machine.GPIO + resetPin machine.GPIO + csPin machine.GPIO +} + +type Buser interface { + configure() + tx(data []byte, isCommand bool) + setAddress(address uint16) +} + +type VccMode uint8 + +// NewI2C creates a new SSD1306 connection. The I2C wire must already be configured. +func NewI2C(bus machine.I2C) Device { + return Device{ + bus: &I2CBus{ + wire: bus, + Address: Address, + }, + } +} + +// NewSPI creates a new SSD1306 connection. The SPI wire must already be configured. +func NewSPI(bus machine.SPI, dcPin uint8, resetPin uint8, csPin uint8) Device { + pinDc := machine.GPIO{dcPin} + pinDc.Configure(machine.GPIOConfig{Mode: machine.GPIO_OUTPUT}) + pinReset := machine.GPIO{resetPin} + pinReset.Configure(machine.GPIOConfig{Mode: machine.GPIO_OUTPUT}) + pinCs := machine.GPIO{csPin} + pinCs.Configure(machine.GPIOConfig{Mode: machine.GPIO_OUTPUT}) + return Device{ + bus: &SPIBus{ + wire: bus, + dcPin: pinDc, + resetPin: pinReset, + csPin: pinCs, + }, + } +} + +// Configure initializes the display with default configuration +func (d *Device) Configure(cfg Config) { + if cfg.Width != 0 { + d.width = cfg.Width + } else { + d.width = 128 + } + if cfg.Height != 0 { + d.height = cfg.Height + } else { + d.height = 64 + } + if cfg.Address != 0 { + d.bus.setAddress(cfg.Address) + } + if cfg.VccState != 0 { + d.vccState = cfg.VccState + } else { + d.vccState = SWITCHCAPVCC + } + d.bufferSize = d.width * d.height / 8 + d.buffer = make([]byte, d.bufferSize) + + d.bus.configure() + + time.Sleep(100 * time.Nanosecond) + d.Command(DISPLAYOFF) + d.Command(SETDISPLAYCLOCKDIV) + d.Command(0x80) + d.Command(SETMULTIPLEX) + d.Command(uint8(d.height - 1)) + d.Command(SETDISPLAYOFFSET) + d.Command(0x0) + d.Command(SETSTARTLINE | 0x0) + d.Command(CHARGEPUMP) + if d.vccState == EXTERNALVCC { + d.Command(0x10) + } else { + d.Command(0x14) + } + d.Command(MEMORYMODE) + d.Command(0x00) + d.Command(SEGREMAP | 0x1) + d.Command(COMSCANDEC) + + if (d.width == 128 && d.height == 64) || (d.width == 64 && d.height == 48) { // 128x64 or 64x48 + d.Command(SETCOMPINS) + d.Command(0x12) + d.Command(SETCONTRAST) + if d.vccState == EXTERNALVCC { + d.Command(0x9F) + } else { + d.Command(0xCF) + } + } else if d.width == 128 && d.height == 32 { // 128x32 + d.Command(SETCOMPINS) + d.Command(0x02) + d.Command(SETCONTRAST) + d.Command(0x8F) + } else if d.width == 96 && d.height == 16 { // 96x16 + d.Command(SETCOMPINS) + d.Command(0x2) + d.Command(SETCONTRAST) + if d.vccState == EXTERNALVCC { + d.Command(0x10) + } else { + d.Command(0xAF) + } + } else { + // fail silently, it might work + println("there's no configuration for this display's size") + } + + d.Command(SETPRECHARGE) + if d.vccState == EXTERNALVCC { + d.Command(0x22) + } else { + d.Command(0xF1) + } + d.Command(SETVCOMDETECT) + d.Command(0x40) + d.Command(DISPLAYALLON_RESUME) + d.Command(NORMALDISPLAY) + d.Command(DEACTIVATE_SCROLL) + d.Command(DISPLAYON) +} + +// ClearBuffer clears the image buffer +func (d *Device) ClearBuffer() { + for i := int16(0); i < d.bufferSize; i++ { + d.buffer[i] = 0 + } +} + +// ClearDisplay clears the image buffer and clear the display +func (d *Device) ClearDisplay() { + d.ClearBuffer() + d.Display() +} + +// Display sends the whole buffer to the screen +func (d *Device) Display() error { + // In the 128x64 (SPI) screen resetting to 0x0 after 128 times corrupt the buffer + // Since we're printing the whole buffer, avoid resetting it + if d.width != 128 || d.height != 64 { + d.Command(COLUMNADDR) + d.Command(0) + d.Command(uint8(d.width - 1)) + d.Command(PAGEADDR) + d.Command(0) + d.Command(uint8(d.height/8) - 1) + } + + d.Tx(d.buffer, false) + return nil +} + +// SetPixel enables or disables a pixel in the buffer +// color.RGBA{0, 0, 0, 255} is consider transparent, anything else +// with enable a pixel on the screen +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + if x < 0 || x >= d.width || y < 0 || y >= d.height { + return + } + byteIndex := x + (y/8)*d.width + if c.R != 0 || c.G != 0 || c.B != 0 { + d.buffer[byteIndex] |= 1 << uint8(y%8) + } else { + d.buffer[byteIndex] &^= 1 << uint8(y%8) + } +} + +// GetPixel returns if the specified pixel is on (true) or off (false) +func (d *Device) GetPixel(x int16, y int16) bool { + if x < 0 || x >= d.width || y < 0 || y >= d.height { + return false + } + byteIndex := x + (y/8)*d.width + return (d.buffer[byteIndex] >> uint8(y%8) & 0x1) == 1 +} + +// SetBuffer changes the whole buffer at once +func (d *Device) SetBuffer(buffer []byte) error { + if int16(len(buffer)) != d.bufferSize { + //return ErrBuffer + return errors.New("Wrong size buffer") + } + for i := int16(0); i < d.bufferSize; i++ { + d.buffer[i] = buffer[i] + } + return nil +} + +// Command sends a command to the display +func (d *Device) Command(command uint8) { + d.bus.tx([]byte{command}, true) +} + +// setAddress sets the address to the I2C bus +func (b *I2CBus) setAddress(address uint16) { + b.Address = address +} + +// setAddress does nothing, but it's required to avoid reflection +func (b *SPIBus) setAddress(address uint16) { + // do nothing + println("trying to Configure an address on a SPI device") +} + +// configure does nothing, but it's required to avoid reflection +func (b *I2CBus) configure() {} + +// configure configures some pins with the SPI bus +func (b *SPIBus) configure() { + b.csPin.Low() + b.dcPin.Low() + b.resetPin.Low() + + b.resetPin.High() + time.Sleep(1 * time.Millisecond) + b.resetPin.Low() + time.Sleep(10 * time.Millisecond) + b.resetPin.High() +} + +// Tx sends data to the display +func (d *Device) Tx(data []byte, isCommand bool) { + d.bus.tx(data, isCommand) +} + +// tx sends data to the display (I2CBus implementation) +func (b *I2CBus) tx(data []byte, isCommand bool) { + if isCommand { + b.wire.WriteRegister(uint8(b.Address), 0x00, data) + } else { + b.wire.WriteRegister(uint8(b.Address), 0x40, data) + } +} + +// tx sends data to the display (SPIBus implementation) +func (b *SPIBus) tx(data []byte, isCommand bool) { + if isCommand { + b.csPin.High() + time.Sleep(1 * time.Millisecond) + b.dcPin.Low() + b.csPin.Low() + + b.wire.Tx(data, nil) + b.csPin.High() + } else { + b.csPin.High() + time.Sleep(1 * time.Millisecond) + b.dcPin.High() + b.csPin.Low() + + b.wire.Tx(data, nil) + b.csPin.High() + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + return d.width, d.height +}