mirror of
https://github.com/arendst/Tasmota.git
synced 2025-07-26 04:06:34 +00:00
Merge remote-tracking branch 'Tasmota/development'
This commit is contained in:
commit
606f675a33
@ -62,6 +62,7 @@ The following binary downloads have been compiled with ESP8266/Arduino library c
|
|||||||
- Change define USE_TASMOTA_SLAVE into USE_TASMOTA_CLIENT
|
- Change define USE_TASMOTA_SLAVE into USE_TASMOTA_CLIENT
|
||||||
- Change commands ``SlaveSend`` and ``SlaveReset`` into ``ClientSend`` and ``ClientReset``
|
- Change commands ``SlaveSend`` and ``SlaveReset`` into ``ClientSend`` and ``ClientReset``
|
||||||
- Fix escape of non-JSON received serial data (#8329)
|
- Fix escape of non-JSON received serial data (#8329)
|
||||||
|
- Fix exception or watchdog on rule re-entry (#8757)
|
||||||
- Add command ``Rule0`` to change global rule parameters
|
- Add command ``Rule0`` to change global rule parameters
|
||||||
- Add command ``Time 4`` to display timestamp using milliseconds (#8537)
|
- Add command ``Time 4`` to display timestamp using milliseconds (#8537)
|
||||||
- Add command ``SetOption94 0/1`` to select MAX31855 or MAX6675 thermocouple support (#8616)
|
- Add command ``SetOption94 0/1`` to select MAX31855 or MAX6675 thermocouple support (#8616)
|
||||||
@ -88,3 +89,4 @@ The following binary downloads have been compiled with ESP8266/Arduino library c
|
|||||||
- Add Library to be used for decoding Teleinfo (French Metering Smart Meter)
|
- Add Library to be used for decoding Teleinfo (French Metering Smart Meter)
|
||||||
- Add basic support for ESP32 ethernet adding commands ``Wifi 0/1`` and ``Ethernet 0/1`` both default ON
|
- Add basic support for ESP32 ethernet adding commands ``Wifi 0/1`` and ``Ethernet 0/1`` both default ON
|
||||||
- Add support for single wire LMT01 temperature Sensor by justifiably (#8713)
|
- Add support for single wire LMT01 temperature Sensor by justifiably (#8713)
|
||||||
|
- Add compile time interlock parameters (#8759)
|
||||||
|
@ -36,10 +36,10 @@ default_envs =
|
|||||||
|
|
||||||
|
|
||||||
[common]
|
[common]
|
||||||
platform = ${core_active.platform}
|
platform = ${core.platform}
|
||||||
platform_packages = ${core_active.platform_packages}
|
platform_packages = ${core.platform_packages}
|
||||||
build_unflags = ${core_active.build_unflags}
|
build_unflags = ${core.build_unflags}
|
||||||
build_flags = ${core_active.build_flags}
|
build_flags = ${core.build_flags}
|
||||||
; *** Optional Debug messages
|
; *** Optional Debug messages
|
||||||
; -DDEBUG_TASMOTA_CORE
|
; -DDEBUG_TASMOTA_CORE
|
||||||
; -DDEBUG_TASMOTA_DRIVER
|
; -DDEBUG_TASMOTA_DRIVER
|
||||||
@ -71,7 +71,7 @@ extra_scripts = ${scripts_defaults.extra_scripts}
|
|||||||
; pio/strip-floats.py
|
; pio/strip-floats.py
|
||||||
; pio/http-uploader.py
|
; pio/http-uploader.py
|
||||||
|
|
||||||
[core_active]
|
[core]
|
||||||
; Activate only (one set) if you want to override the standard core defined in platformio.ini !!!
|
; Activate only (one set) if you want to override the standard core defined in platformio.ini !!!
|
||||||
|
|
||||||
;platform = ${tasmota_stage.platform}
|
;platform = ${tasmota_stage.platform}
|
||||||
@ -87,7 +87,7 @@ extra_scripts = ${scripts_defaults.extra_scripts}
|
|||||||
|
|
||||||
[tasmota_stage]
|
[tasmota_stage]
|
||||||
; *** Esp8266 core for Arduino version Tasmota stage
|
; *** Esp8266 core for Arduino version Tasmota stage
|
||||||
extends = core
|
platform = espressif8266@2.5.3
|
||||||
platform_packages = framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#52b3e5b7b3ccedcede665682f7896b637b64dbf5
|
platform_packages = framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#52b3e5b7b3ccedcede665682f7896b637b64dbf5
|
||||||
build_unflags = ${esp_defaults.build_unflags}
|
build_unflags = ${esp_defaults.build_unflags}
|
||||||
build_flags = ${esp82xx_defaults.build_flags}
|
build_flags = ${esp82xx_defaults.build_flags}
|
||||||
@ -123,7 +123,7 @@ build_flags = ${esp82xx_defaults.build_flags}
|
|||||||
|
|
||||||
[core_stage]
|
[core_stage]
|
||||||
; *** Esp8266 core for Arduino version latest development version
|
; *** Esp8266 core for Arduino version latest development version
|
||||||
extends = core
|
platform = espressif8266@2.5.3
|
||||||
platform_packages = framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git
|
platform_packages = framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git
|
||||||
build_unflags = ${esp_defaults.build_unflags}
|
build_unflags = ${esp_defaults.build_unflags}
|
||||||
build_flags = ${esp82xx_defaults.build_flags}
|
build_flags = ${esp82xx_defaults.build_flags}
|
||||||
@ -160,7 +160,6 @@ build_flags = ${esp82xx_defaults.build_flags}
|
|||||||
|
|
||||||
; *** Debug version used for PlatformIO Home Project Inspection
|
; *** Debug version used for PlatformIO Home Project Inspection
|
||||||
[env:tasmota-debug]
|
[env:tasmota-debug]
|
||||||
extends = core
|
|
||||||
build_type = debug
|
build_type = debug
|
||||||
build_unflags = ${esp_defaults.build_unflags}
|
build_unflags = ${esp_defaults.build_unflags}
|
||||||
build_flags = ${esp82xx_defaults.build_flags}
|
build_flags = ${esp82xx_defaults.build_flags}
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
- Add support for Energy sensor (Denky) for French Smart Metering meter provided by global Energy Providers, need a adaptater. See dedicated full [blog](http://hallard.me/category/tinfo/) about French teleinformation stuff
|
- Add support for Energy sensor (Denky) for French Smart Metering meter provided by global Energy Providers, need a adaptater. See dedicated full [blog](http://hallard.me/category/tinfo/) about French teleinformation stuff
|
||||||
- Add library to be used for decoding Teleinfo (French Metering Smart Meter)
|
- Add library to be used for decoding Teleinfo (French Metering Smart Meter)
|
||||||
- Add support for single wire LMT01 temperature Sensor by justifiably (#8713)
|
- Add support for single wire LMT01 temperature Sensor by justifiably (#8713)
|
||||||
|
- Add compile time interlock parameters (#8759)
|
||||||
|
- Fix exception or watchdog on rule re-entry (#8757)
|
||||||
- Change ESP32 USER GPIO template representation decreasing template message size
|
- Change ESP32 USER GPIO template representation decreasing template message size
|
||||||
- Change define USE_TASMOTA_SLAVE into USE_TASMOTA_CLIENT
|
- Change define USE_TASMOTA_SLAVE into USE_TASMOTA_CLIENT
|
||||||
- Change commands ``SlaveSend`` and ``SlaveReset`` into ``ClientSend`` and ``ClientReset``
|
- Change commands ``SlaveSend`` and ``SlaveReset`` into ``ClientSend`` and ``ClientReset``
|
||||||
|
@ -3,25 +3,26 @@
|
|||||||
supporting outputs on all pins in parallel.
|
supporting outputs on all pins in parallel.
|
||||||
|
|
||||||
Copyright (c) 2018 Earle F. Philhower, III. All rights reserved.
|
Copyright (c) 2018 Earle F. Philhower, III. All rights reserved.
|
||||||
|
Copyright (c) 2020 Dirk O. Kaar.
|
||||||
|
|
||||||
The core idea is to have a programmable waveform generator with a unique
|
The core idea is to have a programmable waveform generator with a unique
|
||||||
high and low period (defined in microseconds or CPU clock cycles). TIMER1
|
high and low period (defined in microseconds or CPU clock cycles). TIMER1 is
|
||||||
is set to 1-shot mode and is always loaded with the time until the next
|
set to 1-shot mode and is always loaded with the time until the next edge
|
||||||
edge of any live waveforms.
|
of any live waveforms.
|
||||||
|
|
||||||
Up to one waveform generator per pin supported.
|
Up to one waveform generator per pin supported.
|
||||||
|
|
||||||
Each waveform generator is synchronized to the ESP clock cycle counter, not
|
Each waveform generator is synchronized to the ESP clock cycle counter, not the
|
||||||
the timer. This allows for removing interrupt jitter and delay as the
|
timer. This allows for removing interrupt jitter and delay as the counter
|
||||||
counter always increments once per 80MHz clock. Changes to a waveform are
|
always increments once per 80MHz clock. Changes to a waveform are
|
||||||
contiguous and only take effect on the next waveform transition,
|
contiguous and only take effect on the next waveform transition,
|
||||||
allowing for smooth transitions.
|
allowing for smooth transitions.
|
||||||
|
|
||||||
This replaces older tone(), analogWrite(), and the Servo classes.
|
This replaces older tone(), analogWrite(), and the Servo classes.
|
||||||
|
|
||||||
Everywhere in the code where "cycles" is used, it means ESP.getCycleCount()
|
Everywhere in the code where "ccy" or "ccys" is used, it means ESP.getCycleCount()
|
||||||
clock cycle count, or an interval measured in CPU clock cycles, but not
|
clock cycle time, or an interval measured in clock cycles, but not TIMER1
|
||||||
TIMER1 cycles (which may be 2 CPU clock cycles @ 160MHz).
|
cycles (which may be 2 CPU clock cycles @ 160MHz).
|
||||||
|
|
||||||
This library is free software; you can redistribute it and/or
|
This library is free software; you can redistribute it and/or
|
||||||
modify it under the terms of the GNU Lesser General Public
|
modify it under the terms of the GNU Lesser General Public
|
||||||
@ -40,576 +41,400 @@
|
|||||||
|
|
||||||
#ifdef ESP8266
|
#ifdef ESP8266
|
||||||
|
|
||||||
|
#include "core_esp8266_waveform.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "ets_sys.h"
|
#include "ets_sys.h"
|
||||||
#include "core_esp8266_waveform.h"
|
#include <atomic>
|
||||||
#include "user_interface.h"
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
// Internal-only calls, not for applications
|
// Timer is 80MHz fixed. 160MHz CPU frequency need scaling.
|
||||||
extern void _setPWMFreq(uint32_t freq);
|
constexpr bool ISCPUFREQ160MHZ = clockCyclesPerMicrosecond() == 160;
|
||||||
extern bool _stopPWM(int pin);
|
// Maximum delay between IRQs, Timer1, <= 2^23 / 80MHz
|
||||||
extern bool _setPWM(int pin, uint32_t val, uint32_t range);
|
constexpr int32_t MAXIRQTICKSCCYS = microsecondsToClockCycles(10000);
|
||||||
extern int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles);
|
// Maximum servicing time for any single IRQ
|
||||||
|
constexpr uint32_t ISRTIMEOUTCCYS = microsecondsToClockCycles(18);
|
||||||
|
// The latency between in-ISR rearming of the timer and the earliest firing
|
||||||
|
constexpr int32_t IRQLATENCYCCYS = microsecondsToClockCycles(2);
|
||||||
|
// The SDK and hardware take some time to actually get to our NMI code
|
||||||
|
constexpr int32_t DELTAIRQCCYS = ISCPUFREQ160MHZ ?
|
||||||
|
microsecondsToClockCycles(2) >> 1 : microsecondsToClockCycles(2);
|
||||||
|
|
||||||
// Maximum delay between IRQs
|
// for INFINITE, the NMI proceeds on the waveform without expiry deadline.
|
||||||
#define MAXIRQUS (10000)
|
// for EXPIRES, the NMI expires the waveform automatically on the expiry ccy.
|
||||||
|
// for UPDATEEXPIRY, the NMI recomputes the exact expiry ccy and transitions to EXPIRES.
|
||||||
|
// for INIT, the NMI initializes nextPeriodCcy, and if expiryCcy != 0 includes UPDATEEXPIRY.
|
||||||
|
enum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, INIT = 3};
|
||||||
|
|
||||||
// Waveform generator can create tones, PWM, and servos
|
// Waveform generator can create tones, PWM, and servos
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t nextServiceCycle; // ESP cycle timer when a transition required
|
uint32_t nextPeriodCcy; // ESP clock cycle when a period begins. If WaveformMode::INIT, temporarily holds positive phase offset ccy count
|
||||||
uint32_t expiryCycle; // For time-limited waveform, the cycle when this waveform must stop
|
uint32_t endDutyCcy; // ESP clock cycle when going from duty to off
|
||||||
uint32_t timeHighCycles; // Actual running waveform period (adjusted using desiredCycles)
|
int32_t dutyCcys; // Set next off cycle at low->high to maintain phase
|
||||||
uint32_t timeLowCycles; //
|
int32_t adjDutyCcys; // Temporary correction for next period
|
||||||
uint32_t desiredHighCycles; // Ideal waveform period to drive the error signal
|
int32_t periodCcys; // Set next phase cycle at low->high to maintain phase
|
||||||
uint32_t desiredLowCycles; //
|
uint32_t expiryCcy; // For time-limited waveform, the CPU clock cycle when this waveform must stop. If WaveformMode::UPDATE, temporarily holds relative ccy count
|
||||||
uint32_t lastEdge; // Cycle when this generator last changed
|
WaveformMode mode;
|
||||||
|
int8_t alignPhase; // < 0 no phase alignment, otherwise starts waveform in relative phase offset to given pin
|
||||||
|
bool autoPwm; // perform PWM duty to idle cycle ratio correction under high load at the expense of precise timings
|
||||||
} Waveform;
|
} Waveform;
|
||||||
|
|
||||||
class WVFState {
|
namespace {
|
||||||
public:
|
|
||||||
Waveform waveform[17]; // State of all possible pins
|
|
||||||
uint32_t waveformState = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code
|
|
||||||
uint32_t waveformEnabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code
|
|
||||||
|
|
||||||
// Enable lock-free by only allowing updates to waveformState and waveformEnabled from IRQ service routine
|
static struct {
|
||||||
uint32_t waveformToEnable = 0; // Message to the NMI handler to start a waveform on a inactive pin
|
Waveform pins[17]; // State of all possible pins
|
||||||
uint32_t waveformToDisable = 0; // Message to the NMI handler to disable a pin from waveform generation
|
uint32_t states = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code
|
||||||
|
uint32_t enabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code
|
||||||
|
|
||||||
uint32_t waveformToChange = 0; // Mask of pin to change. One bit set in main app, cleared when effected in the NMI
|
// Enable lock-free by only allowing updates to waveform.states and waveform.enabled from IRQ service routine
|
||||||
uint32_t waveformNewHigh = 0;
|
int32_t toSetBits = 0; // Message to the NMI handler to start/modify exactly one waveform
|
||||||
uint32_t waveformNewLow = 0;
|
int32_t toDisableBits = 0; // Message to the NMI handler to disable exactly one pin from waveform generation
|
||||||
|
|
||||||
uint32_t (*timer1CB)() = NULL;
|
uint32_t(*timer1CB)() = nullptr;
|
||||||
|
|
||||||
// Optimize the NMI inner loop by keeping track of the min and max GPIO that we
|
bool timer1Running = false;
|
||||||
// are generating. In the common case (1 PWM) these may be the same pin and
|
|
||||||
// we can avoid looking at the other pins.
|
|
||||||
uint16_t startPin = 0;
|
|
||||||
uint16_t endPin = 0;
|
|
||||||
};
|
|
||||||
static WVFState wvfState;
|
|
||||||
|
|
||||||
|
uint32_t nextEventCcy;
|
||||||
|
} waveform;
|
||||||
|
|
||||||
// Ensure everything is read/written to RAM
|
}
|
||||||
#define MEMBARRIER() { __asm__ volatile("" ::: "memory"); }
|
|
||||||
|
// Interrupt on/off control
|
||||||
|
static ICACHE_RAM_ATTR void timer1Interrupt();
|
||||||
|
|
||||||
// Non-speed critical bits
|
// Non-speed critical bits
|
||||||
#pragma GCC optimize ("Os")
|
#pragma GCC optimize ("Os")
|
||||||
|
|
||||||
// Interrupt on/off control
|
static void initTimer() {
|
||||||
static ICACHE_RAM_ATTR void timer1Interrupt();
|
|
||||||
static bool timerRunning = false;
|
|
||||||
|
|
||||||
static __attribute__((noinline)) void initTimer() {
|
|
||||||
if (!timerRunning) {
|
|
||||||
timer1_disable();
|
timer1_disable();
|
||||||
ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL);
|
ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL);
|
||||||
ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt);
|
ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt);
|
||||||
timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE);
|
timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE);
|
||||||
timerRunning = true;
|
waveform.timer1Running = true;
|
||||||
timer1_write(microsecondsToClockCycles(10));
|
timer1_write(IRQLATENCYCCYS); // Cause an interrupt post-haste
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static ICACHE_RAM_ATTR void forceTimerInterrupt() {
|
static void ICACHE_RAM_ATTR deinitTimer() {
|
||||||
if (T1L > microsecondsToClockCycles(10)) {
|
|
||||||
T1L = microsecondsToClockCycles(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PWM implementation using special purpose state machine
|
|
||||||
//
|
|
||||||
// Keep an ordered list of pins with the delta in cycles between each
|
|
||||||
// element, with a terminal entry making up the remainder of the PWM
|
|
||||||
// period. With this method sum(all deltas) == PWM period clock cycles.
|
|
||||||
//
|
|
||||||
// At t=0 set all pins high and set the timeout for the 1st edge.
|
|
||||||
// On interrupt, if we're at the last element reset to t=0 state
|
|
||||||
// Otherwise, clear that pin down and set delay for next element
|
|
||||||
// and so forth.
|
|
||||||
|
|
||||||
constexpr int maxPWMs = 8;
|
|
||||||
|
|
||||||
// PWM machine state
|
|
||||||
typedef struct PWMState {
|
|
||||||
uint32_t mask; // Bitmask of active pins
|
|
||||||
uint32_t cnt; // How many entries
|
|
||||||
uint32_t idx; // Where the state machine is along the list
|
|
||||||
uint8_t pin[maxPWMs + 1];
|
|
||||||
uint32_t delta[maxPWMs + 1];
|
|
||||||
uint32_t nextServiceCycle; // Clock cycle for next step
|
|
||||||
struct PWMState *pwmUpdate; // Set by main code, cleared by ISR
|
|
||||||
} PWMState;
|
|
||||||
|
|
||||||
static PWMState pwmState;
|
|
||||||
static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / 1000;
|
|
||||||
|
|
||||||
|
|
||||||
// If there are no more scheduled activities, shut down Timer 1.
|
|
||||||
// Otherwise, do nothing.
|
|
||||||
static ICACHE_RAM_ATTR void disableIdleTimer() {
|
|
||||||
if (timerRunning && !wvfState.waveformEnabled && !pwmState.cnt && !wvfState.timer1CB) {
|
|
||||||
ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL);
|
ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL);
|
||||||
timer1_disable();
|
timer1_disable();
|
||||||
timer1_isr_init();
|
timer1_isr_init();
|
||||||
timerRunning = false;
|
waveform.timer1Running = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the NMI that a new PWM state is available through the mailbox.
|
extern "C" {
|
||||||
// Wait for mailbox to be emptied (either busy or delay() as needed)
|
|
||||||
static ICACHE_RAM_ATTR void _notifyPWM(PWMState *p, bool idle) {
|
|
||||||
p->pwmUpdate = nullptr;
|
|
||||||
pwmState.pwmUpdate = p;
|
|
||||||
MEMBARRIER();
|
|
||||||
forceTimerInterrupt();
|
|
||||||
while (pwmState.pwmUpdate) {
|
|
||||||
if (idle) {
|
|
||||||
delay(0);
|
|
||||||
}
|
|
||||||
MEMBARRIER();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range);
|
// Set a callback. Pass in NULL to stop it
|
||||||
|
void setTimer1Callback(uint32_t (*fn)()) {
|
||||||
// Called when analogWriteFreq() changed to update the PWM total period
|
waveform.timer1CB = fn;
|
||||||
void _setPWMFreq(uint32_t freq) {
|
std::atomic_thread_fence(std::memory_order_acq_rel);
|
||||||
// Convert frequency into clock cycles
|
if (!waveform.timer1Running && fn) {
|
||||||
uint32_t cc = microsecondsToClockCycles(1000000UL) / freq;
|
|
||||||
|
|
||||||
// Simple static adjustment to bring period closer to requested due to overhead
|
|
||||||
#if F_CPU == 80000000
|
|
||||||
cc -= microsecondsToClockCycles(2);
|
|
||||||
#else
|
|
||||||
cc -= microsecondsToClockCycles(1);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (cc == _pwmPeriod) {
|
|
||||||
return; // No change
|
|
||||||
}
|
|
||||||
|
|
||||||
_pwmPeriod = cc;
|
|
||||||
|
|
||||||
if (pwmState.cnt) {
|
|
||||||
PWMState p; // The working copy since we can't edit the one in use
|
|
||||||
p.cnt = 0;
|
|
||||||
for (uint32_t i = 0; i < pwmState.cnt; i++) {
|
|
||||||
auto pin = pwmState.pin[i];
|
|
||||||
_addPWMtoList(p, pin, wvfState.waveform[pin].desiredHighCycles, wvfState.waveform[pin].desiredLowCycles);
|
|
||||||
}
|
|
||||||
// Update and wait for mailbox to be emptied
|
|
||||||
initTimer();
|
initTimer();
|
||||||
_notifyPWM(&p, true);
|
} else if (waveform.timer1Running && !fn && !waveform.enabled) {
|
||||||
disableIdleTimer();
|
deinitTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper routine to remove an entry from the state machine
|
int startWaveform(uint8_t pin, uint32_t highUS, uint32_t lowUS,
|
||||||
// and clean up any marked-off entries
|
uint32_t runTimeUS, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) {
|
||||||
static void _cleanAndRemovePWM(PWMState *p, int pin) {
|
return startWaveformClockCycles(pin,
|
||||||
uint32_t leftover = 0;
|
microsecondsToClockCycles(highUS), microsecondsToClockCycles(lowUS),
|
||||||
uint32_t in, out;
|
microsecondsToClockCycles(runTimeUS), alignPhase, microsecondsToClockCycles(phaseOffsetUS), autoPwm);
|
||||||
for (in = 0, out = 0; in < p->cnt; in++) {
|
|
||||||
if ((p->pin[in] != pin) && (p->mask & (1<<p->pin[in]))) {
|
|
||||||
p->pin[out] = p->pin[in];
|
|
||||||
p->delta[out] = p->delta[in] + leftover;
|
|
||||||
leftover = 0;
|
|
||||||
out++;
|
|
||||||
} else {
|
|
||||||
leftover += p->delta[in];
|
|
||||||
p->mask &= ~(1<<p->pin[in]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p->cnt = out;
|
|
||||||
// Final pin is never used: p->pin[out] = 0xff;
|
|
||||||
p->delta[out] = p->delta[in] + leftover;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Disable PWM on a specific pin (i.e. when a digitalWrite or analogWrite(0%/100%))
|
|
||||||
ICACHE_RAM_ATTR bool _stopPWM(int pin) {
|
|
||||||
if (!((1<<pin) & pwmState.mask)) {
|
|
||||||
return false; // Pin not actually active
|
|
||||||
}
|
|
||||||
|
|
||||||
PWMState p; // The working copy since we can't edit the one in use
|
|
||||||
p = pwmState;
|
|
||||||
|
|
||||||
// In _stopPWM we just clear the mask but keep everything else
|
|
||||||
// untouched to save IRAM. The main startPWM will handle cleanup.
|
|
||||||
p.mask &= ~(1<<pin);
|
|
||||||
if (!p.mask) {
|
|
||||||
// If all have been stopped, then turn PWM off completely
|
|
||||||
p.cnt = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update and wait for mailbox to be emptied, no delay (could be in ISR)
|
|
||||||
_notifyPWM(&p, false);
|
|
||||||
// Possibly shut down the timer completely if we're done
|
|
||||||
disableIdleTimer();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range) {
|
|
||||||
// Stash the val and range so we can re-evaluate the fraction
|
|
||||||
// should the user change PWM frequency. This allows us to
|
|
||||||
// give as great a precision as possible. We know by construction
|
|
||||||
// that the waveform for this pin will be inactive so we can borrow
|
|
||||||
// memory from that structure.
|
|
||||||
wvfState.waveform[pin].desiredHighCycles = val; // Numerator == high
|
|
||||||
wvfState.waveform[pin].desiredLowCycles = range; // Denominator == low
|
|
||||||
|
|
||||||
uint32_t cc = (_pwmPeriod * val) / range;
|
|
||||||
|
|
||||||
if (cc == 0) {
|
|
||||||
_stopPWM(pin);
|
|
||||||
digitalWrite(pin, LOW);
|
|
||||||
return;
|
|
||||||
} else if (cc >= _pwmPeriod) {
|
|
||||||
_stopPWM(pin);
|
|
||||||
digitalWrite(pin, HIGH);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.cnt == 0) {
|
|
||||||
// Starting up from scratch, special case 1st element and PWM period
|
|
||||||
p.pin[0] = pin;
|
|
||||||
p.delta[0] = cc;
|
|
||||||
// Final pin is never used: p.pin[1] = 0xff;
|
|
||||||
p.delta[1] = _pwmPeriod - cc;
|
|
||||||
} else {
|
|
||||||
uint32_t ttl = 0;
|
|
||||||
uint32_t i;
|
|
||||||
// Skip along until we're at the spot to insert
|
|
||||||
for (i=0; (i <= p.cnt) && (ttl + p.delta[i] < cc); i++) {
|
|
||||||
ttl += p.delta[i];
|
|
||||||
}
|
|
||||||
// Shift everything out by one to make space for new edge
|
|
||||||
for (int32_t j = p.cnt; j >= (int)i; j--) {
|
|
||||||
p.pin[j + 1] = p.pin[j];
|
|
||||||
p.delta[j + 1] = p.delta[j];
|
|
||||||
}
|
|
||||||
int off = cc - ttl; // The delta from the last edge to the one we're inserting
|
|
||||||
p.pin[i] = pin;
|
|
||||||
p.delta[i] = off; // Add the delta to this new pin
|
|
||||||
p.delta[i + 1] -= off; // And subtract it from the follower to keep sum(deltas) constant
|
|
||||||
}
|
|
||||||
p.cnt++;
|
|
||||||
p.mask |= 1<<pin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called by analogWrite(1...99%) to set the PWM duty in clock cycles
|
|
||||||
bool _setPWM(int pin, uint32_t val, uint32_t range) {
|
|
||||||
stopWaveform(pin);
|
|
||||||
PWMState p; // Working copy
|
|
||||||
p = pwmState;
|
|
||||||
// Get rid of any entries for this pin
|
|
||||||
_cleanAndRemovePWM(&p, pin);
|
|
||||||
// And add it to the list, in order
|
|
||||||
if (p.cnt >= maxPWMs) {
|
|
||||||
return false; // No space left
|
|
||||||
}
|
|
||||||
|
|
||||||
_addPWMtoList(p, pin, val, range);
|
|
||||||
|
|
||||||
// Set mailbox and wait for ISR to copy it over
|
|
||||||
initTimer();
|
|
||||||
_notifyPWM(&p, true);
|
|
||||||
disableIdleTimer();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start up a waveform on a pin, or change the current one. Will change to the new
|
// Start up a waveform on a pin, or change the current one. Will change to the new
|
||||||
// waveform smoothly on next low->high transition. For immediate change, stopWaveform()
|
// waveform smoothly on next low->high transition. For immediate change, stopWaveform()
|
||||||
// first, then it will immediately begin.
|
// first, then it will immediately begin.
|
||||||
int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS) {
|
int startWaveformClockCycles(uint8_t pin, uint32_t highCcys, uint32_t lowCcys,
|
||||||
return startWaveformClockCycles(pin, microsecondsToClockCycles(timeHighUS), microsecondsToClockCycles(timeLowUS), microsecondsToClockCycles(runTimeUS));
|
uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm) {
|
||||||
}
|
uint32_t periodCcys = highCcys + lowCcys;
|
||||||
|
if (periodCcys < MAXIRQTICKSCCYS) {
|
||||||
int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles) {
|
if (!highCcys) {
|
||||||
if ((pin > 16) || isFlashInterfacePin(pin)) {
|
periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys;
|
||||||
|
}
|
||||||
|
else if (!lowCcys) {
|
||||||
|
highCcys = periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sanity checks, including mixed signed/unsigned arithmetic safety
|
||||||
|
if ((pin > 16) || isFlashInterfacePin(pin) || (alignPhase > 16) ||
|
||||||
|
static_cast<int32_t>(periodCcys) <= 0 ||
|
||||||
|
static_cast<int32_t>(highCcys) < 0 || static_cast<int32_t>(lowCcys) < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Waveform *wave = &wvfState.waveform[pin];
|
Waveform& wave = waveform.pins[pin];
|
||||||
wave->expiryCycle = runTimeCycles ? ESP.getCycleCount() + runTimeCycles : 0;
|
wave.dutyCcys = highCcys;
|
||||||
if (runTimeCycles && !wave->expiryCycle) {
|
wave.adjDutyCcys = 0;
|
||||||
wave->expiryCycle = 1; // expiryCycle==0 means no timeout, so avoid setting it
|
wave.periodCcys = periodCcys;
|
||||||
}
|
wave.autoPwm = autoPwm;
|
||||||
|
|
||||||
_stopPWM(pin); // Make sure there's no PWM live here
|
std::atomic_thread_fence(std::memory_order_acquire);
|
||||||
|
const uint32_t pinBit = 1UL << pin;
|
||||||
uint32_t mask = 1<<pin;
|
if (!(waveform.enabled & pinBit)) {
|
||||||
MEMBARRIER();
|
// wave.nextPeriodCcy and wave.endDutyCcy are initialized by the ISR
|
||||||
if (wvfState.waveformEnabled & mask) {
|
wave.nextPeriodCcy = phaseOffsetCcys;
|
||||||
// Make sure no waveform changes are waiting to be applied
|
wave.expiryCcy = runTimeCcys; // in WaveformMode::INIT, temporarily hold relative cycle count
|
||||||
while (wvfState.waveformToChange) {
|
wave.mode = WaveformMode::INIT;
|
||||||
delay(0); // Wait for waveform to update
|
wave.alignPhase = (alignPhase < 0) ? -1 : alignPhase;
|
||||||
// No mem barrier here, the call to a global function implies global state updated
|
if (!wave.dutyCcys) {
|
||||||
|
// If initially at zero duty cycle, force GPIO off
|
||||||
|
if (pin == 16) {
|
||||||
|
GP16O = 0;
|
||||||
}
|
}
|
||||||
wvfState.waveformNewHigh = timeHighCycles;
|
else {
|
||||||
wvfState.waveformNewLow = timeLowCycles;
|
GPOC = pinBit;
|
||||||
MEMBARRIER();
|
}
|
||||||
wvfState.waveformToChange = mask;
|
}
|
||||||
// The waveform will be updated some time in the future on the next period for the signal
|
std::atomic_thread_fence(std::memory_order_release);
|
||||||
} else { // if (!(wvfState.waveformEnabled & mask)) {
|
waveform.toSetBits = 1UL << pin;
|
||||||
wave->timeHighCycles = timeHighCycles;
|
std::atomic_thread_fence(std::memory_order_release);
|
||||||
wave->desiredHighCycles = timeHighCycles;
|
if (!waveform.timer1Running) {
|
||||||
wave->timeLowCycles = timeLowCycles;
|
|
||||||
wave->desiredLowCycles = timeLowCycles;
|
|
||||||
wave->lastEdge = 0;
|
|
||||||
wave->nextServiceCycle = ESP.getCycleCount() + microsecondsToClockCycles(1);
|
|
||||||
wvfState.waveformToEnable |= mask;
|
|
||||||
MEMBARRIER();
|
|
||||||
initTimer();
|
initTimer();
|
||||||
forceTimerInterrupt();
|
}
|
||||||
while (wvfState.waveformToEnable) {
|
else if (T1V > IRQLATENCYCCYS) {
|
||||||
|
// Must not interfere if Timer is due shortly
|
||||||
|
timer1_write(IRQLATENCYCCYS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI
|
||||||
|
std::atomic_thread_fence(std::memory_order_release);
|
||||||
|
wave.expiryCcy = runTimeCcys; // in WaveformMode::UPDATEEXPIRY, temporarily hold relative cycle count
|
||||||
|
if (runTimeCcys) {
|
||||||
|
wave.mode = WaveformMode::UPDATEEXPIRY;
|
||||||
|
std::atomic_thread_fence(std::memory_order_release);
|
||||||
|
waveform.toSetBits = 1UL << pin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::atomic_thread_fence(std::memory_order_acq_rel);
|
||||||
|
while (waveform.toSetBits) {
|
||||||
delay(0); // Wait for waveform to update
|
delay(0); // Wait for waveform to update
|
||||||
// No mem barrier here, the call to a global function implies global state updated
|
std::atomic_thread_fence(std::memory_order_acquire);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Set a callback. Pass in NULL to stop it
|
|
||||||
void setTimer1Callback(uint32_t (*fn)()) {
|
|
||||||
wvfState.timer1CB = fn;
|
|
||||||
if (fn) {
|
|
||||||
initTimer();
|
|
||||||
forceTimerInterrupt();
|
|
||||||
}
|
|
||||||
disableIdleTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Speed critical bits
|
|
||||||
#pragma GCC optimize ("O2")
|
|
||||||
|
|
||||||
// Normally would not want two copies like this, but due to different
|
|
||||||
// optimization levels the inline attribute gets lost if we try the
|
|
||||||
// other version.
|
|
||||||
static inline ICACHE_RAM_ATTR uint32_t GetCycleCountIRQ() {
|
|
||||||
uint32_t ccount;
|
|
||||||
__asm__ __volatile__("rsr %0,ccount":"=a"(ccount));
|
|
||||||
return ccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline ICACHE_RAM_ATTR uint32_t min_u32(uint32_t a, uint32_t b) {
|
|
||||||
if (a < b) {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stops a waveform on a pin
|
// Stops a waveform on a pin
|
||||||
int ICACHE_RAM_ATTR stopWaveform(uint8_t pin) {
|
int ICACHE_RAM_ATTR stopWaveform(uint8_t pin) {
|
||||||
// Can't possibly need to stop anything if there is no timer active
|
// Can't possibly need to stop anything if there is no timer active
|
||||||
if (!timerRunning) {
|
if (!waveform.timer1Running) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// If user sends in a pin >16 but <32, this will always point to a 0 bit
|
// If user sends in a pin >16 but <32, this will always point to a 0 bit
|
||||||
// If they send >=32, then the shift will result in 0 and it will also return false
|
// If they send >=32, then the shift will result in 0 and it will also return false
|
||||||
uint32_t mask = 1<<pin;
|
std::atomic_thread_fence(std::memory_order_acquire);
|
||||||
if (wvfState.waveformEnabled & mask) {
|
const uint32_t pinBit = 1UL << pin;
|
||||||
wvfState.waveformToDisable = mask;
|
if (waveform.enabled & pinBit) {
|
||||||
// Cancel any pending updates for this waveform, too.
|
waveform.toDisableBits = 1UL << pin;
|
||||||
if (wvfState.waveformToChange & mask) {
|
std::atomic_thread_fence(std::memory_order_release);
|
||||||
wvfState.waveformToChange = 0;
|
// Must not interfere if Timer is due shortly
|
||||||
|
if (T1V > IRQLATENCYCCYS) {
|
||||||
|
timer1_write(IRQLATENCYCCYS);
|
||||||
}
|
}
|
||||||
forceTimerInterrupt();
|
while (waveform.toDisableBits) {
|
||||||
while (wvfState.waveformToDisable) {
|
|
||||||
MEMBARRIER(); // If it wasn't written yet, it has to be by now
|
|
||||||
/* no-op */ // Can't delay() since stopWaveform may be called from an IRQ
|
/* no-op */ // Can't delay() since stopWaveform may be called from an IRQ
|
||||||
|
std::atomic_thread_fence(std::memory_order_acquire);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disableIdleTimer();
|
if (!waveform.enabled && !waveform.timer1CB) {
|
||||||
|
deinitTimer();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The SDK and hardware take some time to actually get to our NMI code, so
|
|
||||||
// decrement the next IRQ's timer value by a bit so we can actually catch the
|
|
||||||
// real CPU cycle counter we want for the waveforms.
|
|
||||||
|
|
||||||
// The SDK also sometimes is running at a different speed the the Arduino core
|
|
||||||
// so the ESP cycle counter is actually running at a variable speed.
|
|
||||||
// adjust(x) takes care of adjusting a delta clock cycle amount accordingly.
|
|
||||||
#if F_CPU == 80000000
|
|
||||||
#define DELTAIRQ (microsecondsToClockCycles(3))
|
|
||||||
#define adjust(x) ((x) << (turbo ? 1 : 0))
|
|
||||||
#else
|
|
||||||
#define DELTAIRQ (microsecondsToClockCycles(2))
|
|
||||||
#define adjust(x) ((x) >> (turbo ? 0 : 1))
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
static ICACHE_RAM_ATTR void timer1Interrupt() {
|
|
||||||
// Flag if the core is at 160 MHz, for use by adjust()
|
|
||||||
bool turbo = (*(uint32_t*)0x3FF00014) & 1 ? true : false;
|
|
||||||
|
|
||||||
uint32_t nextEventCycles = microsecondsToClockCycles(MAXIRQUS);
|
|
||||||
uint32_t timeoutCycle = GetCycleCountIRQ() + microsecondsToClockCycles(14);
|
|
||||||
|
|
||||||
if (wvfState.waveformToEnable || wvfState.waveformToDisable) {
|
|
||||||
// Handle enable/disable requests from main app
|
|
||||||
wvfState.waveformEnabled = (wvfState.waveformEnabled & ~wvfState.waveformToDisable) | wvfState.waveformToEnable; // Set the requested waveforms on/off
|
|
||||||
wvfState.waveformState &= ~wvfState.waveformToEnable; // And clear the state of any just started
|
|
||||||
wvfState.waveformToEnable = 0;
|
|
||||||
wvfState.waveformToDisable = 0;
|
|
||||||
// No mem barrier. Globals must be written to RAM on ISR exit.
|
|
||||||
// Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t)
|
|
||||||
wvfState.startPin = __builtin_ffs(wvfState.waveformEnabled) - 1;
|
|
||||||
// Find the last bit by subtracting off GCC's count-leading-zeros (no offset in this one)
|
|
||||||
wvfState.endPin = 32 - __builtin_clz(wvfState.waveformEnabled);
|
|
||||||
} else if (!pwmState.cnt && pwmState.pwmUpdate) {
|
|
||||||
// Start up the PWM generator by copying from the mailbox
|
|
||||||
pwmState.cnt = 1;
|
|
||||||
pwmState.idx = 1; // Ensure copy this cycle, cause it to start at t=0
|
|
||||||
pwmState.nextServiceCycle = GetCycleCountIRQ(); // Do it this loop!
|
|
||||||
// No need for mem barrier here. Global must be written by IRQ exit
|
|
||||||
}
|
|
||||||
|
|
||||||
bool done = false;
|
|
||||||
if (wvfState.waveformEnabled || pwmState.cnt) {
|
|
||||||
do {
|
|
||||||
nextEventCycles = microsecondsToClockCycles(MAXIRQUS);
|
|
||||||
|
|
||||||
// PWM state machine implementation
|
|
||||||
if (pwmState.cnt) {
|
|
||||||
int32_t cyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ();
|
|
||||||
if (cyclesToGo < 0) {
|
|
||||||
if (pwmState.idx == pwmState.cnt) { // Start of pulses, possibly copy new
|
|
||||||
if (pwmState.pwmUpdate) {
|
|
||||||
// Do the memory copy from temp to global and clear mailbox
|
|
||||||
pwmState = *(PWMState*)pwmState.pwmUpdate;
|
|
||||||
}
|
|
||||||
GPOS = pwmState.mask; // Set all active pins high
|
|
||||||
if (pwmState.mask & (1<<16)) {
|
|
||||||
GP16O = 1;
|
|
||||||
}
|
|
||||||
pwmState.idx = 0;
|
|
||||||
} else {
|
|
||||||
do {
|
|
||||||
// Drop the pin at this edge
|
|
||||||
if (pwmState.mask & (1<<pwmState.pin[pwmState.idx])) {
|
|
||||||
GPOC = 1<<pwmState.pin[pwmState.idx];
|
|
||||||
if (pwmState.pin[pwmState.idx] == 16) {
|
|
||||||
GP16O = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pwmState.idx++;
|
|
||||||
// Any other pins at this same PWM value will have delta==0, drop them too.
|
|
||||||
} while (pwmState.delta[pwmState.idx] == 0);
|
|
||||||
}
|
|
||||||
// Preserve duty cycle over PWM period by using now+xxx instead of += delta
|
|
||||||
cyclesToGo = adjust(pwmState.delta[pwmState.idx]);
|
|
||||||
pwmState.nextServiceCycle = GetCycleCountIRQ() + cyclesToGo;
|
|
||||||
}
|
|
||||||
nextEventCycles = min_u32(nextEventCycles, cyclesToGo);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto i = wvfState.startPin; i <= wvfState.endPin; i++) {
|
|
||||||
uint32_t mask = 1<<i;
|
|
||||||
|
|
||||||
// If it's not on, ignore!
|
|
||||||
if (!(wvfState.waveformEnabled & mask)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Waveform *wave = &wvfState.waveform[i];
|
|
||||||
uint32_t now = GetCycleCountIRQ();
|
|
||||||
|
|
||||||
// Disable any waveforms that are done
|
|
||||||
if (wave->expiryCycle) {
|
|
||||||
int32_t expiryToGo = wave->expiryCycle - now;
|
|
||||||
if (expiryToGo < 0) {
|
|
||||||
// Done, remove!
|
|
||||||
if (i == 16) {
|
|
||||||
GP16O = 0;
|
|
||||||
}
|
|
||||||
GPOC = mask;
|
|
||||||
wvfState.waveformEnabled &= ~mask;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for toggles
|
|
||||||
int32_t cyclesToGo = wave->nextServiceCycle - now;
|
|
||||||
if (cyclesToGo < 0) {
|
|
||||||
uint32_t nextEdgeCycles;
|
|
||||||
uint32_t desired = 0;
|
|
||||||
uint32_t *timeToUpdate;
|
|
||||||
wvfState.waveformState ^= mask;
|
|
||||||
if (wvfState.waveformState & mask) {
|
|
||||||
if (i == 16) {
|
|
||||||
GP16O = 1;
|
|
||||||
}
|
|
||||||
GPOS = mask;
|
|
||||||
|
|
||||||
if (wvfState.waveformToChange & mask) {
|
|
||||||
// Copy over next full-cycle timings
|
|
||||||
wave->timeHighCycles = wvfState.waveformNewHigh;
|
|
||||||
wave->desiredHighCycles = wvfState.waveformNewHigh;
|
|
||||||
wave->timeLowCycles = wvfState.waveformNewLow;
|
|
||||||
wave->desiredLowCycles = wvfState.waveformNewLow;
|
|
||||||
wave->lastEdge = 0;
|
|
||||||
wvfState.waveformToChange = 0;
|
|
||||||
}
|
|
||||||
if (wave->lastEdge) {
|
|
||||||
desired = wave->desiredLowCycles;
|
|
||||||
timeToUpdate = &wave->timeLowCycles;
|
|
||||||
}
|
|
||||||
nextEdgeCycles = wave->timeHighCycles;
|
|
||||||
} else {
|
|
||||||
if (i == 16) {
|
|
||||||
GP16O = 0;
|
|
||||||
}
|
|
||||||
GPOC = mask;
|
|
||||||
desired = wave->desiredHighCycles;
|
|
||||||
timeToUpdate = &wave->timeHighCycles;
|
|
||||||
nextEdgeCycles = wave->timeLowCycles;
|
|
||||||
}
|
|
||||||
if (desired) {
|
|
||||||
desired = adjust(desired);
|
|
||||||
int32_t err = desired - (now - wave->lastEdge);
|
|
||||||
if (abs(err) < desired) { // If we've lost > the entire phase, ignore this error signal
|
|
||||||
err /= 2;
|
|
||||||
*timeToUpdate += err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nextEdgeCycles = adjust(nextEdgeCycles);
|
|
||||||
wave->nextServiceCycle = now + nextEdgeCycles;
|
|
||||||
nextEventCycles = min_u32(nextEventCycles, nextEdgeCycles);
|
|
||||||
wave->lastEdge = now;
|
|
||||||
} else {
|
|
||||||
uint32_t deltaCycles = wave->nextServiceCycle - now;
|
|
||||||
nextEventCycles = min_u32(nextEventCycles, deltaCycles);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit the loop if we've hit the fixed runtime limit or the next event is known to be after that timeout would occur
|
|
||||||
uint32_t now = GetCycleCountIRQ();
|
|
||||||
int32_t cycleDeltaNextEvent = timeoutCycle - (now + nextEventCycles);
|
|
||||||
int32_t cyclesLeftTimeout = timeoutCycle - now;
|
|
||||||
done = (cycleDeltaNextEvent < 0) || (cyclesLeftTimeout < 0);
|
|
||||||
} while (!done);
|
|
||||||
} // if (wvfState.waveformEnabled)
|
|
||||||
|
|
||||||
if (wvfState.timer1CB) {
|
|
||||||
nextEventCycles = min_u32(nextEventCycles, wvfState.timer1CB());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextEventCycles < microsecondsToClockCycles(5)) {
|
|
||||||
nextEventCycles = microsecondsToClockCycles(5);
|
|
||||||
}
|
|
||||||
nextEventCycles -= DELTAIRQ;
|
|
||||||
|
|
||||||
// Do it here instead of global function to save time and because we know it's edge-IRQ
|
|
||||||
T1L = nextEventCycles >> (turbo ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Speed critical bits
|
||||||
|
#pragma GCC optimize ("O2")
|
||||||
|
|
||||||
|
// For dynamic CPU clock frequency switch in loop the scaling logic would have to be adapted.
|
||||||
|
// Using constexpr makes sure that the CPU clock frequency is compile-time fixed.
|
||||||
|
static inline ICACHE_RAM_ATTR int32_t scaleCcys(const int32_t ccys, const bool isCPU2X) {
|
||||||
|
if (ISCPUFREQ160MHZ) {
|
||||||
|
return isCPU2X ? ccys : (ccys >> 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return isCPU2X ? (ccys << 1) : ccys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ICACHE_RAM_ATTR void timer1Interrupt() {
|
||||||
|
const uint32_t isrStartCcy = ESP.getCycleCount();
|
||||||
|
int32_t clockDrift = isrStartCcy - waveform.nextEventCcy;
|
||||||
|
const bool isCPU2X = CPU2X & 1;
|
||||||
|
if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) {
|
||||||
|
// Handle enable/disable requests from main app.
|
||||||
|
waveform.enabled = (waveform.enabled & ~waveform.toDisableBits) | waveform.toSetBits; // Set the requested waveforms on/off
|
||||||
|
// Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t)
|
||||||
|
waveform.toDisableBits = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waveform.toSetBits) {
|
||||||
|
const int toSetPin = __builtin_ffs(waveform.toSetBits) - 1;
|
||||||
|
Waveform& wave = waveform.pins[toSetPin];
|
||||||
|
switch (wave.mode) {
|
||||||
|
case WaveformMode::INIT:
|
||||||
|
waveform.states &= ~waveform.toSetBits; // Clear the state of any just started
|
||||||
|
if (wave.alignPhase >= 0 && waveform.enabled & (1UL << wave.alignPhase)) {
|
||||||
|
wave.nextPeriodCcy = waveform.pins[wave.alignPhase].nextPeriodCcy + wave.nextPeriodCcy;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
wave.nextPeriodCcy = waveform.nextEventCcy;
|
||||||
|
}
|
||||||
|
if (!wave.expiryCcy) {
|
||||||
|
wave.mode = WaveformMode::INFINITE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// fall through
|
||||||
|
case WaveformMode::UPDATEEXPIRY:
|
||||||
|
// in WaveformMode::UPDATEEXPIRY, expiryCcy temporarily holds relative CPU cycle count
|
||||||
|
wave.expiryCcy = wave.nextPeriodCcy + scaleCcys(wave.expiryCcy, isCPU2X);
|
||||||
|
wave.mode = WaveformMode::EXPIRES;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
waveform.toSetBits = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit the loop if the next event, if any, is sufficiently distant.
|
||||||
|
const uint32_t isrTimeoutCcy = isrStartCcy + ISRTIMEOUTCCYS;
|
||||||
|
uint32_t busyPins = waveform.enabled;
|
||||||
|
waveform.nextEventCcy = isrStartCcy + MAXIRQTICKSCCYS;
|
||||||
|
|
||||||
|
uint32_t now = ESP.getCycleCount();
|
||||||
|
uint32_t isrNextEventCcy = now;
|
||||||
|
while (busyPins) {
|
||||||
|
if (static_cast<int32_t>(isrNextEventCcy - now) > IRQLATENCYCCYS) {
|
||||||
|
waveform.nextEventCcy = isrNextEventCcy;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
isrNextEventCcy = waveform.nextEventCcy;
|
||||||
|
uint32_t loopPins = busyPins;
|
||||||
|
while (loopPins) {
|
||||||
|
const int pin = __builtin_ffsl(loopPins) - 1;
|
||||||
|
const uint32_t pinBit = 1UL << pin;
|
||||||
|
loopPins ^= pinBit;
|
||||||
|
|
||||||
|
Waveform& wave = waveform.pins[pin];
|
||||||
|
|
||||||
|
if (clockDrift) {
|
||||||
|
wave.endDutyCcy += clockDrift;
|
||||||
|
wave.nextPeriodCcy += clockDrift;
|
||||||
|
wave.expiryCcy += clockDrift;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t waveNextEventCcy = (waveform.states & pinBit) ? wave.endDutyCcy : wave.nextPeriodCcy;
|
||||||
|
if (WaveformMode::EXPIRES == wave.mode &&
|
||||||
|
static_cast<int32_t>(waveNextEventCcy - wave.expiryCcy) >= 0 &&
|
||||||
|
static_cast<int32_t>(now - wave.expiryCcy) >= 0) {
|
||||||
|
// Disable any waveforms that are done
|
||||||
|
waveform.enabled ^= pinBit;
|
||||||
|
busyPins ^= pinBit;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const int32_t overshootCcys = now - waveNextEventCcy;
|
||||||
|
if (overshootCcys >= 0) {
|
||||||
|
const int32_t periodCcys = scaleCcys(wave.periodCcys, isCPU2X);
|
||||||
|
if (waveform.states & pinBit) {
|
||||||
|
// active configuration and forward are 100% duty
|
||||||
|
if (wave.periodCcys == wave.dutyCcys) {
|
||||||
|
wave.nextPeriodCcy += periodCcys;
|
||||||
|
wave.endDutyCcy = wave.nextPeriodCcy;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (wave.autoPwm) {
|
||||||
|
wave.adjDutyCcys += overshootCcys;
|
||||||
|
}
|
||||||
|
waveform.states ^= pinBit;
|
||||||
|
if (16 == pin) {
|
||||||
|
GP16O = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
GPOC = pinBit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
waveNextEventCcy = wave.nextPeriodCcy;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
wave.nextPeriodCcy += periodCcys;
|
||||||
|
if (!wave.dutyCcys) {
|
||||||
|
wave.endDutyCcy = wave.nextPeriodCcy;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int32_t dutyCcys = scaleCcys(wave.dutyCcys, isCPU2X);
|
||||||
|
if (dutyCcys <= wave.adjDutyCcys) {
|
||||||
|
dutyCcys >>= 1;
|
||||||
|
wave.adjDutyCcys -= dutyCcys;
|
||||||
|
}
|
||||||
|
else if (wave.adjDutyCcys) {
|
||||||
|
dutyCcys -= wave.adjDutyCcys;
|
||||||
|
wave.adjDutyCcys = 0;
|
||||||
|
}
|
||||||
|
wave.endDutyCcy = now + dutyCcys;
|
||||||
|
if (static_cast<int32_t>(wave.endDutyCcy - wave.nextPeriodCcy) > 0) {
|
||||||
|
wave.endDutyCcy = wave.nextPeriodCcy;
|
||||||
|
}
|
||||||
|
waveform.states |= pinBit;
|
||||||
|
if (16 == pin) {
|
||||||
|
GP16O = 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
GPOS = pinBit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
waveNextEventCcy = wave.endDutyCcy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WaveformMode::EXPIRES == wave.mode && static_cast<int32_t>(waveNextEventCcy - wave.expiryCcy) > 0) {
|
||||||
|
waveNextEventCcy = wave.expiryCcy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static_cast<int32_t>(waveNextEventCcy - isrTimeoutCcy) >= 0) {
|
||||||
|
busyPins ^= pinBit;
|
||||||
|
if (static_cast<int32_t>(waveform.nextEventCcy - waveNextEventCcy) > 0) {
|
||||||
|
waveform.nextEventCcy = waveNextEventCcy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (static_cast<int32_t>(isrNextEventCcy - waveNextEventCcy) > 0) {
|
||||||
|
isrNextEventCcy = waveNextEventCcy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
now = ESP.getCycleCount();
|
||||||
|
}
|
||||||
|
clockDrift = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t callbackCcys = 0;
|
||||||
|
if (waveform.timer1CB) {
|
||||||
|
callbackCcys = scaleCcys(microsecondsToClockCycles(waveform.timer1CB()), isCPU2X);
|
||||||
|
}
|
||||||
|
now = ESP.getCycleCount();
|
||||||
|
int32_t nextEventCcys = waveform.nextEventCcy - now;
|
||||||
|
// Account for unknown duration of timer1CB().
|
||||||
|
if (waveform.timer1CB && nextEventCcys > callbackCcys) {
|
||||||
|
waveform.nextEventCcy = now + callbackCcys;
|
||||||
|
nextEventCcys = callbackCcys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer is 80MHz fixed. 160MHz CPU frequency need scaling.
|
||||||
|
int32_t deltaIrqCcys = DELTAIRQCCYS;
|
||||||
|
int32_t irqLatencyCcys = IRQLATENCYCCYS;
|
||||||
|
if (isCPU2X) {
|
||||||
|
nextEventCcys >>= 1;
|
||||||
|
deltaIrqCcys >>= 1;
|
||||||
|
irqLatencyCcys >>= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firing timer too soon, the NMI occurs before ISR has returned.
|
||||||
|
if (nextEventCcys < irqLatencyCcys + deltaIrqCcys) {
|
||||||
|
waveform.nextEventCcy = now + IRQLATENCYCCYS + DELTAIRQCCYS;
|
||||||
|
nextEventCcys = irqLatencyCcys;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nextEventCcys -= deltaIrqCcys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register access is fast and edge IRQ was configured before.
|
||||||
|
T1L = nextEventCcys;
|
||||||
|
}
|
||||||
|
|
||||||
#endif // ESP8266
|
#endif // ESP8266
|
93
tasmota/core_esp8266_waveform.h
Normal file
93
tasmota/core_esp8266_waveform.h
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
esp8266_waveform - General purpose waveform generation and control,
|
||||||
|
supporting outputs on all pins in parallel.
|
||||||
|
|
||||||
|
Copyright (c) 2018 Earle F. Philhower, III. All rights reserved.
|
||||||
|
Copyright (c) 2020 Dirk O. Kaar.
|
||||||
|
|
||||||
|
The core idea is to have a programmable waveform generator with a unique
|
||||||
|
high and low period (defined in microseconds or CPU clock cycles). TIMER1 is
|
||||||
|
set to 1-shot mode and is always loaded with the time until the next edge
|
||||||
|
of any live waveforms.
|
||||||
|
|
||||||
|
Up to one waveform generator per pin supported.
|
||||||
|
|
||||||
|
Each waveform generator is synchronized to the ESP clock cycle counter, not the
|
||||||
|
timer. This allows for removing interrupt jitter and delay as the counter
|
||||||
|
always increments once per 80MHz clock. Changes to a waveform are
|
||||||
|
contiguous and only take effect on the next waveform transition,
|
||||||
|
allowing for smooth transitions.
|
||||||
|
|
||||||
|
This replaces older tone(), analogWrite(), and the Servo classes.
|
||||||
|
|
||||||
|
Everywhere in the code where "ccy" or "ccys" is used, it means ESP.getCycleCount()
|
||||||
|
clock cycle count, or an interval measured in CPU clock cycles, but not TIMER1
|
||||||
|
cycles (which may be 2 CPU clock cycles @ 160MHz).
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation; either
|
||||||
|
version 2.1 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#ifndef __ESP8266_WAVEFORM_H
|
||||||
|
#define __ESP8266_WAVEFORM_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Start or change a waveform of the specified high and low times on specific pin.
|
||||||
|
// If runtimeUS > 0 then automatically stop it after that many usecs, relative to the next
|
||||||
|
// full period.
|
||||||
|
// If waveform is not yet started on pin, and on pin == alignPhase a waveform is running,
|
||||||
|
// the new waveform is started at phaseOffsetUS phase offset, in microseconds, to that.
|
||||||
|
// Setting autoPwm to true allows the wave generator to maintain PWM duty to idle cycle ratio
|
||||||
|
// under load, for applications where frequency or duty cycle must not change, leave false.
|
||||||
|
// Returns true or false on success or failure.
|
||||||
|
int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS,
|
||||||
|
uint32_t runTimeUS = 0, int8_t alignPhase = -1, uint32_t phaseOffsetUS = 0, bool autoPwm = false);
|
||||||
|
// Start or change a waveform of the specified high and low CPU clock cycles on specific pin.
|
||||||
|
// If runtimeCycles > 0 then automatically stop it after that many CPU clock cycles, relative to the next
|
||||||
|
// full period.
|
||||||
|
// If waveform is not yet started on pin, and on pin == alignPhase a waveform is running,
|
||||||
|
// the new waveform is started at phaseOffsetCcys phase offset, in CPU clock cycles, to that.
|
||||||
|
// Setting autoPwm to true allows the wave generator to maintain PWM duty to idle cycle ratio
|
||||||
|
// under load, for applications where frequency or duty cycle must not change, leave false.
|
||||||
|
// Returns true or false on success or failure.
|
||||||
|
int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCcys, uint32_t timeLowCcys,
|
||||||
|
uint32_t runTimeCcys = 0, int8_t alignPhase = -1, uint32_t phaseOffsetCcys = 0, bool autoPwm = false);
|
||||||
|
// Stop a waveform, if any, on the specified pin.
|
||||||
|
// Returns true or false on success or failure.
|
||||||
|
int stopWaveform(uint8_t pin);
|
||||||
|
|
||||||
|
// Add a callback function to be called on *EVERY* timer1 trigger. The
|
||||||
|
// callback returns the number of microseconds until the next desired call.
|
||||||
|
// However, since it is called every timer1 interrupt, it may be called
|
||||||
|
// again before this period. It should therefore use the ESP Cycle Counter
|
||||||
|
// to determine whether or not to perform an operation.
|
||||||
|
// Pass in NULL to disable the callback and, if no other waveforms being
|
||||||
|
// generated, stop the timer as well.
|
||||||
|
// Make sure the CB function has the ICACHE_RAM_ATTR decorator.
|
||||||
|
void setTimer1Callback(uint32_t (*fn)());
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // ESP8266
|
@ -33,12 +33,6 @@
|
|||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
// Internal-only calls, not for applications
|
|
||||||
extern void _setPWMFreq(uint32_t freq);
|
|
||||||
extern bool _stopPWM(int pin);
|
|
||||||
extern bool _setPWM(int pin, uint32_t val, uint32_t range);
|
|
||||||
extern void resetPins();
|
|
||||||
|
|
||||||
volatile uint32_t* const esp8266_gpioToFn[16] PROGMEM = { &GPF0, &GPF1, &GPF2, &GPF3, &GPF4, &GPF5, &GPF6, &GPF7, &GPF8, &GPF9, &GPF10, &GPF11, &GPF12, &GPF13, &GPF14, &GPF15 };
|
volatile uint32_t* const esp8266_gpioToFn[16] PROGMEM = { &GPF0, &GPF1, &GPF2, &GPF3, &GPF4, &GPF5, &GPF6, &GPF7, &GPF8, &GPF9, &GPF10, &GPF11, &GPF12, &GPF13, &GPF14, &GPF15 };
|
||||||
|
|
||||||
extern void __pinMode(uint8_t pin, uint8_t mode) {
|
extern void __pinMode(uint8_t pin, uint8_t mode) {
|
||||||
@ -91,8 +85,7 @@ extern void __pinMode(uint8_t pin, uint8_t mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern void ICACHE_RAM_ATTR __digitalWrite(uint8_t pin, uint8_t val) {
|
extern void ICACHE_RAM_ATTR __digitalWrite(uint8_t pin, uint8_t val) {
|
||||||
stopWaveform(pin); // Disable any tone
|
stopWaveform(pin);
|
||||||
_stopPWM(pin); // ...and any analogWrite
|
|
||||||
if(pin < 16){
|
if(pin < 16){
|
||||||
if(val) GPOS = (1 << pin);
|
if(val) GPOS = (1 << pin);
|
||||||
else GPOC = (1 << pin);
|
else GPOC = (1 << pin);
|
||||||
@ -140,8 +133,10 @@ typedef struct {
|
|||||||
static interrupt_handler_t interrupt_handlers[16] = { {0, 0, 0, 0}, };
|
static interrupt_handler_t interrupt_handlers[16] = { {0, 0, 0, 0}, };
|
||||||
static uint32_t interrupt_reg = 0;
|
static uint32_t interrupt_reg = 0;
|
||||||
|
|
||||||
void ICACHE_RAM_ATTR interrupt_handler(void*)
|
void ICACHE_RAM_ATTR interrupt_handler(void *arg, void *frame)
|
||||||
{
|
{
|
||||||
|
(void) arg;
|
||||||
|
(void) frame;
|
||||||
uint32_t status = GPIE;
|
uint32_t status = GPIE;
|
||||||
GPIEC = status;//clear them interrupts
|
GPIEC = status;//clear them interrupts
|
||||||
uint32_t levels = GPI;
|
uint32_t levels = GPI;
|
||||||
|
@ -28,12 +28,9 @@
|
|||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
// Internal-only calls, not for applications
|
static uint32_t analogMap = 0;
|
||||||
extern void _setPWMFreq(uint32_t freq);
|
|
||||||
extern bool _stopPWM(int pin);
|
|
||||||
extern bool _setPWM(int pin, uint32_t val, uint32_t range);
|
|
||||||
|
|
||||||
static int32_t analogScale = PWMRANGE;
|
static int32_t analogScale = PWMRANGE;
|
||||||
|
static uint16_t analogFreq = 1000;
|
||||||
|
|
||||||
extern void __analogWriteRange(uint32_t range) {
|
extern void __analogWriteRange(uint32_t range) {
|
||||||
if (range > 0) {
|
if (range > 0) {
|
||||||
@ -43,28 +40,38 @@ extern void __analogWriteRange(uint32_t range) {
|
|||||||
|
|
||||||
extern void __analogWriteFreq(uint32_t freq) {
|
extern void __analogWriteFreq(uint32_t freq) {
|
||||||
if (freq < 40) {
|
if (freq < 40) {
|
||||||
freq = 40;
|
analogFreq = 40;
|
||||||
} else if (freq > 60000) {
|
} else if (freq > 60000) {
|
||||||
freq = 60000;
|
analogFreq = 60000;
|
||||||
} else {
|
} else {
|
||||||
freq = freq;
|
analogFreq = freq;
|
||||||
}
|
}
|
||||||
_setPWMFreq(freq);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern void __analogWrite(uint8_t pin, int val) {
|
extern void __analogWrite(uint8_t pin, int val) {
|
||||||
if (pin > 16) {
|
if (pin > 16) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
uint32_t analogPeriod = microsecondsToClockCycles(1000000UL) / analogFreq;
|
||||||
if (val < 0) {
|
if (val < 0) {
|
||||||
val = 0;
|
val = 0;
|
||||||
} else if (val > analogScale) {
|
} else if (val > analogScale) {
|
||||||
val = analogScale;
|
val = analogScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (analogMap & 1UL << pin) {
|
||||||
|
analogMap &= ~(1 << pin);
|
||||||
|
}
|
||||||
|
else {
|
||||||
pinMode(pin, OUTPUT);
|
pinMode(pin, OUTPUT);
|
||||||
_setPWM(pin, val, analogScale);
|
}
|
||||||
|
uint32_t high = (analogPeriod * val) / analogScale;
|
||||||
|
uint32_t low = analogPeriod - high;
|
||||||
|
// Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t)
|
||||||
|
int phaseReference = __builtin_ffs(analogMap) - 1;
|
||||||
|
if (startWaveformClockCycles(pin, high, low, 0, phaseReference, 0, true)) {
|
||||||
|
analogMap |= (1 << pin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extern void analogWrite(uint8_t pin, int val) __attribute__((weak, alias("__analogWrite")));
|
extern void analogWrite(uint8_t pin, int val) __attribute__((weak, alias("__analogWrite")));
|
||||||
|
@ -280,6 +280,12 @@
|
|||||||
#define APP_DISABLE_POWERCYCLE false // [SetOption65] Disable fast power cycle detection for device reset
|
#define APP_DISABLE_POWERCYCLE false // [SetOption65] Disable fast power cycle detection for device reset
|
||||||
#define DEEPSLEEP_BOOTCOUNT false // [SetOption76] Enable incrementing bootcount when deepsleep is enabled
|
#define DEEPSLEEP_BOOTCOUNT false // [SetOption76] Enable incrementing bootcount when deepsleep is enabled
|
||||||
|
|
||||||
|
#define APP_INTERLOCK_MODE false // [Interlock] Relay interlock mode
|
||||||
|
#define APP_INTERLOCK_GROUP_1 0xFF // [Interlock] Relay bitmask for interlock group 1 (0xFF if undef)
|
||||||
|
//#define APP_INTERLOCK_GROUP_2 0x00 // [Interlock] Relay bitmask for interlock group 2 (0x00 if undef)
|
||||||
|
//#define APP_INTERLOCK_GROUP_3 0x00 // [Interlock] Relay bitmask for interlock group 3 (0x00 if undef)
|
||||||
|
//#define APP_INTERLOCK_GROUP_4 0x00 // [Interlock] Relay bitmask for interlock group 4 (0x00 if undef)
|
||||||
|
|
||||||
// -- Lights --------------------------------------
|
// -- Lights --------------------------------------
|
||||||
#define WS2812_LEDS 30 // [Pixels] Number of WS2812 LEDs to start with (max is 512)
|
#define WS2812_LEDS 30 // [Pixels] Number of WS2812 LEDs to start with (max is 512)
|
||||||
#define LIGHT_MODE true // [SetOption15] Switch between commands PWM or COLOR/DIMMER/CT/CHANNEL
|
#define LIGHT_MODE true // [SetOption15] Switch between commands PWM or COLOR/DIMMER/CT/CHANNEL
|
||||||
|
@ -765,8 +765,11 @@ void SettingsDefaultSet2(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Module
|
// Module
|
||||||
// flag.interlock |= 0;
|
flag.interlock |= APP_INTERLOCK_MODE;
|
||||||
Settings.interlock[0] = 0xFF; // Legacy support using all relays in one interlock group
|
Settings.interlock[0] = APP_INTERLOCK_GROUP_1;
|
||||||
|
Settings.interlock[1] = APP_INTERLOCK_GROUP_2;
|
||||||
|
Settings.interlock[2] = APP_INTERLOCK_GROUP_3;
|
||||||
|
Settings.interlock[3] = APP_INTERLOCK_GROUP_4;
|
||||||
Settings.module = MODULE;
|
Settings.module = MODULE;
|
||||||
Settings.fallback_module = FALLBACK_MODULE;
|
Settings.fallback_module = FALLBACK_MODULE;
|
||||||
ModuleDefault(WEMOS);
|
ModuleDefault(WEMOS);
|
||||||
@ -1188,8 +1191,11 @@ void SettingsDelta(void)
|
|||||||
Settings.param[P_MDNS_DELAYED_START] = 0;
|
Settings.param[P_MDNS_DELAYED_START] = 0;
|
||||||
}
|
}
|
||||||
if (Settings.version < 0x0604010B) {
|
if (Settings.version < 0x0604010B) {
|
||||||
Settings.interlock[0] = 0xFF; // Legacy support using all relays in one interlock group
|
Settings.flag.interlock = APP_INTERLOCK_MODE;
|
||||||
for (uint32_t i = 1; i < MAX_INTERLOCKS; i++) { Settings.interlock[i] = 0; }
|
Settings.interlock[0] = APP_INTERLOCK_GROUP_1;
|
||||||
|
Settings.interlock[1] = APP_INTERLOCK_GROUP_2;
|
||||||
|
Settings.interlock[2] = APP_INTERLOCK_GROUP_3;
|
||||||
|
Settings.interlock[3] = APP_INTERLOCK_GROUP_4;
|
||||||
}
|
}
|
||||||
if (Settings.version < 0x0604010D) {
|
if (Settings.version < 0x0604010D) {
|
||||||
Settings.param[P_BOOT_LOOP_OFFSET] = BOOT_LOOP_OFFSET; // SetOption36
|
Settings.param[P_BOOT_LOOP_OFFSET] = BOOT_LOOP_OFFSET; // SetOption36
|
||||||
|
@ -1265,11 +1265,7 @@ uint32_t ValidPin(uint32_t pin, uint32_t gpio)
|
|||||||
|
|
||||||
bool ValidGPIO(uint32_t pin, uint32_t gpio)
|
bool ValidGPIO(uint32_t pin, uint32_t gpio)
|
||||||
{
|
{
|
||||||
#ifdef ESP8266
|
return (GPIO_USER == ValidPin(pin, BGPIO(gpio))); // Only allow GPIO_USER pins
|
||||||
return (GPIO_USER == ValidPin(pin, gpio)); // Only allow GPIO_USER pins
|
|
||||||
#else // ESP32
|
|
||||||
return (GPIO_USER == ValidPin(pin, gpio >> 5)); // Only allow GPIO_USER pins
|
|
||||||
#endif // ESP8266 - ESP32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ESP8266
|
#ifdef ESP8266
|
||||||
|
@ -1113,10 +1113,8 @@ void CmndGpio(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
char sindex[4] = { 0 };
|
char sindex[4] = { 0 };
|
||||||
#ifdef ESP8266
|
uint32_t sensor_name_idx = BGPIO(sensor_type);
|
||||||
uint32_t sensor_name_idx = sensor_type;
|
#ifdef ESP32
|
||||||
#else // ESP32
|
|
||||||
uint32_t sensor_name_idx = sensor_type >> 5;
|
|
||||||
uint32_t nice_list_search = sensor_type & 0xFFE0;
|
uint32_t nice_list_search = sensor_type & 0xFFE0;
|
||||||
for (uint32_t j = 0; j < ARRAY_SIZE(kGpioNiceList); j++) {
|
for (uint32_t j = 0; j < ARRAY_SIZE(kGpioNiceList); j++) {
|
||||||
uint32_t nls_idx = pgm_read_word(kGpioNiceList + j);
|
uint32_t nls_idx = pgm_read_word(kGpioNiceList + j);
|
||||||
@ -1125,7 +1123,7 @@ void CmndGpio(void)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif // ESP8266 - ESP32
|
#endif // ESP32
|
||||||
const char *sensor_names = kSensorNames;
|
const char *sensor_names = kSensorNames;
|
||||||
if (sensor_name_idx > GPIO_FIX_START) {
|
if (sensor_name_idx > GPIO_FIX_START) {
|
||||||
sensor_name_idx = sensor_name_idx - GPIO_FIX_START -1;
|
sensor_name_idx = sensor_name_idx - GPIO_FIX_START -1;
|
||||||
@ -1156,7 +1154,7 @@ void CmndGpios(void)
|
|||||||
uint32_t ridx = midx;
|
uint32_t ridx = midx;
|
||||||
#else // ESP32
|
#else // ESP32
|
||||||
uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0;
|
uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0;
|
||||||
uint32_t midx = ridx >> 5;
|
uint32_t midx = BGPIO(ridx);
|
||||||
#endif // ESP8266 - ESP32
|
#endif // ESP8266 - ESP32
|
||||||
if ((XdrvMailbox.payload != 255) && GetUsedInModule(midx, cmodule.io)) { continue; }
|
if ((XdrvMailbox.payload != 255) && GetUsedInModule(midx, cmodule.io)) { continue; }
|
||||||
if (!jsflg) {
|
if (!jsflg) {
|
||||||
@ -1627,7 +1625,8 @@ void CmndInterlock(void)
|
|||||||
}
|
}
|
||||||
ResponseAppend_P(PSTR("\"}"));
|
ResponseAppend_P(PSTR("\"}"));
|
||||||
} else {
|
} else {
|
||||||
Settings.flag.interlock = 0; // CMND_INTERLOCK - Enable/disable interlock
|
// never ever reset interlock mode inadvertently if we forced it upon compilation
|
||||||
|
Settings.flag.interlock = APP_INTERLOCK_MODE; // CMND_INTERLOCK - Enable/disable interlock
|
||||||
ResponseCmndStateText(Settings.flag.interlock);
|
ResponseCmndStateText(Settings.flag.interlock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,22 @@ String EthernetMacAddress(void);
|
|||||||
#undef USE_RF_FLASH // Disable RF firmware flash when Sonoff Rf is disabled
|
#undef USE_RF_FLASH // Disable RF firmware flash when Sonoff Rf is disabled
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef APP_INTERLOCK_MODE
|
||||||
|
#define APP_INTERLOCK_MODE false // [Interlock] Relay interlock mode
|
||||||
|
#endif
|
||||||
|
#ifndef APP_INTERLOCK_GROUP_1
|
||||||
|
#define APP_INTERLOCK_GROUP_1 0xFF // [Interlock] Relay bitmask for interlock group 1 - Legacy support using all relays in one interlock group
|
||||||
|
#endif
|
||||||
|
#ifndef APP_INTERLOCK_GROUP_2
|
||||||
|
#define APP_INTERLOCK_GROUP_2 0x00 // [Interlock] Relay bitmask for interlock group 2
|
||||||
|
#endif
|
||||||
|
#ifndef APP_INTERLOCK_GROUP_3
|
||||||
|
#define APP_INTERLOCK_GROUP_3 0x00 // [Interlock] Relay bitmask for interlock group 3
|
||||||
|
#endif
|
||||||
|
#ifndef APP_INTERLOCK_GROUP_4
|
||||||
|
#define APP_INTERLOCK_GROUP_4 0x00 // [Interlock] Relay bitmask for interlock group 4
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef SWITCH_MODE
|
#ifndef SWITCH_MODE
|
||||||
#define SWITCH_MODE TOGGLE // TOGGLE, FOLLOW or FOLLOW_INV (the wall switch state)
|
#define SWITCH_MODE TOGGLE // TOGGLE, FOLLOW or FOLLOW_INV (the wall switch state)
|
||||||
#endif
|
#endif
|
||||||
@ -356,8 +372,10 @@ const char kWebColors[] PROGMEM =
|
|||||||
|
|
||||||
#ifdef ESP8266
|
#ifdef ESP8266
|
||||||
#define AGPIO(x) (x)
|
#define AGPIO(x) (x)
|
||||||
|
#define BGPIO(x) (x)
|
||||||
#else // ESP32
|
#else // ESP32
|
||||||
#define AGPIO(x) (x<<5)
|
#define AGPIO(x) (x<<5)
|
||||||
|
#define BGPIO(x) (x>>5)
|
||||||
#endif // ESP8266 - ESP32
|
#endif // ESP8266 - ESP32
|
||||||
|
|
||||||
#ifdef USE_DEVICE_GROUPS
|
#ifdef USE_DEVICE_GROUPS
|
||||||
|
@ -1728,7 +1728,7 @@ void HandleTemplateConfiguration(void)
|
|||||||
WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, AGPIO(GPIO_USER), D_SENSOR_USER); // }2'255'>User}3
|
WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, AGPIO(GPIO_USER), D_SENSOR_USER); // }2'255'>User}3
|
||||||
}
|
}
|
||||||
uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0;
|
uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0;
|
||||||
uint32_t midx = ridx >> 5;
|
uint32_t midx = BGPIO(ridx);
|
||||||
WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, ridx, GetTextIndexed(stemp, sizeof(stemp), midx, kSensorNames));
|
WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, ridx, GetTextIndexed(stemp, sizeof(stemp), midx, kSensorNames));
|
||||||
#endif // ESP8266 - ESP32
|
#endif // ESP8266 - ESP32
|
||||||
}
|
}
|
||||||
@ -1905,7 +1905,7 @@ void HandleModuleConfiguration(void)
|
|||||||
}
|
}
|
||||||
#else // ESP32
|
#else // ESP32
|
||||||
uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0;
|
uint32_t ridx = pgm_read_word(kGpioNiceList + i) & 0xFFE0;
|
||||||
midx = ridx >> 5;
|
midx = BGPIO(ridx);
|
||||||
if (!GetUsedInModule(midx, cmodule.io)) {
|
if (!GetUsedInModule(midx, cmodule.io)) {
|
||||||
WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, ridx, GetTextIndexed(stemp, sizeof(stemp), midx, kSensorNames));
|
WSContentSend_P(HTTP_MODULE_TEMPLATE_REPLACE_NO_INDEX, ridx, GetTextIndexed(stemp, sizeof(stemp), midx, kSensorNames));
|
||||||
}
|
}
|
||||||
|
@ -470,6 +470,21 @@ uint32_t IrRemoteCmndIrSendRaw(void)
|
|||||||
return IE_INVALID_RAWDATA;
|
return IE_INVALID_RAWDATA;
|
||||||
} // Parameters must be at least 3
|
} // Parameters must be at least 3
|
||||||
|
|
||||||
|
if (strcasecmp(str, "gc") == 0) { //if first parameter is gc then we process global cache data else it is raw
|
||||||
|
uint16_t GC[count+1];
|
||||||
|
for (uint32_t i = 0; i <= count; i++) {
|
||||||
|
GC[i] = strtol(strtok_r(nullptr, ", ", &p), nullptr, 0);
|
||||||
|
if (!GC[i]) {
|
||||||
|
return IE_INVALID_RAWDATA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
irsend_active = true;
|
||||||
|
for (uint32_t r = 0; r <= repeat; r++) {
|
||||||
|
irsend->sendGC(GC, count+1);
|
||||||
|
}
|
||||||
|
return IE_NO_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
uint16_t parm[count];
|
uint16_t parm[count];
|
||||||
for (uint32_t i = 0; i < count; i++) {
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
parm[i] = strtol(strtok_r(nullptr, ", ", &p), nullptr, 0);
|
parm[i] = strtol(strtok_r(nullptr, ", ", &p), nullptr, 0);
|
||||||
|
@ -168,6 +168,7 @@ struct RULES {
|
|||||||
uint16_t vars_event = 0;
|
uint16_t vars_event = 0;
|
||||||
uint8_t mems_event = 0;
|
uint8_t mems_event = 0;
|
||||||
bool teleperiod = false;
|
bool teleperiod = false;
|
||||||
|
bool busy = false;
|
||||||
|
|
||||||
char event_data[100];
|
char event_data[100];
|
||||||
} Rules;
|
} Rules;
|
||||||
@ -564,7 +565,6 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule)
|
|||||||
|
|
||||||
//AddLog_P2(LOG_LEVEL_DEBUG, PSTR("RUL: Match 1 %d"), match);
|
//AddLog_P2(LOG_LEVEL_DEBUG, PSTR("RUL: Match 1 %d"), match);
|
||||||
|
|
||||||
|
|
||||||
if (bitRead(Settings.rule_once, rule_set)) {
|
if (bitRead(Settings.rule_once, rule_set)) {
|
||||||
if (match) { // Only allow match state changes
|
if (match) { // Only allow match state changes
|
||||||
if (!bitRead(Rules.triggers[rule_set], Rules.trigger_count[rule_set])) {
|
if (!bitRead(Rules.triggers[rule_set], Rules.trigger_count[rule_set])) {
|
||||||
@ -751,6 +751,9 @@ bool RulesProcessEvent(char *json_event)
|
|||||||
{
|
{
|
||||||
bool serviced = false;
|
bool serviced = false;
|
||||||
|
|
||||||
|
if (!Rules.busy) {
|
||||||
|
Rules.busy = true;
|
||||||
|
|
||||||
#ifdef USE_DEBUG_DRIVER
|
#ifdef USE_DEBUG_DRIVER
|
||||||
ShowFreeMem(PSTR("RulesProcessEvent"));
|
ShowFreeMem(PSTR("RulesProcessEvent"));
|
||||||
#endif
|
#endif
|
||||||
@ -774,6 +777,9 @@ bool RulesProcessEvent(char *json_event)
|
|||||||
if (RuleSetProcess(i, event_saved)) { serviced = true; }
|
if (RuleSetProcess(i, event_saved)) { serviced = true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rules.busy = false;
|
||||||
|
}
|
||||||
return serviced;
|
return serviced;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -796,7 +802,7 @@ void RulesInit(void)
|
|||||||
|
|
||||||
void RulesEvery50ms(void)
|
void RulesEvery50ms(void)
|
||||||
{
|
{
|
||||||
if (Settings.rule_enabled) { // Any rule enabled
|
if (Settings.rule_enabled && !Rules.busy) { // Any rule enabled
|
||||||
char json_event[120];
|
char json_event[120];
|
||||||
|
|
||||||
if (-1 == Rules.new_power) { Rules.new_power = power; }
|
if (-1 == Rules.new_power) { Rules.new_power = power; }
|
||||||
@ -916,7 +922,7 @@ uint8_t rules_xsns_index = 0;
|
|||||||
|
|
||||||
void RulesEvery100ms(void)
|
void RulesEvery100ms(void)
|
||||||
{
|
{
|
||||||
if (Settings.rule_enabled && (uptime > 4)) { // Any rule enabled and allow 4 seconds start-up time for sensors (#3811)
|
if (Settings.rule_enabled && !Rules.busy && (uptime > 4)) { // Any rule enabled and allow 4 seconds start-up time for sensors (#3811)
|
||||||
mqtt_data[0] = '\0';
|
mqtt_data[0] = '\0';
|
||||||
int tele_period_save = tele_period;
|
int tele_period_save = tele_period;
|
||||||
tele_period = 2; // Do not allow HA updates during next function call
|
tele_period = 2; // Do not allow HA updates during next function call
|
||||||
@ -925,14 +931,14 @@ void RulesEvery100ms(void)
|
|||||||
if (strlen(mqtt_data)) {
|
if (strlen(mqtt_data)) {
|
||||||
mqtt_data[0] = '{'; // {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}
|
mqtt_data[0] = '{'; // {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}
|
||||||
ResponseJsonEnd();
|
ResponseJsonEnd();
|
||||||
RulesProcess();
|
RulesProcessEvent(mqtt_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RulesEverySecond(void)
|
void RulesEverySecond(void)
|
||||||
{
|
{
|
||||||
if (Settings.rule_enabled) { // Any rule enabled
|
if (Settings.rule_enabled && !Rules.busy) { // Any rule enabled
|
||||||
char json_event[120];
|
char json_event[120];
|
||||||
|
|
||||||
if (RtcTime.valid) {
|
if (RtcTime.valid) {
|
||||||
@ -956,7 +962,7 @@ void RulesEverySecond(void)
|
|||||||
|
|
||||||
void RulesSaveBeforeRestart(void)
|
void RulesSaveBeforeRestart(void)
|
||||||
{
|
{
|
||||||
if (Settings.rule_enabled) { // Any rule enabled
|
if (Settings.rule_enabled && !Rules.busy) { // Any rule enabled
|
||||||
char json_event[32];
|
char json_event[32];
|
||||||
|
|
||||||
strncpy_P(json_event, PSTR("{\"System\":{\"Save\":1}}"), sizeof(json_event));
|
strncpy_P(json_event, PSTR("{\"System\":{\"Save\":1}}"), sizeof(json_event));
|
||||||
|
@ -101,9 +101,9 @@ void BuzzerEnabledBeep(uint32_t count, uint32_t duration)
|
|||||||
|
|
||||||
bool BuzzerPinState(void)
|
bool BuzzerPinState(void)
|
||||||
{
|
{
|
||||||
if (XdrvMailbox.index == GPIO_BUZZER_INV) {
|
if (XdrvMailbox.index == AGPIO(GPIO_BUZZER_INV)) {
|
||||||
Buzzer.inverted = 1;
|
Buzzer.inverted = 1;
|
||||||
XdrvMailbox.index -= (GPIO_BUZZER_INV - GPIO_BUZZER);
|
XdrvMailbox.index -= (AGPIO(GPIO_BUZZER_INV) - AGPIO(GPIO_BUZZER));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -206,7 +206,8 @@ void ShutterInit(void)
|
|||||||
Shutter.pwm_frequency[i] = 0;
|
Shutter.pwm_frequency[i] = 0;
|
||||||
Shutter.accelerator[i] = 0;
|
Shutter.accelerator[i] = 0;
|
||||||
analogWriteFreq(Shutter.pwm_frequency[i]);
|
analogWriteFreq(Shutter.pwm_frequency[i]);
|
||||||
ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER);
|
analogWrite(Pin(GPIO_PWM1, i), 0);
|
||||||
|
// ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,8 +365,8 @@ void ShutterUpdatePosition(void)
|
|||||||
while (RtcSettings.pulse_counter[i] < (uint32_t)(Shutter.target_position[i]-Shutter.start_position[i])*Shutter.direction[i]*Shutter.max_pwm_frequency/2000) {
|
while (RtcSettings.pulse_counter[i] < (uint32_t)(Shutter.target_position[i]-Shutter.start_position[i])*Shutter.direction[i]*Shutter.max_pwm_frequency/2000) {
|
||||||
delay(1);
|
delay(1);
|
||||||
}
|
}
|
||||||
//analogWrite(Pin(GPIO_PWM1, i), 0); // removed with 8.3 because of reset caused by watchog
|
analogWrite(Pin(GPIO_PWM1, i), 0); // removed with 8.3 because of reset caused by watchog
|
||||||
ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER);
|
// ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER);
|
||||||
Shutter.real_position[i] = ShutterCounterBasedPosition(i);
|
Shutter.real_position[i] = ShutterCounterBasedPosition(i);
|
||||||
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Real %d, pulsecount %d, start %d"), Shutter.real_position[i],RtcSettings.pulse_counter[i], Shutter.start_position[i]);
|
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("SHT: Real %d, pulsecount %d, start %d"), Shutter.real_position[i],RtcSettings.pulse_counter[i], Shutter.start_position[i]);
|
||||||
|
|
||||||
@ -461,7 +462,7 @@ void ShutterWaitForMotorStop(uint32_t i)
|
|||||||
delay(50);
|
delay(50);
|
||||||
}
|
}
|
||||||
analogWrite(Pin(GPIO_PWM1, i), 0);
|
analogWrite(Pin(GPIO_PWM1, i), 0);
|
||||||
ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER);
|
// ExecuteCommandPower(Settings.shutter_startrelay[i]+2, 0, SRC_SHUTTER);
|
||||||
Shutter.real_position[i] = ShutterCounterBasedPosition(i);
|
Shutter.real_position[i] = ShutterCounterBasedPosition(i);
|
||||||
} else {
|
} else {
|
||||||
ExecuteCommandPower(Settings.shutter_startrelay[i], 0, SRC_SHUTTER);
|
ExecuteCommandPower(Settings.shutter_startrelay[i], 0, SRC_SHUTTER);
|
||||||
|
@ -503,6 +503,7 @@ void CmndClientReset(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CmndClientSend(void) {
|
void CmndClientSend(void) {
|
||||||
|
if (TClient.SerialEnabled) {
|
||||||
if (0 < XdrvMailbox.data_len) {
|
if (0 < XdrvMailbox.data_len) {
|
||||||
TasmotaClient_sendCmnd(CMND_CLIENT_SEND, XdrvMailbox.data_len);
|
TasmotaClient_sendCmnd(CMND_CLIENT_SEND, XdrvMailbox.data_len);
|
||||||
TasmotaClient_Serial->write(char(PARAM_DATA_START));
|
TasmotaClient_Serial->write(char(PARAM_DATA_START));
|
||||||
@ -512,6 +513,7 @@ void CmndClientSend(void) {
|
|||||||
TasmotaClient_Serial->write(char(PARAM_DATA_END));
|
TasmotaClient_Serial->write(char(PARAM_DATA_END));
|
||||||
}
|
}
|
||||||
ResponseCmndDone();
|
ResponseCmndDone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TasmotaClient_ProcessIn(void) {
|
void TasmotaClient_ProcessIn(void) {
|
||||||
|
@ -34,12 +34,29 @@
|
|||||||
|
|
||||||
Adafruit_ILI9341 *tft;
|
Adafruit_ILI9341 *tft;
|
||||||
|
|
||||||
uint16_t tft_scroll = TFT_TOP;
|
|
||||||
uint16_t tft_top = TFT_TOP;
|
uint16_t tft_top = TFT_TOP;
|
||||||
uint16_t tft_bottom = TFT_BOTTOM;
|
uint16_t tft_bottom = TFT_BOTTOM;
|
||||||
|
uint16_t tft_scroll = TFT_TOP;
|
||||||
|
uint16_t tft_cols = 0;
|
||||||
|
|
||||||
/*********************************************************************************************/
|
/*********************************************************************************************/
|
||||||
|
|
||||||
|
bool Ili9341Header(void) {
|
||||||
|
if (Settings.display_cols[0] != tft_cols) {
|
||||||
|
tft_cols = Settings.display_cols[0];
|
||||||
|
if (tft_cols > 17) {
|
||||||
|
tft_top = TFT_TOP;
|
||||||
|
tft_bottom = TFT_BOTTOM;
|
||||||
|
} else {
|
||||||
|
tft_top = 0;
|
||||||
|
tft_bottom = 0;
|
||||||
|
}
|
||||||
|
tft_scroll = tft_top;
|
||||||
|
tft->setScrollMargins(tft_top, tft_bottom);
|
||||||
|
}
|
||||||
|
return (tft_cols > 17);
|
||||||
|
}
|
||||||
|
|
||||||
void Ili9341InitMode(void)
|
void Ili9341InitMode(void)
|
||||||
{
|
{
|
||||||
tft->setRotation(Settings.display_rotate); // 0
|
tft->setRotation(Settings.display_rotate); // 0
|
||||||
@ -52,15 +69,12 @@ void Ili9341InitMode(void)
|
|||||||
tft->setTextColor(ILI9341_WHITE, ILI9341_BLACK);
|
tft->setTextColor(ILI9341_WHITE, ILI9341_BLACK);
|
||||||
tft->setTextSize(1);
|
tft->setTextSize(1);
|
||||||
} else {
|
} else {
|
||||||
tft_top = TFT_TOP;
|
Ili9341Header();
|
||||||
tft_bottom = TFT_BOTTOM;
|
|
||||||
tft->setScrollMargins(tft_top, tft_bottom);
|
|
||||||
tft->setCursor(0, 0);
|
tft->setCursor(0, 0);
|
||||||
tft->setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
|
tft->setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
|
||||||
tft->setTextSize(2);
|
tft->setTextSize(2);
|
||||||
// tft->println("HEADER");
|
// tft->println("HEADER");
|
||||||
|
|
||||||
tft_scroll = tft_top;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +119,8 @@ void Ili9341InitDriver(void)
|
|||||||
#endif // USE_DISPLAY_MODES1TO5
|
#endif // USE_DISPLAY_MODES1TO5
|
||||||
|
|
||||||
Ili9341InitMode();
|
Ili9341InitMode();
|
||||||
|
|
||||||
|
AddLog_P2(LOG_LEVEL_INFO, PSTR("DSP: ILI9341"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +184,7 @@ void Ili9341PrintLog(void)
|
|||||||
tft->setCursor(0, tft_scroll);
|
tft->setCursor(0, tft_scroll);
|
||||||
tft->fillRect(0, tft_scroll, tft->width(), theight, ILI9341_BLACK); // Erase line
|
tft->fillRect(0, tft_scroll, tft->width(), theight, ILI9341_BLACK); // Erase line
|
||||||
tft->print(txt);
|
tft->print(txt);
|
||||||
if (tft_top) { tft_scroll += theight; }
|
tft_scroll += theight;
|
||||||
if (tft_scroll >= (tft->height() - tft_bottom)) {
|
if (tft_scroll >= (tft->height() - tft_bottom)) {
|
||||||
tft_scroll = tft_top;
|
tft_scroll = tft_top;
|
||||||
}
|
}
|
||||||
@ -201,17 +217,13 @@ void Ili9341Refresh(void) // Every second
|
|||||||
// 24-04-2017 13:45:43 = 19 + 1 ('\0') = 20
|
// 24-04-2017 13:45:43 = 19 + 1 ('\0') = 20
|
||||||
// 24-04-2017 13:45 = 16 + 1 ('\0') = 17
|
// 24-04-2017 13:45 = 16 + 1 ('\0') = 17
|
||||||
|
|
||||||
if (Settings.display_cols[0] > 17) {
|
if (Ili9341Header()) {
|
||||||
char tftdt[Settings.display_cols[0] +1];
|
char tftdt[Settings.display_cols[0] +1];
|
||||||
char date4[11]; // 24-04-2017
|
char date4[11]; // 24-04-2017
|
||||||
uint8_t time_size = (Settings.display_cols[0] >= 20) ? 9 : 6; // 13:45:43 or 13:45
|
uint8_t time_size = (Settings.display_cols[0] >= 20) ? 9 : 6; // 13:45:43 or 13:45
|
||||||
char spaces[Settings.display_cols[0] - (8 + time_size)];
|
char spaces[Settings.display_cols[0] - (8 + time_size)];
|
||||||
char time[time_size]; // 13:45:43
|
char time[time_size]; // 13:45:43
|
||||||
|
|
||||||
tft_top = TFT_TOP;
|
|
||||||
tft_bottom = TFT_BOTTOM;
|
|
||||||
tft_scroll = tft_top;
|
|
||||||
tft->setScrollMargins(tft_top, tft_bottom);
|
|
||||||
tft->setTextSize(Settings.display_size);
|
tft->setTextSize(Settings.display_size);
|
||||||
tft->setTextColor(ILI9341_YELLOW, ILI9341_RED); // Add background color to solve flicker
|
tft->setTextColor(ILI9341_YELLOW, ILI9341_RED); // Add background color to solve flicker
|
||||||
tft->setCursor(0, 0);
|
tft->setCursor(0, 0);
|
||||||
@ -224,10 +236,6 @@ void Ili9341Refresh(void) // Every second
|
|||||||
|
|
||||||
tft->print(tftdt);
|
tft->print(tftdt);
|
||||||
} else {
|
} else {
|
||||||
tft_top = 0;
|
|
||||||
tft_bottom = 0;
|
|
||||||
tft_scroll = tft_top;
|
|
||||||
tft->setScrollMargins(tft_top, tft_bottom);
|
|
||||||
tft->setCursor(0, 0);
|
tft->setCursor(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,9 +113,9 @@ void CounterUpdate4(void)
|
|||||||
|
|
||||||
bool CounterPinState(void)
|
bool CounterPinState(void)
|
||||||
{
|
{
|
||||||
if ((XdrvMailbox.index >= GPIO_CNTR1_NP) && (XdrvMailbox.index < (GPIO_CNTR1_NP + MAX_COUNTERS))) {
|
if ((XdrvMailbox.index >= AGPIO(GPIO_CNTR1_NP)) && (XdrvMailbox.index < (AGPIO(GPIO_CNTR1_NP) + MAX_COUNTERS))) {
|
||||||
bitSet(Counter.no_pullup, XdrvMailbox.index - GPIO_CNTR1_NP);
|
bitSet(Counter.no_pullup, XdrvMailbox.index - AGPIO(GPIO_CNTR1_NP));
|
||||||
XdrvMailbox.index -= (GPIO_CNTR1_NP - GPIO_CNTR1);
|
XdrvMailbox.index -= (AGPIO(GPIO_CNTR1_NP) - AGPIO(GPIO_CNTR1));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -188,12 +188,12 @@ bool DhtRead(uint32_t sensor)
|
|||||||
|
|
||||||
bool DhtPinState()
|
bool DhtPinState()
|
||||||
{
|
{
|
||||||
if ((XdrvMailbox.index >= GPIO_DHT11) && (XdrvMailbox.index <= GPIO_SI7021)) {
|
if ((XdrvMailbox.index >= AGPIO(GPIO_DHT11)) && (XdrvMailbox.index <= AGPIO(GPIO_SI7021))) {
|
||||||
if (dht_sensors < DHT_MAX_SENSORS) {
|
if (dht_sensors < DHT_MAX_SENSORS) {
|
||||||
Dht[dht_sensors].pin = XdrvMailbox.payload;
|
Dht[dht_sensors].pin = XdrvMailbox.payload;
|
||||||
Dht[dht_sensors].type = XdrvMailbox.index;
|
Dht[dht_sensors].type = BGPIO(XdrvMailbox.index);
|
||||||
dht_sensors++;
|
dht_sensors++;
|
||||||
XdrvMailbox.index = GPIO_DHT11;
|
XdrvMailbox.index = AGPIO(GPIO_DHT11);
|
||||||
} else {
|
} else {
|
||||||
XdrvMailbox.index = 0;
|
XdrvMailbox.index = 0;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
#include <TasmotaSerial.h>
|
#include <TasmotaSerial.h>
|
||||||
|
|
||||||
#define TMSBSIZ 512
|
#define TMSBSIZ52 512
|
||||||
|
|
||||||
#define HM17_BAUDRATE 9600
|
#define HM17_BAUDRATE 9600
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ void IBEACON_Init() {
|
|||||||
|
|
||||||
// actually doesnt work reliably with software serial
|
// actually doesnt work reliably with software serial
|
||||||
if (PinUsed(GPIO_IBEACON_RX) && PinUsed(GPIO_IBEACON_TX)) {
|
if (PinUsed(GPIO_IBEACON_RX) && PinUsed(GPIO_IBEACON_TX)) {
|
||||||
IBEACON_Serial = new TasmotaSerial(Pin(GPIO_IBEACON_RX), Pin(GPIO_IBEACON_TX),1,0,TMSBSIZ);
|
IBEACON_Serial = new TasmotaSerial(Pin(GPIO_IBEACON_RX), Pin(GPIO_IBEACON_TX),1,0,TMSBSIZ52);
|
||||||
if (IBEACON_Serial->begin(HM17_BAUDRATE)) {
|
if (IBEACON_Serial->begin(HM17_BAUDRATE)) {
|
||||||
if (IBEACON_Serial->hardwareSerial()) {
|
if (IBEACON_Serial->hardwareSerial()) {
|
||||||
ClaimSerial();
|
ClaimSerial();
|
||||||
|
@ -606,13 +606,17 @@ bool MINRFhandleBeacon(scan_entry_t * entry, uint32_t offset){
|
|||||||
* @brief increase beacon timer every second and process the result
|
* @brief increase beacon timer every second and process the result
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
void MINRFbeaconCounter(void){
|
void MINRFbeaconCounter(void) {
|
||||||
if(MINRF.beacon.active) {
|
if (MINRF.beacon.active) {
|
||||||
MINRF.beacon.time++;
|
MINRF.beacon.time++;
|
||||||
|
/*
|
||||||
char stemp[20];
|
char stemp[20];
|
||||||
snprintf_P(stemp, sizeof(stemp),PSTR("{%s:{\"Beacon\": %u}}"),D_CMND_NRF, MINRF.beacon.time);
|
snprintf_P(stemp, sizeof(stemp),PSTR("{%s:{\"Beacon\": %u}}"),D_CMND_NRF, MINRF.beacon.time);
|
||||||
AddLog_P2(LOG_LEVEL_DEBUG, stemp);
|
AddLog_P2(LOG_LEVEL_DEBUG, stemp);
|
||||||
RulesProcessEvent(stemp);
|
RulesProcessEvent(stemp);
|
||||||
|
*/
|
||||||
|
Response_P(PSTR("{%s:{\"Beacon\":%u}}"), D_CMND_NRF, MINRF.beacon.time);
|
||||||
|
XdrvRulesProcess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -977,11 +977,15 @@ void HM10_TaskEvery100ms(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HM10StatusInfo(){
|
void HM10StatusInfo() {
|
||||||
|
/*
|
||||||
char stemp[20];
|
char stemp[20];
|
||||||
snprintf_P(stemp, sizeof(stemp),PSTR("{%s:{\"found\": %u}}"),D_CMND_HM10, MIBLEsensors.size());
|
snprintf_P(stemp, sizeof(stemp),PSTR("{%s:{\"found\": %u}}"),D_CMND_HM10, MIBLEsensors.size());
|
||||||
AddLog_P2(LOG_LEVEL_INFO, stemp);
|
AddLog_P2(LOG_LEVEL_INFO, stemp);
|
||||||
RulesProcessEvent(stemp);
|
RulesProcessEvent(stemp);
|
||||||
|
*/
|
||||||
|
Response_P(PSTR("{%s:{\"found\":%u}}"), D_CMND_HM10, MIBLEsensors.size());
|
||||||
|
XdrvRulesProcess();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user