Arm Cortex-M Context Switching Part 3

In this example we are going to use the PendSV system handler to execute the task switching logic, rather then calling it directly in timer_func(), the effect of this is that the PendSV handler is called after any other exceptions are handled. This allows more critical exceptions to be handled before the “slower” task switch logic is triggered.

In this example we are going to only trigger the task switch once every 100 timer ticks, by deferring execution of the task switching logic, the timer_func() function now only has to monitor and decrement the state of a static variable, which means that the time spent in the timer_func() function is much reduced.

Example code can be cloned from BitBucket

To use the PendSV handler modify vector_table.s as follows:

.syntax unified

.cpu cortex-m3
.thumb

/* Vector Table */

.word 0x20001000        /* Stack Pointer */
.word reset_handler     /* 1 Reset Handler */
.word default_handler   /* 2 NMI */
.word default_handler   /* 3 Hard Fault */
.word default_handler   /* 4 Mem Management Fault */
.word default_handler   /* 5 Bus Fault */
.word usage_handler     /* 6 Usage Fault */
.word default_handler   /* 7 RESERVED */
.word default_handler   /* 8 RESERVED */
.word default_handler   /* 9 RESERVED*/
.word default_handler   /* 10 RESERVED */
.word default_handler   /* 11 SVCall */
.word default_handler   /* 12 Debug Monitor */
.word default_handler   /* 13 RESERVED */
.word task_switch       /* 14 PendSV */
.word timer_func        /* 15 SysTick */

.global reset_handler
.type reset_handler, STT_FUNC
.thumb_func
reset_handler:
    bl main
halt:
    bl halt

.global default_handler
.type default_handler, STT_FUNC
.thumb_func
default_handler:
    bl default_handler

.global usage_handler
.type usage_handler, STT_FUNC
.thumb_func
usage_handler:
    bl usage_handler

Now modify timer_func() in timer.c as follows:

#include <stdint.h>
#include <task.h>

#define SYSTICK_BASE ((uint32_t *) 0xe000e000)
#define SYSTICK_CTRL ((uint32_t *) 0xe000e010)
#define SYSTICK_TRELOAD ((uint32_t *) 0xe000e014)
#define SYSTICK_CTRL_ACTIVATE (0x7)

void timer_start()
{
        *SYSTICK_CTRL = SYSTICK_CTRL_ACTIVATE;
}

void timer_stop()
{
        *SYSTICK_CTRL = 0x0;
}

void timer_reload(uint32_t reload)
{
    *SYSTICK_TRELOAD = reload;
}

__attribute__((interrupt)) void timer_func()
{
    /* The timer_func gets called at approx 100
       times per second, we only do a task switch
           per second */

    static uint32_t no_task_switch = 100; 

    if(no_task_switch) {
        no_task_switch--;
    } else {    
        /* Trigger the pendSV */
        *((uint32_t volatile *)0xE000ED04) = 0x10000000;
        
        /* Reset the counter */
        no_task_switch = 100;
    }
}

The above code now will only trigger the PendSV handler every 100 timer ticks, the value 0xE000ED04 represents the address of the System Control Block Register  Interrupt Control and State (INTCTRL) and the value 0x10000000 is the PENDSVSET bit (Section: 3.5 System Control Block (SCB) Register Descriptions: from the datasheet)

We also need to change the exit behaviour of task_switch() in task.c as follows:

__attribute__((naked)) void task_switch()
{
    /* Get PSP */
    asm volatile("mrs r0, psp");
    
    /* Is it non zero */
    /* then save state */
    asm volatile("cmp r0, #0");
    asm volatile("it ne");
    asm volatile("blne task_save");
        
    struct task_t *task_cur = (struct task_t *) 0;
    struct task_t *task_next = (struct task_t *) 0;
    
    task_cur = task_running();
    task_next = task_ready();

    if(task_next && task_cur) {
        task_cur->state = TASK_READY;
    }
    
    if(task_next) {
        task_next->state = TASK_RUNNING;
        task_restore(task_next->sp);
        /* Never return from here */
    }

    return;
}

We change main.c as follows, now rather than using an arbitary value for the TIMER_RELOAD value we use a value derived from the fact that the QEMU emulator runs a system clock of 12MHz, this value has been tweaked slighty to be more accurate on my particular setup, you may have to calibrate this value as well:

#include <stdint.h>
#include <task.h>


/* QEMU emulates a 12Mhz system clock */
/* So 0x00b71b00 in theory would be used */
/* However the following value is more accurate on my system YMMV */

#define SYSTICKS_PER_SECOND ((uint32_t) 0x00c00000)
 
#define TIMER_RELOAD (SYSTICKS_PER_SECOND / 100)

void task_1(void)
{
    while(1) {
        puts("TASK_1\n");
    }
}

void task_2(void)
{
    while(1) {
        puts("    TASK_2\n");
    }
}

void task_3(void)
{
    while(1) {
        puts("        TASK_3\n");
    }
}

int main() 
{
    task_init();
    task_create(task_1);
    task_create(task_2);
    task_create(task_3);
    
    timer_reload(TIMER_RELOAD);
    timer_start();

    while(1) { ; }

    return 0;    
}

Running make run and make debug as per the previous examples you should now see the output from the tasks running.

 

 

Advertisements