Replies: 10 comments
-
Hm, difficult. There is no main schedulers, it's all implicitly scheduled by the control flow of the program, but you could of course access shared memory. Perhaps something like In case the counter is not zero, you still have to poll for the interrupt as normal, but you don't need to build up the entire call stack, you could poll for the interrupt at a "higher" call point (however that would be implemented). But it won't be as useful as you think. For example all Oh, and then there's also the possiblity of just manually polling
That's a very nice way to say it, since it's really more "actively sleep hostile with extreme prejudice"… 😬 One of the most interesting alternatives is the RTFM scheduler, which is only interrupt-based: C++ Implementation, Rust Implementation. |
Beta Was this translation helpful? Give feedback.
-
But just to make this clear: As soon as C++20 coroutines are available, the whole Protothread/Resumables will be deprecated, and a new, more traditional scheduler will take their place, since coroutines with compiler-generated promises and resume points are even more efficient, flexible and especially user friendly than our Resumable hacks. So if you want to dive deeper, perhaps check out GCC10 coroutines rather than dealing with our limitations. |
Beta Was this translation helpful? Give feedback.
-
Thanks for the pointers, this is very interesting! The idea with a volatile counter would work, I was thinking to transitively pass on a flag in the resumeable result state with the meaning "all children are stopped or waiting for interrupt". Then the main loop can probe the functions once and then know when to go back to sleep. If I am not mistaken, you don't need any actual interrupt handler, take this example:
This will wake up and run once for every interrupt that fires, which turns out to be exactly what we want in this case :) Obviously code that wants to put CPU to sleep can't make use of software timers and need to rely on something that actually makes an hardware interrupt, such as counter match on RTC. It anyway doesn't make much sense to put CPU to sleep for delays in the uS range, as it can take a while to bring the clocks back online (samd21 about 20uS). C++ coroutines are just protothreads/resumeables with nicer syntax and better language support, and AFAIU they do (will?) have the same problem w.r.t sleeping. Another similar example of this is FreeRTOS with collaborative scheduler and tickless idle, however that is not in the same league of memory efficiency. |
Beta Was this translation helpful? Give feedback.
-
Yes, I think that can work. The resumable state type is an 8-bit type with >127 defined as "keep polling", so you can just add another macro that returns >127 ( However I'm still in favor of having an explicit sleep function that is explicitly called by the user in the main while(1) loop, so that when you wake up from an interrupt all resumables get a go at least once. This implies a shared memory implementation with a counter that is incremented/decremented for
I think it is useful to have this sleep counter mechanism as its own API in its own module The user is of course still responsible for making sure it all works, but a separate module would also make it easier to provide a (centralized) log functionality for debugging to understand what task is currently preventing sleep? I think this approach may however break the "pass a flag up the call chain" optimization, since you now have check multiple call sites since you don't know which interrupt they were waiting for. Not sure though.
Yes, of course, but because the compiler encapsulates the local state into an (copyable, movable) object, you don't have to build up the entire call stack to check the "leaf" condition. |
Beta Was this translation helpful? Give feedback.
-
I think we are aligned on the high level, but there are some details where I need to make myself better understood.
Yes, this is also something I want to achieve. At the same time, I don't want to make the main loop responsible for how long to sleep or when to wake up, because that breaks modularity. For example, the UART peripheral might want to wake up on start bit interrupt, and a sensor driver every 60 sec. Adding all these conditions to the main loop will not scale.
My alternative to this is something like:
What this does is to delegate the responsibility for responding to the interrupt to the leaf, which IMO is where it belongs. It will always run the whole call stack to completion on every interrupt, which is fine because sleep interrupts cannot happen that often anyway (due to CPU wake up time). Actually, if we make a "resumeable RTC timer" class, then we can fully encapsulate all the delay related logic in there and the caller would just need to do
|
Beta Was this translation helpful? Give feedback.
-
Ok, I'm convinced, lets poke the CI with some code! |
Beta Was this translation helpful? Give feedback.
-
So I stumbled upon Contiki's "process" concept which turns out to be a generalization of what I am proposing here: https://github.com/contiki-os/contiki/wiki/Processes Essentially, you have a linked list of proto threads and a (priority-based) event queue, the scheduler pulls objects from the queue and calls the proto threads. ISRs can add events to the queue, therefore enabling event-based scheduling. In this model, you can let the CPU sleep if the queue is empty. I like this much more because it separates the event generation from the processing, in my model it is a bit intervened (each resumeable would need some sort of local "event" state to know if the ISR happened or not). It also enables event-driven hardware independent modules, for example buffered UART should really be hardware independent (based on non-buffered UART) and not implemented one time per platform like we have it today. The process class template would look something like this:
The event class template like this:
This means that the buffered UART example would need two processes (rx/tx) which is a total of The event queue can be a statically allocated array that is managed by std::make_heap. We can either assert fail when this goes out of bounds (akin to nested resumeables) or discard the lowest priority event (not sure if that would be a good idea). A new macro Unless there is any high-level feedback, then I can give this a shot by using it to implement a shared buffered UART impl for SAM / STM32. |
Beta Was this translation helpful? Give feedback.
-
I think this is a fantastic concept and would fit quite well into modm's design! Note however, that the |
Beta Was this translation helpful? Give feedback.
-
I prototyped this and I am not convinced. The main problem I encountered is the semantics of yield in a multi-thread context, it took me quite a while to get a simple consumer/producer deadlock-free and it was hard to debug why things aren't progressing. I am considering leaving protothreads as glorified state machines (which is exactly what they are) and instead come up with something based on super lightweight context switching (think fibers). Drawback is obviously the overhead of keeping thread context (32 byte on CM0) and the advantage is that the code looks like C++. The main design goals would be:
Two very close candidates for inspiration is RIOT-OS and uOS++, I believe you can also tune freertos to look very much like this, which is probably what I would start with, given that there is already a modm module for it. The sad thing is that this would start having core modm libraries (i.e. Buffered UART) depend on something like freertos, and I am not sure what the appetite for that is. I guess the main pain points are RAM and code size, I'll try to keep an eye on those. |
Beta Was this translation helpful? Give feedback.
-
Cooperative, stackful multitasking aka. fibers has been on my mind too, I've starred these projects (just FYI):
I think our UART interface needs to be simplified, and the buffer taken out of the implementation. That would imply some sort of callback or polling interface to abstract Rx/Tx interrupts or a pointer/size DMA implementation. |
Beta Was this translation helpful? Give feedback.
-
Currently, modm isn't exactly sleep friendly. I want to improve that.
The resumeables are built around two states; running (wait for condition) and stop. I propose to add a third state "wait for interrupt" which indicates that the resumeable is not going to make progress until an interrupt occurs.
What the main scheduler then can do is to run the resumeables until they all are blocked on wait for interrupt (or stopped), then put CPU to sleep and let the interrupt wake it back up (and repeat). The interrupt could be timer/rtc (for delays), external or anything else the platform supports while idling/sleeping.
Most likely the main challenge is to make sure we don't accidentally put the interrupt source to sleep, however, maybe that could be handled separately by some sleep/power controller.
Happy to hear your thoughts on this before I get my hands dirty and prototype something.
Beta Was this translation helpful? Give feedback.
All reactions