Skip to content

Instantly share code, notes, and snippets.

Last active March 24, 2021 16:20
Show Gist options
  • Save ajfisher/1d57c5f845c376f04fbb to your computer and use it in GitHub Desktop.
Save ajfisher/1d57c5f845c376f04fbb to your computer and use it in GitHub Desktop.
Building an I2C backpack for HC-SR04 ultrasonic sensor

Building an ultrasonic sensor backpack

Note: This is a bit of a scratch to see what's involved in getting the ping sensor to work as an I2C backpack.



I did this on a Trinket as that's what I had to hand to make programming a bit faster. If you use a trinket you will need the custom arduino. Check out the Adafruit Trinket info

You also need a custom form of the TinyWireS library that manages sending multiple registers on one read request (the default implementation doesn't do this and locks up the buffer). This can be had here: Do the usual thing and install into libraries folder of your sketch directory.

If you want to debug then you'll need a special form of Software serial - called [](SoftwareSerialSendOnly - note Zip) which does what you'd expect and only uses a single pin (handy on an ATTINY).


ping.ino can work on both an ATTiny85 and standard Arduino. It sets up as an I2C peripheral device. However, ATTINY can't use the wire library so has to use tiny wire. Read the code for the places to comment / uncomment to get this to work.

includes.h provides the #if hacks for arduino

ping.js A hack of the existing ping.js library in J5 using that as the base. Removed things that were no longer necessary and migrated relevant calls to I2C rather than pin manipulation.

pingtest.js is simply a demo to get it spitting out the data events. Note that this is using a hardcoded port just to eliminate any confusion across devices (I was using an arduino at one point so was getting port conflicts - easier to be explicit).


  • Arduino requires only standard firmata
  • Wire up the Trinket / ATTINY85 so that the ping trigger / echo is on pin 4.
  • Wire up VCC and GND, then SDA and SCL to the arduino master.

Power everything up then run your sketch.

npm install johnny-five
node ping-test.js

You should get an output of distance in CM.

Current status

  • Lag issues have been resolved and were due to pulsein blocking
  • Speed is viable at 100msec pin intervals on an ATTINY85 no problems.
  • Tool chain is very clutzy due to issues with inotool not understanding attiny
  • Ranging is accurate (to the level of accuracy of the actual sensor at least).

Known issues

Some kind of issue with I2C whereby if the JS application is stopped then restarted but the Backpack isn't reset then Firmata times out and won't connect.


  • Track down this lag thing
  • Fix issue with timing delay causing inflated timings.
  • Fix issue with the firmata timeout.
  • Get ATTINY using avr-gcc etc so no arduino deps at all
  • clean up the schmozzle that is the inotool build chain with attiny
  • Figure out how this comes back into Johnny-five as backpack with rwaldron
#include <avr/interrupt.h>
#if defined( __AVR_ATtiny85__ )
#include <TinyWireS.h>
#include <avr/power.h>
#include <SendOnlySoftwareSerial.h>
SendOnlySoftwareSerial Serial(3);
#define PING_PIN 4
#include <Wire.h>
#define PING_PIN 12
#ifndef _DEBUG
#define _DEBUG false
#ifndef _VDEBUG
#define _VDEBUG false
#define I2C_SENSOR_ADDRESS 0x27
#include "includes.h"
#define PING_FREQUENCY 1000 // milliseconds between pings
#define PING_FREQUENCY 100 // go much slower so we can see wtf is going on
byte register_map[REGISTER_MAP_SIZE];
volatile int32_t duration; // duration of the ping
int32_t last_ping_time = 0; // last time the ping occurred in ms
int32_t ping_freq = PING_FREQUENCY;
int32_t ping_emit_time = 0; // time the ping was emitted in us
bool pinging = false; // used to determine when we're pinging
// Interrupt vector for external interrupt on pin PCINT7..0
// This will be called when any of the pins D0 - D4 on the trinket change
// or pins D8 - D13 on an Arduino Uno change.
// the ping pin will flip HIGH at the point when the pulse has completed
// and timing should begin, it will then flip LOW once the sound wave is received
// so we need to detect both of these states
ISR(PCINT0_vect) {
if (digitalRead(PING_PIN) == HIGH) {
pinging = true;
ping_emit_time = micros();
} else {
duration = micros() - ping_emit_time;
void setup() {
#if defined( __AVR_ATtiny85__ )
// Set prescaler so CPU runs at 16MHz
if (F_CPU == 16000000) clock_prescale_set(clock_div_1);
// Use TinyWire for ATTINY
#if _DEBUG
Serial.println("Ping Sensor I2C");
void loop() {
#if defined( __AVR_ATtiny85__ )
// USE this for ATTINY as you can't use delay
void disablePCInterrupt() {
// disable all interrupts temporarily
// disable pin change interrupt
// clear pin change interrupt flag register
#if defined( __AVR_ATtiny85__ )
// re-enable all interrupts
void enablePCInterrupt() {
// disable all interrupts temporarily
// enable pin change interrupt on PB4 (D4 on Trinket, D12 on Uno)
// enable pin change interrupt 0
#if defined( __AVR_ATtiny85__ )
// re-enable all interrupts
void get_distance() {
if ((millis() - last_ping_time) > ping_freq) {
// disable interrupt while pinMode is OUTPUT
// not sure if this is actually necessary, but just
// playing it safe to avoid false interrupt when
// pin mode is OUTPUT
//Serial.print("D: ");
Serial.println((long)duration / 29 / 2);
pinging = false;
digitalWrite(PING_PIN, LOW);
digitalWrite(PING_PIN, HIGH);
digitalWrite(PING_PIN, LOW);
// we'll use a pin change interrupt to notify when the
// ping pin changes rather than being blocked by
// Arduino's built-in pulseIn function
last_ping_time = millis();
void requestData() {
register_map[0] = duration >> 8; // msb
register_map[1] = duration & 0xFF; //LSB
#if defined( __AVR_ATtiny85__ )
// ATTINY you need to do a send of each register individually.
// ATMEGA cat write out a buffer
Wire.write(register_map, REGISTER_MAP_SIZE);
#if _DEBUG
Serial.print("rm: ");
Serial.print(" ");
var events = require("events"),
util = require("util");
// within = require("./mixins/within"),
// __ = require("./fn");
var priv = new Map();
* Ping
* @param {Object} opts Options: addr freq
function Ping(opts) {
if (!(this instanceof Ping)) {
return new Ping(opts);
var last = null;
this.addr = opts && opts.addr || 0x27;
this.freq = opts.freq || 100;
this.board = opts.board || null;
var state = {
value: 0
// Private settings object
var settings = {
addr: this.addr,
readBytes: 2, // the duration that comes off the ping is a 16 bit int
pingFreq: 210, // whatever the required frequency is, go a bit less
// Interval for polling pulse duration as reported in microseconds
setInterval(function() {, settings.readBytes, function(data) {
state.value = (data[0] << 8) + data[1];
}.bind(this), settings.pingFreq);
// Interval for throttled event
setInterval(function() {
var err = null;
if (!state.value) {
// The "read" event has been deprecated in
// favor of a "data" event.
this.emit("data", err, state.value);
// If the state.value for this interval is not the same as the
// state.value in the last interval, fire a "change" event.
if (state.value !== last) {
this.emit("change", err, state.value);
// Store state.value for comparison in next interval
last = state.value;
// Reset samples;
// samples.length = 0;
}.bind(this), this.freq);
Object.defineProperties(this, {
value: {
get: function() {
return state.value;
// Based on the round trip travel time in microseconds,
// Calculate the distance in inches and centimeters
inches: {
get: function() {
return +(state.value / 74 / 2).toFixed(2);
in: {
get: function() {
return this.inches;
cm: {
get: function() {
return +(state.value / 29 / 2).toFixed(3);
priv.set(this, state);
util.inherits(Ping, events.EventEmitter);
module.exports = Ping;
var five = require("johnny-five");
var ping = require('./ping.js');
var board = five.Board({port: '/dev/ttyACM0'});
board.on('ready', function() {
var p = new ping({
addr: 0x27,
freq: 300,
board: this,
p.on("data", function(){
board.on('error', function(err) {
var five = require("johnny-five");
var board = five.Board({port: '/dev/ttyACM0'})
board.on("ready", function() {
setInterval(function() {
this.i2cReadOnce(0x27, 2, function(data) {
console.log("tranformed: " + ((data[0] << 8) + data[1]));
}.bind(this), 1000);
Copy link

If you want to support both the Adafruit Trinket (ATTiny85) and standard Arduino boards (Uno, etc) in the same file, you case use the following pre-processor directives whenever you need to do something different
for ATtiny vs standard Arduino boards:

#if defined( __AVR_ATtiny85__ )
  #include <TinyWireS.h>
  #include <avr/power.h>
  #include <Wire.h>

Copy link

Mark pinging and ping_emit_time as volatile since you're setting them within the ISR. Also, the ISR is triggered when any pin changes (including the I2C pins). I suggest saving the state of the PING_PIN and only handling high and low transition of that specific pin to filter all other pin changes out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment