diff --git a/ws2812/gen-ws2812-arm.go b/ws2812/gen-ws2812-arm.go new file mode 100644 index 0000000..b9b38a2 --- /dev/null +++ b/ws2812/gen-ws2812-arm.go @@ -0,0 +1,185 @@ +// +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. + +// Clock frequencies to support, in MHz. +var clockFrequencies = []int{16, 48, 64, 120} + +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 + // 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.200 / cycleTimeNS)) + maxCyclesT0H := int(math.Floor(0.500 / cycleTimeNS)) + minCyclesT1H := int(math.Ceil(0.550 / cycleTimeNS)) + maxCyclesT1H := int(math.Floor(5.500 / cycleTimeNS)) + minCyclesTLD := int(math.Ceil(0.450 / 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(`// +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) + } + } +} diff --git a/ws2812/ws2812-asm_cortexm.go b/ws2812/ws2812-asm_cortexm.go new file mode 100644 index 0000000..0c0847e --- /dev/null +++ b/ws2812/ws2812-asm_cortexm.go @@ -0,0 +1,354 @@ +// +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" +) + +func (d Device) writeByte16(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + // Timings: + // T0H: 4 - 6 cycles or 250.0ns - 375.0ns + // T1H: 9 - 11 cycles or 562.5ns - 687.5ns + // TLD: 8 - cycles or 500.0ns - + mask := interrupt.Disable() + value := uint32(c) << 24 + device.AsmFull(` + 1: @ send_bit + str {maskSet}, {portSet} @ [2] T0H and T0L start here + lsls {value}, #1 @ [1] + bcs.n 2f @ [1/3] skip_store + str {maskClear}, {portClear} @ [2] T0H -> T0L transition + 2: @ skip_store + nop + nop + nop + nop + nop + str {maskClear}, {portClear} @ [2] T1H -> T1L transition + nop + nop + nop + nop + subs {i}, #1 @ [1] + bne.n 1b @ [1/3] send_bit + `, map[string]interface{}{ + "value": value, + "i": 8, + "maskSet": maskSet, + "portSet": portSet, + "maskClear": maskClear, + "portClear": portClear, + }) + interrupt.Restore(mask) +} + +func (d Device) writeByte48(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + // Timings: + // T0H: 10 - 12 cycles or 208.3ns - 250.0ns + // T1H: 27 - 29 cycles or 562.5ns - 604.2ns + // TLD: 22 - cycles or 458.3ns - + mask := interrupt.Disable() + value := uint32(c) << 24 + device.AsmFull(` + 1: @ send_bit + str {maskSet}, {portSet} @ [2] T0H and T0L start here + nop + nop + nop + nop + nop + nop + lsls {value}, #1 @ [1] + bcs.n 2f @ [1/3] skip_store + str {maskClear}, {portClear} @ [2] T0H -> T0L transition + 2: @ skip_store + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + str {maskClear}, {portClear} @ [2] T1H -> T1L transition + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + subs {i}, #1 @ [1] + bne.n 1b @ [1/3] send_bit + `, map[string]interface{}{ + "value": value, + "i": 8, + "maskSet": maskSet, + "portSet": portSet, + "maskClear": maskClear, + "portClear": portClear, + }) + interrupt.Restore(mask) +} + +func (d Device) writeByte64(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + // Timings: + // T0H: 13 - 15 cycles or 203.1ns - 234.4ns + // T1H: 36 - 38 cycles or 562.5ns - 593.8ns + // TLD: 29 - cycles or 453.1ns - + mask := interrupt.Disable() + value := uint32(c) << 24 + device.AsmFull(` + 1: @ send_bit + str {maskSet}, {portSet} @ [2] T0H and T0L start here + nop + nop + nop + nop + nop + nop + nop + nop + nop + lsls {value}, #1 @ [1] + bcs.n 2f @ [1/3] skip_store + str {maskClear}, {portClear} @ [2] T0H -> T0L transition + 2: @ skip_store + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + str {maskClear}, {portClear} @ [2] T1H -> T1L transition + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + subs {i}, #1 @ [1] + bne.n 1b @ [1/3] send_bit + `, map[string]interface{}{ + "value": value, + "i": 8, + "maskSet": maskSet, + "portSet": portSet, + "maskClear": maskClear, + "portClear": portClear, + }) + interrupt.Restore(mask) +} + +func (d Device) writeByte120(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + // Timings: + // T0H: 24 - 26 cycles or 200.0ns - 216.7ns + // T1H: 66 - 68 cycles or 550.0ns - 566.7ns + // TLD: 54 - cycles or 450.0ns - + mask := interrupt.Disable() + value := uint32(c) << 24 + device.AsmFull(` + 1: @ send_bit + str {maskSet}, {portSet} @ [2] T0H and T0L start here + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + lsls {value}, #1 @ [1] + bcs.n 2f @ [1/3] skip_store + str {maskClear}, {portClear} @ [2] T0H -> T0L transition + 2: @ skip_store + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + str {maskClear}, {portClear} @ [2] T1H -> T1L transition + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + subs {i}, #1 @ [1] + bne.n 1b @ [1/3] send_bit + `, map[string]interface{}{ + "value": value, + "i": 8, + "maskSet": maskSet, + "portSet": portSet, + "maskClear": maskClear, + "portClear": portClear, + }) + interrupt.Restore(mask) +} diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index d87d12e..8e9e283 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -1,6 +1,8 @@ // Package ws2812 implements a driver for WS2812 and SK6812 RGB LED strips. package ws2812 // import "tinygo.org/x/drivers/ws2812" +//go:generate go run gen-ws2812-arm.go + import ( "errors" "image/color" diff --git a/ws2812/ws2812_cortexm.go b/ws2812/ws2812_cortexm.go new file mode 100644 index 0000000..8c10803 --- /dev/null +++ b/ws2812/ws2812_cortexm.go @@ -0,0 +1,34 @@ +// +build cortexm + +package ws2812 + +// This file implements the WS2812 protocol for various Cortex-M +// microcontrollers. It is intended to work with various variants: M0, M0+, M3, +// and M4. Because machine.CPUFrequency() is usually a constant, the value will +// usually be constant-propagated and the switch below will be a direct +// (inlinable function) - thus there is usually no code size penalty over build +// tags per CPU speed. + +import ( + "machine" +) + +// Send a single byte using the WS2812 protocol. +func (d Device) WriteByte(c byte) error { + switch machine.CPUFrequency() { + case 16_000_000: // 16MHz + d.writeByte16(c) + return nil + case 48_000_000: // 48MHz + d.writeByte48(c) + return nil + case 64_000_000: // 64MHz + d.writeByte64(c) + return nil + case 120_000_000: // 120MHz + d.writeByte120(c) + return nil + default: + return errUnknownClockSpeed + } +} diff --git a/ws2812/ws2812_m0_16m.go b/ws2812/ws2812_m0_16m.go deleted file mode 100644 index d44e7a5..0000000 --- a/ws2812/ws2812_m0_16m.go +++ /dev/null @@ -1,55 +0,0 @@ -// +build nrf51 - -package ws2812 - -// This file implements the WS2812 protocol for 16MHz Cortex-M0 -// microcontrollers. - -import ( - "device/arm" - "runtime/interrupt" -) - -// Send a single byte using the WS2812 protocol. -func (d Device) WriteByte(c byte) error { - // For the Cortex-M0 at 16MHz - portSet, maskSet := d.Pin.PortMaskSet() - portClear, maskClear := d.Pin.PortMaskClear() - mask := interrupt.Disable() - - // See: - // https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ - // Note: timings have been increased slightly to also support ws2811 LEDs. - // T0H: 5 cycles or 312.5ns - // T0L: 14 cycles or 875.0ns -> together 19 cycles or 1187.5ns - // T1H: 11 cycles or 687.5ns - // T1H: 8 cycles or 500.0ns -> together 19 cycles or 1187.5ns - value := uint32(c) << 24 - arm.AsmFull(` - 1: @ send_bit - str {maskSet}, {portSet} @ [2] T0H and T0L start here - nop @ [1] - lsls {value}, #1 @ [1] - bcs.n 2f @ [1/3] skip_store - str {maskClear}, {portClear} @ [2] T0H -> T0L transition - 2: @ skip_store - nop @ [4] - nop - nop - nop - str {maskClear}, {portClear} @ [2] T1H -> T1L transition - nop @ [2] - nop - subs {i}, #1 @ [1] - bne.n 1b @ [1/3] send_bit - `, map[string]interface{}{ - "value": value, - "i": 8, - "maskSet": maskSet, - "portSet": portSet, - "maskClear": maskClear, - "portClear": portClear, - }) - interrupt.Restore(mask) - return nil -} diff --git a/ws2812/ws2812_m0_48m.go b/ws2812/ws2812_m0_48m.go deleted file mode 100644 index e42ab74..0000000 --- a/ws2812/ws2812_m0_48m.go +++ /dev/null @@ -1,84 +0,0 @@ -// +build atsamd21 - -package ws2812 - -// This file implements the WS2812 protocol for 48MHz Cortex-M0 -// microcontrollers. - -import ( - "device/arm" - "runtime/interrupt" -) - -// Send a single byte using the WS2812 protocol. -func (d Device) WriteByte(c byte) error { - // For the Cortex-M0 at 48MHz - portSet, maskSet := d.Pin.PortMaskSet() - portClear, maskClear := d.Pin.PortMaskClear() - mask := interrupt.Disable() - - // See: - // https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ - // T0H: 10 cycles or 208.3ns - // T0L: 39 cycles or 812.5ns -> together 49 cycles or 1020.8ns - // T1H: 27 cycles or 562.5ns - // T1L: 22 cycles or 458.3ns -> together 49 cycles or 1020.8ns - value := uint32(c) << 24 - arm.AsmFull(` - 1: @ send_bit - str {maskSet}, {portSet} @ [2] T0H and T0L start here - lsls {value}, #1 @ [1] - nop @ [6] - nop - nop - nop - nop - nop - bcs.n 2f @ [1/3] skip_store - str {maskClear}, {portClear} @ [2] T0H -> T0L transition - 2: @ skip_store - nop @ [15] - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - str {maskClear}, {portClear} @ [2] T1H -> T1L transition - nop @ [16] - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - subs {i}, #1 @ [1] - bne.n 1b @ [1/3] send_bit - `, map[string]interface{}{ - "value": value, - "i": 8, - "maskSet": maskSet, - "portSet": portSet, - "maskClear": maskClear, - "portClear": portClear, - }) - interrupt.Restore(mask) - return nil -} diff --git a/ws2812/ws2812_m4_120m.go b/ws2812/ws2812_m4_120m.go deleted file mode 100644 index ec87297..0000000 --- a/ws2812/ws2812_m4_120m.go +++ /dev/null @@ -1,176 +0,0 @@ -// +build atsamd51 atsame5x - -package ws2812 - -// This file implements the WS2812 protocol for 120MHz Cortex-M4 -// microcontrollers. -// Note: This implementation does not work with tinygo 0.9.0 or older. - -import ( - "device/arm" - "runtime/interrupt" -) - -// Send a single byte using the WS2812 protocol. -func (d Device) WriteByte(c byte) error { - // For the Cortex-M4 at 120MHz - portSet, maskSet := d.Pin.PortMaskSet() - portClear, maskClear := d.Pin.PortMaskClear() - mask := interrupt.Disable() - - // See: - // https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ - // T0H: 32-34 cycles or 266.67ns - 283.33ns - // T0L: 101-103 cycles or 841.67ns - 858.33ns - // +: 133-137 cycles or 1108.33ns - 1141.67ns - // T1H: 73-75 cycles or 608.33ns - 625.00ns - // T1L: 58-60 cycles or 483.33ns - 500.00ns - // +: 131-135 cycles or 1091.67ns - 1125.00ns - // A branch is treated here as 1-3 cycles, because apparently it might get - // speculated. This is more of a guess than hard fact, because the only docs - // by ARM that state this are now considered superseded (by what?). - value := uint32(c) << 24 - arm.AsmFull(` - 1: @ send_bit - str {maskSet}, {portSet} @ [2] T0H and T0L start here - lsls {value}, #1 @ [1] - nop @ [28] - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - bcs.n 2f @ [1-3] skip_store - str {maskClear}, {portClear} @ [2] T0H -> T0L transition - 2: @ skip_store - nop @ [41] - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - str {maskClear}, {portClear} @ [2] T1H -> T1L transition - nop @ [54] - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - subs {i}, #1 @ [1] - bne.n 1b @ [1-3] send_bit - `, map[string]interface{}{ - "value": value, - "i": 8, - "maskSet": maskSet, - "portSet": portSet, - "maskClear": maskClear, - "portClear": portClear, - }) - interrupt.Restore(mask) - return nil -} diff --git a/ws2812/ws2812_m4_64m.go b/ws2812/ws2812_m4_64m.go deleted file mode 100644 index 74a514e..0000000 --- a/ws2812/ws2812_m4_64m.go +++ /dev/null @@ -1,113 +0,0 @@ -// +build nrf52 nrf52840 nrf52833 - -package ws2812 - -// This file implements the WS2812 protocol for 64MHz Cortex-M4 -// microcontrollers. - -import ( - "device/arm" - "runtime/interrupt" -) - -// Send a single byte using the WS2812 protocol. -func (d Device) WriteByte(c byte) error { - // For the Cortex-M4 at 64MHz - portSet, maskSet := d.Pin.PortMaskSet() - portClear, maskClear := d.Pin.PortMaskClear() - mask := interrupt.Disable() - - // See: - // https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ - // T0H: 17-19 cycles or 265.63ns - 296.88ns - // T0L: 54-56 cycles or 843.75ns - 875.00ns - // +: 71-75 cycles or 1109.38ns - 1171.88ns - // T1H: 39-41 cycles or 609.38ns - 640.63ns - // T1L: 30-32 cycles or 468.75ns - 500.0ns - // +: 69-73 cycles or 1078.13ns - 1140.63ns - // A branch is treated here as 1-3 cycles, because apparently it might get - // speculated. This is more of a guess than hard fact, because the only docs - // by ARM that state this are now considered superseded (by what?). - value := uint32(c) << 24 - arm.AsmFull(` - 1: @ send_bit - str {maskSet}, {portSet} @ [2] T0H and T0L start here - lsls {value}, #1 @ [1] - nop @ [13] - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - bcs.n 2f @ [1-3] skip_store - str {maskClear}, {portClear} @ [2] T0H -> T0L transition - 2: @ skip_store - nop @ [22] - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - str {maskClear}, {portClear} @ [2] T1H -> T1L transition - nop @ [26] - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - nop - subs {i}, #1 @ [1] - bne.n 1b @ [1-3] send_bit - `, map[string]interface{}{ - "value": value, - "i": 8, - "maskSet": maskSet, - "portSet": portSet, - "maskClear": maskClear, - "portClear": portClear, - }) - interrupt.Restore(mask) - return nil -}