M5Stack StickS3 Grove I²C issue with SCD41 (works on AtomS3U)

I’m trying to use an SCD41 (CO2L unit) over the built-in Grove port on an M5Stack StickS3 with ESPHome.

Configuration:

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

i2c:
  sda: GPIO9
  scl: GPIO10
  scan: true

sensor:
  - platform: scd4x
    co2:
      name: "CO2"
    temperature:
      name: "Temperature"
    humidity:
      name: "Humidity"

Compile + OTA upload succeed without issues.

However, at runtime I consistently get:

[I][i2c.idf:106]: Recovery: failed, SCL is held low on the bus
[I][i2c.idf:115]: Found no devices
[W][scd4x:116]: Communication failed
[E][component:224]: scd4x.sensor is marked FAILED

What I already verified:

  • Different Grove cable
  • Lower I²C frequency
  • Sensor + cable work perfectly on an AtomS3U using nearly identical YAML
  • Sensor is detected correctly at 0x62 on the AtomS3U

This makes me suspect the issue is specific to the StickS3 Grove/power implementation rather than the SCD41 itself.

I found references suggesting the StickS3 uses a PMIC/co-processor to control external Grove power output. Could it be that the Grove port is not actually powering the SCD41 under ESPHome unless some additional initialization is done?

Has anyone successfully used I²C Grove sensors on the StickS3 with ESPHome?

I don't have your device but if you ever opened the M5 product page you should have noticed:

How to set Grove 5V as output is not so clear though and schematics suck, but try to write gpio4 high using switch or on_boot automation..:

switch:
  - platform: gpio
    pin: GPIO4
    id: grove_power
    restore_mode: ALWAYS_ON

Thanks for the suggestion! It definitely makes sense to think of a GPIO switch, especially since M5Stack used a similar approach on some of their older boards (like the original StickC or Atom) where a dedicated GPIO or an AXP PMIC pin handled the boost circuit.

However, for the M5StickS3 (K150), this unfortunately won't work. I actually dug into the official schematics (K150_Stick_S3_PRJ_V0.6) and the datasheets for the internal components, and it turns out the hardware architecture is completely different:

  1. GPIO4 is mapped elsewhere: On the StickS3, ESP32's GPIO4 is routed directly to the EXT_GPIO4 pin on the HAT2 connector on top of the device. It has no physical trace to the Grove power circuit. (Also, on the PMIC chip itself, there is a pin named IO4, but that's internally multiplexed for battery ADC enabling, not the external 5V).
  2. The actual power path: The 5V for the Grove port (GROVE_5V) is generated by an AW35122 boost-converter chip. This converter is enabled by a signal called EXT_5V_EN, which is driven entirely by the internal PMIC/co-processor (the PY32L020 chip).

The schematics definitely suck and are super confusing with all the overlapping naming conventions between the ESP32 and the PMIC co-processor, but it's good to have this ruled out!

Where I'm currently stuck: Since the official documentation states the boost converter is controlled via the PMIC, I tried to bypass the M5Unified library entirely and manage it directly within ESPHome. I defined two separate I2C buses (an internal one on GPIO11/12 and the external Grove one on GPIO9/10).

During on_boot, I use a high-priority lambda to write directly to the PMIC (address 0x34, register 0x10, data 0x01) to enable the 5V output, combined with a delayed setup_priority (using negative float values like -100.0) to give the SCD41 sensor time to boot up before the external bus starts scanning.

While this successfully fixes the dreaded SCL is held low bus recovery errors on boot, the SCD41 sensor itself still returns scd4x.sensor is marked FAILED: unspecified. The ESP32 clears the lines, but the sensor never actually responds. It feels like the PMIC register configuration isn't sticking or behaves differently under ESPHome's framework compared to the official Arduino/UIFlow environment.

Unless someone has found the magic register combination for the StickS3 PMIC in pure ESPHome, it looks like a hardware bypass (pulling 5V from the 5V HAT pin on top) is the only way forward.

I suggested to try gpio switch from my thin memory about some m5 device power control.
You are right about the power path, but I'm not sure about your approach to enable.

I might be on wrong path here but M5 docs give:
M5.Power.setExtOutput(true); // EXT_5V OUTPUT

Power_Class.ccp has:

void Power_Class::setExtOutput(bool enable, ext_port_mask_t port_mask)
...
...
    case board_t::board_M5StickS3:
    case board_t::board_M5StopWatch:
    case board_t::board_M5PaperColor:
      if (_pmic == pmic_t::pmic_m5pm1)
      {
        // Control 5V output: register 0x06 bit 3 (1=enable, 0=disable)
        uint8_t reg_val = M5.In_I2C.readRegister8(m5pm1_i2c_addr, 0x06, i2c_freq);
        if (enable) {
          reg_val |= 0x08;  // Set bit 3
        } else {
          reg_val &= ~0x08; // Clear bit 3
        }
        M5.In_I2C.writeRegister8(m5pm1_i2c_addr, 0x06, reg_val, i2c_freq);
      }
      break;

So it looks like you have to read current value, modify one bit and write it back.

Wow, amazing catch! You found the missing link in Power_Class.cpp.

I was completely blindsided by some outdated M5 documentation that pointed towards register 0x10 as a standalone switch. Your snippet makes it clear: for the StickS3, it's actually register 0x06``, and we specifically need to flip bit 3 (0x08`) using a read-modify-write approach to avoid messing up other critical PMIC functionalities.

Looking deeper into the M5PM1 datasheet and schematics, it turns out it’s a two-stage process. Register 0x06 bit 3 enables the central DCDC_5V boost circuit, while register 0x10 controls the actual GPIO output configuration of the PMIC to route that 5V out to the Grove port (EXT_5V_EN).

I translated this exact logic into an ESPHome on_boot lambda using the native IDFI2CBus methods under the esp-idf framework, tackling both registers:

`YAML on_boot:
priority: 800.0 # Run very early in the boot process
then:
- lambda: |-
auto* internal_bus = new esphome::i2c::IDFI2CBus();
internal_bus->set_sda_pin(11);
internal_bus->set_scl_pin(12);
internal_bus->set_frequency(100000.0);
internal_bus->setup();

      // --- STEP 1: Read-Modify-Write Register 0x06 ---
      uint8_t reg_addr_06 = 0x06;
      uint8_t reg_val_06 = 0;
      
      if (internal_bus->write(0x34, &reg_addr_06, 1) && internal_bus->read(0x34, &reg_val_06, 1)) {
        reg_val_06 |= 0x08; // Set bit 3 high
        uint8_t write_payload_06[2] = {0x06, reg_val_06};
        internal_bus->write(0x34, write_payload_06, 2);
      }
      
      // --- STEP 2: Configure PMIC GPIO1 Output (Register 0x10) ---
      uint8_t write_payload_10[2] = {0x10, 0x03}; // Push-Pull Output HIGH
      internal_bus->write(0x34, write_payload_10, 2);
      
      delete internal_bus;`

The issue I'm facing now: Even though this C++ code compiles fine and completely fixes the dreaded SCL is held low bus recovery errors on boot, the SCD41 sensor still refuses to cooperate. The ESP32 clears the lines, but the sensor never responds on address 0x62.

Worse yet, pulling raw registers on the M5PM1 without the full M5Unified state-machine running in the background seems to cause severe instability during the early boot phase. The ESP32 occasionally panics and crash-loops so fast that ESPHome triggers an automatic safe-mode OTA rollback (Last reset too quick; OTA rollback detected!), wiping out the custom configuration.

It feels like the PY32 PMIC co-processor requires some extra initialization scaffolding (clocks, power domains, or sleep states) to safely sustain the Grove power rail under pure ESPHome.

Any ideas on what I might be missing here? Has anyone managed to successfully get the Grove power rail stable on the StickS3 within ESPHome?

Last time I participated in topic about M5 power paths, author gave up and took power from where it was readily available. I would have done the same, why to spend whole day with complicated circuits, shit documentation and crap schematics if you can just place a jumper wire and get your power from where it's readily available.
M5 devices are interesting, low cost common devices with practical enclosure and connectors readily available via reputable suppliers. But I rarely use them, too complicated and often questionable design. A while ago I used hx711 module from them, I found clearly the circuit violates the MCU it was designed for (esp32).