diff --git a/library.properties b/library.properties index bdc5630..ef6547e 100644 --- a/library.properties +++ b/library.properties @@ -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) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index f1a80f8..eda7f43 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -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; } } @@ -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 と同一の終端状態) } @@ -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(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 } @@ -1656,7 +1721,7 @@ 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; } } @@ -1664,19 +1729,27 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { 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; @@ -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); diff --git a/src/M5Unified.hpp b/src/M5Unified.hpp index 7e94124..53ebda9 100644 --- a/src/M5Unified.hpp +++ b/src/M5Unified.hpp @@ -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); diff --git a/src/utility/IOExpander_Base.hpp b/src/utility/IOExpander_Base.hpp index 2580e4e..32f901a 100644 --- a/src/utility/IOExpander_Base.hpp +++ b/src/utility/IOExpander_Base.hpp @@ -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; @@ -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; }; } diff --git a/src/utility/M5IOE1_Class.cpp b/src/utility/M5IOE1_Class.cpp index 7b0f08c..bb55ba1 100644 --- a/src/utility/M5IOE1_Class.cpp +++ b/src/utility/M5IOE1_Class.cpp @@ -39,47 +39,45 @@ namespace m5 return _init; } - void M5IOE1_Class::setDirection(uint8_t pin, bool direction) + bool M5IOE1_Class::setDirection(uint8_t pin, bool direction) { - if (!_isValidPin(pin)) { return; } + if (!_isValidPin(pin)) { return false; } // false=input, true=output. MODE bit set means output. const auto reg = _regForPin(M5IOE1_REG_GPIO_MODE_L, pin); const auto bit = _bitForPin(pin); - direction ? bitOn(reg, bit) : bitOff(reg, bit); + return direction ? bitOn(reg, bit) : bitOff(reg, bit); } - void M5IOE1_Class::enablePull(uint8_t pin, bool enablePull) + bool M5IOE1_Class::setPullMode(uint8_t pin, gpio_pull_t mode) { - if (!_isValidPin(pin)) { return; } + if (!_isValidPin(pin)) { return false; } const auto pu_reg = _regForPin(M5IOE1_REG_GPIO_PU_L, pin); const auto pd_reg = _regForPin(M5IOE1_REG_GPIO_PD_L, pin); const auto bit = _bitForPin(pin); - if (enablePull) { - bitOn(pu_reg, bit); - } else { - bitOff(pu_reg, bit); - bitOff(pd_reg, bit); + switch (mode) { + case pull_none: { + const bool pu_ok = bitOff(pu_reg, bit); + const bool pd_ok = bitOff(pd_reg, bit); + return pu_ok && pd_ok; + } + case pull_up: + if (!bitOff(pd_reg, bit)) { return false; } + return bitOn(pu_reg, bit); + case pull_down: + if (!bitOff(pu_reg, bit)) { return false; } + return bitOn(pd_reg, bit); + default: + return false; } } - void M5IOE1_Class::setPullMode(uint8_t pin, bool mode) - { - if (!_isValidPin(pin)) { return; } - const auto pu_reg = _regForPin(M5IOE1_REG_GPIO_PU_L, pin); - const auto pd_reg = _regForPin(M5IOE1_REG_GPIO_PD_L, pin); - const auto bit = _bitForPin(pin); - // false=pull-down, true=pull-up. - mode ? bitOn(pu_reg, bit) : bitOff(pu_reg, bit); - mode ? bitOff(pd_reg, bit) : bitOn(pd_reg, bit); - } - - void M5IOE1_Class::setHighImpedance(uint8_t pin, bool enable) + bool M5IOE1_Class::setHighImpedance(uint8_t pin, bool enable) { - if (!_isValidPin(pin)) { return; } + if (!_isValidPin(pin)) { return false; } // M5IOE1 exposes drive mode here: 0=push-pull, 1=open-drain. const auto reg = _regForPin(M5IOE1_REG_GPIO_DRV_L, pin); const auto bit = _bitForPin(pin); - enable ? bitOn(reg, bit) : bitOff(reg, bit); + return enable ? bitOn(reg, bit) : bitOff(reg, bit); } bool M5IOE1_Class::getWriteValue(uint8_t pin) @@ -88,12 +86,12 @@ namespace m5 return (readRegister8(_regForPin(M5IOE1_REG_GPIO_OUT_L, pin)) & _bitForPin(pin)) != 0; } - void M5IOE1_Class::digitalWrite(uint8_t pin, bool level) + bool M5IOE1_Class::digitalWrite(uint8_t pin, bool level) { - if (!_isValidPin(pin)) { return; } + if (!_isValidPin(pin)) { return false; } const auto reg = _regForPin(M5IOE1_REG_GPIO_OUT_L, pin); const auto bit = _bitForPin(pin); - level ? bitOn(reg, bit) : bitOff(reg, bit); + return level ? bitOn(reg, bit) : bitOff(reg, bit); } bool M5IOE1_Class::digitalRead(uint8_t pin) @@ -111,38 +109,47 @@ namespace m5 return true; } - void M5IOE1_Class::setPwmFrequency(std::uint16_t frequency) + bool M5IOE1_Class::setPwmFrequency(std::uint16_t frequency) { std::uint8_t data[2] = { static_cast(frequency & 0xFF), static_cast(frequency >> 8) }; - writeRegister(M5IOE1_REG_PWM_FREQ_L, data, sizeof(data)); + return writeRegister(M5IOE1_REG_PWM_FREQ_L, data, sizeof(data)); + } + + bool M5IOE1_Class::setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, + pwm_polarity_t polarity, bool enable) + { + if (duty > 100) { return false; } + auto duty12 = duty * 0x0FFF / 100; + return setPwmDuty12bit(channel, duty12, polarity, enable); } - void M5IOE1_Class::setPwmDuty(std::uint8_t channel, std::uint16_t duty12, bool enable, bool polarity) + bool M5IOE1_Class::setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, + pwm_polarity_t polarity, bool enable) { - if (channel > pwm_ch4) { return; } - duty12 &= 0x0FFF; + if (channel > pwm_ch4 || duty12 > 0x0FFF) { return false; } std::uint8_t high = static_cast(duty12 >> 8); if (enable) { high |= M5IOE1_PWM_ENABLE; } - if (polarity) { high |= M5IOE1_PWM_POLARITY; } + if (polarity == pwm_polarity_t::inverted) { high |= M5IOE1_PWM_POLARITY; } std::uint8_t data[2] = { static_cast(duty12 & 0xFF), high }; - writeRegister(static_cast(M5IOE1_REG_PWM1_DUTY_L + channel * 2), data, sizeof(data)); + auto reg = static_cast(M5IOE1_REG_PWM1_DUTY_L + static_cast(channel) * 2); + return writeRegister(reg, data, sizeof(data)); } - void M5IOE1_Class::resetIrq() + bool M5IOE1_Class::resetIrq() { std::uint8_t data[2] = { 0x00, 0x00 }; - writeRegister(M5IOE1_REG_GPIO_IS_L, data, sizeof(data)); + return writeRegister(M5IOE1_REG_GPIO_IS_L, data, sizeof(data)); } - void M5IOE1_Class::disableIrq() + bool M5IOE1_Class::disableIrq() { std::uint8_t data[2] = { 0x00, 0x00 }; - writeRegister(M5IOE1_REG_GPIO_IE_L, data, sizeof(data)); + return writeRegister(M5IOE1_REG_GPIO_IE_L, data, sizeof(data)); } - void M5IOE1_Class::enableIrq() + bool M5IOE1_Class::enableIrq() { std::uint8_t data[2] = { 0xFF, 0x3F }; - writeRegister(M5IOE1_REG_GPIO_IE_L, data, sizeof(data)); + return writeRegister(M5IOE1_REG_GPIO_IE_L, data, sizeof(data)); } } diff --git a/src/utility/M5IOE1_Class.hpp b/src/utility/M5IOE1_Class.hpp index c6c7fc0..f5ca244 100644 --- a/src/utility/M5IOE1_Class.hpp +++ b/src/utility/M5IOE1_Class.hpp @@ -5,6 +5,7 @@ #define __M5_M5IOE1_CLASS_H__ #include "IOExpander_Base.hpp" +#include "pwm_types.hpp" namespace m5 { @@ -44,30 +45,47 @@ namespace m5 bool begin(); - void setDirection(uint8_t pin, bool direction) override; + bool setDirection(uint8_t pin, bool direction) override; - void enablePull(uint8_t pin, bool enablePull) override; + bool setPullMode(uint8_t pin, gpio_pull_t mode) override; - void setPullMode(uint8_t pin, bool mode) override; - - void setHighImpedance(uint8_t pin, bool enable) override; + /// On the M5IOE1 this selects the drive mode (open-drain), which keeps + /// sinking the pin while the output latch is low; it is not a disconnect. + bool setHighImpedance(uint8_t pin, bool enable) override; bool getWriteValue(uint8_t pin) override; - void digitalWrite(uint8_t pin, bool level) override; + bool digitalWrite(uint8_t pin, bool level) override; bool digitalRead(uint8_t pin) override; bool getInputLevel(uint8_t pin, bool* level) override; - void setPwmFrequency(std::uint16_t frequency); + /// set the PWM frequency in Hz. + /// @note The frequency is shared by all PWM channels, so changing it also + /// changes a channel that is already running. + bool setPwmFrequency(std::uint16_t frequency); + + /// set PWM duty in percent. + /// @param channel PWM channel (pwm_ch1 - pwm_ch4). + /// @param duty duty cycle in percent (0-100). + /// @param polarity PWM output polarity. + /// @param enable true=enable / false=disable. + bool setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, + pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); - void setPwmDuty(std::uint8_t channel, std::uint16_t duty12, bool enable = true, bool polarity = false); + /// set PWM duty with 12-bit precision. + /// @param channel PWM channel (pwm_ch1 - pwm_ch4). + /// @param duty12 duty cycle (0-4095). + /// @param polarity PWM output polarity. + /// @param enable true=enable / false=disable. + bool setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, + pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); - void resetIrq() override; + bool resetIrq() override; - void disableIrq() override; + bool disableIrq() override; - void enableIrq() override; + bool enableIrq() override; private: static bool _isValidPin(uint8_t pin) { return pin < 14; } diff --git a/src/utility/PI4IOE5V6408_Class.cpp b/src/utility/PI4IOE5V6408_Class.cpp index f841ed4..44cd71b 100644 --- a/src/utility/PI4IOE5V6408_Class.cpp +++ b/src/utility/PI4IOE5V6408_Class.cpp @@ -20,40 +20,41 @@ bool PI4IOE5V6408_Class::begin() } // false input, true output -void PI4IOE5V6408_Class::setDirection(uint8_t pin, bool direction) +bool PI4IOE5V6408_Class::setDirection(uint8_t pin, bool direction) { + if (pin >= 8) { return false; } if (direction) { - bitOn(0x03, 1 << pin); // Output, set 1 + return bitOn(0x03, 1 << pin); // Output, set 1 } else { - bitOff(0x03, 1 << pin); // Input, set 0 + return bitOff(0x03, 1 << pin); // Input, set 0 } } -void PI4IOE5V6408_Class::enablePull(uint8_t pin, bool enablePull) +bool PI4IOE5V6408_Class::setPullMode(uint8_t pin, gpio_pull_t mode) { - if (enablePull) { - bitOn(0x0B, 1 << pin); - } else { - bitOff(0x0B, 1 << pin); - } -} - -// false down, true up -void PI4IOE5V6408_Class::setPullMode(uint8_t pin, bool mode) -{ - if (mode) { - bitOn(0x0D, 1 << pin); - } else { - bitOff(0x0D, 1 << pin); + if (pin >= 8) return false; + const auto bit = 1 << pin; + switch (mode) { + case pull_none: + return bitOff(0x0B, bit); + case pull_up: + if (!bitOn(0x0D, bit)) { return false; } + return bitOn(0x0B, bit); + case pull_down: + if (!bitOff(0x0D, bit)) { return false; } + return bitOn(0x0B, bit); + default: + return false; } } -void PI4IOE5V6408_Class::setHighImpedance(uint8_t pin, bool enable) +bool PI4IOE5V6408_Class::setHighImpedance(uint8_t pin, bool enable) { + if (pin >= 8) { return false; } if (enable) { - bitOn(0x07, 1 << pin); + return bitOn(0x07, 1 << pin); } else { - bitOff(0x07, 1 << pin); + return bitOff(0x07, 1 << pin); } } @@ -63,12 +64,13 @@ bool PI4IOE5V6408_Class::getWriteValue(uint8_t pin) return (data & (1 << pin)) != 0; } -void PI4IOE5V6408_Class::digitalWrite(uint8_t pin, bool level) +bool PI4IOE5V6408_Class::digitalWrite(uint8_t pin, bool level) { + if (pin >= 8) { return false; } if (level) { - bitOn(0x05, 1 << pin); + return bitOn(0x05, 1 << pin); } else { - bitOff(0x05, 1 << pin); + return bitOff(0x05, 1 << pin); } } @@ -78,18 +80,19 @@ bool PI4IOE5V6408_Class::digitalRead(uint8_t pin) return (data & (1 << pin)) != 0; } -void PI4IOE5V6408_Class::resetIrq() +bool PI4IOE5V6408_Class::resetIrq() { - readRegister8(0x13); + uint8_t value; + return readRegister(0x13, &value, 1); } -void PI4IOE5V6408_Class::disableIrq() +bool PI4IOE5V6408_Class::disableIrq() { - writeRegister8(0x11, 0B11111111); + return writeRegister8(0x11, 0B11111111); } -void PI4IOE5V6408_Class::enableIrq() +bool PI4IOE5V6408_Class::enableIrq() { - writeRegister8(0x11, 0x0); + return writeRegister8(0x11, 0x0); } } diff --git a/src/utility/PI4IOE5V6408_Class.hpp b/src/utility/PI4IOE5V6408_Class.hpp index 97e1a95..6948022 100644 --- a/src/utility/PI4IOE5V6408_Class.hpp +++ b/src/utility/PI4IOE5V6408_Class.hpp @@ -31,26 +31,23 @@ namespace m5 bool begin(); // false input, true output - void setDirection(uint8_t pin, bool direction) override; + bool setDirection(uint8_t pin, bool direction) override; - void enablePull(uint8_t pin, bool enablePull) override; + bool setPullMode(uint8_t pin, gpio_pull_t mode) override; - // false down, true up - void setPullMode(uint8_t pin, bool mode) override; - - void setHighImpedance(uint8_t pin, bool enable) override; + bool setHighImpedance(uint8_t pin, bool enable) override; bool getWriteValue(uint8_t pin) override; - void digitalWrite(uint8_t pin, bool level) override; + bool digitalWrite(uint8_t pin, bool level) override; bool digitalRead(uint8_t pin) override; - void resetIrq() override; + bool resetIrq() override; - void disableIrq() override; + bool disableIrq() override; - void enableIrq() override; + bool enableIrq() override; }; } diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 8531e11..f63a3b2 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -221,6 +221,8 @@ namespace m5 _wakeupPin = GPIO_NUM_2; /// bring up the PM1 early so its status registers are readable below. M5pm1.begin(); + // Enable the PM1 5V boost output (MBUS 5V and 3.3V) + M5pm1.setExtOutput(true); /// KEY1/2/3 are wired to PM1 GPIO0/1/2 (pressed = LOW) M5pm1.setGPIOFunction(M5PM1_Class::gpio0, M5PM1_Class::gpio); M5pm1.setGPIOFunction(M5PM1_Class::gpio1, M5PM1_Class::gpio); @@ -275,6 +277,46 @@ namespace m5 _wakeupPin = GPIO_NUM_4; /// bring up the PM1 early so its status registers are readable below. M5pm1.begin(); + /// GPIO4 drives the buzzer through PM1 PWM channel 1. The PM1 keeps + /// running across ESP resets and retains its PWM state, so put the + /// channel off at boot, then normalize the pin before selecting its PWM + /// function. Stopping the channel before sleep is left to the caller: + /// the PM1 stays powered while the ESP sleeps, so the application may + /// intend the PWM output to remain active, which makes it application + /// policy rather than board initialization. + /// Selecting the PWM function is what makes a retained duty audible + /// again, so it is only done once the channel is known to be off. If that + /// cannot be confirmed, the pin is left as a plain output driving low, + /// which is silent whatever the retained PWM state is. + bool pwm_off = false; + for (int retry = 3; !(pwm_off = M5pm1.setPwmDuty12bit(M5PM1_Class::pwm_ch1, 0, pwm_polarity_t::normal, false)) && --retry; ) + { + m5gfx::delay(10); + } + M5pm1.setGPIODrive(M5PM1_Class::gpio4, M5PM1_Class::push_pull); + M5pm1.setGPIOPull(M5PM1_Class::gpio4, M5PM1_Class::pull_none); + M5pm1.setGPIOOutput(M5PM1_Class::gpio4, false); + M5pm1.setGPIOMode(M5PM1_Class::gpio4, M5PM1_Class::output); + if (pwm_off) + { + M5pm1.setGPIOFunction(M5PM1_Class::gpio4, M5PM1_Class::special); + } + else + { + bool gpio_fallback = false; + for (int retry = 3; !(gpio_fallback = M5pm1.setGPIOFunction(M5PM1_Class::gpio4, M5PM1_Class::gpio)) && --retry; ) + { + m5gfx::delay(10); + } + if (gpio_fallback) + { + M5_LOGE("PM1 PWM ch1 could not be turned off. GPIO4 was switched to GPIO mode."); + } + else + { + M5_LOGE("PM1 PWM ch1 could not be turned off, and GPIO4 could not be switched to GPIO mode; silence cannot be guaranteed."); + } + } /// PM1 は常時給電で ESP のリセットを跨いで状態が残るため、直前に動いて /// いたファームの設定に依存しないよう IRQ 関連を初期化する M5pm1.clearWakeSource(); @@ -292,7 +334,10 @@ namespace m5 auto& ioe1 = M5.getIOExpander(0); ioe1.setDirection(M5IOE1_Class::gpio1, false); ioe1.setHighImpedance(M5IOE1_Class::gpio1, true); - ioe1.enablePull(M5IOE1_Class::gpio1, false); + if (!ioe1.setPullMode(M5IOE1_Class::gpio1, IOExpander_Base::pull_none)) + { + M5_LOGE("M5IOE1 CHG_PROG pull state could not be released."); + } ioe1.setDirection(M5IOE1_Class::gpio3, false); } M5pm1.setBatteryCharge(true); @@ -369,11 +414,17 @@ namespace m5 // M5IOE1: PWM1 drives IO9 (G9 motor). REG_PWM_FREQ 0x25/0x26 Hz LE; REG_PWM1_DUTY 0x1B/0x1C (bit7 EN). constexpr uint16_t motor_pwm_hz = 2000; auto& ioe1 = static_cast(M5.getIOExpander(0)); - ioe1.setPwmFrequency(motor_pwm_hz); - // IO9 (G9 motor / PWM1): push-pull output, duty off until setVibration - ioe1.setHighImpedance(M5IOE1_Class::gpio9, false); - ioe1.setDirection(M5IOE1_Class::gpio9, true); - ioe1.setPwmDuty(M5IOE1_Class::pwm_ch1, 0, false); // PWM off at boot + if (ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch1, 0, pwm_polarity_t::normal, false)) + { + ioe1.setPwmFrequency(motor_pwm_hz); + // IO9 (G9 motor / PWM1): push-pull output, duty off until setVibration + ioe1.setHighImpedance(M5IOE1_Class::gpio9, false); + ioe1.setDirection(M5IOE1_Class::gpio9, true); + } + else + { + M5_LOGE("M5IOE1 PWM ch1 could not be turned off. Motor output was not enabled."); + } } break; @@ -436,7 +487,7 @@ namespace m5 auto& ioe1 = M5.getIOExpander(0); // M5IOE1_G3 -- Charge Status ioe1.setDirection(M5IOE1_Class::gpio3, false); - ioe1.enablePull(M5IOE1_Class::gpio3, false); + ioe1.setPullMode(M5IOE1_Class::gpio3, IOExpander_Base::pull_none); // M5IOE1_G4 -- Boost Control ioe1.setHighImpedance(M5IOE1_Class::gpio4, false); ioe1.setDirection(M5IOE1_Class::gpio4, true); @@ -845,13 +896,13 @@ namespace m5 if (port_mask & ext_port_mask_t::ext_PA) { auto& ioe = M5.getIOExpander(0); - ioe.setPullMode(2, enable); + ioe.setPullMode(2, enable ? IOExpander_Base::pull_up : IOExpander_Base::pull_down); ioe.digitalWrite(2, enable); } if (port_mask & ext_port_mask_t::ext_USB) { auto& ioe = M5.getIOExpander(1); - ioe.setPullMode(3, enable); + ioe.setPullMode(3, enable ? IOExpander_Base::pull_up : IOExpander_Base::pull_down); ioe.digitalWrite(3, enable); } break; @@ -1508,7 +1559,7 @@ namespace m5 #else ESP_LOGD("Power","deepSleep"); #if defined (CONFIG_IDF_TARGET_ESP32C3) || defined (CONFIG_IDF_TARGET_ESP32C6) // || defined (CONFIG_IDF_TARGET_ESP32P4) - + ESP_LOGW("Power","deepSleep: deep sleep is not supported on this target."); #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) @@ -1639,8 +1690,8 @@ namespace m5 (void)touch_wakeup; #else ESP_LOGD("Power","lightSleep"); -#if defined (CONFIG_IDF_TARGET_ESP32C3) || defined (CONFIG_IDF_TARGET_ESP32C6) || defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32H2) || defined (CONFIG_IDF_TARGET_ESP32P4) - +#if defined (CONFIG_IDF_TARGET_ESP32C3) || defined (CONFIG_IDF_TARGET_ESP32C6) || defined (CONFIG_IDF_TARGET_ESP32H2) || defined (CONFIG_IDF_TARGET_ESP32P4) + ESP_LOGW("Power","lightSleep: light sleep is not supported on this target."); #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) @@ -2302,14 +2353,14 @@ namespace m5 auto& ioe1 = M5.getIOExpander(0); if (max_mA >= 650) { - ioe1.enablePull(M5IOE1_Class::gpio3, false); + ioe1.setPullMode(M5IOE1_Class::gpio3, IOExpander_Base::pull_none); ioe1.digitalWrite(M5IOE1_Class::gpio3, false); ioe1.setHighImpedance(M5IOE1_Class::gpio3, false); ioe1.setDirection(M5IOE1_Class::gpio3, true); } else { - ioe1.enablePull(M5IOE1_Class::gpio3, false); + ioe1.setPullMode(M5IOE1_Class::gpio3, IOExpander_Base::pull_none); ioe1.setDirection(M5IOE1_Class::gpio3, false); } } @@ -2340,6 +2391,21 @@ namespace m5 M5pm1.setGPIOOutput(M5PM1_Class::gpio3, true); } break; +#elif defined (CONFIG_IDF_TARGET_ESP32C5) + case pmic_t::pmic_m5pm1: + if (M5.getBoard() == board_t::board_M5ToughC5) + { + // ToughC5 CHG_PROG is IOE1 G1: low selects 830 mA, high selects 180 mA. + // Set the latch before enabling push-pull output to avoid a transient + // selection of the opposite current during the mode transition. + auto& ioe1 = M5.getIOExpander(0); + const bool select_180mA = max_mA < 830; + ioe1.setPullMode(M5IOE1_Class::gpio1, IOExpander_Base::pull_none); + ioe1.digitalWrite(M5IOE1_Class::gpio1, select_180mA); + ioe1.setHighImpedance(M5IOE1_Class::gpio1, false); + ioe1.setDirection(M5IOE1_Class::gpio1, true); + } + return; #endif #endif @@ -2423,7 +2489,9 @@ namespace m5 switch (M5.getBoard()) { #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: - return 1000.0f * Ina226.getShuntCurrent(); + // The shunt is wired so that charge current reads negative; invert to + // match the documented convention (+ = charge / - = discharge). + return -1000.0f * Ina226.getShuntCurrent(); #endif #if defined (CONFIG_IDF_TARGET_ESP32S3) @@ -2531,8 +2599,9 @@ namespace m5 #if defined (CONFIG_IDF_TARGET_ESP32S3) case board_t::board_M5PaperMono: { - // Running from battery (no external power) -> not charging. - if (M5pm1.getPowerSource() == M5PM1_Class::battery) { return is_charging_t::is_discharging; } + // No external power -> not charging. PWR_SRC is a bitmap, and the battery bit may coexist with VIN. + auto sources = M5pm1.getPowerSource(); + if (!(sources & (M5PM1_Class::vin | M5PM1_Class::vinout))) { return is_charging_t::is_discharging; } // External power present. The IP2316 charger reports // its state in REG_CHG_STAT(0xC7): bit7 = charging in progress (measured: // 0x82 charging / 0x45 charge-complete / 0x00 charge-disabled). @@ -2748,13 +2817,13 @@ namespace m5 // M5IOE1 PWM1 (0x1B/0x1C) -> pin IO9 / G9 motor; duty 12-bit in [11:0], EN=bit7 of high byte. auto& ioe1 = static_cast(M5.getIOExpander(0)); if (level == 0) { - ioe1.setPwmDuty(M5IOE1_Class::pwm_ch1, 0, false); + ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch1, 0, pwm_polarity_t::normal, false); } else { // PWM needs IO9 in output mode (M5IOE1 pin index 8 -> GPIO_MODE_H bit0). ioe1.setHighImpedance(M5IOE1_Class::gpio9, false); ioe1.setDirection(M5IOE1_Class::gpio9, true); uint16_t duty12 = static_cast((static_cast(level) * 0x0FFFu) / 255u); - ioe1.setPwmDuty(M5IOE1_Class::pwm_ch1, duty12); + ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch1, duty12); } return; } diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index 5cde802..b02dfd9 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -161,7 +161,8 @@ namespace m5 /// set battery charge current /// @param max_mA milli ampere. - /// @note CoreMatrix selects the nearest supported maximum: 180 mA below 650 mA, otherwise 650 mA. + /// @note CoreMatrix selects 180 mA below 650 mA, otherwise 650 mA. + /// @note ToughC5 selects 180 mA below 830 mA, otherwise 830 mA. /// @attention Non-functioning models : CoreInk , M5Paper , M5Stack(with non I2C IP5306) void setChargeCurrent(std::uint16_t max_mA); @@ -203,7 +204,8 @@ namespace m5 /// Get Power Key Press condition. /// @return 0=none / 1=long pressed / 2=short clicked / 3=both - /// @attention Only for models with AXP192 or AXP2101 + /// @attention Only for models with AXP192, AXP2101, or M5PM1. + /// @attention M5PM1 reports only 0 or 2. /// @attention Once this function is called, the value is reset to 0, and the next time it is pressed on, the value changes. uint8_t getKeyState(void); diff --git a/src/utility/RTC_Class.cpp b/src/utility/RTC_Class.cpp index e52694c..2f227b4 100644 --- a/src/utility/RTC_Class.cpp +++ b/src/utility/RTC_Class.cpp @@ -29,6 +29,16 @@ namespace m5 M5.Power.M5pm1.clearIRQStatus(); } } +#elif defined (CONFIG_IDF_TARGET_ESP32C5) + static void clear_m5pm1_rtc_irq(void) + { + if (M5.getBoard() == board_t::board_M5ToughC5 + && M5.Power.getType() == Power_Class::pmic_t::pmic_m5pm1) + { + M5.Power.M5pm1.clearWakeSource(); + M5.Power.M5pm1.clearIRQStatus(); + } + } #else static void clear_m5pm1_rtc_irq(void) {} #endif diff --git a/src/utility/Speaker_Class.cpp b/src/utility/Speaker_Class.cpp index 38d2fc9..a0df266 100644 --- a/src/utility/Speaker_Class.cpp +++ b/src/utility/Speaker_Class.cpp @@ -682,6 +682,7 @@ namespace m5 uint8_t next_state = ch_info->wavinfo[flip].state.load(std::memory_order_acquire); size_t idx = 0; + bool flush_partial = false; if (current_wav->repeat == 0 || ((next_state & (wav_phase_mask | wav_state_stop_current)) == (wav_phase_published | wav_state_stop_current))) @@ -703,6 +704,7 @@ namespace m5 // further below, once flip has moved off of it - freeing it // here would let a writer claim it while flip still points at // it, and the later retirement would wipe that claim out. + flush_partial = false; current_wav->clear(); } // the finished (or cut) request goes back to the writers before @@ -737,6 +739,11 @@ namespace m5 { // nothing to do; a writer caught mid-publish raises the bit itself. ch_info->diff = 0; ch_info->index = 0; + if (flush_partial) + { // Keep I2S words aligned and avoid a trailing half-word on HW v1. + const size_t flush_length = (idx + 1) & ~size_t{1}; + if (data_length < flush_length) { data_length = flush_length; } + } continue; } self->_play_channel_bits.fetch_or(1 << ch); @@ -768,6 +775,7 @@ namespace m5 current_wav->repeat = --repeat; if (repeat == 0) { + flush_partial = true; goto label_next_wav; } } diff --git a/src/utility/led/LED_PaperMono_Class.cpp b/src/utility/led/LED_PaperMono_Class.cpp index 8650fdd..bb3ab26 100644 --- a/src/utility/led/LED_PaperMono_Class.cpp +++ b/src/utility/led/LED_PaperMono_Class.cpp @@ -76,7 +76,7 @@ namespace m5 ioe1.digitalWrite(ioe1_led_b_pin, b >= 2048); if (g > 4095) { g = 4095; } - ioe1.setPwmDuty(M5IOE1_Class::pwm_ch2, g, g > 0); + ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch2, g, pwm_polarity_t::normal, g > 0); } } diff --git a/src/utility/power/BQ27220_Class.cpp b/src/utility/power/BQ27220_Class.cpp index 8786c1a..db2baf9 100644 --- a/src/utility/power/BQ27220_Class.cpp +++ b/src/utility/power/BQ27220_Class.cpp @@ -3,7 +3,7 @@ #include "BQ27220_Class.hpp" -#include +#include #include #include @@ -21,7 +21,7 @@ namespace m5 if (length == 0) { return true; } - M5.delay(10); + m5gfx::delay(10); if (_i2c->readRegister(_addr, 0x3E, regData, length, _freq)) { return true; } @@ -38,7 +38,7 @@ namespace m5 read_MuxAddrdata(0x00, 0x0001, read_data, 4); // printf("BQ27220 W:0x00->0x0001 R:0x%02X %02X %02X %02X\r\n", read_data[0], read_data[1], read_data[2], read_data[3]); - M5.delay(200); + m5gfx::delay(200); // exit_sealed read_MuxAddrdata(0x00, 0x8000); diff --git a/src/utility/power/M5PM1_Class.cpp b/src/utility/power/M5PM1_Class.cpp index bb95adc..76dc211 100644 --- a/src/utility/power/M5PM1_Class.cpp +++ b/src/utility/power/M5PM1_Class.cpp @@ -30,6 +30,8 @@ namespace m5 static constexpr const uint8_t M5PM1_REG_VBAT_L = 0x22; static constexpr const uint8_t M5PM1_REG_VIN_L = 0x24; static constexpr const uint8_t M5PM1_REG_5VOUT_L = 0x26; + static constexpr const uint8_t M5PM1_REG_PWM0_L = 0x30; + static constexpr const uint8_t M5PM1_REG_PWM_FREQ_L = 0x34; static constexpr const uint8_t M5PM1_REG_IRQ_STATUS1 = 0x40; static constexpr const uint8_t M5PM1_REG_IRQ_STATUS2 = 0x41; static constexpr const uint8_t M5PM1_REG_IRQ_STATUS3 = 0x42; @@ -43,6 +45,8 @@ namespace m5 static constexpr const uint8_t M5PM1_PWR_CFG_BOOST_EN = 1 << 3; static constexpr const uint8_t M5PM1_PWR_CFG_LED_EN = 1 << 4; static constexpr const uint8_t M5PM1_SYS_CMD_SHUTDOWN = 0xA1; + static constexpr const uint8_t M5PM1_PWM_ENABLE = 1 << 4; + static constexpr const uint8_t M5PM1_PWM_POLARITY = 1 << 5; static constexpr bool is_valid_gpio(M5PM1_Class::gpio_t pin) { @@ -54,6 +58,11 @@ namespace m5 return static_cast(pin); } + static constexpr bool is_valid_pwm_channel(M5PM1_Class::pwm_channel_t channel) + { + return static_cast(channel) <= M5PM1_Class::pwm_ch1; + } + bool M5PM1_Class::begin(void) { if (!_init) { @@ -99,9 +108,8 @@ namespace m5 M5PM1_Class::pwr_src_t M5PM1_Class::getPowerSource(void) { - if (!_init) { return unknown; } - auto src = readRegister8(M5PM1_REG_PWR_SRC) & 0x07; - return src <= static_cast(battery) ? static_cast(src) : unknown; + if (!_init) { return none; } + return static_cast(readRegister8(M5PM1_REG_PWR_SRC) & 0x07); } bool M5PM1_Class::getVbatNodePowered(bool* powered) @@ -113,7 +121,8 @@ namespace m5 bool M5PM1_Class::setGPIOFunction(gpio_t pin, gpio_function_t function) { - if (!is_valid_gpio(pin)) { return false; } + if (!is_valid_gpio(pin) + || (function != gpio && function != irq && function != special)) { return false; } auto num = gpio_num(pin); auto reg = num < 4 ? M5PM1_REG_GPIO_FUNC0 : M5PM1_REG_GPIO_FUNC1; auto shift = static_cast((num < 4 ? num : num - 4) * 2); @@ -176,10 +185,38 @@ namespace m5 return readRegister8(M5PM1_REG_GPIO_OUT) & (1 << gpio_num(pin)); } + bool M5PM1_Class::setPwmFrequency(std::uint16_t frequency) + { + std::uint8_t data[2] = + { static_cast(frequency & 0xFF) + , static_cast(frequency >> 8) + }; + return writeRegister(M5PM1_REG_PWM_FREQ_L, data, sizeof(data)); + } + + bool M5PM1_Class::setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, + pwm_polarity_t polarity, bool enable) + { + if (duty > 100) { return false; } + auto duty12 = duty * 0x0FFF / 100; + return setPwmDuty12bit(channel, duty12, polarity, enable); + } + + bool M5PM1_Class::setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, + pwm_polarity_t polarity, bool enable) + { + if (!is_valid_pwm_channel(channel) || duty12 > 0x0FFF) { return false; } + std::uint8_t high = static_cast(duty12 >> 8); + if (enable) { high |= M5PM1_PWM_ENABLE; } + if (polarity == pwm_polarity_t::inverted) { high |= M5PM1_PWM_POLARITY; } + std::uint8_t data[2] = { static_cast(duty12 & 0xFF), high }; + auto reg = static_cast(M5PM1_REG_PWM0_L + static_cast(channel) * 2); + return writeRegister(reg, data, sizeof(data)); + } + bool M5PM1_Class::clearWakeSource(std::uint8_t mask) { - auto src = readRegister8(M5PM1_REG_WAKE_SRC); - return writeRegister8(M5PM1_REG_WAKE_SRC, src & ~mask); + return writeRegister8(M5PM1_REG_WAKE_SRC, static_cast(~mask & 0x7F)); } bool M5PM1_Class::clearGPIOIRQStatus(void) @@ -227,7 +264,7 @@ namespace m5 bool M5PM1_Class::getBatteryCharge(bool* enabled) { - if (!_init) { return false; } + if (!_init || enabled == nullptr) { return false; } std::uint8_t cfg = 0; if (!readRegister(M5PM1_REG_PWR_CFG, &cfg, 1)) { return false; } *enabled = cfg & M5PM1_PWR_CFG_CHG_EN; @@ -237,25 +274,11 @@ namespace m5 bool M5PM1_Class::setChargeCurrent(std::uint16_t max_mA) { return false; - // if (!_init) return false; - // int value = max_mA / 8; // Convert mA to register value (8mA per step) - // if (value > 0) { value -= 1; // 0 = 8mA, 63 = 512mA - // if (value >= 64) value = 63; // max value is 512mA (8 + 63*8) - // } - // return writeRegister8(M5PM1_REG_CHR_CUR, value); } bool M5PM1_Class::setChargeVoltage(std::uint16_t max_mV) { return false; - // if (!_init) return false; - // int value = (max_mV - 3600) / 15; // Convert mV to register value (15mV per step) - // if (value > 0) { value -= 1; // 0 = 3600mV, 63 = 4545mV - // if (value >= 64) value = 63; // max value is 4545mV (3600 + 63*15) - // } - // uint8_t reg_value = readRegister8(M5PM1_REG_CHR_VOL); - // reg_value &= 0xC0; - // return writeRegister8(M5PM1_REG_CHR_VOL, reg_value | value); } std::uint16_t M5PM1_Class::getChargeCurrent(void) @@ -277,14 +300,19 @@ namespace m5 { if (!_init) return 0; uint8_t irq3 = 0; - if (readRegister(M5PM1_REG_IRQ_STATUS3, &irq3, 1)) - { - if (irq3 & ((1 << 0) | (1 << 2))) { - writeRegister8(M5PM1_REG_IRQ_STATUS3, 0); - return 2; - } - } - return 0; + if (!readRegister(M5PM1_REG_IRQ_STATUS3, &irq3, 1)) return 0; + uint8_t pending = irq3 & ((1 << 0) | (1 << 2)); + if (!pending) return 0; + if (!writeRegister8(M5PM1_REG_IRQ_STATUS3, static_cast(~pending & 0x07))) return 0; + _pek_double_pending = (pending & (1 << 2)) != 0; + return 2; + } + + bool M5PM1_Class::wasPekDoubleClicked(void) + { + bool result = _pek_double_pending; + _pek_double_pending = false; + return result; } std::uint16_t M5PM1_Class::getVBUSVoltage(void) @@ -302,7 +330,7 @@ namespace m5 bool M5PM1_Class::getBatteryVoltage(std::uint16_t* millivolt) { - if (!_init) { return false; } + if (!_init || millivolt == nullptr) { return false; } std::uint8_t buf[2] = {}; if (!readRegister(M5PM1_REG_VBAT_L, buf, sizeof(buf))) { return false; } *millivolt = (buf[1] << 8) | buf[0]; diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index fd504e3..88138e4 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -5,6 +5,7 @@ #define __M5_M5PM1_CLASS_H__ #include "../I2C_Class.hpp" +#include "../pwm_types.hpp" namespace m5 { @@ -38,7 +39,7 @@ namespace m5 enum gpio_function_t : std::uint8_t { gpio = 0b00 , irq = 0b01 - , wake = 0b10 + // 0b10 is reserved in the datasheet. , special = 0b11 }; @@ -55,12 +56,18 @@ namespace m5 , open_drain = 1 }; - /// PM1 power source status. + /// PM1 PWM channel. Both channels share the same frequency setting. + enum pwm_channel_t : std::uint8_t + { pwm_ch0 = 0 // GPIO3; may be assigned to another function depending on the board. + , pwm_ch1 = 1 // GPIO4 + }; + + /// PM1 power source bitmap. Multiple values may be combined. enum pwr_src_t : std::uint8_t - { vin = 0 - , vinout = 1 - , battery = 2 - , unknown = 3 + { none = 0 + , vin = 1 << 0 + , vinout = 1 << 1 + , battery = 1 << 2 }; /// set BOOST/Grove 5V output enable. @@ -74,7 +81,7 @@ namespace m5 /// @param enable true=enable / false=disable bool setLDOOutput(bool enable); - /// set PM1 5V DCDC output enable. + /// set PM1 3.3V DCDC rail output enable (PWR_CFG bit1 = 3.3V_DCDC_EN). /// @param enable true=enable / false=disable bool setDCDCOutput(bool enable); @@ -82,7 +89,9 @@ namespace m5 /// @param level true=high / false=low bool setLedEnLevel(bool level); - /// get current PM1 power source. + /// get the PM1 PWR_SRC bitmap. + /// bit0=5VIN valid, bit1=5VINOUT valid (0 while the 5V boost is enabled), + /// bit2=VBAT node valid. Multiple sources may be present simultaneously. pwr_src_t getPowerSource(void); /// get whether PWR_SRC reports the VBAT node rail as powered. @@ -113,7 +122,37 @@ namespace m5 /// get PM1 GPIO output latch level, not the physical input level. bool getGPIOOutputLatch(gpio_t pin); + /// set the PWM frequency in Hz. + /// @note The frequency is shared by both PWM channels, so changing it also + /// changes a channel that is already running. + /// @note PWM channel 0 maps to GPIO3 and channel 1 maps to GPIO4. Call + /// setGPIOFunction() with special separately to route PWM to the pin. + bool setPwmFrequency(std::uint16_t frequency); + + /// set PWM duty in percent. + /// @param channel PWM channel (pwm_ch0 / pwm_ch1). + /// @param duty duty cycle in percent (0-100). + /// @param polarity PWM output polarity. + /// @param enable true=enable / false=disable. + /// @note PWM channel 0 maps to GPIO3 and channel 1 maps to GPIO4. Call + /// setGPIOFunction() with special separately to route PWM to the pin. + bool setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, + pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); + + /// set PWM duty with 12-bit precision. + /// @param channel PWM channel (pwm_ch0 / pwm_ch1). + /// @param duty12 duty cycle (0-4095). + /// @param polarity PWM output polarity. + /// @param enable true=enable / false=disable. + /// @note PWM channel 0 maps to GPIO3 and channel 1 maps to GPIO4. Call + /// setGPIOFunction() with special separately to route PWM to the pin. + bool setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, + pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); + /// clear PM1 wake source bits selected by mask. + /// Single-write selective clear assuming the write-zero-to-clear behavior + /// adopted by the official driver; the datasheet does not specify the write polarity. + /// Bits outside [6:0] are written as zero, matching the full-clear precedent. bool clearWakeSource(std::uint8_t mask = 0x7F); /// clear all PM1 GPIO IRQ status bits. @@ -147,28 +186,42 @@ namespace m5 bool getBatteryCharge(bool* enabled); /// set battery charge current - /// @param max_mA milli ampere. (8 - 512). + /// @param max_mA ignored; the PM1 has no charge current register. + /// @note The PM1 register map exposes no charge current register; this is a permanent stub returning false. bool setChargeCurrent(std::uint16_t max_mA); /// set battery charge voltage - /// @param max_mV milli volt. (3600 - 4545). + /// @param max_mV ignored; the PM1 has no charge voltage register. + /// @note The PM1 register map exposes no charge voltage register; this is a permanent stub returning false. bool setChargeVoltage(std::uint16_t max_mV); /// Get whether the battery is currently charging or not. + /// @note The PM1 register map exposes no charging status register; this is a permanent stub returning false. bool isCharging(void); // get setting value of battery charge current - /// @return milli ampere. (8 - 512). 0=unknown + /// @return always 0. + /// @note The PM1 register map exposes no charge current register; this is a permanent stub returning 0. std::uint16_t getChargeCurrent(void); // get setting value of battery charge voltage - /// @return milli volt. (3600 - 4545). 0=unknown + /// @return always 0. + /// @note The PM1 register map exposes no charge voltage register; this is a permanent stub returning 0. std::uint16_t getChargeVoltage(void); /// Get power key press condition. - /// @return 0=none / 2=short clicked + /// @return 0=none / 2=short clicked. For AXP compatibility, a double click + /// also reports 2; use wasPekDoubleClicked() to distinguish it. + /// Only the consumed click flags are cleared; the WAKEUP flag is preserved. + /// Returns 0 and leaves the event pending if the clear write fails. uint8_t getPekPress(void); + /// Returns whether the most recently reported click (getPekPress() == 2) + /// was a double click, then clears the flag. + /// Call from the task that polls getPekPress() (typically right after + /// M5.update()); the flag is not synchronized across tasks. + bool wasPekDoubleClicked(void); + /// get VIN voltage. /// @return milli volt. 0=read failed std::uint16_t getVBUSVoltage(void); @@ -188,6 +241,9 @@ namespace m5 /// power off PM1. bool powerOff(void); + + private: + bool _pek_double_pending = false; }; } diff --git a/src/utility/pwm_types.hpp b/src/utility/pwm_types.hpp new file mode 100644 index 0000000..0dc6fe0 --- /dev/null +++ b/src/utility/pwm_types.hpp @@ -0,0 +1,21 @@ +// Copyright (c) M5Stack. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#ifndef __M5_PWM_TYPES_H__ +#define __M5_PWM_TYPES_H__ + +#include + +namespace m5 +{ + /// PWM output polarity. + /// normal : the duty is the high time. The output idles low (POL = 0). + /// inverted : the duty is the low time. The output idles high (POL = 1), + /// which the datasheet calls active low. + enum class pwm_polarity_t : std::uint8_t + { normal = 0 + , inverted = 1 + }; +} + +#endif