Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ST7789 async operations #625

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions pixel/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions spi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
51 changes: 48 additions & 3 deletions st7789/st7789.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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()
Expand Down
Loading