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-08-28
Revision:
8:c781f4ae7eb3
Parent:
5:c07438005b16
Child:
9:272f6963c3b8

File content as of revision 8:c781f4ae7eb3:

#include "mbed.h"
#include "platform/mbed_thread.h"

#include <string>
#include <iomanip>
#include <sstream>

#include "SSD1306I2C.h"
#include "MCP9808.h"

#define OLED_ADR 0x3C
#define OLED_SDA I2C_SDA
#define OLED_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

Serial pc(USBTX,USBRX);

SSD1306I2C oled_i2c(OLED_ADR, OLED_SDA, OLED_SCL);

//DS1820 ds1820_sensor(DS18B20_DATA);
//sda,scl,address,freq
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);

DigitalInOut toggleOut(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

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 ds18b20_temperature = -301.0;

//Configurable protection conditions
float MAX_VOLTAGE = 4.23; //default: no more than 4.25v
float MIN_VOLTAGE = 2.5; //default: no less than 2.5v
float MIN_DETECT_VOLTAGE = 0.5; //default: no less than 0.3v
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(Kernel::get_ms_count() - 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();
    
    //pc.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 );

        //pc.printf ( "T: %0.4f C\r\n", myMCP9808_Data.t_a );
        ds18b20_temperature = myMCP9808_Data.t_a;
        
        ThisThread::sleep_for(250);
    }
    

}

// Detect battery presence by voltage activity
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 = ( ( ds18b20_temperature < MAX_TEMPERATURE ) && ( ds18b20_temperature > 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)
            {                
                if( voltage_en && temperature_en && presence_en && buttonPressed )
                {
                    enableCharging = true;
                    chargeStartTime = Kernel::get_ms_count();
                    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
                toggleOut.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
            {
                toggleOut.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) << ds18b20_temperature;
        
        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()
{
    pc.baud(9600);
    pc.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
    toggleOut.mode(OpenDrainNoPull);
    
    // TEMPERATURE INPUT THREAD
    Thread ds18b20_temperature_thread;
    ds18b20_temperature_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.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 lastADCStartTime = now;
    
    while(true)
    {
        // reset min and max values
        bat_voltage_min = 10.0;
        bat_voltage_max = -10.0;
        bat_voltage_avg_sum = 0.0;
        
        lastADCStartTime = Kernel::get_ms_count();
        for(int i = 0; i < BAT_SAMPLERATE; i++)
        {
            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
            ThisThread::sleep_for(1000 / BAT_SAMPLERATE);
            
        }
        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)
        pc.printf("\r\nADC0 Reading: %6.4f\r\n", bat_voltage);
        pc.printf("ADC0 1s min: %6.4f\r\n", bat_voltage_min);
        pc.printf("ADC0 1s max: %6.4f\r\n", bat_voltage_max);
        pc.printf("ADC0 1s avg: %6.4f\r\n", bat_voltage_avg);
        pc.printf("ADC0 time (nominal 1000ms): %llu\r\n", (now-lastADCStartTime));
        pc.printf("VDDREF Reading: %6.4f\r\n", vddref_voltage);
        pc.printf("Temp: %6.4f\r\n", ds18b20_temperature);
        pc.printf("Bat present: %d\r\n", batteryPresenceState);

    }
    
}