add suport for SH1106 display driver

This commit is contained in:
BCG
2022-02-14 22:12:22 -05:00
committed by Ron Evans
parent 8b3075fdba
commit 877342d0ff
4 changed files with 423 additions and 0 deletions
+1
View File
@@ -120,6 +120,7 @@ The following 90 devices are supported.
| [Servo](https://learn.sparkfun.com/tutorials/hobby-servo-tutorial/all) | PWM |
| [Shift register (PISO)](https://en.wikipedia.org/wiki/Shift_register#Parallel-in_serial-out_\(PISO\)) | GPIO |
| [Shift registers (SIPO)](https://en.wikipedia.org/wiki/Shift_register#Serial-in_parallel-out_(SIPO)) | GPIO |
| [SH1106 OLED display](https://www.velleman.eu/downloads/29/infosheets/sh1106_datasheet.pdf) | I2C / SPI |
| [SHT3x Digital Humidity Sensor](https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/2_Humidity_Sensors/Datasheets/Sensirion_Humidity_Sensors_SHT3x_Datasheet_digital.pdf) | I2C |
| [SHTC3 Digital Humidity Sensor (RH/T)](https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/2_Humidity_Sensors/Datasheets/Sensirion_Humidity_Sensors_SHTC3_Datasheet.pdf) | I2C |
| [SPI NOR Flash Memory](https://en.wikipedia.org/wiki/Flash_memory#NOR_flash) | SPI/QSPI |
+56
View File
@@ -0,0 +1,56 @@
//go:build macropad_rp2040
package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/sh1106"
)
var (
display = sh1106.NewSPI(machine.SPI1, machine.OLED_DC, machine.OLED_RST, machine.OLED_CS)
)
func init() {
machine.SPI1.Configure(machine.SPIConfig{
Frequency: 48000000,
})
display.Configure(sh1106.Config{
Width: 128,
Height: 64,
})
}
func main() {
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 == 63 {
deltaY = -deltaY
}
time.Sleep(1 * time.Millisecond)
}
}
+40
View File
@@ -0,0 +1,40 @@
package sh1106
// Registers
const (
Address = 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
)
+326
View File
@@ -0,0 +1,326 @@
// Package sh1106 implements a driver for the SH1106 display controller
//
// Copied from https://github.com/toyo/tinygo-sh1106 (under BSD 3-clause license)
package sh1106 // import "tinygo.org/x/drivers/sh1106"
import (
"errors"
"image/color"
"machine"
"time"
"tinygo.org/x/drivers"
)
// Device wraps an SPI connection.
type Device struct {
bus Buser
buffer []byte
cmdbuf [1]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 drivers.I2C
Address uint16
}
type SPIBus struct {
wire drivers.SPI
dcPin machine.Pin
resetPin machine.Pin
csPin machine.Pin
}
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 drivers.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 drivers.SPI, dcPin, resetPin, csPin machine.Pin) Device {
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
return Device{
bus: &SPIBus{
wire: bus,
dcPin: dcPin,
resetPin: resetPin,
csPin: csPin,
},
}
}
// 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()
// busyWaitDelay(100 * time.Nanosecond)
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)
}
for pg := uint8(0); pg < uint8(d.height/8); pg++ {
d.Command(0xB0 | (pg & 0x07)) // SET_PAGE_ADDR
d.Command(SETLOWCOLUMN | 2)
d.Command(SETHIGHCOLUMN | 0)
d.Tx(d.buffer[uint16(pg)*0x80:uint16(pg+1)*0x80], 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
}
func (d *Device) SetScroll(line int16) {
d.Command(SETSTARTLINE + uint8(line&0b111111))
}
// Command sends a command to the display
func (d *Device) Command(command uint8) {
d.cmdbuf[0] = command
d.bus.tx(d.cmdbuf[:], 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()
// busyWaitDelay(time.Millisecond)
time.Sleep(1 * time.Millisecond)
b.resetPin.Low()
// busyWaitDelay(10 * time.Millisecond)
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()
riseTimeDelay()
b.dcPin.Low()
b.csPin.Low()
b.wire.Tx(data, nil)
b.csPin.High()
} else {
b.csPin.High()
riseTimeDelay()
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
}
// TODO: is this really necessary? seems to work fine without this on macropad-rp2040 at least
func riseTimeDelay() {
busyWaitDelay(1 * time.Microsecond)
}
func busyWaitDelay(duration time.Duration) {
for start := time.Now(); time.Since(start) < duration; {
}
}