Skip to content

Commit cb83af7

Browse files
committed
improved recommended solutions
1 parent 859efb3 commit cb83af7

File tree

1 file changed

+57
-23
lines changed

1 file changed

+57
-23
lines changed

_drafts/why-sleep-for-is-broken-on-esp32.md

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ calls. Here it is:
411411
```c
412412
int usleep(useconds_t us)
413413
{
414-
const int us_per_tick = portTICK_PERIOD_MS * 1000;
414+
const int64_t us_per_tick = portTICK_PERIOD_MS * 1000;
415415
if (us < us_per_tick) {
416416
esp_rom_delay_us((uint32_t) us);
417417
} else {
@@ -572,16 +572,18 @@ int usleep(useconds_t us)
572572
if (us < us_per_tick) {
573573
esp_rom_delay_us((uint32_t) us);
574574
} else {
575-
/* Tick-based sleep may return up to (n-1) tick periods due to tick ISR
576-
being asynchronous to vTaskDelay() call. Specification states we must
577-
sleep at least the specified time, or longer.
575+
/* vTaskDelay may return up to (n-1) tick periods due to the tick ISR
576+
being asynchronous to the call. We must sleep at least the specified
577+
time, or longer. Checking the monotonic clock allows making an
578+
additional call to vTaskDelay when needed to ensure minimal time is
579+
actually slept. Adding `us_per_tick - 1` prevents ever passing 0 to
580+
vTaskDelay().
578581
*/
579-
uint64_t start_us = esp_timer_get_time();
580-
uint64_t now_us = start_us;
581-
uint64_t target_us = start_us + us;
582+
uint64_t now_us = esp_time_impl_get_time();
583+
uint64_t target_us = now_us + us;
582584
do {
583-
vTaskDelay((TickType_t)((target_us - now_us) / us_per_tick)+1);
584-
now_us = esp_timer_get_time();
585+
vTaskDelay((((target_us - now_us) + us_per_tick - 1) / us_per_tick));
586+
now_us = esp_time_impl_get_time();
585587
} while (now_us < target_us);
586588
}
587589
return 0;
@@ -597,25 +599,57 @@ I opened a [PR](https://github.com/espressif/esp-idf/pull/15132) with this
597599
change to IDF. Hopefully it gets approved.
598600

599601
Since I didn't want to work with a custom, patched IDF, I ended up replacing all
600-
calls to `std::this_thread::sleep_for()` with our own function with the same
601-
signature.
602+
calls to `std::this_thread::sleep_for()` with our own function. It has the same
603+
default signature, with the optional ability to specify a "sleep strategy." We
604+
can now force the custom `sleep_for()` to busy wait or to yield:
602605

603606
```c++
607+
enum class SleepStrategy {
608+
Default, // Platform decides when to use busy wait vs. yield
609+
PreciseBusyWait, // Busy wait, which is usually very precise
610+
EfficientYield // Efficently yield to other threads, but will often sleep longer than specified
611+
};
612+
604613
template<typename _Rep, typename _Period>
605-
static void sleep_for(const std::chrono::duration<_Rep, _Period>& rtime) {
606-
if (rtime <= rtime.zero()) return;
607-
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(rtime).count();
608-
uint64_t start_us = esp_timer_get_time();
609-
uint64_t now_us = start_us;
610-
uint64_t target_us = start_us + us;
611-
do {
612-
vTaskDelay((TickType_t)((target_us - now_us) / us_per_tick)+1);
613-
now_us = esp_timer_get_time();
614-
} while (now_us < target_us);
615-
#endif
614+
static void sleep_for(const std::chrono::duration<_Rep, _Period>& rtime, SleepStrategy strat = SleepStrategy::Default) {
615+
616+
static constexpr int64_t us_per_tick = portTICK_PERIOD_MS * 1000;
617+
if (rtime <= rtime.zero()) return;
618+
auto us = std::chrono::duration_cast<std::chrono::microseconds>(rtime).count();
619+
620+
if (strat == SleepStrategy::Default && (us < us_per_tick)) {
621+
// Mimic how std::this_thread::sleep_for() would act, which is to
622+
// (eventually) call usleep() after going through libstdc++. We only do
623+
// this for periods less than a tick because for longer periods the
624+
// implementation is broken (often returns in shorter than specified
625+
// time). If `usleep()` is fixed then we will update this to always call
626+
// it for the default strategy.
627+
usleep(us);
628+
return;
629+
}
630+
631+
uint64_t now_us = esp_timer_get_time();
632+
const uint64_t target_us = now_us + us;
633+
do {
634+
if (strat == SleepStrategy::PreciseBusyWait) {
635+
// This is an "internal and unstable API" according to ESP, but
636+
// `usleep()` is just a wrapper for it anyway. If it does change,
637+
// this function needs to be updated.
638+
esp_rom_delay_us(target_us - now_us);
639+
} else {
640+
// Ensure we never call vTaskDelay(0)
641+
static constexpr int prevent_zero_ticks = us_per_tick - 1;
642+
vTaskDelay((TickType_t)(((target_us - now_us) + prevent_zero_ticks) / us_per_tick));
643+
}
644+
// Validate against monotonic clock
645+
now_us = esp_timer_get_time();
646+
} while (now_us < target_us);
616647
}
617648
```
618649

650+
This approach gives us the minimal change needed to ensure things work correctly
651+
while allowing more control over how to perform the sleep when using C++.
652+
619653
## Conclusion
620654

621655
I cut my teeth on bare metal C code where *everything* was statically allocated.
@@ -633,7 +667,7 @@ instruction pipelines.
633667
It seems today that using C++ for firmware brings up a lot of strong reactions.
634668
A lot of embedded people hate it. A lot of people love it. For myself, I think
635669
it can be a great tool, but it does have much complexity you need to get right,
636-
especially when using it on an MCU. This seems to be a good example of such.
670+
*especially* when using it on an MCU. This seems to be a good example of such.
637671

638672
I sincerely hope `usleep()` is fixed. Until then, don't use
639673
`std::this_thread::sleep_for()` in your IDF v5 projects. It's a waste of time!

0 commit comments

Comments
 (0)