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.
devices/nrzled/nrzled.go

359 lines
11 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"
"image/draw"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/gpio/gpiostream"
"periph.io/x/conn/v3/physic"
"periph.io/x/conn/v3/spi"
)
// DefaultOpts is the recommended default options.
var DefaultOpts = Opts{
NumPixels: 150, // 150 LEDs is a common strip length.
Channels: 3, // RGB.
Freq: 800 * physic.KiloHertz, // Fast LEDs, most common.
}
// Opts defines the options for the device.
type Opts struct {
// NumPixels is the number of pixels to control. If too short, the following
// pixels will be corrupted. If too long, the pixels will be drawn
// unnecessarily but not visible issue will occur.
NumPixels int
// Channels is 1 for single color LEDs, 3 for RGB LEDs and 4 for RGBW (white)
// LEDs.
Channels int
// Freq is the frequency to use to drive the LEDs. It should be either 800kHz
// for fast ICs and 400kHz for the slow ones.
Freq physic.Frequency
}
// NewStream opens a handle to a compatible LED strip.
func NewStream(p gpiostream.PinOut, opts *Opts) (*Dev, error) {
// Allow a wider range in case there's new devices with higher supported
// frequency.
if opts.Freq < 10*physic.KiloHertz || opts.Freq > 100*physic.MegaHertz {
return nil, errors.New("nrzled: specify valid frequency")
}
if opts.Channels != 3 && opts.Channels != 4 {
return nil, errors.New("nrzled: specify valid number of channels (3 or 4)")
}
// 3 symbol bytes per byte, 3/4 bytes per pixel.
streamLen := 3 * (opts.Channels * opts.NumPixels)
// 3 bytes for latch. TODO: duration.
bufSize := streamLen + 3
buf := make([]byte, bufSize)
return &Dev{
name: "nrzled{" + p.Name() + "}",
p: p,
numPixels: opts.NumPixels,
channels: opts.Channels,
b: gpiostream.BitStream{Freq: opts.Freq, Bits: buf, LSBF: false},
rawBuf: buf[:streamLen],
rect: image.Rect(0, 0, opts.NumPixels, 1),
}, nil
}
// NewSPI returns a strip that communicates over SPI to NRZ encoded LEDs.
//
// Due to the tight timing demands of these LEDs, the SPI port speed must be a
// reliable 2.4~2.5MHz; this is 3x 800kHz.
//
// The driver's SPI buffer must be at least 12*num_pixels+3 bytes long.
func NewSPI(p spi.Port, opts *Opts) (*Dev, error) {
const spiFreq = 2500 * physic.KiloHertz
if opts.Freq != spiFreq {
return nil, errors.New("nrzled: expected Freq " + spiFreq.String())
}
if opts.Channels != 3 && opts.Channels != 4 {
return nil, errors.New("nrzled: specify valid number of channels (3 or 4)")
}
// 4 symbol bytes per byte, 3/4 bytes per pixel.
streamLen := 4 * (opts.Channels * opts.NumPixels)
// 3 bytes for latch. 24*400ns = 9600ns. In practice this could be skipped,
// as the overhead for SPI Tx() tear down and the next one is likely at least
// 10µs.
bufSize := streamLen + 3
if l, ok := p.(conn.Limits); ok {
if s := l.MaxTxSize(); s < bufSize {
return nil, errors.New("spi port buffer is too short for the specified number of pixels")
}
}
c, err := p.Connect(spiFreq, spi.Mode3|spi.NoCS, 8)
if err != nil {
return nil, err
}
buf := make([]byte, bufSize)
return &Dev{
name: "nrzled{" + c.String() + "}",
s: c,
numPixels: opts.NumPixels,
channels: opts.Channels,
b: gpiostream.BitStream{Freq: opts.Freq, Bits: buf, LSBF: false},
rawBuf: buf[:streamLen],
rect: image.Rect(0, 0, opts.NumPixels, 1),
}, nil
}
// Dev is a handle to the LED strip.
type Dev struct {
// Immutable.
name string
s spi.Conn
p gpiostream.PinOut
numPixels int
channels int // Number of channels per pixel
rect image.Rectangle // Device bounds
// Mutable.
b gpiostream.BitStream // NRZ encoded bits; cached to reduce heap fragmentation
rawBuf []byte // NRZ encoded bits; excluding the padding
pixels []byte // Double buffer of RGB/RGBW pixels; enables partial Draw()
}
func (d *Dev) String() string {
return d.name
}
// Halt turns the lights off.
//
// It doesn't affect the back buffer.
func (d *Dev) Halt() error {
if d.s == nil {
// zero := nrzMSB3[0]
const a = 0x92
const b = 0x49
const c = 0x24
for i := 0; i < len(d.rawBuf); i += 3 {
d.rawBuf[i+0] = a
d.rawBuf[i+1] = b
d.rawBuf[i+2] = c
}
if err := d.p.StreamOut(&d.b); err != nil {
return fmt.Errorf("nrzled: %v", err)
}
return nil
}
// Zap out the buffer. 0x88 is '0'.
for i := range d.rawBuf {
d.rawBuf[i] = 0x88
}
if err := d.s.Tx(d.b.Bits, nil); err != nil {
return fmt.Errorf("nrzled: %v", err)
}
return nil
}
// ColorModel implements display.Drawer.
//
// It is color.NRGBAModel.
func (d *Dev) ColorModel() color.Model {
return color.NRGBAModel
}
// Bounds implements display.Drawer. Min is guaranteed to be {0, 0}.
func (d *Dev) Bounds() image.Rectangle {
return d.rect
}
// Draw implements display.Drawer.
//
// 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) error {
if r = r.Intersect(d.rect); r.Empty() {
return nil
}
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 srcR.Empty() {
return nil
}
if d.s != nil {
d.rasterSPIImg(d.rawBuf, r, src, srcR)
return d.s.Tx(d.b.Bits, nil)
}
if d.pixels == nil {
// Allocate d.pixels on first Draw() call, in case the user only wants to
// use .Write().
d.pixels = make([]byte, d.numPixels*d.channels)
}
if img, ok := src.(*image.NRGBA); ok {
// Fast path for image.NRGBA.
base := srcR.Min.Y * img.Stride
rasterBits(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
putNRZMSB3(d.b.Bits[3*(j+0):], c.G)
putNRZMSB3(d.b.Bits[3*(j+1):], c.R)
putNRZMSB3(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
putNRZMSB3(d.b.Bits[3*(j+0):], c.G)
putNRZMSB3(d.b.Bits[3*(j+1):], c.R)
putNRZMSB3(d.b.Bits[3*(j+2):], c.B)
putNRZMSB3(d.b.Bits[3*(j+3):], c.A)
}
}
}
return 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")
}
if d.s == nil {
rasterBits(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
}
d.rasterSPI(d.rawBuf, pixels, false)
return len(pixels), d.s.Tx(d.b.Bits, nil)
}
// Bits
// rasterBits 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 rasterBits(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
putNRZMSB3(out[3*(k+0):], in[j+1])
putNRZMSB3(out[3*(k+1):], in[j+0])
putNRZMSB3(out[3*(k+2):], in[j+2])
}
} else {
for i := 0; i < pixels; i++ {
j := i * inChannels
k := 4 * i
putNRZMSB3(out[3*(k+0):], in[j+1])
putNRZMSB3(out[3*(k+1):], in[j+0])
putNRZMSB3(out[3*(k+2):], in[j+2])
putNRZMSB3(out[3*(k+3):], in[j+3])
}
}
}
// putNRZMSB3 writes the byte v as an MSB-first NRZ encoded triplet byte into
// out.
func putNRZMSB3(out []byte, v byte) {
copy(out, nrzMSB3[v][:])
}
// SPI
// rasterSPI serializes a buffer of RGB bytes to the WS2812b SPI format.
//
// It is expected to be given the part where pixels are, not the header nor
// footer.
//
// dst is in WS2812b SPI 32 bits word format. src is in RGB 24 bits, or 32 bits
// word format when srcHasAlpha is true. The src alpha channel is ignored in
// this case.
//
// src cannot be longer in pixel count than dst.
func (d *Dev) rasterSPI(dst []byte, src []byte, srcHasAlpha bool) {
pBytes := 3
if srcHasAlpha {
pBytes = 4
}
length := len(src) / pBytes
stride := 4 //number of spi-bytes in color-byte
for i := 0; i < length; i++ {
sOff := pBytes * i
dOff := 3 * stride * i //3 channels * stride
r, g, b := src[sOff], src[sOff+1], src[sOff+2]
//grb color order, msb first
copy(dst[dOff+stride*0:dOff+stride*1], nrzMSB4[g][:])
copy(dst[dOff+stride*1:dOff+stride*2], nrzMSB4[r][:])
copy(dst[dOff+stride*2:dOff+stride*3], nrzMSB4[b][:])
}
}
// rasterSPIImg is the generic version of raster that converts an image instead
// of raw RGB values.
//
// It has 'fast paths' for image.RGBA and image.NRGBA that extract and convert
// the RGB values directly. For other image types, it converts to image.RGBA
// and then does the same. In all cases, alpha values are ignored.
//
// rect specifies where into the output buffer to draw.
//
// srcR specifies what portion of the source image to use.
func (d *Dev) rasterSPIImg(dst []byte, rect image.Rectangle, src image.Image, srcR image.Rectangle) {
// Render directly into the buffer for maximum performance and to keep
// untouched sections intact.
switch im := src.(type) {
case *image.RGBA:
start := im.PixOffset(srcR.Min.X, srcR.Min.Y)
// srcR.Min.Y since the output display has only a single column
end := im.PixOffset(srcR.Max.X, srcR.Min.Y)
// Offset into the output buffer using rect
d.rasterSPI(dst[4*rect.Min.X:], im.Pix[start:end], true)
case *image.NRGBA:
// Ignores alpha
start := im.PixOffset(srcR.Min.X, srcR.Min.Y)
// srcR.Min.Y since the output display has only a single column
end := im.PixOffset(srcR.Max.X, srcR.Min.Y)
// Offset into the output buffer using rect
d.rasterSPI(dst[4*rect.Min.X:], im.Pix[start:end], true)
default:
// Slow path. Convert to RGBA
b := im.Bounds()
m := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(m, m.Bounds(), src, b.Min, draw.Src)
start := m.PixOffset(srcR.Min.X, srcR.Min.Y)
// srcR.Min.Y since the output display has only a single column
end := m.PixOffset(srcR.Max.X, srcR.Min.Y)
// Offset into the output buffer using rect
d.rasterSPI(dst[4*rect.Min.X:], m.Pix[start:end], true)
}
}
var _ display.Drawer = &Dev{}