mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-07 00:13:42 +00:00
2d32995f6a
The possible branch distance is a lot shorter on ARMv6M (Cortex-M and Cortex-M0+) for conditional branches. Therefore, convert this long conditional branch into an unconditional branch. This probably makes the code a little bit slower but because it is in the low period of the WS2812 signal it shouldn't matter for the protocol. And it avoids difficult workarounds specifically for the RP2040.
287 lines
12 KiB
Go
287 lines
12 KiB
Go
//go:build none
|
|
// +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 Cortex-M chips and 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).
|
|
// It is certainly possible to extend this to other architectures, such as AVR
|
|
// and RISC-V if 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
|
|
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(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",
|
|
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(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
|
|
`,
|
|
},
|
|
}
|
|
|
|
func writeImplementation(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")
|
|
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, " // 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, " mask := interrupt.Disable()\n")
|
|
fmt.Fprintf(buf, " value := %s\n", archImpl.valueTemplate)
|
|
asm := archImpl.template
|
|
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, " device.AsmFull(`%s`, map[string]interface{}{", asm)
|
|
buf.WriteString(`
|
|
"value": value,
|
|
"i": 8,
|
|
"maskSet": maskSet,
|
|
"portSet": portSet,
|
|
"maskClear": maskClear,
|
|
"portClear": portClear,
|
|
})
|
|
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)
|
|
fmt.Fprintln(f, "// +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 (
|
|
"device"
|
|
"runtime/interrupt"
|
|
)
|
|
`)
|
|
for _, megahertz := range clockFrequencies {
|
|
err := writeImplementation(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)
|
|
}
|
|
}
|
|
}
|