How to connect and use an ultrasonic rangefinder
How to connect and talk to an ultrasonic rangefinder (aka sonar) over the Make Controller’s GPIO connections.
Purpose
At some point during your Make Controller tinkering, you may find a need to measure distance. Accurate distance measuring is an important input for tasks such as obstacle avoidance for a robot.
There are two popular components that can be used for measuring distances: Infrared and Ultrasonic. While connecting an analog Infrared distance sensor is straightforward with the Make Controller, there are some limitations to using one.
Infrared sensors work by sending a pulse of light and calculating the distance of the reflection through triangulation. The problem here is that this sensor is not effective against poorly reflective surfaces, and since light does not reflect the same way off every surface, measurements reported from the IR sensor will vary *slightly* for different surfaces , colors, and shades; even if the range is the same. There can also be difficulties when interpreting the analog inputs, but that is beyond the scope of this tutorial. Feel free to read more on this here: http://www.acroname.com/robotics/info/articles/sharp/sharp.html.
Ultrasonic Rangefinders (sometimes called Sonar) work by sending pulses of sound out and reporting the time at which the echo returns back to the sensor. Unlike IR sensors, ultrasonic range finders work well in any lighting environment and can be extremely accurate; however sound absorbing objects, such as a sponge may throw off the sensor. Ultrasonic range finders have a large measuring window that can detect objects between 2cm and 3m (~0.75” – 10’).

This tutorial covers the steps necessary to connect a Parallax Ping))) ultrasonic rangefinder to the Make Controller’s GPIO TX connection. The code covered in this tutorial should also work just fine with other range finders, such as the SRF05. Other goodies you’ll find in here include setting up interrupts on PIO pin changes, building and using custom microsecond timers, and soldering headers onto your Make Controller’s GPIO connections.
If you encounter any errors in my code, procedures, or logic along the way, please report them. I’m new to Make Controllers and electronics in general; I dove into this head first about 4 weeks ago. If nothing else, this tutorial is a documentation of what I’ve learned along the way.
Prerequisities
As explained in the front matter, this tutorial covers the connection of a Parallax Ping))) Ultrasonic range finder, so you’ll need one of those. You can purchase this range finder online through many vendors, including your local radioshack for about $30:
http://www.radioshack.com/product/index.jsp?productId=2909789
This range finder was selected because it is manufactured by a well known brand, it features a 3 pin interface freeing up connections for other fun inputs/outputs, and it was easily accessible (picked mine up at Radioshack).
This tutorial also contains an optional step that involves soldering headers onto the Make Controller board to make the connection to the TX GPIO more secure and stable. If you wish to follow along with this, you’ll need your soldering tools, 0.1” headers, contacts and housing units along with some 24 gauge wire. For this step you’ll need some basic soldering skills.
The code used for this tutorial is written in C using firmware v1.6.0. At present time, I did not write an OSC interface, so this code is currently geared towards those interested in firmware development. You’ll need to be somewhat comfortable with C and the makingthings C API: http://www.makingthings.com/ref/firmware/html/.
Step by step
The first step to setting up an ultrasonic range finder is to locate an available connection on the make controller board. Up near the JTAG connection, you’ll notice 6 open connections with a few labels above them. The labels 3.3v, 0v, TX, RX, RTS, and CTS are indicators for a serial connection. Take note that above the TX, RX, RTS, and CTS are labels for GPIO 0, 1, 2, and 3. These connections are pure in the sense that there is no other complex circuitry in between these connections and the controller, so this is why we are selecting this location as our input/output.
For a stable connection, you’ll want to solder a 0.1” header onto the board. You can purchase a 40x1 0.1” header from digikey for about $1.50 (http://search.digikey.com/scripts/DkSearch/dksus.dll?Detail&name=A26509-40-ND). It is nice to have extra parts lying around for future projects, so I just purchased the 40x1 knowing that for this project I’ll only be using 6 of the headers. If you go down a similar path, just snap off a section of 6 headers and put the remainder back in storage. Insert the 6 header pins into the GPIO holes and secure the headers to the Application board by apply solder to the underside of the board.
0.1” Headers soldered onto the Application Board.
To connect a 24 gauge wire to the header, you’ll need some crimp contacts and 0.1” housing. Again, you can get these from digikey at low cost:
Housing: http://search.digikey.com/scripts/DkSearch/dksus.dll?Detail&name=A26978-ND
Contacts: http://search.digikey.com/scripts/DkSearch/dksus.dll?Detail&name=A3000-ND
After attaching the crimp to the wire, the crimped wire should snap into the housing. This housing lines up and securely connects these wires to the headers that you just soldered on.
Housing and contacts.
With the housing securely mounted on top of the newly soldered headers, we can hook up either of the communication pins (TX, RX, RTS, CTS) to the signal (SIG) connection on the PING))). In addition, the sensor will need to be powered by the application board at 5V. Since the 3.3V connection will not provide enough voltage, you can use the power from any one of the power connections on the digital out (Vout1, Vout2) or analog in (Vin1, Vin2) connectors.
The PING))) sensor’s connection features pins with a 0.1” separation. This makes it easy to plug directly into a solderless bread board for fast prototyping. With this same separation, the housing and crimps used earlier could also be a good fit if the solderless breadboard is not an option. Note that you do not need to add resistors to step down the 5V on signal line to 3.3V, as the IO pins are 5V tolerant; pictured below I connect the TX connection directly to SIG on the PING))) without any extra circuitry in between.
Connecting the PING))) sensor to the Make Controller using a solderless breadboard.
From the PING))) datasheet: "The PING))) sensor works by transmitting an ultrasonic (well above human hearing range) burst and providing an output pulse that corresponds to the time required for the burst echo to return to the sensor. By measuring the echo pulse width, the distance to target can easily be calculated."
Communicating with the PING))) is done by sending and receiving pulses on a single input/output line. To request a distance from the PING))) sensor, you must send a high value over the signal line, keep the line high for at least 2 microseconds (uS), and then send a low value over the signal line. In practice, you can keep the line high for much longer than 2uS. In fact, I have gone as long as 1 second with the signal high; the PING))) doesn't start anything until it sees the signal go back down to low. Once the PING))) sees the signal go low, it will begin the distance measuring by emitting short pulses and listening for the echoes to return.
The PING))) returns its results by sending your board a high signal for a time interval that corresponds to the time it takes for a sound sample to be emitted from the sensor, bounce off an object, and return back to the sensor. In order to gather any distance information from our sensor, we'll need to measure the number of uS that elapse from when the PING))) sensor sends the board a high signal to when it sends the board a low signal. For more understanding of how the PING))) sensor works, check out Parallax's very own documentation: http://www.parallax.com/Portals/0/Downloads/docs/prod/acc/28015-PING-v1.5.pdf
Now that there is a better understanding of how the Parallax PING))) operates, it is time to dive into some firmware development. The plan is to approach this iteratively, by first experimenting with the IO module in the makingthings API. After some basic IO is setup, Interrupt Handling will be explored which will allow our code to respond to input changes immediately for more accurate PING communication. Lastly, I'll go over building a custom timer using the microcontroller's TimerCounter (TC).
The first step in working with the rangefinder is to send a high signal, followed by a low signal. To send these signals to the rangefinder, we'll use the IO module (http://www.makingthings.com/ref/firmware/html/group___io.html). You'll notice that all functions in this module require an index that calls out the connection we plan to manipulate. You can find out which indices correspond to which pins by referring to the controller port table found on makingthings.com under resources: http://www.makingthings.com/resources/schematics/controller-port-table. In this tutorial, we are using the TX GPIO pin, which corresponds with index PA01.
We have an index, and now we can initialize our IO and start communicating with the rangefinder. First startup the IO connection:
if( Io_Start(IO_PA01, true) == CONTROLLER_OK)
{
// then we have access to PA01 and successfully locked it
// Setting the IO pin to be parallel i/o since we'll be
// doing both input and output on a single pin
Io_SetPio( IO_PA01, true );
// Configure the line for output, as the first thing we'll
//do is send a high value to the rangefinder
Io_SetDirection( IO_PA01, IO_OUTPUT );
}
Now that all of that is out of the way, we can communicate with the rangefinder with high and low signals. The code that tells the PING))) to sample a distance is as follows:
Io_SetDirection( IO_PA01, IO_OUTPUT );
Io_SetValue( IO_PA01, 1 );
// stay high for at least 2uS according to the PING documentation
Sleep(1);
Io_SetValue( IO_PA01, 0 );
// set the connection to be input now so we can receive a time interval from PING)))
Io_SetDirection( IO_PA01, IO_INPUT );
We are halfway there with the communication. All that is left is receiving notification of input from the PING))) and measuring the elapsed uS that the sensor sends a high signal to our PA01 pin.
To detect changes on PA01, you can either setup a FastTimer and poll the connection, or you can setup an interrupt that will execute a callback function when an input change occurs. We will implement an interrupt for response to input notifications for better performance and accuracy.
There are two parts to setting up interrupt handling: enabling interrupts via the AIC, and defining the interrupt service routine (the callback that is executed when an input change occurs). To learn more about interrupts, be sure to check out the SAM7’s datasheet, sections 24 and 28.5.10
The following code sets up interrupts for the GPIO TX connection. The first step in setting up an interrupt is to enable the peripheral clock for the PIO of interest. Once this is done, we just need to do set some values in the Advanced Interrupt Controller (AIC). It is here that we hookup our callback responsible for handling the interrupt, and set our trigger conditions and priority. After setting the AIC up, all that is left is enabling PIO interrupts for our pin of interest.
// This function sets up the PIO for interrupts
// Specifically we are interested in the GPIO TX connection, which is indexed at PA01
int id = AT91C_ID_PIOA;
//int id = AT91C_ID_PIOB;
// Enable the peripheral clock
AT91C_BASE_PMC->PMC_PCER = 1 << id;
unsigned int mask = 0x1 << id;
/* Disable the interrupt on the interrupt controller */
AT91C_BASE_AIC->AIC_IDCR = mask ;
/* Save the interrupt handler routine pointer and the interrupt priority */
AT91C_BASE_AIC->AIC_SVR[ id ] = (unsigned int)RangeFinderIsr_Wrapper;
/* Store the Source Mode Register. This line defines what triggers the interrupt.*/
AT91C_BASE_AIC->AIC_SMR[ id ] = AT91C_AIC_SRCTYPE_INT_POSITIVE_EDGE | AT91C_AIC_PRIOR_LOWEST ;
/* Clear the interrupt on the interrupt controller */
AT91C_BASE_AIC->AIC_ICCR = mask ;
// Enables the Input Change Interrupt on the I/O line
// Section 28.7.14 of the SAM7 data sheet covers the PIO_IER.
// We are interested in PA01.
int pa01Mask = 1 << ( index & 0x1F );
// mask code pulled from Io_PioEnable
AT91C_BASE_PIOA->PIO_IER = pa01Mask; // PA01
AT91C_BASE_AIC->AIC_IECR = mask;
// use AT91C_BASE_PIOA->PIO_IDR = IO_PA01 to disable interrupts.
Take note that RangeFinderIsr_Wrapper is defined as the callback to handle input pin changes on PA01. This function is defined in a separate file. This separation is necessary, as all interrupt service routines (isr) must be compiled in the 32bit ARM mode while the above code is compiled in 16bit THUMB mode. Refer to the Makefile that accompanies this tutorial for an example of how this done.
The Wrapper function does nothing more than pass all of the work over to the Handler function. According the documentation over at FreeRTOS.org, this the proper setup:
void RangeFinderIsr_Wrapper( void )
{
/* Save the context of the interrupted task. */
portSAVE_CONTEXT();
/* Call the handler to do the work. This must be a separate
function to ensure the stack frame is set up correctly.
This must be a separate function unless you can guarantee that handling the
interrupt will never use any stack space. */
RangeFinderIsr_Handler();
/* Restore the context of whichever task will execute next. */
portRESTORE_CONTEXT();
}
Recall that PING))) will communicate the time of the echo by pulling high on our pin (PA01) when the sound pulse leaves the sensor, and then pulling low when the echo is received by the sensor. Now that interrupts are enabled on input PA01 pin changes, RangeFinderIsr_Wrapper is called when both of these events occur.
The Handler function is fairly simple as well. When an interrupt is received, check to see if the value is high or low. If the value is high, then the rangefinder has emitted sound pulses; if the pulse is low then the rangefinder has received the echo. In this ISR, the startTime is recorded when a high is received and an LED on the application board is illuminated for diagonostics. The stopTime is recorded when a low is received and the diagnostic LED is turned off.
void RangeFinderIsr_Handler( void )
{
portBASE_TYPE xSwitchRequired = pdFALSE;
/* Read the Interrupt Status register (It acknowledge the IT) */
volatile unsigned portLONG pio_isr = AT91C_BASE_PIOA->PIO_ISR;
if (pio_isr & AT91C_ID_PIOA)
{
if(Io_GetValue(IO_PA01))
{
// only process pin changes that occur inside of the
//time frame where PING is operating.
if (rangeFinderInfo->state == 1)
{
AppLed_SetState( 2, 1 );
int numIntervals = TickCounter_GetNumIntervals();
int numTicks = TickCounter_GetNumTicks();
rangeFinderInfo->start = TickCounter_GetTimeUs(numTicks, numIntervals);
rangeFinderInfo->stop = rangeFinderInfo->start;
}
}
else
{
// only process pin changes that occur inside of the
//time frame where PING is operating.
if (rangeFinderInfo->state == 1)
{
int numIntervals = TickCounter_GetNumIntervals();
int numTicks = TickCounter_GetNumTicks();
rangeFinderInfo->stop = TickCounter_GetTimeUs(numTicks, numIntervals);
AppLed_SetState( 2, 0 );
}
}
/* Clear the interrupt. */
AT91C_BASE_AIC->AIC_EOICR = 0;
}
else
{
AppLed_SetState( 3, 1 );
}
/* If a task was woken by either a frame being received then we may need to
switch to another task. If the unblocked task was of higher priority then
the interrupted task it will then execute immediately that the ISR
completes. */
if( xSwitchRequired )
{
portYIELD_FROM_ISR();
}
return;
}
All that remains is to calculate the number of elapsed microseconds between these two events. In the code snippet above, you may have wondered where that TickCounter came from. Partially for my own education and partially due to some issues I was having with FastTimer, I decided to write my own clock functions using an available clock.
The AT91SAM7 has 3 timer/counters (TC) available (see section 33 of the AT91SAM7 datasheet). The TCs use a 16bit counter that increments at a user defined frequency. At the time of writing this, the current firmware (1.6.0) only uses 2 of these timer/counters: TC0 is used by the Timer module and TC2 is used by the FastTimer module. For slower applications, such as the Timer module, the counter can be updated at a frequency of 1 tick every 21.368 microseconds, whereas high performance applications can increment the counter more frequently -- as fast as 41.73 nano seconds!
For ultrasonic rangefinding, incrementing the counter every 668ns seemed like a sufficient frequency. It doesn't take much time to reach the counter's 16bit limit, 0xFFFF, especially if the counter is incremented every 668ns. Luckily, TCs allow for interrupts to be setup such that a callback can be called whenever the counter reaches a given count. The solution to extend the TickCounter's lifetime is simple: maintain an addition integer that keeps track of the number of times the counter hits a really high number. The following code sets up the TC1 to count ticks every 668ns and the interrupt notify a callback when a defined limit is reached.
// to use a peripheral, its clock must be enabled
AT91C_BASE_PMC->PMC_PCER = 1 << AT91C_ID_TC1;
// Thanks fasttimer.c!
// MCK is 47923200
// DIV1: A tick MCK/2 times a second
// This makes every tick every 41.73344ns
// DIV2: A tick MCK/8 times a second
// This makes every tick every 167ns
// DIV3: A tick MCK/32 times a second
// This makes every tick every 668ns
// DIV4: A tick MCK/128 times a second
// This makes every tick every 2.671us
// DIV5: A tick MCK/1024 times a second
// This makes every tick every 21.368us
AT91C_BASE_TC1->TC_CMR = AT91C_TC_CLKS_TIMER_DIV3_CLOCK | AT91C_TC_CPCTRG;
As described above, the clock is going to tick every 668ns. Thanks to some good documentation found in fasttimer.c, you can see that DIV3 gives that resolution. The second flag set, AT91C_TC_CPCTRG configures the clock to fire a trigger whenever the current tick count reaches a limit defined in TC_RC. A trigger is the mechanism that resets the counter back to 0.
AT91C_BASE_TC1->TC_RC = 0xF00D; // some high number
Additionally, an interrupt needs to be set up that will notify a callback when this RC limit is reached:
// Only interested in interrupts when the RC happens
AT91C_BASE_TC1->TC_IER = AT91C_TC_CPCS;
// Disable all except for RC compare
AT91C_BASE_TC1->TC_IDR = 0xEF;
/* Disable the interrupt on the interrupt controller */
AT91C_BASE_AIC->AIC_IDCR = (1<<AT91C_ID_TC1) ;
/* Save the interrupt handler routine pointer and the interrupt priority */
AT91C_BASE_AIC->AIC_SVR[ AT91C_ID_TC1 ] = (unsigned int)TickCounterIsr_Wrapper;
/* Store the Source Mode Register. This line defines what triggers the interrupt.*/
AT91C_BASE_AIC->AIC_SMR[ AT91C_ID_TC1 ] = AT91C_AIC_SRCTYPE_INT_HIGH_LEVEL | 0x4 ;
/* Clear the interrupt on the interrupt controller */
AT91C_BASE_AIC->AIC_ICCR = 1<<AT91C_ID_TC1 ;
/* Enable the interrupt on the interrupt controller */
AT91C_BASE_AIC->AIC_IECR = 1<<AT91C_ID_TC1;
Finally, enable the clock and initialize the counter. The clock can be manually reset by setting the software trigger flag.
// enable the timer and reset the counter
AT91C_BASE_TC1->TC_CCR = AT91C_TC_CLKEN | AT91C_TC_SWTRG;
With that, the TickCounter is setup and ready to notify TickCounterIsr_Wrapper whenever the limit is reached. The interrupt service routine is rather simple; when called, increment an interval counter to record the number of times RC is reached.
void TickCounterIsr_Handler( void )
{
int status = AT91C_BASE_TC1->TC_SR;
if ( status & AT91C_TC_CPCS )
{
++_numIntervals;
}
/* Clear the interrupt. */
AT91C_BASE_AIC->AIC_EOICR = 0;
return;
}
As with all interrupt handlers, be sure to acknowledge the interrupt by setting AIC_EOICR to any value. Overlooking this step will prevent future interrupts from occurring. The following helper functions were added to the TickCounter module.
void TickCounter_Reset( void )
{
_numIntervals = 0;
// enable the timer and reset the counter
AT91C_BASE_TC1->TC_CCR = AT91C_TC_CLKEN | AT91C_TC_SWTRG;
}
int TickCounter_GetNumTicks( void )
{
return AT91C_BASE_TC1->TC_CV;
}
int TickCounter_GetNumIntervals( void )
{
return _numIntervals;
}
int TickCounter_GetTimeUs( int numTicks, int numIntervals )
{
float ticksPerUs = 0.668;
// convert DIV3 to uS: each tick ~ 2/3 uS
return (numIntervals*((int)TICKCOUNTER_MAXCOUNT)+numTicks)*ticksPerUs;
}
The attached source code, rangerfinder.zip, is a complete firmware implementation containing the code referenced throughout this tutorial. This is a barebones application, where the range finder is instructed to gather distance measurements every 200ms and report the measurements via osc Debug() over USB. To run this software, extract the zip file into the firmware’s project directory and run the make command from the projects/rangefinder_test directory.
With this firmware running, place the ultrasonic rangefinder sensor a certain distance away from some solid obstruction; for my tests I had the sensor face a wall perpendicularly. With a ruler down next to the sensor, I was getting the output expected. Reposition (sliding the sensor closer or further from the wall) also resulted in the correct distance sent to the debug output.
One note about my output: Distance is returned in cms and rounded to the nearest cm. For some reason, my controller would lock up whenever I'd Debug() a string with a float argument != 0.0. It seemed ok when the output was 0.0, but anything else would lock the board up. Truncating the result down to an int did the trick.
The lines of interest is the following:
/debug/message start: 50154672, stop: 50155420, delta: 748
/debug/message distTraveled(cm): 12
The first shows the data that was recorded in the interrupt. start is the time in uS that Ping))) sends a high signal indicating the start of the echo. stop is the time in uS that Ping))) sends a low signal indicating the time of the echo's return. delta is the difference between the two numbers.
Some math is done to convert the delta into cms:
// deltaUs is the time in microseconds for a pulse to travel out and bounce back
int deltaUs = interruptInfo->stop - interruptInfo->start;
// we just care about the distance out to the obstruction, not the round trip distance so cut this time in half
deltaUs = deltaUs*0.5f;
// speed of sound travels at about 1cm in 29.034 uS
// note that this is cast down to an int because Debug seems unstable when outputting floats
int distance = ((float)deltaUs)/29.034f;
Further information
The following resources have been invaluable throughout this endeavor:
James P. Lynch has a fantastic tutorial "Using Open Source Tools for AT91SAM7S Cross Development, Revision C". This tutorial is based on YAGARTO, and can be found at the Atmel web site: http://www.atmel.com/dyn/resources/prod_documents/atmel_tutorial_source.zip
The AT91CSAM7 datasheet takes a few reads to grasp the concepts, but this should be the first place you look when developing firmware: http://www.makingthings.com/resources/datasheets/AT91SAM7X_doc6120.pdf
There is also some useful information regarding interrupts on FreeRTOS's page for our port:
http://www.freertos.org/portsam7xlwIP.html#ConfigAndUsage

