/**
Your goal is to write code for a simplified Battery Management System that reads the voltage of a single Lithium-ion battery cell
and enables a relay if the cell is safe to operate. The Arduino will read in the voltage a potentiometer that is acting as the
simulated cell and simulated load is a resistor and LED. There is a display to report out the voltage and state. The RGB LED
should also light up according to the state the Arduino is in. The button is used to transition between states.
Requirements
1) The Arduino shall read a button press for state transition. This button is used to transition from Ready to Active and from Fault to Init. The button should trigger an interrupt, do not poll the button.
2) The Ardunio is to read the voltage in mV across the potentiometer and display it on the LCD. The screen should show the updated voltage every 100ms.
3) The LCD screen shall display the state of the Arduino.
4) The relay is only closed in the “Active” state.
5) The Arduino will protect the cell by going into Fault state if there is an under or over voltage event. If the voltage crosses the over or under voltage limit for the specified duration then it is considered a fault.
a) Overvoltage Limit: voltage > 4.2v for at least 2 seconds
b) Undervoltage Limit: voltage < 1.9v for at least 3 seconds
6) Do not use any delays or blocking code.
7) The RGD LED must correspond to the state of the Arduino.
8) The relay must open within 50mS after a fault condition (over/under voltage for a given duration).
Disclaimers and Simulator Issues:
1) The button doesn't debounce correctly in this simulation so be aware of that.
2) The "%" operation can produce unexpected results sometimes. Avoid it if possible.
Submitting Project
Download the project and send the zip file to the recruiting and hiring manager.
Important:
Be sure to include as many comments as needed to communicate your design intent and strategy.
Even if you don’t finish the project, leaving comments informs the reviewer of what you were trying to accomplish.
**/
// Includes
#include <LiquidCrystal_I2C.h>
// Pin hardware
#define PIN_BUTTON 2U
#define PIN_LED_BLUE 11U
#define PIN_LED_GREEN 12U
#define PIN_LED_RED 13U
#define PIN_RELAY 4U
#define PIN_CELL A0
// Cell limits in millivolts.
#define CELL_UPPER_LIMIT_MV 4200U
#define CELL_LOWER_LIMIT_MV 1900U
// Qualification time for over/undervolage.
#define CELL_UPPER_LIMIT_QUAL_TIME_MS 2000U
#define CELL_LOWER_LIMIT_QUAL_TIME_MS 3000U
// LCD update period in milliseconds.
#define LCD_UPDATE_PERIOD_MS 100U
// Number of millivolts for each count of the ADC.
// This number assumes the ADC measurement is 5V-referenced.
// More info here: https://www.arduino.cc/reference/en/language/functions/analog-io/analogread/
#define MILLIVOLTS_PER_ADC_UNIT 5U
// State of the state machine.
enum State {
STATE_INIT,
STATE_READY,
STATE_ACTIVE,
STATE_FAULT,
};
// Color of the RGB LED.
enum Color {
COLOR_NONE,
COLOR_RED,
COLOR_BLUE,
COLOR_GREEN,
};
// Inputs to the state machine.
struct ModuleInputs {
// Measurement of the cell voltage in millivolts.
uint16_t cellVoltage;
// Was the button just pressed?
bool isButtonPressed;
};
// Persistence state used by the state machine.
struct ModuleState {
// Current state of the state machine.
State currentState;
// The absolute start time in milliseconds that an overvoltage
// condition started. Set to 0 when there is no overvoltage.
uint32_t overVoltageStartTime;
// The absolute start time in milliseconds that an undervoltage
// condition started. Set to 0 when there is no undervoltage.
uint32_t underVoltageStartTime;
// Time in milliseconds that we last updated the LCD display.
uint32_t lastLcdUpdateTime;
// Is there an unhandled button press?
bool isButtonPressPending;
};
static LiquidCrystal_I2C sLcd(0x27, 20, 4);
static struct ModuleState sModuleState = {
.currentState = STATE_INIT,
.overVoltageStartTime = 0,
.underVoltageStartTime = 0,
.lastLcdUpdateTime = 0,
.isButtonPressPending = false,
};
// Prototypes.
static struct ModuleInputs stateMachineSampleInputs(struct ModuleState* state);
static void stateMachineRun(struct ModuleState* state, struct ModuleInputs const* inputs);
static void lcdDraw(State currentState, uint16_t cellVoltage);
static void ledSetColor(Color color);
static void buttonPressHandler();
static void relayClose();
static void relayOpen();
void setup()
{
// Setup LCD.
sLcd.init();
sLcd.clear();
sLcd.backlight();
sLcd.setCursor(0, 0);
sLcd.print("Volt: mV");
sLcd.setCursor(0, 1);
sLcd.print("State: ");
// Configure GPIOs for the RGB LED. The LED is initially turned off.
unsigned const led_pins[] = { PIN_LED_RED, PIN_LED_GREEN, PIN_LED_BLUE };
for (unsigned pin : led_pins) {
pinMode(pin, OUTPUT);
digitalWrite(pin, LOW);
}
// Setup the button pin in interrupt mode.
pinMode(PIN_BUTTON, INPUT);
attachInterrupt(digitalPinToInterrupt(PIN_BUTTON), buttonPressHandler, FALLING);
// Setup the relay control pin.
pinMode(PIN_RELAY, OUTPUT);
relayOpen();
}
void loop()
{
struct ModuleInputs const inputs = stateMachineSampleInputs(&sModuleState);
stateMachineRun(&sModuleState, &inputs);
}
// Get the inputs needed by the state machine.
static struct ModuleInputs stateMachineSampleInputs(struct ModuleState* state)
{
struct ModuleInputs inputs = { 0 };
// Sample cell voltage.
inputs.cellVoltage = analogRead(PIN_CELL) * MILLIVOLTS_PER_ADC_UNIT;
// Figure out if the button was pressed.
if (state->isButtonPressPending) {
state->isButtonPressPending = false;
inputs.isButtonPressed = true;
} else {
inputs.isButtonPressed = false;
}
return inputs;
}
// Run the state machine for one tick.
static void stateMachineRun(struct ModuleState* state, struct ModuleInputs const* inputs)
{
bool const voltageValid = isVoltageValid(state, inputs->cellVoltage);
switch (state->currentState) {
case STATE_INIT:
relayOpen();
ledSetColor(COLOR_NONE);
if (voltageValid) {
state->currentState = STATE_READY;
} else {
state->currentState = STATE_FAULT;
}
break;
case STATE_READY:
relayOpen();
ledSetColor(COLOR_BLUE);
if (voltageValid) {
if (inputs->isButtonPressed) {
state->currentState = STATE_ACTIVE;
}
} else {
state->currentState = STATE_FAULT;
}
break;
case STATE_ACTIVE:
relayClose();
ledSetColor(COLOR_GREEN);
if (!voltageValid) {
state->currentState = STATE_FAULT;
}
break;
case STATE_FAULT:
relayOpen();
ledSetColor(COLOR_RED);
if (voltageValid && inputs->isButtonPressed) {
state->currentState = STATE_INIT;
}
break;
}
// Figure out if it's time to update the LCD display.
if ((millis() - state->lastLcdUpdateTime) > LCD_UPDATE_PERIOD_MS) {
state->lastLcdUpdateTime = millis();
lcdDraw(state->currentState, inputs->cellVoltage);
}
}
// Determine if the voltage is valid or not.
// This function should only be run once per tick since it updates
// the under/overvoltage start times.
static bool isVoltageValid(struct ModuleState* state, uint16_t cellVoltage)
{
// First, update the over- and under-voltage start times.
int const currentTime = millis();
if (cellVoltage > CELL_UPPER_LIMIT_MV) {
// Only update the over voltage start time once per overvoltage event.
if (0 == state->overVoltageStartTime) {
state->overVoltageStartTime = currentTime;
}
} else if (cellVoltage < CELL_LOWER_LIMIT_MV) {
// Only update the under voltage start time once per overvoltage event.
if (0 == state->underVoltageStartTime) {
state->underVoltageStartTime = currentTime;
}
} else {
// There is no overvoltage or undervoltage.
state->overVoltageStartTime = 0;
state->underVoltageStartTime = 0;
}
// Second, determine if we've had an over/undervoltage condition long
// enough to trigger a fault.
bool voltageValid = true;
if (state->overVoltageStartTime &&
(currentTime - state->overVoltageStartTime) >= CELL_UPPER_LIMIT_QUAL_TIME_MS) {
voltageValid = false;
} else if (state->underVoltageStartTime &&
((currentTime - state->underVoltageStartTime) >= CELL_LOWER_LIMIT_QUAL_TIME_MS)) {
voltageValid = false;
}
return voltageValid;
}
// Helper function to set the color of the RGB LED.
static void ledSetColor(Color color)
{
switch (color) {
case COLOR_NONE:
digitalWrite(PIN_LED_RED, LOW);
digitalWrite(PIN_LED_GREEN, LOW);
digitalWrite(PIN_LED_BLUE, LOW);
case COLOR_RED:
digitalWrite(PIN_LED_RED, HIGH);
digitalWrite(PIN_LED_GREEN, LOW);
digitalWrite(PIN_LED_BLUE, LOW);
break;
case COLOR_BLUE:
digitalWrite(PIN_LED_RED, LOW);
digitalWrite(PIN_LED_GREEN, LOW);
digitalWrite(PIN_LED_BLUE, HIGH);
break;
case COLOR_GREEN:
digitalWrite(PIN_LED_RED, LOW);
digitalWrite(PIN_LED_GREEN, HIGH);
digitalWrite(PIN_LED_BLUE, LOW);
break;
}
}
// Handler that runs when the button is pressed.
static void buttonPressHandler()
{
sModuleState.isButtonPressPending = true;
}
// Helper function to write to the LCD. This function can be modified if needed.
static void lcdDraw(State currentState, uint16_t cellVoltage)
{
// create char array from cell voltage
char voltStr[5] = { 0 };
itoa(cellVoltage, voltStr, 10);
sLcd.setCursor(0, 0);
sLcd.print("Volt: mV");
sLcd.setCursor(6, 0);
sLcd.print(voltStr);
sLcd.setCursor(7, 1);
switch (currentState) {
case STATE_INIT:
sLcd.print("INIT ");
break;
case STATE_READY:
sLcd.print("READY ");
break;
case STATE_ACTIVE:
sLcd.print("ACTIVE");
break;
case STATE_FAULT:
sLcd.print("FAULT ");
break;
}
}
// Close the relay.
static void relayClose()
{
digitalWrite(PIN_RELAY, HIGH);
}
// Open the relay.
static void relayOpen()
{
digitalWrite(PIN_RELAY, LOW);
}