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
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812
@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
@md5sum ./build/test.hex
ifneq ($(AVR), 0)
+68 -37
View File
@@ -60,21 +60,21 @@ var architectures = map[string]architectureImpl{
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",
valueTemplate: "(uint32_t)c << 24",
template: `
1: @ send_bit
str {maskSet}, {portSet} @ [2] T0H and T0L start here
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
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
str %[maskClear], %[portClear] @ [2] T1H -> T1L transition
@DELAY3
subs {i}, #1 @ [1]
beq.n 3f @ [1/3] end
b 1b @ [1/3] send_bit
subs %[i], #1 @ [1]
beq.n 3f @ [1/3] end
b 1b @ [1/3] send_bit
3: @ end
`,
},
@@ -90,25 +90,25 @@ var architectures = map[string]architectureImpl{
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",
valueTemplate: "(uint32_t)c << 23",
template: `
1: // send_bit
sw {maskSet}, {portSet} // [1] T0H and T0L start here
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
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
sw %[maskClear], %[portClear] // [1] T1H -> T1L transition
@DELAY3
addi {i}, {i}, -1 // [1]
bnez {i}, 1b // [1/3] send_bit
addi %[i], %[i], -1 // [1]
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)
// These timings are taken from the table "Updated simplified timing
// constraints for NeoPixel strings" at:
@@ -209,30 +209,51 @@ func writeImplementation(f *os.File, arch string, megahertz int) error {
// 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, "__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, " // 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)
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, " 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(`
"value": value,
"i": 8,
"maskSet": maskSet,
"portSet": portSet,
"maskClear": maskClear,
"portClear": portClear,
})
interrupt.Restore(mask)
}
`)
@@ -271,16 +292,26 @@ package ws2812
// Warning: autogenerated file. Instead of modifying this file, change
// gen-ws2812.go and run "go generate".
import (
"device"
"runtime/interrupt"
)
import "runtime/interrupt"
/*
#include <stdint.h>
`)
for _, megahertz := range clockFrequencies {
err := writeImplementation(f, *arch, megahertz)
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)
}
}
}
+1031 -1004
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff