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:
- 9:272f6963c3b8
- Parent:
- 8:c781f4ae7eb3
File content as of revision 9:272f6963c3b8:
#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); } }