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.
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.
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 issueUsing 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?
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.
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."
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;
}
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 thisCode: [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
And there's also an intermediate option with the state being a pointer to a function.
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
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.
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.).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.
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 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.
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..
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.
https://www.state-machine.com/psicc2
Free Downloads Download complete book in PDF
https://www.state-machine.com/doc/PSiCC2.pdf
I honestly fail to see how function pointers/array of function pointers are different than what i'm doing.
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);
}
}
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: