Add Pico/STM32F329I pw_spi implementations

Add two pw_spi target implementations:

1. pw_spi_pico
2. pw_spi_stm32f429i_disc1_stm32cube

Change-Id: I95c84c7c45693e2311e21f72d7c201eba7963784
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/experimental/+/113077
Commit-Queue: Chris Mumford <cmumford@google.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
diff --git a/build_overrides/pigweed.gni b/build_overrides/pigweed.gni
index 74646ba..fcb35b4 100644
--- a/build_overrides/pigweed.gni
+++ b/build_overrides/pigweed.gni
@@ -52,6 +52,9 @@
       get_path_info("//pw_graphics/pw_display_teensy_ili9341", "abspath")
   dir_pw_draw = get_path_info("//pw_graphics/pw_draw", "abspath")
   dir_pw_framebuffer = get_path_info("//pw_graphics/pw_framebuffer", "abspath")
+  dir_pw_spi_pico = get_path_info("//pw_spi_pico", "abspath")
+  dir_pw_spi_stm32f429i_disc1_stm32cube =
+      get_path_info("//pw_spi_stm32f429i_disc1_stm32cube", "abspath")
   dir_pw_spin_delay = get_path_info("//pw_spin_delay", "abspath")
   dir_pw_spin_delay_arduino =
       get_path_info("//pw_spin_delay_arduino", "abspath")
diff --git a/pw_spi_pico/BUILD.gn b/pw_spi_pico/BUILD.gn
new file mode 100644
index 0000000..7b3d67a
--- /dev/null
+++ b/pw_spi_pico/BUILD.gn
@@ -0,0 +1,44 @@
+# Copyright 2022 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/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_spi_pico") {
+  public_configs = [ ":default_config" ]
+  public = [
+    "public/pw_spi_pico/chip_selector.h",
+    "public/pw_spi_pico/initiator.h",
+  ]
+  public_deps = [ "$dir_pw_status" ]
+  deps = [
+    "$PICO_ROOT/src/common/pico_base",
+    "$PICO_ROOT/src/common/pico_stdlib",
+    "$PICO_ROOT/src/rp2_common/hardware_spi",
+    "$dir_pw_digital_io",
+    "$dir_pw_log",
+    "$dir_pw_spi:chip_selector",
+    "$dir_pw_spi:initiator",
+  ]
+  sources = [
+    "chip_selector.cc",
+    "initiator.cc",
+  ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/pw_spi_pico/chip_selector.cc b/pw_spi_pico/chip_selector.cc
new file mode 100644
index 0000000..54d9552
--- /dev/null
+++ b/pw_spi_pico/chip_selector.cc
@@ -0,0 +1,28 @@
+// Copyright 2022 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_spi_pico/chip_selector.h"
+
+using pw::digital_io::State;
+
+namespace pw::spi {
+
+PicoChipSelector::PicoChipSelector(pw::digital_io::DigitalOut& cs_pin)
+    : cs_pin_(cs_pin) {}
+
+Status PicoChipSelector::SetActive(bool active) {
+  return cs_pin_.SetState(active ? State::kInactive : State::kActive);
+}
+
+}  // namespace pw::spi
diff --git a/pw_spi_pico/initiator.cc b/pw_spi_pico/initiator.cc
new file mode 100644
index 0000000..cff4e5f
--- /dev/null
+++ b/pw_spi_pico/initiator.cc
@@ -0,0 +1,134 @@
+// Copyright 2022 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_spi_pico/initiator.h"
+
+#include <algorithm>
+
+#include "hardware/spi.h"
+#include "pico/stdlib.h"
+#include "pw_assert/check.h"
+#include "pw_log/log.h"
+#include "pw_status/try.h"
+
+namespace pw::spi {
+
+namespace {
+
+constexpr spi_order_t GetBitOrder(BitOrder bit_order) {
+  switch (bit_order) {
+    case BitOrder::kLsbFirst:
+      return SPI_LSB_FIRST;
+    case BitOrder::kMsbFirst:
+      return SPI_MSB_FIRST;
+  }
+  PW_CRASH("Unknown bit order");
+  return SPI_LSB_FIRST;
+}
+
+constexpr spi_cpha_t GetPhase(ClockPhase phase) {
+  switch (phase) {
+    case ClockPhase::kFallingEdge:
+      return SPI_CPHA_1;
+    case ClockPhase::kRisingEdge:
+      return SPI_CPHA_0;
+  }
+  PW_CRASH("Unknown phase");
+  return SPI_CPHA_0;
+}
+
+constexpr spi_cpol_t GetPolarity(ClockPolarity polarity) {
+  switch (polarity) {
+    case ClockPolarity::kActiveHigh:
+      return SPI_CPOL_1;
+    case ClockPolarity::kActiveLow:
+      return SPI_CPOL_0;
+  }
+  PW_CRASH("Unknown polarity");
+  return SPI_CPOL_0;
+}
+
+}  // namespace
+
+PicoInitiator::PicoInitiator(spi_inst_t* spi, uint32_t baud_rate)
+    : spi_(spi),
+      baud_rate_(baud_rate),
+      config_{
+          .polarity = ClockPolarity::kActiveHigh,
+          .phase = ClockPhase::kRisingEdge,
+          .bits_per_word = BitsPerWord(8),
+          .bit_order = BitOrder::kMsbFirst,
+      },
+      desired_bits_per_word_(8) {}
+
+void PicoInitiator::SetOverrideBitsPerWord(BitsPerWord bits_per_word) {
+  // TODO(b/251033990): Remove once changing SPI device config is added.
+  desired_bits_per_word_ = bits_per_word;
+  override_bits_per_word_ = true;
+}
+
+Status PicoInitiator::LazyInit() {
+  // Already initialized - nothing to do.
+  // The Pico SDK needs to call spi_init() earlier so that the
+  // various GPIO pins (MISO, etc.) can be assigned to the SPI
+  // bus.
+  return OkStatus();
+}
+
+Status PicoInitiator::Configure(const Config& config) {
+  config_ = config;
+  // TODO(b/251033990): Remove once changing SPI device config is added.
+  if (override_bits_per_word_) {
+    config_.bits_per_word = desired_bits_per_word_;
+  }
+  spi_set_format(spi_,
+                 config_.bits_per_word(),
+                 GetPolarity(config_.polarity),
+                 GetPhase(config_.phase),
+                 GetBitOrder(config_.bit_order));
+
+  return OkStatus();
+}
+
+Status PicoInitiator::WriteRead(ConstByteSpan write_buffer,
+                                ByteSpan read_buffer) {
+  PW_TRY(LazyInit());
+
+  if (!write_buffer.empty()) {
+    if (!read_buffer.empty()) {
+      PW_CRASH("Not implemented");
+    } else {
+      if (config_.bits_per_word() == 16) {
+        spi_write16_blocking(
+            spi_,
+            reinterpret_cast<const uint16_t*>(write_buffer.data()),
+            write_buffer.size());
+      } else {
+        spi_write_blocking(
+            spi_,
+            reinterpret_cast<const uint8_t*>(write_buffer.data()),
+            write_buffer.size());
+      }
+    }
+  } else {
+    spi_read_blocking(spi_,
+                      /*repeated_tx_data=*/0,
+                      reinterpret_cast<uint8_t*>(read_buffer.data()),
+                      read_buffer.size());
+  }
+
+  return OkStatus();
+}
+
+}  // namespace pw::spi
diff --git a/pw_spi_pico/public/pw_spi_pico/chip_selector.h b/pw_spi_pico/public/pw_spi_pico/chip_selector.h
new file mode 100644
index 0000000..d06f3ca
--- /dev/null
+++ b/pw_spi_pico/public/pw_spi_pico/chip_selector.h
@@ -0,0 +1,34 @@
+// Copyright 2022 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_digital_io/digital_io.h"
+#include "pw_spi/chip_selector.h"
+
+namespace pw::spi {
+
+// Pico SDK implementation of SPI ChipSelector.
+class PicoChipSelector : public ChipSelector {
+ public:
+  PicoChipSelector(pw::digital_io::DigitalOut& cs_pin);
+
+  // Implements pw::spi::ChipSelector:
+  Status SetActive(bool active) override;
+
+ private:
+  pw::digital_io::DigitalOut& cs_pin_;
+};
+
+}  // namespace pw::spi
diff --git a/pw_spi_pico/public/pw_spi_pico/initiator.h b/pw_spi_pico/public/pw_spi_pico/initiator.h
new file mode 100644
index 0000000..7781c0d
--- /dev/null
+++ b/pw_spi_pico/public/pw_spi_pico/initiator.h
@@ -0,0 +1,46 @@
+// Copyright 2022 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 <cstdint>
+
+#include "hardware/spi.h"
+#include "pw_spi/initiator.h"
+
+namespace pw::spi {
+
+// Pico SDK userspace implementation of the SPI Initiator
+class PicoInitiator : public Initiator {
+ public:
+  PicoInitiator(spi_inst_t* spi, uint32_t baud_rate);
+
+  void SetOverrideBitsPerWord(BitsPerWord bits_per_word);
+
+  // Implements pw::spi::Initiator:
+  Status Configure(const Config& config) override;
+  Status WriteRead(ConstByteSpan write_buffer, ByteSpan read_buffer) override;
+
+ private:
+  Status LazyInit();
+
+  spi_inst_t* spi_;
+  uint32_t baud_rate_;
+  Status init_status_;  // The saved LazyInit() status.
+  Config config_;
+  BitsPerWord desired_bits_per_word_;
+  bool override_bits_per_word_ = false;
+};
+
+}  // namespace pw::spi
diff --git a/pw_spi_stm32f429i_disc1_stm32cube/BUILD.gn b/pw_spi_stm32f429i_disc1_stm32cube/BUILD.gn
new file mode 100644
index 0000000..0994580
--- /dev/null
+++ b/pw_spi_stm32f429i_disc1_stm32cube/BUILD.gn
@@ -0,0 +1,43 @@
+# Copyright 2022 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")
+import("$dir_pw_third_party/stm32cube/stm32cube.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_spi_stm32f429i_disc1_stm32cube") {
+  public_configs = [ ":default_config" ]
+  public = [
+    "public/pw_spi_stm32f429i_disc1_stm32cube/chip_selector.h",
+    "public/pw_spi_stm32f429i_disc1_stm32cube/initiator.h",
+  ]
+  public_deps = [ "$dir_pw_status" ]
+  deps = [
+    "$dir_pw_digital_io",
+    "$dir_pw_log",
+    "$dir_pw_spi:chip_selector",
+    "$dir_pw_spi:initiator",
+    "$dir_pw_third_party/stm32cube",
+  ]
+  sources = [
+    "chip_selector.cc",
+    "initiator.cc",
+  ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/pw_spi_stm32f429i_disc1_stm32cube/chip_selector.cc b/pw_spi_stm32f429i_disc1_stm32cube/chip_selector.cc
new file mode 100644
index 0000000..e3075b6
--- /dev/null
+++ b/pw_spi_stm32f429i_disc1_stm32cube/chip_selector.cc
@@ -0,0 +1,30 @@
+// Copyright 2022 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_spi_stm32f429i_disc1_stm32cube/chip_selector.h"
+
+#include "stm32f4xx_hal.h"
+
+using pw::digital_io::State;
+
+namespace pw::spi {
+
+Stm32CubeChipSelector::Stm32CubeChipSelector(pw::digital_io::DigitalOut& cs_pin)
+    : cs_pin_(cs_pin) {}
+
+Status Stm32CubeChipSelector::SetActive(bool active) {
+  return cs_pin_.SetState(active ? State::kInactive : State::kActive);
+}
+
+}  // namespace pw::spi
\ No newline at end of file
diff --git a/pw_spi_stm32f429i_disc1_stm32cube/initiator.cc b/pw_spi_stm32f429i_disc1_stm32cube/initiator.cc
new file mode 100644
index 0000000..8776158
--- /dev/null
+++ b/pw_spi_stm32f429i_disc1_stm32cube/initiator.cc
@@ -0,0 +1,211 @@
+// Copyright 2022 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_spi_stm32f429i_disc1_stm32cube/initiator.h"
+
+#include <algorithm>
+
+#include "pw_log/log.h"
+#include "pw_status/try.h"
+#include "stm32cube/stm32cube.h"
+#include "stm32f4xx_hal.h"
+#include "stm32f4xx_hal_spi.h"
+
+namespace pw::spi {
+
+namespace {
+
+constexpr uint32_t kTimeout = 10000;
+
+constexpr Status ConvertStatus(HAL_StatusTypeDef status) {
+  switch (status) {
+    case HAL_OK:
+      return OkStatus();
+    case HAL_ERROR:
+      return Status::Internal();
+    case HAL_BUSY:
+      return Status::Unavailable();
+    case HAL_TIMEOUT:
+      return Status::DeadlineExceeded();
+  }
+  return Status::NotFound();  // Unreachable.
+}
+
+uint32_t GetDataSize(BitsPerWord bits_per_word) {
+  if (bits_per_word() == 8) {
+    return SPI_DATASIZE_8BIT;
+  } else if (bits_per_word() == 16) {
+    return SPI_DATASIZE_16BIT;
+  }
+  PW_ASSERT(false);
+  return SPI_DATASIZE_8BIT;
+}
+
+constexpr uint32_t GetBitOrder(BitOrder bit_order) {
+  switch (bit_order) {
+    case BitOrder::kLsbFirst:
+      return SPI_FIRSTBIT_LSB;
+    case BitOrder::kMsbFirst:
+      return SPI_FIRSTBIT_MSB;
+  }
+  PW_ASSERT(false);
+  return SPI_FIRSTBIT_MSB;
+}
+
+constexpr uint32_t GetPhase(ClockPhase phase) {
+  switch (phase) {
+    case ClockPhase::kFallingEdge:
+      return SPI_PHASE_1EDGE;
+    case ClockPhase::kRisingEdge:
+      return SPI_PHASE_2EDGE;
+  }
+  PW_ASSERT(false);
+  return SPI_PHASE_1EDGE;
+}
+
+constexpr uint32_t GetPolarity(ClockPolarity polarity) {
+  switch (polarity) {
+    case ClockPolarity::kActiveHigh:
+      return SPI_POLARITY_HIGH;
+    case ClockPolarity::kActiveLow:
+      return SPI_POLARITY_LOW;
+  }
+  PW_ASSERT(false);
+  return SPI_POLARITY_HIGH;
+}
+
+}  // namespace
+
+// A collection of instance variables isolated into a separate structure
+// so that clients of Stm32CubeInitiator aren't forced to have a compile-time
+// dependency on the STM32 header files.
+struct Stm32CubeInitiator::PrivateInstanceData {
+  bool initialized = false;
+  Status init_status;  // The saved LazyInit() status.
+  Config config_;
+  BitsPerWord desired_bits_per_word_;
+  bool override_bits_per_word_ = false;
+  SPI_HandleTypeDef spi_handle;
+
+  PrivateInstanceData()
+      : desired_bits_per_word_(8),
+        config_{
+            .polarity = ClockPolarity::kActiveHigh,
+            .phase = ClockPhase::kRisingEdge,
+            .bits_per_word = desired_bits_per_word_,
+            .bit_order = BitOrder::kMsbFirst,
+        },
+        spi_handle {
+    .Instance = SPI5, .Init {
+      .Mode = SPI_MODE_MASTER, .Direction = SPI_DIRECTION_2LINES,
+      .DataSize = SPI_DATASIZE_8BIT, .CLKPolarity = SPI_POLARITY_LOW,
+      .CLKPhase = SPI_PHASE_1EDGE, .NSS = SPI_NSS_SOFT,
+      .BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2,
+      .FirstBit = SPI_FIRSTBIT_MSB, .TIMode = SPI_TIMODE_DISABLE,
+      .CRCCalculation = SPI_CRCCALCULATION_DISABLE, .CRCPolynomial = 7
+    }
+  }
+  {}
+
+  Status InitSpi() {
+    auto s = HAL_SPI_Init(&spi_handle);
+    auto status = ConvertStatus(s);
+    PW_LOG_INFO("HAL_SPI_Init =>: %s", status.str());
+    return status;
+  }
+};
+
+Stm32CubeInitiator::Stm32CubeInitiator()
+    : instance_data_(new PrivateInstanceData) {}
+
+Stm32CubeInitiator::~Stm32CubeInitiator() { delete instance_data_; }
+
+Status Stm32CubeInitiator::LazyInit() {
+  if (instance_data_->initialized)
+    return instance_data_->init_status;
+  instance_data_->init_status = instance_data_->InitSpi();
+  instance_data_->initialized = true;
+  PW_LOG_INFO("Stm32CubeInitiator::LazyInit: %s",
+              instance_data_->init_status.str());
+  return instance_data_->init_status;
+}
+
+void Stm32CubeInitiator::SetOverrideBitsPerWord(BitsPerWord bits_per_word) {
+  // TODO(b/251033990): Remove once changing SPI device config is added.
+  instance_data_->desired_bits_per_word_ = bits_per_word;
+  instance_data_->override_bits_per_word_ = true;
+  instance_data_->initialized = false;
+}
+
+Status Stm32CubeInitiator::Configure(const Config& config) {
+  instance_data_->config_ = config;
+  // TODO(b/251033990): Remove once changing SPI device config is added.
+  if (instance_data_->override_bits_per_word_) {
+    instance_data_->config_.bits_per_word =
+        instance_data_->desired_bits_per_word_;
+  }
+  instance_data_->spi_handle.Init.DataSize =
+      GetDataSize(instance_data_->config_.bits_per_word);
+  instance_data_->spi_handle.Init.FirstBit =
+      GetBitOrder(instance_data_->config_.bit_order);
+  instance_data_->spi_handle.Init.CLKPhase =
+      GetPhase(instance_data_->config_.phase);
+  instance_data_->spi_handle.Init.CLKPolarity =
+      GetPolarity(instance_data_->config_.polarity);
+  PW_TRY(LazyInit());
+
+  return OkStatus();
+}
+
+Status Stm32CubeInitiator::WriteRead(ConstByteSpan write_buffer,
+                                     ByteSpan read_buffer) {
+  PW_TRY(LazyInit());
+
+  HAL_StatusTypeDef status;
+
+  if (!write_buffer.empty()) {
+    if (!read_buffer.empty()) {
+      // TODO(cmumford): Not yet conforming to the WriteRead contract.
+      uint16_t size = std::min(write_buffer.size(), read_buffer.size());
+      status = HAL_SPI_TransmitReceive(
+          &instance_data_->spi_handle,
+          reinterpret_cast<uint8_t*>(
+              const_cast<std::byte*>(write_buffer.data())),
+          reinterpret_cast<uint8_t*>(read_buffer.data()),
+          size,
+          kTimeout);
+    } else {
+      status =
+          HAL_SPI_Transmit(&instance_data_->spi_handle,
+                           reinterpret_cast<uint8_t*>(
+                               const_cast<std::byte*>(write_buffer.data())),
+                           write_buffer.size(),
+                           kTimeout);
+      if (status != HAL_OK) {
+        PW_LOG_ERROR("Stm32CubeInitiator::WriteRead: write:%ld B, s:%s",
+                     write_buffer.size(),
+                     ConvertStatus(status).str());
+      }
+    }
+  } else {
+    status = HAL_SPI_Receive(&instance_data_->spi_handle,
+                             reinterpret_cast<uint8_t*>(read_buffer.data()),
+                             read_buffer.size(),
+                             kTimeout);
+  }
+
+  return ConvertStatus(status);
+}
+
+}  // namespace pw::spi
diff --git a/pw_spi_stm32f429i_disc1_stm32cube/public/pw_spi_stm32f429i_disc1_stm32cube/chip_selector.h b/pw_spi_stm32f429i_disc1_stm32cube/public/pw_spi_stm32f429i_disc1_stm32cube/chip_selector.h
new file mode 100644
index 0000000..52abba3
--- /dev/null
+++ b/pw_spi_stm32f429i_disc1_stm32cube/public/pw_spi_stm32f429i_disc1_stm32cube/chip_selector.h
@@ -0,0 +1,34 @@
+// Copyright 2022 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_digital_io/digital_io.h"
+#include "pw_spi/chip_selector.h"
+
+namespace pw::spi {
+
+// STM32 implementation of SPI ChipSelector
+class Stm32CubeChipSelector : public ChipSelector {
+ public:
+  Stm32CubeChipSelector(pw::digital_io::DigitalOut& cs_pin);
+
+  // Implements pw::spi::ChipSelector:
+  Status SetActive(bool active) override;
+
+ private:
+  pw::digital_io::DigitalOut& cs_pin_;
+};
+
+}  // namespace pw::spi
diff --git a/pw_spi_stm32f429i_disc1_stm32cube/public/pw_spi_stm32f429i_disc1_stm32cube/initiator.h b/pw_spi_stm32f429i_disc1_stm32cube/public/pw_spi_stm32f429i_disc1_stm32cube/initiator.h
new file mode 100644
index 0000000..04e2603
--- /dev/null
+++ b/pw_spi_stm32f429i_disc1_stm32cube/public/pw_spi_stm32f429i_disc1_stm32cube/initiator.h
@@ -0,0 +1,40 @@
+// Copyright 2022 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_spi/initiator.h"
+
+namespace pw::spi {
+
+// STM32 userspace implementation of the SPI Initiator
+class Stm32CubeInitiator : public Initiator {
+ public:
+  Stm32CubeInitiator();
+  ~Stm32CubeInitiator();
+
+  void SetOverrideBitsPerWord(BitsPerWord bits_per_word);
+
+  // Implements pw::spi::Initiator
+  Status Configure(const Config& config) override;
+  Status WriteRead(ConstByteSpan write_buffer, ByteSpan read_buffer) override;
+
+ private:
+  struct PrivateInstanceData;
+  Status LazyInit();
+
+  PrivateInstanceData* instance_data_;
+};
+
+}  // namespace pw::spi