A number of skeleton templates for embedded systems

On this page is a number of generic task constructions usefull in embedded systems

The is hold on a generic level but is easy convertable to krnl, FreeRtos, … There are some examples in bewteen written for krnl. You can easily get them up and running.

1. Fixed samplingsfrequency (task synchronized by timed semaphore)

In many systems it is critical to have a rock stable samplingsfrequency for obvious purposes (control, signal processing, …)

  • endless loop where you are wairting by a semaphore which is signalled by the kernel with strict regular intervals

  • if you come to late the signal will be accumulated. Please note limit imposed by max limit set in k_crt_sem

  • Task shall normally be among the higheste prioritized on a preemptive kernel so

  • Task will get the CPU when it is started

  • If you dont have highest task priority you might be delay in execution bq the CPU is taken by some one else.

  • Task can easy check if its behind (there is already a signal waiting at the semaphore when the task enter the wait call in the top of the loop)

  • if behind k_wait returns 1. If we have been waited it returns 0. If timeout (no signal) it returns < 0.


#include <krnl.h>
struct k_t *pSem, *pTask;

#define STK 100
char stak[STK];

void doSamplingAlgorithmsAndControl() {
  static boolean light = false;
  k_eat_time(5); // to simulate code running

  if (light) { // debug :-) blinking
    light = false;
    digitalWrite(13, HIGH);

  }
  else {
    light = true;
    digitalWrite(13, LOW);
  }
}

void task()
{

  k_set_sem_timer(pSem, 1000); // let OS send a signal to semaphore every 1000th kernel timer tick

  while (1) {
    k_wait(pSem, 0); // semaphore: pSem. 0: we will wait forever if no signal comes
    doSamplingAlgorithmsAndControl();
  }
}

void setup()
{
  pinMode(13, OUTPUT); // for debugging only
  k_init(1, 1, 0); // 1 task, 1 semaphore, 0 message boxes
  pSem = k_crt_sem(0, 5); // startvalue 0. Clipvalue 5 meaning only 5 "signals" can be accumulated
  pTask = k_crt_task(task, 10, stak, STK);
  k_start(1);
  //  coming here ?? then something went wrong
  // check k_err_cnt - counts nouber of errors in init of system
}

void loop() {}

2. Tasks sharing data protected by a critical region (semaphore)

We have two independent tasks:

  • task1 is saving data in a shared variable struct data

  • task2 is retrieving data from the same struct

  • we do protect the data so only one task at a time can access the data

  • priority is in this example no issue

  • A low prioritized task lowTask and high prioritized task controlTask

  • We are using a critical region protected the shared data

  • We are using a standard semaphore to ensure atomic access to the shared data

  • controlTask is running on a fixed samplingsfrequency

  • lowTask is running now and then and might be looping faster than controlTask.

  • We upcount a shared variable every time we deliver data so controlTask can see if any updates has been lost

  • Important rule is minimize your time in critical as much as possible

  • Pittfall

    • controlTask might wait on the critical region because lowTask is inside the region

  • lowTask can ofcourse also pick up data instead of delivering data - see next example

#include <krnl.h>

struct k_t *sem, *mutexSem, *pT1, *pT2;

#define STK 100

char stakH[STK], stakL[STK];


// HOW TO DEBUG ??? :-)
struct dataTp {
  int i, j, k;
};

struct dataTp data;


void initData() // must be called bef krnl is started
{
  data.i = data.j = data.k = 33;
}

void lowTask()
{
  int a, b, c;
  while (1) {

    k_wait(mutexSem, 0);   // wait forever if needed
    // >>>>>>> enter critical region
    data.i++ ; data.j--; data.k; data.k = data.i + data.j;
    // <<<<<<< leaving critical region
    k_signal(mutexSem);

    k_sleep(100);
  }
}

void controlTask()
{
  int mya, myb, myc;

  k_set_sem_timer(sem, 10); // let krnl send a signal to semaphore every 10th kernel timer tick

  while (1) {
    k_wait(sem, 0); // wait forever if ...

    k_wait(mutexSem, 0); // = so wait forever - might be dangerous
    {
      // >>>>>>> enter critical region
      mya = data.i; myb = data.j; myc = data.k;
      // <<<<<<< leaving critical region
    }
    k_signal(mutexSem);

    // do control stuff here with new data :-)
    k_eat_time(5); // fake for doSamplingAlgorithmsAndControl();
  }
}
void setup()
{
  initData();

  k_init(2, 2, 0); // 2 tasks 2 semaphores
  sem = k_crt_sem(0, 10);
  mutexSem = k_crt_sem(1, 10);

  pT1 = k_crt_task(controlTask, 10, stakH, STK);
  pT2 = k_crt_task(lowTask, 20, stakL, STK);

  k_start(1);

}

void loop() {}


3. Critical region now by ceiling protocol

The example is based on the previous example. The ceiling protocol is a way to avoid starvation of high pririty tasks..

  • We do use a mutex (mutual exclusion) primitive instead of a semaphore.

  • It is basicly the same example but just show how you use a mutex call.

  • It is a semaphore in behind but when you enter the region your priority is set to the value given in k_mut_ceil_set

  • And you revert to your previous priority when leaving the region

  • If your incoming priority is higher than the ceiling priority for the region just dont get the region

  • So check for access like in the example

Example

  • task LowPrio and taskl highPrio do access shared data which is located in a semaphore protected region.

  1. lowPrio task enter critical region using k_wait(xxx,)

  2. highPrio task starts and get cpu due to high priority

  3. higPrio try to enter critical region by k_wait(xxx, ) but must wait until lowPrio leaves region

  4. Now a mediumPrio task start and runs continuous in 30 seconds. So in these 30 seconds lowPrio task has to wait until CPU is free eg mediumPrio tasks stop

  5. lowPrio tasks continue in critical region finalise duty and leve a free critical region by k_signal(xxx)

  6. Now highPrio will immediatly enter critical region, do the proper work and leaved the region

CONCLUSION: highPrio was waiting on lowPrio Task

The ceiling solution

  1. lowPrio is in the critical region

  2. highPrio try to enter region by k_wait(xxx,) but has to wait

  3. Now the kernel upgrade lowPrio task to have highPrio tasks priority until lowPrio leaves critical region

  4. So lowPrio is temporary boosted and the medumPrio task will not get cpu

CONCLUSION: works better :-)

But

  • A set of mutex call can normally detect if they are paired (an enter should be together with a leave

  • Inheritance or ceiling protocols might be part of the mutex. The two protocols try to maximize real time behavior og reduce or remove deadlock

  • The krnl example uses the mutex calls which also implement immediate priority ceiling protocols when a ceiling priority is added prior to k_start wirh k_mut_ceil_set.

  • So run it without setting ceiling and the code runs just like using pairs of wait and signal

  • Set ceiling prority by k_mut_ceil_set and it will be a immediate ceiling protocol.

  • NB NB NB: if running ceiling protocol a task with priority HIGHER than the ceiling priority will not get the critical region and return value CEILINGFAIL (pt -3)

  • So as usual - dont do as me - please check your return values

#include <krnl.h>

struct k_t *sem, *mutexSem, *pT1, *pT2;

#define STK 100

char stakH[STK], stakL[STK];


// HOW TO DEBUG ??? :-)
struct dataTp {
  int i, j, k;
};

struct dataTp data;


void initData() // must be called bef krnl is started
{
  data.i = data.j = data.k = 33;
}

void lowTask()
{
  int a, b, c;
  while (1) {

    k_mut_ceil_enter(mutexSem, 0); // instead of k_wait(mutexSem, 0);   // wait forever if needed
    // >>>>>>> enter critical region
    data.i++ ; data.j--; data.k; data.k = data.i + data.j;
    // <<<<<<< leaving critical region
    k_mut_ceil_leave(mutexSem); // instead of k_signal(mutexSem);

    k_sleep(100);
  }
}

void controlTask()
{
  int mya, myb, myc;

  k_set_sem_timer(sem, 10); // let krnl send a signal to semaphore every 10th kernel timer tick

  while (1) {
    k_wait(sem, 0); // wait forever if ...

    k_mut_ceil_enter(mutexSem, 0); // instead of     k_wait(mutexSem, 0); // = so wait forever - might be dangerous
    {
      // >>>>>>> enter critical region
      mya = data.i; myb = data.j; myc = data.k;
      // <<<<<<< leaving critical region
    }
    k_mut_ceil_leave(mutexSem); // instead of     k_signal(mutexSem);

    // do control stuff here with new data :-)
    k_eat_time(5); // fake for doSamplingAlgorithmsAndControl();
  }
}
void setup()
{
  initData();

  k_init(2, 2, 0); // 2 tasks 2 semaphores
  sem = k_crt_sem(0, 10);
  mutexSem = k_crt_sem(1, 10);
  k_mut_ceil_set(mutexSem,8); // Set ceiling priority

  pT1 = k_crt_task(controlTask, 10, stakH, STK);
  pT2 = k_crt_task(lowTask, 20, stakL, STK);

  k_start(1);

}

void loop() {}


4. Critical region .. wont wait invariant

The example is based on example 2

The high prority control Task use a wait-check call to see if the critical region is vacant and if so then take it.

Drawback is

  • controlTask might never enter the region

  • could have been solved by ceiling as shown just above …

  • So high priority control task will not be delayed but

  • might never (very bad luck ) enter the critical region

#include <krnl.h>

struct k_t *sem, *mutexSem, *pT1, *pT2;

#define STK 100

char stakH[STK], stakL[STK];


// HOW TO DEBUG ??? :-)
struct dataTp {
  int i, j, k;
};

struct dataTp data;


void initData() // must be called bef krnl is started
{
  data.i = data.j = data.k = 33;
}

void lowTask()
{
  int a, b, c;
  while (1) {

    k_wait(mutexSem, 0);   // wait forever if needed
    // >>>>>>> enter critical region
    data.i++ ; data.j--; data.k; data.k = data.i + data.j;
    // <<<<<<< leaving critical region
    k_signal(mutexSem);

    k_sleep(100);
  }
}

void controlTask()
{
  int mya, myb, myc;

  k_set_sem_timer(sem, 10); // let krnl send a signal to semaphore every 10th kernel timer tick

  while (1) {
    k_wait(sem, 0); // wait forever if ... for sampling

    if (0 <=   k_wait(mutexSem, -1) ) // = so no wait  - might be dangerous bq you might miss region
    {
      // >>>>>>> enter critical region
      mya = data.i; myb = data.j; myc = data.k;
      // <<<<<<< leaving critical region
      k_signal(mutexSem);
    }


    // do control stuff here with new data :-)
    k_eat_time(5); // fake for doSamplingAlgorithmsAndControl();
  }
}
void setup()
{
  initData();

  k_init(2, 2, 0); // 2 tasks 2 semaphores
  sem = k_crt_sem(0, 10);
  mutexSem = k_crt_sem(1, 10);

  pT1 = k_crt_task(controlTask, 10, stakH, STK);
  pT2 = k_crt_task(lowTask, 20, stakL, STK);

  k_start(1);

}

void loop() {}


5. wait-signal construction from interrupt to a task

Systems often interact with the enviroment.

You can

  • check the environment for state of the world and act upon than

  • you wait and by started or triggerede when something happens (interrupt)

It might be more easy just to let your task sleep and then be started by an interrupt.

Normally and interrupt is pulling a pin on the PCU high or low (depending on configuration)

The code is for an Arudino UNO and a MEGA. Take a look in the initISR function and the head of the interrupt servcie routine

6. Message passing or buffering

We use a message buffering system - sometimes called a ringbuffer system

Some normal characteristics for a message system

  • has internal buffering of the messages

  • No of elements in the buffer and their size is set by you

  • you have to come with the memory for the buffer (just a char array)

  • if buffer is full you cant deliver any messages - so check your return code (negative if no succes)

  • not like a critical region - you just deliver data

The code is a little complicated :-)

  • This version for an UNO due to integration of interrupt

  • ISR installed on pin 2. Its normally HIGH due to pull up.

  • Activate interrupt by pulling pin2 to to ground

  • ISR (interrupt service routine) will send an integer to message buf named pMsg (ki_send)

  • task t2 is waiting on data coming to this buffer in k_receive

  • task t2 will just forward the received integer to another messagebuf named pMsg2

  • task t1 is waiting on pMsg2 message buffer and will blink with LED upon receiving.

SO here we have a demonstration of simple message passing and integration of interrupt..

The interrupt routine could be for handling a serial prot, a network interface etc. The idea is fast efficient service of the hardware by the interrupt and then a little later the rest of the work carried out by a task. In this way we can ensure short interrupt time which is nice because when we are in the interrupt routine we normally has disabled the interrupt and therefore blocking for all other interrupts that might occur.


#include <krnl.h>

// External triggered ISR
// An Interrupt Service Routine is attached to pin 2
// So when pin2 is drived to ground (by a wire) an interrupt is generated.
// The ISR increment a counter and send it to a message Q
// naming ki_send .... "i" indicates it can be used in an ISR and demand interrupt to be disabled prio to call
// and that no task shift takes place in the call
// demonstrates ISR with  message Q and preemption(task shift) in the ISR
// NB Take a look on the ISR. For 1280 and 2560 it is INT4 but for 168,328,.. it's INTO
// It is taken care by a compile flag
// (c) beerware license JDN 2013

struct k_t * tSm1, *tSm2;
struct k_t * p_t1, *p_t2;

struct k_msg_t *pMsg,*pMsg2;
char mar[10*2];
char mar2[10*2];

#define STK_SIZE 200

char s1[STK_SIZE]; // stak for t1 ... and t2
char s2[STK_SIZE];

volatile int icnt=0;


void doBlink(void) {
  static char flag = 0;
  flag != flag;
  digitalWrite(13,flag);
}

void t1(void) {
  int i;
  while (1) {
    delay(100);
    if (0 <= k_receive(pMsg2,&i,-1,NULL) ) {
      doBlink();
    }
  }
}

void t2(void) {
  int i;
  i = 0;
  while (1) {
    if (0 <= k_receive(pMsg,&i,10,NULL) ) {
      Serial.println(i);
      k_send(pMsg2,&i);
    }
    else {
      Serial.println("-1");
    }
  }
}

#if defined (__AVR_ATmega2560__) || defined (__AVR_ATmega1280__)
ISR(INT4_vect,ISR_NAKED) {
#else
ISR(INT0_vect, ISR_NAKED) {
#endif
  // no local vars ?!?  ! I think
  PUSHREGS();
  if (!k_running)
    goto exitt ;

  icnt++;
  ki_send(pMsg,(void *)&icnt);

  K_CHG_STAK();

exitt:

  POPREGS();
  RETI();
}

void installISR2()
{
  DI();
  pinMode(2,INPUT);  // set som input
  digitalWrite(2,HIGH);  // enable pullup resistor

#if defined (__AVR_ATmega2560__) || defined (__AVR_ATmega1280__)
  // 1280/2560 mega pin2 intr:4, pin5 intr:5
  EIMSK |= (1 << INT4);  // enable external intr
  EICRB |= (1 << ISC41); // trigger INT4 on falling edge
#else
  EIMSK |= (1 << INT0);  // enable external intr
  EICRA |= (1 << ISC01); // trigger INT0 on falling edge
#endif

  EI();
}

void setup() {

  Serial.begin(9600);

  k_init(2,5,2); // from now you can crt task,sem etc

  tSm1 = k_crt_sem(0,10); //
  tSm2 = k_crt_sem(0,10); //
  p_t1 = k_crt_task(t1, 10, s1, STK_SIZE);
  p_t2 = k_crt_task(t2, 9, s2, STK_SIZE);


  // Ringbugffer init. You have to come with date dataarea for the ringbuffe.
  //mar and mar2 is each an array with 10 slots of 2 bytes
  pMsg = k_crt_send_Q(10,2,mar); // create a circular buffer with 10 elements each of size 2 byte
  pMsg2 = k_crt_send_Q(10,2,mar2); // and one more
  pinMode(13,OUTPUT);
  Serial.print("start ");
  Serial.println(KRNL_VRS);
  delay(2000);

  installISR2();
  Serial.println("bef gogo");

  k_start(10); // now we are runnning   with timer 10 msev
  Serial.println("shit - should not come here");

  // main will not come back and will sleep rest of life
}

void loop(void) {/* just for compilation - will never be called*/
}

/* QED :-) */