Lab 2: Programming in RTOS using OSEK and LEGO Mindstorms NXT
Printing: In case you want to print out this page, use the link to a "printer-friendly version" at the bottom!
Introduction
See also the lab introduction slides.
Assignment Goals
In this assignment, you will learn how to do basic programming on an embedded device with a real-time operating system (RTOS). In particular, you will use C to program the microcontroller of a LEGO Mindstorms NXT control brick to make it interact with its environment. It will run an RTOS based on the OSEK standard.
Working in Groups
Solve this assignment in your groups. The lab should be done in groups of 3 people, or in exceptional cases in groups of 2 people. Submissions by a single student will normally not be accepted. All students participating in the group shall be able to describe all parts of the solution. Each group will be given a LEGO Mindstorms package during/right after the lab introduction lecture on 9.9.. The box includes all the necessary parts for solving the assignment, and the group is responsible for handing the package back at the end of the course. All hardware issues (like handing out/taking back the boxes) are handled by Karl Marklund.
Report Hand-in
The report must consist of the following:
- Listings of well commented and well structured programs for the last 3 parts (OIL and C file),
- General descriptions of your solutions and how your code is supposed to work and why,
- Answers to all parts of this assignment.
Solutions have to be submitted via the student portal, deadline for submission is September 28th, 13:00 (extended). No submissions will be accepted after this point.
Note: To get approved on this assignment, you should return the LEGO Mindstorms package to Karl Marklund. Make sure that the package still contains the NXT unit, all sensors (light sensor, 2x touch sensors, sound sensor, distance sensor), all three motors and all the cables. Please also flash the original firmware back into the brick using /it/kurs/realtid/bin/fwflash-original. Please have a look at further information how to organize the box before handing it back.
Note/2: Please make sure you are using clear, concise and fluent language, that your report is clearly structured/layout-ed (which includes good code comments!) and that the technical quality of your solution is sufficient. Hand-ins with non-indented code will be discarded without further consideration.
LEGO Car Competition
The last part of this assignment consists of building a robot car that can follow a car on a track (see below). All groups have to show that their car is able to complete the tour. The group with the fastest car wins, which implies eternal glory and a surprise present.
Please sign up for the competition times on this Doodle poll.
LEGO Mindstorms NXT
The LEGO Mindstorms Robotics Invention System consists of a bunch of LEGO pieces and the NXT unit with some sensors and motors, see picture on the left. The NXT unit is an autonomous programmable microcomputer, using an Atmel 32-bit ARM7 processor (specifically, AT91SAM7S256) running at 48 MHz. The NXT brick can be used to control actuators, like an integrated sound generator, lights, and motors, and read input from various sensors, like light sensors, pressure sensors, rotation sensors, and distance sensors. The NXT brick also has an LCD display (useful for printing information) and USB and Bluetooth communication ports. The NXT unit is built for the easy attachment of LEGO building blocks and pieces.
Note: In this lab, in contrast to previous years, you will enjoy using the LEGO Mindstorms NXT platform as depicted on the picture, i.e., with the gray NXT unit. The old version with yellow RCX bricks is not used in the course anymore. If you are re-doing this lab, many principles introduced in the lab will be familiar to you. However, it has been fundamentally redesigned, being now based on nxtOSEK, and you should go carefully through all parts.
Instead of the standard firmware and default programming platform of the NXT, we will use nxtOSEK, which is a port of OSEK to the LEGO NXT platform. OSEK is an RTOS specification introduced by a consortion of mostly German car manufacturers. It is widely used in industry. The NXT implementation further uses ECRobot for low-level hardware access like sensors and motors and Newlib for standard C functions (similar to libc).
Programs for nxtOSEK contain two parts:
- The program source code, e.g., myprogram.c
- The system's description in OIL format, e.g., myprogram.oil
The compilation toolchain first compiles the C file into an ARM binary and then generates the whole system's binary according to the description in the OIL file. This includes definitions of all tasks, resources, event objects, etc.
Getting Started with nxtOSEK and the ECRobot API
All software necessary to work with OSEK on the NXT platform is installed on the x86 Solaris lab machines in the IT department. This includes software for flashing the firmware, compiling programs and uploading them.
Flashing the Firmware: In order to start with the lab, you first need to flash a custom firmware into the NXT brick, since the original LEGO firmware does not support the nxtOSEK binaries. And does annoying sounds all the time. The alternative firmware can be identified in the "Settings/NXT Version" dialog with the string "FW NBC/NXC". If this is not the case (it displays just "FW" and does annoying sounds), you can flash the firmware as follows: Connect the NXT brick to the USB port of the SunRay you are sitting at. Further, switch the NXT brick on and put it into reset mode by pressing the reset button (at the back of the NXT, upper left corner beneath the USB connector) for more than 5 seconds. The brick will start ticking shortly after. Now run /it/kurs/realtid/bin/fwflash-jh and wait for the procedure to finish (30-50 seconds should be enough). Note that this you only need to do once and never again. The original firmware can be flashed back using /it/kurs/realtid/bin/fwflash-original which you please do before handing back the box.
Program Compilation: In order to compile programs, all you need to do is to have an appropriate Makefile in the current directory. (It is recommended that you use a different subdirectory for each part of the assignment.) Change the name of the TARGET in the makefile to the name of your program and you should be able to compile it using "make all". The resulting binary ends on ".rxe".
Program Upload: Files can be uploaded to the NXT brick using:
- /it/kurs/realtid/bin/nxjupload myprogram.rxe
Make sure that the NXT brick is connected to the USB port of your SunRay terminal and that it is turned on. If you get a write error when uploading a file that is already on the brick (with the intention of overwriting it with a new version), try deleting the old version beforehand. (You do that with the buttons on the brick. In the menu where you run your program, there is a little trash can for deleting the file.)
Further Documentation:
Further documentation for the available API is provided with the following links:
- nxtOSEK C API Reference
- OSEK OS Specification
- OSEK OIL File Specification
- Newlib Reference Manual
- Newlib Math Library Reference Manual
Working at home: If you like to work at home, you can install the compilation and upload toolchain yourself. Since this depends heavily on your setup, we can't give you any direct support. However, the nxtOSEK Homepage and my Solaris installation instructions may give you hints for successfully doing so.
Part 1: Warm-Up
This first part is supposed to get you used to compiling and uploading programs, together with simple input/output operations on the nxtOSEK platform. The program you will write is a simple "hello world!" that additionally prints a sensed light value on the LCD display of the NXT brick.
Program Skeleton
Create a helloworld.c file with the following headers:
#include <stdlib.h> #include "kernel.h" #include "kernel_id.h" #include "ecrobot_interface.h"
Further, you need to define three OSEK hooks:
void ecrobot_device_initialize() {} void ecrobot_device_terminate() {} void user_1ms_isr_type2(void) {}
They are used to execute custom code when:
- The device is initialized,
- The device is shut down,
- A timer interrupt is called once every millisecond.
We will use them later for various purposes; you can leave them empty for now. Further, define the helloworld task as follows:
TASK(HelloworldTask) { // Your code comes here TerminateTask(); }
The task also needs to be declared by placing the following right below the header includes:
DeclareTask(HelloworldTask);
Finally, to complete the skeleton, create a helloworld.oil file with the following contents:
#include "implementation.oil" CPU ATMEL_AT91SAM7S256 { OS LEJOS_OSEK { STATUS = EXTENDED; STARTUPHOOK = FALSE; ERRORHOOK = FALSE; SHUTDOWNHOOK = FALSE; PRETASKHOOK = FALSE; POSTTASKHOOK = FALSE; USEGETSERVICEID = FALSE; USEPARAMETERACCESS = FALSE; USERESSCHEDULER = FALSE; }; /* Definition of application mode */ APPMODE appmode1{}; TASK HelloworldTask { AUTOSTART = TRUE { APPMODE = appmode1; }; PRIORITY = 1; /* Smaller value means lower priority */ ACTIVATION = 1; SCHEDULE = FULL; STACKSIZE = 512; /* Stack size */ }; };
This is the system description that describes the CPU we want to use, certain properties of the OSEK scheduler, and finally one task named "HelloworldTask". The task will be automatically started at system startup ("AUTOSTART = TRUE"). Other properties are not important right now, but will be used in later parts of this assignment (like the task priority).
Writing The Code
Now, attach a light sensor to the NXT brick and fill in the rest of HelloworldTask. Your code should do the following:
- Display "Hello World!"
- Read the light sensor value and display it repeatedly with a delay of 100ms in between.
The following functions may come handy: display_clear(), display_string(), display_update(), ecrobot_get_light_sensor(), systick_wait_ms() and others. Note that your code should include an infinite loop, so that technically, TerminateTask() will never be called.
Consult the nxtOSEK C API Reference for more information.
Note: Light sensors need to be initialized before usage. Add appropriate calls of ecrobot_set_light_sensor_active() and ecrobot_set_light_sensor_inactive():
- Call ecrobot_set_light_sensor_active() in the OSEK hook ecrobot_device_initialize(),
- Call ecrobot_set_light_sensor_inactive() in the OSEK hook ecrobot_device_terminate().
Note/2: If your display isn't showing anything after the second update to the screen, use the display_goto_xy() function. You may have accidentally written your intended output at a place outside the screen. You find more details in the C API.
Make sure your code compiles without error and executes as desired on the NXT brick. Try to measure light values of different surfaces (light ones, dark ones, ...).
Part 2: Event-driven Scheduling
In this part, you will learn how to program event-driven schedules with nxtOSEK. The target application will be a LEGO car that drives forward as long as you press a touch sensor and it senses a table underneath its wheels with the help of a light sensor. For this purpose, build a LEGO car that can drive on wheels. You may find inspiration in the manual included in the LEGO box. Further, connect a touch sensor (using a standard sensor cable) to one of the sensor inputs.
Handling Events
OSEK defines an event mechanism that can be used to schedule actions. Events may be generated by external sources, interrupt service routines (ISRs) or even other tasks. This allows to react immediately to signals from various sources.
With OSEK, events are defined per task. Only the "owner" task can wait for an event to occur. Events are stored in a data structure that needs to be reset after the occurrence of the event, and this reset can as well only be done by the owning task. However, all other tasks as well as ISRs may generate events for other tasks. Further, tasks may read the status of events of other tasks. You find more information about the OSEK event mechanism in the OSEK OS manual, chapters 7 and 13.5.
Each task includes an "event mask" which is a number of bits, each representing a different event. A task can wait for an event using WaitEvent() and is suspended (i.e., blocked) until this event occurs, i.e., until the corresponding bit in its event mask is set. Meanwhile, the CPU is available for other tasks to execute. Tasks can wait for multiple events at once by using an OR operation on the event's bits:
- WaitEvent(Event1 | Event2);
After the event occured, the task is itself responsible for clearing the corresponding bit in the event mask using ClearEvent:
- ClearEvent(Event1);
If a task waited for multiple events at once, it may read its event mask to determine which of them occured:
EventMaskType eventmask = 0; ... WaitEvent(Event1 | Event2); GetEvent(MyTask, &eventmask); if (eventmask & Event2) { // Event 2 occured ClearEvent(Event2); ... }
Create a new program with a task "MotorcontrolTask" that does the following in an infinite loop:
- Wait for event "TouchOnEvent"
- Make the car move forward by activating the motors
- Wait for event "TouchOffEvent"
- Make the car stop
As suggested by the names of the events, the idea is that they should occur as soon as the user presses and releases the attached touch sensor. We will come shortly to the mechanism of making the system generate the events.
Add further some nice status output on the LCD. Don't forget to declare the task using DeclareTask() just as you did in part 1.
In order for the system to recognize the events, they need to be declared using DeclareEvent() right where you also declared the task using DeclareTask(). Further, the OIL file needs to get prepared. Create a new OIL file and specify MotorcontrolTask just as in part 1, with the addition that it should now also include the following lines:
EVENT = TouchOnEvent; EVENT = TouchOffEvent;
Additionally, the events need to be defined themselves, before (!) the task definition:
EVENT TouchOnEvent { MASK = AUTO; }; EVENT TouchOffEvent { MASK = AUTO; };
The keyword "AUTO" tells OSEK to choose itself appropriate bits. You could also specify the bit number directly instead.
Generating Events
Now we just need to generate the touch sensor events. In general, they should be generated by appropriate ISRs that would be called when the corresponding interrupts are released. Unfortunately, the sensors on the NXT are working in a polling mode: They need to be asked for their state again and again, instead of getting active themselves when something interesting happens.
Our workaround for this is to create a small, second task that and checks the sensors periodically (about every 10ms). If the state of the sensor changed, it generates the appropriate event for us.
In order to do this, declare and implement a task "EventdispatcherTask". It should call the appropriate API function to read the touch sensor and compare it to it's old state. (A static variable may be useful for that.) If the state changed, it should release the corresponding event:
- SetEvent(MotorcontrolTask, TouchOnEvent);
... or ...
- SetEvent(MotorcontrolTask, TouchOffEvent);
Just as in part 1, put your code in an infinite loop with a delay in the end of the loop body. (We will do "proper" periodic tasks in the next part.) In order for MotorcontrolTask to have priority over EventdispatcherTask, make sure to assign a lower priority to the latter in the OIL file. Otherwise, the infinite loop containing the sensor reading would just make the system completely busy and it could never react to the generated events.
This should complete your basic event-driven program. Compile and upload the program and try whether the car reacts to your commands. In case of errors, read the error messages of the compiler and/or consult the OSEK documentation.
Extending The Program
Attach a light sensor to your car that is attached somewhere in front of the wheel axis, close to the ground, pointing downwards. Extend the program to also react to this light sensor. The car should stop not only when the touch sensor is released, but also when the light sensor detects that the car is very close to the edge of a table. (You may need to play a little bit with the "Hello World!" program in order to find appropriate light levels.) The car should only start moving again when the car is back on the table and the touch sensor is pressed (again).
The edge detection should happen in EventdispatcherTask and be communicated to MotorcontrolTask via the event mechanism. Use two new events for that purpose. Make sure you declare and define all events properly in both the C and the OIL file. Further, the display should provide some useful information about the state of the car.
Important Notes:
- The job of EventdispatcherTask is just to create the events signaling button and light behavior, independent from each other.
- All "logic" should happen in MotorcontrolTask, i.e., when to or not to move, depending on the current state. The task must not read the sensors by itself nor communicate with EventdispatcherTask by other means than the event system, i.e., shared variables etc. are not allowed!
- A frequent mistake is that the periodic EventdispatcherTask generates an event each time it is executed, reporting the current state of the sensors. Instead, it should only generate events at state changes: one (and only one) in the moment when the touch sensor is pressed down and one when it is released. The same for the light sensor: When the table edge is detected, generate one event. Don't "spam" the event system with redundant information. (Of course, when the touch sensor is pressed again, a new event needs to be generated.)
What To Hand In
Please hand in only the source (C+OIL) of the full (second) program that includes the light sensor code. Make sure you include brief explanations and that your source is well-commented. (Note that hand-ins without meaningful comments will be directly discarded.)
Part 3: Periodic Scheduling
Real-time usually schedule most of their tasks periodically. This usually fits the applications: Sensor data needs to be read periodically and reactions in control loops are also calculated periodically and depend on a constant sampling period. Another advantage over purely event-driven scheduling is that the system becomes much more predictable, since load bursts are avoided and very sophisticated techniques exist to analyze periodic schedules. (You will learn about response-time analysis later during the course.)
The target application in this part will make your car keep a constant distance to some given object in front of it. Additionally, the touch sensor is used to tell the car to move backwards a little bit in order to approach the object again. (Note that this is a new program again, so for now, do not just extend the program from the event-driven assignment part. Create a new program instead.)
Periodic Tasks
The structure of the system in this part is as follows: We have three tasks that are scheduled periodically, with different periods:
- A task "MotorcontrolTask" that only takes care of controlling the motors and receives commands from the other tasks.
- A task "ButtonpressTask" that senses the state of the buttons and sends commands to MotorcontrolTask.
- A task "DisplayTask" that displays some interesting information about what is going on currently.
(Note that there is no task sensing the distance yet. This will come later.)
Obviously, the tasks can have different periods, since while we would like the car to react fast to the button press, we can't (optically, as humans) read updated information from the display faster than in certain intervals anyway.
We start by defining a periodic task. Define "MotorcontrolTask" in the OIL file as follows:
TASK MotorcontrolTask { AUTOSTART = FALSE; PRIORITY = 1; /* Smaller value means lower priority */ ACTIVATION = 1; SCHEDULE = FULL; STACKSIZE = 512; /* Stack size */ };
Note that this task is not automatically started at system start. Instead, we will release it again and again in regular intervals. In order to do so, we need two tools: A counter that is increased every millisecond, and an alarm that can activate the task for us every time the counter reaches value 50, let's say. This will cause a task release every 50ms.
The counter is defined as follows in the OIL file:
COUNTER SysTimerCnt { MINCYCLE = 1; MAXALLOWEDVALUE = 10000; TICKSPERBASE = 1; /* One tick is equal to 1msec */ };
Further, an alarm is defined as follows:
ALARM cyclic_alarm { COUNTER = SysTimerCnt; ACTION = ACTIVATETASK { TASK = MotorcontrolTask; }; AUTOSTART = TRUE { ALARMTIME = 1; CYCLETIME = 10; APPMODE = appmode1; }; };
Note that this alarm will do the desired thing: Each time the specified COUNTER is increased by CYCLETIME, the given TASK is activated. Finally, to make this work, you need to do two things in your C program:
- Declare the counter in your C program using DeclareCounter().
- Since OSEK does not increment the counter for us, we need to do this ourselves. This is where the 1ms ISR hook is useful. Replace the empty definition of user_1ms_isr_type2() in your program with the following:
- void user_1ms_isr_type2(void){ (void)SignalCounter(SysTimerCnt); }
Basic Periodic Schedule
Before we implement the actual tasks, we need a way for them to communicate. We will use a data structure that is used by the sensing tasks to communicate their movement wishes to MotorcontrolTask. Define a "driving command" structure as follows:
struct dc_t { U32 duration; S32 speed; int priority; } dc = {0, 0, PRIO_IDLE};
The sensing tasks can at any time write an new "command" into this (global) structure, and the idea is that they should only succeed in doing so, if they don't already "see" a higher priority in the structure.
Define priorities PRIO_IDLE and PRIO_BUTTON with values 10 and 20 using #define. Further, write a function
- change_driving_command(int priority, int speed, int duration)
that can be used by tasks to update the structure. It should only do so, if the supplied priority is at least as high as the value already contained in "dc.priority".
Using this structure, we will now define the tasks in our system:
- Define a task "MotorcontrolTask" that "executes" the "driving command". If dc.duration is still positive, it should set the speed of the motors accordingly and decrease dc.duration by its period length. As soon as dc.duration is non-positive, it should set dc.priority to PRIO_IDLE and stop the motors. Define the task in the OIL file with a period of 50ms, just as we did above.
- Define a task "ButtonpressTask" that reads the touch sensor. If the button is in the "pressed down" state, it should try to set the driving command to driving backwards with priority PRIO_BUTTON and for 1000 milliseconds. Further, define the task in the OIL file just as you did with the periodic task "EventdispatcherTask" above. Give it a period of 10ms.
Note that both tasks should be periodic. In particular, they should NOT include any infinite loops (since they will be started over and over again) and end with TerminateTask().
- Further, define a third task "DisplayTask" that outputs some useful information on the LCD and is periodic with period 100ms.
Before we are done with the program, there is a potential problem to be taken care of. The structure "dc" is shared by several tasks that are writing to and reading from it. Do you need to protect it in any way? If so, do that. The "Resource" concept of OSEK may be helpful, which provides simple locks: You can declare resources with DeclareResource() and use them with GetResource() and ReleaseResouce(). They further need to be defined in the OIL file. Consult the OIL specification for details.
Finally, compile and run the program. So far, the behavior is not too exciting, since all the car can do is just stopping or moving backwards. The exciting part will come next.
Incorporating Distance Measurement
Add a distance sensor that points forward to your car and connect it to a sensor port. (Leaving the light sensor attached may help later.) Using the above structure of periodic tasks, extend your program with a fourth periodic task "DistanceTask". It should have period 100ms and read the distance sensor value in each instance. Using the sensor reading, it should try to set the driving command to a value that would make the car drive towards a target distance of around 20cm between the cars front and the object. (Note: This means that the car should go forward if it is to far away, and backward if it is too close.) Define the priority PRIO_DIST used for writing to dc as a value between the two already existing priorities. By doing so, the button press will have a higher priority. Make sure you define the task and its releasing alarm in the OIL file.
The task displaying useful information should be extended to display even more useful information. Further, the following hints may help:
- Just as the light sensor, the distance sensor needs to be initialized. Add the appropriate code to the OSEK hooks.
- The distance sensor is not very precise. Use a book or a sheet of paper as a test object to which the car should measure the distance. This will give more stable sensor readings.
- You may play with the task periods in order to experiment with reaction times, but keep in mind that the distance sensor needs a certain minimum time between two sensor readings. Readings faster than every 100ms have shown increasingly degraded data quality.
- As an appropriate speed for driving the car, the following strategy is quite elegant: Choose a speed proportional to the difference between the distance the car DOES have and the distance the care SHOULD have. This is also called a "proportional controller" or just "P controller" in control theory. If you have some knowledge about this field and are bored, you may try to implement a full PID controller.
When you are done, compile, upload and test your program. Make sure that the behavior is as desired: The car should try to keep a distance of 20cm. When the button is pressed, it should drive backwards for one second, after which it should resume the distance-keeping (resulting in driving closer again).
What To Hand In
Please hand in only the source (C+OIL) of the full (second) program that includes the distance sensor code. Make sure you include brief explanations and that your source is well-commented. (Again, note that hand-ins without meaningful comments will be directly discarded.)
Part 4: LEGO Car Race
In the last part you will use all the knowledge you acquired in the above parts in order to create a car that can simultaneously:
- Follow a line that is drawn on the floor (not necessarily a straight one!)
- Keep distance constant (about 20cm) relative to another car that is driving in front of it. The car to be followed will be provided by the teaching assistant and you may assume it only moves forward and won't be too fast, but its speed may vary over time.
You may use any of the techniques you learned above to define and schedule tasks, read sensors and send commands to the motors. The line tracking should be done with the light sensor.
The line to be followed will have the following shape:
A test track is available from Jakaria's office and during the labs in the corresponding lab rooms. 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 most likely take place on a new track in a different room.
In order to pass the lab, all teams need to demonstrate a working car that can do both jobs accurately. This has to be demonstrated on Thursday, 24.09., see the schedule for details. The procedure will be as follows:
- First, each team needs to demonstrate that its car can follow a vehicle that we provide in a reasonable distance. In particular, it should neither crash into it, nor let the distance increase unboundedly, nor leave the track completely. Each team has 3 tries to complete this task for a full lap on the provided track. If all 3 tries failed, the team is disqualified from the competition (but may still be able to pass the lab by demonstrating a fixed and working car later).
- Second, after a full lap, the provided vehicle that your car is following will disappear. From then on, we measure the time your car takes to complete another round alone. A car may not leave the track completely nor take more than 2 minutes to complete the challenge. Otherwise, the team is disqualified (but already passed the lab).
- The team with the shortest time for a lap wins.
Clarified rules:
- You may only use 1 light sensor. In principle, I don't have anything against you playing with/implementing advanced approaches, but this rule is supposed to keep conditions equal for all groups.
- Same holds for the other sensors: You may only use what *one* LEGO box provides, i.e., 3 motors, 1 light sensor, 1 distance sensor, etc.
- You do not have to drive backwards. The car you have to follow won't go backwards, and if you get too close, it is sufficient to just slow down/stop. I only want to see that you are neither crashing into it nor letting the distance get too big.
- Don't assume a direction on the track. Your car must work both clock- and counterclockwise equally well (it's up to you how well...)
Useful Advice
Some issues arise every year to some of the groups. Experience tells that you should take care of the following things:
- Build a physically robust car. Solving the labs and passing the race challenge will be difficult otherwise.
- Do not hard-code the thresholds for the values of the light sensor, i.e., for classification of "track", "off track" or anything else. Demonstrating the car will most likely happen under different light conditions and will fail if you do that. Note that this is the single most frequent reason for students to have serious problems in the demonstration. (Some groups were unable to respect this issue and failed the whole course because of that!) Instead, build some simple-but-smart sensor calibration into the beginning of your program, so your car can adapt to different environments. You may assume that the lighting conditions do not change during the demonstration.
- You have two weeks from hardware hand-out to the car competition. That is significantly less than earlier years. Start early, work intensely and try to finish on time.