Originally published May 8, 2017
Republished June 20, 2024
Last of this series
The code on GitHub
Post 1 Role your own Operating System,
Post 2, TaskTurner First Code,
Post 3, The Basic Code is Working,
Post 4, Building Tasks,
Post 5, Lowering System Power and Timers.
Inter Process Communications
I was going to write all about it. Then Collin Walls posted a link to his great article that gives a complete overview. Read it and be an expert.
Running with multiple tables
The cool thing about having an easy way to run a table of tasks, is that you can use multiple tables. It seems obvious, but should be singled out.
One useful modification, or trick is you can pass the number of tasks to run into the taskrunner() function. If the tasks get large, or you need some control of the timing, you can do multiple calls, like so:
taskrunner(myTaskList[offset], 2); offset += 2; if (NUM_TASKS <= offset) offset = 0; taskrunner(priortyTaskList, NUM_PRIORITY_TASKS);
So you will do two tasks at a time, then run the priority tasks that need more CPU time.
Tasks and products
Also easy to do.
if (product_A) taskrunner(prodATaskList,NUM_PRODA_TASKS) if (product_B) taskrunner(prodBTaskList,NUM_PRODAB_TASKS) if (product_C){ taskrunner(prodATaskList,NUM_PRODA_TASKS) taskrunner(prodBTaskList,NUM_PRODAB_TASKS) }
And so one. The product_X variable could be hardware bits, a board revision, or some sort of license key.
Or, even better:
#ifdef PRODUCT_A static TaskItem projATasks[] = { Big List of tasks } #endif
#ifdef PRODUCT_B static TaskItem projBTasks[] = { Big List of tasks } #endif
It is always preferable to have the system defined at compile time.
The task that runs tasks
Can a task have a task table? Yes a task could call taskrunner(). There is no reason it can not be re-entrant.
Re-entrant means we can call the same function from within or a child of that function. Now, this is handy, and I am not talking about doing a full recursive function. Beware, there is danger here. If the function is not written correctly, it can have side effects. taskrunner() is written so it will work, there are no side effects.
So, in the task table we could do this
{ task_A, task_B, task_Priority, task_C, task_D, task_Priority, task_A, task_D, task_Priority, }
Inside task Priority it will call taskrunner() like so:
static TaskItem priorityTaskList[] = { priority_task_Z, priority_task_X, priority_task_Y, } task_Priority(){ taskrunner(PriorityTaskList, NUM_PRIORITY_TASKS); }
Don’t go crazy with this, or you will break things, including your brain.
Petting the dog
Many embedded systems use a watch dog timer. This is a timer, either on chip or external that will reset the system if the software does not do a write, or take some action periodically. The idea being, if the software locks up, gets stuck, or looses it’s mind, for whatever reason, the processor will get reset after no more than a few seconds.
Easy enough to add a task, usually the last task in the list (just a personal preference that makes sense to me) and use the task to “pet the dog”. If the tasks are not executing, or something gets stuck, the watchdog will cause a system reset. This is also useful if the system is getting busy, or a task will take a long, long time, it is trivial to add a second call to the watchdog task just before running the long task.
Give runNow a reason
I hesitate to mention this, but it can be really useful. If taken to far, it can cause confusion and delay. runNow is a simple boolean variable as the code is written currently. It works fine as a boolean.
runNow could also be an enumerated type.
The enum would be something like:
typedef enum runReason ={ NO_RUN, TIMER_SET, IPC_SIGNAL, RX_PAYLOAD, TX_PAYLOAD }
You can add more if needed. If you want to make things complex, it can be a bit field, so multiple flags get set.
Inside the task, when it runs, the task gets a hint about why it was run, and what work needs to happen. The task may handle a TIMER_SET differently than an IPC_SIGNAL.
Inside taskrunner() keep the “should this task be run” decision simple. It should always be:
if (NO_RUN != taskRunNow) { taskfunc(); }
Don’t check for flags or state in taskrunner().
The interface (function declaration in C) for the task function does not need to be modified. The runNow variable for each function should be visible to the task, because the variable should be declared inside the task C file (module).
Why build TaskTurner
You build things. That is why you like embedded systems. The code presented in the last few posts is pretty simple. I think many people who read the posts thought “simple, I could easily build that”. Sure, but if we jump up a level and think like a manger, is building it yourself the right thing to do?
Its a fun day or two of work, so why not?
Always, as engineers and developers we are working against the clock. You can never buy more time to market. It’s precious.
Unfortunately, I have seen, and participated in these small infrastructure decisions, that wind up taking precious time. A simple choice like “do we build the OS or buy one?”, can take up 10% or 25% of the time to build a prototype. It is an important choice. A simple prototype that will not grow into a product without a complete rewrite and refactor is a management nightmare. Half the value in buying an off the shelf RTOS is that you can get to work on the application, and not spend time on the infrastructure.
This post is just showing some of the flexibility, and a few ideas to expand the taskTurner. If the taskTurner code won’t do what you need, it is time to move to FreeRTOS, Embedded Linux, or a commercial RTOS.
Simple code like the task turner has some thought and experience behind it. It’s simple but can grow easily. So, grab the code and go. Don’t waste precious time debating the best way, pick simple and obvious and make something already!
Leave a Reply