diff --git a/components/lighting/CMakeLists.txt b/components/lighting/CMakeLists.txt new file mode 100644 index 0000000..ca9424d --- /dev/null +++ b/components/lighting/CMakeLists.txt @@ -0,0 +1,8 @@ +idf_component_register( + SRC_DIRS "src" + INCLUDE_DIRS "include" + REQUIRES freeRtosApi + esp_common + esp_driver_gpio + esp_driver_i2c +) diff --git a/components/lighting/include/i2cPort.hpp b/components/lighting/include/i2cPort.hpp new file mode 100644 index 0000000..74a3a27 --- /dev/null +++ b/components/lighting/include/i2cPort.hpp @@ -0,0 +1,50 @@ +/** + * @file i2cPort.hpp + * @author Gustice + * @brief C++ Wrapper for I2C-interface + * + * @copyright Copyright (c) 2024 + */ + +#pragma once + +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "portBase.hpp" +#include + +/** + * @brief I2C-Master Class + * @details More recent implementation of IDF-based I2C master + */ +class I2cMaster : public PortBase { + public: + class Slave { + public: + enum ClockRate { + _100k = 100000, + _400k = 400000, + }; + + Slave(I2cMaster &interface, uint8_t addr, ClockRate rate = _100k); + ~Slave(); + bool writeTo(const std::vector &data); + std::vector readFrom(const std::vector &address, unsigned size); + + protected: + const uint8_t Addr; + I2cMaster &Interface; + i2c_master_dev_handle_t _devHandle; + }; + + I2cMaster(i2c_port_t port, gpio_num_t sda, gpio_num_t scl); + ~I2cMaster() = default; + + bool probe(uint8_t addr); + + const i2c_port_t Port; + + private: + friend class Slave; + i2c_master_bus_handle_t _busHandle; +}; diff --git a/components/lighting/include/portBase.hpp b/components/lighting/include/portBase.hpp new file mode 100644 index 0000000..17ff2bf --- /dev/null +++ b/components/lighting/include/portBase.hpp @@ -0,0 +1,17 @@ +/** + * @file portBase.hpp + * @author Gustice + * @brief Base-class for all hardware ports + * + * @copyright Copyright (c) 2024 + */ +#pragma once + +class PortBase { + public: + PortBase(/* args */) {}; + + // Forbid Copy and Move assignments + PortBase(const PortBase &other) = delete; + PortBase &operator=(const PortBase &other) = delete; +}; diff --git a/components/lighting/include/pwmExpanderPca9685.hpp b/components/lighting/include/pwmExpanderPca9685.hpp new file mode 100644 index 0000000..49bc239 --- /dev/null +++ b/components/lighting/include/pwmExpanderPca9685.hpp @@ -0,0 +1,39 @@ +/** + * @file pwmExpanderPca9685.hpp + * @author Gustice + * @brief PWM-Expander PCA9685 + * @details https://www.nxp.com/docs/en/data-sheet/PCA9685.pdf + * + * @copyright Copyright (c) 2024 + */ +#pragma once + +#include "i2cPort.hpp" +#include +#include +#include + +/** + * @brief I2C-Connected LED-Driver with 16 ports + * @note This module can also be used to generate Servo-Motor signals + */ +class PmwExpanderPca9685 { + public: + static constexpr int PwmChannels = 16; + using GrayValues = std::array; + PmwExpanderPca9685(I2cMaster &port, bool inverted = false, uint8_t address = 0x40); // 0x40 default Address + + void sendImage(GrayValues &grayValues); + void reset(); + void setPwmFreq(float frequency, bool inverted); + + private: + I2cMaster::Slave _port; + const uint8_t _address; + const size_t _count = 16; + // 16 LED-Channels with 4 bytes each + 4 byte overhead + std::vector _data; + + bool write(uint8_t reg, uint8_t data); + std::optional read(uint8_t reg); +}; diff --git a/components/lighting/src/i2cPort.cpp b/components/lighting/src/i2cPort.cpp new file mode 100644 index 0000000..b0e0bbf --- /dev/null +++ b/components/lighting/src/i2cPort.cpp @@ -0,0 +1,74 @@ +/** + * @file i2cPort.cpp + * @author Gustice + * @brief C++ Wrapper for I2C-interface + * + * @copyright Copyright (c) 2020 + */ + +#include "i2cPort.hpp" +#include "esp_log.h" + +const int TxBufDisabled = 0; /*!< I2C master doesn't need buffer */ +const int RxBufDisabled = 0; /*!< I2C master doesn't need buffer */ +constexpr int I2C_TOOL_TIMEOUT_VALUE_MS = 50; + +#define ACK_CHECK_EN 0x1 /*!< I2C master will check ack from slave*/ +#define ACK_CHECK_DIS 0x0 /*!< I2C master will not check ack from slave */ + +I2cMaster::I2cMaster(i2c_port_t port, gpio_num_t sda, gpio_num_t scl) : Port(port) { + i2c_master_bus_config_t i2c_bus_config{ + .i2c_port = port, + .sda_io_num = sda, + .scl_io_num = scl, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + }; + i2c_bus_config.flags.enable_internal_pullup = 1; + ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &_busHandle)); +} + +bool I2cMaster::probe(uint8_t addr) { + esp_err_t ret = i2c_master_probe(_busHandle, addr, I2C_TOOL_TIMEOUT_VALUE_MS); + return ret == ESP_OK; +} + +I2cMaster::Slave::Slave(I2cMaster &interface, uint8_t addr, ClockRate rate) : Addr(addr), Interface(interface) { + i2c_device_config_t i2c_dev_conf{ + .device_address = addr, + .scl_speed_hz = rate, + }; + ESP_ERROR_CHECK(i2c_master_bus_add_device(interface._busHandle, &i2c_dev_conf, &_devHandle)); +} + +I2cMaster::Slave::~Slave() { + i2c_master_bus_rm_device(_devHandle); +} + +bool I2cMaster::Slave::writeTo(const std::vector &data) { + esp_err_t ret = i2c_master_transmit(_devHandle, data.data(), data.size(), I2C_TOOL_TIMEOUT_VALUE_MS); + if (ret != ESP_OK) { + if (ret == ESP_ERR_TIMEOUT) { + ESP_LOGW("I2C", "Bus is busy"); + } + return false; + } + return true; +} +std::vector I2cMaster::Slave::readFrom(const std::vector &address, unsigned size) { + if (size == 0) { + return {}; + } + std::vector buffer(size); + esp_err_t ret = i2c_master_transmit_receive(_devHandle, address.data(), address.size(), buffer.data(), buffer.size(), I2C_TOOL_TIMEOUT_VALUE_MS); + + if (ret != ESP_OK) { + if (ret == ESP_ERR_TIMEOUT) { + ESP_LOGW("I2C", "Bus is busy"); + } + return {}; + } + return buffer; +} + +constexpr int DisableBuffer = 0; /*!< I2C master doesn't need buffers */ diff --git a/components/lighting/src/pwmExpanderPca9685.cpp b/components/lighting/src/pwmExpanderPca9685.cpp new file mode 100644 index 0000000..42b1e05 --- /dev/null +++ b/components/lighting/src/pwmExpanderPca9685.cpp @@ -0,0 +1,117 @@ +/** + * @file pwmExpanderPca9685.cpp + * @author Gustice + * @brief PWM-Expander PCA9685 + * + * @copyright Copyright (c) 2024 + */ + +#include "pwmExpanderPca9685.hpp" +#include "esp_log.h" +#include +#include // for usleepCommand + +enum Pca9685Codes { + PCA9685_MODE1 = 0x00, + PCA9685_MODE2 = 0x01, + Led0_ON_L = 0x06, + Led0_ON_H = 0x07, + Led0_OFF_L = 0x08, + Led0_OFF_H = 0x09, + // Led1_...Led15 + AllLedON_L = 0xFA, + AllLedON_H = 0xFB, + AllLedOFF_L = 0xFC, + AllLedOFF_H = 0xFD, + PCA9685_PRESCALE = 0xFE, +}; + +enum Pca9685_Mode1Bits { + Mode1_Sleep = 0x10, + Mode1_AutoIncrement = 0x20, + Mode1_Restart = 0x80, +}; + +enum Pca9685_Mode2Bits { + Mode2_Invert = 0x10, + Mode2_OutDriver = 0x04, +}; + +constexpr int LedPortRegisterWidth = 2 * 2; +constexpr int BitResolution = 12; +constexpr int PwmMaxValue = 4096 - 1; + +PmwExpanderPca9685::PmwExpanderPca9685(I2cMaster &port, bool inverted, uint8_t address) + : _port(port, address, I2cMaster::Slave::_400k), _address(address & 0x7F) { + _data.resize(PwmChannels * LedPortRegisterWidth + 1); + reset(); + setPwmFreq(400, inverted); +} + +void PmwExpanderPca9685::reset(void) { + uint8_t mode = Mode1_Restart | Mode1_AutoIncrement; + write(PCA9685_MODE1, mode); + usleep(10000); // 10 ms +} + +void PmwExpanderPca9685::setPwmFreq(float frequency, bool inverted) { + frequency *= 0.9f; // Correct for overshoot in the frequency setting (see issue #11). + float prescaleLevel = 25000000; + prescaleLevel /= PwmMaxValue; + prescaleLevel /= frequency; + prescaleLevel -= 1; + + uint8_t prescale = (uint8_t)std::floor(prescaleLevel + 0.5f); + uint8_t oldMode = read(PCA9685_MODE1).value_or(0); + uint8_t newMode = (oldMode & 0x7F) | Mode1_Sleep; // sleep + Autoincrement + write(PCA9685_MODE1, newMode); // go to sleep + write(PCA9685_PRESCALE, prescale); // set the prescaler + write(PCA9685_MODE1, oldMode); + if (inverted) { + write(PCA9685_MODE2, Mode2_OutDriver | Mode2_Invert); + } else { + write(PCA9685_MODE2, Mode2_OutDriver); + } + + usleep(5000); // 5 ms + // This sets the MODE1 register to turn on auto increment. + write(PCA9685_MODE1, oldMode | Mode1_Restart | Mode1_AutoIncrement); +} + +void PmwExpanderPca9685::sendImage(GrayValues &grayValues) { + // Try to distribute load peaks over full duty cycle + // OUT1 ..###______###____###______###____###__.. + // OUT2 ..__###______###____###______###____###.. + // OUT3 ..____###______###______###____###_____.. + // ... + int idx = 0; + int step = UINT16_MAX / 4 / PwmChannels; // max 90 deg phase shift + int curStep = step; + _data[idx++] = Led0_ON_L; + for (size_t i = 0; i < _count; i++) { + curStep = curStep + step; + uint16_t dur = (grayValues[i] + curStep) >> 4; + uint on = curStep >> 4; + // On-Cnt + _data[idx++] = (uint8_t)on; + _data[idx++] = (uint8_t)(on >> 8); + // Off-Cnt + _data[idx++] = (uint8_t)dur; + _data[idx++] = (uint8_t)(dur >> 8); + } + _port.writeTo(_data); +} + +bool PmwExpanderPca9685::write(uint8_t reg, uint8_t word) { + std::vector data{reg, word}; + return _port.writeTo(data); +} + +std::optional PmwExpanderPca9685::read(uint8_t reg) { + auto value = _port.readFrom({reg}, 1); + if (value.size() == 0) { + return std::nullopt; + } + + return {value.front()}; +} diff --git a/main/watch_main.cpp b/main/watch_main.cpp index 5a482de..4283812 100644 --- a/main/watch_main.cpp +++ b/main/watch_main.cpp @@ -26,7 +26,9 @@ #include "ethernet_init.h" #include "esp_netif.h" #include "esp_eth.h" +// Component header #include "Task.hpp" +#include "PwmExpanderPca9685.hpp" static const char *TAG = "Watch"; static esp_eth_handle_t s_eth_handle = NULL; @@ -111,8 +113,28 @@ static void initialize_ethernet(void) void LightingTask() { ESP_LOGI(TAG, "Start Lighting"); + static I2cMaster master(I2C_NUM_0, + /*sda=*/ GPIO_NUM_13, + /*scl=*/ GPIO_NUM_16); + // static PmwExpanderPca9685 pca9685(master); + static PmwExpanderPca9685 ledDriver1(master, /*iverted=*/ true); + static PmwExpanderPca9685 ledDriver2(master, /*iverted=*/ true, 0x41); + PmwExpanderPca9685::GrayValues values[2]; + while (true) { ESP_LOGI("Light", "Tick"); + + values[0].fill(0); + values[1].fill(0); + + values[0][12] = 0x100; + values[0][15] = 0x100; + ledDriver1.sendImage(values[0]); + + values[1][1] = 0x100; + values[1][2] = 0x100; + ledDriver2.sendImage(values[1]); + fos::Task::delayMs(1000); } }