My brain made me build a robot. June 1, 2015
A while back, I was held hostage by my own brain: it refused to let me sleep until I got out of bed and built a robot.
What prompted this was the delivery of some cheap ultrasonic range finders I’d bought. I wanted to try them out in a project; something more interesting than just displaying the distance measured, but not so complex that the range finders became secondary to the rest of the project. Eventually, I came up with an idea for a robot which was both simple and interesting. Unfortunately, this idea came to me while I was on the verge of sleep. The idea was a little too interesting: my brain refused to stop working on it, ultimately issuing its ultimatum. No robot, no sleep.
As usual with these spur-of-the-moment projects, I hacked the robot together from materials I had on-hand; the majority of my time was spent digging through my junk bin for suitable parts and matching screws. The final robot is a combination of random bits including PC case hardware, vintage Erector Set pieces, and a small bit of furniture-grade hardwood, atop a set of toy tank treads and powered by a twin gear motor. All in all, the robot’s body is somewhat unremarkable, other than having been assembled from junk by someone half asleep. The robot’s ‘brain,’ however, is much more interesting.
(Note: again, the code samples cause my theme to render badly in several browsers. My apologies; redoing/modernizing my blog’s theme is on the to-do list.)
One of the few books that has markedly affected the way I think is Vehicles: Experiments in Synthetic Psychology by cyberneticist Valentino Braitenberg. Half my life ago, I stumbled upon a copy in a used bookstore, and reading it was like getting a new pair of eyeglasses. In the book, Braitenberg describes a series of hypothetical robot ‘vehicles’ of increasing complexity. Most are simply one or more light sensors driving two wheels, but some simple arrangements produce what appear to be highly complex behaviors.
I modeled my robot after the second or third vehicle he describes. In short: it has two range finders, one on each of the front corners, angled outwards by about 45 degrees. Each range finder controls the speed of the motor on the opposite side, so a shorter distance means a slower motor. That’s all. I built it with an Arduino, just because it was easy, but even that was overkill: were I an analog guy, the entire thing could have been built with just a comparator or something, no computing at all.
As simple as it is, it works, and it works much better than I’d expected. When the robot approaches an obstacle, the distance on one range finder decreases. This causes the opposite motor to slow down, veering the robot away from the obstacle. Of course, the robot can get stuck in a narrow space, or will bump into things it approaches straight on, but it still does a remarkable job of obstacle avoidance. Here’s the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <NewPing.h> // Range finder constants, pins and such. // The analog input pins are being used as digital I/O (hence // the numbers starting at 14). This was just more convenient // when wiring everything up. const byte ECHO_L = 14; const byte TRIG_L = 15; const byte ECHO_R = 16; const byte TRIG_R = 17; const int MAX_DIST = 500; // PWM pins used for the motors. const byte MOTOR_L = 3; const byte MOTOR_R = 4; NewPing sonarL(TRIG_L, ECHO_L, MAX_DIST); NewPing sonarR(TRIG_R, ECHO_R, MAX_DIST); // Map a distance to a PWM output value. Somewhat more involved // than the standard map() function. byte range2speed(unsigned int range) { // Out-of-range distances sometimes come back as 0. if (range == 0 || range > 75) return 255; return map(range, 1, 75, 80, 255); } void setup() { // Not much to set up. } void loop() { delay(10); unsigned int distanceR = sonarR.ping() / US_ROUNDTRIP_CM; unsigned int distanceL = sonarL.ping() / US_ROUNDTRIP_CM; // This is the trick: have each range finder controll the speed of // the opposite motor. analogWrite(MOTOR_R, range2speed(distanceL)); analogWrite(MOTOR_L, range2speed(distanceR)); } |
Another flaw in version 1 is that moving through a narrow space slows it down. Version 2 (shown below) slightly changes things, making the motor speeds relative to the difference in distances detected, with one or the other always running at full speed. It makes things marginally more complex, but it’s still simple enough to implement entirely in analog, and not too much unlike some neuron behavior I only half-remember.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#include <NewPing.h> const byte ECHO_R = 2; const byte TRIG_R = 3; const byte ECHO_L = 6; const byte TRIG_L = 7; const int MAX_DIST = 15; const byte MOTOR_L = 3; const byte MOTOR_R = 4; NewPing sonarL(TRIG_L, ECHO_L, MAX_DIST); NewPing sonarR(TRIG_R, ECHO_R, MAX_DIST); unsigned int distanceL = 0; unsigned int distanceR = 0; byte range2speed(unsigned int range) { if (range == 0 || range > 75) return 255; return map(range, 1, 75, 80, 255); } void setup() { } void loop() { delay(10); unsigned int distanceR = sonarR.ping() / US_ROUNDTRIP_CM; unsigned int distanceL = sonarL.ping() / US_ROUNDTRIP_CM; distanceR = distanceR ? distanceR : MAX_DIST; distanceL = distanceL ? distanceL : MAX_DIST; float dmax = (float)(max(distanceL, distanceR)); int l = (int)(255 * (distanceR / dmax)); int r = (int)(255 * (distanceL / dmax)); analogWrite(MOTOR_R, range2speed(l)); analogWrite(MOTOR_L, range2speed(r)); } |
Version 3 adds a forward-facing range finder, letting the bot detect if it has run headlong into something. If the forward range goes too low, it momentarily reverses and turns a small amount. It’s more complex on all counts (it requires an H-bridge to drive the motors both forwards and backwards), so it doesn’t have the same conceptual purity. It did, however, get its picture in the Boston Globe!
Leave a Reply
You must be logged in to post a comment.