mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-15 04:13:40 +00:00
delay: add new package for cycle-accurate delays
This is implemented in inline assembly using machine.CPUFrequency() to
know how long a single CPU cycle takes. As long as it is called with a
constant duration, it should be fully inlined and all values can be
const-propagated resulting in very tight inline assembly.
For example, when I convert i2csoft to use this delay function, the
entire delay function compiles to something like this:
8784: movs r6, #100
8786: mov r0, r6
8788: nop
878a: nop
878c: nop
878e: nop
8790: nop
8792: subs r0, #1
8794: bne 0x8788
That means that all the math to calculate the number of cycles is
entirely optimized away (in this case, to 100 loops).
I ran the example on a few boards to see how well it works:
| board | 100ms wait | CPU core
|-----------------------|------------|------
| microbit | 121.6ms | Cortex-M0 so it has 12% overhead
| circuitplay-express | 100.1ms | Cortex-M0+ so it is cycle accurate
| pico | 100.2ms | Cortex-M0+
| pyportal | 100.3ms | Cortex-M4
| circuitplay-bluefruit | 125.8ms | Cortex-M4
| esp8266 | 125.1ms |
This shows that there is some loop overhead because of conservative
estimates, but note that even though there may be a 25% overhead, the
actual overhead per `delay.Sleep()` call is very small. It should be
good enough for software I2C at least, and can potentially be improved
in the future.
This commit is contained in:
committed by
Ron Evans
parent
b0c9f259dc
commit
9fdf0d657d
@@ -0,0 +1,57 @@
|
||||
package delay
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user