Embedded Control Systems Design/Design Patterns
Programming for real-time environments is hard and needs a good understanding of the complete system, hardware as well as software. In the ideal case, every part of the system is well described and easily understandable, but the real world seldom allows this ideal situation. Therefore, the system should be robust against the uncertainties in the system. design patterns are a useful support for all designers: they are generalized solutions to commonly occurring problems, based on experience of what has worked already in the past in a large number of systems. Patterns are also appropriate to create portable code that may be reused and adapted in several applications.
Design patterns always come with a certain context: they are the result of a number of design forces that each pull the design in several directions, and for which the pattern provides a balanced solution. However, the forces in different systems may be so different that also the resulting trade-off gives rise to a different pattern.
- 1 Software patterns without need for an operating system
- 2 Design patterns & Real-time programming for embedded devices with OS
- 2.1 Configuration & execution
- 2.2 Asynchronous & Synchronous design pattern
- 2.3 Event handling
- 2.4 Monitor
- 2.5 Finite state machine pattern
- 2.6 Taskcontext
- 2.7 Problems of real time programming
- 2.8 Immunity aware programming
- 2.9 Further reading
- 2.10 References
Software patterns without need for an operating system
When programming for micro-controller (like a PICmicro), programs will often be written in machine language or low level languages like assembler. Assembler programs tend to be smaller and more efficient with active memory. Nevertheless it's also possible to use higher level languages such as C when the controller has enough resources. The article "Using A High level Programming Language" has a short list of the advantages of using a higher level language over assembler. (Even higher level languages have those same advantages over C, but they are rarely available on small microcontrollers).
In this section we will only discuss design patterns used when programming in assembler. These design patterns seem really basic, but good understanding of them is important. The knowledge of assembler is not needed, but a good help to understand this section.
When you often need to do same operations, you can use subroutines. By putting a value in the register you can give a parameter to the subroutine. The result of the subroutine will be also stored in the register. Here we give a simple example of a subroutine:
Start movlw 02h ; Put a bite in the working register movwf PORTA ; Move the bite to a register, this will turn on a led call Delay ; Call subroutine movlw 00h ; Put a bite in the memory movwf PORTA ; Move the bite to a register, this will turn off a led goto Start Delay ; Subroutine Delay Loop1 decfsz COUNT,1 ; decrease value of COUNT, and skip next instruction if zero goto Loop1 ; goto label Loop1 return
Sometime you need a structure where you can store values in. Or you need to do a conversion but it's easier to get values from table. Then the use of a data table is handy.
On a PICmicro, constant data tables (in Flash memory) are a kind of normal subroutine. When the subroutine is called, the value in the working register will be used to determine the amount of instructions the processor has to skip. This is done by increasing the 'Process counter'? The process counter is the address of the instruction that is currently executed. Increasing the counter will make the processor skip the next instructions. A little example, in the data table we have the order in which 5 leds (5 bits) have to blink.
Start movlw 05h ; Put value 5 in the working register (this is initialization value) movwf COUNT1 ; Assign the value of the working register to COUNT1 Loop1 call Table ; Look the pattern in the table movwf PORTA ; Move the pattern of the working register to the led register call Delay ; Call subroutine decf COUNT1,F ; Decrease, put a byte in the COUNT1 register btfsz STATUS,Z ; Test if overhead goto Loop1 ; No, get next pattern goto Start ; Yes, run the program from the start again Delay ; Subroutine delay Loop2 decfsz COUNT2,F ; Decrease value of COUNT1, and skip next instruction if zero goto Loop2 ; Goto label Loop2 return Table addwf PCL ; Add value to get pattern retlw 01h ; Return pattern 00001b, LED1 - ON retlw 02h ; Return pattern 00010b, LED2 - ON retlw 03h ; Return pattern 00100b, LED3 - ON retlw 04h ; Return pattern 01000b, LED4 - ON retlw 05h ; Return pattern 10000b, LED5 - ON
- The article PIC "Microcontroller Memory Methods: Tables" has detailed information on various ways to access a table, the "crossing a 256 byte page boundary" gotcha (which always occurs with tables longer than 256 bytes, and sometimes occurs even with very short tables), and work-arounds.
- The article "File Select Register" discusses the file select register, which is used for reading and writing arrays of values in RAM.
FIXME: write something about basic interrupt programming
We talk more about interrupt servicing in a later chapter, interrupt servicing.
The watchdog timer is an internal timer in the processor. The purpose of this timer is to avoid that an embedded controller gets stuck.
The principle of a watchdog timer is simple, it resets the embedded controller after a predefined time. But this only happens if the program didn't reset the watchdog timer first.
Therefore the program should reset the watchdog timer on a predefined time. When the watchdog timer doesn't get reset on time, the program probably got stuck and the micro controller will reset itself. See also: RTOS watchdog timers, Watchdog timer.
Design patterns & Real-time programming for embedded devices with OS
Assembler programs are often hardware specific and not very portable and modular. This makes programming of big complex system rather difficult. This can be solved by using an 'abstraction layer' that handles the processor and the hardware interfacing. This layer is also known as 'the kernel' of a system. The kernel also takes care of the hardware specific calls and task management, which allows to stay focused on the most important task; Making a real-time program. Of course more time and more memory is needed, but it makes the program more flexible and modular. Also the use of higher programming language makes the code also portable and easier to debug.
In the next section we will describe some important design patterns:
Configuration & execution
Aim & Problem
The most important difficulty in real-time programming is knowing that a specified operation will happen on a defined moment. Therefore it's important to know exactly how long each operation will take.
This pattern consist of 2 parts. In the first part all operations that have no predictable execution time are done. This includes memory allocation, hardware initialisation and object creation. This part is called the Configuration of the system. When the configuration is completely done, the main loop can be started.
More about: Logical vs. Physical architecture
More about: C++
- All memory allocation should be done _before_ starting.
Asynchronous & Synchronous design pattern
The asynchronous/synchronous design pattern is also known as:
- interrupt service routine / deferred service routine (ISR/DSR)
- first-level interrupt handler / second-level interrupt handler (FLIH/SLIH)
- fast interrupt handlers / slow interrupt handlers
- top-half of interrupt / bottom-half of interrupt
When data arrives on a input, the processor will execute a predefined interrupt function. Sometimes this data needs to be processed before it can be used. The interrupt function should be kept as short as possible to allow new data to arrive, and to avoid to miss this new data (because of the interrupt lock).
In this design pattern the data handling happens in 2 phrases:
- the interrupt service routine (ISR): This is the function that handles the data when the interrupt comes in. Often it just reads the data on the input and writes it in a buffer. It runs with interrupts disabled, and ends with a "return from interrupt" that enables interrupts.
- the deferred service routine (DSR): This is a function that runs continuously in the background and that processes the new data. It runs with interrupts enabled, and ends with a normal "return".
- No data lost
- Sometimes it's possible that more data comes than needed. The DSR function will then only process the needed data.
Finite state machine pattern
This pattern is also known as the State pattern or the FSM pattern
A system often consist of different states or phases, where each state has to complete some actions. Therefore the embedded controller should react differently on inputs depending state of the system.
This can be done by programming a FSM, this is a class where the behaviour of the function differs depending of the state of the system.
We discuss finite state machines in more detail in a later chapter, ../Finite State Machines and Petri Nets
Problems of real time programming
In the computing world deadlock refers to a specific condition when two or more processes are each waiting for another to release a resource, or more than two processes are waiting for resources in a circular chain (see Necessary conditions). Deadlock is a common problem in multiprocessing where many processes share a specific type of mutually exclusive resource known as a software, or soft, lock. Computers intended for the time-sharing and/or real-time markets are often equipped with a hardware lock (or hard lock) which guarantees exclusive access to processes, forcing serialization. Deadlocks are particularly troubling because there is no general solution to avoid (soft) deadlocks.
This situation may be likened to two people who are drawing diagrams, with only one pencil and one ruler between them. If one person takes the pencil and the other takes the ruler, a deadlock occurs when the person with the pencil needs the ruler and the person with the ruler needs the pencil, before he can give up the ruler. Both requests can't be satisfied, so a deadlock occurs.
In scheduling, priority inversion is an antipattern. Priority inversion occurs when a low priority task holds a shared resource that is required by a high priority task. This causes the execution of the high priority task to be blocked until the low priority task has released the resource, effectively "inverting" the relative priorities of the two tasks. If some other medium priority task, that does not depend on the shared resource, attempts to run in the interim, it will take precedence over both the low priority task and the high priority task.
In some cases, priority inversion can occur without causing immediate harm -- the delayed execution of the high priority task goes unnoticed, and eventually the low priority task releases the shared resource. However, there are also many situations in which priority inversion can cause serious problems. If the high priority task is left starved of the resources, it might lead to a system malfunction or the triggering of pre-defined corrective measures, such as a watch dog timer resetting the entire system. The trouble experienced by the Mars Pathfinder is a classic example of problems caused by priority inversion in realtime systems.
Priority inversion can also reduce the perceived performance of the system. Low priority tasks usually have a low priority because it is not important for them to finish promptly (for example, they might be a batch job or another non-interactive activity). Similarly, a high priority task has a high priority because it is more likely to be subject to strict time constraints -- it may be providing data to an interactive user, or acting subject to realtime response guarantees. Because priority inversion results in the execution of the low priority task blocking the high priority task, it can lead to reduced system responsiveness, or even the violation of response time guarantees.