mirror of https://github.com/periph/devices
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
6.3 KiB
Go
224 lines
6.3 KiB
Go
// Copyright 2017 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 nrzled
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"time"
|
|
|
|
"periph.io/x/periph/conn"
|
|
"periph.io/x/periph/conn/gpio/gpiostream"
|
|
"periph.io/x/periph/devices"
|
|
)
|
|
|
|
// NRZ converts a byte into the MSB-first Non-Return-to-Zero encoded 24 bits.
|
|
//
|
|
// The upper 8 bits are zeros and shall be ignored.
|
|
//
|
|
// The Non-return-to-zero protocol is a self-clocking signal that enables
|
|
// one-way communication without the need of a dedicated clock signal, unlike
|
|
// SPI driven LEDs like the apa102.
|
|
//
|
|
// See https://en.wikipedia.org/wiki/Non-return-to-zero for more technical
|
|
// details.
|
|
func NRZ(b byte) uint32 {
|
|
// The stream is 1x01x01x01x01x01x01x01x0 with the x bits being the bits from
|
|
// `b` in reverse order.
|
|
out := uint32(0x924924)
|
|
out |= uint32(b&0x80) << (3*7 + 1 - 7)
|
|
out |= uint32(b&0x40) << (3*6 + 1 - 6)
|
|
out |= uint32(b&0x20) << (3*5 + 1 - 5)
|
|
out |= uint32(b&0x10) << (3*4 + 1 - 4)
|
|
out |= uint32(b&0x08) << (3*3 + 1 - 3)
|
|
out |= uint32(b&0x04) << (3*2 + 1 - 2)
|
|
out |= uint32(b&0x02) << (3*1 + 1 - 1)
|
|
out |= uint32(b&0x01) << (3*0 + 1 - 0)
|
|
return out
|
|
}
|
|
|
|
// New opens a handle to a compatible LED strip.
|
|
//
|
|
// The speed (hz) should either be 800000 for fast ICs and 400000 for the slow
|
|
// ones.
|
|
//
|
|
// channels should be either 1 (White only), 3 (RGB) or 4 (RGBW). For RGB and
|
|
// RGBW, the encoding is respectively GRB and GRBW.
|
|
func New(p gpiostream.PinOut, numPixels, hz int, channels int) (*Dev, error) {
|
|
if hz <= 0 || hz > 1000000000 {
|
|
return nil, errors.New("nrzled: specify valid speed in hz")
|
|
}
|
|
if channels != 3 && channels != 4 {
|
|
return nil, errors.New("nrzled: specify valid number of channels (3 or 4)")
|
|
}
|
|
return &Dev{
|
|
p: p,
|
|
numPixels: numPixels,
|
|
channels: channels,
|
|
b: gpiostream.BitStream{
|
|
Res: time.Second / time.Duration(hz),
|
|
// Each bit is encoded on 3 bits.
|
|
Bits: make([]byte, numPixels*3*channels),
|
|
LSBF: false,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Dev is a handle to the LED strip.
|
|
type Dev struct {
|
|
p gpiostream.PinOut
|
|
numPixels int
|
|
channels int // Number of channels per pixel
|
|
b gpiostream.BitStream // NRZ encoded bits; cached to reduce heap fragmentation
|
|
buf []byte // Double buffer of RGB/RGBW pixels; enables partial Draw()
|
|
}
|
|
|
|
func (d *Dev) String() string {
|
|
return fmt.Sprintf("nrzled{%s}", d.p)
|
|
}
|
|
|
|
// Halt turns the lights off.
|
|
//
|
|
// It doesn't affect the back buffer.
|
|
func (d *Dev) Halt() error {
|
|
zero := NRZ(0)
|
|
a := byte(zero >> 16)
|
|
b := byte(zero >> 8)
|
|
c := byte(zero)
|
|
for i := 0; i < d.channels*d.numPixels; i++ {
|
|
d.b.Bits[3*i+0] = a
|
|
d.b.Bits[3*i+1] = b
|
|
d.b.Bits[3*i+2] = c
|
|
}
|
|
if err := d.p.StreamOut(&d.b); err != nil {
|
|
return fmt.Errorf("nrzled: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ColorModel implements devices.Display.
|
|
//
|
|
// It is color.NRGBAModel.
|
|
func (d *Dev) ColorModel() color.Model {
|
|
return color.NRGBAModel
|
|
}
|
|
|
|
// Bounds implements devices.Display. Min is guaranteed to be {0, 0}.
|
|
func (d *Dev) Bounds() image.Rectangle {
|
|
return image.Rectangle{Max: image.Point{X: d.numPixels, Y: 1}}
|
|
}
|
|
|
|
// Draw implements devices.Display.
|
|
//
|
|
// Using something else than image.NRGBA is 10x slower and is not recommended.
|
|
// When using image.NRGBA, the alpha channel is ignored in RGB mode and used as
|
|
// White channel in RGBW mode.
|
|
//
|
|
// A back buffer is kept so that partial updates are supported, albeit the full
|
|
// LED strip is updated synchronously.
|
|
func (d *Dev) Draw(r image.Rectangle, src image.Image, sp image.Point) {
|
|
r = r.Intersect(d.Bounds())
|
|
srcR := src.Bounds()
|
|
srcR.Min = srcR.Min.Add(sp)
|
|
if dX := r.Dx(); dX < srcR.Dx() {
|
|
srcR.Max.X = srcR.Min.X + dX
|
|
}
|
|
if dY := r.Dy(); dY < srcR.Dy() {
|
|
srcR.Max.Y = srcR.Min.Y + dY
|
|
}
|
|
if d.buf == nil {
|
|
// Allocate d.buf on first Draw() call, in case the user only wants to use
|
|
// .Write().
|
|
d.buf = make([]byte, d.numPixels*d.channels)
|
|
}
|
|
if img, ok := src.(*image.NRGBA); ok {
|
|
// Fast path for image.NRGBA.
|
|
base := srcR.Min.Y * img.Stride
|
|
raster(d.b.Bits, img.Pix[base+4*srcR.Min.X:base+4*srcR.Max.X], d.channels, 4)
|
|
} else {
|
|
// Generic version.
|
|
m := srcR.Max.X - srcR.Min.X
|
|
if d.channels == 3 {
|
|
for i := 0; i < m; i++ {
|
|
c := color.NRGBAModel.Convert(src.At(srcR.Min.X+i, srcR.Min.Y)).(color.NRGBA)
|
|
j := 3 * i
|
|
put(d.b.Bits[3*(j+0):], c.G)
|
|
put(d.b.Bits[3*(j+1):], c.R)
|
|
put(d.b.Bits[3*(j+2):], c.B)
|
|
}
|
|
} else {
|
|
for i := 0; i < m; i++ {
|
|
c := color.NRGBAModel.Convert(src.At(srcR.Min.X+i, srcR.Min.Y)).(color.NRGBA)
|
|
j := 4 * i
|
|
put(d.b.Bits[3*(j+0):], c.G)
|
|
put(d.b.Bits[3*(j+1):], c.R)
|
|
put(d.b.Bits[3*(j+2):], c.B)
|
|
put(d.b.Bits[3*(j+3):], c.A)
|
|
}
|
|
}
|
|
}
|
|
_ = d.p.StreamOut(&d.b)
|
|
}
|
|
|
|
// Write accepts a stream of raw RGB/RGBW pixels and sends it as NRZ encoded
|
|
// stream.
|
|
//
|
|
// This bypasses the back buffer.
|
|
func (d *Dev) Write(pixels []byte) (int, error) {
|
|
if len(pixels)%d.channels != 0 || len(pixels) > d.numPixels*d.channels {
|
|
return 0, errors.New("nrzled: invalid RGB stream length")
|
|
}
|
|
raster(d.b.Bits, pixels, d.channels, d.channels)
|
|
if err := d.p.StreamOut(&d.b); err != nil {
|
|
return 0, fmt.Errorf("nrzled: %v", err)
|
|
}
|
|
return len(pixels), nil
|
|
}
|
|
|
|
//
|
|
|
|
// raster converts a RGB/RGBW input stream into a MSB binary output stream as it
|
|
// must be sent over the GPIO pin.
|
|
//
|
|
// `in` is RGB 24 bits or RGBW 32 bits. Each bit is encoded over 3 bits so the
|
|
// length of `out` must be 3x as large as `in`.
|
|
//
|
|
// Encoded output format is GRB as 72 bits (24 * 3) or 96 bits (32 * 3).
|
|
func raster(out, in []byte, outChannels, inChannels int) {
|
|
pixels := len(in) / inChannels
|
|
if outChannels == 3 {
|
|
for i := 0; i < pixels; i++ {
|
|
j := i * inChannels
|
|
k := 3 * i
|
|
put(out[3*(k+0):], in[j+1])
|
|
put(out[3*(k+1):], in[j+0])
|
|
put(out[3*(k+2):], in[j+2])
|
|
}
|
|
} else {
|
|
for i := 0; i < pixels; i++ {
|
|
j := i * inChannels
|
|
k := 4 * i
|
|
put(out[3*(k+0):], in[j+1])
|
|
put(out[3*(k+1):], in[j+0])
|
|
put(out[3*(k+2):], in[j+2])
|
|
put(out[3*(k+3):], in[j+3])
|
|
}
|
|
}
|
|
}
|
|
|
|
// put writes the byte v as an MSB-first NRZ encoded triplet byte into out.
|
|
func put(out []byte, v byte) {
|
|
w := NRZ(v)
|
|
out[0] = byte(w >> 16)
|
|
out[1] = byte(w >> 8)
|
|
out[2] = byte(w)
|
|
}
|
|
|
|
var _ conn.Resource = &Dev{}
|
|
var _ devices.Display = &Dev{}
|
|
var _ fmt.Stringer = &Dev{}
|