Electronic – PIC32 sleep mode with watchdog wakeup failing

low-powerpicsleepwatchdog

I am in the process of trying to get a PIC32MX795F512L to run with lower power consumption. I'm trying to get it to enter sleep mode and then be woken by the watchdog timer. All pretty standard stuff.

My code works on the first triggering of the watchdog timer, but the second triggering isn't triggered as if it were in sleep mode, but in normal running mode. As a result it doesn't continue execution but instead resets the whole chip.

On the PIC32 the watchdog timer, when in sleep or idle mode, causes an NMI with the same vector as reset. You then check some flags in a register to see if it was caused by the watchdog in sleep mode, etc.

My startup code looks like this:

_reset:
    la      k0, RCON        # Load address of RCON register

    lw      k1, 0(k0)       # Get contents of the register
    and     k1, k1, 0x18    # We are only interested in 0x18
    sub     k1, 0x18        # Subtract 0x18
    beqz    k1, _ret_nmi    # and if the result is 0 (i.e., equal to 0x18) then branch.

    lw      k1, 0(k0)       # Same again but looking for 0x14.
    and     k1, k1, 0x14
    sub     k1, 0x14
    beqz    k1, _ret_nmi
    nop

    la      k0, _startup
    jr      k0                      # Jump to startup code
    nop

_ret_nmi:
    lw      k1, 0(k0)
    and     k1, 0xFFE3
    sw      zero, 0(k0)
    eret

Basically it looks for 0x18 or 0x14 being set in the RCON register, then clearing those bits and returning from an interrupt if they are set.

Sleep mode is entered by setting the SLEEP bit in OSCCON (which needs unlocking first), and according to the power saving manual for PIC32 is done like this:

// Standard unlock sequence
SYSKEY = 0x0;
SYSKEY = 0xAA996655;
SYSKEY = 0x556699AA;
OSCCONSET = 0x10; // Enable sleep mode
SYSKEY = 0x0;

You then enable the watchdog timer, "kick the dog" as it's known, and stop the CPU with a wait instruction:

WDTCONSET = 1<<15; // Turn on
WDTCONSET = 0x01; // Kick the dog!
uint32_t i = disableInterrupts(); // We don't want any old interrupt waking us up
asm volatile("wait");
restoreInterrupts(i);

So that causes the CPU to stop and then the NMI triggers after 1.024 seconds. The CPU restarts from the reset vector, the startup code checks the flags, finds it's an NMI, and returns from interrupt continuing with the next line of code.

The first time.

The second time the RCON register contains 0x10 instead of 0x18, so it acts like a timeout from not kicking the dog.

Inspecting OSCCON after the failed timeout the SLEEP bit seems to have been reset. Setting the SLEEP bit every time through the main loop just before sleeping has no effect.

However

If I do the exact same thing but using idle mode instead of sleep mode, everything works perfectly. The wait instruction continues after 1.024 seconds every time without failure.

So why does this not work as it should do in sleep mode?

Is there something obvious I am missing?

Update

I have tried forcing the CPU priority level to be the lowest possible before sleeping, but it has had no effect. This is the code I am using for it:

asm volatile("mfc0 $8, $12");
asm volatile("ins $8, $0, 10, 3");
asm volatile("mtc0 $8, $12");
asm volatile("wait");

Best Answer

Ok, I now have this working properly. And here's how:

  1. Read the manual.
  2. Throw the manual away.
  3. Write a working program in a different IDE / compiler.
  4. Disassemble the resultant program and see how it should be done.

XC32 builds in code to handle the NMI into its reset handler for you, and the code it generates bears no resemblance to what the manual says you should do. Instead it examines the NMI status of C0. If it was an NMI then do an eret, otherwise start the program normally.

Here's the relevant bits of the program disassembled:

bfc00000:   401a6000    mfc0    k0,c0_status
bfc00004:   7f5a04c0    ext k0,k0,0x13,0x1
bfc00008:   13400005    beqz    k0,bfc00020 <_no_nmi>
bfc0000c:   00000000    nop
bfc00010:   3c1a9d00    lui k0,0x9d00
bfc00014:   275a02a8    addiu   k0,k0,680
bfc00018:   03400008    jr  k0 <_nmi_handler>
bfc0001c:   00000000    nop

9d0002a8 <_nmi_handler>:
9d0002a8:   401a6000    mfc0    k0,c0_status
9d0002ac:   3c1bffbf    lui k1,0xffbf
9d0002b0:   377bffff    ori k1,k1,0xffff
9d0002b4:   035bd024    and k0,k0,k1
9d0002b8:   409a6000    mtc0    k0,c0_status
9d0002bc:   42000018    eret

So you see it doesn't care why the NMI happened - it just erets regardless.

With that style of doing it in my startup routine it now works!