NewPing Library: HC-SR04, SRF05, SRF06, DYP-ME007, Parallax PING))) - v1.7

Mostly I'm ok with rough figures from sonars.
It is usually just

  1. Too close, anchors on
  2. I can get through there
  3. Full speed ahead

@teckel
Thanks for the explanation!, good point as I did not investigate the temperature in it.

yet the humidity, pressure, altitude, etc. makes no difference in the speed (only the temperature affects the speed).

Humidity does make a difference, too. Furthermore the effect of temperature and humidity depends on the frequency of the sound.

see this publication for the math details - http://www.rane.com/pdf/ranenotes/Enviromental_Effects_on_the_Speed_of_Sound.pdf - (updated link)

To get a optimal pulseIn you should play with Timer1 as it can count in HW in steps of 1/16 us. That's as precise as Arduino gets. Use an IRQ to get the edges of the pulse and do a diff on the countervalues. See some of my experiments in this thread - http://arduino.cc/forum/index.php/topic,96971.45.html -

robtillaart:
@teckel
Thanks for the explanation!, good point as I did not investigate the temperature in it.

yet the humidity, pressure, altitude, etc. makes no difference in the speed (only the temperature affects the speed).

Humidity does make a difference, too. Furthermore the effect of temperature and humidity depends on the frequency of the sound.

You're correct with humidity, I misspoke. I meant to say at some point that humidity makes a slight difference. You could add humidity into the calculations, but most of that could be discounted in the margin of error of the sensor itself. And, the distance the sensor can read makes humidity mostly trivial under most situations.

I guess my point was really that using close whole integers is probably good enough for most purposes. If not, use the ping function which outputs the time in microseconds and do your own calculations using a temperature sensor and optionally even a humidity sensor if your readings need to be that precise. But, if your readings really need to be that precise, you probably should use a different ranging sensor.

With all this said, I do plan on looking further into this overhead delay and possibly adding a constant in the calculations if it's found that this delay is a constant. I need to build a testing jig with distance measurements to accurately test multiple sensors in a very controlled manor. But, I'm not even going to do that until the pulseIn is rewritten as that could have an effect on these readings. I could also add a microsecond to cm and inch conversion where you could also specify the temperature from a temperature sensor. I do plan on sticking with using integers for basic conversions to save space and speed. If more precise values can be verified, I'll add those to the code as an option for those who are already using floating point calculations in their sketch or don't have a code space issue.

Tim

wrt floating point, keep the code in the integer/long range. Optionally do the math in millimeters (multiply all with 10) and bring it back to cm in the end (divide by 10) That gives higher precision without the overhead of the float. Did that in one of the conversion variations (prev post).

wrt Humidity, we discussed a year(?) ago measuring the depth of a water pit (well?). There the humidity was close to 100% so it affected the ping sensor readings to the max, so in the end the working allways depends on the context :wink:

I could also add a microsecond to cm and inch conversion where you could also specify the temperature from a temperature sensor. I do plan on sticking with using integers for basic conversions to save space and speed.

imho set the temperature seperately to choose the divider to use from a small lookup table. Temp does not change that often, so conversion time will be minimized, no need to do the lookup agaiin and again.

(snippet, you get the idea)

byte table[] = { ...,57,58,59,60, ...}
div = 58; // default
rnd = div/2;  //rnd is short for rounding

setTemperature(int F)  
{
  F = F/20;
  div = table[F];
  rnd = div/2;
}

unsigned int convert_cm(unsigned int echoTime)
{
  return (echoTime + rnd)/ div;
}

robtillaart:
wrt floating point, keep the code in the integer/long range. Optionally do the math in millimeters (multiply all with 10) and bring it back to cm in the end (divide by 10) That gives higher precision without the overhead of the float. Did that in one of the conversion variations (prev post).

wrt Humidity, we discussed a year(?) ago measuring the depth of a water pit (well?). There the humidity was close to 100% so it affected the ping sensor readings to the max, so in the end the working allways depends on the context :wink:

This is what the ping function is for, so you can do your own calculations based on microseconds as you see fit. The ping_cm and ping_in are designed to be fast, lite and "good enough" for most situations. After all, these kinds of sensors are typically used to provide "too close", and "clear ahead" readings, not really exact measurements. If I could make an accurate calculation, I would. It's simply not possible to be accurate to the mm, so a calculation like that would be pointless. It's silly to have a ping_mm function if it's off by 15mm, no? My goal is to be accurate to the cm, which with these sensors and how pulseIn works will be a real challenge. mm resolution is a pipe dream, I suggest a laser sensor if that's what you need.

Basically, if you need some unique calculation that's accurate down to the mm, use the ping function and real-world calibration from your own unique sensor. But, be warned. Even if your formula is accurate, and you account for temperature and humidity, your result will still be off by as much as a cm or even more as there's other problems with how the readings are timed and the margin of error in the sensor itself.

And once again, this is why I made the statement that I did in my code, that I'm using integers and if you want to use something else, change the numbers. Or better yet, use the ping function and use your own conversion function for your needs.

Tim

Tim, This is an approach that could perhaps be taken more often with libraries in general: Expose relevant internal (previously private) variables for the user who wishes to do additional calculations.

Maybe the library could even be structured so that if a user does that, the code for further/final calculations in the "usual" application is not compiled?

I'm still semi-illiterate about library details and conventions so this is just conjecture.

@Terry
Every published library is open, so users can allways "roll their own" variation of the library. I think Tim does a very good job keeping the lib as simple as possible because that means small footprint and performance most of the time. My additions and ideas is just searching the "edge of the possible" :slight_smile:

@Tim
You should use the link to this thread in your library so people can reread the arguments somewhere in the future. e.g. add a header like

//
//    FILE: newPing.cpp
//  AUTHOR: Tim Eckel
//    DATE: 2012-05-xx
// VERSION: 1.1
//
// PUPROSE: new implementation of ping)))
// LINK: http://...
// HISTORY:
// 1.0: ...
// 1.1: ...

Hi, Rob..
You are always (cut-paste) on the "edge of the possible" :slight_smile:

Many Arduino users have no idea of what the source of a library looks like, or why it has that structure, or how to force it to recompile. They are happy (and capable) using a documented library. But maybe if they could access the library functions at both a lower and higher level it would be useful. Maybe.

I'm not sure what side of the divide I'm on, or even where it is...

I really glad you guys are digging into this.

terryking228:
Tim, This is an approach that could perhaps be taken more often with libraries in general: Expose relevant internal (previously private) variables for the user who wishes to do additional calculations.

Maybe the library could even be structured so that if a user does that, the code for further/final calculations in the "usual" application is not compiled?

I'm still semi-illiterate about library details and conventions so this is just conjecture.

Terry, that's exactly how I see it. I think the primary goal of the NewPing library is to measure the ping/echo time and distance as easily, quickly and accurately as possible. But, when we get into advanced distance conversion, the requirement to consider temperature and to a lesser extent humidity take it outside the scope of an ultrasonic/ping library as other sensors are required. For those with this need, the NewPing library provides the user with the low-level ping/echo timing. And those with this need and knowledge of what other sensors are needed should already know the equation they need to use.

Not that I'm going to wash my hands of this and off-load it. There's just more primary issues, like making pulseIn event-driven and a generally more accurate echo time sensor first.

Tim

robtillaart:
@Terry
Every published library is open, so users can allways "roll their own" variation of the library. I think Tim does a very good job keeping the lib as simple as possible because that means small footprint and performance most of the time. My additions and ideas is just searching the "edge of the possible" :slight_smile:

@Tim
You should use the link to this thread in your library so people can reread the arguments somewhere in the future. e.g. add a header like

//

//    FILE: newPing.cpp
//  AUTHOR: Tim Eckel
//    DATE: 2012-05-xx
// VERSION: 1.1
//
// PUPROSE: new implementation of ping)))
// LINK: http://...
// HISTORY:
// 1.0: ...
// 1.1: ...

Thanks for the suggestions!

Tim

Thanks to Terry King and YourDuino I did a round of range testing with 6 ultrasonic/ping sensors and the results were not as I expected. I tested with 3 different models, (4) SF04, (1) SRF05, (1) SRF06. The testing environment was in an air conditioned room at 75 degrees. I tested at distances of 5cm, 10cm, 25cm, 50cm, and 100cm. Longer distances were not tested in this round. I believe the accuracy within 100cm or important as that's within the primary measurement distance. Beyond 100cm it's not as much of an issue in most situations. Once the pulseIn echo code is reworked, I'll do round 2 of testing out to a further distance.

The good news is that the sensors were all very consistent. With all the SF04 and SRF05 generating nearly identical results (makes sense as the PCBs are nearly identical). The SRF06 uses different components and yields slightly different measurements as a result. It does appear to be a slightly better sensor (makes less clicking noise, hardly no lag, and results closer to actual speed of sound). The maximum distance of the SRF06 seemed to be slightly lower, but that will be answered in round 2 of testing.

The results don't fit at all with the speed of sound calculations previously being used. For the SF04 and SRF05 sensors, the calculation from microseconds to cm is:
cm = (microseconds + 37) / 48

For the SRF06 sensor, the calculation is:
cm = (microseconds + 5) / 51

Using these calculations, I could accurately measure any distance in the 5cm to 100cm range with all 6 sensors. Albeit, not even close to speed of sound at this temperature, which should be right around cm = microseconds / 58. If it was simply a long sensor lag, the additional microseconds added in the equation would be much higher, which they're not.

I have two theories as to why this is happening:

  1. The pulseIn function is simply way off. It's not even measuring time, but counting processor cycles, so there's a good possibility the time conversion is wrong.
  2. Because I'm using port registers the code is faster. And, since the formulas used to compute pulseIn are not based on port register manipulation but using the known slow higher-level digitalWrite command that's where the error is. This doesn't seem likely as while digitalWrite is slow, it's not THAT slow.

At this point, I think diving into the pulseIn code and replacing it is a wise choice. Then at least I'll have some other information to figure out the source of the time discrepancy.

Tim

Rewrote the pulseIn to use micros() instead of loop counting. With that change, I got accurate data that matches exactly with the speed of sound. So, the problem was with pulseIn. However, I'm not sure if the problem is how it calculates time (counting processor cycles) or it has to do with the time it takes for the sensor to send out a ping. In any case, it doesn't matter as I've completely re-written the code to use simple timing which works great. There's not yet an option for the ping to be event driven. But, it is much faster and more accurate now.

I should be releasing v1.2 later tonight.

Tim

Rewrote the pulseIn to use micros() instead of loop counting. With that change, I got accurate data that matches exactly with the speed of sound. So, the problem was with pulseIn.

Beautiful, Tim. You have challenged lots of assumptions with this and it's really good news that you are understanding what's happening. We'll all be listening for your next round!

Rewrote the pulseIn to use micros() instead of loop counting.

Be aware that micros() always round its value to a multiple of 4.

Counting the loops in pulsein works quite well, however if there are interrupt handled during this it will definitely give a wrong (too low) reading. This could (partially) explain why you need a smaller divisor in your formulas.
This "trick" is used especially to be able to read pulselengths smaller than 4 micros() precission.

Hope to see the code soon!

robtillaart:

Rewrote the pulseIn to use micros() instead of loop counting.

Be aware that micros() always round its value to a multiple of 4.

Counting the loops in pulsein works quite well, however if there are interrupt handled during this it will definitely give a wrong (too low) reading. This could (partially) explain why you need a smaller divisor in your formulas.
This "trick" is used especially to be able to read pulselengths smaller than 4 micros() precission.

Hope to see the code soon!

Let's put it this way. The pulseIn function is from 460 to 7120 microseconds off because of the way ultrasonic sensors work. Basically, the echo port state takes that long to settle and pulseIn counts that time. So, I think I'll settle for only 4 microseconds off. :wink:

I may release the code in the source both ways so if you want to try it the other way, you can uncomment it.

Tim

New in version 1.2:

Lots of code clean-up thanks to Adruino Forum members. Rebuilt the ping timing code from scratch, ditched the pulseIn code as it doesn't give correct results (at least with ping sensors). The NewPing library is now VERY accurate and the code was simplified as a bonus. Smaller and faster code as well. Fixed some issues with very close ping results when converting to inches. All functions now return 0 only when there's no ping echo (out of range) and a positive value for a successful ping. This can effectively be used to detect if something is out of range or in-range and at what distance. Now compatible with Arduino 0023.

Download NewPing v1.2

By default, it uses micros() to do the ping echo timing. But, I also included alternate code that uses loop counting (similar to what pulseIn uses). It's streamlined, but follows the same basic concept as pulseIn.

Let me know what you think, and if you have any issues or suggestions.

Tim

Nice job Tim
It took some time to find points for improvement :wink:

All functions now return 0 only when there's no ping echo

That calls for a

#define  NOECHO  0

makes the code more readable

(style remark)
adding a few blank lines in your code might improve readability

Comparing against 0 is faster and makes code a few bytes smaller

// Alternate, uses loop counting to time ping echo.
unsigned int NewPing::ping() 
{
	unsigned long maxLoops = microsecondsToClockCycles(_maxEchoTime + MAX_SENSOR_DELAY) / CLOCK_CYCLES;
	unsigned long maxLoops2 = microsecondsToClockCycles(_maxEchoTime) / CLOCK_CYCLES;

	// Use port registers to trigger a ping.
	*_triggerOutput &= ~_triggerBit;
	delayMicroseconds(2);
	*_triggerOutput |= _triggerBit;
	delayMicroseconds(10);
	*_triggerOutput &= ~_triggerBit;

	// Set a timeout and wait for the echo pin to clear 
	while (*_echoInput & _echoBit)
		if (maxLoops-- == 0) return NOECHO;

        // Wait for the ping to start.
	while (!(*_echoInput & _echoBit))
		if (maxLoops-- == 0) return NOECHO;

	// Ping started, wait for the ping echo to return.
	while (*_echoInput & _echoBit)
		if (maxLoops2-- == 0) return NOECHO;

	// Calculate ping time, 16 = clock cycles of routine overhead.
	return clockCyclesToMicroseconds(numloops * CLOCK_CYCLES + 16); 
}

Still don't like this one...

#define NewPingConvert(echoTime, conversionFactor) (max(echoTime / conversionFactor, (echoTime ? 1 : 0)))

can be improved by (1) adding rounding and (2) working in millimeters as proposed before.

The granularity of measurement = 4 micros(), in 4 micros sound travels approx 1.35 millimeter, or about 1/7 of a centimeter. that's almost 3 bits of precision, assuming the last 2 are noise your lib should be able to measure 0.5cm == 5mm steps (1/10th inch) without problems.

robtillaart:

	// Ping started, wait for the ping echo to return.
while (*_echoInput & _echoBit)
	if (maxLoops2-- == 0) return NOECHO;

// Calculate ping time, 16 = clock cycles of routine overhead.
return clockCyclesToMicroseconds(numloops * CLOCK_CYCLES + 16);


Comparing against 0 is faster and makes code a few bytes smaller

This is with the loop counting ping method, which I don't suggest using, and is mostly pulseIn with slight changes to work correctly with ultrasonic sensors.

Also, your code doesn't quite work, as it must count the loops to figure out the distance in the final loop. Also, numloops is not defined. I tried to implement the corrected version of your code suggestion, but it was actually 2 bytes longer, so one would expect a little slower as well.

I was, however, able to optimize the loop counting ping method to save around 44 bytes. See below:

// Alternate timer, uses loop counting to time the ping echo.
unsigned int NewPing::ping() {
	unsigned long numloops = 0;
	unsigned long cpu_speed = microsecondsToClockCycles(10);
	unsigned long maxLoops = (_maxEchoTime + MAX_SENSOR_DELAY) * cpu_speed / CLOCK_CYCLES / 10;
	unsigned long maxLoops2 = _maxEchoTime * cpu_speed / CLOCK_CYCLES / 10;
	*_triggerOutput &= ~_triggerBit;
	delayMicroseconds(2);
	*_triggerOutput |= _triggerBit;
	delayMicroseconds(10);
	*_triggerOutput &= ~_triggerBit;
	while (*_echoInput & _echoBit)
		if (numloops++ == maxLoops) return NO_ECHO;
	while (!(*_echoInput & _echoBit))
		if (numloops++ == maxLoops) return NO_ECHO;
	numloops = 1;
	while (*_echoInput & _echoBit)
		if (numloops++ == maxLoops2) return NO_ECHO;
	return clockCyclesToMicroseconds(numloops * CLOCK_CYCLES);
}

robtillaart:
Still don't like this one...

#define NewPingConvert(echoTime, conversionFactor) (max(echoTime / conversionFactor, (echoTime ? 1 : 0)))

can be improved by (1) adding rounding and (2) working in millimeters as proposed before.

The granularity of measurement = 4 micros(), in 4 micros sound travels approx 1.35 millimeter, or about 1/7 of a centimeter. that's almost 3 bits of precision, assuming the last 2 are noise your lib should be able to measure 0.5cm == 5mm steps (1/10th inch) without problems.

If you'd like to suggest an alternative, please provide specific code as I can't read your mind and won't try to guess either.

Also, keep in mind that mm conversion is worthless as the sensor is not accurate enough to measure down to the mm (only accurate to 6-7mm). Further, doing the math down to the mm level would not be more accurate because temperature is not being considered and the effect of temperature can be a few cm. In other words, a more complex formula will not yield more accurate results, simply take more program space with no benefit.

Not sure why you're not happy with just changing US_ROUNDTRIP_CM to a fraction or get the microsecond result from ping() and use your own magic conversion code. Am I missing something? Using the sensors in the real world I can't get 1/10th inch results, how exactly are you doing this? Or, is it just a theory you have?

Tim

Thanks for pointing out the error in my code - 3 posts back - numloops get nowhere incremented :blush:

please provide specific code as I can't read your mind

#define NewPingConvert(echoTime, conversionFactor) (max(echoTime / conversionFactor, (echoTime ? 1 : 0)))

add rounding

#define NewPingConvert(echoTime, conversionFactor) (max( (echoTime+conversionFactor/2) / conversionFactor, (echoTime ? 1 : 0)))

Using the sensors in the real world I can't get 1/10th inch results, how exactly are you doing this? Or, is it just a theory you have?

Yes, it is theory to search for what can be reached with the sensors/ code.

You made a good point that the fluctuations due to temperature are large so working in mm makes less/no sense.

robtillaart:
add rounding

#define NewPingConvert(echoTime, conversionFactor) (max( (echoTime+conversionFactor/2) / conversionFactor, (echoTime ? 1 : 0)))

Now I believe I understand what you're getting at. You want to round, say, 10.6cm up to 11cm instead of doing the normal truncate of 10.6cm down to 10cm. I guess what I was thinking was that it doesn't make much sense to round when dealing with cm because the margin of error is already almost a cm. But with inches, I can see where rounding could be logical. I'll do some testing, but probably incorporate it.

I've also been optimizing the loop counting timer and I've got it down to only 8 to 18 bytes longer than the micros() timer. But, the loop counter will never work for event-driven, so I believe eventually I'll remove it from the source.

Tim