Learning Objectives: General principles of embedded programming. Implementation of an embedded program on the SensorTag device.
Embedded Real-Time Operating System¶
Learning Objectives: General principles of embedded programming.
We will start exploring embedded device programming by first going through the structure of a embedded programming program and then adding functionalities by learning how to use various peripheral libraries. In the course we will use the SensorTag device although we will provide information also about the Raspberri Pi Pico as extra material.
Real-Time Operating System¶
The manufacturer of the SensorTag (Texas Instruments) offers a real-time operating system (RTOS) to facilitate programming the device. We will use its libraries and services for practice and eventually for the completion of the course project. This material presents programming examples for the device, but the best way to learn is, of course, by experimenting and doing it yourself. But first, let's go through some related concepts.
An operating system is software that runs computer applications and has two main tasks. Firstly, it provides an execution environment for applications and secondly it handles the computer’s hardware resources, peripherals, and permissions. The operating system offers services through interfaces and libraries to applications so they can utilize those resources. Resources include memory management, file systems, peripheral device usage. The operating system also controls the execution of applications by distributing the processor(s) execution time and memory among them. These topics are covered in more detail in a later Operating Systems course, so we will skip a comprehensive explanation here.
A real-time operating system provides an execution environment for embedded system applications. In this context, real-time means that the operating system guarantees a certain response time for functionalities. For example, a response to an input/event is guaranteed to happen within one millisecond, and the response time may take a maximum of 10 milliseconds. The RTOS itself can be very lightweight, for example, it may simply distribute processor time between applications. A lightweight implementation also supports applications with strict real-time requirements, as very little time is spent on the OS's own routines. The implementation of RTOS services significantly impacts the design, implementation, and maintenance of embedded software.
As mentioned earlier, it is possible to program embedded systems without an operating system and/or firmware, but in that case, the programmer is responsible for handling the functionality of the device, such as programmatically connecting peripheral components to the microcontroller and implementing the interfaces/services required by the application.
Implementing an Embedded Program¶
As mentioned in the earlier C language material, it makes a lot of sense to implement computer programs in a modular way. In the embedded world, it is also reasonable to divide the entire program modularly into different tasks, where the functionality can be logically encapsulated, i.e., separated from each other. Now, each task in the program is assigned its own inputs (events to which it responds) and its own outputs (action or effects that it produces).
For example, in the course, tasks in the SensorTag device could include reading sensor data, updating the display, or communicate with other device. Well, these are not just examples; they are exactly what we will implement in the course project.
Now, it is, of course, reasonable to let the RTOS handle the concurrent execution of tasks to ensure that responses are as efficient as possible. The programmer's responsibility is then to implement the task and define its execution parameters, considering the efficiency requirements. These execution parameters include things like timing, priority, memory allocation, various initial settings, etc. In this regard, learning embedded programming is essentially about learning to use the built-in features of the RTOS through libraries, interfaces, and software structures.
An alternative would be to program the RTOS or firmware yourself, similar to Arduino's superloop structure (a large loop inside the main function that handles all program functionality). As mentioned, the superloop is an acceptable solution for implementing small embedded systems that do not have significant resource or performance requirements. However, in this approach, much of the benefits of modularity are lost, and the programmer may even complicate the program's execution. From a maintenance perspective, such a solution can be a nightmare, as even a small code modification can disrupt carefully tuned timings. Hardly anyone would attempt to implement, for example, task execution based on different priorities within a superloop—it would inevitably be clumsy. Well, in the industry, embedded programming is a separate art compared to general workstation programming, especially when dealing with tasks with strict requirements, peripheral drivers, or even custom operating systems.
In addition to implementing tasks and defining execution parameters, synchronization between tasks requires solutions. For instance, a single input, such as pressing a button on a device, might trigger a response from one or multiple tasks or lead to a series of sequential operations across different tasks. How can such implementation be achieved?
Actually, we are now getting into the territory of the Operating Systems course, so we will leave it at that. However, there is a relatively simple solution from digital technology, which we will use in the course: state machines. The course does not intend to dive deeper into the theory of state machines (a very large topic), but we will use them to implement synchronization between tasks in our program.
In short, a state machine operates by transitioning the program logically from one state to another based on inputs or events. Each state is responsible for performing a specific task and may produce an output. The program tracks the current state using at least one state variable, which updates as events or actions occur. Functions or tasks in the program monitor the state variable and execute their operations based on the current state. The state may change after a task is completed, or it may change in response to an external input, triggering the next appropriate state, where the cycle repeats.
For example, in an embedded system, this could look like the following sequence of state transitions:
IDLE state -> button press -> read sensor data -> display result/transmit message -> IDLE state
In this example, the program could have three or four different tasks: an interrupt handler reacting to a button press, a task for communicating with the sensor, a task for updating the display, and a simultaneous task for wireless communication. Perhaps also a task for data preprocessing, but this depends on the design and, for instance, how much data there is. Now, of course, this logical functionality could be implemented based on a superloop, but in that case, all subtasks would have to account for the state of other functionalities in the program, such as whether communication with peripherals is ongoing. For example, in the above state machine, we would not be able to receive messages while reading sensor data.
Example: A state machine-based implementation of an embedded program for Arduino. Here also there is an interesting example It’s worth checking out these pages just for an overview, and because we will use a similar solution in the course.
Let's present a simple example with Arduino:
enum State {
WAITING,
READ_SENSOR,
PROCESS_DATA,
DISPLAY_RESULT,
TRANSMIT_DATA
};
State currentState = WAITING;
void loop() {
switch (currentState) {
case WAITING:
if (buttonPressed()) {
currentState = READ_SENSOR;
}
break;
case READ_SENSOR:
readSensor();
currentState = PROCESS_DATA;
break;
case PROCESS_DATA:
processData();
currentState = DISPLAY_RESULT;
break;
case DISPLAY_RESULT:
displayResult();
currentState = TRANSMIT_DATA;
break;
case TRANSMIT_DATA:
transmitData();
currentState = WAITING;
break;
}
}
Well, more on this shortly.
Introduction to TI-RTOS¶
The diagram below shows a block diagram of the ready-made functionalities that TI-RTOS, firmware, and device drivers provide for the SensorTag device. The RTOS offers programmers interfaces (the red area in the diagram) that hide the details of the lower-level implementation, such as device and component drivers, real-time features, and communication details. In this course, we focus on the application level, where we use various libraries through interfaces (Application Programming Interface, API). We will stay "within the red area" of the device's operation and will not dive deeper into the internal workings.
We will mention some of these libraries now, but their usage in programs will be explored in detail in later lecture materials.
Services and Libraries¶
The following libraries will be used for program implementation. The terminology will be covered in more detail in the upcoming materials.
- Pin Library for managing microcontroller I/O pins. The buttons and LEDs on the SensorTag are controlled through this library. The GPIO library can also be used to control general I/O pins.
- driverlib Hardware registers and bit masks.
- I2C Library for using the I2C communication bus. Many of SensorTag's sensors can be accessed using this library.
- UART Serial communication protocol for communicating with peripherals. For example, data can be sent to or commands received from a PC using this library.
- (PWM Library for generating Pulse Width Modulation (PWM) on SensorTag’s I/O pins. PWM can be used to supply analog voltage from a digital pin at a certain frequency, for example, to adjust the brightness of an LED.)
(For those interested: More detailed information on the library can be found in the TI-RTOS Kernel User Guide, TI RTOS User's Guide.) and the Technical reference manual of our device family.
Program Operation¶
Let’s take a look at the main function of a SensorTag C program as an example. At first, the C language used by the SensorTag’s RTOS and libraries may seem cryptic because it uses a lot of predefined components. However, by carefully studying the code and thinking about what is happening, it becomes more understandable.
The SensorTag program also has a single main function, from which the program execution begins. Ok, the RTOS does its own thing behind the scenes before the main function runs, but we don’t worry about that in this course.
Below is an example of the
main
function in a SensorTag program:// Include SensorTag libraries
#include <xdc/runtime/System.h>
#include <ti/sysbios/BIOS.h>
#include <ti/sysbios/knl/Task.h>
#include <ti/drivers/I2C.h>
#include <ti/drivers/PIN.h>
int main(void) {
// Note: The initialization of the following variables is omitted here
Board_initGeneral(); // Initialize the device
Board_initI2C(); // Initialize the I2C bus
// I/O definition to enable the button
buttonHandle = PIN_open(&buttonState, buttonConfig);
if(!buttonHandle) {
System_abort("Error initializing button\n");
}
// Set up an interrupt handler for the button (function buttonFxn)
if (PIN_registerIntCb(buttonHandle, &buttonFxn) != 0) {
System_abort("Error registering button callback function");
}
// Initialize a task for the application, implemented in taskFn function
Task_Params_init(&taskParams);
taskParams.stackSize = STACKSIZE;
taskParams.stack = &taskStack;
taskParams.priority=2;
task = Task_create((Task_FuncPtr)taskFxn, &taskParams, NULL);
if (task == NULL) {
System_abort("Task create failed!");
}
// Notify via the debugger that initialization was successful
System_printf("Initialization OK!\n");
System_flush();
// Start the RTOS (BIOS firmware), at which point the task execution begins
BIOS_start();
return 0;
}
As in all C programs, at the beginning of the program, the precompiler directives are used to include the libraries needed for the program. With SensorTag's RTOS, there are quite a few, and this is jus a reduced number of them.
After that, at the beginning of the
In addition, in this example, we want to use the I2C library to communicate with the sensors, so it is initialized with the function call
main
function, the device itself is initialized. Essentially, what is being done here is informing the RTOS that it is running on the SensorTag device. Remember that TI's RTOS is a general-purpose operating system for all TI embedded platforms, so this is needed to take control of the device resources, peripherals, I/O definitions, device-specific connections, etc. In addition, in this example, we want to use the I2C library to communicate with the sensors, so it is initialized with the function call
Board_initI2C
.Once the device is initialized, the selected peripherals are brought into the program by initializing them and setting their I/O configurations. In this example, one of the device's buttons is brought into use, and the handler for the event generated by pressing the button (interrupt) is defined, which in this case is the function
buttonFxn
. Each task in the SensorTag is a C function, and their writing and usage (with some caveats) follow the same rules as C functions generally.Then, in the main function, there would be room to do any other initializations necessary to control the operation of the program. For example, if we are using timers to control the execution of tasks, they can be initialized here.
After that, it's a good idea to send greetings via the debugger to the development environment's console window so we can see that the device initialization was successful. The console can be written to with the
System_printf
function, which works (with minor exceptions) in the same way as the standard C library's printf function. Remember our earlier discussion about how embedded implementations of standard libraries vary in terms of implemented features.Lastly, the "actual" program is started, i.e., the execution of tasks, with the function call
BIOS_start
. After this, the program only responds to the events caused by the peripherals defined in this main function.Note that the code uses a lot of checks after the initializations to ensure that the initializations were successful. These are needed because, when programming embedded systems, we have limited visibility into the internal operation of the device.
SensorTag Program Implementation¶
Rest of the material explains how to create an embedded program for the SensorTag in a task-based manner, so that multiple tasks can run concurrently using the device’s RTOS (Real-Time Operating System).
As hinted in the earlier material, the key idea behind TI-RTOS is that the functionality of the program is implemented as separate tasks that run concurrently based on the device’s processing power. The programmer has full control over how the program is divided into tasks and what each task does internally. A simple program can, of course, be implemented with a single task, but a more complex program that, for example, uses multiple peripherals, is not easily achieved with just one task. Properly implemented multitasking significantly speeds up program execution because tasks don’t need to wait for each other to finish.
This is where the programmer’s design skills come in. It’s not practical to create a separate task for every minor thing, as this would consume resources allocated to the RTOS. Internal functions within a task should also be implemented as functions. For example, calculating the area of a circle doesn’t require a task; a simple C function is enough, which can be called from within a task as needed.
For tasks to run concurrently on a single microcontroller, its real execution time must be shared between the tasks (and the RTOS). Time-sharing can be based on time-based allocation, priority-based allocation, or reactive handling of inputs or various signals when they arrive at the device. When dividing execution time, care must be taken to ensure that no task monopolizes the execution time, so the device's other functions aren’t stalled for too long. For example, this could happen if a high-priority task starves lower-priority tasks by taking all their execution time. Another possible situation could arise if a task communicates with a (relatively) slow peripheral and selfishly holds on to the CPU during the wait time.
How does the TI-RTOS work?¶
Let’s take a look at an example image to see how different tasks (Task) can run concurrently on the SensorTag. In the example, the program implements:
- Two program-related tasks (Task) called Task 1 and Task 2.
- One pre-made task: a communication task, which is assumed to be provided by the library.
- One interrupt handler that manages button press handling in the program.
In the image, each task has a specific execution time and priority. Additionally, it’s noted that once certain tasks finish their execution, they are put to sleep with the
sleep
function call. It is also observed that the program tasks run at a higher priority (2) than the library task (1) and that the interrupt priority is the highest ("infinite").Next, let’s see what happens in the program:
- The program execution starts with Task 1, which runs to completion and is then put to sleep.
- After that, the processor is freed, and the RTOS gives execution control to Task 2, which is also put to sleep after execution.
- Then, the RTOS takes control again to decide what happens in the device, and since there are no tasks of priority 2 currently active, the RTOS runs the known priority 1 task (the communication task).
- When the communication task finishes, the RTOS again notices that Task 1 with priority 2 is active and resumes its execution.
- During the execution of Task 1, an interrupt signal arrives at the microcontroller, and since the interrupt priority is infinite, the interrupt handler takes over in the middle of Task 1’s execution!
- Once the interrupt handler function has finished, the RTOS resumes the execution of Task 1 from where it left off.
- And so, the program execution continues between the three tasks and the interrupt handler endlessly...
Wait a second, what happens during an interrupt? We have to go a little ahead of ourselves here: an interrupt is a hardware-based signal received by the microcontroller, whereas task scheduling is managed by the software (RTOS). In this case, "infinite priority" means that the RTOS has no control over the execution of interrupts — they are handled by the microcontroller. (This is not a complete explanation of how interrupts work, but we’ll cover that in later material).
Ok, one more thing... why isn’t there a
sleep
call after the communication task? The explanation here is that the operation of that specific SensorTag’s wireless radio library requires the task to stay alive and not enter a sleep state. For this reason, the communication task has priority 1, while the higher-priority tasks are at priority 2 to ensure they get execution time.Additionally, there’s one more thing to know about task execution. From this explanation, you might get the impression that each task runs once from start to finish, being triggered each time. However, this is not the case in the course. We actually use an internal superloop for each tasks, i.e., a
while
loop, within which the sleep
function is called! This way, the tasks created in the program remain running and keep the program execution ongoing at all times. This means that the sleep
function is called at the end of every iteration of the superloop. In a task, you start a timer that determines how long the task will sleep, allowing other tasks to execute during this time. The appropriate sleep time depends on the application and is defined by the programmer.Ok, ok, but what would happen if there was no superloop in the tasks? Since a task is a C function, it would only run once, and thus the entire embedded program’s execution would end once all tasks had been executed once. The superloop in the tasks is, therefore, a crucial solution to keep the program running on the device.
RTOS Task Library¶
TI-RTOS allows the implementation of programs and multitasking environments for the SensorTag device through the
Task
library. With this library, tasks can be created as functions, and their operation can be controlled via the RTOS.Let’s take a look at an example of the
Task
library in action:#include <ti/sysbios/BIOS.h>
#include <ti/sysbios/knl/Task.h>
// Each task needs its own internal stack memory
#define STACKSIZE 512
Char myTaskStack[STACKSIZE];
// Task implementation function
Void myTaskFxn(UArg arg0, UArg arg1) {
// The task’s eternal life
while (1) {
System_printf("My arguments are %ld and %ld\n", arg0, arg1);
System_flush();
// Politely go to sleep for a moment
Task_sleep(1000000L / Clock_tickPeriod);
}
}
int main(void) {
// Data structures related to tasks
Task_Params myTaskParams;
Task_Handle myTaskHandle;
// Device initialization
Board_initGeneral();
// Initialize task execution parameters
Task_Params_init(&myTaskParams);
// Assign stack memory to the task
myTaskParams.stackSize = STACKSIZE;
myTaskParams.stack = &myTaskStack;
// Set the task’s priority
myTaskParams.priority = 2;
// Arguments for the task (for demonstration purposes)
myParams.arg0 = 127; // Argument 1
myParams.arg1 = 0xFFFF; // Argument 2
// Create the task
myTaskHandle = Task_create((Task_FuncPtr)myTaskFxn, &myTaskParams, NULL);
if (myTaskHandle == NULL) {
System_abort("Task creation failed");
}
// Greetings to the console
System_printf("Hello world!\n");
System_flush();
// Start the program
BIOS_start();
return (0);
}
Let’s break down this example.
In the task implementation function
myTaskFxn
, we have the aforementioned infinite loop and, at the end of each iteration, the polite Task_sleep
function, which handles the sleep operation and returns control to the RTOS after every iteration.Then, in the
main
function, the execution parameters for the task are set in the data structure myTaskParams
, which is of type Task_Params
. In this example, three things are set for the task, which are always required for every task:- The
Task_params_init
function is called, which sets all parameters to their default values. - The task’s stack memory (at this point, let’s agree that the stack is the task's internal memory area that we don’t need to worry about) is assigned to the members
.stack
and.stackSize
. Every task must have its own stack memory, meaning the same memory area cannot be shared by multiple tasks. - The stack size here is 512 bytes, which is sufficient for most tasks. Exceptions are explained in the course material.
- The task’s priority is set in the member
.priority
. - Program tasks generally have priority 2.
- The communication task has priority 1.
- If you feel the need to change priorities in your project, please consult your instructor first.
Additionally, we set a handle for the task, which can be used to refer to the task in our program. A handle is simply an identifier that uniquely identifies the task. In this example, the handle (
Task_Handle
) is only used to check that the task was successfully created.Arguments can be passed to the task in two unsigned int values (
typedef unsigned int UArg
) through the structure’s members arg0
and arg1
. In this case, the RTOS uses typedef to define its own data types. We will use these as RTOS requires, although passing arguments to tasks might not be necessary in this course, as we may use global variables instead.A task is created using the
Task_create
function, which takes the task’s implementation function (of type Task_FuncPtr
) (here myTaskFxn
) and the parameters myTask_Params
as arguments.We also see the
System_abort
function, which can be used to programmatically abort the execution of the program in case of an error and print an error message to the console window, in this case, if the task fails to start (for example, if the device runs out of memory).The Clock is Ticking¶
The RTOS provides the
Clock.h
library, which includes the Clock_tickPeriod
variable. This tells us how many microseconds there are in one system clock cycle (also known as a tick). By default, the value is 10, meaning one tick equals 10 microseconds. Now, the number of clock cycles can be calculated with the formula n / Clock_tickPeriod
, where *n* is the desired time in microseconds.Actually, for the hardware enthusiasts out there, the RTOS clock cycle is a software constant and doesn’t correspond 1:1 with the device’s hardware clock component.
Conclusion¶
We have become familiar with the basics of program execution in an RTOS. We will delve deeper into using the RTOS libraries in the course lab exercises. In the course lab exercise, students will be given a program template, which includes necessary task implementations and parameter definitions, so you won’t need to look them up outside of the material (although optional additional reading is available).
Free tip 1: If the device gets stuck in the program, the most common cause is a task that runs too long or a missing
Task_sleep
call in the implementation function. Such a task should be redesigned and divided into smaller parts.Free tip 2: It’s not a good idea to place multiple consecutive sleep calls inside the implementation function, as this approach often causes problems when functionality is added to the program later and the previously adjusted timings go wrong. There should be only one
sleep
call in the implementation function, with functionality controlled by conditions and a state machine (see upcoming material).Free tip 3: Sometimes, you’ll see solutions where multiple priority levels are set up. This often leads to device malfunction, especially if additional functionality is later added to the program. It’s better to stick with priority level 2 and reconsider the task’s functionality. Feel free to ask the assistants if needed.
Free tip 4: You definitely shouldn’t implement the SensorTag program Arduino-style with a single task and a superloop, as this will likely cause logic and timing issues between the functionalities. You’ll be doing yourself a disservice.
Give feedback on this content
Comments about this material