Arduino Nano project does not work when powered from battery

Hi,

I have an Arduino project that works perfectly fine when powered from the Arduino's USB port, but when powered from battery it only gets as far as the splash screen on the OLED display.

Parts:
Arduino Nano V3
ADS1115 A/D converter
SSD1306 OLED display
Push button
Buzzer

Battery: 3.7V 18650
TP4056 module to charge battery
DC-DC Boost module to convert battery's 3.7V to 5V
Adjustable DC-DC Step-up convertor to boost 5V to (about) 7.5V

I realize that the power setup is probably overkill, but hey ;-)

The whole circuit in Fritzing diagram here: https://drive.google.com/file/d/1WjLesnqIslUdPBz8YRf5kduhKf_54yvo/view?usp=drive_link

I have measured all the connections, they are fine. 7.5V is delivered to the Arduino. It powers up, OLED splash screen is displayed, then it seems to hang.

Any thoughts or ideas?

Kind regards,
Eelco

A couple of things come to mind:

  1. A Fritzing project (I assume all those files are one project) is no substitute for a proper schematic included with your post. Most folks don't use Fritzing. I certainly don't. So I can't tell if there's any problems with your schematic.
  2. I'd look at your code to see if there was anything that jumped out at me, but you didn't see fit to include it. So I can't tell if there's any problems with your code.

Include a proper schematic in a reply, and your complete sketch formatted with the <CODE/> formatting tool clearly visible in the message reply box, and I'm sure someone will be able to help.

What is the difference in your setup() code logic when powered by USB and when powered by battery? Just the logic part, please.

Waiting on USB?

Hi,

Appreciate the responses and I apologize for leaving out detail.

The schematic is here:

The code is included below. I did not include it initially since it runs as expected when the entire thing is powered through the USB port on the Arduino. There is no logic in setup() to differentiate between USB power and battery power.

Kind regards,
Eelco

/*****************************************************************************
* ej's o2 oled analyzer - v0.21
* http://ejlabs.net/arduino-oled-nitrox-analyzer
*
* License
* -------
*   This program is free software: you can redistribute it and/or modify
*   it under the terms of the GNU General Public License as published by
*   the Free Software Foundation, either version 3 of the License, or
*   (at your option) any later version.
*
*   This program is distributed in the hope that it will be useful,
*   but WITHOUT ANY WARRANTY; without even the implied warranty of
*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*   GNU General Public License for more details.
*
*   You should have received a copy of the GNU General Public License
*   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*
*****************************************************************************/

// prototypes:
unsigned int EEPROMReadInt(int p_address);
int calibrate(int x);
// end prototypes

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_ADS1X15.h>
#include <EEPROM.h>
#include <RunningAverage.h>
// #include <splash.h>

#define EE_DEBUG true // true for printing various debugging messages to Serial, false otherwise
int ee_debug_cnt = 0; // used to control some messaging in debug mode

#define RA_SIZE 20
RunningAverage RA(RA_SIZE);

// ADS1115 creation
Adafruit_ADS1115 ads;

// OLED definitions and declaration for new Adafruit_SSD1306 constructors.
// from: https://github.com/adafruit/Adafruit_SSD1306/blob/master/examples/ssd1306_128x64_i2c/ssd1306_128x64_i2c.ino
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET    -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// // OLED definitions and declarations for Adafruit_SH110X contructors
// // Uncomment the initialize the I2C address , uncomment only one, 
// // If you get a totally blank screen try the other
// #define i2c_Address 0x3c //initialize with the I2C addr 0x3C Typically eBay OLED's
                         // e.g. the one with GM12864-77 written on it
// //#define i2c_Address 0x3d //initialize with the I2C addr 0x3D Typically Adafruit OLED's

// #define SCREEN_WIDTH 128 // OLED display width, in pixels
// #define SCREEN_HEIGHT 64 // OLED display height, in pixels
// #define OLED_RESET -1   //   QT-PY / XIAO
// Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);


// Define pins used
const int buttonPin = 2; // push button
const int buzzerPin = 9; // buzzer
const int ledPin = 13; // led

// Define button press durations in seconds
const int cal_holdTime = 2; // 2 sec button hold to calibration
const int mod_holdTime = 4; // 4 sec hold to po2 mod change
const int max_holdtime = 6; // 6 sec hold to reset max o2 result

// Define variables
double calibrationv; // Calibration return value?
float multiplier;    // Read-out multiplier to convert mV into percent

long millis_held;    // How long the button was held (milliseconds)
long secs_held;      // How long the button was held (seconds)
long prev_secs_held; // How long the button was held in the previous check
byte previous = HIGH;
unsigned long firstTime; // how long since the button was first pressed 
int active = 0;
double result_max = 0;

//
// Calculate MOD (Maximum Operating Depth)
// Inputs:
//   percentage: current O2 percentage in gas
//   ppo2: desired max ppo2 (default to 1.4 bar) 
//
float max_po1 = 1.30;   // lowest ppo2 pressure for MOD calculcation (in bar)
const float max_po2 = 1.60;

float cal_mod (float percentage, float ppo2 = 1.40) {
  return 10 * ( (ppo2/(percentage/100)) - 1 );
}

//
// make beep for x time (seconds, default 1s)
void beep(int x=1) {  
  //digitalWrite(ledPin, HIGH); // led blink disable for battery save
  for(int i=0; i<x; i++) {    
      tone(buzzerPin, 2800, 100);
      delay(200);    
  }
  //digitalWrite(ledPin, LOW);
  noTone(buzzerPin);
}

//
// Read O2 sensor and update RunningAverage
// Input:
//    adc: ADS channel, default 0
//
void read_sensor(int adc=0) {  
  int16_t millivolts = 0;
  millivolts = ads.readADC_Differential_0_1();
  RA.addValue(millivolts);
}

//
// Write to EEPROM
// Inputs:
//    p_address: EEPROM address to write to
//    p_value: value to store
//
void EEPROMWriteInt(int p_address, int p_value)
     {
     byte lowByte = ((p_value >> 0) & 0xFF);
     byte highByte = ((p_value >> 8) & 0xFF);

     EEPROM.write(p_address, lowByte);
     EEPROM.write(p_address + 1, highByte);
     }

//
// Read from EEPROM
// Input:
//    p_address: address to read from
//
unsigned int EEPROMReadInt(int p_address)
     {
     byte lowByte = EEPROM.read(p_address);
     byte highByte = EEPROM.read(p_address + 1);

     return ((lowByte << 0) & 0xFF) + ((highByte << 8) & 0xFF00);
     }

//
// Sensor calibration routine
// Inputs:
//    x: EEPROM address
//
int calibrate(int x) {
  
  // Clear display and print calibrating message
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setCursor(0,0);  
  display.setTextSize(2);
  display.print(F("Calibrate"));
  display.display();
    
  // Clear RunningAverage buffer, get new readout and save to EEPROM;
  double result;  
  for(int cx=0; cx<= RA_SIZE; cx++) {
    read_sensor(0);
  }
  result = RA.getAverage();
  result = abs(result);   // absolute value so works irrespective of the polarity of the sensor connection
  EEPROMWriteInt(x, result); // write to eeprom

  // Beep when done
  beep(1);
  
  // Wait 1 second
  delay(1000);
  active = 0;
  return result;
}

//
// Main gas analysis routine, including menu
// Inputs:
//    x:
//    cal: calibration reading for ambient air
//
void analysing(int x, int cal) {
  double currentmv=0;
  double result;
  double mv = 0.0;

  if(EE_DEBUG && ee_debug_cnt == 0){ Serial.println(F("Main analysing fn")); }

  read_sensor(0);
  currentmv = RA.getAverage();  // read (average) current sensor value
  currentmv = abs(currentmv);   // absolute value so works irrespective of sensor polarity
  
  // Calculate current O2 % based on readout
  result = (currentmv / cal) * 20.9;
  if (result > 99.9) result = 99.9;
  mv = currentmv * multiplier;
 
  // Clear display and print current O2 %
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setCursor(0,34);
  
  if (mv < 0.02 || result <= 0) {
    // Error for too-low sensor readout value
     display.setTextSize(2);
     display.println(F("Sensor"));
     display.print(F("Error!"));
  } else {
//    display.setCursor(0,32);
    display.setTextSize(4);
    display.print(result,1);
    display.println(F("%"));

    // Increment result_max is readout still increasing
    if (result >= result_max) {
      result_max = result;
    }
    
    display.setTextSize(1);
    display.setCursor(0,0);
    display.setTextColor(BLACK, WHITE);    
    display.print(F("Max "));
    display.print(result_max,1);
    display.print(F("%   "));    
    // Print sensor mV reading as well (to check sensor health)
    display.setCursor(70,0);
    display.print(mv,2);    
    display.print(F("mV "));
     
    if (active % 4) {
      display.setCursor(120,0);
      display.setTextColor(WHITE);
      display.print(F("."));
    }  
    
    // Print MOD based on current O2 %
    display.setTextColor(WHITE);
    display.setCursor(0,8);
    display.print(F("ppO2 "));
    display.print(max_po1,1);
    display.print(F(" / "));
    display.print(max_po2,1);
    display.print(F(" MOD"));

    display.setTextSize(2);
    display.setCursor(0,16);
    display.print(cal_mod(result,max_po1),1);
    display.print(F("/"));
    display.print(cal_mod(result,max_po2),1);
    display.print(F("m "));
    
    // menu: switch between modes based on duration of button press
    if (secs_held < 5 && active > 16) {
      display.setTextSize(2);
      display.setCursor(0,0);
      display.setTextColor(BLACK, WHITE);      
      if (secs_held >= cal_holdTime && secs_held < mod_holdTime) {
        display.print(F("   CAL    "));
      }
      if (secs_held >= mod_holdTime && secs_held < max_holdtime) {
        display.print(F("   PO2    "));
      }
      if (secs_held >= max_holdtime && secs_held < 10) {
        display.print(F("   MAX    "));
      }     
    }  

  }
  display.display();

  if(EE_DEBUG){ ee_debug_cnt++; }

} // end analysing()

//
// Lock screen 
// Inputs:
//    pause: timeout to (un-)lock, in ms, defautl 5000ms
//
void lock_screen(long pause = 5000) {
  // beep(1);
  display.setTextSize(1);
  display.setCursor(0,0);  
  display.setTextColor(0xFFFF, 0);
  display.print(F("                "));
  display.setTextColor(BLACK, WHITE);
  display.setCursor(0,0);
  display.print(F("======= LOCK ======="));
  display.display();
  for (int i = 0; i < pause; ++i) {   
    while (digitalRead(buttonPin) == HIGH) {
      }
   }
   active = 0;
} // end lock_screen()

//
// Cycle between Max pPO2 values and confirm selected value on-screen
//
void po2_change() {  
  if (max_po1 == 1.30) max_po1 = 1.40;
  else if (max_po1 == 1.40) max_po1 = 1.50;
  else if (max_po1 == 1.50) max_po1 = 1.60;
  else if (max_po1 == 1.60) max_po1 = 1.30;

  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setCursor(0,0);  
  display.setTextSize(2);
  display.println(F("pO2 set"));
  display.print(max_po1);
  display.display();
  // beep(1);   
  delay(1000);
  active = 0;  
} // end po2_change()


void max_clear() {
  result_max = 0;
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setCursor(0,0);  
  display.setTextSize(2);
  display.println(F("Max result"));
  display.print(F("cleared"));
  display.display();
  // beep(1);   
  delay(1000);
  active = 0;
}

// 
// Setup logic at startup
void setup(void) {  

	Serial.begin(9600);

  if(EE_DEBUG){ Serial.println(F("Begin setup")); }

  delay(500); // wait for screen

  /* power saving stuff for battery power */
  // Disable ADC
  // ADCSRA = 0;
  // Disable the analog comparator by setting the ACD bit
  // (bit 7) of the ACSR register to one.
  // ACSR = B10000000;
  // Disable digital input buffers on all analog input pins
  // DIDR0 = DIDR0 | B00111111;

  // Initialize screen
  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS); 
  // if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
  //   Serial.println(F("SSD1306 allocation failed"));
  //   for(;;); // loop forever
  // }
  if(EE_DEBUG) {
    Serial.println(F("Display initialized"));
    display.display(); // splash screen
    delay(200); // show for 0.2s
  }

  // ADS1115 set gain and initialize
  ads.setGain(GAIN_TWO);
  multiplier = 0.0625F;
  ads.begin();  // ads1115 start
  
  pinMode(buttonPin,INPUT_PULLUP);  
  
  // Clear RunningAverage
  RA.clear();
  for(int cx=0; cx<= RA_SIZE; cx++) {
     read_sensor(0);
  }

  // Calibrate sensor: start by reading last from EEPROM
  calibrationv = EEPROMReadInt(0);  
  if (calibrationv < 100) {
    calibrationv = calibrate(0);
  }
  
  // Beep when done
  beep(1);

  if(EE_DEBUG){ Serial.println(F("End setup")); }

} // end setup()

//
// Main loop while active
void loop(void) {

  if(EE_DEBUG && ee_debug_cnt==0){ Serial.println(F("Starting main loop()")); }
  
  // Read button and measure duration held pressed
  int current = digitalRead(buttonPin);
 
  if (current == LOW && previous == HIGH && (millis() - firstTime) > 200) {
    firstTime = millis();
    active = 17;
  }

  millis_held = (millis() - firstTime);
  secs_held = millis_held / 1000;

  // Menu logic
  if (millis_held > 2) {
    if (current == HIGH && previous == LOW) {
      if (secs_held <= 0) {
        lock_screen();
      }
      if (secs_held >= cal_holdTime && secs_held < mod_holdTime) {        
        calibrationv = calibrate(0);
      }
      if (secs_held >= mod_holdTime && secs_held < max_holdtime) {
        po2_change();
      }
      if (secs_held >= max_holdtime && secs_held < 10) {
        max_clear();
      }
    }
  }

  previous = current;
  prev_secs_held = secs_held;
  
  analysing(0,calibrationv);
  delay(200);
    
  active++;

  if(EE_DEBUG){ ee_debug_cnt++; }

} // end main loop()

Couple more comments on the schematic:

18650 battery is wired to a TP4056 (B+/B- terminals). There is a switch in the + wire turn turn battery power on/off.
The TP4056 OUT+/OUT- terminals are wired to the IN+/IN terminals on the DC-DC Booster.
The OUT+/OUT- of the Booster are wired to the IN+/IN- of the adjustable step-up module. This is adjusted to just over 7.5v (checked with a multimeter).
The OUT+/OUT- are wired to VIN/GND on the Arduino.

On the Arduino:
A4 to SDA on ADS1115 and SSD1306
A4 to SCL on ADS1115 and SSD1306
VIN/GND to VCC and GND on the ADS1115 and SSD1306.

Pushbutton wired to D2/GND on the Arduino.
Buzzer wired to D9/GND on the Arduino.

Sensor wired to A0/A1 on the ADS1115.

Thats it.

THanks,
Eelco

But there is debug capability. Where does your program stop when using debug? You have done that, right????

You may be running out of SRAM memory at this step, but you turned off the error check:

  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS); 
  // if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
  //   Serial.println(F("SSD1306 allocation failed"));
  //   for(;;); // loop forever
  // }

If so, the behavior will be completely unpredictable.

Post the memory statistics reported during upload.

If EE_DEBUG is defined it just writes messages to Serial, nothing else.

I can remove that.

Thanks.
Eelco

Thanks.

Might be, but again: this thing runs perfectly fine when powered from the Arduino's USB port.

Kind regards,
Eelco

Aside: the second boost converter is a waste of energy and battery life. The system will run on 5V.

Thanks - I suspected as much.

I'll remove it at some point, it was added out of over-caution.

KR,
Eelco

OK, that was it: I put an "if(Serial)" around the writes to Serial and now it runs off battery.

Many thanks!
Eelco