Advanced Programming With NQC

 

Building more complex robots requires more knowledge about programming.  Intelligent Robots or Complex Robots require complex programming, so that the robot can respond to its environment, remember actions, and repeat tasks.  The following pages contain more commands for Not Quite C.  Play with the commands in programs one through four to see how the commands work. 

More About Motors

The following is a list of motor commands.  Try them out by changing the four original programs that we used.  Do not change the task the robot completes; only change the lines that cause the motors to work by using some of the following lines.  Try all the other commands that are not needed to complete the tasks. 

 

            On(‘motors’);                                   Switches the motors on

            Off(‘motors’);                                 Switches the motors off

            Float(‘motors’);                               Switches the motors off smoothly

            Fwd(‘motors’);                                 Switches the motors forward (but does not make them drive)

            Rev(‘motors’);                                  Switches the motors backward (but does not make them drive)

            Toggle(‘motors’);                             Toggles the direction of the motors (forward to backwards and back)       

            OnFwd(‘motor’);                              Switches the motors forward and turns them on

            OnRev(‘motors’);                              Switches the motors backwards and turns them on

            OnFor(‘motors’, ‘ticks’);                   Switches the motors on for ticks time

SetOutput(‘motors’, ‘mode’);          Sets the output mode (OUT_ON, OUT_OFF,

OUT_FLOAT)

            SetDirection(‘motors’, ‘dir’);            Sets the output direction (OUT_FWD, OUT_REV, OUT_TOGGLE)

            SetPower(‘motors’, ‘power’);            Sets the output power (0-7)

Useful Tricks

Timers

The RCX has four built in timers.  These timers tick in increments of 1/10 of a second.  The timers are numbered from 0 to 3.  You can reset the value of a timer with the command ClearTimer() and get the current value of the timer with  Timer().  In the beginning of your program you would clear your timer then use the timer in your program.  Replace the Wait()’s in prog1 by using the timer (Hint: you will need to use a loop). 

 

**Don’t for get that timers work in ticks of 1/10 of a second, while the wait command uses ticks of 1/100 of a second.

 

Display

It is possible to control the display of the RCX in two different ways.  First of all, you can indicate what to display: the system clock, one of the sensors or one of the motors.  This is equivalent to using the black view button on the RCX.  To set the display type use the command SelectDisplay().  The following is a list of the different views that are available.

 

            SelectDisplay(DISPLAY_SENSOR_1);               // INPUT 1

            SelectDisplay(DISPLAY_SENSOR_2);              // INPUT 2

            SelectDisplay(DISPLAY_SENSOR_3);              // INPUT 3

            SelectDisplay(DISPLAY_OUT_A);                    // OUTPUT A

            SelectDisplay(DISPLAY_OUT_B);                    // OUTPUT B

            SelectDisplay(DISPLAY_OUT_C);                    // OUTPUT C

            SelectDisplay(DISPLAY_WATCH);       // SYSTEM CLOCK

 

Datalogging

The RCX can store values of variables, sensor readings, and timers, in a piece of memory called the datalog. The values in the datalog cannot be used inside the RCX, but they can be read by your computer. This is useful to check what is going on in your robot. RCX Command Center has a special window in which you can view the current contents of the datalog.

Using the datalog consists of three steps: First, the NQC program must define the size of the datalog, using the command CreateDatalog(‘size’). This also clears the current contents of the datalog. Next, values can be written in the datalog using the command AddToDatalog(‘value’). The values will be written one after the other. (If you look at the display of the RCX you will see that one after the other, four parts of a disk appear. When the disk is complete, the datalog is full.) If the end of the datalog is reached, nothing happens. New values are no longer stored. The third step is to upload the datalog to the PC. For this, choose in RCX Command Center the command Datalog in the Tools menu. Next press the button labelled Upload Datalog, and all the values appear. You can watch them or save them to a file to do something else with them (You could graph the values over a change in time and see if there is a pattern).

More Arguments

      You have used arguments when using condition statements such as if or until.

The following list contains some arguments that you can use in condition statements. 

You may recognize some of them.  Try them out on our basic programs or one of your own.

 

1. != (not equal)

§         If you want a variable or an input value to not be equal to a specific number than try this.

Ex) . . .

      if(SENSOR_1 != threshold)

      . . .

      The task will be completed if SENSOR_1 is not equal to the threshold.

2.       == (equal to)

§         If you want a variable or an input value to be a specific number than try this.

Ex) . . .

      if(SENSOR_1 == threshold)

      . . .

                              The task will be completed if SENSOR_1 is equal to the threshold.

      3.  < (less than)

§         If you want a variable or an input value to be less than a specific number than try this.

Ex) . . .

      if(SENSOR_1 < threshold)

      . . .

                              The task will be completed if SENSOR_1 is less than to the threshold.

4.       <= (less than equal to)

§         If you want a variable or an input value to be less than or equal to a specific number than try this.

Ex) . . .

      if(SENSOR_1 <= threshold)

      . . .

      The task will be completed if SENSOR_1 is less than or equal to the threshold.

5.       > (larger than)

§         If you want a variable or an input value to be greater than a specific number than try this.

Ex) . . .

      if(SENSOR_1 > threshold)

      . . .

      The task will be completed if SENSOR_1 is greater than the threshold.

6.       >= (larger than equal to)

§         If you want a variable or an input value to be greater than or equal to a specific number than try this.

Ex) . . .

      if(SENSOR_1 >= threshold)

      . . .

      The task will be completed if SENSOR_1 is greater than or equal to the threshold.

7.       &&  (and)

§         If you want two conditions to be true in order for the task to be completed

Ex) . . .

      if(x<5 && x>1)

      . . .

                              The task will only be completed if x is true for both x<5 and x>1.

8.        || (or)

§         If you want one or both condition(s) to be true in order to complete the task use this.

Ex) . . .

      if(x<5 || x>1)

      . . .

                              The task will be completed if x is true for one or both x<5 and x>1.

 

9.       true or 1

§         If you want a task to take place only when it is true use this (true is the same thing as 1 or pressed).

Ex) . . .

      if(x == true)

      . . .

                              The task will be completed if x is 1.

10.    false or 0

§         If you want a task to take place when the statement is not true than use this (false is the same thing as 0 or not_pressed).

Ex) . . .

      if(x == false)

      . . .

                              The task will be completed if x is 0.

 

·         Don’t forget that you can use these arguments with ANY condition statement.

Other Condition Statements

We have previously discussed condition statements (if, else, until).  Condition statements are needed in order to distinguish priorities and tasks.  The following are some more condition statements that we have not yet discussed.  Try them out on our basic programs or one of your own.

 

1. while (“condition”)  { “code” }

§         Repeats the code the amount of times that the condition is true.

Ex) . . .       

 while (x > 2)

          {

                  OnFwd(OUT_A + OUT_C);

                  x=x-1;

           } . . .

Try using . . .while (true) {“code”} . . .

What does it do and what is it similar to?

2.     else if (“condition”)  {“code”}

§         Must be pared with an if statement; you do not have to have an else, but you can.

§         Performs code if condition is true.

Ex) . . .

      if(SENSOR_1 == threshold)

                  OnFwd(OUT_A + OUT_B);

      else if(SENSOR_1 > threshold)

                  OnRev(OUT_A + OUT_B);

      else

                  Off(OUT_A + OUT_B);

      . . .

 

3.     do { “code” } while (“condition”)

§         Does code at least once.

§         If condition is true repeats code until false.

Ex) . . .

      do {

                  Off(OUT_C);

                  OnFwd(OUT_A);

                  Wait(100);

                  OnRev(OUT_C);

                  Wait(100);

                  x=x+1;

      }  while(x < 3)

      . . .

Nested Statements

Nested Statement are just condition statements that contain other condition statements inside of them.

            Ex) . . .

                  while(“condition”)

                        { “code”

          if(“condition”)  { “code” }

                         }

            Ex) . . .

if(“condition”)

  {  “code”

      if(“condition”)  { “code” }

      else {“code” }  

  }

. . .

§         An else is pared with the if nearest to it unless it is outside the nested statement the nearest if is contained in.

Ex) . . .

      if(“condition”)

        {  “code”

             if(“condition”)  { “code” }

             else {“code” }   //paired with preceding one

        }

      . . .

      if(“condition”)

        {  “code”

             if(“condition”)  { “code” }

         }

      else {“code” }   //paired with outer if

      . . .

Tasks

In program four you saw dual operating tasks.  One task kept track of the values of the light sensor on the left, while the other watched the right. 

 

A NQC program consists of at most 10 tasks. Each task has a name. One task must have the name main, and this task will be executed. The other tasks will only be executed when a running task tells them to be executed using a start command. From this moment on both tasks are running simultaneously (so the first task continues running). A running task can also stop another running task by using the stop command. Later this task can be restarted again, but it will start from the beginning; not from the place where it was stopped.

 

Let me demonstrate the use of tasks. Put your touch sensor on your robot. We want to make a program in which the robot drives around in squares. But when it hits an obstacle it should react to it. It is difficult to do this in one task, because the robot must do two things at the same moment: drive around (that is, switching on and off motors at the right moments) and watch for sensors. So it is better to use two tasks for this, one task that drives the squares; the other that reacts to the sensors. Here is the program.

 

task main()

{

  SetSensor(SENSOR_1,SENSOR_TOUCH);

  start check_sensors;

  start move_square;

}

 

task move_square()

{

  while(true)

  {

    OnFwd(OUT_A + OUT_C); Wait(100);

    OnRev(OUT_C); Wait(85);

  }

}

 

task check_sensors()

{

  while(true)

  {

    if (SENSOR_1 == 1)

    {

      stop move_square;

      OnRev(OUT_A + OUT_C); Wait(50);

      OnFwd(OUT_A); Wait(85);

      start move_square;

    }

  }

}

 

The main task just sets the sensor type and then starts both other tasks. After this task main is finished. Task move_square moves the robot forever in squares. Task check_sensors checks whether the touch sensor is pushed. If so it takes the following actions: First of all it stops task move_square. This is very important. check_sensors now takes control over the motions of the robot. Next it moves the robot back a bit and makes it turn. Then it can start move_square again to let the robot again drive in squares.

 

It is very important to remember that tasks that you start are running at the same moment. This can lead to unexpected results. Solutions are explained in detail later on.

Subroutines

Sometimes you need the same piece of code at multiple places in your program. In this case you can put the piece of code in a subroutine and give it a name. Now you can execute this piece of code by simply calling its name from within task. NQC (or actually the RCX) allows for at most 8 subroutines. Let us look at an example.

 

sub turn_around()

{

  OnRev(OUT_C); Wait(340);

  OnFwd(OUT_A + OUT_C);

}

task main()

{

  OnFwd(OUT_A + OUT_C);

  Wait(100);

  turn_around();

  Wait(200);

  turn_around();

  Wait(100);

  turn_around();

  Off(OUT_A + OUT_C);

}

 

In this program we have defined a subroutine that makes the robot rotate around its center. The main task calls the subroutine three times. Note that we call the subroutine by writing down its name with parentheses behind it. So it looks the same as many of the commands we have seen. Only there are no parameters, so there is nothing between the parentheses.

Some warnings are in place here. Subroutines are a bit weird. For example, subroutines cannot be called from other subroutines. Subroutines can be called from different tasks but this is not encouraged. It very easily leads to problems because the same subroutine might actually be run twice at the same moment by different tasks. This tends to give unwanted effects. Also, when calling a subroutine from different tasks, due to a limitation in the RCX firmware, you cannot use complicated expressions anymore. So, unless you know precisely what you are doing, don’t call a subroutine from different tasks!

Inline functions

As indicated above, subroutines cause certain problems. The nice part is that they are stored only once in the RCX. This saves memory and, because the RCX does not have so much free memory, this is useful. But when subroutines are short, better use inline functions instead. These are not stored separately but copied at each place they are used. This costs more memory but problems like the ones with using complicated expressions, are no longer present. Also there is no limit on the number of inline functions.

 

Defining and calling inline functions goes exactly the same way as with subroutines. Only use the keyword void rather than sub. (The word void is used because this same word appears in other languages like C.)  Inline functions have another advantage over subroutines.  They can have arguments.  Arguments can be used to pass a value for certain variables to an inline function.  For example here is an inline function with an argument:

 

void turn (int turntime) {

   OnFwd(OUT_A); Wait(turntime);

}

task main() {

   OnFwd(OUT_A + OUT_C);   Wait(100);

   turn(50);

    OnRev(OUT_A + OUT_C);  Wait(100);

   turn(100); }

  

Parallel tasks

 

As has been indicated before, tasks in NQC are executed simultaneously, or in parallel as people usually say. This is extremely useful. It enables you to watch sensors in one task while another task moves the robot around, and yet another task plays music. But parallel tasks can also cause problems. One task can interfere with another.

A wrong program

Consider the following program. Here one task drives the robot around in squares and the second task checks for the touch sensor. When the sensor is touched, it moves a bit backwards, and makes a 90-degree turn.

 

task main()

{

  SetSensor(SENSOR_1,SENSOR_TOUCH);

  start check_sensors;

  while(true)

  {

    OnFwd(OUT_A + OUT_C); Wait(100);

    OnRev(OUT_C); Wait(85);

  }

}

 

task check_sensors()

{

  while(true)

  {

    if (SENSOR_1 == 1)

    {

      OnRev(OUT_A + OUT_C);

      Wait(50);

      OnFwd(OUT_A);

      Wait(85);

      OnFwd(OUT_C);

    }

  }

}

 

This probably looks like a perfectly valid program. But if you execute it you will most likely find some unexpec ted behavior. Try the following: Make the robot touch something while it is turning. It will start going back, but immediately moves forward again, hitting the obstacle. The reason for this is that the tasks may interfere. The following is happening. The robot is turning right, that is, the first task is in its second Wait statement. Now the robot hits the sensor. It start going backward, but at that very moment, the main task is ready with Wait and moves the robot forward again; into the obstacle. The second task is at Wait at this moment so it won’t notice the collision. This is clearly not the behavior we would like to see. The problem is that, while the second task is at Wait we did not realize that the first task was still running, and that its actions interfere with the actions of the second task.

Stopping and restarting tasks

One way of solving this problem is to make sure that at any moment only one task is driving the robot. Look at the revision of the program.

 

task main()

{

  SetSensor(SENSOR_1,SENSOR_TOUCH);

  start check_sensors;

  start move_square;

}

 

task move_square()

{

  while(true)

  {

    OnFwd(OUT_A + OUT_C); Wait(100);

    OnRev(OUT_C); Wait(85);

  }

}

 

task check_sensors()

{

  while(true)

  {

    if (SENSOR_1 == 1)

    {

      stop move_square;

      OnRev(OUT_A + OUT_C); Wait(50);

      OnFwd(OUT_A); Wait(85);

      start move_square;

    }

  }

}

 

The trick is that the check_sensors task only moves the robot after stopping the move_square task. So this task cannot interfere with the moving away from the obstacle. Once the backup procedure is finished, it starts move_square again.

 

Even though this is a good solution for the above problem, there is a problem. When we restart move_square, it starts again at the beginning. This is fine for our small task, but often this is not the required behavior. We would prefer to stop the task where it is and continue it later from that point. Unfortunately this cannot be done easily.

Using semaphores

A standard technique to solve this problem is to use a variable to indicate which task is in control of the motors. The other tasks are not allowed to drive the motors until the first task indicates, using the variable, that it is ready. Such a variable is often called a semaphore. Let sem be such a semaphore. We assume that a value of 0 indicates that no task is steering the motors. Now, whenever a task wants to do something with the motors it executes the following commands:

 

until (sem == 0);

sem = 1;

// Do something with the motors

sem = 0;

 

So we first wait till nobody needs the motors. Then we claim the control by setting sem to 1. Now we can control the motors. When we are done we set sem back to 0. Here you find the program above, implemented using a semaphore. When the touch sensor touches something, the semaphore is set and the backup procedure is performed. During this procedure the task move_square must wait. At the moment the back-up is ready, the semaphore is set to 0 and move_square can continue.

 

int sem;

task main()

{

  sem = 0;

  start move_square;

  SetSensor(SENSOR_1,SENSOR_TOUCH);

  while(true)

  {

    if (SENSOR_1 == pressed)

    {

      until (sem == 0); sem = 1;

      OnRev(OUT_A + OUT_C); Wait(50);

      OnFwd(OUT_A); Wait(85);

      sem = 0;

    }

  }

}

task move_square()

{

  while(true)

  {

    until (sem == 0); sem = 1;

    OnFwd(OUT_A + OUT_C);

    sem = 0;

    Wait(100);

    until (sem == 0); sem = 1;

    OnRev(OUT_C);

    sem = 0;

    Wait(85);

  }

}

 

You could argue that it is not necessary in move_square to set the semaphore to 1 and back to 0. Still this is useful. The reason is that the OnFwd() command is in fact two commands. You don’t want this command sequence to be interrupted by the other task.

Semaphores are very useful and, when you are writing complicated programs with parallel tasks, they are almost always required. (There is still a slight chance they might fail. Try to f