diff --git a/examples/Modules/StamPLC_IO/HotPlugDetection/HotPlugDetection.ino b/examples/Modules/StamPLC_IO/HotPlugDetection/HotPlugDetection.ino new file mode 100644 index 0000000..0008c77 --- /dev/null +++ b/examples/Modules/StamPLC_IO/HotPlugDetection/HotPlugDetection.ino @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + * + * M5StamPLC IO Hot-Plug Detection Example + * + * This example demonstrates hot-plug detection for multiple M5StamPLC IO modules: + * - Automatically scan I2C bus and detect connected modules (0x20-0x2F) + * - Display online modules list + * - Real-time detection of module insertion and removal + */ +#include +#include + +#define MAX_MODULES 16 +#define SCAN_INTERVAL_MS 2000 + +struct ModuleInfo { + uint8_t address; + uint8_t firmware_version; + bool connected; +}; + +ModuleInfo modules[MAX_MODULES]; +uint8_t module_count = 0; +unsigned long last_scan_time = 0; + +void initModules() +{ + for (int i = 0; i < MAX_MODULES; i++) { + modules[i].address = M5StamPLC_IO::I2C_ADDR_MIN + i; + modules[i].firmware_version = 0; + modules[i].connected = false; + } + module_count = 0; +} + +void scanModules() +{ + bool found_map[0x80] = {false}; + m5::In_I2C.scanID(found_map); + + for (int i = 0; i < MAX_MODULES; i++) { + uint8_t addr = M5StamPLC_IO::I2C_ADDR_MIN + i; + if (found_map[addr] && !modules[i].connected) { + M5StamPLC_IO temp_device; + if (temp_device.begin(addr, false)) { + modules[i].address = addr; + modules[i].firmware_version = temp_device.getFirmwareVersion(); + modules[i].connected = true; + module_count++; + Serial.printf("[+] Module connected: 0x%02X (FW: 0x%02X)\n", addr, modules[i].firmware_version); + } + } else if (!found_map[addr] && modules[i].connected) { + Serial.printf("[-] Module disconnected: 0x%02X\n", modules[i].address); + modules[i].connected = false; + module_count--; + } + } +} + +void displayModules() +{ + M5StamPLC.Display.fillScreen(TFT_BLACK); + M5StamPLC.Display.setCursor(0, 0); + + M5StamPLC.Display.setTextColor(TFT_GREENYELLOW); + M5StamPLC.Display.println("=== IO Hot-Plug ==="); + + M5StamPLC.Display.setTextColor(TFT_CYAN); + M5StamPLC.Display.printf("Online: %d\n\n", module_count); + + if (module_count == 0) { + M5StamPLC.Display.setTextColor(TFT_ORANGE); + M5StamPLC.Display.println("No modules found"); + } else { + int displayed = 0; + for (int i = 0; i < MAX_MODULES && displayed < 8; i++) { + if (modules[i].connected) { + M5StamPLC.Display.setTextColor(TFT_GREEN); + M5StamPLC.Display.printf("%d. Addr: 0x%02X\n", displayed + 1, modules[i].address); + M5StamPLC.Display.setTextColor(TFT_YELLOW); + M5StamPLC.Display.printf(" FW: 0x%02X\n", modules[i].firmware_version); + displayed++; + } + } + } +} + +void setup() +{ + M5StamPLC.begin(); + M5StamPLC.Display.setTextScroll(false); + M5StamPLC.Display.setTextSize(1); + M5StamPLC.Display.setFont(&fonts::efontCN_16); + + Serial.begin(115200); + Serial.println("\n=== M5StamPLC IO Hot-Plug Detection ==="); + + initModules(); + + M5StamPLC.Display.fillScreen(TFT_BLACK); + M5StamPLC.Display.setCursor(0, 0); + M5StamPLC.Display.setTextColor(TFT_GREENYELLOW); + M5StamPLC.Display.println("M5StamPLC IO"); + M5StamPLC.Display.println("Hot-Plug Detection"); + M5StamPLC.Display.setTextColor(TFT_CYAN); + M5StamPLC.Display.println("\nScanning..."); + + delay(1000); + scanModules(); + displayModules(); + last_scan_time = millis(); +} + +void loop() +{ + M5StamPLC.update(); + + if (millis() - last_scan_time > SCAN_INTERVAL_MS) { + scanModules(); + displayModules(); + last_scan_time = millis(); + } + + delay(100); +} diff --git a/examples/Modules/StamPLC_IO/PWM_Control/PWM_Control.ino b/examples/Modules/StamPLC_IO/PWM_Control/PWM_Control.ino new file mode 100644 index 0000000..ef764bc --- /dev/null +++ b/examples/Modules/StamPLC_IO/PWM_Control/PWM_Control.ino @@ -0,0 +1,151 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + * + * M5StamPLC IO PWM Control Example + * + * This example demonstrates how to use the PWM control function of the M5StamPLC IO module: + * - Switch between IO mode and PWM mode + * - Set PWM frequency (1-100 Hz) + * - Adjust CH1 and CH2 duty cycle (0-1000 thousandths) + * + * Button functions: + * - Button A: Toggle PWM mode on/off + * - Button B: Increase CH1 duty cycle (+10%) + * - Long press B: Decrease CH1 duty cycle (-10%) + * - Button C: Increase CH2 duty cycle (+10%) + * - Long press C: Decrease CH2 duty cycle (-10%) + */ +#include +#include + +M5StamPLC_IO stamplc_io; + +bool pwm_mode = false; +uint8_t pwm_freq = 50; +uint16_t ch1_duty = 0; +uint16_t ch2_duty = 0; +bool btnB_longPressed = false; +bool btnC_longPressed = false; + +unsigned long lastUpdateTime = 0; + +void updateDisplay() +{ + M5StamPLC.Display.fillScreen(TFT_BLACK); + M5StamPLC.Display.setCursor(0, 0); + + M5StamPLC.Display.setTextColor(TFT_GREENYELLOW); + M5StamPLC.Display.println("=== PWM Control ==="); + + M5StamPLC.Display.setTextColor(TFT_CYAN); + M5StamPLC.Display.printf("Mode: %s\n", pwm_mode ? "PWM" : "IO"); + M5StamPLC.Display.printf("Frequency: %d Hz\n", pwm_freq); + + M5StamPLC.Display.setTextColor(TFT_YELLOW); + M5StamPLC.Display.printf("CH1 Duty: %d.%d%% (%d/1000)\n", ch1_duty / 10, ch1_duty % 10, ch1_duty); + M5StamPLC.Display.printf("CH2 Duty: %d.%d%% (%d/1000)\n", ch2_duty / 10, ch2_duty % 10, ch2_duty); + + M5StamPLC.Display.setTextColor(TFT_DARKGREY); + M5StamPLC.Display.println("A: Toggle PWM mode"); + M5StamPLC.Display.println("B: CH1+ Long press: CH1-"); + M5StamPLC.Display.println("C: CH2+ Long press: CH2-"); +} + +void setup() +{ + M5StamPLC.begin(); + M5StamPLC.Display.setTextScroll(false); + M5StamPLC.Display.setTextSize(1); + M5StamPLC.Display.setFont(&fonts::efontCN_16); + M5StamPLC.Display.println("Try to find M5StamPLC IO module..."); + + /* Init M5StamPLC IO */ + while (!stamplc_io.begin()) { + M5StamPLC.Display.println("Not found, retry in 1s..."); + delay(1000); + } + + M5StamPLC.Display.printf("Found IO module: 0x%02X\n", stamplc_io.getCurrentAddress()); + M5StamPLC.Display.printf("Firmware Version: 0x%02X\n", stamplc_io.getFirmwareVersion()); + + delay(2000); + + pwm_mode = stamplc_io.getPWMMode(); + pwm_freq = stamplc_io.getPWMFrequency(); + ch1_duty = stamplc_io.getChannelDuty(1); + ch2_duty = stamplc_io.getChannelDuty(2); + + if (pwm_freq == 0) { + pwm_freq = 50; + stamplc_io.setPWMFrequency(pwm_freq); + } + + updateDisplay(); + lastUpdateTime = millis(); +} + +void loop() +{ + M5StamPLC.update(); + + bool needUpdate = false; + + // Button A: Toggle PWM mode + if (M5StamPLC.BtnA.wasClicked()) { + pwm_mode = !pwm_mode; + stamplc_io.setPWMMode(pwm_mode); + needUpdate = true; + } + + // Button B: Adjust CH1 duty cycle + if (M5StamPLC.BtnB.wasClicked()) { + // Increase 10% (100/1000) + ch1_duty += 100; + if (ch1_duty > 1000) ch1_duty = 1000; + stamplc_io.setChannelDuty(1, ch1_duty); + needUpdate = true; + } else if (M5StamPLC.BtnB.pressedFor(500) && !btnB_longPressed) { + // Decrease 10% + btnB_longPressed = true; + if (ch1_duty >= 100) { + ch1_duty -= 100; + } else { + ch1_duty = 0; + } + stamplc_io.setChannelDuty(1, ch1_duty); + needUpdate = true; + } else if (!M5StamPLC.BtnB.isPressed()) { + btnB_longPressed = false; + } + + // Button C: Adjust CH2 duty cycle + if (M5StamPLC.BtnC.wasClicked()) { + // Increase 10% (100/1000) + ch2_duty += 100; + if (ch2_duty > 1000) ch2_duty = 1000; + stamplc_io.setChannelDuty(2, ch2_duty); + needUpdate = true; + } else if (M5StamPLC.BtnC.pressedFor(500) && !btnC_longPressed) { + // Decrease 10% + btnC_longPressed = true; + if (ch2_duty >= 100) { + ch2_duty -= 100; + } else { + ch2_duty = 0; + } + stamplc_io.setChannelDuty(2, ch2_duty); + needUpdate = true; + } else if (!M5StamPLC.BtnC.isPressed()) { + btnC_longPressed = false; + } + + // Periodically update display + if (needUpdate || (millis() - lastUpdateTime > 1000)) { + updateDisplay(); + lastUpdateTime = millis(); + } + + delay(10); +} diff --git a/src/modules/M5StamPLC_IO.cpp b/src/modules/M5StamPLC_IO.cpp index 1c5522d..64671f1 100644 --- a/src/modules/M5StamPLC_IO.cpp +++ b/src/modules/M5StamPLC_IO.cpp @@ -8,8 +8,10 @@ static const char* _tag = "M5StamPLC_IO"; -bool M5StamPLC_IO::begin(uint8_t addr) +bool M5StamPLC_IO::begin(uint8_t addr, bool debug) { + esp_log_level_set(_tag, debug ? ESP_LOG_DEBUG : ESP_LOG_NONE); + if (addr == 0) { _current_addr = scanI2CDevices(); if (_current_addr == 0) { @@ -319,20 +321,26 @@ void M5StamPLC_IO::writeINA226Config(uint8_t channel, uint16_t config) return; } - uint8_t lsb = config & 0xFF; - uint8_t msb = (config >> 8) & 0xFF; + uint8_t data[2]; + data[0] = config & 0xFF; + data[1] = (config >> 8) & 0xFF; + uint8_t start_reg; if (channel == 1) { - writeRegister(REG_INA226_CONFIG_CH1_LSB, lsb); - writeRegister(REG_INA226_CONFIG_CH1_MSB, msb); + start_reg = REG_INA226_CONFIG_CH1_LSB; } else if (channel == 2) { - writeRegister(REG_INA226_CONFIG_CH2_LSB, lsb); - writeRegister(REG_INA226_CONFIG_CH2_MSB, msb); + start_reg = REG_INA226_CONFIG_CH2_LSB; } else { ESP_LOGW(_tag, "Invalid channel: %d", channel); return; } + bool success = m5::In_I2C.writeRegister(_current_addr, start_reg, data, 2, 400000); + if (!success) { + ESP_LOGW(_tag, "Failed to write CH%d INA226 config", channel); + return; + } + ESP_LOGI(_tag, "Write CH%d INA226 config: 0x%04X", channel, config); } @@ -393,3 +401,101 @@ esp_err_t M5StamPLC_IO::getINA226Averaging(uint8_t channel, uint8_t* avg) *avg = (config & INA226_AVG_MASK) >> 9; return ESP_OK; } + +void M5StamPLC_IO::setPWMMode(bool enable) +{ + uint8_t currentState = readRegister(REG_IO_CONTROL); + uint8_t newState; + + if (enable) { + newState = currentState | (1 << BIT_PWM_MODE); + ESP_LOGI(_tag, "Enable PWM mode"); + } else { + newState = currentState & ~(1 << BIT_PWM_MODE); + ESP_LOGI(_tag, "Disable PWM mode"); + } + + if (newState != currentState) { + writeRegister(REG_IO_CONTROL, newState); + } +} + +bool M5StamPLC_IO::getPWMMode() +{ + uint8_t state = readRegister(REG_IO_CONTROL); + return (state & (1 << BIT_PWM_MODE)) != 0; +} + +void M5StamPLC_IO::setPWMFrequency(uint8_t freq) +{ + if (freq < 1 || freq > 100) { + ESP_LOGW(_tag, "Invalid PWM frequency: %d, must be 1-100 Hz", freq); + return; + } + + writeRegister(REG_PWM_FREQ, freq); + ESP_LOGI(_tag, "Set PWM frequency: %d Hz", freq); +} + +uint8_t M5StamPLC_IO::getPWMFrequency() +{ + return readRegister(REG_PWM_FREQ); +} + +void M5StamPLC_IO::setChannelDuty(uint8_t channel, uint16_t duty) +{ + if (_current_addr == 0) { + ESP_LOGE(_tag, "Device not initialized"); + return; + } + + if (duty > 1000) { + ESP_LOGW(_tag, "Invalid duty cycle: %d, must be 0-1000", duty); + return; + } + + uint8_t data[2]; + data[0] = duty & 0xFF; + data[1] = (duty >> 8) & 0xFF; + + uint8_t start_reg; + if (channel == 1) { + start_reg = REG_CH1_DUTY_LSB; + } else if (channel == 2) { + start_reg = REG_CH2_DUTY_LSB; + } else { + ESP_LOGW(_tag, "Invalid channel: %d", channel); + return; + } + + bool success = m5::In_I2C.writeRegister(_current_addr, start_reg, data, 2, 400000); + if (!success) { + ESP_LOGW(_tag, "Failed to write CH%d duty cycle", channel); + return; + } + + ESP_LOGI(_tag, "Set CH%d duty cycle: %d/1000 (%d%%)", channel, duty, duty / 10); +} + +uint16_t M5StamPLC_IO::getChannelDuty(uint8_t channel) +{ + uint8_t data[2]; + uint8_t start_reg; + + if (channel == 1) { + start_reg = REG_CH1_DUTY_LSB; + } else if (channel == 2) { + start_reg = REG_CH2_DUTY_LSB; + } else { + ESP_LOGW(_tag, "Invalid channel: %d", channel); + return 0; + } + + if (m5::In_I2C.readRegister(_current_addr, start_reg, data, 2, 400000)) { + uint16_t duty = (data[1] << 8) | data[0]; + return duty; + } else { + ESP_LOGW(_tag, "Failed to read CH%d duty cycle", channel); + return 0; + } +} diff --git a/src/modules/M5StamPLC_IO.h b/src/modules/M5StamPLC_IO.h index da39a83..ddb34dd 100644 --- a/src/modules/M5StamPLC_IO.h +++ b/src/modules/M5StamPLC_IO.h @@ -28,6 +28,11 @@ class M5StamPLC_IO { static constexpr uint8_t REG_I_CH2_EXT1 = 0x0A; static constexpr uint8_t REG_I_CH2_EXT2 = 0x0B; static constexpr uint8_t REG_IO_CONTROL = 0x10; + static constexpr uint8_t REG_PWM_FREQ = 0x30; + static constexpr uint8_t REG_CH1_DUTY_LSB = 0x31; + static constexpr uint8_t REG_CH1_DUTY_MSB = 0x32; + static constexpr uint8_t REG_CH2_DUTY_LSB = 0x33; + static constexpr uint8_t REG_CH2_DUTY_MSB = 0x34; static constexpr uint8_t REG_SYSTEM_STATUS = 0xFB; static constexpr uint8_t REG_FIRMWARE_VER = 0xFE; static constexpr uint8_t REG_ADDR_CONFIG = 0xFF; @@ -49,6 +54,7 @@ class M5StamPLC_IO { static constexpr uint8_t BIT_CH1_PU_EN = 2; static constexpr uint8_t BIT_CH2_PU_EN = 3; static constexpr uint8_t BIT_RELAY_TRIG = 4; + static constexpr uint8_t BIT_PWM_MODE = 5; // SYSTEM STATUS BIT DEFINITION static constexpr uint8_t SYS_CH1_INA226_ERROR = 0; @@ -82,10 +88,11 @@ class M5StamPLC_IO { * @brief Initialize M5StamPLC-IO * * @param addr I2C address (0x20-0x2F), if 0 will auto-scan + * @param debug Enable debug logging (default: false) * @return true if initialization successful * @return false if initialization failed */ - bool begin(uint8_t addr = 0); + bool begin(uint8_t addr = 0, bool debug = false); /** * @brief Scan for I2C devices @@ -260,6 +267,50 @@ class M5StamPLC_IO { return _current_addr; } + /** + * @brief Set PWM mode + * + * @param enable true for PWM mode, false for IO mode + */ + void setPWMMode(bool enable); + + /** + * @brief Get PWM mode + * + * @return true if PWM mode enabled + */ + bool getPWMMode(); + + /** + * @brief Set PWM frequency + * + * @param freq frequency in Hz (1-100), default 50 + */ + void setPWMFrequency(uint8_t freq); + + /** + * @brief Get PWM frequency + * + * @return frequency in Hz + */ + uint8_t getPWMFrequency(); + + /** + * @brief Set channel duty cycle + * + * @param channel 1 or 2 + * @param duty duty cycle in permille (0-1000), default 0 + */ + void setChannelDuty(uint8_t channel, uint16_t duty); + + /** + * @brief Get channel duty cycle + * + * @param channel 1 or 2 + * @return duty cycle in permille (0-1000) + */ + uint16_t getChannelDuty(uint8_t channel); + protected: uint8_t _current_addr = 0; };