
"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

💻 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

🔌 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
.jpg)


FINAL RESULT

