/**
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 READY_LED_PIN 11
#define CHARGE_LED_PIN 12
#define DISCHARGE_LED_PIN 13
#define RELAY_PIN 4
#define closeRelay() digitalWrite(RELAY_PIN, HIGH)
#define openRelay() digitalWrite(RELAY_PIN, LOW)
// used for state machine
enum State {
INIT,
READY,
ACTIVE,
FAULT
};
State currentState = INIT;
// cell voltages are stored in mV
uint16_t cellVoltage = 0;
const uint16_t cellVoltUpperLimit = 4200;
const uint16_t cellVoltLowerLimit = 1900;
// helper function to write to the LCD. This function can be modified if needed.
void drawLCD(){
// create char array from cell voltage
char voltStr[5];
lcd.setCursor(6,0);
itoa(cellVoltage, 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 FAULT:
lcd.print("FAULT ");
break;
}
}
/*******************************************************************************/
/** Forward class declarations for drivers and algorithms. */
class Adc;
class BatteryMonitor;
class Button;
class LedController;
class Relay;
class Scheduler;
class StateMachine;
/**
* Global battery manager state struct.
*/
typedef struct BMState {
/* ADC driver. */
Adc& adc;
/* Button driver. */
Button& button;
/* Battery monitor algorithm. */
BatteryMonitor& bm;
/* Led driver. */
LedController& led;
/* Relay driver. */
Relay& relay;
/* Scheduler algorithm. */
Scheduler& sch;
/* State machine algorithm. */
StateMachine& sm;
/* Constructor. */
BMState(Adc& adc_instance,
Button& button_instance,
BatteryMonitor& bm_instance,
LedController& led_instance,
Relay& relay_instance,
Scheduler& sch_instance,
StateMachine& sm_instance) : adc(adc_instance), button(button_instance), bm(bm_instance),
led(led_instance), relay(relay_instance), sch(sch_instance),
sm(sm_instance) {}
} BMState;
/**
* Periodic runner.
*
* Used to define a task that is run by the Scheduler periodically.
*/
class PeriodicRunner {
public:
/**
* Periodic runner constructor.
*/
PeriodicRunner(uint64_t period_ms, BMState *global_state) : period_(period_ms), state_(global_state) {}
/**
* Run.
*/
virtual void run() = 0;
/**
* Get period.
*/
uint64_t get_period() { return period_; }
protected:
/* Execution period in nanoseconds. */
uint64_t period_;
/* Global state. */
BMState* state_;
};
/**
* ADC driver.
*/
class Adc {
public:
/**
* Adc constructor.
*/
Adc() {}
/**
* Init.
*/
void init(){
// See: https://docs.wokwi.com/chips-api/gpio
pinMode(A0, INPUT);
}
/**
* Get sample value in volts.
*/
float sample() {
// Ref: https://wokwi.com/projects/330112801381024338
return (analogRead(A0) * kVref) / kMaxVal ;
}
private:
/** Vref. See https://docs.wokwi.com/chips-api/analog. */
static constexpr float kVref = 5.0f;
/** ADC max value in 10 bit adc. */
static constexpr float kMaxVal = 1023;
};
/**
* Battery monitor algorithm.
*/
class BatteryMonitor {
public:
/**
* Battery Monitor constructor.
*/
BatteryMonitor() {
reset();
}
/**
* Sample battery voltage.
*/
void add_sample(uint64_t timestamp_ms, float voltage_V) {
voltage_ = voltage_V;
timestamp_ = timestamp_ms;
// Check for overvoltage.
if (voltage_ > kOvervoltageLim) {
if(ov_exceed_timestamp_ms_ == 0){
ov_exceed_timestamp_ms_ = timestamp_;
}
} else {
ov_exceed_timestamp_ms_ = 0;
}
// Check for undervoltage.
if (voltage_ < kUndervoltageLim) {
if (uv_drop_timestamp_ms_ == 0){
uv_drop_timestamp_ms_ = timestamp_;
}
} else {
uv_drop_timestamp_ms_ = 0;
}
}
/**
* Is cell in safe zone.
*/
bool is_safe(void) const {
return !(is_ov() || is_uv());
}
private:
/** Overvoltage limit in volts. */
static constexpr float kOvervoltageLim = 4.2;
/** Overvoltage hold time in nanoseconds. */
static constexpr uint64_t kOvervoltagePeriodMs = 2 * 1000; // 2 seconds
/** Undervoltage limit in volts. */
static constexpr float kUndervoltageLim = 1.9;
/** Undervoltage hold time in nanoseconds. */
static constexpr uint64_t kUndervoltagePeriodMs = 3 * 1000; // 3 seconds
/**
* Reset state.
*/
void reset() {
voltage_ = 0;
timestamp_ = 0;
ov_exceed_timestamp_ms_ = 0;
uv_drop_timestamp_ms_ = 0;
}
/**
* Is overvoltage.
*/
bool is_ov() const {
return voltage_ > kOvervoltageLim &&
ov_exceed_timestamp_ms_ != 0 &&
(timestamp_ - ov_exceed_timestamp_ms_) >= kOvervoltagePeriodMs;
}
/**
* Is undervoltage.
*/
bool is_uv() const {
return voltage_ < kUndervoltageLim &&
uv_drop_timestamp_ms_ != 0 &&
(timestamp_ - uv_drop_timestamp_ms_) >= kUndervoltagePeriodMs;
}
/** Timestamp when voltage exceeded kOvervoltagePeriodMs. */
uint64_t ov_exceed_timestamp_ms_ = 0;
/** Timestamp when voltage dropped below kUndervoltageLim. */
uint64_t uv_drop_timestamp_ms_ = 0;
/** Last sample voltage. */
float voltage_;
/** Last sample timestamp. */
uint64_t timestamp_;
};
/**
* Button driver.
*/
class Button {
public:
/**
* Button constructor.
*/
Button() {}
/**
* Init.
*/
void init(BMState* state){
state_ = state;
// Register for A2 interrupt.
pinMode(2, INPUT);
attachInterrupt(digitalPinToInterrupt(2), Button::button_isr, FALLING);
}
/**
* ISR.
*/
static void button_isr() {
if(state_ != nullptr){
// NOTE: This is called from ISR context.
state_->button.handle_interrupt();
}
}
/**
* Handle interrupt.
*/
void handle_interrupt();
private:
/* Global state*/
static BMState* state_;
/* Debounce time in miliseconds. */
static constexpr uint64_t kDebouncePeriodMs = 1000;
/* Last switch debounce time. */
uint64_t debounce_timestamp_ms_ = 0;
};
/** Button::BMState* used in ISR. */
static BMState* Button::state_ = nullptr;
/**
* LED driver.
*/
class LedController {
public:
/**
* LedController constructor.
*/
LedController() {}
/**
* Init.
*/
void init() {
pinMode(11, OUTPUT);
pinMode(12, OUTPUT);
pinMode(13, OUTPUT);
off();
}
/**
* Set blue.
*/
void blue() {
digitalWrite(11, HIGH);
digitalWrite(12, LOW);
digitalWrite(13, LOW);
}
/**
* Set red.
*/
void red() {
digitalWrite(11, LOW);
digitalWrite(12, LOW);
digitalWrite(13, HIGH);
}
/**
* Set green.
*/
void green() {
digitalWrite(11, LOW);
digitalWrite(12, HIGH);
digitalWrite(13, LOW);
}
/**
* Set off.
*/
void off() {
digitalWrite(11, LOW);
digitalWrite(12, LOW);
digitalWrite(13, LOW);
}
};
/**
* Relay driver.
*/
class Relay {
public:
/**
* Relay constructor.
*/
Relay() {}
/**
* Init.
*/
void init() {
pinMode(4, OUTPUT);
off();
}
/**
* Turn on.
*/
void on() {
digitalWrite(4, HIGH);
}
/**
* Turn off.
*/
void off(){
digitalWrite(4, LOW);
}
};
/**
* Scheduler.
*/
class Scheduler {
public:
/**
* Constructor.
*/
Scheduler() {}
/**
* Exec method that will be called from main loop.
*/
void exec() {
for(uint8_t i = 0; i < count_; i++){
uint64_t now = millis();
if (now < runners_[i].next_run_ms){
runners_[i].runner->run();
runners_[i].next_run_ms = now + runners_[i].runner->get_period();
}
}
}
/**
* Add a runner.
*/
void add_runner(PeriodicRunner *runner){
if(count_ < kMaxRunners && runner != nullptr){
runners_[count_].next_run_ms = (millis()) + runner->get_period();
runners_[count_].runner = runner;
count_++;
}
}
private:
/** Maximim number of runners. */
static constexpr uint8_t kMaxRunners = 2;
/**
* Scheduler data struct.
*/
struct Runner {
uint64_t next_run_ms;
PeriodicRunner* runner;
};
/** Periodic events to schedule. */
Runner runners_[kMaxRunners];
/** Number of known runnuers. */
uint8_t count_ = 0;
};
/**
* State machcine algorithm.
*/
class StateMachine {
public:
/**
* State Machine Constructor.
*/
StateMachine() {}
/**
* Add button event.
*/
button() {
handle_event(Event::BUTTON);
if(battery_safe_){
handle_event(Event::BUTTON_WITH_GOOD_BAT);
}
}
/**
* Battery out of limit event.
*/
battery_bad() {
battery_safe_ = false;
handle_event(Event::BATTERY_BAD);
}
/**
* Battery is safe event.
*/
battery_good() {
battery_safe_ = true;
handle_event(Event::BATTERY_GOOD);
}
/**
* Get state string.
*/
const char* get_state_string() {
switch(state_){
case INIT: return kInit;
case READY: return kReady;
case ACTIVE: return kActive;
case FAULT: return kFault;
}
return kFault;
}
/**
* Get state.
*/
State get_state() { return state_; }
private:
/** Init string. */
const char *kInit = "Init";
/** Ready string. */
const char *kReady = "Ready";
/** Active string. */
const char *kActive = "Active";
/** Fault string. */
const char *kFault = "Fault";
/** States in state machine. */
static constexpr uint8_t kStateMachineLen = 6;
/** Events. */
enum class Event {
BUTTON,
BUTTON_WITH_GOOD_BAT,
BATTERY_BAD,
BATTERY_GOOD
};
/** State machine triggers. */
struct StateTransitions {
State from;
Event trigger;
State to;
};
/**
* State machine.
*
* Each row is:
* { FROM, TRIGGER, TO }
*/
StateTransitions sm_[kStateMachineLen] = {
{INIT, Event::BATTERY_GOOD, READY},
{INIT, Event::BATTERY_BAD, FAULT},
{READY, Event::BUTTON, ACTIVE},
{READY, Event::BATTERY_BAD, FAULT},
{ACTIVE, Event::BATTERY_BAD, FAULT},
{FAULT, Event::BUTTON_WITH_GOOD_BAT, INIT}
};
/**
* Handle state machine transitions.
*/
void handle_event(Event trigger) {
auto state = state_; // Work on a local copy of state because of potential race
// with ISR.
for(auto& s : sm_){
if (state == s.from && trigger == s.trigger){
state_ = s.to;
return;
}
}
}
/** Current state. */
State state_ = INIT;
/** Last known battery state. */
bool battery_safe_;
};
/**
* Lcd printer.
*
* This is a periodic task executed every 100ms.
* It's responsible for updating the lcd screen.
*/
class LcdPrinter : public PeriodicRunner {
public:
/**
* LcdPrinter constructor.
*/
LcdPrinter(BMState *global_state) : PeriodicRunner(kRefreshRateMs, global_state) {}
/**
* Init.
*/
void init(){
lcd.init();
lcd.clear();
lcd.backlight();
}
/**
* See PeriodicRunner:run.
*/
void run() override;
private:
/** Refresh rate. */
static constexpr uint64_t kRefreshRateMs = 100; //100ms
};
/**
* Battery sampler.
*
* This is a periodic task executed every 50ms.
* It samples the battery and updates the state machine. It then
* responds appropriately to the resulting state in the state machine.
*/
class BatterySampler : public PeriodicRunner {
public:
/**
* Battery sampler constructor.
*/
BatterySampler(BMState *global_state) : PeriodicRunner(kSamplingPeriodMs, global_state) {}
/**
* See PeriodicRunner:run.
*/
void run() override;
private:
/** Sampling period. */
static constexpr uint64_t kSamplingPeriodMs = 50; //50ms
};
/**
* Button::handle_interrupt().
*
* Defined here because it depends upon state_->sm.button().
*/
void Button::handle_interrupt(){
uint64_t now = millis();
if (now - debounce_timestamp_ms_ > kDebouncePeriodMs){
debounce_timestamp_ms_ = now;
state_->sm.button();
}
}
/**
* LcdPrinter::run().
*/
void LcdPrinter::run() {
const char* state_string = state_->sm.get_state_string();
char buffer[50];
uint16_t voltage_mv = state_->adc.sample() * 1000;
itoa(voltage_mv, buffer, 10);
lcd.clear();
lcd.backlight();
lcd.setCursor(0,0);
lcd.print("Volt: ");
lcd.print((const char*)buffer);
lcd.print(" mV");
lcd.setCursor(0,1);
lcd.print("State: ");
lcd.print(state_string);
}
/**
* BatterySampler::run()
*/
void BatterySampler::run() {
float voltage = state_->adc.sample();
state_->bm.add_sample(millis(), voltage);
if (state_->bm.is_safe()){
state_->sm.battery_good();
} else {
state_->sm.battery_bad();
}
switch(state_->sm.get_state()) {
case INIT:
state_->relay.off();
state_->led.off();
break;
case READY:
state_->relay.off();
state_->led.blue();
break;
case ACTIVE:
state_->relay.on();
state_->led.green();
break;
case FAULT:
state_->relay.off();
state_->led.red();
break;
}
}
/** Global driver instances. */
Adc g_adc;
Button g_button;
LedController g_led;
Relay g_relay;
/** Global algorithm instances. */
BatteryMonitor g_bm;
Scheduler g_sch;
StateMachine g_sm;
/* Global state. */
BMState g_state(g_adc, g_button, g_bm, g_led, g_relay, g_sch, g_sm);
/** Global PeriodicRunner instances. */
LcdPrinter g_printer(&g_state);
BatterySampler g_sampler(&g_state);
void setup() {
// Setup LCD
g_printer.init();
// Init adc.
g_state.adc.init();
// Init button.
g_state.button.init(&g_state);
// Init relay.
g_state.relay.init();
/**
* Add runners.
* Note: order matters. If two tasks must run at the same time, task
* added first here will be run first. Ensure Sampler runs before
* printer (assuming driving lcd takes longer).
*/
g_state.sch.add_runner(&g_sampler); // Add battery sampler to scheduler.
g_state.sch.add_runner(&g_printer); // Add the lcd printer to scheduler.
}
void loop() {
g_state.sch.exec();
}
/*******************************************************************************/