lib/adc: Implement FPGA to ADC interface

- Read and write to individual or all ADCs
- Continuous read mode function
- Move Arduino SPI library build deps to
  //third_party/BUILD.gn
- Switch sample bus pins from QSPI to the SPI1 peripheral

Change-Id: I40733f251590022793a09d68a0f53d22924d430b
Reviewed-on: https://pigweed-review.googlesource.com/c/gonk/+/185810
Reviewed-by: Eric Holland <hollande@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/README.md b/README.md
index 2dc6ba0..c2ce2ad 100644
--- a/README.md
+++ b/README.md
@@ -192,16 +192,16 @@
 
 ## FPGA Connection
 
-| Net               | Function | FPGA IO# | STM32 Pin | STM32 Function  | Notes               |
-|-------------------|----------|----------|-----------|-----------------|---------------------|
-| FPGA_IO_SPARE_0_2 | rst_i    |      135 | PA0       | GPIO_Output     | Active high         |
-| FPGA_IO_SPARE_0_0 | mode_i   |      137 | PB11      | GPIO_Output     | FPGA operation mode |
-| FPGA_IO_SPARE_0_1 | valid_o  |      136 | PB10      | GPIO_Output     | Data/Transfer Valid |
-| DSPI_IO0          | miso_i   |       73 | PC9       | QUADSPI_BK1_IO0 |                     |
-| DSPI_IO1          | mosi_i   |       74 | PC10      | QUADSPI_BK1_IO1 |                     |
-| DSPI_CS           | cs_n     |       75 | PB6       | QUADSPI_BK1_NCS |                     |
-| DSPI_SCK          | sclk_i   |       76 | PB2       | QUADSPI_CLK     |                     |
-| FPGA_IO_SPARE_2_0 |          |       49 | PA7       | SPI1_MOSI       |                     |
-| FPGA_IO_SPARE_2_1 |          |       48 | PA6       | SPI1_MISO       |                     |
-| FPGA_IO_SPARE_2_2 |          |       47 | PA5       | SPI1_SCK        |                     |
-| FPGA_IO_SPARE_2_3 |          |       45 | PA4       | SPI1_NSS        |                     |
+| Net               | Function | FPGA IO# | STM32 Pin | STM32 Function | Notes               |
+|-------------------|----------|----------|-----------|----------------|---------------------|
+| FPGA_IO_SPARE_0_2 | rst_i    |      135 | PA0       | GPIO_Output    | Active high         |
+| FPGA_IO_SPARE_0_0 | mode_i   |      137 | PB11      | GPIO_Output    | FPGA operation mode |
+| DSPI_CS           | valid_o  |       75 | PB6       | GPIO_Output    | Data/Transfer Valid |
+| FPGA_IO_SPARE_1_1 | miso_i   |      101 | PC2       | SPI1_MISO      |                     |
+| FPGA_IO_SPARE_1_0 | mosi_i   |       99 | PC3       | SPI1_MOSI      |                     |
+| I2C_O_SDA         | cs_n     |       79 | PB9       | SPI1_NSS       |                     |
+| FPGA_IO_SPARE_0_1 | sclk_i   |      136 | PB10      | SPI1_SCK       |                     |
+| FPGA_IO_SPARE_2_0 |          |       49 | PA7       |                |                     |
+| FPGA_IO_SPARE_2_1 |          |       48 | PA6       |                |                     |
+| FPGA_IO_SPARE_2_2 |          |       47 | PA5       |                |                     |
+| FPGA_IO_SPARE_2_3 |          |       45 | PA4       |                |                     |
diff --git a/applications/fpga_config/BUILD.gn b/applications/fpga_config/BUILD.gn
index 934f794..8e42192 100644
--- a/applications/fpga_config/BUILD.gn
+++ b/applications/fpga_config/BUILD.gn
@@ -20,16 +20,13 @@
 pw_executable("fpga_config") {
   include_dirs = [ "//third_party/stm32duino/arduino-core/libraries/SPI/src" ]
 
-  sources = [
-    "//third_party/stm32duino/arduino-core/libraries/SPI/src/SPI.cpp",
-    "//third_party/stm32duino/arduino-core/libraries/SPI/src/utility/spi_com.c",
-    "main.cc",
-  ]
+  sources = [ "main.cc" ]
 
   deps = [
     "$dir_pw_log",
     "$dir_pw_string",
     "$dir_pw_third_party/arduino:arduino_core_sources",
+    "//lib/adc",
     "//lib/fpga_control",
     "//lib/pin_config",
     "//lib/spi_flash",
diff --git a/applications/fpga_config/main.cc b/applications/fpga_config/main.cc
index a2828a9..41bc091 100644
--- a/applications/fpga_config/main.cc
+++ b/applications/fpga_config/main.cc
@@ -18,16 +18,25 @@
 #include <Arduino.h>
 #include <SPI.h>
 
+#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
+#define PW_LOG_MODULE_NAME "main"
+
 #include "pw_log/log.h"
 #include "pw_result/result.h"
 #include "pw_string/format.h"
 
+#include "gonk/adc.h"
 #include "gonk/fpga_control.h"
 #include "gonk/pin_config.h"
 #include "gonk/spi_flash.h"
 
+using gonk::adc::Adc;
 using gonk::fpga_control::FpgaControl;
 using gonk::pin_config::FlashCS;
+using gonk::pin_config::FpgaDspiCS;
+using gonk::pin_config::FpgaIoMode;
+using gonk::pin_config::FpgaIoReset;
+using gonk::pin_config::FpgaIoValid;
 using gonk::pin_config::ICE40Done;
 using gonk::pin_config::PinConfig;
 using gonk::pin_config::StatusLed;
@@ -39,6 +48,8 @@
 FpgaControl fpga_control = FpgaControl(&pin_config);
 SpiFlash spi_flash =
     SpiFlash(FlashCS, /*baudrate=*/1000000, pin_config.flash_spi);
+Adc fpga_adc = Adc(pin_config.fpga_spi, /*fpga_spi_baudrate=*/1000000,
+                   FpgaDspiCS, FpgaIoMode, FpgaIoReset, FpgaIoValid);
 
 void ice40_done_rising_isr() { PW_LOG_DEBUG("ICE40 Done: HIGH"); }
 
@@ -48,6 +59,7 @@
 
 void IdleTask();
 void FpgaConfigTask();
+void ADCTask();
 
 void IdleTask() {
   static uint32_t last_update = millis();
@@ -82,6 +94,52 @@
   }
 }
 
+void ADCTask() {
+  static uint32_t last_update = millis();
+  static uint32_t this_update = millis();
+  static uint16_t update_count = 0;
+
+  this_update = millis();
+
+  // Check for serial input.
+  if (Serial.available()) {
+    int data_in = Serial.read();
+    // Press enter to restart the FPGA config.
+    if (data_in == '\n') {
+      PW_LOG_INFO("Restarting FPGA Config.");
+      current_task = &FpgaConfigTask;
+    }
+  }
+
+  if (this_update > last_update + 1000) {
+    PW_LOG_INFO("ADC update: %d", update_count);
+
+    pw::Status update_result = fpga_adc.UpdateContinuousMeasurements();
+    if (!update_result.ok()) {
+      PW_LOG_ERROR("UpdateContinuousMeasurements() failed");
+    }
+
+    PW_LOG_INFO("  vbus: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, ",
+                fpga_adc.vbus_measurements_[0], fpga_adc.vbus_measurements_[1],
+                fpga_adc.vbus_measurements_[2], fpga_adc.vbus_measurements_[3],
+                fpga_adc.vbus_measurements_[4], fpga_adc.vbus_measurements_[5],
+                fpga_adc.vbus_measurements_[6], fpga_adc.vbus_measurements_[7],
+                fpga_adc.vbus_measurements_[8], fpga_adc.vbus_measurements_[9],
+                fpga_adc.vbus_measurements_[10]);
+    PW_LOG_INFO(
+        "vshunt: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, ",
+        fpga_adc.vshunt_measurements_[0], fpga_adc.vshunt_measurements_[1],
+        fpga_adc.vshunt_measurements_[2], fpga_adc.vshunt_measurements_[3],
+        fpga_adc.vshunt_measurements_[4], fpga_adc.vshunt_measurements_[5],
+        fpga_adc.vshunt_measurements_[6], fpga_adc.vshunt_measurements_[7],
+        fpga_adc.vshunt_measurements_[8], fpga_adc.vshunt_measurements_[9],
+        fpga_adc.vshunt_measurements_[10]);
+
+    last_update = this_update;
+    update_count = (update_count + 1) % UINT16_MAX;
+  }
+}
+
 void CheckSpiFlash() {
   PW_LOG_INFO("Checking SPI Flash");
   spi_flash.StartFlashSpi();
@@ -97,9 +155,13 @@
   auto result = fpga_control.StartConfig();
   if (result.ok()) {
     // FPGA has been configure successfully.
-    current_task = &IdleTask;
+    current_task = &ADCTask;
     // Check if SPI flash is readable.
     CheckSpiFlash();
+    // Init ADCs
+    fpga_adc.InitAdcs();
+    fpga_adc.CheckAllAdcs();
+    fpga_adc.SetContinuousReadMode();
   } else {
     PW_LOG_INFO("Restarting FPGA Config.");
     current_task = &FpgaConfigTask;
diff --git a/applications/spi_flash_test/BUILD.gn b/applications/spi_flash_test/BUILD.gn
index 6714ab6..22f31d2 100644
--- a/applications/spi_flash_test/BUILD.gn
+++ b/applications/spi_flash_test/BUILD.gn
@@ -18,13 +18,7 @@
 import("$dir_pw_build/target_types.gni")
 
 pw_executable("spi_flash_test") {
-  include_dirs = [ "//third_party/stm32duino/arduino-core/libraries/SPI/src" ]
-
-  sources = [
-    "//third_party/stm32duino/arduino-core/libraries/SPI/src/SPI.cpp",
-    "//third_party/stm32duino/arduino-core/libraries/SPI/src/utility/spi_com.c",
-    "main.cc",
-  ]
+  sources = [ "main.cc" ]
 
   deps = [
     "$dir_pw_log",
@@ -32,6 +26,7 @@
     "$dir_pw_third_party/arduino:arduino_core_sources",
     "//lib/pin_config",
     "//lib/spi_flash",
+    "//third_party:stm32duino_spi",
   ]
 
   ldflags = [ "-Wl,--print-memory-usage" ]
diff --git a/fpga/gonk/top.pcf b/fpga/gonk/top.pcf
index 20d0727..586c938 100644
--- a/fpga/gonk/top.pcf
+++ b/fpga/gonk/top.pcf
@@ -17,12 +17,12 @@
 set_io ice_prog_spi_io0 67
 set_io ice_prog_spi_io1 68
 set_io rst_i 135
-set_io data_valid_o 136
+set_io data_valid_o 75
 set_io op_mode_i 137
-set_io sl_cs_i_0 75
-set_io sl_mosi_i_0 74
-set_io sl_miso_o_0 73
-set_io sl_sclk_i_0 76
+set_io sl_cs_i_0 79
+set_io sl_mosi_i_0 99
+set_io sl_miso_o_0 101
+set_io sl_sclk_i_0 136
 set_io cs_o_3 19
 set_io miso_i_4 2
 set_io alertl_i_6 10
diff --git a/lib/adc/BUILD.gn b/lib/adc/BUILD.gn
new file mode 100644
index 0000000..0068028
--- /dev/null
+++ b/lib/adc/BUILD.gn
@@ -0,0 +1,40 @@
+# Copyright 2023 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("adc") {
+  public_configs = [ ":default_config" ]
+  public = [ "public/gonk/adc.h" ]
+
+  sources = [ "adc.cc" ]
+
+  deps = [
+    "$dir_pw_log",
+    "$dir_pw_result",
+    "$dir_pw_span",
+    "$dir_pw_status",
+  ]
+  public_deps = [
+    "$dir_pw_bytes",
+    "$dir_pw_third_party/arduino:arduino_core_sources",
+    "//third_party:stm32duino_spi",
+  ]
+}
diff --git a/lib/adc/adc.cc b/lib/adc/adc.cc
new file mode 100644
index 0000000..96d1613
--- /dev/null
+++ b/lib/adc/adc.cc
@@ -0,0 +1,483 @@
+#include <Arduino.h>
+#include <SPI.h>
+#include <bitset>
+#include <cstddef>
+#include <stdint.h>
+
+#include "gonk/adc.h"
+
+#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
+#define PW_LOG_MODULE_NAME "Adc"
+
+#include "pw_bytes/bit.h"
+#include "pw_bytes/endian.h"
+#include "pw_bytes/span.h"
+#include "pw_log/log.h"
+#include "pw_result/result.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+namespace gonk::adc {
+
+namespace {
+
+volatile uint8_t fpga_valid_pulse_ = 0;
+
+void io_valid_rising_isr() {
+  fpga_valid_pulse_ = 1;
+  PW_LOG_DEBUG("IO Valid: HIGH");
+}
+
+void ClearValidPulse() { fpga_valid_pulse_ = 0; }
+
+} // namespace
+
+constexpr uint8_t INA229_CONFIG = 0x0;
+constexpr uint8_t INA229_ADC_CONFIG = 0x1;
+constexpr uint8_t INA229_SHUNT_CALIBRATION = 0x2;
+constexpr uint8_t INA229_SHUNT_TEMP_COEFFICIENT = 0x3;
+constexpr uint8_t INA229_VSHUNT = 0x4;
+constexpr uint8_t INA229_VBUS = 0x5;
+constexpr uint8_t INA229_DIETEMP = 0x6;
+constexpr uint8_t INA229_CURRENT = 0x7;
+constexpr uint8_t INA229_POWER = 0x8;
+constexpr uint8_t INA229_ENERGY = 0x9;
+constexpr uint8_t INA229_CHARGE = 0xA;
+constexpr uint8_t INA229_DIAG_ALERT = 0xB;
+constexpr uint8_t INA229_SHUNT_OVERVOLT_THRESHOLD = 0xC;
+constexpr uint8_t INA229_SHUNT_UNDERVOLT_THRESHOLD = 0xD;
+constexpr uint8_t INA229_BUS_OVERVOLT_THRESHOLD = 0xE;
+constexpr uint8_t INA229_BUS_UNDERVOLT_THRESHOLD = 0xF;
+constexpr uint8_t INA229_TEMP_LIMIT = 0x10;
+constexpr uint8_t INA229_POWER_LIMIT = 0x11;
+
+constexpr uint8_t INA229_MANUFACTURER_ID = 0x3e;
+constexpr uint8_t INA229_DEVICE_ID = 0x3f;
+
+constexpr uint8_t INA229RegisterByteSize[]{
+    [INA229_CONFIG] = 2,
+    [INA229_ADC_CONFIG] = 2,
+    [INA229_SHUNT_CALIBRATION] = 2,
+    [INA229_SHUNT_TEMP_COEFFICIENT] = 2,
+    [INA229_VSHUNT] = 3,
+    [INA229_VBUS] = 3,
+    [INA229_DIETEMP] = 2,
+    [INA229_CURRENT] = 3,
+    [INA229_POWER] = 3,
+    [INA229_ENERGY] = 5,
+    [INA229_CHARGE] = 5,
+    [INA229_DIAG_ALERT] = 2,
+    [INA229_SHUNT_OVERVOLT_THRESHOLD] = 2,
+    [INA229_SHUNT_UNDERVOLT_THRESHOLD] = 2,
+    [INA229_BUS_OVERVOLT_THRESHOLD] = 2,
+    [INA229_BUS_UNDERVOLT_THRESHOLD] = 2,
+    [INA229_TEMP_LIMIT] = 2,
+    [INA229_POWER_LIMIT] = 2,
+};
+constexpr pw::span<const uint8_t>
+    kINA229RegisterByteSize(INA229RegisterByteSize);
+
+Adc::Adc(SPIClass &fpga_spi, uint16_t fpga_spi_baudrate, uint16_t fpga_cs_pin,
+         uint16_t fpga_mode_pin, uint16_t fpga_reset_pin,
+         uint16_t fpga_valid_pin)
+    : fpga_spi_(fpga_spi), fpga_reset_(fpga_reset_pin),
+      fpga_mode_(fpga_mode_pin), fpga_valid_(fpga_valid_pin),
+      fpga_cs_(fpga_cs_pin),
+      spi_settings_(fpga_spi_baudrate, MSBFIRST, SPI_MODE1) {
+  sample_read_index_ = 0;
+  sample_read_time_.fill(0);
+  vbus_measurements_.fill(0);
+  vshunt_measurements_.fill(0);
+}
+
+void Adc::SetReadWriteMode() {
+  // Reset FPGA logic
+  digitalWrite(fpga_reset_, HIGH);
+  delay(10);
+  // Toggle mode to 0
+  digitalWrite(fpga_mode_, LOW);
+  delay(10);
+  // Release reset
+  digitalWrite(fpga_reset_, LOW);
+  delay(10);
+}
+
+void Adc::SetContinuousReadMode() {
+  // Reset FPGA logic
+  digitalWrite(fpga_reset_, HIGH);
+  delay(10);
+
+  // Valid signal can go high from the FPGA side as soon as mode is set to 1.
+  ClearValidPulse();
+
+  // Toggle mode to 1
+  digitalWrite(fpga_mode_, HIGH);
+  delay(10);
+
+  // Release reset
+  digitalWrite(fpga_reset_, LOW);
+  delay(10);
+}
+
+Status Adc::WaitForFpgaIOValid(uint32_t timeout_ms = 2000) {
+  PW_LOG_DEBUG("FPGA Valid Signal: Waiting");
+
+  uint32_t last_update = millis();
+  uint32_t this_update = millis();
+  bool done = false;
+  int valid = 0;
+
+  while (!done) {
+    valid = digitalRead(fpga_valid_);
+    if (valid == 1) {
+      PW_LOG_DEBUG("FPGA Valid Signal: Result Ready");
+      break;
+    }
+
+    this_update = millis();
+    // If more than two seconds have passed something likely went wrong.
+    if (this_update > last_update + timeout_ms) {
+      PW_LOG_ERROR("FPGA Valid Signal: Timeout");
+      last_update = this_update;
+      return pw::Status::DeadlineExceeded();
+    }
+  }
+
+  return pw::OkStatus();
+}
+
+Status Adc::WaitForFpgaIOValidPulse(uint32_t timeout_ms = 2000) {
+  PW_LOG_DEBUG("FPGA Valid Pulse: Waiting");
+
+  uint32_t last_update = millis();
+  uint32_t this_update = millis();
+  bool done = false;
+
+  while (!done) {
+    if (fpga_valid_pulse_ == 1) {
+      PW_LOG_DEBUG("FPGA Valid Pulse: Detected");
+      break;
+    }
+
+    this_update = millis();
+    // If more than two seconds have passed something likely went wrong.
+    if (this_update > last_update + timeout_ms) {
+      PW_LOG_ERROR("FPGA Valid Pulse: Timeout");
+      last_update = this_update;
+      return pw::Status::DeadlineExceeded();
+    }
+  }
+
+  return pw::OkStatus();
+}
+
+uint32_t Adc::ADCAddress(uint8_t adc_number, uint8_t adc_command,
+                         uint8_t mode = 1) {
+  // Address + Read/Write bit (24 bits)
+  //
+  // [23:18] - Dont Care bits
+  // [17:7] - ADC Select. These 11 bits are used to select one or multiple ADCs
+  //          for a write operation.
+  //   [17] - ADC11 Select (ADC # from schematics)
+  //   [16] - ADC10 Select
+  //   ...
+  //   [8] - ADC2 Select
+  //   [7] - ADC1 Select
+  //   NoTE: Select only one ADC for a read operation.
+  // [6:1] - Register offset address for INA229.
+  // [0] -  R/W bit. 1: READ, 0: WRITE
+
+  const uint8_t adc_index = adc_number - 1;
+
+  return (
+      // ADC selection
+      1 << (adc_index + 7)
+      // ADC Register
+      | ((adc_command & 0x3f) << 1)
+      // 1=read, 0=write
+      | mode);
+}
+
+uint32_t Adc::ADCAddressWrite(uint8_t adc_number, uint8_t adc_command) {
+  // Address with mode=0 for a write.
+  return ADCAddress(adc_number, adc_command, 0);
+}
+
+uint32_t Adc::ADCAddressWriteAll(uint8_t adc_command) {
+  return (
+      // Select all 11 ADCs
+      0x7FF << 7
+      // ADC Register
+      | ((adc_command & 0x3f) << 1)
+      // 1=read, 0=write
+      | 0);
+}
+
+pw::Result<uint8_t> Adc::RegisterSize(uint8_t adc_register) {
+  if (adc_register == INA229_MANUFACTURER_ID ||
+      adc_register == INA229_DEVICE_ID) {
+    return 2;
+  }
+  if (adc_register >= kINA229RegisterByteSize.size()) {
+    PW_LOG_ERROR("Invalid register: %x", adc_register);
+    return pw::Status::OutOfRange();
+  }
+  return INA229RegisterByteSize[adc_register];
+}
+
+void Adc::StartSpiTransaction() {
+  digitalWrite(fpga_cs_, LOW);
+  fpga_spi_.beginTransaction(spi_settings_);
+}
+
+void Adc::EndSpiTransaction() {
+  digitalWrite(fpga_cs_, HIGH);
+  fpga_spi_.endTransaction();
+}
+
+void Adc::WriteAddress(uint32_t adc_address) {
+  uint8_t address[3];
+  address[0] = (adc_address >> 16) & 0xff;
+  address[1] = (adc_address >> 8) & 0xff;
+  address[2] = adc_address & 0xff;
+
+  PW_LOG_DEBUG("WriteAddress: %x %x %x", address[2], address[1], address[0]);
+
+  fpga_spi_.transfer(address, 3);
+}
+
+pw::Result<pw::ConstByteSpan> Adc::ReadData(pw::ByteSpan read_buffer) {
+  fpga_spi_.transfer(read_buffer.data(), read_buffer.size());
+
+  return pw::ConstByteSpan(read_buffer);
+}
+
+Status Adc::WriteData(pw::ByteSpan write_buffer) {
+  fpga_spi_.transfer(write_buffer.data(), write_buffer.size());
+
+  return pw::OkStatus();
+}
+
+pw::Result<pw::ConstByteSpan> Adc::GetManufacturerID(uint8_t adc_number) {
+  return GetRegister(adc_number, INA229_MANUFACTURER_ID);
+}
+
+pw::Result<pw::ConstByteSpan> Adc::GetRegister(uint8_t adc_number,
+                                               uint8_t adc_register) {
+  uint32_t adc_address = ADCAddress(adc_number, adc_register);
+
+  StartSpiTransaction();
+  WriteAddress(adc_address);
+
+  Status wait_result = WaitForFpgaIOValid();
+  if (!wait_result.ok()) {
+    EndSpiTransaction();
+    return wait_result;
+  }
+
+  pw::Result<uint8_t> register_size_result = RegisterSize(adc_register);
+  if (!register_size_result.ok()) {
+    return register_size_result.status();
+  }
+
+  std::array<std::byte, 5> read_buffer;
+  pw::ByteSpan read_span =
+      pw::ByteSpan(read_buffer.data(), register_size_result.value());
+
+  const pw::Result<pw::ConstByteSpan> read_result = ReadData(read_span);
+  if (!read_result.ok()) {
+    return read_result.status();
+  }
+
+  EndSpiTransaction();
+
+  return read_result.value();
+}
+
+pw::Result<pw::ConstByteSpan> Adc::GetThreeBytes() {
+  std::array<std::byte, 3> read_buffer;
+  pw::ByteSpan read_span = pw::ByteSpan(read_buffer);
+
+  return ReadData(read_span);
+}
+
+Status Adc::UpdateContinuousMeasurements() {
+
+  uint32_t start_time = micros();
+
+  StartSpiTransaction();
+
+  Status wait_result = WaitForFpgaIOValidPulse();
+  if (!wait_result.ok()) {
+    EndSpiTransaction();
+    return wait_result;
+  }
+
+  for (int i = 0; i < kStreamingAdcCount; i++) {
+    // Read VBUS
+    std::array<std::byte, 3> vbus_read_buffer;
+    const pw::Result<pw::ConstByteSpan> vbus_read_result =
+        ReadData(pw::ByteSpan(vbus_read_buffer));
+    if (!vbus_read_result.ok()) {
+      PW_LOG_ERROR("vbus_read failed i=%d", i);
+      return vbus_read_result.status();
+    }
+    int32_t vbus_value = VoltageMeasurement(vbus_read_result.value());
+    vbus_measurements_[i] = vbus_value;
+
+    // Read VSHUNT
+    std::array<std::byte, 3> vshunt_read_buffer;
+    const pw::Result<pw::ConstByteSpan> vshunt_read_result =
+        ReadData(pw::ByteSpan(vshunt_read_buffer));
+    if (!vshunt_read_result.ok()) {
+      PW_LOG_ERROR("vshunt_read failed i=%d", i);
+      return vshunt_read_result.status();
+    }
+    int32_t vshunt_value = VoltageMeasurement(vshunt_read_result.value());
+    vshunt_measurements_[i] = vshunt_value;
+  }
+
+  // All data has been read: clear the pulse signal variable.
+  ClearValidPulse();
+
+  EndSpiTransaction();
+
+  uint32_t end_time = micros();
+  sample_read_time_[sample_read_index_] = end_time - start_time;
+  sample_read_index_ = (sample_read_index_ + 1) % sample_read_time_.size();
+
+  return pw::OkStatus();
+}
+
+Status Adc::WriteRegister(uint8_t adc_number, uint8_t adc_register,
+                          pw::ByteSpan write_buffer) {
+  uint32_t adc_address = ADCAddressWrite(adc_number, adc_register);
+
+  StartSpiTransaction();
+  ClearValidPulse();
+  WriteAddress(adc_address);
+  WriteData(write_buffer);
+  Status wait_result = WaitForFpgaIOValidPulse();
+  EndSpiTransaction();
+
+  return wait_result;
+}
+
+Status Adc::WriteRegisterAll(uint8_t adc_register, pw::ByteSpan write_buffer) {
+  uint32_t adc_address = ADCAddressWriteAll(adc_register);
+
+  StartSpiTransaction();
+  ClearValidPulse();
+  WriteAddress(adc_address);
+  WriteData(write_buffer);
+  Status wait_result = WaitForFpgaIOValidPulse();
+  EndSpiTransaction();
+
+  return wait_result;
+}
+
+pw::Result<pw::ConstByteSpan> Adc::GetADCConfiguration(uint8_t adc_number) {
+  return GetRegister(adc_number, INA229_ADC_CONFIG);
+}
+
+Status Adc::SetADCConfiguration(uint8_t adc_number, pw::ByteSpan write_buffer) {
+  return WriteRegister(adc_number, INA229_ADC_CONFIG, write_buffer);
+}
+
+pw::Result<pw::ConstByteSpan> Adc::GetShuntCalibration(uint8_t adc_number) {
+  return GetRegister(adc_number, INA229_SHUNT_CALIBRATION);
+}
+
+pw::Result<int32_t> Adc::GetShuntVoltageMeasurement(uint8_t adc_number) {
+  pw::Result<pw::ConstByteSpan> read_result =
+      GetRegister(adc_number, INA229_VSHUNT);
+  if (!read_result.ok()) {
+    return read_result.status();
+  }
+
+  int32_t value = VoltageMeasurement(read_result.value());
+  PW_LOG_INFO("ADC #%d: ShuntVoltageMeasurement = %d", adc_number, value);
+
+  return value;
+}
+
+pw::Result<int32_t> Adc::GetBusVoltageMeasurement(uint8_t adc_number) {
+  pw::Result<pw::ConstByteSpan> read_result =
+      GetRegister(adc_number, INA229_VBUS);
+  if (!read_result.ok()) {
+    return read_result.status();
+  }
+
+  int32_t value = VoltageMeasurement(read_result.value());
+  PW_LOG_INFO("ADC #%d: BusVoltageMeasurement = %d", adc_number, value);
+
+  return value;
+}
+
+int32_t Adc::VoltageMeasurement(pw::ConstByteSpan read_buffer) {
+  // Convert bits 23 to 4 to a signed integer. Bits 3 to 0 are discarded.
+  //
+  // read_buffer[0] << 12  76543210<-----------|
+  // read_buffer[1] << 4           76543210<---|
+  // read_buffer[2] >> 4                   76543210--->
+  // result                98765432109876543210|
+  //
+  return pw::bytes::SignExtend<20>((uint32_t)read_buffer[0] << 12 |
+                                   (uint32_t)read_buffer[1] << 4 |
+                                   (uint32_t)read_buffer[2] >> 4);
+}
+
+Status Adc::InitAdcs() {
+  attachInterrupt(/*pin=*/fpga_valid_, /*callback=*/&io_valid_rising_isr,
+                  /*mode=*/HIGH);
+
+  SetReadWriteMode();
+
+  PW_LOG_INFO("Init ADCs");
+
+  // Set all ADC_CONFIG registers.
+  std::array<std::byte, 2> adc_config = {
+      // [15-12] MODE=0xB:   Continuous shunt and bus voltage
+      // [8-6]   VBUSCT=0x0: 50us conversion time
+      // [5-3]   VSHCT=0x0:  50us conversion time
+      // [0-2]   AVG=0x0:    1 sample averaging count
+      std::byte(0b10110000),
+      std::byte(0b00000000),
+  };
+  WriteRegisterAll(INA229_ADC_CONFIG, adc_config);
+
+  // Set all DIAG_ALERT registers
+  std::array<std::byte, 2> diag_alert = {
+      // CNVR=1: Enablne conversion ready flag
+      std::byte(0b01000000),
+      // MEMSTAT=1: Normal operation
+      std::byte(0b00000000),
+  };
+  WriteRegisterAll(INA229_DIAG_ALERT, diag_alert);
+
+  return pw::OkStatus();
+}
+
+Status Adc::CheckAllAdcs() {
+  // Check all ADCs are reachable
+  for (int adc_number = 1; adc_number <= kTotalAdcCount; adc_number++) {
+
+    // Read and log the ADC_CONFIG register.
+    const pw::Result<pw::ConstByteSpan> adc_config_result =
+        GetADCConfiguration(adc_number);
+    if (adc_config_result.ok()) {
+      uint16_t adc_value = pw::bytes::ReadInOrder<uint16_t>(
+          pw::endian::big, adc_config_result.value().data());
+      PW_LOG_INFO("ADC #%d: ADC Config = %x", adc_number, adc_value);
+    }
+
+    // Read and log the VSHUNT register.
+    GetShuntVoltageMeasurement(adc_number);
+    // Read and log the VBUS register.
+    GetBusVoltageMeasurement(adc_number);
+  }
+
+  return pw::OkStatus();
+}
+
+} // namespace gonk::adc
diff --git a/lib/adc/public/gonk/adc.h b/lib/adc/public/gonk/adc.h
new file mode 100644
index 0000000..510b9a3
--- /dev/null
+++ b/lib/adc/public/gonk/adc.h
@@ -0,0 +1,84 @@
+#pragma once
+
+#include "SPI.h"
+#include <Arduino.h>
+#include <stdint.h>
+
+#include "pw_bytes/span.h"
+#include "pw_result/result.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+using pw::span;
+using pw::Status;
+
+namespace gonk::adc {
+
+const uint8_t kTotalAdcCount = 11;
+const uint8_t kStreamingAdcCount = 11;
+
+class Adc {
+public:
+  Adc(SPIClass &fpga_spi, uint16_t fpga_spi_baudrate, uint16_t fpga_cs_pin,
+      uint16_t fpga_mode_pin, uint16_t fpga_reset_pin, uint16_t fpga_valid_pin);
+
+  Status InitAdcs();
+  Status CheckAllAdcs();
+
+  void SetReadWriteMode();
+  void SetContinuousReadMode();
+  Status UpdateContinuousMeasurements();
+
+  // Writing and reading data over SPI.
+  void StartSpiTransaction();
+  void EndSpiTransaction();
+  void WriteAddress(uint32_t adc_address);
+  Status WriteData(pw::ByteSpan write_buffer);
+  pw::Result<pw::ConstByteSpan> ReadData(pw::ByteSpan read_buffer);
+
+  // ADC Register Manipulation.
+  pw::Result<pw::ConstByteSpan> GetRegister(uint8_t adc_number,
+                                            uint8_t adc_register);
+  Status WriteRegister(uint8_t adc_number, uint8_t adc_register,
+                       pw::ByteSpan write_buffer);
+  Status WriteRegisterAll(uint8_t adc_register, pw::ByteSpan write_buffer);
+
+  // ADC Specific functions.
+  pw::Result<pw::ConstByteSpan> GetManufacturerID(uint8_t adc_number);
+  pw::Result<pw::ConstByteSpan> GetADCConfiguration(uint8_t adc_number);
+  Status SetADCConfiguration(uint8_t adc_number, pw::ByteSpan write_buffer);
+  pw::Result<pw::ConstByteSpan> GetShuntCalibration(uint8_t adc_number);
+  pw::Result<int32_t> GetShuntVoltageMeasurement(uint8_t adc_number);
+  pw::Result<int32_t> GetBusVoltageMeasurement(uint8_t adc_number);
+
+  std::array<int32_t, kStreamingAdcCount> vbus_measurements_;
+  std::array<int32_t, kStreamingAdcCount> vshunt_measurements_;
+
+private:
+  uint16_t fpga_reset_;
+  uint16_t fpga_mode_;
+  uint16_t fpga_valid_;
+  uint16_t fpga_cs_;
+
+  SPIClass &fpga_spi_;
+  SPISettings spi_settings_;
+
+  uint8_t sample_read_index_;
+  std::array<uint32_t, 16> sample_read_time_;
+
+  Status WaitForFpgaIOValid(uint32_t timeout_ms);
+  Status WaitForFpgaIOValidPulse(uint32_t timeout_ms);
+
+  // FPGA ADC Address selection.
+  uint32_t ADCAddress(uint8_t adc_number, uint8_t adc_command, uint8_t mode);
+  uint32_t ADCAddressWrite(uint8_t adc_number, uint8_t adc_command);
+  uint32_t ADCAddressWriteAll(uint8_t adc_command);
+
+  pw::Result<pw::ConstByteSpan> GetThreeBytes();
+
+  // INA229 register value interpretation.
+  pw::Result<uint8_t> RegisterSize(uint8_t adc_register);
+  int32_t VoltageMeasurement(pw::ConstByteSpan read_buffer);
+};
+
+} // namespace gonk::adc
diff --git a/lib/pin_config/BUILD.gn b/lib/pin_config/BUILD.gn
index 42cbc01..1e3dc85 100644
--- a/lib/pin_config/BUILD.gn
+++ b/lib/pin_config/BUILD.gn
@@ -17,10 +17,7 @@
 import("$dir_pw_build/target_types.gni")
 
 config("default_config") {
-  include_dirs = [
-    "public",
-    "//third_party/stm32duino/arduino-core/libraries/SPI/src",
-  ]
+  include_dirs = [ "public" ]
 }
 
 pw_source_set("pin_config") {
@@ -29,8 +26,9 @@
 
   sources = [ "pin_config.cc" ]
 
-  deps = [
-    "$dir_pw_log",
+  deps = [ "$dir_pw_log" ]
+  public_deps = [
     "$dir_pw_third_party/arduino:arduino_core_sources",
+    "//third_party:stm32duino_spi",
   ]
 }
diff --git a/lib/pin_config/pin_config.cc b/lib/pin_config/pin_config.cc
index 9debc76..ae70ebe 100644
--- a/lib/pin_config/pin_config.cc
+++ b/lib/pin_config/pin_config.cc
@@ -3,6 +3,8 @@
 
 #include "gonk/pin_config.h"
 
+#include "stm32f7xx_hal_qspi.h"
+
 #define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
 #define PW_LOG_MODULE_NAME "PinConfig"
 
@@ -10,7 +12,9 @@
 
 namespace gonk::pin_config {
 
-PinConfig::PinConfig() { flash_spi = SPIClass(FlashMOSI, FlashMISO, FlashCLK); }
+PinConfig::PinConfig() : fpga_spi(FpgaDspiMOSI, FpgaDspiMISO, FpgaDspiCLK) {
+  flash_spi = SPIClass(FlashMOSI, FlashMISO, FlashCLK);
+}
 
 void PinConfig::Init() {
   // Set STATUS LED mode and initial state.
@@ -29,8 +33,21 @@
 
   // Set FPGA configure done signal pin as an input.
   pinMode(ICE40Done, INPUT);
+
+  // FPGA Application IO pins
+  pinMode(FpgaIoReset, OUTPUT);
+  digitalWrite(FpgaIoReset, LOW);
+
+  pinMode(FpgaIoMode, OUTPUT);
+  digitalWrite(FpgaIoMode, LOW);
+
+  pinMode(FpgaIoValid, INPUT);
+
+  InitSampleBus();
 }
 
+void PinConfig::InitSampleBus() { fpga_spi.begin(); }
+
 void PinConfig::FpgaHalt() {
   // Set FlashCS to signal the FPGA it should load the bitstream from external
   // flash.
diff --git a/lib/pin_config/public/gonk/pin_config.h b/lib/pin_config/public/gonk/pin_config.h
index c3488ec..bdb4398 100644
--- a/lib/pin_config/public/gonk/pin_config.h
+++ b/lib/pin_config/public/gonk/pin_config.h
@@ -16,6 +16,14 @@
 constexpr uint16_t FlashCLK = PB3;     // ICE_SPI_SCK
 constexpr uint16_t StatusLed = PB13;   // STATUS
 
+constexpr uint16_t FpgaDspiCS = PB9;   // was PB6;
+constexpr uint16_t FpgaDspiMOSI = PC3; // was PC10;
+constexpr uint16_t FpgaDspiMISO = PC2; // was PC9;
+constexpr uint16_t FpgaDspiCLK = PB10; // was PB2;
+constexpr uint16_t FpgaIoReset = PA0;
+constexpr uint16_t FpgaIoMode = PB11;
+constexpr uint16_t FpgaIoValid = PB6; // was PB10;
+
 class PinConfig {
 public:
   PinConfig();
@@ -24,6 +32,7 @@
   void Init();
   // Init FPGA specific pins.
   void InitFpgaPins();
+  void InitSampleBus();
 
   // Halt the FPGA with the SPI bus released so SPI flash is accessible from the
   // MCU.
@@ -34,6 +43,7 @@
   void SPIDisable();
 
   SPIClass flash_spi;
+  SPIClass fpga_spi;
 
 private:
 };
diff --git a/lib/spi_flash/BUILD.gn b/lib/spi_flash/BUILD.gn
index 902e66e..5873e4a 100644
--- a/lib/spi_flash/BUILD.gn
+++ b/lib/spi_flash/BUILD.gn
@@ -33,6 +33,9 @@
     "$dir_pw_result",
     "$dir_pw_span",
     "$dir_pw_status",
+  ]
+  public_deps = [
     "$dir_pw_third_party/arduino:arduino_core_sources",
+    "//third_party:stm32duino_spi",
   ]
 }
diff --git a/third_party/BUILD.gn b/third_party/BUILD.gn
new file mode 100644
index 0000000..1f6a568
--- /dev/null
+++ b/third_party/BUILD.gn
@@ -0,0 +1,34 @@
+# Copyright 2023 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("stm32duino_spi_config") {
+  include_dirs = [ "//third_party/stm32duino/arduino-core/libraries/SPI/src" ]
+}
+
+pw_source_set("stm32duino_spi") {
+  public_configs = [ ":stm32duino_spi_config" ]
+  public = [ "//third_party/stm32duino/arduino-core/libraries/SPI/src/SPI.h" ]
+
+  sources = [
+    "//third_party/stm32duino/arduino-core/libraries/SPI/src/SPI.cpp",
+    "//third_party/stm32duino/arduino-core/libraries/SPI/src/utility/spi_com.c",
+    "//third_party/stm32duino/arduino-core/libraries/SPI/src/utility/spi_com.h",
+  ]
+
+  public_deps = [ "$dir_pw_third_party/arduino:arduino_core_sources" ]
+}