RTOS versus Executive Loop - Page 1
Forum section: electronics / general

author: Sal Ammoniac
author_rank: Super Contributor
timestamp: June 10, 2016, 02:42:02 pm
content: I'm curious, under what circumstances do you decide to use an RTOS or an executive loop in an MCU project? I tend to use executive loops for very simple projects that don't require a full-blown RTOS, but for anything more complex I generally use an RTOS.So assuming your MCU has enough resources to support an RTOS, how do you make the decision to use one versus something simpler like an executive loop?

author: nctnico
author_rank: Super Contributor
timestamp: June 10, 2016, 02:58:18 pm
content: I'd use an RTOS when I need time slicing (parallel processes) and interrupts don't cut it. Otherwise a super-loop is just simpler and more deterministic. Another advantage of a super loop is that you can share the entire stack between all processes.

author: andersm
author_rank: Super Contributor
timestamp: June 10, 2016, 04:30:04 pm
content: I've started using an RTOS even for main-loop-style projects. It's kind of like a HAL for interrupts and events. The overhead isn't an issue for most modern devices.

author: Sal Ammoniac
author_rank: Super Contributor
timestamp: June 10, 2016, 04:36:29 pm
content: Quote from: nctnico on June 10, 2016, 02:58:18 pmOne disadvantage I've found with super loops is that you need to write lots of big state machines to handle the fact that you run each "task" from the beginning each pass through the loop. It can get quite complex when you need to handle something like an I2C operation on an MCU that requires you to handle each bus state sequentially. I do like the fact that they tend to be more deterministic than even a preemptive RTOS.

author: uncle_bob
author_rank: Unknown
timestamp: June 10, 2016, 04:57:45 pm
content: HiOne big loop plusses:a) Makes sure everything gets hit eventually (unless a task completely locks up).b) Probably uses the minimum amount of RAMc) Is all right in front of you (no hidden libraries of nonsense) since you wrote it from scratch.d) Probably uses the minimum amount of ROM / flashe) Probably handles bit banging betterRTOS advantages:a) Should allow you to schedule things with priorities and time slices (do this 100 times a second, do that 10 times a second, do this no matter what).b) Probably isolates tasks betterc) Likely handles interrupts / stack switching more elegantly than a big loop. This is more evident on complex stuff (Ethernet) than on simple stuff (interrupt from pin).d) Should provide tools that let you check stack usage, and do profiling.e) Likely comes with an ocean of libraries / drivers / goop to "make things easier" (along with a learning curve).Can you find a specific case that contradicts every single item on that list? Sure. In my experience, the other 99 times out of 100, the list above will be pretty close.At least in my case - ARM MCU's with a ton of RAM and a ton of flash are so dirt cheap that the RTOS is an obvious choice.Bob

author: richardman
author_rank: Frequent Contributor
timestamp: June 10, 2016, 07:31:13 pm
content: Most people do not write their own RTOS.So practically speaking, I think it's whether you have used an RTOS before and how comfortable you are with it and how affordable it is.

author: hans
author_rank: Super Contributor
timestamp: June 10, 2016, 08:10:43 pm
content: I like using an RTOS for prioritizing pieces of code while not relying on interrupts for them. Things like USB and Ethernet are nice examples; for example a producer/consumer construction. Big downside is that such a construction (e.g. for ethernet) eats a huge amounts of RAM in buffer space. Additionally context switching also takes a bit of time, and it's very easy to create a chain of task events that propagates through with multiple context-switches, each taking a few us each.In some cases I stick to using an RTOS because I am lazy. 1 hobby project a few weeks back I needed a 3-channel analog input while I only had 2 I/O pins available. I needed to sample and scale these at 1ksps. I grabbed a PIC24F32KA301 from my components bin and started straight away with a RTOS. A timer at a 1ms time interval kicks the ADC task, which samples & scales 3 analog inputs, which then kicks the serial task to print them in a packetized form.The use of a RTOS saved me figuring out how the ADC could be setup for an analog scan sequence (either hardware or software), messy IPC using globals, etc. In this case I could implement all via events and mailboxes.Although this project will never be ported to another microcontroller, it is nice to write all code in "main" threads. Could you write this project with interrupts and main code, while keeping it just as straight forward? Absolutely. However in this case it took me just over an hour to write this project.In terms of resource usage it's not very nice.. RAM usage was about 1600 bytes excluding the main() stack, and 1ksps was about the limit this construction could handle.In another project I'm working to use a RTOS for low-power firmware 'the easy way'. I have implemented my own RTOS for ARM Cortex m3/m4 chip to the CMSIS standard. When all tasks are idle the RTOS will normally go to the idle task. Now instead it will call the BSP functions to enter sleep. The chip must be woken again via a RTC event. In the RTC handler (which can set alarms via the RTOS) it can signal tasks to wake up, which will mean the micro will stay awake. Once no tasks are scheduled it will shutdown via said BSP sleep/wake functions.This creates a situation that I have no explicit sleep points in my code; it happens automatically as long as I keep creating RTC events and keep kicking tasks to do something. The RTOS will take care of the rest.

author: uncle_bob
author_rank: Unknown
timestamp: June 10, 2016, 09:32:56 pm
content: Quote from: richardman on June 10, 2016, 07:31:13 pmHiOnce upon a time (like when I had hair ..) an RTOS was an expensive and daunting item to deal with. Over the last decade or so, that's changed a lot. Just about every ARM MCU vendor will toss in an RTOS of some sort with their "free" software offerings for their chips. Of course, Just because it's free does not mean you should use it . There's also FreeRTOS that is indeed free. It's actually pretty capable for small to mid range projects. I don't know of any of them that come without a very real learning curve. Doing a "free" class or five is a pretty good idea if you switch over to a new one.Writing a big loop should not take you any classes at all. It's a pretty obvious way to do things. If you toss in interrupts (as opposed to polling), it gets a bit more complex. Still, the "coming up to speed" is less than with the RTOS.Bob

author: jnz
author_rank: Frequent Contributor
timestamp: June 11, 2016, 04:37:56 am
content: It comes down to time for me.Yes, I am capable of writing a complex program in a loop. But it sucks - I have stacks and functions that naturally want to block. I have user inputs and data coming in that will absolutely be non-deterministic...I can sit down and write all the blocking code I like and roll it into threads of the RTOS, and actually get something out the door.The question is, how much room do you have? Room for error, safety, ram/rom, how much time do you have, etc.We have OSes for a reason. They aren't the best tool all the time, but they clearly are only getting more popular.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: June 11, 2016, 06:59:16 am
content: I started on the Arduino and moved to bear bones AVR from there. When I began writing my library (see signature) I looked at some HAL and RTOS options but thought they were way too heavy for such a small MCU. They used dynamic allocation and for small MCUs the idea of preemptive scheduling is way overkill IMO. So I wrote my own.My library is in C++ with templates. Not many of you use C++ (or templates) from what I gather. Nonetheless, I think I managed to create a nice way to do a cooperative task without too much overhead or complexity for the dev.I have created a set of macros(picked up from a C++ article on the web) that allows you to easily create a task class. Here's how one could look:(stripped down version of theTimeoutTask)Code:template<class BaseT, typename DelaysT, const uint32_t Timeout>class TimeoutTask : public BaseT{public:/** Call this method repeatedly from the main loop.* Each time the Timeout expires the BaseT::OnTimeout() method is called.*/Task_Begin(Execute){while (true){Task_YieldUntil(DelaysT::Wait(BaseT::getId(), Timeout));BaseT::OnTimeout();}}Task_Endprivate:uint16_t _task; // required by the Task macros};The DelaysT is a library class that counts down delay/wait timeouts.You can see the Execute method has a while loop (!!) and waits until the timeout has passed and then calls the OnTimeout on the BaseT class.Usage is very simple: Declare the tasksCode:TimeoutTask<OutputPinTask<13>, TaskSchedule, 1000> task1;TimeoutTask<OutputPinTask<12>, TaskSchedule, 200> task2;TimeoutTask<OutputPinTask<11>, TaskSchedule, 500> task3;TimeoutTask<OutputPinTask<10>, TaskSchedule, 1500> task4;And call them in the main loop (Arduino)Code:void loop() {// update the scheduler - this counts down the wait-timesTaskSchedule::Update();task1.Execute();task2.Execute();task3.Execute();task4.Execute();}The TaskScheduler is that DelayT class (typedef Delays<Time<Milliseconds>, 4> TaskSchedule;).To my eyes this is one of the most elegant solutions I have seen so far (not just because I made it)- Each Task is isolated in their own class.- Multiple instances of the same Task can be used (like we do here).- There is no dynamic allocation. RAM usage is minimal. A lot of the (template) params are hard coded.My 2c's.

author: uncle_bob
author_rank: Unknown
timestamp: June 11, 2016, 01:45:47 pm
content: HiLike a lot of others, I resisted the whole RTOS thing as "to much baggage" for quite a while. I did the same thing in hanging on to assembly language vs C. Back a few decades ago, both were about the only real choice I had. A "big" MCU with 256 bytes of RAM only leaves you so many choices.More recently the debate has been about parts that are on some sort of family tree. You move up the tree and you get all the peripherals and all the memory. You move down the tree and the cost drops a bit. Package sizes change more or less as you move around. The MCU's all cost less than $6 in volume. The "fine tuning" process swings the price ten to twenty percent. The gizmos they go in normally have other stuff in them that comes in above $100. Inventory fills up every time we buy a couple reels of this or that fine tuned part. Look at the flip side (scrap and obsolescence), we are not saving 10 or 20%. For now it's off to the top of the tree. The intent is to turn five to ten stock items into one or two.The second part is an experiment at this point. We'll see how it shakes out a few years from now. It may or may not give us the inventory benefit we are hoping for. Having the "big part" does take you straight to the RTOS though. Simply put, it's not a one dimensional problem. Lots of other weird (and questionable) decisions feed into it.Bob

author: mikerj
author_rank: Super Contributor
timestamp: June 12, 2016, 10:05:19 am
content: Quote from: obiwanjacobi on June 11, 2016, 06:59:16 amHow does this solve the problem of lengthy tasks that would usually be broken up via a state machine. i.e. does your 'yield()' function have some kind of context save so you can jump back into the task at the appropriate point when the scheduler gives the task more time?

author: obiwanjacobi
author_rank: Super Contributor
timestamp: June 12, 2016, 11:58:07 am
content: Quote from: mikerj on June 12, 2016, 10:05:19 amYes, of course. Otherwise the while loop would hang the program.The first call to a task function (Execute) run normally until one of the yield macros is encountered - then it exits the function and other code will run. The next time around, execution will start again at the same yield-macro position it exited previously - the bool-result of the expression inside the macro will determine if the yield macro will fall through, or yield: exiting the function again. And so on. So the state machine to maintain progress of a (sub)task is taken care of.

author: hans
author_rank: Super Contributor
timestamp: June 12, 2016, 04:11:11 pm
content: Protothreads is a simple library for that.It's called cooperative scheduling, i.e. the performance of scheduling relies on the fact that each task will only consume as short as time it needs. That doesn't mean it solves all timing constraints; depending on the heaviness of calculation.It is also limited to the scope of a single function, because protothreads underneath works like a coroutine i.e. a big switch-case.It's a reasonably low RAM solution, I think it's only like 2-4 bytes per "task". But as I said , it has some limitations.

author: nctnico
author_rank: Super Contributor
timestamp: June 12, 2016, 04:25:57 pm
content: Quote from: hans on June 12, 2016, 04:11:11 pmIMHO making a statemachine results in much more readable code. Protothreads is hiding a lot under the hood through obfustigated macros and still doesn't solve the problem when one process needs too long to do it's processing. If you need to divide processor time between processes you better use an OS which does time slicing and has proper mechanisms to share data between various parallel threads. OTOH you can see an interrupt controller as a form of time slicing as well so all you need to add is thread safe data sharing.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: June 13, 2016, 06:34:53 am
content: It is called cooperative for a reason. All tasks involved have to play nice and not hog up all the processing time. From my limited experience it seems fairly easy to chop up a long sequence of processing in smaller parts YMMV.For instance,here is the task method for debouncing a push button:Code:Task_Begin(ScanButton){SampleButtonState();// debounceif (DebounceButton()){Task_YieldUntil(WaitForDebounce());setPrevState(getState());setState(ConvertToButtonState(BaseT::Read()));}// holdif (getState() == stateClosed){if(WaitForHold()){setPrevState(getState());setState(stateHold);}}else if (DelaysT::IsWaiting(BaseT::getId())){DelaysT::Clear(BaseT::getId());}}Task_EndThis code 'waits' for several timeouts, which are the obvious yield-points...

author: nctnico
author_rank: Super Contributor
timestamp: June 13, 2016, 07:55:14 am
content: The problem with it is that you are mixing OS-ish stuff with regular code. How would you keep your code portable that way or use someone else's code? A lot of code (also for use on server and desktop machine) is written with a super-loop environment in mind where the OS does timeslicing. If the execution paths aren't long you can use this code unmodified in a super-loop otherwise an OS with time-slicing will do the job.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: June 13, 2016, 08:17:55 am
content: Portability is based on abstraction and integrating 3rd party code is usually based on wrapping. The result is only as good as its weakest link. So if the 3rd party lib introduces a loop that screws things up.... you need to take that into account in choosing between cooperative or preemptive multi tasking.My library is based on very small parts of code that can be freely combined and/or replaced. So its very easy to replace the way an LCD is interfaced, without having to replace the protocol the LCD uses, for instance. Downside is that you need a little more knowledge of how the library works, to do that - that can't be helped.I am not saying that there is no need for preemptive time slicing. I am just saying that for most smallish (hobby) projects in combination with the smallish size/grade of MCU used, it is usually overkill. I target my library at that segment.

author: Chris Mr
author_rank: Regular Contributor
timestamp: June 13, 2016, 08:25:32 am
content: Once you have plenty of Rtos code it's a fairly easy mental calculation for me.I first used an Rtos because it was clear that what was required in a loop style was going to make it difficult to write. Once I started using Rtos it was clear that I had waited too longThe bit I like is where you start a new task with...while( 1){ /* Our own processor !*/}but of course you then have to be respectful of all the other processors and keep blocking when you don't need to do anything.Overall I would say it's worth getting into Rtos as soon as you can. It's a higher level of abstraction that can make things easier to understand / pick up in future. We don't talk about the electrons moving inside the transistors that make up the NAND gates that are used to make flip flops that are used to make adders that are used to make an ALU that is used inside a CPU; we just type "i++;"my 2p

author: mikerj
author_rank: Super Contributor
timestamp: June 13, 2016, 09:09:22 am
content: Quote from: obiwanjacobi on June 12, 2016, 11:58:07 amApologies, I didn't word my question very well. What I meant was does your template implement a state machine via macros (i.e. like protothreads, and therefore subject to the same limitations; e.g. no yielding in functions called by a task) or does it actually save/restore the stack/program counter?

author: obiwanjacobi
author_rank: Super Contributor
timestamp: June 13, 2016, 09:26:17 am
content: Quote from: mikerj on June 13, 2016, 09:09:22 amAh yes, it is macro based, but you can nest tasks (although that can become somewhat tricky). It doesnotdo any fancy stack manipulation. It is targeted for simpler scenarios with a small RAM footprint.

author: mikerj
author_rank: Super Contributor
timestamp: June 15, 2016, 09:15:31 am
content: Quote from: obiwanjacobi on June 13, 2016, 09:26:17 amThanks, it looks like a pretty neat solution for C++ users.One annoyance with using the __LINE__ macro is if you have a code module with more than 255 lines then the task state variable requires 16 bits which is undesirable on a small 8 bit micro, and it also means non-consecutive state values so the compiler is more likely to use a timing consuming else-if construct than a jump table. It would be great if you could automatically generate consecutive state values, but I suspect that would require an external code pre-processor.

author: tggzzz
author_rank: Super Contributor
timestamp: June 15, 2016, 09:23:53 am
content: Quote from: obiwanjacobi on June 13, 2016, 09:26:17 amIt is sort-of appealing, but the coding restrictions (especially w.r.t. whatcan'tbe in a function) will unnaturally contort the code. Interactions with library code are another potential problem area, e.g. you can't have a function call to send a packet and wait for a response - all blocking function calls have to be at the top level.Given that, it has relatively few advantages over the traditional design patterns embodied in a for(;loop plus polling and event handling.Shame.

author: obiwanjacobi
author_rank: Super Contributor
timestamp: June 15, 2016, 10:01:32 am
content: Quote from: mikerj on June 15, 2016, 09:15:31 amThanks.One has to work within the limitations of the tools, unfortunately. Not a day goes by that I wish I could do x or yAnd working with C++ it's usually more than one a day.The __LINE__ switch may not be most efficient, but it does the job (for the simpler scenarios). This size of the var its stored in and overflowing it, is a concern.One idea I have been toying with - which is not implemented yet - is to make a data-accessor template class (family) and have an extra template parameter(s) for class that store anything. This would allow you te specify if you need a critical section/lock for accessing the value or if it needs to be volatile etc. (there is nothing you can't fix with another level of indirection). I have not worked out the details yet (how to lock multiple values at once for efficiency, how to prevent having to specify a template param for each class member, how to prevent having to know inner workings of class etc.)...Quote from: tggzzz on June 15, 2016, 09:23:53 amYou're setting the norm for what I consider bad coding practices: I don't think one method should ever both send and receive unlessyouwrite it in your (top level) program (so not in a library) - or they should at least be available separately.Like discussed before, sometimes you need an RTOS.BTW: what sort of thing can't be in a function? - I am not sure I am getting this.

author: tggzzz
author_rank: Super Contributor
timestamp: June 15, 2016, 02:36:08 pm
content: Quote from: obiwanjacobi on June 15, 2016, 10:01:32 amRTFM.It is easy to understand, provided you are aware of how functions and RTOSs are implemented in machine code, and that there is no concept of a stack in C (although that's a typical implementation mechanism). Essentially any form of "protothread switch" trashes the stack, so cannot be invoked except at the "top level".I've no idea what happens with aggressive optimisation, and I would expect it to vary between compiler versions.