diff --git a/cores/esp8266/PolledTimeout.h b/cores/esp8266/PolledTimeout.h
index 1252ce6c29..ae1d7b5e73 100644
--- a/cores/esp8266/PolledTimeout.h
+++ b/cores/esp8266/PolledTimeout.h
@@ -158,10 +158,10 @@ class timeoutTemplate
   IRAM_ATTR // fast
   bool expired()
   {
-    YieldPolicyT::execute(); //in case of DoNothing: gets optimized away
-    if(PeriodicT)           //in case of false: gets optimized away
-      return expiredRetrigger();
-    return expiredOneShot();
+    bool hasExpired = PeriodicT ? expiredRetrigger() : expiredOneShot();
+    if (!hasExpired) //in case of DoNothing: gets optimized away
+      YieldPolicyT::execute();
+    return hasExpired;
   }
 
   IRAM_ATTR // fast
@@ -186,7 +186,7 @@ class timeoutTemplate
   {
     reset();
     _timeout = TimePolicyT::toTimeTypeUnit(newUserTimeout);
-    _neverExpires = (newUserTimeout < 0) || (newUserTimeout > timeMax());
+    _neverExpires = newUserTimeout > timeMax();
   }
 
   // Resets, will trigger after the timeout previously set.
@@ -219,11 +219,27 @@ class timeoutTemplate
     _neverExpires = true;
   }
 
+  void stop()
+  {
+    resetToNeverExpires();
+  }
+
   timeType getTimeout() const
   {
     return TimePolicyT::toUserUnit(_timeout);
   }
 
+  IRAM_ATTR // fast
+  timeType remaining() const
+  {
+    if (_neverExpires)
+      return timeMax();
+    timeType current = TimePolicyT::time();
+    if (checkExpired(current))
+      return TimePolicyT::toUserUnit(0);
+    return TimePolicyT::toUserUnit(_timeout - (current - _start));
+  }
+  
   static constexpr timeType timeMax()
   {
     return TimePolicyT::timeMax;
@@ -235,7 +251,7 @@ class timeoutTemplate
   bool checkExpired(const timeType internalUnit) const
   {
     // canWait() is not checked here
-    // returns "can expire" and "time expired"
+    // returns "can expire" and "time has expired"
     return (!_neverExpires) && ((internalUnit - _start) >= _timeout);
   }
 
@@ -250,7 +266,7 @@ class timeoutTemplate
     timeType current = TimePolicyT::time();
     if(checkExpired(current))
     {
-      unsigned long n = (current - _start) / _timeout; //how many _timeouts periods have elapsed, will usually be 1 (current - _start >= _timeout)
+      timeType n = (current - _start) / _timeout; //how many _timeouts periods have elapsed, will usually be 1 (current - _start >= _timeout)
       _start += n  * _timeout;
       return true;
     }
diff --git a/cores/esp8266/Schedule.cpp b/cores/esp8266/Schedule.cpp
index f6c650fcf9..deab9e04eb 100644
--- a/cores/esp8266/Schedule.cpp
+++ b/cores/esp8266/Schedule.cpp
@@ -17,12 +17,11 @@
 */
 
 #include <assert.h>
-#include <numeric>
 
 #include "Schedule.h"
 #include "PolledTimeout.h"
 #include "interrupts.h"
-#include "coredecls.h"
+#include <atomic>
 
 typedef std::function<void(void)> mSchedFuncT;
 struct scheduled_fn_t
@@ -35,7 +34,6 @@ static scheduled_fn_t* sFirst = nullptr;
 static scheduled_fn_t* sLast = nullptr;
 static scheduled_fn_t* sUnused = nullptr;
 static int sCount = 0;
-static uint32_t recurrent_max_grain_mS = 0;
 
 typedef std::function<bool(void)> mRecFuncT;
 struct recurrent_fn_t
@@ -49,6 +47,20 @@ struct recurrent_fn_t
 
 static recurrent_fn_t* rFirst = nullptr;
 static recurrent_fn_t* rLast = nullptr;
+// The target time for scheduling the next timed recurrent function 
+static std::atomic<decltype(micros())> rTarget;
+
+// As 32 bit unsigned integer, micros() rolls over every 71.6 minutes.
+// For unambiguous earlier/later order between two timestamps,
+// despite roll over, there is a limit on the maximum duration
+// that can be requested, if full expiration must be observable:
+// later - earlier >= 0 for both later >= earlier or (rolled over) later <= earlier
+// Also, expiration should remain observable for a useful duration of time:
+// now - (start + period) >= 0 for now - start >= 0 despite (start + period) >= now
+// A well-balanced solution, not breaking on two's compliment signed arithmetic,
+// is limiting durations to the maximum signed value of the same word size
+// as the original unsigned word.
+constexpr decltype(micros()) HALF_MAX_MICROS = ~static_cast<decltype(micros())>(0) >> 1;
 
 // Returns a pointer to an unused sched_fn_t,
 // or if none are available allocates a new one,
@@ -106,7 +118,7 @@ bool schedule_function(const std::function<void(void)>& fn)
 
 IRAM_ATTR // (not only) called from ISR
 bool schedule_recurrent_function_us(const std::function<bool(void)>& fn,
-    uint32_t repeat_us, const std::function<bool(void)>& alarm)
+    decltype(micros()) repeat_us, const std::function<bool(void)>& alarm)
 {
     assert(repeat_us < decltype(recurrent_fn_t::callNow)::neverExpires); //~26800000us (26.8s)
 
@@ -122,6 +134,19 @@ bool schedule_recurrent_function_us(const std::function<bool(void)>& fn,
 
     esp8266::InterruptLock lockAllInterruptsInThisScope;
 
+    const auto now = micros();
+    const auto itemRemaining = item->callNow.remaining();
+    for (auto _rTarget = rTarget.load(); ;)
+    {
+        const auto remaining = _rTarget - now;
+        if (!rFirst || (remaining <= HALF_MAX_MICROS && remaining > itemRemaining))
+        {
+            // if (!rTarget.compare_exchange_weak(_rTarget, now + itemRemaining)) continue;
+            rTarget = now + itemRemaining; // interrupt lock is active, no ABA issue
+        }
+        break;
+    }
+
     if (rLast)
     {
         rLast->mNext = item;
@@ -132,37 +157,20 @@ bool schedule_recurrent_function_us(const std::function<bool(void)>& fn,
     }
     rLast = item;
 
-    // grain needs to be recomputed
-    recurrent_max_grain_mS = 0;
-
     return true;
 }
 
-uint32_t compute_scheduled_recurrent_grain ()
+decltype(micros()) get_scheduled_recurrent_delay_us()
 {
-    if (recurrent_max_grain_mS == 0)
-    {
-        if (rFirst)
-        {
-            uint32_t recurrent_max_grain_uS = rFirst->callNow.getTimeout();
-            for (auto it = rFirst->mNext; it; it = it->mNext)
-                recurrent_max_grain_uS = std::gcd(recurrent_max_grain_uS, it->callNow.getTimeout());
-            if (recurrent_max_grain_uS)
-                // round to the upper millis
-                recurrent_max_grain_mS = recurrent_max_grain_uS <= 1000? 1: (recurrent_max_grain_uS + 999) / 1000;
-        }
-
-#ifdef DEBUG_ESP_CORE
-        static uint32_t last_grain = 0;
-        if (recurrent_max_grain_mS != last_grain)
-        {
-            ::printf(":rsf %u->%u\n", last_grain, recurrent_max_grain_mS);
-            last_grain = recurrent_max_grain_mS;
-        }
-#endif
-    }
+    if (!rFirst) return HALF_MAX_MICROS;
+    const auto now = micros();
+    const auto remaining = rTarget.load() - now;
+    return (remaining <= HALF_MAX_MICROS) ? remaining : 0;
+}
 
-    return recurrent_max_grain_mS;
+decltype(micros()) get_scheduled_delay_us()
+{
+    return sFirst ? 0 : HALF_MAX_MICROS;
 }
 
 void run_scheduled_functions()
@@ -225,11 +233,14 @@ void run_scheduled_recurrent_functions()
         fence = true;
     }
 
+    decltype(rLast) stop;
     recurrent_fn_t* prev = nullptr;
+    bool done;
+
+    rTarget.store(micros() + HALF_MAX_MICROS);
     // prevent scheduling of new functions during this run
-    auto stop = rLast;
+    stop = rLast;
 
-    bool done;
     do
     {
         done = current == stop;
@@ -258,12 +269,23 @@ void run_scheduled_recurrent_functions()
             }
 
             delete(to_ditch);
-
-            // grain needs to be recomputed
-            recurrent_max_grain_mS = 0;
         }
         else
         {
+            const auto now = micros();
+            const auto currentRemaining = current->callNow.remaining();
+            for (auto _rTarget = rTarget.load(); ;)
+            {
+                const auto remaining = _rTarget - now;
+                if (remaining <= HALF_MAX_MICROS && remaining > currentRemaining)
+                {
+                    // if (!rTarget.compare_exchange_weak(_rTarget, now + currentRemaining)) continue;
+                    esp8266::InterruptLock lockAllInterruptsInThisScope;
+                    if (rTarget != _rTarget) { _rTarget = rTarget; continue; }
+                    rTarget = now + currentRemaining;
+                }
+                break;
+            }
             prev = current;
             current = current->mNext;
         }
diff --git a/cores/esp8266/Schedule.h b/cores/esp8266/Schedule.h
index 362d15b5f3..730e77d513 100644
--- a/cores/esp8266/Schedule.h
+++ b/cores/esp8266/Schedule.h
@@ -22,6 +22,8 @@
 #include <functional>
 #include <stdint.h>
 
+#include "coredecls.h"
+
 #define SCHEDULED_FN_MAX_COUNT 32
 
 // The purpose of scheduled functions is to trigger, from SYS stack (like in
@@ -39,10 +41,10 @@
 // scheduled function happen more often: every yield() (vs every loop()),
 // and time resolution is microsecond (vs millisecond). Details are below.
 
-// compute_scheduled_recurrent_grain() is used by delay() to give a chance to
+// get_scheduled_recurrent_delay_us() is used by delay() to give a chance to
 // all recurrent functions to run per their timing requirement.
 
-uint32_t compute_scheduled_recurrent_grain ();
+decltype(micros()) get_scheduled_recurrent_delay_us();
 
 // scheduled functions called once:
 //
@@ -60,6 +62,13 @@ uint32_t compute_scheduled_recurrent_grain ();
 // * Run the lambda only once next time.
 // * A scheduled function can schedule a function.
 
+// get_scheduled_delay_us() is named for symmetry to get_scheduled_recurrent_delay_us,
+// despite the lack of specific delay times. Therefore it can return only one of two
+// values, viz. 0 in case of any pending scheduled functions, or a large delay time if
+// there is no function in the queue.
+
+decltype(micros()) get_scheduled_delay_us();
+
 bool schedule_function (const std::function<void(void)>& fn);
 
 // Run all scheduled functions.
@@ -86,7 +95,7 @@ void run_scheduled_functions();
 //   any remaining delay from repeat_us is disregarded, and fn is executed.
 
 bool schedule_recurrent_function_us(const std::function<bool(void)>& fn,
-    uint32_t repeat_us, const std::function<bool(void)>& alarm = nullptr);
+    decltype(micros()) repeat_us, const std::function<bool(void)>& alarm = nullptr);
 
 // Test recurrence and run recurrent scheduled functions.
 // (internally called at every `yield()` and `loop()`)
diff --git a/cores/esp8266/core_esp8266_main.cpp b/cores/esp8266/core_esp8266_main.cpp
index d0309cc71f..a0cfb8533c 100644
--- a/cores/esp8266/core_esp8266_main.cpp
+++ b/cores/esp8266/core_esp8266_main.cpp
@@ -22,9 +22,6 @@
 
 //This may be used to change user task stack size:
 //#define CONT_STACKSIZE 4096
-
-#include <numeric>
-
 #include <Arduino.h>
 #include "Schedule.h"
 extern "C" {
@@ -176,13 +173,12 @@ bool esp_try_delay(const uint32_t start_ms, const uint32_t timeout_ms, const uin
         return true; // expired
     }
 
-    // compute greatest chunked delay with respect to scheduled recurrent functions
-    uint32_t grain_ms = std::gcd(intvl_ms, compute_scheduled_recurrent_grain());
+    // compute greatest delay interval with respect to scheduled recurrent functions
+    const uint32_t scheduled_recurrent_delay_ms = get_scheduled_recurrent_delay_us() / 1000UL;
+    const uint32_t max_delay_ms = std::min(intvl_ms, scheduled_recurrent_delay_ms);
 
     // recurrent scheduled functions will be called from esp_delay()->esp_suspend()
-    esp_delay(grain_ms > 0 ?
-        std::min((timeout_ms - expired), grain_ms):
-        (timeout_ms - expired));
+    esp_delay(std::min((timeout_ms - expired), max_delay_ms));
 
     return false; // expiration must be checked again
 }
diff --git a/libraries/SoftwareSerial b/libraries/SoftwareSerial
index bcfd6d10e6..bb133a5a17 160000
--- a/libraries/SoftwareSerial
+++ b/libraries/SoftwareSerial
@@ -1 +1 @@
-Subproject commit bcfd6d10e6a45a0d07705d08728f293defe9cc1d
+Subproject commit bb133a5a177e64655f900a419f36c2c09b8590be