Built-in project · Arduino Uno R3

Deep-sea dive computer on an Arduino Uno

A pressure transducer turned into depth (about 1 atm per 10 m of sea water), a DS18B20 water probe, and an OLED with an ascent-rate warning and a buzzer alarm. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Advanced About 25 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $48. On Mokxi they're free.

Deep-sea dive computer · Arduino Uno R3live0.000 s 0.00x
Click to open it in the editor
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

What you'll learn

  • Sensors
  • Displays
  • Sound
  • Choosing and reading a resistor
  • Difficulty: Advanced
  • Time: About 25 minutes
  • Board: Arduino Uno R3
  • Parts: 7

How it works

A pressure transducer turned into depth (about 1 atm per 10 m of sea water), a DS18B20 water probe, and an OLED with an ascent-rate warning and a buzzer alarm.

Arduino Uno R3 runs the sketch below. The circuit around it: Pressure sender, 0.5-4.5 V, Temperature sensor, DS18B20, Resistor, 4.7k Ω, OLED display, 128x64, Piezo speaker. pinMode sets BUZZER_PIN as an output. analogRead measures the voltage on PRESSURE_PIN as a number. Serial.begin opens the serial port at 115200 baud. tone plays a note on BUZZER_PIN. millis reads the time since power-up, so the sketch can time things without stopping.

Parts list

7 parts, plus the jumper wires. They retail for about $48; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Piezo speaker pin 1Arduino Uno R3 pin 8
Temperature sensor, DS18B20 pin DQArduino Uno R3 pin 2
Resistor, 4.7k Ω pin 1Arduino Uno R3 pin 2
Pressure sender, 0.5-4.5 V pin VCCArduino Uno R3 pin 5V
Temperature sensor, DS18B20 pin VDDArduino Uno R3 pin 5V
Resistor, 4.7k Ω pin 2Arduino Uno R3 pin 5V
OLED display, 128x64 pin VCCArduino Uno R3 pin 5V
Pressure sender, 0.5-4.5 V pin GNDArduino Uno R3 pin GND
Temperature sensor, DS18B20 pin GNDArduino Uno R3 pin GND
OLED display, 128x64 pin GNDArduino Uno R3 pin GND
Piezo speaker pin 2Arduino Uno R3 pin GND
Pressure sender, 0.5-4.5 V pin SIGArduino Uno R3 pin A0
OLED display, 128x64 pin SDAArduino Uno R3 pin A4
OLED display, 128x64 pin SCLArduino Uno R3 pin A5

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Deep-sea dive computer: depth, water temperature and an ascent-rate alarm.
//
//   A0       pressure transducer SIG (0.5 to 4.5 V, 0 to 0.5 MPa gauge)
//   pin 2    DS18B20 water probe DQ (4.7k pull-up to 5 V)
//   pin 8    piezo buzzer
//   A4, A5   OLED SDA and SCL (address 0x3C)
//
// # How pressure becomes depth
//
// Water is heavy. Every 10 m you go down adds about one atmosphere (1 bar,
// 100 kPa) on top of the air's own pressure at the surface. The exact rule is
// pressure = density x gravity x depth. Sea water is about 1025 kg/m3, so one
// metre of it adds 1025 x 9.81 = 10.05 kPa. The sketch turns that around:
// depth = pressure / 10.05 kPa per metre.
//
//   0 m     1 bar of air only              (gauge reading   0 kPa)
//   10 m    2 bar: the air plus one of sea (gauge reading 100 kPa)
//   40 m    5 bar                          (gauge reading 400 kPa)
//
// # Gauge or absolute?
//
// A BMP280 or BME280 is an absolute sensor: it reads 1013 hPa at the surface
// and stops at 1100 hPa, which is only about one metre of water. It is the
// wrong tool for a dive. The part used here is a stainless gauge transducer
// built for water: 0.5 V at no pressure up to 4.5 V at full scale, which is
// 0.5 MPa (500 kPa, about 50 m of sea water) on this model. Gauge means the
// air above the surface is already taken out. The sketch still reads the
// sensor at power-up and calls that zero depth, the way a real dive computer
// does on the boat before it goes in, so a sensor that is a little off at the
// surface does not make the whole dive wrong.
//
// # Ascent rate
//
// The rule of thumb for scuba is to rise no faster than about 10 m a minute,
// because the nitrogen in your blood needs time to leave. The sketch keeps the
// depth from two seconds ago and works out how many metres a minute you have
// moved since. Above the limit the bar on the screen fills past its tick, the
// screen says SLOW DOWN and the piezo beeps. At 40 m, the limit of a
// recreational dive, it sounds a steady tone instead.
//
// Drag the pressure slider to dive. Drag the temperature to cool the water.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <OneWire.h>
#include <DallasTemperature.h>

const int PRESSURE_PIN = A0;
const int PROBE_PIN = 2;
const int BUZZER_PIN = 8;

// The transducer reads 0.5 V at 0 kPa and 4.5 V at 500 kPa, and the converter
// measures against the same 5 V supply. That is 10% to 90% of its 1023 counts.
const float COUNTS_AT_ZERO = 102.3;
const float COUNTS_PER_KPA = 818.4 / 500.0;

// Sea water: 1025 kg/m3 x 9.81 m/s2 = 10.05 kPa for every metre.
const float KPA_PER_METRE = 10.05;

const float MAX_ASCENT = 10.0;  // metres a minute
const float MAX_DEPTH = 40.0;   // metres

Adafruit_SSD1306 display(128, 64, &Wire, -1);
OneWire bus(PROBE_PIN);
DallasTemperature probe(&bus);

float surfaceCounts = COUNTS_AT_ZERO;
float depth = 0;
float maxDepth = 0;
float waterC = 20;
float ascent = 0;  // metres a minute, positive when rising

// The depth a few samples ago, kept in a four-slot ring: one slot per half
// second, so the slot about to be overwritten holds the depth two seconds back.
// Each slot remembers when it was taken, because drawing the screen takes a
// moment and the loop does not tick at exactly 500 ms.
float history[4];
unsigned long stamp[4];
uint8_t slot = 0;

unsigned long lastSample = 0;
unsigned long lastDraw = 0;
unsigned long lastTemp = 0;
unsigned long lastReport = 0;
bool tempAsked = false;

// The average of 16 reads smooths out the converter's last bit of noise.
float readCounts() {
  long sum = 0;
  for (int i = 0; i < 16; i++) sum += analogRead(PRESSURE_PIN);
  return sum / 16.0;
}

float depthFromCounts(float counts) {
  float kpa = (counts - surfaceCounts) / COUNTS_PER_KPA;
  if (kpa < 0) kpa = 0;
  return kpa / KPA_PER_METRE;
}

void alarm(bool tooFast, bool tooDeep) {
  if (tooDeep) {
    tone(BUZZER_PIN, 2000);
  } else if (tooFast && (millis() / 250) % 2 == 0) {
    tone(BUZZER_PIN, 1000);
  } else {
    noTone(BUZZER_PIN);
  }
}

void draw(bool tooFast, bool tooDeep) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);

  // Depth, big.
  display.setTextSize(3);
  display.setCursor(0, 0);
  display.print(depth, 1);
  display.setTextSize(1);
  display.setCursor(104, 14);
  display.print("m");

  // The line under it: deepest point and water temperature.
  display.setCursor(0, 28);
  display.print("MAX ");
  display.print(maxDepth, 1);
  display.print("m  ");
  display.print(waterC, 1);
  display.print("C");

  // Dive time as minutes:seconds.
  unsigned long s = millis() / 1000;
  display.setCursor(0, 38);
  display.print("TIME ");
  display.print(s / 60);
  display.print(":");
  if (s % 60 < 10) display.print("0");
  display.print(s % 60);

  // The ascent gauge: a box that spans 0 to 20 m/min, with a tick at the limit.
  display.drawRect(0, 50, 100, 8, SSD1306_WHITE);
  float up = ascent < 0 ? 0 : ascent;
  int fill = (int)(up * 98 / (2 * MAX_ASCENT));
  if (fill > 98) fill = 98;
  display.fillRect(1, 51, fill, 6, SSD1306_WHITE);
  display.drawFastVLine(50, 48, 12, SSD1306_WHITE);
  display.setCursor(104, 50);
  display.print((int)up);

  // The warning, flashing.
  if ((tooFast || tooDeep) && (millis() / 300) % 2 == 0) {
    display.fillRect(70, 38, 58, 10, SSD1306_WHITE);
    display.setTextColor(SSD1306_BLACK);
    display.setCursor(72, 39);
    display.print(tooDeep ? "TOO DEEP" : "SLOW DOWN");
  }
  display.display();
}

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Uno: dive computer");
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  probe.begin();
  probe.setWaitForConversion(false);  // ask now, read later, never stall
  pinMode(BUZZER_PIN, OUTPUT);

  // Surface calibration: whatever the sensor reads now is zero metres.
  surfaceCounts = readCounts();
  for (int i = 0; i < 4; i++) {
    history[i] = 0;
    stamp[i] = millis();
  }
}

void loop() {
  unsigned long now = millis();

  if (now - lastSample >= 500) {
    lastSample = now;
    depth = depthFromCounts(readCounts());
    if (depth > maxDepth) maxDepth = depth;
    // history[slot] is the depth four samples (about two seconds) ago.
    unsigned long span = now - stamp[slot];
    ascent = span > 0 ? (history[slot] - depth) * 60000.0 / span : 0;  // m/min
    history[slot] = depth;
    stamp[slot] = now;
    slot = (slot + 1) % 4;
  }

  // The probe takes 750 ms to convert, so ask, wait, then read.
  if (!tempAsked && now - lastTemp >= 1000) {
    probe.requestTemperatures();
    tempAsked = true;
    lastTemp = now;
  } else if (tempAsked && now - lastTemp >= 800) {
    float c = probe.getTempCByIndex(0);
    if (c > -100) waterC = c;
    tempAsked = false;
    lastTemp = now;
  }

  bool tooFast = ascent > MAX_ASCENT;
  bool tooDeep = depth >= MAX_DEPTH;
  alarm(tooFast, tooDeep);

  if (now - lastDraw >= 300) {
    lastDraw = now;
    draw(tooFast, tooDeep);
  }

  if (now - lastReport >= 1000) {
    lastReport = now;
    Serial.print("depth ");
    Serial.print(depth, 1);
    Serial.print(" m, max ");
    Serial.print(maxDepth, 1);
    Serial.print(" m, water ");
    Serial.print(waterC, 1);
    Serial.print(" C, ascent ");
    Serial.print(ascent, 1);
    Serial.print(" m/min");
    if (tooDeep) Serial.print(" TOO DEEP");
    else if (tooFast) Serial.print(" SLOW DOWN");
    Serial.println();
  }
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Runs once when the board starts. It gets the pins and libraries ready.

void setup() {
  Serial.begin(115200);
  Serial.println("Mokxi Uno: dive computer");
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  probe.begin();
  probe.setWaitForConversion(false);  // ask now, read later, never stall
  pinMode(BUZZER_PIN, OUTPUT);

  // Surface calibration: whatever the sensor reads now is zero metres.
  surfaceCounts = readCounts();
  for (int i = 0; i < 4; i++) {
    history[i] = 0;
    stamp[i] = millis();
  }
}

loop sketch.ino

Runs over and over for as long as the board has power. This is where the project does its work.

void loop() {
  unsigned long now = millis();

  if (now - lastSample >= 500) {
    lastSample = now;
    depth = depthFromCounts(readCounts());
    if (depth > maxDepth) maxDepth = depth;
    // history[slot] is the depth four samples (about two seconds) ago.
    unsigned long span = now - stamp[slot];
    ascent = span > 0 ? (history[slot] - depth) * 60000.0 / span : 0;  // m/min
    history[slot] = depth;
    stamp[slot] = now;
    slot = (slot + 1) % 4;
  }

  // The probe takes 750 ms to convert, so ask, wait, then read.
  if (!tempAsked && now - lastTemp >= 1000) {
    probe.requestTemperatures();
    tempAsked = true;
    lastTemp = now;
  } else if (tempAsked && now - lastTemp >= 800) {
    float c = probe.getTempCByIndex(0);
    if (c > -100) waterC = c;
    tempAsked = false;
    lastTemp = now;
  }

  bool tooFast = ascent > MAX_ASCENT;
  // ... 24 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The reading is off at zero pressure. Pressure sender
    The output is 0.5 V at no pressure, not 0 V. Subtract that offset, then scale 0.5 V to 4.5 V over the full range of the model.
  • The ADC saturates, or the board is damaged. Pressure sender
    Power it from 5 V. On a 3.3 V board, the 4.5 V output is too high: use a divider. Check the three wires (supply, ground, signal), as colors vary by brand.
  • The sensor reads -127 C or no device is found. Temperature sensor
    Put a 4.7 k resistor between the data line and VCC. Check the pinout, since swapping VCC and GND can overheat the sensor. Then confirm the data pin matches your code.
  • It always shows 85 C. Temperature sensor
    85 C is the power on value, meaning the conversion has not finished or the supply is weak. Wait for the conversion (up to 750 ms at 12 bit) before reading.
  • The LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • The OLED stays blank. OLED display
    Check SDA and SCL are not swapped, and power it with 3.3 V or 5 V as its board allows. Most use address 0x3C, some 0x3D. Call display.begin() with the matching address.
  • Text never shows up even though the code runs. OLED display
    Drawing only writes to a buffer. Call display.display() after drawing to push it out. Also set a text color with setTextColor(WHITE) and clear the buffer between frames.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.