PIC18: Ultrasonic sensor

hc-sr04 In this article we are going to experiment with an ultrasonic sensor, the HC-SR04; it seems quite known and common, especially in the Arduino community, maybe because it is quite cheap and simple to operate.

Wanna discover how we can use it to measure the distance of an object, by interfacing it to the Freedom II development board? Yes? Coool! So... let's get started!

HC-SR04 operation

The sensor comes with four pins:

  • +5V Supply
  • Trigger Pulse Input
  • Echo Pulse Output
  • GND


The datasheet explains how to activate the Trigger pin and what it produces:

You only need to supply a short 10uS pulse to the trigger input to start the ranging, and then the module will send out an 8 cycle burst of ultrasounds at 40 kHz.

This train of ultrasound bursts travel forwards, until it finds an obstacle, and then backwards to the receiving end of the sensor; the echo pin will stay high for the duration of this travel back and forth.

Distance calculation

Ultrasonic of course means we're dealing with sounds, albeit inaudible to the human ear. The speed of sound is at around 340m/s, which is equivalent to 34,000cm/S or 34cm/mS. So if the duration of the high pulse on the echo pin is, say, 2 milliseconds, that means that ultrasonic sound waves had to travel 34 centimeters to the obstacle for 1 mS and then 34 centimeters back to the HC-SR04. The length of the entire travel would be 68 centimeters, but the distance of the object will be half that measure:

In 1ms the distance of the obstacle would be 17cm and in 1uS it would be 0.017cm; so which is the magic number that lets us convert the duration in uS, measured on the Echo pin, to centimeters? Easily said:

x = 1uS / 0.017cm = 58

Let's say we capture 1.76mS (1760uS) on the Echo pin (here below the capture shown on the scope):

Distance(cm) = 1760uS / 58 = 30cm

A high 1760uS pulse on Echo pin means that the obstacle is 30cm far away from the sensor.


Pulses on the Echo pin can be captured by the PIC by configuring its CCP modules in ... well, Capture mode, the other modes being Compare and PWM.

The CCPx pins on the 18F4550 are RC2(CCP1) and RC1(CCP2), which will be connected to the Echo pin of the sensor:

Let's read from the datasheet what happens when a capture event occurs:

15.2 Capture Mode

In Capture mode, the CCPRxH:CCPRxL register pair captures the 16-bit value of the TMR1 or TMR3 registers when an event occurs on the corresponding CCPx pin. An event is defined as one of the following:

  • every falling edge
  • every rising edge...

So this is the plan:

  • CCP1 will be configured to capture a rising edge
  • whenever that occurs TMR3 will be cleared and start to count
  • CCP2 instead will listen for falling edge inputs
  • in which case TMR3 will stop and its value will be saved into CCPR2H:CCPR2L registers
  • that value will represent the duration of the high signal on pin Echo, which is proportional to the distance to the objrct

The program

The core of the program (main routine) is super simple; we are going to take measurements every second, by sending an input to the Trigger and waiting for the Echo. To achieve that an interrupt is generated by TMR0 overflow every 1 second, which sends the requested 10uS +5V to the Trigger pin of the sensor; the HC-SR04 will send the ultrasound bursts and the Echo signal back will be captured, via interrupt, by CCP1/CCP2.

 btfsc REG_FLAG,TMR0        ; Is REG_FLAG,TMR0 (interrupt from TMR0 overflow) set?      
 call SendTrigger        ; YES: call SendTrigger
 btfsc REG_FLAG,CCPInt  ; Is REG_FLAG,CCPInt (interrupt from CCP) set?
 call ReadEcho          ; YES: call ReadEcho
 bra Main

Readecho subroutine

The SendTrigger routine just sets RE0 (Trigger pin) high for 15uS; ReadEcho instead is activated whenever REG_FLAG,CCPInt is set in isr.asm as a consequence of a signal capture:

 btfsc PIR1,CCP1IF  ; A CCP1 rising edge capture?
 bra CAPTURE1       ; YES
 btfss PIR2,CCP2IF  ; a CCP2 falling edge capture?
     goto EndIsr        ; false alarm
 movff CCPR2L,CCPR2Low  ; Get low byte of captured time
 movff CCPR2H,CCPR2High ; Get high byte of captured time
 bcf PIR2,CCP2IF        ; Clear flag
 bsf REG_FLAG,CCPInt    ; Set flag CCP in the custom register REG_FLAG, which is checked in main.asm
 goto EndIsr
 clrf TMR3H         ; Zero count
 clrf TMR3L
 bcf PIR1,CCP1IF        ; Clear flag
 goto EndIsr

Here when a rising edge is captured on CCP1 TMR3 is cleared and starts to count up, to measure the duration of the pulse length; when a falling edge is captured on CCP2 it means that the pulse has ended and we can copy the values of CCPR2x, which contain the values of TMR3x, that is the duration of the pulse.

Going back to main.asm CCPR2High and CCPR2Low are just rough values that need to be converted in order to get the distance of the obstacle.


With a 5MHz Fosc/4, there are 5,000,000 instructions per sec or 1 instruction every 0.2uS (1/5,000,000); TMR3 is prescaled by 8 in order to have less increments, which occur every 1.6uS (0.2uS * 8).

; CCP1_ON init:
    movlw b'00000101'   
    movwf CCP1CON       ; CCP1 module captures +ve edge
    bsf PIE1,CCP1IE     ; Enable interrupts from CCP1
    movlw b'11110001'   ; Timer3 enabled, 16bit write, internal osc, prescale 1:8
    movwf T3CON
    bsf INTCON,PEIE     ; Enable PEIE (bit 6) 

We said before that in order to get the distance we should divide the value in uS by the 58; here we don't have uS, but rather 1,6uS increments, so the new magic number is obtained in this way:

x = 58 / 1.6 = 36.25

36.25 is the divisor that should be applied to the value of TMR3H:TMR3L registers in order to get the distance in centimeters.

How can we divide a number by 36.25? With assembly is not as simple as with a calculator. To start we could divide by 32, which can be easily accomplished with Shift Left Register commands; left-shifting a number is the same as dividing by 2, so if we repeat this last operation 5 times we effectively divide by 32.

movlw .5            ; divide values by .32
movwf shiftr_count
bcf STATUS,C        ; we want the Carry to be 0 right now
rrcf CCPR2High,F    ; shift right CCPR2High with Carry
rrcf CCPR2Low,F     ; shift right CCPR2Low with Carry
decf shiftr_count,F ; is this the fith time we rotate right?
bnz shiftr_again    ; no: shift right again
            ; yes: raw data CCPR2High and CCPR2Low should be divided to .32 now

After the last manipulations CCPRHigh is actually 0, at least for distances under 2 meters, which we are going to use in this experiment; so we can skip it and focus only on CCPR2Low.

So far we got a value which is a little overrated and so we use the routine DivisionByX in math.asm to get a more consistent value:

movf CCPR2Low,W     ; 
call DivisionByX    ; We divided by .32, but to get a better result raw data should be divided by .36
movf quotient,W     ; So, we remove 13% off CCPR2Low (.36/.32=1.13)
subwf CCPR2Low,F    ;

By taking 13% off of it, CCPR2Low should now contain the decimal value, expressed in centimeters, of the distance of the obstacle!

Yes, we did it!

Add new comment

Filtered HTML

  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <img> <p> <h1> <h2> <h3> <h4> <h5> <div> <pre> <object>
  • You may insert videos with [video:URL]
  • Web page addresses and e-mail addresses turn into links automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.