Tuesday, July 8, 2014

Arduino (atmega328) Interrupt-Based Maxbotix EZ Sonar Sensor

So your girlfriend (or boyfriend) gave you an EZ0 Maxbotix Sonar Module for your birthday. It's laying around collecting dust mites and you just don't know what to do with it. Well, you've come to the right place! But seriously, here is a picture of the damn thing.


In most blogs I've come across, they only explain how to interface to the module through the Analog pin, and that's fine. However, analog reads in the Arduino take a lot of CPU cycles, and it could potentially slow down the rest of your code. There is another pin in the module, however, that allows us to get useful readings from the sensor in a faster way: the PW (Pulse width) pin.


Theory

The PW pin essentially outputs a Pulse-width modulated signal whose pulse width is proportional to the distance to the object/s being detected. By using Pin Change Interrupts, we can measure the deltas in the pulse widths, and then convert these to distances without wasting time sampling analog voltages. The following diagram is a high level representation of the interaction between the sonar sensor and our program's logic.



Circuit Diagram

The following image shows a simple circuit setup with an Arduino UNO. 


I am connecting the PW pin from the ultrasonic module to digital pin 4 (PD4) on the Arduino, but you can use any other pin as long as you setup the appropriate PCINT register values. I highly recommend going over the atmega328 datasheet for more details. It may seem confusing at first, but keep at it, and it will start making sense over time. If it doesn't, drop a comment below and I will do my best to help clarify ^_^ 

These are the registers that we need to modify in order to configure Pin Change Interrupts on PD4. You would put these two lines of code somewhere in your initialization routine, before you start attempting to take any measurements from the sensor module. 

PCICR |= (1 << PCIE2) /* Allow PCINT interrupts on port D */
PCMSK2 |= (1 << PCINT20) /* Allow PCINT on pin PD4 */

I put these definitions somewhere in the code in order to organize things a bit. 

#define MICROSECONDS_PER_CM 373.38f
#define CM_PER_MICROSECONDS (1/MICROSECONDS_PER_CM)

#define SONAR_PIN 4 /* Digital pin 4 (PD4) */
#define SONAR_PIN_MASK (1 << SONAR_PIN)

typedef struct _sonar_state
{
  volatile uint32_t t_start;            /* Start of pulse */
  volatile uint32_t dt;                 /* Puse duration in microseconds */
  volatile boolean pulseStarted;        /* Flag used to ensure correct initial conditions */
  volatile boolean sonarPin_lastState;  /* Keep track of the last logical state of PD4 (1 or 0) */
} SONAR_STATE;

SONAR_STATE sonar_state;

I'm sure you've noticed the volatile modifiers above. If you don't know what it does, don't worry. Essentially, you would declare a variable as volatile, when its value may change unexpectedly, like in an interrupt, for instance. When an interrupt handler is called, the state of all variables used within the interrupt handler are saved to the stack, and when the program returns from the interrupt routine, these variables are then restored to their previous values, ignoring the modifications that may have occurred during the interrupt. You can see how this could be a problem. 

The following code is the essence of the interrupt mechanism. Upon any signal change on PD4, the ISR routine (below) will automagically execute, allowing us to record the signal transition times. Thus, being able to calculate the distance to nearby detected objects. In theory anyways ^_^

ISR( PCINT2_vect )
{
  /* If PD4 is high, then save pulse start time (micros() returns the time in microseconds since program started.     Overflow condition is ignored for the sake of simplicity). Otherwise, save pulse end */
  if( PIND & SONAR_PIN_MASK > 0 )
  {
    sonar_state.t_start = micros();
    sonar_state.pulseStarted = 1;
  }
  else if( sonar_state.pulseStarted )
  {
    sonar_state.dt = micros() - sonar_state.t_start;

    /* Calculate distance in centimeter */
    sonar_state.distance = CM_PER_MICROSECONDS * sonar_state.dt;

    /* Clear flag */
    sonar_state.pulseStarted = 0;
  }
}

Note: if we have several pins within port D that allow Pin Change Interrupts, the above ISR will execute on ANY pin change that has been configured in the PCMSK2 register (not only PD4). This would cause erroneous time deltas to be recorded. A way to fix this is to keep track of the state of PD4 and add a check within the interrupt routine to verify that PD4 indeed changed logical states (log-to-high or high-to-low) before recording signal-change times. Just keep in mind that the code above assumes there's no other port D pin with Pin Change Interrupts enabled, other than digital pin 4 (PD4) for the sake of simplicity.

No comments:

Post a Comment