Programming the RCX


Saturday, 02/03/01

Controlling a robot in our case will consist of writing a program that makes use of the robot's motors and sensors. The robot is just a machine with both computational and physical capabilities and the programs we write make use of the former to take advantage of the latter.

In terms of support for writing robot control programs, we'll want some sort of simple operating system (OS) to facilitate working with the motors and sensors (robot I/O), support for basic data types and operations on those data types (e.g., a math library), support for flow of control (e.g., conditionals, loops, and threads), and, finally, utilities for interacting with the robot (e.g., downloading, starting, stopping, and debugging programs). In this lecture, we'll consider three systems, RIS, NQC and legOS, that provide such support in varying degrees. We'll concentrate on Dave Baum's NQC (for "Not Quite C") which depends on the standard firmware, but we'll contrast NQC's capabilities with RIS, an easy to use but limited graphical programming environment that comes with the Lego Mindstorms Robot Invention System, and legOS, a very flexible programming environment based on the C programming language which depends on its own special firmware. (Firmware is just software that resides on the RCX semi-permanently (as long as the batteries hold out) and that typically provides the sort of functionality that one usually expects from the kernel of an OS.)

The Robot Invention System (RIS) comes with the software for a graphical programming language and environment that serves to both write and download programs to the RCX. RIS code is restricted in a number of ways. First, the language itself has limitations, e.g., it allows multiple threads of control called stacks, but only nine of them, it allows loops and conditionals, but only of a limited sort, it allows counters, but not other sorts of math routines or the use of variables. Despite these limitations, the programs that have been developed using RIS are surprisingly sophisticated. The second way in which RIS code is restricted is that it relies on a layer of firmware that implements a simple OS and an interpreter to run RIS programs much as the Java virtual machine in your browser allows you to run programs specified as Java classes and represented in bytecodes that can be easily sent from one computer to another. Indeed, the standard firmware implements a bytecode interpreter for the RCX virtual machine.

Dave Baum's NQC suffers from the second of these two limitations (it relies on the standard firmware that comes with the RIS) but avoids some of the limitations of the RIS language by providing language constructs that allow the programmer to take full advantage of the standard firmware. Baum was one of the first to take advantage of Kekoa Proudfoot's efforts to reverse engineer the RCX brick and the standard firmware. Before anyone could design better tools for programming the RCX they had to know how to talk to the the RCX (i.e., the protocol for transmitting data to the RCX IR port) and then how to get the RCX to do useful stuff. Proudfoot dissected an RCX brick and revealed many of its secrets.

"At the core of the RCX is a Hitachi H8 microcontroller with 32K of external RAM. The microcontroller is used to control three motors, three sensors, and an infrared serial communications port. An on-chip, 16K ROM contains a driver that is run when the RCX is first powered up. The on-chip driver is extended by downloading 16K of firmware to the RCX. Both the driver and firmware accept and execute commands from the PC through the IR communications port. Additionally, user programs are downloaded to the RCX as byte code and are stored in a 6K region of memory. When instructed to do so, the firmware interprets and executes the byte code of these programs." from Kekoa Proudfoot's web page on reverse engineering the the RCX brick.

The RCX (see Kekoa Proudfoot's web page for top and bottom photos of the single-board computer that's at the heart of the RCX) is quite impressive as an engineering artifact. The RCX combines ports, timers, motor controller ICs, analog-to-digital circuitry, and a small but flexible display on one circuit board whose size is less that 6 by 10 cm. The onboard ROM provides the BIOS routines to handle simple I/O and, in particular, download simple programs including the firmware that implements more sophisticated OS functionality.

Top

Bottom

The standard RCX firmware interprets the compiled NQC bytecode. The notion of interpreting (versus compiling) a program is potentially confusing. There is no such thing as an "interpreted language"; eventually every program has to be reduced to a series of instructions and data appropriate for the target machine (the Hitachi H8 Microcontroller in the case of the RCX). Exactly when this reduction occurs varies widely in the manner in which programming languages are implemented. We can abstract from the instruction set and memory architecture for a given machine (hardware) by adding layers of software that provide useful and often needed functionality. The OS typically provides a range of such functionality for program (and process) control and I/O handling but there is no reason why we can't enlarge the functionality supported by the OS however we like. Developing a programming language amounts to providing a layer of abstraction that supports a particular way of thinking about programs.

The RCX firmware interprets a particular bytecode and NQC (the system) provides a simple bytecode compiler for translating NQC programs (i.e., programs written in the NQC syntax) to bytecode sequences that can be transmitted over the IR link to the RCX and interpreted by the RCX firmware. The standard RCX firmware is said to implement a virtual machine (VM) for such bytecode sequences. (The bytecode stream for an RIS or NQC program is a sequence of instructions for the NQC VM. Each instruction consists of a one-byte opcode followed by zero or more operands. The opcode indicates the action to take. If more information is required before the VM can take the action, that information is encoded into one or more operands that immediately follow the opcode. See Kekoa's opcode listing for a glimpse of the machine language for the VM implemented by the standard RCX firmware.)

People have put a lot of effort into making these VMs fast and there are several techniques such as threaded bytecodes that reduce the perceived overhead of interpretation. As an engineering decision, it is by no means obvious that one should always write a native compiler rather than choose the bytecode plus fast VM route. Not to mention, in this day and age of bytecode virtual machines, the distinction between interpretation and compilation is somewhat moot anyway. What some of you might think of as an interpreter may very well not be; environments for ML, Lisp, and Scheme are kind enough to mask the compile-link phase behind the read-evaluate-print loop. That is, you can't tell the implementation technology simply by sitting at the console. (Thanks to Shriram Krishnamurthi for his insights regarding bytecode interpretation and virtual machines; some of the above (undoubtedly the part that makes sense) is paraphrased or excerpted without attribution from comments he's made to me.)

Here are some excerpts from Kekoa Proudfoot's analysis of the standard RCX firmware byte code interpreter. (I suggest you directly visit his page; the only reason that I've included the excerpt below is to ensure that I have this material available when I give this lecture and don't have to contend with the embarrassment of a machine down or a slow connection either here at Brown or at Stanford. Proudfoot's webpage also includes the slides for a talk that he gave which includes information on the RCX VM.)


Tasks and subroutines

8 subroutines per program
10 tasks per program

Memory map

The memory map contains addresses, stored as big endian shorts.  The layout
is as follows:

   index  description

   00-07  prog 0 subs 0-7 start
   08-15  prog 1 subs 0-7 start
   16-23  prog 2 subs 0-7 start
   24-31  prog 3 subs 0-7 start
   32-39  prog 4 subs 0-7 start
   40-49  prog 0 tasks 0-9 start
   50-59  prog 1 tasks 0-9 start
   60-69  prog 2 tasks 0-9 start
   70-79  prog 3 tasks 0-9 start
   80-89  prog 4 tasks 0-9 start
   90     datalog start
   91     datalog next
   92     first free
   93     last valid

The addresses increase monotonically, so the end of the space allocated 
for one item can usually be found by looking at the next address in the map.

The first address is ceba and the last address is e6b9.  Therefore the
total size of user memory is exactly 6K.

The memory map says something about how user memory is managed inside the
RCX.  Whenever an item is added, later items are moved up in memory to make 
room for the new item.  Whenever an item is deleted, later items are moved
down in memory to consolidate free space.

An empty subroutine takes up one byte of space, while an empty task takes
up zero bytes of space.

When a start download or set datalog command is sent, it might fail with an
error code related to amount of memory.  Memory map was used to confirm
this.

Sources

Many commands take parameters as source and argument
Sources are like addressing modes, many of which are unconventional:

   0   variable [0..31]
   1   timers [0,1,2,3]
   2   immediate
   3   motor state [0,1,2] [0x07=power 0x08=fwd 0x40=off 0x80=on 0x00=float]
   4   random [return value is in 0 to argument, inclusive]
   5   reserved
   6   reserved
   7   reserved
   8   current program number
   9   sensors [0,1,2]
   10  sensor type [0,1,2]
   11  sensor mode [0,1,2]
   12  raw sensor value [0,1,2]
   13  boolean sensor value [0,1,2]
   14  minutes on clock/watch [0]
   15  message [0]

Sources 5, 6, and 7 are never valid.  Do not use them.

Loops

The byte code interpreter seems to include a stack of loop counters
The maximum loop counter stack depth is four
Set loop counter pushes stack down and sets the top value
Decrement loop counter and branch decrements counter on top of stack
   Decrement before test
   When top counter is less than zero, stack is popped and branch is taken
   Otherwise branch is not taken
Decrement loop counter and branch only branches forward
   But you can use a second branch to go backwards

Sounds

Sounds seem to be buffered, with buffer size around eight
Sounds are lost when buffer is full
Sounds seem to be asynchronous, meaning their calls seem to return immediately


Hopefully you've all read Baum's introduction to NQC in the course textbook. Let's review what you really need to know in order to start programming. First, you need to know some basic syntax: what's a valid statement, identifier, whitespace, comment, etc. Next, you should try to write your first program and understand the use of a "main" task, additional tasks as threads of control, functions, and subroutines. Let's consider a very simple code fragment and dissect what's going on syntactically and semantically.


/* 
   This is a comment that extends over multiple lines 
   and adopts C-like initiation and termination. 
*/

// Another form of comment preceding a define (or macro).
#define DRIVE OUT_A + OUT_B 
#define BUMP SENSOR_1

// This is a global variable accessible from all tasks.
int x = 0 ;

// This is an NQC inline function; it can't return anything.
void my_function(int w, const int x, int & y, const int & z) {
   // w and x are call by value; y and z are call by reference;
   // y must be a variable; x must be a constant; z can be
   // any expression, can't be modified and is reread each time 
   // it's referenced - consider consider a sensor in a loop.
   y = (w + (x * y) / (z * y)) ;
}

/*
   Subroutines can't call themselves or other subroutines.
   There are a maximum of eight subroutines per program.
   Subroutines allow for a simple form of code reuse. 
*/

// This is an NQC subroutine; it can't take any arguments.
sub my_subroutine() {
   // Subroutines can call functions.
   my_function(x, (3 * 2), x, (- BUMP 1)) ;
}

// Here we define a task that can be stopped or started.
task my_task() {
   // This is a local variable accessible only in this task.
   int y = 0 ; 
   // Here we call our only subroutine.
   my_subroutine() ;
   // Loop forever or at least until the task is stopped.
   while (true) {
      if ( BUMP == 1 ) {
         // What's going to happen to y here? 
         y = y + 1 ;
         my_function(x, 1, y, 2) ;
      }
   }
}

// Each program can have as many as ten tasks.
task your_task() {
}

// Every program has to have a main task.
task main() {
   // Initialize the touch sensor.
   SetSensor( BUMP, SENSOR_TOUCH ) ;
   // Start one subtask.
   start my_task ;
   // Start another subtask. 
   start your_task ;
   // Stop one of the tasks.
   stop your_task ;
}


Now, let's consider some of the limitations of the standard firmware as reflected in the limitations of the NQC bytecode compiler which respects those limitations. In particular: What is the consequence of functions that are always inlined and return only void? Why are there only 8 subroutines per program and why can't they take arguments or return values? Why does the original firmware (RCX 1.0) only allow 32 variables locations, which are of size small int, and only global? Why are only four levels of nested loops allowed? Why only 10 tasks (nine plus a main task)? The new firmware, RCX 2.0, allows local variables (specific to a given task) as well as global variables (shared across all tasks). This means that each of the RCX's ten tasks has its own 16 local locations plus the original 32 global locations for a total of 32 + (10 * 16) = 192 storage locations. RCX 2.0 also provides mechanisms for controlling access to resources like motors that are shared across tasks, monitoring events, working with simple arrays, and using the IR port to send arbitrary bytes of data. NQC with RCX 2.0 is adequate for handling some pretty impressive programming tasks, but it's obviously limited.

NQC is pretty restrictive if you're used to programming in C, C++, Java, or just about any modern programming language. What's the alternative to NQC if you feel too hemmed in by the language and it's reliance on the standard RCX firmware? There are a number of answers to the last question including pbForth (for programmable brick Forth) and legOS both of which have their own custom firmware to replace the standard firmware that comes with the RIS. Forth is a programming language that's been around for some time. Ralph Hempel's pbForth includes extensions to Forth for programming the RCX and depends on special firmware for the RCX implementing a bytecode interpreter for pbForth. The advantage of interpreters such as those implemented by the standard firmware, the pbForth firmware, and interactive C (which is often used with Handy Boards) is that it is easy to interact with and debug programs. The disadvantages most often cited are the reduction in speed that can accompany interpretation and the fact that the "compiler" has to be resident on the robot taking up precious memory. The firmware for pbForth takes less space than the standard firmware leaving 14KB for user programs compared with 6KB in the case of the standard firmware.

So what is legOS? Basically it is new firmware for the RCX that allows you to use the C programming language along with many useful C libraries to write software for the RCX brick. Why is it better than, say, NQC? Well, "better" is open to interpretation (no pun intended)? You could, after all, program in H8 machine language and in some sense that would be "better" than programming in NQC. The legOS provides the programmer with more flexibility (read "more rope to hang yourself") and more control over such things as memory and process control. LegOS supports a more general thread functionality (preemptive multitasking with unix-like process control), enhanced interprocess communication and support for such control constructs as semaphores, a floating point library, full control over the RCX's LCD display, and more powerful data structures including arrays, doubles and strings. In particular, the only limit on the number of variables, processes, subroutines, etc is the amount of available memory.

Along with this increased power come the disadvantages of more difficult debugging, less built-in protection for overwriting protected memory, and generally more complexity. It's nice to have a floating point library but executing (x * 0.85) can take twice as long as executing ((x * 85) / 100) and while an int takes two bytes, a double takes four. It's nice to at least have the option of trading space and time for convenience and precision, however. When you hear about the engineers developing software for the Sony Aibo or the latest i-Cybie from Tiger Electronics (these little robots require very sophisticated software), you wonder how they can develop such complicated software without the use of industrial-strength program development environments.


Previous Home Next


Last updated on February 4, 2001.

Home