mirror of https://github.com/periph/devices
inky: Adds device driver for Inky pHAT e-ink screens (#390)
Only support pHAT for now, but supporting wHAT won't be too hard. Supports all of the Red, Yellow and White-only devices.pull/1/head
parent
441dfbc2a4
commit
e82464a0d0
@ -0,0 +1,12 @@
|
||||
// Copyright 2019 The Periph Authors. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// Package inky drives an Inky pHAT E ink display.
|
||||
|
||||
// Datasheet
|
||||
//
|
||||
// Inky lacks a true datasheet, so the code here is derived from the reference
|
||||
// implementation by Pimoroni:
|
||||
// https://github.com/pimoroni/inky
|
||||
package inky
|
||||
@ -0,0 +1,60 @@
|
||||
// Copyright 2019 The Periph Authors. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package inky_test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"image"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"periph.io/x/periph/conn/gpio/gpioreg"
|
||||
"periph.io/x/periph/conn/spi/spireg"
|
||||
"periph.io/x/periph/experimental/devices/inky"
|
||||
"periph.io/x/periph/host"
|
||||
)
|
||||
|
||||
func Example() {
|
||||
path := flag.String("image", "", "Path to image file (212x104) to display")
|
||||
flag.Parse()
|
||||
|
||||
f, err := os.Open(*path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
img, err := png.Decode(f)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := host.Init(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := spireg.Open("SPI0.0")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
dc := gpioreg.ByName("22")
|
||||
reset := gpioreg.ByName("27")
|
||||
busy := gpioreg.ByName("17")
|
||||
|
||||
dev, err := inky.New(b, dc, reset, busy, &inky.Opts{
|
||||
Model: inky.PHAT,
|
||||
ModelColor: inky.Red,
|
||||
BorderColor: inky.Black,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := dev.Draw(img.Bounds(), img, image.ZP); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,352 @@
|
||||
// Copyright 2019 The Periph Authors. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package inky
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"periph.io/x/periph/conn"
|
||||
"periph.io/x/periph/conn/display"
|
||||
"periph.io/x/periph/conn/gpio"
|
||||
"periph.io/x/periph/conn/physic"
|
||||
"periph.io/x/periph/conn/spi"
|
||||
)
|
||||
|
||||
const (
|
||||
// Constants for an Inky pHAT
|
||||
cols = 104
|
||||
rows = 212
|
||||
)
|
||||
|
||||
// Color is used to define which model of inky is being used, and also for
|
||||
// setting the border color.
|
||||
type Color int
|
||||
|
||||
const (
|
||||
Black = iota
|
||||
Red = iota
|
||||
Yellow = iota
|
||||
White = iota
|
||||
)
|
||||
|
||||
var borderColor = map[Color]byte{
|
||||
Black: 0x00,
|
||||
Red: 0x33,
|
||||
Yellow: 0x33,
|
||||
White: 0xff,
|
||||
}
|
||||
|
||||
// Model lists the supported e-ink display models.
|
||||
type Model int
|
||||
|
||||
const (
|
||||
PHAT Model = iota
|
||||
// TODO: Add wHAT here when supported.
|
||||
)
|
||||
|
||||
// Opts is the options to specify which device is being controlled and its
|
||||
// default settings.
|
||||
type Opts struct {
|
||||
// Model being used.
|
||||
Model Model
|
||||
// Model color.
|
||||
ModelColor Color
|
||||
// Initial border color. Will be set on the first Draw().
|
||||
BorderColor Color
|
||||
}
|
||||
|
||||
// NewpHAT opens a handle to an Inky pHAT.
|
||||
func New(p spi.Port, dc gpio.PinOut, reset gpio.PinOut, busy gpio.PinIn, o *Opts) (*Dev, error) {
|
||||
if o.ModelColor != Black && o.ModelColor != Red && o.ModelColor != Yellow {
|
||||
return nil, fmt.Errorf("Unsupported color: %v", o.ModelColor)
|
||||
}
|
||||
|
||||
c, err := p.Connect(488*physic.KiloHertz, spi.Mode0, 8)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to inky over spi: %v", err)
|
||||
}
|
||||
|
||||
d := &Dev{
|
||||
c: c,
|
||||
dc: dc,
|
||||
r: reset,
|
||||
busy: busy,
|
||||
color: o.ModelColor,
|
||||
border: o.BorderColor,
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Dev is a handle to an Inky.
|
||||
type Dev struct {
|
||||
c conn.Conn
|
||||
// Low when sending a command, high when sending data.
|
||||
dc gpio.PinOut
|
||||
// Reset pin, active low.
|
||||
r gpio.PinOut
|
||||
// High when device is busy.
|
||||
busy gpio.PinIn
|
||||
|
||||
// Color of device screen (red, yellow or black).
|
||||
color Color
|
||||
// Modifiable color of border.
|
||||
border Color
|
||||
}
|
||||
|
||||
// SetBorder changes the border color. This will not take effect until the next Draw().
|
||||
func (d *Dev) SetBorder(c Color) {
|
||||
d.border = c
|
||||
}
|
||||
|
||||
// String implements conn.Resource.
|
||||
func (d *Dev) String() string {
|
||||
return "Inky pHAT"
|
||||
}
|
||||
|
||||
// Halt implements conn.Resource
|
||||
func (d *Dev) Halt() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ColorModel implements display.Drawer
|
||||
// Maps white to white, black to black and anything else as red. Red is used as
|
||||
// a placeholder for the display's third color, i.e., red or yellow.
|
||||
func (d *Dev) ColorModel() color.Model {
|
||||
return color.ModelFunc(func(c color.Color) color.Color {
|
||||
r, g, b, _ := c.RGBA()
|
||||
if r == 0 && g == 0 && b == 0 {
|
||||
return color.RGBA{
|
||||
R: 0,
|
||||
G: 0,
|
||||
B: 0,
|
||||
A: 255,
|
||||
}
|
||||
} else if r == 0xffff && g == 0xffff && b == 0xffff {
|
||||
return color.RGBA{
|
||||
R: 255,
|
||||
G: 255,
|
||||
B: 255,
|
||||
A: 255,
|
||||
}
|
||||
}
|
||||
return color.RGBA{
|
||||
R: 255,
|
||||
G: 0,
|
||||
B: 0,
|
||||
A: 255,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Bounds implements display.Drawer
|
||||
func (d *Dev) Bounds() image.Rectangle {
|
||||
return image.Rect(0, 0, rows, cols)
|
||||
}
|
||||
|
||||
// Draw implements display.Drawer
|
||||
func (d *Dev) Draw(dstRect image.Rectangle, src image.Image, srcPtrs image.Point) error {
|
||||
if dstRect != d.Bounds() {
|
||||
return fmt.Errorf("Partial update not supported")
|
||||
}
|
||||
|
||||
if src.Bounds() != d.Bounds() {
|
||||
return fmt.Errorf("Image must be the same size as bounds: %v", d.Bounds())
|
||||
}
|
||||
|
||||
b := src.Bounds()
|
||||
// Black/white pixels.
|
||||
white := make([]bool, rows*cols)
|
||||
// Red/Transparent pixels.
|
||||
red := make([]bool, rows*cols)
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
i := x*cols + y
|
||||
srcX := x
|
||||
srcY := b.Max.Y - y - 1
|
||||
r, g, b, _ := d.ColorModel().Convert(src.At(srcX, srcY)).RGBA()
|
||||
if r >= 0x8000 && g >= 0x8000 && b >= 0x8000 {
|
||||
white[i] = true
|
||||
} else if r >= 0x8000 {
|
||||
// Red pixels also need white behind them.
|
||||
white[i] = true
|
||||
red[i] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bufA, _ := pack(white)
|
||||
bufB, _ := pack(red)
|
||||
return d.update(borderColor[d.border], bufA, bufB)
|
||||
}
|
||||
|
||||
func (d *Dev) update(border byte, black []byte, red []byte) error {
|
||||
if err := d.reset(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.sendCommand(0x74, []byte{0x54}); err != nil { // Set Analog Block Control.
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand(0x7e, []byte{0x3b}); err != nil { // Set Digital Block Control.
|
||||
return err
|
||||
}
|
||||
|
||||
r := make([]byte, 3)
|
||||
binary.LittleEndian.PutUint16(r, rows)
|
||||
if err := d.sendCommand(0x01, r); err != nil { // Gate setting
|
||||
return err
|
||||
}
|
||||
|
||||
init := []struct {
|
||||
cmd byte
|
||||
data []byte
|
||||
}{
|
||||
{0x03, []byte{0x10, 0x01}}, // Gate Driving Voltage.
|
||||
{0x3a, []byte{0x07}}, // Dummy line period
|
||||
{0x3b, []byte{0x04}}, // Gate line width
|
||||
{0x11, []byte{0x03}}, // Data entry mode setting 0x03 = X/Y increment
|
||||
{0x04, nil}, // Power on
|
||||
{0x2c, []byte{0x3c}}, // VCOM Register, 0x3c = -1.5v?
|
||||
{0x3c, []byte{0x00}},
|
||||
{0x3c, []byte{byte(border)}}, // Border colour
|
||||
}
|
||||
|
||||
for _, c := range init {
|
||||
if err := d.sendCommand(c.cmd, c.data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch d.color {
|
||||
case Black:
|
||||
if err := d.sendCommand(0x32, blackLUT[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
case Red:
|
||||
if err := d.sendCommand(0x32, redLUT[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
case Yellow:
|
||||
if err := d.sendCommand(0x04, []byte{0x07}); err != nil { // Set voltage of VSH and VSL.
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand(0x32, yellowLUT[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
h := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint16(h[2:], rows)
|
||||
write := []struct {
|
||||
cmd byte
|
||||
data []byte
|
||||
}{
|
||||
{0x44, []byte{0x00, cols/8 - 1}}, // Set RAM X Start/End
|
||||
{0x45, h}, // Set RAM Y Start/End
|
||||
{0x43, []byte{0x00}},
|
||||
|
||||
{0x4e, []byte{0x00}},
|
||||
{0x4f, []byte{0x00, 0x00}},
|
||||
{0x24, black},
|
||||
|
||||
{0x43, []byte{0x00}},
|
||||
{0x4f, []byte{0x00, 0x00}},
|
||||
{0x26, red},
|
||||
|
||||
{0x22, []byte{0xc7}},
|
||||
}
|
||||
|
||||
for _, c := range write {
|
||||
if err := d.sendCommand(c.cmd, c.data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
d.busy.In(gpio.PullUp, gpio.FallingEdge)
|
||||
defer d.busy.In(gpio.PullUp, gpio.NoEdge)
|
||||
if err := d.sendCommand(0x20, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.busy.WaitForEdge(-1)
|
||||
|
||||
if err := d.sendCommand(0x10, []byte{0x01}); err != nil { // Enter deep sleep.
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dev) reset() (err error) {
|
||||
if err = d.r.Out(gpio.Low); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if err = d.r.Out(gpio.High); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if err = d.busy.In(gpio.PullUp, gpio.FallingEdge); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err2 := d.busy.In(gpio.PullUp, gpio.NoEdge); err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
if err := d.sendCommand(0x12, nil); err != nil { // Soft Reset
|
||||
return fmt.Errorf("failed to reset inky: %v", err)
|
||||
}
|
||||
d.busy.WaitForEdge(-1)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Dev) sendCommand(command byte, data []byte) error {
|
||||
d.dc.Out(gpio.Low)
|
||||
if err := d.c.Tx([]byte{command}, nil); err != nil {
|
||||
return fmt.Errorf("failed to send command %x to inky: %v", command, err)
|
||||
}
|
||||
if data != nil {
|
||||
if err := d.sendData(data); err != nil {
|
||||
return fmt.Errorf("failed to send data for command %x to inky: %v", command, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dev) sendData(data []byte) error {
|
||||
if len(data) > 4096 {
|
||||
return fmt.Errorf("Sending more data than chunk size: %d > 4096", len(data))
|
||||
}
|
||||
if err := d.dc.Out(gpio.High); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.c.Tx(data, nil); err != nil {
|
||||
return fmt.Errorf("failed to send data to inky: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pack(bits []bool) ([]byte, error) {
|
||||
if len(bits)%8 != 0 {
|
||||
return nil, fmt.Errorf("len(bits) must be multiple of 8 but is %d", len(bits))
|
||||
}
|
||||
|
||||
ret := make([]byte, len(bits)/8)
|
||||
for i, b := range bits {
|
||||
if b {
|
||||
ret[i/8] |= 1 << (7 - uint(i)%8)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
var _ display.Drawer = &Dev{}
|
||||
var _ conn.Resource = &Dev{}
|
||||
@ -0,0 +1,31 @@
|
||||
// Copyright 2019 The Periph Authors. All rights reserved.
|
||||
// Use of this source code is governed under the Apache License, Version 2.0
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package inky
|
||||
|
||||
var (
|
||||
blackLUT = [...]byte{
|
||||
0x48, 0xa0, 0x10, 0x10, 0x13, 0x0, 0x0, 0x48, 0xa0, 0x80, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x48, 0xa5, 0x0, 0xbb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x10, 0x4, 0x4, 0x4, 0x4, 0x10, 0x4, 0x4, 0x4, 0x4, 0x4, 0x8, 0x8,
|
||||
0x10, 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
}
|
||||
|
||||
redLUT = [...]byte{
|
||||
0x48, 0xa0, 0x10, 0x10, 0x13, 0x0, 0x0, 0x48, 0xa0, 0x80, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x48, 0xa5, 0x0, 0xbb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x40, 0xc, 0x20, 0xc, 0x6, 0x10, 0x8, 0x4, 0x4, 0x6, 0x4, 0x8, 0x8,
|
||||
0x10, 0x10, 0x2, 0x2, 0x2, 0x40, 0x20, 0x2, 0x2, 0x2, 0x2, 0x2, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
}
|
||||
|
||||
yellowLUT = [...]byte{
|
||||
0xfa, 0x94, 0x8c, 0xc0, 0xd0, 0x0, 0x0, 0xfa, 0x94, 0x2c, 0x80, 0xe0, 0x0, 0x0, 0xfa, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0xfa, 0x94, 0xf8, 0x80, 0x50, 0x0, 0xcc, 0xbf, 0x58, 0xfc, 0x80,
|
||||
0xd0, 0x0, 0x11, 0x40, 0x10, 0x40, 0x10, 0x8, 0x8, 0x10, 0x4, 0x4, 0x10, 0x8, 0x8, 0x3,
|
||||
0x8, 0x20, 0x8, 0x4, 0x0, 0x0, 0x10, 0x10, 0x8, 0x8, 0x0, 0x20, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
}
|
||||
)
|
||||
Loading…
Reference in New Issue