// SPDX-License-Identifier: Apache-2.0 // Reference: https://github.com/m5stack/StackChan/tree/main/firmware/main/hal/drivers/PY32IOExpander_Class #include #include #include #include #include #define TAG "PY32IOExpander" // --------------------------------------------------------------------------- // Register map // --------------------------------------------------------------------------- static constexpr uint8_t REG_UID_L = 0x00; ///< R - Unique ID low byte static constexpr uint8_t REG_VERSION = 0x02; ///< R - Firmware version static constexpr uint8_t REG_GPIO_MODE_L = 0x03; ///< RW - GPIO direction [15:0] (1=output, 0=input), little-endian static constexpr uint8_t REG_GPIO_OUT_L = 0x05; ///< RW - GPIO output level [15:0], little-endian static constexpr uint8_t REG_GPIO_IN_L = 0x07; ///< R - GPIO input state [15:0], little-endian static constexpr uint8_t REG_LED_CFG = 0x24; ///< RW - [6]=REFRESH [5:0]=LED_COUNT static constexpr uint8_t REG_LED_DATA = 0x30; ///< RW - RGB565 data, 2 bytes per LED (max 32) static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50); #define GET_CONFIG(device) (static_cast((device)->config)) // --------------------------------------------------------------------------- // Driver lifecycle // --------------------------------------------------------------------------- static error_t start(Device* device) { Device* i2c = device_get_parent(device); if (device_get_type(i2c) != &I2C_CONTROLLER_TYPE) { LOG_E(TAG, "Parent is not an I2C controller"); return ERROR_RESOURCE; } const uint8_t addr = GET_CONFIG(device)->address; // PY32 may need a moment after power-on. Retry with increasing delays. uint8_t version = 0; bool online = false; for (int attempt = 0; attempt < 5; attempt++) { if (i2c_controller_register8_get(i2c, addr, REG_VERSION, &version, TIMEOUT) == ERROR_NONE && version != 0x00 && version != 0xFF) { online = true; break; } vTaskDelay(pdMS_TO_TICKS(20 * (attempt + 1))); } if (!online) { LOG_E(TAG, "PY32IOExpander not responding at 0x%02X — LED ring will not work", addr); return ERROR_NONE; // non-fatal: don't crash the kernel } LOG_I(TAG, "PY32IOExpander online (addr=0x%02X, fw=0x%02X)", addr, version); return ERROR_NONE; } static error_t stop(Device* device) { return ERROR_NONE; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- extern "C" { error_t py32_gpio_set_output(Device* device, uint8_t pin, bool value) { if (pin > 15U) return ERROR_RESOURCE; Device* i2c = device_get_parent(device); const uint8_t addr = GET_CONFIG(device)->address; // Ensure pin is in output mode uint16_t mode = 0; error_t err = i2c_controller_register16le_get(i2c, addr, REG_GPIO_MODE_L, &mode, TIMEOUT); if (err != ERROR_NONE) return err; mode |= static_cast(1U << pin); err = i2c_controller_register16le_set(i2c, addr, REG_GPIO_MODE_L, mode, TIMEOUT); if (err != ERROR_NONE) return err; // Set output level uint16_t out = 0; err = i2c_controller_register16le_get(i2c, addr, REG_GPIO_OUT_L, &out, TIMEOUT); if (err != ERROR_NONE) return err; if (value) { out |= static_cast(1U << pin); } else { out &= static_cast(~(1U << pin)); } return i2c_controller_register16le_set(i2c, addr, REG_GPIO_OUT_L, out, TIMEOUT); } error_t py32_gpio_get_input(Device* device, uint8_t pin, bool* value) { if (pin > 15U) return ERROR_RESOURCE; Device* i2c = device_get_parent(device); uint16_t in = 0; error_t err = i2c_controller_register16le_get(i2c, GET_CONFIG(device)->address, REG_GPIO_IN_L, &in, TIMEOUT); if (err != ERROR_NONE) return err; *value = (in >> pin) & 1U; return ERROR_NONE; } error_t py32_led_set_count(Device* device, uint8_t count) { if (count > 32U) return ERROR_RESOURCE; Device* i2c = device_get_parent(device); uint8_t cfg = count & 0x3FU; return i2c_controller_register8_set(i2c, GET_CONFIG(device)->address, REG_LED_CFG, cfg, TIMEOUT); } error_t py32_led_set_color(Device* device, uint8_t index, uint8_t r, uint8_t g, uint8_t b) { if (index > 32U) return ERROR_RESOURCE; Device* i2c = device_get_parent(device); // RGB565: [15:11]=R5 [10:5]=G6 [4:0]=B5, stored little-endian uint16_t rgb565 = static_cast(((r >> 3U) << 11U) | ((g >> 2U) << 5U) | (b >> 3U)); uint8_t buf[2] = {static_cast(rgb565 & 0xFFU), static_cast(rgb565 >> 8U)}; return i2c_controller_write_register(i2c, GET_CONFIG(device)->address, static_cast(REG_LED_DATA + index * 2U), buf, 2, TIMEOUT); } error_t py32_led_refresh(Device* device) { Device* i2c = device_get_parent(device); const uint8_t addr = GET_CONFIG(device)->address; // Read current LED_CFG (contains LED count), then set the REFRESH bit (6) uint8_t cfg = 0; error_t err = i2c_controller_register8_get(i2c, addr, REG_LED_CFG, &cfg, TIMEOUT); if (err != ERROR_NONE) return err; return i2c_controller_register8_set(i2c, addr, REG_LED_CFG, cfg | 0x40U, TIMEOUT); } error_t py32_led_disable(Device* device) { // Write black to all 32 possible LED slots then refresh with count=32 // Using the maximum count ensures no stale color data is displayed. static constexpr uint8_t BLACK[64] = {}; // 32 LEDs × 2 bytes, all zero Device* i2c = device_get_parent(device); uint8_t addr = GET_CONFIG(device)->address; error_t err = i2c_controller_write_register(i2c, addr, REG_LED_DATA, BLACK, sizeof(BLACK), TIMEOUT); if (err != ERROR_NONE) return err; // count=32 | REFRESH return i2c_controller_register8_set(i2c, addr, REG_LED_CFG, 0x60U, TIMEOUT); } Driver py32ioexpander_driver = { .name = "py32ioexpander", .compatible = (const char*[]) {"m5stack,py32ioexpander", nullptr}, .start_device = start, .stop_device = stop, .api = nullptr, .device_type = nullptr, .owner = &py32ioexpander_module, .internal = nullptr }; } // extern "C"