Author Topic: Do i really need to use an RTOS? (Alternatives to finite state machines?)  (Read 15619 times)

0 Members and 2 Guests are viewing this topic.

Offline JPorticiTopic starter

  • Super Contributor
  • ***
  • Posts: 3461
  • Country: it
I have a new project that is screaming for a RTOS.
It will have to use an external can controller (no, can't change MCU. external can controller it is, period.) and i'm trying to decide what to do.
I exclusively use state machines, but my projects with CANbus have lead to really complex ones, where it's really a pain to change the order of operations, or add new features. Then this time i also have to add the SPI communication layer, which would mean a FSM that waits for another FSM to do its job, error handling etc..

Well, I really REALLY want to simplify things and write a single, linear, big function that act as if it was the only thing running. So i need threads or something simillar.

What are my choices?
-Keep using a state machine
-Finally give in and build the firmware over FreeRTOS (will take a while to get started as i only dabbled with it)
-Something crazier, like running the canbus code as a linear function in the main while loop and run EVERYTHING else in interrupt routines set with the correct priorities, as all the rest is actually interrupt driven but that is not so flexible (what if i want to have TWO "threads"?)
-Some C99 trickery that simulates pausing a thread (i saw some time ago some complex macro abortion that provided some sort of context switching somehow)

I have always been reluctant to use a RTOS because most of the code should run bare metal, no libraries or anything wasting cycles, i don't know if i "can" mix my usual style of coding with threads, what are best or recommended practices etc.. and examples i can find are usually NOT helping in this regard
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 19497
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Using an RTOS as an implementation technology won't make implementation of FSMs easier. All it will do is add another layer of complexity, with hidden characteristics.

I presume your FSMs are specified in an FSM description language of whatever kind. Statecharts, CSP, and SDL are well-known techniques, but there are many others. If that isn't the case, then you should start there before implementing anything.

It sounds to me as if you would benefit from using alternative design patterns to implement FSMs. If you are using if/then/else or case statements for FSMs with more than half a dozen states/events, then that will definitely be the case. I've seen the results of such cancerous implementation techniques; the result was very ugly and very expensive. People changed job rather than work on it!

Having said that, there are some limited use cases where linear code involving yield() and waitForEvent() can be tractable.

My preference:
  • each FSM is all in a single thread or a superloop. Multiple FSMs can run in separate threads or sequentially within the superloop
  • external hardware events cause an interrupt, and ISR mutates them into a message that is put in a queue
  • FSM sucks one event from that queue and processes it to completion. Rinse and repeat.
  • FSM can create events, which are also put in the same message queue to be processed by that FSM
  • FSM cannot distinguish whether an message was from hardware event or software
  • choose a design pattern so that you can log events/states in real time, and when modification is required it should be obvious which line of code (i.e. state-event pair) needs changing
  • if appropriate have multiple "worker" threads each of which takes one event from a global queue and processes it to completion. There should be less worker threads than cores
or, of course, just use Hoare's CSP and an embedded multicore processor.
« Last Edit: June 10, 2022, 01:48:56 pm by tggzzz »
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline rstofer

  • Super Contributor
  • ***
  • Posts: 9890
  • Country: us
One of the core ideas behind using an RTOS is to simplify code.  A task only runs if it is known that it is going to do something useful, there are no blocking spin loops.  A task needs to know as little as where to get its input and where to send its output.  It need not concern itself about other tasks.

It is worth the effort to partition the code to work with an RTOS even if the RTOS doesn't do much in the way of scheduling.  Getting rid of the superloop is the goal.  There is no point in wasting cycles testing for something that hasn't happened.

There is a ton of documentation for FreeRTOS and, more important, there are ports for various boards and chips.  You just need to find something that is close to your hardware.

You still have to write the bare iron peripheral code.  The RTOS knows next to nothing about hardware, it relies on semaphores and queues to determine if a character has been received.  The scheduler won't be looking at the USART peripheral itself.
 

Offline brucehoult

  • Super Contributor
  • ***
  • Posts: 4034
  • Country: nz
The C standard library _setjmp() and _longjmp() do exactly what is required to portably write C code that does cooperative threading. Each thread has its own jmp_buf as a global variable. When you want to switch from thread A to thread B you _setjmp(thread_A_jmp_buf) then _longjmp(thread_B_jmp_buf).

The only thing you can't do from in C code is to set the stack pointer for a new thread. You have to either know where it is stored in the jmp_buf and poke it in after a setjmp() from the parent thread, or else use inline assembly language (or an asm function) to set the actual stack pointer register.
« Last Edit: June 10, 2022, 01:58:21 pm by brucehoult »
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 19497
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
One of the core ideas behind using an RTOS is to simplify code. 

Of course.

Sometimes it succeeds, especially where the processes/processing cannot be known at manufacture time.

Sometimes it fails or is unnecessary, especially where all the processing is determined when the device is manufactured.

Quote
A task only runs if it is known that it is going to do something useful, there are no blocking spin loops. 

Yes, there are - buried invisibly inside the RTOS

Quote
A task needs to know as little as where to get its input and where to send its output.  It need not concern itself about other tasks.

It is worth the effort to partition the code to work with an RTOS even if the RTOS doesn't do much in the way of scheduling.  Getting rid of the superloop is the goal.  There is no point in wasting cycles testing for something that hasn't happened.

You need to investigate alternative design patterns and implementation techniques.

You also need to understand the subtle problems multiple threads can bring in their wake. One very visible one screwed up the Mars Pathfinder; fortunately they were able to (very!) remotely debug it and fix it.

Quote
There is a ton of documentation for FreeRTOS and, more important, there are ports for various boards and chips.  You just need to find something that is close to your hardware.

You still have to write the bare iron peripheral code.  The RTOS knows next to nothing about hardware, it relies on semaphores and queues to determine if a character has been received.  The scheduler won't be looking at the USART peripheral itself.

Look, if you want to use FreeRTOS then do so.

If so, don't ask the question you asked, and don't expect it to make your life that much easier.

Fundamentally RTOSs are not a magic bullet for FSMs. Proper specification, design, and implementation are more important than the specific technology used.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline Doctorandus_P

  • Super Contributor
  • ***
  • Posts: 3358
  • Country: nl
What sort of microcontroller are you using?

For example, FreeRTOS has been ported to the AVR's, but those are (for today's standard) quite slow 8-bit processors. They also have other disadvantages for an RTOS. They have a big bank of 32 registers, that all have to be saved and popped on each task switch, and on top of that, they have a very simple ISR architecture with only one level. They also have a limited amount of RAM that you have to divide over multiple stacks for the tasks. I would not recommend using an RTOS on an AVR.

If you've got an ARM Cortex like uC you have more speed to begin with, more efficient task switching, and multiple ISR levels which all help to reduce the extra overhead of an RTOS. With 20KiB or so of RAM you've also got enough room to implement a bunch of tasks with each it's own stack and associated RAM usage.

An RTOS is a quite decent way to add structure to your software and make the individual parts easier to understand, but it's not the only way.

I've had quite a good result with implementing multiple state machines with function pointers.

In main() there is just a simple while loop, which executes each of the state machines. You can add a timer, and for example run some (or all) of the state machines at a fixed rate, (for example 100Hz) This makes it easy to implement fixed delays in those state machines.

The goal is that each function in each of the state machines does it's thing, and it changes the function pointer to another function if the state has to change. It is important that after the function has done it's thing, it returns, so main() gets to execute the next state machine.

This is a very simple structure. It works good and is easy to understand. The only difficulty is that each of the states have to return quite quickly.
One of the advantages of this way of cooperative multitasking is that no ISR's whatsoever are used, and they are all free. Once you start using an RTOS, it uses quite a lot of ISR time for the task switching itself, and this upsets all other ISR's, unless you have an interrupt system with multiple levels of priority.
Because of this simple structure, you also do not have to use semaphores or other synchronization features around messages. Each of the state machines is free to completely format a message, for another state machine, and that statemachine only sees it after the first state machine returns to the main super loop.
« Last Edit: June 10, 2022, 03:54:02 pm by Doctorandus_P »
 
The following users thanked this post: mskeete

Offline JPorticiTopic starter

  • Super Contributor
  • ***
  • Posts: 3461
  • Country: it
Microcontroller is not important, but it's a PIC24FJ. Plenty of ram, nice architecture, supported by some commercial/free RTOS. Code/RAM usage is negligible so recource hog by the RTOS is not an issue

Using an RTOS as an implementation technology won't make implementation of FSMs easier. All it will do is add another layer of complexity, with hidden characteristics.

I use FSMs to achieve multitasking because i can split operations in smaller steps so i can run all functions in the superloop as if they were running at the same time
Using an RTOS would mean that i don't have to use FSM to split the operation in smaller steps, it's the scheduler (or a yield) that pauses the task
Unless i got the core concept wrong?

EDIT:
I am aware of the issues that RTOS can bring in, and the importance of a correct specification and planning ahead, which is why i'm reluctant to use it.
Almost all of the code is already written. But this new hardware release aims to bring CAN support inside the board, instead of having it as an external module.
Incidentally, i have only specified a "CAN Daughterboard" with a pinout simillar to the Click boards (so SPI, control signals, power). Nothing stops me to use a second microcontroller on the daugtherboard that handle only CAN and comms to the master, i could program the firmware of the daughterboard from the master if i wanted to (ICSP is already there, SPI), so firmware design is greatly simplified by dividing the load.

What i want to achieve is being able to write a function like it was dumb, blocking code. Wait inside the function until a flag magically changes (the gods will make the other code execute) and do not overthink things too much, adding a line doesn't make the whole castle fall.

Functions for comms are so much easier to write when you can do it like that..
« Last Edit: June 10, 2022, 03:39:23 pm by JPortici »
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 19497
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Microcontroller is not important, but it's a PIC24FJ. Plenty of ram, nice architecture, supported by some commercial/free RTOS. Code/RAM usage is negligible so recource hog by the RTOS is not an issue

Using an RTOS as an implementation technology won't make implementation of FSMs easier. All it will do is add another layer of complexity, with hidden characteristics.

I use FSMs to achieve multitasking because i can split operations in smaller steps so i can run all functions in the superloop as if they were running at the same time
Using an RTOS would mean that i don't have to use FSM to split the operation in smaller steps, it's the scheduler (or a yield) that pauses the task
Unless i got the core concept wrong?

As stated there, that is a either a strange definition of FSMs or a strange thing to use FSMs to achieve.

If you need preemptive multitasking, then an OS is a reasonable tool. But it isn't necessarily real time, and if task coordination is required then it isn't necessarily simpler.

What are your comments on a precondition I raised earlier? "I presume your FSMs are specified in an FSM description language of whatever kind. Statecharts, CSP, and SDL are well-known techniques, but there are many others. If that isn't the case, then you should start there before implementing anything."
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline nctnico

  • Super Contributor
  • ***
  • Posts: 26906
  • Country: nl
    • NCT Developments
It is worth the effort to partition the code to work with an RTOS even if the RTOS doesn't do much in the way of scheduling.  Getting rid of the superloop is the goal.  There is no point in wasting cycles testing for something that hasn't happened.
IMHO this is a misconception. After all: how does the OS knows that something needs to be done? It has to test whether something happened!
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Offline emece67

  • Frequent Contributor
  • **
  • !
  • Posts: 614
  • Country: 00
« Last Edit: August 19, 2022, 05:33:24 pm by emece67 »
 

Offline JPorticiTopic starter

  • Super Contributor
  • ***
  • Posts: 3461
  • Country: it
What are your comments on a precondition I raised earlier? "I presume your FSMs are specified in an FSM description language of whatever kind. Statecharts, CSP, and SDL are well-known techniques, but there are many others. If that isn't the case, then you should start there before implementing anything."

Please see the EDIT,
Re: your question no i do not use description languages on a piece of software if it's what you ask. But i do write them down on paper/whiteboard as a stetachart so i can decide the number of actions, steps, errors, flags, ...

For example, i have to configure a BLE module via AT commands.
There is a sequence of actions to perform. Because doing it in the dumb way holds the CPU for a noticeable time i had to split everything in small steps, something like this
Code: [Select]
send_data(stateEnum_t set_previous_state, const uint8_t *str, uint8_t strLen);

switch (state) {
  case CHECK_PRESENCE:
    return send_data(CHECK_PRESENCE,"AT",2);
  case SET_BAUD_RATE:
    return send_data(SET_BAUD_RATE,"AT+BAUD2",8);
  .......
  case SEND_DATA:
    if (txFinished) {
      if (strPos >= strSize) {
        //Finished Sending Data
        return WAIT_FOR_ANSWER;
      }
      else {
        //Send More Data
        TXBUF = txData[strPos];
        strPos++;
        return SEND_DATA;
      }
    }
    else {
      //Wait For uart TX to finish
      return SEND_DATA;
    }
  case WAIT_FOR_ANSWER:
    if (timeout) {
      //Timeout. Check content of RX buffer
      if (!strncmp(rxBuffer,"OK",2)) {
        switch(previous_state) {
          case CHECK_PRESENCE:
            //Module is present. Set baud rate
            return SET_BAUD_RATE;
            .......
        }
      else {
        //Unexpected answer. Error handling
        .....
      }
    else {
      //keep waiting for timeout
      return WAIT_FOR_ANSWER;
    }

and that is what i want to avoid doing. I want to have a mutex so i can hold the communication channel, and write it as a dumb linear function.
Implementing protocols over can, frame handling and such has proven to be much much much more complex as a series of state machines, or a very big single state machine. a dumb linear function in a worker thread is what i would do if it was done on a PC
« Last Edit: June 10, 2022, 03:56:23 pm by JPortici »
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 19497
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
What are your comments on a precondition I raised earlier? "I presume your FSMs are specified in an FSM description language of whatever kind. Statecharts, CSP, and SDL are well-known techniques, but there are many others. If that isn't the case, then you should start there before implementing anything."

Please see the EDIT,
Re: your question no i do not use description languages on a piece of software if it's what you ask. But i do write them down on paper/whiteboard as a stetachart so i can decide the number of actions, steps, errors, flags, ...

For example, i have to configure a BLE module via AT commands.
There is a sequence of actions to perform. Because doing it in the dumb way holds the CPU for a noticeable time i had to split everything in small steps, something like this
Code: [Select]
send_data(stateEnum_t set_previous_state, const uint8_t *str, uint8_t strLen);

switch (state) {
  case CHECK_PRESENCE:
    return send_data(CHECK_PRESENCE,"AT",2);
  case SET_BAUD_RATE:
    return send_data(SET_BAUD_RATE,"AT+BAUD2",8);
  .......
  case SEND_DATA:
    if (txFinished) {
      if (strPos >= strSize) {
        //Finished Sending Data
        return WAIT_FOR_ANSWER;
      }
      else {
        //Send More Data
        TXBUF = txData[strPos];
        strPos++;
        return SEND_DATA;
      }
    }
    else {
      //Wait For uart TX to finish
      return SEND_DATA;
    }
  case WAIT_FOR_ANSWER:
    if (timeout) {
      //Timeout. Check content of RX buffer
      if (!strncmp(rxBuffer,"OK",2)) {
        switch(previous_state) {
          case CHECK_PRESENCE:
            //Module is present. Set baud rate
            return SET_BAUD_RATE;
            .......
        }
      else {
        //Unexpected answer. Error handling
        .....
      }
    else {
      //keep waiting for timeout
      return WAIT_FOR_ANSWER;
    }

and that is what i want to avoid doing

That confirms my suspicion.

As I wrote in my first response "It sounds to me as if you would benefit from using alternative design patterns to implement FSMs. If you are using if/then/else or case statements for FSMs with more than half a dozen states/events, then that will definitely be the case. I've seen the results of such cancerous implementation techniques; the result was very ugly and very expensive. People changed job rather than work on it!"

You will benefit from thinking about and understanding alternative design patterns.

For what causes event processing code to be executed, see my first post or Doctorandus_P's post.

For implementing FSMs two common alternatives are:
  • a 2D array of function pointers. One dimension is the state, the other is the event. When an event arrives then the function specified by the event+state is executed. Benefit: you have to explicitly fill in all array elements even if those are doNothing() or shouldNeverOccur()
  • a variant of Doctorandus_P's post. Each different event is a different virtual method, each state is a separate class implementing/overriding the relevant virtual methods. The currentState is a Singleton instance of a one of the state classes. Benefit: hierarchical states are easy to implement, shouldNeverOccur() is only defined in the abstract superclass of all states

Those are very brief overviews of standard design patterns that are well described elsewhere.
« Last Edit: June 10, 2022, 04:15:22 pm by tggzzz »
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline emece67

  • Frequent Contributor
  • **
  • !
  • Posts: 614
  • Country: 00
« Last Edit: August 19, 2022, 05:34:09 pm by emece67 »
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 19497
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
And there's also an intermediate option with the state being a pointer to a function.

Yes. With that
  • the outer level of the superloop's if/the/else/case is removed
  • the state-dependent event processing will often devolve back to if/then/else/case statements. That's anathaema to me since it eventually metastasises into an unmaintainable mess

The objective is to execute a piece of code depending on both the state and the event. I prefer such double-dispatching to be clearly visible in the implementation structure.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline Doctorandus_P

  • Super Contributor
  • ***
  • Posts: 3358
  • Country: nl
And there's also an intermediate option with the state being a pointer to a function.
I already wrote that !

Yes. With that
  • the outer level of the superloop's if/the/else/case is removed
  • the state-dependent event processing will often devolve back to if/then/else/case statements. That's anathaema to me since it eventually metastasises into an unmaintainable mess
There is no cure for sloppy programming.
I like C quite a lot because it gives you freedom to do almost anything you want, but it also gives you the freedom to make a mess of things, but that is entirely upto the programmer. Sloppy programmers write bad and unmaintainable code, while better programmers spend more time about maintaining and improving the structure by regular refactoring during maintenance.

I've had quite alot of experiences where it was easy to add a handful of lines of code to quickly fix a problem, but instead I spend half an hour and in the end removed a handful of lines and changed two or three other lines.

Using function pointers to implement a state machine works quite good in C++, where you can use a class for each state machine. It also works good in C if you  use a separate file for each state machine and make sure you only expose sensible parts though the header files.

"Arduino" like programming is horrible. It's quite sad that lots of people with no prior experience in programming start by learning the sloppy "arduino" way of things. sure it's easy to get a LED blinking, but after that you have to unlearn half of what you learned before you can learn some proper programming.

--------------------------
I never used a PIC24 myself, but assume they've got the right stuff for an RTOS.
Rewriting an application to use an RTOS is a quite big task. Because tasks can be interrupted at any time, you have to be very careful around any communication between the different tasks. Throwing an RTOS at your application is not a magic wand that solves all your problems. Using an RTOS also makes debugging more difficult. When using an RTOS it's still bad form to use software delay loops, and these are usually replaced by some yield() funciton or a delay function which is part of the RTOS. And even when using an RTOS, it's still useful to use state machines to divide big tasks into manageable chunks.

I have not seen much of your application, but I guess it's not going to improve much by adding an RTOS. Having knowledge and experience with an RTOS is still good to have in your toolbox though. Even small microcontrollers are starting to get multiple cores, such as the ESP32 and raspi2040. I'm not sure what became of the "propeller chip", It's sort of an 8-core (or 9?) architecture, but with weird limitations and there was no C-compiler available for it for a long time (I think there is now).

Maybe it's better to keep your current application for what it is, and start using and learning with an RTOS for your next application, even if it does not directly need an RTOS.
« Last Edit: June 10, 2022, 05:31:42 pm by Doctorandus_P »
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 19497
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Yes. With that
  • the outer level of the superloop's if/the/else/case is removed
  • the state-dependent event processing will often devolve back to if/then/else/case statements. That's anathaema to me since it eventually metastasises into an unmaintainable mess
There is no cure for sloppy programming.
I like C quite a lot because it gives you freedom to do almost anything you want, but it also gives you the freedom to make a mess of things, but that is entirely upto the programmer. Sloppy programmers write bad and unmaintainable code, while better programmers spend more time about maintaining and improving the structure by regular refactoring during maintenance.

Agreed, except sloppyness is not unique to C and people aren't always given the opportunity to do the right thing.
 
Quote
I've had quite alot of experiences where it was easy to add a handful of lines of code to quickly fix a problem, but instead I spend half an hour and in the end removed a handful of lines and changed two or three other lines.

I know my coding is going well when the number of lines in the application is reducing. People that use lines of code as a metric have problems with that!

Quote
Using function pointers to implement a state machine works quite good in C++, where you can use a class for each state machine. It also works good in C if you  use a separate file for each state machine and make sure you only expose sensible parts though the header files.

There are many implementation techniques, most of them better than if/the/else/case spaghetti code.

Quote

"Arduino" like programming is horrible. It's quite sad that lots of people with no prior experience in programming start by learning the sloppy "arduino" way of things. sure it's easy to get a LED blinking, but after that you have to unlearn half of what you learned before you can learn some proper programming.

--------------------------
I never used a PIC24 myself, but assume they've got the right stuff for an RTOS.
Rewriting an application to use an RTOS is a quite big task. Because tasks can be interrupted at any time, you have to be very careful around any communication between the different tasks. Throwing an RTOS at your application is not a magic wand that solves all your problems. Using an RTOS also makes debugging more difficult. When using an RTOS it's still bad form to use software delay loops, and these are usually replaced by some yield() funciton or a delay function which is part of the RTOS. And even when using an RTOS, it's still useful to use state machines to divide big tasks into manageable chunks.

I have not seen much of your application, but I guess it's not going to improve much by adding an RTOS. Having knowledge and experience with an RTOS is still good to have in your toolbox though. Even small microcontrollers are starting to get multiple cores, such as the ESP32 and raspi2040. I'm not sure what became of the "propeller chip", It's sort of an 8-core (or 9?) architecture, but with weird limitations and there was no C-compiler available for it for a long time (I think there is now).

Maybe it's better to keep your current application for what it is, and start using and learning with an RTOS for your next application, even if it does not directly need an RTOS.

From what I've seen, I don't think an RTOS will help the OP as much as they hope.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Online hans

  • Super Contributor
  • ***
  • Posts: 1638
  • Country: nl
A RTOS is primarily useful to the minimize the fuzz around blocking calls. The RTOS handles this for you, instead of cooperative multitasking where you would need to store intermediate state, return, and on next call resume the context flow again. This can be quite error prone since you'll need to store/restore program state manually, each time, in the right order/variant..

If you look at how RTOS programs are built, you'll find that handcrafted IPC (like setting some global variables) and time-delays are also a very bad way of coding. RTOS' have stuff like signals, mutexes, mailboxes and queues for a good reason. Generally what I would write, is something like:
Code: [Select]
void commsTask() {
    initCommsChipset();
    osDelay(100); // 100ms chipset delay
    enableComms();
    while(1) {
         uint32_t sig = osSignalWait(SIG_ALL, NO_TIMEOUT);
         if (sig & COMMS_RX) {
             handlePackets();
         }
         if (sig & COMMS_TMR_HEARTBEAT) {
            txHeartbeat();
         }
         // etc.
    }
}
void handlePackets() {
    Packet pkt;
    while (osQueueCount() > 0) {
        osQueuePop(&pkt);
        // TODO: do stuff
    }
}
The "events" like COMMS_RX and COMMS_TMR_HEARTBEAT are a bitfield with unique bits (1, 2, 4, 8, etc). These signals are sent to the task from e.g. interrupt routines or other tasks (like a (OS)timer, or an Ethernet peripheral etc.).
osDelay I would -at most- only use for initialization delays. I personally wouldn't want to have long blocking calls in RTOS threads. It kind of defeats the purpose of having a RTOS IMO. All tasks should return to osSignalWait ASAP. That way other "events" for that specific RTOS task can be handled as soon as needed.

This converges a bit towards an event-driven programming based approach. Tasks only get alive when an event is generated by a "message pump". Everything is on-demand.

The problem is that you can't always avoid to have some blocking calls though. For example, a I2cTxBytes with 256 bytes of data may take a while to complete. Especially if you write code a spinlock that waits for each byte to be transmitted. You could minimize the blocking time for the RTOS thread by using interrupt handling approach, and then sending a signal from IRQ back to the task once the whole packet has been transmitted... but the task context itself is still blocked on the I2cTxBytes function during that whole transaction.

So RTOS is not a magic bullet for all your concurrency options. But it does help a lot, and if you plan your tasks carefully, then these conflicts for context don't have to happen that often.

--

A PIC24 is pretty capable to run a task. All context is put on stack. The architecture can easily store/restore it's own context. There is plenty of RAM (most chips have several kB) available. The lower end chips only run 16 MIPS, but there are also 70 or faster chips (with dozens of kB of RAM) out there that will make a low-end 32-bit MCU make a run for it's money. So no problems to foresee there.

Perhaps one final suggestion: you could take a look at proto-threads.
It's a cooperative multitasking system. You would need to write anything context-switching capable in a single function. Under the hood protothreads is pretty syntax for a large switch-case statement, where each case label is a particular point of progress in your "linear" piece of code. (Basically the program counter). Then each "proto"thread will resume it's latest reached case-label, check if it can do any new work since it last exitted. If Yes) it will run to the next case label. If No) It will return immediately and allow a superloop of multiple protothreads all share CPU time.
You would still need to be careful in storing locals because they will be lost each time a "checkpoint" is crossed. But other than that it can be pretty useful.
« Last Edit: June 10, 2022, 06:43:14 pm by hans »
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 14469
  • Country: fr
Another benefit of using an RTOS is potentially a better program structure than some ad-hoc solution (although you may have very well structured ad-hoc solutions too), and a structure that will be much easier to grasp for new developers taking over the project. That is a major benefit.

If say you used FreeRTOS and some new developer comes along, then if they are familiar with FreeRTOS, they'll quickly figure out what the different tasks are, how they interact, etc. If you used some ad-hoc, personal solution, then the new developer is likely to scratch their head for quite a while before they can figure anything out.

If you're the only one working on your project or a very small team, that's less of a problem.

Another point is that a FSM-based ad-hoc scheduling is likely not to be preemptive. The benefit of a preemptive multitasking solution is that it makes some things much easier to implement, such as dealing with blocking calls. In a non-preemptive multitasking scheme, you can't have calls blocking beyond the max time allowed for a given task, so that makes implementation much trickier. An example I gave a while ago was using FatFs. It's much easier to use with a preemptive OS while not blocking execution for extended periods of time.

OTOH, implementing a library such as FatFs with non-blocking calls only (more favorable for cooperative multitasking) is non-trivial and much more annoying.

Just a few thoughts.
 

Offline nctnico

  • Super Contributor
  • ***
  • Posts: 26906
  • Country: nl
    • NCT Developments
Another benefit of using an RTOS is potentially a better program structure than some ad-hoc solution (although you may have very well structured ad-hoc solutions too), and a structure that will be much easier to grasp for new developers taking over the project. That is a major benefit.

If say you used FreeRTOS and some new developer comes along, then if they are familiar with FreeRTOS, they'll quickly figure out what the different tasks are, how they interact, etc. If you used some ad-hoc, personal solution, then the new developer is likely to scratch their head for quite a while before they can figure anything out.
The same can be said for 'super loop' programs. Just add an extra 'loop' that is in fact a seperate process. The big advantage is that all tasks are handled sequentially so mutexes aren't needed. With an OS you'll need to have mutexes and so on which do add complexity.
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Offline JPorticiTopic starter

  • Super Contributor
  • ***
  • Posts: 3461
  • Country: it
I honestly fail to see how function pointers/array of function pointers are different than what i'm doing. I've read many of those threads in this very forum (and elsewhere of course) read some material but i never was able to grasp what's so different. On the next call you are calling a different function instead of a switch that select the function based on the state. I don't see how it is different, other than it's maybe clearer to write, and maybe easier to visualize. I still have to separate the operation in very small steps and think about how they interact with each other..

And i don't see why what you call "arduino style", or what i call blocking code, is bad the moment you have a mechanism to make your CPU do something useful while the current function/routine/thread is blocked (or, in other words, pause the task until time has elapsed, or signal X has been received.).

hans summarize what i had in mind
Quote
A RTOS is primarily useful to the minimize the fuzz around blocking calls. The RTOS handles this for you, instead of cooperative multitasking where you would need to store intermediate state, return, and on next call resume the context flow again. This can be quite error prone since you'll need to store/restore program state manually, each time, in the right order/variant..
This state machine has become too complex for me to mantain and i don't see how changing the approach in implementing this state machine is going to solve the problem. It looks to me that it may postpone the moment in which is going to be too complex to mantain.
« Last Edit: June 10, 2022, 07:06:12 pm by JPortici »
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 19497
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
...
This converges a bit towards an event-driven programming based approach. Tasks only get alive when an event is generated by a "message pump". Everything is on-demand.

The problem is that you can't always avoid to have some blocking calls though. For example, a I2cTxBytes with 256 bytes of data may take a while to complete. Especially if you write code a spinlock that waits for each byte to be transmitted. You could minimize the blocking time for the RTOS thread by using interrupt handling approach, and then sending a signal from IRQ back to the task once the whole packet has been transmitted... but the task context itself is still blocked on the I2cTxBytes function during that whole transaction.

In FSM terms, processing an event has to be atomic: it must proceed to completion before processing the next event. Failure to observe that will lead to occasional unrepeatable failures, and probably horrible "sticking plaster" code to cover the wound.

The solution is simple, of course: have several concurrent blocking FSMs, each executing independently possibly in separate threads.

Thus in the case you give, the packet would be passed as a message to the second FSM. At that point the first FSM's event processing would then have completed atomically, and it could start processing the next event. The second FSM would be responsible for whatever actions are necessary to transmit the packet.

If the first FSM's event processing is not complete until the packet has finished transmission, then there are a number of alternatives. Firstly simply block processing new events! Secondly, add a new state "TxInProgress", which is exited when the second FSM sends a txCompleted event. Hierarchical FSMs are likely to be a benefit here. There are others.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 19497
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
I honestly fail to see how function pointers/array of function pointers are different than what i'm doing. I've read many of those threads in this very forum (and elsewhere of course) read some material but i never was able to grasp what's so different. On the next call you are calling a different function instead of a switch that select the function based on the state. I don't see how it is different, other than it's maybe clearer to write, and maybe easier to visualize. I still have to separate the operation in very small steps and think about how they interact with each other..

Forums are not a good source for that information, because individuals quickly write partial answers that can - at best - point you in a suitable direction.

What you need is a decent textbook that someone took time and trouble to carefully construct.

Quote
This state machine has become too complex for me to mantain and i don't see how changing the approach is going to solve the problem. It looks to me that it may postpone the moment in which is going to be too complex to mantain.

Key question: is the complexity
  • inherent in the application. If so, tough!
  • part of your FSM. If so, you might need to refactor your FSM, possibly into multiple independent FSMs
  • part of your implementation, e.g. if-then-else-case statements. If so use a different design pattern
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 19497
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
https://www.state-machine.com/psicc2
Free Downloads    Download complete book in PDF
https://www.state-machine.com/doc/PSiCC2.pdf

A quick scan of that book (Chapters 1 and 3) leads me to believe the OP would find it useful.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline tellurium

  • Regular Contributor
  • *
  • Posts: 229
  • Country: ua
I honestly fail to see how function pointers/array of function pointers are different than what i'm doing.

From your original post, I've concluded that you wanted to blend several state machines into a huge single one.
On the other hand, using "function pointers" approach I understand as when each FSM is kept separate, and main loop calls each sequentially, e.g. with two FSMs:

Code: [Select]
void main(void) {
  struct fsm1_state state1 = {....};
  struct fsm2_state state2 = {....};

  fsm1_init(&state1);
  fsm2_init(&state2);

  struct shared_queue queue = {....};
 
  for (;;) {
    fsm1_process(&state1, &queue);
    fsm2_process(&state2, &queue);
  }
}

As already noted in this thread, as soon as "*_process" FSM functions cooperate (do not block for too long), that should work fine.
Open source embedded network library https://mongoose.ws
TCP/IP stack + TLS1.3 + HTTP/WebSocket/MQTT in a single file
 

Offline nctnico

  • Super Contributor
  • ***
  • Posts: 26906
  • Country: nl
    • NCT Developments
I honestly fail to see how function pointers/array of function pointers are different than what i'm doing.

From your original post, I've concluded that you wanted to blend several state machines into a huge single one.
On the other hand, using "function pointers" approach I understand as when each FSM is kept separate, and main loop calls each sequentially, e.g. with two FSMs:
I agree. It is better to have several small statemachines compared to having a single large one.

When dealing with hardware modules (like a modem) that needs to be initialised I use a statemachine that takes care of initialisation (and monitoring whether the device is still working properly). Towards the outside the API is very small. The status is either 'OK' or 'not OK'. Furthermore there can be function calls that send or receive data. This is an easy interface to deal with from a higher level statemachine.

I'm not sure whether function pointers are a good idea. In my experience this solution tends to detach the statemachine's intend a lot from the code which makes it less easy to follow. A switch-case is a much more confined solution. I'd only use function pointers is performance is an issue or when the statemachine is too large to cover with a switch case (which could be an indication that the problem should be split into multiple statemachines).
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf