Introduction: A Micro:bit Dive-O-Meter

About: Scientist working in in-vitro diagnostics industry. Playing with all types of sensor as a spare time hobby. Aiming for simple and inexpensive tools and projects for STEM, with a bit of science and a bit of sil…

Summer is here, its pool time!

An good opportunity to take yourself and your micro:bit outside to, and in this case even into, the swimming pool.

The micro:bit dive-o-meter described here is a simple DIY depth gauge that allows you to measure how deep you are, or were, diving. It consists just of a micro:bit, a battery pack or LiPo, an edge connector for the micro:bit, a BMP280 or BME280 barometric pressure sensor and some jumper cables. Using the Pimoroni enviro:bit does makes things ever simpler. All this is packed into two layers of watertight clear plastic or silicone bags, with some weights added to compensate the buoyant force.

It is a application of the micro:bit pressure sensor device I had described in a previous instructable.

You may use the device e. g. for diving competitions with friends and family, or to find out how deep that pond really is. I tested it using the deepest pool in my neighbourhood, and found that it works at least to a depth of 3.2 meter. About five meter is the theoretical maximum. So far I have not tested its precision in any detail, but the reported numbers were at least in the expected range.

Some remarks:
This is not meant to be a tool for real divers.
Your micro:bit will get damaged if it gets wet.
You use this instructable on your own risk.

Update May 27: Now you can find a MakeCode HEX-script you can load directly to your micro:bit. See step 6.
Update June 13: An Enviro:bit and a cable version added. See steps 7 & 8

Step 1: The Theory Behind the Device

We are living on the bottom of an ocean of air. The pressure down here is about 1020 hPa (hectoPascal) as the weight of the air column form here to space is about 1 kg per square centimeter.

The density of water is much higher, as one liter of air weights about 1.2 g and one liter of water 1 kg, i.e. about the 800-fold. So as the drop in barometric pressure is about 1 hPa for every 8 meters in height, the pressure gain is 1 hPa for every centimeter below the water surface. At a depth of about 10 m, the pressure is 2000 hPa, or two atmospheres.

The pressure sensor used here has a measurement range between 750 and 1500 hPa at a resolution of about one hPa. This means we can measure depths up to 5 meters at a resolution of about 1 cm.

The device would be a Boyle Marriotte type depth gauge. Its assembly is quite simple and described in a later step. The sensor uses the I2C protocol, so an edge connector for the micro:bit comes handy. The most critical part are the watertight bags, as any humidity will damage the micro:bit, the sensor, or the battery. As some air will be trapped inside the bags, the addition of weights helps to compensate the buoyant force.

Step 2: Using the Device

The script, as shown in detail at a later step, is a variation of a script I developed earlier for a pressure meter. To test the device, you may use the simple pressure chamber described there.

For diving purposes it shows the depth in meters, as calculated from pressure measurements, either as a bar graph in 20 cm steps or, upon request, in numbers.

Using the button A on the micro:bit, you will set the current pressure as reference pressure value. To confirm entry, the matrix blinks once.

You may use this either see how deep you are diving, or to record how deep you were diving.

In the first case set the current outside air pressure as reference. In the second case set the pressure at the deepest point you where as pressure reference, which then allows you to show how deep you have been when you are back on the surface.

Button B displays the depth, calculated from the pressure difference, as a numeric value in meters.

Step 3: Materials Required

A micro:bit. E.g. at 13 GBP/16 Euro at Pimoroni UK/DE.

An edge connector (Kitronic or Pimoroni), 5 GBP. I used the Kitronic version.

A BMP/BME280 sensor. I used a BMP280 sensor from Banggood, 4.33 Euro for three units.

Jumper cables to connect sensor and edge connector.

An excellent alternative to the edge connector/sensor combination above could be Pimoroni enviro:bit (not tested by now, see last step).

A battery pack or LiPo for the micro:bit.

A power cable with a switch (optional but helpful).

Clear watertight bags. I used a silicone pouch for a mobile phone and one or two small ziploc bags.
Make sure that the material is thick enough, so the pins on the edge connector won`t damage the bags.

Some weights. I used pieces of lead weight that are used for fishing.


Arduino IDE, and several libraries.

Step 4: Assembly

Install the Arduino IDE and the required libraries. Details are described here.

(Not required for the MakeCode script.)

Given you use the Kitronik edge connector, solder pins to the I2C ports 19 & 20.
This is not required for the Pimoroni edge connector.

Solder the header to the sensor break out and connect sensor and edge connector using jumper cables.
Connect VCC to 3V, GND to 0 V, SCL to port 19 and SDA to port 20.
Alternatively solder the cables directly to the breakout.

Connect the micro:bit to our computer by an USB cable.

Open the provided script and flash it to the micro:bit.

Use the serial monitor or plotter, check if the sensor gives reasonable data.

Disconnect the micro:bit from your computer.

Connect the battery or LiPo to the micro:bit.

Press button B, read the value

Press button A.

Press button B, read the value.

Place the device in two layers of airtight bags, leaving only very little air in the bags.

In case, place a weight to compensate the buoyancy force.

Check if everything is watertight.

Go to the pool and play.

Step 5: The MicroPython Script

The script just takes the pressure value from the sensor, compares it to the reference value, and then calculates the depth from the difference. For displaying the values as a bar graph, the integer and remainder part of the depth value are taken. The first does define the height of the line. The remainder is broken into five bins, which do define the length of the bars. The top level is 0 - 1 m, the lowest 4 - 5 m.

As mentioned before, pressing button A sets the reference pressure, button B displays the "relative depth" in meters, displayed as a numerical value. By now, negative and positive values are presented as bargraph on the LED matrix the same way.

Feel free to optimize the script for your needs. You may unmute certain lines to present the values on the serial monitor or plotter of the Arduino IDE. To emulate the function, you may build the device I described in a previous instructable.

I have not written the part of the script that reads the sensor. I am not sure about the source, but I like to thank the autors. Any corrections or hints for optimization are welcome.

#include <Adafruit_Microbit.h>
#include <Wire.h>
Adafruit_Microbit_Matrix microbit;
#define BME280_ADDRESS 0x76
unsigned long int hum_raw,temp_raw,pres_raw;
signed long int t_fine;
uint16_t dig_T1;
 int16_t dig_T2;
 int16_t dig_T3;
uint16_t dig_P1;
 int16_t dig_P2;
 int16_t dig_P3;
 int16_t dig_P4;
 int16_t dig_P5;
 int16_t dig_P6;
 int16_t dig_P7;
 int16_t dig_P8;
 int16_t dig_P9;
 int8_t  dig_H1;
 int16_t dig_H2;
 int8_t  dig_H3;
 int16_t dig_H4;
 int16_t dig_H5;
 int8_t  dig_H6;
 double press_norm = 1015;               // a starting value
 double depth;                           // calculated depth
//--------------------------------------------------------------------------------------------------------------------
void setup()
{
    uint8_t osrs_t = 1;             //Temperature oversampling x 1
    uint8_t osrs_p = 1;             //Pressure oversampling x 1
    uint8_t osrs_h = 1;             //Humidity oversampling x 1
    uint8_t mode = 3;               //Normal mode
    uint8_t t_sb = 5;               //Tstandby 1000ms
    uint8_t filter = 0;             //Filter off 
    uint8_t spi3w_en = 0;           //3-wire SPI Disable
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en;
    uint8_t ctrl_hum_reg  = osrs_h;
    pinMode(PIN_BUTTON_A, INPUT);
    pinMode(PIN_BUTTON_B, INPUT);
    Serial.begin(9600);                 // set serial port speed
    Serial.print("Pressure [hPa] ");   // header for serial output
    
    Wire.begin();
    writeReg(0xF2,ctrl_hum_reg);
    writeReg(0xF4,ctrl_meas_reg);
    writeReg(0xF5,config_reg);
    readTrim();                    //
    microbit.begin();
//    microbit.print("x");
    delay (1000);
}
//---------------------------------------------------------------------------------------------
void loop()
{
    double temp_act = 0.0, press_act = 0.0, hum_act=0.0;
    signed long int temp_cal;
    unsigned long int press_cal, hum_cal;
    int N;
    int M;
    double press_delta;                      // relative pressure
    
    int depth_m;                            // depth in meters, integer part        
    double depth_cm;                       // remainder in cm  
  
    readData();
//    temp_cal =  calibration_T(temp_raw);
    press_cal = calibration_P(pres_raw);
 //   hum_cal =   calibration_H(hum_raw);
 //   temp_act =  (double)temp_cal / 100.0;
    press_act = (double)press_cal / 100.0;
 //   hum_act =   (double)hum_cal / 1024.0;
    microbit.clear(); //reset LED matrix
  
// Button A sets actual value as reference (P zero)
// Button B display current value as depth in meters (calculated from pressure difference) 
    if (! digitalRead(PIN_BUTTON_A)) { 
//  set normal air pressure as zero
    press_norm = press_act;
//  microbit.print("P0: ");
//  microbit.print(press_norm,0);
//  microbit.print(" hPa");
    microbit.fillScreen(LED_ON); // blink once to confirm
    delay (100);
      
    }else if (! digitalRead(PIN_BUTTON_B)) {
//  display depth in meters 
    microbit.print(depth,2);
    microbit.print("m");
//  Serial.println("");
   }else{
// calculate depth from pressure difference
    press_delta = (press_act - press_norm);    // calculate relative pressure
    depth = (press_delta/100);                 // depth in meters
    depth_m = int(abs(depth));                 // depth im meters
    depth_cm = (abs(depth) - depth_m);         // remainder 
/*  // used for development
    Serial.println(depth);
    Serial.println(depth_m );
    Serial.println(depth_cm);
*/
// Steps for bargraph
    if (depth_cm > 0.8){                       // set length of bars
      (N=4);                           
    } else if (depth_cm > 0.6){
      (N=3);
    } else if (depth_cm > 0.4){
      (N=2);
    } else if (depth_cm > 0.2){
      (N=1);
    } else {
      (N=0);                        
    }
    if (depth_m == 4){                       // set level == meter  
      (M=4);                           
    } else if (depth_m == 3){
      (M=3);
    } else if (depth_m == 2){
      (M=2);
    } else if (depth_m == 1){
      (M=1);
    } else {
      (M=0);                              // upper row                       
    }    
/*  // used for development purposes  
    Serial.print("m: "); 
    Serial.println(depth_m);
    Serial.print("cm: "); 
    Serial.println(depth_cm);
    
    Serial.print("M: ");    Serial.println(M);   // for development purposes
    Serial.print("N: ");    Serial.println(N);   // for development purposes
    delay(500);
*/
    
//    draw bargraph
      microbit.drawLine(0, M, N, M, LED_ON); 

   }
// send value to serial port for plotter
    Serial.print(press_delta);
// draw indicator lines and fix displayed range 
    Serial.print("\t"); Serial.print(0);   
    Serial.print("\t"); Serial.print(-500); 
    Serial.print("\t"); Serial.println(500);
   delay(500);      // Measure twice a second
}
//-----------------------------------------------------------------------------------------------------------------------------------------------------
//  the following is required for the bmp/bme280 sensor,keep as it is
void readTrim()
{
    uint8_t data[32],i=0;                      // Fix 2014/04/06
    Wire.beginTransmission(BME280_ADDRESS);
    Wire.write(0x88);
    Wire.endTransmission();
    Wire.requestFrom(BME280_ADDRESS,24);       // Fix 2014/04/06
    while(Wire.available()){
        data[i] = Wire.read();
        i++;
    }
    Wire.beginTransmission(BME280_ADDRESS);    // Add 2014/04/06
    Wire.write(0xA1);                          // Add 2014/04/06
    Wire.endTransmission();                    // Add 2014/04/06
    Wire.requestFrom(BME280_ADDRESS,1);        // Add 2014/04/06
    data[i] = Wire.read();                     // Add 2014/04/06
    i++;                                       // Add 2014/04/06
    Wire.beginTransmission(BME280_ADDRESS);
    Wire.write(0xE1);
    Wire.endTransmission();
    Wire.requestFrom(BME280_ADDRESS,7);        // Fix 2014/04/06
    while(Wire.available()){
        data[i] = Wire.read();
        i++;    
    }
    dig_T1 = (data[1] << 8) | data[0];
    dig_P1 = (data[7] << 8) | data[6];
    dig_P2 = (data[9] << 8) | data[8];
    dig_P3 = (data[11]<< 8) | data[10];
    dig_P4 = (data[13]<< 8) | data[12];
    dig_P5 = (data[15]<< 8) | data[14];
    dig_P6 = (data[17]<< 8) | data[16];
    dig_P7 = (data[19]<< 8) | data[18];
    dig_T2 = (data[3] << 8) | data[2];
    dig_T3 = (data[5] << 8) | data[4];
    dig_P8 = (data[21]<< 8) | data[20];
    dig_P9 = (data[23]<< 8) | data[22];
    dig_H1 = data[24];
    dig_H2 = (data[26]<< 8) | data[25];
    dig_H3 = data[27];
    dig_H4 = (data[28]<< 4) | (0x0F & data[29]);
    dig_H5 = (data[30] << 4) | ((data[29] >> 4) & 0x0F); // Fix 2014/04/06
    dig_H6 = data[31];                                   // Fix 2014/04/06
}
void writeReg(uint8_t reg_address, uint8_t data)
{
    Wire.beginTransmission(BME280_ADDRESS);
    Wire.write(reg_address);
    Wire.write(data);
    Wire.endTransmission();    
}
void readData()
{
    int i = 0;
    uint32_t data[8];
    Wire.beginTransmission(BME280_ADDRESS);
    Wire.write(0xF7);
    Wire.endTransmission();
    Wire.requestFrom(BME280_ADDRESS,8);
    while(Wire.available()){
        data[i] = Wire.read();
        i++;
    }
    pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
    temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4);
    hum_raw  = (data[6] << 8) | data[7];
}
signed long int calibration_T(signed long int adc_T)
{
    signed long int var1, var2, T;
    var1 = ((((adc_T >> 3) - ((signed long int)dig_T1<<1))) * ((signed long int)dig_T2)) >> 11;
    var2 = (((((adc_T >> 4) - ((signed long int)dig_T1)) * ((adc_T>>4) - ((signed long int)dig_T1))) >> 12) * ((signed long int)dig_T3)) >> 14;
    t_fine = var1 + var2;
    T = (t_fine * 5 + 128) >> 8;
    return T; 
}
unsigned long int calibration_P(signed long int adc_P)
{
    signed long int var1, var2;
    unsigned long int P;
    var1 = (((signed long int)t_fine)>>1) - (signed long int)64000;
    var2 = (((var1>>2) * (var1>>2)) >> 11) * ((signed long int)dig_P6);
    var2 = var2 + ((var1*((signed long int)dig_P5))<<1);
    var2 = (var2>>2)+(((signed long int)dig_P4)<<16);
    var1 = (((dig_P3 * (((var1>>2)*(var1>>2)) >> 13)) >>3) + ((((signed long int)dig_P2) * var1)>>1))>>18;
    var1 = ((((32768+var1))*((signed long int)dig_P1))>>15);
    if (var1 == 0)
    {
        return 0;
    }    
    P = (((unsigned long int)(((signed long int)1048576)-adc_P)-(var2>>12)))*3125;
    if(P<0x80000000)
    {
       P = (P << 1) / ((unsigned long int) var1);   
    }
    else
    {
        P = (P / (unsigned long int)var1) * 2;    
    }
    var1 = (((signed long int)dig_P9) * ((signed long int)(((P>>3) * (P>>3))>>13)))>>12;
    var2 = (((signed long int)(P>>2)) * ((signed long int)dig_P8))>>13;
    P = (unsigned long int)((signed long int)P + ((var1 + var2 + dig_P7) >> 4));
    return P;
}
unsigned long int calibration_H(signed long int adc_H)
{
    signed long int v_x1;
    v_x1 = (t_fine - ((signed long int)76800));
    v_x1 = (((((adc_H << 14) -(((signed long int)dig_H4) << 20) - (((signed long int)dig_H5) * v_x1)) + 
              ((signed long int)16384)) >> 15) * (((((((v_x1 * ((signed long int)dig_H6)) >> 10) * 
              (((v_x1 * ((signed long int)dig_H3)) >> 11) + ((signed long int) 32768))) >> 10) + (( signed long int)2097152)) * 
              ((signed long int) dig_H2) + 8192) >> 14));
   v_x1 = (v_x1 - (((((v_x1 >> 15) * (v_x1 >> 15)) >> 7) * ((signed long int)dig_H1)) >> 4));
   v_x1 = (v_x1 < 0 ? 0 : v_x1);
   v_x1 = (v_x1 > 419430400 ? 419430400 : v_x1);
   return (unsigned long int)(v_x1 >> 12);

Step 6: A Major Simplification: the MakeCode/JavaScript Code

In May 2018, Pimoroni has released the enviro:bit, that comes with a BME280 pressure/humidity/temperature sensor, a TCS3472 light and color sensor and a MEMS microphone. In addition they are offering a JavaScript library for the MakeCode editor and a MicroPython library for these sensors.

I have been using their MakeCode library to develop scripts for my device. Attached you find the according hex files, which you can copy directly to your micro:bit.

Below you find the corresponding JavaScript code. Testing in the pool worked well with a earlier version of the script, so I assume they will work as well. In addition to the basic, bargraph version, there is also a crosshair version (X) and a L-version, intended to make reading easier, especially under low-light conditions. Pick the one you prefer.

let Column = 0
let Meter = 0
let remain = 0
let Row = 0
let Delta = 0
let Ref = 0
let Is = 0
Is = 1012
basic.showLeds(`
    # # # # #
    # . . . #
    # . # . #
    # . . . #
    # # # # #
    `)
Ref = 1180
basic.clearScreen()
basic.forever(() => {
    basic.clearScreen()
    if (input.buttonIsPressed(Button.A)) {
        Ref = envirobit.getPressure()
        basic.showLeds(`
            # . # . #
            . # . # .
            # # # # #
            . # . # .
            # . # . #
            `)
        basic.pause(1000)
    } else if (input.buttonIsPressed(Button.B)) {
        basic.showString("" + Row + "." + remain + " m")
        basic.pause(200)
        basic.clearScreen()
    } else {
        Is = envirobit.getPressure()
        Delta = Is - Ref
        Meter = Math.abs(Delta)
        if (Meter >= 400) {
            Row = 4
        } else if (Meter >= 300) {
            Row = 3
        } else if (Meter >= 200) {
            Row = 2
        } else if (Meter >= 100) {
            Row = 1
        } else {
            Row = 0
        }
        remain = Meter - Row * 100
        if (remain >= 80) {
            Column = 4
        } else if (remain >= 60) {
            Column = 3
        } else if (remain >= 40) {
            Column = 2
        } else if (remain >= 20) {
            Column = 1
        } else {
            Column = 0
        }
        for (let ColA = 0; ColA <= Column; ColA++) {
            led.plot(ColA, Row)
        }
        basic.pause(500)
    }
})

Step 7: The Enviro:bit Version

In the meantime I received the enviro:bit (20 GBP) and the power:bit (6 GBP), both from Pimoroni.

As mentioned before, the enviro:bit comes with the BME280 pressure, humidity and temperature sensor, but also a light and color sensor (see an application here) and a MEMS microphone.

The power:bit is a nice solution to power the micro:bit and comes with a on/off switch.

The great thing is that it both are just click and use, no soldering, cables, breadboards.
Add the enviro:bit to the micro:bit, load you code to the micro:bit, use it.

In this case I used micro, power and enviro:bit, placed them in a Ziploc bag, placed it in a clear water tight plastic bag for mobile phones, ready. A very fast and tidy solution. See the pictures. The switch is large enough to use it through the protection layers.

It has been tested in water, was working well. At a depth of about 1.8 m the measured value was about 1.7 m. Not too bad for a fast & cheap solution, but far from being perfect. It takes a while to adjust, so you may need to stay at a certain depth for about 10-15 seconds.

Step 8: Cable and Sensor Probe Version

This actually was the first idea a had for a micro:bit depth meter, the last to be build.

Here I soldered the BMP280 sensor to 5m of a 4-wire cable and placed female jumper at the other end.
To protect the sensor from water, the cable was run through an used wine cork. The ends of the cork were sealed with hot glue. Before I had cut two notches into the cork, both going all around it. Then I packed the sensor into a sponge ball, placed a balloon around it and fixed the end of the balloon on the cork (lower notch). then I placed 3 40 g pieces of lead weights into a second balloon, wrapped it around the first one, weights placed at the outer side, and fixed the end of the balloon at the second notch. The air was removed from the second balloon, then everything was fixed with duct tape. See images, more detailed ones may follow.

The jumpers were connected to the micro:bit via an edge connector, the device switched on and the reference pressure was set. Then the sensor head was released slowly to the bottom of the pool (10 m jumping tower, about 4.5 m deep).

Results:

To my astonishment, it worked even with this long cable. On the other hand, but not surprisingly, the measurement error seemed to become larger at higher pressures, and an estimated depth of 4 m was reported as about 3 m.

Potential applications:

With some error corrections, the device might be used to measure depth to about 4 m.

In conjunction with a Arduino or Raspberry Pi, this could be use to measure and control the filling point of a pool or water tank, e,g. to evoke a warning if the water levels go above or below certain thresholds.

Outdoor Fitness Challenge

Runner Up in the
Outdoor Fitness Challenge