-
-
Notifications
You must be signed in to change notification settings - Fork 8.5k
esp32/machine_timer.*: Adding support for Timer(-1) soft/virtual timers. #18263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Signed-off-by: Lucien Murray-Pitts <lucienmp_antispam@yahoo.com>
b2b71ad to
0185a64
Compare
|
Updated main comment to answer other questions, and improve readability of reasons for this enhancement |
|
Did you try to run the existing tests |
Q#1: Did I run the tests... ?The tests werent there in 1.26, but I see them in 1.27 and i have run them with some caveats
Q#2: The machine_soft_timer wont pass as is, but can run the timer code if -1 is passed as idThey sort of pass but there is a little bit of confusion from my part. Is doing the following legal; The documentation to the way I read it as machine.Timer(id, ... ) where the ... are optional BUT id is a must and must be argument 0. It maybe -1 if soft timers are supported, otherwise a positive integer but it must always be there. Zephyr does it this way, so i took that as a sign.
This will pass just ok Q#3: Documentation says hard=True, but I default to hard=False since i dont support hard=True types, is it ok?Maybe default hard=False is a better default as most python coders aren't trying to do extremely precise timing and the greater flexibility afforded would be better for the average lower skill programmer? (over hard=True where it maybe confusing why allocation, or other issues are happening to them?) Virtual/Soft TimersIn FreeRTOS soft timers from the OS dont run in the ISR afaik but rather in some task of their own so the callback would never be able to run directly, however there is the concept of scheduled, and called directly. ( I am using xTimerCreate ). Hardware TimersI assume all I would have to do for the hardware timers being hard is to call/not-call mp_sched_schedule, and directly call the callback based on hard=... Below is the current code, failry simple switch between calling sched or not. If thats the case then I can put that into this PR pretty easy. SummaryIn summary overall apart from my putting hard=False as the default if unspecified, not supporting hard virtual timers and the quandry about allowing machine.Timer( freq=1 ) then it all would pass. |
projectgus
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for submitting this, @LucienMP! It's been an important missing feature on esp32 port for a long time.
I see you chose to use FreeRTOS timers for this. Did you consider the ESP Timer feature of ESP-IDF as an alternative? ESP Timer has some advantages (for example: 1us resolution instead of 10ms, timer callbacks run at a much higher priority in the system).
Q#2: The machine_soft_timer wont pass as is, but can run the timer code if -1 is passed as id
You're right about the docs, but this does work on a number of ports so it would be good to support it here (and to be able to pass the test!)
You can follow an approach like this one, from the generic extmod/ implementation:
https://github.com/micropython/micropython/blob/master/extmod/machine_timer.c#L106
Q#3: Documentation says hard=True, but I default to hard=False since i dont support hard=True types, is it ok?
Yes, this is OK. We can't easily run Python code in hard interrupts on esp32 port because we have the "global interpreter lock" when Python code is running.
ports/esp32/machine_timer.c
Outdated
|
|
||
| // Add the timer to the linked-list of timers | ||
| self->next = MP_STATE_PORT(machine_timer_obj_head); | ||
| MP_STATE_PORT(machine_timer_obj_head) = self; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little confusing that this code is in this function, but the same two lines of code (and the equivalent malloc) for hardware timers are in machine_timer_create().
Suggest moving all of this into machine_timer_create() if you can, so it's easier to see what is similar (and different) between soft vs hard timers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to leave the code as much as original, but yes this could all move into create timer and yes it would make more sense. Will do that.
…ers. Signed-off-by: Lucien Murray-Pitts <lucienmp_antispam@yahoo.com>
|
@projectgus ok updated with the changes you suggested;
|
The code example confused me until I realized n_args and n_kw_args were two different things. Can we change the the documentation to be very clear that "classmachine.Timer(id, /, ...)" is specifying id=-1 and soft timer is defaulted to if missing. Actually in other places in the documentation "[ ]" are used so it might be better to document this as "classmachine.Timer([id=-1], /, ...)" https://docs.micropython.org/en/latest/library/machine.Timer.html#machine.Timer |
projectgus
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @LucienMP, I have one suggestion but this approach looks good to me!
Can we change the the documentation to be very clear that "classmachine.Timer(id, /, ...)" is specifying id=-1 and soft timer is defaulted to if missing. Actually in other places in the documentation "[ ]" are used so it might be better to document this as "classmachine.Timer([id=-1], /, ...)"
Yes, we should. I'll submit a PR for this.
| self->index = index; | ||
| self->handle = NULL; | ||
|
|
||
| // Add the timer to the linked-list of timers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than repeating this block, could you do:
bool new_timer = false;
[...]
if (new_timer) {
// Add the timer to the linked-list of timers
[...]
}
[...]
return selfThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Edited my suggestion as I realised it had a bug!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the above still stand, I will review and resubmit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, if you're able to refactor this way then it should be easier to follow. Thanks!
As noted in discussion on PR micropython#18263, the id parameter is optional on ports that support virtual timers. Add some more general explanation of hardware vs virtual timers. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
| self->v_start_tick = 0; | ||
| } | ||
|
|
||
| if (self->callback != mp_const_none) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have a check for None in for hardware timers (see machine_timer_isr_handler). Instead the scheduled function call will raise because it's not actually callable, printing TypeError: 'NoneType' object isn't callable.
Perhaps for consistency we should do that here as well, to make it easier for users to notice a bug where they passed None.
| } | ||
|
|
||
| if (self->callback != mp_const_none) { | ||
| mp_sched_schedule(self->callback, MP_OBJ_FROM_PTR(self)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note for hardware timers there's an additional layer of indirection: The ISR called when the hardware timer triggers is machine_timer_isr, which in turn calls the function stored in self->handler (defaults to machine_timer_isr_handler) which actually performs the mp_sched_schedule call. Reason for this indirection is that it allows using the timer from other C code by replacing handler. For example the UART RXIDLE implementation sets timer->handler = uart_timer_callback.
Optionally if we use the same pattern here we could make virtual timers compatible with such C code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point @DvdGiessen, I missed this! It would be nice to have RXIDLE not consume a hardware timer as well. (Not in this PR, necessarily.)
|
|
||
| if (timer_id == -1) { | ||
| // Virtual timer creation | ||
| self = mp_obj_malloc_with_finaliser(machine_timer_obj_t, &machine_timer_type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that this module now depends on finalisers, should we check for MICROPY_ENABLE_FINALISER? Some other modules have a explicit compile-time check at the top of the file, something like:
#if !MICROPY_ENABLE_FINALISER
#error "machine.Timer requires MICROPY_ENABLE_FINALISER."
#endif| machine_timer_obj_t *prev = NULL; | ||
|
|
||
| // remove our timer from the linked list | ||
| for (machine_timer_obj_t *_timer = MP_STATE_PORT(machine_timer_obj_head); _timer != NULL; _timer = _timer->next) { | ||
| if (_timer == self) { | ||
| // Remove our timer from the list | ||
| if (prev != NULL) { | ||
| prev->next = _timer->next; | ||
| } else { | ||
| MP_STATE_PORT(machine_timer_obj_head) = _timer->next; | ||
| } | ||
|
|
||
| // Remove memory allocation | ||
| if (self->vtimer != NULL) { | ||
| esp_timer_stop(self->vtimer); | ||
| esp_timer_delete(self->vtimer); | ||
| } | ||
| m_del_obj(machine_timer_obj_t, self); | ||
| break; | ||
| } else { | ||
| prev = _timer; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could make the code a tiny bit smaller by eliminating the edge case:
// remove our timer from the linked list
for (machine_timer_obj_t **t = &MP_STATE_PORT(machine_timer_obj_head); *t != NULL; t = &(*t)->next) {
if (*t == self) {
*t = self->next;
// Remove memory allocation
if (self->vtimer != NULL) {
esp_timer_stop(self->vtimer);
esp_timer_delete(self->vtimer);
}
m_del_obj(machine_timer_obj_t, self);
break;
}
}As noted in discussion on PR micropython#18263, the id parameter is optional on ports that support virtual timers. Add some more general explanation of hardware vs virtual timers, and remove redundant documentation about timer callbacks in favour of the isr_rules page. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
As noted in discussion on PR micropython#18263, the id parameter is optional on ports that support virtual timers. Add some more general explanation of hardware vs virtual timers, and remove redundant documentation about timer callbacks in favour of the isr_rules page. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
As noted in discussion on PR micropython#18263, the id parameter is optional on ports that support virtual timers. Add some more general explanation of hardware vs virtual timers, and remove redundant documentation about timer callbacks in favour of the isr_rules page. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
Summary
Soft timers using Timer(-1) has been missing, but appeared to work due to type checking not being right on the ESP32 platforms.
The timers are limited to system hardware timers that are, at most, limited to 4. However there are times when more timers would be very useful so I added soft timers.
See the discussion here: https://github.com/orgs/micropython/discussions/18056
Testing
I tested on Spotpear ESP32 board for which I am running LVGL + MicroPython 1.26 and wrote this extension for.
See: https://github.com/Spotpear-Scratch/board_firmware on the v1.26 branch
I tested manually thru console with the following set of conditions:
Trade-offs and Alternatives
This is an expansion to existing machine.Timer so code size will increase by only a small amount (few functions, and a few additional if statements plus memory structure change for timer), it expands the number of timers available from the less than 4 hardware timers, adding an additional soft/virtual set up to as many as the user has memory from albeit at a lower resolution.