Built-in project · Arduino Uno R3

EGT gauge with peak hold and alarm on an Arduino Uno

Exhaust gas temperature from a K-type thermocouple on a MAX6675, in F and C on a 16x2 LCD with a bar graph, a resettable peak, a 1,400 F demo alarm with hysteresis, and SENSOR OPEN when the probe breaks. The whole build, an Arduino Uno and 6 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 $39. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Sensors
  • Displays
  • Lights
  • Sound
  • Reading a button
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 8

How it works

Exhaust gas temperature from a K-type thermocouple on a MAX6675, in F and C on a 16x2 LCD with a bar graph, a resettable peak, a 1,400 F demo alarm with hysteresis, and SENSOR OPEN when the probe breaks.

Arduino Uno R3 runs the sketch below. The circuit around it: Thermocouple converter, MAX6675, Pushbutton, Resistor, 220 Ω, LED, red, Buzzer, Character LCD with I2C backpack. pinMode sets CS_PIN as an output. digitalWrite drives CS_PIN LOW. digitalRead reads whether BUTTON_PIN is HIGH or LOW. delay waits 250 milliseconds. Serial.begin opens the serial port at 9600 baud.

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
Thermocouple converter, MAX6675 pin SCKArduino Uno R3 pin 13
Thermocouple converter, MAX6675 pin SOArduino Uno R3 pin 12
Thermocouple converter, MAX6675 pin CSArduino Uno R3 pin 10
Buzzer pin 1Arduino Uno R3 pin 6
Resistor, 220 Ω pin 1Arduino Uno R3 pin 5
Pushbutton pin 1aArduino Uno R3 pin 2
Thermocouple converter, MAX6675 pin VCCArduino Uno R3 pin 5V
Character LCD with I2C backpack pin VCCArduino Uno R3 pin 5V
Thermocouple converter, MAX6675 pin GNDArduino Uno R3 pin GND
Pushbutton pin 2aArduino Uno R3 pin GND
LED, red pin CArduino Uno R3 pin GND
Buzzer pin 2Arduino Uno R3 pin GND
Character LCD with I2C backpack pin GNDArduino Uno R3 pin GND
Character LCD with I2C backpack pin SDAArduino Uno R3 pin A4
Character LCD with I2C backpack pin SCLArduino Uno R3 pin A5
LED, red pin AResistor, 220 Ω pin 2

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
// EGT gauge with peak hold and alarm: exhaust gas temperature from a K-type
// thermocouple on a MAX6675, in Fahrenheit and Celsius on a 16x2 I2C LCD,
// with a bar graph, a peak-hold value, a warning and an alarm.
//
//   MAX6675   SCK -> 13 (SCK)   SO -> 12 (MISO)   CS -> 10   VCC -> 5 V
//   LCD       SDA -> A4         SCL -> A5         VCC -> 5 V
//   button    pin 2 to ground (the chip's own pull-up): resets the peak
//   LED       pin 5, through 220 ohms
//   buzzer    pin 6 (an active 5 V buzzer)
//
// 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). On the bench use a bench supply
// or a 12 V battery with a fuse. In a real vehicle the Uno needs a proper
// automotive-rated front end: a fused feed, a regulator that survives load
// dump, TVS clamps, and a divider or level shift for anything at 12 V; 12 V
// never goes straight onto a 5 V logic pin.
//
// SPI. The MAX6675 talks SPI: pull CS low and it puts 16 bits on SO, one per
// clock, most significant first. It only talks, so MOSI (pin 11) is not
// wired. Raising CS starts a conversion that takes up to 0.22 s, and reading
// sooner just returns the last one, so the sketch reads every 250 ms.
//
//   bit 15        always 0
//   bits 14 to 3  the temperature, 0 to 1023.75 C, 0.25 C a count
//   bit 2         1 when the thermocouple is open (a broken or loose wire)
//
// COLD JUNCTION. A thermocouple measures the difference between its hot tip
// and the cold end where its wires meet the copper of the board. The chip
// measures its own temperature there and adds it back in: that is cold
// junction compensation, and why the reading stays right as the board warms.
// It only works if the chip and its terminals are at the same temperature
// and inside the chip's rating (up to 85 C), so mount the amplifier in the
// cabin, well away from the manifold, and run K-type extension wire to it.
// Ordinary copper wire in between makes new junctions and a wrong reading.
//
// THE PROBE. Use a sheathed EGT probe (a stainless or Inconel sheath and a
// compression fitting in a bung welded to the pipe). A bare bead dies quickly
// in exhaust gas, and a broken piece ahead of a turbo can wreck it.
//
// THE LIMIT IS AN EXAMPLE. There is no universal safe EGT. Pre-turbo on a
// gasoline engine people often quote about 1,500 F (815 C) for sustained
// load, and diesel guidance differs again (and depends on where the probe
// sits). This demo warns at 1,300 F and alarms at 1,400 F (760 C). Use the
// number your engine builder or turbo maker gives you.
//
// The alarm has hysteresis: it trips at 1,400 F but clears only below
// 1,350 F, so a reading that hovers at the line does not chatter the buzzer.
// The warning works the same way, 1,300 F on and below 1,250 F off.
//
// Things to try. Drag the thermocouple slider past 760 C and back down: the
// peak stays until you press the button. Then open the board's properties
// and untick probe: the gauge shows SENSOR OPEN, never a made-up number.

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

const int CS_PIN = 10;
const int BUTTON_PIN = 2;
const int LED_PIN = 5;
const int BUZZER_PIN = 6;

// Example limits in whole degrees F. Change them for your engine.
const long WARN_F = 1300;
const long ALARM_F = 1400;
const long HYSTERESIS_F = 50;
// The bar graph's full scale.
const long BAR_FULL_F = 1800;
const int BAR_CELLS = 9;

const unsigned long READ_MS = 250;
const unsigned long DEBOUNCE_MS = 30;

LiquidCrystal_I2C lcd(0x27, 16, 2);

// One to four lit columns of a character cell, for the bar graph. A full
// cell is the display's own character 0xFF.
const uint8_t BAR_ROWS[4] = {0x10, 0x18, 0x1C, 0x1E};

enum Level { LEVEL_OK, LEVEL_WARN, LEVEL_ALARM };

Level level = LEVEL_OK;
bool havePeak = false;
long peakQ = 0;  // the highest good reading, in quarter degrees C
unsigned long lastRead = 0;
bool beep = false;

bool buttonDown = false;
bool buttonSeen = false;
unsigned long buttonChanged = 0;

// One 16-bit read of the MAX6675.
uint16_t readMax6675() {
  SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
  digitalWrite(CS_PIN, LOW);
  uint16_t v = SPI.transfer(0x00);
  v = (v << 8) | SPI.transfer(0x00);
  digitalWrite(CS_PIN, HIGH);
  SPI.endTransaction();
  return v;
}

// Quarter degrees C to whole degrees F, rounded: F = C * 9 / 5 + 32.
long toF(long q) {
  long n = q * 9;
  return (n >= 0 ? n + 10 : n - 10) / 20 + 32;
}

// Quarter degrees C to whole degrees C, rounded.
long toC(long q) {
  return (q >= 0 ? q + 2 : q - 2) / 4;
}

// A number right-aligned in a field, on the LCD or on Serial.
void printPadded(Print &out, long v, int width) {
  int digits = 1;
  for (long t = v < 0 ? -v : v; t >= 10; t /= 10) digits++;
  if (v < 0) digits++;
  for (int i = digits; i < width; i++) out.print(' ');
  out.print(v);
}

void drawBar(long f) {
  long steps = f <= 0 ? 0 : f * (BAR_CELLS * 5) / BAR_FULL_F;
  if (steps > BAR_CELLS * 5) steps = BAR_CELLS * 5;
  lcd.setCursor(0, 1);
  for (int cell = 0; cell < BAR_CELLS; cell++) {
    long lit = steps - cell * 5;
    if (lit >= 5) lcd.write((uint8_t)0xFF);
    else if (lit >= 1) lcd.write((uint8_t)lit);
    else lcd.print(' ');
  }
}

void drawPeak() {
  lcd.setCursor(BAR_CELLS, 1);
  lcd.print(" P");
  if (havePeak) {
    printPadded(lcd, toF(peakQ), 4);
    lcd.print('F');
  } else {
    lcd.print("----F");
  }
}

// The button: a press resets the peak to the reading now.
void checkButton() {
  bool down = digitalRead(BUTTON_PIN) == LOW;
  unsigned long now = millis();
  if (down != buttonSeen) {
    buttonSeen = down;
    buttonChanged = now;
  }
  if (now - buttonChanged >= DEBOUNCE_MS && down != buttonDown) {
    buttonDown = down;
    if (down) {
      havePeak = false;
      Serial.println("Peak reset");
      lastRead = now - READ_MS;  // read again straight away
    }
  }
}

void setup() {
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  SPI.begin();
  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  for (int i = 0; i < 4; i++) {
    uint8_t rows[8];
    for (int r = 0; r < 8; r++) rows[r] = BAR_ROWS[i];
    lcd.createChar(i + 1, rows);
  }
  lcd.print("EGT gauge");
  Serial.println("EGT gauge: warn 1300 F, alarm 1400 F (example limits)");
  // Let the chip finish its first conversion.
  delay(250);
  lastRead = millis() - READ_MS;
}

void loop() {
  checkButton();
  unsigned long now = millis();
  if (now - lastRead < READ_MS) return;
  lastRead = now;

  uint16_t v = readMax6675();
  bool open = v & 0x0004;

  if (open) {
    // The open bit: show it, never a made-up temperature.
    level = LEVEL_OK;
    beep = !beep;
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(LED_PIN, beep ? HIGH : LOW);  // a slow blink: the gauge is blind
    lcd.setCursor(0, 0);
    lcd.print("SENSOR OPEN     ");
    lcd.setCursor(0, 1);
    for (int i = 0; i < BAR_CELLS; i++) lcd.print(' ');
    drawPeak();
    Serial.print("SENSOR OPEN");
  } else {
    long q = (v >> 3) & 0x0FFF;  // quarter degrees C
    long f = toF(q);
    if (!havePeak || q > peakQ) {
      peakQ = q;
      havePeak = true;
    }

    // Warning and alarm, each with the same hysteresis band below its limit.
    if (f >= ALARM_F) level = LEVEL_ALARM;
    else if (level == LEVEL_ALARM && f >= ALARM_F - HYSTERESIS_F) level = LEVEL_ALARM;
    else if (f >= WARN_F) level = LEVEL_WARN;
    else if (level != LEVEL_OK && f >= WARN_F - HYSTERESIS_F) level = LEVEL_WARN;
    else level = LEVEL_OK;

    beep = level == LEVEL_ALARM ? !beep : false;
    digitalWrite(LED_PIN, level == LEVEL_OK ? LOW : HIGH);
    digitalWrite(BUZZER_PIN, beep ? HIGH : LOW);

    lcd.setCursor(0, 0);
    printPadded(lcd, f, 4);
    lcd.print("F ");
    printPadded(lcd, toC(q), 4);
    lcd.print("C ");
    lcd.print(level == LEVEL_ALARM ? "HIGH" : level == LEVEL_WARN ? "WARN" : "    ");
    drawBar(f);
    drawPeak();

    Serial.print("EGT ");
    Serial.print(f);
    Serial.print(" F ");
    Serial.print(toC(q));
    Serial.print(" C");
  }

  Serial.print("  peak ");
  if (havePeak) {
    Serial.print(toF(peakQ));
    Serial.print(" F");
  } else {
    Serial.print("--");
  }
  Serial.print("  ");
  Serial.println(open ? "FAULT" : level == LEVEL_ALARM ? "ALARM" : level == LEVEL_WARN ? "WARN" : "OK");
}

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(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  SPI.begin();
  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  for (int i = 0; i < 4; i++) {
    uint8_t rows[8];
    for (int r = 0; r < 8; r++) rows[r] = BAR_ROWS[i];
    lcd.createChar(i + 1, rows);
  }
  lcd.print("EGT gauge");
  Serial.println("EGT gauge: warn 1300 F, alarm 1400 F (example limits)");
  // Let the chip finish its first conversion.
  delay(250);
  lastRead = millis() - READ_MS;
}

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() {
  checkButton();
  unsigned long now = millis();
  if (now - lastRead < READ_MS) return;
  lastRead = now;

  uint16_t v = readMax6675();
  bool open = v & 0x0004;

  if (open) {
    // The open bit: show it, never a made-up temperature.
    level = LEVEL_OK;
    beep = !beep;
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(LED_PIN, beep ? HIGH : LOW);  // a slow blink: the gauge is blind
    lcd.setCursor(0, 0);
    lcd.print("SENSOR OPEN     ");
    lcd.setCursor(0, 1);
    for (int i = 0; i < BAR_CELLS; i++) lcd.print(' ');
    drawPeak();
    Serial.print("SENSOR OPEN");
  } else {
    long q = (v >> 3) & 0x0FFF;  // quarter degrees C
    long f = toF(q);
    if (!havePeak || q > peakQ) {
      peakQ = q;
      havePeak = true;
    }
  // ... 38 more lines in the editor

Troubleshooting

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

  • The temperature reads 0 or too low or too high. Thermocouple converter
    Check the thermocouple polarity. If the reading falls when heated, swap the two leads. The MAX6675 works with K-type thermocouples only.
  • Readings repeat or look stuck. Thermocouple converter
    A conversion takes about 220 ms, so read no faster than 4 times a second. The pins are SCK, CS and SO only. Keep the chip away from heat, since it measures its own cold junction.
  • 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 LED is dim, burns out, or the pin reads wrong because the resistor value is off. Resistor
    Read the color bands again. A 220 ohm to 330 ohm resistor suits a typical LED on 5 V. 10k is a common pull-up or pull-down value. Check with a multimeter if unsure.
  • A pull-up or pull-down resistor is missing, so the input floats and reads random values. Resistor
    Tie the input to 5 V or ground through about 10k. Or use INPUT_PULLUP and wire the switch to ground.
  • The LED does not light, or it lights once and then dies. LED
    LEDs have polarity. The longer leg (anode) goes toward the positive side. Always add a series resistor, about 220 ohms to 1 k ohm on a 5 V pin.
  • A blue or white LED stays dark on 3.3 V. LED
    Blue and white LEDs drop about 3 V, so 3.3 V leaves almost nothing for the resistor. Use a 5 V supply, or a smaller resistor and a red or green LED.

Change it and keep it

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