diff --git a/examples/ili9341/pyportal_boing/main.go b/examples/ili9341/pyportal_boing/main.go index 16b285bea..b03423386 100644 --- a/examples/ili9341/pyportal_boing/main.go +++ b/examples/ili9341/pyportal_boing/main.go @@ -8,15 +8,16 @@ import ( "tinygo.org/x/drivers/examples/ili9341/initdisplay" "tinygo.org/x/drivers/examples/ili9341/pyportal_boing/graphics" "tinygo.org/x/drivers/ili9341" + "tinygo.org/x/drivers/pixel" ) const ( - BGCOLOR = 0xAD75 - GRIDCOLOR = 0xA815 - BGSHADOW = 0x5285 - GRIDSHADOW = 0x600C - RED = 0xF800 - WHITE = 0xFFFF + BGCOLOR = pixel.RGB565BE(0x75AD) + GRIDCOLOR = pixel.RGB565BE(0x15A8) + BGSHADOW = pixel.RGB565BE(0x8552) + GRIDSHADOW = pixel.RGB565BE(0x0C60) + RED = pixel.RGB565BE(0x00F8) + WHITE = pixel.RGB565BE(0xFFFF) YBOTTOM = 123 // Ball Y coord at bottom YBOUNCE = -3.5 // Upward velocity on ball bounce @@ -25,7 +26,7 @@ const ( ) var ( - frameBuffer = [(graphics.BALLHEIGHT + 8) * (graphics.BALLWIDTH + 8) * 2]uint8{} + frameBuffer = pixel.NewImage[pixel.RGB565BE](graphics.BALLWIDTH+8, graphics.BALLHEIGHT+8) startTime int64 frame int64 @@ -41,7 +42,7 @@ var ( balloldy float32 // Color table for ball rotation effect - palette [16]uint16 + palette [16]pixel.RGB565BE ) var ( @@ -108,6 +109,7 @@ func main() { width = maxx - minx + 1 height = maxy - miny + 1 + buffer := frameBuffer.Rescale(int(width), int(height)) // Ball animation frame # is incremented opposite the ball's X velocity ballframe -= ballvx * 0.5 @@ -128,7 +130,7 @@ func main() { } // Only the changed rectangle is drawn into the 'renderbuf' array... - var c uint16 //, *destPtr; + var c pixel.RGB565BE //, *destPtr; bx := minx - int16(ballx) // X relative to ball bitmap (can be negative) by := miny - int16(bally) // Y relative to ball bitmap (can be negative) bgx := minx // X relative to background bitmap (>= 0) @@ -149,19 +151,20 @@ func main() { (by >= 0) && (by < graphics.BALLHEIGHT) { // inside the ball bitmap area? // Yes, do ball compositing math... p = graphics.Ball[int(by*(graphics.BALLWIDTH/2))+int(bx1/2)] // Get packed value (2 pixels) + var nibble uint8 if (bx1 & 1) != 0 { - c = uint16(p & 0xF) + nibble = p & 0xF } else { - c = uint16(p >> 4) + nibble = p >> 4 } // Unpack high or low nybble - if c == 0 { // Outside ball - just draw grid + if nibble == 0 { // Outside ball - just draw grid if graphics.Background[bgidx]&(0x80>>(bgx1&7)) != 0 { c = GRIDCOLOR } else { c = BGCOLOR } - } else if c > 1 { // In ball area... - c = palette[c] + } else if nibble > 1 { // In ball area... + c = palette[nibble] } else { // In shadow area... if graphics.Background[bgidx]&(0x80>>(bgx1&7)) != 0 { c = GRIDSHADOW @@ -176,8 +179,7 @@ func main() { c = BGCOLOR } } - frameBuffer[(y*int(width)+x)*2] = byte(c >> 8) - frameBuffer[(y*int(width)+x)*2+1] = byte(c) + buffer.Set(x, y, c) bx1++ // Increment bitmap position counters (X axis) bgx1++ } @@ -188,7 +190,7 @@ func main() { bgy++ } - display.DrawRGBBitmap8(minx, miny, frameBuffer[:width*height*2], width, height) + display.DrawBitmap(minx, miny, buffer) // Show approximate frame rate frame++ @@ -205,6 +207,7 @@ func DrawBackground() { w, h := display.Size() byteWidth := (w + 7) / 8 // Bitmap scanline pad = whole byte var b uint8 + buffer := frameBuffer.Rescale(int(w), 1) for j := int16(0); j < h; j++ { for k := int16(0); k < w; k++ { if k&7 > 0 { @@ -213,13 +216,11 @@ func DrawBackground() { b = graphics.Background[j*byteWidth+k/8] } if b&0x80 == 0 { - frameBuffer[2*k] = byte(BGCOLOR >> 8) - frameBuffer[2*k+1] = byte(BGCOLOR & 0xFF) + buffer.Set(int(k), 0, BGCOLOR) } else { - frameBuffer[2*k] = byte(GRIDCOLOR >> 8) - frameBuffer[2*k+1] = byte(GRIDCOLOR & 0xFF) + buffer.Set(int(k), 0, GRIDCOLOR) } } - display.DrawRGBBitmap8(0, j, frameBuffer[0:w*2], w, 1) + display.DrawBitmap(0, j, buffer) } } diff --git a/ili9341/ili9341.go b/ili9341/ili9341.go index 421b3d29f..5ddb46395 100644 --- a/ili9341/ili9341.go +++ b/ili9341/ili9341.go @@ -7,6 +7,7 @@ import ( "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/pixel" ) type Config struct { @@ -31,6 +32,9 @@ type Device struct { rd machine.Pin } +// Image buffer type used in the ili9341. +type Image = pixel.Image[pixel.RGB565BE] + var cmdBuf [6]byte var initCmd = []byte{ @@ -173,6 +177,8 @@ func (d *Device) EnableTEOutput(on bool) { } // DrawRGBBitmap copies an RGB bitmap to the internal buffer at given coordinates +// +// Deprecated: use DrawBitmap instead. func (d *Device) DrawRGBBitmap(x, y int16, data []uint16, w, h int16) error { k, i := d.Size() if x < 0 || y < 0 || w <= 0 || h <= 0 || @@ -187,6 +193,8 @@ func (d *Device) DrawRGBBitmap(x, y int16, data []uint16, w, h int16) error { } // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates +// +// Deprecated: use DrawBitmap instead. func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { k, i := d.Size() if x < 0 || y < 0 || w <= 0 || h <= 0 || @@ -200,6 +208,13 @@ func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { 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 *Device) DrawBitmap(x, y int16, bitmap Image) error { + width, height := bitmap.Size() + return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(width), int16(height)) +} + // FillRectangle fills a rectangle at given coordinates with a color func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { k, i := d.Size() diff --git a/pixel/image.go b/pixel/image.go new file mode 100644 index 000000000..54633e3fc --- /dev/null +++ b/pixel/image.go @@ -0,0 +1,223 @@ +package pixel + +import ( + "unsafe" +) + +// Image buffer, used for working with the native image format of various +// displays. It works a lot like a slice: it can be rescaled while reusing the +// underlying buffer and should be passed around by value. +type Image[T Color] struct { + width int16 + height int16 + data unsafe.Pointer +} + +// NewImage creates a new image of the given size. +func NewImage[T Color](width, height int) Image[T] { + if width < 0 || height < 0 || int(int16(width)) != width || int(int16(height)) != height { + // The width/height are stored as 16-bit integers and should never be + // negative. + panic("NewImage: width/height out of bounds") + } + var zeroColor T + var data unsafe.Pointer + if zeroColor.BitsPerPixel()%8 == 0 { + // Typical formats like RGB888 and RGB565. + // Each color starts at a whole byte offset from the start. + buf := make([]T, width*height) + data = unsafe.Pointer(&buf[0]) + } else { + // Formats like RGB444 that have 12 bits per pixel. + // We access these as bytes, so allocate the buffer as a byte slice. + bufBits := width * height * zeroColor.BitsPerPixel() + bufBytes := (bufBits + 7) / 8 + buf := make([]byte, bufBytes) + data = unsafe.Pointer(&buf[0]) + } + return Image[T]{ + width: int16(width), + height: int16(height), + data: data, + } +} + +// Rescale returns a new Image buffer based on the img buffer. +// The contents is undefined after the Rescale operation, and any modification +// to the returned image will overwrite the underlying image buffer in undefined +// ways. It will panic if width*height is larger than img.Len(). +func (img Image[T]) Rescale(width, height int) Image[T] { + if width*height > img.Len() { + panic("Image.Rescale size out of bounds") + } + return Image[T]{ + width: int16(width), + height: int16(height), + data: img.data, + } +} + +// LimitHeight returns a subimage with the bottom part cut off, as specified by +// height. +func (img Image[T]) LimitHeight(height int) Image[T] { + if height < 0 || height > int(img.height) { + panic("Image.LimitHeight: out of bounds") + } + return Image[T]{ + width: img.width, + height: int16(height), + data: img.data, + } +} + +// Len returns the number of pixels in this image buffer. +func (img Image[T]) Len() int { + return int(img.width) * int(img.height) +} + +// RawBuffer returns a byte slice that can be written directly to the screen +// using DrawRGBBitmap8. +func (img Image[T]) RawBuffer() []uint8 { + var zeroColor T + var numBytes int + if zeroColor.BitsPerPixel()%8 == 0 { + // Each color starts at a whole byte offset. + numBytes = int(unsafe.Sizeof(zeroColor)) * int(img.width) * int(img.height) + } else { + // Formats like RGB444 that aren't a whole number of bytes. + numBits := zeroColor.BitsPerPixel() * int(img.width) * int(img.height) + numBytes = (numBits + 7) / 8 // round up (see NewImage) + } + return unsafe.Slice((*byte)(img.data), numBytes) +} + +// Size returns the image size. +func (img Image[T]) Size() (int, int) { + return int(img.width), int(img.height) +} + +func (img Image[T]) setPixel(index int, c T) { + var zeroColor T + + if zeroColor.BitsPerPixel()%8 == 0 { + // Each color starts at a whole byte offset. + // This is the easy case. + offset := index * int(unsafe.Sizeof(zeroColor)) + ptr := unsafe.Add(img.data, offset) + *((*T)(ptr)) = c + return + } + + if c, ok := any(c).(RGB444BE); ok { + // Special case for RGB444. + bitIndex := index * zeroColor.BitsPerPixel() + if bitIndex%8 == 0 { + byteOffset := bitIndex / 8 + ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) + ptr[0] = uint8(c >> 4) + ptr[1] = ptr[1]&0x0f | uint8(c)<<4 // change top bits + } else { + byteOffset := bitIndex / 8 + ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) + ptr[0] = ptr[0]&0xf0 | uint8(c>>8) // change bottom bits + ptr[1] = uint8(c) + } + return + } + + // TODO: the code for RGB444 should be generalized to support any bit size. + panic("todo: setPixel for odd bits per pixel") +} + +// Set sets the pixel at x, y to the given color. +// Use FillSolidColor to efficiently fill the entire image buffer. +func (img Image[T]) Set(x, y int, c T) { + if uint(x) >= uint(int(img.width)) || uint(y) >= uint(int(img.height)) { + panic("Image.Set: out of bounds") + } + index := y*int(img.width) + x + img.setPixel(index, c) +} + +// Get returns the color at the given index. +func (img Image[T]) Get(x, y int) T { + if uint(x) >= uint(int(img.width)) || uint(y) >= uint(int(img.height)) { + panic("Image.Get: out of bounds") + } + var zeroColor T + index := y*int(img.width) + x // index into img.data + + if zeroColor.BitsPerPixel()%8 == 0 { + // Colors like RGB565, RGB888, etc. + offset := index * int(unsafe.Sizeof(zeroColor)) + ptr := unsafe.Add(img.data, offset) + return *((*T)(ptr)) + } + + if _, ok := any(zeroColor).(RGB444BE); ok { + // Special case for RGB444 that isn't stored in a neat byte multiple. + bitIndex := index * zeroColor.BitsPerPixel() + var c RGB444BE + if bitIndex%8 == 0 { + byteOffset := bitIndex / 8 + ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) + c |= RGB444BE(ptr[0]) << 4 + c |= RGB444BE(ptr[1] >> 4) // load top bits + } else { + byteOffset := bitIndex / 8 + ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) + c |= RGB444BE(ptr[0]&0x0f) << 8 // load bottom bits + c |= RGB444BE(ptr[1]) + } + return any(c).(T) + } + + // TODO: generalize the above code. + panic("todo: Image.Get for odd bits per pixel") +} + +// FillSolidColor fills the entire image with the given color. +// This may be faster than setting individual pixels. +func (img Image[T]) FillSolidColor(color T) { + var zeroColor T + + // Fast pass for colors of 8, 16, 24, etc bytes in size. + if zeroColor.BitsPerPixel()%8 == 0 { + ptr := img.data + for i := 0; i < img.Len(); i++ { + // TODO: this can be optimized a lot. + // - The store can be done as a 32-bit integer, after checking for + // alignment. + // - Perhaps the loop can be unrolled to improve copy performance. + *(*T)(ptr) = color + ptr = unsafe.Add(ptr, unsafe.Sizeof(zeroColor)) + } + return + } + + // Special case for RGB444. + if c, ok := any(color).(RGB444BE); ok { + // RGB444 can be stored in a more optimized way, by storing two colors + // at a time instead of setting each color individually. This avoids + // loading and masking the old color bits for the half-bytes. + var buf [3]uint8 + buf[0] = uint8(c >> 4) + buf[1] = uint8(c)<<4 | uint8(c>>8) + buf[2] = uint8(c) + rawBuf := unsafe.Slice((*[3]byte)(img.data), img.Len()/2) + for i := 0; i < len(rawBuf); i++ { + rawBuf[i] = buf + } + if img.Len()%2 != 0 { + // The image contains an uneven number of pixels. + // This is uncommon, but it can happen and we have to handle it. + img.setPixel(img.Len()-1, color) + } + return + } + + // Fallback for other color formats. + for i := 0; i < img.Len(); i++ { + img.setPixel(i, color) + } +} diff --git a/pixel/image_test.go b/pixel/image_test.go new file mode 100644 index 000000000..42f292c60 --- /dev/null +++ b/pixel/image_test.go @@ -0,0 +1,64 @@ +package pixel_test + +import ( + "image/color" + "testing" + + "tinygo.org/x/drivers/pixel" +) + +func TestImageRGB565BE(t *testing.T) { + image := pixel.NewImage[pixel.RGB565BE](5, 3) + if width, height := image.Size(); width != 5 && height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, c := range []color.RGBA{ + {R: 0xff, A: 0xff}, + {G: 0xff, A: 0xff}, + {B: 0xff, A: 0xff}, + {R: 0x10, A: 0xff}, + {G: 0x10, A: 0xff}, + {B: 0x10, A: 0xff}, + } { + image.Set(4, 2, pixel.NewColor[pixel.RGB565BE](c.R, c.G, c.B)) + c2 := image.Get(4, 2).RGBA() + if c2 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2) + } + } +} + +func TestImageRGB444BE(t *testing.T) { + image := pixel.NewImage[pixel.RGB444BE](5, 3) + if width, height := image.Size(); width != 5 && height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, c := range []color.RGBA{ + {R: 0xff, A: 0xff}, + {G: 0xff, A: 0xff}, + {B: 0xff, A: 0xff}, + {R: 0x11, A: 0xff}, + {G: 0x11, A: 0xff}, + {B: 0x11, A: 0xff}, + } { + encoded := pixel.NewColor[pixel.RGB444BE](c.R, c.G, c.B) + image.Set(0, 0, encoded) + image.Set(0, 1, encoded) + encoded2 := image.Get(0, 0) + encoded3 := image.Get(0, 1) + if encoded != encoded2 { + t.Errorf("failed to roundtrip color %v: expected %d but got %d", c, encoded, encoded2) + } + if encoded != encoded3 { + t.Errorf("failed to roundtrip color %v: expected %d but got %d", c, encoded, encoded3) + } + c2 := encoded2.RGBA() + if c2 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2) + } + c3 := encoded3.RGBA() + if c3 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c3) + } + } +} diff --git a/pixel/pixel.go b/pixel/pixel.go new file mode 100644 index 000000000..940fb1c5c --- /dev/null +++ b/pixel/pixel.go @@ -0,0 +1,194 @@ +// Package pixel contains pixel format definitions used in various displays and +// fast operations on them. +// +// This package is just a base for pixel operations, it is _not_ a graphics +// library. It doesn't define circles, lines, etc - just the bare minimum +// graphics operations needed plus the ones that need to be specialized per +// pixel format. +package pixel + +import ( + "image/color" + "math/bits" +) + +// Pixel with a particular color, matching the underlying hardware of a +// particular display. Each pixel is at least 1 byte in size. +// The color format is sRGB (or close to it) in all cases. +type Color interface { + RGB888 | RGB565BE | RGB444BE + + BaseColor +} + +// BaseColor contains all the methods needed in a color format. This can be used +// in display drivers that want to define their own Color type with just the +// pixel formats the display supports. +type BaseColor interface { + // The number of bits when stored. + // This means for example that RGB555 (which is still stored as a 16-bit + // integer) returns 16, while RGB444 returns 12. + BitsPerPixel() int + + // Return the given color in color.RGBA format, which is always sRGB. The + // alpha channel is always 255. + RGBA() color.RGBA +} + +// NewColor returns the given color based on the RGB values passed in the +// parameters. The input value is assumed to be in sRGB color space. +func NewColor[T Color](r, g, b uint8) T { + // Ugly cast from color.RGBA to T. The type switch and interface casts are + // trivially optimized away after instantiation. + var value T + switch any(value).(type) { + case RGB888: + return any(NewRGB888(r, g, b)).(T) + case RGB565BE: + return any(NewRGB565BE(r, g, b)).(T) + case RGB444BE: + return any(NewRGB444BE(r, g, b)).(T) + default: + panic("unknown color format") + } +} + +// NewLinearColor returns the given color based on the linear RGB values passed +// in the parameters. Use this if the RGB values are actually linear colors +// (like those that are used in most RGB LEDs) and not when it is in the usual +// sRGB color space (which is not linear). +// +// The input is assumed to be in the linear sRGB color space. +func NewLinearColor[T Color](r, g, b uint8) T { + r = gammaEncodeTable[r] + g = gammaEncodeTable[g] + b = gammaEncodeTable[b] + return NewColor[T](r, g, b) +} + +// RGB888 format, more commonly used in other places (desktop PC displays, CSS, +// etc). Less commonly used on embedded displays due to the higher memory usage. +type RGB888 struct { + R, G, B uint8 +} + +func NewRGB888(r, g, b uint8) RGB888 { + return RGB888{r, g, b} +} + +func (c RGB888) BitsPerPixel() int { + return 24 +} + +func (c RGB888) RGBA() color.RGBA { + return color.RGBA{ + R: c.R, + G: c.G, + B: c.B, + A: 255, + } +} + +// RGB565 as used in many SPI displays. Stored as a big endian value. +// +// The color format in integer form is gggbbbbb_rrrrrggg on little endian +// systems, which is the standard RGB565 format but with the top and bottom +// bytes swapped. +// +// There are a few alternatives to this weird big-endian format, but they're not +// great: +// - Storing the value in two 8-bit stores (to make the code endian-agnostic) +// incurs too much of a performance penalty. +// - Swapping the upper and lower bits just before storing. This is still less +// efficient than it could be, since colors are usually constructed once and +// then reused in many store operations. Doing the swap once instead of many +// times for each store is a performance win. +type RGB565BE uint16 + +func NewRGB565BE(r, g, b uint8) RGB565BE { + val := uint16(r&0xF8)<<8 + + uint16(g&0xFC)<<3 + + uint16(b&0xF8)>>3 + // Swap endianness (make big endian). + // This is done using a single instruction on ARM (rev16). + // TODO: this should only be done on little endian systems, but TinyGo + // doesn't currently (2023) support big endian systems so it's difficult to + // test. Also, big endian systems don't seem fasionable these days. + val = bits.ReverseBytes16(val) + return RGB565BE(val) +} + +func (c RGB565BE) BitsPerPixel() int { + return 16 +} + +func (c RGB565BE) RGBA() color.RGBA { + // Note: on ARM, the compiler uses a rev instruction instead of a rev16 + // instruction. I wonder whether this can be optimized further to use rev16 + // instead? + c = c<<8 | c>>8 + color := color.RGBA{ + R: uint8(c>>11) << 3, + G: uint8(c>>5) << 2, + B: uint8(c) << 3, + A: 255, + } + // Correct color rounding, so that 0xff roundtrips back to 0xff. + color.R |= color.R >> 5 + color.G |= color.G >> 6 + color.B |= color.B >> 5 + return color +} + +// Color format that is supported by the ST7789 for example. +// It may be a bit faster to use than RGB565BE on very slow SPI buses. +// +// The color format is native endian as a uint16 (0000rrrr_ggggbbbb), not big +// endian which you might expect. I tried swapping the bytes, but it didn't have +// much of a performance impact and made the code harder to read. It is stored +// as a 12-bit big endian value in Image[RGB444BE] though. +type RGB444BE uint16 + +func NewRGB444BE(r, g, b uint8) RGB444BE { + return RGB444BE(r>>4)<<8 | RGB444BE(g>>4)<<4 | RGB444BE(b>>4) +} + +func (c RGB444BE) BitsPerPixel() int { + return 12 +} + +func (c RGB444BE) RGBA() color.RGBA { + color := color.RGBA{ + R: uint8(c>>8) << 4, + G: uint8(c>>4) << 4, + B: uint8(c>>0) << 4, + A: 255, + } + // Correct color rounding, so that 0xff roundtrips back to 0xff. + color.R |= color.R >> 4 + color.G |= color.G >> 4 + color.B |= color.B >> 4 + return color +} + +// Gamma brightness lookup table: +// https://victornpb.github.io/gamma-table-generator +// gamma = 0.45 steps = 256 range = 0-255 +var gammaEncodeTable = [256]uint8{ + 0, 21, 28, 34, 39, 43, 46, 50, 53, 56, 59, 61, 64, 66, 68, 70, + 72, 74, 76, 78, 80, 82, 84, 85, 87, 89, 90, 92, 93, 95, 96, 98, + 99, 101, 102, 103, 105, 106, 107, 109, 110, 111, 112, 114, 115, 116, 117, 118, + 119, 120, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, + 136, 137, 138, 139, 140, 141, 142, 143, 144, 144, 145, 146, 147, 148, 149, 150, + 151, 151, 152, 153, 154, 155, 156, 156, 157, 158, 159, 160, 160, 161, 162, 163, + 164, 164, 165, 166, 167, 167, 168, 169, 170, 170, 171, 172, 173, 173, 174, 175, + 175, 176, 177, 178, 178, 179, 180, 180, 181, 182, 182, 183, 184, 184, 185, 186, + 186, 187, 188, 188, 189, 190, 190, 191, 192, 192, 193, 194, 194, 195, 195, 196, + 197, 197, 198, 199, 199, 200, 200, 201, 202, 202, 203, 203, 204, 205, 205, 206, + 206, 207, 207, 208, 209, 209, 210, 210, 211, 212, 212, 213, 213, 214, 214, 215, + 215, 216, 217, 217, 218, 218, 219, 219, 220, 220, 221, 221, 222, 223, 223, 224, + 224, 225, 225, 226, 226, 227, 227, 228, 228, 229, 229, 230, 230, 231, 231, 232, + 232, 233, 233, 234, 234, 235, 235, 236, 236, 237, 237, 238, 238, 239, 239, 240, + 240, 241, 241, 242, 242, 243, 243, 244, 244, 245, 245, 246, 246, 247, 247, 248, + 248, 249, 249, 249, 250, 250, 251, 251, 252, 252, 253, 253, 254, 254, 255, 255, +} diff --git a/st7735/st7735.go b/st7735/st7735.go index f9521cd39..6f15781c2 100644 --- a/st7735/st7735.go +++ b/st7735/st7735.go @@ -11,6 +11,7 @@ import ( "errors" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/pixel" ) type Model uint8 @@ -20,12 +21,23 @@ type Model uint8 // Deprecated: use drivers.Rotation instead. type Rotation = drivers.Rotation +// 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 struct { +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 machine.Pin resetPin machine.Pin @@ -39,7 +51,7 @@ type Device struct { batchLength int16 model Model isBGR bool - batchData []uint8 + batchData pixel.Image[T] // "image" with width, height of (batchLength, 1) } // Config is the configuration for the display @@ -54,11 +66,17 @@ type Config struct { // New creates a new ST7735 connection. The SPI wire must already be configured. func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) 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 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}) blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - return Device{ + return DeviceOf[T]{ bus: bus, dcPin: dcPin, resetPin: resetPin, @@ -68,7 +86,7 @@ func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { } // Configure initializes the display with default configuration -func (d *Device) Configure(cfg Config) { +func (d *DeviceOf[T]) Configure(cfg Config) { d.model = cfg.Model if cfg.Width != 0 { d.width = cfg.Width @@ -93,7 +111,7 @@ func (d *Device) Configure(cfg Config) { d.batchLength = d.height } d.batchLength += d.batchLength & 1 - d.batchData = make([]uint8, d.batchLength*2) + d.batchData = pixel.NewImage[T](int(d.batchLength), 1) // reset the device d.resetPin.High() @@ -142,8 +160,16 @@ func (d *Device) Configure(cfg Config) { d.Data(0xEE) d.Command(VMCTR1) d.Data(0x0E) + + // Set the color format depending on the generic type. d.Command(COLMOD) - d.Data(0x05) + 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) @@ -204,12 +230,12 @@ func (d *Device) Configure(cfg Config) { } // Display does nothing, there's no buffer as it might be too big for some boards -func (d *Device) Display() error { +func (d *DeviceOf[T]) Display() error { return nil } // SetPixel sets a pixel in the screen -func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { +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 @@ -218,7 +244,7 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { } // setWindow prepares the screen to be modified at a given rectangle -func (d *Device) setWindow(x, y, w, h int16) { +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 @@ -234,7 +260,7 @@ func (d *Device) setWindow(x, y, w, h int16) { } // SetScrollWindow sets an area to scroll with fixed top and bottom parts of the display -func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) { +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) @@ -246,38 +272,32 @@ func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) { } // SetScroll sets the vertical scroll address of the display. -func (d *Device) SetScroll(line int16) { +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 *Device) StopScroll() { +func (d *DeviceOf[T]) StopScroll() { d.Command(NORON) } // FillRectangle fills a rectangle at a given coordinates with a color -func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { +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) - c565 := RGBATo565(c) - c1 := uint8(c565 >> 8) - c2 := uint8(c565) - for i = 0; i < d.batchLength; i++ { - d.batchData[i*2] = c1 - d.batchData[i*2+1] = c2 - } + 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, false) + d.Tx(d.batchData.RawBuffer(), false) } else { - d.Tx(d.batchData[:i*2], false) + d.Tx(d.batchData.Rescale(int(i), 1).RawBuffer(), false) } i -= d.batchLength } @@ -285,7 +305,9 @@ func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { } // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates -func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { +// +// 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 { @@ -296,8 +318,15 @@ func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { 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 *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { +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 { @@ -315,17 +344,14 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col for k > 0 { for i := int16(0); i < d.batchLength; i++ { if offset+i < l { - c565 := RGBATo565(buffer[offset+i]) - c1 := uint8(c565 >> 8) - c2 := uint8(c565) - d.batchData[i*2] = c1 - d.batchData[i*2+1] = c2 + 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, false) + d.Tx(d.batchData.RawBuffer(), false) } else { - d.Tx(d.batchData[:k*2], false) + d.Tx(d.batchData.Rescale(int(k), 1).RawBuffer(), false) } k -= d.batchLength offset += d.batchLength @@ -334,7 +360,7 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col } // DrawFastVLine draws a vertical line faster than using SetPixel -func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { +func (d *DeviceOf[T]) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { if y0 > y1 { y0, y1 = y1, y0 } @@ -342,7 +368,7 @@ func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { } // DrawFastHLine draws a horizontal line faster than using SetPixel -func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { +func (d *DeviceOf[T]) DrawFastHLine(x0, x1, y int16, c color.RGBA) { if x0 > x1 { x0, x1 = x1, x0 } @@ -350,7 +376,7 @@ func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { } // FillScreen fills the screen with a given color -func (d *Device) FillScreen(c color.RGBA) { +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 { @@ -359,12 +385,12 @@ func (d *Device) FillScreen(c color.RGBA) { } // Rotation returns the currently configured rotation. -func (d *Device) Rotation() drivers.Rotation { +func (d *DeviceOf[T]) Rotation() drivers.Rotation { return d.rotation } // SetRotation changes the rotation of the device (clock-wise) -func (d *Device) SetRotation(rotation drivers.Rotation) error { +func (d *DeviceOf[T]) SetRotation(rotation drivers.Rotation) error { d.rotation = rotation madctl := uint8(0) switch rotation % 4 { @@ -386,23 +412,23 @@ func (d *Device) SetRotation(rotation drivers.Rotation) error { } // Command sends a command to the display -func (d *Device) Command(command uint8) { +func (d *DeviceOf[T]) Command(command uint8) { d.Tx([]byte{command}, true) } // Command sends a data to the display -func (d *Device) Data(data uint8) { +func (d *DeviceOf[T]) Data(data uint8) { d.Tx([]byte{data}, false) } // Tx sends data to the display -func (d *Device) Tx(data []byte, isCommand bool) { +func (d *DeviceOf[T]) Tx(data []byte, isCommand bool) { d.dcPin.Set(!isCommand) d.bus.Tx(data, nil) } // Size returns the current size of the display. -func (d *Device) Size() (w, h int16) { +func (d *DeviceOf[T]) Size() (w, h int16) { if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { return d.width, d.height } @@ -410,7 +436,7 @@ func (d *Device) Size() (w, h int16) { } // EnableBacklight enables or disables the backlight -func (d *Device) EnableBacklight(enable bool) { +func (d *DeviceOf[T]) EnableBacklight(enable bool) { if enable { d.blPin.High() } else { @@ -421,7 +447,7 @@ func (d *Device) EnableBacklight(enable bool) { // 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 *Device) Sleep(sleepEnabled bool) error { +func (d *DeviceOf[T]) Sleep(sleepEnabled bool) error { if sleepEnabled { // Shut down LCD panel. d.Command(SLPIN) @@ -437,7 +463,7 @@ func (d *Device) Sleep(sleepEnabled bool) error { } // InverColors inverts the colors of the screen -func (d *Device) InvertColors(invert bool) { +func (d *DeviceOf[T]) InvertColors(invert bool) { if invert { d.Command(INVON) } else { @@ -446,14 +472,6 @@ func (d *Device) InvertColors(invert bool) { } // IsBGR changes the color mode (RGB/BGR) -func (d *Device) IsBGR(bgr bool) { +func (d *DeviceOf[T]) IsBGR(bgr bool) { d.isBGR = bgr } - -// RGBATo565 converts a color.RGBA to uint16 used in the display -func RGBATo565(c color.RGBA) uint16 { - r, g, b, _ := c.RGBA() - return uint16((r & 0xF800) + - ((g & 0xFC00) >> 5) + - ((b & 0xF800) >> 11)) -} diff --git a/st7789/st7789.go b/st7789/st7789.go index 1e6f1285a..5db2402ba 100644 --- a/st7789/st7789.go +++ b/st7789/st7789.go @@ -14,6 +14,7 @@ import ( "errors" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/pixel" ) // Rotation controls the rotation used by the display. @@ -24,6 +25,13 @@ type Rotation = drivers.Rotation // The color format used on the display, like RGB565, RGB666, and RGB444. type ColorFormat uint8 +// Pixel formats supported by the st7789 driver. +type Color interface { + pixel.RGB444BE | pixel.RGB565BE + + pixel.BaseColor +} + // FrameRate controls the frame rate used by the display. type FrameRate uint8 @@ -32,7 +40,11 @@ var ( ) // Device wraps an SPI connection. -type Device struct { +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 dcPin machine.Pin resetPin machine.Pin @@ -47,6 +59,7 @@ type Device struct { rotation drivers.Rotation frameRate FrameRate batchLength int32 + batchData pixel.Image[T] // "image" with (width, height) of (batchLength, 1) isBGR bool vSyncLines int16 cmdBuf [1]byte @@ -71,11 +84,17 @@ 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 { + 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] { dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - return Device{ + return DeviceOf[T]{ bus: bus, dcPin: dcPin, resetPin: resetPin, @@ -85,7 +104,7 @@ func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { } // Configure initializes the display with default configuration -func (d *Device) Configure(cfg Config) { +func (d *DeviceOf[T]) Configure(cfg Config) { if cfg.Width != 0 { d.width = cfg.Width } else { @@ -137,7 +156,14 @@ func (d *Device) Configure(cfg Config) { d.sendCommand(SLPOUT, nil) // Exit sleep mode // Memory initialization - d.setColorFormat(ColorRGB565) // Set color mode to 16-bit color + var zeroColor T + switch any(zeroColor).(type) { + case pixel.RGB444BE: + d.setColorFormat(ColorRGB444) // 12 bits per pixel + default: + // Use default RGB565 color format. + d.setColorFormat(ColorRGB565) // 16 bits per pixel + } time.Sleep(10 * time.Millisecond) d.setRotation(d.rotation) // Memory orientation @@ -189,7 +215,7 @@ func (d *Device) Configure(cfg Config) { // Send a command with data to the display. It does not change the chip select // pin (it must be low when calling). The DC pin is left high after return, // meaning that data can be sent right away. -func (d *Device) sendCommand(command uint8, data []byte) error { +func (d *DeviceOf[T]) sendCommand(command uint8, data []byte) error { d.cmdBuf[0] = command d.dcPin.Low() err := d.bus.Tx(d.cmdBuf[:1], nil) @@ -202,7 +228,7 @@ func (d *Device) sendCommand(command uint8, data []byte) error { // startWrite must be called at the beginning of all exported methods to set the // chip select pin low. -func (d *Device) startWrite() { +func (d *DeviceOf[T]) startWrite() { if d.csPin != machine.NoPin { d.csPin.Low() } @@ -210,14 +236,23 @@ func (d *Device) startWrite() { // endWrite must be called at the end of all exported methods to set the chip // select pin high. -func (d *Device) endWrite() { +func (d *DeviceOf[T]) endWrite() { if d.csPin != machine.NoPin { d.csPin.High() } } +// getBuffer returns the image buffer, that's always d.batchLength wide and 1 +// pixel high. It can be used as a temporary buffer to transmit image data. +func (d *DeviceOf[T]) getBuffer() pixel.Image[T] { + if d.batchData.Len() == 0 { + d.batchData = pixel.NewImage[T](int(d.batchLength), 1) + } + return d.batchData +} + // Sync waits for the display to hit the next VSYNC pause -func (d *Device) Sync() { +func (d *DeviceOf[T]) Sync() { d.SyncToScanLine(0) } @@ -232,7 +267,7 @@ func (d *Device) Sync() { // NOTE: Use GetHighestScanLine and GetLowestScanLine to obtain the highest // and lowest useful values. Values are affected by front and back porch // vsync settings (derived from VSyncLines configuration option). -func (d *Device) SyncToScanLine(scanline uint16) { +func (d *DeviceOf[T]) SyncToScanLine(scanline uint16) { scan := d.GetScanLine() // Sometimes GetScanLine returns erroneous 0 on first call after draw, so double check @@ -262,7 +297,7 @@ func (d *Device) SyncToScanLine(scanline uint16) { } // GetScanLine reads the current scanline value from the display -func (d *Device) GetScanLine() uint16 { +func (d *DeviceOf[T]) GetScanLine() uint16 { d.startWrite() data := []uint8{0x00, 0x00} d.dcPin.Low() @@ -277,24 +312,24 @@ func (d *Device) GetScanLine() uint16 { } // GetHighestScanLine calculates the last scanline id in the frame before VSYNC pause -func (d *Device) GetHighestScanLine() uint16 { +func (d *DeviceOf[T]) GetHighestScanLine() uint16 { // Last scanline id appears to be backporch/2 + 320/2 return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 160 } // GetLowestScanLine calculate the first scanline id to appear after VSYNC pause -func (d *Device) GetLowestScanLine() uint16 { +func (d *DeviceOf[T]) GetLowestScanLine() uint16 { // First scanline id appears to be backporch/2 + 1 return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 1 } // Display does nothing, there's no buffer as it might be too big for some boards -func (d *Device) Display() error { +func (d *DeviceOf[T]) Display() error { return nil } // SetPixel sets a pixel in the screen -func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { +func (d *DeviceOf[T]) SetPixel(x int16, y int16, c color.RGBA) { if x < 0 || y < 0 || (((d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180) && (x >= d.width || y >= d.height)) || ((d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270) && (x >= d.height || y >= d.width))) { @@ -304,7 +339,7 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { } // setWindow prepares the screen to be modified at a given rectangle -func (d *Device) setWindow(x, y, w, h int16) { +func (d *DeviceOf[T]) setWindow(x, y, w, h int16) { x += d.columnOffset y += d.rowOffset copy(d.buf[:4], []uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)}) @@ -315,45 +350,41 @@ func (d *Device) setWindow(x, y, w, h int16) { } // FillRectangle fills a rectangle at a given coordinates with a color -func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { +func (d *DeviceOf[T]) FillRectangle(x, y, width, height int16, c color.RGBA) error { d.startWrite() err := d.fillRectangle(x, y, width, height, c) d.endWrite() return err } -func (d *Device) fillRectangle(x, y, width, height int16, c color.RGBA) error { +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) - c565 := RGBATo565(c) - c1 := uint8(c565 >> 8) - c2 := uint8(c565) - data := make([]uint8, d.batchLength*2) - for i := int32(0); i < d.batchLength; i++ { - data[i*2] = c1 - data[i*2+1] = c2 - } - j := int32(width) * int32(height) + image := d.getBuffer() + image.FillSolidColor(pixel.NewColor[T](c.R, c.G, c.B)) + j := int(width) * int(height) for j > 0 { // The DC pin is already set to data in the setWindow call, so we can // just write bytes on the SPI bus. - if j >= d.batchLength { - d.bus.Tx(data, nil) + if j >= image.Len() { + d.bus.Tx(image.RawBuffer(), nil) } else { - d.bus.Tx(data[:j*2], nil) + d.bus.Tx(image.Rescale(j, 1).RawBuffer(), nil) } - j -= d.batchLength + j -= image.Len() } return nil } // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates -func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { +// +// 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 { @@ -366,8 +397,15 @@ func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { 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)) +} + // FillRectangleWithBuffer fills buffer with a rectangle at a given coordinates. -func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { +func (d *DeviceOf[T]) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { i, j := d.Size() if x < 0 || y < 0 || width <= 0 || height <= 0 || x >= i || (x+width) > i || y >= j || (y+height) > j { @@ -379,35 +417,32 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col d.startWrite() d.setWindow(x, y, width, height) - k := int32(width) * int32(height) - data := make([]uint8, d.batchLength*2) - offset := int32(0) + k := int(width) * int(height) + image := d.getBuffer() + offset := 0 for k > 0 { - for i := int32(0); i < d.batchLength; i++ { - if offset+i < int32(len(buffer)) { - c565 := RGBATo565(buffer[offset+i]) - c1 := uint8(c565 >> 8) - c2 := uint8(c565) - data[i*2] = c1 - data[i*2+1] = c2 + for i := 0; i < image.Len(); i++ { + if offset+i < len(buffer) { + c := buffer[offset+i] + image.Set(i, 0, pixel.NewColor[T](c.R, c.G, c.B)) } } // The DC pin is already set to data in the setWindow call, so we don't // have to set it here. - if k >= d.batchLength { - d.bus.Tx(data, nil) + if k >= image.Len() { + d.bus.Tx(image.RawBuffer(), nil) } else { - d.bus.Tx(data[:k*2], nil) + d.bus.Tx(image.Rescale(k, 1).RawBuffer(), nil) } - k -= d.batchLength - offset += d.batchLength + k -= image.Len() + offset += image.Len() } d.endWrite() return nil } // DrawFastVLine draws a vertical line faster than using SetPixel -func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { +func (d *DeviceOf[T]) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { if y0 > y1 { y0, y1 = y1, y0 } @@ -415,7 +450,7 @@ func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { } // DrawFastHLine draws a horizontal line faster than using SetPixel -func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { +func (d *DeviceOf[T]) DrawFastHLine(x0, x1, y int16, c color.RGBA) { if x0 > x1 { x0, x1 = x1, x0 } @@ -423,13 +458,13 @@ func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { } // FillScreen fills the screen with a given color -func (d *Device) FillScreen(c color.RGBA) { +func (d *DeviceOf[T]) FillScreen(c color.RGBA) { d.startWrite() d.fillScreen(c) d.endWrite() } -func (d *Device) fillScreen(c color.RGBA) { +func (d *DeviceOf[T]) fillScreen(c color.RGBA) { if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 { d.fillRectangle(0, 0, d.width, d.height, c) } else { @@ -441,13 +476,13 @@ func (d *Device) fillScreen(c color.RGBA) { // The default is RGB565, setting it to any other value will break functions // like SetPixel, FillRectangle, etc. Instead, you can write color data in the // specified color format using DrawRGBBitmap8. -func (d *Device) SetColorFormat(format ColorFormat) { +func (d *DeviceOf[T]) SetColorFormat(format ColorFormat) { d.startWrite() d.setColorFormat(format) d.endWrite() } -func (d *Device) setColorFormat(format ColorFormat) { +func (d *DeviceOf[T]) setColorFormat(format ColorFormat) { // Lower 4 bits set the color format used in SPI. // Upper 4 bits set the color format used in the direct RGB interface. // The RGB interface is not currently supported, so it is left at a @@ -457,12 +492,12 @@ func (d *Device) setColorFormat(format ColorFormat) { } // Rotation returns the current rotation of the device. -func (d *Device) Rotation() drivers.Rotation { +func (d *DeviceOf[T]) Rotation() drivers.Rotation { return d.rotation } // SetRotation changes the rotation of the device (clock-wise) -func (d *Device) SetRotation(rotation Rotation) error { +func (d *DeviceOf[T]) SetRotation(rotation Rotation) error { d.rotation = rotation d.startWrite() err := d.setRotation(rotation) @@ -470,7 +505,7 @@ func (d *Device) SetRotation(rotation Rotation) error { return err } -func (d *Device) setRotation(rotation Rotation) error { +func (d *DeviceOf[T]) setRotation(rotation Rotation) error { madctl := uint8(0) switch rotation % 4 { case drivers.Rotation0: @@ -496,7 +531,7 @@ func (d *Device) setRotation(rotation Rotation) error { } // Size returns the current size of the display. -func (d *Device) Size() (w, h int16) { +func (d *DeviceOf[T]) Size() (w, h int16) { if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { return d.width, d.height } @@ -504,7 +539,7 @@ func (d *Device) Size() (w, h int16) { } // EnableBacklight enables or disables the backlight -func (d *Device) EnableBacklight(enable bool) { +func (d *DeviceOf[T]) EnableBacklight(enable bool) { if enable { d.blPin.High() } else { @@ -515,7 +550,7 @@ func (d *Device) EnableBacklight(enable bool) { // 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 *Device) Sleep(sleepEnabled bool) error { +func (d *DeviceOf[T]) Sleep(sleepEnabled bool) error { if sleepEnabled { d.startWrite() d.sendCommand(SLPIN, nil) @@ -537,7 +572,7 @@ func (d *Device) Sleep(sleepEnabled bool) error { } // InvertColors inverts the colors of the screen -func (d *Device) InvertColors(invert bool) { +func (d *DeviceOf[T]) InvertColors(invert bool) { d.startWrite() if invert { d.sendCommand(INVON, nil) @@ -548,12 +583,12 @@ func (d *Device) InvertColors(invert bool) { } // IsBGR changes the color mode (RGB/BGR) -func (d *Device) IsBGR(bgr bool) { +func (d *DeviceOf[T]) IsBGR(bgr bool) { d.isBGR = bgr } // SetScrollArea sets an area to scroll with fixed top and bottom parts of the display. -func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) { +func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { if d.height < 320 { // The screen doesn't use the full 320 pixel height. // Enlarge the bottom fixed area to fill the 320 pixel height, so that @@ -577,7 +612,7 @@ func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) { } // SetScroll sets the vertical scroll address of the display. -func (d *Device) SetScroll(line int16) { +func (d *DeviceOf[T]) SetScroll(line int16) { if d.rotation == drivers.Rotation180 { // The screen is rotated by 180°, so we have to invert the scroll line // (taking care of the RowOffset). @@ -591,16 +626,8 @@ func (d *Device) SetScroll(line int16) { } // StopScroll returns the display to its normal state. -func (d *Device) StopScroll() { +func (d *DeviceOf[T]) StopScroll() { d.startWrite() d.sendCommand(NORON, nil) d.endWrite() } - -// RGBATo565 converts a color.RGBA to uint16 used in the display -func RGBATo565(c color.RGBA) uint16 { - r, g, b, _ := c.RGBA() - return uint16((r & 0xF800) + - ((g & 0xFC00) >> 5) + - ((b & 0xF800) >> 11)) -}