It’s time for another one of my long posts, this time discussing ROBOTC tasks and some of the do’s and don’ts. I realize this information is going to be way beyond the comprehension of many of the new students reading it, however, don’t worry, this is complicated stuff and most of you don’t really need to know all of these details, it’s aimed at mentors and those obsessive programmers that want to extract the maximum from ROBOTC.
ROBOTC Tasks
There were a couple of threads last week that commented about using tasks in ROBOTC. I don’t really want to explain the whole process again (although re-reading this it seems I more or less do) but there are few things to clear up. For those who want to see some older examples and and my original posts from 2011 here are the links. I made these posts when I was still trying to understand ROBOTC’s specific implementation, that’s four years before I joined Robomatter and had access to the source code. The information presented back then was essentially correct but I’m going to add a little more background and a few tips in this post.
So what is a task?
A task is an independent thread of execution, it has some dedicated storage but also shares the global variables for the program. On the cortex you are allowed a maximum of 20 tasks. One special task is called “main”, this is where the program starts. Tasks are started using the function startTask which takes the task name as a parameter and an optional task priority. Most tasks are started with the same priority as the “main” task, a higher priority task is considered more important and will be allowed to run ahead of tasks with a lower priority, we will get to that in post 2. Here is an example program with three tasks in addition to main.
#pragma config(Sensor, dgtl9, test1, sensorDigitalOut)
#pragma config(Sensor, dgtl10, test2, sensorDigitalOut)
#pragma config(Sensor, dgtl11, test3, sensorDigitalOut)
#pragma config(Sensor, dgtl12, testmain, sensorDigitalOut)
//*!!Code automatically generated by 'ROBOTC' configuration wizard !!*//
task task1()
{
while(1)
{
SensorValue test1 ] = !SensorValue test1 ];
}
}
task task2()
{
while(1)
{
SensorValue test2 ] = !SensorValue test2 ];
}
}
task task3()
{
while(1)
{
SensorValue test3 ] = !SensorValue test3 ];
}
}
task main()
{
startTask( task1 );
startTask( task2 );
startTask( task3 );
while(1)
{
SensorValue testmain ] = !SensorValue testmain ];
}
}
All of the tasks in this program have equal priority and run similar code. Each task toggles one of the digital outputs on the cortex, we can look at these outputs changing over time using an oscilloscope, a device that shows the change in voltage of a signal over time a bit like a graph. This is what I captured when running the above code.
Here we can see when each of the four tasks is running, notice that only one can run at any one time and the ROBOTC scheduler allows each one to run in a “round robin” fashion. We have not used any of the special task control functions in this code, each task is given a “time slice” in which to run, this is controlled by the ROBOTC scheduler. Now lets see what happens when we introduce a 1mS wait in each task except main. code for one task would be as follows.
task task1()
{
while(1)
{
SensorValue test1 ] = !SensorValue test1 ];
wait1Msec(1);
}
}
The first three tasks now toggle their respective digital output and then call the wait1Msec function. You can now see that the main task is running most of the time as the other three are waiting for the timer to expire.
What is a time slice?
There is a mis-conception that equal priority tasks are each given the same amount of time to run. Unfortunately a time slice is not measured using milli-seconds or seconds but rather in a “number of opcodes”. An opcode is the most basic unit of operation, an instruction to the ROBOTC virtual machine (VM) that tells it to perform some type of operation (hence opcode). A simple opcode may instruct the VM to “add two numbers” or “get the value of a sensor”. Every line of C code in the program is converted by the ROBOTC compiler into one or more opcodes. ROBOTC allows each task to run for a pre-determined number of opcodes, typically this is calculated as
time_slice = (1000 / number of tasks in the program)
So when we have four tasks, as in the example code above, each one is allowed to run for exactly 250 operations before being swapped out for the next. This is where things start to get tricky as not all opcodes take the same amount of time to execute.
If we add a new, computationally expensive, statement to main as follows, things get interesting. Here is the revised main task. We still have the 1mS wait in each of the other three and would expect their respective digital output to change every 1mS, lets see what happens.
char str[32];
task main()
{
startTask( task1 );
startTask( task2 );
startTask( task3 );
while(1)
{
SensorValue testmain ] = !SensorValue testmain ];
sprintf(str,"%f %f %f",0.12, 12, 14);
}
}
the resultant scope capture.
sprintf is an expensive (time wise) instruction (you would never actually use it in this way). The while loop in the main task is now slowed down and takes much longer to finish the time slice allocated (250 opcodes) to it. Although we only wanted to wait 1mS in each of the other three tasks they are not allowed to run until main completes its 250 opcode time slice.
So how do we solve this situation?
The above example is not a realistic situation with most of the code we write for robots, however, if faced with this type of loop where lots of cpu intensive operations are happening, we use the abortTimeslice() function. This allows us to “give up” the remainder of our time slice and allow other tasks to run if necessary. If no other task needs to run then control will be returned back and we can continue with the loop. Here is revised code with abortTimeslice() added.
char str[32];
task main()
{
startTask( task1 );
startTask( task2 );
startTask( task3 );
while(1)
{
SensorValue testmain ] = !SensorValue testmain ];
sprintf(str,"%f %f %f",0.12, 12, 14);
abortTimeslice();
}
}
and the scope trace
Now we are allowing task1, task2 and task3 to run (more or less) at the desired point in time.
So that’s the end of part 1 of this discussion. The important points to remember are.
-
All tasks of equal priority run in a “round robin” fashion.
-
A task will run until one of three things happens.
- It uses up its “time slice”, that is, it has executed the allowed number of opcodes.
- It calls wait1Msec, the task will then sleep for at least the requested number of mS, however, it may sleep for longer. There are some other advanced features that will also call wait1Msec, things like semaphoreLock, that’s beyond the scope of this discussion.
- It calls abortTimeslice (or EndTimeSlice, they are exactly the same) and voluntarily allows other waiting tasks to execute.
Part 2 will deal with task priority.