nrzled: add support for ws281x and clones (#165)

- Includes driver and CLI. Completely untested.
pull/1/head
M-A 9 years ago committed by GitHub
parent 7dc1026c65
commit 0b05c62a9e

@ -0,0 +1,22 @@
// 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 is a driver for LEDs ws2811/ws2812/ws2812b and compatible
// devices like sk6812 and ucs1903 that uses a single wire NRZ encoded
// communication protocol.
//
// Note that some ICs are 7 bits with the least significant bit ignored, others
// are using a real 8 bits PWM. The PWM frequency varies across ICs.
//
// Datasheet
//
// This directory contains datasheets for ws2812, ws2812b, ucs190x and various
// sk6812.
//
// https://github.com/cpldcpu/light_ws2812/tree/master/Datasheets
//
// UCS1903 datasheet
//
// http://www.bestlightingbuy.com/pdf/UCS1903%20datasheet.pdf
package nrzled

@ -0,0 +1,223 @@
// 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 Non-Return-to-Zero encoded 24 bits.
//
// The upper 8 bits are zeros.
//
// 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
}
// 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
}
// 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)")
}
// It is more space effective to use gpiostream.Bits than
// gpiostream.EdgeStream.
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(gpiostream.Bits, numPixels*3*channels),
},
}, nil
}
//
// raster converts a RGB/RGBW input stream into a 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])
}
}
}
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{}

@ -0,0 +1,322 @@
// 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 (
"bytes"
"image"
"image/color"
"strconv"
"testing"
"time"
"periph.io/x/periph/conn/gpio/gpiostream"
"periph.io/x/periph/conn/gpio/gpiostream/gpiostreamtest"
)
func TestNRZ(t *testing.T) {
data := []struct {
in byte
expected uint32
}{
{0x00, 0x924924},
{0x01, 0x924926},
{0x02, 0x924934},
{0x04, 0x9249A4},
{0x08, 0x924D24},
{0x10, 0x926924},
{0x20, 0x934924},
{0x40, 0x9A4924},
{0x80, 0xD24924},
{0xFD, 0xDB6DA6},
{0xFE, 0xDB6DB4},
{0xFF, 0xDB6DB6},
}
for i, line := range data {
t.Run(strconv.Itoa(i), func(t *testing.T) {
if actual := NRZ(line.in); line.expected != actual {
t.Fatalf("NRZ(%X): 0x%X != 0x%X", line.in, line.expected, actual)
}
})
}
}
func TestNew_3(t *testing.T) {
g := gpiostreamtest.PinOutPlayback{
N: "Yo",
Ops: []gpiostream.Stream{
&gpiostream.BitStream{
Bits: gpiostream.Bits{
0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92,
0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49,
0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24,
0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92,
0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49,
0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24,
},
Res: 2500 * time.Nanosecond,
},
},
}
d, err := New(&g, 10, 400000, 3)
if err != nil {
t.Fatal(err)
}
if s := d.String(); s != "nrzled{Yo}" {
t.Fatal(s)
}
if c := d.ColorModel(); c != color.NRGBAModel {
t.Fatal(c)
}
if r := d.Bounds(); r != image.Rect(0, 0, 10, 1) {
t.Fatal(r)
}
if err = d.Halt(); err != nil {
t.Fatal(err)
}
if err = g.Close(); err != nil {
t.Fatal(err)
}
}
func TestNew_fail(t *testing.T) {
g := gpiostreamtest.PinOutPlayback{}
if _, err := New(&g, 1, 0, 3); err == nil {
t.Fatal("hz == 0")
}
if _, err := New(&g, 1, 400000, 2); err == nil {
t.Fatal("channels == 2")
}
}
func TestDraw_NRGBA_3(t *testing.T) {
g := gpiostreamtest.PinOutPlayback{
Ops: []gpiostream.Stream{
&gpiostream.BitStream{
Bits: gpiostream.Bits{
0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0xdb,
0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49,
0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24,
0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb,
0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d,
0xb6, 0x92, 0x49, 0xb6, 0x92, 0x49, 0xb4, 0x92, 0x4d, 0x24,
},
Res: 2500 * time.Nanosecond,
},
},
}
d, _ := New(&g, 10, 400000, 3)
img := image.NewNRGBA(d.Bounds())
copy(img.Pix, getRGBW())
d.Draw(d.Bounds(), img, image.Point{})
if err := g.Close(); err != nil {
t.Fatal(err)
}
}
func TestDraw_RGBA_3(t *testing.T) {
g := gpiostreamtest.PinOutPlayback{
Ops: []gpiostream.Stream{
&gpiostream.BitStream{
Bits: gpiostream.Bits{
0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0xdb,
0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49,
0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24,
0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb,
0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xa6, 0xdb, 0x6d, 0xa6, 0xdb, 0x6d,
0xa6, 0xda, 0x49, 0xb6, 0xd3, 0x4d, 0x34, 0xdb, 0x49, 0x36,
},
Res: 2500 * time.Nanosecond,
},
},
}
d, _ := New(&g, 10, 400000, 3)
img := image.NewRGBA(d.Bounds())
copy(img.Pix, getRGBW())
d.Draw(d.Bounds(), img, image.Point{})
if err := g.Close(); err != nil {
t.Fatal(err)
}
}
func TestDraw_RGBA_4(t *testing.T) {
g := gpiostreamtest.PinOutPlayback{
Ops: []gpiostream.Stream{
&gpiostream.BitStream{
Bits: gpiostream.Bits{
0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92,
0x49, 0x24, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0x92, 0x49,
0x24, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6,
0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb,
0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49,
0x24, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6,
0xdb, 0x6d, 0xa6, 0xdb, 0x6d, 0xa6, 0xdb, 0x6d, 0xa6, 0xd2, 0x49, 0x24, 0xda, 0x49, 0xb6, 0xd3,
0x4d, 0x34, 0xdb, 0x49, 0x36, 0x92, 0x4d, 0x26,
},
Res: 2500 * time.Nanosecond,
},
},
}
d, _ := New(&g, 10, 400000, 4)
img := image.NewRGBA(d.Bounds())
copy(img.Pix, getRGBW())
d.Draw(d.Bounds(), img, image.Point{})
if err := g.Close(); err != nil {
t.Fatal(err)
}
}
func TestDraw_Limits(t *testing.T) {
g := gpiostreamtest.PinOutPlayback{
Ops: []gpiostream.Stream{
&gpiostream.BitStream{
Bits: gpiostream.Bits{
0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0xdb,
0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49,
0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24,
0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb,
0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xa6, 0xdb, 0x6d, 0xa6, 0xdb, 0x6d,
0xa6, 0xda, 0x49, 0xb6, 0xd3, 0x4d, 0x34, 0xdb, 0x49, 0x36,
},
Res: 2500 * time.Nanosecond,
},
},
}
d, _ := New(&g, 10, 400000, 3)
img := image.NewRGBA(image.Rect(-1, -1, 20, 20))
copy(img.Pix, getRGBW())
d.Draw(d.Bounds(), img, image.Point{})
if err := g.Close(); err != nil {
t.Fatal(err)
}
}
func TestWrite_3(t *testing.T) {
g := gpiostreamtest.PinOutPlayback{
Ops: []gpiostream.Stream{
&gpiostream.BitStream{
Bits: gpiostream.Bits{
0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0xdb,
0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49,
0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24,
0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0x24, 0xdb,
0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0xdb, 0x6d, 0xb6, 0x92, 0x49, 0xa4, 0x92, 0x49, 0x36, 0x92, 0x49,
0xa6, 0x92, 0x49, 0xb6, 0x92, 0x49, 0xb4, 0x92, 0x4d, 0x24,
},
Res: 2500 * time.Nanosecond,
},
},
}
d, _ := New(&g, 10, 400000, 3)
if n, err := d.Write(getRGB()); n != 30 || err != nil {
t.Fatal(n, err)
}
if err := g.Close(); err != nil {
t.Fatal(err)
}
}
func TestWrite_fail(t *testing.T) {
g := gpiostreamtest.PinOutPlayback{DontPanic: true}
d, _ := New(&g, 10, 400000, 3)
if n, err := d.Write([]byte{1}); n != 0 || err == nil {
t.Fatal(n, err)
}
if n, err := d.Write([]byte{1, 2, 3}); n != 0 || err == nil {
t.Fatal(n, err)
}
if err := g.Close(); err != nil {
t.Fatal(err)
}
}
func TestHalt_fail(t *testing.T) {
g := gpiostreamtest.PinOutPlayback{DontPanic: true}
d, _ := New(&g, 10, 400000, 3)
if d.Halt() == nil {
t.Fatal("expected failure")
}
if err := g.Close(); err != nil {
t.Fatal(err)
}
}
func TestRaster_3_3(t *testing.T) {
data := []byte{
// 24 bits per pixel in RGB
0, 1, 2,
0xFD, 0xFE, 0xFF,
}
expected := []byte{
// 72 bits per pixel in GRB
0x92, 0x49, 0x26, 0x92, 0x49, 0x24, 0x92, 0x49, 0x34,
0xdb, 0x6d, 0xb4, 0xdb, 0x6d, 0xa6, 0xdb, 0x6d, 0xb6,
}
actual := make([]byte, len(expected))
raster(actual, data, 3, 3)
if !bytes.Equal(expected, actual) {
t.Fatalf("\nexpected %#v\n actual %#v", expected, actual)
}
}
func TestRaster_4_4(t *testing.T) {
data := []byte{
// 32 bits per pixel in RGBW
0, 1, 2, 3,
0xFC, 0xFD, 0xFE, 0xFF,
}
expected := []byte{
// 96 bits per pixel in GRBW
0x92, 0x49, 0x26, 0x92, 0x49, 0x24, 0x92, 0x49, 0x34, 0x92, 0x49, 0x36,
0xdb, 0x6d, 0xa6, 0xdb, 0x6d, 0xa4, 0xdb, 0x6d, 0xb4, 0xdb, 0x6d, 0xb6,
}
actual := make([]byte, len(expected))
raster(actual, data, 4, 4)
if !bytes.Equal(expected, actual) {
t.Fatalf("\nexpected %#v\n actual %#v", expected, actual)
}
}
//
func BenchmarkNRZ(b *testing.B) {
for i := 0; i < b.N; i++ {
NRZ(23)
}
}
//
// getRGB returns a buffer of 10 RGB pixels.
func getRGB() []byte {
return []byte{
0x00, 0x00, 0x00,
0x00, 0x00, 0xFF,
0x00, 0xFF, 0x00,
0x00, 0xFF, 0xFF,
0xFF, 0x00, 0x00,
0xFF, 0x00, 0xFF,
0xFF, 0xFF, 0x00,
0xFF, 0xFF, 0xFF,
3, 4, 5,
6, 7, 8,
}
}
// getRGBW returns a buffer of 10 RGB pixels.
func getRGBW() []byte {
return []byte{
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF,
0x00, 0xFF, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0xFF, 0xFF,
0xFF, 0xFF, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0x80,
6, 7, 8, 9,
}
}
Loading…
Cancel
Save