ATtiny Sonar Controller

HC-SR04 sensor

Remember these guys? HC-SR04 ultrasonic rangefinders. Colin used to have three of them, but I’ve recently been reworking his sensor layout. I’m starting by adding five more sensors for a total of eight. Colin’s Arduino has just barely enough free pins to accommodate eight sensors, but I don’t want to spend all of my available pins on sensors. That would leave me no room to expand. Further, I’m not totally sure eight sonar sensors will be enough. My HC-SR04 sensors have a range of about 15 degrees, so it would take 24 sonar sensors to cover 360 degrees. I don’t know if that will be necessary, but I’d like to have the option.

There are a couple of ways to do this. The simplest would probably be to use a shift register. Shift registers allow you to use three I/O pins to control more than a thousand additional pins, but operating them involves some computing overhead and the Arduino still has to be involved in every sensor reading operation.

The chip for my sonar controller, an ATtiny84

An Atmel ATtiny84

The method I’ve chosen is to use a separate microcontroller, an ATtiny84, to handle the sonar sensors. The Aduino tells the ATtiny that it needs new sensor readings. Then it goes and performs some other computations while the ATtiny pings its sensors. Then, when the new sensor data is ready, the ATtiny sends back the new readings all at once. Using this scheme, the Arduino doesn’t have to spend processor time reading sensors. Instead it can focus on other problems. Also, since it can communicate with the Arduino via I2C, it only requires 2 I/O pins! Here’s what Colin looks like with his new sonar layout.

Colin with his new sonar

Colin with his new sonar ring

As with my earlier post on Raspberry Pi to Arduino communication, there’s several steps to this process. Luckily it’s a lot simpler this time.


The Communication Protocol

The communication protocol is going to be very similar to the one that I outlined for communicating between a Raspberry Pi and an Arduino, only simplified. Basically, the Arduino tells the ATtiny when it needs updated sonar readings. The ATtiny then pings all of its sensors and sends back the readings when it’s done. This is how it works:

  • The Arduino sends a byte to the ATtiny
    • The byte has no meaning, it’s just a flag to signal the ATtiny to ping its sensors
  • The ATtiny pings its sensors
  • The ATtiny sends its readings back to the Arduino as 16 bytes
  • The Arduino assembles the bytes into 8 16-bit ints

One other difference with the Raspberry Pi to Arduino protocol is that we’ll be using I2C instead of serial. If you’re not familiar with I2C I’d suggest reading up on it. Sparkfun has a great tutorial.

That’s really all there is to it! The protocol is simple, but there’s some fiddly details to work out in the code.

Top of page


The Sonar Controller

sonar controller with ATtiny84

My ATtiny84-based sonar controller

My sonar controller basically consists of an ATtiny84, with eight inputs for HC-SR04 ultrasonic rangefinders. If you’re interested in making your own, you can download my eagle files for the schematic, board layout, and the gerber files for fabbing the PCB. The gerbers are formatted for fabrication by OSH Park, which I highly recommend for low-volume jobs.

The controllers each have two sets of inputs for power and I2C communication so multiple controllers can be chained together on the same bus. Note that there are two spots for 4.7 kOhm resistors on each controller. These are pull-ups for the I2C buses. If you’re chaining multiple controllers together, only one of them needs pull-up resistors. The resistor spots on the other controllers can be left empty.

I also designed a laser-cut frame to hold the sonar sensors and controller, as well as some fittings to attach the whole business to Colin. The design is pictured below, and you can download the SVG files to make your own parts here.

Colin's new sonar arrangement

Colin with his new sensor arrangement and fittings

If you’d rather not fab a custom circuit board, you can set it up on a breadboard as shown below. It’s not particularly useful on a breadboard, but it will allow you to experiment with and test it.

breadboard wiring

Breadboard wiring for ATtiny sonar controller

Top of page


The Code!

ATtiny Code

Let’s start out with a couple of preliminaries. First, for I2C communication I’m using the TinyWireS and TinyWireM libraries. We need our ATtiny to perform as both master and slave, but there is no existing library that allows this. Fortunately there’s a way to hack around this problem.

I’m also using the NewPing library. The NewPing library doesn’t work with ATtinies, but there’s a hack for this too. You just need to comment out the functions for Timer2 and Timer4 because the ATtiny does not have these timers.

You can find the complete program for the ATtiny here. If you’re not familiar with how to program ATtinies, I’d suggest going through this very thorough tutorial.

Initiating A Sensor Update

The function below pings the eight sonar sensors and records the ping times in microseconds.

void updatePingTimes(uint8_t bytes)
{
  TinyWireS.receive();
  for (int i = 0; i < NUM_SONAR; i++)
  {
    pingTimes[i] = sonar[i].ping();
  }
  sendTimes();
}

The function above is invoked using an interrupt when the ATtiny receives a byte from the Arduino. The line below is called in the setup() function to tell the ATtiny to generate an interrupt and call the updatePingTimes() function whenever data is received from the Arduino.

TinyWireS.onReceive(updatePingTimes);

Sending Data Back To The Arduino

When the sonar sensors have been updated, the sendTimes() function is called to send the updated ping times back to the Arduino. It is important to note that the ATtiny must be functioning as a slave in order to use the onReceive() function. To send data back to the Arduino, the ATtiny must function as a master. This makes the sendTimes() function a bit more complicated.

void sendTimes()
{
  // clear the I2C registers
  USICR = 0;
  USISR = 0;
  USIDR = 0;

  // start I2C with ATtiny as master
  TinyWireM.begin();

  // transmit ping times to Arduino
  TinyWireM.beginTransmission(MASTER_ADDRESS);
  for (int i = 0; i < NUM_SONAR; i++)
  {
    int thisTime = pingTimes[i];
    if (thisTime == 0) thisTime = (double)MAX_DISTANCE / speedOfSound;
    uint8_t firstByte = thisTime & 0xFF;
    uint8_t secondByte = (thisTime >> 8) & 0xFF;
    TinyWireM.write(firstByte);
    TinyWireM.write(secondByte);
  }
  TinyWireM.endTransmission();

  // clear I2C registers again
  USICR = 0;
  USISR = 0;
  USIDR = 0;

  // put ATtiny back in slave mode
  TinyWireS.begin(OWN_ADDRESS);
}

As I said before, I don’t know of any library that allows an ATtiny to function as both master and slave. Fortunately we can hack our way around this problem by clearing the ATtiny’s I2C registers and restarting communication in master mode. After we’ve transmitted the ping times back to the Arduino we need to clear the I2C registers again and put the ATtiny back in slave mode so it can be ready for the next request from the Arduino. I don’t mind telling you it took me a lot of time with the ATtiny’s datasheet to figure out how to make that work.


Arduino Code

For reference you can find the complete program for the Arduino here. To get things going we need to include the Wire library and put the following two lines in the setup() function:

// begin communication with sonar controller
Wire.begin(OWN_ADDRESS);
Wire.onReceive(updateDistances);

Adding the onReceive() function is particularly important because it allows the Arduino to do other processing tasks while the ATtiny pings its sensors. When the ATtiny sends data it generates an interrupt, the Arduino stops what it’s currently doing and receives the new data, then goes back to whatever it was doing before.

Receiving Data

The updateDistances() function works as follows:

// updates sonar distance measurements when a new reading is available
void updateDistances(int bytes)
{
  for (int i = 0; i < NUM_SONAR; i++)
    readSonar(i);
  distancesRead = true;
}

// reads the distance for a single sonar sensor
// expects to receive values as ints broken into 2 byte pairs,
// least significant byte first
void readSonar(int index)
{
  int firstByte = Wire.read();
  int secondByte = Wire.read();
  sonarDistances[index] = ((double)((secondByte << 8) | firstByte)) * speedOfSound * 0.5;
}

Notice that the sonar distance is multiplied by 0.5 when it’s added to the sonarDistances array. This is because the ATtiny returns the total time of flight for the ultrasonic ping. The ping needs to go from the sensor, to the obstacle, and back. This means multiplying the ping time by the speed of sound would result in twice the distance between the obstacle and the sensor.

Requesting An Update

Fortunately, requesting an update is pretty simple. The Arduino just needs to send a byte, any byte, to the ATtiny to let it know it needs new sensor readings.

// requests a sonar update from the sonar controller at the given address
// by sending a meaningless value via I2C to trigger an update
void requestSonarUpdate(int address)
{
  Wire.beginTransmission(address);
  Wire.write(trig);
  Wire.endTransmission();
}

After this function executes, the Arduino can perform other processing tasks until the ATtiny sends back updated sonar readings.

Top of page


Going Further

There are a number of ways to solve the problem of coordinating large numbers of sonar sensors, and the above method is only one possibility. It takes a long time to update sonar sensors, up to 15 milliseconds for 8 sensors. For an Arduino running at 16 MHz, that’s 240,000 processor cycles. So it’s advantageous to use a method that allows the Arduino to do something else while the sensors are being updated. My method does this, but one could also take the Arduino out of the loop entirely and have the Raspberry Pi talk to the sonar controller directly. It would be interesting to implement this method and compare it to the one presented above.

Top of page

Ultrasonic Sensors

If you’ve been through my simple motor control tutorial you now know how to control the input voltage to DC motors using an Arduino. In a differential drive robot like Colin, this means driving in a (nearly) straight line, a circle, or some other preset path. It would be more interesting if we could make a robot that reacts to its surroundings. For this we need sensors and ultrasonic sonar sensors are a good place to start.

Ultrasonic sensors determine the distance between themselves and the nearest obstacle by emitting an ultrasound pulse and timing how long it takes for that pulse to be reflected off the nearest obstacle and back to the sensor. They are easy to use, accurate, and they can be extremely cheap.

They do have limitations, however. Sound reflects best off of hard, smooth objects, so soft or uneven surfaces are not detected very well. Most kinds of fabric are basically invisible to an ultrasonic sensor. Reflection is another problem. To work properly, the sensor needs an obstacle to reflect its emitted pulse straight back to the sensor. If the sensor isn’t aimed straight at a perpendicular surface, the reflected pulse could miss the sensor entirely. In this case the sensor would not detect the obstacle. The datasheets for most sensors specify an angle within which the sensor will be able to reliably detect obstacles.

HC-SR04 sensor

HC-SR04 sensor

For this tutorial I’ll be using an HC-SR04 sensor. The datasheet for the sensor can be found here. Note that it will only reliably detect obstacles within +/- 15 degrees from perpendicular to the face of the sensor. The HC-SR04 definitely isn’t the best ultrasonic sensor out there. More accurate, longer range sensors exist. You cannot, however, find a cheaper ultrasonic sensor than the HC-SR04. If you shop around on Amazon you can find them for $1-3 apiece.

Before I get into the actual tutorial I should mention I’m making use of Tim Eckel’s NewPing library for Arduino here. It makes using these sensors a complete snap. You should definitely take a look at the tutorial on the Arduino Playground in addition to this one. If you don’t have experience installing or using new libraries in Arduino, take a look at this tutorial.


Single Sensor Example

We’ll start out by wiring up and testing a single HC-SR04 sensor. You’ll want to wire it up as in the diagram below.

Wiring diagram for a single HC-SR04 sensor.

Wiring diagram for a single HC-SR04 sensor.

Note the Trig and Echo pins on the sensor can be connected to different pins on the Arduino. The NewPing library makes it possible to connect the Trig and Echo pins to the same Arduino pin. This is great because it means the HC-SR04 doesn’t occupy as many I/O pins.

Note also there is no power source pictured on the above diagram. For the purposes of this exercise we can power the Arduino through its USB port, so no external power source is required.

Example Sketch

#include <NewPing.h>

#define TRIGGER_PIN 2
#define ECHO_PIN 2
#define MAX_DISTANCE 200 // max distance the sensor will return

NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // declare a NewPing object

void setup() {
  Serial.begin(115200);
}

void loop() {
  delay(50);
  int uS = sonar.ping(); 
  Serial.print("Ping: ");
  Serial.print(uS / US_ROUNDTRIP_CM); // convert ping time to distance in cm
  Serial.println("cm");
}

Load the above sketch on your Arduino and open a terminal window. Make sure the baud rate of the window is the same as the baud rate in your Serial.begin();  statement.

When initializing a NewPing object use the following form: NewPing(triggerPin, echoPin, distanceLimit); If you’re using the same pin for the trigger and echo just set triggerPin and echoPin to the same pin.

The example sketch will display the distance from the sensor to the nearest obstacle in centimeters every 50 milliseconds. If the nearest obstacle is beyond the distanceLimit or if the nearest obstacle is not detectable then sonar.Ping() will return 0.

Play around with the single sensor for a bit. Have some fun with it. Eventually start to realize how limited a single sensor is. A robot that can only see directly in front of itself can’t see obstacles at its sides. So if it approaches an obstacle at an angle, the obstacle won’t be detected. So what should we do? Add more sensors, of course!


Three Sensor Example

Now that we’ve got a single sensor up and running it should be a simple matter to add two more sensors. It can be useful to have an array of sensors when we’re trying to detect obstacles around a mobile robot like Colin. Wire the sensors up as in the diagram below.

Wiring diagram for three HC-SR04 sensors.

Wiring diagram for three HC-SR04 sensors.

Example Code

#include <NewPing.h>

// trigger and echo pins for each sensor
#define SONAR1 2
#define SONAR2 3
#define SONAR3 4
#define MAX_DISTANCE 200 // maximum distance for sensors
#define NUM_SONAR 3 // number of sonar sensors

NewPing sonar[NUM_SONAR] = { // array of sonar sensor objects
  NewPing(SONAR1, SONAR1, MAX_DISTANCE),
  NewPing(SONAR2, SONAR2, MAX_DISTANCE),
  NewPing(SONAR3, SONAR3, MAX_DISTANCE)
};

int distance[NUM_SONAR]; // array stores distances for each
                         // sensor in cm

void setup() {
  Serial.begin(115200);
}

void loop() {
  delay(50);
  updateSonar(); // update the distance array
  // print all distances
  Serial.print("Sonar 1: ");
  Serial.print(distance[0]);
  Serial.print("  Sonar 2: ");
  Serial.print(distance[1]);
  Serial.print("  Sonar 3: ");
  Serial.println(distance[2]);
}

// takes a new reading from each sensor and updates the
// distance array
void updateSonar() {
  for (int i = 0; i < NUM_SONAR; i++) {
    distance[i] = sonar[i].ping_cm(); // update distance
    // sonar sensors return 0 if no obstacle is detected
    // change distance to max value if no obstacle is detected
    if (distance[i] == 0)
      distance[i] = MAX_DISTANCE;
  }
}

Note the step in the updateSonar() method that checks if the distance returned by sonar[i].ping_cm() is 0, meaning the nearest obstacle is probably out of range. Technically it’s possible there is an obstacle 0cm away from the sensor, but that is not very likely. If the value returned by sonar[i].ping_cm() is 0, we switch it to MAX_DISTANCE .

Now we can control 3 sonar sensors simultaneously. We can use this in conjunction with our ability to control motors to make a robot that has autonomous behavior! One of the simpler things we can do with these two tools is program an obstacle avoidance routine. If that interests you stay tuned for my next tutorial, because I’m planning to do in on that very topic!