mirror of https://github.com/arendst/Tasmota.git
337 lines
14 KiB
C++
337 lines
14 KiB
C++
/*
|
|
support_pwm.ino - command support for Tasmota
|
|
|
|
Copyright (C) 2021 Theo Arends & Stephan Hadinger
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program 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 General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
int16_t analog_write_state[MAX_GPIO_PIN] = { -1 };
|
|
|
|
void AnalogWrite(uint8_t pin, int val) {
|
|
analog_write_state[pin] = val;
|
|
analogWrite(pin, val);
|
|
}
|
|
|
|
uint32_t AnalogRead(uint8_t pin) {
|
|
return analog_write_state[pin];
|
|
}
|
|
|
|
/***********************************************************************\
|
|
* PWM Control for ESP32
|
|
\***********************************************************************/
|
|
#ifdef ESP32
|
|
|
|
// All changes in PWM have been applied, rearm all change indicators
|
|
void PwmRearmChanges(void) {
|
|
for (uint32_t i = 0; i < MAX_PWMS; i++) {
|
|
// Init expected changes
|
|
TasmotaGlobal.pwm_value[i] = -1; // no change wanted
|
|
TasmotaGlobal.pwm_phase[i] = -1; // no change wanted
|
|
}
|
|
}
|
|
|
|
// Load PWM values from settings and intiliaze values
|
|
// void PwmLoadFromSettings(void) {
|
|
// for (uint32_t i = 0; i < MAX_PWMS; i++) {
|
|
// if (i < MAX_PWMS_LEGACY) {
|
|
// TasmotaGlobal.pwm_cur_value[i] = Settings->pwm_value[i]; // retrieve in Legacy pool for 0..4
|
|
// } else {
|
|
// TasmotaGlobal.pwm_cur_value[i] = Settings->pwm_value_ext[i - MAX_PWMS_LEGACY]; // retrieve in Legacy pool for 5..15
|
|
// }
|
|
// TasmotaGlobal.pwm_cur_phase[i] = 0; // no phase shift for now, will be recomputed at first push to GPIOs
|
|
// }
|
|
// PwmRearmChanges(); // reset expected changes
|
|
// }
|
|
|
|
// Copy current values to Settings
|
|
void PwmSaveToSettings(void) {
|
|
for (uint32_t i = 0; i < MAX_PWMS; i++) {
|
|
if (i < MAX_PWMS_LEGACY) {
|
|
Settings->pwm_value[i] = TasmotaGlobal.pwm_cur_value[i]; // store in Legacy pool for 0..4
|
|
} else {
|
|
Settings->pwm_value_ext[i - MAX_PWMS_LEGACY] = TasmotaGlobal.pwm_cur_value[i]; // retrieve in Legacy pool for 5..15
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************\
|
|
* PWM Control for ESP32
|
|
\***********************************************************************/
|
|
// Apply PWM expected values to actual GPIO PWM
|
|
// As input, `TasmotaGlobal.pwm_value[]` and `TasmotaGlobal.pwm_phase[]` contain the new expected values
|
|
// or `-1` if no change.
|
|
// Auto-phasing is recomputed, and changes are applied to GPIO if there is a physical GPIO configured and an actual change needed
|
|
//
|
|
// force_update_all: force applying the PWM values even if the value didn't change (necessary at initialization)
|
|
void PwmApplyGPIO(bool force_update_all) {
|
|
uint32_t pwm_phase_accumulator = 0; // dephase each PWM channel with the value of the previous
|
|
|
|
uint8_t timer0_resolution = analogGetTimerResolution(0);
|
|
uint32_t timer0_freq = analogGetTimerFrequency(0);
|
|
|
|
// AddLog(LOG_LEVEL_INFO, "PWM: resol0=%i freq0=%i", timer0_resolution, timer0_freq);
|
|
|
|
for (uint32_t i = 0; i < MAX_PWMS; i++) {
|
|
|
|
// compute `pwm_val`, the virtual value of PWM (not taking into account inverted)
|
|
uint32_t pwm_val = TasmotaGlobal.pwm_cur_value[i]; // logical value of PWM, 0..1023
|
|
uint32_t pwm_phase = TasmotaGlobal.pwm_cur_phase[i]; // pwm_phase is the logical phase of the active pulse, ignoring inverted
|
|
|
|
// apply new values to GPIO if GPIO is set
|
|
if (PinUsed(GPIO_PWM1, i)) {
|
|
int32_t pin = Pin(GPIO_PWM1, i);
|
|
int32_t chan = analogGetChannel2(pin);
|
|
uint32_t res = ledcReadResolution(chan);
|
|
uint32_t range = (1 << res) - 1;
|
|
uint32_t freq = ledcReadFreq2(chan);
|
|
|
|
// AddLog(LOG_LEVEL_INFO, "PWM: res0=%i freq0=%i pin=%i chan=%i res=%i timer=%i range=%i freq=%i", timer0_resolution, timer0_freq, pin, chan, res, analogGetTimerForChannel(chan), range, freq);
|
|
|
|
if (TasmotaGlobal.pwm_value[i] >= 0) { pwm_val = TasmotaGlobal.pwm_value[i]; } // new value explicitly specified
|
|
if (pwm_val > range) { pwm_val = range; } // prevent overflow
|
|
|
|
// compute phase
|
|
if (TasmotaGlobal.pwm_phase[i] >= 0) {
|
|
pwm_phase = TasmotaGlobal.pwm_phase[i]; // if explicit set explicitly,
|
|
} else if (Settings->flag5.pwm_force_same_phase) {
|
|
pwm_phase = 0; // if auto-phase is off
|
|
} else {
|
|
if (freq == timer0_freq && res == timer0_resolution) { // only apply if the frequency is equl to global one
|
|
// compute auto-phase only if default frequency
|
|
pwm_phase = pwm_phase_accumulator;
|
|
// accumulate phase for next GPIO
|
|
pwm_phase_accumulator = (pwm_phase + pwm_val) & range;
|
|
}
|
|
}
|
|
// AddLog(LOG_LEVEL_INFO, "PWM: i=%i used=%i pwm_val=%03X vs %03X pwm_phase=%03X vs %03X", i, PinUsed(GPIO_PWM1, i), pwm_val, TasmotaGlobal.pwm_cur_value[i], pwm_phase, TasmotaGlobal.pwm_cur_phase[i]);
|
|
|
|
if (force_update_all || (pwm_val != TasmotaGlobal.pwm_cur_value[i]) || (pwm_phase != TasmotaGlobal.pwm_cur_phase[i])) {
|
|
// GPIO has PWM and there is a chnage to apply, apply it
|
|
analogWritePhase(pin, pwm_val, pwm_phase);
|
|
// AddLog(LOG_LEVEL_INFO, "PWM: analogWritePhase i=%i val=%03X phase=%03X", i, pwm_val, pwm_phase);
|
|
}
|
|
}
|
|
|
|
// set new current values
|
|
TasmotaGlobal.pwm_cur_value[i] = pwm_val;
|
|
TasmotaGlobal.pwm_cur_phase[i] = pwm_phase;
|
|
}
|
|
// AddLog(LOG_LEVEL_INFO, "PWM: Val=%03X-%03X-%03X-%03X-%03X Phase=%03X-%03X-%03X-%03X-%03X Range=%03X",
|
|
// TasmotaGlobal.pwm_cur_value[0], TasmotaGlobal.pwm_cur_value[1], TasmotaGlobal.pwm_cur_value[2], TasmotaGlobal.pwm_cur_value[3],
|
|
// TasmotaGlobal.pwm_cur_value[4],
|
|
// TasmotaGlobal.pwm_cur_phase[0], TasmotaGlobal.pwm_cur_phase[1], TasmotaGlobal.pwm_cur_phase[2], TasmotaGlobal.pwm_cur_phase[3],
|
|
// TasmotaGlobal.pwm_cur_phase[4],
|
|
// Settings->pwm_range
|
|
// );
|
|
PwmSaveToSettings(); // copy to Settings
|
|
PwmRearmChanges(); // reset expected changes
|
|
}
|
|
|
|
void CmndPwm(void)
|
|
{
|
|
if (TasmotaGlobal.pwm_present && (XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_PWMS)) {
|
|
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= Settings->pwm_range) && PinUsed(GPIO_PWM1, XdrvMailbox.index -1)) {
|
|
TasmotaGlobal.pwm_value[XdrvMailbox.index - 1] = XdrvMailbox.payload;
|
|
PwmApplyGPIO(false);
|
|
}
|
|
Response_P(PSTR("{"));
|
|
MqttShowPWMState(); // Render the PWM status to MQTT
|
|
ResponseJsonEnd();
|
|
}
|
|
}
|
|
|
|
void GpioInitPwm(void) {
|
|
PwmRearmChanges();
|
|
|
|
for (uint32_t i = 0; i < MAX_PWMS; i++) { // Basic PWM control only
|
|
if (PinUsed(GPIO_PWM1, i)) {
|
|
analogAttach(Pin(GPIO_PWM1, i), bitRead(TasmotaGlobal.pwm_inverted, i));
|
|
|
|
if (i < TasmotaGlobal.light_type) {
|
|
// force PWM GPIOs to black
|
|
TasmotaGlobal.pwm_value[i] = 0;
|
|
} else {
|
|
TasmotaGlobal.pwm_present = true;
|
|
if (i < MAX_PWMS_LEGACY) {
|
|
TasmotaGlobal.pwm_value[i] = Settings->pwm_value[i];
|
|
} else {
|
|
TasmotaGlobal.pwm_value[i] = Settings->pwm_value_ext[i - MAX_PWMS_LEGACY];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
PwmApplyGPIO(true); // apply all changes
|
|
}
|
|
|
|
/********************************************************************************************/
|
|
|
|
void ResetPwm(void)
|
|
{
|
|
for (uint32_t i = 0; i < MAX_PWMS; i++) { // Basic PWM control only
|
|
TasmotaGlobal.pwm_value[i] = 0;
|
|
}
|
|
PwmApplyGPIO(true);
|
|
analogDetachAll(); // Fix PWM activity on unconfigured PWM GPIOs after restart
|
|
}
|
|
|
|
void CmndPwmfrequency(void)
|
|
{
|
|
int32_t pwm_frequency = Settings->pwm_frequency;
|
|
int32_t pwm = -1; // PWM being targeted, or -1 for global value applied to Timer 0
|
|
|
|
// check if index if above 100, meaning we target only a specific PWM channel
|
|
uint32_t parm[2] = { 0, 0 };
|
|
ParseParameters(2, parm);
|
|
|
|
if (parm[1]) { // we have a second parameter
|
|
pwm = parm[1] - 1;
|
|
if (pwm < 0 || pwm >= MAX_PWMS) { pwm = -1; } // if invalid, revert to global value
|
|
}
|
|
|
|
// AddLog(LOG_LEVEL_INFO, "PWM: payload=%i index=%i pwm=%i pwm_freqency=%i", XdrvMailbox.payload, XdrvMailbox.index , pwm, pwm_frequency);
|
|
if ((1 == XdrvMailbox.payload) || ((XdrvMailbox.payload >= PWM_MIN) && (XdrvMailbox.payload <= PWM_MAX))) {
|
|
pwm_frequency = (1 == XdrvMailbox.payload) ? PWM_FREQ : XdrvMailbox.payload;
|
|
|
|
if (pwm >= 0 && PinUsed(GPIO_PWM1, pwm)) {
|
|
analogWriteFreq(pwm_frequency, Pin(GPIO_PWM1, pwm));
|
|
} else {
|
|
// apply to all default PWM
|
|
// AddLog(LOG_LEVEL_INFO, "PWM: apply global freq=%i", pwm_frequency);
|
|
Settings->pwm_frequency = pwm_frequency;
|
|
analogWriteFreq(pwm_frequency); // Default is 977
|
|
}
|
|
#ifdef USE_LIGHT
|
|
LightReapplyColor();
|
|
LightAnimate();
|
|
#endif // USE_LIGHT
|
|
}
|
|
ResponseCmndNumber(pwm_frequency);
|
|
}
|
|
|
|
#else // now for ESP8266
|
|
|
|
void PwmRearmChanges(void) {}
|
|
|
|
void CmndPwm(void)
|
|
{
|
|
if (TasmotaGlobal.pwm_present && (XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_PWMS)) {
|
|
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= Settings->pwm_range) && PinUsed(GPIO_PWM1, XdrvMailbox.index -1)) {
|
|
uint32_t pwm_index = XdrvMailbox.index - 1;
|
|
if (pwm_index < MAX_PWMS_LEGACY) { // write in the appropriate settings pool
|
|
Settings->pwm_value[pwm_index] = XdrvMailbox.payload;
|
|
} else {
|
|
Settings->pwm_value_ext[pwm_index - MAX_PWMS_LEGACY] = XdrvMailbox.payload;
|
|
}
|
|
AnalogWrite(Pin(GPIO_PWM1, pwm_index), bitRead(TasmotaGlobal.pwm_inverted, pwm_index) ? Settings->pwm_range - XdrvMailbox.payload : XdrvMailbox.payload);
|
|
}
|
|
Response_P(PSTR("{"));
|
|
MqttShowPWMState(); // Render the PWM status to MQTT
|
|
ResponseJsonEnd();
|
|
}
|
|
}
|
|
|
|
void GpioInitPwm(void) {
|
|
for (uint32_t pin = 0; pin < MAX_GPIO_PIN; pin++) {
|
|
analog_write_state[pin] = -1; // No PWM pin (could be GPIO_PWM or GPIO_LED)
|
|
}
|
|
for (uint32_t i = 0; i < MAX_PWMS; i++) { // Basic PWM control only
|
|
if (PinUsed(GPIO_PWM1, i)) {
|
|
pinMode(Pin(GPIO_PWM1, i), OUTPUT);
|
|
if (i < TasmotaGlobal.light_type) {
|
|
// force PWM GPIOs to low or high mode if belongs to the light (always <5), see #7165
|
|
AnalogWrite(Pin(GPIO_PWM1, i), bitRead(TasmotaGlobal.pwm_inverted, i) ? Settings->pwm_range : 0);
|
|
} else {
|
|
TasmotaGlobal.pwm_present = true;
|
|
if (i < MAX_PWMS_LEGACY) {
|
|
AnalogWrite(Pin(GPIO_PWM1, i), bitRead(TasmotaGlobal.pwm_inverted, i) ? Settings->pwm_range - Settings->pwm_value[i] : Settings->pwm_value[i]);
|
|
} else {
|
|
AnalogWrite(Pin(GPIO_PWM1, i), bitRead(TasmotaGlobal.pwm_inverted, i) ? Settings->pwm_range - Settings->pwm_value_ext[i] : Settings->pwm_value_ext[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/********************************************************************************************/
|
|
|
|
void ResetPwm(void)
|
|
{
|
|
for (uint32_t i = 0; i < MAX_PWMS; i++) { // Basic PWM control only
|
|
if (PinUsed(GPIO_PWM1, i)) {
|
|
AnalogWrite(Pin(GPIO_PWM1, i), bitRead(TasmotaGlobal.pwm_inverted, i) ? Settings->pwm_range : 0);
|
|
// AnalogWrite(Pin(GPIO_PWM1, i), bitRead(TasmotaGlobal.pwm_inverted, i) ? Settings->pwm_range - Settings->pwm_value[i] : Settings->pwm_value[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CmndPwmfrequency(void)
|
|
{
|
|
if ((1 == XdrvMailbox.payload) || ((XdrvMailbox.payload >= PWM_MIN) && (XdrvMailbox.payload <= PWM_MAX))) {
|
|
Settings->pwm_frequency = (1 == XdrvMailbox.payload) ? PWM_FREQ : XdrvMailbox.payload;
|
|
analogWriteFreq(Settings->pwm_frequency); // Default is 1000 (core_esp8266_wiring_pwm.c)
|
|
#ifdef USE_LIGHT
|
|
LightReapplyColor();
|
|
LightAnimate();
|
|
#endif // USE_LIGHT
|
|
}
|
|
ResponseCmndNumber(Settings->pwm_frequency);
|
|
}
|
|
|
|
#endif // ESP8266
|
|
|
|
void CmndPwmrange(void) {
|
|
// Support only 8 (=255), 9 (=511) and 10 (=1023) bits resolution
|
|
if ((1 == XdrvMailbox.payload) || ((XdrvMailbox.payload > 254) && (XdrvMailbox.payload < 1024))) {
|
|
uint32_t pwm_range = XdrvMailbox.payload;
|
|
uint32_t pwm_resolution = 0;
|
|
while (pwm_range) {
|
|
pwm_resolution++;
|
|
pwm_range >>= 1;
|
|
}
|
|
pwm_range = (1 << pwm_resolution) - 1;
|
|
uint32_t old_pwm_range = Settings->pwm_range;
|
|
Settings->pwm_range = (1 == XdrvMailbox.payload) ? PWM_RANGE : pwm_range;
|
|
for (uint32_t i = 0; i < MAX_PWMS; i++) {
|
|
if (i < MAX_PWMS_LEGACY) {
|
|
if (Settings->pwm_value[i] > Settings->pwm_range) {
|
|
Settings->pwm_value[i] = Settings->pwm_range;
|
|
}
|
|
} else {
|
|
if (Settings->pwm_value_ext[i - MAX_PWMS_LEGACY] > Settings->pwm_range) {
|
|
Settings->pwm_value_ext[i - MAX_PWMS_LEGACY] = Settings->pwm_range;
|
|
}
|
|
}
|
|
}
|
|
if (Settings->pwm_range != old_pwm_range) { // On ESP32 this prevents loss of duty state
|
|
analogWriteRange(Settings->pwm_range); // Default is 1023 (Arduino.h)
|
|
}
|
|
}
|
|
ResponseCmndNumber(Settings->pwm_range);
|
|
}
|
|
|
|
void MqttShowPWMState(void)
|
|
{
|
|
ResponseAppend_P(PSTR("\"" D_CMND_PWM "\":{"));
|
|
bool first = true;
|
|
for (uint32_t i = 0; i < MAX_PWMS; i++) { // TODO
|
|
if (PinUsed(GPIO_PWM1, i)) {
|
|
uint32_t pwm_val = (i < MAX_PWMS_LEGACY) ? Settings->pwm_value[i] : Settings->pwm_value_ext[i - MAX_PWMS_LEGACY];
|
|
ResponseAppend_P(PSTR("%s\"" D_CMND_PWM "%d\":%d"), first ? "" : ",", i+1, pwm_val);
|
|
first = false;
|
|
}
|
|
}
|
|
ResponseJsonEnd();
|
|
}
|