Sei sulla pagina 1di 10

Cooperative multi-tasking in the C language

Daniel F F Ford
Advanced Solutions Pty Ltd, Gerroa NSW Australia

Issue 1.3, 15 December 2006

Cooperative multi-tasking in the C language

History
Issue

Date

Comments

1.0

23 April 1993

Part of a software manual for AWA Traffic + Information


Systems

1.1

30 June 2002

Multi-tasking sections extracted and made more generic, for


general issue

1.2

27 October 2006

Add section about multiple jbufs for one task

1.3

15 December 2006

Add notes about longjmp 2nd argument

D:\Business\My Documents\Multi-tasking_treatise\Multi-tasking1_3.doc

Page ii

Cooperative multi-tasking in the C language

Table of Contents

History ..................................................................................................ii
Introduction..........................................................................................1
Multi-tasking ........................................................................................1
Program Structure ...............................................................................1
Setjmp/longjmp explanation...............................................................2
Program example.................................................................................3
Longjmp second argument.................................................................4
Multiple jbufs .......................................................................................4
Cautions ...............................................................................................5
Local variables ..........................................................................5
Stack usage ...............................................................................5

List of Illustrations
Fig. 1: Flow chart Display Task .......................................................6
Fig. 2: Typical Multi-tasking Program Flow.......................................7

D:\Business\My Documents\Multi-tasking_treatise\Multi-tasking1_3.doc

Page iii

Cooperative multi-tasking in the C language

Introduction
This document describes a method of creating a cooperative real-time multi-tasking system
using standard C library functions.
The examples used here are extracted from a real project. The assembler code examples are
specific to a particular processor, but should be sufficiently clear for you to gain an
understanding of how the generic C functions work.

Multi-tasking
Multi-tasking is a method in which CPU time is shared amongst many processing tasks. In a
real-time control system, this gives the appearance that these many tasks are operating
simultaneously, whereas in fact the CPU is simply switching rapidly between tasks. There
are two main types of multi-tasking: pre-emptive and cooperative.
In pre-emptive multi-tasking, each task is allocated a time-slice, and the CPU switches on a
timed basis between each of the tasks. There are many variations to this basic method
whereby high priority tasks are given a larger slice of CPU time, but essentially each task gets
only a set amount of time on each complete multi-task cycle.
With cooperative multi-tasking, each task is given control (full use of the CPU) when it needs
it, but hands back control as soon as it reaches a point where it must wait for some event to
occur. Again there are variations, giving higher priority to some tasks, and/or wresting
control from uncooperative tasks that try to hog processor time.
For real-time machine control I have always used cooperative multi-tasking (initially in
assembler, later in C), and this treatise deals only with that type.

Program Structure
A multi-tasking program typically consists of four parts:
1. An executive [usually in Main()], that passes control to tasks when their awaited
event has occurred
2. One or more task functions
3. General functions (not tasks)
4. Interrupt service routines (ISRs)
What generally differentiates a task function from a general function is that a task needs to
wait at some point in its execution, whereas a normal function just runs from start to finish
and then returns. Typical candidates for tasks would be:
an LCD display task, where the slow internal controller of the LCD requires a delay
between commands (especially long delays after a clear or reset command!)
a calibration routine that requires a series of actions and confirmations from the user
(e.g. establishing a series of external conditions, pressing a key after each is set up so
that the calibration routine can measure some specific analogue input values)
a menu tree, where a series of key-strokes moves the task through different paths (submenus)

D:\Business\My Documents\Multi-tasking_treatise\Multi-tasking1_3.doc

Page 1

Cooperative multi-tasking in the C language

While a task is awaiting some event (timer expiry; user key-press, etc.), we dont want the
program to halt, as there are undoubtedly other important functions needing to take place. So
at each of these points the task hands back control to the executive, which then checks what
other events (awaited by other tasks) may have occurred, and gives control to one of those
tasks if required.

Setjmp/longjmp explanation
Crucial to the construction of a multi-tasking system in C are the standard library functions of
setjmp and longjmp. These functions are always used together, and need to be understood
before using them.
In order to use setjmp/longjmp you must first establish some jump buffers; at least one for
the executive and at least one for each task. The structure of a jump buffer (which Ill
abbreviate to jbuf) is defined by your compiler, typically in an include file called something
like setjmp.h. A jbuf is used to store the return address and stack pointer at the point where a
task-switch is to occur.
The source code for the setjmp/longjmp functions are typically written in assembler, since
were dealing with unusual register operations not easily performed in C. The actual code
will obviously depend on the compiler and target processor being used, and must take into
account the calling conventions of the specific compiler. But the following source code is
representative of an efficient setjmp/longjmp pair (this particular processor has only a 16-bit
address range, and thus each jbuf is only 4 bytes long):
;
r2 has a pointer to the buffer; return value goes in r0.
_setjmp:
pop
r1
;get return address
mov
r1,@(2,r2) ;store it in buffer (at 2 bytes offset)
mov
sp,@r2
;store stack pointer in buffer (at zero offset)
sub
r0,r0
;return a zero
jmp
@r1
;return from setjmp call
;
buffer pointer in r2; desired return value in r1; returned in r0
_longjmp:
mov
@r2+,sp
;restore SP from buffer (& auto-increment pointer)
mov
r1,r0
;load specified return value in r0
bne
fin
;OK if non-zero
mov
#1,r0
;else ensure return value is non-zero
fin:
mov
@r2,r1
;get return address from buffer
jmp
@r1
;and return to setjmp location

As you can see, calling setjmp saves the return address and stack pointer in a specified taskswitch buffer, then returns to the caller with the zero flag set. If some other part of the
program later calls longjmp, specifying the same buffer, and with a non-zero argument,
longjmp returns to the point at which setjmp was previously called, but with a non-zero
flag.
Why we do this will become clearer when we look at some code that uses these functions.
Note that if you specify a zero value for longjmps second argument, most compiler libraries
will change it to 1, as a longjmp return from a setjmp must always have a non-zero value.
Note also that you can use the second argument of longjmp to return some useful value to a
task awaiting an event. See Longjmp second argument (page 4) for an example.

D:\Business\My Documents\Multi-tasking_treatise\Multi-tasking1_3.doc

Page 2

Cooperative multi-tasking in the C language

Program example
This example revolves around a display task, whose flow chart is illustrated in Fig. 1 (page
6). As with all tasks, it consists of an infinite loop once started, it runs forever (until the
machine is switched off). But while I say it runs forever, it doesnt have exclusive use of
the CPU! As you can see from the flow chart, every here and there it needs to wait for some
event. In this example the awaited event is either for a new display character (or LCD
command) to become available, or for a delay to expire.
The Display task begins by setting a flag indicating it wants a character to display. It then
marks the current execution position by calling setjmp, then does a longjmp to the Executive
(which continually checks for characters appearing in the display buffer). While waiting for a
display character, the Executive services other event-handlers, and other tasks thus get to run
while Display is waiting. When the Executive finds a character in the display buffer, it hands
control back to Display. Display does some processing on this character, and then (usually)
writes it to the LCD. Because the LCD is so slow, a delay is required before another
operation (read or write) on the LCD, so Display now starts a delay timer and then returns to
the Executive during the short delay. While waiting for this delay, other tasks get a chance to
run, and so on.
Now look at Fig. 2 (page 7), which shows a simple Main function that controls transfers to
the Display task. I have labelled a part of Main as the Executive. This is the part that does
nothing (much) more than continually check for events that might have occurred, and if one
occurs and a task is waiting for it, the Executive gives control to that task. Just as for the
tasks, the Executive, once started, runs forever.
When a task needs to wait for an event, it sets a flag (indicating that it is waiting for a specific
event), saves its return address in its own jbuf, then transfers execution back to the Executive.
Each loop through the Executive, on seeing that a task is waiting for an event, the Executive
checks for the occurrence of that event. If it finds the event has occurred, it passes execution
back to the task, using the return address stored by the task in its jbuf.
Lets run step-by-step through the example depicted in Fig. 2.
Usually one of the first things Main does is call an Initialise() function that configures all the
ports and peripherals in the MCU. Then the tasks (which run forever) have to be started from
Main before it gets into the Executive loop. So it calls setjmp in the statement
if (!setjmp(exec)), which stores a return address (and stack pointer) in the exec jbuf. Then,
because a call to setjmp always returns a zero, display() is called [follow the line labelled (1)].
Display consists of an infinite loop, whose first action is to set a flag and then also call setjmp
(but with its own, exclusive jbuf) to store its return address and stack position, then calls
longjmp, specifying the exec jbuf. Longjmp restores the stack pointer from the jbuf, and then
jumps to the return address also stored in exec jbuf. This returns it [via (2)] to Main. Since a
longjmp always returns a non-zero value, the if condition will be FALSE, so display will not
be called again, and execution will continue in Main at the subsequent instruction (which will
often be similar initialisations of other tasks).
Later, we enter the Executive infinite loop, where events are checked. When it reaches line
if (waitChar), the Executive finds that Display is waiting for a character, so it looks for
something in the display buffer, and if theres something there it clears the display-waiting
flag and calls longjmp using the display jbuf. This causes a return to the display task [via
(3)], which then continues execution (processing the character or command, for example).

D:\Business\My Documents\Multi-tasking_treatise\Multi-tasking1_3.doc

Page 3

Cooperative multi-tasking in the C language

At some later point, display will probably write something to the LCD, and thus need a short
time-delay. For that it will start up a timer (dTimer, shown being decremented in the Systik
ISR), call setjmp to mark its current execution location, then use longjmp to return control to
the Executive at the last return address stored in the exec jbuf [via (4) in this example],
allowing the Executive to regain control and go on checking other event-handlers. In this
way, other tasks will get a turn while display waits for its event. The Executive will later find
that dTimer has expired, and in the same fashion will return control to Display via path (5).
Eventually Display will complete its loop and return to the beginning, where it will again wait
for another character in the display buffer. This time the return to Executive will be via path
(6), to the last point stored in the exec jbuf, and the whole cycle starts afresh.
This example is a very simple (and crude) one. A more elaborate system might use eventrequester and event-handler functions to control transfer back and forth. There may be a
situation where multiple tasks are awaiting the same event, and in this case some form of
queuing is required to ensure that each tasks gets its turn when that event occurs.
Additionally, there may be high and low priority tasks, and the Executive should then give
preference to the high priority ones. An innovative programmer should have no difficulty
catering for these additional complexities, once the basic principle is understood.

Longjmp second argument


As mentioned in the section Setjmp/longjmp explanation (page 2), the second argument of a
longjmp call can be any non-zero value. Most published examples will use 1 for that
argument, but it can instead be some value of use to the waiting task.
Take for example a menu task that waits until the user presses a key. The menu task will call
setjmp to store a return position, and then will longjmp to the executive, which continually
checks for a key-press. Provided all your key-codes are non-zero, it is quite OK for the
executive (once it detects a key-press) to use the key-code for the second argument to
longjmp, so when the non-zero return from setjmp occurs in the task, the task can use that
return value to process the key-press.

Multiple jbufs
Also as mentioned in Setjmp/longjmp explanation, at least one jbuf is required for each task.
Why would you want more than one jbuf for a task?
Take for example a suite of tasks that display menus on a screen, and selection of a menu item
either calls a function or task to perform some operation, or displays a sub-menu. The
structure of a menu task will typically start with some code that draws the menu on the
screen, followed by an infinite loop where it waits for and then processes menu selections.
Where it awaits a selection (whether by key-press, mouse-click or screen touch), it hands
back control to the Executive. When the selection is made and the Executive hands back to
the menu task, that task processes the selection (typically in a switch statement) to determine
the desired action. If the desired action involves longjmping to a sub-task, but that sub-task
doesnt change the screen display, then the sub-task can longjmp back to where it was called
in the menu task, which will then simply continue awaiting and processing further selections.
But if the desired action was the display of another sub-menu, then when that sub-menu task
has finished (its OK/Cancel/Return or whatever selection is activated) it needs to cause
the first menu task to re-draw the previous menu. In this case it needs to longjmp back to a

D:\Business\My Documents\Multi-tasking_treatise\Multi-tasking1_3.doc

Page 4

Cooperative multi-tasking in the C language

point near the beginning of the original menu task so that task can redraw the menu before reentering its infinite loop.
So in this case we might declare two jbufs for the main menu task: call them retbuf and
jbuf, and use them along the lines illustrated in the following code fragment:
setjmp(Task[MainMnu].retbuf);
// draw the menu here
for (;;)
{
if(!setjmp(Task[MainMnu].jbuf))
WaitEvent(MainMnu, DbncdSkey);
switch(c = GetKey())
{
case SfSet:
if (!setjmp(Task[MainMnu].jbuf))
EnterLong(parameters);
break;

/* Setup sub menu return */


/* do forever */
/* waiting for a soft-key */

/* set up return address for EnterLong */


/* Enter new value */

In this example, the task EnterLong, which draws a data-entry field on the screen and then
waits for the users input (returning a long value when he presses OK), would return to the
main menu (with a longjmp) using the retbuf (thus to the second line in the above code
example), so that the main menu would redraw its screen:
longjmp(Task[MainMnu].retbuf, 1);

/* Return to Main Menu (redraw)*/

On the other hand, if the main menu called a sub-task that didnt redraw the screen, that
sub-task would return to the main menu using jbuf (thus to the last line in the above code
example), so it would return to where it was called in the main menu task, which would then
continue around its infinite loop without redrawing the screen:
longjmp(Task[MainMnu].jbuf, 1);

/* Return to Main Menu (no redraw) */

Cautions
Local variables
The structure of a task is the same as any other function in C. Some tasks require variables
which are not used anywhere else, and so would normally be declared as automatic type.
This means that storage for these variables is allocated dynamically on entry into the function,
and de-allocated on exit.
But in this multi-tasking system, automatic variables cannot be used within tasks, even if
confined to that one task. This is because automatic variables are normally allocated on the
stack, but the stacks state changes when setjmp and longjmp are used. Because a task hands
control to the Executive at some point, which in turn may give control to some other task or
call a simple function, the temporary memory (stack) location allocated for that tasks
automatic variable may be used by some other functions automatic variable (since in normal
C programming it is assumed that only one function can be executing at any one time).
Thus all variables used within a task must be declared static (if not global variables).

Stack usage
It is important to realise that, while the stack pointer is preserved across task control transfers,
the stack contents should not be relied upon when returning to a setjmp location. The
purpose of setjmp/longjmp are to keep the stack pointer under control, and not let the stack
grow uncontrollably, as it would if you were doing the task transfers (back-and-forth
repeatedly) using normal function calls.

D:\Business\My Documents\Multi-tasking_treatise\Multi-tasking1_3.doc

Page 5

Cooperative multi-tasking in the C language

DISPLAY

wait for char. in buffer:


waitChar = TRUE;
setjmp/longjmp

LCD
ready
?

wait one command delay:


dTimer = CMD_D;
setjmp/longjmp
wait one command delay:
dTimer = CMD_D;
setjmp/longjmp

send new cursor


position to LCD

Y
Y
still
something
in buffer
?

cursor
jumped
?

set cursor-jumped flag


N

get one char. from buffer

special
command
char.
?

cursor
fallen off
line-end
?

execute command
(most set
cursor-jumped flag)

' \'
or '~'
?

wait one command delay:


dTimer = CMD_D;
setjmp/longjmp

substitute CGRAM
char.

write char. to LCD,


increment cursor
position

Fig. 1: Flow chart Display Task

D:\Business\My Documents\Multi-tasking_treatise\Multi-tasking1_3.doc

Page 6

Cooperative multi-tasking in the C language

(2)

(6)

Main() + Executive
main()
{
initialise();
.
.
.
if (!setjmp(exec))
display();
.
(start other tasks)
.
for (;;) /* Start of Executive */
{
.
.
.
if (waitChar)
{
if (d_rd != d_wr) // char in buffer?
{
waitChar = FALSE;
if (!setjmp(exec))
longjmp(dispBuf, 1);
}
}
if (dExpired)
{
dExpired = FALSE;
if (!setjmp(exec))
longjmp(dispBuf, 1);
}
.
.
.
}
}

(1)

(3)

(5)

Task
void
display(void)
{
static char dummy;
for (;;) // do forever
{
waitChar = TRUE;
if (!setjmp(dispBuf))
longjmp(exec, 1);
.
(process the character)
.
dTimer = CMD_D;
if (!setjmp(dispBuf))
longjmp(exec, 1);
.
(and so on)
.
}
}

(2 & 6)

(4)

Systik ISR
.
.
.
if (dTimer)
{
if (!--dTimer);
dExpired = TRUE;
}
if (aTimer)
{
.
.
.

Fig. 2: Typical Multi-tasking Program Flow

D:\Business\My Documents\Multi-tasking_treatise\Multi-tasking1_3.doc

Page 7

Potrebbero piacerti anche