Embedded Programming Pattern: cooperative kernel

Increase your application's performance using the kernel. Contrary to common belief, even a cooperative kernel provides excellent real-time performance, when used in the right way.

BeRTOS provides a powerful kernel module, which by default is configured to run in cooperative mode. Kernel development has always privileged minimalism and compactness. Using BeRTOS kernel with all optional modules activated (semaphores, signals, messages, etc...) it takes up just a few kilobytes of flash memory.

Performance

Contrary to common belief, even a cooperative kernel provides excellent real-time performance, when used in the right way. Another myth to be exploded is that with a cooperative kernel you must fill your code with sleep to let other processes run.

This may be true in case of processes that use intensely the CPU, but in embedded programming the vast majority of processes are I/O bound, that is they wait input from peripherals.

This happens also in our example: temperature and voltage must be read once every X seconds, when saving/loading parameters we need to wait a keyboard press and we must communicate with a memory, and regarding motor protection, we're always waiting it to set off.

So, it's enough to make sure that drivers that communicate with hardware release the CPU when they are waiting and problem solved. All BeRTOS drivers already do this, so there's no problem.

Implementation

Let's see how we can use the kernel. First, we need to divide the application in parallel and separated execution paths, that we call processes in BeRTOS. It seems natural to use four processes for our example: one to manage temperature levels, on to control the voltage, one to load and save and one for motor protection.

Each process should be thought as it was the only one to run on the CPU, so if we want it to run forever we must use an infinite loop.

This is how we can code temperature and voltage control processes:

void temp_handler(void)
{
    while (1)
    {
        /* Check current temperature state. */
        temp = adc_read(TEMP_CH);
        temp *= T_SCALE + T_OFFSET;
        temp_check(temp);
        prm.temp = temp;
        timer_delay(TEMP_PERIOD);
    }
}

void voltage_handler(void)
{
    while (1)
    {
        /* Acquire and check voltages */
        v = adc_read(VOLT_CH);
        v = v * V_SCALE + V_OFFSET;
        valve = voltage_check(v);
        /* Update output */
        setvalve(valve)
        prm.valve = valve;
        timer_delay(VOLTAGE_PERIOD);
    }
}

As you noticed, there's a timer_delay() at the end of each process, that let the processes execute just once every TEMP_PERIOD or VOLTAGE_PERIOD milliseconds. This way we have the same result as the previous solutions.

Naturally, both timer_delay() and adc_read() functions pass the control to other processes when waiting, so, as I said, even if the kernel is cooperative, we don't need to explicitly release the CPU.

I would like to make a premise regarding save/load and motor protection processes. If we want to get maximum performance and waste the least CPU time as possible, we need to provide interrupt handlers on inputs that wake up both processes. So save/load buttons and the motor alarm pin must trigger interrupts. Of course, polling is always possible, but we don't get the same performance. We need two interrupts: one is generated when pressing a button on the keyboard (for save/load buttons), the other is generated when motor protection sets off. Then it's enough to use BeRTOS' kernel signals to efficiently connect these events.

Let's see first how do we code the ISR that handle interrupts:

void keyboard_isr(void)
{
    /*Read key*/
    .....


    /* Signal load/save process */
    sig_signal(loadsave_proc, SIG_KEYBOARD);
}



void motor_isr(void)
{
    /* Signal motor process */
    sig_signal(motor_proc, SIG_MOTORALARM);
}

Now we can easily write the processes that handle saving/loading and motor protection:

void loadsave_handler(void)
{
    while(1)
    {
        /* Wait for a keypress */
        sig_wait(SIG_KEYBOARD);

        Parameter prm_cpy;

        /* Handle parameter save/load */
        if (load_pressed())
        {
            /* Load */
            load_preset(&prm_cpy);
            memcpy(&prm, &prm_cpy, sizeof(prm));
        }

        if (save_pressed())
        {
            memcpy(&prm_cpy, &prm, sizeof(prm));
            /* Save */
            save_preset(&prm_cpy);
        }
    }
}


void motor_handler(void)
{
    while(1)
    {
        /* Wait for an alarm signal */
        sig_wait(SIG_MOTORALARM);

        /* Check motor alarm */
        a = adc_read(SPEED_CH);
        a *= SPEED_SCALE;
        motor_setSpeed(a);
    }
}

The function sig_wait() is the heart of these processes; it ensures that processes wait until the indicated signal arrives. When the signal is raised, the process executes then it waits again. This method is very efficient because while the process is waiting, the CPU is automatically released to other processes; you don't need to do that explicitly.

I'd like to stress the fact that, even if the various processes use shared data structures (the prm struct) there's no need for complicated access handling code.

This works because, being the kernel cooperative, it's easy to forecast when a context switch will happen, so often there's no need to use locks or semaphores to protect shared data structures from concurrent access. In both temp_handler() and voltage_handler() we access the prm struct without problems, since we are sure that every operation can't be interrupted by other processes.

On the other hand, the process loadsave_handler() works on a copy of the struct. This is because the functions load_preset() and save_preset() converse with a memory, so they could potentially release the CPU in the middle of an operation, resulting in an incoherent load/save operation. To avoid this problem, it's enough and not so expensive to work on a struct's copy on the stack.

To end our example, let's see how to start the four processes:

/* Allocate process stacks */
PROC_DEFINESTACK(temp_stack, KERN_MINSTACKSIZE);
PROC_DEFINESTACK(voltage_stack, KERN_MINSTACKSIZE);
PROC_DEFINESTACK(loadsave_stack, KERN_MINSTACKSIZE);
PROC_DEFINESTACK(motor_stack, KERN_MINSTACKSIZE);

Process *temp_proc;
Process *voltage_proc;
Process *loadsave_proc;
Process *motor_proc;

void init(void)
{
    /* Init timer*/
    timer_init();
    /* Init kernel */
    proc_init();

    /* init other modules */
    
    ...

    /* Create processes */
    temp_proc     = proc_new(temp_handler, NULL, temp_stack, sizeof(temp_stack));
    voltage_proc  = proc_new(voltage_handler, NULL, voltage_stack, sizeof(voltage_stack));
    loadsave_proc = proc_new(loadsave_handler, NULL, loadsave_stack, sizeof(load_save_stack));
    motor_proc    = proc_new(motor_handler, NULL, motor_stack, sizeof(motor_stack));

    /* Raise priority */
    proc_setPri(motor_proc, HIGH_PRIORITY);
}

void main(void)
{
    init();
    while (1)
    {
        monitor_report();
        timer_delay(1000);
    }
}

Let's analize what we did: first we declared a memory area to use as stack for each process. This is the main difference when using the kernel: each process needs some memory to be used as private stack.

The init() function is easy: we initialize drivers and we create processes. The only important thing to point out is the rise of priority of the motor protection process. Normally, all processes are created with the same standard priority. Using proc_setPri() this priority can be changed. When more processes are ready to run, the kernel chooses the one with the highest priority.

The main() functions is even easier: it just calls init() and then it remains idle, reporting about memory usage once each second. monitor_report() is a function provided by BeRTOS' kernel that prints on debug serial information about memory usage of each process, so it's possible to verify if the memory we assigned them is enough.

The question that often arises is: how much real time is this solution? The simple answer is: often enough.

If processes don't use the CPU too much (and in many embedded projects this is the case), a high priority process is run within tens/hundredths microseconds.

Next time we'll see how to serve a high priority process with a fixed delay.

Written by Francesco Sacchi in programming the 19 July 2010. , tag programming patternsembedded programmingkernel
 Download as PDF