Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
20c8af1
PM1: expose PWR_SRC as a bitmap
ainyan03 Aug 17, 2026
d241ff2
Merge pull request #316 from ainyan03/pm1_pwr_src_bitfield
lovyan03 Aug 17, 2026
7103d65
Tab5: fix the battery current sign
ainyan03 Aug 17, 2026
97461e2
Merge pull request #317 from ainyan03/tab5_current_sign
lovyan03 Aug 17, 2026
56f6a92
Add PWM control to M5PM1_Class
ainyan03 Aug 17, 2026
388d61c
Configure the ToughC5 buzzer line at startup
ainyan03 Aug 17, 2026
db19dd7
Merge pull request #318 from ainyan03/toughc5_buzzer
lovyan03 Aug 17, 2026
b201d3c
Replace the IO expander pull enable API with explicit modes
ainyan03 Aug 17, 2026
3f665a2
Give PM1 PWM duty APIs explicit units
ainyan03 Aug 17, 2026
41154f6
Merge pull request #320 from ainyan03/ioe_pull_mode
lovyan03 Aug 17, 2026
5325b9a
Give M5IOE1 PWM duty APIs explicit units
ainyan03 Aug 17, 2026
e23f127
Merge pull request #321 from ainyan03/pwm_duty_units
lovyan03 Aug 17, 2026
4817f6f
Report M5IOE1 PWM frequency write results
ainyan03 Aug 17, 2026
97d5fa0
Merge pull request #322 from ainyan03/ioe1_pwm_frequency_result
lovyan03 Aug 17, 2026
871ed01
Enable PM1 external power output(MBUS) for CoreMatrix.
TinyuZhao Aug 18, 2026
2800183
Harden PWM initialization failure handling
ainyan03 Aug 18, 2026
b477596
Flush the final partial speaker buffer
ainyan03 Aug 18, 2026
7d100f1
Merge pull request #324 from TinyuZhao/develop
lovyan03 Aug 18, 2026
d0b4c18
Merge pull request #326 from ainyan03/pwm_init_hardening
lovyan03 Aug 18, 2026
94dffd8
Round the flushed sample count up to even
ainyan03 Aug 18, 2026
e6bc8b5
Use m5gfx::delay for internal hardware waits
ainyan03 Aug 18, 2026
b20c4f7
Merge pull request #325 from ainyan03/spk_tail_flush
lovyan03 Aug 18, 2026
b92c642
Merge pull request #327 from ainyan03/delay_unify
lovyan03 Aug 18, 2026
9336c57
Return the write status from the IO expander virtuals
ainyan03 Aug 18, 2026
f5afb02
Validate PI4IOE5V6408 pins and state the acknowledged-write contract
ainyan03 Aug 18, 2026
71ab191
Merge pull request #328 from ainyan03/ioexp_bool_status
lovyan03 Aug 18, 2026
0624b6f
Fix PM1 button and wake IRQ handling and correct the power API docs
ainyan03 Aug 18, 2026
01c0752
Reset the double-click latch on every reported click
ainyan03 Aug 18, 2026
b4eb4d2
Merge pull request #329 from ainyan03/pm1_audit_fixes
lovyan03 Aug 18, 2026
7349ed8
Probe the address the board detection is looking for
ainyan03 Aug 19, 2026
9c97ef0
Take the internal bus first, and ask the sensor that is always there
ainyan03 Aug 19, 2026
9709e5f
Confirm the bus before driving it, and keep the probe slot apart
ainyan03 Aug 19, 2026
87e604a
Require the M5GFX that has the software I2C the probe needs
ainyan03 Aug 19, 2026
0ce7b47
Free a data line that is still held before giving up on the bus
ainyan03 Aug 19, 2026
110fe51
Wait for the devices to power up once, not once per address
ainyan03 Aug 19, 2026
3dd1440
Park the lines low before the pins become outputs
ainyan03 Aug 19, 2026
57e8ea2
Merge pull request #331 from ainyan03/board_probe_i2c
lovyan03 Aug 19, 2026
ba089f6
Add ESP32C5 RTC IRQ clear and unsupported-sleep log warnings
TinyuZhao Aug 20, 2026
8531fc9
Add ToughC5 charge current control
TinyuZhao Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion library.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ category=Display
url=https://github.com/m5stack/M5Unified.git
architectures=esp32
includes=M5Unified.h
depends=M5GFX
depends=M5GFX (>=0.2.27)
197 changes: 135 additions & 62 deletions src/M5Unified.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = {
while (*bulk_data) {
uint8_t len = *bulk_data++;
uint8_t r = retry + 1;
while (!M5.In_I2C.writeRegister(i2c_addr, bulk_data[0], &bulk_data[1], len - 1, i2c_freq) && --r) { M5.delay(1); }
while (!M5.In_I2C.writeRegister(i2c_addr, bulk_data[0], &bulk_data[1], len - 1, i2c_freq) && --r) { m5gfx::delay(1); }
bulk_data += len;
}
}
Expand Down Expand Up @@ -764,7 +764,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = {
// 状態へ落としてから終了することで、次回 enable を常に定義済み状態から始める。
M5.In_I2C.bitOff(pi4io1_i2c_addr, 0x05, 0b00000010, 400000); // AMP off (過渡音を出さない)
M5.In_I2C.writeRegister8(es8388_i2c_addr, 25, 0x24, 400000); // DACCONTROL3: mute (SoftRamp 維持)
M5.delay(1); // soft-ramp 遷移待ち
m5gfx::delay(1); // soft-ramp 遷移待ち
M5.In_I2C.writeRegister8(es8388_i2c_addr, 4, 0xC0, 400000); // DACPOWER: DAC L/R down + 全出力 off
M5.In_I2C.writeRegister8(es8388_i2c_addr, 2, 0xFF, 400000); // CHIPPOWER: 全停止 (ADF deinit と同一の終端状態)
}
Expand Down Expand Up @@ -1368,65 +1368,130 @@ static constexpr const uint8_t _pin_table_mbus[][31] = {
static constexpr gpio_num_t CoreInk_BUTTON_PWR_PIN = GPIO_NUM_27;
#endif

bool M5Unified::_detect_i2c_device(uint8_t sda, uint8_t scl, uint8_t addr, const uint8_t* cmd_list)
/// probe を始める前に一度だけ、デバイスの電源が安定するのを待つ。
/// 待ちが要るのは「電源投入から間もない」ことであってアドレスごとの事情ではないので、
/// 判定の入口で一度払う。旧実装は最初の probe が常に真を返して連鎖が止まっていたため
/// 結果的に 1 回しか待っていなかった。それを意図として書き直したもの。
void M5Unified::_wait_i2c_device_power(void)
{
#if !defined(M5UNIFIED_PC_BUILD)
static bool waited = false;
if (!waited)
{
waited = true;
m5gfx::delay(50);
}
#endif
}

bool M5Unified::_probe_i2c_addr(uint8_t sda, uint8_t scl, uint8_t addr)
{
uint32_t result = 0;
#if defined(M5UNIFIED_PC_BUILD)
return result;
return false;
#else
m5gfx::gpio::pin_backup_t pin_backup[] = {scl, sda};
/// アドレスの存在確認はソフトウェア I2C ポートで行う (オープンドレイン駆動で
/// ACK 競合が起きず、ハードウェアのペリフェラルにも触れない)。
/// ポートは M5GFX の autodetect probe (-1) と分ける。ソフト I2C のスロットは
/// 所有権を持たず、init が既存の設定を黙って奪う作りなので、ライブラリ同士で
/// 同じスロットを共有しない。
static constexpr int_fast16_t probe_i2c_port = -2;

_wait_i2c_device_power();

m5gfx::gpio::pin_backup_t pin_backup[] = { scl, sda };

// ここでは待たない。電源安定待ちは判定の入口で一度だけ行う (_wait_i2c_device_power)。
// アドレスごとに待つと、判定が空振りするたびに数十 ms が起動時間へ積み上がる。
m5gfx::pinMode(scl, m5gfx::pin_mode_t::input_pullup);
m5gfx::pinMode(sda, m5gfx::pin_mode_t::input_pullup);

// 指定ピンが「外部プルアップの載った I2C バス」かどうかを先に確かめる。
// ここは I2C ピンとは限らない場所を駆動する機種判別なので、判定を外すと
// 機種の読みが変わる。判定方式は M5GFX の autodetect probe と同一のものを使う
// (実機での実績がある形から動かさない。変えるなら該当機種すべてで再検証が要る)。
//
// 4 回の read のうち、後半 2 回は入力プルダウンにしても High になること
// (= 外部プルアップが内部プルダウンに勝つこと) を確認するもの。
// 弱いプルアップ (内部プルダウンと同程度の抵抗値) では通らないが、
// 「強いプルアップがある = I2C バスである」を要求するのがこの判定の趣旨。
const uint8_t cmd_bus_check_list[] = {
m5gfx::gpio::command_write_low , scl,
m5gfx::gpio::command_read , scl, // low チェック
m5gfx::gpio::command_write_low , sda,
m5gfx::gpio::command_read , sda, // low チェック
m5gfx::gpio::command_mode_input_pulldown, scl,
m5gfx::gpio::command_delay_usec , 10,
m5gfx::gpio::command_read , scl, // 外部プルアップがあるなら High
m5gfx::gpio::command_mode_input_pullup , scl,
m5gfx::gpio::command_mode_input_pulldown, sda,
m5gfx::gpio::command_delay_usec , 10,
m5gfx::gpio::command_read , sda, // 外部プルアップがあるなら High
m5gfx::gpio::command_mode_input_pullup , sda,
m5gfx::gpio::command_end
};
auto bus_check = [&](void) -> uint32_t
{
if(cmd_list == nullptr)
// ラッチを Low にしてから出力へ切り替える。直前は input_pullup (ラッチ High)
// なので、先に出力にすると一瞬 push-pull で High を駆動してしまう。
// ここは相手が何か分からないピンなので、High は一度も駆動しない。
m5gfx::gpio_lo(scl); m5gfx::pinMode(scl, m5gfx::pin_mode_t::output);
m5gfx::gpio_lo(sda); m5gfx::pinMode(sda, m5gfx::pin_mode_t::output);
return m5gfx::gpio::command(cmd_bus_check_list);
};

// 0x02 は「SCL は外部プルアップで戻るのに SDA だけ Low のまま」。前回の通信の
// 途中で止まったデバイスがデータ線を握っている典型で (ソフトリセットでは
// デバイスの電源が切れないため実際に起きる)、プルアップの無いただの Low ピンとは
// このシグネチャで区別できる。この場合だけは STOP を見せて握りを解かせ、
// もう一度確かめる。それ以外の不一致は I2C バスではないとみなして即座に帰る。
uint32_t check = bus_check();
if (check != 0x03 && check != 0x02)
{
for (auto& backup : pin_backup) { backup.restore(); }
return false;
}

// 前回の通信の途中で止まっているデバイスに STOP を見せて論理状態を戻す。
// START だけで戻らないデバイスが実在する (同ファイルの StampS3/Capsule 判別に
// 「STOP を出さないと正しく動作しないデバイスがあった (UnitHEART MAX30100)」の記録がある)。
// 線を Low へ駆動するか解放するかの 2 状態しか使わない (High を駆動しない) ので、
// デバイスが線を握っていてもパッド同士の衝突にはならない。
{
auto line_lo = [](uint8_t pin)
{ m5gfx::gpio_lo(pin); m5gfx::pinMode(pin, m5gfx::pin_mode_t::output); };
auto line_hi = [](uint8_t pin)
{ m5gfx::pinMode(pin, m5gfx::pin_mode_t::input_pullup); };
for (int i = 0; i < 8; ++i)
{
uint8_t cmd_low[] = {
m5gfx::gpio::command_write_low, scl,
m5gfx::gpio::command_mode_output, scl, // SCL
m5gfx::gpio::command_write_low, sda,
m5gfx::gpio::command_mode_output, sda, // SDA
m5gfx::gpio::command_end,
};
m5gfx::gpio::command(cmd_low);
}
else m5gfx::gpio::command(cmd_list);

delay(50); // 延时 50ms,保证设备上电稳定

for (uint8_t i2caddr : (const uint8_t[]){static_cast<uint8_t>(addr << 1)}) { //detect address
delay(2); // 小延时
bool nack = true;
// I2C START
m5gfx::gpio_lo(sda); // SDA LOW = START
for (int cycle = 0; cycle < 20; ++cycle) {
// SCL toggle
m5gfx::gpio_hi(scl);
delay(1);
m5gfx::gpio_lo(scl);
delay(1);

if (cycle & 1) {
if (cycle == 17) {
nack = m5gfx::gpio_in(sda); // 读 ACK
}
} else {
if (i2caddr & 0x80) {
m5gfx::gpio_hi(sda);
} else {
m5gfx::gpio_lo(sda);
}
i2caddr <<= 1;
if (cycle >= 16) {
m5gfx::pinMode(sda, (cycle == 16) ? m5gfx::pin_mode_t::input : m5gfx::pin_mode_t::output);
}
}
}
m5gfx::gpio_hi(sda); // SDA HIGH = STOP
result = result << 1 | nack;
line_lo(scl); m5gfx::delayMicroseconds(5);
line_lo(sda); m5gfx::delayMicroseconds(5);
line_hi(scl); m5gfx::delayMicroseconds(5);
line_hi(sda); m5gfx::delayMicroseconds(5); // SCL High 中の SDA Low->High = STOP
}
}

if (check != 0x03 && bus_check() != 0x03)
{ // 握りが解けなかった。ここから先は probe しても意味がない。
for (auto& backup : pin_backup) { backup.restore(); }
return false;
}

bool hit = false;
if (m5gfx::i2c::init(probe_i2c_port, sda, scl).has_value())
{
// クロックが上がらないバス、前の通信の途中でデバイスがデータ線を握っている
// バスは、いずれも beginTransaction 側が検出して復旧または中断する。
// NACK の場合も beginTransaction 自体は成功を返し (内部で STOP を出して
// エラーをラッチする)、そのエラーは endTransaction が報告する。
// したがって両方の成功をもって「ACK が返った」と判定する。
hit = m5gfx::i2c::beginTransaction(probe_i2c_port, addr, 100000, false).has_value()
&& m5gfx::i2c::endTransaction(probe_i2c_port).has_value();
m5gfx::i2c::release(probe_i2c_port);
}
for (auto& backup : pin_backup) {
backup.restore();
}
return result;
return hit;
#endif
}

Expand Down Expand Up @@ -1656,27 +1721,35 @@ static constexpr const uint8_t _pin_table_mbus[][31] = {

if (board == board_t::board_unknown) {
/// PowerHub ?
if (_detect_i2c_device(45, 48, 0x50)) {
if (_probe_i2c_addr(45, 48, 0x50)) {
board = board_t::board_M5PowerHub;
}
}
break;

case 1: // EFUSE_PKG_VERSION_ESP32S3PICO: // LGA56
if (board == board_t::board_unknown) {
/// 内部 I2C バス (SDA45/SCL0) に載るデバイスで先に決める。他ピンの probe を
/// 間に挟まないので、ここで確定する機種は 48/47 に一切触れずに済む
/// (AtomVoiceS3R では GPIO48 が I2S の DOUT)。
///
/// AtomS3RExt / AtomS3RCam have a BMI270 on the internal I2C bus.
if (_detect_i2c_device(45, 0, 0x68)
|| _detect_i2c_device(45, 0, 0x69)) {
/// この 2 機種は基板が共通で、カメラ部がユーザーの扱えるブレッドボードに
/// なっているため、内部バスにユーザーのデバイスが載りうる。オンボードで
/// 必ず存在する BMI270 を先に見て、後から載ったアドレスに identity を
/// 奪われないようにする。
if (_probe_i2c_addr(45, 0, 0x68)
|| _probe_i2c_addr(45, 0, 0x69)) {
board = board_t::board_M5AtomS3RExt;
}
/// Stamp-S3Bat ?
else if (_detect_i2c_device(48, 47, 0x6E)) {
board = board_t::board_M5StampS3Bat;
}
/// AtomEchoS3R ?
else if(_detect_i2c_device(45, 0, 0x18)) {
/// AtomVoiceS3R ?
else if (_probe_i2c_addr(45, 0, 0x18)) {
board = board_t::board_M5AtomVoiceS3R;
}
/// Stamp-S3Bat ? (内部バスに何も居なかったときだけ別のピンを触る)
else if (_probe_i2c_addr(48, 47, 0x6E)) {
board = board_t::board_M5StampS3Bat;
}
/// StampS3Mini has no other onboard device that can identify it.
else {
board = board_t::board_M5StampS3Mini;
Expand Down Expand Up @@ -2170,13 +2243,13 @@ static constexpr const uint8_t _pin_table_mbus[][31] = {
auto& ioexp = getIOExpander(0);
// lcd backlight
ioexp.setDirection(7, true);
ioexp.setPullMode(7, false);
ioexp.setPullMode(7, IOExpander_Base::pull_down);
ioexp.setHighImpedance(7, false);

for (int i = 0; i < 3; ++i) {
// button a~c
ioexp.setDirection(i, false);
ioexp.setPullMode(i, true);
ioexp.setPullMode(i, IOExpander_Base::pull_up);
ioexp.setHighImpedance(i, false);
}
delay(100);
Expand Down
11 changes: 10 additions & 1 deletion src/M5Unified.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,16 @@ namespace m5
board_t _check_boardtype(board_t);
void _setup_i2c(board_t);
void _setup_led(board_t);
bool _detect_i2c_device(uint8_t sda, uint8_t scl, uint8_t addr, const uint8_t* cmd_list=nullptr);
/// probe を始める前に一度だけ、デバイスの電源が安定するのを待つ。
static void _wait_i2c_device_power(void);

/// 指定ピンのバス上に、指定した 7bit アドレスのデバイスが居るかを調べる。
/// (M5GFX にも同名だった _probe_i2c_addr があるが、あちらは複数アドレスを
/// ビット列で返す別物。取り違えを避けるため名前を分けている)
/// @return true = ACK が返った (デバイスが存在する)。
/// false は「ACK を確認できなかった」であり、不在のほかバスが
/// 成立していない場合・probe 用ポートの初期化に失敗した場合を含む。
bool _probe_i2c_addr(uint8_t sda, uint8_t scl, uint8_t addr);

static void _setup_pinmap(board_t);
static bool _speaker_enabled_cb_core2(void* args, bool enabled);
Expand Down
42 changes: 32 additions & 10 deletions src/utility/IOExpander_Base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,40 @@ namespace m5
class IOExpander_Base : public I2C_Device
{
public:
enum gpio_pull_t : std::uint8_t
{ pull_none = 0
, pull_up = 1
, pull_down = 2
};

/// Mutating methods report their result: the written value is not read
/// back, and read-modify-write accesses are not atomic against
/// concurrent access to the same register.

IOExpander_Base(std::uint8_t i2c_addr, std::uint32_t freq = 400000, m5::I2C_Class* i2c = &m5::In_I2C)
: I2C_Device(i2c_addr, freq, i2c)
{}
IOExpander_Base(const IOExpander_Base&) = delete;

// false input, true output
virtual void setDirection(uint8_t pin, bool direction) = 0;

virtual void enablePull(uint8_t pin, bool enablePull) = 0;
/// @return true when every required register access was acknowledged;
/// false for an invalid pin or I2C failure.
virtual bool setDirection(uint8_t pin, bool direction) = 0;

// false down, true up
virtual void setPullMode(uint8_t pin, bool mode) = 0;
/// Set the GPIO pull resistor state.
/// @return true when every required register access was acknowledged;
/// false for an invalid pin, mode, or I2C failure.
virtual bool setPullMode(uint8_t pin, gpio_pull_t mode) = 0;

virtual void setHighImpedance(uint8_t pin, bool enable) = 0;
/// @return true when every required register access was acknowledged;
/// false for an invalid pin or I2C failure.
virtual bool setHighImpedance(uint8_t pin, bool enable) = 0;

virtual bool getWriteValue(uint8_t pin) = 0;

virtual void digitalWrite(uint8_t pin, bool level) = 0;
/// @return true when every required register access was acknowledged;
/// false for an invalid pin or I2C failure.
virtual bool digitalWrite(uint8_t pin, bool level) = 0;

virtual bool digitalRead(uint8_t pin) = 0;

Expand All @@ -42,11 +58,17 @@ namespace m5
return false;
}

virtual void resetIrq() = 0;
/// @return true when every required register access was acknowledged;
/// false on I2C failure.
virtual bool resetIrq() = 0;

virtual void disableIrq() = 0;
/// @return true when every required register access was acknowledged;
/// false on I2C failure.
virtual bool disableIrq() = 0;

virtual void enableIrq() = 0;
/// @return true when every required register access was acknowledged;
/// false on I2C failure.
virtual bool enableIrq() = 0;
};
}

Expand Down
Loading
Loading