top of page

"Better to be on the boat wishing you were diving, than underwater wishing you were on the boat."

Augusto Pavan

Oxygen Sensor – Part 2: Complete Device Integration

System Architecture

  • MCU: ATmega328p (flashed via ISP programmer)

  • Power supply: Single 18650 Li-ion cell (3.7 V) → protected by charge/discharge circuitry → boosted to 5 V via step-up converter

  • Display: 0.98″ OLED with SSD1331 driver (SPI interface)

  • Signal chain: Teledyne R‑22A galvanic Oâ‚‚ cell → ADS1115 16-bit ADC → MCU logic

ELETRIC SCHEME
schema elettrico

💻 Firmware Overview

The sketch extends the original sensor-only code, adding graphical calibration and visual Oâ‚‚ output:

  • Startup

    • Initialize serial and OLED

    • Text “CALIBRATION” displayed

    • Read 32 raw ADC samples, updating a progress bar on-screen

    • Compute calibration = average(raw readings) as air reference

  • Main Loop

    • Read current ADC channel

    • Compute Oâ‚‚ (%) = (raw / calibration) × 20.90

    • Display value (Oâ‚‚: xx.x%) on OLED

    • Print to serial (%o2: xx.x%)

    • Increment visual progress bar; upon reaching max width, redraw “resetta” in red

PCB
board.png

🔌 Electrical Design & PCB Options

You can have the PCB manufactured by a low-cost supplier, or mill it yourself. I used a 0.1 mm knife-end CNC tool on copper-clad board—clean, precise tracks from just a few passes.

CODE

#include <Wire.h>
#include <Adafruit_ADS1015.h>


//display
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1331.h>
#include <SPI.h>


// You can use any (4 or) 5 pins
#define sclk 13
#define mosi 11
#define cs   10
#define rst  9
#define dc   8


// Color definitions
#define  BLACK           0x0000
#define BLUE            0x001F
#define RED             0xF800
#define GREEN           0x07E0
#define CYAN            0x07FF
#define YELLOW          0xFFE0
#define WHITE           0xFFFF

Adafruit_SSD1331 display = Adafruit_SSD1331(&SPI, cs, dc, rst);


 Adafruit_ADS1115 ads;  /* Use this for the 16-bit version */
//Adafruit_ADS1015 ads;     /* Use thi for the 12-bit version */
float o2=0; //% di ossigeno
float calibration=0;  //variabile calibrazione
int count=0; //variabile contatore

void setup(void) {


  
  Serial.begin(9600);
  Serial.println("Hello!");

  display.begin();

  display.fillScreen(BLACK);

  delay(500);

  
  // The ADC input range (or gain) can be changed via the following
  // functions, but be careful never to exceed VDD +0.3V max, or to
  // exceed the upper and lower limits if you adjust the input range!
  // Setting these values incorrectly may destroy your ADC!
  //                                                                ADS1015  ADS1115
  //                                                                -------  -------
  // ads.setGain(GAIN_TWOTHIRDS);  // 2/3x gain +/- 6.144V  1 bit = 3mV      0.1875mV (default)
  // ads.setGain(GAIN_ONE);        // 1x gain   +/- 4.096V  1 bit = 2mV      0.125mV
  // ads.setGain(GAIN_TWO);        // 2x gain   +/- 2.048V  1 bit = 1mV      0.0625mV
  // ads.setGain(GAIN_FOUR);       // 4x gain   +/- 1.024V  1 bit = 0.5mV    0.03125mV
  // ads.setGain(GAIN_EIGHT);      // 8x gain   +/- 0.512V  1 bit = 0.25mV   0.015625mV
  // ads.setGain(GAIN_SIXTEEN);    // 16x gain  +/- 0.256V  1 bit = 0.125mV  0.0078125mV

  
 
  ads.begin();
  float calibrationraw=0;
  //eseguo 20 letture del sensore
  display.setCursor(12,20);
  display.setTextColor(WHITE);
  display.print("CALIBRAZIONE");

  
  for(int i=0; i<32; i++){
    calibrationraw=calibrationraw+ads.readADC_SingleEnded(0);
    display.fillRect(0,36 ,i*3 , 8, BLUE+i);
    delay(100);
  }
 
  calibration=calibrationraw/32;  //prendo come riferimento la media delle ultime 20 letture

  display.fillScreen(BLACK);
  display.setCursor(0,28);
  display.setTextColor(GREEN, BLACK);

  display.print("O2:");  

}

void loop(void) 
{
  int16_t adc0;

  adc0 = ads.readADC_SingleEnded(0);
  o2=((float)adc0/calibration)*20.90;
  display.setCursor(40,28);
  display.setTextColor(WHITE, BLACK);

  display.print(o2);
  display.print("%");  
  Serial.print("%o2: "); Serial.print(o2);
  Serial.println("%");
  count++;
  display.fillRect(0, 0, count, 5, BLUE);
  delay(1000);
  if(count==96){
    display.setCursor(27,10);
    display.setTextColor(RED, BLACK);
    display.print("resetta");
  }
}

The firmware is built around a minimal loop structure with straightforward logic, optimized for stability and clarity.
At startup, the microcontroller performs a 32-sample calibration from the Oâ‚‚ cell via the ADS1115, displaying a progress bar on the OLED during sampling — an elegant way to provide visual feedback without interrupting the calibration logic.

Once in the main loop:

  • The ADC channel is read via I²C.

  • Oxygen percentage is calculated using the standard linear formula:
    Oâ‚‚ (%) = (raw / calibration) × 20.90

  • The result is:

    • Printed over serial for logging or debugging

    • Displayed on screen in real-time with a large font

  • A moving bar provides visual animation (e.g., gas flow or runtime), and resets after a full cycle with a visual cue (“resetta” in red).

The code is blocking (uses delay()), which is acceptable here since we’re not handling asynchronous events. However, it can be adapted for real-time logging or interrupt-driven tasks if needed.

3D CASE

📦 Enclosure & Final Build

  • Case: 3D-designed in SolidWorks and printed in high-resolution resin

  • Added a power switch on the input line

  • Final assembly showcases professional look and compact ergonomics

IMG_4419 (1).jpg
case.JPG
IMG_4418.jpg
FINAL RESULT
IMG_4348.jpg
aee313_c8df9b3f9fdf45578a43a298fbdd6969~
IMG_6442.jpg

About Me

With a strong passion for electronics and technology, I’ve begun integrating it into another deep interest of mine: scuba diving.

 

© 2023 by Going Places. 

bottom of page