diff --git a/delay/sleep.c b/delay/sleep.c new file mode 100644 index 0000000..bc19022 --- /dev/null +++ b/delay/sleep.c @@ -0,0 +1,54 @@ +#include +#include + +// Loop the given times, where one loop takes four CPU cycles. +bool tinygo_drivers_sleep(uint32_t cycles) { + // In this function, a [n] comment indicates the number of cycles an + // instruction or a set of instructions take. This is typically 1 for most + // arithmetic instructions, and a bit more for branches. +#if __ARM_ARCH_6M__ || __ARM_ARCH_7M__ || __ARM_ARCH_7EM__ + // Inline assembly for Cortex-M0/M0+/M3/M4/M7. + // The Cortex-M0 (but not M0+) takes one more cycle, so is off by 12.5%. + // Others should be basically cycle-accurate (with a slight overhead to + // calculate the number of cycles). Unfortunately, there doesn't appear to + // be a preprocessor macro to detect the Cortex-M0 specifically (although we + // could rely on macros like NRF51). + + // Each loop takes 8 cycles (5 nops, 1 sub, and 2 for the branch). + uint32_t loops = (cycles + 7) / 8; + __asm__ __volatile__( + "1:\n\t" + "nop\n\t" // [5] nops + "nop\n\t" + "nop\n\t" + "nop\n\t" + "nop\n\t" + "subs %[loops], #1\n\t" // [1] + "bne 1b" // [1-4], at least 2 cycles if taken + : [loops]"+r"(loops) + ); + return true; +#elif __XTENSA__ + // Inline assembly for Xtensa. + // I don't know exactly how many cycles a branch takes, so I've taken a + // conservative guess and assume it takes only one cycle. In practice, it's + // probably more than that. + uint32_t loops = (cycles + 7) / 8; + __asm__ __volatile__( + "1:\n\t" + "nop\n\t" // [6] nops + "nop\n\t" + "nop\n\t" + "nop\n\t" + "nop\n\t" + "nop\n\t" + "addi %[loops], %[loops], -1\n\t" // [1] + "bnez %[loops], 1b" // [1?] + : [loops]"+r"(loops) + ); + return true; +#else + // Unknown architecture, so fall back to time.Sleep. + return false; +#endif +} diff --git a/delay/sleep.go b/delay/sleep.go new file mode 100644 index 0000000..4b098fd --- /dev/null +++ b/delay/sleep.go @@ -0,0 +1,57 @@ +package delay + +import ( + "machine" + "time" +) + +/* +#include +#include +bool tinygo_drivers_sleep(uint32_t ticks); +*/ +import "C" + +// Sleep for a very precise short duration by busy-waiting for the given time. +// This is not an efficient way to sleep: it will needlessly burn cycles while +// sleeping. But it is useful for sleeping for a very short duration, for +// example for bit-banged protocols. +// +// Longer durations (longer than a few milliseconds) will be handled by calling +// time.Sleep instead. +// +// This function should be called with a constant duration value, in which case +// the call will typically be fully inlined and only take up around nine +// instructions for the entire loop. +// +//go:inline +func Sleep(duration time.Duration) { + if time.Duration(uint32(duration)&0xff_ffff) != duration { + // This is a long duration (more than 16ms) which shouldn't be done by + // busy-waiting. + time.Sleep(duration) + return + } + + // Calculate the number of cycles we should sleep: + // cycles = duration * freq / 1e9 + // Avoiding a 64-bit division: + // cycles = duration * (freq/1000_000) / 1000 + // + // This assumes: + // * The CPU frequency is a constant and can trivially be + // const-propagated, therefore the divide by 1000_000 is done at compile + // time. + // * The CPU frequency is a multiple of 1000_000, which is true for most + // chips (examples: 16MHz, 48MHz, 120MHz, etc). + // * The division by 1000 can be done efficiently (Cortex-M3 and up), or + // can be fully const-propagated. + // * The CPU frequency is lower than 256MHz. If it is higher, long sleep + // times (1-16ms) may not work correctly. + cycles := uint32(duration) * (machine.CPUFrequency() / 1000_000) / 1000 + slept := C.tinygo_drivers_sleep(cycles) + if !slept { + // Fallback for platforms without inline assembly support. + time.Sleep(duration) + } +} diff --git a/examples/delay/delay.go b/examples/delay/delay.go new file mode 100644 index 0000000..00f5777 --- /dev/null +++ b/examples/delay/delay.go @@ -0,0 +1,17 @@ +package main + +import ( + "time" + + "tinygo.org/x/drivers/delay" +) + +func main() { + time.Sleep(time.Second) // wait for a serial console + start := time.Now() + for i := 0; i < 2000; i++ { + delay.Sleep(50 * time.Microsecond) + } + duration := time.Since(start) + println("sleep of 2000*50µs (100ms) took:", duration.String()) +}