Built-in project · Arduino Uno R3

Breathalyzer with an MQ-3 on an Arduino Uno

An MQ-3 alcohol sensor calibrated in clean air with MQUnifiedsensor: milligrams of alcohol per liter on a 16x2 I2C LCD and a traffic light for the level. The whole build, an Arduino Uno and 3 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.

Intermediate Runs in your browser. Free, and no account needed.

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

Breathalyzer with an MQ-3 · 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.

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
// Breathalyzer: an MQ-3 alcohol sensor, a 16x2 I2C LCD and a traffic light
// module on an Arduino Uno, read with the MQUnifiedsensor library.
//
//   A0      the MQ-3 board's AO (its VCC on 5 V, GND on GND)
//   2, 3, 4 the traffic light module's G, Y and R
//   LCD     SDA -> A4    SCL -> A5    VCC -> 5 V    GND -> GND
//
// The MQ-3 is a heated element whose resistance, Rs, falls as alcohol vapor
// reaches it. Its datasheet plots Rs / R0 against milligrams of alcohol per
// liter of air, where R0 is Rs at 0.4 mg/L, and clean air sits at about 60
// times R0. The library turns a reading into Rs through the board's 200 k load
// resistor, and Rs / R0 into mg/L with the chart's line:
//
//     mg/L = 0.3934 x (Rs / R0) ^ -1.504
//
// So the sketch waits out the heater's warm-up, measures Rs in clean air,
// divides by 60 to find R0, and from then on prints mg/L.
//
// THE CALIBRATION. Clean air is 60 megohms over the 200 k load: AO is only
// about 17 mV. Against the usual 5 V reference that is 3 or 4 counts of 1023,
// and a count either way is a quarter of the reading, so R0 would come out
// 15 % wrong and every reading after it about a fifth off. Averaging does not
// help: the reading is steady, it is just coarse. So the sketch measures clean
// air against the Uno's internal 1.1 V reference instead, where 17 mV is
// about 15 counts and one count is 6 %, then goes back to the 5 V reference to
// read the gas. On a real board the 1.1 V reference is 1.0 to 1.2 V from chip
// to chip; measure yours on the AREF pin and put it in VREF_INTERNAL. And a
// heater that has just warmed up is still drifting, so the sketch waits for
// the clean-air reading to hold still for ten seconds first. Here clean air
// reads 16 counts (17.2 mV), R0 comes out 965 k for the sensor's 1000 k, and
// 0.4 mg/L reads 0.38.
//
// This is a toy, not a breath test. A real sensor drifts with temperature,
// humidity and age, and the law measures alcohol in blood or breath with
// calibrated instruments. The levels below are only for the lights.
//
// Press Run, wait about 40 seconds for "Blow now", then drag the slider on
// the MQ-3.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <MQUnifiedsensor.h>

const int GREEN_PIN = 2;
const int YELLOW_PIN = 3;
const int RED_PIN = 4;

const float CLEAN_AIR_RATIO = 60.0;  // Rs / R0 in clean air, from the datasheet
const float SOME = 0.05;             // mg/L: anything above this is alcohol
const float HIGH_LEVEL = 0.25;       // mg/L: the red light
const float VREF_INTERNAL = 1.1;     // volts: the Uno's internal reference
const float RL_KOHM = 200;           // the MQ-3 board's load resistor

MQUnifiedsensor MQ3("Arduino UNO", 5, 10, A0, "MQ-3");
LiquidCrystal_I2C lcd(0x27, 16, 2);

void lights(bool green, bool yellow, bool red) {
  digitalWrite(GREEN_PIN, green ? HIGH : LOW);
  digitalWrite(YELLOW_PIN, yellow ? HIGH : LOW);
  digitalWrite(RED_PIN, red ? HIGH : LOW);
}

void setup() {
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(YELLOW_PIN, OUTPUT);
  pinMode(RED_PIN, OUTPUT);
  Serial.begin(9600);
  Serial.println("Breathalyzer");
  lcd.init();
  lcd.backlight();

  MQ3.setRegressionMethod(1);  // mg/L = a x ratio ^ b
  MQ3.setA(0.3934);
  MQ3.setB(-1.504);
  MQ3.setRL(RL_KOHM);  // the library works in kilohms
  MQ3.init();

  // The heater needs 20 seconds before a reading means anything.
  lights(false, true, false);
  lcd.print("Warming up...");
  for (int s = 20; s > 0; s--) {
    lcd.setCursor(0, 1);
    lcd.print(s);
    lcd.print(" s  ");
    delay(1000);
  }

  // Calibrate: R0 is Rs in clean air divided by the clean-air ratio, read
  // against the 1.1 V reference. The first reading after a change of
  // reference is thrown away while it settles. A heater that has just warmed
  // up is still drifting, so the sketch waits until the clean-air reading has
  // held still for ten seconds before it believes it.
  analogReference(INTERNAL);
  analogRead(A0);
  delay(10);
  lcd.clear();
  lcd.print("Settling...");
  long sum = 0;
  int steady = 0;
  int lastCount = -1;
  while (steady < 10) {
    sum = 0;
    for (int i = 0; i < 32; i++) sum += analogRead(A0);
    int count = (int)(sum / 32);
    steady = count == lastCount ? steady + 1 : 0;
    lastCount = count;
    lcd.setCursor(0, 1);
    lcd.print(count);
    lcd.print(" of 1023   ");
    delay(1000);
  }
  float volts = sum / 32.0 * VREF_INTERNAL / 1023.0;
  float rsAir = RL_KOHM * (5.0 - volts) / volts;  // the sensor over its load
  float r0 = rsAir / CLEAN_AIR_RATIO;
  MQ3.setR0(r0);
  analogReference(DEFAULT);
  analogRead(A0);
  delay(10);
  Serial.print("Clean air: ");
  Serial.print(volts * 1000, 1);
  Serial.print(" mV, R0 = ");
  Serial.print(r0, 0);
  Serial.println(" kOhm");

  lcd.clear();
  lcd.print("Blow now");
}

void loop() {
  MQ3.update();
  float mgL = MQ3.readSensor();

  lcd.setCursor(0, 0);
  if (mgL < SOME) {
    lights(true, false, false);
    lcd.print("No alcohol      ");
  } else if (mgL < HIGH_LEVEL) {
    lights(false, true, false);
    lcd.print("Some alcohol    ");
  } else {
    lights(false, false, true);
    lcd.print("Too much!       ");
  }
  lcd.setCursor(0, 1);
  lcd.print(mgL, 2);
  lcd.print(" mg/L      ");

  Serial.print(mgL, 2);
  Serial.println(" mg/L");
  delay(500);
}

Parts list

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

How it is wired

9 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Ground: Arduino Uno R3 pin GND; Traffic light module pin GND
  • Arduino Uno R3 pin 4; Traffic light module pin R
  • Arduino Uno R3 pin 3; Traffic light module pin Y
  • Arduino Uno R3 pin 2; Traffic light module pin G
  • Arduino Uno R3 pin 5V; Alcohol sensor, MQ-3 pin VCC; Character LCD with I2C backpack pin VCC
  • Ground: Arduino Uno R3 pin GND; Alcohol sensor, MQ-3 pin GND; Character LCD with I2C backpack pin GND
  • Arduino Uno R3 pin A0; Alcohol sensor, MQ-3 pin AO
  • Arduino Uno R3 pin A4; Character LCD with I2C backpack pin SDA
  • Arduino Uno R3 pin A5; Character LCD with I2C backpack pin SCL

Change it and keep it

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