[blog] A Time-Triggered Cooperative Scheduler Pattern

On a bare-metal system, if we have multiple periodic tasks to run we can choose to dedicate one hardware Timer to each. We may not have enough timers though. Another approach is to share a single Timer between multiple tasks by the means of a Scheduler [1].

Instead of triggering a specific task on a specific time interval, this timer will be responsible for generating a system tick. The scheduler keeps track of these ticks for each task, dispatching a task when it is due.

/*
@file sch.h
@brief Scheduler interface
*/
#ifndef SCH_H_
#define SCH_H_
#include <stdint-gcc.h>
#define NTASK 4 /*max number of tasks*/
/*task data structure */
typedef struct  
{
	void		(*pTask)(void); /*task signature is void Task(void)*/
	uint32_t	delay; /*initial delay*/
	uint32_t	period; /*period*/
	uint8_t		run;	 /*run flag*/
} Task_t;

/*
\brief Execute a pending task, if any
*/
void SCH_Dispatch(void);

/*
\brief Update each task deadline at each time tick.
*/
void SCH_Update(void);

/*
\brief  Add a task to the task pool. 
\param task    Function pointer to the task.
\param delay   Initial delay (tick numbers)
\param period  Task period (tick numbers)
*/
char SCH_Add(void (*task)(void), const uint32_t delay, const uint32_t period);

/*
\brief   Delete a task from the task pool.
\param task_index  Index of the task on the task array
*/
char SCH_Del(const int task_index);

/*
\brief Initialize the System Tick. Is hardware-dependent.
*/
void SCH_TickInit(void);
#endif /* SCH_H_ */

Below, the scheduler implementation. Note that while there is no task to run the CPU goes to sleep.

/*
@file sch.c
@brief Scheduler implementation
*/
#include "sch.h"
#include <atmel_start.h>
/*pool of tasks*/
static Task_t TaskPool[NTASK] = {0};

/*forward declarations*/
static void SCH_LowPowerOn(void);
static void SCH_LowPowerOff(void);	

void SCH_Dispatch(void)
{
	int index;
	for (index = 0; index < NTASK; index++)
	{
		if (TaskPool[index].run > 0) /*task is pending to run*/
		{
			SCH_LowPowerOff(); /*wake-up CPU*/
			TaskPool[index].pTask(); /*run it*/
			TaskPool[index].run -= 1;
			if (TaskPool[index].period == 0) /*if one-shot*/
			{
				SCH_Del(index);
			}
		}
	}
	SCH_LowPowerOn(); /*enable low-power*/
}

void SCH_Update(void)
{
	int index;
	for (index = 0; index < NTASK; index++)
	{
		/*there is a task?*/
		if (TaskPool[index].pTask)
		{
			if (TaskPool[index].delay == 0) /*ready to run?*/
			{
				TaskPool[index].run += 1;
				if (TaskPool[index].period)
				{
					/*schedule to run again*/
					TaskPool[index].delay = TaskPool[index].period;
				}
			}
			else /*decrement delay*/
			{
				TaskPool[index].delay -= 1;
			}
		}
	}
}

char SCH_Add(void (*task)(void), const uint32_t delay, const uint32_t period)
{
	int index=0;
	/*find a slot*/
	while ((TaskPool[index].pTask != 0) && (index < NTASK))
	{
		index++;
	}
	if (index == NTASK) /*array is full*/
	{
		return -1;
	}
	TaskPool[index].pTask = task;
	TaskPool[index].delay = delay;
	TaskPool[index].period = period;
	return index;
}

char SCH_Del(const int task_index)
{
	if (TaskPool[task_index].pTask == 0) return -1; //no task
	TaskPool[task_index].pTask = 0;
	TaskPool[task_index].delay = 0;
	TaskPool[task_index].period = 0;
	TaskPool[task_index].run	= 0;
	return task_index;
}

/*
 Hardware-dependent code. 
 Written for an Atmel Atmega328p MCU.
****/

/* 
 Generate a 4ms system tick for a 16MHz clock
*/
void SCH_TickInit(void)
{
	TCCR0A |= (1 << WGM01); 
	OCR0A = 0xF9;  
	TIMSK0 |= (1 << OCIE0A);
	TCCR0B |= (1 << CS02);
}

/*
 Put CPU on IDLE 
*/
static void SCH_LowPowerOn(void)
{
	cli();
	set_sleep_mode(SLEEP_MODE_IDLE);
	sleep_enable();
	sei();
	sleep_cpu();
}

/*
 Wake up CPU
*/
static void SCH_LowPowerOff(void)
{
	sleep_disable();
}

A simple main program using this scheduler:

#include <atmel_start.h>
#include <assert.h>
#include "sch.h"
#include "usart.h"
/*
 Tasks
*/
void Task1(void)
{
	usart_send_string("Task1\n\r");
}

void Task2(void)
{
	usart_send_string("Task2\n\r");
}

void Task3(void)
{
	usart_send_string("Task3\n\r");
}

void Task4(void)
{
	usart_send_string("Task4\n\r");
}

/*
 ISR for system tick
*/
ISR(TIMER0_COMPA_vect)
{
	SCH_Update();
}

int main(void)
{
	usart_init();
    /*add tasks*/
	assert(SCH_Add(Task1, 0, 1250) != -1); /*schedule to run every 5s*/
	assert(SCH_Add(Task2, 0, 1750) != -1); /*every 7s*/
	assert(SCH_Add(Task3, 0, 2750) != -1); /*every 11s */
	assert(SCH_Add(Task4, 2000, 0) != -1); /*one-shot, 8 sec delay */
	usart_send_string("Sys started\n\r");
   /*start system tick */
	SCH_TickInit();
   /*enable interupts*/
	sei(); 
	while (1)
	{
		SCH_Dispatch();
	}
}

Below, the tasks running. Time-stamps are printed by the terminal to validate tasks periodicity.

References

[1] Patterns for Time-Triggered Embedded Systems, Michael J. Pont

[2] Atmel Atmega328p Datasheet

[µblog] Generic pointers in C

Arithmetic step size of a pointer

A pointer is a variable that holds an address. So, if we declare int *ptr, we are saying that ptr is a variable that holds the address of an integer. Its arithmetic step size is the size of an integer. If we increment it as in ptr++ it would be incremented sizeof(int) bytes. If it was pointer to a byte, char *ptr, its arithmetic step size would be 1 byte. That is, the arithmetic step size of a pointer is the size of the type it points to. What about void* ptr?

Generic pointers

A pointer of type void* is said to be a generic pointer since it can point to any address. It cannot be dereferenced and it does not have an arithmetic step size, so we can’t perform pointer arithmetics on pointers to void.

Generic pointers are very handy to define generic functions that can accept a wide range of different pointers as their input arguments. An example [1]:

#include <stdio.h>
void print_bytes(void* data, size_t length) {
  char delim = ' ';
  unsigned char* ptr = data;
  for (size_t i = 0; i < length; i++) {
    printf("%c 0x%x", delim, *ptr);
    delim = ',';
    ptr++;
  }
  printf("\n");
}
int main(int argc, char** argv) {
 int a = 9;
 double b = 18.9;
 print_bytes(&a, sizeof(int));
 print_bytes(&b, sizeof(double));
 return 0;
}

Output:

 0x9, 0x0, 0x0, 0x0
 0x66, 0x66, 0x66, 0x66, 0x66, 0xe6, 0x32, 0x40

Inside the print_bytes function a pointer to unsigned char was used to iterate over every single byte on the input data memory address, since its arithmetic step size is 1 byte.

References

[1] Extreme C. AMINI, Kamran. Packt, 2019.

[blog] Observer Pattern using Sleep/Wake synchronization mechanism

The Observer Pattern (also known as Publish-Subscribe pattern) provides notification to a set of interested clients that relevant data have changed. The data-server does not need to have any a priori knowledge about its clients. Most commonly data are sent whenever new data arrive, but clients can also be updated periodically.

Figure 1. The Observer Pattern on an automotive software. Speed changes are pushed to a set of observer clients. [Automotive Software Architectures, Staron 2017]

It is a special case of the client-server style. In this blog I show a simple implementation of this pattern using the sleep/wake-up synchronization mechanisms designed for the kernel that has been presented. The goal is to use the machinery provided by the kernel – namely the multitasking and synchronization – to implement this very common pattern.

The publisher server does not know anything about the clients; the clients themselves will “subscribe” by going to sleep while new data do not arrive. The code below shows the SensorSet and SensorGet functions using the kernel calls to wake-up all listeners when new data is sensed, and to sleep while no new data arrive, respectively.

Let’s implement what is depicted on Figure 1.

/*
@file sensors.h
*/
#ifndef SENSORS_H_
#define SENSORS_H_
typedef struct Sensor Sensor_t;
/*Sensor number*/
#define SPEED 1
#define TEMP  2
#define FLOW  3
struct Sensor
{
	int		sensorData;
};
int SensorInit(Sensor_t* sensor);
void SensorSet(Sensor_t* sensor, int data);
int SensorGet(Sensor_t* sensor);
#endif /* SENSORS_H_ */
/*
@file sensors.c
*/
#include "kernel.h"
#include "sensors.h"
int SensorInit(Sensor_t* sensor)
{
	if (sensor != NULL)
	{
		sensor->sensorData = 0;
		return OK;
	}
	return NOK;
}
void SensorSet(Sensor_t* sensor, int data)
{
	if (sensor != NULL)
	{
		sensor->sensorData = data;
        /*wake-up all clients*/
		kCallWake(&sensor->sensorData);
	}
}
int SensorGet(Sensor_t* sensor)
{
	if (sensor != NULL)
	{
        /*sleep for new data*/
		kCallSleep(&sensor->sensorData);
		return sensor->sensorData;
	}
	return NOK;
}

To emulate data changes on a sensor, I will send data via keyboard through UART, following a simple APDU: [SensorNumber, NewValue, Return].

/*
 @file tasks.c
 this usage example shows an observer pattern for sensors
 using sleep/wake up synch mechanisms
*/
#include <stdio.h>
#include <assert.h>
#include "kernel.h"
#include "tasks.h"
#include "sensors.h"

SEMA_t rcvd; /*sema to signal publisher server*/
volatile char uartbuffer[3]; /*apdu buffer*/
volatile int norb = 0; /*n of rcvd bytes*/
volatile char rcvdbyte; /*rcvd byte*/
Sensor_t SpeedSensor; /*speed sensor*/

/*get data changes via uart
 Apdu={sensor type, new value, '\r'} 
*/
ISR(UART_Handler)
{
	__disable_irq();
	if((uart_get_status((Uart*)USART_SERIAL) & UART_SR_RXRDY) == UART_SR_RXRDY)
	{
		uart_read((Uart*)USART_SERIAL, &rcvdbyte);
		uartbuffer[norb] = rcvdbyte;
		norb++;
		if (rcvdbyte == '\r') 
		{
			kSemaSignal(&rcvd); /* signal publisher */
			norb=0;
		}		
	}
	__enable_irq();
}

/*publisher server*/
/*highest priority task*/
void PublisherServer(void* args)
{	
	assert(SensorInit(&SpeedSensor) == OK);
	kCall2(SEMAINIT, &rcvd, 0); /*kcall to init sema*/
	
	while(1)
	{
		WAIT(&rcvd);
        /*wake subscribers*/
		if (uartbuffer[0] == SPEED)
		{ 
			printf("Speed sensor changed\n\r");
			SensorSet(&SpeedSensor, (int)uartbuffer[1]);
		}
	    else
        {
			printf("No route\n\r");
		}
	}
}
/* windshield client*/
void WindshieldClient(void* args)
{
	int value;
	while(1)
	{
        /*Sleep until new data*/
		value = SensorGet(&SpeedSensor);
		printf("Windshield notified. Speed: %d\n\r", value);
        /*do some processing*/
	}
}

/* radio volume client */
void RadioClient(void* args)
{
	int value;
	while(1)
	{
		value = SensorGet(&SpeedSensor);
		printf("Radio notified. Speed: %d\n\r", value);
        /*do some processing*/
	}
}

/*lane departure client*/
void LaneDepartureClient(void* args)
{
	int value;
	while(1)
	{
		value = SensorGet(&SpeedSensor);
		printf("Lane departure notified. Speed: %d\n\r", value);
        /*do some processing*/
	}
}

Note that the SensorGet methods called on the client tasks are not polling or busy-waiting the sensors. They put the tasks to sleep, so they wake up when new data arrive.

The figure below shows the outputs on the PC screen.

Figure 2. The implementation running