pw_analog: Add MicrovoltInput class Add interface for converting an analog sample into a fixed point voltage in microvolts with MicrovoltInput. Testing: Host test -- OK Change-Id: I481e78ed99e170f6b3838a5ce283e869be810ee5 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/43840 Reviewed-by: Ewout van Bekkum <ewout@google.com> Reviewed-by: David Rogers <davidrogers@google.com> Commit-Queue: Kevin Zeng <zengk@google.com>
diff --git a/pw_analog/BUILD b/pw_analog/BUILD index 3d524d9..d602521 100644 --- a/pw_analog/BUILD +++ b/pw_analog/BUILD
@@ -34,6 +34,20 @@ ], ) +pw_cc_library( + name = "microvolt_input", + hdrs = [ + "public/pw_analog/microvolt_input.h", + ], + includes = ["public"], + deps = [ + ":analog_input", + "//pw_chrono:system_clock", + "//pw_result", + "//pw_status", + ], +) + pw_cc_test( name = "analog_input_test", srcs = [ @@ -44,3 +58,14 @@ "//pw_unit_test", ], ) + +pw_cc_test( + name = "microvolt_input_test", + srcs = [ + "microvolt_input_test.cc", + ], + deps = [ + ":microvolt_input", + "//pw_unit_test", + ], +)
diff --git a/pw_analog/BUILD.gn b/pw_analog/BUILD.gn index 8b1eab7..360fc93 100644 --- a/pw_analog/BUILD.gn +++ b/pw_analog/BUILD.gn
@@ -32,8 +32,22 @@ public = [ "public/pw_analog/analog_input.h" ] } +pw_source_set("microvolt_input") { + public_configs = [ ":public_include_path" ] + public_deps = [ + ":pw_analog", + "$dir_pw_chrono:system_clock", + "$dir_pw_result", + "$dir_pw_status", + ] + public = [ "public/pw_analog/microvolt_input.h" ] +} + pw_test_group("tests") { - tests = [ ":analog_input_test" ] + tests = [ + ":analog_input_test", + ":microvolt_input_test", + ] } pw_test("analog_input_test") { @@ -42,6 +56,12 @@ deps = [ ":pw_analog" ] } +pw_test("microvolt_input_test") { + enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != "" + sources = [ "microvolt_input_test.cc" ] + deps = [ ":pw_analog" ] +} + pw_doc_group("docs") { sources = [ "docs.rst" ] }
diff --git a/pw_analog/docs.rst b/pw_analog/docs.rst index 1c5d485..5176465 100644 --- a/pw_analog/docs.rst +++ b/pw_analog/docs.rst
@@ -19,3 +19,12 @@ driver implementation in order to configure and enable the ADC peripheral. Users are responsible for managing multithreaded access to the ADC driver if the ADC services multiple channels. + +pw::analog::MicrovoltInput +-------------------------- +The common interface for obtaining voltage samples in microvolts. This interface +represents a single voltage input or channel. Users will need to supply their +own ADC driver implementation in order to configure and enable the ADC +peripheral in order to provide the reference voltages and to configure and +enable the ADC peripheral where needed. Users are responsible for managing +multithreaded access to the ADC driver if the ADC services multiple channels.
diff --git a/pw_analog/microvolt_input_test.cc b/pw_analog/microvolt_input_test.cc new file mode 100644 index 0000000..e4b412f --- /dev/null +++ b/pw_analog/microvolt_input_test.cc
@@ -0,0 +1,287 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#include "pw_analog/microvolt_input.h" + +#include "gtest/gtest.h" + +namespace pw { +namespace analog { +namespace { + +using namespace std::chrono_literals; + +constexpr int32_t kLimitsMax = 4096; +constexpr int32_t kLimitsMin = 0; +constexpr int32_t kReferenceMaxVoltageUv = 1800000; +constexpr int32_t kReferenceMinVoltageUv = 0; +constexpr chrono::SystemClock::duration kTimeout = 1ms; + +constexpr int32_t kBipolarLimitsMax = 4096; +constexpr int32_t kBipolarLimitsMin = -4096; +constexpr int32_t kBipolarReferenceMaxVoltageUv = 1800000; +constexpr int32_t kBipolarReferenceMinVoltageUv = -1800000; + +constexpr int32_t kCornerLimitsMax = std::numeric_limits<int32_t>::max(); +constexpr int32_t kCornerLimitsMin = std::numeric_limits<int32_t>::min(); +constexpr int32_t kCornerReferenceMaxVoltageUv = + std::numeric_limits<int32_t>::max(); +constexpr int32_t kCornerReferenceMinVoltageUv = + std::numeric_limits<int32_t>::min(); + +constexpr int32_t kInvertedLimitsMax = std::numeric_limits<int32_t>::max(); +constexpr int32_t kInvertedLimitsMin = std::numeric_limits<int32_t>::min(); +constexpr int32_t kInvertedReferenceMaxVoltageUv = + std::numeric_limits<int32_t>::min(); +constexpr int32_t kInvertedReferenceMinVoltageUv = + std::numeric_limits<int32_t>::max(); + +// Dummy test voltage input that's used for testing. +class TestMicrovoltInput : public MicrovoltInput { + public: + constexpr explicit TestMicrovoltInput(AnalogInput::Limits limits, + MicrovoltInput::References reference) + : sample_(0), limits_(limits), reference_(reference) {} + + void SetSampleValue(int32_t sample) { sample_ = sample; } + + private: + Result<int32_t> TryReadUntil(chrono::SystemClock::time_point) override { + return sample_; + } + + Limits GetLimits() const override { return limits_; } + References GetReferences() const override { return reference_; } + + uint32_t sample_; + const Limits limits_; + const References reference_; +}; + +TEST(MicrovoltInputTest, Construction) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kLimitsMin, .max = kLimitsMax}, + {.max_voltage_uv = kReferenceMaxVoltageUv, + .min_voltage_uv = kReferenceMinVoltageUv}); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithSampleAtMin) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kLimitsMin, .max = kLimitsMax}, + {.max_voltage_uv = kReferenceMaxVoltageUv, + .min_voltage_uv = kReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kLimitsMin); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), 0); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithSampleAtMax) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kLimitsMin, .max = kLimitsMax}, + {.max_voltage_uv = kReferenceMaxVoltageUv, + .min_voltage_uv = kReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kLimitsMax); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kReferenceMaxVoltageUv); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithSampleAtHalf) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kLimitsMin, .max = kLimitsMax}, + {.max_voltage_uv = kReferenceMaxVoltageUv, + .min_voltage_uv = kReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kLimitsMax / 2); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kReferenceMaxVoltageUv / 2); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithBipolarAdcAtZero) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kBipolarLimitsMin, .max = kBipolarLimitsMax}, + {.max_voltage_uv = kBipolarReferenceMaxVoltageUv, + .min_voltage_uv = kBipolarReferenceMinVoltageUv}); + voltage_input.SetSampleValue(0); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), 0); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithBipolarAdcAtMin) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kBipolarLimitsMin, .max = kBipolarLimitsMax}, + {.max_voltage_uv = kBipolarReferenceMaxVoltageUv, + .min_voltage_uv = kBipolarReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kBipolarLimitsMin); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kBipolarReferenceMinVoltageUv); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithBipolarAdcAtMax) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kBipolarLimitsMin, .max = kBipolarLimitsMax}, + {.max_voltage_uv = kBipolarReferenceMaxVoltageUv, + .min_voltage_uv = kBipolarReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kBipolarLimitsMax); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kBipolarReferenceMaxVoltageUv); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithBipolarAdcAtUpperHalf) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kBipolarLimitsMin, .max = kBipolarLimitsMax}, + {.max_voltage_uv = kBipolarReferenceMaxVoltageUv, + .min_voltage_uv = kBipolarReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kBipolarLimitsMax / 2); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kBipolarReferenceMaxVoltageUv / 2); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithBipolarAdcAtLowerHalf) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kBipolarLimitsMin, .max = kBipolarLimitsMax}, + {.max_voltage_uv = kBipolarReferenceMaxVoltageUv, + .min_voltage_uv = kBipolarReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kBipolarLimitsMin / 2); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kBipolarReferenceMinVoltageUv / 2); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithBipolarReferenceAtZero) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kLimitsMin, .max = kLimitsMax}, + {.max_voltage_uv = kBipolarReferenceMaxVoltageUv, + .min_voltage_uv = kBipolarReferenceMinVoltageUv}); + voltage_input.SetSampleValue(0); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kBipolarReferenceMinVoltageUv); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithBipolarReferenceAtMin) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kLimitsMin, .max = kLimitsMax}, + {.max_voltage_uv = kBipolarReferenceMaxVoltageUv, + .min_voltage_uv = kBipolarReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kLimitsMin); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kBipolarReferenceMinVoltageUv); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithBipolarReferenceAtMax) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kLimitsMin, .max = kLimitsMax}, + {.max_voltage_uv = kBipolarReferenceMaxVoltageUv, + .min_voltage_uv = kBipolarReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kLimitsMax); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kBipolarReferenceMaxVoltageUv); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithBipolarReferenceAtHalf) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kLimitsMin, .max = kLimitsMax}, + {.max_voltage_uv = kBipolarReferenceMaxVoltageUv, + .min_voltage_uv = kBipolarReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kLimitsMax / 2); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), 0); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithSampleAtMinCornerCase) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kCornerLimitsMin, .max = kCornerLimitsMax}, + {.max_voltage_uv = kCornerReferenceMaxVoltageUv, + .min_voltage_uv = kCornerReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kCornerLimitsMin); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kCornerReferenceMinVoltageUv); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithSampleAtMaxCornerCase) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kCornerLimitsMin, .max = kCornerLimitsMax}, + {.max_voltage_uv = kCornerReferenceMaxVoltageUv, + .min_voltage_uv = kCornerReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kCornerLimitsMax); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kCornerReferenceMaxVoltageUv); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithInvertedReferenceAtMax) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kInvertedLimitsMin, .max = kInvertedLimitsMax}, + {.max_voltage_uv = kInvertedReferenceMaxVoltageUv, + .min_voltage_uv = kInvertedReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kInvertedLimitsMax); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kInvertedReferenceMaxVoltageUv); +} + +TEST(MicrovoltInputTest, ReadMicrovoltsWithInvertedReferenceAtMin) { + TestMicrovoltInput voltage_input = + TestMicrovoltInput({.min = kInvertedLimitsMin, .max = kInvertedLimitsMax}, + {.max_voltage_uv = kInvertedReferenceMaxVoltageUv, + .min_voltage_uv = kInvertedReferenceMinVoltageUv}); + voltage_input.SetSampleValue(kInvertedLimitsMin); + + Result<int32_t> result = voltage_input.TryReadMicrovoltsFor(kTimeout); + ASSERT_TRUE(result.status().ok()); + + EXPECT_EQ(result.value(), kInvertedReferenceMinVoltageUv); +} +} // namespace +} // namespace analog +} // namespace pw
diff --git a/pw_analog/public/pw_analog/microvolt_input.h b/pw_analog/public/pw_analog/microvolt_input.h new file mode 100644 index 0000000..b7c5c0b --- /dev/null +++ b/pw_analog/public/pw_analog/microvolt_input.h
@@ -0,0 +1,85 @@ +// Copyright 2021 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once +#include "pw_analog/analog_input.h" +#include "pw_chrono/system_clock.h" +#include "pw_result/result.h" +#include "pw_status/try.h" + +namespace pw::analog { + +// The common interface for obtaining voltage samples in microvolts. This +// interface represents a single voltage input or channel. Users will need to +// supply their own ADC driver implementation in order to configure and enable +// the ADC peripheral in order to provide the reference voltages and to +// configure and enable the ADC peripheral where needed. Users are responsible +// for managing multithreaded access to the ADC driver if the ADC services +// multiple channels. +class MicrovoltInput : public AnalogInput { + public: + // Specifies the max and min microvolt range the analog input can measure. + // * These values do not change at run time. + // * Inversion of min/max is supported. + struct References { + int32_t max_voltage_uv; // Microvolts at AnalogInput::Limits::max + int32_t min_voltage_uv; // Microvolts at AnalogInput::Limits::min. + }; + + virtual ~MicrovoltInput() = default; + + // Blocks until the specified timeout duration has elapsed or the voltage + // sample has been returned, whichever comes first. + // + // This method is thread safe. + // + // Returns: + // Microvolts (uV). + // ResourceExhuasted: ADC peripheral in use. + // DeadlineExceedded: Timed out waiting for a sample. + // Other statuses left up to the implementer. + Result<int32_t> TryReadMicrovoltsFor(chrono::SystemClock::duration timeout) { + return TryReadMicrovoltsUntil( + chrono::SystemClock::TimePointAfterAtLeast(timeout)); + } + + // Blocks until the deadline time has been reached or the voltage sample has + // been returned, whichever comes first. + // + // This method is thread safe. + // + // Returns: + // Microvolts (uV). + // ResourceExhuasted: ADC peripheral in use. + // DeadlineExceedded: Timed out waiting for a sample. + // Other statuses left up to the implementer. + Result<int32_t> TryReadMicrovoltsUntil( + chrono::SystemClock::time_point deadline) { + PW_TRY_ASSIGN(const int32_t sample, TryReadUntil(deadline)); + + const References reference = GetReferences(); + const AnalogInput::Limits limits = GetLimits(); + + return ((static_cast<int64_t>(sample - limits.min) * + (reference.max_voltage_uv - reference.min_voltage_uv)) / + (limits.max - limits.min)) + + reference.min_voltage_uv; + } + + private: + // Returns the reference voltage needed to calculate the voltage. + // These values do not change at run time. + virtual References GetReferences() const = 0; +}; + +} // namespace pw::analog