diff --git a/BUILDS.md b/BUILDS.md index d17b57901..21c27b038 100644 --- a/BUILDS.md +++ b/BUILDS.md @@ -121,6 +121,7 @@ Note: `minimal` variant is not listed as it shouldn't be used outside of the [up | USE_LM75AD | - | - / x | - | x | - | - | | USE_APDS9960 | - | - / - | - | - | - | - | | USE_MCP230xx | - | - / - | - | - | - | - | +| USE_PCA9632 | - | - / - | - | - | - | - | | USE_PCA9685 | - | - / - | - | - | - | - | | USE_MPR121 | - | - / - | - | - | - | - | | USE_CCS811 | - | - / - | - | x | - | - | diff --git a/I2CDEVICES.md b/I2CDEVICES.md index 744c3ccbf..9c9da3c0d 100644 --- a/I2CDEVICES.md +++ b/I2CDEVICES.md @@ -109,3 +109,4 @@ Index | Define | Driver | Device | Address(es) | Description 72 | USE_INA3221 | xsns_100 | INA3221 | 0x40-0x43 | 3-channels Voltage and Current sensor 73 | USE_HMC5883L | xsns_101 | HMC5883L | 0x1E | 3-channels Magnetic Field Sensor 74 | USE_DISPLAY_TM1650 | xdsp_20 | TM1650 | 0x24 - 0x27, 0x34 - 0x37 | Four-digit seven-segment LED controller + 75 | USE_PCA9632 | xdrv_91 | PCA9632 | 0x60 | 4-channel 4-bit pwm driver diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 4fe411b69..e95f42e14 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -632,6 +632,12 @@ // #define USE_PCA9685 // [I2cDriver1] Enable PCA9685 I2C HW PWM Driver - Must define I2C Address in #define USE_PCA9685_ADDR below - range 0x40 - 0x47 (+1k4 code) // #define USE_PCA9685_ADDR 0x40 // Enable PCA9685 I2C Address to use (Must be within range 0x40 through 0x47 - set according to your wired setup) // #define USE_PCA9685_FREQ 50 // Define default PWM frequency in Hz to be used (must be within 24 to 1526) - If other value is used, it will rever to 50Hz +// #define USE_PCA9632 // [I2cDriver75] Enable PCA9632 I2C HW PWM Driver +// #define USE_PCA9632_ADDR 0x60 // Define PCA9685 I2C Address to use (Must be within range 0x60 through 0x63 - set according to your wired setup) +// #define USE_PCA9632_CM_0 0 // Mapping for channel 0 +// #define USE_PCA9632_CM_1 1 // Mapping for channel 1 +// #define USE_PCA9632_CM_2 2 // Mapping for channel 2 +// #define USE_PCA9632_CM_3 3 // Mapping for channel 3 // #define USE_MPR121 // [I2cDriver23] Enable MPR121 controller (I2C addresses 0x5A, 0x5B, 0x5C and 0x5D) in input mode for touch buttons (+1k3 code) // #define USE_CCS811 // [I2cDriver24] Enable CCS811 sensor (I2C address 0x5A) (+2k2 code) // #define USE_CCS811_V2 // [I2cDriver24] Enable CCS811 sensor (I2C addresses 0x5A and 0x5B) (+2k8 code) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_91_pca9632.ino b/tasmota/tasmota_xdrv_driver/xdrv_91_pca9632.ino new file mode 100644 index 000000000..acf7d1c60 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_91_pca9632.ino @@ -0,0 +1,275 @@ +/* + xdrv_91_pca9632.ino - Support for I2C PCA9632 4-channel 8-bit hardware PWM driver on Tasmota + + Copyright (C) 2022 Pascal Heinrich + + 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 . +*/ + +#ifdef USE_I2C +#ifdef USE_PCA9632 +/*********************************************************************************************\ + * PCA9632 - 4-channel 8-bit pwm driver + * + * I2C Address: 0x60 .. 0x63 +\*********************************************************************************************/ + +#define XDRV_91 91 +#define XI2C_75 75 // See I2CDEVICES.md + +#define PCA9632_REG_MODE1 0x00 +#define PCA9632_REG_MODE2 0x01 +#define PCA9632_REG_PWM_BASE 0x02 +#define PCA9632_REG_PWM_1 PCA9632_REG_PWM_BASE + 0 +#define PCA9632_REG_PWM_2 PCA9632_REG_PWM_BASE + 1 +#define PCA9632_REG_PWM_3 PCA9632_REG_PWM_BASE + 2 +#define PCA9632_REG_PWM_4 PCA9632_REG_PWM_BASE + 3 +#define PCA9632_REG_GRPPWM 0x06 +#define PCA9632_REG_GRPGREQ 0x07 +#define PCA9632_REG_LEDOUT 0x08 +#define PCA9632_AUTO_INC 0x80 + +#ifndef USE_PCA9632_ADDR + #define USE_PCA9632_ADDR 0x62 +#endif + +#ifndef USE_PCA9632_CM_0 + #define USE_PCA9632_CM_0 0 +#endif + +#ifndef USE_PCA9632_CM_1 + #define USE_PCA9632_CM_1 1 +#endif + +#ifndef USE_PCA9632_CM_2 + #define USE_PCA9632_CM_2 2 +#endif + +#ifndef USE_PCA9632_CM_3 + #define USE_PCA9632_CM_3 3 +#endif + +bool pca9632_inverted = false; // invert PWM for open-collector load +bool pca9632_detected = false; +uint8_t pca9632_pin_pwm_value[4]; + +bool PCA9632_Detect(void) { + if (I2cSetDevice(USE_PCA9632_ADDR)) { + uint8_t buffer; + if (I2cValidRead8(&buffer, USE_PCA9632_ADDR, PCA9632_REG_MODE1)) { + I2cWrite8(USE_PCA9632_ADDR, PCA9632_REG_MODE1, 0x10); + if (I2cValidRead8(&buffer, USE_PCA9632_ADDR, PCA9632_REG_MODE1)) { + if (0x10 == buffer) { + I2cSetActiveFound(USE_PCA9632_ADDR, "PCA9632"); + PCA9632_Reset(); // Reset the controller + return pca9632_detected = true; + } + } + } + } + + return false; +} + +void PCA9632_Init(void) { + // configure none inverted and totem pole + I2cWrite8(USE_PCA9632_ADDR, PCA9632_REG_MODE2, 0x14); + + // turn off sleep mode + I2cWrite8(USE_PCA9632_ADDR, PCA9632_REG_MODE1, 0x1); +} + +void PCA9632_Reset(void) { + I2cWrite8(USE_PCA9632_ADDR, PCA9632_REG_MODE1, 0x6); + pca9632_inverted = false; + for (uint8_t pin = 0; pin < 4; pin++) { + PCA9632_SetPWM(pin, 0); + pca9632_pin_pwm_value[pin] = 0; + } + Response_P(PSTR("{\"PCA9632\":{\"RESET\":\"OK\"}}")); +} + +bool PCA9632_SetInvert(bool on) { + uint8_t buffer; + if(I2cValidRead8(&buffer, USE_PCA9632_ADDR, PCA9632_REG_MODE2)) { + I2cWrite8(USE_PCA9632_ADDR, PCA9632_REG_MODE2, buffer | ((on ? 1 : 0) >> 4)); + } + return on; +} + +bool PCA9632_SetPWM(uint8_t pin, uint8_t pwm) { + + uint8_t pin_mapping = PCA9632_PinMapping(pin); + I2cWrite8(USE_PCA9632_ADDR, PCA9632_REG_PWM_BASE + pin_mapping, pwm); + pca9632_pin_pwm_value[pin_mapping] = pwm; + + return pwm > 0; + +} + +bool PCA9632_SetPWM_Bulk(uint8_t *pwm, uint16_t len) { + uint8_t buffer[4]; + + // map the pwm values to the final pins + for (uint8_t pin = 0; pin < 4; pin++) { + uint8_t pin_mapping = PCA9632_PinMapping(pin); + buffer[pin_mapping] = pwm[pin]; + } + + I2cWriteBuffer(USE_PCA9632_ADDR, PCA9632_REG_PWM_BASE | PCA9632_AUTO_INC, buffer, len); + + // set the pwm values for later use + bool enable = false; + for (uint8_t pin = 0; pin < 4; pin++) { + uint8_t value = buffer[pin]; + + pca9632_pin_pwm_value[pin] = value; + + if (value > 0) { + enable |= true; + } + } + + return enable; +} + +void PCA9632_Enable(bool enable) { + DEBUG_TRACE_LOG(PSTR("DRV: PCA9632 enable %d"), enable); + I2cWrite8(USE_PCA9632_ADDR, PCA9632_REG_LEDOUT, enable ? 0xFF : 0x0); +} + +bool PCA9632_Command(void) { + bool serviced = true; + bool validpin = false; + uint8_t paramcount = 0; + if (XdrvMailbox.data_len > 0) { + paramcount = 1; + } else { + serviced = false; + return serviced; + } + char argument[XdrvMailbox.data_len]; + for (uint32_t ca = 0; ca < XdrvMailbox.data_len; ca++) { + if ((' ' == XdrvMailbox.data[ca]) || ('=' == XdrvMailbox.data[ca])) { XdrvMailbox.data[ca] = ','; } + if (',' == XdrvMailbox.data[ca]) { paramcount++; } + } + UpperCase(XdrvMailbox.data, XdrvMailbox.data); + + if (!strcmp(ArgV(argument, 1),"RESET")) { PCA9632_Reset(); return serviced; } + + if (!strcmp(ArgV(argument, 1),"STATUS")) { PCA9632_OutputTelemetry(false); return serviced; } + + if (!strcmp(ArgV(argument, 1),"INVERT")) { + if (paramcount > 1) { + pca9632_inverted = PCA9632_SetInvert(1 == atoi(ArgV(argument, 2))); + Response_P(PSTR("{\"PCA9632\":{\"INVERT\":%i, \"Result\":\"OK\"}}"), pca9632_inverted); + return serviced; + } else { // No parameter was given for invert, so we return current setting + Response_P(PSTR("{\"PCA9632\":{\"INVERT\":%i}}"), pca9632_inverted); + return serviced; + } + } + if (!strcmp(ArgV(argument, 1),"PWM")) { + if (paramcount > 1) { + uint8_t pin = atoi(ArgV(argument, 2)); + if (paramcount > 2) { + if (!strcmp(ArgV(argument, 3), "ON")) { + PCA9632_SetPWM(pin, 255); + Response_P(PSTR("{\"PCA9632\":{\"PIN\":%i,\"PWM\":%i}}"), pin, 255); + serviced = true; + return serviced; + } + if (!strcmp(ArgV(argument, 3), "OFF")) { + PCA9632_SetPWM(pin, 0); + Response_P(PSTR("{\"PCA9632\":{\"PIN\":%i,\"PWM\":%i}}"), pin, 0); + serviced = true; + return serviced; + } + uint16_t pwm = atoi(ArgV(argument, 3)); + if ((pin >= 0 && pin <= 3) && (pwm >= 0 && pwm <= 255)) { + PCA9632_SetPWM(pin, pwm); + Response_P(PSTR("{\"PCA9632\":{\"PIN\":%i,\"PWM\":%i}}"), pin, pwm); + serviced = true; + return serviced; + } + } + } + } + if (!strcmp(ArgV(argument, 1),"ENABLE")) { + PCA9632_Enable(true); + Response_P(PSTR("{\"PCA9632\":{\"ENABLE\":true}}")); + } + if (!strcmp(ArgV(argument, 1),"DISABLE")) { + PCA9632_Enable(false); + Response_P(PSTR("{\"PCA9632\":{\"ENABLE\":false}}")); + } + return serviced; +} + +void PCA9632_OutputTelemetry(bool telemetry) { + ResponseAppend_P(PSTR("\"INVERT\":%i,"), pca9632_inverted?1:0); + for (uint32_t pin = 0; pin < 4; pin++) { + uint8_t pin_mapping = PCA9632_PinMapping(pin); + ResponseAppend_P(PSTR("\"PWM%i\":%i,"), pin_mapping, pca9632_pin_pwm_value[pin_mapping]); + } + ResponseAppend_P(PSTR("\"END\":1}}")); + if (telemetry) { + MqttPublishTeleSensor(); + } +} + +uint8_t PCA9632_PinMapping(uint8_t pin) { + switch(pin) { + case 0: + return USE_PCA9632_CM_0; + case 1: + return USE_PCA9632_CM_1; + case 2: + return USE_PCA9632_CM_2; + case 3: + return USE_PCA9632_CM_3; + default: + return USE_PCA9632_CM_0; + } +} + +bool Xdrv91(uint32_t function) { + if (!I2cEnabled(XI2C_75)) { return false; } + + bool result = false; + + if (FUNC_INIT == function) { + if (PCA9632_Detect()) { + PCA9632_Init(); + } + } + else if (pca9632_detected) { + switch (function) { + case FUNC_EVERY_SECOND: + if (TasmotaGlobal.tele_period == 0) { + PCA9632_OutputTelemetry(true); + } + break; + case FUNC_COMMAND_DRIVER: + if (XDRV_91 == XdrvMailbox.index) { + result = PCA9632_Command(); + } + break; + } + } + return result; +} + +#endif // USE_PCA9632 +#endif // USE_IC2 diff --git a/tasmota/tasmota_xlgt_light/xlgt_11_pca9632.ino b/tasmota/tasmota_xlgt_light/xlgt_11_pca9632.ino new file mode 100644 index 000000000..5d408a607 --- /dev/null +++ b/tasmota/tasmota_xlgt_light/xlgt_11_pca9632.ino @@ -0,0 +1,79 @@ +/* + xlgt_11_pca9632.ino - pca9632 four channel led support for Tasmota + + Copyright (C) 2021 Theo Arends + + 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 . +*/ + +#ifdef USE_LIGHT +#ifdef USE_PCA9632 +/*********************************************************************************************\ + * PCA9632 - Controlling RGBW over I2C + * Copyright (C) 2022 Pascal Heinrich + * +\*********************************************************************************************/ + +#define XLGT_11 11 + +/********************************************************************************************/ + +bool PCA9632_SetChannels(void) { + uint8_t *cur_col = (uint8_t*)XdrvMailbox.data; + + DEBUG_TRACE_LOG(PSTR("LGT: PCA9632 %d - %d - %d - %d"), cur_col[0], cur_col[1], cur_col[2], cur_col[3]); + + PCA9632_Enable(PCA9632_SetPWM_Bulk(cur_col, 4)); + + return true; +} + +bool PCA9632_ModuleSelected(void) { + DEBUG_TRACE_LOG(PSTR("LGT: PCA9632 ModuleSelected")); + + if (PCA9632_Detect()) { + PCA9632_Init(); + + TasmotaGlobal.light_type += LST_RGBW; // Add RGBW to be controlled by PCA9632 + TasmotaGlobal.light_driver = XLGT_11; + + AddLog(LOG_LEVEL_INFO, PSTR("LGT: PCA9632 Found")); + + return true; + } + + return false; +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xlgt11(uint32_t function) { + bool result = false; + + switch (function) { + case FUNC_SET_CHANNELS: + result = PCA9632_SetChannels(); + break; + case FUNC_MODULE_INIT: + result = PCA9632_ModuleSelected(); + break; + } + return result; +} + +#endif // USE_PCA9632 +#endif // USE_LIGHT +