@@ -411,7 +411,7 @@ calls. Here it is:
411411``` c
412412int 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
597599change to IDF. Hopefully it gets approved.
598600
599601Since 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+
604613template<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
621655I cut my teeth on bare metal C code where * everything* was statically allocated.
@@ -633,7 +667,7 @@ instruction pipelines.
633667It seems today that using C++ for firmware brings up a lot of strong reactions.
634668A lot of embedded people hate it. A lot of people love it. For myself, I think
635669it 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
638672I 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