NerdKits - electronics education for a digital generation

You are not logged in. [log in]

NEW: Learning electronics? Ask your questions on the new Electronics Questions & Answers site hosted by CircuitLab.

Microcontroller Programming » How-To: Sleep

October 12, 2009
by mikedoug
mikedoug's Avatar

So my journey with the nerdkit continues to move forward day by day. I'm more of a programming geek than an electronics one -- but I have a couple electronics books on order at the library to fill in those gaps.

Today, driven by years strict programming philosophies of efficiency, I got the sleep mode working with my MCU. I am using the watchdog timer (perhaps the timer timers would be better? I just haven't read up on them yet, and that's next to investigate) set to interrupt mode only, and then powering down the MCU.

The savings in power is astonishing -- 7 mA vs 24mA. Talk about getting your battery to last longer -- 3.4 times longer!

One thing I discovered was that if you do NOT register an interrupt handler for the watchdog (WDT), then the effect will be the MPU restarting (even when you put it in interrupt-only mode). Also, READ the MPU document VERY closely... I missed a key point that took me a while to figure out.

Here's the simple code that allows my project to sleep for a second by powering down the MPU:

#include <avr/sleep.h>

ISR(WDT_vect) {
  if( PORTB & (1<<PB1) )
    PORTB &= ~(1<<PB1);
  else
    PORTB |= (1<<PB1);
}

void sleep() {
  // Power down sleep mode
  SMCR = (1<<SE) | (1<<SM1);

  // Start Watchdog
  cli();
  WDTCSR |= (1<<WDCE) | (1<<WDE);
  WDTCSR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1);
  sei();

  sleep_cpu(); 
}

Now, I'm not entirely certain that you need the cli()/sei() -- but the documentation does state that in order to change the WDTCSR register, you MUST set WDCE and WDE to 1, and then "within the next 4 clock cycles" set the WDTCSR to the desired settings -- given that, it would seem to me that you do not want this code to be interrupted.

This sleeps for 1 second. If you want to sleep otherwise, you'll want to see page 56 of the ATmega168.pdf for your options.

October 18, 2009
by mikedoug
mikedoug's Avatar

A BUG! If you are transmitting any data via the USART prior to calling sleep() here, you'll get corrupted output. I think this is because the USART is getting powered down. You can fix that by doing this in places of power down sleep mode:

// IDLE sleep mode
SMCR = (1<<SE);

Results aren't as nice -- 18-22mA of current. The next thing I will try is to figure out how to wait for transmission to complete and THEN enter the power down state.

MikeDoug

October 19, 2009
by mikedoug
mikedoug's Avatar

I did a little more research, and it all has to do with shutting down power to the USART. If you use the PRR register to turn off the USART and then turn it back on, you get a little corruption -- just like when you turn on the MPU you get some garbage.

I think the only real way around this is to use two pins, on the MPU, solder four wires to the DB9 header from the nerdkit (on the RTS/CTS and DSR/DTR pins); then implement that protocol in the uart.c file, and enable hardware flow control.

In theory, I think, that should prevent the garbage as long at RTS is low when you power on the USART because the receiving end won't be looking for you to be sending data when RTS is low.

So, if you want to save 4 pins, then use the IDLE mode which does not affect power to the USART. In testing today that change uses 18-19mA, which is still a drop from 24mA otherwise.

MikeDoug

October 20, 2009
by rusirius
rusirius's Avatar

not having looked into that part much I have no clue what I'm talking about... But could you not check to make sure no data was being sent/received via (UCSR0A & (1<<UDRE0))==0) and (UCSR0A & (1<<RXC0)==0) before sleeping?

October 20, 2009
by mikedoug
mikedoug's Avatar

rusirius: That's a seriously good idea... I had tried that (as well as checking for TXC0) to no avail. I'd put a while loop in place, nothing helped. It just seems that cutting power to the USART circuitry and then restoring it causes garbage output.

Based on the behavior of the LEDs on the little RS232 circuitboard from the USB nerdkit, it looks like the MPU may be sending a break when the USART is re-powered back on (the TX light burns solid for about a second).

MikeDoug

October 21, 2009
by mrobbins
(NerdKits Staff)

mrobbins's Avatar

Hi MikeDoug,

I think that part of the issue here is that when the USART module is disabled, pins #2 and 3 revert to their "normal" state. Take a look at the ATmega168 datasheet, page 193: "The Transmitter will override normal port operation for the TxDn pin when enabled." However, since when we wrote the original uart.c code we expected that the USART module would be left on, we never really cared to define the state of those pins, with the result that it ends up reverting to being a floating input pin. This means noise is free to drive that line into the serial inverter board and into your computer. To fix this, you should just add two lines somewhere in your code -- perhaps right before the uart_init():

PORTD |= (1<<PD1); // set TXD pin as an output, high
DDRD |= (1<<PD1);  // (because high is the idle state)

That way, when the USART module is turned off, the transmit serial port line will stay idle (asserted high), not floating and susceptible to noise.

Let me know if that helps!

Mike

October 24, 2009
by mikedoug
mikedoug's Avatar

Nope -- that did not work, it still gets garbage.

October 24, 2009
by mikedoug
mikedoug's Avatar

Okay, here's revised code:

volatile uint8_t WDT_triggered;
ISR(WDT_vect) {
  WDT_triggered++;
}

void chew(uint16_t x) {
  // IDLE mode
  SMCR = (1<<SE); // | (1<<SM1);  // SM1 does the POWER OFF mode

  cli();
  WDT_triggered = 0;
  WDTCSR |= (1<<WDCE) | (1<<WDE);
  WDTCSR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1);
  sei();

  // Wait for the WDT interrupt to be called.
  while( !WDT_triggered)
    sleep_cpu();
}

This update of the code handle the case where you have other interrupts that wake up the processor. Those interrupts can take care of whatever they want to do, and then we'll go back to sleep here.

I found this flaw when I added the real-time-clock code (from elsewhere in the forum) to my project (because I like keeping time).

I tried to NOT use a variable, but I could not find anything that changed with the WDTCSR -- I thought maybe I could clear WDE or WDIE inside the WDT_vect -- but I wasn't sure what that would do to the operations of the chip -- so I went with this route.

October 26, 2009
by mikedoug
mikedoug's Avatar

Here's, yet again, another way to do this... I'm using the 16-bit timer1 -- but you could retrofit it to use the 8-bit timer (but you'd need to lower the 4.5 second cycles to a much lower number...

  // Use a timer function to sleep a number of 1/100ths of a second
  volatile uint8_t sleep_timer_hit = 0;
  volatile uint8_t sleep_timer_loops = 0;
  volatile uint16_t sleep_timer_balloon = 0;
  void goto_sleep(uint16_t hsecs) {
    printf_P(PSTR("goto_sleep(%d)\r\n"), hsecs);
    cli();
    // Use OCR1A registers for compare and Clock divided by 1024
    TCCR1B |= (1<<WGM12) | (1<<CS12) | (1<<CS10);

    // 4.50 second cycles with a final balloon payment
    sleep_timer_loops = hsecs / 450;
    sleep_timer_balloon = hsecs % 450;
    OCR1A = (sleep_timer_loops) ? (450 * 143) : (sleep_timer_balloon * 143);

    // Reset the clock value
    TCNT1 = 0;

    printf_P(PSTR("== L: %d B: %d H: %d\r\n"), sleep_timer_loops, sleep_timer_balloon, sleep_timer_hit);

    // Enable the Interrupt
    sleep_timer_hit = 0;
    TIFR1 |= (1<<OCF1A);   // Clear any pending interrupt flag pg. 137
    TIMSK1 |= (1<<OCIE1A); // Enable the interrupt

    sei();

    PORTB |= (1<<PB4);
    while(!sleep_timer_hit) {
      sleep_cpu();
    }
    PORTB &= ~(1<<PB4);
  }

  SIGNAL(SIG_OUTPUT_COMPARE1A) {
    printf_P(PSTR("-> L: %d B: %d H: %d\r\n"), sleep_timer_loops, sleep_timer_balloon, sleep_timer_hit);
    // Disable the timer
    if( sleep_timer_loops ) {
      // Decrement the loops remaining, and switch to balloon timer on last
      if( --sleep_timer_loops == 0 )
      OCR1A = 143 * sleep_timer_balloon;
      printf_P(PSTR("-- L: %d B: %d H: %d\r\n"), sleep_timer_loops, sleep_timer_balloon, sleep_timer_hit);
    return;
    }

    // Otherwise we are in the balloon section.
    TIMSK1 &= ~(1<<OCIE1A);
    sleep_timer_hit++;

    printf_P(PSTR("<- L: %d B: %d H: %d\r\n"), sleep_timer_loops, sleep_timer_balloon, sleep_timer_hit);
  }

Note: There is a potential race condition IF the balloon timer (think balloon loan payment) is a REALLY small number and the timer races past that number inside the signal handler before it sets OCR1A to the new max. That said... I'm running a test right now with a continual loop of 4.51 second (451 hsecs) sleeps and I've done 84 of them with no race condition. So it would seem that, under simple conditions, this race condition isn't a concern. However... A condition which MIGHT cause this would be:

  * Some OTHER interrupt (A) fires
  * Interrupt A's vector begins executing
  * timer1's value goes past sleep_timer_balloon
  * Interrupt A's vector returns
  * timer1's vector begins executing, sets OCR1A = sleep_timer_balloon

Worst case scenario under this SHOULD be an extra 4.58 second delay as timer1 overflows and wraps back around.

Note: The printf_P(...) statements are there for debug, but were pretty fun to watch the operations of the function so I left them in. The 2 PORTB lines are in there so you can easily see the sleep vs. execution time of the application -- change this to a port you want to use -- OR remove it all together. Don't forget you also have to do this somewhere to enable output on that port:

  DDRB |= (1<<PB4);
  PORTB &= ~(1<<PB4);

Note: I picked the same 1024 clock divider that was used in the real-time-clock given elsewhere in this forum. This keeps the same divider -- which, as I read tonight, is pretty important since timer0 and timer1 must be the same click divider. (pg. 138)

October 27, 2009
by mikedoug
mikedoug's Avatar

I ran a test overnight and had over 6900 4.5 second pauses with no race conditions triggered. I do have one other interrupt triggering (the real time clock counter) every 1/100th of a second.

While this is not terribly conclusive, it does point to the fact that it seems to be a hard race condition to trigger. It's been running for almost 7.75 hours, and I'm going to let it keep running.

October 27, 2009
by mikedoug
mikedoug's Avatar

Probably about as good as it needs to be -- 16130 4.5 sleep iterations, and NONE have triggered the race condition.

Post a Reply

Please log in to post a reply.

Did you know that you can connect to certain car computers via the OBD-II port with a microcontroller? Learn more...