//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 `) 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) } } }