Recently I bought an HC-SR04 Ultrasonic Range Sensor from Amazon.com. I thought this would be interesting and fun to play with. The first challenge was writing the code to interface with it. The range sensor is triggered by a pulse of at least 10us on the "trigger" line. The distance is provided as the length of the pulse on the "echo" line, which is actually the time it takes the ultrasonic sound to travel from the sensor to the object and bounce back to the sensor. Therefore the length of the pulse on the "echo" line represents twice the distance between the sensor and the object.
Goals and Constraints
I am writing in assembly. The biggest constraint is that I do not have floating point. Also, the PIC module that will measure the pulse width is dependent on the oscillator speed. So I need to account for that in the conversion. I know that the pulse width will be a 16-bit value. I use the 16/16 divide routine from Microchip's math library to handle that.
As for goals, I want the code to detect error conditions (such as a missing or broken sensor) and indicate out-of-range. I also want to be able to specify an arbitrary "unit", besides inches and centimeters. The conversion should be done in one step: directly from pulse width to desired units.
Calculating the Distance
The distance to the object is half the length of the pulse, measured in seconds, multiplied by the speed of sound, 340.29 meters/sec:
d{m} = t{s} * 340.29{m/s} * 1/2
This is all well and good if you have floating point variables and math and can measure in microseconds, but I am writing in assembly on a PIC and will be using the Capture/Compare/PWM (CCP) module.
The CCP module uses the oscillator to measure time. The base rate is Fosc/4. This rate can be scaled by the prescaler, which can be 1:1 (no prescale), 2:1, 4:1 or 8:1. I am going to use the CCP capture to detect the rising edge of the echo pulse and then the falling edge. The capture module captures the time of the "event" by copying the current TMR1 value to the capture registers. This time is in what I am going to call "ticks". The duration, in seconds, of a "tick" is (prescale * 4 / Fosc), where Fosc is in Hz. To convert ticks to seconds, we can use
t{s} = n * prescale * 4 / Fosc{Hz}
It is a lot more convenient to work in microseconds (us). To rewrite this to give the time in microseconds, we only needs to change the Hz to MHz:
t{us} = n * prescale * 4 / Fosc{Mhz}
If we are working in microseconds, then we need to convert the speed of sound the same way. Also, it helps to change from meters to millimeters, and to round the 340.29 to 340
340.29{m/s} = 340 * 1000 {mm}/ 1000000 {us} = 34{mm}/100 {us}
Now, we can take the ticks we get from the capture module and convert this to millimeters:
d{mm} = n * prescale * 4 / (Fosc{MHz} ) * (1/2) * 34{mm}/100{us}
Finally, we can convert the millimeters to any units that we need. For inches we could multiply by 10{in}/254{mm}. For centimeters, it would be 1{cm}/10{mm}. Note that we select values that are integers. We generalize this with
N_Units{u}/M_MM{mm}
The final equation, before simplifying is
d{u} = n * prescale * 4 / (Fosc{MHz} ) * (1/2) * 34{mm}/100{us} * N_units{u} / M_MM{mm}
We can rearrange this and get
d{u} = n * (17 * prescale * N_units)/(25 * Fosc * M_MM)
The only problem remaining is that the conversion factor (everything after n) is less than zero. Quite a bit less than zero. The easy way to solve this is to divide by the reciprocal:
d{u} = n / ((25 * Fosc * M_MM)/(17 * prescale * N_units))
So now we have a way to calculate a divisor that can convert our pulse width into a distance.
Code Organization
The HCSR04_MeasureDistance routine does the following:
- Verify valid state (low line) on input pin
- Trigger the sensor
- Monitor for rising edge
- Monitor for falling edge
- Compute distance
The code uses a register labelled HCSR04_STATUS to track if there was an error, if the result is out-of-bounds, and to pass a rising/falling edge flag.
I wanted to have some error checking in the code. That is, I did not want to assume that everything would work every time. This is why I check to make sure that the input (echo) line is low before starting. If it is not low, then there is no way to even take a measurement. If there is a problem, then I set the "error" bit in my HCSR04_STATUS register. This error is "returned" to the caller. That is, the HCSR04_STATUS_Error bit of HCSR04_STATUS register can be tested to determine if an error occurred.
To trigger the sensor, I bring up the trigger line, delay for at least 10us, and then bring the line down. I have only tested this with a 20MHz clock (ceramic resonator). I expect it will work at slower clock speeds.
I use the capture module to monitor for the rising and falling edges. This is done with HCSR04_CaptureEchoTransition. This routine sets up the CCP to in capture mode and tells it to look for the either the next rising or falling transition. The caller sets the HCSR04_STATUS_Rising bit of the HCSR04_STATUS register to indicate which edge to look for. This routine resets the timer to zero before starting the capture. The HCSR_MonitorEchoEnd copies the CCPR1L/H registers to the HCSR04_WIDTH_L/H registers if the capture was successful. Note that the HCSR04_CaptureEchoTransition also monitors the TMR1. If the timer rolls-over, then this is considered an error condition and sets the HCSR04_STATUS_Error bit.
To compute the distance, I divide the pulse width by the "divisor" described above. I used the Microchip math library described in application note AN526. The final value is placed in HCSR04_DIST_L/H registers.
Comments