Talking to Ultrasonic Distance Sensor HC-SR04 using an ATtiny84



The HC-SR04 works as follows:

  • Send a 10us HIGH pulse on the Trigger pin.
  • The sensor sends out a “sonic burst” of 8 cycles.
  • Listen to the Echo pin, and the duration of the next HIGH signal will give you the time taken by the sound to go back and forth from sensor to target.

Here, the PB0 pin is used to send out the 10 us pulse. To measure the width of the echo pulse, we can use a pin-change interrupt and a timer. Here is the idea:

  • Setup pin change interrupt PCINT0 so that any logical change on pin will cause an interrupt.
  • Send a 10 us pulse to the trigger pin.
  • Loop till the PCINT0 interrupt sets a flag to indicate that measurement is done.
  • In the PCINT0 interrupt, start an 8-bit timer when you see a rising edge – ie., the echo pulse has gone from low to high. The 8-bit timer is setup to use the overflow interrupt.
  • The 8-bit counter overflows every time it reaches 255, and so when that interrupt fires, we add 255 to a running 32-bit counter value.
  • In the PCINT0 interrupt, stop 8-bit timer when you see a falling edge – ie., the echo pulse has gone from high to low. Update 32 bit count, and set flag to indicate that the measurement is done.
  • The measured pulse width is in terms of a counter value, and we can convert that into seconds, since we know the clock speed. This time value is then used to calculate the distance.

The distance is then sent using serial communications on pin PB1 – I’ve covered this part in a previous post. This is also the reason we cannot use the 16-bit timer to measure the pulse width – it’s already being used for serial communications. Plus it’s fun to learn how to use the 8-bit timer to count large values, right? 😉


Here is the schematic:



And the breadboard looks like this:


 


The full C code is listed below:


 

//
// Talking to ultrasonic sensor HC-SR04 with an ATtiny84, and 
// sending distance data using serial communications.
//
// electronut.in
//

#include <avr/io.h>
#include <string.h>
#include <util/delay.h>
#include <avr/interrupt.h>
 
#define F_CPU 8000000

//
// BEGIN: serials comms
//

char pinTX = PB1;
volatile unsigned char currByte = 0;
volatile int sendingData = 0;

// initialize 
void init_serial()
{
  // set TX pin as output
  DDRB = (1 << pinTX);

  // set up timer1:
  cli();
  // 16 bit timer 
  // Divide by 1
  TCCR1B |= (1<<CS10);
  // Count cycles - (1/9600)*8000000 
  OCR1A = 833;   
  // Put Timer/Counter1 in CTC mode
  TCCR1B |= 1<<WGM12;
  // set interrupt flag
  TIMSK1 |= 1<<OCIE1A;
  sei();
}

// send a byte
void send_byte(char data)
{
  // set flag
  sendingData = 1;
  // set current data byte
  currByte = data;
  // wait till done
  while(sendingData);
}

// which bit was sent last? (0-10) 
// -1 implies none sent
volatile char bitNum = -1;

// 16-bit timer CTC handler - Interrupt Service Routine
ISR(TIM1_COMPA_vect)      
{
  // serial data sent as:
  //
  // 9600 8 N 1 
  //
  // start(low)-0-1-2-3-4-5-6-7-stop(high)-idle(high)-idle(high)
  // 12 bits sent per packet

  // idle => high
  if(!sendingData) {
    PORTB |= (1 << pinTX);
  }
  else {

    if(bitNum < 0) {
      // start bit - low
      PORTB &= ~(1 << pinTX);
      // set bit number
      bitNum = 0;
    }
    else if(bitNum >= 8) {      
      // stop bit - high
      PORTB |= (1 << pinTX);
      // increment
      bitNum++;

      // send 2 idle - high - bits
      // not necessary strictly speaking
      if(bitNum == 10) {
        // done 
        sendingData = 0;
        // unset bit number
        bitNum = -1;
      }
    }
    else { 
      // data bits:

      // bit num is in [0, 7]

      // extract relevant bit from data
      char dataBit = currByte & (1 << bitNum);
      if(dataBit) {
        PORTB |= (1 << pinTX);
      }
      else { 
        PORTB &= ~(1 << pinTX);
      }

      // update bit number
      bitNum++;
    }
  }
}


// write null terminated string
void send_str(const char* str)
{
  int len = strlen(str);
  int i;
  for (i = 0; i < len; i++) {
    send_byte(str[i]);
  }   
}

//
// END: serials comms
//


//
// BEGIN: HC-SR04 code
//
// pins for HC-SR04
int pinEcho = PA0;

// initialize HC-SR04
void initHCSR04()
{
  // initialize HC-SR04 pins
  
  // set Trigger pin (connected to PB0) as output
  DDRB |= (1 << PB0);
}

// 1/0 flag to check if echo is over
volatile char echoDone = 0;

// current timer0 count
uint32_t countTimer0 = 0;

// get distance in cm from HC-SR04
float getDistanceHCSR04()
{
  // Send a 10us HIGH pulse on the Trigger pin.
  // The sensor sends out a “sonic burst” of 8 cycles.
  // Listen to the Echo pin, and the duration of the next HIGH 
  // signal will give you the time taken by the sound to go back 
  // and forth from sensor to target.

  float distance = 0.0f;

  // enable pin-change interrupt on PCINT0:
  cli();
  // enable interrupt
  GIMSK |= (1 << PCIE0);
  // enable pin
  PCMSK0 |= (1 << PCINT0);
  sei();

  // set echo flag
  echoDone = 0;
  // reset counter
  countTimer0 = 0;

  // send 10us trigger pulse
  //    _
  // __| |__
  PORTB &= ~(1 << PB0);
  _delay_us(20);
  PORTB |= (1 << PB0);
  _delay_us(12);
  PORTB &= ~(1 << PB0);
  _delay_us(20);

  // listen for echo and time it
  //       ____________
  // _____|            |___

  // loop till echo pin goes low
  while(!echoDone);

  // disable pin-change interrupt:
  // disable interrupt
  GIMSK &= ~(1 << PCIE0);
  // disable pin
  PCMSK0 &= ~(1 << PCINT0);

  // calculate duration
  float duration = countTimer0/8000000.0;

  // dist = duration * speed of sound * 1/2
  // dist in cm = duration in s * 340.26 * 100 * 1/2
  // = 17013*duration
  distance = 17013.0 * duration;
  
  return distance;
}

// timer0 overflow interrupt
ISR(TIM0_OVF_vect)
{
  // increment
  countTimer0 += 255;
}

// pin-change interrupt handler
ISR(PCINT0_vect)
{
  // read PCINT0 (PA0 - pn 13):
  if(PINA & (1 << pinEcho)) { 
    // rising edge:

    // start 8-bit timer
    // Divide by 1
    TCCR0B |= (1<<CS00);
    // set overflow interrupt flag
    TIMSK0 |= 1<<TOIE0;

  }
  else {
    // falling edge 

    // stop timer
    TCCR0B &= ~(1<<CS00);

    // calculate time passed
    countTimer0 += TCNT0;

    // reset counter in timer0
    TCNT0 = 0;

    // set flag
    echoDone = 1;
  }
}
//
// END: HC-SR04 code
//


//
// main loop:
//
int main (void)
{
  // serial
  init_serial();

  // HC-SR04
  initHCSR04();

  // set as output
  DDRB |= (1 << PB0);

  char str[16];

  float prevDist = 0.0;
  // loop
  while (1) {

    float dist = getDistanceHCSR04();    
    // sensor only works till 400 cm - if it exceeds, this value
    // just send previous reading
    if(dist > 500) {
      dist = prevDist;
    }
    // print distance to serial port
    sprintf(str, "%f\n", dist);
    send_str(str);
    prevDist = dist;

    // wait 
    _delay_ms(70);
  }
 
  return 1;
}

This is the Makefile that goes along with the above code. It is similar to the ones posted before – I’ve just added some extra linker flags to support full printf formatting.


https://gist.github.com/electronut/5763929


And here is the Python code used to plot the data:


https://gist.github.com/electronut/5730160


The Python code is a minor modification to what I posted before on the subject.


You can get the ATtiny84 at element14.