Uppsala universitet
LEGO assignment
Introduction
Getting started
Geiger counter
Static cyclic
Watchdog
Multitasking
Maze
Documentation
Links
Working@home
Report
 
FAQ
 
Print version

Programming Lego Mindstorms using brickOS

Introduction

RCX
    brick with sensors attached

The LEGO Mindstorms Robotics Invention System consists of a bunch of LEGO pieces and the RCX (Robotics command System) unit (see picture on the right). The RCX unit is an autonomous programmable microcomputer, using a Hitachi H8/3932 microcontroller. The RCX brick can be used to control actuators, like a sound generator, lights, and motors, and read input from various sensors, like light sensors, pressure sensors, rotation sensors, and temperature sensors. The RCX brick also has an LCD display (useful for printing information) and an IR-transceiver (for downloading programs and communicating with other bricks). The RCX unit is built for the easy attachment of LEGO building blocks and pieces.

BrickOS (formerly known as LegOS) is a small Operating System for the LEGO Mindstorms RCX unit. BrickOS is cross-compiled on a PC under Linux or Windows 95/98/2000/NT environments. BrickOS programs are written in C or C++ and compiled to native code for the Hitachi H8 (most other programming environments for the RCX depends on the virtual machine that is the default programming environment from Lego). BrickOS offers preemptive multitasking, dynamic memory management, POSIX semaphores, as well as native access to display, buttons, IR communication, motors and sensors. Observe that Brickos is not a hard Real-Time Operating system but does offer functionality similar to that of most soft real-time embedded OSes on the market.

BrickOS is currently (October 2002) in the middle of a development process with a 1.0 release as its goal. Unfortunately the test releases available is not yet stable enough to be used in a course. Therefor we use the latest stable release of its predecessor LegOS, i.e. version 0.2.5 (0.2.6 is not stable).

Getting started

The main purpose of this lab is to make the students familiar with programming embedded RT systems and understanding the demands put on such systems.

The assignment should be solved in groups of two or three persons. It is not recommended that you work alone, since there is a limited number of lab equipments available. For more detailed description of the requirements on the report for the lab, see below.

Before you start make sure that you have the following materials available:

The steps to get one of your programs running on the RCX unit, using one of the PCs running Windows in room 1312 and 1313 are:

  1. Log on to a Windows machine using your username and password.
  2. Check if threr is a folder called C:\cygwin-legos installed on the computer. If not download the cygwin-legos.zip package (56 MB) contining cygwin and LegOS 0.2.6. Unpack it into the root of C:. It is important to unpack exactly to this place, otherwise the paths will not work.
  3. Check if you need to patch the registry. The simplest way to test this is to try to start Cygwin. Cygwin is started by opening Windows Explorer and double-click on cygwin.bat in C:\cywin-legos. This will open a cygwin shell. If the shell gives a green prompt everyting is ok, otherwise you have to patch the registry.
  4. To patch the registry close the cygwin window. Download the registry patch cygwin.reg (for Win2k or higher). This file contains data that cygwin needs to find its paths. Either run it directly when it is fetched or double-click on it afterwards, this will add the needed keys to the registry.
  5. Now start cygwin again. Most referred commands will hereafter be made from the Cygwin shell. The cygwin shell gives you a UNIX-like command line which is far superior to the built-in Windows command line environment. Type:
    cd /
    cd legos-0.2.6
  6. Put the batteries into the RCX and the IR-Transceiver. Connect the IR-Transceiver to COM port 1 of your PC. Turn on the RCX unit (the red on/off button).
  7. Download the LegOS kernel on the RCX board using the program firmdl3:
    util/firmdl3 boot/legos.srec.
    If the transmission fails you should check that there is nothing interupting the IR-beam. You may also try the slow mode (bu using the -s flag).
  8. Investigate the demo/ directory to get some feeling for how some example programs might look like. Files ending with .lx are the Brickos executable files that are downloaded to the RCX and run on it.
  9. Download the helloworld.lx program file to the RCX using the program dll as:
    util/dll demo/helloworld.lx.
    If you run dll without arguments you will get a listing of all possible program options. For example, you can have several programs downloaded on the RCX unit at the same time. If you are unable to download your created program you will get an error message like:
    error deleting program.
  10. You can run the program helloworld on the RCX unit by pressing the green run button (note that the program number on the rightmost position of the display should read ZERO, since the default program position for a download is always zero).
  11. After modifying a .c program you compile it again to .lx format by calling make in the demo directory.
  12. If you want to compile a new .c program you should open the demo/Makefile file and add NAME_OF_YOUR_FILE.lx after the PROGRAMS= row. If you want to link and compile together several .c files into one executable .lx file you should add all your files .c as .o files after the DOBJECTS= row in your Makefile.

To run BrickOS/LegOS at home, see the Running BrickOS at home.

The LegOS Command Reference 1.0-0.2.4 contains a short (and slightly outdated) summary of commands available for programming the RCX unit. If you want details you can investigate the system header files in the include/ directory.

Troubleshooting

If your RCX turn completely dead. That is, nothing happens when the buttons are pressed et.c., then remove the batteries. This will allow the capacitor that preserves the RAM during battery switches to discharge. You will have to let the batteries be removed for som time (at least 10 minutes).

Part 1: A simple light detector

Attach a light sensor to the RCX unit. Write a short program (one task, coded into the main function) which works as a Geiger counter for light, ie. when the light sensor receives a lot of light it should be generating a very frequent sound (ticking). When little light is received the sound (ticking) should be less frequent. The light value should also be displayed on the LCD.

Investigate the demo/robots.c file and the LegOS Command Reference 10-0.2.4 on how to play sounds, read the light sensor, and write something on the LCD display.

Please note that the values you will get from the light sensor might depend on how much power there is left in the batteries. Also note that you can set the light sensor to be in active or passive mode by using the ds_active() and ds_passive() functions. Is there any observable difference between the two modes?

Note: To be able to use the lcd_int() function you should include the conio.h include file.

Part 2: Static cyclic scheduling

One simple way to handle several concurrent tasks is to create a static cyclic schedule for the things that should be performed. This means that we only run one execution thread in the system, and that this thread repeatedly calls functions corresponding to the tasks in the schedule. When all tasks has been performed the schedule starts all over again (maybe after a delay to keep a certain cyclic execution frequency). Due to its simplicity and predictability, static cyclic schedules are very common in industrial applications.

while(1)  /* Scheduler loop */
{
   task1(...);    /* Function call */
   task2(...);    /* Function call */
   task3(...);    /* Function call */
   ...
   taskN(...);    /* Function call */
   delay_until_next_time_unit();  /* Periodic schedule */
}

Construct a vehicle which is able to turn and drive forward. Use the descriptions in the LEGO Mindstorms manuals as inspiration on how to construct your vehicle. To detect if the vehicle has hit an obstacle (and needs to turn away), you should use the touch sensor(s).

Code a schedule which contains four different tasks (each in a separate function): motor_control, forward_run, direction_change, and obstacle_handling. The motor_control task should be the only task that tries to control the motors. It should take driving instructions from the other tasks. The forward_run task will try to set the vehicle to drive forward. The direction_change task should randomly change the direction of the vehicle (choose a low probability if you want your robot to get anywhere). The fourth task, obstacle_handling, should check if the vehicle has hit an obstacle and handle such events by turning away from the obstacle.

Note that each function that implement a task has to return immediately to the scheduler: there should be no "infinite loop" in this type of tasks. The looping is taken care of by the scheduler loop. If you want persistent state in a task, it has to be implemented using local static variables, or global variables. This model of execution is called "single-shot", and is employed in a few hard-real time operating systems.

To give driving orders to the motor_control task the other tasks have to use a (global) command data structure which looks something like:

/* The duration in ms for a time unit */
#define TIME_UNIT_IN_MSEC 100

/* The different directions of the vehicle (add more yourself) */
enum dir_t {turn_left, turn_right, forward, backward, stop, idle};

/* The type of the global data structure */ 
struct driving_command {
  int        priority;
  enum dir_t direction;
  int        speed;
  long int   duration;
}

/* Function for changing the command in the global data structure */
void change_driving_command(int prio, enum t_dir dir, int speed, long int dur)
{
  /* Update driving_command data structure if allowed */
}

The idea is that each task has a certain priority regarding motor control, and that the highest priority takes precedence. This allows us to build a "subsumption" architecture, with several layers of behavior for the robot, each of which takes precedence over lower levels. In our case, obstacle avoidance has the highest priority, and forward_run the lowest. The easiest implementation of the priority handling is to have a special function, change_driving_command, which checks if a new command is more important than the current command, if not it should not change the command.

The speed parameter is used to allow the robot to move quickly or slowly. The interpretation is up to the motor_control task.

The duration of commands as well as the period of the static schedule should be given in time units. This means that you only need to change the #define TIME_UNIT_IN_MSEC 100 line, (the duration in ms for a time unit), to change the behaviour of your system. To avoid wrap-around problems, ie. the whole timing value can not be fitted in completely in the variable store, always store timing values as long int variables.

Every time the motor_control task is called it should read the current command, set the motors accordingly, and decrease the duration left for the current command with 1 time unit. This means that you have to make the task periodic, preferably by keeping track on when the schedule started the first time and using the msleep() function in some smart manner before calling the motor_control function to space out invocations of motor_control and the other tasks.
If there is no duration left for the current command, the command should be set to "idle" and the motors stopped, and the priority should be set to the lowest value, allowing all other tasks to send new commands. The task should also display the current direction and speed on the LCD.

The forward_run task drives the vehicle forward by always trying to set the command to forward. This is performed by checking the priority of the current driving instructions in the data structure. If there is a more important driving command in the data structure, the forward_run task is not allowed to overwrite it.

The direction_change task should change the direction of the vehicle with some probability. To generate random numbers you can use the random() function given in LegOS Command Reference 1.0-0.2.4. Don't forget to initialize the random seed with the srandom() function at program startup.

The obstacle_handling task should, every time it gets called, check if any of the touch sensors has been triggered. If so, it is assumed that the vehicle has hit an obstacle and needs to turn away. You may use both touch sensors to detect where the vehicle hit the obstacle (from the left, right, or from the front), or you might use just one touch sensor for less precise results.

You should be sure that the motor_control tasks is run once every time unit. This means that you have to make the task periodic, preferably by keeping track on when the schedule started the first time and using the msleep() function in some smart manner before calling the motor_control function to space out invocations of motor_control and the other tasks. We suggest an execution frequency of about 100Hz for motor_control, (ie. have #define TIME_UNIT 100), but the exact value is up to your implementation.

Question: Do you need to protect the driving_command data structure in any way, eg. with a semaphore? Why/why not?

Part 3: Adding a watchdog task

BrickOS is a multitasking operating system that let you dynamically create threads (tasks executing in the same memory space) using the execi(...) function. Please read the notes_on_execi document as well as on how to use the execi function more advanced features. You can also investigate page 6 in "Introduction to the LegOS Kernel", by S. Nilsson (.ps or .pdf) to know more about execi. The main() task which is started by default when you start your program is running on PRIO_NORMAL (= 10) priority (see kmain.c).

To check that your static cyclic schedule is alive (it is considered to have crashed if some task holds the CPU longer than its given time slice or enters into an infinite loop), you have to check that the time between two calls of the motor_control task never exceeds 1.5 time units.

Add a watchdog task to monitor motor_control's execution frequency to your static schedule from assignment 2. The watchdog task should be implemented as a separate execution thread, since it has to kick in when the main task has run astray. The watchdog task should be created just once. Every time motor_control task starts executing it should update some data structure the watchdog task is using for its time measurement or in some other smart way restart the time measurement of the watchdog task (can you use a semaphore?).

Whenever the time between two invocations of motor_control exceeds 1.5 time units (ie. before the invocation of the current executing task has finished), the watchdog task should indicate this by writing "PANIC" on the LCD-display, start beeping and and make the robot stop for some seconds.

To synchronize and communicate between the motor_control task and your watchdog thread you can use global variables, semaphores, and/or create smart event handling functions to use with the wait_event system function. If you choose to use the wait_event system call it is described in more detail in the notes_on_wait_event document.

Task: To make sure that you watchdog is working correctly you should simulate an error in your system by changing one of your tasks to sometimes do a very long uncontrollable execution.

Watchdogs are very common in real systems, since they offer a simple and robust (if not the most elegant) solution to the problem of detecting whether a system has crashed. The most common solution to a crash is to reset and reboot the system or in some other way put the system in a safe mode.

Part 4: Multitasking in BrickOS

An alternative way to structure your system is to create several concurrent BrickOS threads, where each thread is responsible for one (or several) of the system tasks. If the scheduler is preemptive (as is the case in BrickOS), a thread which is more important than another can preempt the execution of the less important thread.

In real RTOSes, functionality for dynamically creating execution threads during runtime like execi is not very common. Often, the tasks to be run are specified offline in a separate document (not in the code), and attributes like period, deadline, and offset are given. The OS is then configured to run this set of tasks only. This is most common on smaller 8- and 16-bit systems. Large 32-bit systems usually have dynamic threads.

Reimplement (copy and change the code) Part 2 with each task in a separate thread. Reuse the command data structure from part 2 and 3 for giving driving orders in a structured manner. The watchdog task from part 3 is not needed.

Your tasks should either be periodic or aperiodic. BrickOS does not have any method for you to explicitly specify the period for a thread, but you can achieve the same effect in your code by keeping track of the time the thread started, the time it took to execute the current invocation, and then sleep the remaining time to the next period start point. The priority of a thread is given at startup and cannot be changed.

The motor_control, forward_run and direction_change threads should all be periodic. Make the period of motor_control thread as one time unit and set the period of the remaining periodic tasks to multiple of time units.

Since the obstacle_handling task should only be invocted when an obstacle has been hit you should reimplement it as an aperiodic task waiting for some wait_event condition to be true. (See the notes_on_wait_event document on how to use wait_event). When the condition is fulfilled, the task is released and should start producing driving commands to move the vehicle away from the obstacle.

The maze where your vehicle will be driving have a black tape line to follow (see table in room 1312).

Add a light sensor pointing down at the front of your vehicle. Add a fifth aperiodic task line_handling to your system that checks (by using the light sensor) if the vehicle is following the line. If not, driving instructions for returning to the line should be issued immediately.

Set the priorities of the tasks according to their importance. If you think that two tasks are equally important you can of course set their priorities to the same value.

Question: Do you need to protect the driving_command data structure in any way, eg. with a semaphore?

Part 5: Maze

In the last part you will create a vehicle that can find its way through a maze. You should reuse the program structure you created in part 4.

Maze definition

The maze will be built of bookshelfs laid out on the floor or on a table. In the middle of the bookshelfs a black tape line will be attached. With the help of the light sensor the vehicle should be able to follow the line.

The goal is to find the exit of the maze as quickly as possible. The exit will be represented by a block that the vehicle can detect with a touch sensor when it runs into it. The exit will be on the "outside" of the maze.

  • The maze will be like a connected graph with multiple possible paths from one point to another.
  • The maze will consist of solid lines (the path) approximately 2cm thick that contrast reasonably well with the background (e.g., black electrical tape on a tan wood background or tan masking tape on a dark background).
  • The tape path will not be allowed to extend to the edge of the book-shelf (no drop offs).
  • Nodes of intersecting lines may have no more than four branches (i.e., degree less than or equal to 4).
  • All branch will be multiples of 90 degrees.
  • All branch points will be marked by having a 5 by 5 cm patch of silver tape above them.
  • There will be no curved line segments. The maze will be constructed from straight pieces of tape measuring at least 20cm long.
  • All enclosures (loops) will have heights at least 20cm.
  • Any two line segments of the maze will be at least 20cm apart (e.g., two parallel segments).
  • No robot may leave the book shelf. A robot that falls of the edge must re-start the maze.
  • The robot may not leave any trace that could be used to identify a previous position.
  • Each robot has at most 10 minutes to complete the challenge.
  • The robot that completes the challenge the fastest wins.
  • A practice maze will be available in room 1312.

Below a sample maze is shown.
A sample maze.

Useful Documentation

Useful Links

Lego resources page

Working at home

For a more detailed description on how to get BrickOS up and running see the Running Brickos at home page.

Report

A proper report is to be handed in for the assignment. It should include at least the following:

  • The cover page provided in cover.pdf.
  • A general description of your solutions and how your code is supposed to work.
  • Printouts of well documented code implementing solutions to each part.
  • Answers to the questions.
Some notes on what is important in the report:
  1. Language (clear, concise and fluent).
  2. Readable (ie clear layout )
  3. Complete (all parts)
  4. Results (technical level of solution)

Note

To get approved on the Programming Lego using BrickOS assignment you should return the LEGO Mindstorms Robotics Invention System package to Tobias Amnell in room 1216. Especially, make sure that the package still contains the RCX unit, one light sensor, two push-detectors and two motors.

Valid HTML 4.01!


©- 2002. UPPSALA UNIVERSITET, Box 256, 751 05 Uppsala | Tobias Amnell