diff --git a/include/v2/events.h b/include/v2/events.h index 13ab053..28b0144 100644 --- a/include/v2/events.h +++ b/include/v2/events.h @@ -4,10 +4,10 @@ */ #pragma once -#include - #include +#include + #include "v2/pocketpd.h" namespace pocketpd { @@ -65,7 +65,7 @@ namespace pocketpd { }; /** - * @brief Latest sensor reading from INA226. + * @brief Load-side reading from INA226. */ struct LoadReading { uint32_t timestamp_ms = 0; @@ -74,10 +74,21 @@ namespace pocketpd { }; /** - * @brief Published by SensorTask. Carries one bus reading. + * @brief Supply-side reading Sourced from the V_SENSE ADC divider on HW1.3+ or the AP33772 + * VOLTAGE register on earlier board. + */ + struct SupplyReading { + uint32_t timestamp_ms = 0; + uint32_t mv = 0; + bool valid = false; + }; + + /** + * @brief Published by SensorTask */ struct SensorEvent { LoadReading load; + SupplyReading supply; }; /** diff --git a/include/v2/hal/adc_supply_voltage_source.h b/include/v2/hal/adc_supply_voltage_source.h new file mode 100644 index 0000000..05eef58 --- /dev/null +++ b/include/v2/hal/adc_supply_voltage_source.h @@ -0,0 +1,40 @@ +/** + * @file adc_supply_voltage_source.h + * @brief ADC-backed `SupplyVoltageSource` for HW1.3+ (V_SENSE divider into ADC3). + */ +#pragma once + +#include + +#include + +#include "v2/hal/supply_voltage_source.h" + +namespace pocketpd { + + class AdcSupplyVoltageSource : public SupplyVoltageSource { + private: + uint8_t m_pin; + + // RP2040 ADC: 12-bit, 3.3 V reference. V_SENSE divider = 2/13 of VBUS. + static constexpr uint32_t ADC_REF_MV = 3300; + static constexpr uint16_t ADC_MAX = 4095; + static constexpr uint32_t DIVIDER_NUM = 13; + static constexpr uint32_t DIVIDER_DEN = 2; + + public: + explicit AdcSupplyVoltageSource(uint8_t pin) : m_pin(pin) {} + + void begin() override { + analogReadResolution(12); + } + + SupplyVoltageReading read() override { + const uint32_t raw = analogRead(m_pin); + const uint32_t adc_mv = (raw * ADC_REF_MV) / ADC_MAX; + const uint32_t vbus_mv = (adc_mv * DIVIDER_NUM) / DIVIDER_DEN; + return SupplyVoltageReading{vbus_mv, true}; + } + }; + +} // namespace pocketpd diff --git a/include/v2/hal/ap33772_pd_sink.h b/include/v2/hal/ap33772_pd_sink.h index bd8ca98..aa142fe 100644 --- a/include/v2/hal/ap33772_pd_sink.h +++ b/include/v2/hal/ap33772_pd_sink.h @@ -51,6 +51,9 @@ namespace pocketpd { bool set_pps_pdo(int index, int voltage_mv, int current_ma) override { return m_driver.set_pps_pdo(index, voltage_mv, current_ma); } + int read_vbus_mv() override { + return m_driver.read_voltage(); + } bool reset() const { return m_driver.reset(); } diff --git a/include/v2/hal/ap33772_supply_voltage_source.h b/include/v2/hal/ap33772_supply_voltage_source.h new file mode 100644 index 0000000..62bf073 --- /dev/null +++ b/include/v2/hal/ap33772_supply_voltage_source.h @@ -0,0 +1,31 @@ +/** + * @file ap33772_supply_voltage_source.h + * @brief `SupplyVoltageSource` backed by the AP33772 VOLTAGE register, for + * boards without the V_SENSE ADC divider (HW < 1.3). + */ +#pragma once + +#include "v2/hal/pd_sink_controller.h" +#include "v2/hal/supply_voltage_source.h" + +namespace pocketpd { + + class Ap33772SupplyVoltageSource : public SupplyVoltageSource { + private: + PdSinkController& m_sink; + + public: + explicit Ap33772SupplyVoltageSource(PdSinkController& sink) : m_sink(sink) {} + + void begin() override {} + + SupplyVoltageReading read() override { + const int mv = m_sink.read_vbus_mv(); + if (mv < 0) { + return SupplyVoltageReading{0, false}; + } + return SupplyVoltageReading{static_cast(mv), true}; + } + }; + +} // namespace pocketpd diff --git a/include/v2/hal/pd_sink_controller.h b/include/v2/hal/pd_sink_controller.h index dfb2790..a4cdc25 100644 --- a/include/v2/hal/pd_sink_controller.h +++ b/include/v2/hal/pd_sink_controller.h @@ -53,6 +53,14 @@ namespace pocketpd { /** @brief Request a PPS APDO with explicit voltage and current. */ [[nodiscard]] virtual bool set_pps_pdo(int index, int voltage_mv, int current_ma) = 0; + + // —— Live readings + + /** + * @brief Read VBUS voltage from the sink in mV. Returns -1 on I2C error. + * Used by Ap33772VSenseSource on HW < 1.3 (no ADC voltage divider). + */ + virtual int read_vbus_mv() = 0; }; } // namespace pocketpd diff --git a/include/v2/hal/supply_voltage_source.h b/include/v2/hal/supply_voltage_source.h new file mode 100644 index 0000000..e2b2d56 --- /dev/null +++ b/include/v2/hal/supply_voltage_source.h @@ -0,0 +1,32 @@ +/** + * @file supply_voltage_source.h + * @brief Abstract source for the supply-side voltage. + * + * HW1.3+ reads through the V_SENSE ADC divider. Earlier boards query the + * AP33772 VOLTAGE register through `PdSinkController`. + */ +#pragma once + +#include + +namespace pocketpd { + + /** + * @brief One supply-voltage sample. `valid` false on hardware read error. + */ + struct SupplyVoltageReading { + uint32_t mv = 0; + bool valid = false; + }; + + class SupplyVoltageSource { + public: + virtual ~SupplyVoltageSource() = default; + + virtual void begin() = 0; + + /** @brief Sample the supply voltage. */ + virtual SupplyVoltageReading read() = 0; + }; + +} // namespace pocketpd diff --git a/include/v2/stages/normal/normal_view.h b/include/v2/stages/normal/normal_view.h index fdc2303..706a460 100644 --- a/include/v2/stages/normal/normal_view.h +++ b/include/v2/stages/normal/normal_view.h @@ -29,6 +29,7 @@ namespace pocketpd { bool locked = false; uint8_t arrow_frame = 0; LoadReading load_reading{}; + SupplyReading supply_reading{}; // —— PPS branch (valid when has_profile && is_pps) @@ -79,9 +80,8 @@ namespace pocketpd { std::array buf{}; d.set_font(tempo::Font::XL); - draw_measured( - d, "V", V_MEASURED_Y, vm.load_reading.vbus_mv, buf, vm.readout_visible - ); + const uint32_t v_mv = vm.output_enabled ? vm.load_reading.vbus_mv : vm.supply_reading.mv; + draw_measured(d, "V", V_MEASURED_Y, v_mv, buf, vm.readout_visible); draw_measured( d, "A", A_MEASURED_Y, vm.load_reading.current_ma, buf, vm.readout_visible ); diff --git a/include/v2/stages/normal_stage.h b/include/v2/stages/normal_stage.h index f872743..d2e8a50 100644 --- a/include/v2/stages/normal_stage.h +++ b/include/v2/stages/normal_stage.h @@ -34,7 +34,9 @@ namespace pocketpd { OutputGate& m_output_gate; LoadReading m_load_reading{}; + SupplyReading m_supply_reading{}; bool m_load_init = false; + bool m_supply_init = false; int8_t m_active_pdo_index = -1; int8_t m_last_active_index = -1; @@ -47,6 +49,7 @@ namespace pocketpd { bool m_blink_visible = true; static constexpr uint32_t SENSOR_EMA_DEN = 4; + static constexpr uint32_t SUPPLY_EMA_DEN = 8; static constexpr uint32_t READOUT_BLINK_ON_MS = 1200; static constexpr uint32_t READOUT_BLINK_OFF_MS = 400; static constexpr uint32_t READOUT_BLINK_CYCLE_MS = @@ -190,6 +193,12 @@ namespace pocketpd { ? Filter::ema(m_load_reading, evt.load, SENSOR_EMA_DEN) : evt.load; m_load_init = true; + if (evt.supply.valid) { + m_supply_reading = m_supply_init + ? Filter::ema(m_supply_reading, evt.supply, SUPPLY_EMA_DEN) + : evt.supply; + m_supply_init = true; + } }, [](const auto&) {}, }; @@ -244,6 +253,7 @@ namespace pocketpd { .locked = m_locked, .arrow_frame = m_arrow_frame, .load_reading = m_load_reading, + .supply_reading = m_supply_reading, }; if (!vm.has_profile) { diff --git a/include/v2/tasks/sensor_task.h b/include/v2/tasks/sensor_task.h index bac42b1..60fff36 100644 --- a/include/v2/tasks/sensor_task.h +++ b/include/v2/tasks/sensor_task.h @@ -1,7 +1,7 @@ /** * @file sensor_task.h - * @brief Reads PowerMonitor at 33 ms while NormalStage is active and publishes - * a `SensorEvent` carrying the snapshot. NormalStage consumes the event. + * @brief Polls PowerMonitor and SupplyVoltageSource at 40 ms while NormalStage, + * EnergyStage, or ProfilePickerStage is active. Publishes a fused `SensorEvent`. */ #pragma once @@ -10,6 +10,7 @@ #include "v2/app.h" #include "v2/events.h" #include "v2/hal/power_monitor.h" +#include "v2/hal/supply_voltage_source.h" #include "v2/pocketpd.h" namespace pocketpd { @@ -17,23 +18,28 @@ namespace pocketpd { class SensorTask : public App::StageScopedTask, public App::UsePublisher { private: PowerMonitor& m_monitor; + SupplyVoltageSource& m_supply; public: - static constexpr uint32_t PERIOD_MS = 33; + static constexpr uint32_t PERIOD_MS = 40; - explicit SensorTask(PowerMonitor& monitor) + SensorTask(PowerMonitor& monitor, SupplyVoltageSource& supply) : App::StageScopedTask( PERIOD_MS, App::StageMask::of() ), - m_monitor(monitor) {} + m_monitor(monitor), m_supply(supply) {} void on_tick(uint32_t now_ms) override { - const auto reading = m_monitor.read(); - if (!reading.valid) { + const auto load = m_monitor.read(); + const auto supply = m_supply.read(); + if (!load.valid && !supply.valid) { return; } - publish(SensorEvent{LoadReading{now_ms, reading.mv, reading.ma}}); + publish(SensorEvent{ + LoadReading{now_ms, load.mv, load.ma}, + SupplyReading{now_ms, supply.mv, supply.valid}, + }); } }; diff --git a/include/v2/util/filter.h b/include/v2/util/filter.h index 294d7e9..674a9fe 100644 --- a/include/v2/util/filter.h +++ b/include/v2/util/filter.h @@ -28,6 +28,16 @@ namespace pocketpd { .current_ma = ema(prev.current_ma, sample.current_ma, den), }; } + + /** @brief Per-field EMA across a `SupplyReading`. Caller filters invalid samples. */ + static SupplyReading + ema(const SupplyReading& prev, const SupplyReading& sample, uint32_t den) { + return { + .timestamp_ms = sample.timestamp_ms, + .mv = ema(prev.mv, sample.mv, den), + .valid = sample.valid, + }; + } }; } // namespace pocketpd diff --git a/lib/PocketPDPinOut/PocketPDPinOut.h b/lib/PocketPDPinOut/PocketPDPinOut.h index 7168e80..fd15818 100644 --- a/lib/PocketPDPinOut/PocketPDPinOut.h +++ b/lib/PocketPDPinOut/PocketPDPinOut.h @@ -12,3 +12,5 @@ #define pin_SDA 4 #define pin_SCL 5 +#define pin_VSENSE 29 // ADC3 — V_SENSE divider (2/13 of VBUS) + diff --git a/src/main.cpp b/src/main.cpp index d77d16d..a181bb0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,8 @@ #include "v2/app.h" #include "v2/hal/ap33772_pd_sink.h" +#include "v2/hal/ap33772_supply_voltage_source.h" +#include "v2/hal/adc_supply_voltage_source.h" #include "v2/hal/arduino_clock.h" #include "v2/hal/arduino_output_gate.h" #include "v2/hal/arduino_stream_reader.h" @@ -40,6 +42,12 @@ namespace pocketpd { INA226 ina226_driver{INA226_I2C_ADDR}; Ina226PowerMonitor power_monitor{ina226_driver}; +#if HW_VERSION_MAJOR == 1 && HW_VERSION_MINOR >= 3 + AdcSupplyVoltageSource supply_voltage_source{pin_VSENSE}; +#else + Ap33772SupplyVoltageSource supply_voltage_source{pd_sink}; +#endif + U8g2Display u8g2_display; ArduinoOutputGate output_gate{pin_output_Enable}; @@ -61,7 +69,7 @@ namespace pocketpd { ButtonTask button_task(encoder_button, l_button, r_button); EncoderTask encoder_task(encoder); - SensorTask sensor_task{power_monitor}; + SensorTask sensor_task{power_monitor, supply_voltage_source}; EnergyTask energy_task{output_gate}; CommandTask command_task{arduino_stream_reader, arduino_stream_writer}; @@ -78,6 +86,7 @@ void setup() { ina226_driver.begin(); power_monitor.begin(); + supply_voltage_source.begin(); u8g2_display.begin(); output_gate.begin(); encoder.begin(); diff --git a/test/mocks/MockPdSink.h b/test/mocks/MockPdSink.h index b52e5d5..31f3ef3 100644 --- a/test/mocks/MockPdSink.h +++ b/test/mocks/MockPdSink.h @@ -18,6 +18,7 @@ namespace pocketpd { MOCK_METHOD(int, pdo_max_current_ma, (int index), (const, override)); MOCK_METHOD(bool, set_pdo, (int index), (override)); MOCK_METHOD(bool, set_pps_pdo, (int index, int voltage_mv, int current_ma), (override)); + MOCK_METHOD(int, read_vbus_mv, (), (override)); }; } // namespace pocketpd diff --git a/test/mocks/MockSupplyVoltageSource.h b/test/mocks/MockSupplyVoltageSource.h new file mode 100644 index 0000000..a62df6f --- /dev/null +++ b/test/mocks/MockSupplyVoltageSource.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include + +#include "v2/hal/supply_voltage_source.h" + +namespace pocketpd { + + class MockSupplyVoltageSource : public SupplyVoltageSource { + public: + MOCK_METHOD(void, begin, (), (override)); + MOCK_METHOD(SupplyVoltageReading, read, (), (override)); + }; + + /** + * @brief Scripted SupplyVoltageSource. Same shape as FakePowerMonitor: pops + * the queue each `read()`, returns the last queued reading once drained. + */ + class FakeSupplyVoltageSource : public SupplyVoltageSource { + private: + std::deque m_queue; + SupplyVoltageReading m_last; + bool m_began = false; + + public: + void push(SupplyVoltageReading r) { + m_queue.push_back(r); + m_last = r; + } + + bool began() const { + return m_began; + } + + void begin() override { + m_began = true; + } + + SupplyVoltageReading read() override { + if (m_queue.empty()) { + return m_last; + } + SupplyVoltageReading r = m_queue.front(); + m_queue.pop_front(); + return r; + } + }; + +} // namespace pocketpd diff --git a/test/test_v2_energy/test.cpp b/test/test_v2_energy/test.cpp index 145cde6..9f7a91f 100644 --- a/test/test_v2_energy/test.cpp +++ b/test/test_v2_energy/test.cpp @@ -36,7 +36,7 @@ using TestConductor = App::Conductor; namespace { SensorEvent make_sensor(uint32_t ts_ms, uint32_t mv, uint32_t ma) { - return SensorEvent{LoadReading{ts_ms, mv, ma}}; + return SensorEvent{LoadReading{ts_ms, mv, ma}, SupplyReading{}}; } const EnergyEvent* drain_last_energy(TestQueue& q) { diff --git a/test/test_v2_normal/test.cpp b/test/test_v2_normal/test.cpp index e6a6813..d542374 100644 --- a/test/test_v2_normal/test.cpp +++ b/test/test_v2_normal/test.cpp @@ -355,7 +355,11 @@ TEST(NormalStage, OnEnterPdoBranchRendersVAReadoutAndPdoIndex) { TestConductor conductor; conductor.register_stage(normal); normal.prepare(2); - normal.on_event(conductor, SensorEvent{LoadReading{0, 5000, 1234}}, 0); + normal.on_event( + conductor, + SensorEvent{LoadReading{0, 5000, 1234}, SupplyReading{0, 20000, true}}, + 0 + ); conductor.start(); } @@ -421,6 +425,7 @@ TEST(NormalView, ReadoutHiddenKeepsLabelsHidesValues) { vm.readout_visible = false; vm.active_pdo_index = 0; vm.load_reading = LoadReading{0, 5000, 1234}; + vm.supply_reading = SupplyReading{0, 20000, true}; NormalView::render(display, vm); } diff --git a/test/test_v2_sensor/test.cpp b/test/test_v2_sensor/test.cpp index 0f27774..4702b8b 100644 --- a/test/test_v2_sensor/test.cpp +++ b/test/test_v2_sensor/test.cpp @@ -7,6 +7,7 @@ #define VERSION "\"test\"" #include +#include #include #include #include @@ -35,14 +36,16 @@ namespace { } // namespace -TEST(SensorTask, OnTickPublishesSensorEventWithTimestampAndValues) { +TEST(SensorTask, OnTickPublishesFusedLoadAndSupply) { FakePowerMonitor monitor; + FakeSupplyVoltageSource supply; TestQueue q; TestPublisher pub(q); - SensorTask task(monitor); + SensorTask task(monitor, supply); task.attach_publisher_INTERNAL_DO_NOT_USE(pub); monitor.push({5000, 1234, true}); + supply.push({20000, true}); task.on_tick(42); const auto* evt = pop_sensor(q); @@ -50,21 +53,44 @@ TEST(SensorTask, OnTickPublishesSensorEventWithTimestampAndValues) { EXPECT_EQ(evt->load.timestamp_ms, 42u); EXPECT_EQ(evt->load.vbus_mv, 5000u); EXPECT_EQ(evt->load.current_ma, 1234u); + EXPECT_EQ(evt->supply.timestamp_ms, 42u); + EXPECT_EQ(evt->supply.mv, 20000u); + EXPECT_TRUE(evt->supply.valid); } -TEST(SensorTask, InvalidReadingDoesNotPublish) { +TEST(SensorTask, BothInvalidDoesNotPublish) { FakePowerMonitor monitor; + FakeSupplyVoltageSource supply; TestQueue q; TestPublisher pub(q); - SensorTask task(monitor); + SensorTask task(monitor, supply); task.attach_publisher_INTERNAL_DO_NOT_USE(pub); monitor.push({0, 0, false}); + supply.push({0, false}); task.on_tick(99); EXPECT_EQ(pop_sensor(q), nullptr); } +TEST(SensorTask, LoadValidSupplyInvalidStillPublishesWithInvalidSupply) { + FakePowerMonitor monitor; + FakeSupplyVoltageSource supply; + TestQueue q; + TestPublisher pub(q); + SensorTask task(monitor, supply); + task.attach_publisher_INTERNAL_DO_NOT_USE(pub); + + monitor.push({3000, 500, true}); + supply.push({0, false}); + task.on_tick(100); + + const auto* evt = pop_sensor(q); + ASSERT_NE(evt, nullptr); + EXPECT_EQ(evt->load.vbus_mv, 3000u); + EXPECT_FALSE(evt->supply.valid); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();