Skip to content

Scheduling of periodic tasks

Introduction

You have built your first Keil application, understood the basic principles behind Keil RTX5 and started to put in place methodologies for robust and safe development. When developing real-time applications with a RTOS, a very important aspect is the understanding of scheduling. Real-time applications must guarantee that each task is completed within its deadline.

In this codelab, we address the problem of scheduling periodic tasks. For this purpose, we first apply a Time-Triggered Cyclic Executive (TTCE) scheduling. We then apply a fixed priority-based algorithm, namely the Rate Monotonic algorithm, and demonstrate how Keil RTX5 must be configured for executing periodic tasks based on the Rate Monotonic (RM) algorithm.

What you’ll build

In this codelab, you will build an application with a number of periodic tasks, using both TTCE and RM scheduling.

What you’ll learn

  • How timers can be used for scheduling periodic tasks.
  • The basic principles of the Keil RTX5 scheduler.
  • How to configure the Keil RTX5 scheduler for applying a rate monotonic algorithm.
  • How event recording can be used for analyzing the dynamic behavior of the program schedule.

What you’ll need

Starting point

For this codelab you may start from the project that you created for the Digging into RTX codelab and you will have applied the corrections for fixing all MISRA C 2012 related issues.

Very important

So far, you have added code to the main.c created by the STM32CubeMX. However, this is not good practice. As a result, a modular approach is to be chosen and you will create a folder containing the different functions with their corresponding .h/.c files. In order to do so, follow what specified here. Once done, you will be able to import the functionality in main.c with #include "yourlibrary.h"

Critical note

From now on, it is expected that the code you create will fulfill MISRA C:2012 guidelines. So, make sure you adapt your cppcheck command accordingly.

Project Structure

As explained above, it is important that you apply a modular approach when developing the application. It is also important to structure the project. For the sake of simplicity and uniformness, you are required to apply the following structure for your project:

   /
   ├── Application
      ├── aperiodic_gen.c
      ├── aperiodic_gen.h
      ├── app_config.h
      ├── background.c
      ├── background.h
      ├── deferrable.c
      ├── deferrable.h
      ├── faults.c
      ├── faults.h
      ├── helpers.c
      ├── helpers.h
      ├── monotonic.c
      ├── monotonic.h
      ├── task_info.h
      ├── tasks.c
      ├── tasks.h
      ├── ttce.c
      ├── ttce.h
   ├── README.md
   ├── RTE
      ├── CMSIS
      ├── CMSIS-View
      ├── Device
      └── _DISCO-L475
   ├── .clang-format
   ├── .pre-commit-config.yaml
   ├── cpplint.cfg
   ├── misra.json
   ├── simple-car.uvoptx
   ├── simple-car.uvprojx
   └── VIO

Notes:

  • The structure shown below corresponds to the structure on your hard drive. The structure shown in Keil is slightly different, but the structure in the project should match the folder structure on disk - it means that your project should for instance contain a folder named Application.
  • The files contained in the Application folder will evolve over the different project phases. The name of the files contained in Application may not need change over phases - they should remain the same in all phases, but new files will be created with the project evolving.
  • The project name should be car-sim.uvprojx
  • The content of the folders CMSIS, Compiler, Device and _DISCO-L475 is not shown above for readibility. Obviously, there is content in them.

Implement error handlers

Your code does probably not yet implement any error handler. One possible implementation of error handler functions is given below:

application/faults.c
/**
 ******************************************************************************
 * @file        : faults.c
 * @brief       : error helper module
 * @author      : Serge Ayer <serge.ayer@hefr.ch>
 * @author      : Luca Haab <luca.haab@hefr.ch>
 * @date        : 19. March 2023
 ******************************************************************************
 * @copyright   : Copyright (c) 2023 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Helper module for errors
 ******************************************************************************
 */

/* Although normally "The Standard Library input/output functions shall not be
   used" (Rule 21.6), in this case we make use of it as temporary solution. */
// cppcheck-suppress misra-c2012-21.6 
#include <stdio.h>

#include "EventRecorder.h"

/* Local variables located in shared RAM */
#define STR_LENGTH 128
static char temp_str[STR_LENGTH] __attribute__((section("ram_shared")));

#define ERROR_LOG_ARRAY(x) EventRecordData(EventID(EventLevelError, 0xFD, 0x01), x, STR_LENGTH)

static void error_handler(void)
{
    // report the error and loop
    ERROR_LOG_ARRAY(&temp_str);
    while (1) {
    }
}

void startup_error_handler(void)
{   
    (void)snprintf(temp_str, sizeof(temp_str), "A Fatal Error was detected, the system will halt");
    error_handler();
}

void app_error_handler(uint32_t errNo)
{
    (void)snprintf(temp_str, sizeof(temp_str), "A Application Error was detected (%d), the system will halt", errNo);
    error_handler();
}

Include this code in your project by creating “faults.c/.h” files in the “Application” folder. Make sure to call an error handler when an error occurs at startup or in the application code. Note that the error_handler() function itself can be modified. This implementation loops forever in case of error, but alternatively you may wish to reset the system by calling the NVIC_SystemReset() function.

For a correct display of errors by the EventRecorder, you also need to modify the project as described here. The corresponding SCVD (Software Component View Description) file for configuring your project is available here. Detailed instructions for modifying the project are available here.

Implement a program using Time-Triggered Cyclic Executive Scheduling

We first wish to implement a simple program using TTCE Cyclic Scheduling. The task set to be realized is the one described in this exercice. Note that for an easier time analysis with the event recorder, you may multiply all times by \(10\).

Project structure

Observe the name of the files in which each code snippet has to be copied for matching the required project structure. You must of course create the corresponding header files. In the case that the code shown below does NOT check return codes correctly, it must be fixed and your implementation must properly handle error codes.

For implementing the behavior, we use the Keil RTX5 timer mechanism. We first define a number of constants that represent the TTCE schedule and that will be used in the functions. You must of course replace XXX and YYY with the values corresponding to the computed TTCE schedule.

application/ttce.c
// constants defining major cycle and minor cycle durations
const uint32_t MAJOR_CYCLE_DURATION = XXX;
const uint32_t MINOR_CYCLE_DURATION = YYY;
const uint32_t NBR_OF_MINOR_CYCLES = MAJOR_CYCLE_DURATION/MINOR_CYCLE_DURATION;

Then, in a dedicated thread function, we create a timer using osTimerNew, as shown below:

application/ttce.c
static __NO_RETURN void ttce_task_thread(void *arg) {
  // address unused parameter warning (also misra-c2012-2.7)
  (void)arg;
  // create the periodic timer for launching tasks at each minor cycle start
  osTimerId_t minor_cycle_task_id =
      osTimerNew(task_dispatcher, osTimerPeriodic, (void*) scheduleTable, NULL);
  if (minor_cycle_task_id == NULL) {
    app_error_handler(CANNOT_CREATE_TIMER);
  }

  // start the periodic timer for running tasks in minor cycles
  osStatus_t status = osTimerStart(minor_cycle_task_id, MINOR_CYCLE_DURATION);
  if (status != osOK) {
    app_error_handler_with_info(CANNOT_START_TIMER, status);
  }

  // suspend this thread
  osThreadSuspend(tid_thr_app_main);

  while (1) {
    __WFI();
  }
}

The task_dispatcher() function responsible for launching the tasks in each corresponding minor cycle can be written as:

application/ttce.c
// create the static schedule table (restricted to one task per minor cycle here)
typedef void (*TaskFunc_t)(void *argument);
TaskFunc_t scheduleTable[] =
  { task1, task2, task3, task3, task2, task1, task3, task3, task2, task3};

static void task_dispatcher(void *arg) {
  // cppcheck-suppress misra-c2012-11.5
  // rationale: void* being the one solution in c that
  //            allows generic interfaces - albeit not type safe
  TaskFunc_t* pScheduleTable = ((TaskFunc_t *)arg);

  static uint32_t minor_cycle_index = 0;
  // execute the task corresponding to minor cycle index
  pScheduleTable[minor_cycle_index]((void*) &MINOR_CYCLE_DURATION);

  // update minor cycle index
  minor_cycle_index = (minor_cycle_index + 1) % NBR_OF_MINOR_CYCLES;
}

Each task function may be written as

application/tasks.c
void taskX(void* arg) 
{   
  uint32_t computation_time = (uint32_t) *((uint32_t*) arg);
  busy_wait_ms(computation_time);
}

where X is 1, 2 and 3 and the busy_wait_ms() function is:

application/helpers.c
void busy_wait_ms(uint32_t ms) 
{
#ifdef ENABLE_DEBUG
  static const uint64_t NbrOfIterationsFor1Ms = 10852;
#else
  static const uint64_t NbrOfIterationsFor1Ms = 11340;
#endif

  const uint64_t NbrOfIterations = (uint64_t)(ms * NbrOfIterationsFor1Ms);
  for (uint64_t i = 0; i < NbrOfIterations; i++) {
    __NOP(); 
  }
}

Note

The busy_wait_ms() function performs a wait by iterating for a predefined number of iterations - this number may need to be adapted depending on the compiler options.

Ultimately, we start a thread that executes the ttce_task_thread function. This function must be created in a separate thread because the main thread needs to execute the osKernelStart() function and osKernelStart() does not return in case of success. The app_main_ttce_scheduling() function must be called from your main() function, before the call to osKernelStart().

application/timeline.c
void app_main_ttce_scheduling(void) {
  tid_thr_app_main = osThreadNew(ttce_task_thread, NULL, NULL);
  if (tid_thr_app_main == NULL) {
    app_error_handler(CANNOT_CREATE_THREAD);
  } 
}

Observe TTCE Scheduling Behavior

If you implement the correct behavior, configure the event recorder properly and watch the event recorder log, you should observe a event log similar to:

This event log tells us the following:

  • Three threads are created on startup, namely the ttce_task_thread, the osRtxIdleThread and the osRtxTimerThread threads.
  • The osRtxIdleThread and the osRtxTimerThread threads are created by the kernel upon osKernelStart().
  • The ttce_task_thread thread is suspended almost immediately after it is started.
  • At regular intervals corresponding to the minor cycle duration, the osRtxTimerThread thread is awakened to dispatch the tasks at the beginning of each minor cycle. Each task is thus executed under the control of the osRtxTimerThread thread.

Unfortunately, given that all tasks are executed in the same thread, it is not possible to verify that the implemented task schedule is correct. For verifying this, it is necessary to add tracing at the beginning of each task function, for instance by using a macro like:

#define LOG_TASK(x) EventRecord2(EventID(EventLevelAPI, 0xFD, 0x00), x, 0)

If you do so, then the event log should look like:

Note

For a correct display of the messages, you need to configure the event recorder as explained above. If you do so, you should observe that the event category configured for the car-sim appears in the “Show Event Levels” window:

Note

When adding EventRecorder calls in separate threads (like the osRtxTimerThread thread), you may need to configure the Event Recorder to use “CMSIS-RTOS2 System Timer” as the Time Stamp Source. For reasons that are not well explained, tracing in a separate thread may produce a fault when the Time Stamp Source is configured as “DWT Cycle Counter”.

In the EventRecorder window, you should observe that tasks are scheduled as expected given the solution developed in the related exercice.

Implement a program using Monotonic Rate Scheduling

We wish now to implement a multi-tasking program that uses Monotonic Rate Scheduling. For this purpose, we choose the set of tasks as described in this exercise.

In this implementation, each task must be executed by a separate thread. The task thread function may be implemented as:

application/tasks.c
void task_thread(void* arg) 
{
    struct task_info_t task_info = (struct task_info_t) *((struct task_info_t*) arg);

    if (START_DELAY > 0U) {
        osDelayUntil(START_DELAY);
    }

    uint32_t next_execution_in_ms = START_DELAY;
    for (;;) {
        // wait for the computation time
        busy_wait_ms(task_info.computation_time);

    // Calculate next period start time
    next_execution_in_ms += task_info.period;

        // wait until next execution
        osDelayUntil(next_execution_in_ms);
    }
}

The task thread function parameters are specified using the following structure:

application/task_info.h
struct task_info_t {
    uint32_t computation_time;
    uint32_t period;
};

For creating the task, you need to use a code similar to:

application/monotonic.c
// code part of the "app_main_monotonic_rate()" function called from the "main()" function
const static osThreadAttr_t th1_att = {.name = "Thread1", .priority = osPriorityNormal5};   
static struct task_info_t task_info1 = {150U, 250U, 0U};
osThreadId_t tid_thr_task1 = osThreadNew(task_thread, (void*) &task_info1, &th1_att);
if (tid_thr_task1 == NULL) { 
  app_error_handler(CANNOT_CREATE_THREAD);
}

Note the delay applied for starting the tasks. This is due to the fact that the first instance of the task is executed after the timer period and we wish to have all tasks operating with a phase \(\Phi = 0\) - meaning all tasks starting at the same time. This is mandatory, because the Rate Monotonic Algorithm assumes that the phase of all task periods is \(0\). You may specify the start offset as being the Major Cycle duration.

Important

For realizing a Monotonic Rate Algorithm with Keil RTX5, you need to set the fixed priorities of the threads correctly (based on the task periods). You also need to disable Round Robin Thread Switching in the System Configuration (“RTX_Config.h”).

If you implement the program correctly and use Tracealyzer TraceView tool, you should observe a trace view similar to:

Validation of the observed results

The results obtained by the dynamic analysis of your program should correspond to the solution that you have developed for the corresponding exercise.

A very useful tool for validating the results obtained using dynamic analysis is the use of a scheduling simulator. We suggest to use this simulator. If you configure the three tasks as in the exercise and in your program, and run a simulation using the Monotonic Rate Algorithm, you should get a result similar to: