/**
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
************************************/
// Includes
#include <LiquidCrystal_I2C.h>
/************************************
* EXTERN VARIABLES
************************************/
/************************************
* PRIVATE MACROS AND DEFINES
************************************/
#define DEBUG_SERIAL_PRINT
#undef DEBUG_SERIAL_PRINT
// Pin hardware
#define BTN_PIN 2
#define READY_LED_PIN 11
#define ACTIVE_LED_PIN 12
#define FAULT_LED_PIN 13
#define RELAY_PIN 4
#define CELL_VOLTAGE A0
#define MAXIMUM_MILLIVOLTS (5000.0f)
#define MAXIMUM_ADC_BITS (1023.0f)
#define OVERVOLTAGE_LIMIT_MS ((uint16_t)2000)
#define UNDERVOLTAGE_LIMIT_MS ((uint16_t)3000)
#define TASK_PERIOD_MS 1U
#define LCD_REFRESH_RATE_MS 100U
#define LCD_REFRESH_RATE (LCD_REFRESH_RATE_MS/TASK_PERIOD_MS)
#define DEBOUNCE_TIME_MS (10U)
#define DEBOUNCE_TIME (DEBOUNCE_TIME_MS/TASK_PERIOD_MS)
#define BTNSTATE_NOT_PRESSED HIGH
#define BTNSTATE_PRESSED LOW
#define closeRelay() digitalWrite(RELAY_PIN, HIGH)
#define openRelay() digitalWrite(RELAY_PIN, LOW)
#define enableReadyLED() digitalWrite(READY_LED_PIN, HIGH)
#define disableReadyLED() digitalWrite(READY_LED_PIN, LOW)
#define enableActiveLED() digitalWrite(ACTIVE_LED_PIN, HIGH)
#define disableActiveLED() digitalWrite(ACTIVE_LED_PIN, LOW)
#define enableFaultLED() digitalWrite(FAULT_LED_PIN, HIGH)
#define disableFaultLED() digitalWrite(FAULT_LED_PIN, LOW)
#define getADCValue() analogRead(CELL_VOLTAGE)
#define ENTERING_STATE() (prevState != currentState)
/************************************
* PRIVATE TYPEDEFS
************************************/
// used for state machine
enum State {
NONE,
INIT,
READY,
ACTIVE,
FAULT
};
/************************************
* STATIC VARIABLES
************************************/
/************************************
* GLOBAL VARIABLES
************************************/
// LCD Object
LiquidCrystal_I2C lcd(0x27,20,4);
State prevState = NONE;
State currentState = INIT;
State requestedState = INIT;
// cell voltages are stored in mV
uint16_t cellVoltage = 0;
const uint16_t cellVoltUpperLimit = 4200;
const uint16_t cellVoltLowerLimit = 1900;
volatile byte buttonInt = 0;
bool buttonPressFlag = false;
/************************************
* STATIC FUNCTION PROTOTYPES
************************************/
static void initLCD(void);
static void drawLCD(void);
static void updateButtonState(void);
void buttonPressISR(void);
static void initButton(void);
static void initLED(void);
static void initRelay(void);
static void bmsSM(void);
static bool getCellVoltageInRange(void);
static bool getCellUnderVoltage(void);
static bool getCellOverVoltage(void);
static void getCellVoltage(void);
static bool getButtonPressedFlag(void);
/************************************
* GLOBAL FUNCTIONS
************************************/
void setup(void)
{
#ifdef DEBUG_SERIAL_PRINT
Serial.begin(115200);
Serial.println("Debugging Enabled");
#endif //DEBUG_SERIAL_PRINT
/* initialize all modules */
initLCD();
initButton();
initRelay();
initLED();
}
void loop(void)
{
static uint32_t bmsTimestamp = millis();
uint32_t currentTime = millis();
if( (currentTime-bmsTimestamp) >= TASK_PERIOD_MS)
{
bmsTimestamp = currentTime;
/* run BMS State Machine every 10 ms*/
bmsSM();
}
}
/************************************
* STATIC FUNCTIONS
************************************/
static void bmsSM(void)
{
static uint16_t overVoltCntr = 0;
static uint16_t underVoltCntr = 0;
static uint16_t updateLCDTimestamp = 0;
/* get cell voltage */
getCellVoltage();
bool cellOverVolt = getCellOverVoltage();
bool cellUnderVolt = getCellUnderVoltage();
bool cellVoltInRange = getCellVoltageInRange();
switch (currentState)
{
case INIT:
{
if(ENTERING_STATE())
{
#ifdef DEBUG_SERIAL_PRINT
Serial.println("INIT");
#endif
/* disable all LEDs*/
disableReadyLED();
disableActiveLED();
disableFaultLED();
/* disable relay */
openRelay();
/* reset undervolt counter*/
underVoltCntr = 0;
/* reset overvolt counter*/
overVoltCntr = 0;
}
if(cellUnderVolt)
{
/*cell is undervolt start counting */
underVoltCntr++;
if(UNDERVOLTAGE_LIMIT_MS <= underVoltCntr)
{
/* move state to Fault State */
requestedState = FAULT;
}
}
else
{
underVoltCntr = 0;
}
/* check for overvoltage */
if(cellOverVolt)
{
/* start counting */
overVoltCntr++;
if(OVERVOLTAGE_LIMIT_MS <= overVoltCntr)
{
/* move state to Fault State */
requestedState = FAULT;
}
}
else
{
overVoltCntr = 0;
}
if((false == cellUnderVolt) && (false == cellOverVolt))
{
/* if cell is safe move to next state */
requestedState = READY;
}
break;
}
case READY:
{
if(ENTERING_STATE())
{
#ifdef DEBUG_SERIAL_PRINT
Serial.println("READY");
#endif
/* enable ready led*/
enableReadyLED();
/* reset undervolt counter*/
underVoltCntr = 0;
/* reset overvolt counter*/
overVoltCntr = 0;
}
if(cellUnderVolt)
{
/*cell is undervolt start counting */
underVoltCntr++;
if(UNDERVOLTAGE_LIMIT_MS <= underVoltCntr)
{
/* move state to Fault State */
requestedState = FAULT;
}
}
else
{
underVoltCntr = 0;
}
/* check for overvoltage */
if(cellOverVolt)
{
/* start counting */
overVoltCntr++;
if(OVERVOLTAGE_LIMIT_MS <= overVoltCntr)
{
/* move state to Fault State */
requestedState = FAULT;
}
}
else
{
overVoltCntr = 0;
}
/* run button check */
if(HIGH == getButtonPressedFlag() /* && this prob needs a cell is safe check*/)
{
/* move state to Active State */
requestedState = ACTIVE;
}
break;
}
case ACTIVE:
{
if(ENTERING_STATE())
{
#ifdef DEBUG_SERIAL_PRINT
Serial.println("ACTIVE");
#endif
/* disable Ready led*/
disableReadyLED();
/* enable active led*/
enableActiveLED();
/* enable relay */
closeRelay();
/* reset undervolt counter*/
underVoltCntr = 0;
/* reset overvolt counter*/
overVoltCntr = 0;
}
if(cellUnderVolt)
{
/*cell is undervolt start counting */
underVoltCntr++;
if(UNDERVOLTAGE_LIMIT_MS <= underVoltCntr)
{
/* move state to Fault State */
requestedState = FAULT;
}
}
else
{
underVoltCntr = 0;
}
/* check for overvoltage */
if(cellOverVolt)
{
/* start counting */
overVoltCntr++;
if(OVERVOLTAGE_LIMIT_MS <= overVoltCntr)
{
/* move state to Fault State */
requestedState = FAULT;
}
}
else
{
overVoltCntr = 0;
}
break;
}
case FAULT:
{
if(ENTERING_STATE())
{
#ifdef DEBUG_SERIAL_PRINT
Serial.println("FAULT");
#endif
/* disable relay */
openRelay();
/* disable other LEDs led*/
disableReadyLED();
disableActiveLED();
/* enable active led*/
enableFaultLED();
}
if(HIGH == getButtonPressedFlag() && cellVoltInRange/*cells in range */)
{
requestedState = INIT;
}
break;
}
default:
{
#ifdef DEBUG_SERIAL_PRINT
Serial.println("default");
#endif
/* this should not happen so go to FAULT */
requestedState = INIT;
break;
}
}
prevState = currentState;
if(requestedState != NONE)
{
currentState = requestedState;
requestedState = NONE;
}
updateLCDTimestamp++;
if(updateLCDTimestamp >= LCD_REFRESH_RATE)
{
updateLCDTimestamp = 0;
/* redraw LCD*/
drawLCD();
}
/* update button state*/
updateButtonState();
}
static bool getCellVoltageInRange(void)
{
/* return if cell voltage in range */
return ((cellVoltUpperLimit >= cellVoltage) && (cellVoltLowerLimit <= cellVoltage));
}
static bool getCellUnderVoltage(void)
{
/* return if cell voltage in range */
return (cellVoltLowerLimit > cellVoltage);
}
static bool getCellOverVoltage(void)
{
/* return if cell voltage in range */
return (cellVoltUpperLimit < cellVoltage);
}
static void getCellVoltage(void)
{
cellVoltage = (uint16_t)((MAXIMUM_MILLIVOLTS*((float)getADCValue()))/(MAXIMUM_ADC_BITS));
}
static void initRelay(void)
{
/* initialize relay as output */
pinMode(RELAY_PIN, OUTPUT);
/* initialize relay to offstate */
openRelay();
}
static void initLED(void)
{
/* initialize LED as outputs */
pinMode(READY_LED_PIN, OUTPUT);
pinMode(ACTIVE_LED_PIN, OUTPUT);
pinMode(FAULT_LED_PIN, OUTPUT);
/* disable all LEDs*/
disableReadyLED();
disableActiveLED();
disableFaultLED();
}
static void initLCD(void)
{
// Setup LCD
lcd.init();
lcd.clear();
lcd.backlight();
lcd.setCursor(0,0);
lcd.print("Volt: mV");
lcd.setCursor(0,1);
lcd.print("State: ");
}
// helper function to write to the LCD. This function can be modified if needed.
static void drawLCD(void)
{
static uint16_t prevCellVoltage = 0xFFFF;
if(cellVoltage != prevCellVoltage)
{
prevCellVoltage = cellVoltage;
// create char array from cell voltage
char voltStr[4];
itoa(cellVoltage, voltStr, 10);
lcd.setCursor(6,0);
lcd.print(" ");
lcd.setCursor(6,0);
lcd.print(voltStr);
}
lcd.setCursor(7,1);
switch (currentState)
{
case INIT:
lcd.print("INIT ");
break;
case READY:
lcd.print("READY ");
break;
case ACTIVE:
lcd.print("ACTIVE");
break;
case FAULT:
lcd.print("FAULT ");
break;
}
}
/* Button Code */
static void buttonPressISR(void)
{
/*button pressed byte sized hence it should be atomic write */
buttonInt = true;
}
static void initButton(void)
{
/* Set Button pin to use internal pullup resistor since button is pulling pin low when pressed */
pinMode(BTN_PIN, INPUT_PULLUP);
/* enabling interrupt to BTN Pin, setting buttonPressISR as ISR, and setting interrupt to trigger on falling edge */
attachInterrupt(digitalPinToInterrupt(BTN_PIN), buttonPressISR, FALLING);
}
static bool getButtonPressedFlag(void)
{
bool retVal = false;
if(buttonPressFlag)
{
buttonPressFlag = false;
retVal = true;
}
else
{
/* do nothing */
}
return retVal;
}
static void updateButtonState(void)
{
static bool buttonState = BTNSTATE_NOT_PRESSED;
static bool prevButtonState = BTNSTATE_NOT_PRESSED;
static uint32_t btnHighTS = millis();
uint32_t currentTime = millis();
#ifdef DEBUG_SERIAL_PRINT
static uint32_t pressCount = 0;
#endif
if(buttonState == BTNSTATE_NOT_PRESSED)
{
if(prevButtonState != buttonState)
{
/* if button state has recently changed just*/
prevButtonState = buttonState;
#ifdef DEBUG_SERIAL_PRINT
Serial.println("Button Not Pressed");
#endif
/* clear any pending interrupts */
buttonInt = false;
}
if(buttonInt)
{
buttonInt = false;
/* once interrupt is detected go to button pressed state*/
buttonState = BTNSTATE_PRESSED;
/* set button officially pressed flag to HIGH */
buttonPressFlag = true;
}
else
{
/* if no interrupt detected do nothing*/
}
}
else
{
if(prevButtonState != buttonState)
{
/* if button state has recently changed */
prevButtonState = buttonState;
#ifdef DEBUG_SERIAL_PRINT
Serial.print("Button Pressed: ");
Serial.println(++pressCount);
#endif
/* reset button high timestamp */
btnHighTS = currentTime;
}
/* check if button is back to high state */
if( ( (currentTime - btnHighTS) > DEBOUNCE_TIME) && HIGH == digitalRead(BTN_PIN))
{
buttonState = BTNSTATE_NOT_PRESSED;
}
else if(LOW == digitalRead(BTN_PIN))
{
/* if button goes low even momentarily reset timer */
btnHighTS = currentTime;
}
else
{
/* do nothing */
}
}
return buttonState;
}