Friday, November 2, 2018

Task Scheduling

At the bottom of my new-improved-code pile is the scheduler library which makes possible a semblance of non-pre-emptive multi-tasking. It maintains a list of Task functions to be executed with a single argument (which can be cast to be an arbitrary pointer) and the number of milli-seconds in the future that the function should be fired off. Using this system you will never use the busy-waiting delay() again. It is contained in the schip_scheduler library, and optimally linked through the Arduino cores 'kernel' code. See those directories in my code bolus ref'd in Software Architecture.

Task functions have the signature:

    typedef void(*PFV)(uint16_t);    // pointer to a function for task list

which looks like this when you define one:

    void myTask( uint16_t arg ) { return; }

By default, the function list has 8 entries and attempt to enter more will fail with an error code. The size can be changed in the header file with:

    #define USE_NumTasks 8

There are four user entry points to the library:

    void initScheduler(void);

initScheduler() should be called in the setup() method before any tasks or task related interrupts are posted or enabled.

    void scheduler(void);

scheduler() should be called in the loop() method, and may be the only function there -- modulo a serial message checking block if one uses such -- see subsequent blogging about message Tasks. This is where the busy-waiting is concentrated, but it is only waiting busily when there is nothing else to do.

A task can be inserted into the list using;

    int postTask( uint16_t ticks, PFV func, uint16_t arg );

where 'ticks' is the number of milli-seconds in the future that the task should be executed, 'func' is the task method to execute, and 'arg' is an arbitrary 16 bit sized value to pass when executed, ala: func(arg). A post call might look like this:

    postTask( 100, myFunc, 0 );

It will return the task's index in the list, or -1 if it fails.

An executing task may call:

    void repostTask( uint16_t ticks, uint16_t arg );

to put itself back into the scheduler's list with the given execution delay and argument value.

There is, as of now, no removeTask(). Everything in the list gets executed when it's time comes...

A user task should not run for a long time and should never call delay() or other blocking functions as this will prevent other tasks from running. If the task is going to do a lot of nonsense it should be broken into smaller functions that post each other in succession. This allows 'background' tasks, such as ADCTask() and ServoTask() to get an execution in edgewise. All tasks run to completion in "user memory space" so there are minimal worries about concurrency. However interrupt service routines can both interrupt and post tasks during execution, so some attention should be paid to shared variables in those contexts.

There are two versions of the scheduler library. One is fully in the "user program space" and uses millis() to calculate elapsed time between calls. This has a bit more overhead and can be less precise in it's execution delays. The 'real' one can be hacked into the Arduino cores "kernel" and uses a callout from the milli() tick interrupt to manage the times in the task execution list. It is somewhat more efficient but the drawback is you have to hack it into each kernel version as it comes off the press. There is some discussion in the library header file and in the cores/readme.txt about how to install it.

A secondary advantage to the "kernel" version is that it can contain another callout to a user function on every milli() tick. And THIS can be used to implement, e.g., a stepper motor step sequence. See SCHIPTICK in the files....


Next up: ADCs

No comments:

Post a Comment