Skip to content

Add support for M5Stack CoreMatrix (ESP32-C61) - #299

Merged
lovyan03 merged 5 commits into
m5stack:developfrom
ainyan03:corematrix
Aug 10, 2026
Merged

Add support for M5Stack CoreMatrix (ESP32-C61)#299
lovyan03 merged 5 commits into
m5stack:developfrom
ainyan03:corematrix

Conversation

@ainyan03

@ainyan03 ainyan03 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Adds support for the upcoming M5Stack CoreMatrix (ESP32-C61). Companion PR: m5stack/M5GFX#242.

  • Pin tables: internal / Port A I2C (SDA=G0 / SCL=G1, physically shared through a level shifter), SD SPI (SCLK=G25 / MOSI=G27 / MISO=G26 / CS=G28), and the 30 pin M-Bus map taken from the V0.7 schematic and cross-checked against the factory firmware continuity test order.
  • BtnA/B/C: KEY1/2/3 are wired to PM1 GPIO0/1/2 (pressed = LOW) and are polled over I2C with a single input-register read; the state is only updated when the read succeeds, so a bus error is not reported as a button press. BtnPWR maps to the PM1 power button.
  • Power management: battery voltage/level from the PM1, charge state from the AW32901 CHG_STAT via M5IOE1 G8 with an error-aware read (charge_unknown on I2C failure), TF card power enabled in Power.begin (off at reset), and setExtOutput/getExtOutput driving the Grove port power gate.
  • IMU: the BMI270 is mounted rotated 90 degrees about Z, remapped as X=+Y, Y=-X, Z=+Z (verified against the CoreS3 and Fire with the same sketch: raised edge reads positive, face-up rest reads Z=+1).
  • Battery presence: with no battery attached, the PM1 VBAT ADC reads the AW32901 float voltage (~4.2V) and would report a fully charged battery. The first battery API call briefly pauses the charger (~1.2s, one PM1 ADC update cycle) and checks whether VBAT collapses; the result is cached. Without a battery, getBatteryVoltage() returns 0, getBatteryLevel() returns -1, and isCharging() returns is_discharging (the charger retry otherwise blips CHG_STAT low).
  • Deep sleep: EXT1 ANY_LOW on the PM1 IRQ output (ESP32 G2), with WAKE_SRC cleared before the IRQ status so the line releases. On the C61 the wakeup pad stays owned by the RTC IO mux after an EXT1 wakeup and must be returned to the digital function with rtc_gpio_deinit, otherwise the next sleep entry never completes (the C5 does not exhibit this; verified on a ToughC5).

Verified on hardware: buttons, battery presence detection (battery-less unit), charge state, Grove power readback, SD card mount, IMU axis directions (three-way against CoreS3 and Fire), and repeated timer / EXT1 deep sleep wake cycles.

- I2C pin table: the internal bus is SDA=G0 / SCL=G1. The Grove port
  shares the same bus through a level shifter, so Port A uses the
  same pins.
- SD: SPI SCLK=G25 / MOSI=G27 / MISO=G26 / CS=G28.
- M-Bus 30 pin table, taken from the V0.7 schematic and cross-checked
  against the factory firmware continuity test order.
- BtnA/B/C: KEY1/2/3 are wired to PM1 GPIO0/1/2 (pressed = LOW), not
  to the ESP, so they are polled over I2C. The whole GPIO input
  register is read in one transaction (new M5PM1 getGPIOInputBits),
  and the button state is only updated when the read succeeds, so a
  bus error is not reported as a button press. BtnPWR maps to the
  PM1 power button.
- PM1 GPIO3 is the IRQ output wired to ESP32 G2; configure it as a
  push-pull high output before switching it to the IRQ function, the
  same way as ToughC5. PM1 GPIO4 is the BMI270 INT1 input.
- The M5IOE1 is exposed through getIOExpander(0).
- Battery voltage and level are read from the PM1.
- Charge state: the AW32901 CHG_STAT output is wired to M5IOE1 G8
  (low = charging). A new error-aware IOExpander_Base::getInputLevel
  (overridden by M5IOE1_Class with an I2C result) is used so a bus
  failure reports charge_unknown instead of "charging"; the base
  implementation returns false to mean "not supported".
- The TF card power gate (M5IOE1 G1) is off at reset; enable it in
  Power.begin so the SD card is usable after M5.begin.
- setExtOutput/getExtOutput drive the Grove port power gate
  (M5IOE1 G5, both the 3.3V rail and the 5V boost).

Verified on hardware: battery readings, charge state, Grove power
readback, and SD card mount after power-up.
The BMI270 is mounted differently on CoreMatrix; remap the
accelerometer and gyro axes as X=-Y, Y=-X, Z=-Z (the same form of
correction as ChainCaptain). Determined with an interactive tilt
calibration on hardware.
KEY presses and IMU wake events are funneled into the PM1, whose IRQ
output (wired to ESP32 G2) is the only wakeup pin; arm it as an EXT1
ANY_LOW source. The IRQ line has no external pull-up, so keep the
RTC-domain pull-up enabled while sleeping, the same way as ToughC5.

On wakeup, clear WAKE_SRC before the IRQ status registers: while
WAKE_SRC is set, the WAKEUP bit of IRQ status 3 keeps getting
re-asserted and the IRQ output never releases.

On the ESP32-C61, the wakeup pin is still owned by the RTC IO mux
after an EXT1 wakeup and the digital GPIO input reads low forever,
which made the release wait on the next sleep entry spin without
ever sleeping. Call rtc_gpio_deinit in Power.begin to return the
pad to the digital function. (Verified on a ToughC5 that the C5
does not exhibit this, so this is handled in the CoreMatrix path.)

Verified on hardware: repeated timer wake and EXT1 wake cycles with
re-entry into deep sleep.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds board-level support for the upcoming M5Stack CoreMatrix (ESP32-C61) across M5Unified’s pin mapping, power management, button handling, and IMU axis normalization so the board can be used with existing M5Unified APIs.

Changes:

  • Add CoreMatrix pin tables (internal/Port A I2C, SD SPI, and M-Bus map) and CoreMatrix-specific PM1/IOE1 initialization.
  • Add error-aware GPIO/input reads (PM1 GPIO input bit read; IO expander getInputLevel) and use them for CoreMatrix button polling and charge-state detection.
  • Add CoreMatrix-specific power behavior (Grove power gate, TF power enable, battery presence probe, EXT1 wake handling) and IMU axis remap for BMI270.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/utility/power/M5PM1_Class.hpp Adds PM1 APIs for bulk GPIO input read and charger enable state query.
src/utility/power/M5PM1_Class.cpp Implements the new PM1 APIs used by CoreMatrix integration.
src/utility/Power_Class.hpp Adds cached battery-presence probe state for ESP32-C61 builds.
src/utility/Power_Class.cpp CoreMatrix PM1/IOE1 setup, Grove/TF power handling, battery presence probing, charge-state logic, EXT1 wake tweaks.
src/utility/M5IOE1_Class.hpp Extends IOE1 expander interface with error-aware input read.
src/utility/M5IOE1_Class.cpp Implements IOE1 error-aware input read used for charge status.
src/utility/IOExpander_Base.hpp Adds default getInputLevel API to support error-aware reads across expanders.
src/utility/IMU_Class.cpp Adds CoreMatrix BMI270 axis remap to normalize orientation.
src/M5Unified.cpp Adds CoreMatrix pin tables and CoreMatrix-specific button polling via PM1 GPIO input bits.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/utility/Power_Class.cpp Outdated
Comment on lines +1788 to +1801
bool Power_Class::_batteryPresent(void)
{
if (_batt_present < 0)
{
bool chg_enabled = M5pm1.getBatteryCharge();
std::int32_t pre_mv = M5pm1.getBatteryVoltage();
M5pm1.setBatteryCharge(false);
m5gfx::delay(1200);
std::int32_t post_mv = M5pm1.getBatteryVoltage();
M5pm1.setBatteryCharge(chg_enabled);
_batt_present = (post_mv > 2000) && (pre_mv - post_mv < 500);
}
return _batt_present;
}
Comment thread src/utility/power/M5PM1_Class.cpp Outdated
Comment on lines +221 to +224
bool M5PM1_Class::getBatteryCharge(void)
{
return readRegister8(M5PM1_REG_PWR_CFG) & M5PM1_PWR_CFG_CHG_EN;
}
With no battery attached, the PM1 VBAT ADC reads the AW32901 charger
float voltage (about 4.2V), so getBatteryVoltage() reported a fully
charged battery on battery-less units.

Distinguish the two by briefly pausing the charger on the first battery
API call: without a battery VBAT collapses well below 2V within one PM1
ADC update cycle (about 1 second), while a real battery holds its
voltage. The result is cached, so only the first call blocks (~1.2s).

With no battery attached:
- getBatteryVoltage() returns 0
- getBatteryLevel() returns -1
- isCharging() returns is_discharging (without a battery the charger
  retries periodically and CHG_STAT blips low, which would otherwise
  be reported as charging)
@lovyan03
lovyan03 merged commit fc20dff into m5stack:develop Aug 10, 2026
23 checks passed
@lovyan03
lovyan03 deleted the corematrix branch August 10, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants