Compare commits

...

24 Commits

Author SHA1 Message Date
sago35 3c1aaa9a9f Add error check for d.bus.Tx() 2024-07-18 08:35:57 +09:00
sago35 3f4a9d82f7 Add check for which 2024-07-16 09:00:34 +09:00
sago35 19c9fe6db3 Add error handling 2024-07-14 13:37:13 +09:00
sago35 315dbef694 dht20: add I2C driver for DHT20 temperature and humidity sensor 2024-07-13 10:48:33 +09:00
Ron Evans ee3842f639 pcf8591: add ADC only implementation for I2C ADC/DAC (#690)
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-07-01 22:06:25 +09:00
deadprogram 1bf1a11067 all: prepare release v0.28.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-06-18 11:38:02 +02:00
leongross 848f5def03 nit: increase verbosity of espat NetNotify error (#684)
espat: increase verbosity of espat NetNotify error
2024-06-14 17:36:07 +02:00
Thomas 4a0bcba87c epd2in66b: Waveshare 2.66inch E-Paper Display Module (B) for Raspberry Pi Pico (#673)
epd2in66b: add working driver

Co-authored-by: Thomas Richner <thomas.richner@oviva.com>
2024-06-10 13:16:45 +02:00
Ayke van Laethem 7d983647ad pixel: fix Image[Monochrome].Set for larger images
For bigger images, the pixel index might not fit in a int16. Therefore,
int is needed during the calculation.

While fixing this bug, I've added a few tests that verify the Image
implementation by creating images, filling them with random data, and
then checking whether they still contain the same data. This test failed
before the patch.
2024-05-25 17:09:48 +02:00
deadprogram 831982ad33 servo: add function SetAngle() to simplify API for most common use case
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-05-25 08:34:52 +02:00
deadprogram 9063f46313 uc8151: improvements to speed and also add flicker-free mode based on @antirez code example
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-05-13 22:45:37 +02:00
gmsec 2a621dc3b1 Add driver for MCP9808 i2c temperature sensor (#676)
mcp9808: add support for temperature sensor
2024-05-12 10:42:03 +02:00
deadprogram 7dbca2a543 uc8151: correct DrawBitmap() also refactor SendCommand() and SendData() for clarity
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-05-09 15:37:57 +02:00
deadprogram 50b6f18cc2 uc8151: update to support all functions needed by tinygl and board package Displayer interface
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-05-08 16:35:21 +02:00
sago35 bb0e6b8ba8 Fix typo and move initialization of neo to init() 2024-05-06 11:19:19 +02:00
sago35 5eeba138a4 Simplify examples/ws2812 2024-05-06 11:19:19 +02:00
deadprogram 1095629f62 ssd1306: add Sleep() function for Displayer interface
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-04-24 13:00:20 +02:00
deadprogram bf0b383176 ssd1306: add rotation functions for Displayer interface
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-04-24 13:00:20 +02:00
Elias Naur f206f6f56f ft6336: ignore bogus touch events
At least one ft6336 device reports all 255 values from the first read
after reset or poweron, even after waiting the specified 300ms reset
delay.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-21 12:00:03 +02:00
deadprogram a0293643b3 ssd1306: add DrawBitmap() function to complete Displayer interface
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-04-21 11:52:21 +02:00
deadprogram 60801ba852 pixel: add support for Monochrome types such as the SSD1306 display
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-04-13 15:50:30 +02:00
deadprogram a74770b2e6 rtl8720dn: implement ConnectModeAP
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-04-01 07:40:46 +02:00
deadprogram 3ed09d3fe1 wifinina: implement ConnectModeAP
Signed-off-by: deadprogram <ron@hybridgroup.com>
2024-04-01 07:40:46 +02:00
sago35 22581dfd58 encoders: add atsamd21, atsamd51, atsame5x 2024-02-29 15:29:41 +01:00
41 changed files with 1776 additions and 455 deletions
+42
View File
@@ -1,3 +1,45 @@
0.28.0
---
- **new devices**
- **epd2in66b**
- Waveshare 2.66inch E-Paper Display Module (B) for Raspberry Pi Pico (#673)
- **mcp9808**
- Add driver for MCP9808 i2c temperature sensor (#676)
- **enhancements**
- **encoders**
- add atsamd21, atsamd51, atsame5x
- **pixel**
- add support for Monochrome types such as the SSD1306 display
- **rtl8720dn**
- implement ConnectModeAP
- **servo**
- add function SetAngle() to simplify API for most common use case
- **ssd1306**
- add DrawBitmap() function to complete Displayer interface
- add rotation functions for Displayer interface
- add Sleep() function for Displayer interface
- **uc8151**
- improvements to speed and also add flicker-free mode based on @antirez code example
- update to support all functions needed by tinygl and board package Displayer interface
- **wifinina**
- implement ConnectModeAP
- **bugfixes**
- **ft6336**
- ignore bogus touch events
- **pixel**
- fix Image[Monochrome].Set for larger images
- **uc8151**
- correct DrawBitmap() also refactor SendCommand() and SendData() for clarity
- **ws2812**
- Fix typo and move initialization of neo to init()
- **examples**
- **ws2812**
- Simplify examples/ws2812
0.27.0
---
- **core**
+1 -1
View File
@@ -19,7 +19,7 @@ rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst
# Recursively find all *_test.go files from cwd & reduce to unique dir names
HAS_TESTS = $(sort $(dir $(call rwildcard,,*_test.go)))
# Exclude anything we explicitly don't want to test for whatever reason
EXCLUDE_TESTS = image
EXCLUDE_TESTS = image waveshare-epd/epd2in66b
TESTS = $(filter-out $(addsuffix /%,$(EXCLUDE_TESTS)),$(HAS_TESTS))
unit-test:
+1 -1
View File
@@ -3,7 +3,7 @@
[![PkgGoDev](https://pkg.go.dev/badge/tinygo.org/x/drivers)](https://pkg.go.dev/tinygo.org/x/drivers) [![Build](https://github.com/tinygo-org/drivers/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/drivers/actions/workflows/build.yml)
This package provides a collection of 102 different hardware drivers for devices such as sensors and displays that can be used together with [TinyGo](https://tinygo.org).
This package provides a collection of over 100 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org).
For the complete list, please see:
https://tinygo.org/docs/reference/devices/
+144
View File
@@ -0,0 +1,144 @@
// Package dht20 implements a driver for the DHT20 temperature and humidity sensor.
//
// Datasheet: https://cdn-shop.adafruit.com/product-files/5183/5193_DHT20.pdf
package dht20
import (
"errors"
"time"
"tinygo.org/x/drivers"
)
var (
errUpdateCalledTooSoon = errors.New("Update() called within 80ms is invalid")
)
// Device wraps an I2C connection to a DHT20 device.
type Device struct {
bus drivers.I2C
Address uint16
data [8]uint8
temperature float32
humidity float32
prevAccessTime time.Time
}
// New creates a new DHT20 connection. The I2C bus must already be
// configured.
//
// This function only creates the Device object, it does not touch the device.
func New(bus drivers.I2C) Device {
return Device{
bus: bus,
Address: defaultAddress, // Using the address defined in registers.go
}
}
// Configure sets up the device for communication and initializes the registers if needed.
func (d *Device) Configure() error {
// Get the status word
d.data[0] = 0x71
err := d.bus.Tx(d.Address, d.data[:1], d.data[:1])
if err != nil {
return err
}
if d.data[0] != 0x18 {
// Initialize registers
err := d.initRegisters()
if err != nil {
return err
}
}
// Set the previous access time to the current time
d.prevAccessTime = time.Now()
return nil
}
// initRegisters initializes the registers 0x1B, 0x1C, and 0x1E to 0x00.
func (d *Device) initRegisters() error {
// Initialize register 0x1B
d.data[0] = 0x1B
d.data[1] = 0x00
err := d.bus.Tx(d.Address, d.data[:2], nil)
if err != nil {
return err
}
// Initialize register 0x1C
d.data[0] = 0x1C
d.data[1] = 0x00
err = d.bus.Tx(d.Address, d.data[:2], nil)
if err != nil {
return err
}
// Initialize register 0x1E
d.data[0] = 0x1E
d.data[1] = 0x00
err = d.bus.Tx(d.Address, d.data[:2], nil)
if err != nil {
return err
}
return nil
}
// Update reads data from the sensor and updates the temperature and humidity values.
// Note that the values obtained by this function are from the previous call to Update.
// If you want to use the most recent values, shorten the interval at which Update is called.
func (d *Device) Update(which drivers.Measurement) error {
if which&drivers.Temperature == 0 && which&drivers.Humidity == 0 {
return nil
}
// Check if 80ms have passed since the last access
if time.Since(d.prevAccessTime) < 80*time.Millisecond {
return errUpdateCalledTooSoon
}
// Check the status word Bit[7]
d.data[0] = 0x71
err := d.bus.Tx(d.Address, d.data[:1], d.data[:1])
if err != nil {
return err
}
if (d.data[0] & 0x80) == 0 {
// Read 7 bytes of data from the sensor
err := d.bus.Tx(d.Address, nil, d.data[:7])
if err != nil {
return err
}
rawHumidity := uint32(d.data[1])<<12 | uint32(d.data[2])<<4 | uint32(d.data[3])>>4
rawTemperature := uint32(d.data[3]&0x0F)<<16 | uint32(d.data[4])<<8 | uint32(d.data[5])
// Convert raw values to human-readable values
d.humidity = float32(rawHumidity) / 1048576.0 * 100
d.temperature = float32(rawTemperature)/1048576.0*200 - 50
// Trigger the next measurement
d.data[0] = 0xAC
d.data[1] = 0x33
d.data[2] = 0x00
err = d.bus.Tx(d.Address, d.data[:3], nil)
if err != nil {
return err
}
// Update the previous access time to the current time
d.prevAccessTime = time.Now()
}
return nil
}
// Temperature returns the last measured temperature.
func (d *Device) Temperature() float32 {
return d.temperature
}
// Humidity returns the last measured humidity.
func (d *Device) Humidity() float32 {
return d.humidity
}
+6
View File
@@ -0,0 +1,6 @@
package dht20
// Constants/addresses used for I2C.
// The I2C address which this device listens to.
const defaultAddress = 0x38
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build tinygo && (rp2040 || stm32 || k210 || esp32c3 || nrf || (avr && (atmega328p || atmega328pb)))
//go:build tinygo && (rp2040 || stm32 || k210 || esp32c3 || nrf || sam || (avr && (atmega328p || atmega328pb)))
// Implementation based on:
// https://gist.github.com/aykevl/3fc1683ed77bb0a9c07559dfe857304a
+1 -2
View File
@@ -122,7 +122,7 @@ func (d *Device) NetDisconnect() {
}
func (d *Device) NetNotify(cb func(netlink.Event)) {
// Not supported
fmt.Printf("\r\n%s\r\n", netlink.ErrNotSupported)
}
func (d *Device) GetHostByName(name string) (netip.Addr, error) {
@@ -473,7 +473,6 @@ func (d *Device) parseIPD(end int) error {
}
// load up the socket data
//d.data = append(d.data, d.response[e+1:end]...)
d.data = append(d.data, d.response[e+1:e+1+v]...)
return nil
}
+36
View File
@@ -0,0 +1,36 @@
package main
import (
"machine"
"strconv"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/dht20"
)
var (
i2c = machine.I2C0
)
func main() {
i2c.Configure(machine.I2CConfig{})
sensor := dht20.New(i2c)
sensor.Configure()
// Trigger the first measurement
sensor.Update(drivers.AllMeasurements)
for {
time.Sleep(1 * time.Second)
// Update sensor dasta
sensor.Update(drivers.AllMeasurements)
temp := sensor.Temperature()
hum := sensor.Humidity()
// Note: The sensor values are from the previous measurement (1 second ago)
println("Temperature:", strconv.FormatFloat(float64(temp), 'f', 2, 64), "°C")
println("Humidity:", strconv.FormatFloat(float64(hum), 'f', 2, 64), "%")
}
}
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"fmt"
"machine"
"time"
"tinygo.org/x/drivers/mcp9808"
)
func main() {
//tinygo monitor
time.Sleep(time.Millisecond * 5000)
//Configure I2C (in this case, I2C0 on RPI Pico), and wire the module accordingly
machine.I2C0.Configure(machine.I2CConfig{
SCL: machine.GP1,
SDA: machine.GP0,
})
//Create sensor
sensor := mcp9808.New(machine.I2C0)
if !sensor.Connected() {
println("MCP9808 not found")
return
} else {
println("MCP9808 found")
}
time.Sleep(time.Millisecond * 1000)
//Set resolution
sensor.SetResolution(mcp9808.Maximum)
time.Sleep(time.Millisecond * 1000)
//Read temp.
temp, err := sensor.ReadTemperature()
if err != nil {
println("MCP9808 error reading temperature")
println(err.Error())
return
} else {
fmt.Printf("Temperature: %.2f \n", temp)
}
return
}
+28
View File
@@ -0,0 +1,28 @@
// Connects to a pcf8591 ADC via I2C.
package main
import (
"machine"
"time"
"tinygo.org/x/drivers/pcf8591"
)
var (
i2c = machine.I2C0
)
func main() {
i2c.Configure(machine.I2CConfig{})
adc := pcf8591.New(i2c)
adc.Configure()
// get "CH0" aka "machine.ADC" interface to channel 0 from ADC.
p := adc.CH0
for {
val := p.Get()
println(val)
time.Sleep(50 * time.Millisecond)
}
}
+19 -13
View File
@@ -25,19 +25,25 @@ func main() {
return
}
println("setting to 0°")
s.SetMicroseconds(1000)
time.Sleep(3 * time.Second)
println("setting to 45°")
s.SetMicroseconds(1500)
time.Sleep(3 * time.Second)
println("setting to 90°")
s.SetMicroseconds(2000)
time.Sleep(3 * time.Second)
for {
time.Sleep(time.Second)
println("setting to 0°")
s.SetAngle(0)
time.Sleep(3 * time.Second)
println("setting to 45°")
s.SetAngle(45)
time.Sleep(3 * time.Second)
println("setting to 90°")
s.SetAngle(90)
time.Sleep(3 * time.Second)
println("setting to 135°")
s.SetAngle(135)
time.Sleep(3 * time.Second)
println("setting to 180°")
s.SetAngle(180)
time.Sleep(3 * time.Second)
}
}
+40 -12
View File
@@ -3,7 +3,9 @@ package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/uc8151"
)
@@ -14,30 +16,56 @@ func main() {
led = machine.LED
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.SPI0.Configure(machine.SPIConfig{
Frequency: 12000000,
Frequency: 12 * machine.MHz,
SCK: machine.EPD_SCK_PIN,
SDO: machine.EPD_SDO_PIN,
})
display = uc8151.New(machine.SPI0, machine.EPD_CS_PIN, machine.EPD_DC_PIN, machine.EPD_RESET_PIN, machine.EPD_BUSY_PIN)
display.Configure(uc8151.Config{
Rotation: uc8151.ROTATION_270,
Speed: uc8151.MEDIUM,
Blocking: true,
Rotation: drivers.Rotation270,
Speed: uc8151.TURBO,
FlickerFree: true,
Blocking: false,
})
black := color.RGBA{1, 1, 1, 255}
display.ClearBuffer()
display.Display()
for i := int16(0); i < 37; i++ {
for j := int16(0); j < 16; j++ {
if (i+j)%2 == 0 {
showRect(i*8, j*8, 8, 8, black)
display.ClearDisplay()
mod := int16(1)
for {
// checkerboard
for i := int16(0); i < 11; i++ {
if mod == 1 {
mod = 0
} else {
mod = 1
}
display.ClearBuffer()
for i := int16(0); i < 37; i++ {
for j := int16(0); j < 16; j++ {
if (i+j)%2 == mod {
showRect(i*8, j*8, 8, 8, black)
}
}
}
display.Display()
time.Sleep(500 * time.Millisecond)
}
// moving line
for i := int16(16); i < 21; i++ {
display.ClearBuffer()
for j := int16(0); j < 16; j++ {
if (i+j)%2 == 0 {
showRect(i*8, j*8, 8, 8, black)
}
display.Display()
time.Sleep(250 * time.Millisecond)
}
}
}
display.Display()
}
func showRect(x int16, y int16, w int16, h int16, c color.RGBA) {
+84
View File
@@ -0,0 +1,84 @@
package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/waveshare-epd/epd2in66b"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freemono"
)
var (
black = color.RGBA{0, 0, 0, 0xff}
white = color.RGBA{0xff, 0xff, 0xff, 0xff}
red = color.RGBA{0xff, 0, 0, 0xff}
)
func main() {
machine.Serial.Configure(machine.UARTConfig{})
time.Sleep(2 * time.Second)
machine.SPI1.Configure(machine.SPIConfig{
Frequency: epd2in66b.Baudrate,
})
println("started")
// in case you have a Pico module, you can directly use
// dev, err := epd2in66b.NewPicoModule()
display := epd2in66b.New(machine.SPI1)
cfg := epd2in66b.Config{
DataPin: machine.GP8,
ChipSelectPin: machine.GP9,
ResetPin: machine.GP12,
BusyPin: machine.GP13,
}
err := display.Configure(cfg)
if err != nil {
panic(err)
}
err = display.Reset()
if err != nil {
panic(err)
}
println("draw checkerboard")
drawCheckerBoard(&display)
println("draw 'hello'")
tinyfont.WriteLineRotated(&display, &freemono.Bold24pt7b, 40, 10, "Hello!", white, tinyfont.ROTATION_90)
tinyfont.WriteLineRotated(&display, &freemono.Bold12pt7b, 10, 10, "tinygo rocks", white, tinyfont.ROTATION_90)
err = display.Display()
if err != nil {
panic(err)
}
}
func drawCheckerBoard(display drivers.Displayer) {
s := 8
width, height := display.Size()
for x := 0; x <= int(width)-s; x += s {
for y := 0; y <= int(height)-s; y += s {
c := red
if (x/s)%2 == (y/s)%2 {
c = black
}
showRect(display, x, y, s, s, c)
}
}
}
func showRect(display drivers.Displayer, x int, y int, w int, h int, c color.RGBA) {
for i := x; i < x+w; i++ {
for j := y; j < y+h; j++ {
display.SetPixel(int16(i), int16(j), c)
}
}
}
+5 -4
View File
@@ -4,7 +4,8 @@ package main
import "machine"
// Replace neo and led in the code below to match the pin
// that you are using if different.
var neo = machine.D2
var led = machine.LED
func init() {
// Replace neo in the code below to match the pin
// that you are using if different.
neo = machine.D2
}
+6 -5
View File
@@ -4,8 +4,9 @@ package main
import "machine"
// This is the pin assignment for the Digispark only.
// Replace neo and led in the code below to match the pin
// that you are using if different.
var neo machine.Pin = 0
var led = machine.LED
func init() {
// This is the pin assignment for the Digispark only.
// Replace neo and led in the code below to match the pin
// that you are using if different.
neo = machine.Pin(0)
}
+4 -4
View File
@@ -12,11 +12,12 @@ import (
"tinygo.org/x/drivers/ws2812"
)
var leds [10]color.RGBA
var (
neo machine.Pin
leds [10]color.RGBA
)
func main() {
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
ws := ws2812.NewWS2812(neo)
@@ -35,7 +36,6 @@ func main() {
}
ws.WriteColors(leds[:])
led.Set(rg)
time.Sleep(100 * time.Millisecond)
}
}
+6 -5
View File
@@ -1,10 +1,11 @@
//go:build !digispark && !arduino && !qtpy && !m5stamp_c3 && !thingplus_rp2040
//go:build !digispark && !arduino
package main
import "machine"
// Replace neo and led in the code below to match the pin
// that you are using if different.
var neo machine.Pin = machine.WS2812
var led = machine.LED
func init() {
// Replace neo in the code below to match the pin
// that you are using if different.
neo = machine.WS2812
}
-10
View File
@@ -1,10 +0,0 @@
//go:build qtpy || m5stamp_c3
package main
import "machine"
// Replace neo and led in the code below to match the pin
// that you are using if different.
var neo machine.Pin = machine.WS2812
var led = machine.NoPin
-12
View File
@@ -1,12 +0,0 @@
//go:build thingplus_rp2040
package main
import "machine"
// This is the pin assignment for the internal neopixel of the
// Sparkfun thingplus rp2040.
// Replace neo and led in the code below to match the pin
// that you are using if different.
var neo machine.Pin = machine.GPIO8
var led = machine.LED
+2 -1
View File
@@ -80,7 +80,8 @@ func (d *Device) Read() []byte {
func (d *Device) ReadTouchPoint() touch.Point {
d.Read()
z := 0xFFFFF
if d.buf[0] == 0 {
switch d.buf[0] {
case 0, 255:
z = 0
}
+2
View File
@@ -2,6 +2,8 @@ module tinygo.org/x/drivers
go 1.18
replace tinygo.org/x/drivers/mcp9808 => /home/kasterby/Documents/drivers/mcp9808
require (
github.com/eclipse/paho.mqtt.golang v1.2.0
github.com/frankban/quicktest v1.10.2
+98
View File
@@ -0,0 +1,98 @@
// Package mcp9808 implements a driver for the MCP9808 High Accuracy I2C Temperature Sensor
//
// Datasheet: https://cdn-shop.adafruit.com/datasheets/MCP9808.pdf
// Module: https://www.adafruit.com/product/1782
// Only implemented: temperature reading, resolution read & set
package mcp9808
import (
"encoding/binary"
"errors"
"tinygo.org/x/drivers"
)
type Device struct {
bus drivers.I2C
Address uint16
}
func New(bus drivers.I2C) Device {
return Device{bus, MCP9808_I2CADDR_DEFAULT}
}
func (d *Device) Connected() bool {
data := make([]byte, 2)
d.Read(MCP9808_REG_DEVICE_ID, &data)
return binary.BigEndian.Uint16(data) == MCP9808_DEVICE_ID
}
func (d *Device) ReadTemperature() (float64, error) {
data := make([]byte, 2)
var temp float64
if err := d.Read(MCP9808_REG_AMBIENT_TEMP, &data); err != nil {
return 0, err
}
data[0] = data[0] & 0x1F
if data[0]&0x10 == 0x10 {
data[0] = data[0] & 0x0F
temp = float64(data[0])*16 + float64(data[1])/16.0 - 256
}
temp = float64(data[0])*16 + float64(data[1])/16.0
return temp, nil
}
func (d *Device) ReadResolution() (resolution, error) {
data := make([]byte, 2)
err := d.Read(MCP9808_REG_RESOLUTION, &data)
if err != nil {
return 0, err
}
switch data[0] {
case 0:
return Low, nil
case 1:
return Medium, nil
case 2:
return High, nil
case 3:
return Maximum, nil
default:
return 0, errors.New("unknown resolution")
}
}
func (d *Device) SetResolution(r resolution) error {
switch r {
case Low:
if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x00}); err != nil {
return err
}
case Medium:
if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x01}); err != nil {
return err
}
case High:
if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x02}); err != nil {
return err
}
case Maximum:
if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x03}); err != nil {
return err
}
default:
return nil
}
return nil
}
func (d *Device) Write(register byte, data []byte) error {
buf := append([]byte{register}, data...)
return d.bus.Tx(d.Address, buf, nil)
}
func (d *Device) Read(register byte, data *[]byte) error {
return d.bus.Tx(d.Address, []byte{register}, *data)
}
+53
View File
@@ -0,0 +1,53 @@
// Package mcp9808 implements a driver for the MCP9808 High Accuracy I2C Temperature Sensor
//
// Datasheet: https://cdn-shop.adafruit.com/datasheets/MCP9808.pdf
// Module: https://www.adafruit.com/product/1782
package mcp9808
// Constants/addresses used for I2C.
const (
MCP9808_DEVICE_ID = 0x0400
MCP9808_MANUF_ID = 0x0054
MCP9808_I2CADDR_DEFAULT = 0x18 //default I2C address
MCP9808_REG_CONFIG = 0x01 //MCP9808 config register
MCP9808_REG_CONFIG_SHUTDOWN = 0x0100 //shutdown config
MCP9808_REG_CONFIG_CRITLOCKED = 0x0080 //critical trip lock
MCP9808_REG_CONFIG_WINLOCKED = 0x0040 //alarm window lock
MCP9808_REG_CONFIG_INTCLR = 0x0020 //interrupt clear
MCP9808_REG_CONFIG_ALERTSTAT = 0x0010 //alert output status
MCP9808_REG_CONFIG_ALERTCTRL = 0x0008 //alert output control
MCP9808_REG_CONFIG_ALERTSEL = 0x0004 //alert output select
MCP9808_REG_CONFIG_ALERTPOL = 0x0002 //alert output polarity
MCP9808_REG_CONFIG_ALERTMODE = 0x0001 //alert output mode
MCP9808_REG_UPPER_TEMP = 0x02 //upper alert boundary
MCP9808_REG_LOWER_TEMP = 0x03 //lower alert boundery
MCP9808_REG_CRIT_TEMP = 0x04 //critical temperature
MCP9808_REG_AMBIENT_TEMP = 0x05 //ambient temperature
MCP9808_REG_MANUF_ID = 0x06 //manufacturer ID
MCP9808_REG_DEVICE_ID = 0x07 //device ID
MCP9808_REG_RESOLUTION = 0x08 //resolution
)
/*
======= ============ ==============
value resolution reading Time
======= ============ ==============
0 0.5°C 30 ms
1 0.25°C 65 ms
2 0.125°C 130 ms
3 0.0625°C 250 ms
======= ============ ==============
*/
type resolution uint8
const (
Low resolution = iota
Medium
High
Maximum
)
+1
View File
@@ -36,6 +36,7 @@ var (
ErrFamilyNotSupported = errors.New("Address family not supported")
ErrProtocolNotSupported = errors.New("Socket protocol/type not supported")
ErrStartingDHCPClient = errors.New("Error starting DHPC client")
ErrStartingDHCPServer = errors.New("Error starting DHPC server")
ErrNoMoreSockets = errors.New("No more sockets")
ErrClosingSocket = errors.New("Error closing socket")
ErrNotSupported = errors.New("Not supported")
+84
View File
@@ -0,0 +1,84 @@
// Package pcf8591 implements a driver for the PCF8591 Analog to Digital/Digital to Analog Converter.
//
// Datasheet: https://www.nxp.com/docs/en/data-sheet/PCF8591.pdf
package pcf8591 // import "tinygo.org/x/drivers/pcf8591"
import (
"machine"
"errors"
"tinygo.org/x/drivers"
)
// Device wraps PCF8591 ADC functions.
type Device struct {
bus drivers.I2C
Address uint16
CH0 ADCPin
CH1 ADCPin
CH2 ADCPin
CH3 ADCPin
}
// ADCPin is the implementation of the ADConverter interface.
type ADCPin struct {
machine.Pin
d *Device
}
// New returns a new PCF8591 driver. Pass in a fully configured I2C bus.
func New(b drivers.I2C) *Device {
d := &Device{
bus: b,
Address: defaultAddress,
}
// setup all channels
d.CH0 = d.GetADC(0)
d.CH1 = d.GetADC(1)
d.CH2 = d.GetADC(2)
d.CH3 = d.GetADC(3)
return d
}
// Configure here just for interface compatibility.
func (d *Device) Configure() {
}
// Read analog data from channel
func (d *Device) Read(ch int) (uint16, error) {
if ch < 0 || ch > 3 {
return 0, errors.New("invalid channel for pcf8591 Read")
}
return d.GetADC(ch).Get(), nil
}
// GetADC returns an ADC for a specific channel.
func (d *Device) GetADC(ch int) ADCPin {
return ADCPin{machine.Pin(ch), d}
}
// Get the current reading for a specific ADCPin.
func (p ADCPin) Get() uint16 {
// TODO: also implement DAC
tx := make([]byte, 2)
tx[0] = byte(p.Pin)
rx := make([]byte, 2)
// The result from the measurement triggered by the first write,
// however, the second write is required to get the result.
// See section 8.4 "A/D Conversion" in the datasheet for more info
p.d.bus.Tx(p.d.Address, tx, rx)
p.d.bus.Tx(p.d.Address, tx, rx)
// scale result to 16bit value like other ADCs
return uint16(rx[1] << 8)
}
// Configure here just for interface compatibility.
func (p ADCPin) Configure() {
}
+7
View File
@@ -0,0 +1,7 @@
package pcf8591
// PCF8591 Default Address
const defaultAddress = 0x48
// control bit for DAC
const PCF8591_ENABLE_DAC = 0x40
+49 -8
View File
@@ -22,12 +22,13 @@ func NewImage[T Color](width, height int) Image[T] {
}
var zeroColor T
var data unsafe.Pointer
if zeroColor.BitsPerPixel()%8 == 0 {
switch {
case 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 {
default:
// 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()
@@ -80,10 +81,11 @@ func (img Image[T]) Len() int {
func (img Image[T]) RawBuffer() []uint8 {
var zeroColor T
var numBytes int
if zeroColor.BitsPerPixel()%8 == 0 {
switch {
case zeroColor.BitsPerPixel()%8 == 0:
// Each color starts at a whole byte offset.
numBytes = int(unsafe.Sizeof(zeroColor)) * int(img.width) * int(img.height)
} else {
default:
// 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)
@@ -99,7 +101,20 @@ func (img Image[T]) Size() (int, int) {
func (img Image[T]) setPixel(index int, c T) {
var zeroColor T
if zeroColor.BitsPerPixel()%8 == 0 {
switch {
case zeroColor.BitsPerPixel() == 1:
// Monochrome.
x := index % int(img.width)
y := index / int(img.width)
offset := x + (y/8)*int(img.width)
ptr := (*byte)(unsafe.Add(img.data, offset))
if c != zeroColor {
*((*byte)(ptr)) |= 1 << uint8(y%8)
} else {
*((*byte)(ptr)) &^= 1 << uint8(y%8)
}
return
case zeroColor.BitsPerPixel()%8 == 0:
// Each color starts at a whole byte offset.
// This is the easy case.
offset := index * int(unsafe.Sizeof(zeroColor))
@@ -147,7 +162,15 @@ func (img Image[T]) Get(x, y int) T {
var zeroColor T
index := y*int(img.width) + x // index into img.data
if zeroColor.BitsPerPixel()%8 == 0 {
switch {
case zeroColor.BitsPerPixel() == 1:
// Monochrome.
var c Monochrome
offset := x + (y/8)*int(img.width)
ptr := (*byte)(unsafe.Add(img.data, offset))
c = (*ptr >> uint8(y%8) & 0x1) == 1
return any(c).(T)
case zeroColor.BitsPerPixel()%8 == 0:
// Colors like RGB565, RGB888, etc.
offset := index * int(unsafe.Sizeof(zeroColor))
ptr := unsafe.Add(img.data, offset)
@@ -181,8 +204,26 @@ func (img Image[T]) Get(x, y int) T {
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 {
switch {
case zeroColor.BitsPerPixel() == 1:
// Monochrome.
var colorByte uint8
if color != zeroColor {
colorByte = 0xff
}
numBytes := int(img.width) * int(img.height) / 8
for i := 0; i < numBytes; 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.
ptr := (*byte)(unsafe.Add(img.data, i))
*((*byte)(ptr)) = colorByte
}
return
case zeroColor.BitsPerPixel()%8 == 0:
// Fast pass for colors of 8, 16, 24, etc bytes in size.
ptr := img.data
for i := 0; i < img.Len(); i++ {
// TODO: this can be optimized a lot.
+100
View File
@@ -1,7 +1,9 @@
package pixel_test
import (
goimage "image"
"image/color"
"math/rand"
"testing"
"tinygo.org/x/drivers/pixel"
@@ -62,3 +64,101 @@ func TestImageRGB444BE(t *testing.T) {
}
}
}
func TestImageMonochrome(t *testing.T) {
image := pixel.NewImage[pixel.Monochrome](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 _, expected := range []color.RGBA{
{R: 0xff, G: 0xff, B: 0xff},
{G: 0xff},
{R: 0xff, G: 0xff},
{G: 0xff, B: 0xff},
{R: 0x00},
{G: 0x00, A: 0xff},
{B: 0x00, A: 0xff},
} {
encoded := pixel.NewColor[pixel.Monochrome](expected.R, expected.G, expected.B)
image.Set(4, 2, encoded)
actual := image.Get(4, 2).RGBA()
switch {
case expected.R == 0 && expected.G == 0 && expected.B == 0:
// should be false eg black
if actual.R != 0 || actual.G != 0 || actual.B != 0 {
t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual)
}
case int(expected.R)+int(expected.G)+int(expected.B) > 128*3:
// should be true eg white
if actual.R == 0 || actual.G == 0 || actual.B == 0 {
t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual)
}
}
}
}
// Test pixel formats by filling them with noise and checking whether they
// contain the same data afterwards.
func TestImageNoise(t *testing.T) {
t.Run("RGB888", func(t *testing.T) {
testImageNoise[pixel.RGB888](t)
})
t.Run("RGB565BE", func(t *testing.T) {
testImageNoise[pixel.RGB565BE](t)
})
t.Run("RGB555", func(t *testing.T) {
testImageNoise[pixel.RGB555](t)
})
t.Run("RGB444BE", func(t *testing.T) {
testImageNoise[pixel.RGB444BE](t)
})
t.Run("Monochrome", func(t *testing.T) {
testImageNoise[pixel.Monochrome](t)
})
}
func testImageNoise[T pixel.Color](t *testing.T) {
// Create an image of a random width/height for extra testing.
width := rand.Int()%500 + 10
height := rand.Int()%500 + 10
t.Log("image size:", width, height)
// Create two images: the to-be-tested image object and a reference image.
img := pixel.NewImage[T](width, height)
ref := goimage.NewRGBA(goimage.Rect(0, 0, width, height))
// Fill the two images with noise.
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
// Set a random color in both images.
c := pixel.NewColor[T](uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32()))
img.Set(x, y, c)
ref.Set(x, y, c.RGBA())
}
}
// Compare the two images. They should match.
mismatch := 0
firstX := 0
firstY := 0
var firstExpected, firstActual color.RGBA
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
c := img.Get(x, y).RGBA()
r2, g2, b2, _ := ref.At(x, y).RGBA()
c2 := color.RGBA{R: uint8(r2 >> 8), G: uint8(g2 >> 8), B: uint8(b2 >> 8), A: 255}
if c != c2 {
mismatch++
if mismatch == 1 {
firstX = x
firstY = y
firstExpected = c
firstActual = c2
}
}
}
}
if mismatch != 0 {
t.Errorf("mismatch found: %d pixels are different (first diff at (%d, %d), expected %v, actual %v)", mismatch, firstX, firstY, firstExpected, firstActual)
}
}
+33 -2
View File
@@ -14,9 +14,9 @@ import (
// 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.
// The color format is sRGB (or close to it) in all cases except for 1-bit.
type Color interface {
RGB888 | RGB565BE | RGB555 | RGB444BE
RGB888 | RGB565BE | RGB555 | RGB444BE | Monochrome
BaseColor
}
@@ -50,6 +50,8 @@ func NewColor[T Color](r, g, b uint8) T {
return any(NewRGB555(r, g, b)).(T)
case RGB444BE:
return any(NewRGB444BE(r, g, b)).(T)
case Monochrome:
return any(NewMonochrome(r, g, b)).(T)
default:
panic("unknown color format")
}
@@ -202,6 +204,35 @@ func (c RGB444BE) RGBA() color.RGBA {
return color
}
type Monochrome bool
func NewMonochrome(r, g, b uint8) Monochrome {
// Very simple black/white split.
// This isn't very accurate (especially for sRGB colors) but is close enough.
if int(r)+int(g)+int(b) > 128*3 { // light, convert to white
return Monochrome(true)
}
// dark, convert to black
return Monochrome(false)
}
func (c Monochrome) BitsPerPixel() int {
return 1
}
func (c Monochrome) RGBA() color.RGBA {
value := uint8(0)
if c {
value = 255
}
return color.RGBA{
R: value,
G: value,
B: value,
A: 255,
}
}
// Gamma brightness lookup table:
// https://victornpb.github.io/gamma-table-generator
// gamma = 0.45 steps = 256 range = 0-255
+66 -7
View File
@@ -32,8 +32,9 @@ var (
)
const (
O_NONBLOCK = 1 // note: different value than syscall.O_NONBLOCK (0x800)
RTW_MODE_STA = 0x00000001
O_NONBLOCK = 1 // note: different value than syscall.O_NONBLOCK (0x800)
RTW_MODE_STA = 0x00000001
defaultChannel = 6
)
type sock int32
@@ -135,6 +136,51 @@ func (r *rtl8720dn) connectToAP() error {
return r.startDhcpc()
}
func (r *rtl8720dn) startDhcps() error {
if result := r.rpc_tcpip_adapter_dhcps_start(0); result == -1 {
return netdev.ErrStartingDHCPServer
}
return nil
}
func (r *rtl8720dn) startAP() error {
if len(r.params.Ssid) == 0 {
return netlink.ErrMissingSSID
}
if len(r.params.Passphrase) != 0 && len(r.params.Passphrase) < 8 {
return netlink.ErrShortPassphrase
}
if debugging(debugBasic) {
fmt.Printf("Starting Wifi AP as SSID '%s'...", r.params.Ssid)
}
// Start the connection process
securityType := uint32(0) // RTW_SECURITY_OPEN
if len(r.params.Passphrase) != 0 {
securityType = 0x00400004 // RTW_SECURITY_WPA2_AES_PSK
}
result := r.rpc_wifi_start_ap(r.params.Ssid, r.params.Passphrase, securityType, defaultChannel)
if result != 0 {
if debugging(debugBasic) {
fmt.Printf("FAILED\r\n")
}
return netlink.ErrConnectFailed
}
if debugging(debugBasic) {
fmt.Printf("LISTENING\r\n")
}
if r.notifyCb != nil {
r.notifyCb(netlink.EventNetUp)
}
return r.startDhcps()
}
func (r *rtl8720dn) showDriver() {
if r.driverShown {
return
@@ -246,14 +292,27 @@ func (r *rtl8720dn) netConnect(reset bool) error {
}
r.showDevice()
retry:
for i := 0; r.params.Retries == 0 || i < r.params.Retries; i++ {
if err := r.connectToAP(); err != nil {
if err == netlink.ErrConnectFailed {
continue
switch r.params.ConnectMode {
case netlink.ConnectModeAP:
if err := r.startAP(); err != nil {
if err == netlink.ErrConnectFailed {
continue
}
return err
}
return err
break retry
default:
if err := r.connectToAP(); err != nil {
if err == netlink.ErrConnectFailed {
continue
}
return err
}
break retry
}
break
}
if r.networkDown() {
+23 -1
View File
@@ -1,6 +1,12 @@
package servo
import "machine"
import (
"machine"
"errors"
)
var ErrInvalidAngle = errors.New("servo: invalid angle")
// PWM is the interface necessary for controlling typical servo motors.
type PWM interface {
@@ -80,3 +86,19 @@ func (s Servo) SetMicroseconds(microseconds int16) {
value := uint64(s.pwm.Top()) * uint64(microseconds) / (pwmPeriod / 1000)
s.pwm.Set(s.channel, uint32(value))
}
// SetAngle sets the angle of the servo in degrees. The angle should be between
// 0 and 180, where 0 is the minimum angle and 180 is the maximum angle.
// This function should work for most servos, but if it doesn't work for yours
// you can use SetMicroseconds directly instead.
func (s Servo) SetAngle(angle int) error {
if angle < 0 || angle > 180 {
return ErrInvalidAngle
}
// 0° is 1000µs, 180° is 2000µs. See explanation in SetMicroseconds.
microseconds := angle*1000/180 + 1000
s.SetMicroseconds(int16(microseconds))
return nil
}
+4
View File
@@ -80,6 +80,7 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl6
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13/main.go
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13x/main.go
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd4in2/main.go
tinygo build -size short -o ./build/test.hex -target=pico ./examples/waveshare-epd/epd2in66b/main.go
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812
tinygo build -size short -o ./build/test.bin -target=m5stamp-c3 ./examples/ws2812
tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go
@@ -98,6 +99,7 @@ tinygo build -size short -o ./build/test.hex -target=hifive1b ./examples/ssd1351
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/lis2mdl/main.go
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/max72xx/main.go
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/dht/main.go
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/dht20/main.go
# tinygo build -size short -o ./build/test.hex -target=arduino ./examples/keypad4x4/main.go
tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8523/
tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/alarm/
@@ -105,6 +107,7 @@ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/clk
tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/time/
tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/timer/
tinygo build -size short -o ./build/test.hex -target=pico ./examples/qmi8658c/main.go
tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8591/
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina260/main.go
tinygo build -size short -o ./build/test.hex -target=nucleo-l432kc ./examples/aht20/main.go
tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/sdcard/console/
@@ -130,6 +133,7 @@ tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/n
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
tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/encoders/quadrature-interrupt
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mcp9808/main.go
# network examples (espat)
tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/
# network examples (wifinina)
+47 -2
View File
@@ -11,6 +11,13 @@ import (
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/pixel"
)
var (
errBufferSize = errors.New("invalid size buffer")
errOutOfRange = errors.New("out of screen range")
errNotImplemented = errors.New("not implemented")
)
type ResetValue [2]byte
@@ -245,8 +252,7 @@ func (d *Device) GetPixel(x int16, y int16) bool {
// SetBuffer changes the whole buffer at once
func (d *Device) SetBuffer(buffer []byte) error {
if int16(len(buffer)) != d.bufferSize {
//return ErrBuffer
return errors.New("wrong size buffer")
return errBufferSize
}
for i := int16(0); i < d.bufferSize; i++ {
d.buffer[i] = buffer[i]
@@ -338,3 +344,42 @@ func (b *SPIBus) tx(data []byte, isCommand bool) error {
func (d *Device) Size() (w, h int16) {
return d.width, d.height
}
// DrawBitmap copies the bitmap to the screen at the given coordinates.
func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) error {
width, height := bitmap.Size()
if x < 0 || x+int16(width) > d.width || y < 0 || y+int16(height) > d.height {
return errOutOfRange
}
for i := 0; i < width; i++ {
for j := 0; j < height; j++ {
d.SetPixel(x+int16(i), y+int16(j), bitmap.Get(i, j).RGBA())
}
}
return nil
}
// Rotation returns the currently configured rotation.
func (d *Device) Rotation() drivers.Rotation {
return drivers.Rotation0
}
// SetRotation changes the rotation of the device (clock-wise).
// Would have to be implemented in software for this device.
func (d *Device) SetRotation(rotation drivers.Rotation) error {
return errNotImplemented
}
// Set the sleep mode for this display. When sleeping, the panel uses a lot
// less power. The display won't show an image anymore, but the memory contents
// should be kept.
func (d *Device) Sleep(sleepEnabled bool) error {
if sleepEnabled {
d.Command(DISPLAYOFF)
} else {
d.Command(DISPLAYON)
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package uc8151
// LUTType is the look-up table for the display
type LUTType [42]uint8
type LUTSet struct {
VCOM LUTType
WW LUTType
BW LUTType
WB LUTType
BB LUTType
}
func (lut *LUTType) Clear() {
for i := range lut {
lut[i] = 0
}
}
func (lut *LUTType) SetRow(row int, pat uint8, dur [4]uint8, rep uint8) error {
index := row * 6
lut[index] = pat
lut[index+1] = dur[0]
lut[index+2] = dur[1]
lut[index+3] = dur[2]
lut[index+4] = dur[3]
lut[index+5] = rep
return nil
}
+12 -7
View File
@@ -1,5 +1,7 @@
package uc8151
import "tinygo.org/x/drivers"
// Registers
const (
// Display resolution
@@ -141,13 +143,16 @@ const (
HZ_100 = 0b00111010
HZ_200 = 0b00111001
NO_ROTATION Rotation = 0
ROTATION_90 Rotation = 1 // 90 degrees clock-wise rotation
ROTATION_180 Rotation = 2
ROTATION_270 Rotation = 3
// deprecated constants, just here for backward compatibility.
NO_ROTATION = drivers.Rotation0
ROTATION_90 = drivers.Rotation90
ROTATION_180 = drivers.Rotation180
ROTATION_270 = drivers.Rotation270
DEFAULT Speed = 0
MEDIUM Speed = 1
FAST Speed = 2
TURBO Speed = 3
SLOW Speed = 1
MEDIUM Speed = 2
FAST Speed = 3
FASTER Speed = 4
TURBO Speed = 5
)
+196 -349
View File
@@ -1,6 +1,7 @@
// Package uc8151 implements a driver for e-ink displays controlled by UC8151
//
// Inspired by https://github.com/pimoroni/pimoroni-pico/blob/main/drivers/uc8151/uc8151.cpp
// Additional inspiration from https://github.com/antirez/uc8151_micropython
// Datasheet: https://www.buydisplay.com/download/ic/UC8151C.pdf
package uc8151 // import "tinygo.org/x/drivers/uc8151"
@@ -11,35 +12,43 @@ import (
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/pixel"
)
var (
errOutOfRange = errors.New("out of screen range")
)
type Config struct {
Width int16
Height int16
Rotation Rotation // Rotation is clock-wise
Speed Speed // Value from DEFAULT, MEDIUM, FAST, TURBO
Blocking bool
Width int16
Height int16
Rotation drivers.Rotation // Rotation is clock-wise
Speed Speed // Value from DEFAULT, SLOW, MEDIUM, FAST, FASTER, TURBO
Blocking bool // block on calls to display or return immediately
FlickerFree bool // if we should avoid flickering
UpdateAfter int // if we are using flicker-free mode, how often we should update the screen
}
type Device struct {
bus drivers.SPI
cs machine.Pin
dc machine.Pin
rst machine.Pin
busy machine.Pin
width int16
height int16
buffer []uint8
bufferLength uint32
rotation Rotation
speed Speed
blocking bool
bus drivers.SPI
cs machine.Pin
dc machine.Pin
rst machine.Pin
busy machine.Pin
width int16
height int16
buffer []uint8
bufferLength uint32
rotation drivers.Rotation
speed Speed
blocking bool
flickerFree bool
updateCount, updateAfter int
}
type Rotation uint8
type Speed uint8
// New returns a new epd2in13x driver. Pass in a fully configured SPI bus.
// New returns a new uc8151 driver. Pass in a fully configured SPI bus.
func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device {
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
@@ -69,6 +78,8 @@ func (d *Device) Configure(cfg Config) {
d.rotation = cfg.Rotation
d.speed = cfg.Speed
d.blocking = cfg.Blocking
d.flickerFree = cfg.FlickerFree
d.updateAfter = cfg.UpdateAfter
d.bufferLength = (uint32(d.width) * uint32(d.height)) / 8
d.buffer = make([]uint8, d.bufferLength)
for i := uint32(0); i < d.bufferLength; i++ {
@@ -84,14 +95,14 @@ func (d *Device) Configure(cfg Config) {
d.SendData(RES_128x296 | LUT_REG | FORMAT_BW | SHIFT_RIGHT | BOOSTER_ON | RESET_NONE | SCAN_UP)
}
d.SetLUT(d.speed)
d.SetLUT(d.speed, d.flickerFree)
d.SendCommand(PWR)
d.SendData(VDS_INTERNAL | VDG_INTERNAL)
d.SendData(VCOM_VG | VGHL_16V)
d.SendData(0x2B)
d.SendData(0x2B)
d.SendData(0x2B)
d.SendData(VCOM_VD | VGHL_16V)
d.SendData(0b100110) // +10v VDH
d.SendData(0b100110) // -10v VDL
d.SendData(0b000011) // VDHR default (For red pixels, not used here)
d.SendCommand(PON)
d.WaitUntilIdle()
@@ -102,7 +113,7 @@ func (d *Device) Configure(cfg Config) {
d.SendData(START_10MS | STRENGTH_3 | OFF_6_58US)
d.SendCommand(PFS)
d.SendData(FRAMES_1)
d.SendData(FRAMES_4)
d.SendCommand(TSE)
d.SendData(TEMP_INTERNAL | OFFSET_0)
@@ -111,14 +122,13 @@ func (d *Device) Configure(cfg Config) {
d.SendData(0x22)
d.SendCommand(CDI)
d.SendData(0x4C) // 4C //5C
d.SendData(0b11_00_1100)
d.SendCommand(PLL)
d.SendData(HZ_100)
d.SendCommand(POF)
d.WaitUntilIdle()
}
// Reset resets the device
@@ -135,31 +145,30 @@ func (d *Device) PowerOff() {
d.SendCommand(POF)
}
// PowerOn power on the device
func (d *Device) PowerOn() {
d.SendCommand(PON)
}
// SendCommand sends a command to the display
func (d *Device) SendCommand(command uint8) {
d.sendDataCommand(true, command)
d.dc.Low()
d.cs.Low()
d.bus.Transfer(command)
d.cs.High()
}
// SendData sends a data byte to the display
func (d *Device) SendData(data uint8) {
d.sendDataCommand(false, data)
}
// sendDataCommand sends image data or a command to the screen
func (d *Device) sendDataCommand(isCommand bool, data uint8) {
if isCommand {
d.dc.Low()
} else {
d.dc.High()
}
func (d *Device) SendData(data ...uint8) {
d.dc.High()
d.cs.Low()
d.bus.Transfer(data)
d.bus.Tx(data, nil)
d.cs.High()
}
// 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)
// We use RGBA(0, 0, 0) as white (transparent)
// Anything else as black
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
x, y = d.xy(x, y)
@@ -168,31 +177,60 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
return
}
byteIndex := x/8 + y*(d.width/8)
if c.R == 0 && c.G == 0 && c.B == 0 { // TRANSPARENT / WHITE
d.buffer[byteIndex] &^= 0x80 >> uint8(x%8)
} else { // WHITE / EMPTY
if c.R != 0 || c.G != 0 || c.B != 0 {
d.buffer[byteIndex] |= 0x80 >> uint8(x%8)
} else {
d.buffer[byteIndex] &^= 0x80 >> uint8(x%8)
}
}
// DrawBitmap copies the bitmap to the screen at the given coordinates.
func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) error {
dw, dh := d.Size()
bw, bh := bitmap.Size()
if x < 0 || x+int16(bw) > dw || y < 0 || y+int16(bh) > dh {
return errOutOfRange
}
for i := 0; i < bw; i++ {
for j := 0; j < bh; j++ {
d.SetPixel(x+int16(i), y+int16(j), bitmap.Get(i, j).RGBA())
}
}
return nil
}
// Display sends the buffer to the screen.
func (d *Device) Display() error {
if d.blocking {
d.WaitUntilIdle()
}
d.SendCommand(PON)
if d.flickerFree && d.updateAfter != 0 && d.updateCount%d.updateAfter == 0 {
// we need full refresh here
d.SetLUT(MEDIUM, false)
} else {
d.SetLUT(d.speed, d.flickerFree)
}
d.updateCount++
d.PowerOn()
d.SendCommand(PTOU)
d.SendCommand(DTM2)
for i := uint32(0); i < d.bufferLength; i++ {
d.SendData(d.buffer[i])
}
d.SendData(d.buffer...)
d.SendCommand(DSP)
d.SendCommand(DRF)
d.SetLUT(d.speed, d.flickerFree)
if d.blocking {
d.WaitUntilIdle()
d.PowerOff()
}
return nil
}
@@ -208,13 +246,14 @@ func (d *Device) DisplayRect(x int16, y int16, width int16, height int16) error
if x < 0 || y < 0 || x >= d.width || y >= d.height || width < 0 || height < 0 {
return errors.New("wrong rectangle")
}
if d.rotation == ROTATION_90 {
switch d.rotation {
case drivers.Rotation0:
width, height = height, width
x -= width
} else if d.rotation == ROTATION_180 {
case drivers.Rotation90:
x -= width - 1
y -= height - 1
} else if d.rotation == ROTATION_270 {
case drivers.Rotation180:
width, height = height, width
y -= height
}
@@ -262,6 +301,12 @@ func (d *Device) DisplayRect(x int16, y int16, width int16, height int16) error
// ClearDisplay erases the device SRAM
func (d *Device) ClearDisplay() {
ff := d.flickerFree
d.flickerFree = false
defer func() {
d.flickerFree = ff
}()
d.ClearBuffer()
d.Display()
}
@@ -269,7 +314,7 @@ func (d *Device) ClearDisplay() {
// WaitUntilIdle waits until the display is ready
func (d *Device) WaitUntilIdle() {
for !d.busy.Get() {
time.Sleep(100 * time.Millisecond)
time.Sleep(10 * time.Millisecond)
}
}
@@ -287,15 +332,32 @@ 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.width
}
return d.width, d.height
}
// Rotation returns the currently configured rotation.
func (d *Device) Rotation() drivers.Rotation {
return d.rotation
}
// SetRotation changes the rotation (clock-wise) of the device
func (d *Device) SetRotation(rotation Rotation) {
func (d *Device) SetRotation(rotation drivers.Rotation) error {
d.rotation = rotation
return nil
}
// Set the sleep mode for this display.
func (d *Device) Sleep(sleepEnabled bool) error {
if sleepEnabled {
d.PowerOff()
return nil
}
d.PowerOn()
return nil
}
// SetBlocking changes the blocking flag of the device
@@ -303,16 +365,16 @@ func (d *Device) SetBlocking(blocking bool) {
d.blocking = blocking
}
// xy chages the coordinates according to the rotation
// xy changes 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
@@ -338,296 +400,81 @@ func (d *Device) Invert(invert bool) {
}
}
// SetLUT sets the look up tables for full or partial updates
func (d *Device) SetLUT(speed Speed) {
switch speed {
case MEDIUM:
var lut = [44]uint8{
0x00, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x00, 0x23, 0x23, 0x00, 0x00, 0x02,
0x00, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
}
d.SendCommand(LUT_VCOM)
for i := 0; i < 44; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0x54, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x60, 0x23, 0x23, 0x00, 0x00, 0x02,
0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_WW)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
// SetLUT sets the look up tables for full or partial updates based on
// the speed and flicker-free mode.
// Based on code from https://github.com/antirez/uc8151_micropython
func (d *Device) SetLUT(speed Speed, flickerFree bool) error {
var lut LUTSet
lut = [44]uint8{
0x54, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x60, 0x23, 0x23, 0x00, 0x00, 0x02,
0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_BW)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x60, 0x23, 0x23, 0x00, 0x00, 0x02,
0x54, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_WB)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x60, 0x23, 0x23, 0x00, 0x00, 0x02,
0x54, 0x16, 0x16, 0x0d, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_BB)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
break
case FAST:
var lut = [44]uint8{
0x00, 0x04, 0x04, 0x07, 0x00, 0x01,
0x00, 0x0c, 0x0c, 0x00, 0x00, 0x02,
0x00, 0x04, 0x04, 0x07, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
}
d.SendCommand(LUT_VCOM)
for i := 0; i < 44; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0x54, 0x04, 0x04, 0x07, 0x00, 0x01,
0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02,
0xa8, 0x04, 0x04, 0x07, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_WW)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0x54, 0x04, 0x04, 0x07, 0x00, 0x01,
0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02,
0xa8, 0x04, 0x04, 0x07, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_BW)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0xa8, 0x04, 0x04, 0x07, 0x00, 0x01,
0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02,
0x54, 0x04, 0x04, 0x07, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_WB)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0xa8, 0x04, 0x04, 0x07, 0x00, 0x01,
0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02,
0x54, 0x04, 0x04, 0x07, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_BB)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
break
case TURBO:
var lut = [44]uint8{
0x00, 0x01, 0x01, 0x02, 0x00, 0x01,
0x00, 0x02, 0x02, 0x00, 0x00, 0x02,
0x00, 0x02, 0x02, 0x03, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
}
d.SendCommand(LUT_VCOM)
for i := 0; i < 44; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0x54, 0x01, 0x01, 0x02, 0x00, 0x01,
0x60, 0x02, 0x02, 0x00, 0x00, 0x02,
0xa8, 0x02, 0x02, 0x03, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_WW)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0x54, 0x01, 0x01, 0x02, 0x00, 0x01,
0x60, 0x02, 0x02, 0x00, 0x00, 0x02,
0xa8, 0x02, 0x02, 0x03, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_BW)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0xa8, 0x01, 0x01, 0x02, 0x00, 0x01,
0x60, 0x02, 0x02, 0x00, 0x00, 0x02,
0x54, 0x02, 0x02, 0x03, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_WB)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0xa8, 0x01, 0x01, 0x02, 0x00, 0x01,
0x60, 0x02, 0x02, 0x00, 0x00, 0x02,
0x54, 0x02, 0x02, 0x03, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_BB)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
break
default:
var lut = [44]uint8{
0x00, 0x64, 0x64, 0x37, 0x00, 0x01,
0x00, 0x8c, 0x8c, 0x00, 0x00, 0x04,
0x00, 0x64, 0x64, 0x37, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
}
d.SendCommand(LUT_VCOM)
for i := 0; i < 44; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0x54, 0x64, 0x64, 0x37, 0x00, 0x01,
0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04,
0xa8, 0x64, 0x64, 0x37, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_WW)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0x54, 0x64, 0x64, 0x37, 0x00, 0x01,
0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04,
0xa8, 0x64, 0x64, 0x37, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_BW)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0xa8, 0x64, 0x64, 0x37, 0x00, 0x01,
0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04,
0x54, 0x64, 0x64, 0x37, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_WB)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
lut = [44]uint8{
0xa8, 0x64, 0x64, 0x37, 0x00, 0x01,
0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04,
0x54, 0x64, 0x64, 0x37, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
d.SendCommand(LUT_BB)
// do not send last two bytes
for i := 0; i < 42; i++ {
d.SendData(lut[i])
}
break
// Num. of frames for single direction change.
period := 64
p := uint8(period / (2 ^ (int(speed) - 1)))
if p < 1 {
p = 1
}
// Num. of frames for back-and-forth change.
hperiod := period % 2
hp := uint8(hperiod / (2 ^ (int(speed) - 1)))
if hp < 1 {
hp = 1
}
if speed < FAST && !flickerFree {
// For low speed everything is charge-neutral, even WB/BW.
// Phase 1: long go-inverted-color.
lut.VCOM.SetRow(0, 0x00, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02)
lut.BW.SetRow(0, 0b01_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02)
lut.WB.SetRow(0, 0b10_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02)
// Phase 2: short ping/pong.
lut.VCOM.SetRow(1, 0x00, [4]uint8{hp, hp, 0x00, 0x00}, 0x02)
lut.BW.SetRow(1, 0b10_01_0000, [4]uint8{hp, hp, 0x00, 0x00}, 0x01)
lut.WB.SetRow(1, 0b01_10_0000, [4]uint8{hp, hp, 0x00, 0x00}, 0x01)
// Phase 3: long go-target-color.
lut.VCOM.SetRow(2, 0x00, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02)
lut.BW.SetRow(2, 0b10_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02)
lut.WB.SetRow(2, 0b01_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02)
// For this speed, we use the same LUTs for WW/BB as well.
copy(lut.WW[:], lut.BW[:])
copy(lut.BB[:], lut.WB[:])
} else {
// Speed >= FAST
// For greater than 3 we use non charge-neutral LUTs for WB/BW
// since the inpulse is short and it gets reversed when the
// pixel changes color, so that's not a problem for the display,
// however we still need to use charge-neutral LUTs for WW/BB.
lut.VCOM.SetRow(0, 0x00, [4]uint8{p, p, p, p}, 0x01)
lut.BW.SetRow(0, 0b10_00_00_00, [4]uint8{p * 4, 0x00, 0x00, 0x00}, 0x01)
lut.WB.SetRow(0, 0b01_00_00_00, [4]uint8{p * 4, 0x00, 0x00, 0x00}, 0x01)
lut.WW.SetRow(0, 0b01_10_00_00, [4]uint8{p * 2, p * 2, 0x00, 0x00}, 0x01)
lut.BB.SetRow(0, 0b10_01_00_00, [4]uint8{p * 2, p * 2, 0x00, 0x00}, 0x01)
}
if flickerFree {
// If no flickering mode is enabled, we use an empty
// waveform BB and WW. The screen will need to be periodically fully refreshed.
lut.WW.Clear()
lut.BB.Clear()
}
d.SendCommand(LUT_VCOM)
d.SendData(append(lut.VCOM[:], []uint8{0, 0}...)...)
d.SendCommand(LUT_BW)
d.SendData(lut.BW[:]...)
d.SendCommand(LUT_WB)
d.SendData(lut.WB[:]...)
d.SendCommand(LUT_WW)
d.SendData(lut.WW[:]...)
d.SendCommand(LUT_BB)
d.SendData(lut.BB[:]...)
return nil
}
+1 -1
View File
@@ -2,4 +2,4 @@ package drivers
// Version returns a user-readable string showing the version of the drivers package for support purposes.
// Update this value before release of new version of software.
const Version = "0.27.0"
const Version = "0.28.0"
+287
View File
@@ -0,0 +1,287 @@
// Package epd2in66b implements a driver for the Waveshare 2.66inch E-Paper E-Ink Display Module (B)
// for Raspberry Pi Pico, 296×152, Red / Black / White
// Datasheet: https://files.waveshare.com/upload/e/ec/2.66inch-e-paper-b-specification.pdf
package epd2in66b
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers"
)
const (
displayWidth = 152
displayHeight = 296
)
const Baudrate = 4_000_000 // 4 MHz
type Config struct {
ResetPin machine.Pin
DataPin machine.Pin
ChipSelectPin machine.Pin
BusyPin machine.Pin
}
type Device struct {
bus drivers.SPI
cs machine.Pin
dc machine.Pin
rst machine.Pin
busy machine.Pin
blackBuffer []byte
redBuffer []byte
}
// New allocates a new device.
// The bus is expected to be configured and ready for use.
func New(bus drivers.SPI) Device {
pixelCount := displayWidth * displayHeight
bufLen := pixelCount / 8
return Device{
bus: bus,
blackBuffer: make([]byte, bufLen),
redBuffer: make([]byte, bufLen),
}
}
// Configure configures the device and its pins.
func (d *Device) Configure(c Config) error {
d.cs = c.ChipSelectPin
d.dc = c.DataPin
d.rst = c.ResetPin
d.busy = c.BusyPin
d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.busy.Configure(machine.PinConfig{Mode: machine.PinInput})
return nil
}
func (d *Device) Size() (x, y int16) {
return displayWidth, displayHeight
}
// SetPixel modifies the internal buffer in a single pixel.
// The display has 3 colors: red, black and white
//
// - white = RGBA(255,255,255, 1-255)
// - red = RGBA(1-255,0,0,1-255)
// - Anything else as black
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
if x < 0 || x >= displayWidth || y < 0 || y >= displayHeight {
return
}
bytePos, bitPos := pos(x, y, displayWidth)
if c.R == 0xff && c.G == 0xff && c.B == 0xff && c.A > 0 { // white
set(d.blackBuffer, bytePos, bitPos)
unset(d.redBuffer, bytePos, bitPos)
} else if c.R != 0 && c.G == 0 && c.B == 0 && c.A > 0 { // red-ish
set(d.blackBuffer, bytePos, bitPos)
set(d.redBuffer, bytePos, bitPos)
} else { // black or other
unset(d.blackBuffer, bytePos, bitPos)
unset(d.redBuffer, bytePos, bitPos)
}
}
func set(buf []byte, bytePos, bitPos int) {
buf[bytePos] |= 0x1 << bitPos
}
func unset(buf []byte, bytePos, bitPos int) {
buf[bytePos] &^= 0x1 << bitPos
}
func pos(x, y, stride int16) (bytePos int, bitPos int) {
p := int(x) + int(y)*int(stride)
bytePos = p / 8
// reverse bit position as it is reversed on the device's buffer
bitPos = 7 - p%8
return bytePos, bitPos
}
func (d *Device) Display() error {
// Write RAM (Black White) / RAM 0x24
// 1 == white, 0 == black
if err := d.sendCommandByte(0x24); err != nil {
return err
}
if err := d.sendData(d.blackBuffer); err != nil {
return err
}
// Write RAM (RED) / RAM 0x26)
// 0 == blank, 1 == red
if err := d.sendCommandByte(0x26); err != nil {
return err
}
if err := d.sendData(d.redBuffer); err != nil {
return err
}
return d.turnOnDisplay()
}
func (d *Device) ClearBuffer() {
fill(d.redBuffer, 0x00)
fill(d.blackBuffer, 0xff)
}
func (d *Device) turnOnDisplay() error {
// also documented as 'Master Activation'
if err := d.sendCommandByte(0x20); err != nil {
return err
}
d.WaitUntilIdle()
return nil
}
func (d *Device) Reset() error {
d.hwReset()
d.WaitUntilIdle()
// soft reset & set defaults
if err := d.sendCommandByte(0x12); err != nil {
return err
}
d.WaitUntilIdle()
// data entry mode setting
if err := d.sendCommandSequence([]byte{0x11, 0x03}); err != nil {
return err
}
if err := d.setWindow(0, displayWidth-1, 0, displayHeight-1); err != nil {
return err
}
// display update control 1 - resolution setting
if err := d.sendCommandSequence([]byte{0x21, 0x00, 0x80}); err != nil {
return err
}
if err := d.setCursor(0, 0); err != nil {
return err
}
d.WaitUntilIdle()
return nil
}
func (d *Device) setCursor(x, y uint16) error {
// Set RAM X address counter
if err := d.sendCommandSequence([]byte{0x4e, byte(x & 0x1f)}); err != nil {
return err
}
// Set RAM Y address counter
yLo := byte(y)
yHi := byte(y>>8) & 0x1
if err := d.sendCommandSequence([]byte{0x4f, yLo, yHi}); err != nil {
return err
}
return nil
}
func (d *Device) hwReset() {
d.rst.High()
time.Sleep(50 * time.Millisecond)
d.rst.Low()
time.Sleep(2 * time.Millisecond)
d.rst.High()
time.Sleep(50 * time.Millisecond)
}
func (d *Device) setWindow(xstart, xend, ystart, yend int16) error {
// set RAM X-address start / end position
d1 := byte((xstart >> 3) & 0x1f)
d2 := byte((xend >> 3) & 0x1f)
if err := d.sendCommandSequence([]byte{0x44, d1, d2}); err != nil {
return err
}
// set RAM Y-address start / end position
ystartLo := byte(ystart)
ystartHi := byte(ystart>>8) & 0x1
yendLo := byte(yend)
yendHi := byte(yend>>8) & 0x1
return d.sendCommandSequence([]byte{0x45, ystartLo, ystartHi, yendLo, yendHi})
}
func (d *Device) WaitUntilIdle() {
// give it some time to get busy
time.Sleep(50 * time.Millisecond)
for d.busy.Get() { // high = busy
time.Sleep(10 * time.Millisecond)
}
// give it some extra time
time.Sleep(50 * time.Millisecond)
}
// sendCommandSequence sends the first byte in the buffer as a 'command' and all following bytes as data
func (d *Device) sendCommandSequence(seq []byte) error {
err := d.sendCommandByte(seq[0])
if err != nil {
return err
}
for i := 1; i < len(seq); i++ {
err = d.sendDataByte(seq[i])
if err != nil {
return err
}
}
return nil
}
func (d *Device) sendCommandByte(b byte) error {
d.dc.Low()
d.cs.Low()
_, err := d.bus.Transfer(b)
d.cs.High()
return err
}
func (d *Device) sendDataByte(b byte) error {
d.dc.High()
d.cs.Low()
_, err := d.bus.Transfer(b)
d.cs.High()
return err
}
func (d *Device) sendData(b []byte) error {
d.dc.High()
d.cs.Low()
err := d.bus.Tx(b, nil)
d.cs.High()
return err
}
// fill quickly fills a slice with a given value
func fill(s []byte, b byte) {
s[0] = b
for j := 1; j < len(s); j *= 2 {
copy(s[j:], s[:j])
}
}
+34
View File
@@ -0,0 +1,34 @@
//go:build pico
package epd2in66b
import (
"machine"
)
// DefaultConfig contains the default config for the https://www.waveshare.com/wiki/Pico-ePaper-2.66 module
var DefaultConfig = Config{
DataPin: machine.GP8,
ChipSelectPin: machine.GP9,
ResetPin: machine.GP12,
BusyPin: machine.GP13,
}
// NewPicoModule allocates a new device backed by the https://www.waveshare.com/wiki/Pico-ePaper-2.66 module
// This will also configure the SPI1 bus and configure the device with the DefaultConfig
func NewPicoModule() (Device, error) {
spi := machine.SPI1
if err := spi.Configure(machine.SPIConfig{
Frequency: Baudrate,
}); err != nil {
return Device{}, err
}
dev := New(spi)
if err := dev.Configure(DefaultConfig); err != nil {
return dev, err
}
return dev, nil
}
+92
View File
@@ -0,0 +1,92 @@
package epd2in66b
import (
_ "embed"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
"testing"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freemono"
)
type mockBus struct{}
func (m *mockBus) Tx(w, r []byte) error {
return nil
}
func (m *mockBus) Transfer(b byte) (byte, error) {
return 0, nil
}
func TestBufferDrawing(t *testing.T) {
dev := New(&mockBus{})
tinyfont.WriteLine(&dev, &freemono.Bold9pt7b, 10, 40, "Hello World!", color.RGBA{0xff, 0xff, 0xff, 0xff})
red := color.RGBA{0xff, 0, 0, 0xff}
black := color.RGBA{0xff, 0xff, 0xff, 0xff}
showRect(&dev, 10, 10, 10, 10, black)
showRect(&dev, 10, 20, 10, 10, red)
img := toImage(&dev)
writeImage(img)
}
func toImage(dev *Device) *image.RGBA {
red := color.RGBA{0xff, 0, 0, 0xff}
xMax, yMax := dev.Size()
r := image.Rect(0, 0, int(xMax), int(yMax))
container := image.NewRGBA(r)
draw.Draw(container, container.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Over)
for x := 0; x < int(xMax); x++ {
for y := 0; y < int(yMax); y++ {
bytePos, bitPos := pos(int16(x), int16(y), displayWidth)
if isSet(dev.redBuffer, bytePos, bitPos) {
container.Set(x, y, red)
} else if isSet(dev.blackBuffer, bytePos, bitPos) {
container.Set(x, y, color.Black)
}
}
}
return container
}
func isSet(buf []byte, bytePos, bitPos int) bool {
return (buf[bytePos])&(0x1<<bitPos) != 0
}
func showRect(display drivers.Displayer, x int16, y int16, w int16, h int16, c color.RGBA) {
for i := x; i < x+w; i++ {
for j := y; j < y+h; j++ {
display.SetPixel(i, j, c)
}
}
}
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, 0o644)
if err != nil {
panic(err)
}
defer f.Close()
err = png.Encode(f, img)
if err != nil {
panic(err)
}
return fn
}
+83 -7
View File
@@ -47,6 +47,9 @@ const (
statusConnectFailed connectionStatus = 4
statusConnectionLost connectionStatus = 5
statusDisconnected connectionStatus = 6
statusAPListening connectionStatus = 7
statusAPConnected connectionStatus = 8
statusAPFailed connectionStatus = 9
encTypeTKIP encryptionType = 2
encTypeCCMP encryptionType = 4
@@ -299,6 +302,61 @@ func (w *wifinina) connectToAP() error {
return netlink.ErrConnectTimeout
}
func (w *wifinina) startAP() error {
timeout := w.params.ConnectTimeout
if timeout == 0 {
timeout = netlink.DefaultConnectTimeout
}
if len(w.params.Ssid) == 0 {
return netlink.ErrMissingSSID
}
if debugging(debugBasic) {
fmt.Printf("Starting Wifi AP as SSID '%s'...", w.params.Ssid)
}
start := time.Now()
// Start the connection process
switch {
case w.params.Passphrase != "":
w.setPassphraseForAP(w.params.Ssid, w.params.Passphrase)
default:
w.setNetworkForAP(w.params.Ssid)
}
// Check if we are listening
for {
status := w.getConnectionStatus()
switch status {
case statusAPListening:
if debugging(debugBasic) {
fmt.Printf("LISTENING\r\n")
}
if w.notifyCb != nil {
w.notifyCb(netlink.EventNetUp)
}
return nil
case statusAPFailed:
if debugging(debugBasic) {
fmt.Printf("FAILED (%s)\r\n", w.reason())
}
return netlink.ErrConnectFailed
}
if time.Since(start) > timeout {
break
}
time.Sleep(1 * time.Second)
}
if debugging(debugBasic) {
fmt.Printf("FAILED (timed out)\r\n")
}
return netlink.ErrConnectTimeout
}
func (w *wifinina) netDisconnect() {
w.disconnect()
}
@@ -380,7 +438,12 @@ func (w *wifinina) showIP() {
}
func (w *wifinina) networkDown() bool {
return w.getConnectionStatus() != statusConnected
switch w.getConnectionStatus() {
case statusConnected, statusAPListening, statusAPConnected:
return false
default:
return true
}
}
func (w *wifinina) watchdog() {
@@ -418,15 +481,28 @@ func (w *wifinina) netConnect(reset bool) error {
}
w.showDevice()
retry:
for i := 0; w.params.Retries == 0 || i < w.params.Retries; i++ {
if err := w.connectToAP(); err != nil {
switch err {
case netlink.ErrConnectTimeout, netlink.ErrConnectFailed:
continue
switch w.params.ConnectMode {
case netlink.ConnectModeAP:
if err := w.startAP(); err != nil {
switch err {
case netlink.ErrConnectTimeout, netlink.ErrConnectFailed:
continue
}
return err
}
return err
break retry
default:
if err := w.connectToAP(); err != nil {
switch err {
case netlink.ErrConnectTimeout, netlink.ErrConnectFailed:
continue
}
return err
}
break retry
}
break
}
if w.networkDown() {