From 9b5840e3cde5065ddbbaa99bc83a8d2f7ba9e099 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 8 Dec 2023 21:48:24 +0100 Subject: [PATCH 1/2] pixel: add Split method This method is useful to split a buffer in two parts, so that the two parts can be used independently. --- pixel/image.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pixel/image.go b/pixel/image.go index 54633e3fc..ef7143e63 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -70,6 +70,54 @@ func (img Image[T]) LimitHeight(height int) Image[T] { } } +// Split the buffer into two buffers that can be used independently. +// The top half is split just like LimitHeight. The bottom half is made out of +// the remaining buffer area and can be zero. The topHeight parameter must not +// be larger than the height of the buffer. +// +// Always check the height of the bottom half: it may be zero due to alignment +// issues. +func (img Image[T]) Split(topHeight int) (top, bottom Image[T]) { + if topHeight < 0 || topHeight > int(img.height) { + panic("Image.Split: out of bounds") + } + + // The top half of the buffer, the same as LimitHeight. + top = Image[T]{ + width: img.width, + height: int16(topHeight), + data: img.data, + } + + // Calculate the bottom half of the buffer. + // This is a bit more complicated since it's possible that the bottom half + // can't have all the other bytes: the top half pixels might cross a byte + // boundary (for example with RGB444). So instead we calculate the size of + // the buffer we have, the size of the buffer that the top half will use + // (which is rounded up to a byte boundary), and then calculate the + // remaining bytes at the bottom. + // In practice, I expect it's unlikely that the top half will cross a byte + // boundary since a typical split buffer will have a width that's a nice + // round number, but it's possible so we have to avoid this edge case. + var zeroColor T + dataBytes := (int(img.width)*int(img.height)*zeroColor.BitsPerPixel() + 7) / 8 + topDataBytes := (int(img.width)*int(topHeight)*zeroColor.BitsPerPixel() + 7) / 8 + bottomDataBytes := dataBytes - topDataBytes + if bottomDataBytes < 0 { + // No buffer remaining (not sure whether this is possible in practice + // but guarding just in case). + bottomDataBytes = 0 + } + bottomHeight := (bottomDataBytes * 8 / zeroColor.BitsPerPixel()) / int(img.width) + bottom = Image[T]{ + width: img.width, + height: int16(bottomHeight), + data: unsafe.Add(img.data, topDataBytes), + } + + return +} + // Len returns the number of pixels in this image buffer. func (img Image[T]) Len() int { return int(img.width) * int(img.height) From c3d0697dbc892de1bba13668054c89250e5572d7 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 8 Dec 2023 21:52:21 +0100 Subject: [PATCH 2/2] [DRAFT] st7789: add async SPI operations --- spi.go | 9 +++++++++ st7789/st7789.go | 51 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/spi.go b/spi.go index aa9e41030..8c90dc771 100644 --- a/spi.go +++ b/spi.go @@ -11,3 +11,12 @@ type SPI interface { // If you want to transfer multiple bytes, it is more efficient to use Tx instead. Transfer(b byte) (byte, error) } + +// AsyncSPI is a SPI bus that also implements async operations (using DMA, +// probably). +type AsyncSPI interface { + SPI + IsAsync() bool + StartTx(tx, rx []byte) error + Wait() error +} diff --git a/st7789/st7789.go b/st7789/st7789.go index 5db2402ba..6b1ba089b 100644 --- a/st7789/st7789.go +++ b/st7789/st7789.go @@ -45,7 +45,7 @@ type Device = DeviceOf[pixel.RGB565BE] // DeviceOf is a generic version of Device. It supports multiple different pixel // formats. type DeviceOf[T Color] struct { - bus drivers.SPI + bus drivers.AsyncSPI dcPin machine.Pin resetPin machine.Pin csPin machine.Pin @@ -83,13 +83,13 @@ type Config struct { } // New creates a new ST7789 connection. The SPI wire must already be configured. -func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { +func New(bus drivers.AsyncSPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin) } // NewOf creates a new ST7789 connection with a particular pixel format. The SPI // wire must already be configured. -func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] { +func NewOf[T Color](bus drivers.AsyncSPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] { dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) @@ -404,6 +404,51 @@ func (d *DeviceOf[T]) DrawBitmap(x, y int16, bitmap pixel.Image[T]) error { return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(width), int16(height)) } +// IsAsync returns whether the underlying SPI bus supports async operations. +func (d *DeviceOf[T]) IsAsync() bool { + return d.bus.IsAsync() +} + +// StartDrawBitmap starts sending the given bitmap to the screen. +// After calling StartDrawBitmap, you can only call Wait() or another +// StartDrawBitmap. Calling any other method may result in incorrect behavior. +// The bitmap passed to StartDrawBitmap may not be written to until Wait() has +// been called. +func (d *DeviceOf[T]) StartDrawBitmap(x, y int16, bitmap pixel.Image[T]) error { + // Check that the provided buffer is drawn entirely inside the image. + width, height := bitmap.Size() + displayWidth, displayHeight := d.Size() + if uint(int(x)+width) > uint(int(displayWidth)) || uint(int(y)+height) > uint(int(displayHeight)) { + return errOutOfBounds + } + if width <= 0 || height <= 0 { + return nil // no bitmap to send + } + + // Wait until the previous buffer has been fully sent. + err := d.bus.Wait() + if err != nil { + return err + } + + // Send the next buffer. + d.startWrite() + d.setWindow(x, y, int16(width), int16(height)) + d.bus.StartTx(bitmap.RawBuffer(), nil) + return nil +} + +// Wait until all previous transfers have completed. After this call, the bitmap +// passed to StartDrawBitmap can be reused. +func (d *DeviceOf[T]) Wait() error { + err := d.bus.Wait() + if err != nil { + return err + } + d.endWrite() + return nil +} + // FillRectangleWithBuffer fills buffer with a rectangle at a given coordinates. func (d *DeviceOf[T]) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { i, j := d.Size()