Best approach to run code in a task exactly every millisecond
Best approach to run code in a task exactly every millisecond
Hello,
My application consists of mainly two parts: UART communication and LED control. The UART communication might run any time, depending on what's sent on the external bus. When the LED control is used, it needs to perform accurate fading control where the brightness (PWM duty) is updated continuously towards the target value.
The observed issue is that the fading jitters whenever there's some external bus activity, it's not smooth anymore in that moment and jumps a bit. UART reading and message processing is done in two tasks with priority 1 and 2 (will change that to values around 10). The LED fading control is performed in a timer that runs every 10 ms.
As far as I could read now, timers always run at priority 1 with no core affinity (could be changed, but I also use timers for the communication, so I can't separate those domains with timers).
I'm going to change the LED control timers to a dedicated task that runs the regular code in a loop. The task will have a priority of 30 and be pinned to core 1, whereas I'm pinning comm tasks to core 0 together with the timer. Communication is not time-critical, but LED control is because any delays during fading are well visible to the user.
So I'll have a separate task that needs to update the LED controller every 10 ms. Or even better, every 1 ms, now that I can do that. (Timer resolution is limited to 10 ms; again, if not reconfiguring the tick frequency.) How would I do this? It's also important that the tick frequency in my task is somewhat stable and doesn't drift depending of how long each LED controller update takes. So an external time-keeper is preferred over a constant delay in the loop.
Can I use esp_timer_get_time() and taskYIELD() here? I could set a time baseline, add the desired next run time and just wait for esp_timer_get_time() to return something greater than the baseline. It would run much more often than every millisecond and only yield to allow the idle task to run, too. Is there a more efficient approach? Also, the regular updates are only needed during fadings. As soon as the target brightness has been reached, the task has nothing to do until the next brightness target is set through external communication. Would I use some kind of semaphore here? (The task would self-suspend on core 1 and be resumed from core 0; it must be prevented that race conditions dead-lock the task.) Energy efficiency is not a big concern here, but if it can save something and stay cooler, I'll take that.
I couldn't find any references to this problem online. So nothing to learn from yet.
On the smaller AVR platform (8-bit single-core MCU) I'd just keep the CPU busy, but ESP32/FreeRTOS seems to be treated more like a "big" multi-tasking OS.
My application consists of mainly two parts: UART communication and LED control. The UART communication might run any time, depending on what's sent on the external bus. When the LED control is used, it needs to perform accurate fading control where the brightness (PWM duty) is updated continuously towards the target value.
The observed issue is that the fading jitters whenever there's some external bus activity, it's not smooth anymore in that moment and jumps a bit. UART reading and message processing is done in two tasks with priority 1 and 2 (will change that to values around 10). The LED fading control is performed in a timer that runs every 10 ms.
As far as I could read now, timers always run at priority 1 with no core affinity (could be changed, but I also use timers for the communication, so I can't separate those domains with timers).
I'm going to change the LED control timers to a dedicated task that runs the regular code in a loop. The task will have a priority of 30 and be pinned to core 1, whereas I'm pinning comm tasks to core 0 together with the timer. Communication is not time-critical, but LED control is because any delays during fading are well visible to the user.
So I'll have a separate task that needs to update the LED controller every 10 ms. Or even better, every 1 ms, now that I can do that. (Timer resolution is limited to 10 ms; again, if not reconfiguring the tick frequency.) How would I do this? It's also important that the tick frequency in my task is somewhat stable and doesn't drift depending of how long each LED controller update takes. So an external time-keeper is preferred over a constant delay in the loop.
Can I use esp_timer_get_time() and taskYIELD() here? I could set a time baseline, add the desired next run time and just wait for esp_timer_get_time() to return something greater than the baseline. It would run much more often than every millisecond and only yield to allow the idle task to run, too. Is there a more efficient approach? Also, the regular updates are only needed during fadings. As soon as the target brightness has been reached, the task has nothing to do until the next brightness target is set through external communication. Would I use some kind of semaphore here? (The task would self-suspend on core 1 and be resumed from core 0; it must be prevented that race conditions dead-lock the task.) Energy efficiency is not a big concern here, but if it can save something and stay cooler, I'll take that.
I couldn't find any references to this problem online. So nothing to learn from yet.
On the smaller AVR platform (8-bit single-core MCU) I'd just keep the CPU busy, but ESP32/FreeRTOS seems to be treated more like a "big" multi-tasking OS.
-
nopnop2002
- Posts: 362
- Joined: Thu Oct 03, 2019 10:52 pm
- Contact:
Re: Best approach to run code in a task exactly every millisecond
>Can I use esp_timer_get_time() and taskYIELD() here?
The answer is an incomplete YES.
The task priority must be set to 1 or lower.
The time required for taskYIELD() has a large margin of error.
And the time required for taskYIELD() changes depending on the task priority.
The reason is probably related to the priority of system background tasks.
So the time required for taskYIELD() will be greatly affected by the number and priorities of other tasks.
When the task priority is 2 or higher, task switching is not possible and task_wdt occurs.
To ensure stable task switching regardless of the number of tasks or their priorities,
VtaskDelay(1) must be used.
The answer is an incomplete YES.
The task priority must be set to 1 or lower.
Code: Select all
#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
static const char *TAG = "MAIN";
void task(void *pvParameter)
{
int64_t time1 = 0;
int64_t time2 = 0;
int64_t time3 = 0;
while(1) {
time1 = esp_timer_get_time();
taskYIELD();
//esp_rom_delay_us(10000);
//vTaskDelay(1);
time2 = esp_timer_get_time();
time3 = time2 - time1;
ESP_LOGI(pcTaskGetName(NULL), "time3=%lld us", time3);
}
vTaskDelete( NULL );
}
void app_main()
{
//UBaseType_t uxPriority = 0;
UBaseType_t uxPriority = 1;
//UBaseType_t uxPriority = 2;
xTaskCreate(&task, "Task1", 2048, NULL, uxPriority, NULL);
xTaskCreate(&task, "Task2", 2048, NULL, uxPriority, NULL);
}
The time required for taskYIELD() has a large margin of error.
Code: Select all
I (263) Task1: time3=2611 us
I (263) Task2: time3=2595 us
I (263) Task1: time3=2610 us
I (273) Task2: time3=328 us
I (273) Task1: time3=12 us
I (273) Task2: time3=6 us
I (273) Task1: time3=11 us
I (273) Task2: time3=11 us
And the time required for taskYIELD() changes depending on the task priority.
The reason is probably related to the priority of system background tasks.
So the time required for taskYIELD() will be greatly affected by the number and priorities of other tasks.
Code: Select all
//Priority = 0
I (29443) Task1: time3=9904 us
I (29453) Task2: time3=9903 us
I (29453) Task1: time3=9904 us
I (29463) Task2: time3=9903 us
I (29463) Task1: time3=9904 us
I (29473) Task2: time3=9903 us
I (29473) Task1: time3=9904 us
I (29483) Task2: time3=9903 us
//Priority =1
I (8233) Task1: time3=11 us
I (8233) Task2: time3=11 us
I (8233) Task1: time3=11 us
I (8233) Task2: time3=12 us
I (8243) Task1: time3=11 us
I (8243) Task2: time3=11 us
I (8243) Task1: time3=12 us
When the task priority is 2 or higher, task switching is not possible and task_wdt occurs.
Code: Select all
I (5282) TE (5282) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time:
E (5282) task_wdt: - IDLE0 (CPU 0)
E (5282) task_wdt: Tasks currently running:
E (5282) task_wdt: CPU 0: Task1
E (5282) task_wdt: CPU 1: IDLE1
E (5282) task_wdt: Aborting.
E (5282) task_wdt: Print CPU 0 (current core) backtrace
To ensure stable task switching regardless of the number of tasks or their priorities,
VtaskDelay(1) must be used.
Last edited by nopnop2002 on Wed May 21, 2025 12:47 am, edited 1 time in total.
Re: Best approach to run code in a task exactly every millisecond
Hi
why not use hardware fade ???
https://docs.espressif.com/projects/esp ... g-hardware
all the problems of unevenness will disappear.
why not use hardware fade ???
https://docs.espressif.com/projects/esp ... g-hardware
all the problems of unevenness will disappear.
Re: Best approach to run code in a task exactly every millisecond
Did I understand it correctly that this will wait until the next tick to resume the task? That might be 0..19 ms away with a tick frequency of 100 Hz. I might need to increase that then.To ensure stable task switching regardless of the number of tasks or their priorities,
VtaskDelay(1) must be used.
Still, that's the constant delay in the loop that I mentioned. Depending on how the code runtime aligns with the ticks, my interval drifts away from the ideal fixed interval. A 60-second fading might end up taking 60 to 70 seconds or so.
Maybe. And if I read it right, I would buy other unpleasant problems. The fade cannot be stopped once it's started. Meaning if the light fades out over 10 seconds after the user has left the room, it still needs to go completely dark before it can turn on again if the user returns quickly. Any other automation control is also blocked during that period. And the fading seems to be linear only. I'm using ease-in-out to achieve a natural effect, and also apply the necessary exponential factor to account for logarithmic human eye light perception. I've seen linear PWM duty fadings and they just look ugly and cheap. So this hardware thing isn't practical in reality.why not use hardware fade ???
(...)
all the problems of unevenness will disappear.
I can imagine it were practical if it called my function each time to ask for the new duty, and if it could be aborted at any time. But then I could also do this myself.
Maybe for that I need a hardware timer that gives me an interrupt regularly? I didn't want to do the floating point calculations and ledc_ calls in an ISR though. Maybe it needs even more complexity and have an ISR send a signal to wake up a high-priority task only waiting for the signal? (With a semaphore or the like.)
Re: Best approach to run code in a task exactly every millisecond
HI
Divide a long interval of 10 seconds into 10-50 short intervals. Callback is called at the end of a short interval within each short interval you can set a different rate of increase. In general, you will get a logarithm or a parabola. considering that the callback is called from an interrupt, the misses will not depend on the task priorities.
Divide a long interval of 10 seconds into 10-50 short intervals. Callback is called at the end of a short interval within each short interval you can set a different rate of increase. In general, you will get a logarithm or a parabola. considering that the callback is called from an interrupt, the misses will not depend on the task priorities.
-
MicroController
- Posts: 2705
- Joined: Mon Oct 17, 2022 7:38 pm
- Location: Europe, Germany
Re: Best approach to run code in a task exactly every millisecond
There are basically three different timer functionalities at your disposal:
1. FreeRTOS "software timers",
2. the IDF's "High Resolution Timer" which you can set to work from a dedicated task or directly off interrupts,
3. the IDF's "General Purpose Timers".
W.r.t. jitter, the HR timer using "Interrupt Callback Dispatch" method or the GP timer will give the best results.
You may also be able to compensate for jitter in software, calculating the duty cycle to use based on real time instead of number of interrupts.
1. FreeRTOS "software timers",
2. the IDF's "High Resolution Timer" which you can set to work from a dedicated task or directly off interrupts,
3. the IDF's "General Purpose Timers".
W.r.t. jitter, the HR timer using "Interrupt Callback Dispatch" method or the GP timer will give the best results.
You may also be able to compensate for jitter in software, calculating the duty cycle to use based on real time instead of number of interrupts.
-
nopnop2002
- Posts: 362
- Joined: Thu Oct 03, 2019 10:52 pm
- Contact:
Re: Best approach to run code in a task exactly every millisecond
>Did I understand it correctly that this will wait until the next tick to resume the task?
vTaskDelay(1) has the ability to give up execution privileges of the CPU and switch to another waiting task.
taskYIELD() does not have this functionality.
vTaskDelay(1) has the ability to give up execution privileges of the CPU and switch to another waiting task.
taskYIELD() does not have this functionality.
-
MicroController
- Posts: 2705
- Joined: Mon Oct 17, 2022 7:38 pm
- Location: Europe, Germany
Re: Best approach to run code in a task exactly every millisecond
That's not quite accurate: https://www.freertos.org/Documentation/ ... #taskyieldtaskYIELD() does not have this functionality.
taskYIELD() can switch to another runnable task of the same priority.
-
nopnop2002
- Posts: 362
- Joined: Thu Oct 03, 2019 10:52 pm
- Contact:
Re: Best approach to run code in a task exactly every millisecond
taskYIELD() can switch to another runnable task of the same priority.
Yes, you are right.
taskYIELD() performs a task switch.
taskYIELD() can switch to another runnable task of the same priority.
taskYIELD() can NOT switch to another runnable task of the lower priority.
vTaskDelay(1) gives up execution rights.
This results in task switching.
vTaskDelay(1) can switch to another runnable task of the same priority.
vTaskDelay(1) can switch to another runnable task of the lower priority.
Thank you for pointing that out.
>Can I use esp_timer_get_time() and taskYIELD() here?
The answer depends on the priority of your task.
Who is online
Users browsing this forum: PerplexityBot and 1 guest