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