ATMega328p with DHT22 and SSD1306, code help?

Hi I am trying to make a project that uses DHT22 (Temperature & Humidity Sensor) and SSD1306 (OLED Display) with the ATMega328p. I want the OLED Display to be normally off and should turn on for 5 seconds when a tactile push button is pressed. I also want the DHT22 to only record data every 30 seconds. I want to minimize the power consumption.

Below is my circuit schematic

Below is my code:

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>

#define SCREEN_WIDTH 128 // OLED display width,  in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

#define DHTPIN 2 // pin connected to DHT22 sensor
#define DHTTYPE DHT22

#define SCREEN_BTN_INPUT 3 // pin connected to push button to turn screen on on click

Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); // create SSD1306 display object connected to I2C
DHT dht(DHTPIN, DHTTYPE);

String tempString;
String humString;
unsigned long prevTimeDisplay = 0;
int displayOn = 0;

void initialize_display()
{
  // initialize OLED display with address 0x3C for 128x64
  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C))
  {
    Serial.println(F("SSD1306 allocation failed"));
    while (true)
      ;
  }
  oled.clearDisplay();      // clear display
  oled.setTextSize(2);      // text size
  oled.setTextColor(WHITE); // text color
  oled.setCursor(0, 10);    // position to display
}

void oledDisplayCenter(String temp, String hum)
{
  int16_t x1;
  int16_t y1;
  uint16_t width;
  uint16_t height;

  oled.getTextBounds(temp, 0, 0, &x1, &y1, &width, &height);

  // display on horizontal and vertical center
  oled.clearDisplay(); // clear display
  oled.setCursor((SCREEN_WIDTH - width) / 2, SCREEN_HEIGHT / 2 - height - 2);
  oled.println(temp); // text to display
  oled.setCursor((SCREEN_WIDTH - width) / 2, SCREEN_HEIGHT / 2 + 2);
  oled.println(hum); // text to display
  oled.display();
}

void setup()
{
  pinMode(DHTPIN, INPUT);                  // input of DHT22 sensor data
  pinMode(SCREEN_BTN_INPUT, INPUT_PULLUP); // input when button pressed
  Serial.begin(9600);
  dht.begin();            // initialize DHT22 the temperature and humidity sensor
  tempString.reserve(10); // to avoid fragmenting memory when using String
  humString.reserve(10);  // to avoid fragmenting memory when using String
  initialize_display();
  oled.ssd1306_command(0xAE);
}

void loop()
{
  if (displayOn == 0 && digitalRead(SCREEN_BTN_INPUT) == LOW)
  {
    displayOn = 1;
    prevTimeDisplay = millis();
    oled.ssd1306_command(0xAF);
  }
  if (displayOn == 1 && digitalRead(SCREEN_BTN_INPUT) == HIGH)
  {
    if (millis() > prevTimeDisplay + 5000)
    {
      displayOn = 0;
      oled.ssd1306_command(0xAE);
    }
  }

  float hum = dht.readHumidity();     // read humidity
  float temp = dht.readTemperature(); // read temperature

  // check if any reads failed
  if (isnan(temp))
  {
    tempString = "Error";
  }
  else
  {
    tempString = "T: " + String(temp, 1) + " C";
  }
  if (isnan(hum))
  {
    humString = "Error";
  }
  else
  {
    humString = "H: " + String(hum, 1) + " %";
  }
  Serial.println(tempString + " , " + humString);

  oledDisplayCenter(tempString, humString); // display temperature and humidity on OLED
  // delay(30000);
}

As you can see currently I have commented out the delay of 30 seconds and the code is working. But if I remove the code of delay, it does not function properly, because it is not able to turn the display on as the code is predominantly in the delay phase.

I have also tried to use interrupts for this, but the OLED display does not sleep in the interrupt code

oled.ssd1306_command(0xAE); //this does not work in interrupt subroutine

How can I achieve this, can someone guide me?

Hi and thanks for posting code correctly and posting a schematic.

Run the atmega at 3.3V. Swap out the DHT22 for a SHT21. These can share the i2c bus with the OLED and will run at 3.3V. No need for crystal, use the internal 8MHz oscillator.

R1 is incorrectly placed in your schematic, should be between RESET pin and Vcc.

It is placed correctly in my opinion, it is between RESET and VCC, still if there is any mistake let me know. The physical circuit prototype is working.

Thanks those are optimizations I can do. The main intention of running it at 5V is I will later also connect SIM800 and a SD card module to transmit data to an API. SIM800 uses 4.2V, I just though using a Voltage regulator to step down from 5V to 3.3V would be easier than stepping up?

Apart from this, how can I make the code work to do whats intended?

Using an interrupt would only be appropriate if you were putting the atmega into sleep mode. I would recommend doing that to save power, but first let us help you get your current code working.

Sorry about that, you are correct, the 10K is between RESET and Vcc. It is C4 that is connected incorrectly. It should connect directly to RESET, not via the 10K.

Yes you're right, thanks I will fix it, I have wired it correctly, made a mistake in the schematic

SD card modules also operate at 3.3V. However, many can be powered with 5V and have on-board voltage regulators and level shifting circuits. These circuits have a fault in the design on most boards and can be a problem if you intend to connect any other devices to the SPI bus. If you plan to run the atmega at 3.3V, try to find a 3.3V-only SD card module.

If you are logging remotely, why use an SD card at all? SD cards can use a lot of power.

I can see a problem ahead. The Adafruit_SSD1306 library uses a lot of RAM memory: half of all the memory that an atmega328 has. SD card library also uses a lot of RAM memory. Often projects that use both run into problems because the atmega runs out of RAM and begin to misbehave in strange ways.

if (millis() > prevTimeDisplay + 5000)

I don't think this is the cause of your current problem, but the above code will cause another problem when millis() rolls over after almost 50 days. To avoid that problem, correct it like this:

if (millis() - prevTimeDisplay > 5000)

You need to use millis() to do this also, not delay(), as you have done for the 5s timing for the OLED.

Make another unsigned long variable to record the value of millis() when the data is recorded. Only read and record new data when 30000ms had elapsed.

Thanks for your suggestion, the following code worked

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>

#define SCREEN_WIDTH 128 // OLED display width,  in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

#define DHTPIN 2 // pin connected to DHT22 sensor
#define DHTTYPE DHT22

#define SCREEN_BTN_INPUT 3 // pin connected to push button to turn screen on on click

#define DISPLAY_TIMEOUT 2000
#define DHT_TIMEOUT 30000

Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); // create SSD1306 display object connected to I2C
DHT dht(DHTPIN, DHTTYPE);

String tempString;
String humString;
unsigned long prevTimeDisplay = 0;
unsigned long prevTimeDHT = 0;
int displayOn = 0;
int startFlag = 1;

void initialize_display()
{
  // initialize OLED display with address 0x3C for 128x64
  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C))
  {
    Serial.println(F("SSD1306 allocation failed"));
    while (true)
      ;
  }
  oled.clearDisplay();      // clear display
  oled.setTextSize(2);      // text size
  oled.setTextColor(WHITE); // text color
  oled.setCursor(0, 10);    // position to display
}

void oledDisplayCenter(String temp, String hum)
{
  int16_t x1;
  int16_t y1;
  uint16_t width;
  uint16_t height;

  oled.getTextBounds(temp, 0, 0, &x1, &y1, &width, &height);

  // display on horizontal and vertical center
  oled.clearDisplay(); // clear display
  oled.setCursor((SCREEN_WIDTH - width) / 2, SCREEN_HEIGHT / 2 - height - 2);
  oled.println(temp); // text to display
  oled.setCursor((SCREEN_WIDTH - width) / 2, SCREEN_HEIGHT / 2 + 2);
  oled.println(hum); // text to display
  oled.display();
}

void setup()
{
  pinMode(DHTPIN, INPUT);                  // input of DHT22 sensor data
  pinMode(SCREEN_BTN_INPUT, INPUT_PULLUP); // input when button pressed
  Serial.begin(9600);
  dht.begin();            // initialize DHT22 the temperature and humidity sensor
  tempString.reserve(10); // to avoid fragmenting memory when using String
  humString.reserve(10);  // to avoid fragmenting memory when using String
  initialize_display();
  oled.ssd1306_command(0xAE);
}

void loop()
{
  if (displayOn == 0 && digitalRead(SCREEN_BTN_INPUT) == LOW)
  {
    displayOn = 1;
    prevTimeDisplay = millis();
    oled.ssd1306_command(0xAF);
    oledDisplayCenter(tempString, humString); // display temperature and humidity on OLED
  }
  if (displayOn == 1 && digitalRead(SCREEN_BTN_INPUT) == HIGH)
  {
    if (millis() - prevTimeDisplay > DISPLAY_TIMEOUT)
    {
      displayOn = 0;
      oled.ssd1306_command(0xAE);
    }
  }

  if (startFlag || (millis() - prevTimeDHT > DHT_TIMEOUT))
  {
    startFlag = 0;
    prevTimeDHT = millis();
    float hum = dht.readHumidity();     // read humidity
    float temp = dht.readTemperature(); // read temperature

    // check if any reads failed
    if (isnan(temp))
    {
      tempString = "Error";
    }
    else
    {
      tempString = "T: " + String(temp, 1) + " C";
    }
    if (isnan(hum))
    {
      humString = "Error";
    }
    else
    {
      humString = "H: " + String(hum, 1) + " %";
    }
    Serial.println(tempString + " , " + humString);
  }
}

Will this save any power. Also how do you suggest to put the microcontroller in low power mode and use interrupts?

SDA is connected to 5V in your schematic.

Read:
https://www.gammon.com.au/interrupts
https://www.gammon.com.au/power

Corrected

When you make corrections, don't do it to your original posts. It erases thread history and makes it hard to follow.

The following schematic (Fig-1) of OLED Display Unit says (your schematic of post #1 says reverse) ---
GND : Pin-1
Vcc: Pin-2
SCL: Pin-3
SDA: Pin4

oledSchematc
Figure-1:

No.

The first thing to do is measure what current your circuit and sketch are using now.

Try this at the end of loop().

  set_sleep_mode (SLEEP_MODE_IDLE);  
  sleep_mode();

This is the lightest of the sleep modes, so will not save a large amount of current, but it will allow millis() to work normally, so your current sketch should continue to work without changes.

Also try SLEEP_MODE_ADC.

You will need to add

#include <avr/sleep.h>

at the top of your sketch.

To use deeper sleep modes, your sketch will need to change a lot because millis() will no longer work. Try the above changes and let the forum know what current savings you measure.

Nowhere in these discussions have you discussed your power-budget ... plenty of dancing around sleep, blanking the OLED, and so forth: but you need to become serious and create a list of components, their On VI, their Sleep VI, and primary/secondary battery source. Using a linear regulator to drop 5V to 3.3V is just an engineering crime (hint: DC-DC buck.)

Only when you understand the power budget on the system components can you achieve a good/quality low power design.

Ray

Read Gammon's AVR sleep articles linked previously.
Using that and other resources, I created a tiny85 project that ran from a 3V lithium battery (published as "Hot Yet?" early in the previous decade. A NTC would sense the water spout and change a tri-colored LED: cold, warn, too hot.)

/* 
   M. Ray Burnette 20140210 Open Source: for "Hot Yet?" publication
   Coding Badly Tiny core: https://code.google.com/p/arduino-tiny/
   Binary sketch size: 2,852 bytes (of a 8,192 byte maximum)
   Arduino 1.0.5 No bootloader used.  Upload via ISP.
   Project 3.3V Attiny85 @8MHz internal,  under 10mA idle at 68F

   Schematic:
   x-----------------/\/\/\/\/\/\/\/---------------x---/\/\/\/\/\/\/\----xGND
   |                NTC 10K Thermistor             |  10K 1% Resistor
   |                                               |
   |              ATTINY85 / ARDUINO               |
   |                     +-\/-+                    |
   |    Ain0 (D 5) PB5  1|    |8  Vcc              |
   x--- Ain3 (D 3) PB3  2|    |7  PB2 (D 2) Ain1 --- 
(Blue)- Ain2 (D 4) PB4  3|    |6  PB1 (D 1) pwm1  (Green) ----------|<---x
   |               GND  4|    |5  PB0 (D 0) pwm0  (Red) ------------|<---x
   |                     +----+                                          |    100 Ohm
   x----------------------------------------------------------------|<---x---/\/\/\/\/---Vcc 3.3V

*/
#include <avr/sleep.h>
#include <avr/power.h>
#include <avr/wdt.h>

boolean flag_wdt = 1;

int pinT = PB3;           // Thermistor source voltage
int pinR = PB0;           // Digital pin #0  Red
int pinG = PB1;           // Digital pin #1  Green
int pinB = PB4;           // Digital pin #4  Blue
int r; int g; int b;
const int nToSleep = 50 ; // # of stable temp readings before sleep
const int Delay =    100; // main loop delay in mS
double ADCcount;
double ADCprevious;
int nCount;
int ThermistorPin  = 1 ;  // A1 is physical pin #7 (PB2)

void setup()
{
  // WDTO_15MS, WDTO_30MS, WDTO_60MS, WDTO_120MS, WDTO_250MS, WDTO_500MS, 
  // WDTO_1S, WDTO_2S, WDTO_4S, WDTO_8S
  setup_watchdog(WDTO_4S);  // Periodic Heartbeat to awaken deep sleep()
  sleep_disable();
  pinMode(pinT, OUTPUT); digitalWrite(pinT, HIGH);  // Thermistor Source
  pinMode(pinR, OUTPUT);
  pinMode(pinG, OUTPUT);
  pinMode(pinB, OUTPUT);
}
  
void loop() 
{
  wdt_reset();    // pat K9
  ADCcount = analogRead(ThermistorPin) ;
  if (ADCcount == ADCprevious) ++nCount;
  if ( nCount > nToSleep )
  { // prepare for low current power-down state
    pinMode(pinR, INPUT); digitalWrite(pinR, HIGH);  // pullup enabled
    pinMode(pinG, INPUT); digitalWrite(pinG, HIGH);
    pinMode(pinB, INPUT); digitalWrite(pinB, HIGH);
    SleepLonger:    // Come here to re-sleep
    pinMode(pinT, INPUT); digitalWrite(pinT, HIGH);
      system_sleep();
      sleep_disable();  // deep sleep until WDT kicks
      pinMode(pinT, OUTPUT); digitalWrite(pinT, HIGH);
      delay(50);
      // Yawn, exercise a few reads for stabilization
      for (uint8_t z=0; z<5; z++) {
        ADCcount = analogRead(ThermistorPin) ;
      }
      if (abs(ADCcount - ADCprevious) < 4) goto SleepLonger;  // hysteresis
    // restore LED output drivers ... temp has gone up
    pinMode(pinR, OUTPUT); digitalWrite(pinR, HIGH);
    pinMode(pinG, OUTPUT); digitalWrite(pinG, HIGH);
    pinMode(pinB, OUTPUT); digitalWrite(pinB, HIGH);
    nCount = 0;
  } else {
  // 261 = 32F, 447 = 64F, 537 = 75F, 575 = 82F
  b = map(ADCcount, 261,  447, 100, 255 );
  g = map(ADCcount, 435,  574, 250, 100);  // overlap green & blue
  r = map(ADCcount, 575, 1023, 250,  50);

  if (ADCcount > 574)              // HOT: ADCcount goes up with increase temperature
    {
      // Show only Red when Hot with Red intensity increasing with temperature
      analogWrite(pinR,   r);
      analogWrite(pinG, 255);      // 255 = 100% High == NO LED Current Common Anode --> Vcc
      analogWrite(pinB, 255);      // Blue Off
    } else {                       // Cold to Cool transition with Blue fading into Green
      analogWrite(pinR, 255);      // Red Off
      analogWrite(pinG, g);
      analogWrite(pinB, b);        // Brighter Blue with colder temp
    }
  }
  ADCprevious = ADCcount;
  delay(Delay);
}

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
// http://www.insidegadgets.com/wp-content/uploads/2011/02/ATtiny85_watchdog_example.zip
void system_sleep()
{
    cbi(ADCSRA,ADEN);                    // switch Analog to Digitalconverter OFF
    power_all_disable ();                // power off ADC, Timer 0 and 1, serial interface
    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
    noInterrupts ();                     // timed sequence coming up
    sleep_enable();
    interrupts ();                       // interrupts are required now
    sleep_mode();                        // System sleeps here
    sleep_disable();                     // System continues execution here when watchdog timed out
    power_all_enable ();                 // power everything back on
    sbi(ADCSRA,ADEN);                    // switch Analog to Digitalconverter ON
}

// 0=16ms, 1=32ms, 2=64ms, 3=128ms, 4=250ms, 5=500ms, 6=1 sec,7=2 sec, 8=4 sec, 9= 8sec
void setup_watchdog(int ii)
{
  byte bb;
  int ww;
  if (ii > 9 ) ii=9;
  bb=ii & 7;
  if (ii > 7) bb|= (1<<5);
  bb|= (1<<WDCE);
  ww=bb;

  MCUSR &= ~(1<<WDRF);
  // start timed sequence
  WDTCR |= (1<<WDCE) | (1<<WDE);
  // set new watchdog timeout value
  WDTCR = bb;
  WDTCR |= _BV(WDIE);
}
  
// Watchdog Interrupt Service / is executed when watchdog timed out
ISR(WDT_vect) {
     // wdt_disable();  // disable watchdog
}

```6

I fixed this. My OLED Display has VCC as pin1 and GND as pin2

I have also added a switching regulator (LM2576) to convert the supply between 6V - 9V from 6 AA batteries as power supply. I know AA batteries are not ideal but ease of use and availability is very important so the AA batteries can be replaced easily and are readily available.

Before adding power savings (SLEEP_MODE_IDLE), current draw from the batteries was 88mA and after the power savings the current draw from the batteries is 74mA. To further improve this I will use Gammon's AVR sleep article. Thanks for the help.

Not necessarily. If the awake time is short and the asleep time is long, then it can be more important to achieve a low current during sleep, even if the circuit is a little more wasteful when awake.

Some low drop-out linear regulators have a very low quiescent current. Quiescent current is the current a regulator uses for itself, even without an attached circuit. For example mcp1700 has a quiescent current of only a few uA.

Most buck regulators have a much higher quiescent current, more like 1mA, because their internal circuits are more complex. So while they may be more efficient at higher output currents, they are less efficient at very low currents, because of their quiescent current.

That said, @dakshsaraf 's circuit is nowhere near the low consumption that would make using an LDO linear regulator the more efficient choice. The circuit would have to have a sleep current of no more than a few mA before the question is worth considering.

That's pretty high for a battery powered sensor circuit. Before you get into the complexities of Nick G's article, you should look in more detail at where that current is being consumed in your circuit. You may find that most of it is not the atmega chip, and that you need to concentrate your efforts on other components first.