Real-time operating system (RTOS) is very powerful extension to the Mbed operating system as it allows multiple tasks to run on the processor. There are many applications such as the Internet of Things (IOT) where it may be required to run multiple tasks, usually independent of each other on the same processor. In this chapter we shall be looking at the basic principles of RTOS in general and then develop several projects using the Mbed RTOS functions. In a RTOS the processor responds to external events very fast and in an orderly manner, switching between different tasks as governed by the scheduling algorithm used.
Tasks can be defined to be small self-contained codes that usually run independent of each other in a program. For example in a multidigit 7-segment display application it is required to refresh the display frequently. This process can be implemented as a task independent of the other codes running in the program. Another example may be, suppose that it is required to flash an LED every second in a program. At the same time it may be required to check the status of a push-button switch and take appropriate actions when the button is pressed. In such applications we can either use interrupts to process different tasks, or use a multitasking approach if the programming language that we are using supports it. In multitasking approach the LED flashing code can run as an independent task and we can have another task to check the status of the button. Although the processor can execute only one task at any time, the scheduling algorithms used in multitasking operating systems switch between different tasks quickly so that it seems that different tasks execute at the same time.
Scheduling is the fundamental concept in multitasking systems. Basically, there are two scheduling algorithms: nonpreemptive and preemptive. It is important to know the difference between the two algorithms and this is described briefly in the following subsections.
Nonpreemptive scheduling is the simplest form of task scheduling in a multitasking system. Here, once a task is given the CPU, the CPU cannot be taken away from that task. It is up to the task to give away the CPU and this usually happens when the task completes its operations, or when the task is waiting for some external resources and thus cannot continue. Nonpreemptive scheduling is also called Cooperative Scheduling. Some characteristics of cooperative scheduling are:
Because of its nature, cooperative scheduling-based multitasking is not used in real-time systems where immediate attention of the CPU may be required. Fig. 15.1 shows an example cooperative scheduling of three tasks. Task1 takes the longest time and when it releases the CPU then Task2 starts. Task3 starts after Task2 completes its processing. CPU is given back to Task1 after Task3 releases it. Depending on the algorithm used, the context of a task may or may not be saved. Saving the context of a task enables the task to return and continue from the point where it released the CPU.
Perhaps the simplest way to implement cooperative scheduling in a program is to use a state diagram approach where the tasks can be selected using a switch statement as in the following skeleton code. In this example there are four tasks which are selected sequentially one after the other. Note that in this example the task context is not saved and tasks start from the beginning of their codes:
In a preemptive scheduling once the CPU is given to a task it can be taken away, for example when a higher priority task wants the CPU. Preemptive scheduling is used in real-time systems where the tasks are usually configured with different priorities and time critical tasks are given higher priorities. A higher priority task can stop a lower priority one and grab and use the CPU until it releases it. In preemptive scheduling the task contexts are saved so that the tasks can resume their operations from the point they left when they are given back the CPU.
Preemptive scheduling is normally implemented in two different ways: using Round Robin (RR) scheduling, or using interrupt-based (IB) scheduling.
In RR scheduling all the tasks are given equal amount of CPU times and tasks do not have any priorities. When the CPU is to be given to another task, the context of the current task is saved and the next task is started. The task context is restored when the same task gets control of the CPU. RR scheduling has the advantage that all the tasks get equal amount of the CPU time. It is however not suitable in real-time systems since a time critical task cannot get hold of the CPU when it needs to. Also, a long task can be stopped before it completes its operations. Fig. 15.2 shows an example RR type scheduling with three tasks.
In IB scheduling tasks may be given different priorities and the task with the highest priority gets hold of the CPU. Tasks with same priorities are executed with RR type scheduling where they are all given equal amount of CPU time. IB scheduling is best suited to real-time systems where time critical tasks are given higher priorities. The disadvantages of IB scheduling are that it is complex to implement, and also there is too much overhead in terms of context saving and restoring. Fig. 15.3 shows an example IB type scheduling with three tasks where Task1 has the lowest priority and Task2 has the highest priority.
One of the nice features of Mbed is that it supports preemptive interrupt-based scheduling. Several tasks can be given different priorities, they can be scheduled to start, stop, communicate with other, and share resources. We shall be developing several multitasking projects in the next sections using various Mbed RTOS functions. Note that the RTOS features described in this book refer to Mbed OS 2. Other OS versions may have different functions and features.
In this project two LEDs (LEDA and LEDB) are connected to the Nucleo-F411RE development board. The project flashes LEDA every second, and LEDB every 0.5 s.
This is a multitasking project having two tasks. The aim of this project is to show how two tasks can be created using Mbed function Thread.
LEDA and LEDB are connected to GPIO ports PC_0 and PC_1, respectively, through 390 ohm current limiting resistors.
Fig. 15.4 shows the program PDL.
Function Thread allows defining and creating tasks in a program. Main in a program is a special case of a thread function that is started at system startup time and it has normal priority, known as osPriorityNormal.
A thread is basically a function in a program which normally runs continuously after it is started. Function thread.start(name) starts thread called name. Function Thread::wait(n) should be used to create n milliseconds of delay in a thread.
The program is called MultiLED. Before using the RTOS functions we have to load the RTOS library to our program. The steps are as follows:
Fig. 15.5 shows the program listing. At the beginning of the program, header files mbed.h and rtos.h are included in the program, LEDA and LEDB are configured as digital outputs are assigned to PC_0 and PC_1, respectively. Two functions named LEDAControl and LEDBControl are created to flash the two LEDs. LEDAControl flashes LEDA every second, while LEDBControl flashes LEDB every 0.5 s. Inside the main program threads LEDAControl and LEDBControl are started. The main program loops forever, not doing anything useful. In actual fact, main is another thread and one of the LEDs could have been flashed inside the main. Because main is another thread, in this program there are actually three threads, two of them flashing the two LEDs and the third one just repeating itself in a loop and wasting CPU resources. We can place the main thread in a wait state so that it does not consume any CPU resources. This can be done by declaring a semaphore at the beginning of the program, such as:
And then place the main thread in a forever wait state with the following statement:
We can also set the priority of the main to idle (see the following section) so that it does not consume any CPU resources. Another option is to set the thread to wait forever by the following statement:
The default thread priority is osPriorityNormal. A thread can be set to one of the following priorities (Mbed OS2 only):
|lowest priority (− 3)
|low priority (− 2)
|below normal priority (− 1)
|normal (default) priority (0)
|above normal priority (+ 1)
|high priority (+ 2)
|real-time (highest) priority (+ 3)
|Wait for a thread to terminate
|Terminate a thread
|Set thread priority
|Get thread priority
|p = thread1.get_priority()
|Wait specified milliseconds
|Get thread id of current thread
|t = Thread::gettid()
A thread can be in any one of the following four states at any time:
A WAITING thread can become READY or RUNNING when the event it is waiting for becomes available. A RUNNING thread becomes WAITING if it waits for an event. A RUNNING thread becomes READY if a higher priority thread becomes RAEDY. A thread becomes INACTIVE when it terminates.
In some applications we may want to terminate an active thread. We can for example modify the program given in Fig. 15.5 so that thread LEDAControl is terminated after 10 s. The main code for this new program is as follows the remainder of the program is same as in Fig. 15.5:
There are applications where we may want to pass parameters to a thread when the thread is started. This is done using the callback function. An example program (program: ThreadCallback) is shown in Fig. 15.6 where the flashing rate of the two LEDs are passed as arguments to the two functions where LEDA flashing rate is 250 ms and LEDB flashing rate is 500 ms. Note here that the addresses of the arguments must me passed to the functions and the functions use pointers to read the data.
As we have seen in the project in Chapter 8.2, the digits of a multidigit 7-segment LED need to be refreshed at about every 5–10 ms so that the human eye sees both digits to be ON at all times. The CPU has to spend all of its time to the refreshing process and as a result it cannot do any other tasks. In this project we use a multitasking approach where the refreshing process will be implemented in a separate task. In this project the display will count from 00 to 99 continuously with 1 s delay between each count.
The aim of this project is to show how a multidigit 7-segment display can be refreshed in a multitasking environment.
The block diagram of the project is as in Fig. 8.1.
The circuit diagram of the project is as in Fig. 8.3 where the display segments are connected to PORT C pins of the development board, and an NPN transistor is used to control the display digits.
The program listing (program: RTOS7Segment) is shown in Fig. 15.7. The program is basically very similar to the one given in Fig. 8.5, except that here the display refreshing is done as a separate task. At the beginning of the program header files mbed.h and rtos.h are included in the program and the RTOS library is loaded to the program. The interface between the 7-segment display and the development board are then defined as in the program in Fig. 8.5 and global variable CNT is defined. Inside the main program variable CNT is cleared to 0 and thread Refresh is started. This thread refreshes the display by enabling every digit for 10 ms. The main program increments CNT every second and when it reaches 100 it resets back to 0.
Mbed provides a number of functions for synchronizing threads when multiple threads need to access common global variables. Mutexes and semaphores are used for this purpose. Signals are used to synchronize threads to the occurrence of certain events. For example, an event can be forced to wait for an event to occur and as soon as the event occurs the event can be signaled to continue. In this section we shall be looking at the mutexes, semaphores, and signals in some detail.
Mutexes (or mutual exclusion objects) are programming objects that allow multiple program threads to share the same resource, such as data or file. A mutex is given a unique name when it is created. The resource is shared in locked by a thread. Once locked, other threads cannot access this resource until it is released by the thread that locked it. Therefore, a mutex is like a lock of a shared resource. A deadlock situation arises if a resource required by other threads are locked and never released by a mutex. A mutex is owned by the thread that uses it to lock the resource and any other thread cannot unlock this mutex. It is important to realize that a mutex locks part of a thread and also any data inside this thread. Mbed functions lock() and unlock() are used to lock and unlock a mutex, respectively.
Fig. 15.8 shows an example program (program: Mutex). In this example two threads are created called task1 and task2 which both print messages before and after they run. Without locking the following messages are displayed on the screen:
In Fig. 15.9 (program: MutexEx) the same program is given where a mutex is used to lock the shared printf resource. This modified program displays the following messages on the screen which is what is expected normally:
A semaphore is simply a nonnegative integer which increments and decrements and controls access to a shared resource. A semaphore is initially set to a count equivalent to the number of free resources. A semaphore’s value must be positive to allow access to the shared resource. The semaphore count is decremented by one when a thread uses the semaphore. Similarly, the count is incremented by one when the thread releases the semaphore. A zero count does not allow access to the shared resource. Semaphores with only one count are similar to mutexes. Such semaphores are also known as Binary Semaphores. An analogy of semaphores is the usage of a printer (shared resource), for example, three users sending print requests all at the same time, if all the jobs are to start in parallel at the same time then one user’s output will overlap with another and the outputs will mix up. We can protect this process using semaphored so that the printer resource is blocked when one process is running and then unblocked when it is finished. This process is then repeated for each user so that the jobs do not overlap. Mbed wait() and release() functions are used to lock and unlock a semaphore.
An example program (program: sema) using a semaphore is given in Fig. 15.10. In this program four threads are created where they display the messages First, Second, Third, and Fourth on the screen. By using a semaphore with a count of 2 we restrict access at any time to the printf function so that only two messages are displayed. The other two messages are displayed after a delay of 2 s, that is, the display is as follows:
Signals are also called event flags and they are used for thread synchronization. A thread can be forced to wait for a signal to be set by another thread before it can continue. Mbed function signal_wait() forces the issuing thread to wait until the specified signal is set. Function signal_set() sets a signal. We can have up to 32 signal flags (or event flags) per thread. Fig. 15.11 shows an example where one task generates some data and another one reads this data. The reading task waits for an event flag to indicate that data are available before it continues to read it.
An example program (program: signal) using signals is shown in Fig. 15.12. This program uses the on-board LED and the on-board button. A thread called Flash is created which waits for event flag 1 to be set by calling function signal_wait(0x1). Event flag 1 is set when the button is pressed by calling function signal_set(0x1). After this point thread Flash becomes active and flashes the LED every second. Remember that you have to load the RTOS library to your program before it can be compiled. As we have seen in earlier projects, the on-board LED and button are named as LED1 and BUTTON1, respectively.
This is a simple car park controller project. It is assumed that the car park has a capacity of 100 cars. Entry and exit gate barriers are available at the car park entry and exit points, respectively. The gate output signals are normally at logic HIGH and they go to logic LOW when a gate opens to let a car pass through. The controller counts the number of cars inside the car park and displays messages on the screen to let the drivers know how many spaces are available inside the car park (if any). The following information is displayed on the screen:
Initially the card park is assumed to be closed (e.g., at night) and message CLOSED is displayed. The car park becomes operational when a button called StartButton is pressed. When the car park is full or when it is closed the entry barrier is locked and does not open to let any cars into the car park.
The aim of this project is to show how a car park controller can be designed using a multitasking approach.
The block diagram of the project is shown Fig. 15.13. It is assumed that barriers with switches are used at the entry and exit points of the car park. Normally the outputs of these switches are held at logic HIGH and they become LOW when the barriers are lifted.
The circuit diagram of the project is shown in Fig. 15.14. Entry and exit switches are named as EntrySwitch and ExitSwitch, respectively and they are connected to GPIO pins PC_0 and PC_1 as shown in the figure. In this project two push-button switches are used to simulate the car park barriers. A PC is used to display the car park information.
The PDL of the program is shown in Fig. 15.15.
The program listing (program: CarPark) is shown in Fig. 15.16. At the beginning of the program, header files mbed.h and rtos.h are included in the program, and the serial PC interface is defined. EntrySwitch, ExitSwitch, and StartButton are assigned to PC_0, PC_1, and PC_3, respectively. LockEntryBarrier output when set locks the entry barrier so that it does not open and it is assigned to PC_2. The car park capacity is set to 100 cars. Inside the main program message CLOSED is displayed since the car park is closed at the beginning of the program. Threads CarPark and Display are started. These threads wait for event flags 1 and 2 to be set, respectively, before they can continue. These flags are set when the StartButton is pressed (i.e., StartButton is at logic LOW). Setting the flags starts the two threads. Thread Display displays the spaces available in the car park at screen coordinate (0, 9). Thread CarPark increases or decreases the space count depending on whether the cars are entering or leaving the car park. When a car enters the car park variable Spaces is decremented by one. Similarly, when a car leaves the car park variable Spaces is incremented by one. The program makes sure that the space count is not above the car park capacity or below zero (this should never happen in practice).
Note that this project could have been implemented without using multitasking, but here multitasking is used for demonstration purposes.
A typical display from the program is shown in Fig. 15.17.
Queues allow the user to queue pointers to data from producer threads to consumer threads. The Mbed queue functions are queue.put() to put into the queue and queue.get() to get from a queue. Fig. 15.18 shows a basic queue operation. A queue is created using the keyword Queue.
MemoryPool is used to manage fixed-size memory pools. The Mbed memory pool function alloc() is used to allocate a fixed amount of memory to the thread. Function free() returns the allocated memory block.
An example combined queue and memory pool program (program: queue) is shown in Fig. 15.19. In this program a structure called msg is created with two integer variables no1 and no2. Main program starts two threads called producer and consumer. A memory pool and a queue are defined at the beginning of the program. Thread producer allocates a memory pool and sets variables no1 and no2 to 10 and 20, respectively, and puts them into the queue. Thread consumer reads from the queue and displays the following text on the PC screen:
Here, structure osEvent points to the actual data
Mail is a very useful feature similar to a queue that is used to send and receive messages between threads, with the added advantage that it can allocate memory pool. Mbed Mail function alloc() allocates memory, put() puts a mail in the queue, get() gets a mail from the queue, and free() returns the allocated block.
An example program (program: mail) using Mail is shown in Fig. 15.20. In this program a structure is created as in the previous example with two integer numbers. A mail is created with the name mailbox. Thread producer allocates a memory pool, stores 5 and 20 in variables no1 and no2, respectively. The data are then put into the mail using statement mailbox.put(mail). The producer thread then waits for 2 s before freeing the memory pool. Thread consumer gets the mail and then displays the contents of no1 and no2 on the PC screen. Note that osEvent is a pointer to the data.
The RTOS timer can be used to create and control one-shot as well as periodic timer functions. A timer can be started, restarted, or stopped. Function stop() stops a timer, function start(ms) starts (or restarts) a timer where the argument is the timer period in milliseconds.
An example use of an RTOS timer program (program: rtostmr) is shown in Fig. 15.21. In this example an RTOS Timer is created with the name timer. This timer is then started with a period of 500 ms. After 5 s the timer is stopped.
Multitasking is an important concept in the development of complex real-time systems. In this chapter we have learned the following: