Skip to content

Commit d3bfc99

Browse files
authored
add ws2812b alternative to ws2812 driver (#10)
* add ws2812b alternative to ws2812 driver * fix timings a bit * add working WS2812B implementation!
1 parent 1eaed39 commit d3bfc99

File tree

6 files changed

+305
-7
lines changed

6 files changed

+305
-7
lines changed

rp2-pio/examples/ws2812/main.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,56 @@ import (
1212
func main() {
1313
const ws2812Pin = machine.GP16
1414
sm, _ := pio.PIO0.ClaimStateMachine()
15-
ws, err := piolib.NewWS2812(sm, ws2812Pin)
15+
ws, err := piolib.NewWS2812B(sm, ws2812Pin)
1616
if err != nil {
1717
panic(err.Error())
1818
}
19-
const maxVal = 255
19+
const maxVal = 4
2020
red := color.RGBA{R: maxVal}
2121
amber := color.RGBA{R: maxVal, G: maxVal / 4 * 3}
2222
green := color.RGBA{G: maxVal}
23+
for {
24+
for g := uint8(0); g < 255; g |= 1 {
25+
for r := uint8(0); r < 255; r |= 1 {
26+
for b := uint8(0); b < 255; b |= 1 {
27+
time.Sleep(time.Second / 8)
28+
ws.PutRGB(r, g, b)
29+
b <<= 1
30+
println(r, g, b)
31+
}
32+
println("max blue")
33+
r <<= 1
34+
}
35+
println("max red")
36+
g <<= 1
37+
}
38+
println("max greenb")
39+
}
2340

2441
// Stoplight sequence.
2542
for {
2643
const longWait = 6 * time.Second
2744
const shortWait = 2 * time.Second
2845
// Start Stoplight in red (STOP).
2946
println("red")
30-
ws.SetColor(red)
47+
ws.PutColor(red)
3148
time.Sleep(4 * time.Second)
3249

3350
// Before green we go through a red+yellow stage (PREP. PULL AWAY)
3451
println("green/amber switching")
3552
for i := 0; i < 2; i++ {
3653
const semiSleep = time.Second / 2
37-
ws.SetColor(amber)
54+
ws.PutColor(amber)
3855
time.Sleep(semiSleep)
39-
ws.SetColor(red)
56+
ws.PutColor(red)
4057
time.Sleep(semiSleep)
4158
}
4259
println("green")
43-
ws.SetColor(green)
60+
ws.PutColor(green)
4461
time.Sleep(longWait)
4562

4663
println("amber")
47-
ws.SetColor(amber)
64+
ws.PutColor(amber)
4865
time.Sleep(shortWait)
4966
}
5067
}

rp2-pio/examples/ws2812b/main.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package main
2+
3+
import (
4+
"machine"
5+
"time"
6+
7+
pio "github.com/tinygo-org/pio/rp2-pio"
8+
"github.com/tinygo-org/pio/rp2-pio/piolib"
9+
)
10+
11+
func main() {
12+
const ws2812Pin = machine.GP16
13+
sm, _ := pio.PIO0.ClaimStateMachine()
14+
ws, err := piolib.NewWS2812B(sm, ws2812Pin)
15+
if err != nil {
16+
panic(err.Error())
17+
}
18+
const lightIntensity = 64
19+
rawred := rawcolor(lightIntensity, 0, 0)
20+
rawgreen := rawcolor(0, lightIntensity, 0)
21+
// Make Christmas lights of first part of strip.
22+
ws.EnableDMA(true)
23+
ws.WriteRaw([]uint32{
24+
0,
25+
rawred,
26+
rawgreen,
27+
rawred,
28+
rawgreen,
29+
rawred,
30+
rawgreen,
31+
rawred,
32+
rawgreen,
33+
rawred,
34+
rawgreen,
35+
rawred,
36+
rawgreen,
37+
rawred,
38+
rawgreen,
39+
})
40+
41+
// And sweep first LED.
42+
const sweepIncrement = 1
43+
const sweepPeriod = time.Second / 4
44+
for {
45+
println("red sweep")
46+
for r := uint8(0); r < 255; r += sweepIncrement {
47+
ws.PutRGB(r, 0, 0)
48+
time.Sleep(sweepPeriod)
49+
}
50+
time.Sleep(time.Second)
51+
println("green sweep")
52+
for g := uint8(0); g < 255; g += sweepIncrement {
53+
ws.PutRGB(0, g, 0)
54+
time.Sleep(sweepPeriod)
55+
}
56+
time.Sleep(time.Second)
57+
println("blue sweep")
58+
for b := uint8(0); b < 255; b += sweepIncrement {
59+
ws.PutRGB(0, 0, b)
60+
time.Sleep(sweepPeriod)
61+
}
62+
time.Sleep(time.Second)
63+
}
64+
}
65+
66+
func rawcolor(r, g, b uint8) uint32 {
67+
return uint32(g)<<24 | uint32(r)<<16 | uint32(b)<<8
68+
}

rp2-pio/piolib/all_generate.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var (
2121
//go:generate pioasm -o go pulsar.pio pulsar_pio.go
2222
//go:generate pioasm -o go spi.pio spi_pio.go
2323
//go:generate pioasm -o go ws2812.pio ws2812_pio.go
24+
//go:generate pioasm -o go ws2812b.pio ws2812b_pio.go
2425
//go:generate pioasm -o go i2s.pio i2s_pio.go
2526
//go:generate pioasm -o go spi3w.pio spi3w_pio.go
2627
func gosched() {

rp2-pio/piolib/ws2812b.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//go:build rp2040
2+
3+
package piolib
4+
5+
import (
6+
"image/color"
7+
"machine"
8+
9+
pio "github.com/tinygo-org/pio/rp2-pio"
10+
)
11+
12+
// WS2812B is an RGB LED strip controller implementation, also known as NeoPixel.
13+
type WS2812B struct {
14+
sm pio.StateMachine
15+
dma dmaChannel
16+
offset uint8
17+
}
18+
19+
func NewWS2812B(sm pio.StateMachine, pin machine.Pin) (*WS2812B, error) {
20+
// https://cdn-shop.adafruit.com/datasheets/WS2812B.pdf
21+
const (
22+
baseline = 1250.
23+
baselinesplit = baseline / 3
24+
cycle = baselinesplit / 3
25+
freq = uint32(1e9 / cycle)
26+
)
27+
sm.TryClaim() // SM should be claimed beforehand, we just guarantee it's claimed.
28+
cpufreq := machine.CPUFrequency()
29+
// whole, frac, err := pio.ClkDivFromPeriod(period, cpufreq)
30+
whole, frac, err := pio.ClkDivFromFrequency(freq, cpufreq)
31+
if err != nil {
32+
return nil, err
33+
}
34+
// We add the program to PIO memory and store it's offset.
35+
Pio := sm.PIO()
36+
offset, err := Pio.AddProgram(ws2812b_ledInstructions, ws2812b_ledOrigin)
37+
if err != nil {
38+
return nil, err
39+
}
40+
pin.Configure(machine.PinConfig{Mode: Pio.PinMode()})
41+
sm.SetPindirsConsecutive(pin, 1, true)
42+
cfg := ws2812b_ledProgramDefaultConfig(offset)
43+
cfg.SetSetPins(pin, 1)
44+
// We only use Tx FIFO, so we set the join to Tx.
45+
cfg.SetFIFOJoin(pio.FifoJoinTx)
46+
cfg.SetClkDivIntFrac(whole, frac)
47+
cfg.SetOutShift(false, true, 24)
48+
sm.Init(offset, cfg)
49+
sm.SetEnabled(true)
50+
dev := &WS2812B{sm: sm, offset: offset}
51+
return dev, nil
52+
}
53+
54+
// PutRGB puts a RGB color in the transmit queue. If Queue if full will be discarded.
55+
func (ws *WS2812B) PutRGB(r, g, b uint8) {
56+
// Shift occurs to left for WS2812B to interpret correctly.
57+
color := uint32(g)<<24 | uint32(r)<<16 | uint32(b)<<8
58+
ws.PutRaw(color)
59+
}
60+
61+
// PutRaw puts a raw color value in the PIO state machine queue. The grb uint32 is a WS2812B color
62+
// which can be created with 3 uint8 color values:
63+
//
64+
// color := uint32(green)<<24 | uint32(red)<<16 | uint32(blue)<<8
65+
func (ws *WS2812B) PutRaw(grb uint32) {
66+
ws.sm.TxPut(grb)
67+
}
68+
69+
// IsQueueFull returns true if the PIO Tx FIFO has 8 elements and will discard next call to PutRaw, PutRGB or PutColor.
70+
func (ws *WS2812B) IsQueueFull() bool {
71+
return ws.sm.IsTxFIFOFull()
72+
}
73+
74+
// PutColor wraps PutRGB for a [color.Color] type.
75+
func (ws *WS2812B) PutColor(c color.Color) {
76+
r16, g16, b16, _ := c.RGBA()
77+
ws.PutRGB(uint8(r16>>8), uint8(g16>>8), uint8(b16>>8))
78+
}
79+
80+
// WriteRaw writes raw GRB values to a strip of WS2812B LEDs. Each uint32 is a WS2812B color
81+
// which can be created with 3 uint8 color values::
82+
//
83+
// color := uint32(g)<<24 | uint32(r)<<16 | uint32(b)<<8
84+
func (ws *WS2812B) WriteRaw(rawGRB []uint32) error {
85+
if ws.IsDMAEnabled() {
86+
return ws.writeDMA(rawGRB)
87+
}
88+
dl := ws.dma.dl.newDeadline()
89+
i := 0
90+
for i < len(rawGRB) {
91+
if ws.IsQueueFull() {
92+
if dl.expired() {
93+
return errTimeout
94+
}
95+
gosched()
96+
continue
97+
}
98+
ws.sm.TxPut(rawGRB[i])
99+
i++
100+
}
101+
return nil
102+
}
103+
104+
// EnableDMA enables DMA for vectorized writes.
105+
func (ws *WS2812B) EnableDMA(enabled bool) error {
106+
dmaAlreadyEnabled := ws.IsDMAEnabled()
107+
if !enabled || dmaAlreadyEnabled {
108+
if !enabled && dmaAlreadyEnabled {
109+
ws.dma.Unclaim()
110+
ws.dma = dmaChannel{} // Invalidate DMA channel.
111+
}
112+
return nil
113+
}
114+
channel, ok := _DMA.ClaimChannel()
115+
if !ok {
116+
return errDMAUnavail
117+
}
118+
channel.dl = ws.dma.dl // Copy deadline.
119+
ws.dma = channel
120+
return nil
121+
}
122+
123+
func (ws *WS2812B) writeDMA(w []uint32) error {
124+
dreq := dmaPIO_TxDREQ(ws.sm)
125+
err := ws.dma.Push32(&ws.sm.TxReg().Reg, w, dreq)
126+
if err != nil {
127+
return err
128+
}
129+
return nil
130+
}
131+
132+
// IsDMAEnabled returns true if DMA is enabled.
133+
func (ws *WS2812B) IsDMAEnabled() bool {
134+
return ws.dma.IsValid()
135+
}

rp2-pio/piolib/ws2812b.pio

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
; Timings according to Datasheet
2+
; T0H = 400ns
3+
; T0L = 850ns
4+
; T1H = 800ns
5+
; T1L = 450ns
6+
; Thus, a whole cycle is 1250ns. This will be our base unit of transmission.
7+
; Next unit of time is one that is closest to T0H and T1L.
8+
; If we divide baseline by 3 we get 1250/3 = 416.67, falling between 400 and 450.
9+
; Since we need more than 1 instruction per high/low time, we examine cases where
10+
; we split the split baseline in 3 and then calculate result PIO clockdiv since 3 is minimum
11+
; number of instructions to do first part of high time.
12+
;
13+
; From the equations:
14+
; piofreq = 256*clockfreq / (256*whole + frac)
15+
; where period = 1e9/freq => freq = 1e9/period, so:
16+
; 1e9/period = 256*clockfreq / (256*whole + frac) =>
17+
; 256*whole + frac = 256*clockfreq*period/1e9
18+
; So, the previous equations yield:
19+
; 416.67/3=138.89 -> ???
20+
21+
.program ws2812b_led
22+
public entry_point:
23+
pull ifempty block ; Do autopull, blocks if Tx empty.
24+
bitloop:
25+
; 3 instructions high logic level
26+
set pins, 1 ; Drive pin high at start of pulse.
27+
out y, 1 ; Shift 1 bit out, and write it to y.
28+
jmp !y lolo ; Jump to LOW bit part.
29+
jmp hilo [2] ; Jump to
30+
31+
lolo:
32+
set pins, 0 [2]; To create T0L we need 6 cycles, 3 of them are part of hi branch.
33+
hilo:
34+
set pins, 0
35+
jmp !osre bitloop [1]
36+
37+
% go {
38+
//go:build rp2040
39+
package piolib
40+
41+
import (
42+
pio "github.com/tinygo-org/pio/rp2-pio"
43+
)
44+
%}

rp2-pio/piolib/ws2812b_pio.go

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)