RtOS – how do I benefit from it?

All right, so in that post I have shown You what are conditions which will let You use RtOS to You potential benefit. But what is the benefit exactly?

Maintenance costs.

Just that. Nothing more.

Your code won’t be faster, because calling RtOS will take some time. Tiny amount, but always. Your code won’t be smaller, because You will have to add an RtOS. Again, it won’t be much more, because the state machine You move to RtOS will be simpler than without the RtOS, but some additional code will be added.

The only, but very important benefit will be such, that Your code will be much, much cleaner.

Are You nuts?! Getting RtOS only for having some cleaner code?!

Well… yes?

And now a bit of warning: I do come from an assembly language. I like it. And if You also code in assembly the actual benefit from RtOS may be not very large. But if You code in C…. well… it will be tremendous.

So let us start with some example.

Imagine You have two state machines looking approximately like that:

Two state machines.

Machine “A” is simple one. Machine “B” is intentionally a bit trickier.

Now let us see how could we implement state machine “A” in C-like language.

int state
void machine_A_init()
{
  ... some initialization code
  state = ZERO
};
void machine_A()
{
  switch(state)
  {
     case ZERO:
          do something
          setup for waiting
          state=WAITING_FOR_ONE
          return;
    case WAITING_FOR_ONE:
          if (waiting condition found)
          {
            state = STATE_ONE
          };
          return;
     ...
  }
}

Let us for a moment skip machine “B” and take a look at main program loop:

void main()
{
   machine_A_init();
   machine_B_init();
   for(;;)
   {
      machine_A();
      machine_B();
   }; 
}

Doesn’t look so bad, right? We have implemented state machine as a simple switch/case. It is relatively easy to understand, if not the WAITING_FOR_ONE state. But explaining this must wait for later.

Now how about state machine “B”? Will it also be so simple?

It depends. It may be simple if we flatten repeat loops and just make them a sequence of identical states:

void machine_B()
{
  switch(state)
  {
     case REPEAT_0_1:
             do_0(); 
             state= REPEAT_0_1_WAIT_FOR; 
             return;
     case REPEAT_0_1_WAIT_FOR:
             if (wait_condition)
             {
                state = REPEAT_0_2;
             };
             return;
     case REPEAT_0_2:    
             do_0(); 
             state= REPEAT_0_2_WAIT_FOR; 
             return;
     .....
  }
}

Now it looks nasty, but we could use a single state variable. Of course we could also do it like that:

int rep_counter;
void machine_B_init()
{
   ...
   rep_counter = 2;
};
void machine_B() 
{   
   switch(state)
   {
     case REPT0:
           do_0();
           state = REPT0_WAIT_FOR;
           return; 
     case REPT0_WAIT_FOR:
          if (wait_condition)
          {
             if (--rep_counter==0)
             {
                 state=REPT1;
                 rep_counter=4;
             }else
             {
                 state=REPT0;
             }   
          };
          return;
     case REPT1:....
   };
};

This time we have to use two state variables. Of course in this approach we will need one more variable to handle the nested loop.

Switch/case is not the only way to go. If Your CPU is efficient in indirect calls and stack handling then the state machine may be also implemented using state handler functions like:

void (*stateA)(); 
void machine_A_init()
{
  stateA = &ZERO;
}
void ZERO()
{
   ...
   stateA = &WAITING_FOR_ONE;
};
void WAITING_FOR_ONE()
{
  ...
}
.....
void main()
{
   machine_A_init();
   machine_B_init();
   for(;;)
   {
     (*stateA)()
     (*stateB)()
   }
};

The state handler function may in many cases result in better performance since there is no switch comparison each time the state machine is called. This type of handling is natural for assembler and for interrupts, but with C and interrupts care must be taken to not force a full state save/restore due to the indirect call. But we are not in interrupt now, so it is not a problem.

What is wrong with it?

Waiting in state machines

Now it is a time to explain the …WAIT_FOR states.

All right, so take again a look at the main loop. It just calls first and then second state machine. At each loop repetition it is, in generic, unknown if both machines do wait for something or not. Usually they will wait, but it will be tricky to guess what exactly are they waiting for. Since we don’t know for what kind of event they wait we can’t put the main loop on pause. It must loop and loop infinitely and each machine must pool it’s wait condition.

Of course we could think about a kind of “waiting for” information and implement it like:

void main()
{
  ...
  for(;;)
  {
     machine_A();
     machine_B();
     wait_for( machine_A_wait | machine_B_wait );
  }
}

It is doable.

Is it easy to maintain? The code of each of machines is split into three blocks: init, machine loop and wait conditions. In machine “A” it is not so bad, but machine “B” needs two additional states to handle the nested loop.

My experience shows that any code which executes in the order which is not exactly like the “lexical flow” of the source code text is very hard to analyze and maintain.

How would it look in cooperative RtOS?

This time I will start from machine “B” because the effect will be visible at the first glance.

void machine_B()
{
   ... initialize
   for(;;)
   {
       for(int i=0;i<2;i++)
       {
          .... do something
          setup wait conditions in RtOS
          yield();
       }
       for(int i=0;i<4;i++)
       {
          .... do something
          setup wait conditions in RtOS
          yield();
          for(int j=0;j<2;j++)
          {
               .... do something
               setup wait conditions in RtOS
               yield();
          }
       }
   }
}

The yield()is a call to a subroutine with which the task is telling RtOS: “I don’t need the CPU anymore, You can take it and let other tasks run. Awake me again if waiting conditions are meet and return from yield() then.”

The main loop, in case of embedded program where tasks are set-up at compile time, just jumps to the RtOS kernel loop. If tasks are dynamically allocated it might however look like:

void main()
{
  rtos_add_task(&machine_A,stack_size_of_machine_A);
  rtos_add_task(&machine_B,stack_size_of_machine_B);
  rtos_run();
}

Can You see the difference?

Now please compare how the state machine “B” looks like when implemented with switch/case and how does it look when implemented with a help of a cooperative RtOS. If You can’t see the difference then…. well…. you must be nuts.

Just kidding.

Simply imagine that this is not Your code. Somebody else at your company wrote it. And imagine You have been given the job to fix something in state machine “B”. Take a look at both implementations again and decide which of them would You like to play with, the switch/case or the cooperative RtOS one?

But the RtOS is soooo complex and expensive!

Yes, yes, I know. Operating system must be complex and You will have to buy it, right? Adding such a complexity to 8-bit microcontroller is silly and will simply not fit in its limited resources, right?

Is that what You think?

If it is, You are wrong.

Cooperative RtOS is very, very simple. In assembly the kernel itself is usually around 100 of instructions. You can write one for Yourself in a day or two. Well… maybe a week if You are doing it for a first time and have a nasty C compiler to deal with.

Interested in it? Would You like to know how to make it? Let me know or wait for a next part of a blog.

Summary

After reading this blog entry You should know where is the true benefit of the cooperative RtOS. It is in the code clarity. Clear, easy to read and understand code is easier to write and maintain. And a code which is easy to write and easy to maintain is usually better in terms of quality and robustness.

It is also worth to mention, that it means that less time is spent on it and less time spent means less money spent. What doesn’t matter if company You work for is not sharing its profits with the employees, but do matter if You own the company.

Leave a comment