Tics Realtime -----


 
Home Services Products Tutorials Contact Us
- - - - -

How to Break an Application Down into Tasks

This article provides some guidelines that may be useful when designing software for real-time multi-tasking systems. Specifically, it addresses how an application is broken down into multiple concurrent tasks.

When Multi-tasking is Required

Multi-tasking is generally required in the following situations.
  1. When operations must occur concurrently.
  2. When a common resource (like a printer) needs to be managed.
  3. When it is desirable to defer interrupt processing.
These topics are addressed in detail below.

Implementing Concurrent Operations as Separate Tasks

Consider a simple data acquisition system with the following requirements:
  1. Read data every 100 ms.
  2. Display averaged data once per second.
  3. Wait for and respond to keyboard input.
This application could be implemented as 1 task, or 3 tasks. Both the single and 3 task solutions are shown below.

Single Task Solution

void doAllTask(void)
{
	int i;

	for (i = 0; ; i++) {
		pause(100L);
		if (kbhit()) processKey();
		readAndAverageData();
		if (i == 9) {
			displayAverageData();
			i = 0;
		}
	}
}

Three Task Solution

void dataAcquisitionTask(void)
{
	while (TRUE) {
		pause(100L);
		readAndAverageData();
	}
}

void keyboardTask(void)
{
	while (TRUE) {
		pause(100L);
		if (kbhit()) processKey();
	}
}

void displayTask(void)
{
	while (TRUE) {
		pause(1000L);
		displayAverageData();
	}
}
The question is: which approach is better? The answer for this case is the three task solution, and the reasons are as follows.
  1. Three tasks will not significantly add to system overhead.
  2. The code is probably easier to understand.
  3. Future modifications are more easily implemented.
  4. Each task can be debugged separately.
More tasks are not always the desired solution however. In general task count should be kept to a minimum.

Keep the Task Count Down

Task proliferation should be avoided. Consider a system with 32 RS-232 channels, each channel being connected to a simple CRT terminal. Let us consider the handling of incoming data only. Each RS-232 channel has an incoming queue associated with it and incoming characters are placed into their respective queues by the receive isr. Let us further assume that the system is to simply perform the typical tasks that a CRT terminal requires: echo characters to the screen, backspace, delete, etc.

One may be tempted to create a generic task and instantiate it 32 times - once for each channel. Each instance would wake up every 100 ms and check the incoming data queue associated with its channel. This would have the effect of creating 32 task control blocks, 32 stacks, and 32 on-going timers. A better approach might be to create a single task that wakes up every 100 ms and services all 32 queues. This approach would cut down on overhead, run faster, and make debugging easier.

In our previous example, we chose the 3 task approach instead of the single task. Note, however, the differences here.

  1. 32 tasks and 32 timers can begin to add significant overhead to the system.
  2. The tasks are identical, making the single task approach easy to implement.
  3. The single task approach simplifies development and debugging.

When Multiple Task Instances Make Sense

Multiple task instances can make sense when requirements become more complex. Consider the example given above with the requirement that each RS-232 line's incoming queue must be polled at a different rate. Now the single task approach becomes quite difficult in that the single task must start 32 different timers, and when each timer expires, match it to the appropriate buffer. Furthermore, one of the benefits of using the single task was the avoidance of 32 pending timers, but with this new requirement we need 32 pending timers even with one task. A simpler approach is to create a generic task and instantiate it 32 times.
void rs232Task(void) 
{
	typeRS232Data * d;

	d = (typeRS232Data *) ActiveTcb->ptr;

	while (TRUE) {
		pause(d->pauseValueInMs);
		processLine(d);
	}
}
When each task instance is created, the "ptr" field of its tcb is pointed to a data structure that contains data specific for that task instance (instance data). In this way, each task instance pauses for a different interval.

Implementing Resource Management as a Task

If tasks simply write to a printer when they so desire, interleaved printout can result. A solution to this problem is to create a separate server task whose job is to process print requests (messages) from other tasks. If all tasks follow the rule, then printing will be accomplished in an orderly fashion with interleaving eliminated.

In general, a server task should be created whenever tasks contend for a resource. Although a semaphore could be used, this approach can cause problems. See a previous article for details.

Defering Interrupt Processing to a Task

It is generally desirable to get in and out of an isr as soon as possible. For this and other reasons it is sometimes desirable to defer isr processing to a task. This can be done by creating a task that will process a message from the isr as shown below.
void taskToHandleInterrupt(void) 
{
	typeMsg * msg;

	msg = waitMsg(ISR_MSG);
	processIsrMsg(msg);
	freeMsg(msg);
}
The task above remains dormant until the isr is entered and the message sent. One could argue that the isr processing could be performed within the isr itself, but sometimes this is not easily done. Consider an isr that is subject to "bursts". That is, 5 interrupts in a row may occur very quickly, followed by a period of inactivity. During a burst, the isr may not have time to process the interrupt before another interrupt occurs. In this case, the isr would be re-entered (assuming interrupts were enabled while inside the isr) and as a result handling would become more complex. Deferring interrupt processing to a task solves this problem. Since messages are queued by the multi-tasking kernel, burst processing is not an issue. Messages will be queued to the handler task and each will be handled in its turn. Of course this assumes that the kernel message system is fast enough to send the message before another interrupt occurs.


We welcome comments. Let us know what subjects you would like written up. Send comments to Mike@TicsRealtime.com

Copyright © 2000, Tics Realtime