From 7d3404f06091ef3bfb316d37c2c21d2fe6df2855 Mon Sep 17 00:00:00 2001 From: soypat Date: Wed, 30 Apr 2025 15:18:21 -0300 Subject: [PATCH] apply suggestions from team --- apa102/apa102.go | 9 +- apa102/softspi.go | 26 +- bmi160/bmi160.go | 37 ++- buzzer/buzzer.go | 15 +- internal/legacy/pinhal.go | 29 ++ internal/legacy/pinhal_baremetal.go | 12 + internal/legacy/pinhal_os.go | 5 + pins.go | 8 +- st7735/st7735v2.go | 475 ++++++++++++++++++++++++++++ 9 files changed, 573 insertions(+), 43 deletions(-) create mode 100644 internal/legacy/pinhal.go create mode 100644 internal/legacy/pinhal_baremetal.go create mode 100644 internal/legacy/pinhal_os.go create mode 100644 st7735/st7735v2.go diff --git a/apa102/apa102.go b/apa102/apa102.go index 98253bf..f383f73 100644 --- a/apa102/apa102.go +++ b/apa102/apa102.go @@ -5,9 +5,9 @@ package apa102 // import "tinygo.org/x/drivers/apa102" import ( "image/color" - "machine" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" ) const ( @@ -37,8 +37,11 @@ func New(b drivers.SPI) *Device { // NewSoftwareSPI returns a new APA102 driver that will use a software based // implementation of the SPI protocol. -func NewSoftwareSPI(sckPin, sdoPin machine.Pin, delay uint32) *Device { - return New(&bbSPI{SCK: sckPin, SDO: sdoPin, Delay: delay}) +func NewSoftwareSPI(sckPin, sdoPin legacy.PinOutput, delay uint32) *Device { + return New(&bbSPI{SCK: sckPin.Set, SDO: sdoPin.Set, Delay: delay, config: func() { + legacy.ConfigurePinOut(sckPin) + legacy.ConfigurePinOut(sdoPin) + }}) } // WriteColors writes the given RGBA color slice out using the APA102 protocol. diff --git a/apa102/softspi.go b/apa102/softspi.go index 89e8a85..20ca87a 100644 --- a/apa102/softspi.go +++ b/apa102/softspi.go @@ -1,6 +1,8 @@ package apa102 -import "machine" +import ( + "tinygo.org/x/drivers" +) // bbSPI is a dumb bit-bang implementation of SPI protocol that is hardcoded // to mode 0 and ignores trying to receive data. Just enough for the APA102. @@ -8,17 +10,17 @@ import "machine" // most purposes other than the APA102 package. It might be desirable to make // this more generic and include it in the TinyGo "machine" package instead. type bbSPI struct { - SCK machine.Pin - SDO machine.Pin - Delay uint32 + SCK drivers.PinOutput + SDO drivers.PinOutput + Delay uint32 + config func() } // Configure sets up the SCK and SDO pins as outputs and sets them low func (s *bbSPI) Configure() { - s.SCK.Configure(machine.PinConfig{Mode: machine.PinOutput}) - s.SDO.Configure(machine.PinConfig{Mode: machine.PinOutput}) - s.SCK.Low() - s.SDO.Low() + s.config() + s.SCK(false) + s.SDO(false) if s.Delay == 0 { s.Delay = 1 } @@ -47,19 +49,19 @@ func (s *bbSPI) Transfer(b byte) (byte, error) { for i := uint8(0); i < 8; i++ { // half clock cycle high to start - s.SCK.High() + s.SCK(true) s.delay() // write the value to SDO (MSB first) if b&(1<<(7-i)) == 0 { - s.SDO.Low() + s.SDO(false) } else { - s.SDO.High() + s.SDO(true) } s.delay() // half clock cycle low - s.SCK.Low() + s.SCK(false) s.delay() // for actual SPI would try to read the SDI value here diff --git a/bmi160/bmi160.go b/bmi160/bmi160.go index cd3e882..3bb788c 100644 --- a/bmi160/bmi160.go +++ b/bmi160/bmi160.go @@ -1,31 +1,35 @@ package bmi160 import ( - "machine" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" ) // DeviceSPI is the SPI interface to a BMI160 accelerometer/gyroscope. There is // also an I2C interface, but it is not yet supported. type DeviceSPI struct { // Chip select pin - CSB machine.Pin + CSB drivers.PinOutput buf [7]byte // SPI bus (requires chip select to be usable). - Bus drivers.SPI + Bus drivers.SPI + config func() } // NewSPI returns a new device driver. The pin and SPI interface are not // touched, provide a fully configured SPI object and call Configure to start // using this device. -func NewSPI(csb machine.Pin, spi drivers.SPI) *DeviceSPI { +func NewSPI(csb legacy.PinOutput, spi drivers.SPI) *DeviceSPI { return &DeviceSPI{ - CSB: csb, // chip select + CSB: csb.Set, // chip select Bus: spi, + config: func() { + legacy.ConfigurePinOut(csb) + }, } } @@ -33,8 +37,7 @@ func NewSPI(csb machine.Pin, spi drivers.SPI) *DeviceSPI { // configures the BMI160, but it does not configure the SPI interface (it is // assumed to be up and running). func (d *DeviceSPI) Configure() error { - d.CSB.Configure(machine.PinConfig{Mode: machine.PinOutput}) - d.CSB.High() + d.CSB(true) // The datasheet recommends doing a register read from address 0x7F to get // SPI communication going: @@ -86,9 +89,9 @@ func (d *DeviceSPI) ReadTemperature() (temperature int32, err error) { data[0] = 0x80 | reg_TEMPERATURE_0 data[1] = 0 data[2] = 0 - d.CSB.Low() + d.CSB(false) err = d.Bus.Tx(data, data) - d.CSB.High() + d.CSB(true) if err != nil { return } @@ -123,9 +126,9 @@ func (d *DeviceSPI) ReadAcceleration() (x int32, y int32, z int32, err error) { for i := 1; i < len(data); i++ { data[i] = 0 } - d.CSB.Low() + d.CSB(false) err = d.Bus.Tx(data, data) - d.CSB.High() + d.CSB(true) if err != nil { return } @@ -153,9 +156,9 @@ func (d *DeviceSPI) ReadRotation() (x int32, y int32, z int32, err error) { for i := 1; i < len(data); i++ { data[i] = 0 } - d.CSB.Low() + d.CSB(false) err = d.Bus.Tx(data, data) - d.CSB.High() + d.CSB(true) if err != nil { return } @@ -201,9 +204,9 @@ func (d *DeviceSPI) readRegister(address uint8) uint8 { data := d.buf[:2] data[0] = 0x80 | address data[1] = 0 - d.CSB.Low() + d.CSB(false) d.Bus.Tx(data, data) - d.CSB.High() + d.CSB(true) return data[1] } @@ -217,7 +220,7 @@ func (d *DeviceSPI) writeRegister(address, data uint8) { buf[0] = address buf[1] = data - d.CSB.Low() + d.CSB(false) d.Bus.Tx(buf, buf) - d.CSB.High() + d.CSB(true) } diff --git a/buzzer/buzzer.go b/buzzer/buzzer.go index 04c1129..b7f92f6 100644 --- a/buzzer/buzzer.go +++ b/buzzer/buzzer.go @@ -2,22 +2,23 @@ package buzzer // import "tinygo.org/x/drivers/buzzer" import ( - "machine" - "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" ) // Device wraps a GPIO connection to a buzzer. type Device struct { - pin machine.Pin + pin drivers.PinOutput High bool BPM float64 } // New returns a new buzzer driver given which pin to use -func New(pin machine.Pin) Device { +func New(pin legacy.PinOutput) Device { return Device{ - pin: pin, + pin: pin.Set, High: false, BPM: 96.0, } @@ -25,14 +26,14 @@ func New(pin machine.Pin) Device { // On sets the buzzer to a high state. func (l *Device) On() (err error) { - l.pin.Set(true) + l.pin(true) l.High = true return } // Off sets the buzzer to a low state. func (l *Device) Off() (err error) { - l.pin.Set(false) + l.pin(false) l.High = false return } diff --git a/internal/legacy/pinhal.go b/internal/legacy/pinhal.go new file mode 100644 index 0000000..5052a4d --- /dev/null +++ b/internal/legacy/pinhal.go @@ -0,0 +1,29 @@ +package legacy + +// PinOutput represents a pin hardware abstraction layer for a pin that can output a digital signal. +// This is an alternative to drivers.PinOutput abstraction which is a function type. Pros and cons +// of both approaches have been discussed in the [relevant issue]. PinOutput should only be used +// to expose an initialization function of a driver that receives pins of this type. Ideally +// driver developers should also expose the initialization with drivers.Pin type: +// +// func New(p1, p2, p3 legacy.PinOutput) *Device { +// return NewWithPinfuncs(p1.Set, p2.Set, p3.Set) +// } +// +// func NewWithPinfuncs(p1, p2, p3 drivers.PinOutput) *Device { +// return &Device{p1:p1, p2:p2, p3:p3} +// } +// +// [relevant issue]: https://github.com/tinygo-org/drivers/pull/749/files +type PinOutput interface { + Set(level bool) +} + +// ConfigurePinOut is a legacy function used to configure pins as outputs. +// +// Deprecated: Do not configure pins in drivers. +// This is a legacy feature and should only be used by drivers that +// previously configured pins in initialization to avoid breaking users. +func ConfigurePinOut(p PinOutput) { + configurePinOut(p) +} diff --git a/internal/legacy/pinhal_baremetal.go b/internal/legacy/pinhal_baremetal.go new file mode 100644 index 0000000..ac7c31f --- /dev/null +++ b/internal/legacy/pinhal_baremetal.go @@ -0,0 +1,12 @@ +//go:build baremetal + +package legacy + +import "machine" + +func configurePinOut(p PinOutput) { + machinePin, ok := p.(machine.Pin) + if ok { + machinePin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + } +} diff --git a/internal/legacy/pinhal_os.go b/internal/legacy/pinhal_os.go new file mode 100644 index 0000000..14d12db --- /dev/null +++ b/internal/legacy/pinhal_os.go @@ -0,0 +1,5 @@ +//go:build !baremetal + +package legacy + +func configurePinOut(p PinOutput) {} diff --git a/pins.go b/pins.go index a7662cb..ae81ac1 100644 --- a/pins.go +++ b/pins.go @@ -1,9 +1,9 @@ package drivers -// PinInput is hardware abstraction for a pin which receives a -// digital signal and reads it (high or low voltage). -type PinInput func() (level bool) - // PinOutput is hardware abstraction for a pin which outputs a // digital signal (high or low voltage). type PinOutput func(level bool) + +// PinInput is hardware abstraction for a pin which receives a +// digital signal and reads it (high or low voltage). +type PinInput func() (level bool) diff --git a/st7735/st7735v2.go b/st7735/st7735v2.go new file mode 100644 index 0000000..91a27f3 --- /dev/null +++ b/st7735/st7735v2.go @@ -0,0 +1,475 @@ +// Package st7735 implements a driver for the ST7735 TFT displays, it comes in various screen sizes. +// +// Datasheet: https://www.crystalfontz.com/controllers/Sitronix/ST7735R/319/ +package st7735 // import "tinygo.org/x/drivers/st7735" + +import ( + "image/color" + "time" + + "errors" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/pixel" +) + +type Model uint8 + +// Pixel formats supported by the st7735 driver. +type Color interface { + pixel.RGB444BE | pixel.RGB565BE + + pixel.BaseColor +} + +var ( + errOutOfBounds = errors.New("rectangle coordinates outside display area") +) + +// Device wraps an SPI connection. +type Device = DeviceOf[pixel.RGB565BE] + +// DeviceOf is a generic version of Device, which supports different pixel +// formats. +type DeviceOf[T Color] struct { + bus drivers.SPI + dcPin drivers.PinOutput + resetPin drivers.PinOutput + csPin drivers.PinOutput + blPin drivers.PinOutput + width int16 + height int16 + columnOffset int16 + rowOffset int16 + rotation drivers.Rotation + batchLength int16 + model Model + isBGR bool + batchData pixel.Image[T] // "image" with width, height of (batchLength, 1) +} + +// Config is the configuration for the display +type Config struct { + Width int16 + Height int16 + Rotation drivers.Rotation + Model Model + RowOffset int16 + ColumnOffset int16 +} + +// New creates a new ST7735 connection. The SPI wire must already be configured. +func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin drivers.PinOutput) Device { + return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin) +} + +// NewOf creates a new ST7735 connection with a particular pixel format. The SPI +// wire must already be configured. +func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin drivers.PinOutput) DeviceOf[T] { + return DeviceOf[T]{ + bus: bus, + dcPin: dcPin, + resetPin: resetPin, + csPin: csPin, + blPin: blPin, + } +} + +func (d *DeviceOf[T]) Reset() { + d.resetPin(true) + time.Sleep(5 * time.Millisecond) + d.resetPin(false) + time.Sleep(20 * time.Millisecond) + d.resetPin(true) + time.Sleep(150 * time.Millisecond) +} + +// Configure initializes the display with default configuration +func (d *DeviceOf[T]) Configure(cfg Config) { + d.model = cfg.Model + if cfg.Width != 0 { + d.width = cfg.Width + } else { + if d.model == MINI80x160 { + d.width = 80 + } else { + d.width = 128 + } + } + if cfg.Height != 0 { + d.height = cfg.Height + } else { + d.height = 160 + } + d.rotation = cfg.Rotation + d.rowOffset = cfg.RowOffset + d.columnOffset = cfg.ColumnOffset + + d.batchLength = d.width + if d.height > d.width { + d.batchLength = d.height + } + d.batchLength += d.batchLength & 1 + d.batchData = pixel.NewImage[T](int(d.batchLength), 1) + + d.Reset() + + // Common initialization + d.Command(SWRESET) + time.Sleep(150 * time.Millisecond) + d.Command(SLPOUT) + time.Sleep(500 * time.Millisecond) + d.Command(FRMCTR1) + d.Data(0x01) + d.Data(0x2C) + d.Data(0x2D) + d.Command(FRMCTR2) + d.Data(0x01) + d.Data(0x2C) + d.Data(0x2D) + d.Command(FRMCTR3) + d.Data(0x01) + d.Data(0x2C) + d.Data(0x2D) + d.Data(0x01) + d.Data(0x2C) + d.Data(0x2D) + d.Command(INVCTR) + d.Data(0x07) + d.Command(PWCTR1) + d.Data(0xA2) + d.Data(0x02) + d.Data(0x84) + d.Command(PWCTR2) + d.Data(0xC5) + d.Command(PWCTR3) + d.Data(0x0A) + d.Data(0x00) + d.Command(PWCTR4) + d.Data(0x8A) + d.Data(0x2A) + d.Command(PWCTR5) + d.Data(0x8A) + d.Data(0xEE) + d.Command(VMCTR1) + d.Data(0x0E) + + // Set the color format depending on the generic type. + d.Command(COLMOD) + var zeroColor T + switch any(zeroColor).(type) { + case pixel.RGB444BE: + d.Data(0x03) // 12 bits per pixel + default: + d.Data(0x05) // 16 bits per pixel + } + + if d.model == GREENTAB { + d.InvertColors(false) + } else if d.model == MINI80x160 { + d.isBGR = true + d.InvertColors(true) + } + + // common color adjustment + d.Command(GMCTRP1) + + d.Data(0x02) + d.Data(0x1C) + d.Data(0x07) + d.Data(0x12) + d.Data(0x37) + d.Data(0x32) + d.Data(0x29) + d.Data(0x2D) + d.Data(0x29) + d.Data(0x25) + d.Data(0x2B) + d.Data(0x39) + d.Data(0x00) + d.Data(0x01) + d.Data(0x03) + d.Data(0x10) + d.Command(GMCTRN1) + d.Data(0x03) + d.Data(0x1D) + d.Data(0x07) + d.Data(0x06) + d.Data(0x2E) + d.Data(0x2C) + d.Data(0x29) + d.Data(0x2D) + d.Data(0x2E) + d.Data(0x2E) + d.Data(0x37) + d.Data(0x3F) + d.Data(0x00) + d.Data(0x00) + d.Data(0x02) + d.Data(0x10) + + d.Command(NORON) + time.Sleep(10 * time.Millisecond) + d.Command(DISPON) + time.Sleep(500 * time.Millisecond) + + if cfg.Model == MINI80x160 { + d.Command(MADCTL) + d.Data(0xC0) + } + + d.SetRotation(d.rotation) + + d.blPin(true) +} + +// Display does nothing, there's no buffer as it might be too big for some boards +func (d *DeviceOf[T]) Display() error { + return nil +} + +// SetPixel sets a pixel in the screen +func (d *DeviceOf[T]) SetPixel(x int16, y int16, c color.RGBA) { + w, h := d.Size() + if x < 0 || y < 0 || x >= w || y >= h { + return + } + d.FillRectangle(x, y, 1, 1, c) +} + +// setWindow prepares the screen to be modified at a given rectangle +func (d *DeviceOf[T]) setWindow(x, y, w, h int16) { + if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { + x += d.columnOffset + y += d.rowOffset + } else { + x += d.rowOffset + y += d.columnOffset + } + d.Tx([]uint8{CASET}, true) + d.Tx([]uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)}, false) + d.Tx([]uint8{RASET}, true) + d.Tx([]uint8{uint8(y >> 8), uint8(y), uint8((y + h - 1) >> 8), uint8(y + h - 1)}, false) + d.Command(RAMWR) +} + +// SetScrollWindow sets an area to scroll with fixed top and bottom parts of the display +func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { + // TODO: this code is broken, see the st7789 and ili9341 implementations for + // how to do this correctly. + d.Command(VSCRDEF) + d.Tx([]uint8{ + uint8(topFixedArea >> 8), uint8(topFixedArea), + uint8(d.height - topFixedArea - bottomFixedArea>>8), uint8(d.height - topFixedArea - bottomFixedArea), + uint8(bottomFixedArea >> 8), uint8(bottomFixedArea)}, + false) +} + +// SetScroll sets the vertical scroll address of the display. +func (d *DeviceOf[T]) SetScroll(line int16) { + d.Command(VSCRSADD) + d.Tx([]uint8{uint8(line >> 8), uint8(line)}, false) +} + +// SpotScroll returns the display to its normal state +func (d *DeviceOf[T]) StopScroll() { + d.Command(NORON) +} + +// FillRectangle fills a rectangle at a given coordinates with a color +func (d *DeviceOf[T]) FillRectangle(x, y, width, height int16, c color.RGBA) error { + k, i := d.Size() + if x < 0 || y < 0 || width <= 0 || height <= 0 || + x >= k || (x+width) > k || y >= i || (y+height) > i { + return errors.New("rectangle coordinates outside display area") + } + d.setWindow(x, y, width, height) + + d.batchData.FillSolidColor(pixel.NewColor[T](c.R, c.G, c.B)) + i = width * height + for i > 0 { + if i >= d.batchLength { + d.Tx(d.batchData.RawBuffer(), false) + } else { + d.Tx(d.batchData.Rescale(int(i), 1).RawBuffer(), false) + } + i -= d.batchLength + } + return nil +} + +// DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates +// +// Deprecated: use DrawBitmap instead. +func (d *DeviceOf[T]) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { + k, i := d.Size() + if x < 0 || y < 0 || w <= 0 || h <= 0 || + x >= k || (x+w) > k || y >= i || (y+h) > i { + return errOutOfBounds + } + d.setWindow(x, y, w, h) + d.Tx(data, false) + return nil +} + +// DrawBitmap copies the bitmap to the internal buffer on the screen at the +// given coordinates. It returns once the image data has been sent completely. +func (d *DeviceOf[T]) DrawBitmap(x, y int16, bitmap pixel.Image[T]) error { + width, height := bitmap.Size() + return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(width), int16(height)) +} + +// FillRectangle fills a rectangle at a given coordinates with a buffer +func (d *DeviceOf[T]) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { + k, l := d.Size() + if x < 0 || y < 0 || width <= 0 || height <= 0 || + x >= k || (x+width) > k || y >= l || (y+height) > l { + return errors.New("rectangle coordinates outside display area") + } + k = width * height + l = int16(len(buffer)) + if k != l { + return errors.New("buffer length does not match with rectangle size") + } + + d.setWindow(x, y, width, height) + + offset := int16(0) + for k > 0 { + for i := int16(0); i < d.batchLength; i++ { + if offset+i < l { + c := buffer[offset+i] + d.batchData.Set(int(i), 0, pixel.NewColor[T](c.R, c.G, c.B)) + } + } + if k >= d.batchLength { + d.Tx(d.batchData.RawBuffer(), false) + } else { + d.Tx(d.batchData.Rescale(int(k), 1).RawBuffer(), false) + } + k -= d.batchLength + offset += d.batchLength + } + return nil +} + +// DrawFastVLine draws a vertical line faster than using SetPixel +func (d *DeviceOf[T]) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { + if y0 > y1 { + y0, y1 = y1, y0 + } + d.FillRectangle(x, y0, 1, y1-y0+1, c) +} + +// DrawFastHLine draws a horizontal line faster than using SetPixel +func (d *DeviceOf[T]) DrawFastHLine(x0, x1, y int16, c color.RGBA) { + if x0 > x1 { + x0, x1 = x1, x0 + } + d.FillRectangle(x0, y, x1-x0+1, 1, c) +} + +// FillScreen fills the screen with a given color +func (d *DeviceOf[T]) FillScreen(c color.RGBA) { + if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { + d.FillRectangle(0, 0, d.width, d.height, c) + } else { + d.FillRectangle(0, 0, d.height, d.width, c) + } +} + +// Rotation returns the currently configured rotation. +func (d *DeviceOf[T]) Rotation() drivers.Rotation { + return d.rotation +} + +// SetRotation changes the rotation of the device (clock-wise) +func (d *DeviceOf[T]) SetRotation(rotation drivers.Rotation) error { + d.rotation = rotation + madctl := uint8(0) + switch rotation % 4 { + case drivers.Rotation0: + madctl = MADCTL_MX | MADCTL_MY + case drivers.Rotation90: + madctl = MADCTL_MY | MADCTL_MV + case drivers.Rotation180: + // nothing to do + case drivers.Rotation270: + madctl = MADCTL_MX | MADCTL_MV + } + if d.isBGR { + madctl |= MADCTL_BGR + } + d.Command(MADCTL) + d.Data(madctl) + return nil +} + +// Command sends a command to the display +func (d *DeviceOf[T]) Command(command uint8) { + d.Tx([]byte{command}, true) +} + +// Command sends a data to the display +func (d *DeviceOf[T]) Data(data uint8) { + d.Tx([]byte{data}, false) +} + +func (d *DeviceOf[T]) TxData(data []byte) error { + return d.Tx(data, false) +} + +// Tx sends data to the display +func (d *DeviceOf[T]) Tx(data []byte, isCommand bool) error { + d.dcPin(!isCommand) + return d.bus.Tx(data, nil) +} + +// Size returns the current size of the display. +func (d *DeviceOf[T]) Size() (w, h int16) { + if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { + return d.width, d.height + } + return d.height, d.width +} + +// EnableBacklight enables or disables the backlight +func (d *DeviceOf[T]) EnableBacklight(enable bool) { + if enable { + d.blPin(true) + } else { + d.blPin(false) + } +} + +// Set the sleep mode for this LCD panel. When sleeping, the panel uses a lot +// less power. The LCD won't display an image anymore, but the memory contents +// will be kept. +func (d *DeviceOf[T]) Sleep(sleepEnabled bool) error { + if sleepEnabled { + // Shut down LCD panel. + d.Command(SLPIN) + time.Sleep(5 * time.Millisecond) // 5ms required by the datasheet + } else { + // Turn the LCD panel back on. + d.Command(SLPOUT) + // The st7735 datasheet says it is necessary to wait 120ms before + // sending another command. + time.Sleep(120 * time.Millisecond) + } + return nil +} + +// InverColors inverts the colors of the screen +func (d *DeviceOf[T]) InvertColors(invert bool) { + if invert { + d.Command(INVON) + } else { + d.Command(INVOFF) + } +} + +// IsBGR changes the color mode (RGB/BGR) +func (d *DeviceOf[T]) IsBGR(bgr bool) { + d.isBGR = bgr +}