ws2812: write inline assembly using C instead of Go

This is better for various reasons:

  * I don't like the inline assembly implementation in TinyGo. It kinda
    works, but it's hard to use correctly.
  * The Go version had a subtle bug: the i and value variables weren't
    marked as modified. The compiler produced correct code by chance. In
    GCC-style inline assembly, it's possible to correctly mark it as an
    input+output operand.
  * LLVM 14 has some changes to inline assembly that change how memory
    operands can be created. Instead of supporting that, I'd like to get
    rid of memory operands entirely (and possibly inline assembly in
    general).

I did some light testing: the ARM assembly changed a bit but not in any
way that should have a practical effect, and the RISC-V assembly didn't
change at all.
This commit is contained in:
Ayke van Laethem
2022-04-14 16:19:07 +02:00
committed by Ron Evans
parent f2f470973d
commit 06e298c514
4 changed files with 2207 additions and 2135 deletions
+2
View File
@@ -159,6 +159,8 @@ smoke-test:
@md5sum ./build/test.hex @md5sum ./build/test.hex
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812 tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812
@md5sum ./build/test.hex @md5sum ./build/test.hex
tinygo build -size short -o ./build/test.bin -target=m5stamp-c3 ./examples/ws2812
@md5sum ./build/test.bin
tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go
@md5sum ./build/test.hex @md5sum ./build/test.hex
ifneq ($(AVR), 0) ifneq ($(AVR), 0)
+68 -37
View File
@@ -60,21 +60,21 @@ var architectures = map[string]architectureImpl{
minBaseCyclesT1H: 1 + 1 + 2, // shift + branch (taken) + store minBaseCyclesT1H: 1 + 1 + 2, // shift + branch (taken) + store
maxBaseCyclesT1H: 1 + 3 + 2, // shift + branch (taken) + store maxBaseCyclesT1H: 1 + 3 + 2, // shift + branch (taken) + store
minBaseCyclesTLD: 1 + 2 + 2, // subtraction + branch x2 + store (in next cycle) minBaseCyclesTLD: 1 + 2 + 2, // subtraction + branch x2 + store (in next cycle)
valueTemplate: "uint32(c) << 24", valueTemplate: "(uint32_t)c << 24",
template: ` template: `
1: @ send_bit 1: @ send_bit
str {maskSet}, {portSet} @ [2] T0H and T0L start here str %[maskSet], %[portSet] @ [2] T0H and T0L start here
@DELAY1 @DELAY1
lsls {value}, #1 @ [1] lsls %[value], #1 @ [1]
bcs.n 2f @ [1/3] skip_store bcs.n 2f @ [1/3] skip_store
str {maskClear}, {portClear} @ [2] T0H -> T0L transition str %[maskClear], %[portClear] @ [2] T0H -> T0L transition
2: @ skip_store 2: @ skip_store
@DELAY2 @DELAY2
str {maskClear}, {portClear} @ [2] T1H -> T1L transition str %[maskClear], %[portClear] @ [2] T1H -> T1L transition
@DELAY3 @DELAY3
subs {i}, #1 @ [1] subs %[i], #1 @ [1]
beq.n 3f @ [1/3] end beq.n 3f @ [1/3] end
b 1b @ [1/3] send_bit b 1b @ [1/3] send_bit
3: @ end 3: @ end
`, `,
}, },
@@ -90,25 +90,25 @@ var architectures = map[string]architectureImpl{
minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store
maxBaseCyclesT1H: 1 + 3 + 1, // shift + branch (taken) + store maxBaseCyclesT1H: 1 + 3 + 1, // shift + branch (taken) + store
minBaseCyclesTLD: 1 + 1 + 1, // subtraction + branch + store (in next cycle) minBaseCyclesTLD: 1 + 1 + 1, // subtraction + branch + store (in next cycle)
valueTemplate: "uint32(c) << 23", valueTemplate: "(uint32_t)c << 23",
template: ` template: `
1: // send_bit 1: // send_bit
sw {maskSet}, {portSet} // [1] T0H and T0L start here sw %[maskSet], %[portSet] // [1] T0H and T0L start here
@DELAY1 @DELAY1
slli {value}, {value}, 1 // [1] shift value left by 1 slli %[value], %[value], 1 // [1] shift value left by 1
bltz {value}, 2f // [1/3] skip_store bltz %[value], 2f // [1/3] skip_store
sw {maskClear}, {portClear} // [1] T0H -> T0L transition sw %[maskClear], %[portClear] // [1] T0H -> T0L transition
2: // skip_store 2: // skip_store
@DELAY2 @DELAY2
sw {maskClear}, {portClear} // [1] T1H -> T1L transition sw %[maskClear], %[portClear] // [1] T1H -> T1L transition
@DELAY3 @DELAY3
addi {i}, {i}, -1 // [1] addi %[i], %[i], -1 // [1]
bnez {i}, 1b // [1/3] send_bit bnez %[i], 1b // [1/3] send_bit
`, `,
}, },
} }
func writeImplementation(f *os.File, arch string, megahertz int) error { func writeCAssembly(f *os.File, arch string, megahertz int) error {
cycleTimeNS := 1 / float64(megahertz) cycleTimeNS := 1 / float64(megahertz)
// These timings are taken from the table "Updated simplified timing // These timings are taken from the table "Updated simplified timing
// constraints for NeoPixel strings" at: // constraints for NeoPixel strings" at:
@@ -209,30 +209,51 @@ func writeImplementation(f *os.File, arch string, megahertz int) error {
// ignore I/O errors. // ignore I/O errors.
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
fmt.Fprintf(buf, "\n") fmt.Fprintf(buf, "\n")
fmt.Fprintf(buf, "func (d Device) writeByte%d(c byte) {\n", megahertz) fmt.Fprintf(buf, "__attribute__((always_inline))\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\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, " // Timings:\n")
fmt.Fprintf(buf, " // T0H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT0H, actualMaxCyclesT0H, actualMinNanosecondsT0H, actualMaxNanosecondsT0H) 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, " // 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, " // TLD: %2d - cycles or %.1fns -\n", actualMinCyclesTLD, actualMinNanosecondsTLD)
fmt.Fprintf(buf, " mask := interrupt.Disable()\n") fmt.Fprintf(buf, " uint32_t value = %s;\n", archImpl.valueTemplate)
fmt.Fprintf(buf, " value := %s\n", archImpl.valueTemplate)
asm := archImpl.template asm := archImpl.template
asm = strings.TrimSpace(asm)
asm = strings.ReplaceAll(asm, " @DELAY1\n", strings.Repeat(" nop\n", delay1)) 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, " @DELAY2\n", strings.Repeat(" nop\n", delay2))
asm = strings.ReplaceAll(asm, " @DELAY3\n", strings.Repeat(" nop\n", delay3)) asm = strings.ReplaceAll(asm, " @DELAY3\n", strings.Repeat(" nop\n", delay3))
asm = strings.ReplaceAll(asm, "\n", "\n\t") asm = strings.ReplaceAll(asm, "\n", "\n\t")
fmt.Fprintf(buf, " device.AsmFull(`%s`, map[string]interface{}{", asm) 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), portSet, portClear, maskSet, maskClear)\n", megahertz)
buf.WriteString(` buf.WriteString(`
"value": value,
"i": 8,
"maskSet": maskSet,
"portSet": portSet,
"maskClear": maskClear,
"portClear": portClear,
})
interrupt.Restore(mask) interrupt.Restore(mask)
} }
`) `)
@@ -271,16 +292,26 @@ package ws2812
// Warning: autogenerated file. Instead of modifying this file, change // Warning: autogenerated file. Instead of modifying this file, change
// gen-ws2812.go and run "go generate". // gen-ws2812.go and run "go generate".
import ( import "runtime/interrupt"
"device"
"runtime/interrupt" /*
) #include <stdint.h>
`) `)
for _, megahertz := range clockFrequencies { for _, megahertz := range clockFrequencies {
err := writeImplementation(f, *arch, megahertz) err := writeCAssembly(f, *arch, megahertz)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "could not generate WS2812 assembly code for %s and %dMHz: %s\n", *arch, megahertz, err) fmt.Fprintf(os.Stderr, "could not generate WS2812 assembly code for %s and %dMHz: %s\n", *arch, megahertz, err)
os.Exit(1) 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)
}
}
} }
+1031 -1004
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff