mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
Compare commits
23 Commits
v0.26.0
...
gbadisplay
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d1b0c39cc | |||
| 6cf07aff4c | |||
| af33129f41 | |||
| 100aadf585 | |||
| 4a9667ffef | |||
| f96a70915e | |||
| 3e64e754a2 | |||
| 2c2da5c7bb | |||
| 14994a3f31 | |||
| 47dfeb9e94 | |||
| f20d1759d3 | |||
| 6b79a1b386 | |||
| 7dd1067cc1 | |||
| 4edb771c5b | |||
| e95cfe66c1 | |||
| 4edb58c0c5 | |||
| f384e2db48 | |||
| 7c96387845 | |||
| 182d4c6ebb | |||
| dc2de4f9ed | |||
| 36a12dd7a2 | |||
| 00992756eb | |||
| 715c33d4b0 |
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
// Connects to an MAG3110 I2C magnetometer.
|
||||
// Connects to an DS3231 I2C Real Time Clock (RTC).
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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}},
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user