-
-
Notifications
You must be signed in to change notification settings - Fork 245
Expand file tree
/
Copy pathgen-ws2812.go
More file actions
360 lines (339 loc) · 14.6 KB
/
Copy pathgen-ws2812.go
File metadata and controls
360 lines (339 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
//go:build none
package main
import (
"bytes"
"flag"
"fmt"
"math"
"os"
"strconv"
"strings"
)
// This file generates assembly to precisely time the WS2812 protocol for
// various chips. Just add a new frequency below and run `go generate` to add
// the new assembly implementation - no fiddly timings to calculate and no nops
// to count!
//
// Right now this is specific to specific chips:
// On Cortex-M chips it assume the following things:
// - Arithmetic operations (shift, add, sub) take up 1 clock cycle.
// - The nop instruction also takes up 1 clock cycle.
// - Store instructions (to the GPIO pins) take up 2 clock cycles.
// - Branch instructions can take up 1 to 3 clock cycles. On the Cortex-M0, this
// depends on whether the branch is taken or not. On the M4, the documentation
// is less clear but it appears the instruction is still 1 to 3 cycles
// (possibly including some branch prediction).
// On RISC-V chips it assumes the following things:
// - Arithmetic operations (shift, add, sub) take up 1 clock cycle.
// - The nop instruction also takes up 1 clock cycle.
// - Store instructions (to the GPIO pins) take up 1 clock cycle.
// - Branch instructions can take up 1 or 3 clock cycles, depending on branch
// prediction. This is based on the SiFive FE310 CPU, but hopefully it
// generalizes to other RISC-V chips as well.
// It is certainly possible to extend this to other architectures, such as AVR as needed.
//
// Here are two important resources. For the timings:
// https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
// For the assembly (more or less):
// https://cpldcpu.wordpress.com/2014/01/19/light_ws2812-library-v2-0/
// The timings deviate a little bit from the code here, but so far the timings
// from wp.josh.com seem to be fine for the ws2812.
// Architecture implementation. Describes the template and the timings of the
// blocks of instructions so that most code can remain architecture-independent.
type architectureImpl struct {
buildTag string
minBaseCyclesT0H int
maxBaseCyclesT0H int
minBaseCyclesT1H int
maxBaseCyclesT1H int
minBaseCyclesTLD int
valueTemplate string // template for how to pass the 'c' byte to assembly
funcAttr string // C function attribute (default: always_inline)
template string // assembly template
}
var architectures = map[string]architectureImpl{
"cortexm": {
// Assume that a branch is 1 to 3 cycles, no matter whether it's taken
// or not. This is a rather conservative estimate, for Cortex-M+ for
// example the instruction cycles are precisely known.
buildTag: "cortexm",
minBaseCyclesT0H: 1 + 1 + 2, // shift + branch (not taken) + store
maxBaseCyclesT0H: 1 + 3 + 2, // shift + branch (not taken) + store
minBaseCyclesT1H: 1 + 1 + 2, // shift + branch (taken) + store
maxBaseCyclesT1H: 1 + 3 + 2, // shift + branch (taken) + store
minBaseCyclesTLD: 1 + 2 + 2, // subtraction + branch x2 + store (in next cycle)
valueTemplate: "(uint32_t)c << 24",
template: `
1: @ send_bit
str %[maskSet], %[portSet] @ [2] T0H and T0L start here
@DELAY1
lsls %[value], #1 @ [1]
bcs.n 2f @ [1/3] skip_store
str %[maskClear], %[portClear] @ [2] T0H -> T0L transition
2: @ skip_store
@DELAY2
str %[maskClear], %[portClear] @ [2] T1H -> T1L transition
@DELAY3
subs %[i], #1 @ [1]
beq.n 3f @ [1/3] end
b 1b @ [1/3] send_bit
3: @ end
`,
},
"tinygoriscv": {
// Largely based on the SiFive FE310 CPU:
// - stores are 1 cycle
// - branches are 1 or 3 cycles, depending on branch prediction
// - ALU operations are 1 cycle (as on most CPUs)
// Hopefully this generalizes to other chips.
buildTag: "tinygo.riscv32 && !esp32c3",
minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store
maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store
minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store
maxBaseCyclesT1H: 1 + 3 + 1, // shift + branch (taken) + store
minBaseCyclesTLD: 1 + 1 + 1, // subtraction + branch + store (in next cycle)
valueTemplate: "(uint32_t)c << 23",
template: `
1: // send_bit
sw %[maskSet], %[portSet] // [1] T0H and T0L start here
@DELAY1
slli %[value], %[value], 1 // [1] shift value left by 1
bltz %[value], 2f // [1/3] skip_store
sw %[maskClear], %[portClear] // [1] T0H -> T0L transition
2: // skip_store
@DELAY2
sw %[maskClear], %[portClear] // [1] T1H -> T1L transition
@DELAY3
addi %[i], %[i], -1 // [1]
bnez %[i], 1b // [1/3] send_bit
`,
},
"esp32c3": {
// ESP32-C3 RISC-V core:
// - stores are 1 cycle
// - branches are 1 or 3 cycles
// - ALU operations are 1 cycle
// Uses the same instruction timing as the SiFive FE310, but the
// function is placed in IRAM instead of flash to avoid instruction
// cache miss stalls that would destroy WS2812 timing.
buildTag: "esp32c3",
minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store
maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store
minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store
maxBaseCyclesT1H: 1 + 3 + 1, // shift + branch (taken) + store
minBaseCyclesTLD: 1 + 1 + 1, // subtraction + branch + store (in next cycle)
valueTemplate: "(uint32_t)c << 23",
funcAttr: `__attribute__((section(".iram1"), noinline))`,
template: `
1: // send_bit
sw %[maskSet], %[portSet] // [1] T0H and T0L start here
@DELAY1
slli %[value], %[value], 1 // [1] shift value left by 1
bltz %[value], 2f // [1/3] skip_store
sw %[maskClear], %[portClear] // [1] T0H -> T0L transition
2: // skip_store
@DELAY2
sw %[maskClear], %[portClear] // [1] T1H -> T1L transition
@DELAY3
addi %[i], %[i], -1 // [1]
bnez %[i], 1b // [1/3] send_bit
`,
},
}
func writeCAssembly(f *os.File, arch string, megahertz int) error {
cycleTimeNS := 1 / float64(megahertz)
// These timings are taken from the table "Updated simplified timing
// constraints for NeoPixel strings" at:
// https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
// Here is a copy:
// Symbol Parameter Min Typical Max Units
// T0H 0 code, high voltage time 200 350 500 ns
// T1H 1 code, high voltage time 550 700 5500 ns
// TLD data, low voltage time 450 600 5000 ns
// TLL latch, low voltage time 6000 ns
// The equivalent table for WS2811 LEDs would be the following:
// Symbol Parameter Min Typical Max Units
// T0H 0 code, high voltage time 350 500 650 ns
// T1H 1 code, high voltage time 1050 1200 5500 ns
// TLD data, low voltage time 1150 1300 5000 ns
// TLL latch, low voltage time 6000 ns
// Combining the two (min and max) leads to the following table:
// Symbol Parameter Min Typical Max Units
// T0H 0 code, high voltage time 350 - 500 ns
// T1H 1 code, high voltage time 1050 - 5500 ns
// TLD data, low voltage time 1150 - 5000 ns
// TLL latch, low voltage time 6000 ns
// These comined timings are used so that the ws2812 package is compatible
// with both WS2812 and with WS2811 chips.
// T0H is the time the pin should be high to send a "0" bit.
// T1H is the time the pin should be high to send a "1" bit.
// TLD is the time the pin should be low between bits.
// TLL is the time the pin should be low to apply (latch) the new colors.
minCyclesT0H := int(math.Ceil(0.350 / cycleTimeNS))
maxCyclesT0H := int(math.Floor(0.500 / cycleTimeNS))
minCyclesT1H := int(math.Ceil(1.050 / cycleTimeNS))
maxCyclesT1H := int(math.Floor(5.500 / cycleTimeNS))
minCyclesTLD := int(math.Ceil(1.150 / cycleTimeNS))
// The assembly template looks something like this:
// 1: @ send_bit
// str {maskSet}, {portSet} @ [2] T0H and T0L start here
// ...delay 1
// lsls {value}, #1 @ [1]
// bcs.n 2f @ [1/3] skip_store
// str {maskClear}, {portClear} @ [2] T0H -> T0L transition
// 2: @ skip_store
// ...delay 2
// str {maskClear}, {portClear} @ [2] T1H -> T1L transition
// ...delay 3
// subs {i}, #1 @ [1]
// bne.n 1b @ [1/3] send_bit
//
// We need to calculate the number of nop instructions in the three delays.
archImpl, ok := architectures[arch]
if !ok {
return fmt.Errorf("unknown architecture: %s", arch)
}
// Determine number of nops for delay1. This is primarily based on the T0H
// delay, which is relatively short (<500ns).
delay1 := minCyclesT0H - archImpl.minBaseCyclesT0H
if delay1 < 0 {
// The minCyclesT0H constraint could not be satisfied. Don't insert
// nops, in the hope that it isn't too long.
delay1 = 0
}
if delay1+archImpl.maxBaseCyclesT0H > maxCyclesT0H {
return fmt.Errorf("MCU appears to be too slow to satisfy minimum requirements for the T0H signal")
}
actualMinCyclesT0H := archImpl.minBaseCyclesT0H + delay1
actualMaxCyclesT0H := archImpl.maxBaseCyclesT0H + delay1
actualMinNanosecondsT0H := float64(actualMinCyclesT0H) / float64(megahertz) * 1000
actualMaxNanosecondsT0H := float64(actualMaxCyclesT0H) / float64(megahertz) * 1000
// Determine number of nops for delay2. This is delay1 plus some extra time
// so that the pulse is long enough for T1H.
minBaseCyclesT1H := delay1 + archImpl.minBaseCyclesT1H // delay1 + asssembly cycles
maxBaseCyclesT1H := delay1 + archImpl.maxBaseCyclesT1H // delay1 + asssembly cycles
delay2 := minCyclesT1H - minBaseCyclesT1H
if delay2 < 0 {
delay2 = 0
}
if delay2+maxBaseCyclesT1H > maxCyclesT1H {
// Unlikely, we have 5500ns for this operation.
return fmt.Errorf("MCU appears to be too slow to satisfy minimum requirements for the T1H signal")
}
actualMinCyclesT1H := minBaseCyclesT1H + delay2
actualMaxCyclesT1H := maxBaseCyclesT1H + delay2
actualMinNanosecondsT1H := float64(actualMinCyclesT1H) / float64(megahertz) * 1000
actualMaxNanosecondsT1H := float64(actualMaxCyclesT1H) / float64(megahertz) * 1000
// Determine number of nops for delay3. This is based on the TLD delay, the
// time between two high pulses.
delay3 := minCyclesTLD - archImpl.minBaseCyclesTLD
if delay3 < 0 {
delay3 = 0
}
actualMinCyclesTLD := archImpl.minBaseCyclesTLD + delay3
actualMinNanosecondsTLD := float64(actualMinCyclesTLD) / float64(megahertz) * 1000
// Create the Go function in a buffer. Using a buffer here to be able to
// ignore I/O errors.
buf := &bytes.Buffer{}
fmt.Fprintf(buf, "\n")
funcAttr := archImpl.funcAttr
if funcAttr == "" {
funcAttr = "__attribute__((always_inline))"
}
fmt.Fprintf(buf, "%s\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", funcAttr, megahertz)
fmt.Fprintf(buf, " // Timings:\n")
fmt.Fprintf(buf, " // T0H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT0H, actualMaxCyclesT0H, actualMinNanosecondsT0H, actualMaxNanosecondsT0H)
fmt.Fprintf(buf, " // T1H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT1H, actualMaxCyclesT1H, actualMinNanosecondsT1H, actualMaxNanosecondsT1H)
fmt.Fprintf(buf, " // TLD: %2d - cycles or %.1fns -\n", actualMinCyclesTLD, actualMinNanosecondsTLD)
fmt.Fprintf(buf, " uint32_t value = %s;\n", archImpl.valueTemplate)
asm := archImpl.template
asm = strings.TrimSpace(asm)
asm = strings.ReplaceAll(asm, " @DELAY1\n", strings.Repeat(" nop\n", delay1))
asm = strings.ReplaceAll(asm, " @DELAY2\n", strings.Repeat(" nop\n", delay2))
asm = strings.ReplaceAll(asm, " @DELAY3\n", strings.Repeat(" nop\n", delay3))
asm = strings.ReplaceAll(asm, "\n", "\n\t")
fmt.Fprintf(buf, " char i = 8;\n")
fmt.Fprintf(buf, " __asm__ __volatile__(\n")
for _, line := range strings.Split(asm, "\n") {
fmt.Fprintf(buf, "\t\t%#v\n", line+"\n")
}
// Note: [value] and [i] must be input+output operands because they modify
// the value.
fmt.Fprintf(buf, ` : [value]"+r"(value),
[i]"+r"(i)
: [maskSet]"r"(maskSet),
[portSet]"m"(*portSet),
[maskClear]"r"(maskClear),
[portClear]"m"(*portClear));
}
`)
// Now write the buffer contents (with the assembly function) to a file.
_, err := f.Write(buf.Bytes())
return err
}
func writeGoWrapper(f *os.File, arch string, megahertz int) error {
// Create the Go function in a buffer. Using a buffer here to be able to
// ignore I/O errors.
buf := &bytes.Buffer{}
fmt.Fprintf(buf, "\n")
fmt.Fprintf(buf, "func (d Device) writeByte%d(c byte) {\n", megahertz)
fmt.Fprintf(buf, " portSet, maskSet := d.Pin.PortMaskSet()\n")
fmt.Fprintf(buf, " portClear, maskClear := d.Pin.PortMaskClear()\n")
fmt.Fprintf(buf, "\n")
fmt.Fprintf(buf, " mask := interrupt.Disable()\n")
fmt.Fprintf(buf, " C.ws2812_writeByte%d(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))\n", megahertz)
buf.WriteString(`
interrupt.Restore(mask)
}
`)
// Now write the buffer contents (with the assembly function) to a file.
_, err := f.Write(buf.Bytes())
return err
}
func main() {
arch := flag.String("arch", "cortexm", "architecture to output to")
flag.Parse()
// Remaining parameters are all clock frequencies.
var clockFrequencies []int
for _, s := range flag.Args() {
freq, err := strconv.Atoi(s)
if err != nil {
fmt.Fprintln(os.Stderr, "cannot parse frequency:", s)
os.Exit(1)
}
clockFrequencies = append(clockFrequencies, freq)
}
f, err := os.Create("ws2812-asm_" + *arch + ".go")
if err != nil {
fmt.Fprintln(os.Stderr, "could not generate WS2812 assembly code:", err)
os.Exit(1)
}
defer f.Close()
fmt.Fprintln(f, "//go:build", architectures[*arch].buildTag)
f.WriteString(`
package ws2812
// Warning: autogenerated file. Instead of modifying this file, change
// gen-ws2812.go and run "go generate".
import "runtime/interrupt"
import "unsafe"
/*
#include <stdint.h>
`)
for _, megahertz := range clockFrequencies {
err := writeCAssembly(f, *arch, megahertz)
if err != nil {
fmt.Fprintf(os.Stderr, "could not generate WS2812 assembly code for %s and %dMHz: %s\n", *arch, megahertz, err)
os.Exit(1)
}
}
f.WriteString(`*/
import "C"
`)
for _, megahertz := range clockFrequencies {
err := writeGoWrapper(f, *arch, megahertz)
if err != nil {
fmt.Fprintf(os.Stderr, "could not generate Go wrapper: %w\n", err)
os.Exit(1)
}
}
}