Previously I looked at the hardware needed to build a Raspberry Pi soft power supply. This week I’m looking the state machine for the microcontroller. Why is such a complicated circuit necessary? I am replacing a Super Famicom (SNES) motherboard with the Pi. The trick is, I want to use the original power switch to turn the Pi on and off. This requirement presents a problem. When the switch goes into the “OFF” position, power needs to stay on long enough for the Pi to properly shutdown. So the switch itself can’t provide power to the Pi directly. With minor changes, the code in this state machine could be made to work with push buttons as well. If I add that feature in the future, I’ll update the code on the RPSPC GitHub project.
Before continuing with the state machine, first I need to thank all the mailing list members. You guys really rock. When I asked for state machine diagraming tool suggestions, you guys sent me enough options for an entire (future) post to compare them. 
Regarding hardware requirements, this program should work fine on most 8-bit Arduino or AVR boards. You just need enough pins for your project. The 32u4 in my custom controller is the same AVR chip used on the Arduino Leonardo. The RPSPC doesn’t use any special features of the 32u4. If the AVR is only controlling the Pi’s power, you might be able to get away with just an ATTiny.

The Code

Here’s the code as of the time I write this article. I would recommend you check the GitHub repository for the latest. I won’t update this post as the code changes.

//retropie-pwr-cntrl-v4.ino
// This code expects a SPST slide switch
// For more information, visit baldengineer.com and search "SNES"

// Arduino Leonardo Pin Names
// Constants
const byte loadEnable = A1; // The Pi!
const byte offSignalPath = A3; //PF4 Verify: no additional Z, adc 4 on 32u4
const byte switchSense = A2;    //PF5
const byte signalToPi = A5;// PF0 (is HeartBeat on Pi)
const byte signalFromPi = 3;  //PD0 is SCL

const byte heartBeatLED = 6;  
const byte frontLED = A4;       // PF1 

// Extra Pins
const byte debugLED = 12;
const byte extraA1 = A1;  // PF6, Mine: A4 - Actually A1
const byte switchLED = A0; // PD6 (debug signal)

#define enableON HIGH
#define enableOFF LOW

// Timers
unsigned long previousOFFSignalCount = millis();
unsigned long previousOFFSignalInterval = 1000;

unsigned long heartBeatPreviousMillis = millis();
unsigned long heartBeatInterval = 500;
bool heartBeatState = true;

unsigned long timerOffPreviousMillis = millis();
unsigned long timerOffInterval = 15000; 

unsigned long capDrainPreviousMillis = millis();
unsigned long capDrainInterval = 1000; 

unsigned long forcePowerOff = millis();
unsigned long forcePowerOffInterval = 45000UL; 
bool forcePowerOffState = false;

bool currentButtonState;
bool previousButtonState;

bool currentPiSignalState;
bool previousPiSignalState;

enum controllerStates {
	POWER_UP,
	BOOTING_PI,
	BOOTED,
	SHUT_DOWN_PI,
	POWER_DOWN
};
enum controllerStates controllerState = POWER_UP;
enum controllerStates previousControllerState = controllerState;

void setup() {
	Serial.begin(9600);	// debugging 
	//pinMode(debugLED, OUTPUT);

	pinMode(offSignalPath, INPUT);

	pinMode(heartBeatLED, OUTPUT);
	pinMode(switchSense, INPUT);
	pinMode(signalFromPi, INPUT);

	// Don't want signal to go high until Pi has power
	pinMode(signalToPi, OUTPUT);
	digitalWrite(signalToPi, LOW);
	
	// Not ready to turn on the Pi yet
	pinMode(loadEnable, OUTPUT);
	digitalWrite(loadEnable, enableOFF);
}

void heartBeat(unsigned long millisTime) {
	if (millisTime - heartBeatPreviousMillis >= heartBeatInterval) {
		heartBeatState = !heartBeatState;
		digitalWrite(heartBeatLED, heartBeatState);
		heartBeatPreviousMillis = millisTime;	
	}
}

void stateDebug() {
	if (previousControllerState != controllerState) {
		Serial.println(controllerState);
		previousControllerState = controllerState;
	}
}

void loop() {
	// blink the LED
	heartBeat(millis());

	// check where the Pi is at
	currentPiSignalState = digitalRead(signalFromPi);
	if (previousPiSignalState != currentPiSignalState) {
		// in case has some noise wait and re-sample
		delay(10);
		currentPiSignalState = digitalRead(signalFromPi);
		previousPiSignalState = currentPiSignalState;
	} 

	// how is the button doing?
	currentButtonState = digitalRead(switchSense);
	if (previousButtonState != currentButtonState) {
		// in case of bounce, wait and re-sample
		delay(20);
		currentButtonState = digitalRead(switchSense);
		previousButtonState = currentButtonState;

		// let the Pi know if it should be on or off
		digitalWrite(signalToPi, currentButtonState);pi

		if (currentButtonState == LOW) {
			// user is forcing shutdown, so start emergency timer
			//timerOffPreviousMillis = millis(); 
			pinMode(offSignalPath, OUTPUT);
			digitalWrite(offSignalPath, HIGH);
			forcePowerOff = millis();
		} else {
			// Let the switch keep the cap charged
			pinMode(offSignalPath, INPUT);
		}
	}

	switch (controllerState) {
		case POWER_UP:
			stateDebug();
			// disable heart beat 
			heartBeatPreviousMillis = millis();
			heartBeatState = false;

			// should be a no brainer.
			if (currentButtonState) {
				//pwrButtonState = SW_ON;
				controllerState = BOOTING_PI;
			}	
		break;

		case BOOTING_PI:
			//stateDebug();
			heartBeatInterval = 1000;

			// Turn on the Pi
			digitalWrite(loadEnable, enableON);

			// wait for Pi to boot
			if (currentPiSignalState) {
				controllerState = BOOTED;
			}

			// kill the power if the Pi never boot (or we never)
			// get our PiAlive signal.
			if (currentButtonState == LOW) {
				capDrainPreviousMillis = millis();
				digitalWrite(signalToPi, LOW);
				controllerState = POWER_DOWN;
			}
		break;

		case BOOTED:
			//stateDebug();
			heartBeatInterval = 500;

			// wait for Pi signal that it is shutting down
			if (currentPiSignalState == LOW) {
				// Pi Initiated Shutdown
				timerOffPreviousMillis = millis();
				forcePowerOff = millis();
				controllerState = SHUT_DOWN_PI;
			}

			if (currentButtonState == LOW) {
				// User is telling Pi to shut down.
				timerOffPreviousMillis = millis(); // moved to switch change
				forcePowerOff = millis();
				controllerState = SHUT_DOWN_PI;
			} else {
				// let the swtich keep the cap up.
				pinMode(offSignalPath, INPUT);
			}
		break;

		case SHUT_DOWN_PI:
			//stateDebug();
			heartBeatInterval = 250;
			// give the Pi some time to finish up its power-down
			if (currentPiSignalState == LOW) {
				if (millis() - timerOffPreviousMillis >= timerOffInterval) {
					controllerState = POWER_DOWN;
					capDrainPreviousMillis = millis();
				}
			}

			// or a reboot is occured
			if (currentPiSignalState) {
				controllerState = BOOTED;
			}

			// Shutdown took way too long
			if (millis() - forcePowerOff >= forcePowerOffInterval) {
				controllerState = POWER_DOWN;
				capDrainPreviousMillis = millis();
			}
		break;

		case POWER_DOWN:
			//stateDebug();
			heartBeatInterval = 100;

			// let the cap drain once the switch goes off.
			if (currentButtonState == LOW) {
				// turn off the Pi
				digitalWrite(loadEnable, enableOFF);
				if (millis() - capDrainPreviousMillis >= capDrainInterval) {
					// drain the cap
					pinMode(offSignalPath, OUTPUT);
					digitalWrite(offSignalPath, LOW);
				}				
			}
		break;
	}
}

RPSPC GitHub Project

The State Machine

It took me several sessions to get the state machine to work correctly. On the left is the original state machine I developed and on the right is the re-designed code. I asked the mailing list for suggestions on drawing a state diagram. I received many good suggestions, in fact, enough I plan a post on it. In the end, though, I ended up using  “dot”, which is part of GraphViz.

Pins

There are four critical pins for the power controller: loadEnable, offSignalPath, switchSense, signalToPi, and signalFromPi. There are a couple of LEDs defined in the current version of the code: heartBeatLED, frontLED. The heartBeatLED simplifies debugging and is meant to be an onboard indicator. For my RetroPie application, I have a LED on the front panel. I’m not sure yet, but these may have the same behavior.

Extra pins

If you look at the hardware design on GitHub, you’ll see a bunch of pins with SNES in the name. Another function of my power controller is to provide an interface to the SNES controller ports of my RetroPie install.

Constants

I’m using #define for a few text constants. Early on I prototyped with an LDO that has a rare ACTIVE LOW LDO enable pin. Knowing that I was going to be using an LDO with the active HIGH, I used a #define macro to make code changes easier.

Timers

There are multiple uses of millis() and intervals for various functions. In fact, I need to add a few more. If you use a push button with the power controller, you’ll probably want to know if it was a short or long press.

Power States

The controller operations with five power states, each described below. Originally I intended just to refactor my previous version of the state machine. However, I found that I over complicated the states and conditions for changing them. This new code is much smaller, less error-prone, and easier to follow.

POWER_UP

The default state when the AVR turns on is POWER_UP. The state will likely immediately exit since it’s only real work is to advance the machine towards BOOTING_PI.

BOOTING_PI

This state turns on the Pi. It waits until the Pi sends a HIGH signal back. If that never occurs and the on/off switch goes back LOW, then we will jump directly to POWER_DOWN. This escape will likely happen because the Pi doesn’t have software to tell us it’s powered state. So when the switch goes low, we might as well kill the power.

BOOTED

Ideally, this is where the controller will spend most of its time. The Pi sent a signal to let us know it is alive. Now we wait until it sends us a signal to say it is shutting down. Alternatively, the user can shutdown the Pi by putting the slide switch to OFF. The timer “forcePowerOff” is a very long timeout, which is used by the next state.

SHUT_DOWN_PI

There are three if-statements here. The first one is waiting for the Pi’s signal to go LOW. Once it does, at least 15 seconds must pass before advancing states. This delay gives the Pi enough time to finish shutdown and/or reboot. If a reboot occurs, then the 2nd if-statement will eventually become TRUE. As the Pi boots, the state machine gets pushed back to BOOTED. If SHUT_DOWN_PI is entered because of the user-initiated shutdown, the 3rd if-statement gives the Pi 45 seconds to gracefully shutdown. If that fails, then we force a shutdown.

POWER_DOWN

The power controller will stay in this state until the power switch goes LOW. This rule means the Pi will stay on until the switch is turned off. Once the power switch goes LOW, the Pi is disabled, and a short delay occurs. Then the hold-up capacitor on the controller circuit’s enable-pin discharges. During this discharge, the power controller will power itself off.

Next Steps

I’d also like to rework the state machine so that either a push button or slide switch works. Also, for push buttons, we should be able to tell the difference between a long or short press. That way you can do a power-on, reboot or shutdown from the same button (like the PS2.) I’d like to build a serial interface to the microcontroller. This interface would allow a user to modify the timeout delays and behavior of the Pi power. Ideally, I’d like to implement this kind of feature with Serial, I2C, and SPI. The advantage to those 3 is that we can connect to the Pi from the GPIO header. For the latest build notes check out the log on the RPSPC Hackster.io page. Next post I’ll look at the stuff you need to do on the Raspberry Pi to make all of this work!
Author

Fan of making things beep, blink and fly. Created AddOhms. Stream on Twitch. Video Host on element14 Presents and writing for Hackster.IO. Call sign KN6FGY.

2 Comments

    • Yeah, I found this originally when I started doing my research. I found the board to be ridiculously over-designed and complicated to use. But it has two positives: it is very flexible AND it only requires a single GPIO pin.

Write A Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.