Tom's Main Menu

Physical Computing Home

Intro to Physical Computing Syllabus

Networked Objects

Sustainable Practices

blog

Resources

code, circuits, & construction

my del.icio.us links

 

Multitasking using the BX-24

 
Common to all of the programs we've seen in this site is a method of making the microcontroller loop over and over again, to keep the program going indefinitely. On the BX-24, we've done it by putting an infinite do loop in our main subroutine, like this:
Sub main()
	call delay(0.5)  ' start  program with a half-second delay 

    do
        ' do stuff
    loop
end sub

If we want to call a subroutine over and over, we put it in the do loop, and it gets called once every time the loop repeats. Throughout all of this, we can always trace the program's flow one line at a time. If the main loop calls a subroutine, flow passes to that subroutine, and back to the main loop when the subroutine reaches its end. Each instruction waits until the BX-24 reaches that line in the program.

Sometimes, however, we want the computer to keep track of more than one flow at a time. For example, when we have a servomotor attached to the chip, we need to pulse the servo once every 20 milliseconds no matter what. It would be nice to have a second loop that does nothing but pulses the servo, then does nothing for 20 milliseconds. If this loop could share the processor with the main loop, we'd be very happy. That way, we'd be assured that the servo would get its pulses, and the main loop wouldn't have to keep track of whether 20 milliseconds have passed.; it could just do the things it does in the time it takes to do them.

What we're describing is called multitasking. Each loop, the main loop and the servo loop, is a task, and the tasks share time on the processor. To make this happen, we do three things: First, we write the task as a subroutine with its own infinite do loop, and second, we set aside some memory for the second task to use as a stack. Then, we use the callTask command to start the second loop running.

Writing the task

A task looks similar to the main subroitine. In addition to the infinite loop, it has a call to a command called sleep, to tell it when to let the other tasks use the processor. I usually give my tasks the suffix "task", so I know. For example:

sub servoTask ()
    do
        ' pulse the servo:
        call pulseout(13, angleVar, 1)

        ' give up the processor for 20 milliseconds
        call sleep(0.02)
    loop
end sub

When the task calls the sleep routine, it does nothing, and therefore the main loop and other tasks can do what they do. The shorter a task's sleep, the more processor time it takes, and the less other tasks get to run. When you use tasks, you should also put a sleep call at the end of your main do loop as well.

Setting aside memory for a task stack

Normally, the processor sets aside some memory called a stack, as a place for the main loop to keep track of local variables, and other housekeeping chores. If we plan to set up a second loop, we have to give it some memory for its own stack. Figuring out how much memory to allot is a trial and error process. If you add up the byte sizes of all the variables used in the new task and add some padding, say 20 percent, you usually have enough. Put a debug.print statement at the end of your task's do loop, right before the sleep call, and run the program. If the debug.print statement isn't printing, usually it means you need to give the task more memory.

To set aside memory, you make a byte array, as follows:

dim servoStackVar(1 to 50) as byte

This will make an array of 50 bytes available.

Calling the task

To call a task, you use callTask, giving it the name of the task, and the array variable for the task's stack, like so:

callTask "servoTask", servoStackVar

Whenever you call this line, the task will start running on its own, and not stop until it reaches its "end sub" line. The task and the main loop will operate at the same time, sharing the processor's time. If you want to share information between them, you can use a global variable that both have access to. Generally it's best to make sure only one of the two tasks can actually change the variable. For example, in our servo program, the main loop might be able to modify the variable angleVar, and the servo task can only read it and use it to pulse the servo. This way, there is no danger that one task will modify the variable at the same time as the other task.

A typical program with two tasks (a main loop and one extra task) would look like this:

dim servoStackVar(1 to 50) as byte
dim angleVar as single

Sub main()
	call delay(0.5)  ' start  program with a half-second delay 

    ' give angleVar an initial value:
    angleVar = 0.0

    ' start the servo task before we start the main do loop:
    callTask "servoTask", servoStackVar

    ' main do loop:
    do
        ' read sensors, do calculations, etc

        ' give up the processor at the end of the do loop:
        call sleep(0.0001)
    loop
end sub

sub servoTask()
    ' task's do loop:
    do
        call pulseout(13, angleVar, 1)
        
        ' give up the processor for 20 msec, 

        call sleep(0.02)
    loop
end sub

For more on tasks, see the BX-24 Operating System Reference from NetMedia.