mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +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.
55 lines
1.9 KiB
C
55 lines
1.9 KiB
C
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
|
|
// 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
|
|
}
|