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