Programming Lego Mindstorms using brickOS
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).
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:
-
Log on to a Windows machine using your username and password.
- 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.
- 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.
- 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.
- 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
-
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).
- 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).
-
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.
- 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.
-
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).
- After modifying a .c program you
compile it again to .lx format by calling make
in the demo directory.
-
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).
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.
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?
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.
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?
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. 
Lego resources page
For a more detailed description on how to get BrickOS up and
running see the Running Brickos
at home page.
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:
- Language (clear, concise and fluent).
- Readable (ie clear layout )
- Complete (all parts)
- 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.
|