WelcomeUser Guide
ToSPrivacyCanary
DonateBugsLicense

©2024 Poal.co

285

Embedded state machine alternative: a simple tasker.

Most embedded C projects I see manage the execution of their application like this:

void main() { for(;;){ switch(mode){ case this: do_that(); break; case that: do_this(); break; case the_other_thing: farts(); break; } } }

An alternative I've been playing around with lately is using a simple task engine. Simple tasker is a task type, a data structure of tasks, and tasks. It is not preemptive, it is about as simple as it can get.

Start with prototypes of the tasks:

uint8_t task0(void); uint8_t task1(void); uint8_t task2(void); uint8_t task3(void); uint8_t task4(void);

An enumeration is needed to provide a task ID for each task:

enum TaskEnum{ TASK0, TASK1, TASK2, TASK3, TASK4, TOTAL_TASKS }taskEnum;

A task pointer type is defined:

typedef uint8_t (* taskPtr_t)(void);

Task data structure type is defined

typedef volatile struct Task_t{ bool exec; uint8_t priority; uint8_t task_id; taskPtr_t task_ptr; }TASK_T;

Number of priority levels is defined:

const uint8_t maxPriority = 3; // or use a preprocessor define, whatever I don't give a shit

The structure containing task list {exec, priority, task_id, task_ptr}:

TASK_T taskList[TOTAL_TASKS] = { {0, 0, TASK0, task0}, {0, 1, TASK1, task1}, {0, 1, TASK2, task2}, {0, 1, TASK3, task3}, {0, 1, TASK4, task4}, };

A couple of global vars are used to keep track of things:

volatile uint8_t taskCounter = 0; volatile uint8_t currentPriority = 0;

Each task returns a uint8_t:

uint8_t task_ret;

Your main loop only needs to consist of the "tasker engine"

void main(void) { for(;;) { while(currentPriority < maxPriority) { if((taskList[taskCounter].priority == currentPriority) && taskList[taskCounter].exec) { // Reset awaiting exec taskList[taskCounter].exec = false; // Execute task and get error code task_ret = (*taskList[taskCounter].task_id)(); } taskCounter++; if(task_ret) { error_handler(); } if(TOTAL TASKS == taskCounter) { taskCounter = 0; // Reset task counter currentPriority++; } }
currentPriority = 0; // Reset priority } }

To queue a task, you can set the "exec" bit in the task list:

taskList[TASK0].exec = true;

Or write a function to do it if you want to make it conditional.

What I've been doing is setting up a timebase ISR (heartbeat in other words) of 10 mS that queues task0(); task0() is used as "task handler" which queues all of the other tasks that need to be performed on a regular basis.

interrupt timer_ISR(void) { clear_interrupt_flag(); queue_task(TASK0); }

If I need something done right away, a task can be "rushed." For example, a LCD update task is normally queued at a rate of 1 second, but maybe you might want the LCD to update right away after a button press to avoid having any input lag:

rush_task(uint8_t id) { taskCounter = taskList[id].task_id; currentPriority = taskList[id].priority; }

I've been rather enjoying using this method so far. Helpful to have more control of the order of which tasks are executed. Another thing I like about it is I can queue up tasks for execution from the interrupt-line code, without directly making calls to it, thus appearing in multiple call graphs and forcing the function to be duplicated by the compiler. One more good thing is it simplifies error trapping. A drawback is if your code gets complicated, you'll want to keep track of all the places tasks are being queued, or only queue tasks from task0. It could become tricky to debug if you let it get out of control.. don't do that.

Just putting this out there so maybe someone smarter than me will come along and say "you idiot, you should never do this because...," or has ideas to improve upon it, or just wants to talk about it... If you've actually made it this far, congratulations, and thanks for reading.

Embedded state machine alternative: a simple tasker. Most embedded C projects I see manage the execution of their application like this: >void main() { for(;;){ switch(mode){ case this: do_that(); break; case that: do_this(); break; case the_other_thing: farts(); break; } } } An alternative I've been playing around with lately is using a simple task engine. Simple tasker is a task type, a data structure of tasks, and tasks. It is not preemptive, it is about as simple as it can get. Start with prototypes of the tasks: >uint8_t task0(void); uint8_t task1(void); uint8_t task2(void); uint8_t task3(void); uint8_t task4(void); An enumeration is needed to provide a task ID for each task: >enum TaskEnum{ TASK0, TASK1, TASK2, TASK3, TASK4, TOTAL_TASKS }taskEnum; A task pointer type is defined: >typedef uint8_t (* taskPtr_t)(void); Task data structure type is defined >typedef volatile struct Task_t{ bool exec; uint8_t priority; uint8_t task_id; taskPtr_t task_ptr; }TASK_T; Number of priority levels is defined: >const uint8_t maxPriority = 3; // or use a preprocessor define, whatever I don't give a shit The structure containing task list {exec, priority, task_id, task_ptr}: >TASK_T taskList[TOTAL_TASKS] = { {0, 0, TASK0, task0}, {0, 1, TASK1, task1}, {0, 1, TASK2, task2}, {0, 1, TASK3, task3}, {0, 1, TASK4, task4}, }; A couple of global vars are used to keep track of things: >volatile uint8_t taskCounter = 0; volatile uint8_t currentPriority = 0; Each task returns a uint8_t: >uint8_t task_ret; Your main loop only needs to consist of the "tasker engine" > void main(void) { for(;;) { while(currentPriority < maxPriority) { if((taskList[taskCounter].priority == currentPriority) && taskList[taskCounter].exec) { // Reset awaiting exec taskList[taskCounter].exec = false; // Execute task and get error code task_ret = (*taskList[taskCounter].task_id)(); } taskCounter++; if(task_ret) { error_handler(); } if(TOTAL TASKS == taskCounter) { taskCounter = 0; // Reset task counter currentPriority++; } } currentPriority = 0; // Reset priority } } To queue a task, you can set the "exec" bit in the task list: >taskList[TASK0].exec = true; Or write a function to do it if you want to make it conditional. What I've been doing is setting up a timebase ISR (heartbeat in other words) of 10 mS that queues task0(); task0() is used as "task handler" which queues all of the other tasks that need to be performed on a regular basis. >interrupt timer_ISR(void) { clear_interrupt_flag(); queue_task(TASK0); } If I need something done right away, a task can be "rushed." For example, a LCD update task is normally queued at a rate of 1 second, but maybe you might want the LCD to update right away after a button press to avoid having any input lag: >rush_task(uint8_t id) { taskCounter = taskList[id].task_id; currentPriority = taskList[id].priority; } I've been rather enjoying using this method so far. Helpful to have more control of the order of which tasks are executed. Another thing I like about it is I can queue up tasks for execution from the interrupt-line code, without directly making calls to it, thus appearing in multiple call graphs and forcing the function to be duplicated by the compiler. One more good thing is it simplifies error trapping. A drawback is if your code gets complicated, you'll want to keep track of all the places tasks are being queued, or only queue tasks from task0. It could become tricky to debug if you let it get out of control.. don't do that. Just putting this out there so maybe someone smarter than me will come along and say "you idiot, you should never do this because...," or has ideas to improve upon it, or just wants to talk about it... If you've actually made it this far, congratulations, and thanks for reading.

(post is archived)

[–] 1 pt

Youre halfway to an RTOS

This only really gets used on my 8-bit microcontroller projects. These things are stuffed full of hardware peripherals that take care of a great deal of things without CPU intervention. For example, PIC18F2x/4xK42 has DMA, 6 hardware timers, ADC with computation, hardware multiplier, fully-configurable hardware CRC generator, etc... Never really have had to go with an RTOS for 8-bitter projects. It's like half of the application "scheduling" happens through interrupts-how you configure the hardware registers and is under direct hardware control most of the time, and this is where the precisely timed part of the application lives. The other half is the mainline code.

I'm currently lost when it comes to coding on platforms without all of these hardware peripherals, necessitating an RTOS. Wouldn't know where to start. Don't even know if I have the concept correct in my head, really.