Changing the CPU clock in real time (run-time), to save some battery power

Hi,

I have been experimenting with different methods to save some power when running my Arduino on battery power.

Among other things, I have discovered that you can change the MPU Main Clock, at runtime.

I find that I can thus use a standard Arduino (with no hardware modification), and the standard opti bootloader (don't need to compile my own version), and I don't need to use the sleep functions, so my sketch is not interrupted all the time.

It does however turn out that functions like millis() and Serial() internal settings are determined at compile time. So the easy way to get a correct millis() count and correct Serial.begin(baud rate) setting, is to change the "uno.build.f_cpu=" setting in the "boards.txt file.

Below is an example of the "boards.txt" setting for compiling an Arduino Uno to run at 1MHz, and a sample sketch of what you need inside your sketch, to setup a matching MPU Prescaler setting, so millis() and Serial.begin(x) works correctly.

The file "boards.txt" can be found in the following path:
< arduino unpack/install library >\hardware\arduino

Example of a boards.txt entry

##############################################################

unoct.name=Arduino Uno (Compile for 1MHz)
unoct.upload.protocol=arduino
unoct.upload.maximum_size=32256
unoct.upload.speed=115200
unoct.bootloader.low_fuses=0xff
unoct.bootloader.high_fuses=0xde
unoct.bootloader.extended_fuses=0x05
unoct.bootloader.path=optiboot
unoct.bootloader.file=optiboot_atmega328.hex
unoct.bootloader.unlock_bits=0x3F
unoct.bootloader.lock_bits=0x0F
unoct.build.mcu=atmega328p
unoct.build.f_cpu=1000000L
unoct.build.core=arduino
unoct.build.variant=standard

##############################################################

// Example sketch that changes the MPU clock speed in real time (at runtime)

/*************************************************************************************
 *
 * Blink and Serial example for an Arduino running at only 1MHz (to save some battery power)
 *
 * This example can be run on a standard Arduino Uno if you add the text below
 * to your "boards.txt" file
 *
 * The file "boards.txt" can be found in the following path:
 * < arduino unpack/install library >\hardware\arduino
 *
  
 
##############################################################

unoct.name=Arduino Uno (Compile for 1MHz)
unoct.upload.protocol=arduino
unoct.upload.maximum_size=32256
unoct.upload.speed=115200
unoct.bootloader.low_fuses=0xff
unoct.bootloader.high_fuses=0xde
unoct.bootloader.extended_fuses=0x05
unoct.bootloader.path=optiboot
unoct.bootloader.file=optiboot_atmega328.hex
unoct.bootloader.unlock_bits=0x3F
unoct.bootloader.lock_bits=0x0F
unoct.build.mcu=atmega328p
unoct.build.f_cpu=1000000L
unoct.build.core=arduino
unoct.build.variant=standard

##############################################################


 * To make sure that your hardware is actually running at 1MHz at run-time
 * you need to include a call to the "setPrescale(CLOCK_PRESCALE_DEFAULT);" 
 * in the Setup() routine of your sketch. As is shown in this example.
 *
 * The advantage of setting CLOCK_PRESCALE in your sketch, and setting the 
 * Arduino IDE to compile for a 1MHz CPU, is that you can do this on any
 * standard Arduino without modifying any hardware or AVR flags.
 * 
 * ( FYI, for further experimenting:
     When CLOCK_PRESCALE is set no lower than 1MHz, the functions  
 *   millis() and Serial.begin() still works without any modifications or hacks).
 *
 * ( If CLOCK_PRESCALE is set below 1MHz, then millis() and Serial.begin() will need
 *   further hacking to the core arduino files to function correctly).
 *
 *
 * This example code is in the public domain.
 *
 * alvin@labitat.dk (March 2014)
 * 
 *
 *
 * Prescaler division define options
 *
 * (Prescale devision is controlled by the CLKPR register of the AVR)
 *
 * (FYI: If you set the DIV8 flag of the AVR chip, the Prescale register is 
 *  automatically loaded with the CLOCK_PRESCALER_8 setting at MPU power-on time.
 *  You can however still change/reset the Prescale register at run-time).
 *
 *                                             (FYI:        testing this sketch 
 *                                                          on an Arduino Pro Mini @5V Vcc w. LEDs removed   */
#define CLOCK_PRESCALER_1   (0x0)  //  16MHz   (FYI: 15mA   on an Arduino Pro Mini @5V Vcc w. LEDs removed)
#define CLOCK_PRESCALER_2   (0x1)  //   8MHz   (FYI: 10mA   on an Arduino Pro Mini @5V Vcc w. LEDs removed)
#define CLOCK_PRESCALER_4   (0x2)  //   4MHz   (FYI:  8mA   on an Arduino Pro Mini @5V Vcc w. LEDs removed)
#define CLOCK_PRESCALER_8   (0x3)  //   2MHz   (FYI:  6.5mA on an Arduino Pro Mini @5V Vcc w. LEDs removed)
#define CLOCK_PRESCALER_16  (0x4)  //   1MHz   (FYI:  5.5mA on an Arduino Pro Mini @5V Vcc w. LEDs removed)
#define CLOCK_PRESCALER_32  (0x5)  // 500KHz   (FYI:  5mA   on an Arduino Pro Mini @5V Vcc w. LEDs removed)
#define CLOCK_PRESCALER_64  (0x6)  // 250KHz   (FYI:  4.8mA on an Arduino Pro Mini @5V Vcc w. LEDs removed)
#define CLOCK_PRESCALER_128 (0x7)  // 125KHz   (FYI:  4.8mA on an Arduino Pro Mini @5V Vcc w. LEDs removed)
#define CLOCK_PRESCALER_256 (0x8)  // 62.5KHz  (FYI:  4.6mA on an Arduino Pro Mini @5V Vcc w. LEDs removed)

// Choose which default Prescaler option that will be used in this sketch
#define    CLOCK_PRESCALE_DEFAULT   CLOCK_PRESCALER_16
 

//// Debug LED settings 
#define DEBUG_LED 13
#define DEBUG_LED_ON_DURATION  500   //change this value to adjust the number of MilliSeconds the LED is ON
#define DEBUG_LED_OFF_DURATION 500   //change this value to adjust the number of MilliSeconds the LED is OFF



/***************************************************************************************
**** SETUP
****
**** the setup routine runs once when you press reset:
****************************************************************************************/
void setup() 
{   
  setPrescale();   // reduce MPU power usage at run-time by slowing down the Main Clock source  
  
  // initialize the digital debug pin as an output.
  pinMode(DEBUG_LED, OUTPUT); 

  Serial.begin(9600); 
 }



/***************************************************************************************
**** MAIN LOOP
****
**** the loop routine runs over and over again forever:
***************************************************************************************/
void loop() 
{
  if( DebugBlink() ) Serial.println("blink");
}




/***************************************************************************************
**** Blink routine without the use of delay()
***************************************************************************************/

boolean DebugBlink() 
{ 
  static unsigned long debug_blink_millis = 0;
  static boolean debug_blink_on = true;
  
  if (debug_blink_millis <= millis() )  // if its time to change the blink
  { 
    if (debug_blink_on)  //use a flag to determine wether to turn on or off the Blink LED
    {
      digitalWrite(DEBUG_LED, HIGH);   // set the LED ON (active HIGH)
      debug_blink_on = false;       // do the OFF next time 
 
      // set the time to do next blink 
      debug_blink_millis = millis() + DEBUG_LED_OFF_DURATION;
    } 
    else
    {
      digitalWrite(DEBUG_LED, LOW);    // set the LED OFF (active HIGH)
      debug_blink_on = true;        // do the ON next time 

      // set the time to do next blink 
      debug_blink_millis = millis() + DEBUG_LED_ON_DURATION;
    } 
    return true;
  }
  else
  {
    return false;
  }
}

/***********************************************************************
****
****   at run-time - MPU speed modifications
****
***********************************************************************/

void setPrescale() {
  
  /* 
   * Setting the Prescale is a timed event, 
   * meaning that two MCU instructions must be executed  
   * within a few clockcycles. 
   */
  
  //To ensure timed events, first turn off interrupts
  cli();                   // Disable interrupts
  CLKPR = _BV(CLKPCE);     //  Enable change. Write the CLKPCE bit to one and all the other to zero. Within 4 clock cycles, set CLKPR again
  CLKPR = CLOCK_PRESCALE_DEFAULT; // Change clock division. Write the CLKPS0..3 bits while writing the CLKPE bit to zero
  sei();                   // Enable interrupts
  
  
  // To get the fastest (and still reliable) ADC (Analog to Digital Converter)
  // operations, when changing the prescale register,
  // you also need to set the ADC_Clk_prescale in the ADCSRA register
  
                                                     // Preferred: 50KHz < ADC_Clk < 200KHz 
#if  CLOCK_PRESCALE_DEFAULT == CLOCK_PRESCALER_1
#define ADC_SPEED 7                                  //ADC_Clk = F_CPU_Pre / 128 => 125KHz
#elif CLOCK_PRESCALE_DEFAULT == CLOCK_PRESCALER_2
#define ADC_SPEED 6                                  //ADC_Clk = F_CPU_Pre /  64 => 125KHz
#elif CLOCK_PRESCALE_DEFAULT == CLOCK_PRESCALER_4
#define ADC_SPEED 5                                  //ADC_Clk = F_CPU_Pre /  32 => 125KHz
#elif CLOCK_PRESCALE_DEFAULT == CLOCK_PRESCALER_8
#define ADC_SPEED 4                                  //ADC_Clk = F_CPU_Pre /  16 => 125KHz
#elif CLOCK_PRESCALE_DEFAULT == CLOCK_PRESCALER_16
#define ADC_SPEED 3                                  //ADC_Clk = F_CPU_Pre /   8 => 125KHz
#elif CLOCK_PRESCALE_DEFAULT == CLOCK_PRESCALER_32
#define ADC_SPEED 2                                  //ADC_Clk = F_CPU_Pre /   4 => 125KHz
#elif CLOCK_PRESCALE_DEFAULT == CLOCK_PRESCALER_64
#define ADC_SPEED 1                                  //ADC_Clk = F_CPU_Pre /   2 => 125KHz
#elif CLOCK_PRESCALE_DEFAULT == CLOCK_PRESCALER_128
#define ADC_SPEED 0                                  //ADC_Clk = F_CPU_Pre /   1 => 125KHz
#elif CLOCK_PRESCALE_DEFAULT == CLOCK_PRESCALER_256
#define ADC_SPEED 0                                  //ADC_Clk = F_CPU_Pre /   1 => 62.5KHz
#endif

  ADCSRA = ( 0x80 | ADC_SPEED);  // Activate ADC and set ADC_Clk 
}

PrescaleBasics.ino (7.97 KB)

This was an interesting approach. I have done some work with Cosa to get low power built-in but only used the power attributes. Below is a sketch I wrote to demonstrate different low power tweaks.

#include "Cosa/Event.hh"
#include "Cosa/Pins.hh"
#include "Cosa/Power.hh"
#include "Cosa/ExternalInterrupt.hh"
#include "Cosa/Watchdog.hh"

#define USE_DISABLE_MODULES
#define USE_DISABLE_PINS
#define USE_EVENT_AWAIT
#define USE_WATCHDOG_DELAY

OutputPin led(Board::LED);

class Button : public ExternalInterrupt {
  OutputPin* m_led;
public:
  Button(Board::ExternalInterruptPin pin, OutputPin* led) : 
    ExternalInterrupt(pin, ExternalInterrupt::ON_LOW_LEVEL_MODE, true),
    m_led(led)
  {}

  virtual void on_interrupt(uint16_t arg = 0) 
  {
    if (m_led != NULL) m_led->on();
#ifdef USE_EVENT_AWAIT
    Event::push(Event::NULL_TYPE, NULL);
#endif
    disable();
  }
};

Button wakeup(Board::EXT0, &led);

void setup()
{
#if defined(USE_DISABLE_MODULES)
  // 0 uA, already done by startup
  ACSR = _BV(ACD);
  ADCSRA = 0;  
  UCSR0B = 0;
#endif
#if defined(USE_DISABLE_PINS)
  // 2 uA, possible uart pin needed disconnecting
  DDRB = 0b11111111;
  PORTB = 0b00000000;
  DDRC = 0b11111111;
  PORTC = 0b00000000;
  DDRD = 0b11111011;
  PORTD = 0b00000100;
#endif
  Power::all_disable();
  wakeup.enable();
}

void loop()
{
#ifdef USE_EVENT_AWAIT
  // 180 uA - (BOD + PIN disable = 23 uA)
  Event event;
  Event::queue.await(&event, SLEEP_MODE_PWR_DOWN);
#else
  // 180 uA - (BOD + PIN disable = 23 uA)
  Power::sleep(SLEEP_MODE_PWR_DOWN);
#endif

#ifdef USE_WATCHDOG_DELAY
  // 1,5 mA, 64 ms blink
  Watchdog::begin(16, SLEEP_MODE_PWR_DOWN);
  while (wakeup.is_low()) {
    led.toggle();
    Watchdog::delay(64);
  }
  led.off();
  Watchdog::end();
#else
  // 15 mA, 16 ms blink
  while (wakeup.is_low()) {
    led.toggle();
    for (uint8_t i = 0; i < 4; i++) DELAY(16000U);
  }
  led.off();
#endif

  Power::all_disable();
  wakeup.enable();  
}

Link to the example sketch: Cosa/CosaLowPower.ino at master · mikaelpatel/Cosa · GitHub

The results are for Arduino Mini Pro, approx. 3 mA idle, power LED on.

  1. Modifications; removed power LED resistor, 1 Kohm.
  2. Powered via FTDI USB/TTY adapter 5 V
    a. Connected to VCC/GND, 180 uA
    b. Connected to RAW/GND, 840 uA
    c. Connected to VCC/GND, + BOD disabled, 160 uA (See Power.hh)
  3. Connected to VCC/GND, + disable pins, 157 uA
  4. Powered with LiPo 3,7 V
    a. Connected to VCC/GND, 32 uA
    b. Connected to RAW/GND, 840 uA

And for Arduino Lilypad, 8 Mhz, no modifications

  1. Powered via FTDI USB/TTY adapter 5 V, 18 uA
  2. Powered with LiPo 3,7 V, 16 uA

I have added frequency scaling to Cosa but there are ripple effects to consider before testing. Mainly as you write on timers and ADC. Cosa/Power.cpp at master · mikaelpatel/Cosa · GitHub

Cheers! And thanks for the inspiration.

MrAlvin:
The file "boards.txt" can be found in the following path:
< arduino unpack/install library >\hardware\arduino

MORE DOCUMENTATION needs to be produced with respect to files like boards.txt, as well as official documentation on writing your own 'cores' and 'variants'.

(so thanks for posting that, it's the best solution to make your own 'boards.txt' entry to get F_CPU correct)

for extreme power saving check - http://www.gammon.com.au/forum/?id=11497 -