Compare commits

...

23 Commits

Author SHA1 Message Date
Ayke van Laethem 5d1b0c39cc gbadisplay: add simple driver for the GameBoy Advance display
This only implements the 15 bits per pixel mode, not any of the other
possible display modes. This matches conventional SPI displays most
closely. Future additions might add more display modes.

Normally I'd argue interfacing with chip/board specific hardware should
be done in the machine package using a generic interface, but the GBA is
kind of special and I don't want the machine package to depend on the
tinygo.org/x/drivers/pixel package.
2023-11-23 22:39:33 +01:00
Ayke van Laethem 6cf07aff4c pyportal_boing: update to use DrawBitmap
Replace DrawRGBBitmap8 with DrawBitmap, following the change in the
previous commit.

This improves performance from 86fps to 100fps! I didn't investigate
why, but I suspect it's because it now needs only a single store instead
of two to update a pixel.
2023-11-23 17:27:00 +01:00
Ayke van Laethem af33129f41 ili9341: st7735: st7789: add DrawBitmap method
This adds a new DrawBitmap method, which is meant to replace
DrawRGBBitmap8.
2023-11-23 17:27:00 +01:00
Ayke van Laethem 100aadf585 st7789: make the display generic over RGB565 and RGB444
Same as for st7735 in the previous commit.

In addition, this avoids allocating a big chunk of memory on _every_
draw operation (even SetPixel) and instead reuses it across draw
operations. This makes the driver a whole lot more efficient.
2023-11-23 17:27:00 +01:00
Ayke van Laethem 4a9667ffef st7735: make the display generic over RGB565 and RGB444
Using RGB444 instead of RGB565 can speed up graphics operations by up to
25%, especially on slow screens. But for full support, all parts of the
driver need to be aware of the color format.

It's possible to do this using a regular configuration variable, but
it's unlikely to be very efficient. Hence the usage of generics.
2023-11-23 17:27:00 +01:00
Ayke van Laethem f96a70915e pixel: add package for efficiently working with raw pixel buffers
This has been optimized for working with SPI displays like the ST7789.
By working directly in the native color format of the display, graphics
operations can be much, _much_ faster.

Also, this makes it easier to use a different color format like RGB444
simply by changing the generic type.
2023-11-23 17:27:00 +01:00
Ayke van Laethem 3e64e754a2 epd2in13: use better black/white approximation
The previoius behavior was that entirely black pixels were treated as
white, and anything else as black. That's at least counter-intuitive.
This patch changes the behavior to actually look at the color values and
use a cutoff around medium gray: darker colors are treated as black, and
lighter colors are treated as white.

This is a backwards incompatible change, but I think this behavior makes
a lot more sense.
2023-11-09 10:07:19 +01:00
Ayke van Laethem 2c2da5c7bb epd2in13: add Sleep method like other displays
For details, see: https://github.com/tinygo-org/drivers/pull/548
2023-11-07 10:39:14 +01:00
Ayke van Laethem 14994a3f31 epd2in13: unify rotation configuration with other displays
This commit updates rotation behavior to match other displays.
For more information, see: https://github.com/tinygo-org/drivers/pull/550

This changes the signature of `SetRotation` but I don't think any
existing code will be affected by this change.
2023-11-07 10:39:14 +01:00
deadprogram 47dfeb9e94 examples/lora/lorawan: modify atcmd and basic demo to support choosing
any one of the supported regions at compile time by using ldflags.

Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-10-26 08:32:04 +02:00
deadprogram f20d1759d3 examples/lora/lorawan: add missing functions for simulated interface
Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-10-26 08:32:04 +02:00
deadprogram 6b79a1b386 lora/lorawan: refactor shared functionality for ChannelUS, etc into embedded type
named channel, do the same for RegionSettings, and then change RegionSettings to
just Settings to avoid the redundant naming.

Signed-off-by: deadprogram <ron@hybridgroup.com>
2023-10-26 08:32:04 +02:00
Emilio Garcia 7dd1067cc1 LoRa WAN add setter functions 2023-10-26 08:32:04 +02:00
Emilio Garcia 4edb771c5b fix lora us915 logic to step into 500 kHz range 2023-10-26 08:32:04 +02:00
Mirac Kara e95cfe66c1 LoRa WAN US915 Support
Adds a driver for us 915 protocol to the lora WAN drivers.
2023-10-26 08:32:04 +02:00
Thomas Richner 4edb58c0c5 fix-tests: fix broken testrunner 2023-10-25 09:57:40 +02:00
Thomas Richner f384e2db48 fix-tests: migrated legacy I2C 2023-10-25 09:57:40 +02:00
sago35 7c96387845 sh1106: fix I2C interface and add smoketest 2023-10-14 11:43:06 +02:00
Christian Ege 182d4c6ebb sh1106: fixed the description of the device struct
It looks like there was an copy paste left over. This is corrected to be
concise.

Signed-off-by: Christian Ege <ch@ege.io>
2023-10-09 08:59:44 +02:00
Thomas Richner dc2de4f9ed adafruit4650: support for Adafruite 4650 feather OLED 2023-10-06 09:03:32 +02:00
Christian Ege 36a12dd7a2 at24cx: fixed the description of the device struct
It looks like there was an copy paste left over. This is corrected to be
concise.

Signed-off-by: Christian Ege <ch@ege.io>
2023-10-06 08:44:12 +02:00
Christian Ege 00992756eb ds3231: fix the description in the example
It looks like there was a copy paste leftover. This got corrected to be
concise.

Signed-off-by: Christian Ege <ch@ege.io>
2023-10-06 08:44:12 +02:00
Ayke van Laethem 715c33d4b0 all: prepare for CGo changes in TinyGo
For details, see: https://github.com/tinygo-org/tinygo/pull/3927
This change just means we need to be more careful to use the right type,
now that types like C.uint32_t don't match to the Go equivalent (like
uint32).
2023-10-05 10:01:25 +02:00
37 changed files with 1555 additions and 295 deletions
+196
View File
@@ -0,0 +1,196 @@
// Package adafruit4650 implements a driver for the Adafruit FeatherWing OLED - 128x64 OLED display.
// The display is backed itself by a SH1107 driver chip.
//
// Store: https://www.adafruit.com/product/4650
//
// Documentation: https://learn.adafruit.com/adafruit-128x64-oled-featherwing
package adafruit4650
import (
"image/color"
"time"
"tinygo.org/x/drivers"
)
const DefaultAddress = 0x3c
const (
commandSetLowColumn = 0x00
commandSetHighColumn = 0x10
commandSetPage = 0xb0
)
const (
width = 128
height = 64
)
// Device represents an Adafruit 4650 device
type Device struct {
bus drivers.I2C
Address uint8
buffer []byte
width int16
height int16
}
// New creates a new device, not configuring anything yet.
func New(bus drivers.I2C) Device {
return Device{
bus: bus,
Address: DefaultAddress,
width: width,
height: height,
}
}
// Configure initializes the display with default configuration
func (d *Device) Configure() error {
bufferSize := d.width * d.height / 8
d.buffer = make([]byte, bufferSize)
// This sequence is an amalgamation of the datasheet, official Arduino driver, CircuitPython driver and other drivers
initSequence := []byte{
0xae, // display off, sleep mode
//0xd5, 0x41, // set display clock divider (from original datasheet)
0xd5, 0x51, // set display clock divider (from Adafruit driver)
0xd9, 0x22, // pre-charge/dis-charge period mode: 2 DCLKs/2 DCLKs (POR)
0x20, // memory mode
0x81, 0x4f, // contrast setting = 0x4f
0xad, 0x8a, // set dc/dc pump
0xa0, // segment remap, flip-x
0xc0, // common output scan direction
0xdc, 0x00, // set display start line 0 (POR=0)
0xa8, 0x3f, // multiplex ratio, height - 1 = 0x3f
0xd3, 0x60, // set display offset mode = 0x60
0xdb, 0x35, // VCOM deselect level = 0.770 (POR)
0xa4, // entire display off, retain RAM, normal status (POR)
0xa6, // normal (not reversed) display
0xaf, // display on
}
err := d.writeCommands(initSequence)
if err != nil {
return err
}
// recommended in the datasheet, same in other drivers
time.Sleep(100 * time.Millisecond)
return nil
}
// ClearDisplay clears the image buffer as well as the actual display
func (d *Device) ClearDisplay() error {
d.ClearBuffer()
return d.Display()
}
// ClearBuffer clears the buffer
func (d *Device) ClearBuffer() {
bzero(d.buffer)
}
// SetPixel modifies the internal buffer. Since this display has a bit-depth of 1 bit any non-zero
// color component will be treated as 'on', otherwise 'off'.
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
if x < 0 || x >= d.width || y < 0 || y >= d.height {
return
}
// RAM layout
// *-----> y
// |
// x| col0 col1 ... col63
// v p0 a0 b0 ..
// a1 b1 ..
// .. .. ..
// a7 b7 ..
// p1 a0 b0
// a1 b1
//
//flip y - so the display orientation matches the silk screen labeling etc.
y = d.height - y - 1
page := x / 8
bytesPerPage := d.height
byteIndex := y + bytesPerPage*page
bit := x % 8
if (c.R | c.G | c.B) != 0 {
d.buffer[byteIndex] |= 1 << uint8(bit)
} else {
d.buffer[byteIndex] &^= 1 << uint8(bit)
}
}
// Display sends the whole buffer to the screen
func (d *Device) Display() error {
bytesPerPage := d.height
pages := (d.width + 7) / 8
for page := int16(0); page < pages; page++ {
err := d.setRAMPosition(uint8(page), 0)
if err != nil {
return err
}
offset := page * bytesPerPage
err = d.writeRAM(d.buffer[offset : offset+bytesPerPage])
if err != nil {
return err
}
}
return nil
}
// setRAMPosition updates the device's current page and column position
func (d *Device) setRAMPosition(page uint8, column uint8) error {
if page > 15 {
panic("page out of bounds")
}
if column > 127 {
panic("column out of bounds")
}
setPage := commandSetPage | (page & 0xF)
lo := column & 0xF
setLowColumn := commandSetLowColumn | lo
hi := (column >> 4) & 0x7
setHighColumn := commandSetHighColumn | hi
cmds := []byte{
setPage,
setLowColumn,
setHighColumn,
}
return d.writeCommands(cmds)
}
// Size returns the current size of the display.
func (d *Device) Size() (w, h int16) {
return d.width, d.height
}
func (d *Device) writeCommands(commands []byte) error {
onlyCommandsFollowing := byte(0x00)
return d.bus.Tx(uint16(d.Address), append([]byte{onlyCommandsFollowing}, commands...), nil)
}
func (d *Device) writeRAM(data []byte) error {
onlyRAMFollowing := byte(0x40)
return d.bus.Tx(uint16(d.Address), append([]byte{onlyRAMFollowing}, data...), nil)
}
func bzero(buf []byte) {
for i := range buf {
buf[i] = 0
}
}
+176
View File
@@ -0,0 +1,176 @@
package adafruit4650
import (
"bytes"
_ "embed"
"encoding/hex"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
"testing"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freemono"
)
//go:embed expected_hello_world.png
var expectedHelloWorld []byte
// mockBus mocks a fake i2c device adafruit4650 display.
// The memory layout assumes that clients set up the device in a particular way and always send complete
// pages to the device buffer.
type mockBus struct {
img draw.Image
line int
addr uint8
currentPage int
currentColumn int
}
func (m *mockBus) Tx(addr uint16, w, r []byte) error {
if addr != uint16(m.addr) {
panic("unexpected address")
}
if r != nil {
panic("mock does not support reads")
}
if w[0] == 0x00 {
if w[1]&0xf0 == 0xb0 {
m.currentPage = int(w[1] & 0x0f)
lo := w[2] & 0x0f
hi := w[2] & 0x07
m.currentColumn = int(hi<<4 | lo)
}
return nil
}
if w[0] != 0x40 {
panic("unexpected first byte: " + hex.EncodeToString(w[0:1]))
}
return m.writeRAM(w[1:])
}
func newMock() *mockBus {
m := image.NewRGBA(image.Rect(0, 0, width, height))
return &mockBus{img: m, addr: DefaultAddress, currentPage: -1, currentColumn: -1}
}
func (m *mockBus) writeRAM(data []byte) error {
// RAM layout
// *-----> y
// |
// x| col0 col1 ... col63
// v p0 a0 b0 ..
// a1 b1 ..
// .. .. ..
// a7 b7 ..
// p1 a0 b0
// a1 b1
//
fmt.Printf("writing page %d\n", m.currentPage)
// assuming entire pages will be written
for x := 0; x < 8; x++ {
for y := 0; y < height; y++ {
col := data[y]
c := color.Black
if col&(1<<x) != 0 {
c = color.White
}
m.img.Set(x+m.currentPage*8, height-y-1, c)
}
}
return nil
}
func (m *mockBus) toImage() *image.RGBA {
container := image.NewRGBA(m.img.Bounds().Inset(-1))
draw.Draw(container, container.Bounds(), image.NewUniform(color.RGBA{G: 255, A: 255}), image.Point{}, draw.Over)
draw.Draw(container, m.img.Bounds(), m.img, image.Point{}, draw.Over)
return container
}
func TestDevice_Display(t *testing.T) {
bus := newMock()
dev := New(bus)
dev.Configure()
drawPlus(&dev)
drawHellowWorld(&dev)
//when
dev.Display()
//then
actual := bus.toImage()
expected, err := png.Decode(bytes.NewReader(expectedHelloWorld))
if err != nil {
panic(err)
}
assertEqualImages(t, actual, expected)
}
func drawPlus(d drivers.Displayer) {
for i := int16(0); i < 128; i++ {
d.SetPixel(i, 32, color.RGBA{R: 1})
}
for i := int16(0); i < 64; i++ {
d.SetPixel(64, i, color.RGBA{R: 1})
}
}
func drawHellowWorld(d drivers.Displayer) {
tinyfont.WriteLine(d, &freemono.Regular9pt7b, 0, 32, "Hello World!", color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
}
func assertEqualImages(t testing.TB, actual, expected image.Image) {
if actual.Bounds().Dx() != expected.Bounds().Dx() || actual.Bounds().Dy() != expected.Bounds().Dy() {
f := writeImage(actual)
t.Fatalf("differing size: was %v, expected %v, saved actual to %s", actual.Bounds(), expected.Bounds(), f)
}
bb := expected.Bounds()
for x := bb.Min.X; x < bb.Max.X; x++ {
for y := bb.Min.Y; y < bb.Max.Y; y++ {
actualBB := actual.Bounds()
if actual.At(x+actualBB.Min.X, y+actualBB.Min.Y) != expected.At(x, y) {
f := writeImage(actual)
t.Fatalf("different pixel at %d/%d: %v != %v, saved actual at %s", x, y, actual.At(x, y), expected.At(x, y), f)
}
}
}
}
func writeImage(img image.Image) string {
fn := fmt.Sprintf("%d.png", time.Now().Unix())
f, err := os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
defer f.Close()
err = png.Encode(f, img)
if err != nil {
panic(err)
}
return fn
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

+1 -1
View File
@@ -11,7 +11,7 @@ import (
"tinygo.org/x/drivers"
)
// Device wraps an I2C connection to a DS3231 device.
// Device wraps an I2C connection to an AT24CX device.
type Device struct {
bus drivers.I2C
Address uint16
+1 -1
View File
@@ -49,7 +49,7 @@ func Sleep(duration time.Duration) {
// * The CPU frequency is lower than 256MHz. If it is higher, long sleep
// times (1-16ms) may not work correctly.
cycles := uint32(duration) * (machine.CPUFrequency() / 1000_000) / 1000
slept := C.tinygo_drivers_sleep(cycles)
slept := C.tinygo_drivers_sleep(C.uint32_t(cycles))
if !slept {
// Fallback for platforms without inline assembly support.
time.Sleep(duration)
+42
View File
@@ -0,0 +1,42 @@
package main
import (
"image/color"
"machine"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/adafruit4650"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freemono"
)
func main() {
machine.I2C0.Configure(machine.I2CConfig{})
dev := adafruit4650.New(machine.I2C0)
err := dev.Configure()
if err != nil {
panic(err)
}
drawPlus(&dev)
drawHelloWorld(&dev)
err = dev.Display()
if err != nil {
panic(err)
}
}
func drawPlus(d drivers.Displayer) {
for i := int16(0); i < 128; i++ {
d.SetPixel(i, 32, color.RGBA{R: 1})
}
for i := int16(0); i < 64; i++ {
d.SetPixel(64, i, color.RGBA{R: 1})
}
}
func drawHelloWorld(d drivers.Displayer) {
tinyfont.WriteLine(d, &freemono.Regular9pt7b, 0, 32, "Hello World!", color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Connects to an MAG3110 I2C magnetometer.
// Connects to an DS3231 I2C Real Time Clock (RTC).
package main
import (
+23 -22
View File
@@ -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)
}
}
+4 -4
View File
@@ -23,20 +23,20 @@ Builds/flashes atcmd console application with simulator instead of actual LoRa r
tinygo flash -target pico ./examples/lora/lorawan/atcmd/
```
## PyBadge with LoRa Featherwing
## PyBadge with LoRa Featherwing for EU868 region
Builds/flashes atcmd console application on PyBadge using LoRa Featherwing (RFM95/SX1276).
```
tinygo flash -target pybadge -tags featherwing ./examples/lora/lorawan/atcmd/
tinygo flash -target pybadge -tags featherwing -ldflags="-X main.reg=EU868" ./examples/lora/lorawan/atcmd/
```
## LoRa-E5
## LoRa-E5 for US915 region
Builds/flashes atcmd console application on Lora-E5 using onboard SX126x.
```
tinygo flash -target lorae5 ./examples/lora/lorawan/atcmd/
tinygo flash -target lorae5 -ldflags="-X main.reg=US915" ./examples/lora/lorawan/atcmd/
```
## Joining a Public Lorawan Network
+12 -1
View File
@@ -32,6 +32,8 @@ var (
defaultTimeout uint32 = 1000
)
var reg string
func main() {
uart.Configure(machine.UARTConfig{TX: tx, RX: rx})
@@ -45,7 +47,16 @@ func main() {
otaa = &lorawan.Otaa{}
lorawan.UseRadio(radio)
lorawan.UseRegionSettings(region.EU868())
switch reg {
case "AU915":
lorawan.UseRegionSettings(region.AU915())
case "EU868":
lorawan.UseRegionSettings(region.EU868())
case "US915":
lorawan.UseRegionSettings(region.US915())
default:
lorawan.UseRegionSettings(region.EU868())
}
for {
if uart.Buffered() > 0 {
+4 -4
View File
@@ -21,16 +21,16 @@ loraConnect: Connected !
tinygo flash -target pico ./examples/lora/lorawan/basic-demo
```
## PyBadge with LoRa Featherwing
## PyBadge with LoRa Featherwing for EU868 region
```
tinygo flash -target pybadge -tags featherwing ./examples/lora/lorawan/basic-demo
tinygo flash -target pybadge -tags featherwing -ldflags="-X main.reg=EU868" ./examples/lora/lorawan/basic-demo
```
## LoRa-E5
## LoRa-E5 for US915 region
```
tinygo flash -target lorae5 ./examples/lora/lorawan/basic-demo
tinygo flash -target lorae5 -ldflags="-X main.reg=US915" ./examples/lora/lorawan/basic-demo
```
+14 -3
View File
@@ -12,7 +12,10 @@ import (
"tinygo.org/x/drivers/lora/lorawan/region"
)
var debug string
var (
reg string
debug string
)
const (
LORAWAN_JOIN_TIMEOUT_SEC = 180
@@ -67,8 +70,16 @@ func main() {
// Connect the lorawan with the Lora Radio device.
lorawan.UseRadio(radio)
lorawan.UseRegionSettings(region.EU868())
switch reg {
case "AU915":
lorawan.UseRegionSettings(region.AU915())
case "EU868":
lorawan.UseRegionSettings(region.EU868())
case "US915":
lorawan.UseRegionSettings(region.US915())
default:
lorawan.UseRegionSettings(region.EU868())
}
// Configure AppEUI, DevEUI, APPKey, and public/private Lorawan Network
setLorawanKeys()
+12 -7
View File
@@ -23,13 +23,18 @@ func (sr *SimLoraRadio) Rx(timeoutMs uint32) ([]uint8, error) {
return nil, nil
}
func (sr *SimLoraRadio) SetFrequency(freq uint32) {}
func (sr *SimLoraRadio) SetIqMode(mode uint8) {}
func (sr *SimLoraRadio) SetCodingRate(cr uint8) {}
func (sr *SimLoraRadio) SetBandwidth(bw uint8) {}
func (sr *SimLoraRadio) SetCrc(enable bool) {}
func (sr *SimLoraRadio) SetSpreadingFactor(sf uint8) {}
func (sr *SimLoraRadio) LoraConfig(cnf lora.Config) {}
func (sr *SimLoraRadio) SetFrequency(freq uint32) {}
func (sr *SimLoraRadio) SetIqMode(mode uint8) {}
func (sr *SimLoraRadio) SetCodingRate(cr uint8) {}
func (sr *SimLoraRadio) SetBandwidth(bw uint8) {}
func (sr *SimLoraRadio) SetCrc(enable bool) {}
func (sr *SimLoraRadio) SetSpreadingFactor(sf uint8) {}
func (sr *SimLoraRadio) SetHeaderType(headerType uint8) {}
func (sr *SimLoraRadio) SetPreambleLength(pLen uint16) {}
func (sr *SimLoraRadio) SetPublicNetwork(enabled bool) {}
func (sr *SimLoraRadio) SetSyncWord(syncWord uint16) {}
func (sr *SimLoraRadio) SetTxPower(txPower int8) {}
func (sr *SimLoraRadio) LoraConfig(cnf lora.Config) {}
func FirmwareVersion() string {
return "simulator " + CurrentVersion()
+85
View File
@@ -0,0 +1,85 @@
// Package gbadisplay implements a simple driver for the GameBoy Advance
// display.
package gbadisplay
import (
"device/gba"
"errors"
"image/color"
"runtime/volatile"
"unsafe"
"tinygo.org/x/drivers/pixel"
)
// Image buffer type used by the GameBoy Advance.
type Image = pixel.Image[pixel.RGB555]
const (
displayWidth = 240
displayHeight = 160
)
var (
errOutOfBounds = errors.New("rectangle coordinates outside display area")
)
type Device struct{}
// New returns a new GameBoy Advance display object.
func New() Device {
return Device{}
}
var displayFrameBuffer = (*[160 * 240]volatile.Register16)(unsafe.Pointer(uintptr(gba.MEM_VRAM)))
type Config struct {
// TODO: add more display modes here.
}
// Configure the display as a regular 15bpp framebuffer.
func (d Device) Configure(config Config) {
// Use video mode 3 (in BG2, a 16bpp bitmap in VRAM) and Enable BG2.
gba.DISP.DISPCNT.Set(gba.DISPCNT_BGMODE_3<<gba.DISPCNT_BGMODE_Pos |
gba.DISPCNT_SCREENDISPLAY_BG2_ENABLE<<gba.DISPCNT_SCREENDISPLAY_BG2_Pos)
}
// Size returns the fixed size of this display.
func (d Device) Size() (x, y int16) {
return displayWidth, displayHeight
}
// Display is a no-op: the display framebuffer is modified directly.
func (d Device) Display() error {
// Nothing to do here.
return nil
}
// SetPixel changes the pixel at (x, y) to the given color.
func (d Device) SetPixel(x, y int16, c color.RGBA) {
if x < 0 || y < 0 || x >= displayWidth || y > displayHeight {
// Out of bounds, so ignore.
return
}
val := pixel.NewColor[pixel.RGB555](c.R, c.G, c.B)
displayFrameBuffer[(int(y))*240+int(x)].Set(uint16(val))
}
// DrawBitmap updates the rectangle at (x, y) to the image stored in buf.
func (d Device) DrawBitmap(x, y int16, buf Image) error {
width, height := buf.Size()
if x < 0 || y < 0 || int(x)+width > displayWidth || int(y)+height > displayHeight {
return errOutOfBounds
}
// TODO: try to do a 4-byte memcpy if possible. That should significantly
// speed up the copying of this image.
for bufY := 0; bufY < int(height); bufY++ {
for bufX := 0; bufX < int(width); bufX++ {
val := buf.Get(bufX, bufY)
displayFrameBuffer[(int(y)+bufY)*240+int(x)+bufX].Set(uint16(val))
}
}
return nil
}
+15
View File
@@ -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()
+3
View File
@@ -80,6 +80,9 @@ const (
const (
MHz_868_1 = 868100000
MHz_868_5 = 868500000
MHz_902_3 = 902300000
Mhz_903_0 = 903000000
MHZ_915_0 = 915000000
MHz_916_8 = 916800000
MHz_923_3 = 923300000
)
+31 -25
View File
@@ -30,11 +30,11 @@ const (
var (
ActiveRadio lora.Radio
Retries = 15
regionSettings region.RegionSettings
regionSettings region.Settings
)
// UseRegionSettings sets current Lorawan Regional parameters
func UseRegionSettings(rs region.RegionSettings) {
func UseRegionSettings(rs region.Settings) {
regionSettings = rs
}
@@ -52,13 +52,13 @@ func SetPublicNetwork(enabled bool) {
}
// ApplyChannelConfig sets current Lora modulation according to current regional settings
func applyChannelConfig(ch *region.Channel) {
ActiveRadio.SetFrequency(ch.Frequency)
ActiveRadio.SetBandwidth(ch.Bandwidth)
ActiveRadio.SetCodingRate(ch.CodingRate)
ActiveRadio.SetSpreadingFactor(ch.SpreadingFactor)
ActiveRadio.SetPreambleLength(ch.PreambleLength)
ActiveRadio.SetTxPower(ch.TxPowerDBm)
func applyChannelConfig(ch region.Channel) {
ActiveRadio.SetFrequency(ch.Frequency())
ActiveRadio.SetBandwidth(ch.Bandwidth())
ActiveRadio.SetCodingRate(ch.CodingRate())
ActiveRadio.SetSpreadingFactor(ch.SpreadingFactor())
ActiveRadio.SetPreambleLength(ch.PreambleLength())
ActiveRadio.SetTxPower(ch.TxPowerDBm())
// Lorawan defaults to explicit headers
ActiveRadio.SetHeaderType(lora.HeaderExplicit)
ActiveRadio.SetCrc(true)
@@ -84,24 +84,30 @@ func Join(otaa *Otaa, session *Session) error {
return err
}
// Prepare radio for Join Tx
applyChannelConfig(regionSettings.JoinRequestChannel())
ActiveRadio.SetIqMode(lora.IQStandard)
ActiveRadio.Tx(payload, LORA_TX_TIMEOUT)
if err != nil {
return err
}
for {
joinRequestChannel := regionSettings.JoinRequestChannel()
joinAcceptChannel := regionSettings.JoinAcceptChannel()
// Wait for JoinAccept
applyChannelConfig(regionSettings.JoinAcceptChannel())
ActiveRadio.SetIqMode(lora.IQInverted)
resp, err = ActiveRadio.Rx(LORA_RX_TIMEOUT)
if err != nil {
return err
}
// Prepare radio for Join Tx
applyChannelConfig(joinRequestChannel)
ActiveRadio.SetIqMode(lora.IQStandard)
ActiveRadio.Tx(payload, LORA_TX_TIMEOUT)
if err != nil {
return err
}
if resp == nil {
return ErrNoJoinAcceptReceived
// Wait for JoinAccept
if joinAcceptChannel.Frequency() != 0 {
applyChannelConfig(joinAcceptChannel)
}
ActiveRadio.SetIqMode(lora.IQInverted)
resp, err = ActiveRadio.Rx(LORA_RX_TIMEOUT)
if err == nil && resp != nil {
break
}
if !joinAcceptChannel.Next() {
return ErrNoJoinAcceptReceived
}
}
err = otaa.DecodeJoinAccept(resp, session)
+21 -23
View File
@@ -7,43 +7,41 @@ const (
AU915_DEFAULT_TX_POWER_DBM = 20
)
type RegionSettingsAU915 struct {
joinRequestChannel *Channel
joinAcceptChannel *Channel
uplinkChannel *Channel
type ChannelAU struct {
channel
}
func AU915() *RegionSettingsAU915 {
return &RegionSettingsAU915{
joinRequestChannel: &Channel{lora.MHz_916_8,
func (c *ChannelAU) Next() bool {
return false
}
type SettingsAU915 struct {
settings
}
func AU915() *SettingsAU915 {
return &SettingsAU915{settings: settings{
joinRequestChannel: &ChannelAU{channel: channel{lora.MHz_916_8,
lora.Bandwidth_125_0,
lora.SpreadingFactor9,
lora.CodingRate4_5,
AU915_DEFAULT_PREAMBLE_LEN,
AU915_DEFAULT_TX_POWER_DBM},
joinAcceptChannel: &Channel{lora.MHz_923_3,
AU915_DEFAULT_TX_POWER_DBM}},
joinAcceptChannel: &ChannelAU{channel: channel{lora.MHz_923_3,
lora.Bandwidth_500_0,
lora.SpreadingFactor9,
lora.CodingRate4_5,
AU915_DEFAULT_PREAMBLE_LEN,
AU915_DEFAULT_TX_POWER_DBM},
uplinkChannel: &Channel{lora.MHz_916_8,
AU915_DEFAULT_TX_POWER_DBM}},
uplinkChannel: &ChannelAU{channel: channel{lora.MHz_916_8,
lora.Bandwidth_125_0,
lora.SpreadingFactor9,
lora.CodingRate4_5,
AU915_DEFAULT_PREAMBLE_LEN,
AU915_DEFAULT_TX_POWER_DBM},
}
AU915_DEFAULT_TX_POWER_DBM}},
}}
}
func (r *RegionSettingsAU915) JoinRequestChannel() *Channel {
return r.joinRequestChannel
}
func (r *RegionSettingsAU915) JoinAcceptChannel() *Channel {
return r.joinAcceptChannel
}
func (r *RegionSettingsAU915) UplinkChannel() *Channel {
return r.uplinkChannel
func Next(c *ChannelAU) bool {
return false
}
+42
View File
@@ -0,0 +1,42 @@
package region
type Channel interface {
Next() bool
Frequency() uint32
Bandwidth() uint8
SpreadingFactor() uint8
CodingRate() uint8
PreambleLength() uint16
TxPowerDBm() int8
SetFrequency(v uint32)
SetBandwidth(v uint8)
SetSpreadingFactor(v uint8)
SetCodingRate(v uint8)
SetPreambleLength(v uint16)
SetTxPowerDBm(v int8)
}
type channel struct {
frequency uint32
bandwidth uint8
spreadingFactor uint8
codingRate uint8
preambleLength uint16
txPowerDBm int8
}
// Getter functions
func (c *channel) Frequency() uint32 { return c.frequency }
func (c *channel) Bandwidth() uint8 { return c.bandwidth }
func (c *channel) SpreadingFactor() uint8 { return c.spreadingFactor }
func (c *channel) CodingRate() uint8 { return c.codingRate }
func (c *channel) PreambleLength() uint16 { return c.preambleLength }
func (c *channel) TxPowerDBm() int8 { return c.txPowerDBm }
// Set functions
func (c *channel) SetFrequency(v uint32) { c.frequency = v }
func (c *channel) SetBandwidth(v uint8) { c.bandwidth = v }
func (c *channel) SetSpreadingFactor(v uint8) { c.spreadingFactor = v }
func (c *channel) SetCodingRate(v uint8) { c.codingRate = v }
func (c *channel) SetPreambleLength(v uint16) { c.preambleLength = v }
func (c *channel) SetTxPowerDBm(v int8) { c.txPowerDBm = v }
+19 -25
View File
@@ -7,43 +7,37 @@ const (
EU868_DEFAULT_TX_POWER_DBM = 20
)
type RegionSettingsEU868 struct {
joinRequestChannel *Channel
joinAcceptChannel *Channel
uplinkChannel *Channel
type ChannelEU struct {
channel
}
func EU868() *RegionSettingsEU868 {
return &RegionSettingsEU868{
joinRequestChannel: &Channel{lora.MHz_868_1,
func (c *ChannelEU) Next() bool {
return false
}
type SettingsEU868 struct {
settings
}
func EU868() *SettingsEU868 {
return &SettingsEU868{settings: settings{
joinRequestChannel: &ChannelEU{channel: channel{lora.MHz_868_1,
lora.Bandwidth_125_0,
lora.SpreadingFactor9,
lora.CodingRate4_7,
EU868_DEFAULT_PREAMBLE_LEN,
EU868_DEFAULT_TX_POWER_DBM},
joinAcceptChannel: &Channel{lora.MHz_868_1,
EU868_DEFAULT_TX_POWER_DBM}},
joinAcceptChannel: &ChannelEU{channel: channel{lora.MHz_868_1,
lora.Bandwidth_125_0,
lora.SpreadingFactor9,
lora.CodingRate4_7,
EU868_DEFAULT_PREAMBLE_LEN,
EU868_DEFAULT_TX_POWER_DBM},
uplinkChannel: &Channel{lora.MHz_868_1,
EU868_DEFAULT_TX_POWER_DBM}},
uplinkChannel: &ChannelEU{channel: channel{lora.MHz_868_1,
lora.Bandwidth_125_0,
lora.SpreadingFactor9,
lora.CodingRate4_7,
EU868_DEFAULT_PREAMBLE_LEN,
EU868_DEFAULT_TX_POWER_DBM},
}
}
func (r *RegionSettingsEU868) JoinRequestChannel() *Channel {
return r.joinRequestChannel
}
func (r *RegionSettingsEU868) JoinAcceptChannel() *Channel {
return r.joinAcceptChannel
}
func (r *RegionSettingsEU868) UplinkChannel() *Channel {
return r.uplinkChannel
EU868_DEFAULT_TX_POWER_DBM}},
}}
}
-16
View File
@@ -1,16 +0,0 @@
package region
type Channel struct {
Frequency uint32
Bandwidth uint8
SpreadingFactor uint8
CodingRate uint8
PreambleLength uint16
TxPowerDBm int8
}
type RegionSettings interface {
JoinRequestChannel() *Channel
JoinAcceptChannel() *Channel
UplinkChannel() *Channel
}
+25
View File
@@ -0,0 +1,25 @@
package region
type Settings interface {
JoinRequestChannel() Channel
JoinAcceptChannel() Channel
UplinkChannel() Channel
}
type settings struct {
joinRequestChannel Channel
joinAcceptChannel Channel
uplinkChannel Channel
}
func (r *settings) JoinRequestChannel() Channel {
return r.joinRequestChannel
}
func (r *settings) JoinAcceptChannel() Channel {
return r.joinAcceptChannel
}
func (r *settings) UplinkChannel() Channel {
return r.uplinkChannel
}
+82
View File
@@ -0,0 +1,82 @@
package region
import "tinygo.org/x/drivers/lora"
const (
US915_DEFAULT_PREAMBLE_LEN = 8
US915_DEFAULT_TX_POWER_DBM = 20
US915_FREQUENCY_INCREMENT_DR_0 = 200000 // only for 125 kHz Bandwidth
US915_FREQUENCY_INCREMENT_DR_4 = 1600000 // only for 500 kHz Bandwidth
)
type ChannelUS struct {
channel
}
func (c *ChannelUS) Next() bool {
switch c.Bandwidth() {
case lora.Bandwidth_125_0:
freq, ok := stepFrequency125(c.frequency)
if ok {
c.frequency = freq
} else {
c.frequency = lora.Mhz_903_0
c.bandwidth = lora.Bandwidth_500_0
}
case lora.Bandwidth_500_0:
freq, ok := stepFrequency500(c.frequency)
if ok {
c.frequency = freq
} else {
// there are no more frequencies to check after sweeping all 8 500 kHz channels
return false
}
}
return true
}
func stepFrequency125(freq uint32) (uint32, bool) {
f := freq + US915_FREQUENCY_INCREMENT_DR_0
if f >= lora.MHZ_915_0 {
return 0, false
}
return f, true
}
func stepFrequency500(freq uint32) (uint32, bool) {
f := freq + US915_FREQUENCY_INCREMENT_DR_4
if f >= lora.MHZ_915_0 {
return 0, false
}
return f, true
}
type SettingsUS915 struct {
settings
}
func US915() *SettingsUS915 {
return &SettingsUS915{settings: settings{
joinRequestChannel: &ChannelUS{channel: channel{lora.MHz_902_3,
lora.Bandwidth_125_0,
lora.SpreadingFactor10,
lora.CodingRate4_5,
US915_DEFAULT_PREAMBLE_LEN,
US915_DEFAULT_TX_POWER_DBM}},
joinAcceptChannel: &ChannelUS{channel: channel{0,
lora.Bandwidth_500_0,
lora.SpreadingFactor9,
lora.CodingRate4_5,
US915_DEFAULT_PREAMBLE_LEN,
US915_DEFAULT_TX_POWER_DBM}},
uplinkChannel: &ChannelUS{channel: channel{lora.Mhz_903_0,
lora.Bandwidth_500_0,
lora.SpreadingFactor9,
lora.CodingRate4_5,
US915_DEFAULT_PREAMBLE_LEN,
US915_DEFAULT_TX_POWER_DBM}},
}}
}
+223
View File
@@ -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)
}
}
+64
View File
@@ -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)
}
}
}
+225
View File
@@ -0,0 +1,225 @@
// 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 | RGB555 | 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 RGB555:
return any(NewRGB555(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 used on the GameBoy Advance among others.
//
// Colors are stored as native endian values, with bits 0bbbbbgg_gggrrrrr (red
// is least significant, blue is most significant).
type RGB555 uint16
func NewRGB555(r, g, b uint8) RGB555 {
return RGB555(r)>>3 | (RGB555(g)>>3)<<5 | (RGB555(b)>>3)<<10
}
func (c RGB555) BitsPerPixel() int {
// 15 bits per pixel, but there are 16 bits when stored
return 16
}
func (c RGB555) RGBA() color.RGBA {
color := color.RGBA{
R: uint8(c>>10) << 3,
G: uint8(c>>5) << 3,
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 >> 5
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,
}
+5 -4
View File
@@ -10,6 +10,7 @@ import (
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
)
// Device wraps an SPI connection.
@@ -51,7 +52,7 @@ type Buser interface {
type VccMode uint8
// NewI2C creates a new SSD1306 connection. The I2C wire must already be configured.
// NewI2C creates a new SH1106 connection. The I2C wire must already be configured.
func NewI2C(bus drivers.I2C) Device {
return Device{
bus: &I2CBus{
@@ -61,7 +62,7 @@ func NewI2C(bus drivers.I2C) Device {
}
}
// NewSPI creates a new SSD1306 connection. The SPI wire must already be configured.
// NewSPI creates a new SH1106 connection. The SPI wire must already be configured.
func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) Device {
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
@@ -283,9 +284,9 @@ func (d *Device) Tx(data []byte, isCommand bool) {
// tx sends data to the display (I2CBus implementation)
func (b *I2CBus) tx(data []byte, isCommand bool) {
if isCommand {
b.wire.WriteRegister(uint8(b.Address), 0x00, data)
legacy.WriteRegister(b.wire, uint8(b.Address), 0x00, data)
} else {
b.wire.WriteRegister(uint8(b.Address), 0x40, data)
legacy.WriteRegister(b.wire, uint8(b.Address), 0x40, data)
}
}
+1 -1
View File
@@ -133,7 +133,7 @@ func runSmokeTest(filename string) error {
result := <-job.resultChan
os.Stdout.Write(job.output.Bytes())
if result != nil {
return err
return result
}
}
+2
View File
@@ -5,6 +5,7 @@
# get an md5sum).
tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/adafruit4650
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adt7410/main.go
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adxl345/main.go
tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/amg88xx
@@ -135,3 +136,4 @@ tinygo build -size short -o ./build/test.hex -target=pico ./examples/ndir/main_n
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ndir/main_ndir.go
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ndir/main_ndir.go
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu9150/main.go
tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/sh1106/macropad_spi
+70 -52
View File
@@ -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))
}
+99 -72
View File
@@ -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))
}
+41 -19
View File
@@ -15,8 +15,8 @@ import (
type Config struct {
Width int16 // Width is the display resolution
Height int16
LogicalWidth int16 // LogicalWidth must be a multiple of 8 and same size or bigger than Width
Rotation Rotation // Rotation is clock-wise
LogicalWidth int16 // LogicalWidth must be a multiple of 8 and same size or bigger than Width
Rotation drivers.Rotation
}
type Device struct {
@@ -30,10 +30,11 @@ type Device struct {
height int16
buffer []uint8
bufferLength uint32
rotation Rotation
rotation drivers.Rotation
}
type Rotation uint8
// Deprecated: use drivers.Rotation instead.
type Rotation = drivers.Rotation
// Look up table for full updates
var lutFullUpdate = [30]uint8{
@@ -130,6 +131,17 @@ func (d *Device) DeepSleep() {
d.WaitUntilIdle()
}
// Set the sleep mode of the panel. The display will still show its contents,
// but will go into a lower-power state.
func (d *Device) Sleep(sleepEnabled bool) error {
if sleepEnabled {
d.DeepSleep()
} else {
d.Reset()
}
return nil
}
// SendCommand sends a command to the display
func (d *Device) SendCommand(command uint8) {
d.sendDataCommand(true, command)
@@ -167,18 +179,22 @@ func (d *Device) SetLUT(fullUpdate bool) {
}
// SetPixel modifies the internal buffer in a single pixel.
// The display have 2 colors: black and white
// We use RGBA(0,0,0, 255) as white (transparent)
// Anything else as black
// The display have 2 colors: black and white. We use a very simple cutoff to
// determine whether a pixel is black or white (darker colors are black, lighter
// colors are white).
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
x, y = d.xy(x, y)
if x < 0 || x >= d.logicalWidth || y < 0 || y >= d.height {
return
}
byteIndex := (x + y*d.logicalWidth) / 8
if c.R == 0 && c.G == 0 && c.B == 0 { // TRANSPARENT / WHITE
// Very simle black/white split.
// This isn't very accurate (especially for sRGB colors) but is close enough
// to the truth that it probably doesn't matter much - especially on an
// e-paper display.
if int(c.R)+int(c.G)+int(c.B) > 128*3 { // light, convert to white
d.buffer[byteIndex] |= 0x80 >> uint8(x%8)
} else { // WHITE / EMPTY
} else { // dark, convert to black
d.buffer[byteIndex] &^= 0x80 >> uint8(x%8)
}
}
@@ -209,13 +225,13 @@ func (d *Device) DisplayRect(x int16, y int16, width int16, height int16) error
if x < 0 || y < 0 || x >= d.logicalWidth || y >= d.height || width < 0 || height < 0 {
return errors.New("wrong rectangle")
}
if d.rotation == ROTATION_90 {
if d.rotation == drivers.Rotation90 {
width, height = height, width
x -= width
} else if d.rotation == ROTATION_180 {
} else if d.rotation == drivers.Rotation180 {
x -= width - 1
y -= height - 1
} else if d.rotation == ROTATION_270 {
} else if d.rotation == drivers.Rotation270 {
width, height = height, width
y -= height
}
@@ -301,27 +317,33 @@ func (d *Device) ClearBuffer() {
// Size returns the current size of the display.
func (d *Device) Size() (w, h int16) {
if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 {
if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 {
return d.height, d.logicalWidth
}
return d.logicalWidth, d.height
}
// SetRotation changes the rotation (clock-wise) of the device
func (d *Device) SetRotation(rotation Rotation) {
// Rotation returns the current rotation of the device.
func (d *Device) Rotation() drivers.Rotation {
return d.rotation
}
// SetRotation changes the rotation of the device.
func (d *Device) SetRotation(rotation drivers.Rotation) error {
d.rotation = rotation
return nil
}
// xy chages the coordinates according to the rotation
func (d *Device) xy(x, y int16) (int16, int16) {
switch d.rotation {
case NO_ROTATION:
case drivers.Rotation0:
return x, y
case ROTATION_90:
case drivers.Rotation90:
return d.width - y - 1, x
case ROTATION_180:
case drivers.Rotation180:
return d.width - x - 1, d.height - y - 1
case ROTATION_270:
case drivers.Rotation270:
return y, d.height - x - 1
}
return x, y
+6 -4
View File
@@ -1,5 +1,7 @@
package epd2in13
import "tinygo.org/x/drivers"
// Registers
const (
DRIVER_OUTPUT_CONTROL = 0x01
@@ -24,8 +26,8 @@ const (
SET_RAM_Y_ADDRESS_COUNTER = 0x4F
TERMINATE_FRAME_READ_WRITE = 0xFF
NO_ROTATION Rotation = 0
ROTATION_90 Rotation = 1 // 90 degrees clock-wise rotation
ROTATION_180 Rotation = 2
ROTATION_270 Rotation = 3
NO_ROTATION = drivers.Rotation0
ROTATION_90 = drivers.Rotation90 // 90 degrees clock-wise rotation
ROTATION_180 = drivers.Rotation180
ROTATION_270 = drivers.Rotation270
)
+1 -1
View File
@@ -251,7 +251,7 @@ func writeGoWrapper(f *os.File, arch string, megahertz int) error {
fmt.Fprintf(buf, " portClear, maskClear := d.Pin.PortMaskClear()\n")
fmt.Fprintf(buf, "\n")
fmt.Fprintf(buf, " mask := interrupt.Disable()\n")
fmt.Fprintf(buf, " C.ws2812_writeByte%d(C.char(c), (*uint32)(unsafe.Pointer(portSet)), (*uint32)(unsafe.Pointer(portClear)), maskSet, maskClear)\n", megahertz)
fmt.Fprintf(buf, " C.ws2812_writeByte%d(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))\n", megahertz)
buf.WriteString(`
interrupt.Restore(mask)
}
+6 -6
View File
@@ -1328,7 +1328,7 @@ func (d Device) writeByte16(c byte) {
portClear, maskClear := d.Pin.PortMaskClear()
mask := interrupt.Disable()
C.ws2812_writeByte16(C.char(c), (*uint32)(unsafe.Pointer(portSet)), (*uint32)(unsafe.Pointer(portClear)), maskSet, maskClear)
C.ws2812_writeByte16(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
interrupt.Restore(mask)
}
@@ -1338,7 +1338,7 @@ func (d Device) writeByte48(c byte) {
portClear, maskClear := d.Pin.PortMaskClear()
mask := interrupt.Disable()
C.ws2812_writeByte48(C.char(c), (*uint32)(unsafe.Pointer(portSet)), (*uint32)(unsafe.Pointer(portClear)), maskSet, maskClear)
C.ws2812_writeByte48(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
interrupt.Restore(mask)
}
@@ -1348,7 +1348,7 @@ func (d Device) writeByte64(c byte) {
portClear, maskClear := d.Pin.PortMaskClear()
mask := interrupt.Disable()
C.ws2812_writeByte64(C.char(c), (*uint32)(unsafe.Pointer(portSet)), (*uint32)(unsafe.Pointer(portClear)), maskSet, maskClear)
C.ws2812_writeByte64(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
interrupt.Restore(mask)
}
@@ -1358,7 +1358,7 @@ func (d Device) writeByte120(c byte) {
portClear, maskClear := d.Pin.PortMaskClear()
mask := interrupt.Disable()
C.ws2812_writeByte120(C.char(c), (*uint32)(unsafe.Pointer(portSet)), (*uint32)(unsafe.Pointer(portClear)), maskSet, maskClear)
C.ws2812_writeByte120(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
interrupt.Restore(mask)
}
@@ -1368,7 +1368,7 @@ func (d Device) writeByte125(c byte) {
portClear, maskClear := d.Pin.PortMaskClear()
mask := interrupt.Disable()
C.ws2812_writeByte125(C.char(c), (*uint32)(unsafe.Pointer(portSet)), (*uint32)(unsafe.Pointer(portClear)), maskSet, maskClear)
C.ws2812_writeByte125(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
interrupt.Restore(mask)
}
@@ -1378,7 +1378,7 @@ func (d Device) writeByte168(c byte) {
portClear, maskClear := d.Pin.PortMaskClear()
mask := interrupt.Disable()
C.ws2812_writeByte168(C.char(c), (*uint32)(unsafe.Pointer(portSet)), (*uint32)(unsafe.Pointer(portClear)), maskSet, maskClear)
C.ws2812_writeByte168(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
interrupt.Restore(mask)
}
+2 -2
View File
@@ -1114,7 +1114,7 @@ func (d Device) writeByte160(c byte) {
portClear, maskClear := d.Pin.PortMaskClear()
mask := interrupt.Disable()
C.ws2812_writeByte160(C.char(c), (*uint32)(unsafe.Pointer(portSet)), (*uint32)(unsafe.Pointer(portClear)), maskSet, maskClear)
C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
interrupt.Restore(mask)
}
@@ -1124,7 +1124,7 @@ func (d Device) writeByte320(c byte) {
portClear, maskClear := d.Pin.PortMaskClear()
mask := interrupt.Disable()
C.ws2812_writeByte320(C.char(c), (*uint32)(unsafe.Pointer(portSet)), (*uint32)(unsafe.Pointer(portClear)), maskSet, maskClear)
C.ws2812_writeByte320(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
interrupt.Restore(mask)
}
+1 -1
View File
@@ -60,7 +60,7 @@ func (d Device) WriteByte(c byte) error {
switch machine.CPUFrequency() {
case 16e6: // 16MHz
C.ws2812_writeByte16(C.char(c), (*uint8)(unsafe.Pointer(port)), maskSet, maskClear)
C.ws2812_writeByte16(C.char(c), (*C.uint8_t)(unsafe.Pointer(port)), C.uint8_t(maskSet), C.uint8_t(maskClear))
interrupt.Restore(mask)
return nil
default: