diff --git a/app/modules/gpio_pulse.c b/app/modules/gpio_pulse.c index 64fe84f3f6..170d837385 100644 --- a/app/modules/gpio_pulse.c +++ b/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"); } diff --git a/app/platform/hw_timer.c b/app/platform/hw_timer.c index 959d1b289f..0a0e45db08 100644 --- a/app/platform/hw_timer.c +++ b/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; }