FreeRTOS Reference

FreeRTOS Reference

This page is a quick reference for using FreeRTOS on the Zynq in EMBS. FreeRTOS is a lightweight real-time operating system that provides multitasking, scheduling, and inter-task communication primitives. For full details, consult the FreeRTOS Reference Manual (PDF).

Note

Setting up FreeRTOS on the Zynq is covered in Practical 5. This page assumes you already have a working FreeRTOS project.

Task Management

Creating Tasks

Tasks are created with xTaskCreate. Each task is an independent function that runs concurrently with other tasks under the FreeRTOS scheduler.

Creating a task
#include "FreeRTOS.h"
#include "task.h"

#define THREAD_STACKSIZE 1024

TaskHandle_t myTaskHandle;

void my_task(void *params) {
    while (1) {
        xil_printf("Task running\r\n");
        vTaskDelay(1000 / portTICK_RATE_MS); // Delay 1 second
    }
}

int main() {
    xTaskCreate(
        my_task,            // Task function
        "my_task",          // Name (for debugging only)
        THREAD_STACKSIZE,   // Stack size in words
        NULL,               // Parameters passed to task function
        DEFAULT_THREAD_PRIO,// Priority
        &myTaskHandle       // Handle (can be NULL if not needed)
    );

    vTaskStartScheduler();
    return 0;
}

The parameters to xTaskCreate are:

ParameterDescription
pvTaskCodePointer to the task function. Must have signature void func(void *params).
pcNameA descriptive name for the task (used by debuggers, not by FreeRTOS itself).
usStackDepthStack size in words (not bytes). On the Zynq ARM, 1 word = 4 bytes.
pvParametersA pointer passed as the argument to the task function. Use to pass configuration data.
uxPriorityTask priority. Higher number = higher priority.
pxCreatedTaskOptional handle to the created task. Pass NULL if you don’t need it.

Deleting Tasks

Deleting a task
vTaskDelete(myTaskHandle); // Delete a specific task
vTaskDelete(NULL);         // Delete the calling task (self-deletion)

Self-deletion is commonly used for setup tasks that create other tasks and then remove themselves, as seen in the EMBS network startup code.

Task Priorities

  • DEFAULT_THREAD_PRIO is defined in the BSP and is a sensible default priority.
  • configMAX_PRIORITIES defines the maximum number of priority levels (set in FreeRTOSConfig.h).
  • Higher number = higher priority. A task with priority 3 will preempt a task with priority 2.
  • Tasks at the same priority are scheduled round-robin.

Important

Avoid setting all tasks to the same priority unless you specifically want round-robin scheduling. If a high-priority task never blocks, lower-priority tasks will starve.

Stack Sizes

The THREAD_STACKSIZE define (typically 1024 words = 4096 bytes) is used throughout the EMBS examples. This is usually sufficient for simple tasks.

Guidelines for sizing stacks:

  • Each local variable, function call, and interrupt frame consumes stack space.
  • Tasks that use printf/xil_printf or deep call chains need larger stacks.
  • If a task crashes or produces corrupted output, try doubling its stack size.

Important

Stack overflow is one of the most common causes of mysterious crashes in FreeRTOS. If your system is crashing and you can’t work out why, try increasing the stack size first.

Delays

Task delays
// Delay for a fixed number of milliseconds (relative delay)
vTaskDelay(500 / portTICK_RATE_MS); // Delay 500ms

// Delay until an absolute time (for periodic tasks)
TickType_t xLastWakeTime = xTaskGetTickCount();
while (1) {
    vTaskDelayUntil(&xLastWakeTime, 1000 / portTICK_RATE_MS); // Every 1 second
    // Do periodic work here
}
  • vTaskDelay delays for a duration relative to when it is called. Timing will drift if the task body takes variable time.
  • vTaskDelayUntil delays until an absolute tick count, giving precise periodic execution.
  • Always use portTICK_RATE_MS to convert milliseconds to ticks.

Starting the Scheduler

Starting the scheduler
vTaskStartScheduler(); // Start FreeRTOS - this never returns

Once vTaskStartScheduler() is called, FreeRTOS takes over and your tasks begin executing. Code after vTaskStartScheduler() will only run if vTaskEndScheduler() is called (which is rare in embedded systems).

Inter-Task Communication

Queues

Queues are the primary mechanism for passing data between tasks. They are thread-safe and can be used to send messages of any fixed size.

Using queues to pass network messages
// Adapted from www.freertos.org/Documentation/02-Kernel/04-API-references/06-Queues/03-xQueueSend

#include "FreeRTOS.h"
#include "queue.h"
#include <string.h>

struct AMessage
{
    char ucMessageID;
    char ucData[ 20 ];
} xMessage;

unsigned long ulVar = 10UL;

void vATask( void *pvParameters )
{
    QueueHandle_t xQueue1, xQueue2;
    struct AMessage *pxMessage;

    /* Create a queue capable of containing 10 unsigned long values. */
    xQueue1 = xQueueCreate( 10, sizeof( unsigned long ) );

    /* Create a queue capable of containing 10 pointers to AMessage structures.
       These should be passed by pointer as they contain a lot of data. */
    xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) );

    /* ... */

    if( xQueue1 != 0 )
    {
        /* Send an unsigned long. Wait for 10 ticks for space to become
           available if necessary. */
        if( xQueueSend( xQueue1,
                        ( void * ) &ulVar,
                        ( TickType_t ) 10 ) != pdPASS )
        {
            /* Failed to post the message, even after 10 ticks. */
        }
    }

    if( xQueue2 != 0 )
    {
        /* Send a pointer to a struct AMessage object. Don't block if the
           queue is already full. */
        pxMessage = & xMessage;
        xQueueSend( xQueue2, ( void * ) &pxMessage, ( TickType_t ) 0 );
    }

    /* ... Rest of task code. */
}

Key queue functions:

FunctionDescription
xQueueCreate(length, itemSize)Create a queue. Returns NULL on failure.
xQueueSend(queue, &item, timeout)Send an item to the back of the queue. Blocks up to timeout ticks.
xQueueSendFromISR(queue, &item, NULL)Non-blocking send for use in interrupt/callback context, i.e. if we are coming from udp_get_handler().
xQueueReceive(queue, &item, timeout)Receive an item. Use portMAX_DELAY to block indefinitely.

Semaphores

Binary semaphores are useful for signalling between an interrupt handler and a task.

Binary semaphore for interrupt signalling
#include "FreeRTOS.h"
#include "semphr.h"

SemaphoreHandle_t dataSemaphore;

void setup(void) {
    dataSemaphore = xSemaphoreCreateBinary();
}

// Called from an interrupt or callback
void data_ready_isr(void) {
    xSemaphoreGiveFromISR(dataSemaphore, NULL); // Signal that data is ready
}

void worker_task(void *p) {
    while (1) {
        // Block until semaphore is given
        if (xSemaphoreTake(dataSemaphore, portMAX_DELAY) == pdTRUE) {
            xil_printf("Data ready - processing\r\n");
            // Process the data
        }
    }
}

Note

A binary semaphore starts in the “empty” state. The first xSemaphoreTake will block until xSemaphoreGive is called.

Mutexes

Mutexes protect shared resources from concurrent access by multiple tasks.

Mutex protecting shared resource
#include "FreeRTOS.h"
#include "semphr.h"

SemaphoreHandle_t uartMutex;

void setup(void) {
    uartMutex = xSemaphoreCreateMutex();
}

void task_a(void *p) {
    while (1) {
        xSemaphoreTake(uartMutex, portMAX_DELAY);
        xil_printf("Task A output\r\n"); // Protected access
        xSemaphoreGive(uartMutex);
        vTaskDelay(100 / portTICK_RATE_MS);
    }
}

void task_b(void *p) {
    while (1) {
        xSemaphoreTake(uartMutex, portMAX_DELAY);
        xil_printf("Task B output\r\n"); // Protected access
        xSemaphoreGive(uartMutex);
        vTaskDelay(200 / portTICK_RATE_MS);
    }
}

Mutexes vs binary semaphores:

  • Mutexes include priority inheritance: if a high-priority task is waiting on a mutex held by a low-priority task, the low-priority task temporarily inherits the higher priority. This prevents priority inversion.
  • Use mutexes for protecting shared resources. Use semaphores for signalling between tasks or from interrupts.

Common Pitfalls on the Zynq

Stack Overflow

Symptoms: Random crashes, corrupted output, funky behaviour.

Solutions:

  • Increase THREAD_STACKSIZE for the affected task.
  • Enable stack overflow detection in FreeRTOSConfig.h by setting configCHECK_FOR_STACK_OVERFLOW to 1 or 2.
  • Avoid large local arrays in functions. You can use static or global buffers instead.

Blocking in Interrupt/Callback Context

The udp_get_handler callback runs in the context of the lwIP network thread, not as a regular task. You must not call blocking FreeRTOS functions from within it.

Do not use in udp_get_handler:

  • vTaskDelay
  • xQueueSend (use xQueueSendFromISR instead)
  • xSemaphoreTake
  • Any function that may block

Instead: Use the FromISR variants of FreeRTOS functions, or write data to a global buffer and signal a task to process it.

Priority Inversion

If a low-priority task holds a resource (mutex) that a high-priority task needs, and a medium-priority task preempts the low-priority one, the high-priority task is effectively blocked by the medium-priority task. FreeRTOS mutexes (created with xSemaphoreCreateMutex) include priority inheritance to mitigate this. Always use mutexes rather than binary semaphores for resource protection.

Printf/xil_printf Thread Safety

printf and xil_printf are not thread-safe. If multiple tasks call them concurrently, output can be interleaved or corrupted. Protect print calls with a mutex if multiple tasks need to print, or designate a single task for serial output.

lwIP Integration

When using lwIP with FreeRTOS on the Zynq:

  • SOCKET API mode is required. Set api_mode to SOCKET_API in the BSP settings.
  • DHCP timers must be called periodically. The EMBS network code handles this in network_thread by calling dhcp_fine_tmr() and dhcp_coarse_tmr() at appropriate intervals.
  • The network thread must run continuously so do not delete it.
  • Always free pbuf structures after processing received packets.

Useful Links