📜 ⬆️ ⬇️

Alternative Embedded Software Design Approach

I decided to write this topic after reading the article “Two approaches to software design for embedded” . While reading, I stumbled upon the phrase: “If the system is going to become big, connecting many different actions and reactions that are also time critical, then there is no alternative to using the real-time OS.” “How is it not?” I thought. Of course, if we are talking about large, high-loaded real-time systems that use large processors, then the OS can not do without, but for more modest microcontroller solutions there is an alternative option. After all, tasks can be performed using the usual switch-case and at the same time provide the necessary reaction time.


Why I personally do not use RTOS



Comrade olekl told about RTOS, I will not focus on this. I will note a couple of points that I singled out for myself - why I don’t use RTOS:

')

Switch-case approach



The terminology is not strong, so let it be such a name.
It is more convenient to look at an example. It uses pseudocode.

The device has two temperature sensors. The polling time of the first sensor is not critical: “interrogated, yes, okay”, let it be a periodicity of 0.2 ms. On exceeding the specified threshold temperature will light the LED. The readings of the second sensor are very important for us. It must be interrogated as often as possible and, on exceeding a predetermined threshold, output “1” per pin in order to turn on the cooling fan with a discrete signal. When the temperature drops to another threshold, turn off the fan. Somewhere every 100 ms the value from the second sensor must be written to the ROM.
An implementation will require a hardware timer interrupt. After all, the only way we can guarantee the execution of tasks in the time allotted to them. In this case, the possibilities for using other interrupts are drastically reduced.
Work with most of the peripherals can be done without interruptions, and very important communication interrupts (for example: UART / SCI) usually have a higher priority than the timer and usually serve to record received / sent bytes, i.e. a lot of time will not take away.
The approach, when the timer only counts time for tasks, and the tasks themselves are performed in the background (or while super-cycle) without prohibiting interruptions does not guarantee the desired execution response.

First, let's make a temperature sensor driver. Its main function is to read the temperature value by SPI.

Structure:
typedef struct { unsigned char SpiCh; //   SPI (A, B, C) unsigned int SpiBrr; //  SPI- unsigned int Value; //    void (*ChipSelect) (unsigned char level_); // Callback    … // -  }TSensor; 

Temperature sensor polling function:

 void SensorDriver(TSensor *p) { p->ChipSelect(0); //   p->Value = SpiRead(p->SpiCh, p->SpiBrr); //    SPI p->ChipSelect(1); //    } 

Our driver is ready. To use it need initialization. The structure can be initialized as a whole using #define, or each field can be used separately. We have two temperature sensors. We create two structures.
 TSensor Sensor1; TSensor Sensor2; void Init(void) { Sensor1.ChipSelect = &ChipSelectFunc1; //      Sensor1.SpiCh = 0; //  SPI Sensor1.SpiBrr = 1000; //  SPI Sensor2.ChipSelect = &ChipSelectFunc2; Sensor2.SpiCh = 0; Sensor2.SpiBrr = 1000; } 


The main function of the driver is to read the temperature. What to do with this data we will decide outside the driver.

Light up the LED:

 void SensorLed(void) { if (Sensor1.Value >= SENSOR_LED_LIMIT) LedPin = 1; else If (Sensor1.Value < SENSOR_LED_LIMIT) LedPin = 0; } 

Turn on / off the fan discrete leg:

 void SensorCooler(void) { if (Sensor2.Value >= SENSOR_LED_LIMIT) CoolerPin = 1; else if (Sensor1.Value < SENSOR_LED_LIMIT) CoolerPin = 0; } 

Strange, but the functions turned out surprisingly similar :)
Write to ROM will be as follows:
the function of the ROM driver will be cyclically performed at a frequency of 1 kHz, while waiting for the data to be written, the instruction “what to do with them” and at what address in memory. Those. it's enough for us to check the readiness of the memory and send it data with instructions from anywhere in the program.

 void SensorValueRecord() { unsigned int Data = Sensor2.Value; //     unsigned int Address = 0; //    if (EepromReady()) //    { //  ,   ,     EepromFunction(Address, Data, WRITE); } } 

We sent the data and when the memory driver comes into operation (and it does it 100 times faster than the SensorValueRecord function), it will already know what to do.
Our functions are ready. Now they need to properly organize.
To do this, we start the interrupt timer with a frequency of 10 kHz (100 μs). This will be our maximum guaranteed task call frequency. Let it be enough. We create functions of the task scheduler, in which we will determine when to start which task.

 #define MAIN_HZ 10000 #define TASK0_FREQ 1000 #define TASK1_FREQ 50 #define TASK2_FREQ 10 //    void AlternativeTaskManager(void) { SensorDriver(&Sensor2); //      SensorCooler(); //     Task0_Execute(); //     } //  1 void Task0_Execute(void) { switch (TaskIndex0) { case 0: EepromDriver(&Eeprom); break; case 1: Task1_Execute(); break; case 2: Task2_Execute(); break; } //   if (++TaskIndex0 >= MAIN_HZ / TASK0_FREQ) TaskIndex0 = 0; } //    50  void Task1_Execute(void) { switch (TaskIndex1) { case 0: SensorDriver(&Sensor1); break; case 1: SensorLed(); break; } if (++TaskIndex1 >= TASK0_FREQ / TASK1_FREQ) TaskIndex1 = 0; } //    10  void Task2_Execute(void) { switch (TaskIndex2) { case 0: SensorValueRecord(); break; case 1: break; } if (++TaskIndex2 >= TASK0_FREQ / TASK2_FREQ) TaskIndex2 = 0; } 


Now it remains to run the scheduler in the interrupt timer and ready.

 interrupt void Timer1_Handler(void) { AlternativeTaskManager(); } 

This system looks like a kind of mechanism with gears: the most important gear directly on the motor shaft and it turns the rest gears.
Puzzles are performed "on the ring." The frequency of their execution depends on the location of the call. The Task0_Execute function will be executed with a frequency of 10kHz, since we call it directly in the timer interrupt (our main gear). It is the frequency division and using the switch-case with TaskIndex0 is determined for what task the time has come. The frequency of calling tasks should be less than 10 kHz.
We set the frequency of tasks for the Task0_Execute cycle equal to 1 kHz, so 10 tasks with a frequency of 1 kHz can be performed in it:

 #define MAIN_HZ 10000 #define TASK0_FREQ 1000 if (++TaskIndex0 >= MAIN_HZ / TASK0_FREQ) 


The structure of the switch-case system


Similarly for Task1_Execute and Task2_Execute. Call them with a frequency of 1 kHz. In the first cycle, tasks should be performed with a frequency of 50 Hz, and in the second - 10 Hz. We get a total of 20 and 100 tasks, respectively.
After completing the dispatcher's tasks, the program returns to the background (super cycle background).
Any non-critical reaction time, they can be placed there.

 void main(void) { Init(); while (1) { DoSomething(); } } 

A DAC is added to the device, and together with the ignition of the LED you need to generate a 4-20 signal? No problem. Create a DAC driver and run it. We add two lines to the SensorLed function that will tell the driver what value to output to it and the dispatcher calls the driver function.

 void SensorLed(void) { if (Sensor1.Value >= SENSOR_LED_LIMIT) { LedPin = 1; Dac.Value = 20; //     } else If (Sensor1.Value < SENSOR_LED_LIMIT) { LedPin = 0; Dac.Value = 4; //     } } 

 //    50  void Task1_Execute(void) { switch (TaskIndex1) { case 0: SensorDriver(&Sensor1); break; case 1: SensorLed(); break; case 2: DacDriver(&Dac) break; //    } if (++TaskIndex1 >= TASK0_FREQ / TASK1_FREQ) TaskIndex1 = 0; } 


Added a two-line indicator? Not a problem either. We launch its driver at a frequency of 1 kHz, since characters need to be transmitted quickly, and in other slower functions we indicate to the driver exactly which characters and lines will need to be displayed.

Load rating



In order to estimate the load, you must enable the second hardware timer, which operates at the same frequency as the first timer. On good to make so that the period of timers was not end-to-end.
Before launching the task manager, the timer counter was reset, and after work, its value was calculated. Evaluation of loading is carried out on the period of the timer. For example, the period of the first timer is 100. That is, the counter will count to 100 and an interrupt will occur. If the counter of the second timer (CpuTime) counted less than 100, then - well. If it is too close or more, it’s bad: the reaction time of the tasks will float.
 unsigned int CpuTime = 0; unsigned int CpuTimeMax = 0; interrupt void Timer1_Handler(void) { Timer2.TimerValue = 0; //   AlternativeTaskManager(); //  switch-case   CpuTime = Timer2.Value; //    =  if (CpuTime > CpuTimeMax ) //    CpuTime = CpuTimeMax; } 


What's the result



What kind of benefits did I personally get compared to RTOS:
- Resource consumption when working dispatcher scanty.
- Organization of tasks, though not simple, but it comes down to the definition: where to start which function. There are no semaphores, mutexes, etc. No need to read multipage manuals for RTOS. Not to say that an advantage, but I'm so used to it.
- The code can be easily transferred from one controller to another. The main thing is not to forget about the types that are used.

Disadvantage:
- Complication of software. If, in the case of RTOS, you can write a function and immediately start it, if there are enough resources, then in the case of a switch-case, you will have to approach the optimization more closely. You have to think about how this or that effect will affect the performance of the entire system. An extra set of actions can lead to a violation of the "movement of gears." The larger the system, the more complex the software. If the function can be performed for an operating system in one run, then it may be necessary to break down the steps in more detail (state machine). For example, the indicator driver does not immediately send all the characters, but in lines:
1) set the gate, forwarded the top line, went out;
2) forwarded the bottom line, removed the strobe so that the characters were displayed, went out.
If there is little progress, this approach will affect the speed of development.
I use this approach is not the first year. There are many developments, libraries. But for beginners will be difficult.

In this article, I tried to reveal an alternative approach to designing software for embedded systems without using RTOS. I hope someone learned something useful.

Upd. I do not reject the idea of ​​using RTOS. I am not saying that RTOS is bad and should be immediately abandoned by everyone. I personally described my attitude on this matter and do not impose it on anyone. This article originated from the topic about software design for Embedded, where the author indicated that there is no alternative. I, on the contrary, showed that an alternative does exist, and exists in commercial projects.

Source: https://habr.com/ru/post/148992/


All Articles