HC-SR04 ultrasonic ranging module with interrupts

Go To Last Post
13 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hello !
I've been trying to win with my HC-SR04 ultrasonic ranging module and make it work but after many approaches I couldn't do it. I am using Arduino UNO but I don't want to use their libraries and Arudino IDE. Of course I tried their way - by doing while loops in pulseIn function but this doesn't seem as a good solution for me. So here is my idea:

Here is my code.

#define F_CPU 16000000UL

#include 
#include 
#include 
#include 

#define MYDDR DDRB
#define MYPORT PORTB
#define MYPIN PINB

#define TRIG 4	// PB4			// digital12
#define ECHO 5	// PCINT5 - PB5 // digital13
#define LED 0	// PB0			// digital8

#define BAUDRATE 9600
#define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1)
#define MAX_RESP_TIME_MS 38
#define INSTR_PER_MS 16000
#define MAX_TICKS MAX_RESP_TIME_MS*INSTR_PER_MS

volatile int przepelnienia=0;
volatile int result=0;
volatile bool running=false;
char dist_char[6];

#pragma region USART
void usartInit(void)
{
	UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
	UBRR0L = (uint8_t)(BAUD_PRESCALLER);
	UCSR0B = (1<<RXEN0)|(1<<TXEN0);
	UCSR0C = ((1<<UCSZ00)|(1<<UCSZ01));
}

void usartSend( unsigned char data)
{
	while(!(UCSR0A & (1<<UDRE0)));
	UDR0 = data;
}

void usartPutstring(char* StringPtr)
{
	while(*StringPtr != 0x00){
		usartSend(*StringPtr);
	StringPtr++;}
}
#pragma endregion USART

void initHC()
{
	MYDDR |= (1 << TRIG);	// trigger output
	MYDDR &= ~(1 << ECHO);	// echo input
}

void initTimer1()
{
	// normal mode
	TCCR1A &= ~((1 << WGM10) | (1 << WGM11));
	TCCR1B &= ~((1 << WGM12) | (1 << WGM13));
	// no prescaling
	TCCR1B |= (1 << CS10);
	// enable overflow interrupt
	TIMSK1 |= (1 << TOIE1);
}

void initLED()
{
	MYDDR |= (1 << LED);		// led output	
}

void initInterrupts()
{
	PCICR |= (1 << PCIE0);		// interrupts can be triggered on pins PCINT[7:0]
	PCMSK0 |= (1 << PCINT5);	// PCINT5 will trigger an interrupt
	
	sei();	// set interrupts
}

void trigger()
{
	MYPORT &= ~(1 << TRIG);	// na wszelki wypadek
	_delay_us(2);
	MYPORT |= (1 << TRIG);	// 10 mikrosekund sygnału
	_delay_us(10);
	MYPORT &= ~(1 << TRIG);
}

ISR(PCINT0_vect)
{
	if((PINB & (1 << ECHO)))	// if echo==1
	{
		running=true;
		TCNT1=0;
		przepelnienia=0;
	}
	else						// if echo==0
	{
		running=false;
		uint32_t ticks=przepelnienia*65536+TCNT1;
		result=(ticks/2)/29.1;
		sprintf(dist_char, "% d", result);
		usartPutstring("Odleglosc: ");
		usartPutstring(dist_char);
		usartPutstring("mm\n\r");
	}
}

ISR(TIMER1_OVF_vect)
{
	if(running)
		przepelnienia++;
}

int main()
{
	initHC();
	initInterrupts();
	usartInit();
	initLED();
	initTimer1();
	
	_delay_ms(3000);
	
	while(1)
	{
		if(!running)
		{
			_delay_ms(100);
			trigger();
		}
	}
	
	return 0;
}

In ISR(PCINT0_vect) you can see that if there is any change on ECHO pin, I check whether it is from 0 to 1 or from 1 to 0. In the first situation I say the time counting has started (running=true), I initialize counter register with 0 and I initialize overflow counter variable (przepelnienia) with 0. If the change on ECHO pin is from 1 to 0, the counting is finished and I save the results and count the duration.

I think it should work fine but I am surely wrong because it gives me close but not exact distances. And the difference between the real and the measured distance is not always the same, it grows as the real distance grows.

Please, help me...

Greetings,
Daniel

Last Edited: Sat. Jan 24, 2015 - 12:18 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0
      result=(ticks/2)/29.1; 

29.1 is not an integer! So you will get a scaling error. Use floating point for your calculations. You are also using ints where you want unsigned ints.

The input capture is a better choice for measuring time more accurately.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I can't help with the C, but a few more thoughts.

As Kartman said, look in the data sheet for Input Capture. That is the "best" way to measure the echo pulse width.

Using an ISR on Pin change is a reasonable approach, however, especially if you don't have access to the Pin change interrupt pins, (For example on the AVR Butterfly).

Here there will be a delay in turning On the Timer/Counter after the interrupt occurs. Time to get into the interrupt, push a bunch of registers, and either reset to 0 or start the T/C running.

It is reasonable to stop the T/C, reset it 0, and then just start it in the interrupt. This will be a little faster.

Note that much of this "error" is offset by the delay in turning off the Timer/Counter, or reading it on the fly, at the end of the pulse. The "errors" will come close to cancelling each other out.

Note, too, that at 16 MHz, or 20 MHz, etc., the "error" from the delay is actually rather insignificant. But worth recognizing as part of analyzing the technique. If you look at the actual code, or use a debugger, or use the simulator, (none of which I do), you can calculate the actual clock cycles and hence error at both ends using the ISR technique.

Next, look at the sensor's data sheet to see its +/- distance error specification.

No matter how good or perfect your code, is, you can NOT get better accuracy than the sensor is capable of providing. (That is actually 1/2 true, you could to some voltage, temperature, atmospheric pressure, and humidity measurements and track them, and calibrate the sensor, but that's more an academic exercise than worth the effort.)

Also, What is the clock you are using for the uC? If it is the Internal RC oscillator then it can be off by quite a bit. The accuracy is listed in the data sheet. Since the timing determines the distance, if the timing is off, the distance will be off.

If the oscillator is off by 10%, then as the echo range gets greater, so will the absolute distance error, but the percentage error ought to be roughly constant.

Next, what is the environment in which you are testing the sensor? There may be some secondary echos that impact the reading.

Regardless of the environment, and the reflector being used as a target, it would be wise to take a bunch of readings and average them. Averaging 16 readings would be reasonable.

This will cut down some of the jitter in the readings.

The SR04 is truly a surprisingly great sensor for its cost!

JC

Last Edited: Sun. Aug 31, 2014 - 10:33 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Thank you for your reply ! I changed my code, no need to show it, I think. The result is quite the same. Still got noticeable (like 2cm and growing) difference between the real distance and the measured one. Tomorrow I'll try to use the input capture but it will be my first approach to this thing.

Can you tell me, looking at my code up there, whether my idea of counting the signal duration is good or bad? I believe the input capture may be more accurate but I just can't find any problem looking at what I wrote... That is so annoying!

EDIT

DocJC, I saw you response just after I posted mine. Thank you! Answering your doubts and questions:
The ranging accuracy can reach to 3mm. I use Arduino UNO board where you can find a 16MHz clock. My environment is my room :P.

If I knew the error is caused by the ranging module itself, I wouldn't create a new topic here. I am sure it's my fault but I spent a few days on searching forums and couldn't find the mistake.
I tried another thing - I loaded some code from Arduino IDE (it can be found as an example use of this hc-sr04) and, surprisingly, it worked really well! The error was little, for me insignificant. When I looked at the actual code of the fuction which does the entire job (pulseIn) it contained three while loops - first one was waiting for the random signal on ECHO pin to go away. The second loop was waiting for the signal on ECHO pin to start and the last one was actually counting the signal duration by counting the iterations. I have no idea how it worked - I tried that too and my loops didn't even see the short signal on ECHO pin.

Thank you for your response, it's very interesting, I will check the things you suggested to check. As I already wrote - I will try the Input Capture tomorrow and tell you whether it worked or not.

Goodnight from me for now !

Last Edited: Sun. Aug 31, 2014 - 10:56 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Quote:
Can you tell me, looking at my code up there, whether my idea of counting the signal duration is good or bad?

Quote:
Using an ISR on Pin change is a reasonable approach, however, especially if you don't have access to the Pin change interrupt pins,

JC

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Show us your changed code. From what you've described, the measurement sounds ok but your math is out. I'd suggest you look at casting your vars when doing your tick calculation - the compiler might only be doing 16 bit math when you want 32. Just because your destination var is 32 bit doesnt make it so.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Here is what I changed - the pin interrupt handler. Also I was advised to change variables from int to unsigned (at the top of my code) - I did so, too.

ISR(PCINT0_vect) // ten był odkomentowany
{
	if((MYPIN & (1 << ECHO)))	// jeśli echo==1
	{
		running=true;
		TCNT1=0;
		przepelnienia=0;
	}
	else						// jeśli echo==0
	{
		uint32_t ticks=przepelnienia*65536+TCNT1;
		running=false;
		float floating=(ticks/2)/29.1;
		unsigned int notFloating=floating;
		float ulamek=floating-notFloating;
		unsigned int ulamekInt=(int) (ulamek*10000);
		
		sprintf (dist_char, "% d.% 04d", notFloating, ulamekInt);
//		sprintf(dist_char, "% d", result);
		usartPutstring("Odleglosc: ");
		usartPutstring(dist_char);
		usartPutstring("mm\n\r");
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0
      uint32_t ticks=przepelnienia*65536+TCNT1; 
  Lets try some casts to be explicit:
      uint32_t ticks=(uint32_t)przepelnienia*65536UL+(uint32_t)TCNT1; 
      

To isolate measurement issues from calculation issues, print out the raw values and see how they look. Then introduce your calculations.

Also, you can use the simulator and fake up your measurement values to see how your calculations actually work. Single stepping through the calculations should allow you to ser if something goes wrong.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hello again ! Finally I found time to implement the input capture unit for my ranging module. Sadly it gives me same results.

@Kartman
I followed your advise and I try to be more explicit. Also, I threw out unnecessary code from the interrupt handler so it only contains things that need to be done for further calculations.

I really have no idea why this method is worse than doing a simple three while loops (as Arduino does...). I'm completely out of ideas.

#define F_CPU 16000000UL

#include 
#include 
#include 
#include 

#define MYDDR DDRB
#define MYPIN PINB
#define MYPORT PORTB

#define TRIG 4	// PB4			// digital12
#define ECHO 0	// PB0 - ICP1	// digital8

#define BAUDRATE 9600
#define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1)
#define MAX_RESP_TIME_MS 38
#define INSTR_PER_MS 16000
#define MAX_TICKS MAX_RESP_TIME_MS*INSTR_PER_MS

volatile uint32_t overflows=0, ovf_capt=0;
volatile uint16_t capture1=0, capture2=0;
volatile uint32_t result=0;
volatile uint8_t flag=0;
char dist_char[100];

#pragma region USART
void usartInit(void)
{
	UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
	UBRR0L = (uint8_t)(BAUD_PRESCALLER);
	UCSR0B = (1<<RXEN0)|(1<<TXEN0);
	UCSR0C = ((1<<UCSZ00)|(1<<UCSZ01));
}

void usartSend( unsigned char data)
{
	while(!(UCSR0A & (1<<UDRE0)));
	UDR0 = data;
}

void usartPutstring(char* StringPtr)
{
	while(*StringPtr != 0x00){
		usartSend(*StringPtr);
	StringPtr++;}
}
#pragma endregion USART

void initHC()
{
	MYDDR |= (1 << TRIG);	// trigger output
	MYDDR &= ~(1 << ECHO);	// echo input
}

void initTimer1()
{
	// normal mode
	TCCR1A &= ~((1 << WGM10) | (1 << WGM11));
	TCCR1B &= ~((1 << WGM12) | (1 << WGM13));
	// no prescaling (clk/1)
	TCCR1B |= (1 << CS10);
	// enable overflow interrupt and input capture interrupt
	TIMSK1 |= (1 << TOIE1) | (1 << ICIE1);
	// first capture on rising edge
	TCCR1B |= (1 << ICES1);
}

void initInterrupts()
{
	sei();	// set interrupts
}

void trigger()
{
	MYPORT &= ~(1 << TRIG);	// just in case
	_delay_us(1);
	MYPORT |= (1 << TRIG);	// 10us of signal
	_delay_us(10);
	MYPORT &= ~(1 << TRIG);
}

ISR(TIMER1_OVF_vect)
{
	overflows++;
}

ISR(TIMER1_CAPT_vect)
{
	if(flag==0)
	{
		capture1=(uint16_t)ICR1;
		overflows=0;
		TCCR1B &= ~(1 << ICES1);	// next capture on falling edge
	}
	else
	{
		capture2=(uint16_t)ICR1;
		ovf_capt=overflows;
		TCCR1B |= (1 << ICES1);		// next capture on rising edge
	}
	
	flag++;
}

int main()
{
	initHC();
	initInterrupts();
	usartInit();
	initTimer1();
	
	_delay_ms(3000);
	
	trigger();
	
	while(1)
	{
		if(flag==2)
		{
			result=(uint32_t) (65536UL * ovf_capt) + (uint32_t) (capture2 - capture1);
			
			float floating=(result/2)/29.1;
			unsigned int nonFloating=floating;
			float fraction=floating-nonFloating;
			unsigned int fractionInt=(int) (fraction*10000);
			
			sprintf(dist_char, "% d.% 04d", nonFloating, fractionInt);
			usartPutstring("Odleglosc: ");
			usartPutstring(dist_char);
			usartPutstring("mm\n\r");
			
			flag=0;
			_delay_ms(100);
			trigger();
		}
	}
	
	return 0;
}

Using simulator sounds good but dealing with stimuli files seems more complicated, I'll try that when there is nothing more you can suggest. I haven't actually ever used any avr simulator or debugger :S. I made a little line graph comparing the real distance and the measured one - it looks quite like a linear function so I guess there must be some constant offset in my calculations or somewhere else but I cannot find it.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I've not used stim files with the simulator - i would just put known values into your variables and step through you code that does the calcs. You can watch the values and see if it goes wrong.
Is your error a constant offset or scaling?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Ooh I get your suggestion with simulator now - but I'm not sure if it is necessary because the problem is not with maths - I've already made the program show (using usart) the overflow counter and other things and I calculated the duration and the distance by myself on a paper. I got same results as when the program was doing the maths. So I would keep getting back to the way I measure the signal duration. I think this must be wrong. Concerning the difference between the real distance and measured - I'm attaching a little table I made for myself before. It's clearer than words I guess. Unit is [cm].

Attachment(s): 

Last Edited: Fri. Sep 26, 2014 - 11:25 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

this is my code can any one help i just want to display distance on lcd with ultrasonic module hcsr04 without using interrupt.

#include <avr/io.h>
#include <util/delay.h>
#include "lcdprog.h"

#define sensorddr DDRC
#define sensorpin PINC
#define TrigerPin PINC1
#define EchoPin PINC2

void triggerinitiate(void);

 char this;
 char next=0;
 uint16_t buffer;
 float distance;

int main(void)
{
    sensorddr |= 1<<TrigerPin;
    sensorddr &= ~(1<<EchoPin);
    initializelcd();
    while(1)
    {
     this = sensorpin&(1<<TrigerPin);    
     triggerinitiate();
     if(next && !this)
     {
          TCCR1B |=(1<<CS10);
          TCNT1 |= 0x00;
     }
     next=this;
    
 
     if(sensorpin&(1<<EchoPin))
     {
         buffer=TCNT1;
         distance=buffer/58.82;
         TCNT1=0x00;
         TCCR1B=0x00;
         Send_Float_Value_Locatoin(2,1,distance,4,2,4);
     }
    }
}

void triggerinitiate()
{
 _delay_us(12);    
 PORTC |= 1<<TrigerPin;
 _delay_us(12);
 PORTC &= ~(1<<TrigerPin);
}

K.K.C
 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

KKC please do not keep on posting the same thing everywhere, I have deleted 3 other posts in other threads. *and I have deleted his entry in the projects area and a new Wiki. Ross*

 

Start a new thread, this one locked.

John Samperi

Ampertronics Pty. Ltd.

www.ampertronics.com.au

* Electronic Design * Custom Products * Contract Assembly

Last Edited: Sat. Jan 24, 2015 - 12:17 AM
Topic locked