Author Topic: Waking ULPDelay(INFINITE) in loop() - From RADIO onCallbacks - No Pin waste  (Read 12506 times)

tolson

  • Global Moderator
  • *****
  • Posts: 870
  • Karma: +20/-0
    • View Profile
    • Thomas Olson Consulting
Here is Library Lazarus. With it you can wake up the RFduino main loop() from INFINITE sleep mode from within RADIO callbacks.

The RFduino library provides an Ultra Low Power Sleep Mode using RFduinoBLE_ULPDelay(). When called within the main loop() with the argument INFINITE, the RFduino will sleep forever. The loop() will not continue until an external signal arrives on a GPIO pin defined as a PinWake interrupt.

The RFduino RADIO callbacks are not affected by the ULPDelay() in the main loop(). RADIO callbacks will wake up the RFduino, do their specific tasks, and go back to sleep. RADIO callbacks have to be quick routines less problems occur.

If a RADIO callback needs to perform a time consuming or complicated task it needs to count on the main loop() to perform that task. Setting a FLAG that the main loop() can test for is typical. This works great if the main loop() doesn't sleep or only sleeps for small units of time. If the main loop() is sleeping for a long time then it will be a long time before the FLAG can be tested and may well be too late.

If it is desired to have the main loop() sleep for a long time, or forever, yet take priority commands from RADIO callbacks, then a  method to force the loop() to wake up is highly desired. Ideally, a method that does not waste any of the precious 7 GPIO pins  currently available; and preferably a firmware based solution.

Here comes Lazarus. Lazarus uses an unused GPIO pin internal to the RFduino to perform it's magic. My library is attached.

Here is the just of how to use it...

Code: [Select]

/* Lazarus
   by Thomas Olson 20140807.01
   This is an example of how to wake an RFduino loop() from
   INFINTIE sleep mode from a BLE event instead of an RFduino
   Pin Change.
*/
#include <RFduinoBLE.h>
#include <Lazarus.h>
Lazarus Lazarus;

void setup() {
  Serial.begin(9600);
  RFduinoBLE.advertisementData = "Lazarus Test";
  RFduinoBLE.begin();
}

void loop() {

  Serial.println("RFduino LOOP going to sleep forever");
  RFduinoBLE_ULPDelay(INFINITE);
  if(Lazarus.lazarusArising()){
    Serial.println("Lazarus has awakened!");
    Serial.println("");
  }
 
  // Do whatever you need to do in the LOOP while alive again!
  delay(100); // just waste some time for this test

}

// I want to wake up main loop() from INFINITE sleep
// when BLE_onConnect occurs
void RFduinoBLE_onConnect() {
  Serial.println("Connected");
  Lazarus.ariseLazarus(); // Tell Lazarus to arise.
}

// In this example I don't do anything with onDisconnect
void RFduinoBLE_onDisconnect() {
  Serial.println("Disconnected");
}


How does it work? I've discovered that diddling the PinWake feature for a pin can cause the sleep to trip off and back on. This can be done with one of the 7 GPIO pins if you really want to and are using that pin anyways as an interrupt. However, doing this hack to an internal pin allows you to specifically deal with RADIO only callbacks.

In a more complicated arrangement you would be testing for external Pin Wake signals to deal with whatever those interrupts are doing hardware wise, and also testing the Lazarus to see if the RADIO caused the awakening. No need to waste multiple internal pins either for different RADIO callbacks. Each type of callback can set their own defined FLAG, and just call Lazarus, then in the main loop within the Lazarus test you test for the possible FLAGs.

Have Fun! Phew!
« Last Edit: August 08, 2014, 09:03:51 PM by tolson »

timb103

  • RFduino Newbie
  • *
  • Posts: 4
  • Karma: +0/-0
    • View Profile
Thanks, tolson!  I tried it out, and found it works pretty well - now we can make low power and responsive applications.

My testing found that it works well for the internal pin (31), but when I tried GPIO pin 3 it didn't work. I suppose it's possible that some pins are wired differently. No matter.   Also,  there seems to be a small wrinkle in that the sleep is broken spontaneously the first time without the pin being twiddled (i.e. without calling 'arise' in terms of your library), but after that it behaves well. Resetting the pin at the start (in the constructor) cures this:

Code: [Select]
Lazarus::Lazarus()
{
  _lazarus = 31; // MAGIC NUMBER
  pinMode(_lazarus, INPUT);
  RFduino_pinWake(_lazarus,LOW);
  RFduino_resetPinWake(_lazarus); // +++ <-- timb103 added this line 
}


This may make a difference to those testing with INFINITE sleeps.

Well Done!

TomWS

  • RFduino Jr. Member
  • **
  • Posts: 26
  • Karma: +0/-0
    • View Profile
Very clever using a buried pin!  Well done!

I've been think about a mesh application but was concerned about having to keep each device in the mesh awake enough to maintain inter-device communication and message forwarding.  Handling just radio events until something 'interesting' needs to be done is perfect!

Thanks!


tolson

  • Global Moderator
  • *****
  • Posts: 870
  • Karma: +20/-0
    • View Profile
    • Thomas Olson Consulting

My testing found that it works well for the internal pin (31), but when I tried GPIO pin 3 it didn't work. I suppose it's possible that some pins are wired differently. No matter.   Also,  there seems to be a small wrinkle in that the sleep is broken spontaneously the first time without the pin being twiddled (i.e. without calling 'arise' in terms of your library), but after that it behaves well. Resetting the pin at the start (in the constructor) cures this:

Code: [Select]
Lazarus::Lazarus()
{
  _lazarus = 31; // MAGIC NUMBER
  pinMode(_lazarus, INPUT);
  RFduino_pinWake(_lazarus,LOW);
  RFduino_resetPinWake(_lazarus); // +++ <-- timb103 added this line 
}

The other part of the magic of this hack is that the pinWake trick seems to only work reliably for LOW. Seems like combinations with HIGH don't always work. This could well be an issue when used on external pins as many times one wants the WAKE to be on a HIGH. I forgot to mention that. I don't see lazarus has having much usefulness on an external pin. So I highly recommend using the lazarus trick on the internal pin only.

Hopefully for whatever reason this hack works, at all, it will continue to work this way on future releases of the RFduino libraries.


« Last Edit: August 08, 2014, 09:24:54 AM by tolson »

TomWS

  • RFduino Jr. Member
  • **
  • Posts: 26
  • Karma: +0/-0
    • View Profile
It is curious that the keyword "Lazarus" isn't highlighted in the source code.  The keyword file has it defined as "KEYWORD1"...

I'm new to Arduino tools so maybe something else needs to be done other than copying the files into the libraries folder and restarting the tool.


tolson

  • Global Moderator
  • *****
  • Posts: 870
  • Karma: +20/-0
    • View Profile
    • Thomas Olson Consulting
Also,  there seems to be a small wrinkle in that the sleep is broken spontaneously the first time without the pin being twiddled (i.e. without calling 'arise' in terms of your library), but after that it behaves well. Resetting the pin at the start (in the constructor) cures this:

Code: [Select]
Lazarus::Lazarus()
{
  _lazarus = 31; // MAGIC NUMBER
  pinMode(_lazarus, INPUT);
  RFduino_pinWake(_lazarus,LOW);
  RFduino_resetPinWake(_lazarus); // +++ <-- timb103 added this line 
}

Good addition to the constructor. I hadn't notice the false trigger at startup as it was done before I could bring up my serial monitor. But I verified what you said by adding a 5 second delay in setup and temporarily moving the Lazarus declararion after the setup(). That gave me time to bring up the serial monitor and observe that in deed lazarus was already speaking. I've added your line to my library and re-uploaded it. Thanks.

tolson

  • Global Moderator
  • *****
  • Posts: 870
  • Karma: +20/-0
    • View Profile
    • Thomas Olson Consulting
It is curious that the keyword "Lazarus" isn't highlighted in the source code.  The keyword file has it defined as "KEYWORD1"...

I'm new to Arduino tools so maybe something else needs to be done other than copying the files into the libraries folder and restarting the tool.

Yep, I'm not up on KEYWORD usage. KEYWORD1 is suppose to be for classes, I think. I thought the separation of the fields in the file had to be TAB separated, not any old white space. But it turns out I had two TABS in that particular line to make the fields line up and look pretty. Now I realize it requires 1 TAB and only 1 TAB between fields. That works. I fixed it in the tarball.

TomWS

  • RFduino Jr. Member
  • **
  • Posts: 26
  • Karma: +0/-0
    • View Profile
<...snip>

Yep, I'm not up on KEYWORD usage. KEYWORD1 is suppose to be for classes, I think. I thought the separation of the fields in the file had to be TAB separated, not any old white space. But it turns out I had two TABS in that particular line to make the fields line up and look pretty. Now I realize it requires 1 TAB and only 1 TAB between fields. That works. I fixed it in the tarball.
Gosh!  What is this, Python?!!!

Thanks for figuring this out.  It's not that important, but one of those things that continues to nag...

Tom

tolson

  • Global Moderator
  • *****
  • Posts: 870
  • Karma: +20/-0
    • View Profile
    • Thomas Olson Consulting
A couple more things I discovered. While the signal to a pin is in the wake level no other pin that uses pinWake can respond or rather be responded to. I found a bug in my library where I was returning true for my tests. This is probably what kept me from seeing activity on buttons. So we need to make sure our external signals to pinWake pins don't stay in the trigger level for long.

Also, I was using pinWake on LOW, which meant that there had to be pullups on the internal pin. The goal we are trying to attain is long sleep with minimal drain. So I changed to waking on HIGH and initialize with pulldowns. Now I am using the  nRF low level commands to toggle the internal pullup when I want to mimick a pin high connection. And then immediately toggle it the pulldown again, so as to not lock up the use of other pinWake pins.

I've uploaded the new library above. Included is a new example using the 2 buttons on the RGB shield on pins 5 and 6. As well as accepting wake ups from the RADIO.

The RGB shield has pulldowns on board and the buttons short to 3V for a high so will set the pinWake(HIGH). But, If the board is not installed the input pins will drift high by default  causing trouble. So use the pinMode(INPUT_PULLDOWN) to be failsafe.

Here is the example... LazarusButton.ino
Code: [Select]
/* Lazarus and Buttons Test
   by Thomas Olson 20140808.01
   
   This is an example of how to wake an RFduino loop() from
   INFINITE sleep mode from a BLE event.
   And the two buttons on the RGB shield
*/
#include <RFduinoBLE.h>
#include <Lazarus.h>
Lazarus Lazarus;

char buf[20]; //RFduino currently limited to 20 byte packets.
char BLE_FLAG;

#define buttonPin 5
#define buttonAlt 6

void setup() {
  delay(3000); // for debugging give me plenty of time to start serial monitor
  Serial.begin(9600);
  RFduinoBLE.advertisementData = "Lazarus Buttons";
  RFduinoBLE.begin();

  // Note if using RGB shield where buttons pullup and resistors pulldown OK. But
  // if you run without the board installed then better have pulldowns on CPU board.
  // Also PinWake will lock up if buttons get stuck on. So make sure input trigger
  // signals always return to normal. 
  pinMode(buttonPin,INPUT_PULLDOWN); // So make sure failsafe.. turn on cpu pulldowns
  RFduino_pinWake(buttonPin,HIGH);
  pinMode(buttonAlt,INPUT_PULLDOWN);
  RFduino_pinWake(buttonAlt,HIGH);
}
 
void loop() {

  Serial.println("RFduino LOOP going to sleep forever");
  RFduinoBLE_ULPDelay(INFINITE);
  if(Lazarus.lazarusArising()){
    Serial.println("Lazarus has awakened!");
    Serial.println("");
    // insert call to function to deal with RADIO initiated demands.
    lazarus_duty();
  }
  if(RFduino_pinWoke(buttonPin)){
    RFduino_resetPinWake(buttonPin);
    // insert call to function to deal with buttonPin activity
    buttonPinActivity();
  }
 
  if(RFduino_pinWoke(buttonAlt)){
    RFduino_resetPinWake(buttonAlt);
    // insert call to routine to deal with buttonAlt activity
    buttonAltActivity();
  }
 
  // Do whatever you need to do in the LOOP while alive again!
  delay(100); // just waste some time for this test

}

void buttonPinActivity(){
  Serial.print("Button Pin: ");
  Serial.print(buttonPin);
  Serial.println(" Pressed!");   
}

void buttonAltActivity(){ 
  Serial.print("Button Alt: ");
  Serial.print(buttonAlt);
  Serial.println(" Pressed!");
}

void lazarus_duty(){
  switch(BLE_FLAG){
    case 'C':
      Serial.println("BLE Connected");
      break;
    case 'R':
      Serial.println("BLE Received Buffer");
      Serial.print("buf[0]= ");
      Serial.println(buf[0], HEX);
      break;
    case 'D':
      Serial.println("BLE Disconnected");
    default:
      break;
  }
}

void RFduinoBLE_onConnect() {
  BLE_FLAG = 'C';
  Lazarus.ariseLazarus(); // Tell Lazarus to arise.
}

void RFduinoBLE_onDisconnect() {
  BLE_FLAG = 'D';
  Lazarus.ariseLazarus();
}

void RFduinoBLE_onReceive(char *data, int len) {
  if (len>=1){
    buf[0] = data[0];
  }
  BLE_FLAG = 'R';
  Lazarus.ariseLazarus();
}



« Last Edit: August 08, 2014, 09:30:46 PM by tolson »

TomWS

  • RFduino Jr. Member
  • **
  • Posts: 26
  • Karma: +0/-0
    • View Profile
Tolson,
In looking at the new ariseLazarus() function, it seemed as if the call to RFDuino_pinWake() was unnecessary since the pin was already configured to wake and the essential code was toggling the pullup/pulldown control.  However, I noticed that the configuration statements weren't preserving the existing states of the other bits in the CNF word (including the Sense fields that may have been set by the RFDuino_pinWake() function).

I suspect something like this may be appropriate:
Code: [Select]
void Lazarus::ariseLazarus()
{
  // need to bring internal pin high somehow.. so..
  NRF_GPIO->PIN_CNF[_lazarus] = (NRF_GPIO->PIN_CNF[_lazarus] & ~GPIO_PIN_CNF_PULL_Msk) |
      (GPIO_PIN_CNF_PULL_Pullup<<GPIO_PIN_CNF_PULL_Pos);
  // RFduino_pinWake(_lazarus,HIGH);  // TWS: Shouldn't be needed again
  __NOP(); __NOP(); __NOP(); __NOP(); // TWS: but might need these to provide time for interrupt recognition
  // need to bring internal pin low again.. so..
  NRF_GPIO->PIN_CNF[_lazarus] = (NRF_GPIO->PIN_CNF[_lazarus] & ~GPIO_PIN_CNF_PULL_Msk) |
      (GPIO_PIN_CNF_PULL_Pulldown<<GPIO_PIN_CNF_PULL_Pos);
}

Unfortunately I won't be able to test this change for a couple of days (my daughter is visiting  :)
but I will some time next week if you don't get to it sooner.

Tom

tolson

  • Global Moderator
  • *****
  • Posts: 870
  • Karma: +20/-0
    • View Profile
    • Thomas Olson Consulting
The reason I still have the pinWake call in the riseLazarus function is it is doing something else besides just manipulating the PIN_CNF bits. Perhaps it is manipulating GIOPTE. Would be nice to get source code or insite from RF Digital into exactly what pinWake is doing.

A few things learned.
pinMODE(LazarusPin, INPUT_PULLDOWN) does not activate pulldowns. Hmm! So need to do that with PIN_CNF.
By default, the input buffer is not connected. Activating pulldown or pullup will activate the input buffer so no need to fiddle with those PIN_CNFs.

Doing pinWake in riseLazarus will not work unless the pullup is on. pinWake will then kick off immediately even though it does not set the sense bits in PIN_CNF.

Forcing sense level in PIN_CNF makes no difference kicking off the WAKE. The mere act of executing pinWake HIGH with the PULLUP on does what is needed. Just need to turn PULLDOWN back on to keep from ignoring future pinWakes particular on other pins.

Here is Lazarus for testing without Library.

Code: [Select]
#include <RFduinoBLE.h>

#define LazarusPin 31

void setup() {
  Serial.begin(9600);
  RFduinoBLE.advertisementData = "Lazurus Test";
  RFduinoBLE.begin();
  delay(1500);
  Serial.print("setup(): Default: ");
  printL();
  pinMode(LazarusPin,INPUT_PULLDOWN);
  Serial.print("setup(): pinMode to input pulldown: ");
  printL();
  NRF_GPIO->PIN_CNF[LazarusPin] =
      (GPIO_PIN_CNF_PULL_Pulldown<<GPIO_PIN_CNF_PULL_Pos);
  Serial.print("setup(): pulldown ");
  printL(); 

// As long as pinWake is included in the ariseLazarus function... not needed here.
// Uncomment if you wish to see.
//  RFduino_pinWake(LazarusPin,HIGH);
//  Serial.print("setup(): post pinWake HIGH: ");
//  printL();
}

void loop() {

  Serial.println("main loop() going to sleep forever");
  RFduinoBLE_ULPDelay(INFINITE);
  if(lazarusArising()){
    Serial.println("Lazarus has awakened!");
    printL();
    Serial.println("");
  }
 
  // Do whatever you have to do while are alive again!
  delay(100); //just waste some time
 
}

bool lazarusArising(){
  if(RFduino_pinWoke(LazarusPin)){
Serial.print("pinWoke Test: ");
printL();
    RFduino_resetPinWake(LazarusPin);
Serial.print("resetPinWake: ");
printL();
    return HIGH;
  }
  return LOW;
}

void ariseLazurus(){
  Serial.print("\n\nAttempting to Wake Lazarus from Infinite Sleep!  \n ");
 Serial.print("arise entry: ");
 printL();

// NOTE: SENSE and Connect not really needed. Just PULL and call to pinWake!
// Uncomment the extra PIN_CNF lines here and below to see..
    NRF_GPIO->PIN_CNF[LazarusPin] =
//        (GPIO_PIN_CNF_INPUT_Connect<<GPIO_PIN_CNF_INPUT_Pos) |
//        (GPIO_PIN_CNF_SENSE_High<<GPIO_PIN_CNF_SENSE_Pos) |
        (GPIO_PIN_CNF_PULL_Pullup<<GPIO_PIN_CNF_PULL_Pos);
 Serial.print("arise post pullup: ");
 printL();

  RFduino_pinWake(LazarusPin,HIGH);
  Serial.print("arise post pinWake: ");
 printL();

    NRF_GPIO->PIN_CNF[LazarusPin] =
//        (GPIO_PIN_CNF_INPUT_Connect<<GPIO_PIN_CNF_INPUT_Pos) |     
//        (GPIO_PIN_CNF_SENSE_High<<GPIO_PIN_CNF_SENSE_Pos) |
        (GPIO_PIN_CNF_PULL_Pulldown<<GPIO_PIN_CNF_PULL_Pos);
 Serial.print("arise post pulldown pre-exiting: ");
 printL();
}

void printL(){
  Serial.print("Lazarus PIN CNF: ");
   printVal(NRF_GPIO->PIN_CNF[LazarusPin]);
   Serial.println("");
}
void printVal(uint32_t val){
 printf("%02x %02x %02x %02x\n",val>>24 & 0xFF,val>>16&0xFF,val>>8&0xFF,val&0xFF);
}

void RFduinoBLE_onConnect() {
  Serial.println("Connected");
  ariseLazurus();

}

void RFduinoBLE_onDisconnect() {
  Serial.println("Disconnected");
}

« Last Edit: August 10, 2014, 12:45:58 AM by tolson »

digibruce

  • RFduino Newbie
  • *
  • Posts: 1
  • Karma: +0/-0
    • View Profile
Thanks, tolson, that works even for a clueless and lazy iOS dev like me! And such important functionality! But my head hurts just from reading carefully through this thread...

TomWS

  • RFduino Jr. Member
  • **
  • Posts: 26
  • Karma: +0/-0
    • View Profile
The reason I still have the pinWake call in the riseLazarus function is it is doing something else besides just manipulating the PIN_CNF bits. Perhaps it is manipulating GIOPTE. Would be nice to get source code or insite from RF Digital into exactly what pinWake is doing.

A few things learned.
pinMODE(LazarusPin, INPUT_PULLDOWN) does not activate pulldowns. Hmm! So need to do that with PIN_CNF.
By default, the input buffer is not connected. Activating pulldown or pullup will activate the input buffer so no need to fiddle with those PIN_CNFs.

Doing pinWake in riseLazarus will not work unless the pullup is on. pinWake will then kick off immediately even though it does not set the sense bits in PIN_CNF.

Forcing sense level in PIN_CNF makes no difference kicking off the WAKE. The mere act of executing pinWake HIGH with the PULLUP on does what is needed. Just need to turn PULLDOWN back on to keep from ignoring future pinWakes particular on other pins.

Here is Lazarus for testing without Library.


Fascinating... Like you said, it would be nice to have the source for the PinWake and GPIOTE functions. 

Thanks for sending the test code.  Unconvinced that saving the sense bits didn't matter I played around with that and also dumped the GPIOTE Config registers at key points.  What I found is that you were exactly right, the sense bits, even though they changed at various states, didn't seem to matter in the workings of things.  More interestingly, the GPIOTE Config registers were always 0!

Finally, it appears, at least with my testing, that the PinWake MUST be executed inside ariseLazarus.  This confounds me.  I had expected to be able to do call pinWake once within setup() and, as long as I save the PIN_CNF bits, setting Pullup High would cause the event - not so, nothing happened unless pinWake was executed inside ariseLazarus.

Confounded, but, at least, with working code, I thank you for your efforts.  This will serve my needs nicely.

PS: I was curious about whether the pinWakeCallback function might be useful in this regard, but I'm not seeing it.  The callback allows you to do things 'under the covers' without exiting the ULPDelay()...  Lazarus allows us to force an exit from ULPDelay() without consuming an RFDuino GPIO.

tolson

  • Global Moderator
  • *****
  • Posts: 870
  • Karma: +20/-0
    • View Profile
    • Thomas Olson Consulting
... it appears, at least with my testing, that the PinWake MUST be executed inside ariseLazarus.  This confounds me.  I had expected to be able to do call pinWake once within setup() and, as long as I save the PIN_CNF bits, setting Pullup High would cause the event - not so, nothing happened unless pinWake was executed inside ariseLazarus.

Effectively, the wakeup will be activated during the pinWake setup call if the signal on the pin is already in the trigger state. This is why in my first release of the library there was an initial false trigger when instantiating the library as discovered by timb103. I was using trigger on LOW and the default GPIO pin configurations were set to LOW. Thus pinWake immediately did an interrupt.

Since I am using the lazarus pin, which has no other usefulness, it doesn't really matter what any of it's pin configurations are as long as it is initially set pulled LOW and then pulled HIGH just before calling the pinWAKE HIGH and pulling LOW again immediately afterward. Since the lazarus pin is a buried pin the only way we can change it's apparent input condition is using the PULLUP and PULLDOWN features.

Have fun!

TomWS

  • RFduino Jr. Member
  • **
  • Posts: 26
  • Karma: +0/-0
    • View Profile
I suspect that, somewhere along the way, we'll discover that a significant portion of the logic in the undocumented functions are implemented purely in software and have nothing to do with pin configurations and, in all probability, are table driven with the table sizes limited to existing RFDuino pins, rather than the max supported by the underlying ARM.  Consequently, you need to have the pin in its trigger state prior to calling pinWake, otherwise it won't get detected.  And setting pinMode on an internal pin is ignored... 

I did try the pinWakeCallback with no trigger occurring when logically it should have.  Unless the implementation is a limited table-driven one.  Just a guess.

Too bad we don't have the code...  These aren't exactly 'intellectual property' pieces of code we're talking about...

In any case, it's good to experiment and maybe we can piece it all together on our own.

Later...

T