esp8266 / Arduino

ESP8266 core for Arduino
GNU Lesser General Public License v2.1
15.99k stars 13.33k forks source link

ESP.getResetReason() doesn't allow to distinguish between timer wakeup and physical reset during deep sleep #7067

Closed php4fan closed 4 years ago

php4fan commented 4 years ago

Consider these three different situations:

  1. During program execution, the ESP gets reset by shorting RST to ground.
  2. During deep sleep, the ESP gets woken up by externally shorting RST to ground.
  3. The ESP has GPIO16 connected to RST and it gets up from deep sleep when the built-in timer goes off.

Expected: ESP.getResetReason() should return different values in these situations, allowing you to distinguish 2 from 3 as well as from 1.

Observed: ESP.getResetReason() returns "External System" in case 1, but it returns "Deep-Sleep Wake" both in cases 2 and 3, so they are not distinguishable.

I know that the way automatic wakeup works is that GPIO16 goes low, hence driving RST to ground exactly as you would do by physically shorting it to ground before the timer goes off, but there are plenty of ways you can internally accomplish the result of getting 3 different values for the 3 different sistuations. There is a built-in RTC that you are talking to after all, which keeps running during deep sleep.

Pablo2048 commented 4 years ago

Well, your "plenty of ways" are just two IMHO - store this info to the (simulated) EEPROM, or use RTC storage (the really only way to accomplish this without burning out the Flash). I'm also using RTC storage in my application/s so I don't like that somewhere in the core is something, that can sometime overwrite my own content (or block part of RTC memory even when I not need this feature). There is no need to put this technique to the core, but you can do it in your own code the same way as you described. What do you think?

php4fan commented 4 years ago

store this info to the (simulated) EEPROM, or use RTC storage

None of those will help. The second is what getResetReason() already does and it's what allows it to distinguish between case 1 on one hand, and case 2 or 3 on the other, but not between 2 and 3. It writes something to RTC storage just before it goes to sleep, and it resets it when it wakes up: so, when it wakes up (before it resets whatever it is it wrote), if that thing is in the storage, it knows that it has waken up from deep sleep (as opposed to resetting during execution); but it can't know whether it woke up because the timer went off or because RST was externally shorted to ground.

I'm sure the RTC hardware interface provides a way to know whether it fired a wakeup or not.

Some time ago I looked a bit into this and found out the following. There is a low level function of which I can't remember the name that returns the time of the RTC, that is, since it was last reset. I found out the actual code in the source code of the espressif SDK that actively reset the RTC at startup, so that function actually always return the time since boot, which makes it identical to another function that is supposed to do that.

Let's invent fake names for these functions whose actual names I don't remember, because otherwise this is confusing:

get_system_time() : returns time since program execution began get_rtc_time(): returns time since the RTC was reset.

Because the SOFTWARE is resetting the RTC at startup, and there's nothing I can do to prevent it from doing that, those two return the same value (perhaps a few microseconds apart).

If only the software wasn't resetting the RTC at startup, I could easily know how logn it's been since I went to deepsleep, and, with a little bit of work (just knowing how long I had set the deepsleep to be) I could know within a small margin of error whether or not this is an automatic wakeup or not (or whether or not I've been asleep as long as the deepsleep was supposed to be).

devyte commented 4 years ago

My understanding was that it's not possible to distinguish the cases, but from your comments maybe that's wrong. To discuss further, please provide the details you mention:

php4fan commented 4 years ago

My understanding was that it's not possible to distinguish the cases, but from your comments maybe that's wrong.

To clarify:

However, I don't know exactly where the espressif SDK ends and esp8266-Arduino begins, so I'm not sure whether it's something that you can fix in esp8266-Arduino or it requires a fix by espressif in the SDK itself. Perhaps there might be something in the RTC system memory (or somewhere) that they have "forgot" to reset and that you may use as a workaround?

code in the sdk that resets the RTC

Unfortunately when I found it I didn't take notes.

function that tells time since last RTC reset

system_get_rtc_time()

Because the RTC gets reset at startup, system_get_rtc_time() becomes essentially a useless duplicate of system_get_time() with a small almost-fixed offset.

devyte commented 4 years ago

@php4fan we've discussed this internally. Assuming you're right, and it is in fact possible to differentiate between 2 and 3 via software, we consider the effort likely to be high for the return. You're the user who needs this. Are you willing to pursue yourself? We can provide guidance and answer questions as we can, but the effort would need to be yours.

php4fan commented 4 years ago

Assuming you're right, and it is in fact possible to differentiate between 2 and 3 via software, we consider the effort likely to be high for the return.

Seriously??

I think you are seriously underestimating the "return". You are not thinking about the use cases.

You're the user who needs this.

That made me laugh hard. THE user. Right.

When googling for a solution I found quite a few instances (even from years ago) of people asking how to accomplish this (of course the answer always being you cannot). Here's an example: https://bbs.espressif.com/viewtopic.php?f=6&t=2383

Being able to know whether the board has been reset manually or woken up at the programmed time is a very basic need for any project, that currently is not satisfied. The "return" here is for any user (i.e. developer) to be able to do such a basic obvious thing. To overcome a huge, nonsensical limitation. I can't imagine an effort too big to be worth that.

Here's my example use case: I have an IoT device that normally "does its thing" once a day. Its thing is to connect to the internet, download data from a given URL, and refresh an image on an e-paper. Because the maximum deep-sleep time is about three hours, it wakes up about every 3 hours, check whether it's time to do its thing (by counting the time it has woken up this way), and either do it or go back to sleep. But the user should also be able to force an update at any time by pressing a button. This button is just a reset button. Because of the issue at hand, the program can't tell whether it has woken up from deepsleep because the timer has gone off, or because the user has pressed the button. So a button press is indistinguishable from a wake-up.

So my current workaround is to instruct my users to push the button twice rather than once. That I can easily distinguish (I leave the how as an exercise to the reader). However this is a shitty user interface, counterintuitive AF. A single button press is something the user is never supposed to do, and if they do, it results in nothing immediately, and in the next update happening at the wrong time (only the next, and again I leave how I manage to trigger the following updates at the right time as an exercise to the reader).

If you replace "downloading data from the internet and refresh an image on an e-paper display" with "doing something", the description of my use case becomes:

"an IoT device that spends most of its time in deep sleep and periorically wakes up to do stuff. But it can also be forced to do that stuff at any given time by pressing a button".

To me, that seems like the description of practically any IoT device (and several kinds of non-IoT devices).

Of course, you can add hardware to accomplish the task. Either by adding an external RTC clock, or by adding circuitry that, at the press of the button, will keep a voltage high for a short time, and having the software read that voltage via an input (that implies that the button must be "decoupled" from the contact between RST and GND, because otherwise, the system wakeup would trigger the same thing that the button does; so, the button needs to trigger that voltage change and drive a switch that closes the contact between RST and GND. Just in case you missed how much circuitry one would need to add).

Having to add extra circuitry just because the software is too lazy to do its job - no, actually, because the software is overzealous and does something that it shouldn't even do, that is, reset the RTC. It's a ridiculous amount of circuitry you need to add. In my case the limiting factor is that all the GPIOs of my ESP8266 are already taken by the interface with the e-paper display, so I wouldn't even have that one spare pin to read that hypothetical voltage triggered by the button (ignoring all the additional circuitry needed to do that), let alone to communicate with an external RTC.

Now, that's for the "return". Regarding the effort.

Ok so maybe you understand the above and are not underestimating the return, but the effort is indeed great.

Now, if tackled where it should be tackled, that is at the SDK level, it cannot be a great effort at all. I think I have found where the RTC is reset: https://github.com/espressif/ESP8266_RTOS_SDK/blob/master/components/esp8266/source/startup.c#L85

I'm not entirely sure, I can't find the source of rtc_init() but I guess it resets the RTC.

Anyway, I can easily be wrong about that line of code, but it's clear that all is needed is to not reset the RTC at startup.

Now, from a esp8266-Arduino perspective. Fixing it from esp8266-Arduino could indeed be difficult. Actually, it could be completely impossible. If the SDK code is resetting everything, I'm afraid there might be nothing you can do at the arduino level.

the effort would need to be yours

I'm afraid I wouldn't even know where to start. I know nothing about programming an arduino core; I barely even know any C++ at all. I have a hard enough time programming in Arduino already.

Anyway, here's what I tried and failed, in case it helps. Tried from the "user space" if I can call it that - from an arduino sketch.

1. Using preinit()

No luck there. When preinit() is called, the RTC has already been reset, so it doesn't help

2. This

// This is a .ino sketch
#include "user_interface.h"

class MyTestClass {

  private:
    uint32_t t = 0;
  public:
    MyTestClass() {
      this->t = system_get_rtc_time();
    }
    uint32_t getTime() {
      return this->t;
    }
};

MyTestClass test = MyTestClass();

const int SERIAL_BAUD_RATE = 74880;

uint32_t systime_before_init=0;

void preinit() {
  systime_before_init = test.getTime();
}

void setup() {
   Serial.begin(SERIAL_BAUD_RATE);

   Serial.println("---\n");
   Serial.printf("RTC time before init: %d\n", systime_before_init);
}

I thought this way I might be calling system_get_rtc_time() before whatever code resets the RTC.

And I suspect I actually am. The problem is, it doesn't work, it always returns 0.

I tried adding a delay(5) before the call, to rule out that this was still running after the RTC reset (hence meaning it would be a hopeless useless approach) no more than zero milliseconds after the RTC is reset. That doesn't seem to be the case, as even after adding the 5ms delay, I still get systematically 0. So I guess I have managed to run the call before the RTC is reset, but probably it's called even before other initialization that is needed in order for system_get_rtc_time() to work as expected.

If I'm right, then that gives me some hope. Maybe we can just talk to the RTC directly and get the time.

But it might also be that I'm missing something and that this approach makes no sense.

php4fan commented 4 years ago

I think I have found where the RTC is reset:

Scratch that, I think I was looking at the wrong SDK. The relevant one is the NONOS, right?

Also, there seems to be no source code at all?

No wonder you were saying it's going to be difficult to fix...

devyte commented 4 years ago

Correct, now you're starting to understand: what you're requesting amounts to at least a partial reverse engineering of the rtc handling code, which is in the closed lib blobs from Espressif. The FreeRTOS sdk can provide some hints, but it means investing a large amount of effort, of the order of several months, and none of the current maintainers are willing to spend it. There is a very long list of much higher priority issues on our plates, and an even longer list of issues waiting after that. So if you are unable/unwilling to pursue yourself, this should be closed. As I said, if you do intend to pursue, we can answer questions about what we know.

Tech-TX commented 4 years ago

The RTC is reset by the blob during the boot phase, so the user needs to use RTC RAM to track changes across Deep Sleep. By the time the blob passes control to SYS the details needed to discern between Deep Sleep and external reset are gone. We aren't going to get help from Espressif on Non-OS SDK as they've abandoned it and moved on to IDF.

I haven't looked at the BBS post yet, but I'd guess it's saying the same. I'll look later when I get home.

Delay() doesn't run in preinit, it just returns immediately as the timers aren't set up yet.

php4fan commented 4 years ago

so the user needs to use RTC RAM to track changes across Deep Sleep

If you're talking about writing to RTC RAM and then reading what you (i.e. the user) have written in it, then again, you are talking about distinguishing between 1 and {2,3} (already accomplished by getResetReason()) but not between 2 and 3.

php4fan commented 4 years ago

The RTC is reset by the blob during the boot phase

So if I understand correctly, that is before the constructor of MyTestClass in my example runs, right? (I guess that ugly example of mine can be simplified by just writing instructions at the top of the sketch outside of setup(), but I was under the impression you can't do that)

Then I guess there's nothing at all we (we users, and I'm afraid also you) can do, if we don't get a chance to read from the RTC before it is reset.

devyte commented 4 years ago

Execution of the global object constructors is under our control. See file esp8266_main.cpp functions user_init() at the bottom and init_done() above it for the start sequence, including where preinit() and do_global_ctors() are called. Of course, the app start happens only after eboot is already done.

Tech-TX commented 4 years ago

so the user needs to use RTC RAM to track changes across Deep Sleep

If you're talking about writing to RTC RAM and then reading what you (i.e. the user) have written in it, then again, you are talking about distinguishing between 1 and {2,3} (already accomplished by getResetReason()) but not between 2 and 3.

Yes, I saw the same issue when I was researching Deep Sleep, and couldn't find a spot early enough to save the RTC count. It was already cleared, thus impossible without a binary patch to the blob to NOP the RTC clear. That could be problematic if the blob does an integrity check.

Tech-TX commented 4 years ago

@php4fan One thing you might look at to see if it helps: FRC2, the free-running 32-bit counter that's used to set the ETS_Timer increments. It's apparently not cleared by the bootloader, so you might be able to read it in the RF_PRE_INIT() stage before it gets re-initialized. No bet on that, as I haven't looked at it yet. I'm pretty sure the ETS_Timers aren't running until somewhere around preinit(). I think the resolution of FRC2 is around 3.2 microseconds. If you grab it before it's cleared, then subtract off the time since boot began and your Deep Sleep interval, and if the remainder is a small number then it probably woke via GPIO16 and not external reset. I don't know how variable the boot time is, so it'll take some experimenting.

edit: No joy, sorry. On a cold start or after external reset, FRC2 shows a count of 222. After Deep Sleep (either RTC or external reset) it shows a count of 218, so it's cleared roughly 700us before RF_PRE_INIT(). FRC1 is no different: it's restarted before RF_PRE_INIT(). I also looked at the RTC counter, and it's count is all over the map in RF_PRE_INIT(). I didn't see anything that you could use there to determine reset cause.

php4fan commented 4 years ago

Thank you for the attempt.

But do any of those counters/clocks keep running during deep sleep? If not, there's no point in reading them even if they weren't reset.

Is patching the binary to nop the RTC reset something reasonably doable to try, or is it something that would require months of reverse engineering work? Because if it works, the benefit would be huge.

devyte commented 4 years ago

Patching itself is likely not hard. The hard part is figuring out where the magic happens, and what the sequence is. That involves disassembly of the blobs and investigation of the results. Every case is different, it could take a few weeks, or it could take several months, depending on your prior experience with reverse engineering. In addition to the obvious benefit, every bit we pull out of the blob and into our core as C/C++ code is a good thing.

Tech-TX commented 4 years ago

The RTC is running with a count up if I remember correctly; that's what fires the GPIO16 to drop when it reaches the end count. I think the Reset Cause is handled in the bootloader, not certain on that point. Search the dump for the address of the RTC count register and you might find it (objdump -S yourfile.elf > dump.txt) If it's in the bootloader you're out of luck. I renamed a copy of xtensa-lx106-elf-objdump to make it easier to type...

Everything but the RTC is shut down in Deep Sleep, and the RTC starts increasing in speed as the chip cools, so it's not an accurate clock. It's all you have, though.

5chufti commented 4 years ago

guess what? deepsleep wakeup works even without gpio16-rst connection, waking the chip and showing the correct reset reason/bootmode

ets Jan 8 2013,rst cause:5, boot mode:(3,6)

ets_main.c

but stops dead. SURPRISE espressif f'd the bootloader and it does not know what to do in this situation so the gpio16-rst seems to be a bad workaround for a borked bootloader.

php4fan commented 4 years ago

Wow, that's pathetic. I wonder if it would be possible to somehow patch that. That would fix not only the issue at hand, but also free up a valuable GPIO pin and stop us from having to put a jumper on and off (or put a switch and turn it on and off, as I do) every time we need to switch between uploading a sketch and enabling wakeup.

And now I also wonder about the fact that the deep sleep duration (i.e. the unit of increment of the RTC) is set in microseconds rather than milliseconds. Such precision is completely useless given that the clock drifts with a margin of error of around 10%. I bet sub- millisecond deep sleep is also useless as I'm pretty sure the time of going to sleep plus the boot time is way more than a ms anyway. So, there's no point in having the sleep time be set in microseconds, and if it was milliseconds, we would not be limited to 3 hours. I always assumed it had to be that way because of how the hardware works, but now I wonder if that was a deliberate idiotic design decision too.

devyte commented 4 years ago

The deep sleep issue is in the rom, and has been known for something like half a decade now. Fixing it would require making a new set of manufacturing masks, which has a 7 digit price tag, so no, especially given the gpio16-rst workaround, which works well enough. The discussion has gone off topic, and I don't see an avenue of resolution open yet. I'm closing the issue. If additional information is added here that shows results worthy of further investigation, I'll reopen at that time.

Rob58329 commented 3 years ago

Although not 100% reliable, the code below is able to determine if an ESP8266 re-start from DeepSleep was caused by the GPIO-16 rtc-timer, or by a Reset-Button-press.

extern "C" {
  #include "user_interface.h"
}

void setup() {
  uint32_t count[2]={RTCSV,RTCCV};         // see "esp8266_peri.h" for further info
  // uint32_t count=RTCSV;                 // see "esp8266_peri.h" for further info
  // uint32_t count=RTCCV;                 // see "esp8266_peri.h" for further info
  // uint32_t count=system_get_rtc_time(); // NB. need to: #include "user_interface.h"
  // uint32_t count=ESP.getCycleCount();   // doesn't change at all across boots!

  uint32_t reset_reason=system_get_rst_info()->reason;
  Serial.begin(74880);
  if ((reset_reason==REASON_DEFAULT_RST) or (reset_reason==REASON_EXT_SYS_RST)) {
    Serial.println("\nWait..."); delay(8000); Serial.println("Boot=*PowerUp*");
    Serial.println("NB. After PowerUp OR USB-SoftwareUpload, you must wait for at-least");
    Serial.println("1 normal DeepSleep wakeup before the 'Reset-Button' sensor will work!\n");
  }
  else if (reset_reason==REASON_DEEP_SLEEP_AWAKE) Serial.println("\nBoot=DeepSleep-Wake");
  else {Serial.println("\nBoot=reason "); Serial.println(reset_reason);}

  uint32_t count_old[2];
  ESP.rtcUserMemoryRead(0,(uint32_t*)&count_old,8); // 8 bytes
  boolean smaller[2];
  uint32_t difference[2];
  boolean reset_button_press=false;
  for (byte x=0;x<2;x++) {
    smaller[x]=count[x]<count_old[x] ;
    if (smaller[x]) difference[x]=count_old[x]-count[x]; else difference[x]=count[x]-count_old[x];
    Serial.print("count "); Serial.print(x); Serial.print(" change: "); Serial.print(count_old[x]); Serial.print("->"); Serial.print(count[x]);
    Serial.print(" difference="); if (smaller[x]) Serial.write('-'); Serial.println(difference[x]);
    #define minimum_difference 9 // reducing this number will increase the sensitivity of the Reset-Button-detection, but will also increase the number of false indications on normal GPIO16-wake.
    if (reset_reason==REASON_DEEP_SLEEP_AWAKE) {if (smaller[x] and (difference[x]>=minimum_difference) and (difference[x]<=60)) reset_button_press=true;}
  }
  if (reset_button_press) Serial.println("[****************************** Reset-Button-wake ******************************]"); else Serial.println("[GPIO16-wake]");
  ESP.rtcUserMemoryWrite(0,(uint32_t*)&count,8); // 8 bytes
  if (reset_button_press) {
    Serial.println("Going to DeepSleep for 1second to reset Button-Press counters..."); // Necessary as need to have 1 complete DeepSleep period before Button-Sensor will work again!
    ESP.deepSleep(1000000, WAKE_RF_DISABLED);
  } else { 
    Serial.println("\nWaiting in loop for 10secs (pressing External-Reset-Button now will not Reboot)");
    Serial.println("[But pressing the On-PCB-Reset-Button will cause a Boot=PowerUp]...");
    delay(10000);
    Serial.println("\nGoing to DeepSleep for 20seconds");
    Serial.println("[A short-button-press now will look like a GPIO16-wake, but");
    Serial.println(" a 2second or more button-press will be seen as a 'Button-Press-wake']...");
    ESP.deepSleep(20000000, WAKE_RF_DISABLED);
  }
}

void loop() {}

Note that this code uses the fact that all three of the below rtc-counters vary a little-bit on every reboot, and by assessing the change in rtc-counter each DeepSleep-wake (cf. the previous wake) you can guess what caused the DeepSleep-wake. (The code stores the previous reboot-count in RTCMEM so it is retained during DeepSleep.)

uint32_t count=RTCSV; // NB. need to: #include "esp8266_peri.h" uint32_t count=RTCCV; // NB. need to: #include "esp8266_peri.h" uint32_t count=system_get_rtc_time(); // NB. need to: #include "user_interface.h"

In my tests, I found the most reliable counters for this be “RTCSV”.& “RTCCV”, which are used in the above example. In my projects I test both and use EITHER between 9 and 60 to mean a Reset-Button-press. NB: all these counters normally INCREASE slightly on every GPIO16-DeepSleep-wake. However, on a Reset-Button-press DeepSleep-wake the counters DECREASE, and the longer the button is pressed before release gives a larger decrease (most of the time anyway). For me the best results were achieved using the above [negative] “(difference>=9) and (difference<=60)” test. (I typically use D1mini ESP8266’s.)

Notes: (1) After first PowerUp, also after USB-Software-Upload, and also after each detected “Reset-Button-press”, you must wait for at least 1 normal “GPIO-16 DeepSleep wake” before this “Reset-Button” code will work again. (The above example code does a short 1 second DeepSleep after each detected “Reset-Button-press” to achieve this – it might be possible to avoid this extra DeepSleep by manually overwriting/re-setting the RTCSV and RTCCV counters instead, but I have not found a way of doing this.)

(2) A very quick button-press gives too small a difference for the above code to register (and is instead registered as just a normal GPIO16-DeepSleep-wake). To work the button needs to be pressed for 2seconds or more.

(3) Note that as all 3 of these counters seem to get gradually higher every normal GPIO16-DeepSleep wake: after a number of normal GPIO16 DeepSleep wakes the counters will reach a point when they re-start themselves at a low number again (hence the above negative “difference<=60” to try and catch and ignore this), or sometimes just have a small 1 to ~8 digit reduction (which the negative “difference>=9” ensures is ignored).

(4) It is also interesting to note that the ESP8266 “processor-ticks-since-boot” given by “ESP.getCycleCount()” does NOT change across boot/wakes, so the fact that the above rtc-counters are changing is perhaps related to when in the boot process they are reset; and as button-press-duration changes the numbers, perhaps also what they are reset to.

(5) I typically use a 560R resistor between the GPIO-16 pin and the Reset-Pin, as this seems to give a more stable ESP8266 (unless I am using a transistor/zener -arrangement as show in the below schematics). Note that this code does NOT work unless GPIO-16 is connected to the Reset-Pin in some way.

As the above is not 100% reliable, it is obviously not ideal, but is reliable enough to be of use in many of my projects. However it does NOT give any indication of how long the DeepSleep has lasted up to that point.

Note that you can test the above sketch using just the on-PCB RESET-Button on the D1mini; but in case useful for anyone, the schematic below shows how I connected the external-reset button in my projects. (Edit: the simpler 1-transistor circuit originally shown did NOT work, as the 2.7v zener stopped GPIO16 pulling the RST pin low enough to wake the ESP8266.)

[I use a single 3 position DPDT non-latching toggle switch (on-off-on, normal centre position off): one side of which I use to wake the ESP8266 from DeepSleep (which the circuit then disables once awake to stop further presses causing another reboot); the other side of which I connect to two other GPIO pins and use during normal running to select options. But this DPDT switch can be replaced with a simple normally-open push-button (Reset/Wake-Press-Button) if the extra switch-sense options are not required. Note that as GPIO16 goes open-circuit during normal running, this disables the Momentary Reset-Switch (left-side) until DeepSleep is started.]

Reset_Pin_Schematic_v1_LowRes

6leonardo commented 3 years ago

I have solved with this

Screenshot 2021-03-08 at 14 35 36
6leonardo commented 3 years ago

u can use any schottky diode ... the second diode is useful to be able to flash the est without disconnect the wire

teo1978 commented 3 years ago

I have solved with this

Let me see if I understand it correctly. You are using an additional input pin (GPIO14). You have built a button that does two things at the same time: reset, and take the input voltage of GPIO14 down for a short time. This way, you can look at GPIO on wakeup. If it's low, you are waking up via the button. If it's not, then you are waking up via the timer.

Assuming that works (I don't quite understand how the timer reset doesn't have the exact same effect and bring GPIO14 down exactly like using the physical button), that's a solution I had already thought of. The point of the issue is you shouldn't need to waste an additional input for that. I don't have an unused input pin in my usecase or I would already have followed this approach.

6leonardo commented 3 years ago

I have tried many solution but for me only this is working....if you can find e better (and maybe easy solution) let me know...I need for my project also...

teo1978 commented 3 years ago

Thank you for sharing

6leonardo commented 3 years ago

Sorry I have done a mistake in the schematics.... the capacitor is 100µF and not 100nF

Rob58329 commented 3 years ago

@6leonardo I have not used a "Gizwits WiFi Witty ESP-12F", but doesn't the RST/REST pin need to be pulled up to 3v3 (usually via a resistor) to allow it to run?

If this is the case, then if I understand correctly, pressing the button at any time (ie. both during sleep and during normal running) will reset the ESP8266, and your circuit can tell this had happened by reading GPIO14 after re-boot.

FYI there are several discussion of these type of circuits about, three interesting webpages might be:

[Although more complicated, the example circuit I showed above was designed so that pressing the button during normal ESP8266 running would NOT trigger a reset. (When the EPS8266 is running normally, the zener diode drops the voltage fed back from the RST pin to the 2N2222 transistor base to below 0.6V, so the transistor is off, meaning that pressing the button then will NOT reset the ESP8266.) This means that a single Dual-Pole switch/button, as well as waking the ESP8266 from sleep, could also be used with a separate GPIO when the ESP8266 is running normally to make choices.]