This code is something that I have used for about 3-4 simple embedded systems projects.
The idea is something like this:
- This is non-OS based implementation, everything is done in an outer infinite loop. No dynamic memory involved or advised.
- It's an interrupt-driven system, polling for peripherals is not used anywhere.
- Peripherals like Timer, UART, I2C, etc loads a particular value based on the event, in a Queue-like variable.
- Non-trivial tasks like blinking an LED is done in a common
Idle
task. - Dispatcher loads the function handlers based on the event added in the queue.
- (UPDATE) All read/write activities on CPU registers are managed in the respective Interrupt Service Routine. Adding the event in queue is possibly the last thing the code does in the ISR.
Enum for possible events:
typedef enum {
TASK_EMPTY = 0,
TASK_INITIALIZE = 1,
TASK_IDLE,
TASK_SOMETHING_1,
TASK_SOMETHING_2,
TASK_SOMETHING_3,
TASK_SOMETHING_4,
TASK_SOMETHING_N,
MAX_TASKS,
TASK_INVALID = -1,
} eTasks_t ;
A constant array which contains the event mapped to the handler:
const stTask_t GlobalTasksList[MAX_TASKS] =
{
{ TASK_EMPTY, Placeholder },
{ TASK_INITIALIZE, SystemInitialization },
{ TASK_IDLE, Idle },
{ TASK_SOMETHING_1, Handler_1 },
{ TASK_SOMETHING_2, Handler_2 },
{ TASK_SOMETHING_3, Handler_3 },
{ TASK_SOMETHING_4, Handler_4 },
{ TASK_SOMETHING_N, Handler_N },
};
I talked about a Queue
:
eTasks_t Queue[MAX_TASKS];
eTasks_t qTasksToAdd[MAX_TASKS * 2];
There is a structure which binds an event and it's handler together:
typedef struct stTask {
eTasks_t TaskID;
bool (*handler) (void); // Would return true for success, and false for failure
} stTask_t;
stTask_t Task;
A Dispatcher
function will take care of queued events:
void Dispatcher(void)
{
while(1)
{
// Assign the task handler
Task.handler = GlobalTasksList[Task.TaskID].handler;
//Invoke handler
if(FAILURE == Task.handler())
{
//Something went wrong with handler, Error!
// Error handling
}
//Does the task needs a retry, or is a recurring task?
// Reload if yes, else discard and load the next
if(bTaskRetry == FALSE)
{
Queue_DiscardTask0();
}
//Assign the 0th task from Queue
Task.TaskID = Queue[0];
}
}
Let's look at an example of adding the event in the Queue from an ISR:
#pragma vector=PORT2_VECTOR
__interrupt void PORT2_Interrupt(void)
{
// Clear interrupt
CLR_BIT(P2IFG, 2); //P2IFG is a CPU register which indicates the Port pin that has generated an interrupt.
// Check if the queue is overflowing
if(qIndexTasksToAdd < MAX_TASKS-1)
{
// Add event
qTasksToAdd[qIndexTasksToAdd++] = TASK_SOMETHING_1;
}
}
As of now, this strategy works fine without any questionable latency. I have used the same skeleton for systems that could have as many as 20 events lined-up, and it works as per requirement to say the least.
Another advantage of this implementation that I felt is in regards with the efficiency this has when I am debugging an issue. Everything is routed through the handler/dispatcher, so I precisely know where to have a debug point/print setup.
The concern that I have with this implementation is the fixed memory footprint is has. For example, this won't be memory efficient for a LED blinking project.
Any thoughts where I could improve on this?