From 116df3753defc1eb25ff203d0123a1db666f0642 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 26 Jul 2019 11:57:55 +0200 Subject: [PATCH 01/14] Added dimmer module skeleton + add/remove --- components/modules/Kconfig | 6 ++ components/modules/dimmer.c | 157 ++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 components/modules/dimmer.c diff --git a/components/modules/Kconfig b/components/modules/Kconfig index 574f0bfdd2..2b5cf29c1e 100644 --- a/components/modules/Kconfig +++ b/components/modules/Kconfig @@ -92,6 +92,12 @@ config LUA_MODULE_DHT help Includes the dht module. +config LUA_MODULE_DIMMER + bool "Dimmer controller module" + default "n" + help + Includes the dimmer controller module. + config LUA_MODULE_ENCODER bool "Encoder module" default "n" diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c new file mode 100644 index 0000000000..4b0470030f --- /dev/null +++ b/components/modules/dimmer.c @@ -0,0 +1,157 @@ +// Module for interfacing with an MQTT broker +#include "driver/timer.h" +#include "esp_log.h" +#include "lauxlib.h" +#include "lmem.h" +#include "lnodeaux.h" +#include "module.h" +#include "platform.h" +#include "rom/gpio.h" +#include "task/task.h" + +#include + +#define DIMMER_METATABLE "dimmer.mt" +#define TAG "DIMMER" + +typedef struct { + int pin; + uint8_t mode; + int level; +} dim_t; + +static intr_handle_t s_timer_handle; +static int timerGroup; +static int timerNum; +static int t = 0; +//static bool reset = false; +static dim_t* dims = NULL; +static int dimCount = 0; + +static timg_dev_t* timerGroupDev; + +static void timer_isr(void* arg) { + t++; + // static int io_state = 0; + timerGroupDev->int_clr_timers.t0 = 1; + timerGroupDev->hw_timer[timerNum].config.alarm_en = 1; + + // io_state ^= 1; //Toggle the pins state + // gpio_set_direction(5, 0x00000002); + // gpio_set_level(5, io_state); +} + +/* +static void zc_isr(void* arg) { +} +*/ + +// pin +static int dimmer_add(lua_State* L) { + int pin = luaL_checkint(L, 1); + for (int i = 0; i < dimCount; i++) { + if (dims[i].pin == pin) { + return 0; + } + } + dims = luaM_realloc_(L, dims, dimCount * sizeof(dim_t), (dimCount + 1) * sizeof(dim_t)); + dims[dimCount].pin = pin; + dims[dimCount].level = 0; + dims[dimCount].mode = 0; + dimCount++; + return 0; +} + +static int dimmer_remove(lua_State* L) { + int pin = luaL_checkint(L, 1); + for (int i = 0; i < dimCount; i++) { + if (dims[i].pin == pin) { + for (int j = i; j < dimCount - 1; j++) { + dims[j] = dims[j + 1]; + } + dims = luaM_realloc_(L, dims, dimCount * sizeof(dim_t), (dimCount - 1) * sizeof(dim_t)); + dimCount--; + return 0; + } + } + luaL_error(L, "Error: pin %d is not dimmed.", pin); + return 0; +} + +static int dimmer_list_debug(lua_State* L) { + for (int i = 0; i < dimCount; i++) { + ESP_LOGD(TAG, "pin=%d, mode=%d, level=%d", dims[i].pin, dims[i].mode, dims[i].level); + } + return 0; +} + +// hw timer group, hw_timer num, zc pin +static int dimmer_setup(lua_State* L) { + timerGroup = luaL_checkint(L, 1); + timerNum = luaL_checkint(L, 2); + int zcPin = luaL_checkint(L, 3); + ESP_LOGD(TAG, "Dimmer setup. TG=%d, TN=%d, ZC=%d", timerGroup, timerNum, zcPin); + + if (timerGroup == 0) { + timerGroupDev = &TIMERG0; + } else { + timerGroupDev = &TIMERG1; + } + + timer_config_t config = { + .alarm_en = true, + .counter_en = false, + .intr_type = TIMER_INTR_LEVEL, + .counter_dir = TIMER_COUNT_UP, + .auto_reload = true, + .divider = 4000 /* 50 us per tick */ + }; + + timer_init(timerGroup, timerNum, &config); + + timer_set_counter_value(timerGroup, timerNum, 0); + printf("timer_set_counter_value\n"); + + timer_set_alarm_value(timerGroup, timerNum, 1000000); + printf("timer_set_alarm_value\n"); + timer_enable_intr(timerGroup, timerNum); + printf("timer_enable_intr\n"); + timer_isr_register(timerGroup, timerNum, &timer_isr, NULL, 0, &s_timer_handle); + printf("timer_isr_register\n"); + + timer_start(timerGroup, timerNum); + printf("timer_start\n"); + + return 0; +} + +// map client methods to functions: +static const LUA_REG_TYPE dimmer_metatable_map[] = + { + /* + {LSTRKEY("connect"), LFUNCVAL(mqtt_connect)}, + {LSTRKEY("close"), LFUNCVAL(mqtt_close)}, + {LSTRKEY("lwt"), LFUNCVAL(mqtt_lwt)}, + {LSTRKEY("publish"), LFUNCVAL(mqtt_publish)}, + {LSTRKEY("subscribe"), LFUNCVAL(mqtt_subscribe)}, + {LSTRKEY("unsubscribe"), LFUNCVAL(mqtt_unsubscribe)}, + {LSTRKEY("on"), LFUNCVAL(mqtt_on)}, + {LSTRKEY("__gc"), LFUNCVAL(mqtt_delete)}, + {LSTRKEY("__index"), LROVAL(mqtt_metatable_map)}, + */ + {LNILKEY, LNILVAL}}; + +// Module function map +static const LUA_REG_TYPE dimmer_map[] = { + {LSTRKEY("setup"), LFUNCVAL(dimmer_setup)}, + {LSTRKEY("add"), LFUNCVAL(dimmer_add)}, + {LSTRKEY("remove"), LFUNCVAL(dimmer_remove)}, + {LSTRKEY("list"), LFUNCVAL(dimmer_list_debug)}, + {LNILKEY, LNILVAL}}; + +int luaopen_dimmer(lua_State* L) { + luaL_rometatable(L, DIMMER_METATABLE, (void*)dimmer_metatable_map); // create metatable for dimmer + return 0; +} + +NODEMCU_MODULE(DIMMER, "dimmer", dimmer_map, luaopen_dimmer); From 5290f7da0e9c2b29b4f1fbdab21663e0ce03e539 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Wed, 22 Jan 2020 23:29:16 +0100 Subject: [PATCH 02/14] first working implementation --- components/modules/dimmer.c | 293 ++++++++++++++++++++++++++---------- 1 file changed, 212 insertions(+), 81 deletions(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index 4b0470030f..fee2384444 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -1,69 +1,105 @@ // Module for interfacing with an MQTT broker +#include +#include "driver/gpio.h" #include "driver/timer.h" +#include "esp_clk.h" +#include "esp_ipc.h" #include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/xtensa_timer.h" #include "lauxlib.h" #include "lmem.h" #include "lnodeaux.h" #include "module.h" #include "platform.h" #include "rom/gpio.h" -#include "task/task.h" - -#include +#include "soc/cpu.h" +#include "soc/rtc_wdt.h" -#define DIMMER_METATABLE "dimmer.mt" #define TAG "DIMMER" typedef struct { int pin; - uint8_t mode; - int level; + int mode; + uint32_t level; + bool switched; } dim_t; -static intr_handle_t s_timer_handle; -static int timerGroup; -static int timerNum; -static int t = 0; -//static bool reset = false; +typedef struct { + esp_err_t err; +} config_interrupt_t; + +#define DEBOUNCE_CYCLES 400000 // good for any CPU and mains frequency + +#define DIM_TIMER0 0x0 +#define DIM_TIMER1 0x1 +#define DIM_TIMER2 0x2 +#define DIM_TIMER3 0x3 +#define DIM_MODE_LEADING_EDGE 0x0 +#define DIM_MODE_TRAILING_EDGE 0x1 + +// menuconfig changes required in components/ESP32-specific... +// removed: "Also watch CPU1 tick interrupt" +// removed: "Watch CPU1 idle task" + +static volatile bool disable_loop = false; +static volatile bool disable_loop_ack = false; +static volatile int zCount = 0; +static int zcPin = -1; +static volatile uint32_t zcTimestamp = 0; +static uint32_t p = 0; static dim_t* dims = NULL; static int dimCount = 0; -static timg_dev_t* timerGroupDev; - -static void timer_isr(void* arg) { - t++; - // static int io_state = 0; - timerGroupDev->int_clr_timers.t0 = 1; - timerGroupDev->hw_timer[timerNum].config.alarm_en = 1; +static int check_err(lua_State* L, esp_err_t err) { + switch (err) { + case ESP_ERR_INVALID_ARG: + luaL_error(L, "invalid argument"); + case ESP_ERR_INVALID_STATE: + luaL_error(L, "internal logic error"); + case ESP_OK: + break; + } + return 0; +} - // io_state ^= 1; //Toggle the pins state - // gpio_set_direction(5, 0x00000002); - // gpio_set_level(5, io_state); +static void enable_interrupts() { + disable_loop = false; + disable_loop_ack = false; } -/* -static void zc_isr(void* arg) { +static void disable_interrupts() { + disable_loop = true; + while (!disable_loop_ack) + ; } -*/ // pin static int dimmer_add(lua_State* L) { int pin = luaL_checkint(L, 1); + int mode = luaL_optint(L, 2, DIM_MODE_LEADING_EDGE); for (int i = 0; i < dimCount; i++) { if (dims[i].pin == pin) { return 0; } } + check_err(L, gpio_set_direction(pin, GPIO_MODE_OUTPUT)); + disable_interrupts(); dims = luaM_realloc_(L, dims, dimCount * sizeof(dim_t), (dimCount + 1) * sizeof(dim_t)); dims[dimCount].pin = pin; - dims[dimCount].level = 0; - dims[dimCount].mode = 0; + dims[dimCount].level = (mode == DIM_MODE_LEADING_EDGE) ? p * 2 : 0; + dims[dimCount].mode = mode; + dims[dimCount].switched = false; dimCount++; + gpio_set_level(pin, 0); + enable_interrupts(); return 0; } static int dimmer_remove(lua_State* L) { int pin = luaL_checkint(L, 1); + disable_interrupts(); for (int i = 0; i < dimCount; i++) { if (dims[i].pin == pin) { for (int j = i; j < dimCount - 1; j++) { @@ -71,87 +107,182 @@ static int dimmer_remove(lua_State* L) { } dims = luaM_realloc_(L, dims, dimCount * sizeof(dim_t), (dimCount - 1) * sizeof(dim_t)); dimCount--; + enable_interrupts(); return 0; } } + enable_interrupts(); luaL_error(L, "Error: pin %d is not dimmed.", pin); return 0; } static int dimmer_list_debug(lua_State* L) { + ESP_LOGW(TAG, "p=%u, zcount=%d, zcTimestamp=%u, esp_freq=%d", p, zCount, zcTimestamp, esp_clk_cpu_freq()); + disable_interrupts(); + int dc = dimCount; + dim_t dimsc[dimCount]; + memcpy(dimsc, dims, sizeof(dim_t) * dimCount); + enable_interrupts(); + + for (int i = 0; i < dc; i++) { + ESP_LOGW(TAG, "pin=%d, mode=%d, level=%u", dimsc[i].pin, dimsc[i].mode, dimsc[i].level); + } + return 0; +} + +static int dimmer_set_level(lua_State* L) { + int pin = luaL_checkint(L, 1); + for (int i = 0; i < dimCount; i++) { - ESP_LOGD(TAG, "pin=%d, mode=%d, level=%d", dims[i].pin, dims[i].mode, dims[i].level); + if (dims[i].pin == pin) { + int level = luaL_checkint(L, 2); + if (dims[i].mode == DIM_MODE_LEADING_EDGE) { + level = 1000 - level; + } + if (level >= 1000) { + dims[i].level = p * 2; // ensure it will never switch. + } else if (level <= 0) { + dims[i].level = 0; + } else { + dims[i].level = (uint32_t)((((double)level) / 1000.0) * ((double)p)); + } + return 0; + } } + + luaL_error(L, "Cannot set dim level of unconfigured pin %d", pin); return 0; } -// hw timer group, hw_timer num, zc pin -static int dimmer_setup(lua_State* L) { - timerGroup = luaL_checkint(L, 1); - timerNum = luaL_checkint(L, 2); - int zcPin = luaL_checkint(L, 3); - ESP_LOGD(TAG, "Dimmer setup. TG=%d, TN=%d, ZC=%d", timerGroup, timerNum, zcPin); - - if (timerGroup == 0) { - timerGroupDev = &TIMERG0; - } else { - timerGroupDev = &TIMERG1; +static int dimmer_mainsFrequency(lua_State* L) { + int mainsF = 0; + if (p > 0) { + mainsF = (int)((esp_clk_cpu_freq() * 50.0) / p); } + lua_pushinteger(L, mainsF); + return 1; +} - timer_config_t config = { - .alarm_en = true, - .counter_en = false, - .intr_type = TIMER_INTR_LEVEL, - .counter_dir = TIMER_COUNT_UP, - .auto_reload = true, - .divider = 4000 /* 50 us per tick */ - }; +static void IRAM_ATTR disable_timer() { + /* + Enable the timer interrupt at the device level. Don't write directly + to the INTENABLE register because it may be virtualized. + */ + /* +#ifdef __XTENSA_CALL0_ABI__ + asm volatile( + "movi a2, XT_TIMER_INTEN\n") + "call0 xt_ints_off") +#else + asm volatile( + "movi a6, XT_TIMER_INTEN\n" + "call4 xt_ints_off") +#endif +*/ + portDISABLE_INTERRUPTS(); + ESP_INTR_DISABLE(XT_TIMER_INTNUM); + portENABLE_INTERRUPTS(); +} - timer_init(timerGroup, timerNum, &config); +static void IRAM_ATTR stuff(void* parms) { + //Find a way to disable the cpu1 tick interrupt. + disable_timer(); + rtc_wdt_protect_off(); + rtc_wdt_disable(); - timer_set_counter_value(timerGroup, timerNum, 0); - printf("timer_set_counter_value\n"); + /* + int level = 0; + gpio_set_direction(16, GPIO_MODE_OUTPUT); + while (1) { + GPIO_OUTPUT_SET(16, level); + if (level == 0) { + level = 1; + } else { + level = 0; + } + delayMicroseconds(1000000); + } + */ + uint32_t volatile now; + uint32_t volatile elapsed; + while (true) { + if (disable_loop) { + disable_loop_ack = true; + while (!disable_loop) + ; + } + // xthal_get_ccount(); + RSR(CCOUNT, now) + elapsed = now - zcTimestamp; + if ((elapsed > 600000) && (GPIO_INPUT_GET(zcPin) == 1)) { + zCount++; + for (int i = 0; i < dimCount; i++) { + if (dims[i].level == 0) { + GPIO_OUTPUT_SET(dims[i].pin, (1 - dims[i].mode)); + dims[i].switched = true; + } else { + GPIO_OUTPUT_SET(dims[i].pin, dims[i].mode); + dims[i].switched = false; + } + } + zcTimestamp = now; + p = elapsed; + } else { + for (int i = 0; i < dimCount; i++) { + if ((dims[i].switched == false) && (elapsed > dims[i].level)) { + GPIO_OUTPUT_SET(dims[i].pin, (1 - dims[i].mode)); + dims[i].switched = true; + } + } + } + } +} - timer_set_alarm_value(timerGroup, timerNum, 1000000); - printf("timer_set_alarm_value\n"); - timer_enable_intr(timerGroup, timerNum); - printf("timer_enable_intr\n"); - timer_isr_register(timerGroup, timerNum, &timer_isr, NULL, 0, &s_timer_handle); - printf("timer_isr_register\n"); +#define STACK_SIZE 4096 - timer_start(timerGroup, timerNum); - printf("timer_start\n"); +StaticTask_t xTaskBuffer; + +// hw timer group, hw_timer num, zc pin +static int dimmer_setup(lua_State* L) { + zcPin = luaL_checkinteger(L, -1); + + check_err(L, gpio_set_direction(zcPin, GPIO_MODE_INPUT)); + check_err(L, gpio_set_pull_mode(zcPin, GPIO_PULLUP_ONLY)); + + ESP_LOGD(TAG, "Dimmer setup. ZC=%d", zcPin); + TaskHandle_t handle; + xTaskCreatePinnedToCore( + stuff, // Function that implements the task. + "stuff", // Text name for the task. + STACK_SIZE, // Stack size in bytes, not words. + (void*)1, // Parameter passed into the task. + tskIDLE_PRIORITY + 20, + &handle, + // xStack, // Array to use as the task's stack. + // &xTaskBuffer, // Variable to hold the task's data structure. + 1); return 0; } -// map client methods to functions: -static const LUA_REG_TYPE dimmer_metatable_map[] = - { - /* - {LSTRKEY("connect"), LFUNCVAL(mqtt_connect)}, - {LSTRKEY("close"), LFUNCVAL(mqtt_close)}, - {LSTRKEY("lwt"), LFUNCVAL(mqtt_lwt)}, - {LSTRKEY("publish"), LFUNCVAL(mqtt_publish)}, - {LSTRKEY("subscribe"), LFUNCVAL(mqtt_subscribe)}, - {LSTRKEY("unsubscribe"), LFUNCVAL(mqtt_unsubscribe)}, - {LSTRKEY("on"), LFUNCVAL(mqtt_on)}, - {LSTRKEY("__gc"), LFUNCVAL(mqtt_delete)}, - {LSTRKEY("__index"), LROVAL(mqtt_metatable_map)}, - */ - {LNILKEY, LNILVAL}}; - // Module function map -static const LUA_REG_TYPE dimmer_map[] = { - {LSTRKEY("setup"), LFUNCVAL(dimmer_setup)}, - {LSTRKEY("add"), LFUNCVAL(dimmer_add)}, - {LSTRKEY("remove"), LFUNCVAL(dimmer_remove)}, - {LSTRKEY("list"), LFUNCVAL(dimmer_list_debug)}, - {LNILKEY, LNILVAL}}; +LROT_BEGIN(dimmer) +LROT_FUNCENTRY(setup, dimmer_setup) +LROT_FUNCENTRY(add, dimmer_add) +LROT_FUNCENTRY(remove, dimmer_remove) +LROT_FUNCENTRY(setLevel, dimmer_set_level) +LROT_FUNCENTRY(list, dimmer_list_debug) +LROT_FUNCENTRY(mainsFrequency, dimmer_mainsFrequency) +LROT_NUMENTRY(TIMER_0, DIM_TIMER0) +LROT_NUMENTRY(TIMER_1, DIM_TIMER1) +LROT_NUMENTRY(TIMER_2, DIM_TIMER2) +LROT_NUMENTRY(TIMER_3, DIM_TIMER3) + +LROT_END(dimmer, NULL, 0) int luaopen_dimmer(lua_State* L) { - luaL_rometatable(L, DIMMER_METATABLE, (void*)dimmer_metatable_map); // create metatable for dimmer + //luaL_rometatable(L, DIMMER_METATABLE, (void*)dimmer_metatable_map); // create metatable for dimmer return 0; } -NODEMCU_MODULE(DIMMER, "dimmer", dimmer_map, luaopen_dimmer); +NODEMCU_MODULE(DIMMER, "dimmer", dimmer, luaopen_dimmer); From 0b80c6bd619e3e30c91805491f0cecb153196abf Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 24 Jan 2020 14:36:29 +0100 Subject: [PATCH 03/14] change to pulldown + some docs --- components/modules/dimmer.c | 66 +++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index fee2384444..8787ebe816 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -164,21 +164,6 @@ static int dimmer_mainsFrequency(lua_State* L) { } static void IRAM_ATTR disable_timer() { - /* - Enable the timer interrupt at the device level. Don't write directly - to the INTENABLE register because it may be virtualized. - */ - /* -#ifdef __XTENSA_CALL0_ABI__ - asm volatile( - "movi a2, XT_TIMER_INTEN\n") - "call0 xt_ints_off") -#else - asm volatile( - "movi a6, XT_TIMER_INTEN\n" - "call4 xt_ints_off") -#endif -*/ portDISABLE_INTERRUPTS(); ESP_INTR_DISABLE(XT_TIMER_INTNUM); portENABLE_INTERRUPTS(); @@ -190,32 +175,29 @@ static void IRAM_ATTR stuff(void* parms) { rtc_wdt_protect_off(); rtc_wdt_disable(); - /* - int level = 0; - gpio_set_direction(16, GPIO_MODE_OUTPUT); - while (1) { - GPIO_OUTPUT_SET(16, level); - if (level == 0) { - level = 1; - } else { - level = 0; - } - delayMicroseconds(1000000); - } - */ - uint32_t volatile now; - uint32_t volatile elapsed; + // max_target determines a timeout waiting to register a zero crossing. It is calculated + // as the max number of CPU cycles in half a mains cycle for 50Hz mains (worst case), increased by 10% for margin. + uint32_t max_target = esp_clk_cpu_freq() / 50 /*Hz*/ / 2 /*half cycle*/ * 11 / 10 /* increase by 10%*/; + + // target specifies how many CPU cycles to wait before we poll GPIO to search for zero crossing + uint32_t target = 0; + while (true) { if (disable_loop) { disable_loop_ack = true; while (!disable_loop) ; } - // xthal_get_ccount(); - RSR(CCOUNT, now) - elapsed = now - zcTimestamp; - if ((elapsed > 600000) && (GPIO_INPUT_GET(zcPin) == 1)) { + // now contains the current value of the CPU cycle counter + uint32_t volatile now = xthal_get_ccount(); + // elapsed contains the number of CPU cycles since the last zero crossing + uint32_t volatile elapsed = now - zcTimestamp; + + // the following avoids polling GPIO unless we are close to a zero crossing: + if ((elapsed > target) && (GPIO_INPUT_GET(zcPin) == 1)) { zCount++; + // Zero crossing has been detected. Reset dimmers according to their mode + // (Leading edge or trailing edge) for (int i = 0; i < dimCount; i++) { if (dims[i].level == 0) { GPIO_OUTPUT_SET(dims[i].pin, (1 - dims[i].mode)); @@ -226,8 +208,14 @@ static void IRAM_ATTR stuff(void* parms) { } } zcTimestamp = now; - p = elapsed; + // some dimmer modules keep the signal high when there is no mains input + // The following sets period to 0, to signal there is no mains input. + p = elapsed < 10000 ? 0 : elapsed; + + // Calibrate new target as 90% of the elapsed time. + target = 9 * elapsed / 10; } else { + // Check if it is time to turn on or off any dimmed output: for (int i = 0; i < dimCount; i++) { if ((dims[i].switched == false) && (elapsed > dims[i].level)) { GPIO_OUTPUT_SET(dims[i].pin, (1 - dims[i].mode)); @@ -235,6 +223,12 @@ static void IRAM_ATTR stuff(void* parms) { } } } + // if too much time has pased since we detected a zero crossing, set our target to 0 to resync. + // This could happen if mains power is cut. + if (elapsed > max_target) { + target = 0; + p = 0; // mark period as 0 (no mains detected) + } } } @@ -247,7 +241,7 @@ static int dimmer_setup(lua_State* L) { zcPin = luaL_checkinteger(L, -1); check_err(L, gpio_set_direction(zcPin, GPIO_MODE_INPUT)); - check_err(L, gpio_set_pull_mode(zcPin, GPIO_PULLUP_ONLY)); + check_err(L, gpio_set_pull_mode(zcPin, GPIO_PULLDOWN_ONLY)); ESP_LOGD(TAG, "Dimmer setup. ZC=%d", zcPin); TaskHandle_t handle; From 3a6f44c1a69e016f01bbd6e5285b34f826140a5d Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Fri, 24 Jan 2020 15:39:43 +0100 Subject: [PATCH 04/14] messages --- components/modules/dimmer.c | 203 ++++++++++++++++++++---------------- 1 file changed, 111 insertions(+), 92 deletions(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index 8787ebe816..5fb59fd673 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -26,116 +26,92 @@ typedef struct { bool switched; } dim_t; +typedef enum { + MT_IDLE = 0, + MT_ADD = 1, + MT_REMOVE = 2, + MT_SETLEVEL = 3, +} message_type_t; + typedef struct { - esp_err_t err; -} config_interrupt_t; + message_type_t messageType; + int pin; + union { + int mode; + uint32_t level; + esp_err_t err; + }; -#define DEBOUNCE_CYCLES 400000 // good for any CPU and mains frequency +} dimmer_message_t; -#define DIM_TIMER0 0x0 -#define DIM_TIMER1 0x1 -#define DIM_TIMER2 0x2 -#define DIM_TIMER3 0x3 #define DIM_MODE_LEADING_EDGE 0x0 #define DIM_MODE_TRAILING_EDGE 0x1 +#define STACK_SIZE 4096 // menuconfig changes required in components/ESP32-specific... // removed: "Also watch CPU1 tick interrupt" // removed: "Watch CPU1 idle task" -static volatile bool disable_loop = false; -static volatile bool disable_loop_ack = false; +static volatile dimmer_message_t message = {.messageType = MT_IDLE}; static volatile int zCount = 0; static int zcPin = -1; static volatile uint32_t zcTimestamp = 0; static uint32_t p = 0; static dim_t* dims = NULL; static int dimCount = 0; +StaticTask_t xTaskBuffer; -static int check_err(lua_State* L, esp_err_t err) { +static void check_err(lua_State* L, esp_err_t err) { switch (err) { case ESP_ERR_INVALID_ARG: luaL_error(L, "invalid argument"); case ESP_ERR_INVALID_STATE: luaL_error(L, "internal logic error"); + case ESP_ERR_NO_MEM: + luaL_error(L, "out of memory"); case ESP_OK: break; + default: + luaL_error(L, "Error code %d", err); } - return 0; -} - -static void enable_interrupts() { - disable_loop = false; - disable_loop_ack = false; -} - -static void disable_interrupts() { - disable_loop = true; - while (!disable_loop_ack) - ; } -// pin -static int dimmer_add(lua_State* L) { - int pin = luaL_checkint(L, 1); - int mode = luaL_optint(L, 2, DIM_MODE_LEADING_EDGE); +static inline esp_err_t IRAM_ATTR msg_add_dimmer() { for (int i = 0; i < dimCount; i++) { - if (dims[i].pin == pin) { - return 0; + if (dims[i].pin == message.pin) { + return ESP_OK; } } - check_err(L, gpio_set_direction(pin, GPIO_MODE_OUTPUT)); - disable_interrupts(); - dims = luaM_realloc_(L, dims, dimCount * sizeof(dim_t), (dimCount + 1) * sizeof(dim_t)); - dims[dimCount].pin = pin; - dims[dimCount].level = (mode == DIM_MODE_LEADING_EDGE) ? p * 2 : 0; - dims[dimCount].mode = mode; + dims = realloc(dims, (dimCount + 1) * sizeof(dim_t)); + if (dims == NULL) { + return ESP_ERR_NO_MEM; + } + dims[dimCount].pin = message.pin; + dims[dimCount].level = (message.mode == DIM_MODE_LEADING_EDGE) ? p * 2 : 0; + dims[dimCount].mode = message.mode; dims[dimCount].switched = false; dimCount++; - gpio_set_level(pin, 0); - enable_interrupts(); - return 0; + return ESP_OK; } -static int dimmer_remove(lua_State* L) { - int pin = luaL_checkint(L, 1); - disable_interrupts(); +static inline esp_err_t IRAM_ATTR msg_remove_dimmer() { for (int i = 0; i < dimCount; i++) { - if (dims[i].pin == pin) { + if (dims[i].pin == message.pin) { for (int j = i; j < dimCount - 1; j++) { dims[j] = dims[j + 1]; } - dims = luaM_realloc_(L, dims, dimCount * sizeof(dim_t), (dimCount - 1) * sizeof(dim_t)); + dims = realloc(dims, (dimCount - 1) * sizeof(dim_t)); dimCount--; - enable_interrupts(); - return 0; + return ESP_OK; } } - enable_interrupts(); - luaL_error(L, "Error: pin %d is not dimmed.", pin); - return 0; + return ESP_ERR_INVALID_ARG; } -static int dimmer_list_debug(lua_State* L) { - ESP_LOGW(TAG, "p=%u, zcount=%d, zcTimestamp=%u, esp_freq=%d", p, zCount, zcTimestamp, esp_clk_cpu_freq()); - disable_interrupts(); - int dc = dimCount; - dim_t dimsc[dimCount]; - memcpy(dimsc, dims, sizeof(dim_t) * dimCount); - enable_interrupts(); - - for (int i = 0; i < dc; i++) { - ESP_LOGW(TAG, "pin=%d, mode=%d, level=%u", dimsc[i].pin, dimsc[i].mode, dimsc[i].level); - } - return 0; -} - -static int dimmer_set_level(lua_State* L) { - int pin = luaL_checkint(L, 1); - +static inline esp_err_t IRAM_ATTR msg_set_level() { for (int i = 0; i < dimCount; i++) { - if (dims[i].pin == pin) { - int level = luaL_checkint(L, 2); + if (dims[i].pin == message.pin) { + int level = message.level; if (dims[i].mode == DIM_MODE_LEADING_EDGE) { level = 1000 - level; } @@ -146,21 +122,10 @@ static int dimmer_set_level(lua_State* L) { } else { dims[i].level = (uint32_t)((((double)level) / 1000.0) * ((double)p)); } - return 0; + return ESP_OK; } } - - luaL_error(L, "Cannot set dim level of unconfigured pin %d", pin); - return 0; -} - -static int dimmer_mainsFrequency(lua_State* L) { - int mainsF = 0; - if (p > 0) { - mainsF = (int)((esp_clk_cpu_freq() * 50.0) / p); - } - lua_pushinteger(L, mainsF); - return 1; + return ESP_ERR_INVALID_ARG; } static void IRAM_ATTR disable_timer() { @@ -170,7 +135,6 @@ static void IRAM_ATTR disable_timer() { } static void IRAM_ATTR stuff(void* parms) { - //Find a way to disable the cpu1 tick interrupt. disable_timer(); rtc_wdt_protect_off(); rtc_wdt_disable(); @@ -183,11 +147,23 @@ static void IRAM_ATTR stuff(void* parms) { uint32_t target = 0; while (true) { - if (disable_loop) { - disable_loop_ack = true; - while (!disable_loop) - ; + if (message.messageType != MT_IDLE) { + switch (message.messageType) { + case MT_ADD: + message.err = msg_add_dimmer(); + break; + case MT_REMOVE: + message.err = msg_remove_dimmer(); + break; + case MT_SETLEVEL: + message.err = msg_set_level(); + break; + default: + break; + } + message.messageType = MT_IDLE; } + // now contains the current value of the CPU cycle counter uint32_t volatile now = xthal_get_ccount(); // elapsed contains the number of CPU cycles since the last zero crossing @@ -232,11 +208,59 @@ static void IRAM_ATTR stuff(void* parms) { } } -#define STACK_SIZE 4096 +static void awaitAck(lua_State* L) { + while (message.messageType != MT_IDLE) + ; + check_err(L, message.err); +} -StaticTask_t xTaskBuffer; +// pin +static int dimmer_add(lua_State* L) { + message.pin = luaL_checkint(L, 1); + message.mode = luaL_optint(L, 2, DIM_MODE_LEADING_EDGE); + check_err(L, gpio_set_direction(message.pin, GPIO_MODE_OUTPUT)); + gpio_set_level(message.pin, 0); + + message.messageType = MT_ADD; + awaitAck(L); + + return 0; +} + +static int dimmer_remove(lua_State* L) { + message.pin = luaL_checkint(L, 1); + message.messageType = MT_REMOVE; + awaitAck(L); + return 0; +} + +static int dimmer_list_debug(lua_State* L) { + ESP_LOGW(TAG, "p=%u, zcount=%d, zcTimestamp=%u, esp_freq=%d", p, zCount, zcTimestamp, esp_clk_cpu_freq()); + + for (int i = 0; i < dimCount; i++) { + ESP_LOGW(TAG, "pin=%d, mode=%d, level=%u", dims[i].pin, dims[i].mode, dims[i].level); + } + return 0; +} + +static int dimmer_set_level(lua_State* L) { + message.pin = luaL_checkint(L, 1); + message.level = luaL_checkint(L, 2); + message.messageType = MT_SETLEVEL; + awaitAck(L); + + return 0; +} + +static int dimmer_mainsFrequency(lua_State* L) { + int mainsF = 0; + if (p > 0) { + mainsF = (int)((esp_clk_cpu_freq() * 50.0) / p); + } + lua_pushinteger(L, mainsF); + return 1; +} -// hw timer group, hw_timer num, zc pin static int dimmer_setup(lua_State* L) { zcPin = luaL_checkinteger(L, -1); @@ -267,11 +291,6 @@ LROT_FUNCENTRY(remove, dimmer_remove) LROT_FUNCENTRY(setLevel, dimmer_set_level) LROT_FUNCENTRY(list, dimmer_list_debug) LROT_FUNCENTRY(mainsFrequency, dimmer_mainsFrequency) -LROT_NUMENTRY(TIMER_0, DIM_TIMER0) -LROT_NUMENTRY(TIMER_1, DIM_TIMER1) -LROT_NUMENTRY(TIMER_2, DIM_TIMER2) -LROT_NUMENTRY(TIMER_3, DIM_TIMER3) - LROT_END(dimmer, NULL, 0) int luaopen_dimmer(lua_State* L) { From 24cbb8955e431b02d3373fa315bf3f052bfeec0b Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Sun, 26 Jan 2020 23:31:11 +0100 Subject: [PATCH 05/14] check_setup and cleanup --- components/modules/dimmer.c | 59 ++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index 5fb59fd673..2d72d79853 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -64,7 +64,7 @@ StaticTask_t xTaskBuffer; static void check_err(lua_State* L, esp_err_t err) { switch (err) { case ESP_ERR_INVALID_ARG: - luaL_error(L, "invalid argument"); + luaL_error(L, "invalid argument or gpio pin"); case ESP_ERR_INVALID_STATE: luaL_error(L, "internal logic error"); case ESP_ERR_NO_MEM: @@ -76,6 +76,12 @@ static void check_err(lua_State* L, esp_err_t err) { } } +static void check_setup(lua_State* L) { + if (zcPin == -1) { + luaL_error(L, "dimmer module not initialized"); + } +} + static inline esp_err_t IRAM_ATTR msg_add_dimmer() { for (int i = 0; i < dimCount; i++) { if (dims[i].pin == message.pin) { @@ -128,14 +134,10 @@ static inline esp_err_t IRAM_ATTR msg_set_level() { return ESP_ERR_INVALID_ARG; } -static void IRAM_ATTR disable_timer() { +static void IRAM_ATTR cpu1_loop(void* parms) { portDISABLE_INTERRUPTS(); ESP_INTR_DISABLE(XT_TIMER_INTNUM); portENABLE_INTERRUPTS(); -} - -static void IRAM_ATTR stuff(void* parms) { - disable_timer(); rtc_wdt_protect_off(); rtc_wdt_disable(); @@ -216,6 +218,7 @@ static void awaitAck(lua_State* L) { // pin static int dimmer_add(lua_State* L) { + check_setup(L); message.pin = luaL_checkint(L, 1); message.mode = luaL_optint(L, 2, DIM_MODE_LEADING_EDGE); check_err(L, gpio_set_direction(message.pin, GPIO_MODE_OUTPUT)); @@ -228,22 +231,15 @@ static int dimmer_add(lua_State* L) { } static int dimmer_remove(lua_State* L) { + check_setup(L); message.pin = luaL_checkint(L, 1); message.messageType = MT_REMOVE; awaitAck(L); return 0; } -static int dimmer_list_debug(lua_State* L) { - ESP_LOGW(TAG, "p=%u, zcount=%d, zcTimestamp=%u, esp_freq=%d", p, zCount, zcTimestamp, esp_clk_cpu_freq()); - - for (int i = 0; i < dimCount; i++) { - ESP_LOGW(TAG, "pin=%d, mode=%d, level=%u", dims[i].pin, dims[i].mode, dims[i].level); - } - return 0; -} - static int dimmer_set_level(lua_State* L) { + check_setup(L); message.pin = luaL_checkint(L, 1); message.level = luaL_checkint(L, 2); message.messageType = MT_SETLEVEL; @@ -261,25 +257,39 @@ static int dimmer_mainsFrequency(lua_State* L) { return 1; } +static int dimmer_list_debug(lua_State* L) { + ESP_LOGW(TAG, "p=%u, zcount=%d, zcTimestamp=%u, esp_freq=%d", p, zCount, zcTimestamp, esp_clk_cpu_freq()); + + for (int i = 0; i < dimCount; i++) { + ESP_LOGW(TAG, "pin=%d, mode=%d, level=%u", dims[i].pin, dims[i].mode, dims[i].level); + } + return 0; +} + static int dimmer_setup(lua_State* L) { + if (zcPin != -1) { + return 0; + } zcPin = luaL_checkinteger(L, -1); check_err(L, gpio_set_direction(zcPin, GPIO_MODE_INPUT)); check_err(L, gpio_set_pull_mode(zcPin, GPIO_PULLDOWN_ONLY)); - ESP_LOGD(TAG, "Dimmer setup. ZC=%d", zcPin); TaskHandle_t handle; - xTaskCreatePinnedToCore( - stuff, // Function that implements the task. - "stuff", // Text name for the task. - STACK_SIZE, // Stack size in bytes, not words. - (void*)1, // Parameter passed into the task. + BaseType_t result = xTaskCreatePinnedToCore( + cpu1_loop, + "cpu1 loop", + STACK_SIZE, + NULL, tskIDLE_PRIORITY + 20, &handle, - // xStack, // Array to use as the task's stack. - // &xTaskBuffer, // Variable to hold the task's data structure. 1); + if (result != pdPASS) { + luaL_error(L, "Error starting dimmer task in CPU1"); + zcPin = -1; + } + return 0; } @@ -291,10 +301,11 @@ LROT_FUNCENTRY(remove, dimmer_remove) LROT_FUNCENTRY(setLevel, dimmer_set_level) LROT_FUNCENTRY(list, dimmer_list_debug) LROT_FUNCENTRY(mainsFrequency, dimmer_mainsFrequency) +LROT_NUMENTRY(TRAILING_EDGE, DIM_MODE_TRAILING_EDGE) +LROT_NUMENTRY(LEADING_EDGE, DIM_MODE_LEADING_EDGE) LROT_END(dimmer, NULL, 0) int luaopen_dimmer(lua_State* L) { - //luaL_rometatable(L, DIMMER_METATABLE, (void*)dimmer_metatable_map); // create metatable for dimmer return 0; } From ecd55b86a719b4b7ca260f47cf06ec87542a8a9f Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Sun, 26 Jan 2020 23:57:42 +0100 Subject: [PATCH 06/14] cleanup includes --- components/modules/dimmer.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index 2d72d79853..776bd9eff9 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -1,20 +1,15 @@ // Module for interfacing with an MQTT broker #include #include "driver/gpio.h" -#include "driver/timer.h" #include "esp_clk.h" -#include "esp_ipc.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/xtensa_timer.h" #include "lauxlib.h" -#include "lmem.h" #include "lnodeaux.h" #include "module.h" -#include "platform.h" #include "rom/gpio.h" -#include "soc/cpu.h" #include "soc/rtc_wdt.h" #define TAG "DIMMER" From 14a256ebbca4afec938d3b80bdda81d95590f963 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 27 Jan 2020 00:17:05 +0100 Subject: [PATCH 07/14] adjust task stack size --- components/modules/dimmer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index 776bd9eff9..4d124819fa 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -41,7 +41,7 @@ typedef struct { #define DIM_MODE_LEADING_EDGE 0x0 #define DIM_MODE_TRAILING_EDGE 0x1 -#define STACK_SIZE 4096 +#define STACK_SIZE 512 // menuconfig changes required in components/ESP32-specific... // removed: "Also watch CPU1 tick interrupt" From 154db428479a8adee83ca7a6649252bdc2ef7cd9 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 27 Jan 2020 01:23:33 +0100 Subject: [PATCH 08/14] added intro and some comments --- components/modules/dimmer.c | 38 +++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index 4d124819fa..438c7780ab 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -1,4 +1,26 @@ -// Module for interfacing with an MQTT broker +// Module for driving TRIAC-based dimmers +// This module implements phase-dimming for TRIAC-based mains dimmers. +// brief description on how TRIAC-based dimming works: https://www.lamps-on-line.com/leading-trailing-edge-led-dimmers + +// Example hardware: https://robotdyn.com/ac-light-dimmer-module-1-channel-3-3v-5v-logic-ac-50-60hz-220v-110v.html +// These modules come with a TRIAC whose gate is driven by a GPIO pin, which is isolated +// from mains by an optocoupler. These modules also come with a zero-crossing detector, +// that raises a pin when the AC mains sine wave signal crosses 0V. + +// Phase dimming is implemented by dedicating CPU1 entirely to this purpose... Phase dimming +// requires very accurate timing. Configuring timer interrupts in the "busy" CPU0 +// does not cut it, with FreeRTOS scheduler, WiFi and so on demanding their share of the CPU +// at random times, which would make dimmed lamps flicker. + +// Once the dimmer module is started, by means of dimmer.setup(), a busy loop is launched +// on CPU1 that monitors zero-crossing signals from the dimmer and turns on/off +// the TRIAC at the appropriate time, with nanosecond precision. + +// To use this module, change the following in menuconfig: +// * Enable FreeRTOS in both cores by unselecting "Component Config/FreeRTOS/Run FreeRTOS only on first core" +// * Unselect "ComponentConfig/ESP32-specific/Also watch CPU1 tick interrupt" +// * Unselect "ComponentConfig/ESP32-specific/Watch CPU1 idle task" + #include #include "driver/gpio.h" #include "esp_clk.h" @@ -12,13 +34,17 @@ #include "rom/gpio.h" #include "soc/rtc_wdt.h" +// TAG for debug logging #define TAG "DIMMER" +// dim_t defines a dimmer pin configuration typedef struct { - int pin; - int mode; + int pin; // pin to apply phase-dimming to. + int mode; // dimming mode: LEADING_EDGE or TRAILING_EDGE + // level indicates the brightness level, computed as the number of + // CPU cycles to wait since the last zero crossing until switching the signal on or off. uint32_t level; - bool switched; + bool switched; //wether this output has already been switched on or off in the current cycle. } dim_t; typedef enum { @@ -43,10 +69,6 @@ typedef struct { #define DIM_MODE_TRAILING_EDGE 0x1 #define STACK_SIZE 512 -// menuconfig changes required in components/ESP32-specific... -// removed: "Also watch CPU1 tick interrupt" -// removed: "Watch CPU1 idle task" - static volatile dimmer_message_t message = {.messageType = MT_IDLE}; static volatile int zCount = 0; static int zcPin = -1; From 8dda8f34df2aa5c6f5361e0bf91674ca6b6b844e Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 27 Jan 2020 01:36:16 +0100 Subject: [PATCH 09/14] more comments --- components/modules/dimmer.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index 438c7780ab..ba53346805 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -2,7 +2,8 @@ // This module implements phase-dimming for TRIAC-based mains dimmers. // brief description on how TRIAC-based dimming works: https://www.lamps-on-line.com/leading-trailing-edge-led-dimmers -// Example hardware: https://robotdyn.com/ac-light-dimmer-module-1-channel-3-3v-5v-logic-ac-50-60hz-220v-110v.html +// Schematics for a homemade module: https://hackaday.io/project/165927-a-digital-ac-dimmer-using-arduino/details +// Example commercial hardware: https://robotdyn.com/ac-light-dimmer-module-1-channel-3-3v-5v-logic-ac-50-60hz-220v-110v.html // These modules come with a TRIAC whose gate is driven by a GPIO pin, which is isolated // from mains by an optocoupler. These modules also come with a zero-crossing detector, // that raises a pin when the AC mains sine wave signal crosses 0V. @@ -21,6 +22,12 @@ // * Unselect "ComponentConfig/ESP32-specific/Also watch CPU1 tick interrupt" // * Unselect "ComponentConfig/ESP32-specific/Watch CPU1 idle task" +// In your program, call dimmer.setup(zc_pin), indicating the pin input where you connected the zero crossing detector. +// Use dimmer.add(pin, type) to have the module control that pin. +// Type can be dimmer.LEADING_EDGE (default) or dimmer.TRAILING_EDGE, depending on the type of load/lightbulb. +// User dimmer.setLevel(pin, value) to set the desired brightness level. Value can be from 0 (off) to 1000 (fully on). +// Use dimmer.remove(pin) to have the dimmer module stop controlling that pin. + #include #include "driver/gpio.h" #include "esp_clk.h" From 5809b258da489b802887c2a8870d5c919aca2dd7 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 28 Jan 2020 01:42:39 +0100 Subject: [PATCH 10/14] Added brightness transition --- components/modules/dimmer.c | 76 +++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index ba53346805..d4825b19ef 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -48,10 +48,14 @@ typedef struct { int pin; // pin to apply phase-dimming to. int mode; // dimming mode: LEADING_EDGE or TRAILING_EDGE - // level indicates the brightness level, computed as the number of + // level indicates the current brightness level, computed as the number of // CPU cycles to wait since the last zero crossing until switching the signal on or off. uint32_t level; - bool switched; //wether this output has already been switched on or off in the current cycle. + // targetLevel indicates the desired brightness level, computed in CPU cycles, as above. + // every mains cycle, `level` will get closer to `targetLevel`, so brightness changes gradually. + // this avoids damaging power supplies and rushing current through the dimmer module. + uint32_t targetLevel; + bool switched; //wether this output has already been switched on or off in the current mains cycle. } dim_t; typedef enum { @@ -66,7 +70,7 @@ typedef struct { int pin; union { int mode; - uint32_t level; + uint32_t targetLevel; esp_err_t err; }; @@ -76,6 +80,15 @@ typedef struct { #define DIM_MODE_TRAILING_EDGE 0x1 #define STACK_SIZE 512 +// Every mains cycle, the lamp brightness will gradually get closer to the desired brightness. +// this avoids damaging power supplies and rushing current through the dimmer module. +// DEFAULT_TRANSITION_SPEED controls the default fade speed. +// It is defined as a per mille (‰) brightness delta per half mains cycle (10ms if 50Hz) +// for example, if set to 20, a light would go from zero to full brightness in 1000 / 20 * 10ms = 500ms +// Set to 1000 to disable fading as default. +// This is a default. The user can define a specific value when calling dimmer.setup() +#define DEFAULT_TRANSITION_SPEED 100 /* 1-1000 */ + static volatile dimmer_message_t message = {.messageType = MT_IDLE}; static volatile int zCount = 0; static int zcPin = -1; @@ -83,8 +96,11 @@ static volatile uint32_t zcTimestamp = 0; static uint32_t p = 0; static dim_t* dims = NULL; static int dimCount = 0; +static int transitionSpeed; StaticTask_t xTaskBuffer; +#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) + static void check_err(lua_State* L, esp_err_t err) { switch (err) { case ESP_ERR_INVALID_ARG: @@ -117,7 +133,7 @@ static inline esp_err_t IRAM_ATTR msg_add_dimmer() { return ESP_ERR_NO_MEM; } dims[dimCount].pin = message.pin; - dims[dimCount].level = (message.mode == DIM_MODE_LEADING_EDGE) ? p * 2 : 0; + dims[dimCount].targetLevel = dims[dimCount].level = (message.mode == DIM_MODE_LEADING_EDGE) ? p * 2 : 0; dims[dimCount].mode = message.mode; dims[dimCount].switched = false; dimCount++; @@ -141,16 +157,16 @@ static inline esp_err_t IRAM_ATTR msg_remove_dimmer() { static inline esp_err_t IRAM_ATTR msg_set_level() { for (int i = 0; i < dimCount; i++) { if (dims[i].pin == message.pin) { - int level = message.level; + int targetLevel = message.targetLevel; if (dims[i].mode == DIM_MODE_LEADING_EDGE) { - level = 1000 - level; + targetLevel = 1000 - targetLevel; } - if (level >= 1000) { - dims[i].level = p * 2; // ensure it will never switch. - } else if (level <= 0) { - dims[i].level = 0; + if (targetLevel >= 1000) { + dims[i].targetLevel = p * 2; // ensure it will never switch. + } else if (targetLevel <= 0) { + dims[i].targetLevel = 0; } else { - dims[i].level = (uint32_t)((((double)level) / 1000.0) * ((double)p)); + dims[i].targetLevel = (uint32_t)((((double)targetLevel) / 1000.0) * ((double)p)); } return ESP_OK; } @@ -197,22 +213,34 @@ static void IRAM_ATTR cpu1_loop(void* parms) { // the following avoids polling GPIO unless we are close to a zero crossing: if ((elapsed > target) && (GPIO_INPUT_GET(zcPin) == 1)) { + // Zero crossing has been detected. zCount++; - // Zero crossing has been detected. Reset dimmers according to their mode - // (Leading edge or trailing edge) + p = (elapsed < 1000) ? 0 : elapsed; + uint32_t levelStep = p * transitionSpeed / 1000; + for (int i = 0; i < dimCount; i++) { - if (dims[i].level == 0) { - GPIO_OUTPUT_SET(dims[i].pin, (1 - dims[i].mode)); - dims[i].switched = true; + dim_t* dim = &dims[i]; + //Reset dimmers according to their mode + // (Leading edge or trailing edge) + if (dim->level == 0) { + GPIO_OUTPUT_SET(dim->pin, (1 - dim->mode)); + dim->switched = true; } else { - GPIO_OUTPUT_SET(dims[i].pin, dims[i].mode); - dims[i].switched = false; + GPIO_OUTPUT_SET(dim->pin, dim->mode); + dim->switched = false; + } + // For this cycle that starts, adjust the brightness level one step + // closer to the target brightness level. + int32_t levelDiff = dim->targetLevel - dim->level; + if (levelDiff > 0) { + dim->level += MIN(levelStep, levelDiff); + } else if (levelDiff < 0) { + dim->level -= MIN(levelStep, -levelDiff); } } zcTimestamp = now; // some dimmer modules keep the signal high when there is no mains input // The following sets period to 0, to signal there is no mains input. - p = elapsed < 10000 ? 0 : elapsed; // Calibrate new target as 90% of the elapsed time. target = 9 * elapsed / 10; @@ -265,7 +293,7 @@ static int dimmer_remove(lua_State* L) { static int dimmer_set_level(lua_State* L) { check_setup(L); message.pin = luaL_checkint(L, 1); - message.level = luaL_checkint(L, 2); + message.targetLevel = luaL_checkint(L, 2); message.messageType = MT_SETLEVEL; awaitAck(L); @@ -290,11 +318,17 @@ static int dimmer_list_debug(lua_State* L) { return 0; } +// dimmer.setup(zcPin, transitionSpeed) +// zcPin: input pin connected to the zero-crossing detector +// transitionSpeed: 1-1000. Allows controlling the speed at which to change brightness. +// set to 1000 to disable fading altogether (instant change). +// defaults to a quick ~100ms fade. static int dimmer_setup(lua_State* L) { if (zcPin != -1) { return 0; } - zcPin = luaL_checkinteger(L, -1); + zcPin = luaL_checkint(L, 1); + transitionSpeed = luaL_optint(L, 2, DEFAULT_TRANSITION_SPEED); check_err(L, gpio_set_direction(zcPin, GPIO_MODE_INPUT)); check_err(L, gpio_set_pull_mode(zcPin, GPIO_PULLDOWN_ONLY)); From 3e5fda2bc83cb7f380caaa3b9cd44f97ab645743 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Tue, 28 Jan 2020 23:26:05 +0100 Subject: [PATCH 11/14] with linked list --- components/modules/dimmer.c | 253 +++++++++++++++++++----------------- 1 file changed, 137 insertions(+), 116 deletions(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index d4825b19ef..24c31c6f5f 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -36,6 +36,7 @@ #include "freertos/task.h" #include "freertos/xtensa_timer.h" #include "lauxlib.h" +#include "lmem.h" #include "lnodeaux.h" #include "module.h" #include "rom/gpio.h" @@ -45,17 +46,18 @@ #define TAG "DIMMER" // dim_t defines a dimmer pin configuration -typedef struct { +typedef struct dim_t { int pin; // pin to apply phase-dimming to. int mode; // dimming mode: LEADING_EDGE or TRAILING_EDGE // level indicates the current brightness level, computed as the number of // CPU cycles to wait since the last zero crossing until switching the signal on or off. uint32_t level; - // targetLevel indicates the desired brightness level, computed in CPU cycles, as above. - // every mains cycle, `level` will get closer to `targetLevel`, so brightness changes gradually. + // target_level indicates the desired brightness level, computed in CPU cycles, as above. + // every mains cycle, `level` will get closer to `target_level`, so brightness changes gradually. // this avoids damaging power supplies and rushing current through the dimmer module. - uint32_t targetLevel; + uint32_t target_level; bool switched; //wether this output has already been switched on or off in the current mains cycle. + struct dim_t* next; } dim_t; typedef enum { @@ -66,12 +68,11 @@ typedef enum { } message_type_t; typedef struct { - message_type_t messageType; - int pin; + message_type_t message_type; + esp_err_t err; union { - int mode; - uint32_t targetLevel; - esp_err_t err; + int pin; + dim_t* dim; }; } dimmer_message_t; @@ -89,14 +90,13 @@ typedef struct { // This is a default. The user can define a specific value when calling dimmer.setup() #define DEFAULT_TRANSITION_SPEED 100 /* 1-1000 */ -static volatile dimmer_message_t message = {.messageType = MT_IDLE}; -static volatile int zCount = 0; -static int zcPin = -1; -static volatile uint32_t zcTimestamp = 0; -static uint32_t p = 0; -static dim_t* dims = NULL; -static int dimCount = 0; -static int transitionSpeed; +static volatile dimmer_message_t message = {.message_type = MT_IDLE}; +static volatile int zc_count = 0; +static int zc_pin = -1; +static uint32_t zc_timestamp = 0; +static uint32_t mains_period = 0; +static dim_t* dims_head = NULL; +static int transition_speed; StaticTask_t xTaskBuffer; #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) @@ -107,8 +107,6 @@ static void check_err(lua_State* L, esp_err_t err) { luaL_error(L, "invalid argument or gpio pin"); case ESP_ERR_INVALID_STATE: luaL_error(L, "internal logic error"); - case ESP_ERR_NO_MEM: - luaL_error(L, "out of memory"); case ESP_OK: break; default: @@ -117,60 +115,46 @@ static void check_err(lua_State* L, esp_err_t err) { } static void check_setup(lua_State* L) { - if (zcPin == -1) { + if (zc_pin == -1) { luaL_error(L, "dimmer module not initialized"); } } -static inline esp_err_t IRAM_ATTR msg_add_dimmer() { - for (int i = 0; i < dimCount; i++) { - if (dims[i].pin == message.pin) { - return ESP_OK; +static dim_t* find(int pin) { + for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { + if (dim->pin == pin) { + return dim; } } - dims = realloc(dims, (dimCount + 1) * sizeof(dim_t)); - if (dims == NULL) { - return ESP_ERR_NO_MEM; - } - dims[dimCount].pin = message.pin; - dims[dimCount].targetLevel = dims[dimCount].level = (message.mode == DIM_MODE_LEADING_EDGE) ? p * 2 : 0; - dims[dimCount].mode = message.mode; - dims[dimCount].switched = false; - dimCount++; - return ESP_OK; + return NULL; } -static inline esp_err_t IRAM_ATTR msg_remove_dimmer() { - for (int i = 0; i < dimCount; i++) { - if (dims[i].pin == message.pin) { - for (int j = i; j < dimCount - 1; j++) { - dims[j] = dims[j + 1]; - } - dims = realloc(dims, (dimCount - 1) * sizeof(dim_t)); - dimCount--; - return ESP_OK; - } - } - return ESP_ERR_INVALID_ARG; +static inline esp_err_t IRAM_ATTR msg_add_dimmer() { + dim_t* dim = find(message.dim->pin); + if (dim != NULL) + return ESP_ERR_INVALID_ARG; + + message.dim->next = dims_head; + dims_head = message.dim; + message.dim = NULL; + return ESP_OK; } -static inline esp_err_t IRAM_ATTR msg_set_level() { - for (int i = 0; i < dimCount; i++) { - if (dims[i].pin == message.pin) { - int targetLevel = message.targetLevel; - if (dims[i].mode == DIM_MODE_LEADING_EDGE) { - targetLevel = 1000 - targetLevel; - } - if (targetLevel >= 1000) { - dims[i].targetLevel = p * 2; // ensure it will never switch. - } else if (targetLevel <= 0) { - dims[i].targetLevel = 0; +static inline esp_err_t IRAM_ATTR msg_remove_dimmer() { + dim_t* previous = NULL; + for (dim_t* current = dims_head; current != NULL; current = current->next) { + if (current->pin == message.pin) { + message.dim = current; + if (previous == NULL) { + dims_head = dims_head->next; } else { - dims[i].targetLevel = (uint32_t)((((double)targetLevel) / 1000.0) * ((double)p)); + previous->next = current->next; } return ESP_OK; } + previous = current; } + message.dim = NULL; return ESP_ERR_INVALID_ARG; } @@ -189,39 +173,30 @@ static void IRAM_ATTR cpu1_loop(void* parms) { uint32_t target = 0; while (true) { - if (message.messageType != MT_IDLE) { - switch (message.messageType) { + if (message.message_type != MT_IDLE) { + switch (message.message_type) { case MT_ADD: message.err = msg_add_dimmer(); break; case MT_REMOVE: message.err = msg_remove_dimmer(); break; - case MT_SETLEVEL: - message.err = msg_set_level(); - break; default: break; } - message.messageType = MT_IDLE; + message.message_type = MT_IDLE; } // now contains the current value of the CPU cycle counter - uint32_t volatile now = xthal_get_ccount(); + uint32_t now = xthal_get_ccount(); // elapsed contains the number of CPU cycles since the last zero crossing - uint32_t volatile elapsed = now - zcTimestamp; + uint32_t elapsed = now - zc_timestamp; // the following avoids polling GPIO unless we are close to a zero crossing: - if ((elapsed > target) && (GPIO_INPUT_GET(zcPin) == 1)) { + if ((elapsed > target) && (GPIO_INPUT_GET(zc_pin) == 1)) { // Zero crossing has been detected. - zCount++; - p = (elapsed < 1000) ? 0 : elapsed; - uint32_t levelStep = p * transitionSpeed / 1000; - - for (int i = 0; i < dimCount; i++) { - dim_t* dim = &dims[i]; - //Reset dimmers according to their mode - // (Leading edge or trailing edge) + // Reset dimmers according to their mode, Leading edge or trailing edge + for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { if (dim->level == 0) { GPIO_OUTPUT_SET(dim->pin, (1 - dim->mode)); dim->switched = true; @@ -229,27 +204,38 @@ static void IRAM_ATTR cpu1_loop(void* parms) { GPIO_OUTPUT_SET(dim->pin, dim->mode); dim->switched = false; } - // For this cycle that starts, adjust the brightness level one step - // closer to the target brightness level. - int32_t levelDiff = dim->targetLevel - dim->level; + } + + // For this cycle that starts, adjust the brightness level one step + // closer to the target brightness level. + // tempting to combine the below loop into the above one, but we want + // to make sure pins are reset ASAP. + zc_count++; + zc_timestamp = now; + uint32_t levelStep = mains_period * transition_speed / 1000; + + for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { + int32_t levelDiff = dim->target_level - dim->level; if (levelDiff > 0) { dim->level += MIN(levelStep, levelDiff); } else if (levelDiff < 0) { dim->level -= MIN(levelStep, -levelDiff); } } - zcTimestamp = now; - // some dimmer modules keep the signal high when there is no mains input + + // some dimmer modules keep the zc signal high when there is no mains input // The following sets period to 0, to signal there is no mains input. + mains_period = (elapsed < 1000) ? 0 : elapsed; // Calibrate new target as 90% of the elapsed time. target = 9 * elapsed / 10; + } else { // Check if it is time to turn on or off any dimmed output: - for (int i = 0; i < dimCount; i++) { - if ((dims[i].switched == false) && (elapsed > dims[i].level)) { - GPIO_OUTPUT_SET(dims[i].pin, (1 - dims[i].mode)); - dims[i].switched = true; + for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { + if ((dim->switched == false) && (elapsed > dim->level)) { + GPIO_OUTPUT_SET(dim->pin, (1 - dim->mode)); + dim->switched = true; } } } @@ -257,86 +243,121 @@ static void IRAM_ATTR cpu1_loop(void* parms) { // This could happen if mains power is cut. if (elapsed > max_target) { target = 0; - p = 0; // mark period as 0 (no mains detected) + mains_period = 0; // mark period as 0 (no mains detected) } } } -static void awaitAck(lua_State* L) { - while (message.messageType != MT_IDLE) +static void await_ack(lua_State* L) { + while (message.message_type != MT_IDLE) ; + if (message.dim != NULL) { + luaM_freemem(L, message.dim, sizeof(dim_t)); + message.dim = NULL; + } check_err(L, message.err); } // pin static int dimmer_add(lua_State* L) { check_setup(L); - message.pin = luaL_checkint(L, 1); - message.mode = luaL_optint(L, 2, DIM_MODE_LEADING_EDGE); - check_err(L, gpio_set_direction(message.pin, GPIO_MODE_OUTPUT)); - gpio_set_level(message.pin, 0); - message.messageType = MT_ADD; - awaitAck(L); + int pin = luaL_checkint(L, 1); + int mode = luaL_optint(L, 2, DIM_MODE_LEADING_EDGE); + + check_err(L, gpio_set_direction(pin, GPIO_MODE_OUTPUT)); + gpio_set_level(pin, 0); + + dim_t* dim = message.dim = (dim_t*)luaM_malloc(L, sizeof(dim_t)); + dim->pin = pin; + dim->mode = mode; + dim->next = NULL; + dim->target_level = dim->level = (mode == DIM_MODE_LEADING_EDGE) ? mains_period * 2 : 0; + dim->switched = false; + + message.message_type = MT_ADD; + await_ack(L); return 0; } static int dimmer_remove(lua_State* L) { check_setup(L); + message.pin = luaL_checkint(L, 1); - message.messageType = MT_REMOVE; - awaitAck(L); + + message.message_type = MT_REMOVE; + await_ack(L); return 0; } -static int dimmer_set_level(lua_State* L) { +static int dimmer_setLevel(lua_State* L) { check_setup(L); - message.pin = luaL_checkint(L, 1); - message.targetLevel = luaL_checkint(L, 2); - message.messageType = MT_SETLEVEL; - awaitAck(L); + + int pin = luaL_checkint(L, 1); // pin to configure + int brightness = luaL_checkint(L, 2); // brightness level (0-1000) + + dim_t* dim = find(pin); + if (dim == NULL) { + luaL_error(L, "invalid pin"); + return 0; + } + + // translate brightness level 0-1000 to the number of CPU cycles signal must be on or off + // after a mains zero crossing + if (dim->mode == DIM_MODE_LEADING_EDGE) { // invert value for leading edge dimmers + brightness = 1000 - brightness; + } + if (brightness >= 1000) { + // put a big value to ensure it will never switch. (keep on at all times) + dim->target_level = mains_period << 1; + } else if (brightness <= 0) { + dim->target_level = 0; + } else { + // set target level as a per-mille of the total mains period, measured in CPU cycles. + dim->target_level = (uint32_t)((((double)brightness) / 1000.0) * ((double)mains_period)); + } return 0; } static int dimmer_mainsFrequency(lua_State* L) { int mainsF = 0; - if (p > 0) { - mainsF = (int)((esp_clk_cpu_freq() * 50.0) / p); + if (mains_period > 0) { + mainsF = (int)((esp_clk_cpu_freq() * 50.0) / mains_period); } lua_pushinteger(L, mainsF); return 1; } static int dimmer_list_debug(lua_State* L) { - ESP_LOGW(TAG, "p=%u, zcount=%d, zcTimestamp=%u, esp_freq=%d", p, zCount, zcTimestamp, esp_clk_cpu_freq()); + ESP_LOGW(TAG, "mains_period=%u, zc_count=%d, zc_timestamp=%u, esp_freq=%d", mains_period, zc_count, zc_timestamp, esp_clk_cpu_freq()); - for (int i = 0; i < dimCount; i++) { - ESP_LOGW(TAG, "pin=%d, mode=%d, level=%u", dims[i].pin, dims[i].mode, dims[i].level); + for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { + ESP_LOGW(TAG, "pin=%d, mode=%d, level=%u", dim->pin, dim->mode, dim->level); } return 0; } -// dimmer.setup(zcPin, transitionSpeed) -// zcPin: input pin connected to the zero-crossing detector -// transitionSpeed: 1-1000. Allows controlling the speed at which to change brightness. +// dimmer.setup(zc_pin, transition_speed) +// zc_pin: input pin connected to the zero-crossing detector +// transition_speed: 1-1000. Allows controlling the speed at which to change brightness. // set to 1000 to disable fading altogether (instant change). // defaults to a quick ~100ms fade. static int dimmer_setup(lua_State* L) { - if (zcPin != -1) { + if (zc_pin != -1) { return 0; } - zcPin = luaL_checkint(L, 1); - transitionSpeed = luaL_optint(L, 2, DEFAULT_TRANSITION_SPEED); + zc_pin = luaL_checkint(L, 1); + transition_speed = luaL_optint(L, 2, DEFAULT_TRANSITION_SPEED); - check_err(L, gpio_set_direction(zcPin, GPIO_MODE_INPUT)); - check_err(L, gpio_set_pull_mode(zcPin, GPIO_PULLDOWN_ONLY)); + check_err(L, gpio_set_direction(zc_pin, GPIO_MODE_INPUT)); + check_err(L, gpio_set_pull_mode(zc_pin, GPIO_PULLDOWN_ONLY)); TaskHandle_t handle; BaseType_t result = xTaskCreatePinnedToCore( cpu1_loop, - "cpu1 loop", + "dimmer", STACK_SIZE, NULL, tskIDLE_PRIORITY + 20, @@ -345,7 +366,7 @@ static int dimmer_setup(lua_State* L) { if (result != pdPASS) { luaL_error(L, "Error starting dimmer task in CPU1"); - zcPin = -1; + zc_pin = -1; } return 0; @@ -356,7 +377,7 @@ LROT_BEGIN(dimmer) LROT_FUNCENTRY(setup, dimmer_setup) LROT_FUNCENTRY(add, dimmer_add) LROT_FUNCENTRY(remove, dimmer_remove) -LROT_FUNCENTRY(setLevel, dimmer_set_level) +LROT_FUNCENTRY(setLevel, dimmer_setLevel) LROT_FUNCENTRY(list, dimmer_list_debug) LROT_FUNCENTRY(mainsFrequency, dimmer_mainsFrequency) LROT_NUMENTRY(TRAILING_EDGE, DIM_MODE_TRAILING_EDGE) From c3bb3fec5914b78703736bb9b0f0c0708bd5736c Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Wed, 29 Jan 2020 00:14:38 +0100 Subject: [PATCH 12/14] comments --- components/modules/dimmer.c | 134 ++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 51 deletions(-) diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index 24c31c6f5f..076437b801 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -8,10 +8,10 @@ // from mains by an optocoupler. These modules also come with a zero-crossing detector, // that raises a pin when the AC mains sine wave signal crosses 0V. -// Phase dimming is implemented by dedicating CPU1 entirely to this purpose... Phase dimming +// Phase dimming is implemented in this module by dedicating CPU1 entirely to this purpose... Phase dimming // requires very accurate timing. Configuring timer interrupts in the "busy" CPU0 -// does not cut it, with FreeRTOS scheduler, WiFi and so on demanding their share of the CPU -// at random times, which would make dimmed lamps flicker. +// does not work properly, with FreeRTOS scheduler, WiFi and so on demanding their share of the CPU +// at random intervals, which would make dimmed lamps flicker. // Once the dimmer module is started, by means of dimmer.setup(), a busy loop is launched // on CPU1 that monitors zero-crossing signals from the dimmer and turns on/off @@ -60,26 +60,29 @@ typedef struct dim_t { struct dim_t* next; } dim_t; +// lua interface running on CPU0 communicates with the busy loop in CPU1 by means +// of a message structure defined here: + +// message_type_t defines the two types of messages: typedef enum { - MT_IDLE = 0, - MT_ADD = 1, - MT_REMOVE = 2, - MT_SETLEVEL = 3, + MT_IDLE = 0, // indicates no message waiting to be read + MT_ADD = 1, // message contains information for a new pin to be controlled + MT_REMOVE = 2, // message indicates dimmer must stop controlling the given pin } message_type_t; typedef struct { - message_type_t message_type; - esp_err_t err; + message_type_t message_type; // type of message, as defined above + esp_err_t err; // return value set by the CPU1 busy loop after processing the message union { - int pin; - dim_t* dim; + int pin; // used for MT_REMOVE, the pin to be removed from control + dim_t* dim; // used for MT_ADD; pointer to a dim_t containing info about the pin to be controlled and how }; } dimmer_message_t; #define DIM_MODE_LEADING_EDGE 0x0 #define DIM_MODE_TRAILING_EDGE 0x1 -#define STACK_SIZE 512 +#define STACK_SIZE 512 // Stack size for the CPU1 FreeRTOS task. // Every mains cycle, the lamp brightness will gradually get closer to the desired brightness. // this avoids damaging power supplies and rushing current through the dimmer module. @@ -90,17 +93,19 @@ typedef struct { // This is a default. The user can define a specific value when calling dimmer.setup() #define DEFAULT_TRANSITION_SPEED 100 /* 1-1000 */ +// message is a shared variable among CPU0 (lua) and CPU1 (busy control loop) +// CPU0 fills the structure and CPU1 processes it static volatile dimmer_message_t message = {.message_type = MT_IDLE}; -static volatile int zc_count = 0; -static int zc_pin = -1; -static uint32_t zc_timestamp = 0; -static uint32_t mains_period = 0; -static dim_t* dims_head = NULL; -static int transition_speed; -StaticTask_t xTaskBuffer; +static int zc_pin = -1; // input pin to watch for zero crossing sync pulses +static uint32_t zc_timestamp = 0; // measured in CPU cycles, contains the last time a zero crossing was seen. +static uint32_t mains_period = 0; // estimated period of half a mains cycle, measured in CPU cycles. +static dim_t* dims_head = NULL; // linked list head to a list of dimmer-controlled pins +static int transition_speed; //configured transition speed (1-1000) +StaticTask_t xTaskBuffer; // required by FreeRTOS to allocate a static task #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) +// check_err translates a ESP error to a Lua error static void check_err(lua_State* L, esp_err_t err) { switch (err) { case ESP_ERR_INVALID_ARG: @@ -114,13 +119,16 @@ static void check_err(lua_State* L, esp_err_t err) { } } +// check_setup ensures dimmer.setup() has been called static void check_setup(lua_State* L) { if (zc_pin == -1) { luaL_error(L, "dimmer module not initialized"); } } -static dim_t* find(int pin) { +// findPin searches the dimmer-controlled list of pins and returns +// a pointer to the found structure, or NULL otherwise. +static dim_t* IRAM_ATTR findPin(int pin) { for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { if (dim->pin == pin) { return dim; @@ -129,22 +137,26 @@ static dim_t* find(int pin) { return NULL; } +/**** the following code runs in CPU1 only ****/ + +// msg_add_dimmer processes a add dimmer message, updating the linked list. static inline esp_err_t IRAM_ATTR msg_add_dimmer() { - dim_t* dim = find(message.dim->pin); + dim_t* dim = findPin(message.dim->pin); if (dim != NULL) return ESP_ERR_INVALID_ARG; message.dim->next = dims_head; dims_head = message.dim; - message.dim = NULL; + message.dim = NULL; // mark as NULL to avoid await_ack in CPU0 from freeing this memory return ESP_OK; } +// msg_remove_dimmer processes a remove dimmer message, updating the linked list static inline esp_err_t IRAM_ATTR msg_remove_dimmer() { dim_t* previous = NULL; for (dim_t* current = dims_head; current != NULL; current = current->next) { if (current->pin == message.pin) { - message.dim = current; + message.dim = current; // store the to-be-removed dim in the message so await_ack in CPU0 frees it. if (previous == NULL) { dims_head = dims_head->next; } else { @@ -158,21 +170,27 @@ static inline esp_err_t IRAM_ATTR msg_remove_dimmer() { return ESP_ERR_INVALID_ARG; } +// cpu1_loop is the entry point for the dimmer control busy loop static void IRAM_ATTR cpu1_loop(void* parms) { + // disable FreeRTOS scheduler tick timer for CPU1, since we only want this task running + // and don't want the scheduler to get in the way and make our lights flicker portDISABLE_INTERRUPTS(); ESP_INTR_DISABLE(XT_TIMER_INTNUM); portENABLE_INTERRUPTS(); + + //avoid the whatchdog to reset our chip thinking some task is hanging the CPU rtc_wdt_protect_off(); rtc_wdt_disable(); + // target specifies how many CPU cycles to wait before we poll GPIO to search for zero crossing + uint32_t target = 0; + // max_target determines a timeout waiting to register a zero crossing. It is calculated // as the max number of CPU cycles in half a mains cycle for 50Hz mains (worst case), increased by 10% for margin. uint32_t max_target = esp_clk_cpu_freq() / 50 /*Hz*/ / 2 /*half cycle*/ * 11 / 10 /* increase by 10%*/; - // target specifies how many CPU cycles to wait before we poll GPIO to search for zero crossing - uint32_t target = 0; - while (true) { + // check if there is a message waiting to be processed: if (message.message_type != MT_IDLE) { switch (message.message_type) { case MT_ADD: @@ -184,17 +202,17 @@ static void IRAM_ATTR cpu1_loop(void* parms) { default: break; } - message.message_type = MT_IDLE; + message.message_type = MT_IDLE; // this will release CPU0 waiting in await_ack() } - // now contains the current value of the CPU cycle counter + // `now` contains the current value of the CPU cycle counter uint32_t now = xthal_get_ccount(); - // elapsed contains the number of CPU cycles since the last zero crossing + // `elapsed` contains the number of CPU cycles since the last zero crossing uint32_t elapsed = now - zc_timestamp; // the following avoids polling GPIO unless we are close to a zero crossing: if ((elapsed > target) && (GPIO_INPUT_GET(zc_pin) == 1)) { - // Zero crossing has been detected. + // At this point, zero crossing has been detected. // Reset dimmers according to their mode, Leading edge or trailing edge for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { if (dim->level == 0) { @@ -210,8 +228,6 @@ static void IRAM_ATTR cpu1_loop(void* parms) { // closer to the target brightness level. // tempting to combine the below loop into the above one, but we want // to make sure pins are reset ASAP. - zc_count++; - zc_timestamp = now; uint32_t levelStep = mains_period * transition_speed / 1000; for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { @@ -223,10 +239,14 @@ static void IRAM_ATTR cpu1_loop(void* parms) { } } + // `elapsed` now contains the measured duration of the period, so store it. // some dimmer modules keep the zc signal high when there is no mains input - // The following sets period to 0, to signal there is no mains input. + // The following sets period to 0 if it is too small, to signal there is no mains input. mains_period = (elapsed < 1000) ? 0 : elapsed; + // remember when this zero crossing happened: + zc_timestamp = now; + // Calibrate new target as 90% of the elapsed time. target = 9 * elapsed / 10; @@ -248,17 +268,27 @@ static void IRAM_ATTR cpu1_loop(void* parms) { } } +/**** the following code runs in CPU0 only ****/ + +// await_ack spin locks waiting for CPU1 to mark the message as processed. static void await_ack(lua_State* L) { while (message.message_type != MT_IDLE) - ; + ; // wait until CPU1 sets the message back to MT_IDLE. + //should be a few milliseconds + if (message.dim != NULL) { + // Free dimmed pin memory in case an item was removed or we were trying to add an already + // existing pin. luaM_freemem(L, message.dim, sizeof(dim_t)); message.dim = NULL; } check_err(L, message.err); } -// pin +// lua: dimmer.add(pin, mode): Adds a pin to the dimmer module. +// pin: the GPIO pin to control. Will configure the pin as output. +// mode: (optional) dimming mode, either dimmer.LEADING_EDGE (default) or dimmer.TRAILING_EDGE +// will throw an error if the pin was already added or an incorrect GPIO pin is provided. static int dimmer_add(lua_State* L) { check_setup(L); @@ -275,29 +305,37 @@ static int dimmer_add(lua_State* L) { dim->target_level = dim->level = (mode == DIM_MODE_LEADING_EDGE) ? mains_period * 2 : 0; dim->switched = false; + // the moment message_type is set, CPU1 will process it, so set this value the last one message.message_type = MT_ADD; await_ack(L); return 0; } +// lua: dimmer.remove(pin): Removes a pin from the dimmer module's control +// pin: pin to remove +// will throw an error if the pin is not currently controlled by the module static int dimmer_remove(lua_State* L) { check_setup(L); message.pin = luaL_checkint(L, 1); + // the moment message_type is set, CPU1 will process it, so set this value the last one message.message_type = MT_REMOVE; await_ack(L); return 0; } +// lua: dimmer.setLevel(pin, brightness): changes the brightness level for a dimmed pin +// pin: pin to configure +// brightness: per-mille brightness level, 0: off, 1000 fully on. static int dimmer_setLevel(lua_State* L) { check_setup(L); - int pin = luaL_checkint(L, 1); // pin to configure - int brightness = luaL_checkint(L, 2); // brightness level (0-1000) + int pin = luaL_checkint(L, 1); + int brightness = luaL_checkint(L, 2); - dim_t* dim = find(pin); + dim_t* dim = findPin(pin); if (dim == NULL) { luaL_error(L, "invalid pin"); return 0; @@ -321,6 +359,7 @@ static int dimmer_setLevel(lua_State* L) { return 0; } +// lua: dimmer.mainsFrequency(): returns the measured mains frequency, in cHz static int dimmer_mainsFrequency(lua_State* L) { int mainsF = 0; if (mains_period > 0) { @@ -330,30 +369,24 @@ static int dimmer_mainsFrequency(lua_State* L) { return 1; } -static int dimmer_list_debug(lua_State* L) { - ESP_LOGW(TAG, "mains_period=%u, zc_count=%d, zc_timestamp=%u, esp_freq=%d", mains_period, zc_count, zc_timestamp, esp_clk_cpu_freq()); - - for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { - ESP_LOGW(TAG, "pin=%d, mode=%d, level=%u", dim->pin, dim->mode, dim->level); - } - return 0; -} - -// dimmer.setup(zc_pin, transition_speed) +// lua: dimmer.setup(zc_pin, transition_speed) : starts the dimmer module // zc_pin: input pin connected to the zero-crossing detector // transition_speed: 1-1000. Allows controlling the speed at which to change brightness. // set to 1000 to disable fading altogether (instant change). // defaults to a quick ~100ms fade. static int dimmer_setup(lua_State* L) { if (zc_pin != -1) { - return 0; + return 0; // already setup } + zc_pin = luaL_checkint(L, 1); transition_speed = luaL_optint(L, 2, DEFAULT_TRANSITION_SPEED); + // configure the zc pin as output check_err(L, gpio_set_direction(zc_pin, GPIO_MODE_INPUT)); check_err(L, gpio_set_pull_mode(zc_pin, GPIO_PULLDOWN_ONLY)); + // launch the CPU1 busy loop task TaskHandle_t handle; BaseType_t result = xTaskCreatePinnedToCore( cpu1_loop, @@ -362,7 +395,7 @@ static int dimmer_setup(lua_State* L) { NULL, tskIDLE_PRIORITY + 20, &handle, - 1); + 1); // core 1 if (result != pdPASS) { luaL_error(L, "Error starting dimmer task in CPU1"); @@ -378,7 +411,6 @@ LROT_FUNCENTRY(setup, dimmer_setup) LROT_FUNCENTRY(add, dimmer_add) LROT_FUNCENTRY(remove, dimmer_remove) LROT_FUNCENTRY(setLevel, dimmer_setLevel) -LROT_FUNCENTRY(list, dimmer_list_debug) LROT_FUNCENTRY(mainsFrequency, dimmer_mainsFrequency) LROT_NUMENTRY(TRAILING_EDGE, DIM_MODE_TRAILING_EDGE) LROT_NUMENTRY(LEADING_EDGE, DIM_MODE_LEADING_EDGE) From 5d569aadf7a44d4bfdfd44665fcda961622ca195 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Wed, 29 Jan 2020 23:51:16 +0100 Subject: [PATCH 13/14] documentation --- components/modules/dimmer.c | 10 +-- docs/modules/dimmer.md | 135 ++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 docs/modules/dimmer.md diff --git a/components/modules/dimmer.c b/components/modules/dimmer.c index 076437b801..220169fa39 100644 --- a/components/modules/dimmer.c +++ b/components/modules/dimmer.c @@ -25,7 +25,7 @@ // In your program, call dimmer.setup(zc_pin), indicating the pin input where you connected the zero crossing detector. // Use dimmer.add(pin, type) to have the module control that pin. // Type can be dimmer.LEADING_EDGE (default) or dimmer.TRAILING_EDGE, depending on the type of load/lightbulb. -// User dimmer.setLevel(pin, value) to set the desired brightness level. Value can be from 0 (off) to 1000 (fully on). +// Use dimmer.setLevel(pin, value) to set the desired brightness level. Value can be from 0 (off) to 1000 (fully on). // Use dimmer.remove(pin) to have the dimmer module stop controlling that pin. #include @@ -126,9 +126,9 @@ static void check_setup(lua_State* L) { } } -// findPin searches the dimmer-controlled list of pins and returns +// find_pin searches the dimmer-controlled list of pins and returns // a pointer to the found structure, or NULL otherwise. -static dim_t* IRAM_ATTR findPin(int pin) { +static dim_t* IRAM_ATTR find_pin(int pin) { for (dim_t* dim = dims_head; dim != NULL; dim = dim->next) { if (dim->pin == pin) { return dim; @@ -141,7 +141,7 @@ static dim_t* IRAM_ATTR findPin(int pin) { // msg_add_dimmer processes a add dimmer message, updating the linked list. static inline esp_err_t IRAM_ATTR msg_add_dimmer() { - dim_t* dim = findPin(message.dim->pin); + dim_t* dim = find_pin(message.dim->pin); if (dim != NULL) return ESP_ERR_INVALID_ARG; @@ -335,7 +335,7 @@ static int dimmer_setLevel(lua_State* L) { int pin = luaL_checkint(L, 1); int brightness = luaL_checkint(L, 2); - dim_t* dim = findPin(pin); + dim_t* dim = find_pin(pin); if (dim == NULL) { luaL_error(L, "invalid pin"); return 0; diff --git a/docs/modules/dimmer.md b/docs/modules/dimmer.md new file mode 100644 index 0000000000..74bc88d89c --- /dev/null +++ b/docs/modules/dimmer.md @@ -0,0 +1,135 @@ +# dimmer Module +| Since | Origin / Contributor | Maintainer | Source | +| :--------- | :---------------------------------------------- | :---------------------------------------------- | :-------------------------------------------- | +| 2020-01-29 | [Javier Peletier](https://github.com/jpeletier) | [Javier Peletier](https://github.com/jpeletier) | [dimmer.c](../../components/modules/dimmer.c) | + +This module implements phase-dimming for TRIAC-based mains dimmers. + +## How phase-dimming works +Find here a [brief description on how TRIAC-based dimming works](https://www.lamps-on-line.com/leading-trailing-edge-led-dimmers). + +In essence, phase dimming implies generating a square-wave PWM signal, with the special characteristic that it must be perfectly synchronized with mains, modulating it, and thus delivering more or less effective power to the load depending on the PWM signal duty cycle. + +In order to do this, a dimmer module or circuit must be used to a) detect when the mains signal crosses 0V, to use it as a synchronization point and b) modulate mains. The module must isolate mains from the microcontroller. + +Some reference hardware: +* [Schematics for a homemade module](https://hackaday.io/project/165927-a-digital-ac-dimmer-using-arduino/details) +* [Example commercial hardware](https://robotdyn.com/ac-light-dimmer-module-1-channel-3-3v-5v-logic-ac-50-60hz-220v-110v.html) + +These modules come with a TRIAC whose gate is intended to be driven by a GPIO pin, isolated from mains by an optocoupler. These modules also come with a zero-crossing detector, also isolated, that raises a pin voltage when the AC mains sine wave signal crosses 0V. + +## Architecture of this nodemcu module + +The above scheme is implemented in this module by dedicating ESP32's CPU1 entirely to this purpose... Phase dimming requires very accurate timing. Configuring timer interrupts in the "busy" CPU0 does not work properly, with FreeRTOS scheduler, WiFi and so on demanding their share of the CPU at random intervals, which would make dimmed lamps flicker. + +Once the dimmer module is started, by means of `dimmer.setup()`, a busy loop is launched on CPU1 that monitors zero-crossing signals from the dimmer and turns on/off the TRIAC at the appropriate time, with nanosecond precision. + +## Required SDK `menuconfig` changes +To use this module, change the following in menuconfig (`make menuconfig`): + +* Enable *FreeRTOS in both cores* by unselecting *"Component Config/FreeRTOS/Run FreeRTOS only on first core"*. This will allow the module to use CPU1. +* Unselect *"Component Config/ESP32-specific/Also watch CPU1 tick interrupt"* +* Unselect *"Component Config/ESP32-specific/Watch CPU1 idle task"* + +The last two settings disable the reset watchdog for CPU1. This is necessary since CPU1 is purposefully going to be running an infinite loop. + +## Example + +```lua +dimmer.setup(14) -- configure pin 14 as zero-crossing detector +dimmer.add(22) -- TRIAC gate controlling lightbulb is connected to GPIO pin 22 +dimmer.setLevel(300) -- set brightness to 300‰ (30%) +``` + + +## dimmer.setup() + +Initializes the dimmer module. This will start a task that continuously monitors for signal zero crossing and turns on/off the different dimmed pins at the right time. + +#### Syntax + +`dimmer.setup(zc_pin, [transition_speed])` + +#### Parameters + +* `zc_pin`: GPIO pin number where the zero crossing detector is connected. `dimmer.setup()` will configure this pin as input. +* `transition_speed`: integer. Specifies a transition time or fade to be applied every time a brightness level is changed. It is defined as a per mille (‰) brightness delta per half mains cycle (10ms if 50Hz). For example, if set to 20, a light would go from zero to full brightness in 1000 / 20 * 10ms = 500ms + * Value must be between 1 and 1000 (instant change). + * Defaults to a quick ~100ms fade. + +#### Returns + +Throws errors if CPU1 task cannot be started or `zc_pin` cannot be configured as input. + +It will reset if core 1 CPU is not active or their watchdogs are still enabled. See + +## dimmer.add() + +Adds the provided pin to the dimmer module. The pin will be configured as output. + +#### Syntax + +`dimmer.add(pin, [dimming_mode])` + +#### Parameters + +* `pin`: the GPIO pin to control. +* `dimming_mode`: Dimming mode, either `dimmer.LEADING_EDGE` (default) or `dimmer.TRAILING_EDGE`, depending on the type of load or lightbulb. + +#### Returns + +`dimmer.add()` will throw an error if the pin was already added or if an incorrect GPIO pin is provided. + +## dimmer.remove() + +Removes the given pin from the dimmer module + +#### Syntax + +`dimmer.remove(pin)` + +#### Parameters + +* `pin`: the GPIO pin to stop controlling. + +#### Returns + +Will throw an error if the pin is not currently controlled by the module. + +## dimmer.setLevel() + +Changes the brightness level for a dimmed pin + +#### Syntax + +`dimmer.setLevel(pin, brightness)` + +#### Parameters + +* `pin`: Pin to configure +* `brightness`: Integer. Per-mille (‰) brightness level or duty cycle, 0: off, 1000: fully on, or anything in between. + +#### Returns + +Will throw an error if attempting to configure a pin that was not added previously. + +## dimmer.mainsFrequency() + +Returns the last measured mains frequency, in cHz (100ths of Hz). You must call `dimmer.setup()` first. Note that at least half a mains cycle must have passed (10ms at 50Hz) after `dimmer.setup()` for this function to work! + +#### Syntax + +`dimmer.mainsFrequency()` + +#### Returns + +Integer with the measured mains frequency, in cHz. `0` if mains is not detected. + +#### Example + +```lua +dimmer.setup(14) -- Zero crossing detector connected to GPIO14 +tmr.create():alarm(1000, tmr.ALARM_AUTO, function() + print("Mains frequency is %d Hz", dimmer.mainsFrequency() / 100) +end) +``` diff --git a/mkdocs.yml b/mkdocs.yml index fb44555a5e..39a296b24f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,7 @@ pages: - 'can': 'modules/can.md' - 'crypto': 'modules/crypto.md' - 'dac': 'modules/dac.md' + - 'dimmer': 'modules/dimmer.md' - 'dht': 'modules/dht.md' - 'encoder': 'modules/encoder.md' - 'eth': 'modules/eth.md' From e20b879b37e0943a0a2872b13644a7079ae63930 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Mon, 3 Feb 2020 10:01:06 +0100 Subject: [PATCH 14/14] add missing reference in documentation --- docs/modules/dimmer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/dimmer.md b/docs/modules/dimmer.md index 74bc88d89c..4bd5fb84c6 100644 --- a/docs/modules/dimmer.md +++ b/docs/modules/dimmer.md @@ -61,7 +61,7 @@ Initializes the dimmer module. This will start a task that continuously monitors Throws errors if CPU1 task cannot be started or `zc_pin` cannot be configured as input. -It will reset if core 1 CPU is not active or their watchdogs are still enabled. See +It will reset if core 1 CPU is not active or their watchdogs are still enabled. See "Required SDK `menuconfig` changes" above. ## dimmer.add()