Checklist clipartFor those who are considering switching from easyC to RobotC (or if you’re like us, and you have switched but are still in the learning phase), I thought I’d write about an item in RobotC that does not exist in easyC: tasks.

The VEX cortex (as it stands now, in the 2017-and-before universe) cannot multitask; it has no capacity for doing so. In easyC, the best you can do with this limitation is some very well-crafted and efficient functions—called one after the other, each doing its thing and returning to the main loop—but you just cannot make it do 2 things at once.

Technically, RobotC can’t either, but it has the ability to make it seem like it’s multitasking using things called … wait for it … tasks. I include here text from the aweseome jpearman about how RobotC’s tasks work, because I couldn’t possibly say it better myself. RobotC’s faux-multitasking is handled by the “task scheduler”; he offers a wonderful analogy of how this works.

Consider a group of students sitting around a table having a discussion with their teacher. If a student wants to speak then they have to raise their hand and wait for the teacher to give them permission. If several students have their hands raised, the teacher will choose each one in turn so everyone can be heard.

Some students may want to speak often and keep raising their hands, others may be sleepy and not participate much at all. The teacher is also watching a clock, if a student speaks for too long then the teacher will interrupt them and give someone else a turn. At one point the school principal joins the debate, He also has to raise his hand to be able to speak but, as he is considered more important, the teacher always gives him priority.

* * *

This analogy describes quite well how the RobotC task scheduler works. The task scheduler (the teacher in my example) determines which task (student) can run. The principal represents a high-priority task that has precedence over the others. When several tasks of the same priority want to run, the scheduler chooses them in turn. If a task continues running too long, then the scheduler stops it and gives time to others.

Using Tasks

If you look at a RobotC competition template, you’ll see that it already includes tasks; these special tasks areused by the tournament control system to make everyone start & stop simultaneously:

task autonomous() { //stuff here }
task usercontrol() { //stuff here }

OK, so these are tasks.

  • Do I make my own tasks and run them inside these tasks? Yes!
  • And I can make functions and put them inside of tasks? Yes!
  • Can I use both tasks and functions in my program? Yes again!
  • Can I put a task inside of a function? Yes!*
  • Can I nest my own tasks inside one another? Yes!*

* For these last 2 items in the list above, I have a recommendation for those brand-new to using RobotC tasks: start small and simple, with something that you can easily troubleshoot. Give yourself time to test and adjust and debug well in advance of your next competition. (Even if you copy something from a nice person’s post online that’s all cool and fancy, you still need to make sure that it works for your robot the way you think it does.)

I turn now to the very helpful George Gillard and his Beginner’s Guide to RobotC, Volume 2. If you’ve gotten this far in reading, the following assumes that you understand what functions are (George’s guide also has a section on functions, for those who would like to read more).

Tasks are similar in concept to functions in the sense that you can replace huge chunks of repetitive code with them. . . . Tasks are started or stopped, and work in the background, rather than sequentially like functions. This makes tasks more ideal for multitasking purposes.

For example, it is quite common to use functions to control a drive train, but a task to control an arm. This means that the wheels can continue to drive on with the next instructions, while the arm begins to lift, without having to wait.

. . .

Tasks are defined much like a function, however they cannot have any input parameters. . . . If you wish to have the equivalent of an input parameter, you’ll need to set up a global variable.

Autonomous Example

George offers a wonderful example using an autonomous drive train and an arm, as mentioned in the text above; I’ve simplified it a little here.

int desiredHeight    // global variable
const int threshold = 50

task arm() {
   while (true) {
      if potentiometer (POT) < desiredHeight - threshold
      armMotor power = 127

      else if POT > desiredHeight + threshold
      armMotor power = -127

      else armMotor power = 0

      wait1Msec(20);
   }
}

Like functions, you define tasks up at the top of the program, outside of the existing pre_auton() function and the autonomous() and usercontrol() tasks.

Ok, so what’s the big deal? It looks a lot like a function. Indeed it does! However, you’ll notice that task arm() has all of its code inside an infinite while loop; tasks must have code in a while loop, as shown in this example. If you did that in a function (which is handled sequentially), it would never move on to the next step! However, since tasks run continuously until you tell them to stop, and they’re not in the main flow of the program (they are “independent threads”, as jpearman calls them), then their code must be placed in a loop in order to keep functioning.

jpearman‘s rule of thumb: if your task doesn’t have a while loop, it should be a function. (For advanced programmers, there are surely exceptions to this rule, but my post here is for mere mortals.)

OK, so what’s next? How do I use this? Well, you’d have something like the following to start with. Tasks will not run unless you tell them to start, just as a function will not run unless you call it.

task autonomous() {
   startTask(arm);
   // rest of code here
}

Here’s the rest of Gerorge’s example, with arm and chassis instructions going on simultaneously:

task autonomous() {
   desiredHeight = 100;  // Set the height to 100 (low)
   startTask(arm);       // Start the arm
   driveForward(5000);

   desiredHeight = 1500; // Change arm height to 1500 (high)
   turnLeft(1000);

   desiredHeight = 800;  // Change arm height to 800 (medium)
   driveForward(2500);

   stopTask(arm);        // Stop the arm
   stopDrive();
}

Let’s take this step by step:

  1. desiredHeight = 100.  First, declare what the initial desiredHeight should be; once we start the arm() task, it needs a value to start comparing against the potentiometer.
  2. startTask(arm).  Get the arm task running; desiredHeight has been set, so it will start the armMotor running to get POT up to 100.
  3. driveForward(5000).  Immediately after starting the arm task (without waiting for the arm to actually do anything), it will start driving forward for 5000 milliseconds.
    • While it’s driving forward, the arm is being moved to and held at POT = 100 by the task.
  4. desiredHeight = 1500.  When it’s done driving forward, we tell it we want a height of 1500; the task will turn on the arm motor to move it up.
  5. turnLeft(1000).  Immediately after giving the instruction of desiredHeight = 1500, the chassis does a turn for 1000 milliseconds.
    • During this time the arm will be moving to or holding at POT = 1500.
  6. desiredHeight = 800.  After the turn is complete, we tell the task that now we want height 800; the task gets to work, running the motors downward.
  7. driveForward(2500).  Immediately after giving the desiredHeight = 800 command, it starts driving forward again, this time for 2500 milliseconds.
    • During this time the arm will be moving toward and then holding at height of POT = 800.
  8. stopTask(arm) and stopDrive().  After moving forward, it will stop the arm task and stop the chassis.

As you can see from the step-by-step, this code—using a very small number of lines—is making the robot move its arm as it’s driving, without having to do arm, drive, arm, drive, and so on, one at a time.

Looking at this code, notice that the task is never “called” after it is started. With a function, we would call it each time we wanted to move the arm—such as armFunction(desiredHeight = 100). Here, because the task is running in parallel to the main loop, “calling” it requires only changing the global desiredHeight variable. Genius!

Driver Control Example

Tasks are very helpful in driver control too!

This need came up for my team this week. We have a mobile goal (MG) lifter on our current In The Zone robot. When the girls want to pick up a MG, that lifter must be at its lowest position in order to slide under the lip of the MG. So we have an encoder on the MG lifter, and have a button on the driver’s joystick that automatically moves the lifter to the straight-down position.

We use the encoder value with a while loop to run the lifter down to the target value. A simplified version is like so, with 5U & 5D controlling the normal up & down action, and button 6D as the “auto down” action:

// standard up/down button movement on 5U/5D
mgLiftPower = (Btn5U – Btn5D) * 80;
setMotor(mgLift, mgLiftPower);

// auto-down button
if Btn6D == 1 {
   while (EncoderValue > target) {
      setMotor(mgLift, -80);
      wait1Msec(20);
   }
   // turn off motor after reaching target
   setMotor(mgLift, 0);
}

When the girls had this code in the usercontrol() section of the program, any time someone would press the auto-down button, all other joystick control (on both main & partner joysticks) would be frozen out until the while loop above completed its work. Not helpful!

So we moved all of the above code to a task, which gets started within the usercontrol() task, right before the main joystick while loop:

task mgLiftTask() {
   while(true) {
      // all of above code here
   }
}
task usercontrol() {
   startTask(mgLiftTask);
   while(true) {
      // all other joystick code here
   }
   stopTask(mgLiftTask);
}

Now the program is checking whether 5U, 5D, and 6D are pressed in a separate task running simultaneously to usercontrol. It’s awesome.

NOTE: The startTask command is outside of the joystick infinite while loop. This way, the task is started/created just once. If one were to put startTask(mgLiftTask) right *after* the while(true) statement, then every single time through the loop, the program would create another instance of the task. I don’t know what the precise effect this would have on the operation of the robot, but it would certainly be an inefficient use of limited CPU resources.

Other Uses & Examples

Tasks are not only useful in autonomous, as in the example above. Other uses:

  • In pre-autonomous, running your LCD auton-selector code.
  • Using jpearman‘s Smart Motor Library (see my post on that). His library relies on the faux-multitasking to constantly check what’s being asked of the motors and to take any corrective action or apply slew rate.
  • Displaying status on an LCD while you drive the robot.
  • PID control loops.

More Specs

  • You can run a maximum of 20 tasks; seems like more than enough for most situations, but hey, what do I know — I’m a beginner.
  • Tasks should have a teeny tiny wait statement that allows the “teacher” (as described at the top of this post) to move on to another “student” (task). Even a 1 millisecond wait will accomplish this (I think, based on the various jpearman posts), but in any situation (task or no) it doesn’t make sense to check sensor values or give motor instructions more quickly than every 20ms.
  • The task universe includes more than just startTask() and stopTask(): there are a host of other commands, plus setting priority levels for each task, etc.
    • Here’s the RobotC VEX Help section for task commands.
    • See this other jpearman VEX Forum post, which is basically a technical manual on tasks. Very detailed and well-organized.
    • For those seeking even more, he links to several other of his lengthy posts on this topic that you can click through to read.
    • Regarding priority, in the post linked above jpearman says:

      When all tasks have the same priority, they will be serviced in a round-robin fashion and may not get the CPU time they need. If a task is given a higher priority it will run when it needs to; something like a PID control loop should probably do this. Some tasks can be given a lower priority; I would do this for perhaps a task displaying information on the LCD.

♦           ♦           ♦

That’s it from me on tasks so far; now you know probably the same amount that I do. I hope that this description and the links offered here will make using this very powerful programming tool at your disposal in RobotC a little easier.

Share this post: