//go:build none // +build none package main import ( "bytes" "fmt" "math" "os" "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. // Clock frequencies to support, in MHz. var clockFrequencies = []int{16, 48, 64, 120, 168} func writeImplementation(f *os.File, 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)) // Assembly template: // 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. // Determine number of nops for delay1. This is primarily based on the T0H // delay, which is relatively short (<500ns). minBaseCyclesT0H := 1 + 1 + 2 // shift + branch + store maxBaseCyclesT0H := 1 + 3 + 2 // shift + branch + store delay1 := minCyclesT0H - 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+maxBaseCyclesT0H > maxCyclesT0H { return fmt.Errorf("MCU appears to be too slow to satisfy minimum requirements for the T0H signal") } actualMinCyclesT0H := minBaseCyclesT0H + delay1 actualMaxCyclesT0H := 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 + 1 + 1 + 2 // delay1 + shift + branch + store maxBaseCyclesT1H := delay1 + 1 + 3 + 2 // delay1 + shift + branch + store 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. minBaseCyclesTLD := 1 + 1 + 2 // subtraction + branch + store (in next cycle) delay3 := minCyclesTLD - minBaseCyclesTLD if delay3 < 0 { delay3 = 0 } actualMinCyclesTLD := 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 := uint32(c) << 24\n") fmt.Fprintf(buf, " device.AsmFull(`\n") fmt.Fprintf(buf, " 1: @ send_bit\n") fmt.Fprintf(buf, " str {maskSet}, {portSet} @ [2] T0H and T0L start here\n") buf.WriteString(strings.Repeat(" nop\n", delay1)) fmt.Fprintf(buf, " lsls {value}, #1 @ [1]\n") fmt.Fprintf(buf, " bcs.n 2f @ [1/3] skip_store\n") fmt.Fprintf(buf, " str {maskClear}, {portClear} @ [2] T0H -> T0L transition\n") fmt.Fprintf(buf, " 2: @ skip_store\n") buf.WriteString(strings.Repeat(" nop\n", delay2)) fmt.Fprintf(buf, " str {maskClear}, {portClear} @ [2] T1H -> T1L transition\n") buf.WriteString(strings.Repeat(" nop\n", delay3)) fmt.Fprintf(buf, " subs {i}, #1 @ [1]\n") fmt.Fprintf(buf, " bne.n 1b @ [1/3] send_bit\n") fmt.Fprintf(buf, " `, map[string]interface{}{") 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() { f, err := os.Create("ws2812-asm_cortexm.go") if err != nil { fmt.Fprintln(os.Stderr, "could not generate WS2812 assembly code:", err) os.Exit(1) } defer f.Close() f.WriteString(`//go:build cortexm // +build cortexm package ws2812 // Warning: autogenerated file. Instead of modifying this file, change // gen-ws2812-arm.go and run "go generate". import ( "device" "runtime/interrupt" ) `) for _, megahertz := range clockFrequencies { err := writeImplementation(f, megahertz) if err != nil { fmt.Fprintf(os.Stderr, "could not generate WS2812 assembly code for %dMHz: %s\n", megahertz, err) os.Exit(1) } } }