electronut Programming & Embedded Systems - by Mahesh Venkitachalam

Talking to Ultrasonic Distance Sensor HC-SR04 using nRF51822



In this project, we will talk to the popular ultrasonic distance sensor HC-SR04 using the Nordic nRF51-DK board, and transmit the distance information over BLE using the NUS (Nordic UART Service). We will be utilizing GPIO and Timer1 for this purpose.


Before you read further, please take a look at my previous article on nRF51-DK programming using GCC, since we’re going to use the same development setup here.

Communicating with HC-SR04

I have written about this sensor before, interfacing it with Arduino as well as ATtiny84. Here’s how the HC-SR04 works:

  • 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.

So we need (a) a way to send the trigger pulse and (b) a method to measure the echo pulse.


Here is how the nRF51-DK is hooked up to the HC-SR04:

HC-SR04 nRF51-DK
Trig P0.01
Echo P0.02 via R DIV

The HC-SR04 runs on 5V, but our nRF51422 runs on 3.3V logic. It’s OK to send data to the HC-SR04 on 3.3 V TTL, but it’s not OK to receive a 5V pulse from the sensor. Hence, we route the Echo through a resistor divider as shown below.


Now let’s look at how to send and receive data.

Trigger Pulse

You can send the trigger pulse using the GPIO feature of the nRF51. The code looks like this:

// send 12us trigger pulse
//    _
// __| |__

What you are doing above is toggling a GPIO pin HIGH and LOW, creating a pulse, and sure enough, you get a response from the Echo pin of the HC-SR04.

Echo Pulse

The distance is encoded in the width of the HIGH pulse returned by the HC-SR04. To measure this width, we will make use of the nRF51 Timer1 peripheral on the nRF51822. The Timer1 is a 8/16 bit timer which runs at 16 MHz.

In their SDK, Nordic provides higher level APIs (which start with nrf_drv_timer) to set up these timers. But I found their documentation and API to be confusing, so I decided to use the registers directly. I think this is a better approach in any case, as it corresponds directly to what you read in the nRF51 reference manual. Here is the code that sets up the timer and the interrupt routine:

// set up and start Timer1
void start_timer(void)
  // set prescalar n
  // f = 16 MHz / 2^(n)
  uint8_t prescaler = 0;
	NRF_TIMER1->PRESCALER = prescaler;

  // 16 MHz clock generates timer tick every 1/(16000000) s = 62.5 nano s
  // With compare enabled, the interrupt is fired every: 62.5 * comp1 nano s
  // = 0.0625*comp1 micro seconds
  // multiply this by 2^(prescalar)

  uint16_t comp1 = 500;
  // set compare
	NRF_TIMER1->CC[1] = comp1;

  // set conversion factor
  countToUs = 0.0625*comp1*(1 << prescaler);

  printf("timer tick = %f us\n", countToUs);

  // enable compare 1

  // use the shorts register to clear compare 1

  // enable IRQ

  // start timer

// Timer 1 IRQ handler
// just increment count
void TIMER1_IRQHandler(void)

    // clear compare register event


In the start_timer() method above, we start by setting the prescaler of the timer to 0. The timer frequency is given by \(f_{TIMER} = \frac{16 MHz}{2^{PRESCALER}}\). The 16 MHz clock generates timer tick every 1/(16000000) s = 62.5 nano seconds. We then set the compare1 register to a value of 500. This means that the timer generates an interrup every \(62.5*500\) nano seconds or 31.25 microseconds. We set up the flags for compare1, enable the interrupt, and set the SHORTS register to clear the task when the interrupt fires. (This is like a shortcut so that you don’t need to manually do this in the interrupt routine.)

In the interrupt routine, we increment a global counter tCount and clear the compare1 event. If you don’t do the latter, you will not get an interrupt the next time around. It’s best to have minimal amount of code TIMER1_IRQHandler() as otherwise, it will introduce latency into the timer, and that will mess up the accurancy of your timing.

Computing the Distance

Now that we are counting every 31.25 microseconds, let’s see how we put that use to compute the distance. Here is the relevant portion from the distance computation method:

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

  // wait till Echo pin goes high
  // reset counter
  tCount = 0;
  // wait till Echo pin goes low

  // calculate duration in us
  float duration = countToUs*tCount;

  // dist = duration * speed of sound * 1/2
  // dist in cm = duration in us * 10^-6 * 340.29 * 100 * 1/2
  float distance = duration*0.017;

As soon as we send the trigger pulse, we loop around pinEcho till it goes high. Then we reset tCount so that the timer can start counting it, and loop around till pinEcho goes low. At this point tCount will be proportional to the pulse width, and we convert it to microseconds using the factor computed earlier. The distance can then be computed using the good old physics equation \(x = vt\). The factor \(1/2\) above comes in because the sound is travelling back and forth to the sensor

  • so you just need to consider half the time measured by the sensor.

The BLE Part

This project uses the S110 softdevice. Once we have the distance information, we send it over BLE via the Nordic UART Service (NUS). You can view it on your BLE device (phone/tablet) using the Nordic nRFtoolbox app.


You can get the complete source code for this project here:



I’d like to thank Aryan from Nordic Developer Zone for clearing my doubts on Timer1 usage.


  1. nRF51 Series Reference Manual Version 3.0.

If you liked this article, please consider supporting my efforts by purchasing my book.

Python Playground, published by No Starch Press, USA, is a collection of imaginative programming projects that will inspire you to use Python to make art and music, build simulations of real-world phenomena, and interact with hardware like the Arduino and Raspberry Pi. Do check it out!