Built-in project · Arduino Uno R3

Tire pressure check station on an Arduino Uno

An MPX5700 gauge pressure sensor on a shop air chuck, auto-zeroed at power up: the tire in psi, kPa and bar on a 20x4 LCD, a target you step to your door-jamb placard, and lights and a buzzer for under, on target, over and past the sidewall max. 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.

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

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

The circuit runs here once the page has loaded.

What you'll learn

  • Sensors
  • Displays
  • Sound
  • Reading a button
  • Making sound with a buzzer
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 7

How it works

An MPX5700 gauge pressure sensor on a shop air chuck, auto-zeroed at power up: the tire in psi, kPa and bar on a 20x4 LCD, a target you step to your door-jamb placard, and lights and a buzzer for under, on target, over and past the sidewall max.

Arduino Uno R3 runs the sketch below. The circuit around it: Traffic light module, Pressure sensor, MPX5010 / MPX5700, Pushbutton, Buzzer, Character LCD, 20x4, with I2C backpack. pinMode sets BUTTON_PIN as an input with its pull-up on. digitalRead reads whether BUTTON_PIN is HIGH or LOW. analogRead measures the voltage on SENSOR_PIN as a number. delay waits 100 milliseconds. Serial.begin opens the serial port at 115200 baud.

Parts list

7 parts, plus the jumper wires. They retail for about $53; 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
Traffic light module pin GNDArduino Uno R3 pin GND
Pressure sensor, MPX5010 / MPX5700 pin GNDArduino Uno R3 pin GND
Pushbutton pin 2aArduino Uno R3 pin GND
Buzzer pin 2Arduino Uno R3 pin GND
Character LCD, 20x4, with I2C backpack pin GNDArduino Uno R3 pin GND
Traffic light module pin RArduino Uno R3 pin 11
Traffic light module pin YArduino Uno R3 pin 10
Traffic light module pin GArduino Uno R3 pin 9
Buzzer pin 1Arduino Uno R3 pin 6
Pushbutton pin 1aArduino Uno R3 pin 2
Pressure sensor, MPX5010 / MPX5700 pin VSArduino Uno R3 pin 5V
Character LCD, 20x4, with I2C backpack pin VCCArduino Uno R3 pin 5V
Pressure sensor, MPX5010 / MPX5700 pin VOUTArduino Uno R3 pin A0
Character LCD, 20x4, with I2C backpack pin SDAArduino Uno R3 pin A4
Character LCD, 20x4, with I2C backpack 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
// Tire pressure check station: an NXP MPX5700GP gauge pressure sensor on a
// shop air line's tire chuck, the pressure in psi, kPa and bar on a 20x4 I2C
// LCD, a target you dial in from the door-jamb placard, and green, yellow and
// red lights with a buzzer for under, on target and over.
//
//   A0      the MPX5700GP's VOUT (pin 1; pin 2 GND, pin 3 VS on 5 V)
//   2       the target button to ground (the chip's pull-up holds it high)
//   6       an active buzzer
//   9 10 11 the traffic light module's G, Y and R
//   LCD     SDA -> A4    SCL -> A5    VCC -> 5 V    GND -> GND
//
// SAFETY: this is a teaching simulation. Never connect a project like this
// to a running car's safety systems (brakes, airbags, steering, ECU wiring,
// anything that can strand or hurt someone). Power it from USB, a bench
// supply, or a fused 12 V battery on the bench through a 5 V regulator; any
// real vehicle install needs isolation or an automotive-rated front end (a
// fuse, TVS and load-dump protection, and a divider or level step down from
// 12 V to 5 V logic). Never inflate past the sidewall's maximum, stand to the
// side of the tire while filling, and never reinflate a tire that was driven
// flat without having it inspected.
//
// THE SENSOR. The MPX5700GP reads gauge pressure, the line over the air
// around it, 0 to 700 kPa (0 to about 101 psi), and its output is
// ratiometric: a fraction of its supply, by the datasheet
//
//   VOUT = VS x (0.0012858 x P + 0.04)      P in kPa
//
// so 0.2 V at 0 and 4.7 V at 700 kPa on 5 V. The Uno's converter measures
// against the same 5 V (count = Vin x 1024 / 5 V), so the sketch works in
// fractions of the supply and a supply that sags a little cancels out:
//
//   fraction = count / 1024
//   kPa      = (fraction - zeroFraction) / 0.0012858
//   psi      = kPa / 6.89476        (1 psi = 6.89476 kPa)
//   bar      = kPa / 100            (1 bar = 100 kPa = 14.5 psi)
//
// One converter step is 0.76 kPa, about 0.11 psi.
//
// AUTO-ZERO. A real MPX5700's output at zero pressure is anywhere from
// 0.088 to 0.313 V, not exactly 0.2 V. So at power up, with the chuck OFF
// the tire (the sensor open to the air), the sketch measures the zero and
// subtracts it from every reading. If the reading at power up is outside
// that band, something is pushing on the sensor (the chuck is still on a
// tire), so it asks you to unhook it and waits rather than zero on a tire.
//
// HYSTERESIS. A reading sitting right on the edge of the plus or minus
// 2 psi window would flicker between two lights. Once inside the window, the
// reading has to go 0.3 psi past its edge to leave it again.
//
// Press the button to step the target 1 psi, 26 to 41 psi, then back to 26.
// The sidewall maximum (44 psi here) is a hard limit: the target can never
// be within 3 psi of it, and the alarm sounds the moment you reach it.

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

const int SENSOR_PIN = A0;
const int BUTTON_PIN = 2;
const int BUZZER_PIN = 6;
const int GREEN_PIN = 9;
const int YELLOW_PIN = 10;
const int RED_PIN = 11;

// The sensor, from the MPX5700 datasheet.
const float SLOPE = 0.0012858;           // fraction of VS per kPa
const float ZERO_LOW = 0.088 / 5.0;      // the zero's band, as fractions of 5 V
const float ZERO_HIGH = 0.313 / 5.0;
const float FULL_SCALE_KPA = 690.0;      // near 700 kPa the output saturates
const float KPA_PER_PSI = 6.89476;

// The tire. Read SIDEWALL_MAX_PSI off your own tire's sidewall ("MAX PRESS").
const int SIDEWALL_MAX_PSI = 44;
const float BAND = 2.0;                  // on target within plus or minus 2 psi
const float HYST = 0.3;                  // how far past an edge to leave the window
const int TARGET_MIN = 26;
const int TARGET_MAX = SIDEWALL_MAX_PSI - 3;   // target + 2 stays under the max
const int TARGET_DEFAULT = 35;
const float CONNECT_PSI = 3.0;           // above this, a tire is on the chuck
const float DISCONNECT_PSI = 2.0;        // under this, it is off again
const int TARGET_ADDRESS = 0;            // the target's byte in EEPROM

enum State { NO_TIRE, UNDER, ON_TARGET, OVER, OVER_MAX };

LiquidCrystal_I2C lcd(0x27, 20, 4);
float zeroFraction = 0.04;
int target = TARGET_DEFAULT;
State state = NO_TIRE;
float psi = 0;
bool overRange = false;
bool lastButton = HIGH;
unsigned long lastPress = 0;
unsigned long chirpUntil = 0;
unsigned long lastShow = 0;
char rows[4][21];                        // the LCD's next four lines
int nextRow = 4;                         // the row to draw next; 4 when done

// The sensor's output as a fraction of the supply, averaged over 32 reads.
float readFraction() {
  long sum = 0;
  for (int i = 0; i < 32; i++) sum += analogRead(SENSOR_PIN);
  return sum / 32.0 / 1024.0;
}

// One LCD row, padded to its 20 characters so nothing old is left behind.
void showRow(int row, const char *text) {
  char line[21];
  snprintf(line, sizeof line, "%-20s", text);
  lcd.setCursor(0, row);
  lcd.print(line);
}

// Lay out one row for later: loop() draws one row a pass, so a whole LCD
// update never holds up the over-inflation alarm.
void setRow(int row, const char *text) {
  snprintf(rows[row], sizeof rows[row], "%s", text);
}

// The next state, with hysteresis: only a state already inside the window
// gets the wider edges.
State nextState(State s, float p) {
  if (overRange || p >= SIDEWALL_MAX_PSI) return OVER_MAX;
  if (s == OVER_MAX && p >= SIDEWALL_MAX_PSI - HYST) return OVER_MAX;
  if (s == NO_TIRE && p < CONNECT_PSI) return NO_TIRE;
  if (s != NO_TIRE && p < DISCONNECT_PSI) return NO_TIRE;
  float low = target - BAND;
  float high = target + BAND;
  if (s == ON_TARGET) {
    low -= HYST;
    high += HYST;
  }
  if (p < low) return UNDER;
  if (p > high) return OVER;
  return ON_TARGET;
}

// The TPMS light comes on about 25 % under the placard pressure.
bool tpmsLow() {
  return state == UNDER && psi < target * 0.75;
}

const char *stateName() {
  switch (state) {
    case NO_TIRE: return "NO TIRE";
    case UNDER: return tpmsLow() ? "TPMS LOW" : "UNDER";
    case ON_TARGET: return "OK";
    case OVER: return "OVER";
    default: return overRange ? "OVER RANGE" : "OVER MAX";
  }
}

void autoZero() {
  lcd.clear();
  showRow(0, "Tire pressure check");
  showRow(1, "Zeroing: chuck off");
  delay(100);                            // the sensor's 20 ms warm up, and more
  float f = readFraction();
  bool warned = false;
  while (f < ZERO_LOW || f > ZERO_HIGH) {
    if (!warned) {
      Serial.print("Zero out of band (");
      Serial.print(f * 5.0, 3);
      Serial.println(" V): unhook the chuck from the tire");
      showRow(1, "Unhook the chuck");
      showRow(2, "from the tire to");
      showRow(3, "zero the sensor");
      warned = true;
    }
    delay(250);
    f = readFraction();
  }
  zeroFraction = f;
  Serial.print("Zero: ");
  Serial.print(zeroFraction * 5.0, 3);
  Serial.println(" V");
  lcd.clear();
}

void checkButton() {
  bool now = digitalRead(BUTTON_PIN);
  if (now == LOW && lastButton == HIGH && millis() - lastPress > 50) {
    lastPress = millis();
    target = target >= TARGET_MAX ? TARGET_MIN : target + 1;
    EEPROM.update(TARGET_ADDRESS, target);
    chirpUntil = millis() + 30;          // a click to say it took
    Serial.print("Target: ");
    Serial.print(target);
    Serial.println(" psi");
    // Judge the pressure afresh against the new window.
    if (state != NO_TIRE) state = nextState(UNDER, psi);
  }
  lastButton = now;
}

void driveOutputs() {
  unsigned long t = millis();
  bool flash = (t / 100) % 2;            // 5 beeps a second
  bool slow = (t / 400) % 2;
  digitalWrite(GREEN_PIN, state == ON_TARGET ? HIGH : LOW);
  digitalWrite(YELLOW_PIN, state == UNDER && (!tpmsLow() || slow) ? HIGH : LOW);
  digitalWrite(RED_PIN, state == OVER || state == OVER_MAX ? HIGH : LOW);
  bool buzz = state == OVER_MAX || (state == OVER && flash) || t < chirpUntil;
  digitalWrite(BUZZER_PIN, buzz ? HIGH : LOW);
}

void show() {
  float kpa = psi * KPA_PER_PSI;
  char num[10];
  char text[24];

  dtostrf(psi, 1, 1, num);
  snprintf(text, sizeof text, "Tire %s psi", overRange ? ">100" : num);
  setRow(0, text);

  char bar[10];
  dtostrf(kpa / 100.0, 1, 2, bar);
  snprintf(text, sizeof text, "%d kPa  %s bar", (int)(kpa + 0.5), bar);
  setRow(1, text);

  snprintf(text, sizeof text, "Target %d  max %d", target, SIDEWALL_MAX_PSI);
  setRow(2, text);

  float off = psi - target;
  dtostrf(off < 0 ? -off : off, 1, 1, num);
  switch (state) {
    case NO_TIRE: snprintf(text, sizeof text, "Connect the chuck"); break;
    case UNDER: snprintf(text, sizeof text, "%s: add %s", tpmsLow() ? "TPMS LOW" : "LOW", num); break;
    case ON_TARGET: snprintf(text, sizeof text, "OK: within 2 psi"); break;
    case OVER: snprintf(text, sizeof text, "HIGH: let out %s", num); break;
    default: snprintf(text, sizeof text, overRange ? "OVER SENSOR RANGE!" : "OVER SIDEWALL MAX!"); break;
  }
  setRow(3, text);

  nextRow = 0;

  Serial.print(psi, 1);
  Serial.print(" psi  ");
  Serial.print((int)(kpa + 0.5));
  Serial.print(" kPa  ");
  Serial.print(kpa / 100.0, 2);
  Serial.print(" bar  target ");
  Serial.print(target);
  Serial.print("  ");
  Serial.println(stateName());
}

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(YELLOW_PIN, OUTPUT);
  pinMode(RED_PIN, OUTPUT);
  Serial.begin(115200);
  lcd.init();
  lcd.backlight();
  Wire.setClock(400000);                 // fast I2C: an LCD row in a few ms

  // The target from last time, or the default on a new board (0xFF).
  int saved = EEPROM.read(TARGET_ADDRESS);
  target = saved >= TARGET_MIN && saved <= TARGET_MAX ? saved : TARGET_DEFAULT;
  Serial.print("Tire pressure check station: target ");
  Serial.print(target);
  Serial.print(" psi, sidewall max ");
  Serial.print(SIDEWALL_MAX_PSI);
  Serial.println(" psi");

  autoZero();
}

void loop() {
  // Read fast (every 15 ms or so) so the over-inflation alarm is quick; show
  // the numbers four times a second.
  float kpa = (readFraction() - zeroFraction) / SLOPE;
  if (kpa < 0) kpa = 0;
  overRange = kpa >= FULL_SCALE_KPA;
  psi = kpa / KPA_PER_PSI;

  State before = state;
  state = nextState(state, psi);
  checkButton();
  // A chirp as the pressure comes up into the window: stop filling.
  if (before == UNDER && state == ON_TARGET) chirpUntil = millis() + 150;
  driveOutputs();

  if (millis() - lastShow >= 250) {
    lastShow = millis();
    show();
  }
  if (nextRow < 4) {
    showRow(nextRow, rows[nextRow]);
    nextRow++;
  } else {
    delay(10);
  }
}

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() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(YELLOW_PIN, OUTPUT);
  pinMode(RED_PIN, OUTPUT);
  Serial.begin(115200);
  lcd.init();
  lcd.backlight();
  Wire.setClock(400000);                 // fast I2C: an LCD row in a few ms

  // The target from last time, or the default on a new board (0xFF).
  int saved = EEPROM.read(TARGET_ADDRESS);
  target = saved >= TARGET_MIN && saved <= TARGET_MAX ? saved : TARGET_DEFAULT;
  Serial.print("Tire pressure check station: target ");
  Serial.print(target);
  Serial.print(" psi, sidewall max ");
  Serial.print(SIDEWALL_MAX_PSI);
  Serial.println(" psi");

  autoZero();
}

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() {
  // Read fast (every 15 ms or so) so the over-inflation alarm is quick; show
  // the numbers four times a second.
  float kpa = (readFraction() - zeroFraction) / SLOPE;
  if (kpa < 0) kpa = 0;
  overRange = kpa >= FULL_SCALE_KPA;
  psi = kpa / KPA_PER_PSI;

  State before = state;
  state = nextState(state, psi);
  checkButton();
  // A chirp as the pressure comes up into the window: stop filling.
  if (before == UNDER && state == ON_TARGET) chirpUntil = millis() + 150;
  driveOutputs();

  if (millis() - lastShow >= 250) {
    lastShow = millis();
    show();
  }
  if (nextRow < 4) {
    showRow(nextRow, rows[nextRow]);
    nextRow++;
  } else {
    delay(10);
  }
}

Troubleshooting

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

  • One color never lights, or all three light together. Traffic light module
    Check each wire goes to its own pin labeled R, Y, G, and that GND is connected. A pin driven high lights its LED. Resistors are already on the board, so do not add more.
  • Lights seem dim or the wrong color comes on in a sequence. Traffic light module
    Confirm the pins in your code match the wires, and that the module gets the right supply voltage. Keep each state's delay long enough to see, and turn the old color off before the next.
  • A 3.3 V board gives clipped or damaged readings. Pressure sensor
    The output spans 0.2 to 4.7 V with a 5 V supply, which is above a 3.3 V ADC. Use a resistor divider and scale in code. The supply must be 5 V, about 4.75 to 5.25 V.
  • Pressure readings are wrong or reversed. Pressure sensor
    Know which port is which. The differential versions have two ports, and pressure on the vacuum side reads lower. Gauge parts compare to atmosphere. Convert volts to kPa using the datasheet transfer function.
  • The input reads random HIGH and LOW when the button is not pressed. Pushbutton
    A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW.
  • The button seems always pressed, or never changes. Pushbutton
    A 4-leg tactile switch has legs joined in pairs. Place it across the breadboard center gap so the pairs sit on different rows. Wired along one side, the legs are already connected.
  • The buzzer makes a constant tone, or you cannot change its pitch. Buzzer
    An active buzzer has its own oscillator, so it only beeps on and off. A passive buzzer needs tone() to set pitch. Check which type you have.
  • The buzzer is silent or very quiet. Buzzer
    Check polarity: the plus mark goes to the pin or supply. For loud buzzers, drive them through a transistor instead of straight from a pin.

Change it and keep it

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