Hooks into the CE pin of TP4056 to add some extra features - overvolt cutoff - overtime cutoff - overtemperature cutoff (by use of MCP9808) - info on little OLED screen - battery presence detection Future features - current detection and cutoff (waiting for INA219 breakout for this) - Runtime configurable parameters by serial - Send stats over serial to desktop application Known flaws - see readme Circuit schematic coming soon (tm), see readme Designed and tested for nucleo F303RE but should be easily adaptable to any board. License: GPL v3
Dependencies: OLED_SSD1306 MCP9808
main.cpp
- Committer:
- kuutei
- Date:
- 2020-09-15
- Revision:
- 10:4ac5d8748268
- Parent:
- 9:272f6963c3b8
File content as of revision 10:4ac5d8748268:
#include "mbed.h"
#include "platform/mbed_thread.h"
#include <string>
#include <iomanip>
#include <sstream>
#include "SSD1306I2C.h"
#include "MCP9808.h"
#include "serialCLI.h"
#define OLED_ADR 0x3C
#define OLED_SDA I2C_SDA
#define OLED_SCL I2C_SCL
#define MCP9808_SDA I2C_SDA
#define MCP9808_SCL I2C_SCL
//#define MCP9808_SDA PB_5
//#define MCP9808_SCL PA_8
#define BAT_SAMPLERATE 1000
// Blinking rate in milliseconds
#define BLINKING_RATE_MS 5000
#define VOLTAGE_DIVIDER_R1 99800
#define VOLTAGE_DIVIDER_R2 99100
//Buffered UARTSerial used, but any buffered serial object with read() and write() should work
UARTSerial pc(USBTX,USBRX,115200);
serialCLI sCLI(&pc);
SSD1306I2C oled_i2c(OLED_ADR, OLED_SDA, OLED_SCL);
//DS1820 ds1820_sensor(DS18B20_DATA);
//sda,scl,address,freq
//TODO: write MCP9808 constructor that doesn't change frequency
MCP9808 myMCP9808 ( MCP9808_SDA, MCP9808_SCL, MCP9808::MCP9808_ADDRESS_0, 400000 ); // I2C_SDA | I2C_SCL
//DigitalIn mybutton(USER_BUTTON);
AnalogIn divider_analogin(A0);
AnalogIn vref(ADC_VREF);
//AnalogIn adc_refint(VREF_INT);
//Open drain with no pull up/down since the CE pin is pulled up to 5V
//This also means a 5V tolereant pin (FT or FTf) must be used
DigitalInOut tp4056ChipEnable(PA_14, PIN_OUTPUT, OpenDrainNoPull, 0);
bool TP4056ChargingState = false; //reflects internal state of TP4056 control thread
uint64_t chargingTimePassed = 0; //reflects charge time recorded by control thread
DigitalIn tp4056ChargeDone(PA_1);
InterruptIn chargeButton(USER_BUTTON);
volatile bool buttonPressed = false; //set to true by button ISR, set to false when acknowledged
bool batteryPresenceState = false;
float bat_voltage = -1.11;
float bat_voltage_avg = -1.11;
float vddref_voltage = -1.11;
float temperatureSenseC = -301.0;
//Configurable protection conditions
float MAX_VOLTAGE = 4.20; //default: no more than ___
float MIN_VOLTAGE = 2.5; //default: no less than 2.5v
float MIN_DETECT_VOLTAGE = 0.5; //default: no less than 0.5v
float MAX_TEMPERATURE = 35.0; //default: no more than 35C at the temperature sensor
float MIN_TEMPERATURE = 10.0; //default: no less than 10C
uint64_t MAX_TIME_ms = 3600000; //default: no more than 1h
void blinkled()
{
// Initialise the digital pin LED1 as an output
DigitalOut led(LED1);
while (true) {
led = !led;
ThisThread::sleep_for(BLINKING_RATE_MS);
}
}
// Button to initiate / stop charging
// Called on button rise
// Set to true to signal to EN control thread that button was pressed
void buttonISR()
{
static uint64_t lastButtonPushTime = 0;
uint64_t now = Kernel::get_ms_count();
const uint64_t buttonMinimumWaitTime = 500; //minimum 500 ms wait between button pushes
if(now - lastButtonPushTime > buttonMinimumWaitTime)
{
buttonPressed = true;
lastButtonPushTime = now;
}
}
float readRefVoltage()
{
double vdd;
double vdd_calibed;
double vref_calibed;
double vref_f;
uint16_t vref_u16;
uint16_t vref_cal;
vref_cal= *((uint16_t*)VREFINT_CAL_ADDR); //F303RE
vref_u16 = vref.read_u16();
//1.22 comes from 3.3 * 1524 / 4095 - voltage at calibration time times calibration measurement divided by maximum counts
//vdd = 1.228132 / vref.read();
vdd = 3.3 * (double)vref_cal / 4095.0 / vref.read();
return vdd;
}
//Reads voltage divider, and uses internal calibrated reference voltage to calculate real voltage
//Not 100% accurate, but better than assuming 3.3v
float readVoltageDivider_Calibrated()
{
uint16_t vref_cal= *((uint16_t*)VREFINT_CAL_ADDR); //factory calibration value for 3.3v
uint16_t vref_u16 = vref.read_u16(); //read the internal voltage calibration
float vdd = 3.3 * (double)vref_cal / 4095.0 / vref.read();
//ain.read() returns float value between 0 and 1
float reading = divider_analogin.read();
//sCLI.printf("raw reading: %f\r\n", reading);
return reading * vdd * (VOLTAGE_DIVIDER_R1 + VOLTAGE_DIVIDER_R2) / VOLTAGE_DIVIDER_R2;
}
//Measurement assuming 3.3v - for comparison to calibrated reading only
//Can be very inaccurate as nucleo voltage regulator drops as far as 3.2v
float readVoltageDivider_3v3()
{
float vdd = 3.3;
//ain.read() returns float value between 0 and 1
float reading = divider_analogin.read();
return reading * vdd * (VOLTAGE_DIVIDER_R1 + VOLTAGE_DIVIDER_R2) / VOLTAGE_DIVIDER_R2;
}
// Reads DS18B20 sense temperature
void TemperatureInputThread()
{
MCP9808::MCP9808_status_t aux;
MCP9808::MCP9808_config_reg_t myMCP9808_Config;
MCP9808::MCP9808_data_t myMCP9808_Data;
// Shutdown the device, low-power mode enabled
aux = myMCP9808.MCP9808_GetCONFIG ( &myMCP9808_Config );
myMCP9808_Config.shdn = MCP9808::CONFIG_SHDN_SHUTDOWN;
aux = myMCP9808.MCP9808_SetCONFIG ( myMCP9808_Config );
// Get manufacturer ID
aux = myMCP9808.MCP9808_GetManufacturerID ( &myMCP9808_Data );
// Get device ID and device revision
aux = myMCP9808.MCP9808_GetDeviceID ( &myMCP9808_Data );
// Configure the device
// - T_UPPER and T_LOWER limit hysteresis at 0C
// - Continuous conversion mode
// - T_CRIT unlocked
// - Window lock unlocked
// - Alert output control disabled
// - Alert output select: Alert for T_UPPER, T_LOWER and T_CRIT
// - Alert output polaruty: Active-low
// - Alert output mode: Comparator output
//
myMCP9808_Config.t_hyst = MCP9808::CONFIG_T_HYST_0_C;
myMCP9808_Config.shdn = MCP9808::CONFIG_SHDN_CONTINUOUS_CONVERSION;
myMCP9808_Config.t_crit = MCP9808::CONFIG_CRIT_LOCK_UNLOCKED;
myMCP9808_Config.t_win_lock = MCP9808::CONFIG_WIN_LOCK_UNLOCKED;
myMCP9808_Config.alert_cnt = MCP9808::CONFIG_ALERT_CNT_DISABLED;
myMCP9808_Config.alert_sel = MCP9808::CONFIG_ALERT_SEL_TUPPER_TLOWER_TCRIT;
myMCP9808_Config.alert_pol = MCP9808::CONFIG_ALERT_POL_ACTIVE_LOW;
myMCP9808_Config.alert_mod = MCP9808::CONFIG_ALERT_MOD_COMPARATOR_OUTPUT;
aux = myMCP9808.MCP9808_SetCONFIG ( myMCP9808_Config );
// Set resolution: +0.0625C ( t_CON ~ 250ms )
myMCP9808_Data.resolution = MCP9808::RESOLUTION_0_0625_C;
aux = myMCP9808.MCP9808_SetResolution ( myMCP9808_Data );
while(true)
{
// Get ambient temperature
aux = myMCP9808.MCP9808_GetTA ( &myMCP9808_Data );
//sCLI.printf ( "T: %0.4f C\r\n", myMCP9808_Data.t_a );
temperatureSenseC = myMCP9808_Data.t_a;
ThisThread::sleep_for(250);
}
}
// Detect battery presence by voltage activity
// TODO: voltage must be above threshold for a minimum time?
void BatteryPresenceThread()
{
while(true)
{
if(bat_voltage_avg > MIN_DETECT_VOLTAGE)
{
batteryPresenceState = true;
}
else
{
batteryPresenceState = false;
}
ThisThread::sleep_for(250);
}
}
void TP4056ControlThread()
{
//uint64_t now = Kernel::get_ms_count();
uint64_t chargeStartTime = 0;
bool enableCharging = false; //internal variable to track whether TP4056 CE should be enabled or not
while(true)
{
//First check conditions to see if charging should be enabled or disabled
bool temperature_en = ( ( temperatureSenseC < MAX_TEMPERATURE ) && ( temperatureSenseC > MIN_TEMPERATURE ) );
//bool temperature_en = true; //override as sensor code is bugged
bool voltage_en = ( ( bat_voltage_avg < MAX_VOLTAGE ) && ( bat_voltage_avg > MIN_VOLTAGE ) );
bool presence_en = batteryPresenceState;
bool charge_time_exceeded = ( chargingTimePassed > MAX_TIME_ms );
//Charging can be enabled if battery is present, no protections triggered, and user starts the charge
if(enableCharging == false)
{
//button must be pressed to start charge, but can also be pressed when eg battery not inserted
if(buttonPressed)
{
if( voltage_en && temperature_en && presence_en )
{
enableCharging = true;
chargeStartTime = Kernel::get_ms_count();
}
//regardless of if charging was started, acknowledge the button press
buttonPressed = false;
}
}
//Charging must be stopped if overvoltage, overtemperature, overtime, battery removed, or user pushes button
//Charging time passed is maintained, but will be reset if user starts charge again
//TODO: Only reset charge time once battery removed?
else
{
//Disable charging if any protection condition is triggered
if( !voltage_en || !temperature_en || !presence_en || charge_time_exceeded )
{
enableCharging = false;
}
//or if user pushed button
else if(buttonPressed)
{
enableCharging = false;
buttonPressed = false;
}
}
//With charge state calculated, realize it on the CE pin
//Allow pullup to '5V' to enable charging at CE pin
if(enableCharging)
{
//Using HiZ still results in internal clamping diode activating, resulting in 3.6v
tp4056ChipEnable.write(1); //open drain mode -> HiZ
//Continually update surpassed charging time if it is enabled
chargingTimePassed = Kernel::get_ms_count() - chargeStartTime ;
}
//To disable charging, open drain the CE pin -> 0V
else
{
tp4056ChipEnable.write(0); //open drain, pull to GND -> overpull 470k pullup that brings 5V to CE
}
//Update flag that indicates state of TP4056 CE pin to other threads
TP4056ChargingState = enableCharging;
ThisThread::sleep_for(100);
}
}
void oledOutputThread()
{
std::stringstream volts_stream, max_volts_stream;
std::stringstream temperature_stream;
std::stringstream chargetimepassed_stream;
std::stringstream chargetimemax_stream;
while(true)
{
volts_stream.str("");
volts_stream << std::fixed << std::setprecision(2) << bat_voltage_avg;
max_volts_stream.str("");
max_volts_stream << std::fixed << std::setprecision(2) << MAX_VOLTAGE;
temperature_stream.str("");
temperature_stream << std::fixed << std::setprecision(2) << temperatureSenseC;
chargetimepassed_stream.str("");
chargetimepassed_stream << std::fixed << std::setprecision(1) << (float(chargingTimePassed)/1000/60);
chargetimemax_stream.str("");
chargetimemax_stream << std::fixed << std::setprecision(1) << (float(MAX_TIME_ms)/1000/60);
std::string str_temp = "";
//clear the screen first
oled_i2c.clear();
//If no battery present, show screen indicating one should be inserted
if( !batteryPresenceState )
{
oled_i2c.setFont(ArialMT_Plain_16);
oled_i2c.drawString(0, 0, "Insert battery");
str_temp = volts_stream.str() + "v " + temperature_stream.str() + "ºC";
oled_i2c.drawString(0,16, str_temp.c_str() );
}
//battery present, show screen with voltage, CE status, temperature
//Further splits into charge enabled or disabled - if charge enabled, show time so far and time limit
else
{
oled_i2c.setFont(ArialMT_Plain_16);
if(TP4056ChargingState){
str_temp = "Charge to " + max_volts_stream.str();
oled_i2c.drawString(0, 0, str_temp.c_str());
//TODO: shows 'charge complete' if above threshold
} else {
oled_i2c.drawString(0, 0, "Push to charge");
}
str_temp = volts_stream.str() + "v " + temperature_stream.str() + "ºC";
oled_i2c.drawString(0, 16, str_temp.c_str() );
str_temp = "T: " + chargetimepassed_stream.str() + "m/" + chargetimemax_stream.str() + "m";
oled_i2c.drawString(0,16*2, str_temp.c_str() );
//std::string voltage_output = "Voltage: "+std::to_string(bat_voltage);
//str_temp = "Voltage: " + volts_stream.str();
///oled_i2c.drawString(0, 16, str_temp.c_str() );
//str_temp = "Temp: " + temperature_stream.str();
//oled_i2c.drawString(0,16*2, str_temp.c_str() );
}
oled_i2c.display();
ThisThread::sleep_for(250);
}
}
int main()
{
//sCLI.baud(115200);
sCLI.printf("Started\r\n");
//BUILT IN LED THREAD
Thread led1;
//osStatus err = led1.start(&blinkled);
led1.start(blinkled);
// CONFIGURE TP4056 CE CONTROL FOR OPEN DRAIN WITH EXTERNAL 5V PULLUP
//https://forums.mbed.com/t/how-to-configure-open-drain-output-pin-on-stm32/7007
tp4056ChipEnable.mode(OpenDrainNoPull);
// TEMPERATURE INPUT THREAD
Thread temperatureSenseC_thread;
temperatureSenseC_thread.start(TemperatureInputThread);
// CHARGE ENABLE CONTROL THREAD
Thread tp4056_control_thread;
tp4056_control_thread.start(TP4056ControlThread);
// PRESENCE DETECTION THREAD
Thread bat_presence_thread;
bat_presence_thread.start(BatteryPresenceThread);
//OLED 128x64 INIT AND THREAD
//initialize ssd1306 on i2c0
oled_i2c.init();
//oled_i2c.flipScreenVertically();
oled_i2c.setFont(ArialMT_Plain_16);
oled_i2c.drawString(0,0,"init!");
oled_i2c.setBrightness(64);
oled_i2c.display();
Thread oled_thread;
oled_thread.start(oledOutputThread);
// START/STOP CHARGE BUTTON
chargeButton.rise(&buttonISR);
float bat_voltage_min;
float bat_voltage_max;
float bat_voltage_avg_sum;
uint64_t now = Kernel::get_ms_count();
uint64_t lastADCAverageStartTime = now;
uint64_t lastADCReadStartTime = now;
uint64_t time_tmp = 0;
while(true)
{
// reset min and max values
bat_voltage_min = 10.0;
bat_voltage_max = -10.0;
bat_voltage_avg_sum = 0.0;
lastADCAverageStartTime = Kernel::get_ms_count();
for(int i = 0; i < BAT_SAMPLERATE; i++)
{
lastADCReadStartTime = Kernel::get_ms_count();
bat_voltage = readVoltageDivider_Calibrated();
vddref_voltage = readRefVoltage();
bat_voltage_avg_sum += bat_voltage;
if( bat_voltage < bat_voltage_min )
{
bat_voltage_min = bat_voltage;
}
if( bat_voltage > bat_voltage_max )
{
bat_voltage_max = bat_voltage;
}
//sleep appropriate interval to meet specified sample rate, evenly spaced over 1s
//This sleep is also where the other threads run
//sleep amount calculated as: (nominal time between bat voltage reads) - (time it has taken to do the current measurement)
time_tmp = 1000/BAT_SAMPLERATE - (lastADCReadStartTime - Kernel::get_ms_count()) ;
//if we have already exceeded our time window, sleep nominal time as sample rate is not possible to meet
if( time_tmp < 0 )
{
ThisThread::sleep_for(1000/BAT_SAMPLERATE);
}
else
{
//otherwise, sleep the remaining time in our window to try to exactly meet our sample rate
ThisThread::sleep_for(time_tmp);
}
}
now = Kernel::get_ms_count();
bat_voltage_avg = bat_voltage_avg_sum / BAT_SAMPLERATE;
//TODO: Dedicated serial thread, output messages when conditions change (eg OTP, OVP, charge started/stopped)
sCLI.printf("\r\nADC0 Last Reading: %6.4f\r\n", bat_voltage);
sCLI.printf("ADC0 1s min: %6.4f\r\n", bat_voltage_min);
sCLI.printf("ADC0 1s max: %6.4f\r\n", bat_voltage_max);
sCLI.printf("ADC0 1s avg: %6.4f\r\n", bat_voltage_avg);
sCLI.printf("ADC0 time (nominal 1000ms): %llu\r\n", (now-lastADCAverageStartTime));
sCLI.printf("VDDREF Reading: %6.4f\r\n", vddref_voltage);
sCLI.printf("Temp: %6.4f\r\n", temperatureSenseC);
sCLI.printf("Bat present: %d\r\n", batteryPresenceState);
}
}