Embedded real-time operating system¶
Learning goals: Basic principles of embedded programming.
We will start familiarizing ourselves with embedded systems programming by first reviewing the structure of a program (in Raspberry Pi Pico). After that we will add functionality by learning the usage of different peripheral device libraries.
Real-time operating system¶
In this course, along with the Raspberry Pi Pico we’ll be using the FreeRTOS real-time operating system (RTOS) to aid us in our programming tasks. Both will be used for practice and eventually for our project work. Throughout the material, you’ll find programming examples for the device, but the best way to learn is by experimenting and hands-on practice. Let’s dive in!
An operating system is software that runs applications on a computer. It has two main tasks: providing an execution environment for applications and managing the computer’s hardware resources, peripherals, and access rights. The operating system offers services to applications through interfaces and libraries, allowing them to utilize these resources—for example, memory management, file systems, peripheral device access, and data transfer from the system to external sources. Additionally, the operating system controls application execution by allocating processor time and memory among different applications. More detailed discussions on these topics are covered in later Operating Systems-course, so we’ll skip an exhaustive presentation here.
On the other hand, a real-time operating system provides an execution environment for applications in embedded systems. Real-time responsiveness in this context means that the operating system guarantees specific response times for functionalities. For instance, it ensures that responses to input or events occur within one millisecond and that the maximum response time is ten milliseconds. An RTOS can be lightweight, merely sharing processor time among applications. This lightweight design supports applications with strict real-time requirements because the operating system's own routines consume minimal time. The implementation of RTOS services significantly impacts the design, implementation, and maintenance of embedded programs.
As mentioned earlier, it is possible to program embedded systems without an operating system or device firmware. In such cases, the programmer is responsible for implementing the device’s corresponding functionalities, connecting peripheral components to the microcontroller programmatically, and implementing the necessary interfaces/services for the application.
Implementation of an embedded program¶
As mentioned in the previous C language material, modular programming makes a lot of sense and also sense when implementing computer programs. In the embedded world, it’s also sensible to divide the overall program into different tasks in a modular way, where each task’s functionality can be logically encapsulated or in other words, separated. Now, in the program, each task is defined with its own inputs to which it responds and its own outputs.
For example, in the Pico device used in the course, tasks could involve reading sensor data, updating the display, or performing wireless communication. Well, these are not just examples; they are precisely the tasks we’ll implement in the project work.
Now, it makes sense to let the RTOS handle the parallel execution of tasks to ensure efficient responsiveness. The programmer’s responsibility is to implement the actual task and define its execution parameters, considering efficiency requirements. These execution parameters include timing, priority, memory allocation, initial settings, and more. In this context, learning embedded programming involves understanding how to use these ready-made RTOS features through libraries, interfaces, and software structures.
An alternative would be to program the RTOS or device firmware manually, following an Arduino-style superloop architecture (a large loop within the main function that contains all program functionality). As mentioned, the superloop is an acceptable solution for small embedded systems with modest resource and performance requirements. However, it sacrifices many benefits of modularity, and the programmer might inadvertently hinder program execution. Administratively, such an approach can become a so called nightmare because even minor code changes may disrupt finely tuned timings. It’s unlikely that anyone would manually implement task prioritization within a superloop; the solution would inevitably be cumbersome. In the professional world, embedded programming is its own art form compared to general workstation programming, especially when dealing with demanding tasks, peripheral drivers, or even custom operating systems.
In addition to implementing tasks and defining execution parameters, synchronizing task behavior requires solutions. For instance, a single input (such as a button press on the device) might trigger one or multiple tasks or cause a sequence of consecutive actions across different tasks. How can such a design be achieved?
Actually, now we’re getting into the territory of the Operating Systems course, so let’s leave those matters there. However, there’s a familiar and straightforward solution from digital technology that we’ll be using in the course: state machines. The course doesn’t aim to delve deeper into the theory of state machines (which is quite extensive), but we’ll use them to implement synchronization between tasks in our program.
In short, the idea behind a state machine is that the logical execution of a program progresses from one state to another based on inputs, tasks, and responses. To achieve this, the program defines (at least one) state variable whose value changes according to events and corresponding tasks. Software components (read: functions) that react to tasks and events receive information about state changes and perform actions accordingly. Afterward, the state transitions to the next state, and the software components react to it again. For example, state transitions in an embedded program could look like this:
waiting state -> button press -> read sensor data -> display result/message -> waiting state
In this example, the program could have three or four different tasks: an interrupt handler that responds to button presses, a task for communicating with the sensor, a task for updating the display, and possibly a concurrent task for wireless data transfer. Perhaps even a task for preprocessing data, depending on factors like the amount of data. Now, you could certainly implement this logical functionality using a superloop, but then you’d need to consider the state of other program functionalities in each subtask—for instance, whether you’re currently communicating with peripheral devices. In the example above, we couldn’t receive messages simultaneously while reading sensor data.
Here’s an example: A state machine-based implementation of an embedded program for Arduino. Even just looking at the overview on that page is worthwhile, especially because we’ll be using a similar approach in the course. Well, more on this shortly.
Introduction of FreeRTOS and Pico SDK¶
FreeRTOS, the real-time operating system used in the course, does not in itself provide anything else besides the basic tools needed to implement programs requiring real-time operations. Luckily, we have Pico C/C++ SDK (later only Pico SDK) maintained by Raspberry Pi. This SDK allows us to use for example the pins and peripheral devices connected to the Pico. This functionality is hidden below API (application programming interface) in various different libraries. By using these libraries, we can enable these functionalities in our program.
We will mention some of these libraries now, but the usage of them in programs will be discussed in detail in later sections of the lecture material.
(For those who are interested: Pico SDK and FreeRTOS are open-source software. This means that their source code is publicly available, so anyone can view it here and here).
Services and libraries¶
We will use the following libraries to implement programs. Don't worry, the terms used below will be discussed in lecture material later.
- hardware_gpio A library to control the I/O pins of the microcontroller. The buttons and LEDs of the Pico are controlled using this library.
- Display A library implemented by the course staff to produce text and graphics to an LCD-screen.
- hardware_i2c A library to use the i2c data transfer bus. Many of the devices connected to Pico can be accessed by using this library.
- hardware_uart Serial communications protocol to communicate with peripheral devices. For example, data could be sent to PC, or commands could be received by using this library.
- (hardware_pwm A library for producing pulse width modulation (PWM) with the I/O pins of the Pico. By using PWM, we can feed analog current with a specific frequency from a digital pin to for example control the brightness of an LED)
(For those who are interested: More information about the libraries can be found from the documentation of Pico SDK)
Functionality of a program¶
Let’s take a closer look at the main function in a Pico C program. When working with FreeRTOS and the Pico SDK, the C language might initially appear cryptic due to the extensive use of predefined elements. However, understanding the code and considering what actually happens can be helpful.
In the context of Pico programming, there is indeed a main function from which the program execution begins. Although FreeRTOS performs some background tasks before executing the main function, we won’t delve into those details in this course.
Below is an example of the {{main}}-function of a FreeRTOS-program:
//Including the libraries of FreeRTOS and Pico
#include <FreeRTOS.h>
#include <task.h>
#include <stdio.h>
#include <pico/stdlib.h>
#include <inttypes.h>
#include <pins.h>
void buttonFxn(uint gpio, uint32_t eventMask);
void taskFxn(void * pvParams);
#define STACKSIZE 1024
int main(void) {
TaskHandle_t myTaskHandle = NULL;
// (en) I/O-definition that is used to enable a push button
gpio_init(BUTTON0);
gpio_set_dir(BUTTON0, 0);
// (en) We set and interrupt handler for the button, in this case the function buttonFxn
gpio_set_irq_enabled_with_callback(BUTTON0, GPIO_IRQ_EDGE_FALL, true, buttonFxn);
// (en) We initialize one task to the application, implementation is in taskFn-function
BaseType_t result = xTaskCreate(
taskFxn,
"BUTTON_TASK",
STACKSIZE,
NULL,
1,
&myTaskHandle
);
if(result != pdPASS) {
printf("Task create failed!\n");
return 0;
}
// (en) Tell that the initialization of the device succeeded
printf("Initialization OK!\n");
return 0;
}
In all C programs, libraries are brought into the program using preprocessor directives at the beginning of the program. With FreeRTOS, there are quite a few of these libraries, and we haven’t covered them all here (not even close).
Next, we include the selected peripherals in the program by initializing them and defining various I/O settings. In the example, we enable one of the push buttons and define the event handler (interrupt) for button presses, which is the function
buttonFxn
. Each FreeRTOS task is essentially a C function, and the rules for writing and using them are (with some reservations) the same as for regular C functions.After that, there’s room in the main function to perform other initializations to control the program’s behavior. For example, if we use timers to control task execution, we can initialize them here.
Finally, it’s a good idea to send greetings to the development environment’s console window using the debugger. This way, we can verify that the device initialization was successful. The
System_printf
function can be used to write to the console window, and it works (with minor exceptions) similarly to the standard C library’s printf function. Remember that embedded implementations of standard libraries can vary in terms of implemented features.Note that the code includes many post-initialization checks to ensure that the initializations were successful. These checks are necessary, because when programming embedded systems, our view of the device’s internal operation is limited..
Conclusion¶
We’ve now covered the basics of executing programs in FreeRTOS. We’ll delve deeper into using FreeRTOS libraries in the course’s lab exercises.
The example code above may seem cryptic after the clear C language we’ve seen before. However, upon closer examination, you’ll notice that FreeRTOS code relies heavily on existing libraries, their function calls (with their own naming conventions), predefined data structures, pointers, and constants. In essence, it’s still the same C language..