mirror of
https://github.com/nodemcu/nodemcu-firmware.git
synced 2025-01-30 21:12:55 +08:00
526d21dab4
The PR removed the bulk of non-newlib headers from the NodeMCU source base. app/libc has now been cut down to the bare minimum overrides to shadow the corresponding functions in the SDK's libc. The old c_xyz.h headerfiles have been nuked in favour of the standard <xyz.h> headers, with a few exceptions over in sdk-overrides. Again, shipping a libc.a without headers is a terrible thing to do. We're still living on a prayer that libc was configured the same was as a default-configured xtensa gcc toolchain assumes it is. That part I cannot do anything about, unfortunately, but it's no worse than it has been before. This enables our source files to compile successfully using the standard header files, and use the typical malloc()/calloc()/realloc()/free(), the strwhatever()s and memwhatever()s. These end up, through macro and linker magic, mapped to the appropriate SDK or ROM functions.
598 lines
18 KiB
C
598 lines
18 KiB
C
/******************************************************************************
|
|
* Copyright 2013-2014 Espressif Systems (Wuxi)
|
|
*
|
|
* FileName: hw_timer.c
|
|
*
|
|
* Description: hw_timer driver
|
|
*
|
|
* 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
|
|
* a small numeric value that is known not to clash.
|
|
*******************************************************************************/
|
|
#include "platform.h"
|
|
#include <stdio.h>
|
|
#include <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
|
|
|
|
//TIMER PREDIVIDED MODE
|
|
typedef enum {
|
|
DIVIDED_BY_1 = 0, //timer clock
|
|
DIVIDED_BY_16 = 4, //divided by 16
|
|
DIVIDED_BY_256 = 8, //divided by 256
|
|
} TIMER_PREDIVIDED_MODE;
|
|
|
|
typedef enum { //timer interrupt mode
|
|
TM_LEVEL_INT = 1, // level interrupt
|
|
TM_EDGE_INT = 0, //edge interrupt
|
|
} TIMER_INT_MODE;
|
|
|
|
/*
|
|
* 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()
|
|
|
|
/*
|
|
* It is possible to reserve the timer exclusively, for one module alone.
|
|
* This way the interrupt overhead is minimal.
|
|
* Drawback is that no other module can use the timer at same time.
|
|
* If flag if true, indicates someone reserved the timer exclusively.
|
|
* Unline shared used (default), only one client can reserve exclusively.
|
|
*/
|
|
static bool reserved_exclusively = false;
|
|
|
|
/*
|
|
* 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();
|
|
}
|
|
|
|
// 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
|
|
* Description : set a trigger timer delay for this timer.
|
|
* Parameters : os_param_t owner
|
|
* uint32 ticks :
|
|
* Returns : true if it worked
|
|
*******************************************************************************/
|
|
bool ICACHE_RAM_ATTR platform_hw_timer_arm_ticks(os_param_t owner, uint32_t ticks)
|
|
{
|
|
if (reserved_exclusively) return false;
|
|
|
|
timer_user *tu = find_tu_and_remove(owner);
|
|
|
|
if (!tu) {
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/******************************************************************************
|
|
* FunctionName : platform_hw_timer_arm_us
|
|
* Description : set a trigger timer delay for this timer.
|
|
* Parameters : os_param_t owner
|
|
* uint32 microseconds :
|
|
* in autoload mode
|
|
* 50 ~ 0x7fffff; for FRC1 source.
|
|
* 100 ~ 0x7fffff; for NMI source.
|
|
* in non autoload mode:
|
|
* 10 ~ 0x7fffff;
|
|
* Returns : true if it worked
|
|
*******************************************************************************/
|
|
bool ICACHE_RAM_ATTR platform_hw_timer_arm_us(os_param_t owner, uint32_t microseconds)
|
|
{
|
|
return platform_hw_timer_arm_ticks(owner, US_TO_RTC_TIMER_TICKS(microseconds));
|
|
}
|
|
|
|
/******************************************************************************
|
|
* FunctionName : platform_hw_timer_set_func
|
|
* Description : set the func, when trigger timer is up.
|
|
* Parameters : os_param_t owner
|
|
* void (* user_hw_timer_cb_set)(os_param_t):
|
|
timer callback function
|
|
* os_param_t arg
|
|
* Returns : true if it worked
|
|
*******************************************************************************/
|
|
bool platform_hw_timer_set_func(os_param_t owner, void (* user_hw_timer_cb_set)(os_param_t), os_param_t arg)
|
|
{
|
|
if (reserved_exclusively) return false;
|
|
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
hw_timer_isr_cb(NULL);
|
|
}
|
|
|
|
/******************************************************************************
|
|
* FunctionName : platform_hw_timer_get_delay_ticks
|
|
* Description : figure out how long since th last timer interrupt
|
|
* Parameters : os_param_t owner
|
|
* Returns : the number of ticks
|
|
*******************************************************************************/
|
|
uint32_t ICACHE_RAM_ATTR platform_hw_timer_get_delay_ticks(os_param_t owner)
|
|
{
|
|
if (reserved_exclusively) return 0;
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
/******************************************************************************
|
|
* FunctionName : platform_hw_timer_init
|
|
* Description : initialize the hardware isr timer for shared use i.e. multiple owners.
|
|
* Parameters : os_param_t owner
|
|
* FRC1_TIMER_SOURCE_TYPE source_type:
|
|
* FRC1_SOURCE, timer use frc1 isr as isr source.
|
|
* NMI_SOURCE, timer use nmi isr as isr source.
|
|
* bool autoload:
|
|
* 0, not autoload,
|
|
* 1, autoload mode,
|
|
* Returns : true if it worked
|
|
*******************************************************************************/
|
|
bool platform_hw_timer_init(os_param_t owner, FRC1_TIMER_SOURCE_TYPE source_type, bool autoload)
|
|
{
|
|
if (reserved_exclusively) return false;
|
|
|
|
timer_user *tu = find_tu_and_remove(owner);
|
|
|
|
if (!tu) {
|
|
tu = (timer_user *) malloc(sizeof(*tu));
|
|
if (!tu) {
|
|
return false;
|
|
}
|
|
memset(tu, 0, sizeof(*tu));
|
|
tu->owner = owner;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
LOCK();
|
|
tu->next = inactive;
|
|
inactive = tu;
|
|
UNLOCK();
|
|
|
|
return true;
|
|
}
|
|
|
|
/******************************************************************************
|
|
* FunctionName : platform_hw_timer_close
|
|
* Description : ends use of the hardware isr timer.
|
|
* Parameters : os_param_t owner.
|
|
* Returns : true if it worked
|
|
*******************************************************************************/
|
|
bool ICACHE_RAM_ATTR platform_hw_timer_close(os_param_t owner)
|
|
{
|
|
if (reserved_exclusively) return false;
|
|
|
|
timer_user *tu = find_tu_and_remove(owner);
|
|
|
|
if (tu) {
|
|
if (tu == inactive) {
|
|
inactive == NULL;
|
|
} else {
|
|
LOCK();
|
|
tu->next = inactive;
|
|
inactive = tu;
|
|
UNLOCK();
|
|
}
|
|
}
|
|
|
|
// This will never actually run....
|
|
if (!active && !inactive) {
|
|
/* Set no reload mode */
|
|
RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
|
|
DIVIDED_BY_16 | TM_EDGE_INT);
|
|
|
|
TM1_EDGE_INT_DISABLE();
|
|
ETS_FRC1_INTR_DISABLE();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/******************************************************************************
|
|
* FunctionName : platform_hw_timer_init_exclusive
|
|
* Description : initialize the hardware isr timer for exclusive use by the caller.
|
|
* Parameters : FRC1_TIMER_SOURCE_TYPE source_type:
|
|
* FRC1_SOURCE, timer use frc1 isr as isr source.
|
|
* NMI_SOURCE, timer use nmi isr as isr source.
|
|
* bool autoload:
|
|
* 0, not autoload,
|
|
* 1, autoload mode,
|
|
* void (* frc1_timer_cb)(os_param_t): timer callback function when FRC1_SOURCE is being used
|
|
* os_param_t arg : argument passed to frc1_timer_cb or NULL
|
|
* void (* nmi_timer_cb)(void) : timer callback function when NMI_SOURCE is being used
|
|
* Returns : true if it worked, false if the timer is already served for shared or exclusive use
|
|
*******************************************************************************/
|
|
bool platform_hw_timer_init_exclusive(
|
|
FRC1_TIMER_SOURCE_TYPE source_type,
|
|
bool autoload,
|
|
void (* frc1_timer_cb)(os_param_t),
|
|
os_param_t arg,
|
|
void (*nmi_timer_cb)(void)
|
|
)
|
|
{
|
|
if (active || inactive) return false;
|
|
if (reserved_exclusively) return false;
|
|
reserved_exclusively = true;
|
|
|
|
RTC_REG_WRITE(FRC1_CTRL_ADDRESS, (autoload ? FRC1_AUTO_LOAD : 0) | DIVIDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
|
|
|
|
if (source_type == NMI_SOURCE) {
|
|
ETS_FRC_TIMER1_NMI_INTR_ATTACH(nmi_timer_cb);
|
|
} else {
|
|
ETS_FRC_TIMER1_INTR_ATTACH((void (*)(void *))frc1_timer_cb, (void*)arg);
|
|
}
|
|
|
|
TM1_EDGE_INT_ENABLE();
|
|
ETS_FRC1_INTR_ENABLE();
|
|
|
|
return true;
|
|
}
|
|
|
|
/******************************************************************************
|
|
* FunctionName : platform_hw_timer_close_exclusive
|
|
* Description : ends use of the hardware isr timer in exclusive mode.
|
|
* Parameters :
|
|
* Returns : true if it worked
|
|
*******************************************************************************/
|
|
bool ICACHE_RAM_ATTR platform_hw_timer_close_exclusive()
|
|
{
|
|
if (!reserved_exclusively) return true;
|
|
reserved_exclusively = false;
|
|
|
|
/* Set no reload mode */
|
|
RTC_REG_WRITE(FRC1_CTRL_ADDRESS, DIVIDED_BY_16 | TM_EDGE_INT);
|
|
|
|
TM1_EDGE_INT_DISABLE();
|
|
ETS_FRC1_INTR_DISABLE();
|
|
|
|
return true;
|
|
}
|
|
|
|
/******************************************************************************
|
|
* FunctionName : platform_hw_timer_arm_ticks_exclusive
|
|
* Description : set a trigger timer delay for this timer.
|
|
* Parameters : uint32 ticks :
|
|
* Returns : true if it worked
|
|
*******************************************************************************/
|
|
bool ICACHE_RAM_ATTR platform_hw_timer_arm_ticks_exclusive(uint32_t ticks)
|
|
{
|
|
RTC_REG_WRITE(FRC1_LOAD_ADDRESS, ticks);
|
|
return true;
|
|
}
|
|
|
|
/******************************************************************************
|
|
* FunctionName : platform_hw_timer_arm_us_exclusive
|
|
* Description : set a trigger timer delay for this timer.
|
|
* Parameters : uint32 microseconds :
|
|
* in autoload mode
|
|
* 50 ~ 0x7fffff; for FRC1 source.
|
|
* 100 ~ 0x7fffff; for NMI source.
|
|
* in non autoload mode:
|
|
* 10 ~ 0x7fffff;
|
|
* Returns : true if it worked
|
|
*******************************************************************************/
|
|
bool ICACHE_RAM_ATTR platform_hw_timer_arm_us_exclusive(uint32_t microseconds)
|
|
{
|
|
RTC_REG_WRITE(FRC1_LOAD_ADDRESS, US_TO_RTC_TIMER_TICKS(microseconds));
|
|
return true;
|
|
}
|