Discussion on using tasks in ROBOTC

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.

scope_task_1.png

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);  
      }
}

scope_task_2.png

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.

scope_task_4.png

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
scope_task_5.png

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.

  1. All tasks of equal priority run in a “round robin” fashion.

  2. 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.

3 Likes

Task priority

This is a fairly easy concept to understand but a bit more difficult to demonstrate, however, here we go.

A task running with a higher priority than other tasks will retain control until it changes its state from running to waiting by calling wait1Msec (or one of the aliases for wait1Msec, sleep, delay etc.) abortTimeslice is much less useful with tasks that have a high priority unless there are other tasks with that same high priority.

To try and demonstrate what happens we will use a modified version of the code from post #1. We are again using a digital output that keeps changing state (going from 0 to 1 then back to 0) as a way of determining when a task is running. Task 1 is now modified to be as follows.


task task1()
{
    while(1)
      {
      for(int i=0;i<200;i++)
       SensorValue test1 ] = !SensorValue test1 ];
      wait1Msec(10);
      }
}

The idea here is to simulate the task doing some “work”, calculations, checking sensors etc. by toggling the digital output 200 times in a loop, the task then goes to sleep for 10mS. Lets see how this looks when we capture that digital output on the oscilloscope.

scope_task_6.png

The scope trace shows the digital output toggling for about 5mS, the task then sleeps for a further 10mS before repeating the sequence. The other tasks, including main, from the example code are not running in this initial experiment.

We now add back into the test the following code.

task task2()
{
    while(1)
      {
      for(int i=0;i<25;i++)
        SensorValue test2 ] = !SensorValue test2 ];
      wait1Msec(5);
      }
}
task task3()
{
    while(1)
      {
      for(int i=0;i<25;i++)
        SensorValue test3 ] = !SensorValue test3 ];
      wait1Msec(5);
      }
}

This is also a modification of our original example from post #1. These two tasks now toggle their respective digital outputs for a short time also before going to sleep for 5mS. We use the following code to start all three tasks and then capture the resultant output on the oscilloscope.

startTask( task1 );
startTask( task2 );
startTask( task3 );

scope_task_7.png

The two gaps in the Task 1 trace show where that task was not running. The ROBOTC scheduler allowed task 2 to run after the time slice for task 1 was completed, execution then returned back to task 1 as there were no other tasks waiting to run at that point. Task 1 was again interrupted when another time slice had completed and, as task 3 was now wanting to run, the scheduler allowed that to happen. Finally task 1 was able to complete the 200 iterations of the “for” loop and placed itself into a waiting state for the next 10mS. While task 1 was sleeping, tasks 2 and 3 both were allowed to run when their wait periods (of 5mS) expired.

Lets repeat this experiment with task 1 now started with a higher priority than task 2 or 3.

startTask( task1, kDefaultTaskPriority + 1 );
startTask( task2 );
startTask( task3 );

scope_task_8.png

Task 1 now gets exclusive use of the processor until it calls wait1Msec. When the time slice for task 1 is complete, the scheduler determines that task 1 is still wanting to run and is of a higher priority than other tasks. The execution of task 2 and task 3 is delayed until task 1 calls wait1Msec, this means that, although they may have requested to sleep for only 5mS, they will have been blocked from running for longer than that.

hogCPU() and releaseCPU()

These functions are also related to task priority. hogCPU() will raise the priority of the current task above all others. When the time slice for the current task has ended, it will continue to run for an additional time slice if hogCPU() has been called in that task. releaseCPU returns operation of the scheduler to normal. hogCPU() and releaseCPU() are normally used to surround very critical code. There are very few instances when programming a VEX competition robot that you would ever need to use these.

My recommendation is “do not use hogCPU”.

Task priority limitations

There is one major limitation to the ROBOTC implementation of task priority as compared to other real time operating systems such as ChibiOS/RT, the underlying RTOS in ConVEX. A higher priority task cannot interrupt the operation of a lower priority task until that task returns control to the scheduler. The lower priority task will do this in one of the three ways I described in post #1, the time slice completes after the allocated number of opcodes, the task calls abortTimeslice or the task calls wait1Msec. We can demonstrate this by including a computationally expensive call in one of the tasks such that the time slice is stretched (remember, a time slice is not measured by time but by the number of operations to be executed).

The control for the experiment is the following code.

task task1()
{
    while(1)
      {
      SensorValue testmain ] = !SensorValue testmain ];
      for(int i=0;i<200;i++)
       SensorValue test1 ] = !SensorValue test1 ];
      wait1Msec(2);
      }
}

task task2()
{
    while(1)
      {
      for(int i=0;i<25;i++)
        SensorValue test3 ] = !SensorValue test3 ];
      wait1Msec(5);
      }
}

Started as follows.

startTask( task1, kDefaultTaskPriority + 1 );
startTask( task2 );

Task 1, a high priority task, toggles the digital output in the same way as before but only sleeps for 2mS. Task 2 runs for a short time and then sleeps for 5mS.

scope_task_9.png

Pretty much what we would expect, task 1 does its thing and then sleeps, task 2 is delayed from executing by the higher priority task 1 so, even though we only asked it to sleep for 5mS, that time was stretched to about 7mS by the high priority task 1.

Now we add the computationally expensive call in task 2 and remove the delay.

task task2()
{
    char str[32];
    while(1)
      {
      for(int i=0;i<25;i++) {
        SensorValue test2 ] = !SensorValue test2 ];
        sprintf(str,"%f %f %f",0.12, 12, 14);
      }
     // wait1Msec(5);
      }
}

I use sprintf in this demonstration but all that matters is that the call, although being only a single operation, takes a long time to execute.

This is the resultant scope capture.

scope_task_10.png

The sprintf call stretches out the time that task 2 is running, the time slice for task 2 is constant but the operations take longer. Task 1 will not be allowed to run, despite being at a higher priority than task 2, until task 2 completes its time slice. This is one situation where adding an abortTimeslice to the for loop in task 2 would be appropriate, see post #1 for that explanation.

That concludes the discussion of task priority, in summary.

  • A high priority task is given priority over other tasks by the ROBOTC scheduler when it has to make a choice. A high priority task jumps ahead of other lower priority tasks that may want processor time.
  • A task running at a higher priority than other tasks will not be interrupted to allow them to run.
  • All tasks, including high priority tasks, will only be scheduled to run when the currently running task completes its time slice, calls abortTimeslice or wait1Msec.
  • Avoid using hogCPU() and instead set the priority of critical tasks higher.
2 Likes

Part 1 and 2 of this discussion addressed how and when ROBOTC tasks are run.
This part will discuss one of the more common pitfalls.

Sharing resources between tasks

All ROBOTC programs that are controlling competition robots have similar functionality.

  • Reading the joystick
  • Reading sensors
  • controlling the motors
  • controlling output ports
  • displaying information on the LCD or in the debug stream.

Some of this functionality is safe to use in different tasks, some should be limited to a single task.
Consider the following code.

#pragma config(Motor,  port1,            ,             tmotorVex393_HBridge, openLoop)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//

task partnerJoystick()
{
    while(1)
      {
      // Partner joystick control
      if( vexRT Btn8DXmtr2 ] == 1 )
        motor port1 ] = 50;
      else
        motor port1 ] = 0;
      
      wait1Msec(15);
      }
}

task main()
{
    startTask( partnerJoystick );
    
    while(1)
      {
      // Main joystick control
      if( vexRT Btn8D ] == 1 )
        motor port1 ] = 50;
      else
        motor port1 ] = 0;
      
      wait1Msec(10);
      }
}

The idea was to have a dedicated task monitoring the partner joystick and controlling a motor. The same motor is being controlled by the main task and the primary joystick. It’s pretty easy to imagine what will happen, if one of the two conditional statements is true and the other false, the motor will be sent a different value depending on which task is running at that specific moment. The motor will jitter as the different values are sent to it. Playing with the wait1Msec delays (for example, making them both the same) may help but the control in this example is fundamentally flawed.

How about this code.

#pragma config(Sensor, dgtl1,  limitSwitch,    sensorDigitalIn)
#pragma config(Motor,  port1,            ,             tmotorVex393_HBridge, openLoop)
#pragma config(Motor,  port2,            ,             tmotorVex393_MC29, openLoop)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//

task task1()
{
    while(1)
      {
      // check a switch
      if( SensorValue limitSwitch ] == 0 )  
        motor port1 ] = 50;
      else
        motor port1 ] = 0;
      
      wait1Msec(15);
      }
}

task main()
{
    startTask( task1 );
    
    while(1)
      {
      // check a switch
      if( SensorValue limitSwitch ] == 0 )  
        motor port2 ] = 50;
      else
        motor port2 ] = 0;
      
      wait1Msec(10);
      }
}

We check a switch on a digital input and set two different motors on or off accordingly.

This code is ok, there is no conflict between task1 and the main task.

A few good rules would be;

  • Only ever control an individual motor from one task
  • It’s ok to read the same sensor in more than one task.

and a more generalized form

  • It’s ok to read devices in many tasks but limit writing to one task.

One final example.

int  MyArray[1000];

task task1()
{
    int lastValue = 0;
    
    while(1)
      {
      if( MyArray[0] != lastValue ) {
        lastValue = MyArray[0];
        writeDebugStreamLine("MyArray changed");
        writeDebugStreamLine("value of   0 is %d", MyArray[0] );
        writeDebugStreamLine("value of 500 is %d", MyArray[500] );
        writeDebugStreamLine("value of 999 is %d", MyArray[999] );
      }
            
      wait1Msec(50);
      }
}

task main()
{
    startTask( task1 );
    int count = 0;
    
    while(1)
      {
      for(int i=0;i<sizeof(MyArray)/sizeof(int);i++)
        MyArray* = count;
      count++;
      wait1Msec(50);
      }
}

Very artificial code but lets examine what it is doing and what will happen.

The main task fills an array of 1000 elements with the value of the variable count, count is then incremented and the task goes to sleep. All the elements of the array will be first 0, then 1, then 2 and so on. Task 1 is waiting for the value sorted in the array to change, it then prints out what the array contains at index 0, 500 and 999. You would hope these three value would all be the same but think about how we understand tasks to operate. What happens if task1 runs when the time slice for the main task finished, the main task may be part way through filling in the array. We can test this and look at some real results, here is the debug stream I captured.

MyArray changed
value of   0 is 1
value of 500 is 1
value of 999 is 1
MyArray changed
value of   0 is 2
value of 500 is 2
value of 999 is 2
MyArray changed
value of   0 is 3
value of 500 is 3
value of 999 is 3
MyArray changed
value of   0 is 4
value of 500 is 3
value of 999 is 3
MyArray changed
value of   0 is 5
value of 500 is 5
value of 999 is 5
MyArray changed
value of   0 is 6
value of 500 is 6
value of 999 is 6

We start off ok, all values are 1, then 2, but look at what happened when we stated to fill the array with the value 4. The task that prints out the value ran when we were part way through filling in the array, the value at index 0 was 4 but the others were still at 3.

There are a couple of ways to solve this, one is to increase the priority of the main task over task1. If the main task has a higher priority then task1 will not run until the main task sleeps.

A second way is to protect the resource, that is the array, with what we call a “resource semaphore”. A semaphore is a special type of variable which we use so that only one task can have access at any one time to something special, in this case it would be our array. I posted about semaphores a while back, it is an advanced feature and something we do not use very often in ROBOTC, however, no discussion about tasks would be complete without at least understanding that they exist.

My original post on semaphores is here.
https://vexforum.com/index.php/conversation/post/54070

Here is a modified version of the previous code with the array protected by a semaphore.

int  MyArray[1000];
TSemaphore MySemaphore;

task task1()
{
    int lastValue = 0;
    
    while(1)
      {
      // Try and get the semaphore
      semaphoreLock( MySemaphore, 100 );
      
      // did we get the semaphore ?
      if( bDoesTaskOwnSemaphore( MySemaphore ) ) {
        if( MyArray[0] != lastValue ) {
          lastValue = MyArray[0];
          writeDebugStreamLine("MyArray changed");
          writeDebugStreamLine("value of   0 is %d", MyArray[0] );
          writeDebugStreamLine("value of 500 is %d", MyArray[500] );
          writeDebugStreamLine("value of 999 is %d", MyArray[999] );
        }
        // now release it
        semaphoreUnlock( MySemaphore );
      }
            
      wait1Msec(50);
      }
}

task main()
{
    semaphoreInitialize( MySemaphore );
    startTask( task1 );
    int count = 0;
    
    while(1)
      {
      // Try and get the semaphore
      semaphoreLock( MySemaphore, 100 );
      
      // did we get the semaphore ?
      if( bDoesTaskOwnSemaphore( MySemaphore ) ) {
        for(int i=0;i<sizeof(MyArray)/sizeof(int);i++)
          MyArray* = count;
        count++;
        // now release it
        semaphoreUnlock( MySemaphore );
      }
      
      wait1Msec(50);
      }
}

So I admit this is getting complicated, it’s probably not something anyone needs to worry about unless you are trying to write more advanced code, even with a shared resource like the debug stream or the LCD THEORETICALLY you may need a semaphore, in practice you do not.

That’s all I’m going to post about for now, this became way more time consuming than I wanted it too but hopefully it helps clear up some of the mysteries about the implementation behind tasks.
**

1 Like

Wow. That was actually really illuminating. I had never used the priority system but it sounds useful in the right circumstances.I had planned to spend some time poking around at how the task structure worked but never had gotten around to it. Thanks for this explanation.

I wouldn’t have thought the issue with race conditions would be that bad in most cases in VEX but working on embed projects where the exact timing of stuff matters I have been getting a better appreciation for what you are discussing here.

1 Like

When you say that a time slice is 250 opcodes, I assume that there can be multiple opcodes for a line of C code, i.e. equations. Is this a correct assumption?

If so, are we guaranteed that a line of C code will fully complete its execution before a task is allowed to switch?

1 Like

250 opcodes when running four tasks. Number of opcodes per task time slice changes depending the number of tasks.

A line of C code will be one or more opcodes, for example.

Assignment of a constant to an integer variable is one opcode, here we assign 12 to an integer on the stack.
The opcode is (in hex) CB000C00

i = 12;
0003: CB000C00   i:S00(slong) = 12                   // long/float

Assigning a joystick value to a motor becomes two opcodes, a “getProperty” call that saves the value into a local variable followed by a setProperty call.

motor port1 ] = vexRT Ch2 ];
0007: 4D2229040001   S04(short) = (short) GetProperty(propertyIFIRadioControl, Ch2:1)
000D: 570A29040000   SetProperty(propertyMotorPower, port1:0, S04(short))

ROBOTC allows you to view the opcodes generated for any program, use “Assembly” in the view menu after compiling (when super user menu level is also selected, window->menu level.

No, the scheduler has no knowledge of the original C code. A task can effectively be paused in the middle of a single line of code.

1 Like

Thanks, I know we had discussed atomic data access and such in the past, but I was under the impression that it was thought the line was completed. But, maybe it was just the opcode.

In any event, one of the things our kids are taught to do is to use the hog/releaseCPU functions to force “atomic” operation for motors. For example, if you have a flywheel shooter with 2 motors on each side we use hogCPU prior to writing to the motors, 4 quick writes, and release it afterwards. This forces the motors to all be updated at the same time without possible interruption. It’s not a shared resource so we don’t use a semaphore, it is a set of code that we want o make sure always starts and completes in sequence without task interruption.

Is there a better way to do this in RobotC?

1 Like

I think we did somewhere. Many simple things are atomic, assignment to variables, things like that, the opcode (and associated parameters needed to execute it) are the smallest unit of execution.

You can use HogCpu(), you could raise the priority for that task, both would achieve the same thing. If the time slice for that task ran out in the middle of the four opcodes, then if the “hog cpu” flag is set the task will continue to run. If the task is the highest priority task then it will also continue to run.

There is, however, one small gotcha I did not discuss in the previous posts, it’s an issue independent of the task scheduler. Lets look again at one of the scope captures from a previous post.

scope_task_10.png

Notice where the arrow points for the “Tasks 2 running” label. There is a pause in the waveform where the digital output stopped toggling for about 1mS, this is where a hardware interrupt occurred and ROBOTC had to communicate with the cortex master processor (remember there are two micro controllers inside the cortex, user processor where our code runs and master processor where VEXnet control and things like that happen). This will happen once every 15mS and is completely independent from the virtual machine where the user code is running. This is not ideal but the way the ROBOTC firmware was created several years ago, there is very specific timing that needed to be adhered to and all of commercial software uses software delays to achieve this. (If I were not a Robomatter employee I would say here that ConVEX does not have this limitation as I implemented this communication using other means that do not tie up the cpu in this way, but I am, so I won’t go there :slight_smile: )

1 Like

@jpearman thank you for the writeup and, since you are a Robomatter employee, is there a way to read some variable and know when the last hardware interrupt from the master processor had happened?

While it is not ideal that it blocks the CPU, it is not a big problem if you could predict when the next one will occur and plan your delays around it.

Also, is it the only interrupt that could happen and the one that uses SPI to retrieve new state from joystick controls and send out updated motor values for the ports 2-9?

1 Like

There is a system property called nIfiSPIMsgCounts that increments every time communication occurs with the master processor. This is not any sort of secret, I used it in a thread from 2011 here.
https://vexforum.com/t/answered-power-a-problem/14944/1

This is the process that retrieves joystick data and sends motor values (It’s actually not all done at interrupt level, interrupt sets a flag that indicates to VM that this work needs to be done and that effectively blocks the user code, this is all scheduled by a 1mS timer tick interrupt). There are many interrupts happening, serial comms, quad encoder reading, I2C communications etc. but, as with most embedded control, we do not spend any significant amount of time in the actual interrupt handlers, a few uS at most.

1 Like

So here is some code (no guarantee on this) that synchronizes two tasks with the SPI comms.

#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               !!*//

long  spiTimer;

void spiSync()
{
    wait1Msec(1000);
    // Synchronize with spi
    long oldspi = nIfiSPIMsgCounts;
    while( nIfiSPIMsgCounts == oldspi ) {
        ; // block
    }
    // now do it again for safety
    oldspi = nIfiSPIMsgCounts;
    while( nIfiSPIMsgCounts == oldspi ) {
        ; // block
    }
    spiTimer = nSysTime;
}

task task1()
{
    while(1)
      {
      while(1) {
        if( (nSysTime - spiTimer) % 15 >= 8 ) {
          for(int i=0;i<10;i++)
            // do the task work
            SensorValue test1 ] = !SensorValue test1 ];
          break;
        }
        else
          abortTimeslice();
      }
      
      wait1Msec(13);  // somewhere close, but less than, 15
      }
}

task task2()
{
    while(1)
      {
      while(1) {
        if( (nSysTime - spiTimer) % 15 >= 4 ) {
          for(int i=0;i<10;i++)
            // do the task work
            SensorValue test2 ] = !SensorValue test2 ];
          break;
        }
        else
          abortTimeslice();
      }
            
      wait1Msec(13); // somewhere close, but less than, 15
      }
}

task task3()
{
    int oldspi, newspi;
    
    while( true )
        {
        // digi out low
        SensorValue test3 ] = 0;

        // did nIfiSPIMsgCounts change
        newspi = nIfiSPIMsgCounts;
        if( oldspi != newspi )
            SensorValue test3 ] = 1;    

        // note for next time around
        oldspi = newspi;
        
        // next task
        abortTimeslice();            
        }
}

task main()
{
    spiSync();

    startTask( task1 );
    startTask( task2 );    
    startTask( task3 );    

    while(1)
      {
      SensorValue testmain ] = !SensorValue testmain ];
      abortTimeslice();
      }
}

Task1 is synchronized at SPI comms + 8mS
Task2 is synchronized at SPI comms + 4mS
Task3 just shows, by detection, where the SPI comms counter changes.
main is always running so you can see where the SPI comms is occurring.

and the scope capture showing the result. This is for the mentors that have RTOS experience and will understand what is happening here, students, don’t worry about this.

scope_task_11.png

2 Likes

Great stuff,

I had a few more questions about tasks and variables, I hope this is the correct thread to post them.

I am assuming that since this is an interpretive machine, the opcodes are atomic and all data operations, including operations on floating point numbers, are guaranteed to be atomic, i.e. finish the operation and not allow the operation of retrieving or storing any type of variable to be interrupted by another task (not worried about it being interrupted by system interrupts).

So, if I have a global variable that is shared amongst 2 tasks, one sets the value (sensor) and one reads it (velocity control), I would be concerned that operations on the variable may be trashed in midstream of an equation evaluation (just a simplified example).

In the below case I would be concerned that (xx) could be executed, then a task switch occurs to task 2, changes the global x, then relinquishes control, then the "output = x + (xx)" equation continues to evaluate with “x” as a new value so we really end up with output = x(new) + (x(old) * x(old)).

Or, do we need to treat all data as shared resources and utilize semaphores?

Also, if a made a local variable in task 1 named z, could I use “z = x” at the beginning of the function and just use z in place of x? In this case, are we guaranteed the compiler will not optimize the “z = x” away?


int x;

task task1()
{
    while(1)
    {
        output = x + (x*x);
        wait1Msec(20);
    }

}

task task2()
{
    while(1)
    {
        x = SensorValue[test1];
        wait1Msec(15);
    }
}

task main()
{
    while(1)
    {
        abortTimeslice();
    }
}

1 Like

As good as any.

Yes, you don’t have to worry about things like assignment to 32 bit numbers being able to be interrupted by the scheduler.

If the write and read operations become single opcodes then there is no need to worry.

This case could be a problem, simplest way to check this is to look at the opcodes generated by the code (view->Assembly menu item)

task task1()
//
//Code segment: task1(); Task: 1
// 
{
    int output;
    
    while(1)
    {
        output = x + (x*x);
0000: C600080000                output:S00(slong) = x:G00(slong)    // long/float
0005: BF2B0400080000080000      S04(slong) = x:G00(slong) * x:G00(slong)
000F: 84002B0400                output:S00(slong) += S04(slong)     // long
        wait1Msec(20);
0014: 44001400                  wait1Msec(20)                      
    }
0018: 79E7BranchNear(L0000)                   // Jump to end Expression
}

So the line of code “output = x + (x*x);” becomes three opcodes. The equivalent C code would be

        int y = x;
        int z = (x*x);
        output = y + z;

Lets see. Here is the Assembly output.


task task1()
//
//Code segment: task1(); Task: 1
// 
{
    int output;
    
    while(1)
    {
        int z = x;
0000: C604080000              z:S04(slong) = x:G00(slong)         // long/float
        output = z + (z*z);
0005: C6002B0400              output:S00(slong) = z:S04(slong)    // long/float
000A: BF2B08002B04002B0400    S08(slong) = z:S04(slong) * z:S04(slong)
0014: 84002B0800              output:S00(slong) += S08(slong)     // long
        wait1Msec(20);
0019: 44001400wait1Msec(20)                      
    }
001D: 79E2BranchNear(L0000)                   // Jump to end Expression
}

That works, the compiler does do some optimization, there are some settings in the preferences that will change what it does, but they are rarely used and I don’t think would affect using a local variable. I suggest leaving compiler optimization at the default settings, there have been instances of missed bugs in the past when QA has not tested the compiler with settings other than the defaults.

1 Like

Thanks, I looked for the “volatile” keyword and couldn’t find it, so I assume RobotC doesn’t have it. In that case, we would rely on the compiler as you show above. So it seems best, if you are sharing data amongst tasks, that you copy the shared variable to a local at the top of your infinite while() loop, then operate on it, then sleep for your desired time, i.e. 20 msec, then repeat.

Yes, this could be handled with semaphores and such (locking out the resource until the equation is complete) but it seems easiest to describe to the kids by holding it locally. The equation is just a simple example, the same thing would happen if you were checking a global variable in an if()then()else()if()… type statement, it could change after evaluation of the first if() and leave you in a bad state.

Most importantly, shared global variables should be treated like shared resources, i.e. UART, when using tasks I would think.

1 Like

Thank you @jpearman, the code looks less complicated that I was afraid it would need to be for such synchronization.

It, certainly, makes sense to take all sensor readings and command motors on ports 2-9 about 1 ms before the SPI communication takes place.

If this lets you reduce command delay from 35 ms to 20 ms it could only improve stability of PID for any high speed acceleration application.

1 Like

I must have been taught this a long time ago and forgot the reason. I like to have the kids use local variables of the sensor value inside tasks for PID management but had no idea it was actually meaningful. Thanks James!

It also helps from season to season to replace the sensor value names in one spot and keep the rest of the code relatively intact.

It really depends on how much work the task will do. In the trivial example we use above.

int x;
task task1()
{
    int output;    
    while(1)
    {
        output = x + (x*x);
        wait1Msec(20);
    }
}

This would not actually be an issue as the loop only has four opcodes, they are guaranteed to all run, even when using the maximum number of tasks allowed (20 on the cortex). The time slice for that task would be 1000/20 = 50 opcodes in that extreme case. Remember this from the first post.

  • All tasks, including high priority tasks, will only be scheduled to run when the currently running task completes its time slice, calls abortTimeslice or wait1Msec.

The trivial task will always run until the wait statement.

1 Like

Thanks,

I agree, but was making a general statement since, unless we want to count the opcodes, we aren’t sure when that limit is hit.

Also, I guess I am concerned about using sensor values (or any value updated by interrupt tasks) for the same reason and thought it best to grab those to locals at the top of the loop before use.

This could be an issue in the main() without using tasks.

As seen below, but using the SensorValue] array that is updated by RobotC, if an interrupt comes in the middle of the execution of the if() else() then SensorValue] might return different values for comparison in the second else()?

for example:


#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               !!*//

int x;
task task1()
{
    int output;    
    while(1)
    {
        if((SensorValue[test1] == 0) && (SensorValue[test2] == 0))
        {
            do something 1;
        } else if((SensorValue[test1] == 1) && (SensorValue[test2] == 1))
        {
            do something 2;
        } esle .....
.......
        wait1Msec(20);
    }
}

1 Like

Understood.

Certainly race conditions can occur, not much we can do about that. Semaphores work at the ROBOTC VM level, interrupt handlers do not check them. Depending on the sensor, SensorValue will be handled differently. Reading a digital port causes a GPIO read on the requested pin, every time SensorValue is called the pin will be re-read. Code that expects consistent values should probably read the pin once and then base further decisions on that value. This is not specific to ROBOTC but a common issue in all embedded systems, but you know that. Other sensors will be a little different, gyro value is calculated every mS, quad encoder values change in an interrupt handler. Only advanced programmers (or programmers with advanced mentors :slight_smile: ) should worry about this. Programmers that want to get 100% out of the cortex and have fine grain control over every aspect of the code should probably switch to one of the open source firmware solutions available.

1 Like

Yeah… I heard of something called Concave, Contained, … Or some such thing :slight_smile:

One last question. Are system interrupts disabled during opcode VM interpreting? Like disabled, interpreted, back on, back off, interpreted, etc…,.?