Zeidman Technology is a company that develops the SynthOS tool for automatically synthesizing an optimized real-time operating system (RTOS) based on the user’s configuration requirements and the applications running on the system. In this article we discuss building a robot based on an off-the-shelf kit, developing proper control algorithms, and using SynthOS to create the robot firmware. We are going to show that the adaption of SynthOS to a very constrained platform is nonetheless straightforward, and writing code for SynthOS is easy, and the tool is available free at www.SynthOSonline.com.
DFRobotShop Rover V2
We started from the off-the-shelf DFRobotShop Rover V2 kit, which contains the following:
- a printed circuit board having
- a battery
- a battery charger
- a twin-motor gear boxes
- two rubber tracks
- a pair of encoders (motion feedback sensors)
- a buzzer
- I/O expansion shield
- a USB cable.
- a daughterboard having
- infrared LEDs
- an ultrasound sensor
- pan and tilt servo motors
The board uses a slightly modified Arduino UNO reference design. The board that we used has an Atmel ATmega328p microprocessor (the signature is 1E 95 0F), although the user guide claims it is an ATmega329 (look for the PCB v2.0 description). The website mentions just ATmega328. The ATmega328p is a system-on-chip with 2K SRAM and 32K flash memory running at 16 MHz Note, that the CPU uses the Harvard architecture where code and data reside in separate address spaces.
Assembling the Robot
We want to emphasize some key instructions to help anybody who would like to repeat our experiment:
- Assemble the gear box in position C. Not only does this give a higher torque, but this position is optimal for the length of the rubber tracks. The distance between the front and the rear gears depends on the assembly position.
- Do not use the battery and the external power supply at the same time.
- Remove the charging circuit jumper if you use the 4xAA battery pack.
Some other points not mentioned in the guide are also important:
- Place a piece of foam behind the ultrasonic sensor. This will greatly reduce the frequency of measurement artifacts.
- Set the I/O expansion shield voltage jumper to 5V and set the run/prog switch to prog.
- Make sure that the gap between the encoder wheel and the encoder sensor is around 1 mm. Placing it too far results in periodic reading artifacts.
- Remember that some pins are not usable because they are already used. Analog inputs A0 and A1 are used by the encoders. Digital pins D0 and D1 are used by the USART. Digital pins D5-D8 are used by the motor controller.
Our goal was to build a robot that can avoid hitting walls and objects, even small ones. The robot should not get trapped in narrow sites. Also, it should be able to adjust the speed of the left and right tracks independently and shut the power down and beep if any of the tracks get stuck.
Sensors and Scanning
We use the ultrasonic sensor to detect objects since they can detect them at a greater distance and more precisely than the infrared sensor. Following the instructions, we attached the ultrasonic sensor to the pan servo motor. The robot continuously scans the surroundings turning the sensor left and right. The needed viewing sector is a bit smaller than 180 degrees. This is because the robot can approach a wall under a small angle. For sound or light, the angle of reflection is equal to the angle of incidence, so the robot may have trouble detecting vertically inclined surfaces, and some curvy surfaces can cause erroneous distance readings.
To find out the minimum distance to an object at which the robot needs to make an action, we noted that the robot will see an object when the robot head moves to the opposite side of the scanning sector and returns back ti the initial position. So, the head should turn fast enough (or the robot should move slowly enough) to avoid hitting the object before that cycle completes. This gives the relation between the robot speed and the full turn time.
When the robot detects an object that is close, it turns left. Moving its two tracks in different directions, allowing it to make turns with a nearly zero radius. Since the robot is not round it may hit the wall by its right rear corner, if it is too close to the wall. This adds another constraint to the minimal distance to the object.
Motors, Tracks, and Movement
Because the robot constantly turn in the same direction, it does not get stuck in narrow sites. This is like the common algorithm for traversing a maze. After robot turns, it must stop and make one full scanning turn before moving any further.
The robot constantly monitors the speed of both tracks using the encoders. The encoder wheel has 16 sectors. Connected to an analog input, the encoder submits a sine wave whenever the wheel rolls. To gauge the speed, we need to measure the time between two adjacent half-waves. We sample the time whenever the reading first goes above or below a certain threshold. To eliminate outliers, we arrange every 3 neighboring samples in ascending order and pick the middle one.
If any of the tracks gets stuck, the robot beeps and powers itself down. The torque of each motor is adjusted independently. Turning requires higher torque than moving straight. Since the motion speed is not high, the motors controller does not overheat. We do not use a temperature sensor. One difficult problem is to find an optimal initial torque because the real speed of the motors highly depends on the battery charge level which is inaccessible by the program.
Servo motors are controlled by pulse width. It might take several pulses to reach the desired position, if the current position and the required one are far apart. It is recommended to send a pulse a pulse every 20 milliseconds. To send pulses of precise width we use busy delays with interrupts disabled. This works because the delays are below 2 milliseconds.
The processor has 3 timers and we use timer number 2 for all time related tasks. The divider is set to 1024, so the time counter register increments every 1024*1/16000000=64 microseconds. The timer generates an interrupt when the counter reaches 156, so the interrupt happens around every 10 milliseconds. Using a 16 bit clock variable results in a wraparound time of around 10 minutes.
The ultrasonic sensor reports the moments of transmitting a burst and receiving an echo by raising and dropping its output, so the width of the pulse is equal to the time required for the sound to travel to and from the object. Since the time counter register increments every 64 microsecond, the measuring error is around 0.011 meters (~0.43 inch).
Sometimes, the ultrasonic sensor might get no response and will set the pulse width to the maximum. Beside open space cases and curvy or inclined surfaces, this could be caused by objects that do not reflect sound, for example pillows.
Developing the Code
To use SynthOS to generate our application specific operating system (ASOS), we had to program the CPU in pure C. There are plenty of websites telling how to do that, such as the Can’t Hack, Won’t Hack site. Our code is described below, and the actual code can be found here on GitHub. Documentation on SynthOS can be found here.
To begin our development, we needed to install the following components:
- avr-gcc, binutils-avr, avr-libc – GNU tool chain for Atmel chips
- avrdude – firmware upload utility that talks to Arduino bootloader
Adapting SynthOS to the platform was fairly straightforward. All we needed to do was define three routines to deal with interrupts, and we defined the following tasks:
- Left motor control loop task
- Right motor control loop task
- Scanning and high level control loop task
- A couple of call tasks
Since SynthOS does not allow a task to have several instances, we had to copy the left motor task to the right motor task by swapping the words left and right in the code.
SynthOS made writing code for task communication simple. The SynthOS_wait(cond) primitive is used to wait for any condition that can be expressed using global variables and constants. On the trigger side, nothing needed to be done at all; SynthOS automatically inserted the code that monitors the affected variables and activates tasks when necessary.
Programming the delays was very easy. The only variable used was a tick counter that we named clock, and xyz_timer for every waiting task which holds the counter value at the delay start. The timer interrupt routine just incremented clock and SynthOS did the rest. Note that the timer interrupt routine had no knowledge about waiting tasks, and SynthOS had no knowledge about how our timekeeping routines worked. SynthOS treated clock just like any other synchronization variable. If we need a more precise delay, we can first use the SynthOS_wait() primitive to approximate the delay and a loop containing a SynthOS_sleep() primitive for the rest of the interval.
We needed the USART for debugging. We followed the traditional approach of using circular input and output buffers. Again, we used SynthOS’s ability to track variables to synchronize tasks with the USART interrupt handlers. The interrupt handlers updated buffer pointers and tasks and waited for proper conditions.
Unfortunately, we could not use the standard printf() function because it calls the blocking putchar() routine, and SynthOS restricts the use of blocking primitives to top-level functions. So, we re-implemented printf() as a SynthOS call task, which embedded SynthOS blocking primitives. Combining ellipses and the SynthOS_call() primitive is generally possible now, but SynthOS did not support it at the time, so we had to make our print function taking a fixed number of arguments.
It is worth mentioning that in SynthOS there is no need to declare synchronization variables as volatile unless they are modified by interrupt handlers. This is because all control transfers are transparent to the compiler.
A Video of Our Robot
Here is a video of our robot in action.
Our Arduino robot was a fun project. We learned a lot about ultrasonic sensors, motors, the Arduino processor, and robots in general. Using SynthOS allowed us to quickly come up with clean and succinct code that has very small footprint. We used less than 1K of SRAM and around 13.5K of flash. Taking out debugging code reduced the SRAM usage to 0.5K and 8.6K of flash. We ported the code to FreeRTOS, where it required 1.25K or SRAM and 12.8K of flash, significantly more than our SynthOS-based solution.
You can try SynthOS at www.SynthOSonline.com.
Senior Software Engineer
VP of Product Management