Please help with RTC and interrupts

Hi, I have a circuit that turns 12V power on and off using an ATMEGA328P and a MOSFET, at intervals controlled with a DS3231 clock that wakes the sleeping MCU with alarm interrupts. In the setup the program sets the clock, sets the alarm for 15 seconds later, runs a test on-and-off of the MOSFET that controls the 12V power. Then the clock VCC is turned off (it then runs on its battery). Then in 15 seconds, the main program loop starts, the ATMEGA wakes up, the 12V power turns on for a while, and then turns off. The clock power is turned on for communication, the alarm is set for 5 minutes later. Then the ATMEGA sleeps until the alarm.

The 5-minute interval works fine from there as long as I don't turn off the clock VCC at the end of the main program loop. But I need to turn that off to save power. If I turn it off, the MOSFET is fired either randomly but short intervals--or maybe resetting the whole program; I'm not sure. I have struggled with learning about the interrupts and will continue with that, but I thought I'd post this here in case someone would just be able to look at this and figure out why I can't turn off that clock and still have the interval work. Can anyone help? Thank you!

#include <DS3232RTC.h>      // https://github.com/JChristensen/DS3232RTC
#include <LowPower.h>

#define ALARM_PIN 2 
#define MOSFET_PIN 7
#define CLOCK_VCC_PIN 8
#define SDA A4 
#define SCL A5 

const time_t ALARM_INTERVAL(300);  

void setup() 
{
  pinMode(MOSFET_PIN, OUTPUT); 
  pinMode(CLOCK_VCC_PIN, OUTPUT);

  mosfetOn();
  clockvccOn(); 
  time_t t = RTC.get(); 
  
  RTC.setAlarm(ALM1_MATCH_DATE, 0, 0, 0, 1);
  RTC.setAlarm(ALM2_MATCH_DATE, 0, 0, 0, 1);
  RTC.alarm(ALARM_1);
  RTC.alarm(ALARM_2);
  RTC.alarmInterrupt(ALARM_1, false);
  RTC.alarmInterrupt(ALARM_2, false);
  RTC.squareWave(SQWAVE_NONE); 
  
  pinMode(ALARM_PIN, INPUT_PULLUP); 
  attachInterrupt(digitalPinToInterrupt(ALARM_PIN), alarmIsr, FALLING);

  time_t alarmTime = t + 15;
  RTC.setAlarm(ALM1_MATCH_HOURS, second(alarmTime), minute(alarmTime), hour(alarmTime), 0);
  RTC.alarm(ALARM_1);           
  RTC.alarmInterrupt(ALARM_1, true);

  delay(3000); 
  mosfetOff(); 
  clockvccOff();
}

volatile boolean alarmIsrWasCalled = false; 

void alarmIsr()
{
  alarmIsrWasCalled = true; 
}

void loop() 
{
  attachInterrupt(digitalPinToInterrupt(ALARM_PIN), alarmIsr, FALLING);
  pinMode(A4, INPUT);
  pinMode(A5, INPUT);
  LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);
  pinMode(A4, OUTPUT); 
  pinMode(A5, OUTPUT);

  detachInterrupt(digitalPinToInterrupt(ALARM_PIN));

  clockvccOn();
  delay(50);
  
  RTC.alarm(ALARM_1);
  RTC.alarm(ALARM_2); 
  alarmIsrWasCalled = false;
  time_t t = RTC.get(); 

  time_t alarmTime = t + ALARM_INTERVAL;    
  
  RTC.setAlarm(ALM1_MATCH_HOURS, second(alarmTime), minute(alarmTime), hour(alarmTime), 0);
  RTC.alarm(ALARM_1);                  
  RTC.alarmInterrupt(ALARM_1, true);
  delay(100);
  LipoControl();
  // clockvccOff();
}

void LipoControl() {
  mosfetOn();
  delay(10000); 
  mosfetOff();
}

void mosfetOn() {
  digitalWrite(MOSFET_PIN, HIGH);
}

void mosfetOff() {
  digitalWrite(MOSFET_PIN, LOW);
}

void clockvccOn() {
  digitalWrite(CLOCK_VCC_PIN, HIGH);
}

void clockvccOff() {
  digitalWrite(CLOCK_VCC_PIN, LOW);
}

// function to return the compile date and time as a time_t value
time_t compileTime()
{
    const time_t FUDGE(10);    //fudge factor to allow for upload time, etc. (seconds, YMMV)
    const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
    char compMon[4], *m;

    strncpy(compMon, compDate, 3);
    compMon[3] = '\0';
    m = strstr(months, compMon);

    tmElements_t tm;
    tm.Month = ((m - months) / 3 + 1);
    tm.Day = atoi(compDate + 4);
    tm.Year = atoi(compDate + 7) - 1970;
    tm.Hour = atoi(compTime);
    tm.Minute = atoi(compTime + 3);
    tm.Second = atoi(compTime + 6);

    time_t t = makeTime(tm);
    return t + FUDGE;        //add fudge factor to allow for compile time
}type or paste code here
1 Like

I haven't looked at your code, but if it works properly so long as you keep the RTC's Vcc powered, it's probably a hardware problem.

Are you using the ZS-042 module for the DS3231? If so, remember that its INT output pin is open drain, and it's pulled up to Vcc on the module. But with the power off, Vcc will fall to ground, which makes the resistor a pull-down resistor, and the INT pin will just stay low when Vcc is powered down, and it won't function properly as an interrupt source.

So any pullup resistor connected to the INT pin from the Vcc that's being shut down needs to be disconnected. Instead, INT should be pulled up to a supply which is NOT turned off. That would be either an external resistor connected to the Arduino's Vcc, or the internal pullup resistor on the pin used for the interrupt.

If that's no help, I'd ask you to post a schematic of your circuit.

I'll attempt to post my first picture to this new forum software which shows where to cut the trace on the ZS-042 module.

Thanks very much for the reply. I am using a standalone DS3231 chip on a custom PCB. I'll look at the resistors we've used and compare it with your note. I can post the schematic, but the thing is, in the "setup" part of the program, the alarm function seems to work ok with the clock off. Once it's in the main program loop it does not.

It would be helpful if you could pin down whether the problem is that the INT isn't being generated by the RTC, or that the processor isn't responding. Do you have a voltmeter you could use to see what's happening on the RTC INT pin?

I notice that you only turn SDA and SCL off, then back on, in the loop. Does it work if you comment out those four lines?

Thanks very much for the suggestions! Unfortunately it's going to be a couple of days before I can get back to this. I will try them then.

I suspect the problem is what you are doing to the I2C lines for sleep. I found this topic that discusses the issue, and apparently it's a fairly involved process to make sure Wire works again after you wake up:

https://forum.arduino.cc/t/problem-to-reinit-i2c-bus-after-sleeping-and-power-off/324688

Within that thread is a link to a Nick Gammon blog on the same subject, which contains this:

void loop()
{

  // power up clock chip
  digitalWrite (CLOCK_POWER, HIGH);
  pinMode (CLOCK_POWER, OUTPUT);

  // activate I2C
  Wire.begin();

  // find the time  
  DateTime now = RTC.now();

  // time now available in now.hour(), now.minute() etc.

  // finished with clock
  pinMode (CLOCK_POWER, INPUT);  
  digitalWrite (CLOCK_POWER, LOW); 

  // turn off I2C
  TWCR &= ~(bit(TWEN) | bit(TWIE) | bit(TWEA));

  // turn off I2C pull-ups
  digitalWrite (A4, LOW);
  digitalWrite (A5, LOW);


  // -------- do something here if required by the time of day



  // sleep for a total of 64 seconds (8 x 8)
  for (int i = 0; i < 8; i++)
    myWatchdogEnable (); 

}  // end of loop

A couple of tricky parts:

   We make the pin high before switching it to output, to save forcing the capacitor low
    The RTC chip is actually "alive" all the time from its battery backup, it just is not allowed to use     I2C without external power. So we don't have to wait long before we use it.
    Once finished we set the pin to input first, and then to low, again to save draining the capacitor
    We turn off the I2C hardware
    We turn off the I2C pull-ups to save power
 

It seems there are two things to deal with when minimizing power consumption during sleep. The first is current drawn internally by the I2C peripheral - its clock, etc. But I wonder if all that isn't shut down automatically in power-down sleep mode.

The other issue is the pullup resistors on SDA and SCL. But I don't believe the DS3231 will draw any current from those lines when its Vcc is powered down. So I'm not sure you need to bother with the lines.

In any case, I think the problem is that after wakeup, Wire is no longer communicating with the DS3231.

OK, thanks for the tips. This will take some self-education to figure out what's going on. I will work on it and get back when I can.

In case I have time to play with this myself, can you tell me what your circuit looks like with respect to the pullup resistors on SDA and SCL, and to the INT pin of the DS3231? Are they pulled up to CLOCK_VCC_PIN, or to some other power rail that doesn't get switched off? Or are you using the internal INPUT_PULLUP resistors?

I have attached the portion of the schematic that shows the clock connections. I am not using the clock module option shown--I'm using a surface-mount DS3231.

Ok, so there's no external pullup on the INT pin. You're just using the internal pullup on D2.

The pullups on SDA and SCL are connected to the DS3231's Vcc pin. So if you bring D8 low, those resistors are then connected to ground. But my memory is that Wire.h, which your library calls for I2C stuff, sets SDA and SCL to INPUT_PULLUP. So that could be a conflict. Well, if I get time I'll rig up your circuit and run your software and see what might work to get the current draw to a minimum.

OK, thanks. I am reading-up but there is a lot to read about this.

For some reason, at the moment it's working with the clock turning off in the main loop. I don't know why. I will test over a longer period and report back.

OK, I have tested the circuit overnight and it worked fine. Before it "fixed" itself I did a lot of poking around with the voltmeter, especially on the clock's INT pin, to answer your questions. Looking down between the legs of the clock chip, it's not entirely clean down there--a few stray bits of solder. Perhaps the poking around dislodged a piece on the legs that was sometimes messing with the INT pin's voltage--the VCC pin is adjacent. The problem behavior wasn't entirely consistent--so maybe the piece was moving around. Just a guess. I did have a stray-solder-bit problem in a previous project.
In any case, your help pointed me toward that INT pin, allowed me to move forward, and I learned more about the components of my project. I still need to get a better understanding of some aspects of the software program. Perhaps I can lower the power consumption even more. Thanks very much!

It's great that you got it working. The only remaining question is how much current is drawn when awake and asleep.

With my multimeter I measure 4 ma awake and .02 ma asleep.

Earlier with the clock powered with vcc and mcu asleep it was 0.09 ma.

20uA is a fairly good sleep current, but I don't know whether that includes regulator idle current or not. The 328P will do a power-down sleep at less than 1uA if everything is turned off properly.

The datasheet for the 328P says that in power-down mode theTWI address recognition function is still enabled. So that raises the question of whether leaving the TWI peripheral enabled results in additional sleep current. That may be why Gammon and others turn off the TWEN bit of the TWCR register, then re-enable TWI after waking up with a Wire.begin(). Apparently there is no Wire.end(). EDIT: It turns out this isn't necessary. The peripheral doesn't draw current during power-down sleep.

But that still leaves the question of the the state of the pullup resistors on A4 and A5 after you've turned off TWEN. I suspect Wire leaves them enabled.

I've never experimented with this stuff to find the answers, and in particular what affects sleep current and what doesn't. But I'll try to find some time to work on it.

I suspect your turning A4 and A5 into outputs after wakeup is probably not right. I would be curious to know if everything still works if you comment out those two lines. That would leave them as INPUTs, with internal pullups disabled, which in theory should work because you have external pullups.

I was able to do some testing today using a modified version of a Ralph Bacon sketch. I found that it was not necessary to turn off the TWI peripheral since it doesn't draw any current during sleep.

However, the Wire.begin() function, which your library calls, leaves SDA and SCL in INPUT_PULLUP mode. If you leave them that way, current will flow through them when the RTC Vcc line goes low. So sleep current will be significant.

The solution is to turn off the internal pullups immediately after Wire.begin(), or after your library's DS3231 initialization call. They should stay that way from then on. One way to do that is to do a digitalWrite LOW to those pins, which in input mode just turns off the pullups. But just to be sure, the code below prints out the DDR and PORT register values for port C just before going to sleep. Both should be zero, meaning the pins are configured as inputs, and the pullup resistors are disabled.

With the pullups disabled, I get sleep current drawn by the 328P of some fraction of one microamp. If your sleep current is more than that, your low.power.h may not be giving you the lowest current, and you might want to see if my code does any better.


/*
This is a sketch for the DS3231 RTC and ATmega328P Arduinos to measure current draw during
POWER_DOWN sleep mode. Pressing a button sets the RTC alarm time for 30 seconds from now,
turns off the RTC's Vcc power (so it runs on its coin battery), and puts the processor to
sleep. At the alarm time, the RTC's INT/SQW pin triggers an external interrupt on D2, and
the processor wakes up, turns on the DS3231's Vcc power, and double-blinks pending the next
button push. Current drawn by the ATmega 328P can be measured during the sleep time, and
should be less than 1uA.  An Arduino Pro Mini, with regulator and power LED removed, can be
used to measure processor current.

A ZS-042 module can be used for the DS3231, with the following modifications:

1. Disable the "charging" curcuit of the CR2032 battery (remove the diode or the resistor,
   or cut a trace).
2. Remove the module's power LED, or its resistor, or cut a trace.
3. Disable the on-module pullup resistor on the INT/SQW pin by cutting a trace.  Pullup is
   provided by the internal pullup resistor on the Arduino's D2.

Key to minimizing power consumption during sleep is turning off the internal pullup resistors
on SDA and SCL.  The Wire.h library's Wire.begin() function leaves these pins in INPUT_PULLUP
mode.  But since the ZS-042 module already has pullups on these lines, it's necessary to
disable the Arduino pullups.  Otherwise current will flow through them, then through the ZS-042
pullups to Vcc, which is grounded during sleep.

A digitalWrite LOW to these two pins turns off the pullups.  This can be done immediately after
Wire.begin(), and should remain effective thereafter unless the code does another Wire.begin().

The DS3231 library used is by Petre Rodan:

https://github.com/rodan/ds3231

Thanks to Ralph Bacon for the original version of this sketch.
*/

#include <avr/sleep.h>
#include <avr/wdt.h>    // probably not needed since bootloader turns off WDT
#include <Wire.h>
#include <ds3231.h>     // https://github.com/rodan/ds3231

#define sleepPin 8      // When low, makes 328P go to sleep
#define wakePin 2       // when low, makes 328P wake up
#define ledPin 10       // output pin for the LED (to show it is awake)
#define RTCVCCpin 6     // power RTC Vcc from this pin
#define sleepPeriod 30  // number of seconds to sleep
//SDA = A4
//SCL = A5

// DS3231 alarm time
byte wake_HOUR;
byte wake_MINUTE;
byte wake_SECOND;
#define BUFF_MAX 256

struct ts t;
byte prevADCSRA;

void setup() {
  Serial.begin(9600);

  // Keep pins high until we ground them
  pinMode(sleepPin, INPUT_PULLUP);
  pinMode(wakePin, INPUT_PULLUP);

  pinMode(RTCVCCpin, OUTPUT);
  digitalWrite(RTCVCCpin, HIGH);

  // Flashing LED just to show the Controller is running
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  Wire.begin();
  digitalWrite(A4, LOW);                  // turn off internal pullup resistors
  digitalWrite(A5, LOW);

  DS3231_init(DS3231_CONTROL_INTCN);
  DS3231_clear_a1f();                     // clear alarm

  Serial.println("Setup completed.");
}

// The loop blinks an LED when not in sleep mode
void loop() {
  static byte oldSec = 99;
  char buff[BUFF_MAX];

  // Just blink LED twice to show we're running
  doBlink();

  // Is the "go to sleep" pin now LOW?
  if (digitalRead(sleepPin) == LOW)
  {
    // Send a message just to show we are about to sleep
    Serial.println("Good night!");
    Serial.print("DDRC = "); Serial.println(DDRC,BIN);    // see current state of pullup resistors
    Serial.print("PORTC = "); Serial.println(PORTC,BIN);  //   on SDA and SCL.
    Serial.flush();

    // Set the DS3231 alarm to wake up in X seconds
    setNextAlarm();

    noInterrupts();
    attachInterrupt(digitalPinToInterrupt(wakePin), sleepISR, LOW);
    interrupts();

    // And enter sleep mode
    digitalWrite(RTCVCCpin, LOW);       // turn off RTC Vcc
    prevADCSRA = ADCSRA;                // save ADC state
    ADCSRA = 0;                         // disable ADC for power saving
    wdt_disable();                      // disable WDT for power saving
    set_sleep_mode (SLEEP_MODE_PWR_DOWN); // Deep sleep
    sleep_enable();
    sleep_bod_disable();                // disable brownout detector during sleep
                                        //    must be immediately before sleep_cpu()
    sleep_cpu();                        // now go to sleep

    //  Wake Up

    digitalWrite(RTCVCCpin, HIGH);      // turn on RTC Vcc
    ADCSRA = prevADCSRA;                // Restore ADC
    Serial.println("I'm awake!");

    // Clear existing alarm so int pin goes high again
    DS3231_clear_a1f();

  }
  else
  {
    // Get the time
    DS3231_get(&t);

    // If the seconds has changed, display the (new) time
    if (t.sec != oldSec)
    {
      // display current time
      snprintf(buff, BUFF_MAX, "%d.%02d.%02d %02d:%02d:%02d\n", t.year,
               t.mon, t.mday, t.hour, t.min, t.sec);
      Serial.print(buff);
      oldSec = t.sec;
    }
  }
}

void sleepISR() {
  // Prevent sleep mode, so we don't enter it again, except deliberately, by code
  sleep_disable();

  // Detach the interrupt that brought us out of sleep
  detachInterrupt(digitalPinToInterrupt(wakePin));
}

// Double blink just to show we are running. Note that we do NOT
// use the delay for the final delay here, this is done by checking
// millis instead (non-blocking)
void doBlink() {
  static unsigned long lastMillis = 0;

  if (millis() > lastMillis + 1000)
  {
    digitalWrite(ledPin, HIGH);
    delay(10);
    digitalWrite(ledPin, LOW);
    delay(200);
    digitalWrite(ledPin, HIGH);
    delay(10);
    digitalWrite(ledPin, LOW);
    lastMillis = millis();
  }
}

// Set the next alarm
void setNextAlarm(void)
{
  // flags define what calendar component to be checked against the current time in order
  // to trigger the alarm - see datasheet
  // A1M1 (seconds) (0 to enable, 1 to disable)
  // A1M2 (minutes) (0 to enable, 1 to disable)
  // A1M3 (hour)    (0 to enable, 1 to disable)
  // A1M4 (day)     (0 to enable, 1 to disable)
  // DY/DT          (dayofweek == 1/dayofmonth == 0)
  byte flags[5] = { 0, 0, 0, 1, 1 };

  // get current time so we can calc the next alarm
  DS3231_get(&t);

  wake_SECOND = t.sec;
  wake_MINUTE = t.min;
  wake_HOUR = t.hour;

  // Add some seconds to current time. If overflow increment minutes etc.
  wake_SECOND = wake_SECOND + sleepPeriod;
  if (wake_SECOND > 59)
  {
    wake_MINUTE++;
    wake_SECOND = wake_SECOND - 60;

    if (wake_MINUTE > 59)
    {
      wake_HOUR++;
      wake_MINUTE -= 60;
    }
  }

  // Set the alarm time (but not yet activated)
  DS3231_set_a1(wake_SECOND, wake_MINUTE, wake_HOUR, 0, flags);

  // Turn the alarm on
  DS3231_set_creg(DS3231_CONTROL_INTCN | DS3231_CONTROL_A1IE);
}

Thank you; I will study your reply more when I get a chance. For now, I will report some results. First, I found that while I needed to start my multimeter at the milliamp setting at the start of the program (while the MOSFET was on), I then needed to switch my multimeter to microamps during the sleep cycle--the 0.02 milliamps reading was false. It's actually 3 microamps. Sorry about that! While I am interested in lowering this further if possible, it is quite adequate for my objective.

Second, I tried commenting out the SDA and SCL pin settings. When I got rid of both of them, the sleep current was 0.25 milliamps. With just the pinMode OUTPUT lines commented out, I got 2.8 microamps. I tried adding in the digitalWrite LOW commands to those pins and got a much higher reading, but I was not sure where in the program to add those lines. I don't have a Wire.begin() line (I think the other libraries start that), and I wasn't sure where was the clock initialization call.

The 3 microamps is with all the pinMode lines left in. For now I can move forward with my project with the program the way it is, but I will revisit your latest post when I get a chance, so I can understand things better. Thank you!

Well 3uA is certainly very good. So it's probably not worth spending a lot more time getting it lower.

The .25mA is what you get when the internal pullup resistors on SDA and SCL remain enabled. You can turn them off either by pinMode instructions making them INPUT pins as your code does, or, since they are probably already INPUT_PULLUP, you can just use digitalWrite LOW instructions to disable the pullups, as my code does. But I think changing them to output pins after wakeup is probably not a good idea. If the pullups remain disabled after wakeup it doesn't matter because you have external pullups installed.

If you want to temporarily enable the Serial function, you can print the values of the DDRC and PORTC registers at any point in your sketch, and that will tell you the current state of SDA and SCL and their internal pullups.