Alternative Software Design Approach for Embedded

I decided to write this topic after reviewing the article “Two approaches to software design for embedded” . While reading which I came across the phrase: "If the system is going to become large, combining many different actions and reactions that are also critical in time, 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, highly loaded real-time systems, where large processors are used, then the OS can not do without, but for more modest microcontroller solutions, there is an alternative. After all, tasks can be performed using a conventional switch-case and at the same time provide the necessary reaction time.


Why personally I do not use RTOS



Comrade olekl talked about RTOS, I will not focus on this. I’ll note a couple of points that I personally highlighted for myself - why I do not use RTOS:
  • OS for its work requires the resources of a microcontroller: memory and system time there are not many of them. Let them go to the puzzles, but you have to give it to the dispatcher. Perhaps this is the most basic point for me.
  • Not an easy way for me to organize tasks. Mutexes, semaphores, priorities, etc. - you can get lost.
  • Some RTOS cost money. And not small ones.
  • There are some doubts about the support of RTOS controllers. Suddenly I want to transfer the project to the latest controller, and for it there is no support for this OS yet.
  • Doubt: what if there is an error in the kernel? Surely the proposed RTOS is tested a million times, but who knows: suddenly something flies out a million for the first time.


Switch-case approach



In terminology is not strong, so let it be such a name.
It is more convenient to consider an example. It uses pseudo code.

The device has two temperature sensors. The polling time of the first sensor is not critical: “polled, yes, okay”, let it be 0.2 ms. After exceeding the set temperature threshold, we will light the LED. The readings of the second sensor are very important for us. It should be interrogated as often as possible, and after exceeding the specified threshold, issue “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.
Implementation will require a hardware timer interrupt. Indeed, only in this way can we guarantee the fulfillment of tasks in the time allotted to them. In this case, the possibilities for using other interrupts are sharply reduced.
Work with most peripherals can be done without interruptions, and very important communication interrupts (for example: UART / SCI) usually have a higher priority than a timer and usually serve to fix received / sent bytes, i.e. They won’t take much time.
The approach, when the timer only counts the time for tasks, and the tasks themselves are performed in the background (or while supercycle) without prohibition of interrupts, does not guarantee the necessary reaction of execution.

First, we’ll 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 probe 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, you need initialization. The structure can be initialized entirely with #define, or each field can be individually. 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 will be decided outside the driver.

We light 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 with a discrete foot:

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 to be surprisingly similar :)
We will write to the ROM as follows:
the ROM driver function will be cyclically executed at a frequency of 1 kHz, while waiting for the data to be written, the instruction “what should be done with them” and at what address in memory. Those. it is 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), then it will already know what to do.
Our functions are ready. Now they need to be properly organized.
To do this, we will start a timer interrupt with a frequency of 10 kHz (100 μs). This will be our maximum guaranteed frequency of calling tasks. Let that be enough. We create the functions of the task scheduler, in which we will determine when what task to run.

#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 timer interrupt and you're done.

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

This system looks like a kind of mechanism with gears: the most important gear is directly on the motor shaft and it twists the remaining gears.
Tasks are performed "on the ring." The frequency of their execution depends on the place of the call. The Task0_Execute function will be executed with a frequency of 10 kHz, since we call it directly in the timer interrupt (our main gear). Frequency division occurs in it and using the switch-case with TaskIndex0 it is determined for which task the time has come. The challenge frequency should be less than 10kHz.
We set the task frequency for the Task0_Execute cycle to 1 kHz, which means that 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)


System switch-case structure


Similarly for Task1_Execute and Task2_Execute. We 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 that in total there will be 20 and 100 tasks, respectively.
After completing the tasks of the dispatcher, the program returns to the background (supercycle background).
Any reactions that are not critical in time, it is quite possible to put them there.

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

A DAC is added to the device and, together with the ignition of the LED, do you need to generate a 4-20 signal? No problem. Create the DAC driver and run it. We add two lines to the SensorLed function, which 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, because characters need to be transferred quickly, and in other slower functions we tell the driver which characters and lines will need to be displayed.

Load rating



In order to evaluate the load, you must enable the second hardware timer, which works at the same frequency as the first timer. It’s good to make sure that the period of timers is not close.
Before starting the task manager, the timer counter was reset, and after work, its value was read. The load rating is based on the timer period. 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) has counted less than 100, then that’s good. If end-to-end or more, it’s bad: the reaction time of 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 as a result



What personally I have received advantages in comparison with RTOS:
- Consumption of resources when the dispatcher is miserable.
- The organization of tasks, although not simple, but it comes down to determining: where is which function to run. There are no semaphores, mutexes, etc. No need to read multi-page manuals for RTOS. Not to say 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 software. If in the case of RTOS you can write a function and run it right there, if you have enough resources, then in the case of switch-case you will have to take a more tight approach to optimization. You’ll have to think about how this or that action will affect the performance of the entire system. An extra set of actions can lead to disruption of the “movement of gears”. The larger the system, the more complex the software. If for an OS, a function can be performed in one go, then here you may have to break it down in steps in more detail (state machine). For example, the indicator driver does not immediately forward all the characters, but on the lines:
1) set the strobe, forwarded the top line, left;
2) sent the bottom line, removed the strobe so that the characters are displayed, exited.
If there are few developments, then this approach will affect the speed of development.
I have been using this approach for several years. There are many developments, libraries. But for beginners it will be difficult.

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

Upd. I do not abandon the idea of ​​using RTOS. I'm not saying that RTOS is bad and everyone needs to drop it right away. I personally described my attitude about this and I do not impose it on anyone. This article arose thanks to a topic about software design for Embedded, where the author indicated that there is no alternative. On the contrary, I showed that an alternative still exists, and exists in commercial projects.

Also popular now: