mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-16 12:53:23 +00:00
ws2812: add support for the ESP32-C3 processor
This adds support to the WS2812 for the ESP32-C3 processor which is a RISC-V processor from Espressif. Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
//go:build !digispark && !arduino && !arduino_uno
|
//go:build !digispark && !arduino && !arduino_uno && !xiao_esp32c3
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build xiao_esp32c3
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "machine"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Replace neo in the code below to match the pin
|
||||||
|
// that you are using if different.
|
||||||
|
neo = machine.D6
|
||||||
|
}
|
||||||
@@ -93,6 +93,7 @@ tinygo build -size short -o ./build/test.bin -target=m5stamp-c3 ./examp
|
|||||||
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
|
||||||
tinygo build -size short -o ./build/test.hex -target=arduino ./examples/ws2812
|
tinygo build -size short -o ./build/test.hex -target=arduino ./examples/ws2812
|
||||||
tinygo build -size short -o ./build/test.hex -target=digispark ./examples/ws2812
|
tinygo build -size short -o ./build/test.hex -target=digispark ./examples/ws2812
|
||||||
|
tinygo build -size short -o ./build/test.bin -target=xiao-esp32c3 ./examples/ws2812
|
||||||
tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bme280/main.go
|
tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bme280/main.go
|
||||||
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/microphone/main.go
|
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/microphone/main.go
|
||||||
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/buzzer/main.go
|
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/buzzer/main.go
|
||||||
|
|||||||
+49
-5
@@ -17,7 +17,8 @@ import (
|
|||||||
// the new assembly implementation - no fiddly timings to calculate and no nops
|
// the new assembly implementation - no fiddly timings to calculate and no nops
|
||||||
// to count!
|
// to count!
|
||||||
//
|
//
|
||||||
// Right now this is specific to Cortex-M chips and assume the following things:
|
// 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.
|
// - Arithmetic operations (shift, add, sub) take up 1 clock cycle.
|
||||||
// - The nop instruction also takes up 1 clock cycle.
|
// - The nop instruction also takes up 1 clock cycle.
|
||||||
// - Store instructions (to the GPIO pins) take up 2 clock cycles.
|
// - Store instructions (to the GPIO pins) take up 2 clock cycles.
|
||||||
@@ -25,8 +26,15 @@ import (
|
|||||||
// depends on whether the branch is taken or not. On the M4, the documentation
|
// 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
|
// is less clear but it appears the instruction is still 1 to 3 cycles
|
||||||
// (possibly including some branch prediction).
|
// (possibly including some branch prediction).
|
||||||
// It is certainly possible to extend this to other architectures, such as AVR
|
// On RISC-V chips it assumes the following things:
|
||||||
// and RISC-V if needed.
|
// - 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:
|
// 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/
|
// https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
|
||||||
@@ -45,6 +53,7 @@ type architectureImpl struct {
|
|||||||
maxBaseCyclesT1H int
|
maxBaseCyclesT1H int
|
||||||
minBaseCyclesTLD int
|
minBaseCyclesTLD int
|
||||||
valueTemplate string // template for how to pass the 'c' byte to assembly
|
valueTemplate string // template for how to pass the 'c' byte to assembly
|
||||||
|
funcAttr string // C function attribute (default: always_inline)
|
||||||
template string // assembly template
|
template string // assembly template
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +92,7 @@ var architectures = map[string]architectureImpl{
|
|||||||
// - branches are 1 or 3 cycles, depending on branch prediction
|
// - branches are 1 or 3 cycles, depending on branch prediction
|
||||||
// - ALU operations are 1 cycle (as on most CPUs)
|
// - ALU operations are 1 cycle (as on most CPUs)
|
||||||
// Hopefully this generalizes to other chips.
|
// Hopefully this generalizes to other chips.
|
||||||
buildTag: "tinygo.riscv32",
|
buildTag: "tinygo.riscv32 && !esp32c3",
|
||||||
minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store
|
minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store
|
||||||
maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store
|
maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store
|
||||||
minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store
|
minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store
|
||||||
@@ -103,6 +112,37 @@ var architectures = map[string]architectureImpl{
|
|||||||
@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
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
"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
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -208,7 +248,11 @@ func writeCAssembly(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, "__attribute__((always_inline))\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", megahertz)
|
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, " // 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)
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
//go:build esp32c3
|
||||||
|
|
||||||
|
package ws2812
|
||||||
|
|
||||||
|
// Warning: autogenerated file. Instead of modifying this file, change
|
||||||
|
// gen-ws2812.go and run "go generate".
|
||||||
|
|
||||||
|
import "runtime/interrupt"
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
__attribute__((section(".iram1"), noinline))
|
||||||
|
void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||||
|
// Timings:
|
||||||
|
// T0H: 56 - 58 cycles or 350.0ns - 362.5ns
|
||||||
|
// T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns
|
||||||
|
// TLD: 184 - cycles or 1150.0ns -
|
||||||
|
uint32_t value = (uint32_t)c << 23;
|
||||||
|
char i = 8;
|
||||||
|
__asm__ __volatile__(
|
||||||
|
"1: // send_bit\n"
|
||||||
|
"\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t slli %[value], %[value], 1 // [1] shift value left by 1\n"
|
||||||
|
"\t bltz %[value], 2f // [1/3] skip_store\n"
|
||||||
|
"\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n"
|
||||||
|
"\t2: // skip_store\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t nop\n"
|
||||||
|
"\t addi %[i], %[i], -1 // [1]\n"
|
||||||
|
"\t bnez %[i], 1b // [1/3] send_bit\n"
|
||||||
|
: [value]"+r"(value),
|
||||||
|
[i]"+r"(i)
|
||||||
|
: [maskSet]"r"(maskSet),
|
||||||
|
[portSet]"m"(*portSet),
|
||||||
|
[maskClear]"r"(maskClear),
|
||||||
|
[portClear]"m"(*portClear));
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
func (d Device) writeByte160(c byte) {
|
||||||
|
portSet, maskSet := d.Pin.PortMaskSet()
|
||||||
|
portClear, maskClear := d.Pin.PortMaskClear()
|
||||||
|
|
||||||
|
mask := interrupt.Disable()
|
||||||
|
C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
|
||||||
|
|
||||||
|
interrupt.Restore(mask)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build tinygo.riscv32
|
//go:build tinygo.riscv32 && !esp32c3
|
||||||
|
|
||||||
package ws2812
|
package ws2812
|
||||||
|
|
||||||
@@ -11,378 +11,6 @@ import "unsafe"
|
|||||||
/*
|
/*
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
__attribute__((always_inline))
|
|
||||||
void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
|
||||||
// Timings:
|
|
||||||
// T0H: 56 - 58 cycles or 350.0ns - 362.5ns
|
|
||||||
// T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns
|
|
||||||
// TLD: 184 - cycles or 1150.0ns -
|
|
||||||
uint32_t value = (uint32_t)c << 23;
|
|
||||||
char i = 8;
|
|
||||||
__asm__ __volatile__(
|
|
||||||
"1: // send_bit\n"
|
|
||||||
"\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t slli %[value], %[value], 1 // [1] shift value left by 1\n"
|
|
||||||
"\t bltz %[value], 2f // [1/3] skip_store\n"
|
|
||||||
"\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n"
|
|
||||||
"\t2: // skip_store\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t nop\n"
|
|
||||||
"\t addi %[i], %[i], -1 // [1]\n"
|
|
||||||
"\t bnez %[i], 1b // [1/3] send_bit\n"
|
|
||||||
: [value]"+r"(value),
|
|
||||||
[i]"+r"(i)
|
|
||||||
: [maskSet]"r"(maskSet),
|
|
||||||
[portSet]"m"(*portSet),
|
|
||||||
[maskClear]"r"(maskClear),
|
|
||||||
[portClear]"m"(*portClear));
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((always_inline))
|
__attribute__((always_inline))
|
||||||
void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||||
// Timings:
|
// Timings:
|
||||||
@@ -1109,16 +737,6 @@ void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_
|
|||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
func (d Device) writeByte160(c byte) {
|
|
||||||
portSet, maskSet := d.Pin.PortMaskSet()
|
|
||||||
portClear, maskClear := d.Pin.PortMaskClear()
|
|
||||||
|
|
||||||
mask := interrupt.Disable()
|
|
||||||
C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
|
|
||||||
|
|
||||||
interrupt.Restore(mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Device) writeByte320(c byte) {
|
func (d Device) writeByte320(c byte) {
|
||||||
portSet, maskSet := d.Pin.PortMaskSet()
|
portSet, maskSet := d.Pin.PortMaskSet()
|
||||||
portClear, maskClear := d.Pin.PortMaskClear()
|
portClear, maskClear := d.Pin.PortMaskClear()
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
package ws2812 // import "tinygo.org/x/drivers/ws2812"
|
package ws2812 // import "tinygo.org/x/drivers/ws2812"
|
||||||
|
|
||||||
//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 160 168 200
|
//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 160 168 200
|
||||||
//go:generate go run gen-ws2812.go -arch=tinygoriscv 160 320
|
//go:generate go run gen-ws2812.go -arch=tinygoriscv 320
|
||||||
|
//go:generate go run gen-ws2812.go -arch=esp32c3 160
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build esp32c3
|
||||||
|
|
||||||
|
package ws2812
|
||||||
|
|
||||||
|
import "machine"
|
||||||
|
|
||||||
|
// Send a single byte using the WS2812 protocol.
|
||||||
|
func (d Device) WriteByte(c byte) error {
|
||||||
|
switch machine.CPUFrequency() {
|
||||||
|
case 160_000_000: // 160MHz
|
||||||
|
d.writeByte160(c)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errUnknownClockSpeed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build tinygo.riscv32
|
//go:build tinygo.riscv32 && !esp32c3
|
||||||
|
|
||||||
package ws2812
|
package ws2812
|
||||||
|
|
||||||
@@ -7,9 +7,6 @@ import "machine"
|
|||||||
// Send a single byte using the WS2812 protocol.
|
// Send a single byte using the WS2812 protocol.
|
||||||
func (d Device) WriteByte(c byte) error {
|
func (d Device) WriteByte(c byte) error {
|
||||||
switch machine.CPUFrequency() {
|
switch machine.CPUFrequency() {
|
||||||
case 160_000_000: // 160MHz, e.g. esp32c3
|
|
||||||
d.writeByte160(c)
|
|
||||||
return nil
|
|
||||||
case 320_000_000: // 320MHz, e.g. fe310
|
case 320_000_000: // 320MHz, e.g. fe310
|
||||||
d.writeByte320(c)
|
d.writeByte320(c)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user