mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-03 06:27:47 +00:00
9fdf0d657d
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.
58 lines
1.8 KiB
Go
58 lines
1.8 KiB
Go
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)
|
|
}
|
|
}
|