|
35 | 35 | namespace |
36 | 36 | { |
37 | 37 |
|
38 | | -// Per-command CMD_DELAY values from UG6921 Tables 2/5/6. Where a step is not |
39 | | -// explicitly tagged, the doc's default of 2 ms applies. |
40 | | -constexpr uint16_t kDefaultCmdDelayMs = 2; |
41 | | -constexpr uint16_t kSetDateTimeDelayMs = 5; |
42 | | -constexpr uint16_t kSetCalIndexDelayMs = 5; |
43 | | -constexpr uint16_t kSetSpo2CoeffsDelayMs = 5; |
| 38 | +// Per-command CMD_DELAY values from UG6921 Tables 2/5/6, biased upward to |
| 39 | +// match what the original 1.0.x driver shipped (uniform 45 ms + extra ad-hoc |
| 40 | +// delays). UG6921 §1.1 quotes a 2 ms default; in practice the hub returns |
| 41 | +// 0xFE (busy) on commands that proxy to the MAX30101 over its secondary I2C |
| 42 | +// bus when the host polls that aggressively, and the doc itself recommends |
| 43 | +// "Increase the CMD_DELAY" as the remedy. We err on the safe side here and |
| 44 | +// rely on writeImpl()/readImpl() retry-on-0xFE for residual races. |
| 45 | +constexpr uint16_t kDefaultCmdDelayMs = 10; |
| 46 | +constexpr uint16_t kSetDateTimeDelayMs = 10; |
| 47 | +constexpr uint16_t kSetCalIndexDelayMs = 10; |
| 48 | +constexpr uint16_t kSetSpo2CoeffsDelayMs = 10; |
44 | 49 | constexpr uint16_t kCalibVectorChunkDelayMs = 30; |
45 | | -constexpr uint16_t kEnableAgcDelayMs = 20; |
46 | | -constexpr uint16_t kEnableAfeDelayMs = 40; |
47 | | -constexpr uint16_t kEnableBptDelayMs = 500; |
48 | | -constexpr uint16_t kDisableAfeDelayMs = 40; |
49 | | -constexpr uint16_t kDisableBptDelayMs = 500; |
50 | | -constexpr uint16_t kDisableAgcDelayMs = 20; |
51 | | -constexpr uint16_t kPostEnableSettleMs = 100; |
52 | | -constexpr uint16_t kResetSettleMs = 1000; |
53 | | -constexpr uint16_t kEnterAppModeDelayMs = 10; |
54 | | -constexpr uint16_t kRawModeSettleMs = 180; |
| 50 | +constexpr uint16_t kEnableAgcDelayMs = 30; |
| 51 | +constexpr uint16_t kEnableAfeDelayMs = 60; |
| 52 | +constexpr uint16_t kEnableBptDelayMs = 600; |
| 53 | +constexpr uint16_t kDisableAfeDelayMs = 60; |
| 54 | +constexpr uint16_t kDisableBptDelayMs = 600; |
| 55 | +constexpr uint16_t kDisableAgcDelayMs = 30; |
| 56 | +constexpr uint16_t kPostEnableSettleMs = 200; |
| 57 | +constexpr uint16_t kResetSettleMs = 1500; |
| 58 | +constexpr uint16_t kEnterAppModeDelayMs = 20; |
| 59 | +constexpr uint16_t kRawModeSettleMs = 250; |
| 60 | +// LED-current writes (0x40/0x03/RegAddr/Val) proxy to the MAX30101 over the |
| 61 | +// hub's secondary I2C bus — give them substantial headroom over the default. |
| 62 | +constexpr uint16_t kLedCurrentDelayMs = 25; |
| 63 | + |
| 64 | +// Retry policy for 0xFE responses. UG6921 §1.1 says "Device is busy. Try |
| 65 | +// again. Increase the CMD_DELAY." We retry up to kBusyRetryMax times and |
| 66 | +// double the post-write delay each attempt (capped at kBusyMaxDelayMs). |
| 67 | +constexpr uint8_t kBusyRetryMax = 4; |
| 68 | +constexpr uint16_t kBusyMaxDelayMs = 400; |
55 | 69 |
|
56 | 70 | // I2C chunk size for the long calibration-vector upload. Kept at 30 to match |
57 | 71 | // the AVR Wire BUFFER_LENGTH of 32 with two bytes of headroom; on platforms |
@@ -359,12 +373,14 @@ Max32664Status Max32664::startRaw() |
359 | 373 | delay(kRawModeSettleMs); |
360 | 374 |
|
361 | 375 | // LED1 (red) and LED2 (IR) currents to half-scale. Must come AFTER the |
362 | | - // algorithm enable or the values are overwritten during init. |
| 376 | + // algorithm enable or the values are overwritten during init. The 0x40/03 |
| 377 | + // family proxies to the MAX30101 over the secondary I2C bus, so it needs |
| 378 | + // more headroom than the trivial-command default. |
363 | 379 | s = writeCmd3(0x40, 0x03, kMax30101Led1RegAddr, kMax30101LedHalfScale, 0, |
364 | | - kDefaultCmdDelayMs); |
| 380 | + kLedCurrentDelayMs); |
365 | 381 | if (s != Max32664Status::Ok) return s; |
366 | 382 | return writeCmd3(0x40, 0x03, kMax30101Led2RegAddr, kMax30101LedHalfScale, 0, |
367 | | - kDefaultCmdDelayMs); |
| 383 | + kLedCurrentDelayMs); |
368 | 384 | } |
369 | 385 |
|
370 | 386 | Max32664Status Max32664::readRaw(Max32664RawSample *out, size_t cap, size_t *count, bool wantRed) |
@@ -628,162 +644,191 @@ void Max32664::parseSample(const uint8_t *buf, Max32664Sample &s) const |
628 | 644 | // Low-level I2C primitives |
629 | 645 | ///////////////////////////////////////////////////////////////////////////////////////// |
630 | 646 |
|
| 647 | +// All command transactions go through writeImpl()/readImpl(), which: |
| 648 | +// - send the pre-built command frame on the I2C bus, |
| 649 | +// - delay CMD_DELAY, |
| 650 | +// - read the hub status byte (and any payload, for the read variant), |
| 651 | +// - retry up to kBusyRetryMax times on 0xFE (busy), doubling the delay |
| 652 | +// each attempt up to kBusyMaxDelayMs. |
| 653 | +// Per UG6921 §1.1: "0xFE: Device is busy. Try again. Increase the CMD_DELAY." |
| 654 | + |
| 655 | +Max32664Status Max32664::writeImpl(const uint8_t *frame, size_t frameLen, uint16_t cmdDelayMs) |
| 656 | +{ |
| 657 | + uint16_t actualDelay = cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs; |
| 658 | + |
| 659 | + for (uint8_t attempt = 0; attempt <= kBusyRetryMax; ++attempt) |
| 660 | + { |
| 661 | + _bus.beginTransmission(MAX32664_I2C_ADDR); |
| 662 | + for (size_t i = 0; i < frameLen; ++i) _bus.write(frame[i]); |
| 663 | + if (_bus.endTransmission() != 0) |
| 664 | + { |
| 665 | + tracef("write fam=0x%02X idx=0x%02X i2c-nack", |
| 666 | + frameLen > 0 ? frame[0] : 0, frameLen > 1 ? frame[1] : 0); |
| 667 | + return Max32664Status::HostCommError; |
| 668 | + } |
| 669 | + delay(actualDelay); |
| 670 | + if (_bus.requestFrom(uint8_t(MAX32664_I2C_ADDR), uint8_t(1)) != 1) |
| 671 | + return Max32664Status::HostCommError; |
| 672 | + uint8_t st = uint8_t(_bus.read()); |
| 673 | + |
| 674 | + if (st == 0x00) return Max32664Status::Ok; |
| 675 | + if (st != 0xFE) |
| 676 | + { |
| 677 | + tracef("write fam=0x%02X idx=0x%02X status=0x%02X", |
| 678 | + frameLen > 0 ? frame[0] : 0, frameLen > 1 ? frame[1] : 0, st); |
| 679 | + return Max32664Status(st); |
| 680 | + } |
| 681 | + if (attempt == kBusyRetryMax) |
| 682 | + { |
| 683 | + tracef("write fam=0x%02X idx=0x%02X 0xFE busy after %u retries", |
| 684 | + frameLen > 0 ? frame[0] : 0, frameLen > 1 ? frame[1] : 0, attempt); |
| 685 | + return Max32664Status::DeviceBusy; |
| 686 | + } |
| 687 | + actualDelay = (actualDelay < kBusyMaxDelayMs / 2) |
| 688 | + ? uint16_t(actualDelay * 2u) |
| 689 | + : kBusyMaxDelayMs; |
| 690 | + } |
| 691 | + return Max32664Status::DeviceBusy; |
| 692 | +} |
| 693 | + |
| 694 | +Max32664Status Max32664::readImpl(const uint8_t *frame, size_t frameLen, |
| 695 | + uint8_t *out, size_t outLen, uint16_t cmdDelayMs) |
| 696 | +{ |
| 697 | + uint16_t actualDelay = cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs; |
| 698 | + |
| 699 | + for (uint8_t attempt = 0; attempt <= kBusyRetryMax; ++attempt) |
| 700 | + { |
| 701 | + _bus.beginTransmission(MAX32664_I2C_ADDR); |
| 702 | + for (size_t i = 0; i < frameLen; ++i) _bus.write(frame[i]); |
| 703 | + if (_bus.endTransmission() != 0) |
| 704 | + { |
| 705 | + tracef("read fam=0x%02X idx=0x%02X i2c-nack", |
| 706 | + frameLen > 0 ? frame[0] : 0, frameLen > 1 ? frame[1] : 0); |
| 707 | + return Max32664Status::HostCommError; |
| 708 | + } |
| 709 | + delay(actualDelay); |
| 710 | + |
| 711 | + // Drain (1 + outLen) bytes across as many requestFrom() calls as the |
| 712 | + // platform's Wire buffer requires. The leading byte is the status; on |
| 713 | + // 0xFE we discard the rest of the chunk and retry the whole command. |
| 714 | + size_t remaining = outLen; |
| 715 | + uint8_t *cursor = out; |
| 716 | + bool firstChunk = true; |
| 717 | + uint8_t status = 0; |
| 718 | + bool transient = false; |
| 719 | + |
| 720 | + while (remaining > 0 || firstChunk) |
| 721 | + { |
| 722 | + uint16_t want = uint16_t((firstChunk ? 1u : 0u) + remaining); |
| 723 | + if (want > kReadChunkBytes) want = kReadChunkBytes; |
| 724 | + uint8_t got = _bus.requestFrom(uint8_t(MAX32664_I2C_ADDR), uint8_t(want)); |
| 725 | + if (got != want) return Max32664Status::HostCommError; |
| 726 | + |
| 727 | + if (firstChunk) |
| 728 | + { |
| 729 | + status = uint8_t(_bus.read()); |
| 730 | + --got; |
| 731 | + firstChunk = false; |
| 732 | + if (status != 0x00) |
| 733 | + { |
| 734 | + while (got--) _bus.read(); |
| 735 | + transient = (status == 0xFE); |
| 736 | + break; |
| 737 | + } |
| 738 | + } |
| 739 | + while (got-- && remaining > 0) |
| 740 | + { |
| 741 | + *cursor++ = uint8_t(_bus.read()); |
| 742 | + --remaining; |
| 743 | + } |
| 744 | + if (remaining == 0) break; |
| 745 | + } |
| 746 | + |
| 747 | + if (status == 0x00) return Max32664Status::Ok; |
| 748 | + if (!transient) |
| 749 | + { |
| 750 | + tracef("read fam=0x%02X idx=0x%02X status=0x%02X", |
| 751 | + frameLen > 0 ? frame[0] : 0, frameLen > 1 ? frame[1] : 0, status); |
| 752 | + return Max32664Status(status); |
| 753 | + } |
| 754 | + if (attempt == kBusyRetryMax) |
| 755 | + { |
| 756 | + tracef("read fam=0x%02X idx=0x%02X 0xFE busy after %u retries", |
| 757 | + frameLen > 0 ? frame[0] : 0, frameLen > 1 ? frame[1] : 0, attempt); |
| 758 | + return Max32664Status::DeviceBusy; |
| 759 | + } |
| 760 | + actualDelay = (actualDelay < kBusyMaxDelayMs / 2) |
| 761 | + ? uint16_t(actualDelay * 2u) |
| 762 | + : kBusyMaxDelayMs; |
| 763 | + } |
| 764 | + return Max32664Status::DeviceBusy; |
| 765 | +} |
| 766 | + |
631 | 767 | Max32664Status Max32664::writeCmd(uint8_t fam, uint8_t idx, uint16_t cmdDelayMs) |
632 | 768 | { |
633 | | - _bus.beginTransmission(MAX32664_I2C_ADDR); |
634 | | - _bus.write(fam); |
635 | | - _bus.write(idx); |
636 | | - if (_bus.endTransmission() != 0) return Max32664Status::HostCommError; |
637 | | - delay(cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs); |
638 | | - if (_bus.requestFrom(uint8_t(MAX32664_I2C_ADDR), uint8_t(1)) != 1) |
639 | | - return Max32664Status::HostCommError; |
640 | | - uint8_t st = uint8_t(_bus.read()); |
641 | | - return st == 0x00 ? Max32664Status::Ok : Max32664Status(st); |
| 769 | + uint8_t frame[2] = {fam, idx}; |
| 770 | + return writeImpl(frame, 2, cmdDelayMs); |
642 | 771 | } |
643 | 772 |
|
644 | 773 | Max32664Status Max32664::writeCmd(uint8_t fam, uint8_t idx, uint8_t v0, uint16_t cmdDelayMs) |
645 | 774 | { |
646 | | - _bus.beginTransmission(MAX32664_I2C_ADDR); |
647 | | - _bus.write(fam); |
648 | | - _bus.write(idx); |
649 | | - _bus.write(v0); |
650 | | - if (_bus.endTransmission() != 0) return Max32664Status::HostCommError; |
651 | | - delay(cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs); |
652 | | - if (_bus.requestFrom(uint8_t(MAX32664_I2C_ADDR), uint8_t(1)) != 1) |
653 | | - return Max32664Status::HostCommError; |
654 | | - uint8_t st = uint8_t(_bus.read()); |
655 | | - return st == 0x00 ? Max32664Status::Ok : Max32664Status(st); |
| 775 | + uint8_t frame[3] = {fam, idx, v0}; |
| 776 | + return writeImpl(frame, 3, cmdDelayMs); |
656 | 777 | } |
657 | 778 |
|
658 | 779 | Max32664Status Max32664::writeCmd(uint8_t fam, uint8_t idx, uint8_t v0, uint8_t v1, |
659 | 780 | uint16_t cmdDelayMs) |
660 | 781 | { |
661 | | - _bus.beginTransmission(MAX32664_I2C_ADDR); |
662 | | - _bus.write(fam); |
663 | | - _bus.write(idx); |
664 | | - _bus.write(v0); |
665 | | - _bus.write(v1); |
666 | | - if (_bus.endTransmission() != 0) return Max32664Status::HostCommError; |
667 | | - delay(cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs); |
668 | | - if (_bus.requestFrom(uint8_t(MAX32664_I2C_ADDR), uint8_t(1)) != 1) |
669 | | - return Max32664Status::HostCommError; |
670 | | - uint8_t st = uint8_t(_bus.read()); |
671 | | - return st == 0x00 ? Max32664Status::Ok : Max32664Status(st); |
| 782 | + uint8_t frame[4] = {fam, idx, v0, v1}; |
| 783 | + return writeImpl(frame, 4, cmdDelayMs); |
672 | 784 | } |
673 | 785 |
|
674 | 786 | Max32664Status Max32664::writeCmd3(uint8_t fam, uint8_t idx, |
675 | 787 | uint8_t v0, uint8_t v1, uint8_t v2, |
676 | 788 | uint16_t cmdDelayMs) |
677 | 789 | { |
678 | | - _bus.beginTransmission(MAX32664_I2C_ADDR); |
679 | | - _bus.write(fam); |
680 | | - _bus.write(idx); |
681 | | - _bus.write(v0); |
682 | | - _bus.write(v1); |
683 | | - _bus.write(v2); |
684 | | - if (_bus.endTransmission() != 0) return Max32664Status::HostCommError; |
685 | | - delay(cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs); |
686 | | - if (_bus.requestFrom(uint8_t(MAX32664_I2C_ADDR), uint8_t(1)) != 1) |
687 | | - return Max32664Status::HostCommError; |
688 | | - uint8_t st = uint8_t(_bus.read()); |
689 | | - return st == 0x00 ? Max32664Status::Ok : Max32664Status(st); |
| 790 | + uint8_t frame[5] = {fam, idx, v0, v1, v2}; |
| 791 | + return writeImpl(frame, 5, cmdDelayMs); |
690 | 792 | } |
691 | 793 |
|
692 | 794 | Max32664Status Max32664::writeCmd(uint8_t fam, uint8_t idx, |
693 | 795 | const uint8_t *payload, size_t len, |
694 | 796 | uint16_t cmdDelayMs) |
695 | 797 | { |
696 | | - _bus.beginTransmission(MAX32664_I2C_ADDR); |
697 | | - _bus.write(fam); |
698 | | - _bus.write(idx); |
699 | | - for (size_t i = 0; i < len; ++i) _bus.write(payload[i]); |
700 | | - if (_bus.endTransmission() != 0) return Max32664Status::HostCommError; |
701 | | - delay(cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs); |
702 | | - if (_bus.requestFrom(uint8_t(MAX32664_I2C_ADDR), uint8_t(1)) != 1) |
703 | | - return Max32664Status::HostCommError; |
704 | | - uint8_t st = uint8_t(_bus.read()); |
705 | | - return st == 0x00 ? Max32664Status::Ok : Max32664Status(st); |
| 798 | + // Inline assemble: max payload across callers is 12 bytes (SpO2 coeffs). |
| 799 | + uint8_t frame[16]; |
| 800 | + if (2 + len > sizeof(frame)) return Max32664Status::BufferTooSmall; |
| 801 | + frame[0] = fam; |
| 802 | + frame[1] = idx; |
| 803 | + for (size_t i = 0; i < len; ++i) frame[2 + i] = payload[i]; |
| 804 | + return writeImpl(frame, 2 + len, cmdDelayMs); |
706 | 805 | } |
707 | 806 |
|
708 | 807 | Max32664Status Max32664::writeCmd(uint8_t fam, uint8_t idx, uint8_t sub, |
709 | 808 | const uint8_t *payload, size_t len, |
710 | 809 | uint16_t cmdDelayMs) |
711 | 810 | { |
712 | | - _bus.beginTransmission(MAX32664_I2C_ADDR); |
713 | | - _bus.write(fam); |
714 | | - _bus.write(idx); |
715 | | - _bus.write(sub); |
716 | | - for (size_t i = 0; i < len; ++i) _bus.write(payload[i]); |
717 | | - if (_bus.endTransmission() != 0) return Max32664Status::HostCommError; |
718 | | - delay(cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs); |
719 | | - if (_bus.requestFrom(uint8_t(MAX32664_I2C_ADDR), uint8_t(1)) != 1) |
720 | | - return Max32664Status::HostCommError; |
721 | | - uint8_t st = uint8_t(_bus.read()); |
722 | | - return st == 0x00 ? Max32664Status::Ok : Max32664Status(st); |
723 | | -} |
724 | | - |
725 | | -// Internal helper for the read primitives below: pulls (1 + len) bytes off |
726 | | -// the bus across as many requestFrom() calls as the platform's Wire buffer |
727 | | -// requires, validates the leading status byte, and writes the payload bytes |
728 | | -// to `out`. |
729 | | -namespace |
730 | | -{ |
731 | | -Max32664Status drainResponse(TwoWire &bus, uint8_t *out, size_t len) |
732 | | -{ |
733 | | - size_t remaining = len; |
734 | | - uint8_t *cursor = out; |
735 | | - bool firstChunk = true; |
736 | | - |
737 | | - while (remaining > 0 || firstChunk) |
738 | | - { |
739 | | - uint16_t want = uint16_t((firstChunk ? 1u : 0u) + remaining); |
740 | | - if (want > kReadChunkBytes) want = kReadChunkBytes; |
741 | | - uint8_t got = bus.requestFrom(uint8_t(MAX32664_I2C_ADDR), uint8_t(want)); |
742 | | - if (got != want) return Max32664Status::HostCommError; |
743 | | - |
744 | | - if (firstChunk) |
745 | | - { |
746 | | - uint8_t st = uint8_t(bus.read()); |
747 | | - --got; |
748 | | - firstChunk = false; |
749 | | - if (st != 0x00) |
750 | | - { |
751 | | - while (got--) bus.read(); |
752 | | - return Max32664Status(st); |
753 | | - } |
754 | | - } |
755 | | - while (got-- && remaining > 0) |
756 | | - { |
757 | | - *cursor++ = uint8_t(bus.read()); |
758 | | - --remaining; |
759 | | - } |
760 | | - if (remaining == 0) break; |
761 | | - } |
762 | | - return Max32664Status::Ok; |
| 811 | + uint8_t frame[16]; |
| 812 | + if (3 + len > sizeof(frame)) return Max32664Status::BufferTooSmall; |
| 813 | + frame[0] = fam; |
| 814 | + frame[1] = idx; |
| 815 | + frame[2] = sub; |
| 816 | + for (size_t i = 0; i < len; ++i) frame[3 + i] = payload[i]; |
| 817 | + return writeImpl(frame, 3 + len, cmdDelayMs); |
763 | 818 | } |
764 | | -} // namespace |
765 | 819 |
|
766 | 820 | Max32664Status Max32664::readBytes(uint8_t fam, uint8_t idx, |
767 | 821 | uint8_t *out, size_t len, uint16_t cmdDelayMs) |
768 | 822 | { |
769 | | - _bus.beginTransmission(MAX32664_I2C_ADDR); |
770 | | - _bus.write(fam); |
771 | | - _bus.write(idx); |
772 | | - if (_bus.endTransmission() != 0) return Max32664Status::HostCommError; |
773 | | - delay(cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs); |
774 | | - return drainResponse(_bus, out, len); |
| 823 | + uint8_t frame[2] = {fam, idx}; |
| 824 | + return readImpl(frame, 2, out, len, cmdDelayMs); |
775 | 825 | } |
776 | 826 |
|
777 | 827 | Max32664Status Max32664::readBytes(uint8_t fam, uint8_t idx, uint8_t sub, |
778 | 828 | uint8_t *out, size_t len, uint16_t cmdDelayMs) |
779 | 829 | { |
780 | | - _bus.beginTransmission(MAX32664_I2C_ADDR); |
781 | | - _bus.write(fam); |
782 | | - _bus.write(idx); |
783 | | - _bus.write(sub); |
784 | | - if (_bus.endTransmission() != 0) return Max32664Status::HostCommError; |
785 | | - delay(cmdDelayMs ? cmdDelayMs : kDefaultCmdDelayMs); |
786 | | - return drainResponse(_bus, out, len); |
| 830 | + uint8_t frame[3] = {fam, idx, sub}; |
| 831 | + return readImpl(frame, 3, out, len, cmdDelayMs); |
787 | 832 | } |
788 | 833 |
|
789 | 834 | ///////////////////////////////////////////////////////////////////////////////////////// |
|
0 commit comments