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

0 Members and 1 Guest are viewing this topic.

Offline Doctorandus_P

  • Super Contributor
  • ***
  • Posts: 3369
  • Country: nl
Using a big switch compared to function pointers is not very different. Function pointers are probably a bit quicker, because the pointer can just be loaded and executed, while a switch has to search through it's states, but the difference is very likely negligible.

For me the biggest advantage is readability.
Using function pointers removes a few layers of indentation in the source code, and with each layer of indentation you loose overview and code becomes harder to maintain.

For example I've written a networks stack that sends packets over a UART with RS485 drivers.
For simple applications I have a class that just formats a packet and sends it and can also receive packets, but not much more. It's just a handful of functions and ISR's.

I also have a state machine layer that is optional. The state machine is used for more complex operations. It does automatic resends after variable and automatically adjusted timeouts. It can call callback functions, and those callback functions can optionally re-start the transceiver state machine, by sending a new packet and waiting for a response, etc. This state machine makes a lot of use of a timer, so I have a timer embedded in the state machine itself. The state machine looks like:

Code: [Select]
uint8_t TTransceiver::Run (void) { // Run Statemachine, call this repeatedly.

Status |= TRANSCEIVER_STATUS_RUN;
if (TimeTicks) {
TimeTicks--;
}
while (Status & TRANSCEIVER_STATUS_RUN) {
(this->*pSMState)( ); // Execute the current/new state.
}
return Status;
}

At the point the function pointer is executed you have 2 levels of indentation. If it had a switch there, it would add another level of indentation, but by using a function pointer it reduces the indentation.

Also, because it's just 12 lines of code surrounding the function call, it is very easy to analyze what happens around the state machine. From this code you can see that it calls states repeatedly untill the RUN flag is cleared.

By using separate functions you can also use static variables in the state functions to keep track of data that only has a local context, but which must be saved in between function calls. (Although I did not use that in this statemachine).

I find that keeping functions to below a page, or even below 30 lines is preferable. It makes all problems local and it adds reviewable and easy to understand structure.
If there was for example a switch() with 400 lines of code in the place of the function pointer, then it becomes a lot harder to verify what the while loop actually does and if there is more code in that while loop or not. Now you see it instantly.
« Last Edit: June 10, 2022, 09:21:31 pm by Doctorandus_P »
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19529
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
It seems we've all come to a similar conclusion, whether or not it is correct :)

Multiple cooperating FSMs are a traditional engineering solution, and in many cases are actually explicit in the specification. The FSMs can be implemented using any of the design patterns including but not limited to one FSM per RTOS thread.

A key indication that an implementation technique is sound is if you can easily associate an erroneous behaviour with a specific block of code. That can easily become excessively difficult with nested if/then/else/case statements. It can be easy with one function/method for each state+event pair.
« Last Edit: June 10, 2022, 11:25:00 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 cv007

  • Frequent Contributor
  • **
  • Posts: 828
Quote
(i saw some time ago some complex macro abortion that provided some sort of context switching somehow)
Its possible you are referring to protothreads, although its not complex but is mostly macros. Not a big fan of any macros, but it would be an easy method to produce 'tasks' with sequential code without having to create any state/switch/case directly.
 

Offline Siwastaja

  • Super Contributor
  • ***
  • Posts: 8180
  • Country: fi
Interrupt driven works really well. Do utilize the following:

* Interrupt vectors relocated in RAM. This way, you can change states by changing the vector, making code smaller and more readable, because instead of massive switch-case handlers, you have functions, and just set a function pointer.

* NVIC->STIR! Do utilize software interrupts, to demote ISR priority. For example, in priority 2 SPI handlers functions, collect data in one of the two buffers. At the last state, swap pointers between the two buffers and trigger a priority 14 processing function. Being lower priority, it will run after the SPI handler function returns. This processing function can take long time, and SPI data collection ISR functions for the next dataset keep pre-empting the processing.

* What if the slow processing function needs to start doing something even slower? Just trigger priority 15 software interrupt from there.

A recent software project of ours interfaces numerous sensors (of SPI and other types, some with very esoteric protocols requiring bit-banging), processes the data and outputs to the CAN. The basic structure is like this:

init_sensor():
   init one-shot timer for a sensor
   set interrupt vector for the timer to sensor_init0()

sensor_init0():
   do first initialization write, etc.
   set interrupt vector for the timer to sensor_init1(), re-arm timer
...

sensor_run0():
   read result Y from peripheral FIFO
   start SPI read operation for register X
   re-arm timer for run1
   if(!processing_done) error(123); // processing ISR took too long
   swap(p_accumulation, p_processing);
   NVIC->STIR = PROCESSING_ISRn;
   processing_done = 0;

sensor_run1():
   read result X from peripheral FIFO
   start SPI read operation for register Y
   re-arm timer for run0

processing_isr():
   do_math_wizardry();
   can_send(...);
   processing_done = 1;

This seems to have scaled well, despite the fact that originally what was supposed to interface with two sensor, soon started to interface with 5-6 sensors. All runs in parallel.
« Last Edit: June 11, 2022, 06:14:42 am by Siwastaja »
 

Offline Simon

  • Global Moderator
  • *****
  • Posts: 17821
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
I recently came up with an idea to manage my own code. I tend to use a counter to increment a time keeping variable and at set intervals run a function full of code, usually other functions so that I can break it all down into files. This is a bit of a mess. What I will trial next is to give every function an ID, the ID being it's priority. So when the counter interrupt fires a variable or several are set to all 1's. each bit is a task and any function will have a number in it that the priority variable is compared to. This way whatever order I put my functions in they will execute in their priority order, at the end of the function it's bit in the variable is reset. I suppose this means that if in one function something specific happens that means another should run sooner than the next timeout by resetting the bit for that function.

When i hear about things like an RTOS I always wonder how the microcontroller can actually switch from one function to another mid function and generally think that this should not be necessary if any one function is small enough that completing it's execution is not a problem in terms of time. Real urgent things are on interrupts anyway..
 

Offline peter-h

  • Super Contributor
  • ***
  • Posts: 3701
  • Country: gb
  • Doing electronics since the 1960s...
I've been coding under FreeRTOS for over a year and while I have not used ~99% of its vast array of features, I would go for it every time, even for simple projects.

But then I am speaking from the POV of having already got it set up (by somebody else). I reckon he spent a week on that. And I will build every future project on the same platform; it is easily fast enough. My actuarial life expectancy is about 20 years :)

The perf hit is very small. I stepped through some mutex code (I use mutexes) and the exec time was around 1us (168MHz 32F417). I have not used message queues.
« Last Edit: June 11, 2022, 06:38:45 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19529
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Using mutexes can become difficult to control and to prove their will never be deadlock or lovelock.

Putting messages in queues makes it easier to see and understand the interactions between processes, and to measure where work is building up in the system (look at the queue length). It also allows workload to be distributed across multiple worker threads, which can be beneficial in some systems
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 tggzzz

  • Super Contributor
  • ***
  • Posts: 19529
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
When i hear about things like an RTOS I always wonder how the microcontroller can actually switch from one function to another mid function and generally think that this should not be necessary if any one function is small enough that completing it's execution is not a problem in terms of time. Real urgent things are on interrupts anyway..

You can't do that in C; a small amount of assembler is required to fiddle with the stack pointer and save any "hidden" context that is unknown to C.

It is also worth being aware of the fundamental differences between preemptive RTOSs and cooperative non-preemptive RTOSs.
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 peter-h

  • Super Contributor
  • ***
  • Posts: 3701
  • Country: gb
  • Doing electronics since the 1960s...
Yes, FreeRTOS contains a fair bit of assembler.

The basic thing is that an RTOS does not magically deliver performance where there isn't enough. In fact I would say an RTOS cannot improve on a well designed state machine combined with interrupts.

I have been coding stuff as above since c. 1980 but by ~1989 things got so unwieldy (multi channel serial comms products with protocol conversion going on on multiple channels) I wrote my own simple RTOS. It switched tasks every 10ms and had calls like Yield() for use when a task was waiting.

And this is the basic thing an RTOS gives you: you can write each task like it owns the machine.

Like I said, an RTOS won't magic CPU power from nowhere. Most embedded systems have a number of low priority tasks and 1 or 2 high priority ones. That lends itself to a loop or a state machine together with a timer or event ISR, but it also lends itself to an RTOS plus a timer or event ISR and you get the added benefit that your code is much easier to understand. In some cases one can use an RTOS which runs the high priority task(s) as RTOS tasks (instead of ISRs) but with a high priority, and that's quite neat also... if you can make it work.

The problem is that some people build complicated edifices around an RTOS, fiddling with task priorities to make things work. These are probably working close to the CPU perf limit. Then they write a 300 page book on RTOSes :) And then you get caught when you really need to run some task ASAP but you had already set it to a low priority to make something else run fast enough. I am sure there are eloquent solutions, if you are paid by the hour and it isn't your own company :)

In the end there is only one CPU. The best way to use an RTOS is by writing each task to co-operate i.e. yield when waiting for something. Then one can often write the whole project as all tasks with idle priority.
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 14495
  • Country: fr
Using mutexes can become difficult to control and to prove their will never be deadlock or lovelock.

Putting messages in queues makes it easier to see and understand the interactions between processes, and to measure where work is building up in the system (look at the queue length). It also allows workload to be distributed across multiple worker threads, which can be beneficial in some systems

Yep. They're a very powerful IPC/sync mechanism that is much too often overlooked.
 

Offline Simon

  • Global Moderator
  • *****
  • Posts: 17821
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
well my aim is usually for simplicity so that i can understand my own code later. I here of multicore microcontrollers and think OK I guess it needs some sort of OS. I get a little confused when you have RTOS available on a fairly simple CPU like AVR or the lower power ARCM like M0(+), I mean threads, one task yielding when it is idle? Any code I have ever written is never "idles" a function is called and it runs and exits, the only waiting is for stuff where you put a while in to test a peripheral register bit usually as a matter of good practice than because you expect to wait several clock cycles for the register to be ready. Things like an ADC I write so that they fire an interrupt when done rather than wait for the ready bit to clear.

It seems to me that the real embedded world has been invaded by people who want to be able to say that they are programming these microcontrollers when in actual fact they are just programmers with OS experience and cannot cope with actually working out how to manage their resources.
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19529
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
well my aim is usually for simplicity so that i can understand my own code later.

My objective is to be able to predict what will happen when the code executes. Very different, but the solution addresses both objectives.

Quote
I here of multicore microcontrollers and think OK I guess it needs some sort of OS. I get a little confused when you have RTOS available on a fairly simple CPU like AVR or the lower power ARCM like M0(+), I mean threads, one task yielding when it is idle? Any code I have ever written is never "idles" a function is called and it runs and exits, the only waiting is for stuff where you put a while in to test a peripheral register bit usually as a matter of good practice than because you expect to wait several clock cycles for the register to be ready. Things like an ADC I write so that they fire an interrupt when done rather than wait for the ready bit to clear.

It seems to me that the real embedded world has been invaded by people who want to be able to say that they are programming these microcontrollers when in actual fact they are just programmers with OS experience and cannot cope with actually working out how to manage their resources.

I first used a simple cooperative RTOS on a Z80, back in 1982.

"Yield" is a shorthand. A typical operation is more usually "waitUntilConditionOrEvent", where the wait implies yielding.

The RTOS implements the yield function to look at all tasks events and conditions, and decide which yield has completed, i.e. whick task resumes.
« Last Edit: June 12, 2022, 06:49:10 am 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 Simon

  • Global Moderator
  • *****
  • Posts: 17821
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
so you are executing code, that function running right now has control of the CPU, how does anything else decide to put that function on hold and run another? My assumption is that the code itself must be written in such a way that it is broken into small function chunks that at the end of each a decision can be made about suspending the whole stream of functions gathered into a task? It feels like writing code on an RTOS requires it to be written in a way that works with the RTOS rather than just chucking your code into an RTOS so it's quite a different way of working. My projects are usually quite simple, I don't have a situation where something can take too long, if it does I break it up nott hat that has happened yet. The only time I had to worry abut how long something took was when using arduino to talk to a hitachi style display, the arduino code is naturally some simple code using delays, the whole thing took 25ms. When I recreated the program in bare metal I wrote the screen handling around a state machine and a 100µs interrupt, so I made sure that the CPU never had to wait, it would always carry out every 100µs one half of the task of sending a character. Naturally this implementation is a bit clunky for portability which is where I came up with my very simple scheduling system.
 

Online gf

  • Super Contributor
  • ***
  • Posts: 1196
  • Country: de
so you are executing code, that function running right now has control of the CPU, how does anything else decide to put that function on hold and run another?

With preemptive multitasking, a thread can be suspended at any time by the OS, when another thread with a higher priority becomes runnable.
With cooperative multitasking, a thread can only give up CPU itself (either when it waits for something, or by voluntarily yielding every now and then, even if has more work to do) in order that a re-scheduling can happen.
 

Online tellurium

  • Regular Contributor
  • *
  • Posts: 232
  • Country: ua
My assumption is that the code itself must be written in such a way that it is broken into small function chunks that at the end of each a decision can be made about suspending the whole stream of functions gathered into a task?

Sometimes it is very difficult to do so, and the only solution is to resort to RTOS.

A real life example: ESP8266 chip with non-OS SDK (no RTOS). It is a wifi chip, where "tasks" decide themselves when to yield.

Wifi requires periodic frame exchange to remain on the network. Now, one implements a networking code that uses TLS, and server side uses certificates that require several seconds to handshake. Wifi gets lost when crypto calculation finishes. Long story short: the only solution was to switch to RTOS, because even in the middle of the busy crypto calculation, an RTOS interrupts it and gives time to the low-level WiFi tasks that keeps a connection open.

Would it be possible to keep the firmware no-OS? Theoretically, yes. By using a crypto library that sprinkles yield calls all over the code implementing coroutine-type API. I am not aware of such library. And I think it would be notoriously hard to implement one.
Open source embedded network library https://mongoose.ws
TCP/IP stack + TLS1.3 + HTTP/WebSocket/MQTT in a single file
 

Offline peter-h

  • Super Contributor
  • ***
  • Posts: 3701
  • Country: gb
  • Doing electronics since the 1960s...
That is another issue. AFAIK all the "internet" type libs require an RTOS, for the reasons given. A state machine would be too complicated.

Those functions (like TLS) are so complicated anyway that very few people understand them.

And a lot of the code has been lifted from Linux which is an "RTOS", sort of :)
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline Simon

  • Global Moderator
  • *****
  • Posts: 17821
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics
My assumption is that the code itself must be written in such a way that it is broken into small function chunks that at the end of each a decision can be made about suspending the whole stream of functions gathered into a task?

Sometimes it is very difficult to do so, and the only solution is to resort to RTOS.

A real life example: ESP8266 chip with non-OS SDK (no RTOS). It is a wifi chip, where "tasks" decide themselves when to yield.


I think you misunderstood. I meant that to use an RTOS the code must be written in this way. The CPU can only execute what it is being told to so the code must be written in small blocks so that there is a break where the system can decide what to do next.
 

Online nctnico

  • Super Contributor
  • ***
  • Posts: 26928
  • Country: nl
    • NCT Developments
The best way to use an RTOS is by writing each task to co-operate i.e. yield when waiting for something. Then one can often write the whole project as all tasks with idle priority.
This is true (required) for any OS. If you are not yielding then the OS has no clue which process really needs time to do something usefull.
There are small lies, big lies and then there is what is on the screen of your oscilloscope.
 

Offline Siwastaja

  • Super Contributor
  • ***
  • Posts: 8180
  • Country: fi
Sometimes it is very difficult to do so, and the only solution is to resort to RTOS.

A real life example: ESP8266 chip with non-OS SDK (no RTOS). It is a wifi chip, where "tasks" decide themselves when to yield.

Wifi requires periodic frame exchange to remain on the network. Now, one implements a networking code that uses TLS, and server side uses certificates that require several seconds to handshake. Wifi gets lost when crypto calculation finishes. Long story short: the only solution was to switch to RTOS, because even in the middle of the busy crypto calculation, an RTOS interrupts it and gives time to the low-level WiFi tasks that keeps a connection open.

Would it be possible to keep the firmware no-OS? Theoretically, yes. By using a crypto library that sprinkles yield calls all over the code implementing coroutine-type API. I am not aware of such library. And I think it would be notoriously hard to implement one.

I don't think I follow. Ever heard of interrupts? What's the problem? The task which is interrupted does not need to know about the existence of interrupts, or the fact it's being interrupted. Nothing needs to be "sprinkled" in crypto processing code.
 

Offline tggzzz

  • Super Contributor
  • ***
  • Posts: 19529
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
My assumption is that the code itself must be written in such a way that it is broken into small function chunks that at the end of each a decision can be made about suspending the whole stream of functions gathered into a task?

Sometimes it is very difficult to do so, and the only solution is to resort to RTOS.

A real life example: ESP8266 chip with non-OS SDK (no RTOS). It is a wifi chip, where "tasks" decide themselves when to yield.


I think you misunderstood. I meant that to use an RTOS the code must be written in this way. The CPU can only execute what it is being told to so the code must be written in small blocks so that there is a break where the system can decide what to do next.

Not true, for preemptive RTOSs where a task can be suspended between any two instructions.

The RTOS contains a scheduler, whose job is to determine which task should be executing at any given instant. The scheduler is not executed unless there is a reason to re-determine which task should be running. Hence until something happens, the current task will continue executing.

Such reasons are essentially:
  • an external hardware interrupt occurs. That can be any peripheral input occurring or output completing, including timers that can be used to "timeslice" tasks of equal priority.
  • the currently executing process executes a system call. That can be anything including blocking and non-blocking i/o and yield().

Since the hardware interrupts can occur at any point in the execution of a task, a task can be suspended between any two instructions. It it the RTOSs' responsibility to ensure the task does not detect whether or not that occurs.

Incidentally, there are good theoretical and practical reasons to use the same hardware and software mechanisms to implement both mechanisms. Some processors do that, some don't.

Non-preemptive RTOSs don't have all those properties.
« Last Edit: June 12, 2022, 07:09:54 am 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 peter-h

  • Super Contributor
  • ***
  • Posts: 3701
  • Country: gb
  • Doing electronics since the 1960s...
Quote
This is true (required) for any OS. If you are not yielding then the OS has no clue which process really needs time to do something usefull.

It may be true for best performance but one can have an RTOS which chops tasks every x ms regardless. I wrote one of those in the 1980s. It didn't do a whole lot else, but it supported a Yield call, and I reckon the overall result was the same as the vastly more complicated FreeRTOS :smile:

Actually I think FreeRTOS does chop tasks regardless, at every tick, but only to switch to a higher priority one.

Here is my old RTOS. The company vanished in 1993 (I left in 1991).

Code: [Select]

.COMMENT \

A SIMPLE REAL-TIME EXECUTIVE (RTX) for Z180/Z280. MUB is true for Z280.

1. Supports any number of tasks (limited to 255 in some places).
2. Total timer interrupt task switch time is less than 60us (Z180/6MHz).
3. Each task must have its own stack.


The RTX has five entry points:

1. Initialisation.
Entered (with JP) just once after power-up, to set-up initial PC and
SP values.  This entry kicks-off the RTX and never returns.

2. Timer interrupt (timer tick).
This is the normal entry point.  At each timer tick, the RTX switches
to the next task.

3. Switch to next task.
This entry point can be used by the currently executing task to
ask the RTX to switch to the next task, e.g. if the task has nothing
more to do.  On the Z180, this entry point is the same as the timer
tick entry point, because the timer int pushes the same things on the
stack as a CALL does.
On the MUB, this entry is implemented as RST 38h, for compact code.

4. Suspend a task (until re-activated).
This 'kills' a task.  The RTX will ignore that task, until the task is
re-activated with the function below.

5. Re-activate a (suspended) task.
This re-activates a (previously suspended) task.


For each task, the RTX maintains the data area below:

offset contents initial value
+0 af from table
+2 bc from table
+4 de from table
+6 hl from table
+8 ix from table
+10 iy from table
+12 sp task's sp (*before* the timer interrupt)
+14 pc task's entry point
+16 msr task's MSR value (used with Z280 only)
+18 status 0: task active, 1: task suspended
+19 reserved

The RTX uses push/pop instructions to save/reload registers because these are
probably the fastest way.

The RTX loads iopage to 00h (as part of clearing the timer 0 pending int) - this
is OK because throughout the MUB ints are OFF whenever iopage<>0 and the RTX
should therefore never tick with iopage<>0.

Each task's MSR is preserved, although currently all tasks run with 'ei all_ints'
and preserving MSR is not really necessary; it could just be forced to 'all_ints'
when each task is entered.



\



global REAL_RTX_SWITCH
global RTX_ACTIVATE_ENTRY
global RTX_GET_TASK_STATUS
global RTX_INIT_ENTRY
global RTX_SUSPEND_ENTRY
global RTX_TIMER_INT_ENTRY

; code (all these are RTX tasks)
external BIG_REQUESTS
external FRONT_PANEL
external LANC_OPERATIONS
external MAIN_DATA_LOOP
external NEW_LINKMAPS
external ONESEC
external PROCESS_IPC_BUFS
external PROCESS_OP_TIMEOUTS
external PROCESS_OUT_RCBUFS
external PROCESS_REM_PLOFN
external SC_EAROM_UPDATE
external SETUP_MODE

; data
external HL_SAVE
external PC_SAVE
external RTX_ALL_SUSPENDED
external RTX_CURRENT_TASK
external RTX_DATA_AREAS
external RTX_DATA_PTR
external RTX_TIMER
external MSR_SAVE
external SP_SAVE
external STACK_START


INCLUDE DEFS.MUB
SECTION CODE,X


MUB EQU TRUE


; RTX initialisation table.  Terminated by a line with PC=0.
; In the future, this should be modded to hold ix,iy,sp,pc,msr values only.
; All tasks have equal priority and execute on a round-robin basis.

; ** The task *order* (i.e. their #s) is assumed by calls to rtx_activate & rtx_suspend **
; ** THEREFORE: ADD NEW TASKS ONLY AT THE *END* OF THE TABLE **

; ** Dont forget to EQUate rtx_tasks in defs.mub to #tasks below (incl PC=0 task) **

RTX_INITIAL_REGS: ; task# v
; v
;    AF BC DE HL IX IY SP    PC            MSR    stat+res  v

 DW  0  0  0  0 0  0  STACK_START+(1*STACK_SIZE)  MAIN_DATA_LOOP      ALL_INTS    0  ; 0
 DW  0  0  0  0 0  0  STACK_START+(2*STACK_SIZE)  LANC_OPERATIONS     ALL_INTS    0  ; 1
 DW  0  0  0  0 0  0  STACK_START+(3*STACK_SIZE)  BIG_REQUESTS        ALL_INTS    0  ; 2
 DW  0  0  0  0 0  0  STACK_START+(4*STACK_SIZE)  SC_EAROM_UPDATE     ALL_INTS 0001H ; 3*
 DW  0  0  0  0 0  0  STACK_START+(5*STACK_SIZE)  NEW_LINKMAPS        ALL_INTS 0001H ; 4*
 DW  0  0  0  0 0  0  STACK_START+(6*STACK_SIZE)  SETUP_MODE        ALL_INTS    0  ; 5
 DW  0  0  0  0 0  0  STACK_START+(7*STACK_SIZE)  ONESEC        ALL_INTS 0001H ; 6*
 DW  0  0  0  0 0  0  STACK_START+(8*STACK_SIZE)  FRONT_PANEL        ALL_INTS 0001H ; 7*
 DW  0  0  0  0 0  0  STACK_START+(9*STACK_SIZE)  PROCESS_OP_TIMEOUTS ALL_INTS 0001H ; 8*
 DW  0  0  0  0 0  0  STACK_START+(10*STACK_SIZE) PROCESS_OUT_RCBUFS  ALL_INTS 0001H ; 9*
 DW  0  0  0  0 0  0  STACK_START+(11*STACK_SIZE) PROCESS_IPC_BUFS    ALL_INTS 0001H ;10*
 DW  0  0  0  0 0  0  STACK_START+(12*STACK_SIZE) PROCESS_REM_PLOFN   ALL_INTS 0001H ;11*
 DW  0  0  0  0 0  0  0    0        0    0

; (*) the above tasks such marked run only once and suspend themselves, and are
;      re-activated by some event.  The '0001H' value sets status=01h; this makes
;      the task *initially* suspended (e.g. we *dont* want to update the eeprom at
; every power-up, do we?).


; RTX initialisation/entry point.

RTX_INIT_ENTRY:

DI ; Necessary ! (in case of entry with EI)

; Copy initial register values from ROM into RAM.

LD HL, RTX_INITIAL_REGS
LD DE, RTX_DATA_AREAS
LD BC, RTX_TASKS*RTX_DATA_SIZE
LDIR

; Reset ptr (IX) to base+20 of the first task

LDW (RTX_DATA_PTR), RTX_DATA_AREAS+RTX_DATA_SIZE

LD (RTX_TIMER), RTX_TC ; RTX downcounter time constant

LD (RTX_CURRENT_TASK),0 ; Initial task # := 0

; Enable timer interrupt

  IF MUB
; (no action necessary because timer 0 int is already always enabled
;  at the timer, and rtx1st does an EI etc)
  ELSE
IN0A TCR
SET 5, A ; Timer 1 ints (RTX) ON
OUT0A TCR
  ENDIF

; Set-up SP to 1st POP task's initial values and jump to 1st task

LD SP, RTX_DATA_AREAS
JP RTX1ST ; (also does EI)




; This is the timer interrupt service routine which switches tasks.
; At entry here (and at exit) rtx_data_ptr points to the 1st byte after the
; task's data area (i.e. byte @base+20).  Therefore, when we load SP from
; rtx_data_ptr and start PUSHing the registers, the first word pushed (MSR)
; is written in the right place.  Rtx_data_ptr does NOT point at base+0.

RTX_TIMER_INT_ENTRY:

; First, preserve the regs which we are going to corrupt, etc, while
; adjusting SP back to its value *before* the timer interrupt.
; All this must be done with instructions which dont modify flags.
  ; We get here from ct0 interrupt, with AF'.

  IF MUB
LD (RTX_TIMER), RTX_TC ; Reload the down-counter

EXX
IOPAGE 0FEH    ; Clear CC bit (reset 'int pending')
LD A, 11100000B ; (this could be done *after* the RTX code
OUT (0E1H), A ;  but for some reason doing it there did not
IOPAGE 0 ;  work properly; it does not really matter)
EXX

EX AF, AF' ; and switch back to main AF

INC SP ; Skip over Mode 3 Int Reason Code
INC SP ;  (always the same - boring)
LD (HL_SAVE), HL ; Save HL of interrupted task
POP (MSR_SAVE) ; Save MSR of interrupted task

; This entry point is used by rtx_switch, which has already loaded
; HL and MSR into hl_save and msr_save, and disabled all interrupts.

RTX_ES: POP HL ; HL := PC of interrupted task
LD (SP_SAVE), SP ; Save SP (the value *before* the timer int)
  ELSE
LD (HL_SAVE), HL ; Save HL of interrupted task
POP HL ; HL := PC of interrupted task
LD (SP_SAVE), SP ; Save SP of interrupted task
  ENDIF

; HL now holds the interrupted task's PC value.
; We save the interrupted task's registers by a series of PUSHes.

LD SP, (RTX_DATA_PTR) ; Set SP to interrupted task's data area
  IF MUB
DEC SP ; Skip over status+reserved bytes
DEC SP
PUSH (MSR_SAVE) ; Save MSR
PUSH HL ; Save PC
PUSH (SP_SAVE) ; Save SP (the value *before* this interrupt)
PUSH IY ; Save IY
PUSH IX ; Save IX
PUSH (HL_SAVE) ; Save HL
  ELSE
DEC SP ; Skip over status+reserved bytes
DEC SP
DEC SP ; Z180: 'push' a dummy value instead of MSR
DEC SP
PUSH HL ; Save PC
LD HL, (SP_SAVE)
PUSH HL ; Save SP
PUSH IY ; Save IY
PUSH IX ; Save IX
LD HL, (HL_SAVE)
PUSH HL ; Save HL
  ENDIF
PUSH DE ; Save DE
PUSH BC ; Save BC
PUSH AF ; Save AF

; Interrupted task is now saved.  Go to next task. 
; If next task's PC=0, this is the end of the task table and we wrap.

LD B, RTX_TASKS ; Max # of tasks to go through

RTX_NX: LD IX, (RTX_DATA_PTR)
LD SP, IX ; This loads SP just right for the POPs below
LD DE, RTX_DATA_SIZE
ADD IX, DE ; Move rtx_data_ptr to next task's data area
LD HL, RTX_CURRENT_TASK
INC (HL) ; Current task # ++
  IF MUB
LD DE, (IX-6) ; Check if next task's PC=0
LD A, D
OR E
  ELSE
LD A, (IX-6)
OR A, (IX-5)
  ENDIF
JR NZ, RTX_01 ; NZ: no, continue

LD IX, RTX_DATA_AREAS+RTX_DATA_SIZE ; else reset ptr to 1st task
LD SP, RTX_DATA_AREAS ; and set SP to start POPping for 1st task
LD (HL), 0 ; and reset 'current task #'

RTX_01: LD (RTX_DATA_PTR), IX

LD A, (IX-2) ; Now check that the newly-selected task is
OR A ; not suspended.
JR Z, RTX1ST ; Z: not suspended - continue
DJNZ RTX_NX ; else loop through all the tasks in the table

; (If all tasks are suspended, we fall out here and the next task gets
;  executed anyway, which does not really matter unless one actually
;  relies on suspended tasks to *never* execute.  Doing this properly
;  is more complicated).

LD A, 1 ; Mark 'all suspended' condition detected
LD (RTX_ALL_SUSPENDED), A

; Load registers for next task.
      ; This is also the entry point for starting the RTX (enter with
;  SP = base of 1st task's data area)

RTX1ST: POP AF ; Load AF
POP BC ; Load BC
POP DE ; Load DE
  IF MUB
POP (HL_SAVE) ; Recover HL
POP IX ; Load IX
POP IY ; Load IY
POP (SP_SAVE) ; Recover SP
POP (PC_SAVE) ; Recover PC
POP HL ; Recover MSR
  ELSE
POP HL
LD (HL_SAVE), HL
POP IX
POP IY
POP HL  
LD (SP_SAVE), HL
POP HL
LD (PC_SAVE), HL
  ENDIF
LD SP, (SP_SAVE) ; Load SP

; Clear the pending interrupt, enable ints and enter the new task.

  IF MUB
PUSH (PC_SAVE) ; Put PC on stack (for RETIL to pop-off)
PUSH HL ; Likewise with MSR
LD HL, (HL_SAVE) ; Load HL
RETIL ; Enter new task
  ELSE
LD HL, (PC_SAVE)
PUSH HL ; Put PC on stack (for RET to pop-off)
LD HL, (HL_SAVE) ; Load HL
EX AF, AF'
IN0A TCR ; Clear the pending timer 1 interrupt bit
IN0A TMDR1L
EX AF, AF'
EI ; Re-enable interrupts
RET ; Enter new task
  ENDIF



; CALL this entry point to cause a switch to the next task.  Done via RST 38H.
; This entry also reloads the RTX tick down-counter, so that the next RTX tick
; cannot occur until after a whole tick period.

REAL_RTX_SWITCH:

  IF MUB
DI ; Prevent 'normal' RTX tick coming in here
PUSH AF
LD A, (RTX_TIMER) ; If rtx is NOT disabled,
INC A
JR Z, RRS_NL
LD (RTX_TIMER), RTX_TC ; then reload its down-counter
RRS_NL: PUSH BC
LD (HL_SAVE), HL ; Save HL straight into RTX's HL save location
LD C, 0
LDCTL HL, (C) ; Read current MSR
LD A, L
OR ALL_INTS ; Correct for 'di' above having cleared the EI bits
LD L, A
LD (MSR_SAVE), HL ; Save MSR straight into RTX's MSR save location
POP BC
POP AF
JP RTX_ES ; Do (nearly) as if a timer tick occured
  ELSE
DI
EX AF, AF'
LD A, RTX_TC ; Reload RTX downcounter time constant
LD (RTX_TIMER), A
EX AF, AF'
JP RTX_TIMER_INT_ENTRY ; Do as if timer tick occurred
  ENDIF



; CALL this entry point to suspend a task.
; Enter with E = task # (0..254).
; If E=255, then the task currently executing is suspended.  This feature
; is useful if a task does not know its own task # (i.e. 'suspend me').

; ALSO, READ THE LARGE COMMENT BELOW.  It effectively means that if a routine
; wants to suspend itself, it must use the 'task#=255' call, NOT a 'task=own#'
; call !!

; Kills DE,HL.

RTX_SUSPEND_ENTRY:

LD D, E ; Make a copy of task#
INC E ; If E=0FFh
JR NZ, RSE_05
LD A, (RTX_CURRENT_TASK) ; then use the 'current' task # instead
LD E, A
INC E
RSE_05: DEC E
  IF MUB
LD A, RTX_DATA_SIZE ; Index to the task's data area
MULTU A, E
ADDW HL, RTX_DATA_AREAS+18
  ELSE
PUSH DE
LD D, RTX_DATA_SIZE
MULDE
LD HL, RTX_DATA_AREAS+18
ADD HL, DE
POP DE
  ENDIF
LD (HL), 1  ; and mark it 'suspended'

; If entry task# = 255, then we have a 'suspend me' call, and the
; task is suspended IMMEDIATELY.  Normally, this is what would be
; really required.  However, if task#<>255, then we have a case where
; one task (or int routine, etc) is suspending another task, and we
; do NOT then perform a rtx_switch.  Doing an rtx_switch makes sense
; only on a 'suspend me' request, not on a 'suspend task n' request.

INC D
CALL Z, REAL_RTX_SWITCH ; and switch immediately to next task

RET ; Return via here when task is re-activated



; CALL this entry point to re-activate a task.
; Enter with E = task # (0..).
; Kills AF,HL (+DE on Z180).

RTX_ACTIVATE_ENTRY:

  IF MUB
LD A, RTX_DATA_SIZE ; Index to the task's data area
MULTU A, E
ADDW HL, RTX_DATA_AREAS+18
  ELSE
LD D, RTX_DATA_SIZE
MULDE
LD HL, RTX_DATA_AREAS+18
ADD HL, DE
  ENDIF
LD (HL), 0 ; and mark it 'active'
RET


; CALL this entry point to find out whether a task is currently activated (not
; suspended).
; Enter with E = task # (0..).
; Returns A=00 if activated, 01 if suspended.
; Kills AF,HL (+DE on Z180).

RTX_GET_TASK_STATUS:
  IF MUB
LD A, RTX_DATA_SIZE ; Index to the task's data area
MULTU A, E
ADDW HL, RTX_DATA_AREAS+18
  ELSE
LD D, RTX_DATA_SIZE
MULDE
LD HL, RTX_DATA_AREAS+18
ADD HL, DE
  ENDIF
LD A, (HL) ; and get the 'status' byte
RET




Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline TomS_

  • Frequent Contributor
  • **
  • Posts: 834
  • Country: gb
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!
FreeRTOS handles this at event time, therefore the "testing" only happens once in response to an event occurring.

If a task is blocked (e.g. waiting for an item to appear in a queue) then it will sit indefinitely in the blocked task list and can gather dust there as far as the scheduler is concerned.

Only once something places an item in the queue does FreeRTOS check to see if a task is blocked on that queue, and if there was, it places the task into the ready queue so that it can run again.

IIRC maybe the only thing that FreeRTOS continually checks in a "dumb" way is whether any tasks have been deleted so that it can remove them from the scheduler. This happens in the underlying idle loop when nothing else is going on, so by the time that is running you're not wasting CPU cycles that could have had a better use.
 

Online tellurium

  • Regular Contributor
  • *
  • Posts: 232
  • Country: ua
I don't think I follow. Ever heard of interrupts? What's the problem? The task which is interrupted does not need to know about the existence of interrupts, or the fact it's being interrupted. Nothing needs to be "sprinkled" in crypto processing code.

Exactly, that's what an RTOS does. It installs a timer interrupt handler, which saves current tasks's registers and switches to another task.
« Last Edit: June 12, 2022, 07:59:18 am by tellurium »
Open source embedded network library https://mongoose.ws
TCP/IP stack + TLS1.3 + HTTP/WebSocket/MQTT in a single file
 

Offline Simon

  • Global Moderator
  • *****
  • Posts: 17821
  • Country: gb
  • Did that just blow up? No? might work after all !!
    • Simon's Electronics


Not true, for preemptive RTOSs where a task can be suspended between any two instructions.

The RTOS contains a scheduler, whose job is to determine which task should be executing at any given instant. The scheduler is not executed unless there is a reason to re-determine which task should be running. Hence until something happens, the current task will continue executing.


Ah yes of course, i was not considering that the RTOS can do the work in a timed interrupt. That makes sense. So If I understand correctly the stack has to be saved so that the current task can be picked up again and when it is anything changed in the task that it gave way to is still unknown to the resumed task. Wow, so these must be quite isolated from each other and have enough to do that doing the swap thing is worth the overhead. I don't think my programs are there yet as if anything they are made in small chunks and talk to each other with global variables.
 

Offline Siwastaja

  • Super Contributor
  • ***
  • Posts: 8180
  • Country: fi
I don't think I follow. Ever heard of interrupts? What's the problem? The task which is interrupted does not need to know about the existence of interrupts, or the fact it's being interrupted. Nothing needs to be "sprinkled" in crypto processing code.

Exactly, that's what an RTOS does. It installs a timer interrupt handler, which saves current tasks's registers and switches to another task.

I still don't follow. This is not what RTOS does. Normally either the CPU core (for example, ARM Cortex), or the C compiler does this register saving and task switching. You of course have to configure the timer interrupt but this is not difficult at all.

Of course, the practical limitation is that when interrupt happens, it starts from the beginning of the handler function. From programmer viewpoint, RTOS makes it possible to have two larger functions running seemingly "concurrently" so that context is switched between the two functions.

Very simplified:
Function A statements:
   A1
   A2
   A3
   A4

Function B statements:
   B1
   B2
   B3
   B4

Interrupt based design:
A1, A2, B1, B2, B3, B4, A3, A4 is easily possible.
A1, A2, B1, B2, A3, B3, B4, A4 is not.

With RTOS, latter is easily possible. You can write long pieces of linear code executed "in parallel", either switched by time-slicing arbitrarily, or specific yielding calls. Whether this helps or hinders completely depends on the case.

Even with just interrupts, assuming you have a modern enough MCU with HW interrupt priorities (nested interrupts), you can easily build complex priority based patterns: A interrupted by B, which is interrupted by C, which triggers low priority task D which is not yet executed, then B is finished, then back to doing A, which is interrupted by E, after which A finally finishes and finally, D gets done.

Now I don't see how the crypto-WiFi example requires the RTOS pattern. If it does, it's very poor design. The fact that manufacturers even advertise non-RTOS networking frameworks tells the story this is easily achievable.
« Last Edit: June 12, 2022, 11:30:40 am by Siwastaja »
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf