Browse Source

Initial version of code to support multiple hardware timers (#2497)

* Initial version of code to support multiple hardware timers
* MAde the time sinca last tick work again
* Add some documentation to the code
Philip Gladstone 5 years ago
parent
commit
d75830407e
2 changed files with 355 additions and 52 deletions
  1. 2 2
      app/modules/gpio_pulse.c
  2. 353 50
      app/platform/hw_timer.c

+ 2 - 2
app/modules/gpio_pulse.c

@@ -10,7 +10,7 @@
 #include "pin_map.h"
 #include "driver/gpio16.h"
 
-#define TIMER_OWNER 'P'
+#define TIMER_OWNER (('P' << 8) + 'U')
 
 #define xstr(s) str(s)
 #define str(s) #s
@@ -434,7 +434,7 @@ static int gpio_pulse_start(lua_State *L) {
   pulser->next_adjust = initial_adjust;
 
   // Now start things up
-  if (!platform_hw_timer_init(TIMER_OWNER, FRC1_SOURCE, TRUE)) {
+  if (!platform_hw_timer_init(TIMER_OWNER, FRC1_SOURCE, FALSE)) {
     // Failed to init the timer
     luaL_error(L, "Unable to initialize timer");
   }

+ 353 - 50
app/platform/hw_timer.c

@@ -7,21 +7,28 @@
 *
 * Modification history:
 *     2014/5/1, v1.0 create this file.
-*     
+*
 * Adapted for NodeMCU 2016
-* 
+*
 * The owner parameter should be a unique value per module using this API
-* It could be a pointer to a bit of data or code 
-* e.g.   #define OWNER    ((os_param_t) module_init)   
-* where module_init is a function. For builtin modules, it might be 
+* It could be a pointer to a bit of data or code
+* e.g.   #define OWNER    ((os_param_t) module_init)
+* where module_init is a function. For builtin modules, it might be
 * a small numeric value that is known not to clash.
 *******************************************************************************/
+#include "platform.h"
+#include "c_stdio.h"
+#include "c_stdlib.h"
 #include "ets_sys.h"
 #include "os_type.h"
 #include "osapi.h"
 
 #include "hw_timer.h"
 
+//#define DEBUG_HW_TIMER
+//#undef NODE_DBG
+//#define NODE_DBG dbg_printf
+
 #define FRC1_ENABLE_TIMER  BIT7
 #define FRC1_AUTO_LOAD  BIT6
 
@@ -37,11 +44,227 @@ typedef enum {			//timer interrupt mode
     TM_EDGE_INT   = 0,	//edge interrupt
 } TIMER_INT_MODE;
 
-static os_param_t the_owner;
-static os_param_t callback_arg;
-static void (* user_hw_timer_cb)(os_param_t);
+/*
+ * This represents a single user of the timer functionality. It is keyed by the owner
+ * field.
+ */
+typedef struct _timer_user {
+  struct _timer_user *next;
+  bool autoload;
+  int32_t delay; // once on the active list, this is difference in delay from the preceding element
+  int32_t autoload_delay;
+  uint32_t expected_interrupt_time;
+  os_param_t owner;
+  os_param_t callback_arg;
+  void (* user_hw_timer_cb)(os_param_t);
+#ifdef DEBUG_HW_TIMER
+  int cb_count;
+#endif
+} timer_user;
+
+/*
+ * There are two lists of timer_user blocks. The active list are those which are waiting
+ * for timeouts to happen, and the inactive list contains idle blocks. Unfortunately
+ * there isn't a way to clean up the inactive blocks as some modules call the
+ * close method from interrupt level.
+ */
+static timer_user *active;
+static timer_user *inactive;
+
+/*
+ * There are a fair number of places when interrupts need to be disabled as many of
+ * the methods can be called from interrupt level. The lock/unlock calls support
+ * multiple LOCKs and then the same number of UNLOCKs are required to re-enable
+ * interrupts. This is imolemeted by counting the number of times that lock is called.
+ */
+static uint8_t lock_count;
+static uint8_t timer_running;
+
+static uint32_t time_next_expiry;
+static int32_t last_timer_load;
+
+#define LOCK()   do { ets_intr_lock(); lock_count++; } while (0)
+#define UNLOCK()   if (--lock_count == 0) ets_intr_unlock()
+
+/*
+ * To start a timer, you write to FRCI_LOAD_ADDRESS, and that starts the counting
+ * down. When it reaches zero, the interrupt fires -- but the counting continues.
+ * The counter is 23 bits wide. The current value of the counter can be read
+ * at FRC1_COUNT_ADDRESS. The unit is 200ns, and so it takes somewhat over a second
+ * to wrap the counter.
+ */
+
+#ifdef DEBUG_HW_TIMER
+void ICACHE_RAM_ATTR hw_timer_debug() {
+  dbg_printf("timer_running=%d\n", timer_running);
+  timer_user *tu;
+  for (tu = active; tu; tu = tu->next) {
+    dbg_printf("Owner: 0x%x, delay=%d, autoload=%d, autoload_delay=%d, cb_count=%d\n",
+        tu->owner, tu->delay, tu->autoload, tu->autoload_delay, tu->cb_count);
+  }
+}
+#endif
+
+static void ICACHE_RAM_ATTR set_timer(int delay, const char *caller) {
+  if (delay < 1) {
+    delay = 1;
+  }
+  int32_t time_left = (RTC_REG_READ(FRC1_COUNT_ADDRESS)) & ((1 << 23) - 1);
+  RTC_REG_WRITE(FRC1_LOAD_ADDRESS, delay);
+
+  if (time_left > last_timer_load) {
+    // We have missed the interrupt
+    time_left -= 1 << 23;
+  }
+  NODE_DBG("%s(%x): time_next=%d, left=%d (load=%d), delay=%d  => %d\n", caller, active->owner, time_next_expiry, time_left, last_timer_load, delay, time_next_expiry - time_left + delay);
+  time_next_expiry = time_next_expiry - time_left + delay;
+  last_timer_load = delay;
+
+  timer_running = 1;
+}
+
+static void ICACHE_RAM_ATTR adjust_root() {
+  // Can only ge called with interrupts disabled
+  // change the initial active delay so that relative stuff still works
+  // Also, set the last_timer_load to be now
+  int32_t time_left = (RTC_REG_READ(FRC1_COUNT_ADDRESS)) & ((1 << 23) - 1);
+  if (time_left > last_timer_load) {
+    // We have missed the interrupt
+    time_left -= 1 << 23;
+  }
+
+  if (active && timer_running) {
+    active->delay = time_left;
+  }
+
+  if (active) {
+    NODE_DBG("adjust(%x): time_left=%d (last_load=%d)\n", active->owner, time_left, last_timer_load);
+  } else {
+    NODE_DBG("adjust: time_left=%d (last_load=%d)\n", time_left, last_timer_load);
+  }
+
+  last_timer_load = time_left;
+}
+/*
+ * Find the timer_user block for this owner. This just returns
+ * a pointer to the block, or NULL.
+ */
+static timer_user * ICACHE_RAM_ATTR find_tu(os_param_t owner) {
+  // Try the inactive chain first
+  timer_user **p;
+
+  LOCK();
+
+  for (p = &inactive; *p; p = &((*p)->next)) {
+    if ((*p)->owner == owner) {
+      timer_user *result = *p;
+
+      UNLOCK();
+
+      return result;
+    }
+  }
+
+  for (p = &active; *p; p = &((*p)->next)) {
+    if ((*p)->owner == owner) {
+      timer_user *result = *p;
+
+      UNLOCK();
+
+      return result;
+    }
+  }
+
+  UNLOCK();
+  return NULL;
+}
+
+/*
+ * Find the timer_user block for this owner. This just returns
+ * a pointer to the block, or NULL. If it finds the block, then it is
+ * removed from whichever chain it is on. Note that this may require
+ * triggering a timer.
+ */
+static timer_user * ICACHE_RAM_ATTR find_tu_and_remove(os_param_t owner) {
+  // Try the inactive chain first
+  timer_user **p;
+
+  LOCK();
+
+  for (p = &inactive; *p; p = &((*p)->next)) {
+    if ((*p)->owner == owner) {
+      timer_user *result = *p;
+      *p = result->next;
+      result->next = NULL;
+
+      UNLOCK();
+
+      return result;
+    }
+  }
+
+  for (p = &active; *p; p = &((*p)->next)) {
+    if ((*p)->owner == owner) {
+      timer_user *result = *p;
+
+      bool need_to_reset = (result == active) && result->next;
+
+      if (need_to_reset) {
+        adjust_root();
+      }
 
-#define VERIFY_OWNER(owner)  if (owner != the_owner) { if (the_owner) { return 0; } the_owner = owner; }
+      // Increase the delay on the next element
+      if (result->next) {
+        result->next->delay += result->delay;
+      }
+
+      // Cut out of chain
+      *p = result->next;
+      result->next = NULL;
+
+      if (need_to_reset) {
+        set_timer(active->delay, "find_tu");
+      }
+
+      UNLOCK();
+      return result;
+    }
+  }
+
+  UNLOCK();
+  return NULL;
+}
+
+/*
+ * This inserts a timer_user block into the active chain. This is a sightly
+ * complex process as it can involve triggering a timer load.
+ */
+static void ICACHE_RAM_ATTR insert_active_tu(timer_user *tu) {
+  timer_user **p;
+
+  LOCK();
+
+  tu->expected_interrupt_time = time_next_expiry - last_timer_load + tu->delay;
+
+  for (p = &active; *p; p = &((*p)->next)) {
+    if ((*p)->delay >= tu->delay) {
+      break;
+    }
+    tu->delay -= (*p)->delay;
+  }
+
+  if (*p) {
+    (*p)->delay -= tu->delay;
+  }
+  tu->next = *p;
+  *p = tu;
+
+  if (tu == active) {
+    // We have a new leader
+    set_timer(active->delay, "insert_active");
+  }
+  UNLOCK();
+}
 
 /******************************************************************************
 * FunctionName : platform_hw_timer_arm_ticks
@@ -52,10 +275,23 @@ static void (* user_hw_timer_cb)(os_param_t);
 *******************************************************************************/
 bool ICACHE_RAM_ATTR platform_hw_timer_arm_ticks(os_param_t owner, uint32_t ticks)
 {
-  VERIFY_OWNER(owner);
-  RTC_REG_WRITE(FRC1_LOAD_ADDRESS, ticks);
+  timer_user *tu = find_tu_and_remove(owner);
+
+  if (!tu) {
+    return false;
+  }
 
-  return 1;
+  tu->delay = ticks;
+  tu->autoload_delay = ticks;
+
+  NODE_DBG("arm(%x): ticks=%d\n", owner, ticks);
+
+  LOCK();
+  adjust_root();
+  insert_active_tu(tu);
+  UNLOCK();
+
+  return true;
 }
 
 /******************************************************************************
@@ -72,10 +308,7 @@ bool ICACHE_RAM_ATTR platform_hw_timer_arm_ticks(os_param_t owner, uint32_t tick
 *******************************************************************************/
 bool ICACHE_RAM_ATTR platform_hw_timer_arm_us(os_param_t owner, uint32_t microseconds)
 {
-  VERIFY_OWNER(owner);
-  RTC_REG_WRITE(FRC1_LOAD_ADDRESS, US_TO_RTC_TIMER_TICKS(microseconds));
-
-  return 1;
+  return platform_hw_timer_arm_ticks(owner, US_TO_RTC_TIMER_TICKS(microseconds));
 }
 
 /******************************************************************************
@@ -89,24 +322,67 @@ bool ICACHE_RAM_ATTR platform_hw_timer_arm_us(os_param_t owner, uint32_t microse
 *******************************************************************************/
 bool platform_hw_timer_set_func(os_param_t owner, void (* user_hw_timer_cb_set)(os_param_t), os_param_t arg)
 {
-  VERIFY_OWNER(owner);
-  callback_arg = arg;
-  user_hw_timer_cb = user_hw_timer_cb_set;
-  return 1;
+  timer_user *tu = find_tu(owner);
+  if (!tu) {
+    return false;
+  }
+  tu->callback_arg = arg;
+  tu->user_hw_timer_cb = user_hw_timer_cb_set;
+  NODE_DBG("set-CB(%x): %x, %x\n", tu->owner, tu->user_hw_timer_cb, tu->callback_arg);
+  return true;
 }
 
+/*
+ * This is the timer ISR. It has to find the timer that was running and trigger the callback
+ * for that timer. By this stage, the next timer may have expired as well, and so the process
+ * iterates. Note that if there is an autoload timer, then it should be restarted immediately.
+ * Also, the callbacks typically do re-arm the timer, so we have to be careful not to
+ * assume that nothing changes during the callback.
+ */
 static void ICACHE_RAM_ATTR hw_timer_isr_cb(void *arg)
 {
-  if (user_hw_timer_cb != NULL) {
-    (*(user_hw_timer_cb))(callback_arg);
+  bool keep_going = true;
+  adjust_root();
+  timer_running = 0;
+
+  while (keep_going && active) {
+    keep_going = false;
+
+    timer_user *fired = active;
+    active = fired->next;
+    if (fired->autoload) {
+      fired->expected_interrupt_time += fired->autoload_delay;
+      fired->delay = fired->expected_interrupt_time - (time_next_expiry - last_timer_load);
+      insert_active_tu(fired);
+      if (active->delay <= 0) {
+        keep_going = true;
+      }
+    } else {
+      fired->next = inactive;
+      inactive = fired;
+      if (active) {
+        active->delay += fired->delay;
+        if (active->delay <= 0) {
+          keep_going = true;
+        }
+      }
+    }
+    if (fired->user_hw_timer_cb) {
+#ifdef DEBUG_HW_TIMER
+      fired->cb_count++;
+#endif
+      NODE_DBG("CB(%x): %x, %x\n", fired->owner, fired->user_hw_timer_cb, fired->callback_arg);
+      (*(fired->user_hw_timer_cb))(fired->callback_arg);
+    }
+  }
+  if (active && !timer_running) {
+    set_timer(active->delay, "isr");
   }
 }
 
 static void ICACHE_RAM_ATTR hw_timer_nmi_cb(void)
 {
-  if (user_hw_timer_cb != NULL) {
-    (*(user_hw_timer_cb))(callback_arg);
-  }
+  hw_timer_isr_cb(NULL);
 }
 
 /******************************************************************************
@@ -117,9 +393,21 @@ static void ICACHE_RAM_ATTR hw_timer_nmi_cb(void)
 *******************************************************************************/
 uint32_t ICACHE_RAM_ATTR platform_hw_timer_get_delay_ticks(os_param_t owner)
 {
-  VERIFY_OWNER(owner);
+  timer_user *tu = find_tu(owner);
+  if (!tu) {
+    return 0;
+  }
+
+  LOCK();
+  adjust_root();
+  UNLOCK();
+  int ret = (time_next_expiry - last_timer_load) - tu->expected_interrupt_time;
 
-  return (- RTC_REG_READ(FRC1_COUNT_ADDRESS)) & ((1 << 23) - 1);
+  if (ret < 0) {
+    NODE_DBG("delay ticks = %d, last_timer_load=%d, tu->expected_int=%d, next_exp=%d\n", ret, last_timer_load, tu->expected_interrupt_time, time_next_expiry);
+  }
+
+  return ret < 0 ? 0 : ret;
 }
 
 /******************************************************************************
@@ -136,25 +424,34 @@ uint32_t ICACHE_RAM_ATTR platform_hw_timer_get_delay_ticks(os_param_t owner)
 *******************************************************************************/
 bool platform_hw_timer_init(os_param_t owner, FRC1_TIMER_SOURCE_TYPE source_type, bool autoload)
 {
-  VERIFY_OWNER(owner);
-  if (autoload) {
-    RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
-		  FRC1_AUTO_LOAD | DIVIDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
-  } else {
-    RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
-		  DIVIDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
+  timer_user *tu = find_tu_and_remove(owner);
+
+  if (!tu) {
+    tu = (timer_user *) c_malloc(sizeof(*tu));
+    if (!tu) {
+      return false;
+    }
+    memset(tu, 0, sizeof(*tu));
+    tu->owner = owner;
   }
 
-  if (source_type == NMI_SOURCE) {
-    ETS_FRC_TIMER1_NMI_INTR_ATTACH(hw_timer_nmi_cb);
-  } else {
+  tu->autoload = autoload;
+
+  if (!active && !inactive) {
+    RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
+		  DIVIDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
     ETS_FRC_TIMER1_INTR_ATTACH(hw_timer_isr_cb, NULL);
+
+    TM1_EDGE_INT_ENABLE();
+    ETS_FRC1_INTR_ENABLE();
   }
 
-  TM1_EDGE_INT_ENABLE();
-  ETS_FRC1_INTR_ENABLE();
+  LOCK();
+  tu->next = inactive;
+  inactive = tu;
+  UNLOCK();
 
-  return 1;
+  return true;
 }
 
 /******************************************************************************
@@ -165,19 +462,25 @@ bool platform_hw_timer_init(os_param_t owner, FRC1_TIMER_SOURCE_TYPE source_type
 *******************************************************************************/
 bool ICACHE_RAM_ATTR platform_hw_timer_close(os_param_t owner)
 {
-  VERIFY_OWNER(owner);
-
-  /* Set no reload mode */
-  RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
-		DIVIDED_BY_16 | TM_EDGE_INT);
+  timer_user *tu = find_tu_and_remove(owner);
 
-  TM1_EDGE_INT_DISABLE();
-  ETS_FRC1_INTR_DISABLE();
+  if (tu) {
+    LOCK();
+    tu->next = inactive;
+    inactive = tu;
+    UNLOCK();
+  }
 
-  user_hw_timer_cb = NULL;
+  // This will never actually run....
+  if (!active && !inactive) {
+    /* Set no reload mode */
+    RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
+                  DIVIDED_BY_16 | TM_EDGE_INT);
 
-  the_owner = 0;
+    TM1_EDGE_INT_DISABLE();
+    ETS_FRC1_INTR_DISABLE();
+  }
 
-  return 1;
+  return true;
 }