/**
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>
// LCD Object
LiquidCrystal_I2C lcd(0x27,20,4);
// Pin hardware
#define BTN_PIN 2
#define BLUE_LED_PIN 11
#define GREEN_LED_PIN 12
#define RED_LED_PIN 13
#define RELAY_PIN 4
// Button Debouncing Parameters
#define BUTTON_DEBOUNCE_TIME_MS 10000
// LCD REFRESH TIME
#define REFRESH_LCD_TIME_MS 0
#define closeRelay() digitalWrite(RELAY_PIN, HIGH)
#define openRelay() digitalWrite(RELAY_PIN, LOW)
// used for state machine
typedef enum State {
INIT,
READY,
ACTIVE,
FAULT,
RESET
}state_t;
static state_t currentState = INIT;
typedef enum LED_STATE{
RED,
GREEN,
BLUE,
OFF
}ledstate_t;
// Event Queue
typedef enum EVENTS
{
BUTTON_PRESS_SIG,
SAFE_VOLT_SIG,
UNSAFE_VOLT_SIG,
MAX_SIGS
}EventSignals_t;
#define EVENT_QUEUE_SIZE 10
EventSignals_t eventQueue[EVENT_QUEUE_SIZE];
static uint8_t eventHead = 0;
static uint8_t eventTail = 0;
void postEvent(EventSignals_t event) {
uint8_t nextEvent = (eventHead + 1);
if(nextEvent>=EVENT_QUEUE_SIZE)
{
nextEvent = 0;
}
if (nextEvent != eventTail) {
eventQueue[eventHead] = event;
eventHead = nextEvent;
}
}
EventSignals_t getEvent() {
EventSignals_t event = MAX_SIGS;
if (eventTail != eventHead) {
event = eventQueue[eventTail];
eventTail = (eventTail + 1);
if(eventTail>=EVENT_QUEUE_SIZE)
{
eventTail = 0;
}
}
return event;
}
typedef enum CELL_VOLTAGE_STATE
{
UNDERFLOW,
OVERFLOW,
SAFEFLOW,
UNKOWN
}voltage_state_t;
#define ADC_MAX 1023
#define VOLTAGE_MAX_MV 5000
#define ADC_TO_MILLIVOLT_SCALAR (VOLTAGE_MAX_MV/ADC_MAX)
#define OVER_VOLTAGE_REFRESH_TIME_MS 2000
#define UNDER_VOLTAGE_REFRESH_TIME_MS 3000
#define SAFE_VOLTAGE_REFRESH_TIME_MS 3000
// cell voltages are stored in mV
const static uint16_t cellVoltUpperLimit = 4200;
const static uint16_t cellVoltLowerLimit = 1900;
static uint64_t current_time = 0;
static uint16_t cellVoltageInMv = 0;
void monitor_voltage_level()
{
static uint64_t current_time = 0;
static bool start_safe_timer = true;
static bool start_underflow_timer = true;
static bool start_overflow_timer = true;
static voltage_state_t voltage_state = UNKOWN;
int sensorValue = analogRead(A0);
// Convert the ADC value to voltage (assuming a 5V reference)
cellVoltageInMv = sensorValue * ADC_TO_MILLIVOLT_SCALAR;
if((cellVoltageInMv > cellVoltUpperLimit) && (voltage_state != OVERFLOW))
{
if(start_overflow_timer)
{
current_time = millis();
start_overflow_timer = false;
}
else
{
if((millis() - current_time) >= OVER_VOLTAGE_REFRESH_TIME_MS)
{
postEvent(UNSAFE_VOLT_SIG);
// At any given point, if the voltage move from safe to unsafe voltage or vice-versa
// then reset the flags for all the timer
start_safe_timer = true;
start_underflow_timer = true;
start_overflow_timer = true;
voltage_state = OVERFLOW;
}
}
}
else if((cellVoltageInMv < cellVoltLowerLimit) && (voltage_state != UNDERFLOW))
{
if(start_underflow_timer)
{
current_time = millis();
start_underflow_timer = false;
}
else
{
if((millis() - current_time) >= UNDER_VOLTAGE_REFRESH_TIME_MS)
{
postEvent(UNSAFE_VOLT_SIG);
start_safe_timer = true;
start_underflow_timer = true;
start_overflow_timer = true;
voltage_state = UNDERFLOW;
}
}
}
else if(voltage_state != SAFEFLOW)
{
if(start_safe_timer)
{
current_time = millis();
start_safe_timer = false;
}
else
{
if((millis() - current_time) >= SAFE_VOLTAGE_REFRESH_TIME_MS)
{
postEvent(SAFE_VOLT_SIG);
start_safe_timer = true;
start_underflow_timer = true;
start_overflow_timer = true;
voltage_state = SAFEFLOW;
}
}
}
}
// helper function to write to the LCD.
void drawLCD(){
// create char array from cell voltage
char voltStr[5];
static uint64_t time = 0;
// Only Update LCD every REFRESH_LCD_TIME_MS.
uint64_t current_time = millis();
if ((current_time - time) >= REFRESH_LCD_TIME_MS)
{
lcd.setCursor(6,0);
itoa(cellVoltageInMv, voltStr, 10);
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 RESET:
case FAULT:
lcd.print("FAULT ");
break;
}
time = current_time;
}
}
void updateRgbLed(const ledstate_t state)
{
digitalWrite(RED_LED_PIN, LOW);
digitalWrite(GREEN_LED_PIN, LOW);
digitalWrite(BLUE_LED_PIN, LOW);
switch(state)
{
case RED:
digitalWrite(RED_LED_PIN, HIGH);
break;
case GREEN:
digitalWrite(GREEN_LED_PIN, HIGH);
break;
case BLUE:
digitalWrite(BLUE_LED_PIN, HIGH);
break;
default:
break;
}
}
void checkCurrentState()
{
EventSignals_t current_event = getEvent();
char str[10];
lcd.setCursor(10,1);
itoa(current_event, str, 15);
lcd.print(str);
switch(currentState)
{
case INIT:
openRelay();
updateRgbLed(OFF);
if(current_event == SAFE_VOLT_SIG)
{
// This will get change after SAFE VOLTAGE REFRESH RATE
currentState = READY;
}
else if(current_event == UNSAFE_VOLT_SIG)
{
currentState = FAULT;
}
break;
case READY:
openRelay();
updateRgbLed(BLUE);
if(current_event == BUTTON_PRESS_SIG)
{
currentState = ACTIVE;
}
else if(current_event == UNSAFE_VOLT_SIG)
{
currentState = FAULT;
}
break;
case ACTIVE:
closeRelay();
updateRgbLed(GREEN);
if(current_event == UNSAFE_VOLT_SIG)
{
currentState = FAULT;
}
break;
case FAULT:
openRelay();
updateRgbLed(RED);
if(current_event == SAFE_VOLT_SIG)
{
currentState = RESET;
}
case RESET:
openRelay();
updateRgbLed(RED);
if(current_event == BUTTON_PRESS_SIG)
{
currentState = INIT;
}
default:
break;
}
}
void setup()
{
// Setup LCD
lcd.init();
lcd.clear();
lcd.backlight();
lcd.setCursor(0,0);
lcd.print("Volt: mV");
lcd.setCursor(0,1);
lcd.print("State: ");
// Setup Button GPIO and external interrupt
pinMode(BTN_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BTN_PIN), ButtonPressISR, CHANGE);
}
void loop()
{
monitor_voltage_level();
checkCurrentState();
drawLCD();
}
// This will protect any debouncing signals
void ButtonPressISR()
{
uint64_t currentTime = millis();
static uint64_t lastDebounceTime = 0;
if (currentTime - lastDebounceTime > BUTTON_DEBOUNCE_TIME_MS)
{
lastDebounceTime = currentTime;
postEvent(BUTTON_PRESS_SIG);
}
}