Skip to main content
Department of Information Technology

Problem 5

The race track is available for loan at Johns office, room 1237.
Show your solution to one of the assistants before November 1st,
if you want to compete we will measure the time for a single lap.
The group with the fastest lap time by November 1st will receive the price.

Programming Lego Mindstorms using BrickOS

Introduction

rcx.gif

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 (March 2003) in the middle of a development process with a 1.0 release as its goal. Unfortunately the test releases available are not yet stable enough to be used in a course. Therefore we use the latest (stable) release of its predecessor LegOS, i.e. version 0.2.6.

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 three persons. It is not recommended that you work alone or in pairs of two, since there is only a limited number of lab equipment available. For a more detailed description of the requirements of the lab report, 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 1245 are:

  1. Log on to a Windows machine using your (windows) username and password.
  2. Open Windows Explorer and find the file cygwin.bat in G:\cygwin-legos. Start cygwin.bat by double-clicking on it. You will now get a cygwin shell. 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 /legos
  1. 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).
  2. Download the LegOS kernel on the RCX board using the program firmdl3:
util/firmdl3 boot/legos.srec

If the RCX does not respond, it may already have LegOS on it - in that case try continuing with the next step. If the transmission fails you should check that there is nothing interrupting the IR-beam. You may also try the slow mode (bu using the -s flag).

  1. Investigate the demo/ directory to get some feeling for how some example programs might look like. Files ending with .lx are the LegOS executable files that are downloaded to the RCX and run on it.
  2. 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
  1. 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).
  2. After modifying a .c program you compile it again to .lx format by calling make in the demo directory.
  3. 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 contains a short 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 turns completely dead. That is, nothing happens when the buttons are pressed etc, 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 some 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 1.0 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 have 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 implements 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 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 of 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. 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 make sure that the motor_control tasks are run once every time unit. This means that you have to make the task periodic, preferably by keeping track of 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 lets you dynamically create threads (tasks executing in the same memory space) using the execi(...) function. Please read the notes_on_execi document in order to understand how to use the execi function's 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 make the robot stop for a number of 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 your watchdog is working correctly you should simulate an error in your system by changing one of your tasks to sometimes perform 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 invoced 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 track where your vehicle will be driving has a black 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?
Tip: You can borrow a linetracking strategy from linetrack.c in the folder legos/demo.

Part 5: LEGO Indianapolis 500

In the last part you will create a vehicle that can follow a line quickly and accurately. You should reuse the program structure you created in part 4. The LEGO Indianapolis 500 track where your vehicle will be driving has a black printed line to follow. In room 1312 there will be a big printed test pad with the competition track.

A prize will be handed to the winning group of this contest! The race will be until the 1:st of November.

Track definition

The track is printed on a big white paper that is placed on the floor or on a large table. With the help of the light sensor the vehicle should be able to follow the black line. For accurate sensing you will have to recalibrate the reflection values of the track and the background before each race. The reflection value depends on the ambient lighting and the track condition. The race will probably take place on a new track in a different room.

Each vehicle will run individually on the 8-shaped track. The goal is to complete one lap as quickly as possible.

If the robot crashes or leaves the path completely, the distance completed before the crash is measured and used to compare with other robots in the case that no robot finishes the whole race. If the robot takes more than 7 minutes to finish, the race is interrupted and the distance completed in this time is measured (as above).

  • The track will consist of solid lines (the path) approximately 25 mm thick, that contrast well with the white background.
  • The branch point will be marked by having an 5 by 5 cm patch of highly reflective silver tape covering it.
  • No robot may leave the track path completely. A robot that leaves the track is considered to have crashed.
  • No glue or other material is allowed. Only LEGO parts that came with the box are allowed.
  • Each robot has at most 7 minutes to complete the challenge.
  • The robot that has the shortest time wins.
  • A practice track will be available in room 1312.

Below a sample track is shown.

track.gif

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 available in swedish or english.
  • A general description of your solution 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 (i.e. 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 the teaching assistent. Also, make sure that the package still contains the RCX unit, one light sensor, two push-detectors and two motors.

Updated  2004-10-27 10:47:54 by John Håkansson.