From 5f43a414e756e778f57fce240ad4ab6ce17d7013 Mon Sep 17 00:00:00 2001 From: Nikolay Fiykov Date: Sat, 25 May 2019 23:08:13 +0300 Subject: [PATCH] Add pwm2 module (#2747) --- .gitignore | 1 + app/driver/pwm2.c | 251 ++++++++++++++++++++++++++++++ app/include/driver/pwm2.h | 65 ++++++++ app/include/user_modules.h | 1 + app/modules/pwm2.c | 145 +++++++++++++++++ app/platform/hw_timer.c | 125 ++++++++++++++- app/platform/hw_timer.h | 8 + docs/modules/pwm2.md | 309 +++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 9 files changed, 899 insertions(+), 7 deletions(-) create mode 100644 app/driver/pwm2.c create mode 100644 app/include/driver/pwm2.h create mode 100644 app/modules/pwm2.c create mode 100644 docs/modules/pwm2.md diff --git a/.gitignore b/.gitignore index ddf3c2d2..abca1f17 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ tools/toolchains/ .cproject .project .settings/ +.vscode diff --git a/app/driver/pwm2.c b/app/driver/pwm2.c new file mode 100644 index 00000000..ad41307c --- /dev/null +++ b/app/driver/pwm2.c @@ -0,0 +1,251 @@ +/* + * Software PWM using soft-interrupt timer1. + * Supports higher frequencies compared to Espressif provided one. + * + * Nikolay Fiykov + */ + +#include +#include "c_types.h" +#include "mem.h" +#include "pin_map.h" +#include "platform.h" +#include "hw_timer.h" +#include "driver/pwm2.h" + +#define PWM2_TMR_MAGIC_80MHZ 16 +#define PWM2_TMR_MAGIC_160MHZ 32 + +// module vars, lazy initialized, allocated only if pwm2 is being used + +static pwm2_module_data_t *moduleData = NULL; + +//############################ +// tools + +static bool isPinSetup(const pwm2_module_data_t *data, const uint8_t pin) { + return data->setupData.pin[pin].pulseResolutions > 0; +} + +static uint32_t getCPUTicksPerSec() { + return system_get_cpu_freq() * 1000000; +} + +static uint8_t getCpuTimerTicksDivisor() { + return system_get_cpu_freq() == 80 ? PWM2_TMR_MAGIC_80MHZ : PWM2_TMR_MAGIC_160MHZ; +} + +static uint32_t findGCD(uint32_t n1, uint32_t n2) { + uint32_t n3; + while (n2 != 0) { + n3 = n1; + n1 = n2; + n2 = n3 % n2; + } + return n1; +} + +static uint32_t findGreatesCommonDividerForTimerTicks(uint32_t newTimerTicks, uint32_t oldTimerTicks) { + return oldTimerTicks == 0 ? newTimerTicks : findGCD(newTimerTicks, oldTimerTicks); +} + +static uint16_t findAllEnabledGpioMask(pwm2_module_data_t *moduleData) { + uint16_t enableGpioMask = 0; + for (int i = 1; i < GPIO_PIN_NUM; i++) { + if (moduleData->setupData.pin[i].pulseResolutions > 0) { + enableGpioMask |= moduleData->interruptData.pin[i].gpioMask; + } + } + return enableGpioMask; +} + +static uint32_t findCommonCPUTicksDivisor(pwm2_module_data_t *moduleData) { + uint32_t gcdCPUTicks = 0; + for (int i = 1; i < GPIO_PIN_NUM; i++) { + if (moduleData->setupData.pin[i].pulseResolutions > 0) { + gcdCPUTicks = findGreatesCommonDividerForTimerTicks(moduleData->setupData.pin[i].resolutionCPUTicks, gcdCPUTicks); + } + } + return gcdCPUTicks; +} + +static uint32_t cpuToTimerTicks(uint32_t cpuTicks) { + return cpuTicks / getCpuTimerTicksDivisor(); +} + +static void updatePinResolutionToInterruptsMultiplier(pwm2_pin_setup_t *sPin, uint32_t timerCPUTicks) { + sPin->resolutionInterruptCounterMultiplier = sPin->resolutionCPUTicks / timerCPUTicks; +} + +static void updatePinPulseToInterruptsCounter(pwm2_pin_interrupt_t *iPin, pwm2_pin_setup_t *sPin) { + iPin->pulseInterruptCcounter = (sPin->pulseResolutions + 1) * sPin->resolutionInterruptCounterMultiplier; +} + +static uint8_t getDutyAdjustment(const uint32_t duty, const uint32_t pulse) { + if (duty == 0) { + return 0; + } else if (duty == pulse) { + return 2; + } else { + return 1; + } +} + +static void updatePinOffCounter(pwm2_pin_interrupt_t *iPin, pwm2_pin_setup_t *sPin) { + iPin->offInterruptCounter = (sPin->duty + getDutyAdjustment(sPin->duty, sPin->pulseResolutions)) * sPin->resolutionInterruptCounterMultiplier; +} + +static void reCalculateCommonToAllPinsData(pwm2_module_data_t *moduleData) { + moduleData->interruptData.enabledGpioMask = findAllEnabledGpioMask(moduleData); + moduleData->setupData.interruptTimerCPUTicks = findCommonCPUTicksDivisor(moduleData); + moduleData->setupData.interruptTimerTicks = cpuToTimerTicks(moduleData->setupData.interruptTimerCPUTicks); + for (int i = 1; i < GPIO_PIN_NUM; i++) { + if (isPinSetup(moduleData, i)) { + updatePinResolutionToInterruptsMultiplier(&moduleData->setupData.pin[i], moduleData->setupData.interruptTimerCPUTicks); + updatePinPulseToInterruptsCounter(&moduleData->interruptData.pin[i], &moduleData->setupData.pin[i]); + updatePinOffCounter(&moduleData->interruptData.pin[i], &moduleData->setupData.pin[i]); + } + } +} + +static uint64_t enduserFreqToCPUTicks(const uint64_t divisableFreq, const uint64_t freqDivisor, const uint64_t resolution) { + return (getCPUTicksPerSec() / (freqDivisor * resolution)) * divisableFreq; +} + +static uint16_t getPinGpioMask(uint8_t pin) { + return 1 << GPIO_ID_PIN(pin_num[pin]); +} + +static void set_duty(pwm2_module_data_t *moduleData, const uint8_t pin, const uint32_t duty) { + pwm2_pin_setup_t *sPin = &moduleData->setupData.pin[pin]; + pwm2_pin_interrupt_t *iPin = &moduleData->interruptData.pin[pin]; + sPin->duty = duty; + updatePinOffCounter(iPin, sPin); +} + +static void configureAllPinsAsGpioOutput(pwm2_module_data_t *moduleData) { + for (int i = 1; i < GPIO_PIN_NUM; i++) { + if (isPinSetup(moduleData, i)) { + PIN_FUNC_SELECT(pin_mux[i], pin_func[i]); // set pin as gpio + PIN_PULLUP_EN(pin_mux[i]); // set pin pullup on + } + } +} + +static void resetPinCounters(pwm2_module_data_t *moduleData) { + for (int i = 1; i < GPIO_PIN_NUM; i++) { + if (isPinSetup(moduleData, i)) { + moduleData->interruptData.pin[i].currentInterruptCounter = 0; + } + } +} + +//############################ +// interrupt handler related + +static inline void computeIsPinOn(pwm2_pin_interrupt_t *pin, uint16_t *maskOn) { + if (pin->currentInterruptCounter == pin->pulseInterruptCcounter) { + pin->currentInterruptCounter = 1; + } else { + pin->currentInterruptCounter++; + } + // ets_printf("curr=%u on=%u\n", pin->currentInterruptCounter, (pin->currentInterruptCounter < pin->offInterruptCounter)); + if (pin->currentInterruptCounter < pin->offInterruptCounter) { + *maskOn |= pin->gpioMask; + } +} + +static inline bool isPinSetup2(const pwm2_interrupt_handler_data_t *data, const uint8_t pin) { + return data->pin[pin].gpioMask > 0; +} + +static inline uint16_t findAllPinOns(pwm2_interrupt_handler_data_t *data) { + uint16_t maskOn = 0; + for (int i = 1; i < GPIO_PIN_NUM; i++) { + if (isPinSetup2(data, i)) { + computeIsPinOn(&data->pin[i], &maskOn); + } + } + return maskOn; +} + +static inline void setGpioPins(const uint16_t enabledGpioMask, const register uint16_t maskOn) { + GPIO_REG_WRITE(GPIO_OUT_W1TS_ADDRESS, maskOn); + const register uint16_t maskOff = ~maskOn & enabledGpioMask; + GPIO_REG_WRITE(GPIO_OUT_W1TC_ADDRESS, maskOff); +} + +static void ICACHE_RAM_ATTR timerInterruptHandler(os_param_t arg) { + pwm2_interrupt_handler_data_t *data = (pwm2_interrupt_handler_data_t *)arg; + setGpioPins(data->enabledGpioMask, findAllPinOns(data)); +} + +//############################ +// driver's public API + +void pwm2_init() { + moduleData = os_malloc(sizeof(pwm2_module_data_t)); + memset(moduleData, 0, sizeof(*moduleData)); +} + +pwm2_module_data_t *pwm2_get_module_data() { + return moduleData; +} + +bool pwm2_is_pin_setup(const uint8_t pin) { + return isPinSetup(moduleData, pin); +} + +void pwm2_setup_pin( + const uint8_t pin, + const uint32_t divisableFreq, + const uint32_t freqDivisor, + const uint32_t resolution, + const uint32_t initDuty + ) +{ + moduleData->setupData.pin[pin].pulseResolutions = resolution; + moduleData->setupData.pin[pin].divisableFrequency = divisableFreq; + moduleData->setupData.pin[pin].frequencyDivisor = freqDivisor; + moduleData->setupData.pin[pin].resolutionCPUTicks = enduserFreqToCPUTicks(divisableFreq, freqDivisor, resolution); + moduleData->interruptData.pin[pin].gpioMask = getPinGpioMask(pin); + reCalculateCommonToAllPinsData(moduleData); + set_duty(moduleData, pin, initDuty); +} + +void pwm2_release_pin(const uint8_t pin) { + moduleData->setupData.pin[pin].pulseResolutions = 0; + moduleData->interruptData.pin[pin].gpioMask = 0; +} + +void pwm2_stop() { + if (!moduleData->setupData.isStarted) { + return; + } + platform_hw_timer_close_exclusive(); + GPIO_REG_WRITE(GPIO_ENABLE_W1TC_ADDRESS, moduleData->interruptData.enabledGpioMask); // clear pins of being gpio output + moduleData->setupData.isStarted = false; +} + +bool pwm2_start() { + if (moduleData->setupData.isStarted) { + return true; + } + if (!platform_hw_timer_init_exclusive(FRC1_SOURCE, TRUE, timerInterruptHandler, (os_param_t)&moduleData->interruptData, (void (*)(void))NULL)) { + return false; + } + configureAllPinsAsGpioOutput(moduleData); + resetPinCounters(moduleData); + GPIO_REG_WRITE(GPIO_ENABLE_W1TS_ADDRESS, moduleData->interruptData.enabledGpioMask); // set pins as gpio output + moduleData->setupData.isStarted = true; + platform_hw_timer_arm_ticks_exclusive(moduleData->setupData.interruptTimerTicks); + return true; +} + +bool pwm2_is_started() { + return moduleData->setupData.isStarted; +} + +void pwm2_set_duty(const uint8_t pin, const uint32_t duty) { + set_duty(moduleData, pin, duty); +} diff --git a/app/include/driver/pwm2.h b/app/include/driver/pwm2.h new file mode 100644 index 00000000..c0fad44f --- /dev/null +++ b/app/include/driver/pwm2.h @@ -0,0 +1,65 @@ +/* + * Software PWM using soft-interrupt timer1. + * Supports higher frequencies compared to Espressif provided one. + * + * Nikolay Fiykov + */ + +#ifndef __PWM2_H__ +#define __PWM2_H__ + +#include "c_types.h" +#include "pin_map.h" + +typedef struct { + uint32_t offInterruptCounter; + uint32_t pulseInterruptCcounter; + uint32_t currentInterruptCounter; + uint16_t gpioMask; +} pwm2_pin_interrupt_t; + +typedef struct { + pwm2_pin_interrupt_t pin[GPIO_PIN_NUM]; + uint16_t enabledGpioMask; +} pwm2_interrupt_handler_data_t; + +typedef struct { + uint32_t pulseResolutions; + uint32_t divisableFrequency; + uint32_t frequencyDivisor; + uint32_t duty; + uint32_t resolutionCPUTicks; + uint32_t resolutionInterruptCounterMultiplier; +} pwm2_pin_setup_t; + +typedef struct { + pwm2_pin_setup_t pin[GPIO_PIN_NUM]; + uint32_t interruptTimerCPUTicks; + uint32_t interruptTimerTicks; + bool isStarted; +} pwm2_setup_data_t; + +typedef struct { + pwm2_interrupt_handler_data_t interruptData; + pwm2_setup_data_t setupData; +} pwm2_module_data_t; + +// driver's public API + +void pwm2_init(); +pwm2_module_data_t *pwm2_get_module_data(); +bool pwm2_is_pin_setup(const uint8_t pin); +void pwm2_setup_pin( + const uint8_t pin, + const uint32_t divisableFreq, + const uint32_t freqDivisor, + const uint32_t resolution, + const uint32_t initDuty + ); +void pwm2_release_pin(const uint8_t pin); +void pwm2_stop(); +bool pwm2_start(); +bool pwm2_is_started(); +void pwm2_set_duty(const uint8_t pin, const uint32_t duty); + +#endif diff --git a/app/include/user_modules.h b/app/include/user_modules.h index 2818ed7f..ecb2f446 100644 --- a/app/include/user_modules.h +++ b/app/include/user_modules.h @@ -43,6 +43,7 @@ //#define LUA_USE_MODULES_PCM //#define LUA_USE_MODULES_PERF //#define LUA_USE_MODULES_PWM +//#define LUA_USE_MODULES_PWM2 //#define LUA_USE_MODULES_RC //#define LUA_USE_MODULES_RFSWITCH //#define LUA_USE_MODULES_ROTARY diff --git a/app/modules/pwm2.c b/app/modules/pwm2.c new file mode 100644 index 00000000..027b4019 --- /dev/null +++ b/app/modules/pwm2.c @@ -0,0 +1,145 @@ +/* + * Software PWM using soft-interrupt timer1. + * Supports higher frequencies compared to Espressif provided one. + * + * Nikolay Fiykov + */ + +// Module for interfacing with PWM2 driver + +#include "c_types.h" +#include "lauxlib.h" +#include "module.h" +#include "driver/pwm2.h" + +#define luaL_argcheck2(L, cond, numarg, extramsg) \ + if (!(cond)) return luaL_argerror(L, (numarg), (extramsg)) + +//############################ +// lua bindings + +static int lpwm2_open(lua_State *L) { + pwm2_init(); + return 0; +} + +static int lpwm2_get_timer_data(lua_State *L) { + lua_pushboolean(L, pwm2_get_module_data()->setupData.isStarted); + lua_pushinteger(L, pwm2_get_module_data()->setupData.interruptTimerCPUTicks); + lua_pushinteger(L, pwm2_get_module_data()->setupData.interruptTimerTicks); + return 3; +} + +static int lpwm2_get_pin_data(lua_State *L) { + const uint8 pin = luaL_checkinteger(L, 1); + luaL_argcheck2(L, pin > 0 && pin <= GPIO_PIN_NUM, 1, "invalid pin number"); + lua_pushinteger(L, pwm2_is_pin_setup(pin)); + lua_pushinteger(L, pwm2_get_module_data()->setupData.pin[pin].duty); + lua_pushinteger(L, pwm2_get_module_data()->setupData.pin[pin].pulseResolutions); + lua_pushinteger(L, pwm2_get_module_data()->setupData.pin[pin].divisableFrequency); + lua_pushinteger(L, pwm2_get_module_data()->setupData.pin[pin].frequencyDivisor); + lua_pushinteger(L, pwm2_get_module_data()->setupData.pin[pin].resolutionCPUTicks); + lua_pushinteger(L, pwm2_get_module_data()->setupData.pin[pin].resolutionInterruptCounterMultiplier); + return 7; +} + +static int lpwm2_setup_pin_common(lua_State *L, const bool isFreqHz) { + if (pwm2_is_started()) { + return luaL_error(L, "pwm2 : already started, stop it before setting up pins."); + } + const int pin = lua_tointeger(L, 1); + const int freq = lua_tointeger(L, 2); + const int resolution = lua_tointeger(L, 3); + const int initDuty = lua_tointeger(L, 4); + const int freqFractions = luaL_optinteger(L, 5, 1); + + luaL_argcheck2(L, pin > 0 && pin <= GPIO_PIN_NUM, 1, "invalid pin number"); + luaL_argcheck2(L, freq > 0, 2, "invalid frequency"); + luaL_argcheck2(L, resolution > 0, 3, "invalid frequency resolution"); + luaL_argcheck2(L, initDuty >= 0 && initDuty <= resolution, 4, "invalid duty"); + luaL_argcheck2(L, freqFractions > 0, 5, "invalid frequency fractions"); + + if (isFreqHz) { + pwm2_setup_pin(pin, freqFractions, freq, resolution, initDuty); + } else { + pwm2_setup_pin(pin, freq, freqFractions, resolution, initDuty); + } + return 0; +} + +static int lpwm2_setup_pin_hz(lua_State *L) { + return lpwm2_setup_pin_common(L, true); +} + +static int lpwm2_setup_pin_sec(lua_State *L) { + return lpwm2_setup_pin_common(L, false); +} + +static int lpwm2_set_duty(lua_State *L) { + int pos = 0; + while (true) { + int pin = luaL_optinteger(L, ++pos, -1); + if (pin == -1) { + break; + } + luaL_argcheck2(L, pin > 0 && pin <= GPIO_PIN_NUM, pos, "invalid pin number"); + + int duty = luaL_optinteger(L, ++pos, -1); + luaL_argcheck2(L, duty >= 0 && duty <= pwm2_get_module_data()->setupData.pin[pin].pulseResolutions, pos, "invalid duty"); + + if (!pwm2_is_pin_setup(pin)) { + return luaL_error(L, "pwm2 : pin=%d is not setup yet", pin); + } + pwm2_set_duty(pin, duty); + } + return 0; +} + +static int lpwm2_release_pin(lua_State *L) { + if (pwm2_is_started()) { + return luaL_error(L, "pwm2 : pwm is started, stop it first."); + } + int pos = 0; + while (true) { + int pin = luaL_optinteger(L, ++pos, -1); + if (pin == -1) { + break; + } + luaL_argcheck2(L, pin > 0 && pin <= GPIO_PIN_NUM, pos, "invalid pin number"); + pwm2_release_pin(2); + } + return 0; +} + +static int lpwm2_stop(lua_State *L) { + pwm2_stop(); + return 0; +} + +static int lpwm2_start(lua_State *L) { + if (!pwm2_start()) { + luaL_error(L, "pwm2: currently platform timer1 is being used by another module.\n"); + lua_pushboolean(L, false); + } else { + lua_pushboolean(L, true); + } + return 1; +} + +// Module function map +LROT_BEGIN(pwm2) +LROT_FUNCENTRY(setup_pin_hz, lpwm2_setup_pin_hz) +LROT_FUNCENTRY(setup_pin_sec, lpwm2_setup_pin_sec) +LROT_FUNCENTRY(release_pin, lpwm2_release_pin) +LROT_FUNCENTRY(start, lpwm2_start) +LROT_FUNCENTRY(stop, lpwm2_stop) +LROT_FUNCENTRY(set_duty, lpwm2_set_duty) +LROT_FUNCENTRY(get_timer_data, lpwm2_get_timer_data) +LROT_FUNCENTRY(get_pin_data, lpwm2_get_pin_data) +// LROT_FUNCENTRY(print_setup, lpwm2_print_setup) +// LROT_FUNCENTRY( time_it, lpwm2_timeit) +// LROT_FUNCENTRY( test_tmr_manual, lpwm2_timing_frc1_manual_load_overhead) +// LROT_FUNCENTRY( test_tmr_auto, lpwm2_timing_frc1_auto_load_overhead) +LROT_END(pwm2, NULL, 0) + +NODEMCU_MODULE(PWM2, "pwm2", pwm2, lpwm2_open); diff --git a/app/platform/hw_timer.c b/app/platform/hw_timer.c index 0a0e45db..938d1809 100644 --- a/app/platform/hw_timer.c +++ b/app/platform/hw_timer.c @@ -86,6 +86,15 @@ 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. @@ -275,6 +284,8 @@ static void ICACHE_RAM_ATTR insert_active_tu(timer_user *tu) { *******************************************************************************/ 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) { @@ -322,6 +333,8 @@ 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) { + if (reserved_exclusively) return false; + timer_user *tu = find_tu(owner); if (!tu) { return false; @@ -393,6 +406,8 @@ 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) { + if (reserved_exclusively) return 0; + timer_user *tu = find_tu(owner); if (!tu) { return 0; @@ -412,7 +427,7 @@ uint32_t ICACHE_RAM_ATTR platform_hw_timer_get_delay_ticks(os_param_t owner) /****************************************************************************** * FunctionName : platform_hw_timer_init -* Description : initialize the hardware isr timer +* 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. @@ -424,6 +439,8 @@ 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) { + if (reserved_exclusively) return false; + timer_user *tu = find_tu_and_remove(owner); if (!tu) { @@ -456,19 +473,25 @@ bool platform_hw_timer_init(os_param_t owner, FRC1_TIMER_SOURCE_TYPE source_type /****************************************************************************** * FunctionName : platform_hw_timer_close -* Description : ends use of the hardware isr timer -* Parameters : os_param_t owner +* 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) { - LOCK(); - tu->next = inactive; - inactive = tu; - UNLOCK(); + if (tu == inactive) { + inactive == NULL; + } else { + LOCK(); + tu->next = inactive; + inactive = tu; + UNLOCK(); + } } // This will never actually run.... @@ -484,3 +507,91 @@ bool ICACHE_RAM_ATTR platform_hw_timer_close(os_param_t owner) 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; +} diff --git a/app/platform/hw_timer.h b/app/platform/hw_timer.h index f2290fc5..ee2f8e2d 100644 --- a/app/platform/hw_timer.h +++ b/app/platform/hw_timer.h @@ -30,5 +30,13 @@ bool ICACHE_RAM_ATTR platform_hw_timer_close(os_param_t owner); uint32_t ICACHE_RAM_ATTR platform_hw_timer_get_delay_ticks(os_param_t owner); +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) ); + +bool ICACHE_RAM_ATTR platform_hw_timer_close_exclusive(); + +bool ICACHE_RAM_ATTR platform_hw_timer_arm_ticks_exclusive(uint32_t ticks); + +bool ICACHE_RAM_ATTR platform_hw_timer_arm_us_exclusive(uint32_t microseconds); + #endif diff --git a/docs/modules/pwm2.md b/docs/modules/pwm2.md new file mode 100644 index 00000000..de467961 --- /dev/null +++ b/docs/modules/pwm2.md @@ -0,0 +1,309 @@ +# PWM2 Module +| Since | Origin / Contributor | Maintainer | Source | +| :----- | :-------------------- | :---------- | :------ | +| 2019-02-12 | [fikin](https://github.com/fikin) | [fikin](https://github.com/fikin) | [pwm2.c](../../../app/modules/pwm2.c)| + +Module to generate PWM impulses to any of the GPIO pins. + +PWM is being generated by software using soft-interrupt TIMER1 FRC1. This module is using the timer in exclusive mode. See [understanding timer use](#understanding-timer-use) for more. + +Supported frequencies are roughly from 120kHZ (with 50% duty) up to pulse/53sec (or 250kHz and 26 sec for CPU160). See [understanding frequencies](#understand-frequencies) for more. + +Supported are also frequency fractions even for integer-only firmware builds. + +Supported are all of the GPIO pins except pin 0. + +One can generate different PWM signals to any of them at the same time. See [working with multiple frequencies](#working-with-multiple-frequencies) for more. + +This module supports CPU80MHz as well as CPU160MHz. Frequency boundaries are same but by using CPU160MHz one can hope of doing more work meantime. + +Typical usage is as following: +```lua +pwm2.setup_pin_hz(3,250000,2,1) -- pin 3, PWM freq of 250kHz, pulse period of 2 steps, initial duty 1 period step +pwm2.setup_pin_hz(4,1,2,1) -- pin 4, PWM freq of 1Hz, pulse period of 2 steps, initial duty 1 period step +pwm2.start() -- starts pwm, internal led will blink with 0.5sec interval +... +pwm2.set_duty(4, 2) -- led full off (pin is high) +... +pwm2.set_duty(4, 0) -- led full on (pin is low) +... +pwm2.stop() -- PWM stopped, gpio pin released, timer1 released +``` + +## Understand frequencies + +All frequencines and periods internally are expressed as CPU ticks using following formula: `cpuTicksPerSecond / (frequencyHz * period)`. For example, 1kHz with 1000 period for CPU80MHz results in 80 CPU ticks per period i.e. period is 1uS long. + +In order to allow for better tunning, I've added an optional frequencyDivisor argument when setting pins up. With it one can express the frequency as division between two values : `frequency / divisor`. For example to model 100,1Hz frequency one has to specify frequency of 1001 and divisor 10. + +An easy way to express sub-Hz frequencies, i.e. the ones taking seconds to complete one impulse, is to use setup in seconds methods. For them formula to compute CPU ticks is `cpuTicksPerSecond * frequencySec / period`. Max pulse duration is limited by roll-over of the ESP's internal CPU 32bits ticks counter. For CPU80 that would be every 53 seconds, for CPU160 that would be half. + +## Frequency precision and limits + +ESP's TIMER1 FRC1 is operating at fixed, own frequency of 5MHz. Therefore the precision of individual interrupt is 200ns. But that limit cannot be attained. + +OS timer interrupt handler code itself has internal overhead. For auto-loaded interrupts it is about 50CPUTicks. For short periods of time one can interrupt at approximately 1MHz but then watchdog will intervene. + +PWM2 own interrupt handler has an overhead of 162CPUTicks + 12CPUTicks per each used pin. + +With the fastest setup i.e. 1 pin, 50% duty cycle (pulse period of 2) and CPU80 one could expect to achive PWM frequency of 125kHz. +For 12 pins that would drop to about 100kHz. With CPU160 one could reach 220kHz with 1 pin. + +Frequencies internally are expressed as CPU ticks first then to TIMER1 ticks. Because TIMER1 frequency is 1/16 of CPU frequency, some frequency precision is lost when converting from CPU to TIMER ticks. One can inspect exact values used via [pwm2.get_timer_data()](#pwm2get_timer_data). Value of `interruptTimerCPUTicks` represents desired interrupt period in CPUTicks. And `interruptTimerTicks` represents actually used interrupt period as TIMER1 ticks (1/16 of CPU). + +## Working with multiple frequencies + +When working with multiple pins, this module auto-discovers what would be the right underlying interrupt frequency. It does so by computing the greatest common frequency divisor and use it as common frequency for all pins. + +When using same frequency for many pins, tunning frequency of single pin is enough to ensure precision. + +When using different frequencies, one has to pay close attention at their greates common divisor when expressed as CPU ticks. For example, mixing 100kHz with period 2 and 0.5Hz with period 2 results in underlying interrupt period of 800CPU ticks. But changing to 100kHz+1 will easily result to divisor of 1. This is clearly non-working combination. +Another example is frequency of 120kHz with period 2, which results in period of 333CPU ticks. If combined with even-resulting frequency like 1Hz with period of 2, this will lead to common divisor of 1, which is clearly a non-working setup either. +For the moment best would be to use [pwm2.get_timer_data()](#pwm2get_timer_data) and observe how `interruptTimerCPUTicks` and `interruptTimerTicks` change with given input. + +## Understanding timer use + +This module is using soft-interrupt TIMER1 FRC1 to generate PWM signal. Since its interrupts can be masked, as some part of OS are doing it, it is possible to have some impact on the quality of generated PWM signal. As a general principle, one should not expect high precision signal with this module. +Also note that interrupt masking is dependent on other activities happening within the ESP besides pwm2 module. + +Additionally this timer is used by other modules like pwm, pcm, ws2812 and etc. Since an exclusive lock is maintained on the timer, simultaneous use of such modules would not be possible. + +## Troubleshooting watchdog timeouts + +Watchdog interrupt typically will occur if choosen frequency (and period) is too big i.e. too small timer ticks value. For CPU80MHz I guess threshold is around 125kHz with period of 2 and single pin (CPU80), given not much other load on the system. For CPU160 threshold is 225kHz. + +Another reason for watchdog interrupt to occur is due to mixing otherwise not very compatible frequencies when multiple pins are used. See [working with multiple frequencies](#working-with-multiple-frequencies) for more. + +Both cases are best anlyzed using [pwm2.get_timer_data()](#pwm2get_timer_data) watching values of `interruptTimerCPUTicks` and `interruptTimerTicks`. For `interruptTimerCPUTicks` with CPU80 anything below (330/630) for (1/12) pins would be cause for special attention. + +## Differences with PWM module + +PWM and PWM2 are modules doing similar job and have much in common. +Here are few PWM2 highlights compared to PWM module: + +- PWM2 is using TIMER1 exclusively, which allows for possibly a better quality PWM signal +- PWM2 can generate PWM frequencies in the range of 1pulse/53 seconds up to 125kHz (26sec/225kHz for CPU160) +- PWM2 can generate PWM frequencies with fractions i.e. 1001kHz +- PWM2 supports CPU160 +- PWM2 supports virtually all GPIO ports at the same time + +Unlike PWM2, PWM can: + +- generate PWM pulse with a little bit bigger duty cycle i.e. 1kHz at 1000 pulse period +- can be used at the same time with some other modules like gpio.pulse + +## pwm2.setup_pin_hz() + +Assigns PWM frequency expressed as Hz to given pin. +This method is suitable for setting up frequencies in the range of >= 1Hz. + +### Syntax + +`pwm2.setup_pin_hz(pin,frequencyAsHz,pulsePeriod,initialDuty [,frequencyDivisor])` + +### Parameters + +- `pin` 1-12 +- `frequencyAsHz` desired frequency in Hz, for example 1000 for 1KHz +- `pulsePeriod` discreet steps in single PWM pulse, for example 100 +- `initialDuty` initial duty in pulse period steps i.e. 50 for 50% pulse of 100 resolution +- `frequencyDivisor` an integer to divide product of frequency and pulsePeriod. Used to form frequency fractions. By default not required. + +### Returns + +`nil` + +### See also + +- [pwm2.setup_pin_sec()](#pwm2setup_pin_sec) +- [pwm2.start()](#pwm2start) +- [pwm2.release_pin()](#pwm2release_pin) +- [understanding frequencies](#understand-frequencies) +- [working with multiple frequencies](#working-with-multiple-frequencies) +- [pwm2.get_timer_data()](#pwm2get_timer_data) + +## pwm2.setup_pin_sec() + +Assigns PWM frequency expressed as one impulse per second(s) to given pin. +This method is suitable for setting up frequencies in the range of 0 < 1Hz but expressed as seconds instead. +For example 0.5Hz are expressed as 2 seconds impulse. + +### Syntax + +`pwm2.setup_pin_sec(pin,frequencyAsSec,pulsePeriod,initialDuty [,frequencyDivisor])` + +### Parameters + +- `pin` 1-12 +- `frequencyAsSec` desired frequency as one impulse for given seconds, for example 2 means PWM with impulse long 2 seconds. +- `pulsePeriod` discreet steps in single PWM pulse, for example 100 +- `initialDuty` initial duty in pulse period steps i.e. 50 for 50% pulse of 100 resolution +- `frequencyDivisor` an integer to divide product of frequency and pulsePeriod. Used to form frequency fractions. By default not required. + +### Returns + +`nil` + +### See also + +- [pwm2.setup_pin_hz()](#pwm2setup_pin_hz) +- [pwm2.start()](#pwm2start) +- [pwm2.release_pin()](#pwm2release_pin) +- [understanding frequencies](#understand-frequencies) +- [working with multiple frequencies](#working-with-multiple-frequencies) +- [pwm2.get_timer_data()](#pwm2get_timer_data) + +## pwm2.start() + +Starts PWM for all setup pins. +At this moment GPIO pins are marked as output and TIMER1 is being reserved for this module. +If the TIMER1 is already reserved by another module this method reports a Lua error and returns false. + +### Syntax + +`pwm2.start()` + +### Parameters + +`nil` + +### Returns + +- `bool` true if PWM started ok, false of TIMER1 is reserved by another module. + +### See also + +- [pwm2.setup_pin_hz()](#pwm2setup_pin_hz) +- [pwm2.setup_pin_sec()](#pwm2setup_pin_sec) +- [pwm2.set_duty()](#pwm2set_duty) +- [pwm2.stop()](#pwm2stop) + +## pwm2.stop() + +Stops PWM for all pins. All GPIO pins and TIMER1 are being released. +One can resume PWM with previous pin settings by calling [pwm2.start()](#pwm2start) right after stop. + +### Syntax + +`pwm2.stop()` + +### Parameters + +`nil` + +### Returns + +`nil` + +### See also + +- [pwm2.start()](#pwm2start) +- [pwm2.release_pin()](#pwm2release_pin) + +## pwm2.set_duty() + +Sets duty cycle for one or more a pins. This method takes immediate effect to ongoing PWM generation. + +### Syntax + +`pwm2.set_duty(pin, duty [,pin,duty]*)` + +### Parameters + +- `pin` 1~12, IO index +- `duty` 0~period, pwm duty cycle + +### Returns + +`nil` + +### See also + +- [pwm2.stop()](#pwm2stop) + +## pwm2.release_pin() + +Releases given pin from previously done setup. This method is applicable when PWM is stopped and given pin is not needed anymore. +Releasing pins is not strictly needed. This method is useful for start-stop-start situations when pins do change. + +### Syntax + +`pwm2.release_pin(pin)` + +### Parameters + +- `pin` 1~12, IO index + +### Returns + +`nil` + +### See also + +- [pwm2.setup_pin_hz()](#pwm2setup_pin_hz) +- [pwm2.setup_pin_sec()](#pwm2setup_pin_sec) +- [pwm2.stop()](#pwm2stop) + +## pwm2.get_timer_data() + +Prints internal data structures related to the timer. This method is usefull for people troubleshooting frequency side effects. + +### Syntax + +`pwm2.get_timer_data()` + +### Parameters + +`nil` + +### Returns + +- `isStarted` bool, if true PWM2 has been started +- `interruptTimerCPUTicks` int, desired timer interrupt period in CPU ticks +- `interruptTimerTicks` int, actual timer interrupt period in timer ticks + +### Example + +``` +isStarted, interruptTimerCPUTicks, interruptTimerTicks = pwm2.get_timer_data() +``` + +### See also + +- [pwm2.setup_pin_hz()](#pwm2setup_pin_hz) +- [pwm2.setup_pin_sec()](#pwm2setup_pin_sec) +- [pwm2.get_pin_data()](#pwm2get_pin_data) + +## pwm2.get_pin_data() + +Prints internal data structures related to given GPIO pin. This method is usefull for people troubleshooting frequency side effects. + +### Syntax + +`pwm2.get_pin_data(pin)` + +### Parameters + +- `pin` 1~12, IO index + +### Returns + +- `isPinSetup` bool, if 1 pin is setup +- `duty` int, assigned duty +- `pulseResolutions` int, assigned pulse periods +- `divisableFrequency` int, assigned frequency +- `frequencyDivisor` int, assigned frequency divisor +- `resolutionCPUTicks` int, calculated one pulse period in CPU ticks +- `resolutionInterruptCounterMultiplier` int, how many timer interrupts constitute one pulse period + +### Example + +``` +isPinSetup, duty, pulseResolutions, divisableFrequency, frequencyDivisor, resolutionCPUTicks, resolutionInterruptCounterMultiplier = pwm2..get_pin_data(4) +``` + +### See also + +- [pwm2.setup_pin_hz()](#pwm2setup_pin_hz) +- [pwm2.setup_pin_sec()](#pwm2setup_pin_sec) +- [pwm2.get_timer_data()](#pwm2get_timer_data) diff --git a/mkdocs.yml b/mkdocs.yml index 746c8929..bd79d837 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -88,6 +88,7 @@ pages: - 'pcm' : 'modules/pcm.md' - 'perf': 'modules/perf.md' - 'pwm' : 'modules/pwm.md' + - 'pwm2' : 'modules/pwm2.md' - 'rc' : 'modules/rc.md' - 'rfswitch' : 'modules/rfswitch.md' - 'rotary' : 'modules/rotary.md'