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