Built-in project · Arduino Uno R3

Generator transfer switch on an Arduino Uno

The control logic of an automatic transfer switch with low-voltage stand-ins: two interlocked contactors that never both close, a dead time between break and make, and the outage, start, transfer, return and cool-down sequence. A teaching model, never for real wiring. The whole build, an Arduino Uno and 22 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.

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

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

The circuit runs here once the page has loaded.

What you'll learn

  • Controllers
  • School
  • Reading a button
  • Choosing and reading a resistor
  • How an LED lights and why it needs a resistor
  • Difficulty: Advanced
  • Time: About 60 minutes
  • Board: Arduino Uno R3
  • Parts: 29

How it works

The control logic of an automatic transfer switch with low-voltage stand-ins: two interlocked contactors that never both close, a dead time between break and make, and the outage, start, transfer, return and cool-down sequence. A teaching model, never for real wiring.

Arduino Uno R3 runs the sketch below. The circuit around it: Pushbutton, Resistor, 220 Ω, LED, green, Resistor, 1k Ω, Transistor (NPN or PNP), Resistor, 10k Ω. pinMode sets PIN_READY as an input. digitalWrite drives PIN_UTIL LOW. digitalRead reads whether PIN_AUTO is HIGH or LOW. Serial.begin opens the serial port at 115200 baud. millis reads the time since power-up, so the sketch can time things without stopping.

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
Pushbutton pin 2aArduino Uno R3 pin GND
LED, green (1) pin CArduino Uno R3 pin GND
Transistor (NPN or PNP) (1) pin EArduino Uno R3 pin GND
Transistor (NPN or PNP) (2) pin EArduino Uno R3 pin GND
Resistor, 4.7k Ω pin 2Arduino Uno R3 pin GND
Slide switch (1) pin 1Arduino Uno R3 pin GND
Slide switch (2) pin 2Arduino Uno R3 pin GND
Slide switch (3) pin 2Arduino Uno R3 pin GND
Pushbutton pin 1aArduino Uno R3 pin 9
Resistor, 220 Ω pin 1Arduino Uno R3 pin 8
Resistor, 1k Ω (2) pin 1Arduino Uno R3 pin 7
Resistor, 1k Ω (1) pin 1Arduino Uno R3 pin 6
Slide switch (3) pin 3Arduino Uno R3 pin 5
Slide switch (2) pin 3Arduino Uno R3 pin 4
Resistor, 10k Ω pin 2Arduino Uno R3 pin 3
Resistor, 4.7k Ω pin 1Arduino Uno R3 pin 3
Slide switch (1) pin 2Arduino Uno R3 pin 2
Slide switch (1) pin 3Arduino Uno R3 pin 5V
LED, green (1) pin AResistor, 220 Ω pin 2
Transistor (NPN or PNP) (1) pin BResistor, 1k Ω (1) pin 2
DPDT relay (1) pin COIL2Transistor (NPN or PNP) (1) pin C
Transistor (NPN or PNP) (2) pin BResistor, 1k Ω (2) pin 2
DPDT relay (2) pin COIL2Transistor (NPN or PNP) (2) pin C
DPDT relay (1) pin NO2Resistor, 10k Ω pin 1
Slide switch (4) pin 2Resistor, 10k Ω pin 1
DPDT relay (2) pin NC1DPDT relay (1) pin COIL1
DPDT relay (1) pin COM15 V
Resistor, 330 Ω (1) pin 2DPDT relay (1) pin NO1
DPDT relay (2) pin COIL1DPDT relay (1) pin NC1
DPDT relay (2) pin COM2DPDT relay (1) pin COM2
Filament lamp pin 1DPDT relay (1) pin COM2
Resistor, 100k Ω (1) pin 1DPDT relay (1) pin NC2
DPDT relay (2) pin COM15 V
Resistor, 330 Ω (2) pin 2DPDT relay (2) pin NO1
DPDT relay (2) pin NO29 V
Resistor, 100k Ω (2) pin 1DPDT relay (2) pin NC2
Slide switch (4) pin 112 V
Filament lamp pin 2Ground
LED, green (2) pin AResistor, 330 Ω (1) pin 1
LED, green (2) pin CGround
LED, yellow pin CGround
Resistor, 100k Ω (1) pin 2Ground
Resistor, 100k Ω (2) pin 2Ground
LED, yellow pin AResistor, 330 Ω (2) pin 1

The note on the canvas

Word for word, as it sits beside the circuit in the editor.

Try it
1. Press Run and flip AUTO on. After the 2 s dead time the UTILITY contactor closes and the lamp runs on 12 V.
2. Cut the utility. After 5 s the START LED comes on. Flip generator ready: 3 s later the utility contactor opens, 2 s after that the generator closes (9 V).
3. Restore the utility: 10 s later it transfers back, then stops the generator after a cool-down.
4. Hold the bench fault button: both outputs go high, but the wiring keeps only one contactor closed.

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
// Generator transfer switch: the control logic of an automatic transfer
// switch (ATS), modeled with low-voltage stand-ins. Two contactors, UTILITY
// and GENERATOR, feed one load and may NEVER both be closed.
//
// SAFETY. This is a teaching model. Real transfer switches are listed
// equipment, installed by a licensed electrician to the code. A generator
// that is back-fed into a building's wiring, or into a panel, without a
// proper transfer switch energizes the utility lines outside and kills line
// workers. Nothing here is for connecting to a building, a panel or a real
// generator.
//
//   D2   generator ready, in: high from a slide switch (the engine's "running
//        and voltage OK" contact in a real unit)
//   D3   utility present, in: the 12 V utility stand-in through a 10k / 4.7k
//        divider, about 3.8 V when the utility is up and 0 V when it is out
//        (a real unit uses a voltage-sensing relay or module, such as a
//        ZMPT101B on a small AC source)
//   D4   AUTO selector, in, pulled up, low when the AUTO switch is on
//   D5   GEN selector, in, pulled up, low when the GEN switch is on
//        (neither on: OFF. Both on is not a position a real selector has, so
//        it is treated as OFF.)
//   D6   UTILITY contactor, out, through a 1k to an NPN that pulls the coil
//   D7   GENERATOR contactor, out, the same
//   D8   generator START, out, an LED here (a dry contact to the engine)
//   D9   bench fault button, in, pulled up: see HARDWARE LOCKOUT
//
// HARDWARE LOCKOUT. Each coil gets its 5 V through the OTHER contactor's
// normally closed contact. While the generator contactor is pulled in, the
// utility coil has no supply however the pins are driven, and the same the
// other way. So even if this code failed and drove both outputs high, the
// second contactor could not pull in. Real units add a mechanical interlock
// too. Limits of the wiring: if both coils are energized at the very same
// moment from both released, both can start to pull in, which is one more
// reason the code leaves a dead time and why a real mechanical interlock
// matters. Hold the bench fault button to drive both outputs on and watch
// the hardware keep only one closed. The button is a bench demonstration of a
// firmware fault, not a feature.
//
// BREAK BEFORE MAKE. A contactor is opened, then nothing closes for DEAD_MS,
// then the other one closes. The dead time is also applied at power on.
//
// AUTO SEQUENCE (times scaled down so a run takes seconds; real units use
// adjustable timers, minutes for the retransfer and the cool-down):
//   LOST_MS          5000  utility must stay out this long before starting
//   READY_STABLE_MS  3000  generator ready for this long before transfer
//   START_TIMEOUT_MS 15000 no ready by then: release start, lock out
//   RETURN_MS        10000 utility back and stable this long before retransfer
//   COOL_MS          5000  generator keeps running unloaded after retransfer
//   DEAD_MS          2000  both contactors open between a break and a make
// MANUAL: OFF opens everything and stops the generator, GEN starts it and
// transfers once it is stable, whatever the utility does.

const int PIN_READY = 2;
const int PIN_UTIL_SENSE = 3;
const int PIN_AUTO = 4;
const int PIN_GENSEL = 5;
const int PIN_UTIL = 6;
const int PIN_GEN = 7;
const int PIN_START = 8;
const int PIN_FAULT = 9;

const unsigned long DEAD_MS = 2000;
const unsigned long LOST_MS = 5000;
const unsigned long READY_STABLE_MS = 3000;
const unsigned long START_TIMEOUT_MS = 15000;
const unsigned long RETURN_MS = 10000;
const unsigned long COOL_MS = 5000;
const unsigned long STATUS_MS = 5000;

enum Source { NONE, UTIL, GEN };
enum State { S_OFF, S_UTIL, S_LOST, S_START, S_GEN, S_COOL, S_FAIL };
enum Mode { M_OFF, M_AUTO, M_GEN };

const char *const SOURCE_NAME[3] = {"NONE", "UTILITY", "GENERATOR"};
const char *const STATE_NAME[7] = {"OFF", "ON UTILITY", "UTILITY LOST", "STARTING", "ON GENERATOR", "COOL-DOWN", "GENERATOR FAULT"};
const char *const MODE_NAME[3] = {"OFF", "AUTO", "GEN"};

Source closed = NONE;       // which contactor the outputs hold closed
Source want = NONE;         // which one the control wants
unsigned long openedAt = 0; // when a contactor last opened
State st = S_OFF;
Mode mode = M_OFF;
unsigned long stateAt = 0;
bool genStart = false;
bool coolRunning = false;
unsigned long coolAt = 0;
bool utilPresent = false;
unsigned long utilSince = 0;
bool genReady = false;
unsigned long genSince = 0;
bool forcing = false;
unsigned long lastStatus = 0;
unsigned long loopNow = 0;  // the time this pass of loop() works with, so no timer reads ahead of it

void stamp() {
  unsigned long t = millis() / 100;
  Serial.print("[");
  Serial.print(t / 10);
  Serial.print('.');
  Serial.print(t % 10);
  Serial.print(" s] ");
}

void say(const char *msg) {
  stamp();
  Serial.println(msg);
}

void enter(State next, const char *msg) {
  st = next;
  stateAt = loopNow;
  coolRunning = false;
  stamp();
  Serial.print(STATE_NAME[next]);
  Serial.print(": ");
  Serial.println(msg);
}

void setStart(bool on) {
  if (on == genStart) return;
  genStart = on;
  digitalWrite(PIN_START, on ? HIGH : LOW);
  say(on ? "generator START requested" : "generator START released, engine stops");
}

Mode readMode() {
  bool a = digitalRead(PIN_AUTO) == LOW;
  bool g = digitalRead(PIN_GENSEL) == LOW;
  if (a && !g) return M_AUTO;
  if (g && !a) return M_GEN;
  return M_OFF;
}

void changeMode(Mode m, unsigned long now) {
  mode = m;
  stamp();
  Serial.print("selector: ");
  Serial.println(MODE_NAME[m]);
  if (m == M_OFF) {
    enter(S_OFF, "contactors open, generator stopping");
  } else if (m == M_GEN) {
    if (closed == GEN && genReady) {
      enter(S_GEN, "manual generator");
    } else {
      setStart(true);
      enter(S_START, "manual generator, waiting for ready");
    }
  } else if (closed == GEN) {
    enter(S_GEN, "auto, carrying the load");
  } else if (genStart) {
    if (utilPresent) enter(S_COOL, "auto, generator running unloaded");
    else enter(S_START, "auto, waiting for generator ready");
  } else {
    if (utilPresent) enter(S_UTIL, "auto, utility present");
    else enter(S_LOST, "auto, utility is out");
  }
  (void)now;
}

// The control: decides which source it wants and when to start the engine.
void control(unsigned long now) {
  bool utilStable = utilPresent && now - utilSince >= RETURN_MS;
  bool genStable = genReady && now - genSince >= READY_STABLE_MS;
  switch (st) {
    case S_OFF:
      want = NONE;
      if (closed == NONE) setStart(false);
      break;
    case S_UTIL:
      want = UTIL;
      if (!utilPresent) enter(S_LOST, "utility lost, waiting 5 s before starting");
      else if (genStart) enter(S_COOL, "generator still running, cooling down");
      break;
    case S_LOST:
      want = UTIL;
      if (utilPresent) {
        enter(S_UTIL, "utility back before the timer ended, no start");
      } else if (now - stateAt >= LOST_MS) {
        setStart(true);
        enter(S_START, "utility out 5 s, start generator");
      }
      break;
    case S_START:
      want = UTIL;
      if (mode == M_AUTO && utilStable) {
        enter(S_COOL, "utility stable 10 s, stop generator after cool-down");
      } else if (genStable) {
        enter(S_GEN, "generator ready and stable 3 s, transfer to generator");
      } else if (now - stateAt >= START_TIMEOUT_MS) {
        setStart(false);
        enter(S_FAIL, "GENERATOR FAIL: not ready in 15 s, start released, lockout");
      }
      break;
    case S_GEN:
      want = GEN;
      if (!genReady) {
        setStart(false);
        enter(S_FAIL, "GENERATOR FAIL: ready signal lost, contactor opens");
      } else if (mode == M_AUTO && utilStable) {
        enter(S_COOL, "utility stable 10 s, transfer back to utility");
      }
      break;
    case S_COOL:
      want = UTIL;
      if (mode == M_AUTO && !utilPresent) {
        enter(S_LOST, "utility lost again, waiting 5 s");
      } else if (closed == UTIL) {
        if (!coolRunning) {
          coolRunning = true;
          coolAt = now;
        } else if (now - coolAt >= COOL_MS) {
          setStart(false);
          enter(S_UTIL, "cool-down done, generator stopped");
        }
      }
      break;
    case S_FAIL:
      want = NONE;
      setStart(false);
      if (mode == M_AUTO && utilStable) enter(S_UTIL, "utility stable, fault cleared");
      break;
  }
}

// The interlock in code: one contactor opens, then DEAD_MS passes, then the
// other closes. The only place the outputs are written.
void drive(unsigned long now) {
  if (closed != want) {
    if (closed != NONE) {
      stamp();
      Serial.print(SOURCE_NAME[closed]);
      Serial.println(" contactor OPEN");
      closed = NONE;
      openedAt = now;
    } else if (now - openedAt >= DEAD_MS) {
      closed = want;
      stamp();
      Serial.print(SOURCE_NAME[closed]);
      Serial.println(" contactor CLOSED");
    }
  }
  bool fault = digitalRead(PIN_FAULT) == LOW;
  if (fault != forcing) {
    forcing = fault;
    say(fault ? "BENCH FAULT: both outputs forced on, the hardware lockout must hold" : "bench fault released");
  }
  digitalWrite(PIN_UTIL, (closed == UTIL || forcing) ? HIGH : LOW);
  digitalWrite(PIN_GEN, (closed == GEN || forcing) ? HIGH : LOW);
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_READY, INPUT);
  pinMode(PIN_UTIL_SENSE, INPUT);
  pinMode(PIN_AUTO, INPUT_PULLUP);
  pinMode(PIN_GENSEL, INPUT_PULLUP);
  pinMode(PIN_FAULT, INPUT_PULLUP);
  pinMode(PIN_UTIL, OUTPUT);
  pinMode(PIN_GEN, OUTPUT);
  pinMode(PIN_START, OUTPUT);
  digitalWrite(PIN_UTIL, LOW);
  digitalWrite(PIN_GEN, LOW);
  digitalWrite(PIN_START, LOW);
  Serial.println("Mokxi Arduino Uno: generator transfer switch (control-logic model, low-voltage stand-ins)");
  Serial.println("Teaching model only: real transfer switches are listed equipment installed by a licensed electrician.");
  utilPresent = digitalRead(PIN_UTIL_SENSE) == HIGH;
  genReady = digitalRead(PIN_READY) == HIGH;
  loopNow = millis();
  utilSince = loopNow;
  genSince = loopNow;
  openedAt = loopNow; // the dead time applies at power on too
  changeMode(readMode(), loopNow);
}

void loop() {
  unsigned long now = millis();
  loopNow = now;

  bool u = digitalRead(PIN_UTIL_SENSE) == HIGH;
  if (u != utilPresent) {
    utilPresent = u;
    utilSince = now;
    say(u ? "utility PRESENT" : "utility LOST");
  }
  bool r = digitalRead(PIN_READY) == HIGH;
  if (r != genReady) {
    genReady = r;
    genSince = now;
    say(r ? "generator READY" : "generator not ready");
  }
  Mode m = readMode();
  if (m != mode) changeMode(m, now);

  control(now);
  drive(now);

  if (now - lastStatus >= STATUS_MS) {
    lastStatus = now;
    stamp();
    Serial.print("status: ");
    Serial.print(MODE_NAME[mode]);
    Serial.print(" | ");
    Serial.print(STATE_NAME[st]);
    Serial.print(" | closed ");
    Serial.println(SOURCE_NAME[closed]);
  }
}

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() {
  Serial.begin(115200);
  pinMode(PIN_READY, INPUT);
  pinMode(PIN_UTIL_SENSE, INPUT);
  pinMode(PIN_AUTO, INPUT_PULLUP);
  pinMode(PIN_GENSEL, INPUT_PULLUP);
  pinMode(PIN_FAULT, INPUT_PULLUP);
  pinMode(PIN_UTIL, OUTPUT);
  pinMode(PIN_GEN, OUTPUT);
  pinMode(PIN_START, OUTPUT);
  digitalWrite(PIN_UTIL, LOW);
  digitalWrite(PIN_GEN, LOW);
  digitalWrite(PIN_START, LOW);
  Serial.println("Mokxi Arduino Uno: generator transfer switch (control-logic model, low-voltage stand-ins)");
  Serial.println("Teaching model only: real transfer switches are listed equipment installed by a licensed electrician.");
  utilPresent = digitalRead(PIN_UTIL_SENSE) == HIGH;
  genReady = digitalRead(PIN_READY) == HIGH;
  loopNow = millis();
  utilSince = loopNow;
  genSince = loopNow;
  openedAt = loopNow; // the dead time applies at power on too
  changeMode(readMode(), loopNow);
}

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() {
  unsigned long now = millis();
  loopNow = now;

  bool u = digitalRead(PIN_UTIL_SENSE) == HIGH;
  if (u != utilPresent) {
    utilPresent = u;
    utilSince = now;
    say(u ? "utility PRESENT" : "utility LOST");
  }
  bool r = digitalRead(PIN_READY) == HIGH;
  if (r != genReady) {
    genReady = r;
    genSince = now;
    say(r ? "generator READY" : "generator not ready");
  }
  Mode m = readMode();
  if (m != mode) changeMode(m, now);

  control(now);
  drive(now);

  if (now - lastStatus >= STATUS_MS) {
    lastStatus = now;
    stamp();
    Serial.print("status: ");
    Serial.print(MODE_NAME[mode]);
    Serial.print(" | ");
  // ... 5 more lines in the editor

Troubleshooting

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

  • 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.
  • The load never turns on, or is always on, because the legs are in the wrong order. Transistor (NPN or PNP)
    Check the pinout in the datasheet for your part. A 2N2222 or BC547 can have emitter, base, collector in a different order. Flat side orientation is easy to get wrong.
  • The base is wired straight to the pin with no resistor, or the motor has no flyback diode. Transistor (NPN or PNP)
    Put about 1k between the pin and the base. For motors and relays, add a diode across the coil with the stripe toward the positive side.

Change it and keep it

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