FreeRTOS Design Routine Based on i.MX9352 Development Board M Core

In the embedded systems, the application of embedded real-time operating systems (RTOS) is becoming increasingly widespread. Using an RTOS can utilize CPU resources more reasonably and efficiently. As a lightweight and mature real-time operating system kernel, FreeRTOS has complete core functions, including task management, time management (such as delays and timers), synchronization mechanisms (semaphores, mutexes), inter-process communication (message queues), and so on. These features enable it to meet the needs of small and medium-sized embedded systems with relatively limited resources.

i.MX 9352 is a new generation of lightweight edge AI processor launched by NXP, which integrates 2 x Cortex-A55 cores and 1 x Cortex-M33 real-time core. Its architecture design fully reflects the balance between real-time and complex task processing capabilities. To help developers fully leverage the real-time capabilities of the i.MX 9352 M33 core, the FreeRTOS examples provided in the accompanying M-core SDK package are divided into two categories. One category introduces the features of FreeRTOS system components, such as semaphores, mutexes, and queues. The other category shows how to use peripheral interfaces in FreeRTOS. Examples from each of these two categories are selected for demonstration.

▊ Demo platform: Forlinx Embedded OK-MX9352-C Development Board

Forlinx Embedded OK-MX9352-C Development Board

1. FreeRTOS-generic

The sample code of FreeRTOS features supported by the Forlinx Embedded OK-MX9352-C is as follows:

  • freertos_event: Demonstration Routine for Task Event
  • freertos_queue: Demonstration routine for implementing inter-task communication using queue messages
  • freertos_mutex: Routine for using mutexes
  • freertos_swtimer: Usage of software timers and their callbacks.
  • freertos_tickless: Routine for delayed wake-up using LPTMR or wake-up by hardware interrupt.
  • freertos_generic: Demonstration routine for the combined use of tasks, queues, software timers, tick hooks, and semaphores.

Since the FreeRTOS_generic routine uses many FreeRTOS features, let's focus on analyzing this routine.

(1)Software implementation

The content of the example program includes: task creation, queues, software timers, system tick clocks, semaphores, and exception handling. Specifically:

Task creation:

The main function creates three tasks: a queue sending task, a queue receiving task, and a semaphore task.

// Create the queue receiving task
if (xTaskCreate(prvQueueReceiveTask, "Rx", configMINIMAL_STACK_SIZE + 166, NULL, mainQUEUE_RECEIVE_TASK_PRIORITY, NULL) != pdPASS)
// Create the queue sending task
if (xTaskCreate(prvQueueSendTask, "TX", configMINIMAL_STACK_SIZE + 166, NULL, mainQUEUE_SEND_TASK_PRIORITY, NULL) != pdPASS)
// Create the semaphore task
if (xTaskCreate(prvEventSemaphoreTask, "Sem", configMINIMAL_STACK_SIZE + 166, NULL, mainEVENT_SEMAPHORE_TASK_PRIORITY, NULL) != pdPASS)

Queues:

The queue sending task blocks for 200ms and then sends data to the queue. The queue receiving task blocks to read from the queue. If the data is read correctly, it prints the current number of received items in the queue.

// The queue sending task blocks for 200ms and then sends data to the queue 
static void prvQueueSendTask(void *pvParameters)  
{  
    TickType_t xNextWakeTime;  
    const uint32_t ulValueToSend = 100UL;  
    xNextWakeTime = xTaskGetTickCount();  
    for (;;)  
    {  
        // The task blocks until the 200ms delay ends.  
        vTaskDelayUntil(&xNextWakeTime, mainQUEUE_SEND_PERIOD_MS);  
        // Send data to the queue. A blocking time of 0 means it will return immediately when the queue is full.  
        xQueueSend(xQueue, &ulValueToSend, 0);  
    }  
} 
//The queue receives the task, and the task is blocked to read the queue. If the data is read correctly, the number received by the queue at this time is printed.
static void prvQueueReceiveTask(void *pvParameters)  
{  
    uint32_t ulReceivedValue;  
    for (;;)  
    {  
        //The task keeps blocking until data is read from the queue  
        xQueueReceive(xQueue, &ulReceivedValue, portMAX_DELAY);  
        //The queue data is consistent with the sending, and the queue receiving quantity+1 outputs the queue receiving quantity at this time  
        if (ulReceivedValue == 100UL)  
        {  
            ulCountOfItemsReceivedOnQueue++;  
            PRINTF("Receive message counter: %d.\r\n", ulCountOfItemsReceivedOnQueue);  
        }  
    }  
}  

Software timers:

Set the software timer period to 1 second. When the time is up, call the callback function, record the number of times, and print it via the serial port.

// Create a software timer task with a time of 1 second and cyclic operation.  
xExampleSoftwareTimer = xTimerCreate(  
                                     "LEDTimer",  
                                     mainSOFTWARE_TIMER_PERIOD_MS,  
                                     pdTRUE,  
                                     (void *)0,  
                                     vExampleTimerCallback);  
//Start the software timer  
xTimerStart(xExampleSoftwareTimer, 0); 
   //Callback function
static void vExampleTimerCallback(TimerHandle_t xTimer)  
{  
    //Enter the callback function once every 1s, and the count increases  
    ulCountOfTimerCallbackExecutions++;  
    PRINTF("Soft timer: %d s.\r\n", ulCountOfTimerCallbackExecutions);  
}

System tick clock:

Set the task tick interrupt frequency by setting configTICK_RATE_HZ in the FreeRTOSConfig.h file. When starting the task scheduler, the system will calculate the value to be written to the tick counter according to another variable configCPU_CLOCK_HZ (CPU frequency) and start the timer interrupt.

// Set the system tick clock to 1000/200 = 5ms  
#define configTICK_RATE_HZ                      ((TickType_t)200)

Semaphores:

In each system tick clock interrupt, call the function vApplicationTickHook. After accumulating 500 times, which is 500 * 5ms = 2.5s, send a semaphore. After the semaphore task acquires the semaphore, it counts and prints the accumulated number of times.

// The system tick is 5ms. Release the event semaphore every 500 * 5ms = 2.5s.  
void vApplicationTickHook(void)  
{  
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;  
    static uint32_t ulCount             = 0;  
    ulCount++;  
    if (ulCount >= 500UL)  
    {  
        //Release the event semaphore in the interrupt  
        xSemaphoreGiveFromISR(xEventSemaphore, &xHigherPriorityTaskWoken);  
        ulCount = 0UL;  
    }  
} 
//The task blocks and waits for the semaphore. After receiving, the number of receiving times increases and is printed through the serial port.  
static void prvEventSemaphoreTask(void *pvParameters)  
{  
    for (;;)  
    {  
        //Task blocks until semaphore can be acquired  
        if (xSemaphoreTake(xEventSemaphore, portMAX_DELAY) != pdTRUE)  
        {  
            PRINTF("Failed to take semaphore.\r\n");  
        }  
        //Accumulate the number of times the semaphore is received  
        ulCountOfReceivedSemaphores++;  
        PRINTF("Event task is running. Get semaphore :%d \r\n",ulCountOfReceivedSemaphores);  
    }  
}  

Exception handling:

When memory allocation fails, a stack error occurs, or a task is idle, the program enters the corresponding function. Users can add corresponding handling functions.

// Memory allocation failure function. When memory allocation fails, the program enters this function.  
void vApplicationMallocFailedHook(void)  
{  
    for (;;)  
        ;  
}
//Stack error check function, which is entered when stack overflow occurs  
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)  
{  
    (void)pcTaskName;  
    (void)xTask;  
    for (;;)  
        ;  
}  
// Idle task, with the lowest priority and no practical significance. It's just to keep the CPU busy. Users can add their own functions.  
void vApplicationIdleHook(void)  
{  
    volatile size_t xFreeStackSpace;  
    xFreeStackSpace = xPortGetFreeHeapSize();  
    if (xFreeStackSpace > 100)  
    {  
    }  
}  

(2)Experimental Phenomenon Part

① Compile the program: Manually load the M-core program in U-Boot.

② Queue: Every 200 milliseconds, the sending task of the queue sends data, and the receiving task of the queue retrieves data. The receiving task transitions from the blocked state to the running state and prints the count.

③ Software timer: Every 1s, when the time is up, the callback function is called, and the count is printed.

④ Semaphore: Every 5ms, the system clock tick interrupt calls a function. After more than 500 times, the semaphore is released. The semaphore task acquires the semaphore, changes from the blocked state to the running state, and prints the count.

Experimental Phenomenon Part

2. FreeRTOS-Peripherals

The Forlinx Embedded OK-MX9352-C development board supports using FreeRTOS to drive various peripherals. The following are some example codes:

  • freertos_uart: FreeRTOS UART demonstration routine
  • freertos_lpi2c_b2b: FreeRTOS I2C demonstration routine
  • freertos_lpspi_b2b: FreeRTOS SPI demonstration routine

Since the freertos_uart routine uses typical FreeRTOS features, focus on analyzing this routine.

(1) Software implementation

The example program content includes: serial port initialization task, serial port sending task, and serial port receiving task. Specifically:

Serial port initialization task:

It mainly includes the initialization of serial port peripherals, sending and receiving mutexes, and sending and receiving event groups. The initialization of serial port peripherals has been demonstrated in the bare-metal running serial port example, so it will not be detailed here.

// Create a serial port sending mutex.
handle->txSemaphore = xSemaphoreCreateMutex();
// Create a serial port receiving mutex.
handle->rxSemaphore = xSemaphoreCreateMutex(); 
// Create a flag group sending events
handle->txEvent     = xEventGroupCreate();
// Create a flag group receiving events
handle->rxEvent     = xEventGroupCreate();

Serial port sending:

The semaphore is obtained before sending, the sending process is started, and the sending completion event flag is set in the interrupt. After acquiring the event, the send task releases the send semaphore.

//1 Get the send semaphore  
if (pdFALSE == xSemaphoreTake(handle->txSemaphore, 0))   
{  
    return kStatus_Fail;  
}  
handle->txTransfer.data     = (uint8_t *)buffer;  
handle->txTransfer.dataSize = (uint32_t)length;  
//2 blocking transmission  
status = UART_TransferSendNonBlocking(handle->base, handle->t_state, &handle->txTransfer);  
if (status != kStatus_Success)  
{  
    (void)xSemaphoreGive(handle->txSemaphore);   
    return kStatus_Fail;  
}  
// 3. Wait for the event of transmission completion  
ev = xEventGroupWaitBits(handle->txEvent, RTOS_UART_COMPLETE, pdTRUE, pdFALSE, portMAX_DELAY);// Wait and evaluate multiple event flags  
if ((ev & RTOS_UART_COMPLETE) == 0U)  
{  
    retval = kStatus_Fail;  
}  
// 4 Transmission completed, release the transmission semaphore.  
if (pdFALSE == xSemaphoreGive(handle->txSemaphore)) // Release the transmission semaphore.  
{  
    retval = kStatus_Fail;  
} 

Serial port receiving:

Before receiving, obtain the semaphore, call the serial port receiving function, and set the event flag in the interrupt. After the receiving task obtains the event, release the receiving semaphore.

// 1. Obtain the receiving semaphore.  
if (pdFALSE == xSemaphoreTake(handle->rxSemaphore, portMAX_DELAY))    
{  
    return kStatus_Fail;  
}  
handle->rxTransfer.data     = buffer;  
handle->rxTransfer.dataSize = (uint32_t)length;  
//2 serial port receiving function  
status = UART_TransferReceiveNonBlocking(handle->base, handle->t_state, &handle->rxTransfer, &n);  
if (status != kStatus_Success)  
{  
    (void)xSemaphoreGive(handle->rxSemaphore);   
    return kStatus_Fail;  
}  
//3 Obtain the receiving event  
ev = xEventGroupWaitBits(handle->rxEvent,RTOS_UART_COMPLETE | RTOS_UART_RING_BUFFER_OVERRUN | RTOS_UART_HARDWARE_BUFFER_OVERRUN, pdTRUE, pdFALSE, portMAX_DELAY);   // Wait and check the event bit indicating the completion of receiving  
// 3.1 Hardware receiving error  
if ((ev & RTOS_UART_HARDWARE_BUFFER_OVERRUN) != 0U)   
{  
    UART_TransferAbortReceive(handle->base, handle->t_state);  
    (void)xEventGroupClearBits(handle->rxEvent, RTOS_UART_COMPLETE);    // Clear the event bit indicating receiving completion.  
    retval         = kStatus_UART_RxHardwareOverrun;  
    local_received = 0;  
}  
//3.2 Receiving buffer overload error  
else if ((ev & RTOS_UART_RING_BUFFER_OVERRUN) != 0U)   
{  
    UART_TransferAbortReceive(handle->base, handle->t_state);  
    (void)xEventGroupClearBits(handle->rxEvent, RTOS_UART_COMPLETE);    // Clear the event bit indicating receiving completion.  
    retval         = kStatus_UART_RxRingBufferOverrun;  
    local_received = 0;  
}  
//3.3 Receiving completed  
else if ((ev & RTOS_UART_COMPLETE) != 0U)     
{  
    retval         = kStatus_Success;  
    local_received = length;  
}  
else  
{  
    retval         = kStatus_UART_Error;  
    local_received = 0;  
}  
//4. Release the received signal quantity  
if (pdFALSE == xSemaphoreGive(handle->rxSemaphore))   
{  
    retval = kStatus_Fail;  
} 

(2)Experimental Phenomenon Part

① Compile the program and manually load the M-core program in U-Boot.

② After the device is powered on, the serial port prints the program information. At this time, input 4 characters via the keyboard, and the M-core debugging serial port will echo. Repeating the input and echo of characters proves that the program runs successfully.

Experimental Phenomenon Part

The above is an example demonstration of FreeRTOS software design on the M-core of the Forlinx Embedded i.MX 9352 development board. Hope it can be helpful to all engineer friends.




Dear friends, we have created an exclusive embedded technical exchange group on Facebook, where our experts share the latest technological trends and practical skills. Join us and grow together!