FreeRTOS is a real-time operating system that lets you run multiple tasks on a microcontroller as if they were running simultaneously. On a single-core chip like the TM4C123, it does this by rapidly switching between tasks — so fast that it feels parallel.
This guide covers the practical side of getting FreeRTOS running on the TM4C123, with real examples from a health monitoring project that used a pulse sensor, temperature sensor, LCD display, and WiFi module all running concurrently.
Why Use FreeRTOS
Without an RTOS, you write one big loop and everything runs sequentially. Reading a sensor, updating a display, sending data over WiFi — they all block each other. The faster task has to wait for the slower one.
With FreeRTOS, each thing gets its own task with its own priority. The RTOS scheduler decides who runs when. Your sensor reading does not wait for WiFi to finish. Your display updates do not block your sensor sampling.
For any project with more than two things happening at different rates, FreeRTOS makes the code cleaner and more reliable.
Setting Up FreeRTOS on TM4C123
TivaWare includes a FreeRTOS port for the TM4C123. In Code Composer Studio:
- Create a new TivaWare project
- Add the FreeRTOS source files to your project:
tasks.c,queue.c,list.c,timers.c,event_groups.c- The port files:
port.candportmacro.hfor the Cortex-M4F
- Add
FreeRTOSConfig.h— this is the configuration file for your specific setup
A minimal FreeRTOSConfig.h for the TM4C123 running at 80 MHz:
#define configUSE_PREEMPTION 1
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configCPU_CLOCK_HZ 80000000UL
#define configTICK_RATE_HZ 1000
#define configMAX_PRIORITIES 5
#define configMINIMAL_STACK_SIZE 128
#define configTOTAL_HEAP_SIZE 8192
#define configMAX_TASK_NAME_LEN 16
#define configUSE_TRACE_FACILITY 0
#define configUSE_16_BIT_TICKS 0
#define configIDLE_SHOULD_YIELD 1
#define configUSE_MUTEXES 1
#define configUSE_COUNTING_SEMAPHORES 1
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY 2
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH 128
#define INCLUDE_vTaskDelay 1
#define INCLUDE_vTaskDelete 1
#define INCLUDE_vTaskSuspend 1
Creating Tasks
A FreeRTOS task is just a C function that never returns. It runs in an infinite
loop and calls vTaskDelay to yield time to other tasks.
#include "FreeRTOS.h"
#include "task.h"
void vTemperatureTask(void *pvParameters) {
while (1) {
float temp = readLM35(); // read sensor
updateTemperatureDisplay(temp); // update LCD
vTaskDelay(pdMS_TO_TICKS(500)); // wait 500ms
}
}
void vPulseTask(void *pvParameters) {
while (1) {
int bpm = readPulseSensor();
updateBPMDisplay(bpm);
vTaskDelay(pdMS_TO_TICKS(20)); // sample at 50Hz
}
}
int main(void) {
// hardware init here
xTaskCreate(vTemperatureTask, "TempTask", 256, NULL, 2, NULL);
xTaskCreate(vPulseTask, "PulseTask", 256, NULL, 3, NULL);
vTaskStartScheduler();
while(1); // never reached
}
xTaskCreate parameters: task function, name, stack size in words, parameters,
priority, task handle. Higher priority number means higher priority.
Queues — Passing Data Between Tasks
A queue is a FIFO buffer that tasks use to send data to each other safely. One task writes to it, another reads from it. The RTOS handles the synchronization.
This is better than using a global variable because a global variable can be corrupted if two tasks write to it at the same time.
#include "queue.h"
QueueHandle_t xTempQueue;
void vSensorTask(void *pvParameters) {
float temperature;
while (1) {
temperature = readLM35();
xQueueSend(xTempQueue, &temperature, 0); // send, no wait
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void vDisplayTask(void *pvParameters) {
float receivedTemp;
while (1) {
if (xQueueReceive(xTempQueue, &receivedTemp, pdMS_TO_TICKS(1000))) {
displayTemperature(receivedTemp);
}
}
}
int main(void) {
xTempQueue = xQueueCreate(5, sizeof(float)); // 5 items, each a float
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 2, NULL);
xTaskCreate(vDisplayTask, "Display", 256, NULL, 1, NULL);
vTaskStartScheduler();
while(1);
}
The queue holds up to 5 floats. If the sensor task fills it before the display
task reads, xQueueSend will drop the new item (with timeout 0). You can change
this behavior by passing a timeout instead of 0.
Semaphores — Synchronization Between Tasks
A semaphore signals between tasks. One task gives it, another waits for it. Useful when one task needs to wait for something to happen before proceeding.
Binary semaphore example — WiFi task waits for data to be ready:
#include "semphr.h"
SemaphoreHandle_t xDataReadySemaphore;
void vSensorTask(void *pvParameters) {
while (1) {
collectAllSensorData();
xSemaphoreGive(xDataReadySemaphore); // signal data is ready
vTaskDelay(pdMS_TO_TICKS(5000)); // collect every 5 seconds
}
}
void vWiFiTask(void *pvParameters) {
while (1) {
// wait until data is ready, up to 10 seconds
if (xSemaphoreTake(xDataReadySemaphore, pdMS_TO_TICKS(10000))) {
sendDataOverWiFi();
}
}
}
int main(void) {
xDataReadySemaphore = xSemaphoreCreateBinary();
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 2, NULL);
xTaskCreate(vWiFiTask, "WiFi", 512, NULL, 1, NULL);
vTaskStartScheduler();
while(1);
}
Software Timers
Instead of creating a task just to do something periodically, use a software timer. It calls a callback function at a set interval without occupying a full task stack.
#include "timers.h"
TimerHandle_t xHeartbeatTimer;
void vHeartbeatCallback(TimerHandle_t xTimer) {
toggleLED(); // blink an LED every second
}
int main(void) {
xHeartbeatTimer = xTimerCreate(
"Heartbeat",
pdMS_TO_TICKS(1000), // period: 1 second
pdTRUE, // auto-reload
0,
vHeartbeatCallback
);
xTimerStart(xHeartbeatTimer, 0);
vTaskStartScheduler();
while(1);
}
Common Problems
Stack overflow — each task needs its own stack. If a task uses local arrays or
calls deep functions, increase its stack size. Enable configCHECK_FOR_STACK_OVERFLOW
in FreeRTOSConfig.h to catch this during development.
Priority inversion — a high priority task blocked waiting for a resource held by a low priority task. Use mutexes instead of binary semaphores when protecting shared resources — mutexes have priority inheritance built in.
Race conditions on shared peripherals — if two tasks use the same I2C bus or UART, protect it with a mutex. Only one task should access the peripheral at a time.
MutexHandle_t xI2CMutex;
void vTask1(void *p) {
while(1) {
xSemaphoreTake(xI2CMutex, portMAX_DELAY);
i2c_write(...); // safe, exclusive access
xSemaphoreGive(xI2CMutex);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
Summary
FreeRTOS on the TM4C123 is well supported and reliable once configured correctly. Tasks for concurrent work, queues for passing data between tasks, semaphores for signaling, and software timers for periodic callbacks — these four primitives cover the vast majority of real-time embedded applications.