Skip to content

Commit f69b543

Browse files
committed
chore: add date to interrupt live callout
1 parent fc995b3 commit f69b543

File tree

1 file changed

+18
-17
lines changed

1 file changed

+18
-17
lines changed

_posts/2025-02-20-why-sleep-for-is-broken-on-esp32.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ performance, but only in the new version of IDF due to an interaction between
1919
<!-- excerpt end -->
2020

2121
> 🎬
22-
> _[Listen to Steve on Interrupt Live](https://youtube.com/live/dwL-PI7TuDY?feature=share)
23-
> talk about the content and motivations behind writing this article._
22+
> [Listen to Steve on Interrupt Live](https://youtube.com/live/dwL-PI7TuDY?feature=share)
23+
> at **9AM PT | 12PM ET | 6PM CET on Friday, March 7th** talk about the content
24+
> and motivations behind writing this article.
2425
2526
{% include newsletter.html %}
2627

@@ -160,7 +161,7 @@ hardware timer to do it.
160161
All of this is to say the following:
161162

162163
1. If you care about precision timing, details matter.
163-
2. If you *don't care* about precision timing but are using it, you are probably
164+
2. If you _don't care_ about precision timing but are using it, you are probably
164165
wasting resources.
165166

166167
## C++: the Solution to and Cause of All My Problems
@@ -447,9 +448,9 @@ use `usleep()` instead? Well, `usleep()` is just a wrapper to
447448
consider `usleep()` the "public" exposure of `esp_rom_delay_us()`, but only when
448449
the specified time is less than a system tick period. As mentioned above, this
449450
is a busy wait, and since it does not disable the scheduler, it still allows
450-
other threads *of equal or higher priority* to run. So, the timing represents a
451-
guaranteed *minimum* only. More importantly, if there are other threads of lower
452-
priority, it will *not* context switch during this busy time. It will just sit
451+
other threads _of equal or higher priority_ to run. So, the timing represents a
452+
guaranteed _minimum_ only. More importantly, if there are other threads of lower
453+
priority, it will _not_ context switch during this busy time. It will just sit
453454
in the thread until the wait is over.
454455
455456
This is all good. A guaranteed minimum is how I expect `usleep()` to work.
@@ -463,11 +464,11 @@ say:
463464
> longer than one FreeRTOS tick period. If the time is shorter, the thread will
464465
> busy-wait instead of yielding to another RTOS task.
465466
466-
It should say for sleeping *equal to* or longer than one tick period cause
467+
It should say for sleeping _equal to_ or longer than one tick period cause
467468
yielding vs. busy waiting. In any case, the yielding is done via `vTaskDelay()`.
468469
469470
There is a problem here though. The ticks to yield calculations often produce
470-
times yielded *less than* the specified amount.
471+
times yielded _less than_ the specified amount.
471472
472473
Let's play out an example. If we wanted to sleep for 15 milliseconds, the
473474
calculations would give us `vTaskDelay(2)`:
@@ -488,7 +489,7 @@ Even though the comment says it is rounding up to compensate for the first tick
488489
potentially not blocking at all, the compensation does not account for the
489490
worse-case minimal timing. In the example I gave, a 15-millisecond request will
490491
sometimes only sleep for 10 milliseconds. Likewise, a 10 millisecond `usleep()`
491-
will sometimes sleep about 0 milliseconds. The greatest *potential* differential
492+
will sometimes sleep about 0 milliseconds. The greatest _potential_ differential
492493
comes with calling `usleep()` with a multiple of the tick period. In that case,
493494
the time spent may be short by an entire tick period.
494495
@@ -500,7 +501,7 @@ a problem.
500501
501502
According to `man 3 sleep` and
502503
[POSIX](https://pubs.opengroup.org/onlinepubs/009695399/functions/usleep.html),
503-
`usleep()` should always sleep *at least* the time specified. It is allowed to
504+
`usleep()` should always sleep _at least_ the time specified. It is allowed to
504505
sleep more if needed.
505506
506507
> The usleep() function shall cause the calling thread to be suspended from
@@ -523,7 +524,7 @@ the thread will sleep between 0 and 10 milliseconds. It will usually sleep for
523524
less than the time specified.
524525
525526
In IDF v5, calling `std::this_thread::sleep_for(10ms)` almost always calls
526-
`usleep()` *twice*. The first time will use `vTaskDelay(1)`, and it will usually
527+
`usleep()` _twice_. The first time will use `vTaskDelay(1)`, and it will usually
527528
sleep for less than the time specified. Then, back in `libstdc++`
528529
`__sleep_for()`, the monotonic clock will be checked and it will be seen that
529530
some fractional component of 10 milliseconds remains, causing a second call to
@@ -547,20 +548,20 @@ system tick period, so the blocking `esp_rom_delay_us()` is now called.
547548
548549
So what about time slicing? Even if `esp_rom_delay_us()` blocks, the FreeRTOS
549550
scheduler can switch to another task. Firstly, if this thread is of a higher
550-
priority, *no lower priorities will ever run*. But even if everything is of the
551+
priority, _no lower priorities will ever run_. But even if everything is of the
551552
same priority, the CPU will just switch back to the blocking call on the next
552553
round robin, continuing the blocking wait. In our current scenario, this is
553554
horribly inefficient, unnecessary, and unexpected.
554555
555556
Any call to `sleep_for()` greater than the tick period has this problem because
556557
the tick interrupt is asynchronous to the `sleep_for()` call. This means when
557558
the scheduler returns from `vTaskDelay()` some random remainder of time will be
558-
done with `esp_rom_delay_us()` in order to sleep for the *precise* amount of
559+
done with `esp_rom_delay_us()` in order to sleep for the _precise_ amount of
559560
time requested.
560561
561562
The new version of `sleep_for()` is much more precise, but it is at the cost of
562563
computing efficiency on the ESP32 because some fraction of the tick period will
563-
be *busy waited* instead of yielded. That is very bad to do on an MCU.
564+
be _busy waited_ instead of yielded. That is very bad to do on an MCU.
564565
565566
Of course, none of this is transparent to the application code, and I doubt it
566567
was something intentional from Espressif. It is just a consequence of upgrading
@@ -572,7 +573,7 @@ Did Espressif actually implement `usleep()` wrong? Yes. It needs to be fixed.
572573
573574
For periods at or longer than the system tick, `usleep()` can return before the
574575
specified time. It shouldn't do that. It must error on the side of sleeping too
575-
long to ensure it *never* sleeps too little. So yes, it is broken in my view.
576+
long to ensure it _never_ sleeps too little. So yes, it is broken in my view.
576577
`stdlibc++` isn't to blame.
577578
578579
Since `usleep()` is sometimes short by 1 system tick period, we could just add
@@ -671,7 +672,7 @@ while allowing more control over how to perform the sleep when using C++.
671672

672673
## Conclusion
673674

674-
I cut my teeth on bare metal C code where *everything* was statically allocated.
675+
I cut my teeth on bare metal C code where _everything_ was statically allocated.
675676
No `malloc()`. No floating point math because there was no FPU. Custom linker
676677
scripts. Debugging using GPIO pins and an oscilloscope. Using precalculated
677678
value tables to save a few microseconds in an ISR. We ran at 24 MHz. At that
@@ -686,7 +687,7 @@ instruction pipelines.
686687
It seems today that using C++ for firmware brings up a lot of strong reactions.
687688
A lot of embedded people hate it. A lot of people love it. For myself, I think
688689
it can be a great tool, but it does have much complexity you need to get right,
689-
*especially* when using it on an MCU. This seems to be a good example of such.
690+
_especially_ when using it on an MCU. This seems to be a good example of such.
690691

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

0 commit comments

Comments
 (0)