Skip to content

RK0 Profiling

Antonio Giacomelli edited this page Dec 21, 2025 · 31 revisions

Preemptive test

The following test was performed on an ARM Cortex-M4F @ 80MHz (Nucleo STM32F401RE)

It has been compiled as follows:

OPT      := -O2
  DEBUG    :=  -DNDEBUG 
  CFLAGS_EXTRA := -g -fstack-usage
  ASFLAGS_EXTRA := -g -D__FPU_PRESENT=0 -DNDEBUG
  STRIPFLAGS :=

COMMON_WARN := -Wall -Wextra -Wsign-compare -Wsign-conversion -pedantic
CFLAGS  := -std=gnu11 $(MCU_FLAGS) $(STMHAL) $(BOARD) $(DEBUG) \
           $(COMMON_WARN) -ffunction-sections -fdata-sections \
           $(CFLAGS_EXTRA) $(OPT) $(INC_DIRS)

ASFLAGS := $(MCU_FLAGS) -x assembler-with-cpp $(ASFLAGS_EXTRA) -ffunction-sections -fdata-sections
LDFLAGS := -nostartfiles -T $(LINKER_SCRIPT) $(MCU_FLAGS) \
           -Wl,-Map=$(BUILD_DIR)/rk0_demo.map,--cref -Wl,--gc-sections \
           -specs=nano.specs -lc

What it means:

  • NDEBUG flag (error checking is disabled)
  • FPU is disabled
  • Stack overflow checking is disabled
  • Optmisation level: -O2 (adequate for release, not strictly optimised for speed)
  • Debug symbols were not stripped off (-g flags ) so we can inspect symbols for the running test.

Test bench code

This test is based on the ThreadX test-suit

Reproducing the description:

1.3. Preemptive Scheduling Test ? This test consists of 5 threads that each have a unique priority. In this test, all threads except the lowest priority thread are left in a suspended state. The lowest priority thread will resume the next highest priority thread. That thread will resume the next highest priority thread and so on until the highest priority thread executes. Each thread will increment its run count and then call thread suspend. Eventually the processing will return to the lowest priority thread, which is still in the middle of the thread resume call. Once processing returns to the lowest priority thread, it will increment its run counter and once again resume the next highest priority thread - starting the whole process over once again.

The ThreadX code is here: https://github.com/eclipse-threadx/threadx/blob/master/utility/benchmarks/thread_metric/tm_preemptive_scheduling_test.c

For RK0, the same is written is as follows:

#include <application.h>
#include <kapi.h>

/* Preemptive scheduling profiling */
/* Each run has 30 sec */
/* Criteria: counters will be at most +/-1 from the average */

 #define TM_TEST_DURATION   30000UL     /* ms (tick = 1ms) */
 #define TM_FLAG            0x01UL

 #define kSuspendSelf(timeout) \
     do { (void)kTaskFlagsGet(TM_FLAG, RK_FLAGS_ANY, NULL, (timeout)); } while (0)

 #define kResumeTask(taskHandle) \
     do { (void)kTaskFlagsSet((taskHandle), TM_FLAG); } while (0)

#define STACKSIZE 128

RK_DECLARE_TASK(task1Handle, Task1, stack1, STACKSIZE)
RK_DECLARE_TASK(task2Handle, Task2, stack2, STACKSIZE)
RK_DECLARE_TASK(task4Handle, Task4, stack4, STACKSIZE)
RK_DECLARE_TASK(task5Handle, Task5, stack5, STACKSIZE)
RK_DECLARE_TASK(task3Handle, Task3, stack3, STACKSIZE)
RK_DECLARE_TASK(task6Handle, Task6, stack6, STACKSIZE)

 /* counters */

volatile ULONG counter1 = 0UL;
volatile ULONG counter2 = 0UL;
volatile ULONG counter3 = 0UL;
volatile ULONG counter4 = 0UL;
volatile ULONG counter5 = 0UL;


 VOID kApplicationInit(VOID)
 {
     /* lowest priority */
     kCreateTask(&task1Handle, Task1, RK_NO_ARGS, "T0", stack1, STACKSIZE, 6, RK_PREEMPT);
     kCreateTask(&task2Handle, Task2, RK_NO_ARGS, "T1", stack2, STACKSIZE, 5, RK_PREEMPT);
     kCreateTask(&task3Handle, Task3, RK_NO_ARGS, "T2", stack3, STACKSIZE, 4, RK_PREEMPT);
     kCreateTask(&task4Handle, Task4, RK_NO_ARGS, "T3", stack4, STACKSIZE, 3, RK_PREEMPT);
     kCreateTask(&task5Handle, Task5, RK_NO_ARGS, "T4", stack5, STACKSIZE, 2, RK_PREEMPT);
     /* highest priority - reporting task */
     kCreateTask(&task6Handle, Task6, RK_NO_ARGS, "REP", stack6, STACKSIZE, 1, RK_PREEMPT);
 }

/* lowest priority counter task: does not suspend itself */
/* it should be preempted by higher priority tasks */
 VOID Task1(VOID *args)
 {
     RK_UNUSEARGS
     while (1)
     {
         kResumeTask(task2Handle);
         counter1++;
     }
 }

 /*  2..4: start suspended, resume, increment, self-suspend */
VOID Task2(VOID *args)
 {
     RK_UNUSEARGS

     kSuspendSelf(RK_WAIT_FOREVER);

     while (1)
     {
         kResumeTask(task3Handle);
         counter2++;
         kSuspendSelf(RK_WAIT_FOREVER);
     }
 }

 VOID Task3(VOID *args)
 {
     RK_UNUSEARGS

     kSuspendSelf(RK_WAIT_FOREVER);

     while (1)
     {
         kResumeTask(task4Handle);
         counter3++;
         kSuspendSelf(RK_WAIT_FOREVER);
     }
 }

 VOID Task4(VOID *args)
 {
     RK_UNUSEARGS;

     kSuspendSelf(RK_WAIT_FOREVER);

     while (1)
     {
         kResumeTask(task5Handle);
         counter4++;
         kSuspendSelf(RK_WAIT_FOREVER);
     }
 }
/* highest priority task: does not resume any task */
 VOID Task5(VOID *args)
 {
     RK_UNUSEARGS

     kSuspendSelf(RK_WAIT_FOREVER);

     while (1)
     {
         counter5++;
         kSuspendSelf(RK_WAIT_FOREVER);
     }
 }
 
 volatile ULONG error = 0UL; 
 volatile ULONG roundn = 0UL;
 volatile RK_TICK time0 = 0UL;
 volatile RK_TICK time1 = 0UL;
 volatile ULONG total = 0UL;
 volatile ULONG average = 0UL;


 /* REPORTING TASK: */
 /* Highest priority, starts, sleeps for 30000 ticks (30sec), interrupts and
evaluate */
  VOID Task6(VOID *args)
 {
     RK_UNUSEARGS

     while (1)
     {
         time0 = kTickGetMs();
         kSleep(TM_TEST_DURATION);
         time1 = kTickGetMs();
         total = counter1 + counter2 + counter3 + counter4 + counter5;
         average = total / 5;
         roundn += 1;

         /* check: each counter within +-1 of average */
         if ( ((LONG)counter1 < (LONG)average - 1) || ((LONG)counter1 > (LONG)average + 1) ||
              ((LONG)counter2 < (LONG)average - 1) || ((LONG)counter2 > (LONG)average + 1) ||
              ((LONG)counter3 < (LONG)average - 1) || ((LONG)counter3 > (LONG)average + 1) ||
              ((LONG)counter4 < (LONG)average - 1) || ((LONG)counter4 > (LONG)average + 1) ||
              ((LONG)counter5 < (LONG)average - 1) || ((LONG)counter5 > (LONG)average + 1) )
         {

             error += 1;
         }

     }
 }

Results

The results can be seen below:

Preemptive test results

It would be reasonable to expect some drifting (it has happened within other RTOSes I have been testing). Happily, RK0 seems to be achieving its deterministic-first design approach:

150 secs test

Given the counters we could roughly say:

Average Counter per Task: 5,353,331

Total Time: 150,000 ms

Preemption Frequency: 35.69 preemptions/ms

Note this average value takes into account all the handoff on tasks. To measure the context switching latency, we need a different approach.

Context Switch (Minimal) Latency (in cycles)

For measuring the context switching latency the following approach was used:

/* Two tasks at the same priority cooperatively yielding */
/* So we can measure the cycles in the context switch on the low-level scheduler */
/* Tick is 10 ms for less intrusion */

#include <application.h>
#include <kapi.h>

#define STACKSIZE 128

RK_DECLARE_TASK(task1Handle, Task1, stack1, STACKSIZE)
RK_DECLARE_TASK(task2Handle, Task2, stack2, STACKSIZE)

volatile ULONG counter1=0;
volatile ULONG counter2=0;

 VOID kApplicationInit(VOID)
 {
     kCreateTask(&task1Handle, Task1, RK_NO_ARGS, "T0", stack1, STACKSIZE, 1, RK_PREEMPT);
     kCreateTask(&task2Handle, Task2, RK_NO_ARGS, "T1", stack2, STACKSIZE, 1, RK_PREEMPT);

 }
 
 VOID Task1(VOID *args)
 {
     RK_UNUSEARGS
     while (1)
     {
         counter1++;
         kYield();
     }
 }

VOID Task2(VOID *args)
 {
     RK_UNUSEARGS

     while (1)
     {
         counter2++;
         kYield();
     }
 }

The PendSV was instrumented this way using the CoreDebug/DWT infrastructure:

(As NDEBUG is defined, FPU is disabled, and Stack Overflow checking is disabled - excerpts of the code dealing with checking, FPU extended frame and stack overflow checking mechanisms were removed for clarity):

.equ DWT_CYCCNT, 0xE0001004 /* Register Address for Data Watch Pointer - Cycle Counter*/
.global ctxtSwtchCycles
.global ctxtSwtchStart


global PendSV_Handler
.type PendSV_Handler, %function
.thumb_func
.text 
.align 4
PendSV_Handler:
    CPSID I
 /* ---- CYCCNT start ---- */
     LDR  R0, =DWT_CYCCNT
     LDR  R3, [R0]               /* R3 = start */
     LDR  R2, =ctxtSwtchStart
     STR  R3, [R2]
 /* this takes 8 cycles  */

    MRS R12, PSP

    STMDB R12!, {R4-R11}
    LDR R0, =RK_gRunPtr
    LDR R1, [R0]

    STR R12, [R1, #SP_OFFSET]     /* save PSP */
    STR LR, [R1, #LR_OFFSET]     /* save LR  */
    /* call scheduler */
    BL kSwtch


    /* load new task context */
    LDR R0, =RK_gRunPtr
     LDR R1, [R0]


    RESTORE:

    MOV R12, #RUNNING
    STR R12, [R1, #STATUS_OFFSET]

    /* get saved LR at TCB */
    LDR LR, [R1, #LR_OFFSET]

    /*  get saved PSP value */
    LDR R2, [R1, #SP_OFFSET]
    LDMIA R2!, {R4-R11}


 /* assign PSP */


    MSR PSP, R2
    DSB
 /* ---- CYCCNT end/delta ---- */
   LDR  R0, =DWT_CYCCNT
   LDR  R3, [R0]               /* R3 = end */
   LDR  R2, =ctxtSwtchStart
   LDR  R1, [R2]               /* R1 = start */
   SUB  R3, R3, R1             /* delta (wrap OK unsigned) */
   LDR  R2, =ctxtSwtchCycles
   STR  R3, [R2]
/* this adds  12 cycles */
/* CPSIE I and BX LR adds 2 cycles */
    CPSIE I
    BX LR

/* the ctxtSwtchCycles then must be ctxtSwtchCycles + 2 cycles (CPSI+BX LR) - 20 cycles (DWT math overhead) */

Results

The result is as follows:

Context Switch Latency is ~114 cycles

Note

@ 80MHz it means 1.42 microseconds.

Note this is pure-hardware context-switch latency

Important

  • This is a context switch minimal latency.
  • The worst-case scenario is application dependent.
  • Classicaly it is said that for a given task, the worst-execution time is when a task transitions to READY, when (all) higher priority tasks do so, and all them will run for their worst-case execution time. That is why flushing semaphores/cond vars/sleep queues, the choice for task periods and their relative phases must be handled carefully.
  • Furthermore, blocking and the anomalies both introduces (as priority inversion), nested interrupts, will all add up to a critical path that can or cannnot be predicted offline. **_

Tip

The design choices in RK0 (FIFO for same-priority with no time-slice, fully transitive priority inheritance mutexes, priority-driven >message-passing, all queues are enforced by priority, static tasks, word-aligned memory for data-exchange, the Most-Recent Message >Protocol, etc., are there to make this worst case to be easier to reason, while minimising anomalies.

Clone this wiki locally