A modern C++ approach to sleep with power-down and watchdog timer

Go To Last Post
17 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

tl;dr https://github.com/ricardocosme/...

 

I've recently learned about watchdog timers and sleep modes to save power when taking sensor values using a regular interval of time. I want to read the sensor and put the system on sleep for a period of time before taking another reading.

I've realized that there is a boilerplate and there are some opportunities to make mistakes when coding a solution to put the MCU on sleep using the power-down mode and the watchdog timer to wake up.

Let's consider an Ad hoc solution to blink an LED. The led state is toggled at each 3 seconds. After the definition of the state, the system sleeps for 3 seconds using the power-down mode and it wakes up after that using the watchdog timer:

//ATtiny85 using avr-libc

[[gnu::section(".noinit")]] uint8_t cnt;

ISR(WDT_vect)
{ ++cnt; }

inline bool less_than_3s() {
    auto duration = 3000; /*ms*/
    auto prescaler = 1000; /*ms*/
    return cnt < (duration / prescaler);
}

int main() {
    //Defines LED_PIN as an output pin
    DDRB = 1 << LED_PIN;

    while(true) {
        //Toggle the LED when the MCU wakes up
        PORTB = PORTB ^ (1 << LED_PIN);

        cnt = 0;
        while(less_than_3s()) {
            //setup the watchdog timer to 1s
            WDTCR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1);
            sei();
            //setup and enable the power-down sleep mode
            MCUCR = MCUCR | (1<<SE) | (1<<SM1);
            sleep_cpu();
        }
        //power off the watchdog timer
        cli();
        WDTCR = WDTCR | (1<<WDCE) | (1<<WDE);
        asm("out 0x21, __zero_reg__");
        sei();
    }
}

Yeah, it's not too simple to code the sleep operation. It's a bit tricky and the above code doesn't say per se what is going on at some parts. For example, what we need to do to setup the watchdog timer with 1s prescaler? Right, we need to lookup at some table at the datasheet to figure out. The statement `WDTCR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1);` doesn't say what we would like to know in a high level sense. Actually, what it's normally desired is something like: `watchdog::interrupt::at_1s::on()`. Power on or off a watchdog timer can't be one trivial instruction because it could require a timed-sequence operation to this happen.

However, if performance is a goal, there will be more than on flavor to this Ad hoc solution, for example, we need a 16bits counter if the sleep duration can't be represented using an 8bits counter with the appropriate prescaler, or the counter may be not useful at all if we can sleep for a period of time equal to any prescaler. In the end, we can make mistakes writing theses solutions in a handmade style that can be only seen at runtime, and last but not least, these Ad hoc solutions are full of noise to the reader. The snippet above can be replaced by one single line with the same performance:

power_down::sleep<3s>();

This function make choices at the compile-time with a goal to choose the better implementation. Some errors that will be noticed only at runtime are moved to the compile phase. For example:

sleep::power_down::sleep<17ms>(); //compile-timer error
//static assertion failed: Duration should be multiple of 16ms, 32ms, 64ms, 125ms, 500ms, 1000ms, 2000ms, 4000ms or 8000ms.

sleep::power_down::sleep<35min>(); //compile-time error
//static assertion failed: The duration is too high to be used with the selected prescaler.
//The macro AVRCXX_WATCHDOG_COUNTER_RESOLUTION can be defined  to 16 to allow a 16bits software counter.

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

Last Edited: Sun. Nov 1, 2020 - 04:00 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:
Yeah, it's not too simple to code the sleep operation.

I guess.

 

[from memory]  I thought '85 and friends required the two-step process for setting up watchdog interrupts, like '88 family.  But not?

 

I don't see how you are ensuring that the 4-cycle limitation is met.  And doesn't your apparent toolchain like to reorder instructions?

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

[from memory]  I thought '85 and friends required the two-step process for setting up watchdog interrupts, like '88 family.  But not?

It was my first impression when I read the outcome generated by avr-libc to ATtyin85 to enable the watchdog timer. But, there are two "safety levels"[1]. Here, I'm considering the first safety level[2], which means that the WDTON fuse is unprogrammed. In this level there isn't a requirement for a timed sequence to enable the timer, only to disable it.[3]

 

Quotes from the datasheet:

[1] Section 8.4: "To prevent unintentional disabling of the Watchdog or unintentional change of time-out period, two different safetylevels are selected by the fuse WDTON..."

[2] Section 8.4 table 8-1: Safety level 1 -> WDT Initial state=Disabled -> How to disable the WDT=Timed sequence -> How to change time-out=No limitations

[3] Section 8.4.1.1: "In this mode, the Watchdog Timer is initially disabled, but can be enabled by writing the WDE bit to one without anyrestriction. A timed sequence is needed when disabling an enabled Watchdog Timer..."

I don't see how you are ensuring that the 4-cycle limitation is met.

I don't see nothing wrong here. I'm using a timed sequence to disable and one single operation to enable. Let me know if I'm loosing something here

 

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

Last Edited: Thu. Oct 29, 2020 - 03:04 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:
Some errors that will be noticed only at runtime are moved to the compile phase.
fyi, C++20 adds constraints.

C++20 Serves Up Intriguing Embedded Features | Electronic Design

by William G. Wong

OCT 02, 2020

Seventh-generation C++20 is coming to a C/C++ compiler near you soon.

[1/3 page]

Concepts and Constraints

[in second paragraph]

...

Class templates, function templates, and non-template functions can have a constraint attached to them. The constraint is a predicate that’s evaluated at compile time. 

...

 

"Dare to be naïve." - Buckminster Fuller

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

fyi, C++20 adds constraints.

I already know that. I'm using it in some projects.

I would like to add a support to C++20 concepts to this work without loosing the ability to use the library in C++17.

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:
I'm using a timed sequence to disable
rcosme wrote:

        WDTCR = WDTCR | (1<<WDCE) | (1<<WDE);
        WDTCR = 0x00;

theusch wrote:
I don't see how you are ensuring that the 4-cycle limitation is met.  And doesn't your apparent toolchain like to reorder instructions?

For one, optimization level may change things.

 

[edit]  Nothing to do with my other comments, but the initial post says to wake up after three seconds using the watchdog.  But aren't you waking every second using the watchdog?

 

BTW, does your code work?

 

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

Last Edited: Thu. Oct 29, 2020 - 07:52 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

theusch wrote:

rcosme wrote:

 

        WDTCR = WDTCR | (1<<WDCE) | (1<<WDE);
        WDTCR = 0x00;

theusch wrote:

I don't see how you are ensuring that the 4-cycle limitation is met.  And doesn't your apparent toolchain like to reorder instructions?

 

For one, optimization level may change things.

Sorry, I can't see your point in the snippet above. WDTCR is a pointer qualified as volatile. The language doesn't allow reordering of volatile reads or writes. We can't have for example something like:

        WDTCR = 0x00;
        WDTCR = WDTCR | (1<<WDCE) | (1<<WDE);

If what I'm saying is not your point, please, can you explain yourself with a bit more details?

 

theusch wrote:

[edit]  Nothing to do with my other comments, but the initial post says to wake up after three seconds using the watchdog.  But aren't you waking every second using the watchdog?

Yes, the watchdog wakes up after each 1s, but the sleep operation executed through some call like `sleep::power_down::sleep<Duration>()` (which means that we aren't talking about the instruction `sleep`) "wakes up" after 3s if Duration is equal to 3s.

 

theusch wrote:

BTW, does your code work?

Yes

 

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Your atomic does not allow for the possibility that irq's were off-

https://github.com/ricardocosme/...

 

Not quite sure why there is .noinit in use for a counter var. I cannot find where it is used, but the powerdown sleep does not reset the mcu, so any var/counter in use is preserved and no need for .noinit (if you want to preserve a var from a reset, then use .noinit, but you also have to determine if the noinit var is valid by checking reset flags).

 

I don't think you give the C programmer enough credit. They can also clean up the original example and give themselves access to things in a friendly way. The C++ programmer has advantages in the naming department and organization and many other things that make life easier, but the way some of these things become used only makes it more complicated. When a user (could be only you) is required to use multiple :: and a template parameter for a function call, then it may be time to rethink for a better way. There seems to be a fine line between taking advantage of c++, and overusing c++ features just because they are available.

 

C++

https://godbolt.org/z/3f38s5

 

C

https://godbolt.org/z/Yjv9e7

 

 

 

 

>For one, optimization level may change things.

 

About the only optimization that can screw these up is -O0, which no one in their right mind will use. Since these registers are defined as volatile, there is no reordering (otherwise our whole mcu world would collapse), and since the quoted code is writing the registers back to back, the worst you will get is a ldi between the two writes because of the scond write value (even at -O1). If you make that second write do anything but assignment then you are probably in trouble.

 

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

curtvm wrote:

Your atomic does not allow for the possibility that irq's were off-

https://github.com/ricardocosme/...

It's a TODO as you can see at: https://github.com/ricardocosme/...

However, I'm seeing that there is a bug at enable()/disable(). There isn't any memory barrier there.

 

curtvm wrote:

Not quite sure why there is .noinit in use for a counter var. I cannot find where it is used, but the powerdown sleep does not reset the mcu, so any var/counter in use is preserved and no need for .noinit (if you want to preserve a var from a reset, then use .noinit, but you also have to determine if the noinit var is valid by checking reset flags).

Right. I don't need the startup code to initialize the counter: it's an optimization. The counter is initialized at the begging of each call to power_down::sleep<Duration>()

 

curtvm wrote:

I don't think you give the C programmer enough credit.

I wouldn't like to hear this... My post is not about anything negative to C. I'm here only to share a solution using C++.

 

curtvm wrote:

They can also clean up the original example and give themselves access to things in a friendly way.

I agree with you.

 

curtvm wrote:

The C++ programmer has advantages in the naming department and organization and many other things that make life easier, but the way some of these things become used only makes it more complicated.

Yes, there is a trade-off to pay using modern C++.

 

curtvm wrote:

When a user (could be only you) is required to use multiple :: and a template parameter for a function call, then it may be time to rethink for a better way.

I don't agree with you. There is no need to rethink anything at all if the C++ programmer are using "multiple ::" or "a template parameter for a function call".

 

curtvm wrote:

overusing c++ features just because they are available.

It's the language. A language that have more than 30 years old but it is evolving at each 3 years.

 

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

Last Edited: Thu. Oct 29, 2020 - 11:43 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:

The language doesn't allow reordering of volatile reads or writes. We can't have for example something like:

        WDTCR = 0x00;
        WDTCR = WDTCR | (1<<WDCE) | (1<<WDE);

If what I'm saying is not your point, please, can you explain yourself with a bit more details?

I am assuming the GCC toolchain.  There has been much discussion over the years on code reordering with that toolchain, including recently.  The gurus will need to confirm.  Now, even if the lines in question aren't reordered, without precautions other code may get in the way of the for-cycle limitation.  That is why the provided e.g. watchdog handling facilities go to some pains to use the correct incantations to carry out the limitation.  [what will be done when the supply of eye-of-newt runs out?]

 

With the trivial posted program you probably can never force it.  But in a real app...

 

[edit]  The gurus discussed something very similar here:  https://www.avrfreaks.net/forum/...

 

 

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

Last Edited: Thu. Oct 29, 2020 - 11:42 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 1

gchapman wrote:

rcosme wrote:
Some errors that will be noticed only at runtime are moved to the compile phase.
fyi, C++20 adds constraints.

C++20 Serves Up Intriguing Embedded Features | Electronic Design

by William G. Wong

OCT 02, 2020

Seventh-generation C++20 is coming to a C/C++ compiler near you soon.

[1/3 page]

Concepts and Constraints

[in second paragraph]

...

Class templates, function templates, and non-template functions can have a constraint attached to them. The constraint is a predicate that’s evaluated at compile time. 

...

 

 

You can catch errors at compile time without resorting to C++20.

http://nerdralph.blogspot.com/20...

 

 

I have no special talents.  I am only passionately curious. - Albert Einstein

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

> The gurus discussed something very similar here

 

14 years ago a STS LDI STS did not meet the timing requirements? Seems odd to me.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

curtvm wrote:
14 years ago a STS LDI STS did not meet the timing requirements? Seems odd to me.

Can never trust those gurus.  [ I seem to recall a discussion of what "4" is at some point in the past]

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

ralphd wrote:

You can catch errors at compile time without resorting to C++20.

http://nerdralph.blogspot.com/20...

 

 

Thanks for ^.  Another advantage of LTO.   I will be digging into that in the future.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

theusch wrote:

Can never trust those gurus.  [ I seem to recall a discussion of what "4" is at some point in the past]

WDTCR = WDTCR | (1<<WDCE) | (1<<WDE);
WDTCR = 0x00;

The second statement should be executed within the next four cycles(ATtiny85 Datasheet Section 8.4.1.1.2). The avr-gcc can at least generate a set of instructions to this operation using 5 cycles when the program is compiled with -O0:

ldi r24,lo8(65)
ldi r25,0
movw r30,r24
st Z,__zero_reg

The 'st Z, Rr' instruction takes 2 cycles here.

https://godbolt.org/z/8153To

 

BTW, the snippet above written in C is from the datasheet. I think that should be a note about this in the example using C to disable WDT.

In the end, I didn't write the component thinking that the programmer will be using it with -O0, *but* I think that there shouldn't be a bug because of this.

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

rcosme wrote:
*but* I think that there shouldn't be a bug because of this.

So your facilities will be provided for all, with "all" meaning only those who might code according to your expectations.  Of course you can do whatever you want.  But be sure to consider that the toolchain gurus went to some pains to make provided tool  snippets bulletproof and applicable to all situations.

 

 

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

theusch wrote:

So your facilities will be provided for all, with "all" meaning only those who might code according to your expectations.

What I'm trying to say is that I think that a library can't have bug when the user is using -O0. BTW, I already have fixed the implementation[1] before my reply #15.

 

[1] https://github.com/ricardocosme/...

github.com/ricardocosme

avrIO: Operation of I/O port pins and I/O registers.