From 4992b4368916e7a8cdb8a35b08fee75071197379 Mon Sep 17 00:00:00 2001 From: andrethomas Date: Fri, 21 Sep 2018 23:43:58 +0200 Subject: [PATCH] PCA9685 I2C Hardware PWM - Experimental --- sonoff/user_config.h | 2 + sonoff/xdrv_15_pca9685.ino | 184 +++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 sonoff/xdrv_15_pca9685.ino diff --git a/sonoff/user_config.h b/sonoff/user_config.h index 438983a66..ee374d41c 100644 --- a/sonoff/user_config.h +++ b/sonoff/user_config.h @@ -298,6 +298,8 @@ // #define USE_MCP230xx_ADDR 0x20 // Enable MCP23008/MCP23017 I2C Address to use (Must be within range 0x20 through 0x27 - set according to your wired setup) // #define USE_MCP230xx_OUTPUT // Enable MCP23008/MCP23017 OUTPUT support through sensor29 commands (+1k5 code) // #define USE_MCP230xx_DISPLAYOUTPUT // Enable MCP23008/MCP23017 to display state of OUTPUT pins on Web UI (+0k2 code) +// #define USE_PCA9685 // 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 MCP23008/MCP23017 I2C Address to use (Must be within range 0x20 through 0x27 - set according to your wired setup) // #define USE_MPR121 // Enable MPR121 controller (I2C addresses 0x5A, 0x5B, 0x5C and 0x5D) in input mode for touch buttons (+1k3 code) // #define USE_CCS811 // Enable CCS811 sensor (I2C address 0x5A) (+2k2 code) // #define USE_MPU6050 // Enable MPU6050 sensor (I2C address 0x68 AD0 low or 0x69 AD0 high) (+2k6 code) diff --git a/sonoff/xdrv_15_pca9685.ino b/sonoff/xdrv_15_pca9685.ino new file mode 100644 index 000000000..e53b8d17f --- /dev/null +++ b/sonoff/xdrv_15_pca9685.ino @@ -0,0 +1,184 @@ +/* + xdrv_15_pca9685.ino - Support for I2C PCA9685 12bit 16 pin hardware PWM driver + + Copyright (C) 2018 Andre Thomas and 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_I2C +#ifdef USE_PCA9685 + +#define XDRV_15 15 + +#define PCA9685_REG_MODE1 0x00 +#define PCA9685_REG_LED0_ON_L 0x06 +#define PCA9685_REG_PRE_SCALE 0xFE + +uint8_t pca9685_detected = 0; +uint16_t pca9685_freq = 50; + +void PCA9685_Detect(void) +{ + if (pca9685_detected) { return; } + + uint8_t buffer; + + if (I2cValidRead8(&buffer, USE_PCA9685_ADDR, PCA9685_REG_MODE1)) { + I2cWrite8(USE_PCA9685_ADDR, PCA9685_REG_MODE1, 0x20); + if (I2cValidRead8(&buffer, USE_PCA9685_ADDR, PCA9685_REG_MODE1)) { + if (0x20 == buffer) { + pca9685_detected = 1; + snprintf_P(log_data, sizeof(log_data), S_LOG_I2C_FOUND_AT, "PCA9685", USE_PCA9685_ADDR); + AddLog(LOG_LEVEL_DEBUG); + PCA9685_Reset(); // Reset the controller + } + } + } +} + +void PCA9685_Reset(void) +{ + I2cWrite8(USE_PCA9685_ADDR, PCA9685_REG_MODE1, 0x80); + PCA9685_SetPWMfreq(50); + for (uint8_t pin=0;pin<16;pin++) { + PCA9685_SetPWM(pin,0,false); + } + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"PCA9685\":{\"RESET\":\"OK\"}}")); +} + +void PCA9685_SetPWMfreq(double freq) { +/* + 7.3.5 from datasheet + prescale value = round(25000000/(4096*freq))-1; + */ + pca9685_freq=freq; + uint8_t pre_scale_osc = round(25000000/(4096*freq))-1; + if (1526 == freq) pre_scale_osc=0xFF; // force setting for 24hz because rounding causes 1526 to be 254 + uint8_t current_mode1 = I2cRead8(USE_PCA9685_ADDR, 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, PCA9685_REG_MODE1, sleep_mode1); // Let's sleep a little + I2cWrite8(USE_PCA9685_ADDR, PCA9685_REG_PRE_SCALE, pre_scale_osc); // Set the pre-scaler + I2cWrite8(USE_PCA9685_ADDR, PCA9685_REG_MODE1, current_mode1 | 0xA0); // Reset MODE1 register to original state and enable auto increment +} + +void PCA9685_SetPWM_Reg(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, led_reg, on); + I2cWrite8(USE_PCA9685_ADDR, led_reg+1, (on >> 8)); + I2cWrite8(USE_PCA9685_ADDR, led_reg+2, off); + I2cWrite8(USE_PCA9685_ADDR, led_reg+3, (off >> 8)); +} + +void PCA9685_SetPWM(uint8_t pin, uint16_t pwm, bool inverted) { + if (4096 == pwm) { + PCA9685_SetPWM_Reg(pin, 4096, 0); // Special use additional bit causes channel to turn on completely without PWM + } else { + PCA9685_SetPWM_Reg(pin, 0, pwm); + } +} + +bool PCA9685_Command(void) +{ + boolean serviced = true; + boolean validpin = false; + uint8_t paramcount = 0; + if (XdrvMailbox.data_len > 0) { + paramcount=1; + } else { + serviced = false; + return serviced; + } + char sub_string[XdrvMailbox.data_len]; + for (uint8_t ca=0;ca 1) { + uint16_t new_freq = atoi(subStr(sub_string, XdrvMailbox.data, ",", 2)); + if ((new_freq >= 24) && (new_freq <= 1526)) { + PCA9685_SetPWMfreq(new_freq); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"PCA9685\":{\"PWMF\":%i, \"Result\":\"OK\"}}")); + return serviced; + } + } else { // No parameter was given for setfreq, so we return current setting + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"PCA9685\":{\"PWMF\":%i}}"),pca9685_freq); + return serviced; + } + } + if (!strcmp(subStr(sub_string, XdrvMailbox.data, ",", 1),"PWM")) { + if (paramcount > 1) { + uint8_t pin = atoi(subStr(sub_string, XdrvMailbox.data, ",", 2)); + if (paramcount > 2) { + if (!strcmp(subStr(sub_string, XdrvMailbox.data, ",", 3), "ON")) { + PCA9685_SetPWM(pin, 4096, false); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"PCA9685\":{\"PIN\":%i,\"PWM\":%i}}"),pin,4096); + serviced = true; + return serviced; + } + if (!strcmp(subStr(sub_string, XdrvMailbox.data, ",", 3), "OFF")) { + PCA9685_SetPWM(pin, 0, false); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"PCA9685\":{\"PIN\":%i,\"PWM\":%i}}"),pin,0); + serviced = true; + return serviced; + } + uint16_t pwm = atoi(subStr(sub_string, XdrvMailbox.data, ",", 3)); + if ((pin >= 0 && pin <= 15) && (pwm >= 0 && pwm <= 4096)) { + PCA9685_SetPWM(pin, pwm, false); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"PCA9685\":{\"PIN\":%i,\"PWM\":%i}}"),pin,pwm); + serviced = true; + return serviced; + } + } + } + } + return serviced; +} + +boolean Xdrv15(byte function) +{ + boolean result = false; + + if (i2c_flg) { + switch (function) { + case FUNC_MQTT_DATA: + break; + case FUNC_EVERY_SECOND: + PCA9685_Detect(); + break; + case FUNC_EVERY_50_MSECOND: + break; + case FUNC_JSON_APPEND: + break; + case FUNC_COMMAND: + if (XDRV_15 == XdrvMailbox.index) { + PCA9685_Command(); + } + break; + case FUNC_WEB_APPEND: + break; + default: + break; + } + } + return result; +} + +#endif // USE_PCA9685 +#endif // USE_IC2