diff --git a/idf_component.yml b/idf_component.yml index c2c1144..080e9a2 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -2,7 +2,7 @@ description: Unified library for M5Stack series issues: https://github.com/m5stack/M5Unified/issues repository: https://github.com/m5stack/M5Unified.git url: https://github.com/m5stack/M5Unified.git -version: 0.2.20 +version: 0.2.21 dependencies: m5stack/m5gfx: - version: ">=0.2.27" + version: ">=0.2.28" diff --git a/library.json b/library.json index b6fd8f6..3795cb1 100644 --- a/library.json +++ b/library.json @@ -13,10 +13,10 @@ "dependencies": [ { "name": "M5GFX", - "version": ">=0.2.27" + "version": ">=0.2.28" } ], - "version": "0.2.20", + "version": "0.2.21", "frameworks": ["arduino", "espidf", "*"], "platforms": ["espressif32", "native"], "headers": "M5Unified.h" diff --git a/library.properties b/library.properties index bdc5630..55152ac 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5Unified -version=0.2.20 +version=0.2.21 author=M5Stack maintainer=M5Stack sentence=Unified library for M5Stack series @@ -8,4 +8,4 @@ category=Display url=https://github.com/m5stack/M5Unified.git architectures=esp32 includes=M5Unified.h -depends=M5GFX +depends=M5GFX (>=0.2.28) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index f1a80f8..e44dd99 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -126,6 +126,7 @@ static constexpr const uint8_t _pin_table_i2c_ex_in[][5] = { { board_t::board_M5NanoH2 , 255 ,255 , GPIO_NUM_1 ,GPIO_NUM_2 }, { board_t::board_unknown , 255 ,255 , 255 ,255 }, #elif defined (CONFIG_IDF_TARGET_ESP32P4) +{ board_t::board_M5CoreP4X , GPIO_NUM_9 ,GPIO_NUM_11 , GPIO_NUM_16,GPIO_NUM_18 }, // CoreP4X { board_t::board_M5Tab5 , GPIO_NUM_32,GPIO_NUM_31 , GPIO_NUM_54,GPIO_NUM_53 }, // Tab5 { board_t::board_M5UnitPoEP4 , GPIO_NUM_1 ,GPIO_NUM_0 , GPIO_NUM_54,GPIO_NUM_53 }, { board_t::board_unknown , 255 ,255 , 255 ,255 }, @@ -219,6 +220,7 @@ static constexpr const uint8_t _pin_table_sd[][7] = { { board_t::board_M5CoreMatrix , GPIO_NUM_25, GPIO_NUM_27, GPIO_NUM_26, 255 , 255 , GPIO_NUM_28 }, #elif defined (CONFIG_IDF_TARGET_ESP32H2) #elif defined (CONFIG_IDF_TARGET_ESP32P4) +{ board_t::board_M5CoreP4X , GPIO_NUM_10, GPIO_NUM_7 , GPIO_NUM_8 , 255 , 255 , GPIO_NUM_50 }, { board_t::board_M5Tab5 , GPIO_NUM_43, GPIO_NUM_44, GPIO_NUM_39, GPIO_NUM_40, GPIO_NUM_41, GPIO_NUM_42 }, #elif defined (CONFIG_IDF_TARGET_ESP32C5) { board_t::board_M5ToughC5 , GPIO_NUM_9 , GPIO_NUM_7 , GPIO_NUM_8 , 255 , 255 , GPIO_NUM_10 }, @@ -299,6 +301,23 @@ static constexpr const uint8_t _pin_table_other1[][2] = { static constexpr const uint8_t _pin_table_mbus[][31] = { #if defined (CONFIG_IDF_TARGET_ESP32P4) +{ board_t::board_M5CoreP4X, + 255 , GPIO_NUM_17, + 255 , GPIO_NUM_20, + 255 , 255 , + GPIO_NUM_7 , GPIO_NUM_21, + GPIO_NUM_8 , GPIO_NUM_22, + GPIO_NUM_10, 255 , + GPIO_NUM_38, GPIO_NUM_37, + GPIO_NUM_15, GPIO_NUM_14, + GPIO_NUM_11, GPIO_NUM_9 , + GPIO_NUM_18, GPIO_NUM_16, + GPIO_NUM_39, GPIO_NUM_12, + GPIO_NUM_34, GPIO_NUM_23, + 255 , GPIO_NUM_19, + 255 , 255 , + 255 , 255 , +}, { board_t::board_M5Tab5 , 255 , GPIO_NUM_16, 255 , GPIO_NUM_17, @@ -449,6 +468,10 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { void M5Unified::_setup_pinmap(board_t id) { + if (id == board_t::board_M5Tab5X) { + id = board_t::board_M5Tab5; + } + constexpr const std::pair tbl[] = { { _pin_table_i2c_ex_in, sizeof(_pin_table_i2c_ex_in[0]) }, { _pin_table_port_bc, sizeof(_pin_table_port_bc[0]) }, @@ -481,7 +504,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 +787,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 と同一の終端状態) } @@ -772,6 +795,78 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { return true; } + static void _corep4x_audio_power(M5Unified* self, bool enabled) + { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + // Speaker and microphone share M5IOE1_G1, so keep the rail enabled at runtime. + if (!enabled) { return; } + auto& ioe1 = self->getIOExpander(0); + ioe1.setHighImpedance(M5IOE1_Class::gpio1, false); + ioe1.setDirection(M5IOE1_Class::gpio1, true); + ioe1.digitalWrite(M5IOE1_Class::gpio1, true); + self->delay(20); +#else + (void)self; + (void)enabled; +#endif + } + + bool M5Unified::_speaker_enabled_cb_corep4x(void* args, bool enabled) + { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + auto self = (M5Unified*)args; + static constexpr const uint8_t enabled_bulk_data[] = { + // ES8311 slave, 24 kHz, 16-bit I2S, MCLK = 256 * sample rate. + 2, 0x0D, 0xFA, + 2, 0x44, 0x08, + 2, 0x44, 0x08, + 2, 0x01, 0x30, + 2, 0x02, 0x00, + 2, 0x03, 0x10, + 2, 0x16, 0x24, + 2, 0x04, 0x10, + 2, 0x05, 0x00, + 2, 0x0B, 0x00, + 2, 0x0C, 0x00, + 2, 0x10, 0x1F, + 2, 0x11, 0x7F, + 2, 0x00, 0x80, + 2, 0x01, 0x3F, + 2, 0x06, 0x03, + 2, 0x13, 0x10, + 2, 0x1B, 0x0A, + 2, 0x1C, 0x6A, + 2, 0x44, 0x58, + 2, 0x09, 0x00, + 2, 0x17, 0xBF, + 2, 0x0E, 0x02, + 2, 0x12, 0x00, + 2, 0x14, 0x1A, + 2, 0x0D, 0x01, + 2, 0x15, 0x40, + 2, 0x37, 0x08, + 2, 0x45, 0x00, + 2, 0x07, 0x00, + 2, 0x08, 0xFF, + 2, 0x32, 0xBF, + 0 + }; + _corep4x_audio_power(self, enabled); + auto& ioe1 = self->getIOExpander(0); + ioe1.setHighImpedance(M5IOE1_Class::gpio3, false); + ioe1.setDirection(M5IOE1_Class::gpio3, true); + ioe1.digitalWrite(M5IOE1_Class::gpio3, enabled); + if (enabled) + { + in_i2c_bulk_write(es8311_i2c_addr0, enabled_bulk_data, 100000, 3); + } +#else + (void)args; + (void)enabled; +#endif + return true; + } + bool M5Unified::_speaker_enabled_cb_hat_spk(void* args, bool enabled) { (void)args; @@ -1333,6 +1428,56 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { return true; } + bool M5Unified::_microphone_enabled_cb_corep4x(void* args, bool enabled) + { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + auto self = (M5Unified*)args; + _corep4x_audio_power(self, enabled); + self->In_I2C.writeRegister8(es7210_i2c_addr, 0x00, 0xFF, 400000); + if (enabled) + { + static constexpr uint8_t data[] = + { + 2, 0x00, 0x41, // RESET_CTL + 2, 0x01, 0x1f, // CLK_ON_OFF + 2, 0x06, 0x00, // DIGITAL_PDN + 2, 0x07, 0x20, // ADC_OSR + 2, 0x08, 0x10, // MODE_CFG + 2, 0x09, 0x30, // TCT0_CHPINI + 2, 0x0A, 0x30, // TCT1_CHPINI + 2, 0x20, 0x0a, // ADC34_HPF2 + 2, 0x21, 0x2a, // ADC34_HPF1 + 2, 0x22, 0x0a, // ADC12_HPF2 + 2, 0x23, 0x2a, // ADC12_HPF1 + 2, 0x02, 0xC1, + 2, 0x04, 0x01, + 2, 0x05, 0x00, + 2, 0x11, 0x60, + 2, 0x40, 0x42, // ANALOG_SYS + 2, 0x41, 0x70, // MICBIAS12 + 2, 0x42, 0x70, // MICBIAS34 + 2, 0x43, 0x1B, // MIC1_GAIN + 2, 0x44, 0x1B, // MIC2_GAIN + 2, 0x45, 0x1B, // MIC3_GAIN (AEC input) + 2, 0x46, 0x1B, // MIC4_GAIN (TDM slot 4) + 2, 0x47, 0x00, // MIC1_LP + 2, 0x48, 0x00, // MIC2_LP + 2, 0x49, 0x00, // MIC3_LP + 2, 0x4A, 0x00, // MIC4_LP + 2, 0x4B, 0x00, // MIC12_PDN + 2, 0x4C, 0x00, // MIC34_PDN + 2, 0x01, 0x14, // CLK_ON_OFF + 0, + }; + in_i2c_bulk_write(es7210_i2c_addr, data, 100000, 3); + } +#else + (void)args; + (void)enabled; +#endif + return true; + } + bool M5Unified::_microphone_enabled_cb_cardputer_adv(void* args, bool enabled) { (void)args; @@ -1368,65 +1513,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 + { + // ラッチを 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 を駆動しない) ので、 + // デバイスが線を握っていてもパッド同士の衝突にはならない。 { - if(cmd_list == nullptr) + 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 +1866,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 +1874,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; @@ -1817,10 +2035,19 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { #elif defined (CONFIG_IDF_TARGET_ESP32P4) if (board == board_t::board_unknown) { +#if defined (BOARD_ID) && BOARD_ID == 31 + board = board_t::board_M5CoreP4X; +#else m5gfx::pinMode(GPIO_NUM_32, m5gfx::pin_mode_t::input_pulldown); m5gfx::pinMode(GPIO_NUM_0, m5gfx::pin_mode_t::input_pulldown); - if (m5gfx::gpio_in(GPIO_NUM_32)) // M5Tab5 G32 always High - board = board_t::board_M5Tab5; + if (m5gfx::gpio_in(GPIO_NUM_32)) // M5Tab5 and M5Tab5X G32 always High + { + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + board = chip_info.revision >= 300 + ? board_t::board_M5Tab5X + : board_t::board_M5Tab5; + } else if(m5gfx::gpio_in(GPIO_NUM_0)) // M5UnitPoEP4 G0 always High board = board_t::board_M5UnitPoEP4; else @@ -1831,6 +2058,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { ? board_t::board_M5StampP4X : board_t::board_M5StampP4; } +#endif } #endif @@ -1888,7 +2116,15 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { switch (board) { #if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5CoreP4X: + { + auto ioexp = new M5IOE1_Class(0x4F); + ioexp->begin(); + _io_expander[0].reset(ioexp); + } + break; case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: for (int i = 0; i < 2; ++i) { auto ioexp = new PI4IOE5V6408_Class(0x43 + i); @@ -2170,13 +2406,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); @@ -2257,6 +2493,18 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { break; #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5CoreP4X: + { + auto& ioe1 = getIOExpander(0); + ioe1.setHighImpedance(M5IOE1_Class::gpio1, false); + ioe1.setHighImpedance(M5IOE1_Class::gpio3, false); + ioe1.setDirection(M5IOE1_Class::gpio1, true); + ioe1.setDirection(M5IOE1_Class::gpio3, true); + ioe1.digitalWrite(M5IOE1_Class::gpio1, false); + ioe1.digitalWrite(M5IOE1_Class::gpio3, false); + } + break; + case board_t::board_M5UnitPoEP4: m5gfx::pinMode(GPIO_NUM_45, m5gfx::pin_mode_t::input); break; @@ -2296,7 +2544,23 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { { #if defined (M5UNIFIED_PC_BUILD) #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5CoreP4X: + if (cfg.internal_mic) + { + mic_cfg.pin_mck = GPIO_NUM_2; + mic_cfg.pin_bck = GPIO_NUM_6; + mic_cfg.pin_ws = GPIO_NUM_4; + mic_cfg.pin_data_in = GPIO_NUM_5; + mic_cfg.magnification = 2; + mic_cfg.sample_rate = 24000; + mic_cfg.input_channel = input_channel_t::input_stereo; + mic_cfg.i2s_port = I2S_NUM_0; + mic_enable_cb = _microphone_enabled_cb_corep4x; + } + break; + case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: if (cfg.internal_mic) { mic_cfg.pin_mck = GPIO_NUM_30; @@ -2502,7 +2766,22 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { break; #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5CoreP4X: + if (cfg.internal_spk) + { + spk_cfg.pin_mck = GPIO_NUM_2; + spk_cfg.pin_bck = GPIO_NUM_6; + spk_cfg.pin_ws = GPIO_NUM_4; + spk_cfg.pin_data_out = GPIO_NUM_3; + spk_cfg.magnification = 4; + spk_cfg.sample_rate = 24000; + spk_cfg.i2s_port = I2S_NUM_0; + spk_enable_cb = _speaker_enabled_cb_corep4x; + } + break; + case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: if (cfg.internal_spk) { spk_cfg.pin_mck = GPIO_NUM_30; @@ -2904,7 +3183,9 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { #if defined (M5UNIFIED_PC_BUILD) #elif defined ( CONFIG_IDF_TARGET_ESP32P4 ) #define ENABLE_M5MODULE - if (_board == board_t::board_M5Tab5) + if (_board == board_t::board_M5Tab5 + || _board == board_t::board_M5Tab5X + || _board == board_t::board_M5CoreP4X) #elif defined ( CONFIG_IDF_TARGET_ESP32S3 ) #define ENABLE_M5MODULE if (_board == board_t::board_M5StackCoreS3 @@ -3089,6 +3370,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { tb_k = 364; // (65536*3/540) break; case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: tb_y = 1280; tb_k = 273; // (65536*3/540) break; @@ -3444,6 +3726,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { height = 960; break; case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: height = 1280; break; default: diff --git a/src/M5Unified.hpp b/src/M5Unified.hpp index 7e94124..aa228d8 100644 --- a/src/M5Unified.hpp +++ b/src/M5Unified.hpp @@ -166,7 +166,13 @@ namespace m5 #elif defined (CONFIG_IDF_TARGET_ESP32C3) = board_t::board_M5StampC3; #elif defined (CONFIG_IDF_TARGET_ESP32P4) +#if defined (BOARD_ID) && BOARD_ID == 31 + = board_t::board_M5CoreP4X; +#elif defined (BOARD_ID) && BOARD_ID == 35 + = board_t::board_M5Tab5X; +#else = board_t::board_M5Tab5; +#endif #elif defined (CONFIG_IDF_TARGET_ESP32C5) = board_t::board_M5StampC5; #elif defined (CONFIG_IDF_TARGET_ESP32) || !defined (CONFIG_IDF_TARGET) @@ -406,7 +412,7 @@ namespace m5 #if defined ( __M5GFX_M5MODULEDISPLAY__ ) if (cfg.external_display.module_display) { #if defined (CONFIG_IDF_TARGET_ESP32P4) - if (_board == board_t::board_M5Tab5) + if (_board == board_t::board_M5Tab5 || _board == board_t::board_M5Tab5X) #elif defined (CONFIG_IDF_TARGET_ESP32S3) if (_board == board_t::board_M5StackCoreS3 || _board == board_t::board_M5StackCoreS3SE || _board == board_t::board_M5StackChan) @@ -666,7 +672,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); @@ -676,6 +691,7 @@ namespace m5 static bool _speaker_enabled_cb_stopwatch(void* args, bool enabled); static bool _speaker_enabled_cb_chain_captain(void* args, bool enabled); static bool _speaker_enabled_cb_tab5(void* args, bool enabled); + static bool _speaker_enabled_cb_corep4x(void* args, bool enabled); static bool _speaker_enabled_cb_cardputer_adv(void* args, bool enabled); static bool _speaker_enabled_cb_atom_echos3r(void* args, bool enabled); static bool _speaker_enabled_cb_atomic_echo(void* args, bool enabled); @@ -688,6 +704,7 @@ namespace m5 static bool _microphone_enabled_cb_stopwatch(void* args, bool enabled); static bool _microphone_enabled_cb_chain_captain(void* args, bool enabled); static bool _microphone_enabled_cb_tab5(void* args, bool enabled); + static bool _microphone_enabled_cb_corep4x(void* args, bool enabled); static bool _microphone_enabled_cb_cardputer_adv(void* args, bool enabled); static bool _microphone_enabled_cb_atomic_echo(void* args, bool enabled); static bool _microphone_enabled_cb_atom_echos3r(void* args, bool enabled); diff --git a/src/gitTagVersion.h b/src/gitTagVersion.h index 2953222..b744c2f 100644 --- a/src/gitTagVersion.h +++ b/src/gitTagVersion.h @@ -1,4 +1,4 @@ #define M5UNIFIED_VERSION_MAJOR 0 #define M5UNIFIED_VERSION_MINOR 2 -#define M5UNIFIED_VERSION_PATCH 20 +#define M5UNIFIED_VERSION_PATCH 21 #define M5UNIFIED_VERSION F( M5UNIFIED_VERSION_MAJOR "." M5UNIFIED_VERSION_MINOR "." M5UNIFIED_VERSION_PATCH ) diff --git a/src/utility/IMU_Class.cpp b/src/utility/IMU_Class.cpp index 4e402a4..eb465c6 100644 --- a/src/utility/IMU_Class.cpp +++ b/src/utility/IMU_Class.cpp @@ -91,6 +91,12 @@ namespace m5 _internal_axisorder_fixed[sensor_index_accel] = (internal_axisorder_t)(axis_invert_x | axis_invert_z); // X軸,Z軸反転 _internal_axisorder_fixed[sensor_index_gyro ] = (internal_axisorder_t)(axis_invert_x | axis_invert_z); // X軸,Z軸反転 } +#elif defined(CONFIG_IDF_TARGET_ESP32P4) + if (board == m5::board_t::board_M5CoreP4X) + { + _internal_axisorder_fixed[sensor_index_accel] = (internal_axisorder_t)(axis_invert_z); + _internal_axisorder_fixed[sensor_index_gyro ] = (internal_axisorder_t)(axis_invert_z); + } #elif defined(CONFIG_IDF_TARGET_ESP32S3) if ((board == m5::board_t::board_M5StackCoreS3 || board == m5::board_t::board_M5StackChan) && bmi2->getAddress() == 0x69) 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..27b38a4 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -118,7 +118,25 @@ namespace m5 default: break; + case board_t::board_M5CoreP4X: + { + _pmic = pmic_t::pmic_m5pm1; + M5pm1.begin(); + + auto& ioe1 = M5.getIOExpander(0); + // M5IOE1_G12 supplies the shared 3V3 rail for MBUS, TF card and sensors. + ioe1.setHighImpedance(M5IOE1_Class::gpio12, false); + ioe1.setDirection(M5IOE1_Class::gpio12, true); + ioe1.digitalWrite(M5IOE1_Class::gpio12, true); + + // M5IOE1_G6 is the active-low charger status input. + ioe1.setDirection(M5IOE1_Class::gpio6, false); + ioe1.setPullMode(M5IOE1_Class::gpio6, IOExpander_Base::pull_up); + } + break; + case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: { static constexpr std::uint8_t reg_array_0x43[] = { /// +--------- HP_DET : Headphone detect @@ -154,6 +172,13 @@ namespace m5 }; M5.getIOExpander(0).writeRegister8Array(reg_array_0x43, sizeof(reg_array_0x43)); M5.getIOExpander(1).writeRegister8Array(reg_array_0x44, sizeof(reg_array_0x44)); + if (M5.getBoard() == board_t::board_M5Tab5X) + { + auto& ioe = M5.getIOExpander(0); // PI4IOE 0x43, ADDR grounded, bottom Hat power + ioe.setHighImpedance(3, false); + ioe.setDirection(3, true); + ioe.digitalWrite(3, true); + } Ina226.begin(); INA226_Class::config_t cfg; cfg.sampling_rate = INA226_Class::Sampling::Rate16; @@ -221,6 +246,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 +302,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 +359,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 +439,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 +512,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); @@ -841,17 +917,44 @@ namespace m5 switch (M5.getBoard()) { #if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5CoreP4X: + { + auto& ioe1 = M5.getIOExpander(0); + if (port_mask & ext_port_mask_t::ext_PA) + { + ioe1.setHighImpedance(M5IOE1_Class::gpio5, false); + ioe1.setDirection(M5IOE1_Class::gpio5, true); + ioe1.digitalWrite(M5IOE1_Class::gpio5, enable); + } + if (port_mask & ext_port_mask_t::ext_USB) + { + ioe1.setHighImpedance(M5IOE1_Class::gpio2, false); + ioe1.setDirection(M5IOE1_Class::gpio2, true); + ioe1.digitalWrite(M5IOE1_Class::gpio2, enable); + } + } + break; + case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: 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 (M5.getBoard() == board_t::board_M5Tab5X + && (port_mask & ext_port_mask_t::ext_EXT)) + { + auto& ioe = M5.getIOExpander(0); + ioe.setHighImpedance(3, false); + ioe.setDirection(3, true); + ioe.digitalWrite(3, 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; @@ -1008,8 +1111,13 @@ namespace m5 { #if defined (M5UNIFIED_PC_BUILD) #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5CoreP4X: + return M5.getIOExpander(0).getWriteValue(M5IOE1_Class::gpio5); + case board_t::board_M5Tab5: return M5.getIOExpander(0).getWriteValue(2); + case board_t::board_M5Tab5X: + return M5.getIOExpander(0).getWriteValue(3); #elif defined (CONFIG_IDF_TARGET_ESP32C6) case board_t::board_ArduinoNessoN1: @@ -1092,6 +1200,12 @@ namespace m5 (void)enable; switch (M5.getBoard()) { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5CoreP4X: + M5.getIOExpander(0).digitalWrite(M5IOE1_Class::gpio2, enable); + break; +#endif + #if defined (CONFIG_IDF_TARGET_ESP32S3) case board_t::board_M5StackCoreS3: case board_t::board_M5StackCoreS3SE: @@ -1109,6 +1223,11 @@ namespace m5 { switch (M5.getBoard()) { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5CoreP4X: + return M5.getIOExpander(0).getWriteValue(M5IOE1_Class::gpio2); +#endif + #if defined (CONFIG_IDF_TARGET_ESP32S3) case board_t::board_M5StackCoreS3: case board_t::board_M5StackCoreS3SE: @@ -1303,6 +1422,13 @@ namespace m5 } break; +#elif defined (CONFIG_IDF_TARGET_ESP32P4) + case pmic_t::pmic_m5pm1: + if (!withTimer) { + M5pm1.powerOff(); + } + break; + #elif defined (CONFIG_IDF_TARGET_ESP32C61) || defined (CONFIG_IDF_TARGET_ESP32C5) case pmic_t::pmic_m5pm1: { @@ -1391,6 +1517,7 @@ namespace m5 default: break; #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: for (int i = 0; i < 10; ++i) { M5.getIOExpander(1).digitalWrite(4, i & 1); // io1.gpio4 == PWROFF_PLUSE @@ -1508,7 +1635,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 +1766,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) @@ -1874,6 +2001,8 @@ namespace m5 case pmic_t::pmic_m5pm1: return M5pm1.getVBUSVoltage(); #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case pmic_t::pmic_m5pm1: + return M5pm1.getVBUSVoltage(); #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) @@ -2064,6 +2193,8 @@ namespace m5 } return M5pm1.getBatteryVoltage(); #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case pmic_t::pmic_m5pm1: + return M5pm1.getBatteryVoltage(); #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case pmic_t::pmic_ip5306: @@ -2097,6 +2228,7 @@ namespace m5 switch (M5.getBoard()) { #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: return Ina226.getBusVoltage() * 1000; #endif @@ -2143,6 +2275,15 @@ namespace m5 } break; #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case pmic_t::pmic_m5pm1: + { + int16_t bat_mv = getBatteryVoltage(); + if (bat_mv <= 0) { + return -1; + } + mv = bat_mv; + } + break; #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case pmic_t::pmic_ip5306: @@ -2181,6 +2322,7 @@ namespace m5 switch (M5.getBoard()) { #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: // 2S Li-Po ( * 1000 / 2 == * 500) mv = Ina226.getBusVoltage() * 500; break; @@ -2220,6 +2362,9 @@ namespace m5 M5pm1.setBatteryCharge(enable); return; #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case pmic_t::pmic_m5pm1: + M5pm1.setBatteryCharge(enable); + return; #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case pmic_t::pmic_ip5306: @@ -2270,6 +2415,7 @@ namespace m5 switch (M5.getBoard()) { #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: M5.getIOExpander(1).digitalWrite(7, enable); break; #endif @@ -2302,19 +2448,22 @@ 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); } } return; #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case pmic_t::pmic_m5pm1: + (void)max_mA; + return; #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case pmic_t::pmic_ip5306: @@ -2340,6 +2489,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 @@ -2347,7 +2511,8 @@ namespace m5 default: #if defined (CONFIG_IDF_TARGET_ESP32P4) switch (M5.getBoard()) { - case board_t::board_M5Tab5: { + case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: { switch (max_mA) { case 0: // charge disable @@ -2423,7 +2588,10 @@ namespace m5 switch (M5.getBoard()) { #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: - return 1000.0f * Ina226.getShuntCurrent(); + case board_t::board_M5Tab5X: + // 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) @@ -2470,6 +2638,7 @@ namespace m5 switch (M5.getBoard()) { #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: // TODO:implement #endif default: @@ -2509,6 +2678,14 @@ namespace m5 } return is_charging_t::charge_unknown; #elif defined (CONFIG_IDF_TARGET_ESP32P4) + case pmic_t::pmic_m5pm1: + { + bool level; + if (!M5.getIOExpander(0).getInputLevel(M5IOE1_Class::gpio6, &level)) { + return is_charging_t::charge_unknown; + } + return level ? is_charging_t::is_discharging : is_charging_t::is_charging; + } #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) @@ -2531,8 +2708,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). @@ -2584,6 +2762,7 @@ namespace m5 #endif #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: return M5.getIOExpander(1).digitalRead(6) // io1.gpio6 == CHG_STAT ? is_charging_t::is_charging : is_charging_t::is_discharging; #endif @@ -2707,6 +2886,10 @@ namespace m5 case pmic_t::pmic_m5pm1: return M5pm1.getPekPress(); +#elif defined (CONFIG_IDF_TARGET_ESP32P4) + case pmic_t::pmic_m5pm1: + return M5pm1.getPekPress(); + #elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case pmic_t::pmic_axp192: return Axp192.getPekPress(); @@ -2748,13 +2931,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..69c4aef 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -46,6 +46,7 @@ namespace m5 , ext_USB = 1 << 5 // M5Station external USB. ※ Not for CoreS3 main USB. , ext_PWR485 = 1 << 6 // M5PowerHub external RS485. , ext_PWRCAN = 1 << 7 // M5PowerHub external CAN. + , ext_EXT = 1 << 8 // M5Tab5X bottom Hat power. , ext_MAIN = 1 << 15 }; @@ -161,7 +162,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 +205,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); @@ -234,6 +237,7 @@ namespace m5 M5PM1_Class M5pm1; #elif defined (CONFIG_IDF_TARGET_ESP32P4) + M5PM1_Class M5pm1; INA226_Class Ina226 = { 0x41 }; #else diff --git a/src/utility/RTC_Class.cpp b/src/utility/RTC_Class.cpp index e52694c..57cd730 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 @@ -44,7 +54,9 @@ namespace m5 switch (board) { #if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5CoreP4X: case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: instance.reset(new RX8130_Class(RX8130_Class::DEFAULT_ADDRESS, 400000, i2c)); break; #endif diff --git a/src/utility/RTC_Class.hpp b/src/utility/RTC_Class.hpp index 265ccfd..5cafa7f 100644 --- a/src/utility/RTC_Class.hpp +++ b/src/utility/RTC_Class.hpp @@ -43,7 +43,7 @@ namespace m5 /// Set timer IRQ /// @param timer_msec - /// @return the set number of msec. (0 == disable) + /// @return the set number of msec. 0 == disabled, or the timer could not be set (communication failure). std::uint32_t setTimerIRQ(std::uint32_t timer_msec); // deprecated 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/AXP192_Class.cpp b/src/utility/power/AXP192_Class.cpp index 80299d6..06b2080 100644 --- a/src/utility/power/AXP192_Class.cpp +++ b/src/utility/power/AXP192_Class.cpp @@ -169,17 +169,15 @@ namespace m5 } void AXP192_Class::setChargeVoltage(std::uint16_t max_mV) - { - max_mV = (max_mV / 10) - 410; - if (max_mV > 436 - 410) { max_mV = 436 - 410; } - static constexpr std::uint8_t table[] = - { 415 - 410 /// 4150mV - , 420 - 410 /// 4200mV - , 436 - 410 /// 4360mV - , 255 - }; - size_t i = 0; - while (table[i] <= max_mV) { ++i; } + { /// reg 0x33 bit6:5 selects the target voltage. Compare in millivolts: + /// the earlier form subtracted a bias first, which underflowed the + /// unsigned argument for anything below the lowest step and then clamped + /// to the highest one - a request for less charge voltage produced more. + static constexpr std::uint16_t table[] = { 4100, 4150, 4200, 4360 }; + size_t i = (sizeof(table) / sizeof(table[0])) - 1; + /// pick the highest step that does not exceed the request, and the lowest + /// step when the request is under all of them. + while (i && table[i] > max_mV) { --i; } std::uint8_t val = 0; if (readRegister(0x33, &val, 1)) diff --git a/src/utility/power/AXP192_Class.hpp b/src/utility/power/AXP192_Class.hpp index e423565..dd1691f 100644 --- a/src/utility/power/AXP192_Class.hpp +++ b/src/utility/power/AXP192_Class.hpp @@ -34,6 +34,11 @@ namespace m5 /// set battery charge voltage /// @param max_mV milli volt. (4100 - 4360). + /// set the constant-voltage charge target. + /// @param max_mV the highest step at or below this value is selected. + /// Supported steps are 4100 / 4150 / 4200 / 4360 mV; a request under the + /// lowest step selects that step, and one above the highest selects the + /// highest. void setChargeVoltage(std::uint16_t max_mV); /// Get whether the battery is currently charging or not. diff --git a/src/utility/power/AXP2101_Class.cpp b/src/utility/power/AXP2101_Class.cpp index 30a072e..c46931b 100644 --- a/src/utility/power/AXP2101_Class.cpp +++ b/src/utility/power/AXP2101_Class.cpp @@ -125,22 +125,25 @@ namespace m5 } void AXP2101_Class::setChargeVoltage(std::uint16_t max_mV) - { - max_mV = (max_mV / 10) - 400; - if (max_mV > 460 - 400) { max_mV = 460 - 400; } - static constexpr std::uint8_t table[] = - { 410 - 400 /// 4100mV - , 420 - 400 /// 4200mV - , 435 - 400 /// 4350mV - , 440 - 400 /// 4400mV - , 460 - 400 /// 4600mV - , 255 - }; - size_t i = 0; - while (table[i] <= max_mV) { ++i; } - - if (++i >= 0b110) { i = 0; } - writeRegister8(0x64, i); + { /// reg 0x64 selects the constant-voltage target: 1 = 4.0V through 5 = 4.4V, + /// with 0 reserved. An early revision of the datasheet also documented a + /// 4.6V setting, which later revisions dropped; nothing this library runs + /// on carries a cell that charges to 4.6V, so a request that high is held + /// at the highest step both revisions agree on rather than sent to a code + /// whose meaning depends on the silicon. + /// + /// The earlier form subtracted a bias from the argument before comparing. + /// The argument is unsigned, so a request under the lowest step wrapped + /// around and selected code 0, as did a request that reached the 4.6V + /// entry - asking for a gentler charge voltage produced either a reserved + /// code or the highest voltage, depending on the silicon. + static constexpr std::uint16_t table[] = { 4000, 4100, 4200, 4350, 4400 }; + size_t i = (sizeof(table) / sizeof(table[0])) - 1; + /// pick the highest step that does not exceed the request, and the lowest + /// step when the request is under all of them. + while (i && table[i] > max_mV) { --i; } + + writeRegister8(0x64, static_cast(i + 1)); } std::int8_t AXP2101_Class::getBatteryLevel(void) diff --git a/src/utility/power/AXP2101_Class.hpp b/src/utility/power/AXP2101_Class.hpp index faf3736..e5aa66d 100644 --- a/src/utility/power/AXP2101_Class.hpp +++ b/src/utility/power/AXP2101_Class.hpp @@ -86,6 +86,11 @@ namespace m5 /// set battery charge voltage /// @param max_mV milli volt. (4100 - 4360). + /// set the constant-voltage charge target. + /// @param max_mV the highest step at or below this value is selected. + /// Supported steps are 4000 / 4100 / 4200 / 4350 / 4400 mV; a request under + /// the lowest step selects that step, and one above the highest selects + /// the highest. void setChargeVoltage(std::uint16_t max_mV); /// @return -1:discharge / 0:standby / 1:charge 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/IP5306_Class.cpp b/src/utility/power/IP5306_Class.cpp index 208863c..8a20379 100644 --- a/src/utility/power/IP5306_Class.cpp +++ b/src/utility/power/IP5306_Class.cpp @@ -104,9 +104,30 @@ namespace m5 } bool IP5306_Class::isCharging(void) - { + { /// This needs both of the flags the datasheet describes, not one of them: + /// REG_READ0 bit3 tells charging from discharging, and REG_READ1 bit3 + /// tells whether the cell has already been filled. Only the first was + /// read. It stays set once charging is enabled and a supply is present - + /// the completed charge included - so a finished charge was reported as an + /// ongoing one, as was a board running with no cell installed at all. + /// + /// The two sit at adjacent addresses but are read separately on purpose. + /// The register document only ever shows a single-byte read and nowhere + /// states that the address auto-increments, so reading both in one + /// transaction would rest on behaviour that is not specified. + /// + /// Two limits worth knowing. The full flag has no defined reset value, so + /// shortly after power-up it can read as full before the charger has + /// settled, and nothing here can tell that from a real full charge. And a + /// failed read is reported the same way as "not charging", because this + /// return type has no room to say that the question could not be answered. std::uint8_t val = 0; - return (readRegister(REG_READ0, &val, 1)) && (val & 0x08); + if (!readRegister(REG_READ0, &val, 1)) { return false; } + /// discharging: either charging is disabled or there is no supply + if (!(val & 0x08)) { return false; } + if (!readRegister(REG_READ1, &val, 1)) { return false; } + /// already full, so nothing is going into the cell + return !(val & 0x08); } bool IP5306_Class::setPowerBoostKeepOn(bool en) { 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 diff --git a/src/utility/rtc/RTC_Base.hpp b/src/utility/rtc/RTC_Base.hpp index 4b0622b..8391bf7 100644 --- a/src/utility/rtc/RTC_Base.hpp +++ b/src/utility/rtc/RTC_Base.hpp @@ -89,7 +89,8 @@ namespace m5 /// Set timer IRQ /// @param milliseconds (0 == disable) - /// @return the set number of milliseconds. (0 == disable) + /// @return the set number of milliseconds. 0 == disabled, or the timer could not be set + /// (communication failure; the driver then stops the timer on a best-effort basis). virtual std::uint32_t setTimerIRQ(std::uint32_t timer_msec) { return 0; }; /// Set alarm by time diff --git a/src/utility/rtc/RX8130_Class.cpp b/src/utility/rtc/RX8130_Class.cpp index 256fef9..484c471 100644 --- a/src/utility/rtc/RX8130_Class.cpp +++ b/src/utility/rtc/RX8130_Class.cpp @@ -118,54 +118,95 @@ namespace m5 std::uint32_t RX8130_Class::setTimerIRQ(std::uint32_t msec) { - // タイマー周期の除数 - uint32_t div = 1; - // タイマー周期の乗数 - uint32_t mul = 1; + // Source clocks in the order they are tried. period = mul_ms / div [ms]. + // max_ms = 65535 * period, so msec <= max_ms keeps msec * div within uint32. + // max_cnt = min(65535, 0xFFFFFFFF / mul_ms), so cnt * mul_ms (the period) stays within uint32. + struct clk_t { std::uint32_t mul_ms; std::uint32_t div; std::uint32_t max_ms; std::uint16_t max_cnt; std::uint8_t tsel; }; + static constexpr clk_t clks[] = { + { 1000, 64, 1023984, 65535, 0x01 }, // 64 Hz + { 1000, 1, 65535000, 65535, 0x02 }, // 1 Hz + { 60000, 1, 3932100000, 65535, 0x03 }, // 1/60 Hz + { 3600000, 1, 0xFFFFFFFF, 1193, 0x04 }, // 1/3600 Hz + { 1000, 4096, 15999, 65535, 0x00 }, // 4096 Hz (last resort: its /IRQ pulse is only 122us) + }; + static constexpr std::size_t NCLK = sizeof(clks) / sizeof(clks[0]); + // The /IRQ pulse auto-releases after 122us with the 4096Hz clock but 7.57ms with the others, + // so take the finest non-4096Hz clock whose rounded count keeps the period error under 1/256 + // and has >= MIN_COUNT counts (the first countdown can be short by up to one source clock, + // 1s for the 1/60Hz and 1/3600Hz clocks, so this bounds that to ~6% or less). + static constexpr std::uint32_t MIN_COUNT = 16; - uint8_t tsel_bits = 0; - if (msec < 65536 * 1000 / 4096) { // 約16秒 - tsel_bits = 0x00; - div = 4096; - } else if (msec < 65536 * 1000 / 64) { // 約1024秒(約17分) - tsel_bits = 0x01; - div = 64; - } else if (msec < 65536 * 1000) { // 約65535秒(約18時間) - tsel_bits = 0x02; - } else if (msec < 65536 * 60) { // 約3,932,160秒(約45日) - mul = 60; - tsel_bits = 0x03; - } else { // msec < 65536*3600 // 約39,321,600秒(約1年3ヶ月) - mul = 3600; - tsel_bits = 0x04; + std::uint32_t cycle = 0; + const clk_t* sel = nullptr; + if (msec != 0) { + bool overflowed = false; // a finer clock ran out of range: round up so the period never steps back + for (std::size_t i = 0; i < NCLK; ++i) { + const clk_t& c = clks[i]; + if (msec > c.max_ms) { overflowed = true; continue; } + // Everything below is in units of msec * div: cnt counts of mul_ms each, err the remainder. + std::uint32_t num = msec * c.div; + std::uint32_t cnt = num / c.mul_ms; + std::uint32_t err = num % c.mul_ms; + if (overflowed ? (err != 0) : (err * 2 >= c.mul_ms)) { ++cnt; err = c.mul_ms - err; } + if (cnt > c.max_cnt) { cnt = c.max_cnt; err = num - cnt * c.mul_ms; } + // Accept when the error is within 1/256 (~0.39%) of the request (err < mul_ms, so no overflow). + if (i + 1 == NCLK || (cnt >= MIN_COUNT && (err << 8) <= num)) { + sel = &c; cycle = cnt; break; + } + } + if (sel == nullptr) { return 0; } // unreachable (1/3600Hz covers all of uint32); fail safe = stay stopped } - std::uint32_t result = 0; - std::uint8_t regdata[3]; - if (readRegister(0x1A, regdata, 3)) { - mul *= 1000; - uint32_t cycle = (msec * div + (mul >> 1)) / mul; - if (cycle > 65535) { cycle = 65535; } - result = cycle * mul / div; - - regdata[0] = cycle & 0xff; - regdata[1] = (cycle >> 8) & 0xff; - if (cycle > 0) { - // Clear timer select bits & TE flag - std::uint8_t reg0x1C = regdata[2] & ~0x17; - // Set timer select bits & TE flag - reg0x1C |= 0x10 | tsel_bits; - regdata[2] = reg0x1C; - bitOn(0x1E, 0x10); - } else { - // Clear TE flag - regdata[2] &= ~0x10; - bitOff(0x1E, 0x10); + // Sequence per datasheet Figure 48: TE=0 (+TSEL) -> clear TF -> TIE -> preset -> TE=1 last, + // so the first event cannot precede TIE. On any I2C failure the timer is stopped (verified by + // read-back where the bus allows it) and 0 is returned; the caller cannot tell that from a + // requested stop, and if even the stop fails the hardware state is unknown. + // 0x1D flags are write-0-to-clear (writing 1 is ignored, VBFF is read-only), so TF is cleared + // with a single write that leaves the other flags untouched (a read-modify-write would drop + // a flag raised in between). + static constexpr std::uint8_t FLAG_CLEAR_TF = 0xAF; + auto stop_timer = [this](void) -> bool { + for (int retry = 0; retry < 3; ++retry) { + std::uint8_t ext = 0, ctl = 0; + if (bitOff(0x1C, 0x10) && bitOff(0x1E, 0x10) + && readRegister(0x1C, &ext, 1) && readRegister(0x1E, &ctl, 1) + && !(ext & 0x10) && !(ctl & 0x10)) { return true; } } - writeRegister(0x1A, regdata, 3); + return false; + }; + std::uint8_t reg0x1C = 0; + if (cycle == 0) { + stop_timer(); + writeRegister8(0x1D, FLAG_CLEAR_TF); + return 0; } - - return result; + bool ok = readRegister(0x1C, ®0x1C, 1); + if (ok) { + reg0x1C = (reg0x1C & ~0x17) | sel->tsel; + ok = writeRegister8(0x1C, reg0x1C) + && writeRegister8(0x1D, FLAG_CLEAR_TF) + && bitOn(0x1E, 0x10); + } + if (ok) { + // While TE=0 the counter registers read back the preset, so verify the write took + // (a corrupted preset was observed on a shared bus) and retry a few times. + std::uint8_t regdata[2] = { (std::uint8_t)(cycle & 0xff), (std::uint8_t)((cycle >> 8) & 0xff) }; + ok = false; + for (int retry = 0; retry < 3 && !ok; ++retry) { + std::uint8_t verify[2] = { 0, 0 }; + ok = writeRegister(0x1A, regdata, 2) + && readRegister(0x1A, verify, 2) + && verify[0] == regdata[0] && verify[1] == regdata[1]; + } + } + if (ok) { ok = writeRegister8(0x1C, reg0x1C | 0x10); } + if (!ok) { + stop_timer(); + return 0; + } + // Actual period rounded to the nearest ms (cycle * mul_ms fits by max_cnt); never 0 while running. + std::uint32_t result = (cycle * sel->mul_ms + (sel->div >> 1)) / sel->div; + return result ? result : 1; } int RX8130_Class::setAlarmIRQ(const rtc_date_t* date, const rtc_time_t* time) @@ -239,7 +280,7 @@ namespace m5 void RX8130_Class::clearIRQ(void) { if (isEnabled()) { - bitOff(0x1D, 0x18); + writeRegister8(0x1D, 0xA7); // W0C: clear TF and AF only } } @@ -247,7 +288,7 @@ namespace m5 { if (isEnabled()) { bitOff(0x1E, 0x18); - bitOff(0x1D, 0x18); + writeRegister8(0x1D, 0xA7); // W0C: clear TF and AF only } }