How to build a cheap robot from a Nikko RC Car
So, I wanted to play around with robotics, but alas, even though I can design electronics or program microcontrollers, it seems I'm not too good at building mechanical contraptions! I tried, and couldn't manage to build gear boxes or chassises. Does this mean I need to give up on robotics? No. Instead, I've taken a cheap remote controlled car and turned it into a robotics platform.
- A photo of the whole robot.
Why do I call it a platform and not just a robot? Well, because I view this hardware and associated firmware as just a general robot that doesn't do anything in particular. Really, my aims were to build a moving craft which could communicate. In that way, this work represents an advanced starting point for future experiments, even though it doesn't do anything on its own.
The robot is sort of bare bones, but supports these features:
- Forward and reverse, with 128 increment speed control via PWM.
- Left and right, with 128 increment angle control via PWM.
- Full-duplex, true ad-hoc communications via modulated infrared light.
- A front and rear bumper.
- Room for software expansion:
- Only 399 of 2048 words from program space are used.
- Only 24 of 224 bytes from ram are used.
- Only TIMER0 is used; TIMER1 and TIMER2 are unused, as is the enhanced CCP module.
- Five I/O pins are unused (though four of them are wired to LEDs as a status indicator).
As cool as it is, I am completely unhappy with these aspects of the design:
- It uses rack and pinion steering, which is completely unintuitive when writing software. I would prefer differential steering.
- It has no odometry, which is to say that all navigation is open-loop and therefore unpredictable. Even if I added this, odometry for rack and pinion steering involves some annoying math that I would rather not implement on an 8-bit processor without floating point.
- I couldn't manage to get the PIC's CCP1 Module to generate the 38KHz carrier signal for communications, and had to fall back to using an external 555 timer.
To begin with, I bought a Nikko RC Ford GT for about $20 from Toys'R'Us. It was easy to disassemble---just a few screws and the plastic shell comes off. Take off the plastic cover to expose the RC circuitry, and snip all of the wires going into the board. You are left with:
- A Chassis
- A DC Motor with gearbox for driving
- A DC Motor with gearbox, spring loaded, for rack and pinion steering
- A battery holder (which holds up to 6x AAs), and a power switch.
- 4x Wheels
I also modified the batter pack slightly. In its stock configuration, the pack is six AA batteries in series. However, I wanted two separate battery supplies, so that the motors do not draw too much from the logic. Thus, I modded the case so that there were four AAs in series to create 6V for the logic, and two AAs in series to create 3V for the motors.
- A photo of the car with the Ford GT shell removed, and with a few holes drilled for wires.
- A photo of the modification to the battery pack, top view.
- A photo of the modification to the battery pack, bottom view.
This is all pretty straight-forward, but I'll give you some highlights.
The brains for this bug are a PIC16F628A microcontroller. I choose this microcontroller because it's cheap (supplier), and because it has a USART and a (relatively) large amount of RAM (a whopping 224 bytes). The software on it is detailed below.
- The schematic of the PIC microcontroller.
- The schematic of the power supply.
- The schematic of the bumpers (it's just two switches).
RF costs more than I want to put into this; the cheapest RF tranceiver pair I found was $12. So, instead I've chosen to pursue modulated infrared as a communications medium, for a cost of less than three dollars per tranceiver pair (three IR LEDs and three IR detectors per pair).
So, the pic transmits data at 1200 baud out of its USART, while a 555 generates a 38KHz signal. These two signals are combined using a LM386 power amplifier so that it outputs 38KHz when the data is low, and nothing otherwise. Its output drives three infrared leds (in parallel).
The reciever is the Sharp GP1UX501QS. This chip outputs a logic low value when it detects an infrared light turning on and off at a rate of 38.0KHz. I combine three of these through a diode OR gate, where it feeds into the microcontroller's USART.
I used the NJM2670 Dual H-Bridge driver IC to power the motors, as the output pins of the microcontroller are not at the right level, nor do they drive enough current. For each motor, the microcontroller outputs three signals: "Direction Plus", "Direction Minus" and "Speed." Direction Plus and Minus control the direction that current travels through the motor, or stops the motor, and Speed is a pulse-width modulated signal to control.
There is not yet any odometry.
- The schematic of the H-Bridge and motors.
Despite its compact size, the software is quite complex, and it will take me a while to create a good writeup on it's operation. In the mean time, you can download it's source here, and read some informal notes below.
The software can be viewed as four distinct parts: the boot procedure, the event loop, the transceiver and the motor controller. The boot procedure simply initializes the other modules and the PIC's peripherals, and is pretty standard. The event loop simply waits until "events" have occured, and then acts upon them. These events are, at the moment, limited to packets which have been received.
The other two halves--the transceiver and motor controller--operate during interrupts, and thus can be mostly ignored by the event loop. This design model (though I would argue was necessary, since peripherals are asynchronous) proved to work very well; I like to think of it as sort of an operating system for the robot. I will detail the tranceiver and motor controller below.
The robot features full-duplex packet-based communications at 1200 baud over modulated infrared. I created this communications protocol for this project; make no mistake, this is not IrDA standard.
- Must work in my room, which is to say that it doesn't need to work in a football field or across my apartment.
- Simple enough for a PIC microcontroller to use it, without excessive load. In particular, this means that the protocol should:
- be byte oriented;
- employ short packets; and
- use a computationally-cheap checksumming method.
- Since IR is a shared medium, and as I would ultimately like to have multiple devices using this protocol on that single medium, the protocol/implementation should:
- allow for the detection "doubling," or that two devices transmitted at once;
- retransmit upon doubling;
- be able to re-synchronize after a broken packet has been received;
- actively avoid doubling by holding transmissions until the line is perceived unused; and
- have unique network addresses for each device, as well as a "broadcast" address and optional "base station" address.
- And finally, because I think that autonomous robotic systems are much cooler than remote-controlled stations:
- The ability for any two or more devices on the network to build a network without the aid of a "base station." In otherwords, I wanted these to operate as an Ad-hoc network.
- This simply means that identical devices must be capable of determining unique network addresses.
- As a consequence, it precludes the use of "request-to-send"/"clear-to-send" style line multiplexing.
- As another consequence, it precludes token-ring style line multiplexing.
Here is the design of the communications stack, expressed in terms of the OSI Model:
1. Physical Layer - Network Hardware.
Physical communications occur over a modulated infrared link. The wavelenth of light should be between 840nm and 940nm, with the higher end being preferred. This light is then turned on and off at 38KHz. The presence of such light represents a logic '0'; the absence of such light represents a logic '1'. Attempts should be made to isolate the transmitter and receiver hardware, so that the device does not detect "echos" of its own communications; this may be impossible, in which case it is handled by the transport layer.
I simply transmit bytes via the PIC's UART at 1200 baud 8N1, and modulate that signal against a 38KHz square wave and pipe it to some IR LEDs. However, for completeness... Bits are transmitted in a serial fashion at 1200 baud. Bytes are transmitted with one start bit at logic '0', eight data bits, least significant bit first, and one stop bit at logic '1'. The hardware will begin to receive a byte when the start bit arrives. If the stop bit is not read as logic '1', we call this a frame error. If bytes arrive faster than the firmware can process them, a loss of bytes will result, and we call this an overflow error.
2. Data Link Layer - Communication between Adjacent Nodes.
At this level of the network stack, we assume that each network device has a unique network identifier (NID), which is an 8-bit number. We reserve the NID 0x00 for the base station, and the nid 0xFF as the broadcast address. Additionally, we reserve the NID 0xA5; no device should use NID 0xA5 for reasons that will soon be made clear.
All communications are organized into 6-byte packets. The byte fields of these packets are as follows:
- Start-Byte (one byte)
- Sender-NID (one byte)
- Recipient-NID (one byte)
- Payload-0 (one byte)
- Payload-1 (one byte)
- Checksum (one byte)
The start byte is always 0xA5, and has a purpose analogous the the start bit in serial communications.
The Sender-NID and Recipient-NID bytes contain the purported sender and intended recipient of this packet. The Recipient-NID may be 0xFF to denote a broadcast to all devices. Neither of these bytes may contain 0xA5. If after a valid packet has been received, the Recipient-NID does not match the device's NID, and if the Recipient-NID is not the broadcast address, the packet is dropped.
The Payload-0 and Payload-1 bytes hold message specific information which are defined in the Presentation Layer and Application Layer. Payload-0 may not contain 0xA5, though Payload-1 may.
The Checksum is an 8-bit quantity with a special property. The Checksum's value is chosen so that the sum of all bytes of the packet (in 8-bit 2's-complement arithmetic) is zero. The Checksum may take any value necessary to achieve this. If the Checksum's property does not hold, then the message is invalid and should be discarded.
3. Network Layer - Communication between Non-Adjacent Nodes.
There is no network layer. We assume that the infrared light will "flood" the room, thus all network devices are adjacent to all other network devices.
4. Transport Layer - Reliable Communications and Error Recovery.
The transport layer can be thought of as a method for error detection, a method for error recovery (resetting the receiver, fast-reset on the receiver, and retransmission), and a method to reduce the number of errors which occur (transmitter yields to receiver).
Digression: What are the error scenarios, which can we detect, and from which can we recover? Transmissions can fail to reach their recipient (out-of-range errors), or can be only partially received; transmissions may end prematurely upon device failure (hanging-packet errors), transmissions may be corrupted during transit (broken-packet errors), two devices may transmit at the same time over the shared channel (double errors) which will most likely corrupt the data (broken-packet); or as a special case, we might detect a transmission while we are transmitting (first-person double errors).
Out-of-range errors cannot be detected unless higher-level protocols request a receipt for their packets. Hanging-packet errors are detected by imposing a timeout on the duration of a packet. Broken-packet errors are detected by frame errors, inadmissable field values (such as Sender-NID == 0xA5), or invalid checksums. Because the value 0xA5 is only admissable in the Start-Byte, the Payload-1 byte and in the Checksum byte, the receiver state machine is able to very quickly recover from broken-packet errors. Overflow errors actually cause broken-packet errors, and are detected by the UART hardware.
Double errors are hard to detect by a third party, to whom these simply look like broken-packet errors. However, first-person double errors can be detected and, to a large degree, avoided.
To avoid double-errors, each network device will not transmit while the receiver has received part of a packet. Transmission will begin after the packet has been received. However, there is a twist: in the case of a hanging-packet error, this wait will be indefinate. Thus, when the transmitter has waited a certain number of cycles for a packet to be received, the transmitter will abort the packet and put the receiver into its ready state. It will then commence transmission.
However, if the transmitter has begun transmission, and only then does an incoming packet arrive, we have detected a first-person double error. To further compound issues, this first-person double error may be caused by an imperfect isolation of the transmitter and receiver hardware (remember, IR will bounce off of walls). Thus, a heuristic approach is used to mitigate this situation. The receiver decides the severity of a first-person double error based on this assumption: if a double error caused a disruption of communications, then the packet being received will be invalid, and thus first-person double errors only warrant a retransmission if the received packet was invalid.
If a first-person double error occurs, we set a software flag called "double." If this flag is set once the transmitter has completed a packet, the transmitter will wait until the receiver has decided upon the severity of the error, or will timeout. If a timeout occurred, or if the receiver decided that a retransmission is necessary, then the transmitter will try to transmit the packet again as usual.
The most important lesson here is that error detection is stronger at the receiver's end than at the transmitter's end.
5. Session Layer - Synchronicity
The session layer is not used; packets have no sense of order or timeliness.
6. Presentation Layer - Specifies Data Formats.
We have already defined the NID quantity as an 8-bit quantity, where 0x00 denotes the base station, 0xFF denotes the broadcast address (destination only), and 0xA5 is reserved. We need to define a few more.
A Motor-Specification quantity is an 8-bit quantity used to define the modulator state for a motor. The most significant bit of a Motor-Specification quantity denotes direction, where '0' means forward and '1' means reverse. The lower seven bits of a Motor-Specification quantity are an unsigned 7-bit magnitude quantity, where 0x7F means 100% duty and 0x00 means 0% duty. The Motor-Specification 0x00 means full stop, which means that the two leads going to the wire should be connected to ground (break).
An Orientation-Specification quantity is an 8-bit unsigned integer represented in bogo-radians. Bogo-radians are an integer measure of angle, computed as:
value-in-bogoradians = round( 256 * value-in-radians / (2pi) )
One bogoradian is about 1.4 degrees or about 0.025 radians.
7. Application Layer - Specifies Data Meanings.
The application layer defines several packet types, detailed in the table below. Additionally, the Application layer describes the Boot-Ping protocol, which is the method by which each of these devices chooses a unique NID.
The application layer is the first layer that is implemented in the event loop, and not in an interrupt service routine.
|0x00||XX (int)||Ping. Requests ackowledgement, where XX is a random number. This is also used in the boot-ping protocol, described below.|
|0x02||dc||Request sensor readings|
|0x03||dc||Reset. Causes the recipient to allow its watchdog timer to expire, thus causing a device reset.|
|0x04||XX (Motor-Spec)||Set Motor1/DriveMotor to XX|
|0x05||XX (Motor-Spec)||Set Motor2/SteerMotor to XX|
|0x06||dc||Request absolute position. This is currently under-implemented, since these robots do not have any odometry|
|0x08||LSB(X) (int)||Report the least-significant byte of the X-component of the robot's absolute position. Units are implementation specific.|
|0x09||MSB(X) (int)||Report the most-significant byte of the X-component of the robot's absolute position.|
|0x0A||LSB(Y) (int)||Report the least-significant byte of the Y-component of the robot's absolute position.|
|0x0B||MSB(Y) (int)||Report the most-significant byte of the Y-component of the robot's absolute position.|
|0x0C||O (bogo-radians)||Report the absolute orientation of the robot in bogo-radians.|
|0x0D||dc||Report that the absolute position is unknown. At the moment, these robots always respond to a Request Absolute Position packet with one of these.|
|0x0E||XX (int)||Report sensor reading. The format of XX is implementation specific|
|0x0F||XX (int)||Pong. XX is the 8-bit 2's-complement negative of the XX sent in the Ping.|
|0xA5||dc||Invalid value for Payload-0|
Key: dc means "don't care."
The application layer also defines the Boot-Ping protocol for the resolution of NID conflicts. During bootup, each device (except for the base-station, if present) should select a random NID. It then sends a Boot-Ping. A Boot-Ping differs from a Ping in two important ways. First, it is sent both from and to the device's NID. Second, the random value chosed for Payload-1 is saved in RAM. If a device receives a Boot-Ping, that Ping was either sent by that device, or by another device with the same NID, and these two cases can be decided by the value of Payload-1 and the saved Payload-1 in RAM. If we have detected that someone else shares our NID, we choose a new random NID and repeat the Boot-Ping process.
The code for the communications stack (layer 1 -- 4) is implemented in transceiver.asm. It is (so-far) the largest component of the N1 software, occupying about 216 instruction words. The protocol stack's specification was realized using two finite state machines (FSM); one for the transmitter and one for the receiver. Just so you know, the average case behavior of these machines is hilighted with bold blue arcs.
By performing a simple transformation on these graphs, we can also view them as interrupt service routines, which have a single entry and single exit point. When reading the code, you will realize that the names for each of the labels is take from these diagrams.
Additionally, I would like to post some timing diagrams which explain the Tx/Rx interaction/error scenarios; if anyone knows of a decent timing diagram editer for linux, please drop me a line.
Coming soon. Key concept is that it makes two pulse-width modulated signals that are passed to the H-bridge driver, and that is operates upon TIMER0 overflow...
So, that didn't seem hard enough, so I'm already planning the next step. In particular, I plan:
- A custom built chassis, using differential steering and these gearmotors and wheels.
- Odometry, using these cheap mechanical rotary encoders, so that the robot can keep track of it's position.
- Change the communications protocol so that a single packet can hold eight payload bytes instead of just two.
- To get the CCP1 module and TIMER2 to generate the 38KHz signal, so I don't need an external 555.
- Some primitive robotic cartography. For instance, I build three of these robots, put them in an environment with a few obstructions, and tell them all to get somewhere. The robots would record the location of each obstacle they bump into and update their course to go around it, all the while collaboratively building a map of the room, which I could download and plot.