/* xdrv_15_pca9685_v2.ino - Support for I2C PCA9685 12bit 16 pin hardware PWM driver on Tasmota Copyright (C) 2021 Andre Thomas and Theo Arends 2023 Fabrizio Amodio 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_PCA9685_V2 /*********************************************************************************************\ * PCA9685 - 16-channel 12-bit pwm driver * * I2C Address: 0x40 .. 0x47 \*********************************************************************************************/ /* 2023-06-05 v2.0 Changelog by F.Amodio - Code Refactoring - Support for multiple PCA9685 without breaking support for previuos driver version all command (PWM,INVERT,etc) now support the suffix from 0 to PCA9685_MAX_COUNT to address the board, without the suffix the command is relative to the board 0, e.g. DRIVER15 PWM 0 350 Board address 0 pin 0 value 350 DRIVER15 PWM0 2 300 Board address 0 pin 2 value 300 DRIVER15 PWM1 3 235 Board address 1 pin 3 value 235 DRIVER15 RESET2 Reset Board 2 - new command INTCLK to fine tuning the internal clock setting, unit: 0.1 MHz this is a not permanent setting! DRIVER15 INTCLK 270 Set to 27.0 MHz DRIVER15 INTCLK 250 Set to 25.0 Mhz (default value, use USE_PCA9685_INT_CLOCK to change it at compile time) - new command PWMTO to move all the required pin from the current pin position to a new one, all the move will be completed into the required time, so each motor will be stepped relative to timing - new command PWMSTOP to stop all running movement Updated Command List: DRIVER15 STATUS // Will return a JSON string containing all the current settings / parameters for all board DRIVER15 RESET[0-8] // Reset to power-up settings - i.e. F=50hz and all pins in OFF state for a specific board DRIVER15 INVERT[0-8],pin[,0-1] // print or set the inversion bit on pin of the specific board DRIVER15 INTCLK[0-8],clock // where clock is the Interal Clock value in 1/10 MHz (default USE_PCA9685_INT_CLOCK = 250) DRIVER15 PMWF[0-8],frequency // where frequency is the PWM frequency from 24 to 1526 in Hz DRIVER15 PWM[0-8],pin,pwmvalue // where pin=LED, pin 0 through 15 and pwmvalue is the pulse width between 0 and 4096 DRIVER15 PWM[0-8],pin,ON // Fully turn a specific board/pin/LED ON DRIVER15 PWM[0-8],pin,OFF // Fully turn a specific board/pin/LED OFF DRIVER15 PWMTO[0-8] tensecs,pin,value[,pin,value[,pin,value...]] // Move all the specified pin to a new location in the specified time (1/10 sec resolution), if "tensecs" is zero it's equivalent to PWM command for all the pins e.g. PWMTO 40 0 327 1 550 2 187 3 200 this move the PIN0 of the Board 0 from the current position to 327 the PIN1 of the Board 0 from the current position to 550 the PIN2 of the Board 0 from the current position to 187 the PIN3 of the Board 0 from the current position to 200 all the movements will be completed in 4 seconds, every PIN will be stepped relative to that @ 50ms step. PWMTO1 40 0 327 1 550 2 187 3 200 same logic on the board #1 DRIVER15 PWMSTOP[0-8] // stop all the moment on the relative board */ #define XDRV_15 15 #define XI2C_01 1 // See I2CDEVICES.md /* default prescale value from datasheet 7.3.5 round(25000000/(4096*freq))-1; */ #ifndef USE_PCA9685_INT_CLOCK #define USE_PCA9685_INT_CLOCK 250 #endif #ifndef USE_PCA9685_ADDR #define USE_PCA9685_ADDR 0x40 #endif #ifndef USE_PCA9685_FREQ #define USE_PCA9685_FREQ 50 #endif #ifndef PCA9685_MAX_COUNT #define PCA9685_MAX_COUNT 4 #endif #define PCA9685_REG_MODE1 0x00 #define PCA9685_REG_LED0_ON_L 0x06 #define PCA9685_REG_PRE_SCALE 0xFE typedef struct { uint16_t pwm; bool running; uint16_t step; int16_t every; uint16_t target; int16_t direction; // 1 == UP , 0 == stop; -1 == down } tMotor; struct PCA9685 { uint8_t count; char name[10]; bool inverted[PCA9685_MAX_COUNT]; bool detected[PCA9685_MAX_COUNT]; uint16_t intclk[PCA9685_MAX_COUNT]; uint16_t freq[PCA9685_MAX_COUNT]; tMotor motor[PCA9685_MAX_COUNT][16]; } pca9685; #include Ticker TickerPCA9685; void PCA9685_SetName(uint8_t pca) { if (pca9685.count > 1) { pca9685.name[7] = IndexSeparator(); pca9685.name[8] = '0' + pca; pca9685.name[9] = 0; } else { pca9685.name[7] = 0; } } void PCA9685_Detect(void) { memset(&pca9685, 0x0, sizeof(PCA9685)); strcpy_P(pca9685.name, PSTR("PCA9685")); for (uint8_t dev = 0; dev < PCA9685_MAX_COUNT; dev++) { uint32_t addr = USE_PCA9685_ADDR + dev; if (!I2cSetDevice(addr)) { continue; } pca9685.freq[dev] = USE_PCA9685_FREQ; pca9685.intclk[dev] = USE_PCA9685_INT_CLOCK; uint8_t buffer; if (I2cValidRead8(&buffer, addr, PCA9685_REG_MODE1)) { I2cWrite8(addr, PCA9685_REG_MODE1, 0x20); if (I2cValidRead8(&buffer, addr, PCA9685_REG_MODE1)) { if (0x20 == buffer) { // AddLog(LOG_LEVEL_DEBUG, "PCA9685[%02x] found", addr); pca9685.count++; pca9685.detected[dev] = true; I2cSetActiveFound(addr, PSTR("PCA9685")); PCA9685_Reset(dev); // Reset the controller } } } } if (pca9685.count > 0) { TickerPCA9685.attach_ms(50, PCA9685_RunMotor); } } void PCA9685_Reset(uint8_t pca) { if (!pca9685.detected[pca]) { return; } PCA9685_SetName(pca); I2cWrite8(USE_PCA9685_ADDR + pca, PCA9685_REG_MODE1, 0x80); PCA9685_SetPWMfreq(pca, USE_PCA9685_FREQ); pca9685.inverted[pca] = false; for (uint32_t pin = 0; pin < 16; pin++) { PCA9685_SetPWM(pca, pin, 0, pca9685.inverted[pca]); pca9685.motor[pca][pin].pwm = PCA9685_GetPWMvalue(0, pca9685.inverted[pca]); } Response_P(PSTR("{\"%s\":"), pca9685.name); ResponseAppend_P(S_JSON_COMMAND_SVALUE, D_CMND_RESET, PSTR("OK")); ResponseJsonEnd(); } uint16_t PCA9685_GetPWMvalue(uint16_t pwm, bool inverted) { uint16_t pwm_val = pwm; if (inverted) { pwm_val = 4096 - pwm; } return pwm_val; } void PCA9685_SetPWMfreq(uint8_t pca, double freq) { if (freq > 23 && freq < 1527) { pca9685.freq[pca] = freq; } else { pca9685.freq[pca] = 50; } uint8_t pre_scale_osc = round((pca9685.intclk[pca] * 100000) / (4096 * pca9685.freq[pca])) - 1; if (1526 == pca9685.freq[pca]) pre_scale_osc = 0xFF; // force setting for 24hz because rounding causes 1526 to be 254 uint8_t current_mode1 = I2cRead8(USE_PCA9685_ADDR + pca, PCA9685_REG_MODE1); // read current value of MODE1 register uint8_t sleep_mode1 = (current_mode1 & 0x7F) | 0x10; // Determine register value to put PCA to sleep I2cWrite8(USE_PCA9685_ADDR + pca, PCA9685_REG_MODE1, sleep_mode1); // Let's sleep a little I2cWrite8(USE_PCA9685_ADDR + pca, PCA9685_REG_PRE_SCALE, pre_scale_osc); // Set the pre-scaler I2cWrite8(USE_PCA9685_ADDR + pca, PCA9685_REG_MODE1, current_mode1 | 0xA0); // Reset MODE1 register to original state and enable auto increment } void PCA9685_SetPWM_Reg(uint8_t pca, uint8_t pin, uint16_t on, uint16_t off) { uint8_t led_reg = PCA9685_REG_LED0_ON_L + 4 * pin; uint32_t led_data = 0; I2cWrite8(USE_PCA9685_ADDR + pca, led_reg, on); I2cWrite8(USE_PCA9685_ADDR + pca, led_reg + 1, (on >> 8)); I2cWrite8(USE_PCA9685_ADDR + pca, led_reg + 2, off); I2cWrite8(USE_PCA9685_ADDR + pca, led_reg + 3, (off >> 8)); } void PCA9685_SetPWM(uint8_t pca, uint8_t pin, uint16_t pwm, bool inverted) { uint16_t pwm_val = PCA9685_GetPWMvalue(pwm, inverted); if (4096 == pwm_val) { PCA9685_SetPWM_Reg(pca, pin, 4096, 0); // Special use additional bit causes channel to turn on completely without PWM } else { PCA9685_SetPWM_Reg(pca, pin, 0, pwm_val); } pca9685.motor[pca][pin].pwm = pwm_val; } void PCA9685_RunMotor() { for (uint8_t dev = 0; dev < PCA9685_MAX_COUNT; dev++) { if (!pca9685.detected[dev]) continue; for (uint8_t pin = 0; pin < 15; pin++) { tMotor *m = &(pca9685.motor[dev][pin]); if (!m->running) continue; if (m->every == -1 || (m->direction > 0 && m->pwm >= m->target) || (m->direction < 0 && m->pwm <= m->target)) { m->running = false; if (m->pwm != m->target) { PCA9685_SetPWM(dev, pin, m->target, pca9685.inverted[dev]); } continue; } if (m->step == 0 || (m->step % m->every == 0)) { // AddLog(LOG_LEVEL_DEBUG, "PCA9685: MOTOR dev=%u pin=%u s=%u e=%u pwm=%lu target=%lu dir=%d", // dev, // pin, // m->step, // m->every, // m->pwm, // m->target, // m->direction); PCA9685_SetPWM(dev, pin, m->pwm + m->direction, pca9685.inverted[dev]); } m->step++; } } } void PCA9685_getCmdSuffix(char *command, uint8_t *suffixNumber) { size_t commandLength = strlen(command); uint8_t result = 0; *suffixNumber = 0; if (isdigit(command[commandLength - 1])) { result = command[commandLength - 1] - '0'; if (result >= 0 && result <= 7) { *suffixNumber = result; command[commandLength - 1] = '\0'; } } } bool PCA9685_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); char command[CMDSZ] = {0}; char pcaName[10]; uint8_t dev; PCA9685_getCmdSuffix(ArgV(command, 1), &dev); if (!strcmp(command, "RESET")) { PCA9685_Reset(dev); return serviced; } if (!strcmp(command, "STATUS")) { PCA9685_OutputTelemetry(false); return serviced; } PCA9685_SetName(dev); if (!strcmp(command, "INVERT")) { if (paramcount > 1) { pca9685.inverted[dev] = (1 == atoi(ArgV(argument, 2))); Response_P(PSTR("{\"%s\":{\"INVERT\":%i, \"Result\":\"OK\"}}"), pca9685.name, pca9685.inverted[dev] ? 1 : 0); return serviced; } else { // No parameter was given for invert, so we return current setting Response_P(PSTR("{\"%s\":{\"INVERT\":%i}}"), pca9685.name, pca9685.inverted[dev] ? 1 : 0); return serviced; } } if (!strcmp(command, "INTCLK")) { if (paramcount > 1) { pca9685.intclk[dev] = atoi(ArgV(argument, 2)); Response_P(PSTR("{\"%s\":{\"INTCLK\":%lu, \"Result\":\"OK\"}}"), pca9685.name, pca9685.intclk[dev]); return serviced; } else { Response_P(PSTR("{\"%s\":{\"INTCLK\":%lu}}"), pca9685.name, pca9685.intclk[dev]); return serviced; } } /* PWMTO timeinsec,pin,target[[,pin,target]...] */ if (!strcmp(command, "PWMTO")) { uint8_t paramFrom = 1; while (true) { if (paramcount > (2 + paramFrom)) { uint16_t tids = atoi(ArgV(argument, 2)); // time in 1/10 of second to complete all the motors move uint16_t pin = atoi(ArgV(argument, 2 + paramFrom)); /* Sanity check - To be refactored */ if (pin > 15) pin = 15; if (tids < 2) tids = 0; // min 2/10 seconds to complete all the moves if (tids > 600) tids = 600; // max 60 seconds to complete all the moves tMotor *m = &pca9685.motor[dev][pin]; m->target = atoi(ArgV(argument, 2 + paramFrom + 1)); if (m->target != m->pwm) { m->step = 0; m->direction = m->target < m->pwm ? -1 : 1; if( tids == 0 ) { m->every = -1; m->running = true; } else { // AddLog(LOG_LEVEL_DEBUG, "PCA9685: PWMTO dev=%u pin=%u tids=%u e=? pwm=%lu target=%lu dir=%d", // dev, // pin, // tids, // m->pwm, // m->target, // m->direction); m->every = 0; while (m->every < 1) { uint16_t stepValue = abs((int16_t)m->pwm - (int16_t)m->target) / abs(m->direction); if (stepValue < 1) { m->direction += m->target < m->pwm ? -1 : 1; continue; } m->every = round((tids * 200) / stepValue); if (m->every < 1) { m->direction += m->target < m->pwm ? -1 : 1; continue; } } m->running = true; } } else { m->running = false; } paramFrom += 2; } else { break; } } Response_P(PSTR("{\"%s\":{\"PWMTO\":\"OK\"}}"), pca9685.name); return serviced; } if (!strcmp(command, "PWMSTOP")) { if (pca9685.detected[dev]) { for (uint8_t pin = 0; pin < 15; pin++) { pca9685.motor[dev][pin].running = false; } Response_P(PSTR("{\"%s\":{\"PWMSTOP\":\"OK\"}}"), pca9685.name); return serviced; } } if (!strcmp(command, "PWMF")) { if (paramcount > 1) { uint16_t new_freq = atoi(ArgV(argument, 2)); if ((new_freq >= 24) && (new_freq <= 1526)) { PCA9685_SetPWMfreq(dev, new_freq); Response_P(PSTR("{\"%s\":{\"PWMF\":%i, \"Result\":\"OK\"}}"), pca9685.name, new_freq); return serviced; } } else { // No parameter was given for setfreq, so we return current setting Response_P(PSTR("{\"%s\":{\"PWMF\":%i}}"), pca9685.name, pca9685.freq[dev]); return serviced; } } if (!strcmp(command, "PWM")) { if (paramcount > 1) { uint8_t pin = atoi(ArgV(argument, 2)); if (paramcount > 2) { // force motor stop pca9685.motor[dev][pin].running = false; if (!strcmp(ArgV(argument, 3), "ON")) { PCA9685_SetPWM(dev, pin, 4096, pca9685.inverted[dev]); Response_P(PSTR("{\"%s\":{\"PIN\":%i,\"PWM\":%i}}"), pca9685.name, pin, 4096); serviced = true; return serviced; } if (!strcmp(ArgV(argument, 3), "OFF")) { PCA9685_SetPWM(dev, pin, 0, pca9685.inverted[dev]); Response_P(PSTR("{\"%s\":{\"PIN\":%i,\"PWM\":%i}}"), pca9685.name, pin, 0); serviced = true; return serviced; } uint16_t pwm = atoi(ArgV(argument, 3)); if ((pin >= 0 && pin <= 15 || pin == 61) && (pwm >= 0 && pwm <= 4096)) { PCA9685_SetPWM(dev, pin, pwm, pca9685.inverted[dev]); Response_P(PSTR("{\"%s\":{\"PIN\":%i,\"PWM\":%i}}"), pca9685.name, pin, pwm); serviced = true; return serviced; } } } } return serviced; } void PCA9685_OutputTelemetry(bool telemetry) { ResponseTime_P(PSTR("")); for (uint8_t dev = 0; dev < PCA9685_MAX_COUNT; dev++) { if (!pca9685.detected[dev]) { continue; } PCA9685_SetName(dev); ResponseAppend_P(PSTR(",\"%s\":{\"PWM_FREQ\":%i"), pca9685.name, pca9685.freq[dev]); ResponseAppend_P(PSTR(",\"INVERT\":%i"), pca9685.inverted[dev] ? 1 : 0); ResponseAppend_P(PSTR(",\"INTCLK\":%lu"), pca9685.intclk[dev]); for (uint32_t pin = 0; pin < 16; pin++) { uint16_t pwm_val = PCA9685_GetPWMvalue(pca9685.motor[dev][pin].pwm, pca9685.inverted[dev]); // return logical (possibly inverted) pwm value ResponseAppend_P(PSTR(",\"PWM%i\":%i"), pin, pwm_val); } ResponseJsonEnd(); } ResponseJsonEnd(); if (telemetry) { MqttPublishTeleSensor(); } } bool Xdrv15(uint32_t function) { if (!I2cEnabled(XI2C_01)) { return false; } bool result = false; if (FUNC_INIT == function) { PCA9685_Detect(); } else if (pca9685.count > 0) { switch (function) { case FUNC_EVERY_SECOND: if (TasmotaGlobal.tele_period == 0) { PCA9685_OutputTelemetry(true); } break; case FUNC_COMMAND_DRIVER: if (XDRV_15 == XdrvMailbox.index) { result = PCA9685_Command(); } break; } } return result; } #endif // USE_PCA9685_V2 #endif // USE_IC2