Ben's Notebook

Noteworthy topics, mostly technical

RCX Deep Dive

Part 2: TLG official firmware

The only permanent storage on the RCX is the 16-kilobyte ROM, which is not enough to manage the entire unit. The ROM contains a basic interface with 5 built-in programs, but firmware is needed to run custom code. The firmware lives in RAM alongside the user programs and often calls chunks of code in ROM to control the hardware. There is no flash memory - when power is removed from the board and the capacitors have discharged, all firmware and programs are wiped. This is why so many RCX bricks have been damaged by battery leakage - nobody wanted to pull out the batteries after each play session and have to spend 5 annoying minutes re-downloading everything next time.

Interpreter

Programs and commands sent by the Mindstorms software take the form of bytecode: compact code that describes "high-level" operations. The Hitachi processor can't execute it directly. Rather, the firmware contains an interpreter that reads the bytecode and performs the corresponding low-level operations. If you have ever used the NQC language, it compiles a C-like language to bytecode, allowing textual code to run on the stock firmware.

In the RCX SDK, Lego refers to the interpreter as a VM (virtual machine). In a sense, the firmware is a VM that emulates a theoretical computer that executes bytecode directly.

The interpreter has access to 32 global variables (signed 16-bit integers), a pseudorandom number generator, three timers with 0.1 second resolution, and (2.0 firmware only) three timers with 0.01 second resolution.

Each program consists of one to ten tasks, which are essentially threads. Tasks may be started and stopped from within the program, and the interpreter multitasks rapidly between all active tasks. Each program may include up to eight subroutines, reusable chunks of code like functions. Subroutines cannot take parameters.

Opcodes

Each bytecode command is defined by its first byte (the opcode), and a number of parameter bytes may follow. Usually the last three bits of the opcode indicate the proper number of parameter bytes, with 6 and 7 mapping to 0 and 1, respectively.

Many opcodes require sources, which describe where to get data from. Each source byte is followed by an argument byte. Source 0 is global variables (the argument indicates which one), Source 2 is a constant value (the argument itself), and so on.

I have condensed the opcode reference of RCX Internals into a compact document. Information about 2.0 exclusive opcodes comes from Lego's firmware reference. The document below is largely complete, but some things need to be documented better in a future revision.

Download opcode listing (PDF)

Example

To see how bytecode works, let's manually compile a small program. The following NQC snippet describes a program that beeps each time a touch sensor on port 1 is pressed, but every fifth press plays a long rising tone instead.

int count = 0;
SetSensor(SENSOR_1, SENSOR_TOUCH);

while (1)
{
    if (SENSOR_1 == 1)
    {
        count += 1;
        
        if (count % 5 == 0)
            PlaySound(SOUND_UP);
        else
            PlaySound(SOUND_CLICK);
        
        until (SENSOR_1 == 0);
    }
}

Each line of code can be translated into bytecodes. Try to follow along using the opcode reference. I'll use colors to indicate which branch placeholders point to which bytes.

Pseudocode Bytecode Comments
int count = 0; 14 00 02 00 00 Variable 0 stores the value of count.
SetSensor(SENSOR_1, SENSOR_TOUCH); 32 00 01
while (1) { ... } ... 27 [] As in assembly code, loops are formed in bytecode by placing a backward branch opcode at the end of the loop. We don't yet know how far to branch, hence the placeholder [].
if (SENSOR_1 == 1) { ... } 85 82 09 01 00 00 [] Source 2 of opcode 85 cannot be an immediate value. This comparison is actually performed as: if (1 ≠ SENSOR_1) { branch ahead }. Note that the constant 1 must be written as the little-endian short 01 00.
count += 1; 24 00 02 01 00
if (count % 5 == 0) { ... } 14 01 00 00 00
44 01 02 05 00
54 01 02 05 00
85 82 00 00 00 01 []
There is no built-in modulus instruction, but we can compare count to (count / 5) * 5 since all operations are integer-only. Variable 1 is used to hold the intermediate results. Again this comparison is inverted: if (count ≠ (count / 5) * 5) { branch ahead }
PlaySound(SOUND_UP); 51 03
27 []
The end of the if-path must branch to the end of the else-path.
PlaySound(SOUND_CLICK); 51 00
until (SENSOR_1 == 0); 95 82 09 00 00 00 [] [] This branch should point back to itself to form a waiting loop.

Now all the bytecodes are put together.

14 00 02 00 00 32 00 01 85 82 09 01 00 00 [] 24 00 02 01 00 14 01 00 00 00 44 01 02 05 00 54 01 02 05 00 85 82 00 00 00 01 [] 51 03 27 [] 51 00 95 82 09 00 00 00 [] [] 27 []

The final compiled code is 58 (0x3a) bytes long.

14 00 02 00 00 32 00 01 85 82 09 01 00 00 2a 24 00 02 01 00 14 01 00 00 00 44 01 02 05 00 54 01 02 05 00 85 82 00 00 00 01 05 51 03 27 03 51 00 95 82 09 00 00 00 fa ff 27 b1

I will download this program from the Linux command line using the send utility available at RCX Internals → Tools. First I delete all tasks of program 1. The brick must be prepared for the download with opcode 25/2d. Then the program is downloaded to task 1 in chunks; 20 bytes maximum seems like a good size. The checksum of each chunk, tacked onto the end, is the sum of all data bytes mod 256 (0x100). The whole process looks like this:

ben@UbuntuLTS:~/RCX$ sudo ./send 40
0000: bf
ben@UbuntuLTS:~/RCX$ sudo ./send 2d 00 00 00 3a 00
0000: d2 00
ben@UbuntuLTS:~/RCX$ sudo ./send 45 01 00 14 00 14 00 02 00 00 32 00 01 85 82 09 01 00 00 2a 24 00 02 01 00 ab
0000: ba 00
ben@UbuntuLTS:~/RCX$ sudo ./send 4d 02 00 14 00 14 01 00 00 00 44 01 02 05 00 54 01 02 05 00 85 82 00 00 00 c4
0000: b2 00
ben@UbuntuLTS:~/RCX$ sudo ./send 45 00 00 12 00 01 05 51 03 27 03 51 00 95 82 09 00 00 00 fa ff 27 b1 c6
0000: ba 00
ben@UbuntuLTS:~/RCX$ |

Lo and behold, the replies contain a zero byte, indicating everything went well. The program works as expected - a beep for every sensor press, but every 5 presses, the sound is different.

In practice nobody compiles by hand, but this was a good learning exercise. In fact, I initially made three slight mistakes in the above listings - and this wasn't a very complex program. The much more reliable NQC produces nearly identical code when configured for RCX 1.0.