tinygo.org/x/drivers@v0.27.1-0.20240509133757-7dbca2a54349/sh1106/sh1106.go (about)

     1  // Package sh1106 implements a driver for the SH1106 display controller
     2  //
     3  // Copied from https://github.com/toyo/tinygo-sh1106 (under BSD 3-clause license)
     4  package sh1106 // import "tinygo.org/x/drivers/sh1106"
     5  
     6  import (
     7  	"errors"
     8  	"image/color"
     9  	"machine"
    10  	"time"
    11  
    12  	"tinygo.org/x/drivers"
    13  	"tinygo.org/x/drivers/internal/legacy"
    14  )
    15  
    16  // Device wraps an SPI connection.
    17  type Device struct {
    18  	bus        Buser
    19  	buffer     []byte
    20  	cmdbuf     [1]byte
    21  	width      int16
    22  	height     int16
    23  	bufferSize int16
    24  	vccState   VccMode
    25  }
    26  
    27  // Config is the configuration for the display
    28  type Config struct {
    29  	Width    int16
    30  	Height   int16
    31  	VccState VccMode
    32  	Address  uint16
    33  }
    34  
    35  type I2CBus struct {
    36  	wire    drivers.I2C
    37  	Address uint16
    38  }
    39  
    40  type SPIBus struct {
    41  	wire     drivers.SPI
    42  	dcPin    machine.Pin
    43  	resetPin machine.Pin
    44  	csPin    machine.Pin
    45  }
    46  
    47  type Buser interface {
    48  	configure()
    49  	tx(data []byte, isCommand bool)
    50  	setAddress(address uint16)
    51  }
    52  
    53  type VccMode uint8
    54  
    55  // NewI2C creates a new SH1106 connection. The I2C wire must already be configured.
    56  func NewI2C(bus drivers.I2C) Device {
    57  	return Device{
    58  		bus: &I2CBus{
    59  			wire:    bus,
    60  			Address: Address,
    61  		},
    62  	}
    63  }
    64  
    65  // NewSPI creates a new SH1106 connection. The SPI wire must already be configured.
    66  func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) Device {
    67  	dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
    68  	resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
    69  	csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
    70  	return Device{
    71  		bus: &SPIBus{
    72  			wire:     bus,
    73  			dcPin:    dcPin,
    74  			resetPin: resetPin,
    75  			csPin:    csPin,
    76  		},
    77  	}
    78  }
    79  
    80  // Configure initializes the display with default configuration
    81  func (d *Device) Configure(cfg Config) {
    82  	if cfg.Width != 0 {
    83  		d.width = cfg.Width
    84  	} else {
    85  		d.width = 128
    86  	}
    87  	if cfg.Height != 0 {
    88  		d.height = cfg.Height
    89  	} else {
    90  		d.height = 64
    91  	}
    92  	if cfg.Address != 0 {
    93  		d.bus.setAddress(cfg.Address)
    94  	}
    95  	if cfg.VccState != 0 {
    96  		d.vccState = cfg.VccState
    97  	} else {
    98  		d.vccState = SWITCHCAPVCC
    99  	}
   100  	d.bufferSize = d.width * d.height / 8
   101  	d.buffer = make([]byte, d.bufferSize)
   102  
   103  	d.bus.configure()
   104  
   105  	// busyWaitDelay(100 * time.Nanosecond)
   106  	time.Sleep(100 * time.Nanosecond)
   107  	d.Command(DISPLAYOFF)
   108  	d.Command(SETDISPLAYCLOCKDIV)
   109  	d.Command(0x80)
   110  	d.Command(SETMULTIPLEX)
   111  	d.Command(uint8(d.height - 1))
   112  	d.Command(SETDISPLAYOFFSET)
   113  	d.Command(0x0)
   114  	d.Command(SETSTARTLINE | 0x0)
   115  	d.Command(CHARGEPUMP)
   116  	if d.vccState == EXTERNALVCC {
   117  		d.Command(0x10)
   118  	} else {
   119  		d.Command(0x14)
   120  	}
   121  	d.Command(MEMORYMODE)
   122  	d.Command(0x00)
   123  	d.Command(SEGREMAP | 0x1)
   124  	d.Command(COMSCANDEC)
   125  
   126  	if (d.width == 128 && d.height == 64) || (d.width == 64 && d.height == 48) { // 128x64 or 64x48
   127  		d.Command(SETCOMPINS)
   128  		d.Command(0x12)
   129  		d.Command(SETCONTRAST)
   130  		if d.vccState == EXTERNALVCC {
   131  			d.Command(0x9F)
   132  		} else {
   133  			d.Command(0xCF)
   134  		}
   135  	} else if d.width == 128 && d.height == 32 { // 128x32
   136  		d.Command(SETCOMPINS)
   137  		d.Command(0x02)
   138  		d.Command(SETCONTRAST)
   139  		d.Command(0x8F)
   140  	} else if d.width == 96 && d.height == 16 { // 96x16
   141  		d.Command(SETCOMPINS)
   142  		d.Command(0x2)
   143  		d.Command(SETCONTRAST)
   144  		if d.vccState == EXTERNALVCC {
   145  			d.Command(0x10)
   146  		} else {
   147  			d.Command(0xAF)
   148  		}
   149  	} else {
   150  		// fail silently, it might work
   151  		println("there's no configuration for this display's size")
   152  	}
   153  
   154  	d.Command(SETPRECHARGE)
   155  	if d.vccState == EXTERNALVCC {
   156  		d.Command(0x22)
   157  	} else {
   158  		d.Command(0xF1)
   159  	}
   160  	d.Command(SETVCOMDETECT)
   161  	d.Command(0x40)
   162  	d.Command(DISPLAYALLON_RESUME)
   163  	d.Command(NORMALDISPLAY)
   164  	d.Command(DEACTIVATE_SCROLL)
   165  	d.Command(DISPLAYON)
   166  }
   167  
   168  // ClearBuffer clears the image buffer
   169  func (d *Device) ClearBuffer() {
   170  	for i := int16(0); i < d.bufferSize; i++ {
   171  		d.buffer[i] = 0
   172  	}
   173  }
   174  
   175  // ClearDisplay clears the image buffer and clear the display
   176  func (d *Device) ClearDisplay() {
   177  	d.ClearBuffer()
   178  	d.Display()
   179  }
   180  
   181  // Display sends the whole buffer to the screen
   182  func (d *Device) Display() error {
   183  	// In the 128x64 (SPI) screen resetting to 0x0 after 128 times corrupt the buffer
   184  	// Since we're printing the whole buffer, avoid resetting it
   185  	if d.width != 128 || d.height != 64 {
   186  		d.Command(COLUMNADDR)
   187  		d.Command(0)
   188  		d.Command(uint8(d.width - 1))
   189  		d.Command(PAGEADDR)
   190  		d.Command(0)
   191  		d.Command(uint8(d.height/8) - 1)
   192  	}
   193  
   194  	for pg := uint8(0); pg < uint8(d.height/8); pg++ {
   195  		d.Command(0xB0 | (pg & 0x07)) // SET_PAGE_ADDR
   196  		d.Command(SETLOWCOLUMN | 2)
   197  		d.Command(SETHIGHCOLUMN | 0)
   198  		d.Tx(d.buffer[uint16(pg)*0x80:uint16(pg+1)*0x80], false)
   199  	}
   200  
   201  	return nil
   202  }
   203  
   204  // SetPixel enables or disables a pixel in the buffer
   205  // color.RGBA{0, 0, 0, 255} is consider transparent, anything else
   206  // with enable a pixel on the screen
   207  func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
   208  	if x < 0 || x >= d.width || y < 0 || y >= d.height {
   209  		return
   210  	}
   211  	byteIndex := x + (y/8)*d.width
   212  	if c.R != 0 || c.G != 0 || c.B != 0 {
   213  		d.buffer[byteIndex] |= 1 << uint8(y%8)
   214  	} else {
   215  		d.buffer[byteIndex] &^= 1 << uint8(y%8)
   216  	}
   217  }
   218  
   219  // GetPixel returns if the specified pixel is on (true) or off (false)
   220  func (d *Device) GetPixel(x int16, y int16) bool {
   221  	if x < 0 || x >= d.width || y < 0 || y >= d.height {
   222  		return false
   223  	}
   224  	byteIndex := x + (y/8)*d.width
   225  	return (d.buffer[byteIndex] >> uint8(y%8) & 0x1) == 1
   226  }
   227  
   228  // SetBuffer changes the whole buffer at once
   229  func (d *Device) SetBuffer(buffer []byte) error {
   230  	if int16(len(buffer)) != d.bufferSize {
   231  		//return ErrBuffer
   232  		return errors.New("wrong size buffer")
   233  	}
   234  	for i := int16(0); i < d.bufferSize; i++ {
   235  		d.buffer[i] = buffer[i]
   236  	}
   237  	return nil
   238  }
   239  
   240  func (d *Device) SetScroll(line int16) {
   241  	d.Command(SETSTARTLINE + uint8(line&0b111111))
   242  }
   243  
   244  // Command sends a command to the display
   245  func (d *Device) Command(command uint8) {
   246  	d.cmdbuf[0] = command
   247  	d.bus.tx(d.cmdbuf[:], true)
   248  }
   249  
   250  // setAddress sets the address to the I2C bus
   251  func (b *I2CBus) setAddress(address uint16) {
   252  	b.Address = address
   253  }
   254  
   255  // setAddress does nothing, but it's required to avoid reflection
   256  func (b *SPIBus) setAddress(address uint16) {
   257  	// do nothing
   258  	println("trying to Configure an address on a SPI device")
   259  }
   260  
   261  // configure does nothing, but it's required to avoid reflection
   262  func (b *I2CBus) configure() {}
   263  
   264  // configure configures some pins with the SPI bus
   265  func (b *SPIBus) configure() {
   266  	b.csPin.Low()
   267  	b.dcPin.Low()
   268  	b.resetPin.Low()
   269  
   270  	b.resetPin.High()
   271  	// busyWaitDelay(time.Millisecond)
   272  	time.Sleep(1 * time.Millisecond)
   273  	b.resetPin.Low()
   274  	// busyWaitDelay(10 * time.Millisecond)
   275  	time.Sleep(10 * time.Millisecond)
   276  	b.resetPin.High()
   277  }
   278  
   279  // Tx sends data to the display
   280  func (d *Device) Tx(data []byte, isCommand bool) {
   281  	d.bus.tx(data, isCommand)
   282  }
   283  
   284  // tx sends data to the display (I2CBus implementation)
   285  func (b *I2CBus) tx(data []byte, isCommand bool) {
   286  	if isCommand {
   287  		legacy.WriteRegister(b.wire, uint8(b.Address), 0x00, data)
   288  	} else {
   289  		legacy.WriteRegister(b.wire, uint8(b.Address), 0x40, data)
   290  	}
   291  }
   292  
   293  // tx sends data to the display (SPIBus implementation)
   294  func (b *SPIBus) tx(data []byte, isCommand bool) {
   295  	if isCommand {
   296  		b.csPin.High()
   297  		riseTimeDelay()
   298  		b.dcPin.Low()
   299  		b.csPin.Low()
   300  
   301  		b.wire.Tx(data, nil)
   302  		b.csPin.High()
   303  	} else {
   304  		b.csPin.High()
   305  		riseTimeDelay()
   306  		b.dcPin.High()
   307  		b.csPin.Low()
   308  
   309  		b.wire.Tx(data, nil)
   310  		b.csPin.High()
   311  	}
   312  }
   313  
   314  // Size returns the current size of the display.
   315  func (d *Device) Size() (w, h int16) {
   316  	return d.width, d.height
   317  }
   318  
   319  // TODO: is this really necessary? seems to work fine without this on macropad-rp2040 at least
   320  func riseTimeDelay() {
   321  	busyWaitDelay(1 * time.Microsecond)
   322  }
   323  
   324  func busyWaitDelay(duration time.Duration) {
   325  	for start := time.Now(); time.Since(start) < duration; {
   326  	}
   327  }