📜 ⬆️ ⬇️

Time Triggered design - another approach to software design for embedded systems

Once upon a time I wrote an article about the principles of designing an application for embedded systems. Then I said that there are two basic principles - an infinite loop and a real-time OS. But recently I heard that there is a third approach - the so-called Time Triggered Design.

Michael J. Pont's “Patterns for time-triggered embedded systems” book was used as an introduction to the approach, and for those interested, www.safetty.net/publications/pttes
I will try to outline here the concept.

The concept is based on the following ideas:

This set of principles is also called the cooperative task scheduler (cooperative scheduler). Classic RTOS uses what is called a preemptive scheduler.

The author cites the advantages of ease of implementation, very low overheads and, strange as it may sound, reliability.
As drawbacks - the need for more careful design. For example, one of the requirements is that the task execution time be as short as possible, ideally, if it is significantly less than the interruption period.
')
Pseudocode demonstrating this principle.

void main(void) { scheduler_init(); add_task(Function_A, 2); add_task(Function_B, 10); add_task(Function_C, 15); scheduler_start(); while(1) { dispatch_tasks(); } } 


So far everything has to be clear - we initialize the scheduler, add three tasks that will be executed at specified intervals in ticks, start the scheduler and go into the endless cycle of the task manager.

Structure describing the context of the task:

 typedef struct { void (* pTask)(void); uint32 Period; uint32 PeriodCur; uint8 RunMe; } task_descriptor_t; 


Indeed, compared to RTOS, the “overhead” is much smaller — a pointer to a function, the frequency of the launch, the current value — how many more ticks to wait for the launch, and how many times the task should be started.

 task_descriptor_t all_task_list[MAX_TASKS]; 


The task list is a regular array of predetermined length.

The scheduler itself hangs up to interrupt the timer, configured to occur at a specified frequency, for example, 1 ms - the same tick.

 void scheduler_update(void) interrupt { foreach (task in all_task_list) { task.PeriodCur--; if (task.PeriodCur == 0) { task.PeriodCur = task.Period; task.RunMe++; } } } 


In the handler, we go through the entire task list, decrement the current value of the remaining ticks before the launch, and if it reaches 0, overwrite it and increment the run counter.

And finally - the dispatcher. Which turns in an endless loop.

 void dispatch_tasks(void) { foreach (task in all_task_list) { if (task.RunMe > 0) { task.pTask(); task.RunMe--; } } } 


We are still going through the task list, and if the task has a launch counter greater than zero, we start this task by directly calling its function, and decrement the start counter.

Actually everything!

Indeed, the implementation is ridiculously simple (and therefore easily portable anywhere). Indeed, better than an endless loop. Indeed, no means of synchronization is required, such as semaphore-queue-critical sections. Indeed, no context switch is required.

But I do not like. And that's why.

  1. The requirement of one and only one interruption in the system means that all work with the periphery should occur in polling mode. What imposes its limitations. And reduces the response time of the system.
  2. Go through the task list. Those. before management reaches the last task in the list, at worst all previous tasks in the list will be called. The reaction time to an external event is again far from the most predictable.
  3. If something happens to one of the tasks, it may not reach the last one on the list. We have a cooperative mode, and each task must return control to the dispatcher itself!
  4. Restriction on the execution time of a single task. From which it follows that in the case of any more or less prolonged action, instead of a simple linear code, we will have to fence state machines.


For the last point there was a wonderful example in the same book. In addition to focusing on shedulers, two thirds of the book is devoted to a story about the basics of embedded systems, work with peripherals and protocols. Here is an example of working with SPI from this book.

 /*------------------------------------------------------------------*- SPI_X25_Write_Byte() Store a byte of data on the EEPROM. -*------------------------------------------------------------------*/ void SPI_X25_Write_Byte(const tWord ADDRESS, const tByte DATA) { // 0. We check the status register SPI_X25_Read_Status_Register(); // 1. Pin /CS is pulled low to select the device SPI_CS = 0; // 2. The 'Write Enable' instruction is sent (0x06) SPI_Exchange_Bytes(0x06); // 3. The /CS must now be pulled high SPI_CS = 1; // 4. Wait (briefly) SPI_Delay_T0(); // 5. Pin /CS is pulled low to select the device SPI_CS = 0; // 6. The 'Write' instruction is sent (0x02) SPI_Exchange_Bytes(0x02); // 7. The address we wish to read from is sent. // NOTE: we send a 16-bit address: // - depending on the size of the device, some bits may be ignored. SPI_Exchange_Bytes((ADDRESS >> 8) & 0x00FF); // Send MSB SPI_Exchange_Bytes(ADDRESS & 0x00FF); // Send LSB // 8. The data to be written is shifted out on MOSI SPI_Exchange_Bytes(DATA); // 9. Pull the /CS pin high to complete the operation SPI_CS = 1; } 

Simple and clear linear code, it seems. But when you try to use it in the design described above, the following problems will appear:


Well, it’s somehow strange that the authors for some reason did not give an example of the code of the above example of working with SPI, adapted to their own design. Scared, I guess.

Considered more and so-called. hybrid sheduler when another interrupt is enabled, in the context of which another high-priority task can be performed. But still it does not change the general essence.

The second part of the book discusses how to adapt the design for a system of several microcontrollers. The main idea is that the master microcontroller uses its timer as a source of interruptions for a tick, and all others use an external interrupt as a tick interrupt, which the master sends from its handler the interrupt through one of the GPIO pins connected to the external interrupt inputs of the other microcontrollers. Thus, synchronization of all microcontrollers is achieved. The idea is interesting, but the problems described above do not solve much, unfortunately.

In general, the approach probably has the right to life. Somewhere, where a small microcontroller, not doing anything 99% of the time, handles a couple of external events that do not require an immediate response within a specified time. On the other hand, here and supercycle normally work, while you can use multiple interrupts.

But for situations where there are more events, where it is much easier to use interrupts to work with peripherals, and where reaction time and stability are more or less critical, and where you need to use the controller's performance to the maximum - I will still remain a supporter of using RTOS. Let the system be a certain unpredictability and the need for proper use of synchronization tools - the benefits of a more strict separation of tasks from each other and from the features of the sheduler are all the same, it seems to me, more.

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


All Articles