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
- You need to have finished the Digging into RTX.
- You need to have completed the Robust Development Methodologies (I) codelab.
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 inApplication
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:
/**
******************************************************************************
* @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.
// 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:
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:
// 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
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:
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()
.
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
, theosRtxIdleThread
and theosRtxTimerThread
threads. - The
osRtxIdleThread
and theosRtxTimerThread
threads are created by the kernel uponosKernelStart()
. - 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 theosRtxTimerThread
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:
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:
struct task_info_t {
uint32_t computation_time;
uint32_t period;
};
For creating the task, you need to use a code similar to:
// 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: