Author Topic: Working with tasks  (Read 12567 times)

Offline mark

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 3234
    • View Profile
    • uTasker
Working with tasks
« on: August 03, 2007, 11:38:11 AM »
Hi All

One question which is often asked is about how to create new tasks. The basics to this are in the document about the operating system basics but it would be interesting to have various examples so here are some:

1. Every task requires first a unique name. For example the watchdog task has the name "Wdog". This string is quite useful when working in debug mode in the tasking part of the code since the name can be displayed. However the actual scheduling works only with the first letter of the name - this is since it is much more efficient to compare just the first letter than a string and keeps with the goals of the uTasker which are to achieve a high level of code size and speed efficiency but still keep a useful degree of comfort.

2. In order to work with the task a simple define is required:
#define TASK_WATCHDOG           'W'                                      // watchdog task
which corresponds to the first letter of the name.

3. In order to add a new task, simply define a name with unique first letter or number, or any other sign. Try to match the name to the tasks job but still do this without colliding with an existing name - capitals and small letters are of course not equal.

#define TASK_MY_NEW_ONE           'n'                                    // my new task task
"new task.."                      // the name we will give it

4. Every task requires a function which is called when it is scheduled

Code: [Select]
#include "config.h"

#define OWN_TASK  TASK_MY_NEW_ONE

void fnMyNewTask(TTASKTABLE *ptrTaskTable)                                // task function
{
}

but before it can actually be used it must be defined in the task table as well as in the node list.
Note that the task table entry defines the properties of the task (at least when it first starts) but it doesn't cause the task actually to be created. It is only created when its reference is ALSO in the node list. The idea behind this is that various node lists can be defined (eg. one for each node in a distributed system) which then create only the tasks which are actually needed for it's own work.

const UTASK_TASK ctNodes[] = {                                           // we use a single fixed configuration (single node)
  DEFAULT_NODE_NUMBER,                                                   // configuration our single node
  TASK_MY_NEW_ONE,
  0,                                                                     // end of single configuration

  // insert more node configurations here if required
  0                                                                      // end of configuration list
};

extern void fnMyNewTask(TTASKTABLE *);

const UTASKTABLEINIT ctTaskTable[] = {
  { "Wdog",      fnTaskWatchdog, NO_QUE,   0, (DELAY_LIMIT)( 0.2 * SEC ),  UTASKER_GO}, // watchdog task (runs immediately and then periodically)
  { "new task..",fnMyNewTask, NO_QUE,   0, 0,  UTASKER_STOP},                           // new task not doing much yet
};

When the system starts, the resources for the new task will now be created. In this case the task will have no input queue and no timers associated with it. In fact it will not do much and never be scheduled by the tasker.
It could however be activated from another task in the system when it calls uTaskerStateChange(TASK_MY_NEW_ONE, UTASKER_ACTIVATE);

This will cause the new task to be scheduled once before it is set back to the UTASKER_STOP state.

If it is started using uTaskerStateChange(TASK_MY_NEW_ONE, UTASKER_GO); it will cause the task to be scheduled every time the tasker cycles. This will then convert the task into a polling task.

However it can of course also be immediately started as a polling task by already defining it like that in the task list.
  { "new task..",fnMyNewTask, NO_QUE,   0, 0,  UTASKER_GO},

5. Now it may be that you have some code which you would like to add to the project which looks something like this:

Code: [Select]
int main(void)
{
    fnInitialiseSomeThings();

    while (1) {
        if (event1 != 0) {
            fnHandleEvent1();
        }
        if (event2 != 0) {
            fnHandleEvent2();
        }
        if (event3 != 0) {
            fnHandleEvent3();
        }
    }
}

SUch code don't assume any operating system and is build around polling for events to take place, which may be flagged from interrupt routines or by other events. The question is how to allow this code or may be several similar modules which are constructed like this to operate in a way that all receive processing rources and all can live along side the uTasker demo project (or similar) with its TCP/IP resouces?

The answer is in this case thankfully very simple. Since it is based on polling anyway we simply add each module to a polling task:

Code: [Select]
void fnMyNewTask(TTASKTABLE *ptrTaskTable)
{
    static iTaskState = 0;

    if (iTaskState == 0) {
        fnInitialiseSomeThings();
        iTaskState = 1;
    }

    if (event1 != 0) {
        fnHandleEvent1();
    }
    if (event2 != 0) {
        fnHandleEvent2();
    }
    if (event3 != 0) {
        fnHandleEvent3();
    }
}


Now we have these modules and the complete demo project working along side each other.

6. If for some reason one of the modules needs to start delayed this can be achieved by defining the task characteristics accordingly:

  { "new task..",fnMyNewTask, NO_QUE,   (DELAY_LIMIT)(5.10 * SEC), 0,  UTASKER_STOP},

Now this task will be started after a delay of 5.1s. However it will only be started once but it can turn itself into a polling task by simply making the following change:

Code: [Select]
    if (iTaskState == 0) {
        fnInitialiseSomeThings();
        iTaskState = 1;
        uTaskerStateChange(OWN_TASK, UTASKER_GO);                        // switch to polling mode of operation
    }


7. If the polling rate of the task, which is presently as fast as the task list is being worked through, is not needed to be so fast it could be set to a user-defined rate by changing the task definition again (and removing the uTaskerStateChange(OWN_TASK, UTASKER_GO); command):
  { "new task..",fnMyNewTask, NO_QUE,   (DELAY_LIMIT)(5.10 * SEC), (DELAY_LIMIT)(0.10 * SEC),  UTASKER_STOP},
Now the first time the task is scheduled is after 5.1s and afterwards it will be scheduled (polled) every 100ms.

8. The polling is in fact using a monostable task time which was created according to the task configuration. The task still has no input queue, but this if OK because the monostable task timer wakes the task for scheduling without needing to place any messages into its queue. The disadvantage of this is simply that it is only possible to have a single timer event. If the different event should be polled at different rates we could do this by using a different technique. (Note that when using GLOBAL_TIMER_TASK support a single task can also have several timers associated to it - this is described in the corresponding document!).

  { "new task..",fnMyNewTask, SMALL_QUEUE,   (DELAY_LIMIT)(5.10 * SEC), 0,  UTASKER_STOP},

Code: [Select]
void fnMyNewTask(TTASKTABLE *ptrTaskTable)
{
    static iTaskState = 0;
    QUEUE_HANDLE        PortIDInternal = ptrTaskTable->TaskID;           // queue ID for task input
    unsigned char       ucInputMessage[SMALL_QUEUE];                     // reserve space for receiving messages

    if (iTaskState == 0) {
        fnInitialiseSomeThings();
        iTaskState = 1;
        uTaskerMonoTimer( OWN_TASK, (DELAY_LIMIT)(1.5*SEC), E_TIMER_EVENT1 );
    }

    while ( fnRead( PortIDInternal, ucInputMessage, HEADER_LENGTH )) {   // check input queue
        switch ( ucInputMessage[ MSG_SOURCE_TASK ] ) {                   // switch depending on message source
        case TIMER_EVENT:
            switch (ucInputMessage[ MSG_TIMER_EVENT ]) {
            case E_TIMER_EVENT1:
                fnHandleEvent1();
                uTaskerMonoTimer( OWN_TASK, (DELAY_LIMIT)(0.1*SEC), E_TIMER_EVENT2 );
                break;
            case E_TIMER_EVENT2:
                fnHandleEvent2();
                uTaskerMonoTimer( OWN_TASK, (DELAY_LIMIT)(0.05*SEC), E_TIMER_EVENT3 );
                break;
            case E_TIMER_EVENT3:
                fnHandleEvent3();
                uTaskerMonoTimer( OWN_TASK, (DELAY_LIMIT)(1.5*SEC), E_TIMER_EVENT1 );
                break;
            default:
                break;
            }
            break;
        default;
            break;
        }
    }
}

Now the task is controlling its own delays on an event basis [1.5s, 100ms, 50ms]. It needs now an input queue so that it can distinguish between the timer events. Its task definition still has a 5.1s delay before it is first scheduled and this causes it to also to be defined with the appropriate timer resources.

9. If there is no start delay required, the following can be used:
  { "new task..",fnMyNewTask, SMALL_QUEUE,   (DELAY_LIMIT)(NO_DELAY_RESERVE_MONO), 0,  UTASKER_ACTIVATE},

This will cause it to immediately be scheduled once at start. It is however created with the required timer resources which it will use later.


Using these simple techniques quite interesting things can already be performed.
In the next part of this series of examples I will show how the operating system support and its drivers can enable a more modular software design of a project which is easier to maintain and expand. These features will allow the simple reconstruction of projects which are using less efficient polling techniques to become fully event driven. Tasks will only operate when there is really work to do.

Regards

Mark


Offline Richard

  • Newbie
  • *
  • Posts: 44
    • View Profile
Re: Working with tasks
« Reply #1 on: November 13, 2007, 06:33:04 AM »
I've had mixed success defining tasks.  The one that has defeated me is an initialization task that I want to run once, as the system starts, and never again.  Starting from the demo tutorial code, I've included something like the following in TaskConfig.h:
Code: [Select]
  #define TASK_GINIT      'g'         // g initialization

  extern void fnGinit(TTASKTABLE *);

  const UTASK_TASK ctNodes[] = {        // we use a single fixed configuration (single node)
    DEFAULT_NODE_NUMBER,                // configuration our single node
  //...
    TASK_GINIT,                         // g initialization
  //...

  const UTASKTABLEINIT ctTaskTable[] = {
  //...
  { "gInitialize", fnGinit, NO_QUE, 0, 0, UTASKER_ACTIVATE }, // gInitialization
  //...

and in Gfiles.c, is something like
 
Code: [Select]
  void fnGinit(TTASKTABLE *ptrTaskTable)
  {
      unsigned int i;

      for (i = 0; i < WHATEVER; i++) {
          //...
      }
  }

The result is that the system works for a short while, going through fnGinit and another task I've created that runs every second.  After two or three seconds, the processor is restarted by the watchdog!

If I simply comment out the line
{ "gInitialize", fnGinit, NO_QUE, 0, 0, UTASKER_ACTIVATE }, // gInitialization
the problem goes away.

If I leave the line in and change UTASKER_ACTIVATE to UTASKER_STOP, the problem remains, despite the fact that the process will not run in the UTASKER_STOP state.  Thus, nothing the process might do can account for the problem.

This leads me to believe that it is the line in the ctTaskTable that causes the problem, but I can't begin to imagine why.

Any comments appreciated!  Thanks,
Richard

Offline mark

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 3234
    • View Profile
    • uTasker
Re: Working with tasks
« Reply #2 on: November 13, 2007, 12:56:10 PM »
Hi Richard

I believe that this due to a bug  in the M5223X SP5. I have added a patch in the following thread:
http://www.utasker.com/forum/index.php?topic=40.msg175#msg175   [Patch 11].

If you run your set up in the simulator, the simulator should inform that the watchdog is firing after about 2 s (as the target resets after about 2s). If it is due to this, it is not that you are doing anything incorrectly - your initialisation task method is perfectly correct. It is due to a bug seen with your task (which doesn't use any task timers) being counted as a task with timers and the first task (usually the watchdog) gets 'robbed' of its timer. The fix solves this.

Sorry about that... it is strange that the bug has only very recently been reported by someone else, although it has been in the SP5 since its release. I can only conclude that new tasks without timers are only rarely added to the system!

Regards

Mark

PS: The bug doen't affect other processors.


Offline syakovlev

  • Newbie
  • *
  • Posts: 20
    • View Profile
Re: Working with tasks
« Reply #3 on: January 14, 2011, 04:27:29 AM »
Hi Mark,

Question related to example #7

7. If the polling rate of the task, which is presently as fast as the task list is being worked through, is not needed to be so fast it could be set to a user-defined rate by changing the task definition again (and removing the uTaskerStateChange(OWN_TASK, UTASKER_GO); command):
  { "new task..",fnMyNewTask, NO_QUE,   (DELAY_LIMIT)(5.10 * SEC), (DELAY_LIMIT)(0.10 * SEC),  UTASKER_STOP},
Now the first time the task is scheduled is after 5.1s and afterwards it will be scheduled (polled) every 100ms.

Q:
When will be task scheduled for the fist time if defined like this:
  { "new task..",fnMyNewTask, NO_QUE,   (DELAY_LIMIT)(5.10 * SEC), (DELAY_LIMIT)(0.10 * SEC),  UTASKER_GO}, ?

Thanks,
Sergei.


Offline mark

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 3234
    • View Profile
    • uTasker
Re: Working with tasks
« Reply #4 on: January 16, 2011, 03:10:42 PM »
Hi Sergei

In this case the task will be scheduled immediately once.

Since the task is configured as a repetition task it will not stay in the UTASKER_GO state but instead will be put back to the UTASKER_STOP state.
The result of this is that it is then scheduled again after the start delay and subsequently periodically as defined by the repetition period.

If, however, the task were to be defined defined with a start-up delay without a repetition period and put to  the state UTASKER_GO it will be called immediatley (there is no sense in setting a delay and an immediate start since they contradict each other) but the task stays in the UTASKER_GO state, which is a polling state in which it is scheduled as fast as possible.

Regards

Mark