Add STM32F769I-DISC0 targets

Add build targets for the STM32F769I-DISC0(1). This device has a
display, but this change does not add a display driver so
blinky and strings are the only applications currently built
for this target.

(1) https://www.st.com/en/evaluation-tools/32f769idiscovery.html

Change-Id: Ib4e93171164d546449a4615066aeb35273b3f006
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/experimental/+/118550
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
Commit-Queue: Chris Mumford <cmumford@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 0663b34..424339c 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -93,6 +93,16 @@
   ]
 }
 
+# stm32f769i_disc0 specific targets.
+group("stm32f769i_disc0") {
+  _default_toolchain = "//targets/stm32f769i_disc0:stm32f769i_disc0_debug"
+  _testing_toolchain = "${_default_toolchain}_tests"
+  deps = [
+    # ":app(${_default_toolchain})",
+    ":tests(${_testing_toolchain})",
+  ]
+}
+
 # This group is built during bootstrap to setup the interactive Python
 # environment.
 pw_python_group("python") {
@@ -114,6 +124,7 @@
   "$dir_pw_env_setup:core_pigweed_python_packages",
   "$dir_pigweed/targets/lm3s6965evb_qemu/py",
   "$dir_pigweed/targets/stm32f429i_disc1/py",
+  "//targets/stm32f769i_disc0/py",
 ]
 
 _all_python_packages =
@@ -211,6 +222,14 @@
     "//applications/strings:all(//targets/stm32f429i_disc1:stm32f429i_disc1_debug)",
   ]
 
+  # STMicroelectronics STM32F769I-DISC0 applications steps.
+  deps += [
+    ":applications_tests(//targets/stm32f769i_disc0:stm32f769i_disc0_debug_tests)",
+    "//applications/blinky:blinky(//targets/stm32f769i_disc0:stm32f769i_disc0_debug)",
+    "//applications/rpc:all(//targets/stm32f769i_disc0:stm32f769i_disc0_debug)",
+    "//applications/strings:all(//targets/stm32f769i_disc0:stm32f769i_disc0_debug)",
+  ]
+
   if (dir_pw_third_party_stm32cube_f4 != "") {
     # STMicroelectronics STM32F429I-DISC1 STM32Cube applications steps.
     deps += [
@@ -230,6 +249,14 @@
     deps += [ "//applications/blinky:blinky(//targets/stm32f207zg-nucleo:stm32f207zg_nucleo_debug)" ]
   }
 
+  if (dir_pw_third_party_stm32cube_f4 != "") {
+    # STMicroelectronics STM32F769I-DISC0 STM32Cube applications steps.
+    deps += [
+      ":applications_tests(//targets/stm32f769i_disc0_stm32cube:stm32f769i_disc0_stm32cube_debug)",
+      "//applications/blinky:blinky(//targets/stm32f769i_disc0_stm32cube:stm32f769i_disc0_stm32cube_debug)",
+    ]
+  }
+
   if (dir_pw_third_party_stm32cube_l5 != "") {
     deps += [ "//applications/blinky:blinky(//targets/stm32l552ze-nucleo:stm32l552ze_nucleo_debug)" ]
   }
diff --git a/README.md b/README.md
index a62e396..3088382 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,29 @@
 openocd -f third_party/pigweed/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg -c "program out/stm32f429i_disc1_stm32cube_debug/obj/applications/terminal_display/bin/terminal_demo.elf verify reset exit"
 ```
 
+#### **[STM32F769-DISC0](https://www.st.com/en/evaluation-tools/32f769idiscovery.html)**
+
+**First time setup:**
+```
+pw package install stm32cube_f7
+```
+
+**Compile:**
+
+```sh
+gn gen out --export-compile-commands --args="
+  dir_pw_third_party_stm32cube_f7=\"//environment/packages/stm32cube_f7\"
+"
+ninja -C out
+```
+
+**Flash:**
+
+```
+openocd -f targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/openocd_stm32f7xx.cfg \
+  -c "program out/stm32f769i_disc0_debug/obj/applications/blinky/bin/blinky.elf verify reset exit"
+```
+
 #### **Linux, Windows or Mac**
 
 **Compile:**
diff --git a/build_overrides/pigweed.gni b/build_overrides/pigweed.gni
index 9b9cb9f..03cc222 100644
--- a/build_overrides/pigweed.gni
+++ b/build_overrides/pigweed.gni
@@ -40,6 +40,9 @@
   dir_pw_board_led_stm32f429i_disc1 =
       get_path_info("$dir_pigweed_experimental/pw_board_led_stm32f429i_disc1",
                     "abspath")
+  dir_pw_board_led_stm32f769i_disc0 =
+      get_path_info("$dir_pigweed_experimental/pw_board_led_stm32f769i_disc0",
+                    "abspath")
   dir_pw_board_led_stm32cube =
       get_path_info("$dir_pigweed_experimental/pw_board_led_stm32cube",
                     "abspath")
@@ -119,6 +122,12 @@
   dir_pw_spin_delay_stm32cube =
       get_path_info("$dir_pigweed_experimental/pw_spin_delay_stm32cube",
                     "abspath")
+  dir_pw_spin_delay_stm32f769i_disc0 =
+      get_path_info("$dir_pigweed_experimental/pw_spin_delay_stm32f769i_disc0",
+                    "abspath")
+  dir_pw_sys_io_baremetal_stm32f769 =
+      get_path_info("$dir_pigweed_experimental/pw_sys_io_baremetal_stm32f769",
+                    "abspath")
   dir_pw_touchscreen =
       get_path_info("$dir_pigweed_experimental/pw_graphics/pw_touchscreen",
                     "abspath")
diff --git a/pw_board_led_stm32f769i_disc0/BUILD.gn b/pw_board_led_stm32f769i_disc0/BUILD.gn
new file mode 100644
index 0000000..1d31ea4
--- /dev/null
+++ b/pw_board_led_stm32f769i_disc0/BUILD.gn
@@ -0,0 +1,25 @@
+# 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")
+
+pw_source_set("pw_board_led_stm32f769i_disc0") {
+  deps = [
+    "$dir_pw_board_led:pw_board_led.facade",
+    dir_pw_preprocessor,
+  ]
+  sources = [ "led.cc" ]
+}
diff --git a/pw_board_led_stm32f769i_disc0/led.cc b/pw_board_led_stm32f769i_disc0/led.cc
new file mode 100644
index 0000000..47658fb
--- /dev/null
+++ b/pw_board_led_stm32f769i_disc0/led.cc
@@ -0,0 +1,108 @@
+// 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.
+
+#include "pw_board_led/led.h"
+
+#include <cinttypes>
+
+#include "pw_preprocessor/compiler.h"
+
+namespace pw::board_led {
+namespace {
+
+// Base address for everything peripheral-related on the STM32F7xx.
+constexpr uint32_t kPeripheralBaseAddr = 0x40000000u;
+// Base address for everything AHB1-related on the STM32F7xx.
+constexpr uint32_t kAhb1PeripheralBase = kPeripheralBaseAddr + 0x00020000u;
+
+constexpr uint32_t RCC_AHB1ENR_GPIOJEN_Pos = 9;
+
+// Reset/clock configuration block (RCC).
+// `reserved` fields are unimplemented features, and are present to ensure
+// proper alignment of registers that are in use.
+PW_PACKED(struct) RccBlock {
+  uint32_t reserved1[12];
+  uint32_t ahb1_config;
+  uint32_t reserved2[4];
+  uint32_t apb2_config;
+};
+
+// GPIO register block definition.
+PW_PACKED(struct) GpioBlock {
+  uint32_t modes;
+  uint32_t out_type;
+  uint32_t out_speed;
+  uint32_t pull_up_down;
+  uint32_t input_data;
+  uint32_t output_data;
+  uint32_t gpio_bit_set;
+  uint32_t port_config_lock;
+  uint32_t alt_low;
+  uint32_t alt_high;
+};
+
+// Constants related to GPIO mode register masks.
+constexpr uint32_t kGpioPortModeMask = 0x3u;
+constexpr uint32_t kGpio13PortModePos = 26;
+constexpr uint32_t kGpioPortModeOutput = 1;
+
+// Constants related to GPIO output mode register masks.
+constexpr uint32_t kGpioOutputModeMask = 0x1u;
+constexpr uint32_t kGpio13OutputModePos = 13;
+constexpr uint32_t kGpioOutputModePushPull = 0;
+
+constexpr uint32_t kGpio13BitSetHigh = 0x1u << 13;
+constexpr uint32_t kGpio13BitSetLow = kGpio13BitSetHigh << 16;
+
+// Mask for ahb1_config (AHB1ENR) to enable the "J" GPIO pins.
+constexpr uint32_t kGpioJEnable = 0x1u << RCC_AHB1ENR_GPIOJEN_Pos;
+
+// Declare a reference to the memory mapped RCC block.
+volatile RccBlock& platform_rcc =
+    *reinterpret_cast<volatile RccBlock*>(kAhb1PeripheralBase + 0x3800U);
+
+// Declare a reference to the 'J' GPIO memory mapped block.
+volatile GpioBlock& gpio_j =
+    *reinterpret_cast<volatile GpioBlock*>(kAhb1PeripheralBase + 0x2400U);
+
+}  // namespace
+
+void Init() {
+  // Enable 'J' GIPO clocks.
+  platform_rcc.ahb1_config |= kGpioJEnable;
+
+  // Enable Pin 13 in output mode.
+  gpio_j.modes = (gpio_j.modes & ~(kGpioPortModeMask << kGpio13PortModePos)) |
+                 (kGpioPortModeOutput << kGpio13PortModePos);
+
+  // Enable Pin 13 in output mode "push pull"
+  gpio_j.out_type =
+      (gpio_j.out_type & ~(kGpioOutputModeMask << kGpio13OutputModePos)) |
+      (kGpioOutputModePushPull << kGpio13OutputModePos);
+}
+
+void TurnOff() { gpio_j.gpio_bit_set = kGpio13BitSetLow; }
+
+void TurnOn() { gpio_j.gpio_bit_set = kGpio13BitSetHigh; }
+
+void Toggle() {
+  // Check if the LED is on. If so, turn it off.
+  if (gpio_j.output_data & kGpio13BitSetHigh) {
+    TurnOff();
+  } else {
+    TurnOn();
+  }
+}
+
+}  // namespace pw::board_led
diff --git a/pw_spin_delay_stm32f769i_disc0/BUILD.gn b/pw_spin_delay_stm32f769i_disc0/BUILD.gn
new file mode 100644
index 0000000..2daffab
--- /dev/null
+++ b/pw_spin_delay_stm32f769i_disc0/BUILD.gn
@@ -0,0 +1,25 @@
+# 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")
+
+pw_source_set("pw_spin_delay_stm32f769i_disc0") {
+  deps = [
+    "$dir_pw_spin_delay:pw_spin_delay.facade",
+    dir_pw_preprocessor,
+  ]
+  sources = [ "delay.cc" ]
+}
diff --git a/pw_spin_delay_stm32f769i_disc0/delay.cc b/pw_spin_delay_stm32f769i_disc0/delay.cc
new file mode 100644
index 0000000..365070b
--- /dev/null
+++ b/pw_spin_delay_stm32f769i_disc0/delay.cc
@@ -0,0 +1,57 @@
+// 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.
+
+#include "pw_spin_delay/delay.h"
+
+#include <cstddef>
+#include <cstdint>
+
+namespace pw::spin_delay {
+
+// !!!WARNING!!!: This delay is not truly accurate! It's mostly just a rough
+// estimate! Also, it only works in a baremetal context with no interrupts
+// getting in the way or threads getting CPU time.
+//
+// TODO(amontanez): Replace this implementation with a loop checking a
+// pw_chrono clock.
+void WaitMillis(size_t delay_ms) {
+  // Default core clock. This is technically not a constant, but since Pigweed
+  // doesn't change the system clock a constant will suffice.
+  constexpr uint32_t kSystemCoreClock = 16000000;
+  constexpr uint32_t kCyclesPerMs = kSystemCoreClock / 1000;
+
+  // This is not totally accurate, but is close enough.
+  for (size_t i = 0; i < delay_ms; i++) {
+    // Do a 4 instruction loop enough times to be running for a millisecond.
+    // This is set up with assembly rather than a regular loop to make the
+    // instruction count predictable (no compiler variation).
+    uint32_t cycles = kCyclesPerMs;
+    asm volatile(
+        " mov r0, %[cycles] \n"
+        " mov r1, #0        \n"
+        "loop:              \n"
+        " cmp r1, r0        \n"
+        " itt lt            \n"
+        " addlt r1, r1, #4  \n"
+        " blt loop          \n"
+        // clang-format off
+        : /*output=*/
+        : /*input=*/[cycles]"r"(cycles)
+        : /*clobbers=*/"r0", "r1"
+        // clang-format on
+    );
+  }
+}
+
+}  // namespace pw::spin_delay
diff --git a/pw_sys_io_baremetal_stm32f769/BUILD.bazel b/pw_sys_io_baremetal_stm32f769/BUILD.bazel
new file mode 100644
index 0000000..e1f4fed
--- /dev/null
+++ b/pw_sys_io_baremetal_stm32f769/BUILD.bazel
@@ -0,0 +1,38 @@
+# 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.
+
+load(
+    "//pw_build:pigweed.bzl",
+    "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+    name = "pw_sys_io_baremetal_stm32f769",
+    srcs = ["sys_io_baremetal.cc"],
+    hdrs = ["public/pw_sys_io_baremetal_stm32f769/init.h"],
+    includes = ["public"],
+    target_compatible_with = [
+        "//pw_build/constraints/chipset:stm32f769",
+        "@platforms//os:none",
+    ],
+    deps = [
+        "//pw_preprocessor",
+        "//pw_sys_io:default_putget_bytes",
+        "//pw_sys_io:facade",
+    ],
+)
diff --git a/pw_sys_io_baremetal_stm32f769/BUILD.gn b/pw_sys_io_baremetal_stm32f769/BUILD.gn
new file mode 100644
index 0000000..9dc27ee
--- /dev/null
+++ b/pw_sys_io_baremetal_stm32f769/BUILD.gn
@@ -0,0 +1,44 @@
+# 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")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_sys_io_baremetal_stm32f769") {
+  public_configs = [ ":default_config" ]
+  public = [ "public/pw_sys_io_baremetal_stm32f769/init.h" ]
+  public_deps = [
+    "$dir_pw_boot_cortex_m:armv7m",
+    "$dir_pw_preprocessor",
+  ]
+  deps = [
+    "$dir_pw_sys_io:default_putget_bytes",
+    "$dir_pw_sys_io:facade",
+  ]
+  sources = [ "sys_io_baremetal.cc" ]
+}
+
+pw_doc_group("docs") {
+  sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io_baremetal_stm32f769/OWNERS b/pw_sys_io_baremetal_stm32f769/OWNERS
new file mode 100644
index 0000000..61bb96c
--- /dev/null
+++ b/pw_sys_io_baremetal_stm32f769/OWNERS
@@ -0,0 +1 @@
+cmumford@google.com
diff --git a/pw_sys_io_baremetal_stm32f769/docs.rst b/pw_sys_io_baremetal_stm32f769/docs.rst
new file mode 100644
index 0000000..40a57cd
--- /dev/null
+++ b/pw_sys_io_baremetal_stm32f769/docs.rst
@@ -0,0 +1,61 @@
+.. _module-pw_sys_io_baremetal_stm32f769:
+
+-----------------------------
+pw_sys_io_baremetal_stm32f769
+-----------------------------
+
+``pw_sys_io_baremetal_stm32f769`` implements the ``pw_sys_io`` facade over
+UART.
+
+The STM32F769 baremetal sys IO backend provides device startup code and a UART
+driver layer that allows applications built against the ``pw_sys_io`` interface
+to run on a STM32F769 chip and do simple input/output via UART. The code is
+optimized for the STM32F769I-DISC0, using USART1 (which is connected to the
+virtual COM port on the embedded ST-LINKv2 chip). However, this should work with
+all STM32F769 variations (and even some STM32F7xx chips).
+
+This backend has no configuration options. The point of it is to provide bare-
+minimum platform code needed to do UART reads/writes.
+
+Setup
+=====
+This module requires relatively minimal setup:
+
+  1. Write code against the ``pw_sys_io`` facade.
+  2. Specify the ``dir_pw_sys_io_backend`` GN global variable to point to this
+     backend.
+  3. Build an executable with a main() function using a toolchain that
+     supports Cortex-M4.
+
+.. note::
+  This module provides early firmware init and a linker script, so it will
+  conflict with other modules that do any early device init or provide a linker
+  script.
+
+Module usage
+============
+After building an executable that utilizes this backend, flash the
+produced .elf binary to the development board. Then, using a serial
+communication terminal like minicom/screen (Linux/Mac) or TeraTerm (Windows),
+connect to the device at a baud rate of 115200 (8N1). If you're not using a
+STM32F769I-DISC0 development board, manually connect a USB-to-serial TTL adapter
+to pins ``PA9`` (MCU TX) and ``PA10`` (MCU RX), making sure to match logic
+levels (e.g. 3.3V versus 1.8V).
+
+Sample connection diagram
+-------------------------
+
+.. code-block:: text
+
+  --USB Serial--+    +-----STM32F769 MCU-----
+                |    |
+             TX o--->o PA10/USART1_RX
+                |    |
+             RX o<---o PA9/USART1_TX
+                |    |
+  --------------+    +-----------------------
+
+Dependencies
+============
+  * ``pw_sys_io`` facade
+  * ``pw_preprocessor`` module
diff --git a/pw_sys_io_baremetal_stm32f769/public/pw_sys_io_baremetal_stm32f769/init.h b/pw_sys_io_baremetal_stm32f769/public/pw_sys_io_baremetal_stm32f769/init.h
new file mode 100644
index 0000000..6c3ba73
--- /dev/null
+++ b/pw_sys_io_baremetal_stm32f769/public/pw_sys_io_baremetal_stm32f769/init.h
@@ -0,0 +1,23 @@
+// 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.
+#pragma once
+
+#include "pw_preprocessor/util.h"
+
+PW_EXTERN_C_START
+
+// The actual implementation of PreMainInit() in sys_io_BACKEND.
+void pw_sys_io_stm32f769_Init();
+
+PW_EXTERN_C_END
diff --git a/pw_sys_io_baremetal_stm32f769/sys_io_baremetal.cc b/pw_sys_io_baremetal_stm32f769/sys_io_baremetal.cc
new file mode 100644
index 0000000..dc6c1e2
--- /dev/null
+++ b/pw_sys_io_baremetal_stm32f769/sys_io_baremetal.cc
@@ -0,0 +1,217 @@
+// 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.
+
+#include <cinttypes>
+
+#include "pw_preprocessor/compiler.h"
+#include "pw_sys_io/sys_io.h"
+
+namespace {
+
+// Default core clock. This is technically not a constant, but since this app
+// doesn't change the system clock a constant will suffice.
+constexpr uint32_t kSystemCoreClock = 16000000;
+
+// Base address for everything peripheral-related on the STM32F7xx.
+constexpr uint32_t kPeripheralBaseAddr = 0x40000000u;
+// Base address for everything AHB1-related on the STM32F7xx.
+constexpr uint32_t kAhb1PeripheralBase = kPeripheralBaseAddr + 0x00020000u;
+// Base address for everything APB2-related on the STM32F7xx.
+constexpr uint32_t kApb2PeripheralBase = kPeripheralBaseAddr + 0x00010000u;
+
+// Reset/clock configuration block (RCC).
+// `reserved` fields are unimplemented features, and are present to ensure
+// proper alignment of registers that are in use.
+PW_PACKED(struct) RccBlock {
+  uint32_t reserved1[12];
+  uint32_t ahb1_config;
+  uint32_t reserved2[4];
+  uint32_t apb2_config;
+};
+
+// Mask for ahb1_config (AHB1ENR) to enable the "A" GPIO pins.
+constexpr uint32_t kGpioAEnable = 0x1u;
+
+// Mask for apb2_config (APB2ENR) to enable USART1.
+constexpr uint32_t kUsart1Enable = 0x1u << 4;
+
+// GPIO register block definition.
+PW_PACKED(struct) GpioBlock {
+  uint32_t modes;
+  uint32_t out_type;
+  uint32_t out_speed;
+  uint32_t pull_up_down;
+  uint32_t input_data;
+  uint32_t output_data;
+  uint32_t gpio_bit_set;
+  uint32_t port_config_lock;
+  uint32_t alt_low;
+  uint32_t alt_high;
+};
+
+// Constants related to GPIO mode register masks.
+constexpr uint32_t kTxPortModePos = 18;
+constexpr uint32_t kRxPortModePos = 20;
+constexpr uint32_t kGpioPortModeAlternate = 2;
+
+// Constants related to GPIO port speed register masks.
+constexpr uint32_t kTxPortSpeedPos = 18;
+constexpr uint32_t kRxPortSpeedPos = 20;
+constexpr uint32_t kGpioSpeedVeryHigh = 3;
+
+// Constants related to GPIO pull up/down resistor type masks.
+constexpr uint32_t kTxPullTypePos = 18;
+constexpr uint32_t kRxPullTypePos = 20;
+constexpr uint32_t kPullTypePullUp = 1;
+
+// Constants related to GPIO port speed register masks.
+constexpr uint32_t kTxAltModeHighPos = 4;
+constexpr uint32_t kRxAltModeHighPos = 8;
+
+// Alternate function for pins PA9(TX) and PA10(RX) that enable USART1.
+constexpr uint8_t kGpioAlternateFunctionUsart1 = 0x07u;
+
+// USART configuration flags for control1 register.
+// Note: a large number of configuration flags have been omitted as they default
+// to reasonable values and we don't need to change them.
+constexpr uint32_t kReceiveEnable = 0x1 << 2;
+constexpr uint32_t kTransmitEnable = 0x1 << 3;
+constexpr uint32_t kEnableUsart = 0x1 << 0;
+// USART configuration flags for interrupt_and_status register.
+constexpr uint32_t kReadDataReady = 0x1 << 5;
+constexpr uint32_t kTxRegisterEmpty = 0x1 << 7;
+
+// Layout of memory mapped registers for USART blocks.
+PW_PACKED(struct) UsartBlock {
+  uint32_t control1;
+  uint32_t control2;
+  uint32_t control3;
+  uint32_t baud_rate;
+  uint32_t guard_time_and_prescalar;
+  uint32_t receiver_timeout;
+  uint32_t request;
+  uint32_t interrupt_and_status;
+  uint32_t interrupt_flag_clear;
+  uint32_t receive_data;
+  uint32_t transmit_data;
+};
+
+// Sets the UART baud register using the peripheral clock and target baud rate.
+// These calculations are specific to the default oversample by 16 mode.
+uint32_t CalcBaudRegister(uint32_t clock, uint32_t target_baud) {
+  uint32_t div_fac = (clock * 25) / (4 * target_baud);
+  uint32_t mantissa = div_fac / 100;
+  uint32_t fraction = ((div_fac - mantissa * 100) * 16 + 50) / 100;
+
+  return (mantissa << 4) + (fraction & 0xFFu);
+}
+
+// Declare a reference to the memory mapped RCC block.
+volatile RccBlock& platform_rcc =
+    *reinterpret_cast<volatile RccBlock*>(kAhb1PeripheralBase + 0x3800U);
+
+// Declare a reference to the 'A' GPIO memory mapped block.
+volatile GpioBlock& gpio_a =
+    *reinterpret_cast<volatile GpioBlock*>(kAhb1PeripheralBase + 0x0000U);
+
+// Declare a reference to the memory mapped block for USART1.
+volatile UsartBlock& usart1 =
+    *reinterpret_cast<volatile UsartBlock*>(kApb2PeripheralBase + 0x1000U);
+
+}  // namespace
+
+extern "C" void pw_sys_io_stm32f769_Init() {
+  // Enable 'A' GIPO clocks.
+  platform_rcc.ahb1_config |= kGpioAEnable;
+
+  // Enable Uart TX pin.
+  // Output type defaults to push-pull (rather than open/drain).
+  gpio_a.modes |= kGpioPortModeAlternate << kTxPortModePos;
+  gpio_a.out_speed |= kGpioSpeedVeryHigh << kTxPortSpeedPos;
+  gpio_a.pull_up_down |= kPullTypePullUp << kTxPullTypePos;
+  gpio_a.alt_high |= kGpioAlternateFunctionUsart1 << kTxAltModeHighPos;
+
+  // Enable Uart RX pin.
+  // Output type defaults to push-pull (rather than open/drain).
+  gpio_a.modes |= kGpioPortModeAlternate << kRxPortModePos;
+  gpio_a.out_speed |= kGpioSpeedVeryHigh << kRxPortSpeedPos;
+  gpio_a.pull_up_down |= kPullTypePullUp << kRxPullTypePos;
+  gpio_a.alt_high |= kGpioAlternateFunctionUsart1 << kRxAltModeHighPos;
+
+  // Initialize USART1. Initialized to 8N1 at the specified baud rate.
+  platform_rcc.apb2_config |= kUsart1Enable;
+
+  // Warning: Normally the baud rate register calculation is based off
+  // peripheral 2 clock. For this code, the peripheral clock defaults to
+  // the system core clock so it can be used directly.
+  usart1.baud_rate = CalcBaudRegister(kSystemCoreClock, /*target_baud=*/115200);
+
+  usart1.control1 = kEnableUsart | kReceiveEnable | kTransmitEnable;
+}
+
+namespace pw::sys_io {
+
+// Wait for a byte to read on USART1. This blocks until a byte is read. This is
+// extremely inefficient as it requires the target to burn CPU cycles polling to
+// see if a byte is ready yet.
+Status ReadByte(std::byte* dest) {
+  while (true) {
+    if (TryReadByte(dest).ok()) {
+      return OkStatus();
+    }
+  }
+}
+
+// Wait for a byte to read on USART1. This blocks until a byte is read. This is
+// extremely inefficient as it requires the target to burn CPU cycles polling to
+// see if a byte is ready yet.
+Status TryReadByte(std::byte* dest) {
+  if (!(usart1.interrupt_and_status & kReadDataReady)) {
+    return Status::Unavailable();
+  }
+  *dest = static_cast<std::byte>(usart1.receive_data);
+  usart1.interrupt_flag_clear &= ~kReadDataReady;
+  return OkStatus();
+}
+
+// Send a byte over USART1. Since this blocks on every byte, it's rather
+// inefficient. At the default baud rate of 115200, one byte blocks the CPU for
+// ~87 micro seconds. This means it takes only 10 bytes to block the CPU for
+// 1ms!
+Status WriteByte(std::byte b) {
+  // Wait for TX buffer to be empty. When the buffer is empty, we can write
+  // a value to be dumped out of UART.
+  while (!(usart1.interrupt_and_status & kTxRegisterEmpty)) {
+  }
+  usart1.transmit_data = static_cast<uint32_t>(b);
+  return OkStatus();
+}
+
+// Writes a string using pw::sys_io, and add newline characters at the EOL.
+StatusWithSize WriteLine(const std::string_view& s) {
+  size_t chars_written = 0;
+  StatusWithSize result = WriteBytes(as_bytes(span(s)));
+  if (!result.ok()) {
+    return result;
+  }
+  chars_written += result.size();
+
+  // Write trailing EOL characters.
+  result = WriteBytes(as_bytes(span("\r\n", 2)));
+  chars_written += result.size();
+
+  return StatusWithSize(result.status(), chars_written);
+}
+
+}  // namespace pw::sys_io
diff --git a/targets/stm32f429i_disc1_stm32cube/boot.cc b/targets/stm32f429i_disc1_stm32cube/boot.cc
index 1dccb42..f192e87 100644
--- a/targets/stm32f429i_disc1_stm32cube/boot.cc
+++ b/targets/stm32f429i_disc1_stm32cube/boot.cc
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// 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
diff --git a/targets/stm32f769i_disc0/BUILD.gn b/targets/stm32f769i_disc0/BUILD.gn
new file mode 100644
index 0000000..539b59a
--- /dev/null
+++ b/targets/stm32f769i_disc0/BUILD.gn
@@ -0,0 +1,49 @@
+# 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")
+import("$dir_pw_malloc/backend.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("target_toolchains.gni")
+
+generate_toolchains("toolchains") {
+  toolchains = toolchains_list
+}
+
+config("pw_malloc_active") {
+  if (pw_malloc_BACKEND != "") {
+    defines = [ "PW_MALLOC_ACTIVE=1" ]
+  }
+}
+
+if (current_toolchain != default_toolchain) {
+  pw_source_set("pre_init") {
+    configs = [ ":pw_malloc_active" ]
+    public_deps = [
+      "$dir_pw_boot",
+      "$dir_pw_boot_cortex_m",
+      "$dir_pw_sys_io_baremetal_stm32f769",
+    ]
+    deps = [
+      "$dir_pw_malloc",
+      "$dir_pw_preprocessor",
+    ]
+    sources = [
+      "boot.cc",
+      "vector_table.c",
+    ]
+  }
+}
diff --git a/targets/stm32f769i_disc0/boot.cc b/targets/stm32f769i_disc0/boot.cc
new file mode 100644
index 0000000..9ff552f
--- /dev/null
+++ b/targets/stm32f769i_disc0/boot.cc
@@ -0,0 +1,68 @@
+// 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.
+
+#include "pw_boot/boot.h"
+
+#include "pw_boot_cortex_m/boot.h"
+#include "pw_malloc/malloc.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_sys_io_baremetal_stm32f769/init.h"
+
+// Note that constexpr is used inside of this function instead of using a static
+// constexpr or declaring it outside of this function in an anonymous namespace,
+// because constexpr makes it available for the compiler to evaluate during
+// compile time but does NOT require it to be evaluated at compile time and we
+// have to be incredibly careful that this does not end up in the .data section.
+void pw_boot_PreStaticMemoryInit() {
+  // TODO(pwbug/17): Optionally enable Replace when Pigweed config system is
+  // added.
+#if PW_ARMV7M_ENABLE_FPU
+  // Enable FPU if built using hardware FPU instructions.
+  // CPCAR mask that enables FPU. (ARMv7-M Section B3.2.20)
+  constexpr uint32_t kFpuEnableMask = (0xFu << 20);
+
+  // Memory mapped register to enable FPU. (ARMv7-M Section B3.2.2, Table B3-4)
+  volatile uint32_t& arm_v7m_cpacr =
+      *reinterpret_cast<volatile uint32_t*>(0xE000ED88u);
+  arm_v7m_cpacr |= kFpuEnableMask;
+
+  // Ensure the FPU configuration is committed and enabled before continuing and
+  // potentially executing any FPU instructions, however rare that may be during
+  // startup.
+  asm volatile(
+      " dsb \n"
+      " isb \n"
+      // clang-format off
+      : /*output=*/
+      : /*input=*/
+      : /*clobbers=*/"memory"
+      // clang-format on
+  );
+#endif  // PW_ARMV7M_ENABLE_FPU
+}
+
+void pw_boot_PreStaticConstructorInit() {
+#if PW_MALLOC_ACTIVE
+  pw_MallocInit(&pw_boot_heap_low_addr, &pw_boot_heap_high_addr);
+#endif  // PW_MALLOC_ACTIVE
+}
+
+void pw_boot_PreMainInit() { pw_sys_io_stm32f769_Init(); }
+
+PW_NO_RETURN void pw_boot_PostMain() {
+  // In case main() returns, just sit here until the device is reset.
+  while (true) {
+  }
+  PW_UNREACHABLE;
+}
diff --git a/targets/stm32f769i_disc0/pw_target_toolchains.gni b/targets/stm32f769i_disc0/pw_target_toolchains.gni
new file mode 100644
index 0000000..9080da1
--- /dev/null
+++ b/targets/stm32f769i_disc0/pw_target_toolchains.gni
@@ -0,0 +1,156 @@
+# 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_compilation_testing/negative_compilation_test.gni")
+import("$dir_pw_rpc/system_server/backend.gni")
+import("$dir_pw_sys_io/backend.gni")
+import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
+
+# declare_args() {
+#   # Enable the pw_target_runner for on-device testing.
+#   pw_use_test_server = false
+# }
+
+_target_config = {
+  # TODO(b/241565082): Enable NC testing in GN Windows when it is fixed.
+  pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = host_os != "win"
+
+  # Use the logging main.
+  pw_unit_test_MAIN = "$dir_pw_unit_test:logging_main"
+
+  # Configuration options for Pigweed executable targets.
+  pw_build_EXECUTABLE_TARGET_TYPE = "stm32f769i_executable"
+
+  pw_build_EXECUTABLE_TARGET_TYPE_FILE =
+      get_path_info("stm32f769i_executable.gni", "abspath")
+
+  # Path to the bloaty config file for the output binaries.
+  pw_bloat_BLOATY_CONFIG = "$dir_pw_boot_cortex_m/bloaty_config.bloaty"
+
+  # TODO: Fix test server: likely have to fork stm32cube-disc1 implementation
+  # if (pw_use_test_server) {
+  #   _test_runner_script = "py/stm32f769i_disc0_utils/unit_test_client.py"
+  #   pw_unit_test_AUTOMATIC_RUNNER =
+  #       get_path_info(_test_runner_script, "abspath")
+  # }
+
+  # Facade backends
+  pw_assert_BACKEND = dir_pw_assert_basic
+  pw_boot_BACKEND = "$dir_pw_boot_cortex_m"
+  pw_cpu_exception_ENTRY_BACKEND =
+      "$dir_pw_cpu_exception_cortex_m:cpu_exception"
+  pw_cpu_exception_HANDLER_BACKEND = "$dir_pw_cpu_exception:basic_handler"
+  pw_cpu_exception_SUPPORT_BACKEND = "$dir_pw_cpu_exception_cortex_m:support"
+  pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
+      "$dir_pw_sync_baremetal:interrupt_spin_lock"
+  pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+  pw_log_BACKEND = dir_pw_log_basic
+  pw_sys_io_BACKEND = dir_pw_sys_io_baremetal_stm32f769
+  pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"
+  pw_malloc_BACKEND = dir_pw_malloc_freelist
+
+  pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
+    "PW_BOOT_FLASH_BEGIN=0x08000400",
+    "PW_BOOT_FLASH_SIZE=2048K",
+
+    # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
+    # least 6K bytes in heap when using pw_malloc_freelist. The heap size
+    # required for tests should be investigated.
+    #
+    # TLS realted tests such as $dir_pw_third_party/boringssl:tests require
+    # much larger heap for dynamic allocation. The current number is an
+    # estimated requirement. The acutal required size will be further investigated
+    # when all TLS tests are in place.
+    "PW_BOOT_HEAP_SIZE=366K",
+    "PW_BOOT_MIN_STACK_SIZE=1K",
+    "PW_BOOT_RAM_BEGIN=0x20020000",
+    "PW_BOOT_RAM_SIZE=384K",
+    "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
+    "PW_BOOT_VECTOR_TABLE_SIZE=1024",
+  ]
+
+  pw_build_LINK_DEPS = []
+  pw_build_LINK_DEPS = [
+    "$dir_pw_assert:impl",
+    "$dir_pw_cpu_exception:entry_impl",
+    "$dir_pw_log:impl",
+    "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support",
+  ]
+
+  current_cpu = "arm"
+  current_os = ""
+}
+
+_toolchain_properties = {
+  final_binary_extension = ".elf"
+}
+
+_target_default_configs = [
+  "$dir_pw_build:extra_strict_warnings",
+  "$dir_pw_toolchain/arm_gcc:enable_float_printf",
+]
+
+pw_target_toolchain_stm32f769i_disc0 = {
+  _excluded_members = [
+    "defaults",
+    "name",
+  ]
+
+  debug = {
+    name = "stm32f769i_disc0_debug"
+    _toolchain_base = pw_toolchain_arm_gcc.cortex_m7f_debug
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    forward_variables_from(_toolchain_properties, "*")
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*")
+      forward_variables_from(_target_config, "*")
+      default_configs += _target_default_configs
+    }
+  }
+
+  speed_optimized = {
+    name = "stm32f769i_disc0_speed_optimized"
+    _toolchain_base = pw_toolchain_arm_gcc.cortex_m7f_speed_optimized
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    forward_variables_from(_toolchain_properties, "*")
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*")
+      forward_variables_from(_target_config, "*")
+      default_configs += _target_default_configs
+    }
+  }
+
+  size_optimized = {
+    name = "stm32f769i_disc0_size_optimized"
+    _toolchain_base = pw_toolchain_arm_gcc.cortex_m7f_size_optimized
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    forward_variables_from(_toolchain_properties, "*")
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*")
+      forward_variables_from(_target_config, "*")
+      default_configs += _target_default_configs
+    }
+  }
+}
+
+# This list just contains the members of the above scope for convenience to make
+# it trivial to generate all the toolchains in this file via a
+# `generate_toolchains` target.
+pw_target_toolchain_stm32f769i_disc0_list = [
+  pw_target_toolchain_stm32f769i_disc0.debug,
+  pw_target_toolchain_stm32f769i_disc0.speed_optimized,
+  pw_target_toolchain_stm32f769i_disc0.size_optimized,
+]
diff --git a/targets/stm32f769i_disc0/py/BUILD.gn b/targets/stm32f769i_disc0/py/BUILD.gn
new file mode 100644
index 0000000..e95a6da
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/BUILD.gn
@@ -0,0 +1,35 @@
+# 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/python.gni")
+
+pw_python_package("py") {
+  setup = [
+    "pyproject.toml",
+    "setup.cfg",
+    "setup.py",
+  ]
+  sources = [
+    "stm32f769i_disc0_utils/__init__.py",
+    "stm32f769i_disc0_utils/stm32f769i_detector.py",
+    "stm32f769i_disc0_utils/unit_test_client.py",
+    "stm32f769i_disc0_utils/unit_test_runner.py",
+    "stm32f769i_disc0_utils/unit_test_server.py",
+  ]
+  pylintrc = "$dir_pigweed/.pylintrc"
+  mypy_ini = "$dir_pigweed/.mypy.ini"
+  python_deps = [ "$dir_pw_cli/py" ]
+}
diff --git a/targets/stm32f769i_disc0/py/pyproject.toml b/targets/stm32f769i_disc0/py/pyproject.toml
new file mode 100644
index 0000000..78668a7
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/pyproject.toml
@@ -0,0 +1,16 @@
+# 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.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/targets/stm32f769i_disc0/py/setup.cfg b/targets/stm32f769i_disc0/py/setup.cfg
new file mode 100644
index 0000000..a5d9848
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/setup.cfg
@@ -0,0 +1,36 @@
+# 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.
+[metadata]
+name = stm32f769i_disc0_utils
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Target-specific python scripts for the stm32f769i-disc0 target
+
+[options]
+packages = find:
+zip_safe = False
+install_requires =
+    pyserial>=3.5,<4.0
+    coloredlogs
+
+[options.entry_points]
+console_scripts =
+    stm32f769i_disc0_unit_test_runner = stm32f769i_disc0_utils.unit_test_runner:main
+    stm32f769i_disc0_detector = stm32f769i_disc0_utils.stm32f769i_detector:main
+    stm32f769i_disc0_test_server = stm32f769i_disc0_utils.unit_test_server:main
+    stm32f769i_disc0_test_client = stm32f769i_disc0_utils.unit_test_client:main
+
+[options.package_data]
+stm32f769i_disc0_utils = py.typed
diff --git a/targets/stm32f769i_disc0/py/setup.py b/targets/stm32f769i_disc0/py/setup.py
new file mode 100644
index 0000000..b7024fa
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/setup.py
@@ -0,0 +1,18 @@
+# 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.
+"""stm32f769i_disc0_utils"""
+
+import setuptools  # type: ignore
+
+setuptools.setup()  # Package definition in setup.cfg
diff --git a/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/BUILD.bazel b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/BUILD.bazel
new file mode 100644
index 0000000..2ee2e17
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/BUILD.bazel
@@ -0,0 +1,18 @@
+# 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.
+
+package(default_visibility = ["//visibility:public"])
+
+# Allow other packages to use this configuration file.
+exports_files(["openocd_stm32f7xx.cfg"])
diff --git a/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/__init__.py b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/__init__.py
new file mode 100644
index 0000000..4ce3458
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/__init__.py
@@ -0,0 +1,19 @@
+# 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.
+"""This package provides tooling specific to the stm32f769i-disc0 target."""
+
+from stm32f769i_disc0_utils.unit_test_runner import TestingFailure
+from stm32f769i_disc0_utils.unit_test_runner import flash_device
+from stm32f769i_disc0_utils.unit_test_runner import run_device_test
+from stm32f769i_disc0_utils.stm32f769i_detector import detect_boards
diff --git a/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/openocd_stm32f7xx.cfg b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/openocd_stm32f7xx.cfg
new file mode 100644
index 0000000..08a606c
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/openocd_stm32f7xx.cfg
@@ -0,0 +1,37 @@
+# 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.
+
+# This openocd configuration is compatible with all STM32F7xx cores.
+
+interface hla
+hla_layout stlink
+hla_device_desc "ST-LINK/V2-1"
+hla_vid_pid 0x0483 0x374b
+
+# If PW_STLINK_SERIAL is specified, use that device.
+if { [info exists ::env(PW_STLINK_SERIAL)] } {
+  hla_serial $::env(PW_STLINK_SERIAL)
+}
+
+# If PW_GDB_PORT is specified, use that port.
+if { [info exists ::env(PW_GDB_PORT)] } {
+  gdb_port $::env(PW_GDB_PORT)
+}
+
+transport select hla_swd
+
+source [find target/stm32f7x.cfg]
+
+# Use hardware reset.
+reset_config srst_only srst_nogate
diff --git a/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/py.typed b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/py.typed
diff --git a/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/stm32f769i_detector.py b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/stm32f769i_detector.py
new file mode 100644
index 0000000..54195d5
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/stm32f769i_detector.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+# 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.
+"""Detects attached stm32f769i-disc0 boards connected via mini usb."""
+
+import logging
+import typing
+
+import coloredlogs  # type: ignore
+import serial.tools.list_ports  # type: ignore
+
+# Vendor/device ID to search for in USB devices.
+# Note the STM32F429I-DISC1 and the STM32F769I-DISC0 have the same vendor/model.
+_ST_VENDOR_ID = 0x0483
+_DISCOVERY_MODEL_ID = 0x374b
+
+_LOG = logging.getLogger('stm32f769i_detector')
+
+
+class BoardInfo(typing.NamedTuple):
+    """Information about a connected dev board."""
+    dev_name: str
+    serial_number: str
+
+
+def detect_boards() -> list:
+    """Detect attached boards, returning a list of Board objects."""
+    boards = []
+    all_devs = serial.tools.list_ports.comports()
+    for dev in all_devs:
+        if dev.vid == _ST_VENDOR_ID and dev.pid == _DISCOVERY_MODEL_ID:
+            boards.append(
+                BoardInfo(dev_name=dev.device,
+                          serial_number=dev.serial_number))
+    return boards
+
+
+def main():
+    """This detects and then displays all attached discovery boards."""
+
+    # Try to use pw_cli logs, else default to something reasonable.
+    try:
+        import pw_cli.log  # pylint: disable=import-outside-toplevel
+        pw_cli.log.install()
+    except ImportError:
+        coloredlogs.install(level='INFO',
+                            level_styles={
+                                'debug': {
+                                    'color': 244
+                                },
+                                'error': {
+                                    'color': 'red'
+                                }
+                            },
+                            fmt='%(asctime)s %(levelname)s | %(message)s')
+
+    boards = detect_boards()
+    if not boards:
+        _LOG.info('No attached boards detected')
+    for idx, board in enumerate(boards):
+        _LOG.info('Board %d:', idx)
+        _LOG.info('  - Port: %s', board.dev_name)
+        _LOG.info('  - Serial #: %s', board.serial_number)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/unit_test_client.py b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/unit_test_client.py
new file mode 100755
index 0000000..e10b8c8
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/unit_test_client.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+# 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.
+"""Launch a pw_target_runner client that sends a test request."""
+
+import argparse
+import subprocess
+import sys
+from typing import Optional
+
+_TARGET_CLIENT_COMMAND = 'pw_target_runner_client'
+
+
+def parse_args():
+    """Parses command-line arguments."""
+
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('binary', help='The target test binary to run')
+    parser.add_argument('--server-port',
+                        type=int,
+                        help='Port the test server is located on')
+
+    return parser.parse_args()
+
+
+def launch_client(binary: str, server_port: Optional[int]) -> int:
+    """Sends a test request to the specified server port."""
+    cmd = [_TARGET_CLIENT_COMMAND, '-binary', binary]
+
+    if server_port is not None:
+        cmd.extend(['-port', str(server_port)])
+
+    return subprocess.call(cmd)
+
+
+def main() -> int:
+    """Launch a test by sending a request to a pw_target_runner_server."""
+    args = parse_args()
+    return launch_client(args.binary, args.server_port)
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/unit_test_runner.py b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/unit_test_runner.py
new file mode 100755
index 0000000..c9b1c50
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/unit_test_runner.py
@@ -0,0 +1,312 @@
+#!/usr/bin/env python3
+# 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.
+"""This script flashes and runs unit tests on stm32f769i-disc0 boards."""
+
+import argparse
+import logging
+import os
+import subprocess
+import sys
+import threading
+from typing import List
+
+import coloredlogs  # type: ignore
+import serial  # type: ignore
+from stm32f769i_disc0_utils import stm32f769i_detector
+
+# Path used to access non-python resources in this python module.
+_DIR = os.path.dirname(__file__)
+
+# Path to default openocd configuration file.
+_OPENOCD_CONFIG = os.path.join(_DIR, 'openocd_stm32f7xx.cfg')
+
+# Path to scripts provided by openocd.
+_OPENOCD_SCRIPTS_DIR = os.path.join(
+    os.getenv('PW_PIGWEED_CIPD_INSTALL_DIR', ''), 'share', 'openocd',
+    'scripts')
+
+_LOG = logging.getLogger('unit_test_runner')
+
+# Verification of test pass/failure depends on these strings. If the formatting
+# or output of the simple_printing_event_handler changes, this may need to be
+# updated.
+_TESTS_STARTING_STRING = b'[==========] Running all tests.'
+_TESTS_DONE_STRING = b'[==========] Done running all tests.'
+_TEST_FAILURE_STRING = b'[  FAILED  ]'
+
+# How long to wait for the first byte of a test to be emitted. This is longer
+# than the user-configurable timeout as there's a delay while the device is
+# flashed.
+_FLASH_TIMEOUT = 5.0
+
+
+class TestingFailure(Exception):
+    """A simple exception to be raised when a testing step fails."""
+
+
+class DeviceNotFound(Exception):
+    """A simple exception to be raised when unable to connect to a device."""
+
+
+def parse_args():
+    """Parses command-line arguments."""
+
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('binary', help='The target test binary to run')
+    parser.add_argument('--openocd-config',
+                        default=_OPENOCD_CONFIG,
+                        help='Path to openocd configuration file')
+    parser.add_argument('--stlink-serial',
+                        default=None,
+                        help='The serial number of the stlink to use when '
+                        'flashing the target device')
+    parser.add_argument('--port',
+                        default=None,
+                        help='The name of the serial port to connect to when '
+                        'running tests')
+    parser.add_argument('--baud',
+                        type=int,
+                        default=115200,
+                        help='Target baud rate to use for serial communication'
+                        ' with target device')
+    parser.add_argument('--test-timeout',
+                        type=float,
+                        default=5.0,
+                        help='Maximum communication delay in seconds before a '
+                        'test is considered unresponsive and aborted')
+    parser.add_argument('--verbose',
+                        '-v',
+                        dest='verbose',
+                        action="store_true",
+                        help='Output additional logs as the script runs')
+
+    return parser.parse_args()
+
+
+def log_subprocess_output(level, output):
+    """Logs subprocess output line-by-line."""
+
+    lines = output.decode('utf-8', errors='replace').splitlines()
+    for line in lines:
+        _LOG.log(level, line)
+
+
+def reset_device(openocd_config, stlink_serial):
+    """Uses openocd to reset the attached device."""
+
+    # Name/path of openocd.
+    default_flasher = 'openocd'
+    flash_tool = os.getenv('OPENOCD_PATH', default_flasher)
+
+    cmd = [
+        flash_tool, '-s', _OPENOCD_SCRIPTS_DIR, '-f', openocd_config, '-c',
+        'init', '-c', 'reset run', '-c', 'exit'
+    ]
+    _LOG.debug('Resetting device')
+
+    env = os.environ.copy()
+    if stlink_serial:
+        env['PW_STLINK_SERIAL'] = stlink_serial
+
+    # Disable GDB port to support multi-device testing.
+    env['PW_GDB_PORT'] = 'disabled'
+    process = subprocess.run(cmd,
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.STDOUT,
+                             env=env)
+    if process.returncode:
+        log_subprocess_output(logging.ERROR, process.stdout)
+        raise TestingFailure('Failed to reset target device')
+
+    log_subprocess_output(logging.DEBUG, process.stdout)
+
+    _LOG.debug('Successfully reset device')
+
+
+def read_serial(port, baud_rate, test_timeout) -> bytes:
+    """Reads lines from a serial port until a line read times out.
+
+    Returns bytes object containing the read serial data.
+    """
+
+    serial_data = bytearray()
+    device = serial.Serial(baudrate=baud_rate,
+                           port=port,
+                           timeout=_FLASH_TIMEOUT)
+    if not device.is_open:
+        raise TestingFailure('Failed to open device')
+
+    # Flush input buffer and reset the device to begin the test.
+    device.reset_input_buffer()
+
+    # Block and wait for the first byte.
+    serial_data += device.read()
+    if not serial_data:
+        raise TestingFailure('Device not producing output')
+
+    device.timeout = test_timeout
+
+    # Read with a reasonable timeout until we stop getting characters.
+    while True:
+        bytes_read = device.readline()
+        if not bytes_read:
+            break
+        serial_data += bytes_read
+        if serial_data.rfind(_TESTS_DONE_STRING) != -1:
+            # Set to much more aggressive timeout since the last one or two
+            # lines should print out immediately. (one line if all fails or all
+            # passes, two lines if mixed.)
+            device.timeout = 0.01
+
+    # Remove carriage returns.
+    serial_data = serial_data.replace(b'\r', b'')
+
+    # Try to trim captured results to only contain most recent test run.
+    test_start_index = serial_data.rfind(_TESTS_STARTING_STRING)
+    return serial_data if test_start_index == -1 else serial_data[
+        test_start_index:]
+
+
+def flash_device(binary, openocd_config, stlink_serial):
+    """Flash binary to a connected device using the provided configuration."""
+
+    # Name/path of openocd.
+    default_flasher = 'openocd'
+    flash_tool = os.getenv('OPENOCD_PATH', default_flasher)
+
+    openocd_command = ' '.join(['program', binary, 'reset', 'exit'])
+    cmd = [
+        flash_tool, '-s', _OPENOCD_SCRIPTS_DIR, '-f', openocd_config, '-c',
+        openocd_command
+    ]
+    _LOG.info('Flashing firmware to device')
+
+    env = os.environ.copy()
+    if stlink_serial:
+        env['PW_STLINK_SERIAL'] = stlink_serial
+
+    # Disable GDB port to support multi-device testing.
+    env['PW_GDB_PORT'] = 'disabled'
+    process = subprocess.run(cmd,
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.STDOUT,
+                             env=env)
+    if process.returncode:
+        log_subprocess_output(logging.ERROR, process.stdout)
+        raise TestingFailure('Failed to flash target device')
+
+    log_subprocess_output(logging.DEBUG, process.stdout)
+
+    _LOG.debug('Successfully flashed firmware to device')
+
+
+def handle_test_results(test_output):
+    """Parses test output to determine whether tests passed or failed."""
+
+    if test_output.find(_TESTS_STARTING_STRING) == -1:
+        raise TestingFailure('Failed to find test start')
+
+    if test_output.rfind(_TESTS_DONE_STRING) == -1:
+        log_subprocess_output(logging.INFO, test_output)
+        raise TestingFailure('Tests did not complete')
+
+    if test_output.rfind(_TEST_FAILURE_STRING) != -1:
+        log_subprocess_output(logging.INFO, test_output)
+        raise TestingFailure('Test suite had one or more failures')
+
+    log_subprocess_output(logging.DEBUG, test_output)
+
+    _LOG.info('Test passed!')
+
+
+def _threaded_test_reader(dest, port, baud_rate, test_timeout):
+    """Parses test output to the mutable "dest" passed to this function."""
+    dest.append(read_serial(port, baud_rate, test_timeout))
+
+
+def run_device_test(binary,
+                    test_timeout,
+                    openocd_config,
+                    baud,
+                    stlink_serial=None,
+                    port=None) -> bool:
+    """Flashes, runs, and checks an on-device test binary.
+
+    Returns true on test pass.
+    """
+
+    if stlink_serial is None and port is None:
+        _LOG.debug('Attempting to automatically detect dev board')
+        boards = stm32f769i_detector.detect_boards()
+        if not boards:
+            error = 'Could not find an attached device'
+            _LOG.error(error)
+            raise DeviceNotFound(error)
+        stlink_serial = boards[0].serial_number
+        port = boards[0].dev_name
+
+    _LOG.debug('Launching test binary %s', binary)
+    try:
+        # Begin capturing test output via another thread BEFORE flashing the
+        # device since the test will automatically run after the image is
+        # flashed. This reduces flake since there isn't a need to time a reset
+        # correctly relative to the start of capturing device output.
+        result: List[bytes] = []
+        threaded_reader_args = (result, port, baud, test_timeout)
+        read_thread = threading.Thread(target=_threaded_test_reader,
+                                       args=threaded_reader_args)
+        read_thread.start()
+        _LOG.info('Running test')
+        flash_device(binary, openocd_config, stlink_serial)
+        read_thread.join()
+        if result:
+            handle_test_results(result[0])
+    except TestingFailure as err:
+        _LOG.error(err)
+        return False
+
+    return True
+
+
+def main():
+    """Set up runner, and then flash/run device test."""
+    args = parse_args()
+
+    # Try to use pw_cli logs, else default to something reasonable.
+    try:
+        import pw_cli.log  # pylint: disable=import-outside-toplevel
+        log_level = logging.DEBUG if args.verbose else logging.INFO
+        pw_cli.log.install(level=log_level)
+    except ImportError:
+        coloredlogs.install(level='DEBUG' if args.verbose else 'INFO',
+                            level_styles={
+                                'debug': {
+                                    'color': 244
+                                },
+                                'error': {
+                                    'color': 'red'
+                                }
+                            },
+                            fmt='%(asctime)s %(levelname)s | %(message)s')
+
+    if run_device_test(args.binary, args.test_timeout, args.openocd_config,
+                       args.baud, args.stlink_serial, args.port):
+        sys.exit(0)
+    else:
+        sys.exit(1)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/unit_test_server.py b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/unit_test_server.py
new file mode 100644
index 0000000..4fa8d31
--- /dev/null
+++ b/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/unit_test_server.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+# 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.
+"""Launch a pw_target_runner server to use for multi-device testing."""
+
+import argparse
+import logging
+import sys
+import tempfile
+from typing import IO, List, Optional
+
+import pw_cli.process
+import pw_cli.log
+
+from stm32f769i_disc0_utils import stm32f769i_detector
+
+_LOG = logging.getLogger('unit_test_server')
+
+_TEST_RUNNER_COMMAND = 'stm32f769i_disc0_unit_test_runner'
+
+_TEST_SERVER_COMMAND = 'pw_target_runner_server'
+
+
+def parse_args():
+    """Parses command-line arguments."""
+
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('--server-port',
+                        type=int,
+                        default=8080,
+                        help='Port to launch the pw_target_runner_server on')
+    parser.add_argument('--server-config',
+                        type=argparse.FileType('r'),
+                        help='Path to server config file')
+    parser.add_argument('--verbose',
+                        '-v',
+                        dest='verbose',
+                        action="store_true",
+                        help='Output additional logs as the script runs')
+
+    return parser.parse_args()
+
+
+def generate_runner(command: str, arguments: List[str]) -> str:
+    """Generates a text-proto style pw_target_runner_server configuration."""
+    # TODO(amontanez): Use a real proto library to generate this when we have
+    # one set up.
+    for i, arg in enumerate(arguments):
+        arguments[i] = f'  args: "{arg}"'
+    runner = ['runner {', f'  command:"{command}"']
+    runner.extend(arguments)
+    runner.append('}\n')
+    return '\n'.join(runner)
+
+
+def generate_server_config() -> IO[bytes]:
+    """Returns a temporary generated file for use as the server config."""
+    boards = stm32f769i_detector.detect_boards()
+    if not boards:
+        _LOG.critical('No attached boards detected')
+        sys.exit(1)
+    config_file = tempfile.NamedTemporaryFile()
+    _LOG.debug('Generating test server config at %s', config_file.name)
+    _LOG.debug('Found %d attached devices', len(boards))
+    for board in boards:
+        test_runner_args = [
+            '--stlink-serial', board.serial_number, '--port', board.dev_name
+        ]
+        config_file.write(
+            generate_runner(_TEST_RUNNER_COMMAND,
+                            test_runner_args).encode('utf-8'))
+    config_file.flush()
+    return config_file
+
+
+def launch_server(server_config: Optional[IO[bytes]],
+                  server_port: Optional[int]) -> int:
+    """Launch a device test server with the provided arguments."""
+    if server_config is None:
+        # Auto-detect attached boards if no config is provided.
+        server_config = generate_server_config()
+
+    cmd = [_TEST_SERVER_COMMAND, '-config', server_config.name]
+
+    if server_port is not None:
+        cmd.extend(['-port', str(server_port)])
+
+    return pw_cli.process.run(*cmd, log_output=True).returncode
+
+
+def main():
+    """Launch a device test server with the provided arguments."""
+    args = parse_args()
+
+    # Try to use pw_cli logs, else default to something reasonable.
+    pw_cli.log.install()
+    if args.verbose:
+        _LOG.setLevel(logging.DEBUG)
+
+    exit_code = launch_server(args.server_config, args.server_port)
+    sys.exit(exit_code)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/targets/stm32f769i_disc0/stm32f769i_executable.gni b/targets/stm32f769i_disc0/stm32f769i_executable.gni
new file mode 100644
index 0000000..9ce30ce
--- /dev/null
+++ b/targets/stm32f769i_disc0/stm32f769i_executable.gni
@@ -0,0 +1,33 @@
+# 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_malloc/backend.gni")
+
+# Executable wrapper that includes some baremetal startup code.
+template("stm32f769i_executable") {
+  target("executable", target_name) {
+    forward_variables_from(invoker, "*")
+    if (!defined(deps)) {
+      deps = []
+    }
+    deps += [ "//targets/stm32f769i_disc0:pre_init" ]
+    if (pw_malloc_BACKEND != "") {
+      if (!defined(configs)) {
+        configs = []
+      }
+      configs += [ "$dir_pw_malloc:pw_malloc_wrapper_config" ]
+    }
+  }
+}
diff --git a/targets/stm32f769i_disc0/target_toolchains.gni b/targets/stm32f769i_disc0/target_toolchains.gni
new file mode 100644
index 0000000..87a90c0
--- /dev/null
+++ b/targets/stm32f769i_disc0/target_toolchains.gni
@@ -0,0 +1,64 @@
+# 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("//targets/common_backends.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_third_party/nanopb/nanopb.gni")
+import("pw_target_toolchains.gni")
+
+target_toolchain_stm32f769i_disc0 = {
+  _excluded_members = [
+    "defaults",
+    "name",
+  ]
+  _excluded_defaults = [
+    "pw_cpu_exception_ENTRY_BACKEND",
+    "pw_cpu_exception_HANDLER_BACKEND",
+    "pw_cpu_exception_SUPPORT_BACKEND",
+  ]
+
+  debug = {
+    name = "stm32f769i_disc0_debug"
+    _toolchain_base = pw_target_toolchain_stm32f769i_disc0.debug
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*", _excluded_defaults)
+      forward_variables_from(toolchain_overrides, "*")
+      pw_board_led_BACKEND = "$dir_pw_board_led_stm32f769i_disc0"
+      pw_spin_delay_BACKEND = "$dir_pw_spin_delay_stm32f769i_disc0"
+    }
+  }
+
+  # Toolchain for tests only.
+  debug_tests = {
+    name = "stm32f769i_disc0_debug_tests"
+    _toolchain_base = pw_target_toolchain_stm32f769i_disc0.debug
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*", _excluded_defaults)
+      forward_variables_from(toolchain_overrides, "*")
+
+      # Force tests to use basic log backend to avoid generating and loading its
+      # own tokenized database.
+      pw_log_BACKEND = dir_pw_log_basic
+    }
+  }
+}
+
+toolchains_list = [
+  target_toolchain_stm32f769i_disc0.debug,
+  target_toolchain_stm32f769i_disc0.debug_tests,
+]
diff --git a/targets/stm32f769i_disc0/vector_table.c b/targets/stm32f769i_disc0/vector_table.c
new file mode 100644
index 0000000..184bfe6
--- /dev/null
+++ b/targets/stm32f769i_disc0/vector_table.c
@@ -0,0 +1,58 @@
+// 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.
+
+#include <stdbool.h>
+
+#include "pw_boot/boot.h"
+#include "pw_boot_cortex_m/boot.h"
+
+// Default handler to insert into the ARMv7-M vector table (below).
+// This function exists for convenience. If a device isn't doing what you
+// expect, it might have hit a fault and ended up here.
+static void DefaultFaultHandler(void) {
+  while (true) {
+    // Wait for debugger to attach.
+  }
+}
+
+// This is the device's interrupt vector table. It's not referenced in any
+// code because the platform (STM32F7xx) expects this table to be present at the
+// beginning of flash. The exact address is specified in the pw_boot_cortex_m
+// configuration as part of the target config.
+//
+// For more information, see ARMv7-M Architecture Reference Manual DDI 0403E.b
+// section B1.5.3.
+
+// This typedef is for convenience when building the vector table. With the
+// exception of SP_main (0th entry in the vector table), all the entries of the
+// vector table are function pointers.
+typedef void (*InterruptHandler)(void);
+
+PW_KEEP_IN_SECTION(".vector_table")
+const InterruptHandler vector_table[] = {
+    // The starting location of the stack pointer.
+    // This address is NOT an interrupt handler/function pointer, it is simply
+    // the address that the main stack pointer should be initialized to. The
+    // value is reinterpret casted because it needs to be in the vector table.
+    [0] = (InterruptHandler)(&pw_boot_stack_high_addr),
+
+    // Reset handler, dictates how to handle reset interrupt. This is the
+    // address that the Program Counter (PC) is initialized to at boot.
+    [1] = pw_boot_Entry,
+
+    // NMI handler.
+    [2] = DefaultFaultHandler,
+    // HardFault handler.
+    [3] = DefaultFaultHandler,
+};
diff --git a/targets/stm32f769i_disc0_stm32cube/BUILD.gn b/targets/stm32f769i_disc0_stm32cube/BUILD.gn
new file mode 100644
index 0000000..ce81104
--- /dev/null
+++ b/targets/stm32f769i_disc0_stm32cube/BUILD.gn
@@ -0,0 +1,84 @@
+# 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")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_malloc/backend.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("target_toolchains.gni")
+
+generate_toolchains("target_toolchains") {
+  toolchains = pw_target_toolchain_stm32f769i_disc0_list
+}
+
+config("module_config_options") {
+  cflags = [
+    "-include",
+    rebase_path("module_config_overrides.h", root_build_dir),
+  ]
+}
+
+pw_source_set("module_config_overrides") {
+  public_configs = [ ":module_config_options" ]
+  sources = [ "module_config_overrides.h" ]
+}
+
+config("pw_malloc_active") {
+  if (pw_malloc_BACKEND != "") {
+    defines = [ "PW_MALLOC_ACTIVE=1" ]
+  }
+}
+
+if (current_toolchain != default_toolchain) {
+  pw_source_set("pre_init") {
+    configs = [ ":pw_malloc_active" ]
+    public_deps = [
+      "$dir_pw_boot",
+      "$dir_pw_boot_cortex_m:armv7m",
+      "$dir_pw_sys_io_stm32cube",
+    ]
+    deps = [
+      "$dir_pw_malloc",
+      "$dir_pw_preprocessor",
+      "$dir_pw_string",
+      "$dir_pw_third_party/freertos",
+      "$dir_pw_third_party/stm32cube",
+    ]
+    sources = [
+      "boot.cc",
+      "vector_table.c",
+    ]
+  }
+
+  config("config_includes") {
+    include_dirs = [ "config" ]
+  }
+
+  pw_source_set("stm32f7xx_hal_config") {
+    public_configs = [ ":config_includes" ]
+    public = [ "config/stm32f7xx_hal_conf.h" ]
+  }
+
+  pw_source_set("stm32f7xx_freertos_config") {
+    public_configs = [ ":config_includes" ]
+    public_deps = [ "$dir_pw_third_party/freertos:config_assert" ]
+    public = [ "config/FreeRTOSConfig.h" ]
+  }
+}
+
+pw_doc_group("target_docs") {
+  sources = [ "target_docs.rst" ]
+}
diff --git a/targets/stm32f769i_disc0_stm32cube/README.md b/targets/stm32f769i_disc0_stm32cube/README.md
new file mode 100644
index 0000000..2c56ac3
--- /dev/null
+++ b/targets/stm32f769i_disc0_stm32cube/README.md
@@ -0,0 +1,18 @@
+##Building
+In order to build this target, the submodules in `//third_party/stm32cubef7`
+need to be checked out and the following flag needs to be added to your
+gn args (gn args out)
+
+```
+pw_third_party_stm32cubef7_enabled = "yes"
+```
+
+##Flashing
+
+Images can be flashed using the same scripts as the in-tree variant.
+
+This command can be used to flash the blinky example:
+
+```
+openocd -s ${PW_PIGWEED_CIPD_INSTALL_DIR}/share/openocd/scripts -f ${PW_ROOT}/targets/stm32f769i_disc0/py/stm32f769i_disc0_utils/openocd_stm32f7xx.cfg -c "program out/stm32f769i_disc0_stm32cube_debug/obj/applications/blinky/bin/blinky.elf reset exit"
+```
diff --git a/targets/stm32f769i_disc0_stm32cube/boot.cc b/targets/stm32f769i_disc0_stm32cube/boot.cc
new file mode 100644
index 0000000..b975653
--- /dev/null
+++ b/targets/stm32f769i_disc0_stm32cube/boot.cc
@@ -0,0 +1,193 @@
+// 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.
+
+#include "pw_boot/boot.h"
+
+#include <array>
+
+#include "FreeRTOS.h"
+#include "pw_boot_cortex_m/boot.h"
+#include "pw_malloc/malloc.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_string/util.h"
+#include "pw_sys_io_stm32cube/init.h"
+#include "stm32f7xx.h"
+#include "task.h"
+
+namespace {
+
+// TODO(cmumford): Remove hard-coded hack. At present this cache is here, which
+// is used for vApplicationGetIdleTaskMemory, and the application has its own
+// stack. Determine if both are needed.
+std::array<StackType_t, 100 /*configMINIMAL_STACK_SIZE*/> freertos_idle_stack;
+StaticTask_t freertos_idle_tcb;
+
+std::array<StackType_t, configTIMER_TASK_STACK_DEPTH> freertos_timer_stack;
+StaticTask_t freertos_timer_tcb;
+
+std::array<char, configMAX_TASK_NAME_LEN> temp_thread_name_buffer;
+
+RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {
+    .PeriphClockSelection = RCC_PERIPHCLK_CLK48,
+    .PLLI2S = {},
+    .PLLSAI =
+        {
+            .PLLSAIN = 192,
+            .PLLSAIQ = 4,
+            .PLLSAIR = 0,
+            .PLLSAIP = RCC_PLLSAIP_DIV4,
+        },
+    .PLLI2SDivQ = 0,
+    .PLLSAIDivQ = 0,
+    .PLLSAIDivR = 0,
+    .RTCClockSelection = 0,
+    .I2sClockSelection = 0,
+    .TIMPresSelection = 0,
+    .Sai1ClockSelection = 0,
+    .Sai2ClockSelection = 0,
+    .Usart1ClockSelection = 0,
+    .Usart2ClockSelection = 0,
+    .Usart3ClockSelection = 0,
+    .Uart4ClockSelection = 0,
+    .Uart5ClockSelection = 0,
+    .Usart6ClockSelection = 0,
+    .Uart7ClockSelection = 0,
+    .Uart8ClockSelection = 0,
+    .I2c1ClockSelection = 0,
+    .I2c2ClockSelection = 0,
+    .I2c3ClockSelection = 0,
+    .I2c4ClockSelection = 0,
+    .Lptim1ClockSelection = 0,
+    .CecClockSelection = 0,
+    .Clk48ClockSelection = RCC_CLK48SOURCE_PLLSAIP,
+    .Sdmmc1ClockSelection = 0,
+    .Sdmmc2ClockSelection = 0,
+    .Dfsdm1ClockSelection = 0,
+    .Dfsdm1AudioClockSelection = 0,
+};
+
+}  // namespace
+
+extern "C" {
+
+// Initializes clock to its max, 180Mhz. Note that this naming follows CubeMX's
+// naming out of convention. It's not required that this target provides a
+// symbol named SystemClock_Config. This function shares the same purpose as
+// the symbol of the same name that is generated by CubeMX.
+void SystemClock_Config() {
+  __HAL_RCC_PWR_CLK_ENABLE();
+  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
+
+  RCC_OscInitTypeDef RCC_OscInitStruct = {
+      .OscillatorType = RCC_OSCILLATORTYPE_HSE,
+      .HSEState = RCC_HSE_ON,
+      .LSEState = RCC_LSE_OFF,
+      .HSIState = RCC_HSI_OFF,
+      .HSICalibrationValue = 0x0,
+      .LSIState = RCC_LSI_OFF,
+      .PLL =
+          {
+              .PLLState = RCC_PLL_ON,
+              .PLLSource = RCC_PLLSOURCE_HSE,
+              .PLLM = 25,
+              .PLLN = 400,
+              .PLLP = RCC_PLLP_DIV2,
+              .PLLQ = 8,
+              .PLLR = 7,
+          },
+  };
+
+  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
+    pw_boot_PostMain();
+  }
+
+  // OverDrive required for operation > 168Mhz
+  if (HAL_PWREx_EnableOverDrive() != HAL_OK) {
+    pw_boot_PostMain();
+  }
+
+  if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) {
+    pw_boot_PostMain();
+  }
+
+  RCC_ClkInitTypeDef RCC_ClkInitStruct = {
+      .ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
+                    RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2),
+      .SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK,
+      .AHBCLKDivider = RCC_SYSCLK_DIV1,
+      .APB1CLKDivider = RCC_HCLK_DIV4,
+      .APB2CLKDivider = RCC_HCLK_DIV2,
+  };
+
+  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_6) != HAL_OK) {
+    pw_boot_PostMain();
+  }
+}
+
+// Functions needed when configGENERATE_RUN_TIME_STATS is on.
+void configureTimerForRunTimeStats(void) {}
+unsigned long getRunTimeCounterValue(void) { return uwTick; }
+
+// Required for configCHECK_FOR_STACK_OVERFLOW.
+void vApplicationStackOverflowHook(TaskHandle_t, char* pcTaskName) {
+  pw::string::Copy(pcTaskName, temp_thread_name_buffer);
+  PW_CRASH("Stack OVF for task %s", temp_thread_name_buffer.data());
+}
+
+// Required for configUSE_TIMERS.
+void vApplicationGetTimerTaskMemory(StaticTask_t** ppxTimerTaskTCBBuffer,
+                                    StackType_t** ppxTimerTaskStackBuffer,
+                                    uint32_t* pulTimerTaskStackSize) {
+  *ppxTimerTaskTCBBuffer = &freertos_timer_tcb;
+  *ppxTimerTaskStackBuffer = freertos_timer_stack.data();
+  *pulTimerTaskStackSize = freertos_timer_stack.size();
+}
+
+void vApplicationGetIdleTaskMemory(StaticTask_t** ppxIdleTaskTCBBuffer,
+                                   StackType_t** ppxIdleTaskStackBuffer,
+                                   uint32_t* pulIdleTaskStackSize) {
+  *ppxIdleTaskTCBBuffer = &freertos_idle_tcb;
+  *ppxIdleTaskStackBuffer = freertos_idle_stack.data();
+  *pulIdleTaskStackSize = freertos_idle_stack.size();
+}
+
+void pw_boot_PreStaticMemoryInit() {}
+
+void pw_boot_PreStaticConstructorInit() {
+  // Provided by STMicroelectronics SDK. Can be configured to be provided
+  // elsewhere by changing pw_third_party_stm32cube_CMSIS_INIT.
+  SystemInit();
+
+  // Provided by the STMicroelectronics SDK.
+  HAL_Init();
+
+  // Typically provided by CubeMX codegen, SystemClock_Config() is instead
+  // provided as part of this target.
+  SystemClock_Config();
+
+#if PW_MALLOC_ACTIVE
+  pw_MallocInit(&pw_boot_heap_low_addr, &pw_boot_heap_high_addr);
+#endif  // PW_MALLOC_ACTIVE
+}
+
+void pw_boot_PreMainInit() { pw_sys_io_Init(); }
+
+PW_NO_RETURN void pw_boot_PostMain() {
+  // In case main() returns, just sit here until the device is reset.
+  while (true) {
+  }
+  PW_UNREACHABLE;
+}
+
+}  // extern "C"
diff --git a/targets/stm32f769i_disc0_stm32cube/config/FreeRTOSConfig.h b/targets/stm32f769i_disc0_stm32cube/config/FreeRTOSConfig.h
new file mode 100644
index 0000000..159b64a
--- /dev/null
+++ b/targets/stm32f769i_disc0_stm32cube/config/FreeRTOSConfig.h
@@ -0,0 +1,89 @@
+// 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.
+#pragma once
+
+#include <stdint.h>
+
+// Disable formatting to make it easier to compare with other config files.
+// clang-format off
+
+// Externally defined variables that must be forward-declared for FreeRTOS to
+// use them.
+extern uint32_t SystemCoreClock;
+extern void configureTimerForRunTimeStats(void);
+extern unsigned long getRunTimeCounterValue(void);
+
+#define configUSE_16_BIT_TICKS                  0
+#define configUSE_CO_ROUTINES                   0
+#define configUSE_IDLE_HOOK                     0
+#define configUSE_MALLOC_FAILED_HOOK            0
+#define configUSE_MUTEXES                       1
+#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
+#define configUSE_PREEMPTION                    1
+#define configUSE_TICK_HOOK                     0
+#define configUSE_TIMERS                        1
+#define configUSE_TRACE_FACILITY                1
+
+#define configGENERATE_RUN_TIME_STATS           1
+#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS  configureTimerForRunTimeStats
+#define portGET_RUN_TIME_COUNTER_VALUE          getRunTimeCounterValue
+
+#define configCHECK_FOR_STACK_OVERFLOW          2
+#define configCPU_CLOCK_HZ                      (SystemCoreClock)
+#define configENABLE_BACKWARD_COMPATIBILITY     0
+#define configMAX_CO_ROUTINE_PRIORITIES         (2)
+#define configMAX_PRIORITIES                    (7)
+#define configMAX_TASK_NAME_LEN                 (16)
+#define configMESSAGE_BUFFER_LENGTH_TYPE        size_t
+#define configMINIMAL_STACK_SIZE                ((uint16_t)(4 * 1024))
+#define configQUEUE_REGISTRY_SIZE               8
+#define configRECORD_STACK_HIGH_ADDRESS         1
+#define configTICK_RATE_HZ                      ((TickType_t)1000)
+#define configTIMER_QUEUE_LENGTH                10
+#define configTIMER_TASK_PRIORITY               (6)
+#define configTIMER_TASK_STACK_DEPTH            512
+
+/* Memory allocation related definitions. */
+#define configSUPPORT_STATIC_ALLOCATION         1
+#define configSUPPORT_DYNAMIC_ALLOCATION        1
+#define configTOTAL_HEAP_SIZE                   ((size_t)(1 * 1024))
+#define configAPPLICATION_ALLOCATED_HEAP        0
+
+/* __NVIC_PRIO_BITS in CMSIS */
+#define configPRIO_BITS                         4
+
+#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY      15
+#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
+#define configKERNEL_INTERRUPT_PRIORITY \
+  (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
+#define configMAX_SYSCALL_INTERRUPT_PRIORITY \
+  (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
+
+#define INCLUDE_uxTaskPriorityGet               1
+#define INCLUDE_vTaskCleanUpResources           0
+#define INCLUDE_vTaskDelay                      1
+#define INCLUDE_vTaskDelayUntil                 0
+#define INCLUDE_vTaskDelete                     1
+#define INCLUDE_vTaskPrioritySet                1
+#define INCLUDE_vTaskSuspend                    1
+#define INCLUDE_xTaskGetSchedulerState          1
+#define INCLUDE_uxTaskGetStackHighWaterMark     1
+
+// Instead of defining configASSERT(), include a header that provides a
+// definition that redirects to pw_assert.
+#include "pw_third_party/freertos/config_assert.h"
+
+#define vPortSVCHandler     SVC_Handler
+#define xPortPendSVHandler  PendSV_Handler
+#define xPortSysTickHandler SysTick_Handler
diff --git a/targets/stm32f769i_disc0_stm32cube/config/stm32f7xx_hal_conf.h b/targets/stm32f769i_disc0_stm32cube/config/stm32f7xx_hal_conf.h
new file mode 100644
index 0000000..0fe8986
--- /dev/null
+++ b/targets/stm32f769i_disc0_stm32cube/config/stm32f7xx_hal_conf.h
@@ -0,0 +1,266 @@
+// 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.
+
+#pragma once
+
+/* Clock setup */
+#define HSI_VALUE 16000000U
+#define LSI_VALUE 32000U
+
+// The F429-disc has an 8Mhz external crystal
+#define HSE_VALUE 8000000U
+#define HSE_STARTUP_TIMEOUT 100U
+
+// The F429-disc has no LSE
+#define LSE_VALUE 0U
+#define LSE_STARTUP_TIMEOUT 5000U
+
+#define EXTERNAL_CLOCK_VALUE 0U
+
+/* HAL Config */
+#define TICK_INT_PRIORITY 0x0FU
+#define USE_RTOS 0U
+#define PREFETCH_ENABLE 1U
+#define INSTRUCTION_CACHE_ENABLE 1U
+#define DATA_CACHE_ENABLE 1U
+
+#define assert_param(expr) ((void)0U)
+
+/* Ethernet driver buffers size + count
+ * (also used by FreeRTOS_Plus_TCP's stm32 driver) */
+#define ETH_RX_BUF_SIZE ETH_MAX_PACKET_SIZE
+#define ETH_TX_BUF_SIZE ETH_MAX_PACKET_SIZE
+#define ETH_RXBUFNB 4U
+#define ETH_TXBUFNB 4U
+
+/* Ethernet PHY Defines (unused by FreeRTOS_Plus_TCP's driver) */
+#define PHY_RESET_DELAY 0x000000FFU
+#define PHY_CONFIG_DELAY 0x00000FFFU
+
+#define PHY_READ_TO 0x0000FFFFU
+#define PHY_WRITE_TO 0x0000FFFFU
+
+/* Common PHY Registers */
+#define PHY_BCR ((uint16_t)0x0000)
+#define PHY_BSR ((uint16_t)0x0001)
+
+#define PHY_RESET ((uint16_t)0x8000)
+#define PHY_LOOPBACK ((uint16_t)0x4000)
+#define PHY_FULLDUPLEX_100M ((uint16_t)0x2100)
+#define PHY_HALFDUPLEX_100M ((uint16_t)0x2000)
+#define PHY_FULLDUPLEX_10M ((uint16_t)0x0100)
+#define PHY_HALFDUPLEX_10M ((uint16_t)0x0000)
+#define PHY_AUTONEGOTIATION ((uint16_t)0x1000)
+#define PHY_RESTART_AUTONEGOTIATION ((uint16_t)0x0200)
+#define PHY_POWERDOWN ((uint16_t)0x0800)
+#define PHY_ISOLATE ((uint16_t)0x0400)
+
+#define PHY_AUTONEGO_COMPLETE ((uint16_t)0x0020)
+#define PHY_LINKED_STATUS ((uint16_t)0x0004)
+#define PHY_JABBER_DETECTION ((uint16_t)0x0002)
+
+/* Extended PHY Registers */
+#define PHY_SR ((uint16_t)0x0010)
+#define PHY_MICR ((uint16_t)0x0011)
+#define PHY_MISR ((uint16_t)0x0012)
+
+#define PHY_LINK_STATUS ((uint16_t)0x0001)
+#define PHY_SPEED_STATUS ((uint16_t)0x0002)
+#define PHY_DUPLEX_STATUS ((uint16_t)0x0004)
+
+#define PHY_MICR_INT_EN ((uint16_t)0x0002)
+#define PHY_MICR_INT_OE ((uint16_t)0x0001)
+
+#define PHY_MISR_LINK_INT_EN ((uint16_t)0x0020)
+#define PHY_LINK_INTERRUPT ((uint16_t)0x2000)
+
+// SPI config
+#define USE_SPI_CRC 1U
+
+/** HAL Headers: comment out defines + include to remove **/
+/* primary HAL headers */
+#define HAL_CORTEX_MODULE_ENABLED
+#include "stm32f7xx_hal_cortex.h"
+
+#define HAL_DMA_MODULE_ENABLED
+#include "stm32f7xx_hal_dma.h"
+
+#define HAL_EXTI_MODULE_ENABLED
+#include "stm32f7xx_hal_exti.h"
+
+#define HAL_GPIO_MODULE_ENABLED
+#include "stm32f7xx_hal_gpio.h"
+
+#define HAL_RCC_MODULE_ENABLED
+#include "stm32f7xx_hal_rcc.h"
+
+/* remaining headers (can be commented out if desired) */
+#define HAL_ADC_MODULE_ENABLED
+#define USE_HAL_ADC_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_adc.h"
+
+#define HAL_CAN_MODULE_ENABLED
+#define USE_HAL_CAN_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_can.h"
+
+// #define HAL_CAN_LEGACY_MODULE_ENABLED
+// #include "stm32f7xx_hal_can_legacy.h"
+
+#define HAL_CEC_MODULE_ENABLED
+#define USE_HAL_CEC_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_cec.h"
+
+#define HAL_CRC_MODULE_ENABLED
+#include "stm32f7xx_hal_crc.h"
+
+#define HAL_CRYP_MODULE_ENABLED
+#define USE_HAL_CRYP_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_cryp.h"
+
+#define HAL_DAC_MODULE_ENABLED
+#define USE_HAL_DAC_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_dac.h"
+
+#define HAL_DCMI_MODULE_ENABLED
+#define USE_HAL_DCMI_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_dcmi.h"
+
+#define HAL_DMA2D_MODULE_ENABLED
+#define USE_HAL_DMA2D_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_dma2d.h"
+
+#define HAL_DFSDM_MODULE_ENABLED
+#define USE_HAL_DFSDM_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_dfsdm.h"
+
+#define HAL_DSI_MODULE_ENABLED
+#define USE_HAL_DSI_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_dsi.h"
+
+#define HAL_ETH_MODULE_ENABLED
+#define USE_HAL_ETH_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_eth.h"
+
+#define HAL_FLASH_MODULE_ENABLED
+#include "stm32f7xx_hal_flash.h"
+
+#define HAL_HASH_MODULE_ENABLED
+#define USE_HAL_HASH_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_hash.h"
+
+#define HAL_HCD_MODULE_ENABLED
+#define USE_HAL_HCD_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_hcd.h"
+
+#define HAL_I2C_MODULE_ENABLED
+#define USE_HAL_I2C_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_i2c.h"
+
+#define HAL_I2S_MODULE_ENABLED
+#define USE_HAL_I2S_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_i2s.h"
+
+#define HAL_IRDA_MODULE_ENABLED
+#define USE_HAL_IRDA_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_irda.h"
+
+#define HAL_IWDG_MODULE_ENABLED
+#include "stm32f7xx_hal_iwdg.h"
+
+#define HAL_LPTIM_MODULE_ENABLED
+#define USE_HAL_LPTIM_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_lptim.h"
+
+#define HAL_LTDC_MODULE_ENABLED
+#define USE_HAL_LTDC_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_ltdc.h"
+
+#define HAL_MMC_MODULE_ENABLED
+#define USE_HAL_MMC_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_mmc.h"
+
+#define HAL_NAND_MODULE_ENABLED
+#define USE_HAL_NAND_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_nand.h"
+
+#define HAL_NOR_MODULE_ENABLED
+#define USE_HAL_NOR_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_nor.h"
+
+#define HAL_PCD_MODULE_ENABLED
+#define USE_HAL_PCD_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_pcd.h"
+
+#define HAL_PWR_MODULE_ENABLED
+#include "stm32f7xx_hal_pwr.h"
+
+#define HAL_QSPI_MODULE_ENABLED
+#define USE_HAL_QSPI_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_qspi.h"
+
+#define HAL_RNG_MODULE_ENABLED
+#define USE_HAL_RNG_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_rng.h"
+
+#define HAL_RTC_MODULE_ENABLED
+#define USE_HAL_RTC_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_rtc.h"
+
+#define HAL_SAI_MODULE_ENABLED
+#define USE_HAL_SAI_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_sai.h"
+
+#define HAL_SD_MODULE_ENABLED
+#define USE_HAL_SD_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_sd.h"
+
+#define HAL_SDRAM_MODULE_ENABLED
+#define USE_HAL_SDRAM_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_sdram.h"
+
+#define HAL_SMARTCARD_MODULE_ENABLED
+#define USE_HAL_SMARTCARD_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_smartcard.h"
+
+#define HAL_SMBUS_MODULE_ENABLED
+#define USE_HAL_SMBUS_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_smbus.h"
+
+#define HAL_SPDIFRX_MODULE_ENABLED
+#define USE_HAL_SPDIFRX_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_spdifrx.h"
+
+#define HAL_SPI_MODULE_ENABLED
+#define USE_HAL_SPI_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_spi.h"
+
+#define HAL_SRAM_MODULE_ENABLED
+#define USE_HAL_SRAM_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_sram.h"
+
+#define HAL_TIM_MODULE_ENABLED
+#define USE_HAL_TIM_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_tim.h"
+
+#define HAL_UART_MODULE_ENABLED
+#define USE_HAL_UART_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_uart.h"
+
+#define HAL_USART_MODULE_ENABLED
+#define USE_HAL_USART_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_usart.h"
+
+#define HAL_WWDG_MODULE_ENABLED
+#define USE_HAL_WWDG_REGISTER_CALLBACKS 0U
+#include "stm32f7xx_hal_wwdg.h"
diff --git a/targets/stm32f769i_disc0_stm32cube/module_config_overrides.h b/targets/stm32f769i_disc0_stm32cube/module_config_overrides.h
new file mode 100644
index 0000000..c67d1ee
--- /dev/null
+++ b/targets/stm32f769i_disc0_stm32cube/module_config_overrides.h
@@ -0,0 +1,20 @@
+// 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.
+#pragma once
+
+// Configure the UART for pw_sys_io_stm32cube.
+#define PW_SYS_IO_STM32CUBE_USART_NUM 1
+#define PW_SYS_IO_STM32CUBE_GPIO_PORT A
+#define PW_SYS_IO_STM32CUBE_GPIO_TX_PIN 9
+#define PW_SYS_IO_STM32CUBE_GPIO_RX_PIN 10
diff --git a/targets/stm32f769i_disc0_stm32cube/stm32f769i_executable.gni b/targets/stm32f769i_disc0_stm32cube/stm32f769i_executable.gni
new file mode 100644
index 0000000..ce54079
--- /dev/null
+++ b/targets/stm32f769i_disc0_stm32cube/stm32f769i_executable.gni
@@ -0,0 +1,35 @@
+# 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_malloc/backend.gni")
+
+# Executable wrapper that includes some baremetal startup code.
+template("stm32f769i_executable") {
+  target("executable", target_name) {
+    forward_variables_from(invoker, "*")
+    if (!defined(deps)) {
+      deps = []
+    }
+    deps += [
+      "$dir_pigweed_experimental/targets/stm32f769i_disc0_stm32cube:pre_init",
+    ]
+    if (pw_malloc_BACKEND != "") {
+      if (!defined(configs)) {
+        configs = []
+      }
+      configs += [ "$dir_pw_malloc:pw_malloc_wrapper_config" ]
+    }
+  }
+}
diff --git a/targets/stm32f769i_disc0_stm32cube/target_toolchains.gni b/targets/stm32f769i_disc0_stm32cube/target_toolchains.gni
new file mode 100644
index 0000000..039c98c
--- /dev/null
+++ b/targets/stm32f769i_disc0_stm32cube/target_toolchains.gni
@@ -0,0 +1,175 @@
+# 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_rpc/system_server/backend.gni")
+import("$dir_pw_sys_io/backend.gni")
+import("$dir_pw_third_party/stm32cube/stm32cube.gni")
+import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
+
+_target_config = {
+  # Use the logging main.
+  pw_unit_test_MAIN = "$dir_pw_unit_test:logging_main"
+
+  # Configuration options for Pigweed executable targets.
+  pw_build_EXECUTABLE_TARGET_TYPE = "stm32f769i_executable"
+
+  pw_build_EXECUTABLE_TARGET_TYPE_FILE =
+      get_path_info("stm32f769i_executable.gni", "abspath")
+
+  # Path to the bloaty config file for the output binaries.
+  pw_bloat_BLOATY_CONFIG = "$dir_pw_boot_cortex_m/bloaty_config.bloaty"
+
+  #TODO: Fix test server: likely have to fork stm32f769i-disc0 implementation
+  # if (pw_use_test_server) {
+  #   _test_runner_script = "py/stm32f769i_disc0_utils/unit_test_client.py"
+  #   pw_unit_test_AUTOMATIC_RUNNER =
+  #       get_path_info(_test_runner_script, "abspath")
+  # }
+
+  # Facade backends
+  pw_assert_BACKEND = dir_pw_assert_basic
+  pw_boot_BACKEND = "$dir_pw_boot_cortex_m:armv7m"
+
+  # TODO(cmumford): Review backend - adding to allow compile.
+  pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+
+  ### pw_cpu_exception not yet used
+  # pw_cpu_exception_ENTRY_BACKEND =
+  #     "$dir_pw_cpu_exception_cortex_m:cpu_exception_armv7m"
+  # pw_cpu_exception_HANDLER_BACKEND = "$dir_pw_cpu_exception:basic_handler"
+  # pw_cpu_exception_SUPPORT_BACKEND =
+  #     "$dir_pw_cpu_exception_cortex_m:support_armv7m"
+  pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
+      "$dir_pw_sync_baremetal:interrupt_spin_lock"
+  pw_log_BACKEND = dir_pw_log_basic
+  pw_sys_io_BACKEND = dir_pw_sys_io_stm32cube
+  pw_sys_io_stm32cube_CONFIG = "$dir_pigweed_experimental/targets/stm32f769i_disc0_stm32cube:module_config_overrides"
+
+  pw_third_party_freertos_CONFIG = "$dir_pigweed_experimental/targets/stm32f769i_disc0_stm32cube:stm32f7xx_freertos_config"
+  pw_third_party_freertos_PORT =
+      "$dir_pigweed_experimental/third_party/freertos:arm_cm7_freertos_port"
+
+  #TODO: remove dependency on stm32f769i-disc0 rpc server.
+  pw_rpc_system_server_BACKEND =
+      "$dir_pigweed/targets/stm32f769i_disc0:system_rpc_server"
+  pw_malloc_BACKEND = dir_pw_malloc_freelist
+
+  pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
+    "PW_BOOT_FLASH_BEGIN=0x08000400",
+    "PW_BOOT_FLASH_SIZE=2048K",
+
+    # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
+    # least 6K bytes in heap when using pw_malloc_freelist. The heap size
+    # required for tests should be investigated.
+    "PW_BOOT_HEAP_SIZE=7K",
+    "PW_BOOT_MIN_STACK_SIZE=8K",
+    "PW_BOOT_RAM_BEGIN=0x20000000",
+    "PW_BOOT_RAM_SIZE=512K",
+    "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
+    "PW_BOOT_VECTOR_TABLE_SIZE=0x400",
+  ]
+
+  pw_build_LINK_DEPS = [
+    "$dir_pw_assert:impl",
+    "$dir_pw_log:impl",
+  ]
+
+  current_cpu = "arm"
+  current_os = ""
+
+  pw_board_led_BACKEND = dir_pw_board_led_stm32cube
+  pw_board_led_stm32cube_gpio_port = "J"
+  pw_board_led_stm32cube_gpio_pin = "13"
+  app_common_BACKEND = "//applications/app_common_impl:stm32cube"
+  pw_lcd_width = "320"
+  pw_lcd_height = "240"
+  pw_lcd_cs_port_char = "C"
+  pw_lcd_cs_pin_num = "2"
+  pw_lcd_dc_port_char = "D"
+  pw_lcd_dc_pin_num = "13"
+  pw_lcd_rst_pin_num = "-1"
+
+  pw_spin_delay_BACKEND = dir_pw_spin_delay_stm32cube
+
+  # Configure backend for pw_touchscreen
+  pw_touchscreen_BACKEND = "$dir_pw_touchscreen_null"
+
+  dir_pw_third_party_stm32cube = dir_pw_third_party_stm32cube_f7
+  pw_third_party_stm32cube_PRODUCT = "STM32F769xx"
+  pw_third_party_stm32cube_CONFIG = "$dir_pigweed_experimental/targets/stm32f769i_disc0_stm32cube:stm32f7xx_hal_config"
+  pw_third_party_stm32cube_CORE_INIT = ""
+}
+
+_toolchain_properties = {
+  final_binary_extension = ".elf"
+}
+
+_target_default_configs = [
+  "$dir_pw_build:extra_strict_warnings",
+  "$dir_pw_toolchain/arm_gcc:enable_float_printf",
+]
+
+pw_target_toolchain_stm32f769i_disc0 = {
+  _excluded_members = [
+    "defaults",
+    "name",
+  ]
+
+  debug = {
+    name = "stm32f769i_disc0_stm32cube_debug"
+    _toolchain_base = pw_toolchain_arm_gcc.cortex_m7f_debug
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    forward_variables_from(_toolchain_properties, "*")
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*")
+      forward_variables_from(_target_config, "*")
+      default_configs += _target_default_configs
+    }
+  }
+
+  speed_optimized = {
+    name = "stm32f769i_disc0_stm32cube_speed_optimized"
+    _toolchain_base = pw_toolchain_arm_gcc.cortex_m7f_speed_optimized
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    forward_variables_from(_toolchain_properties, "*")
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*")
+      forward_variables_from(_target_config, "*")
+      default_configs += _target_default_configs
+    }
+  }
+
+  size_optimized = {
+    name = "stm32f769i_disc0_stm32cube_size_optimized"
+    _toolchain_base = pw_toolchain_arm_gcc.cortex_m7f_size_optimized
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    forward_variables_from(_toolchain_properties, "*")
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*")
+      forward_variables_from(_target_config, "*")
+      default_configs += _target_default_configs
+    }
+  }
+}
+
+# This list just contains the members of the above scope for convenience to make
+# it trivial to generate all the toolchains in this file via a
+# `generate_toolchains` target.
+pw_target_toolchain_stm32f769i_disc0_list = [
+  pw_target_toolchain_stm32f769i_disc0.debug,
+  pw_target_toolchain_stm32f769i_disc0.speed_optimized,
+  pw_target_toolchain_stm32f769i_disc0.size_optimized,
+]
diff --git a/targets/stm32f769i_disc0_stm32cube/vector_table.c b/targets/stm32f769i_disc0_stm32cube/vector_table.c
new file mode 100644
index 0000000..ea2a340
--- /dev/null
+++ b/targets/stm32f769i_disc0_stm32cube/vector_table.c
@@ -0,0 +1,81 @@
+// 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.
+
+#include <stdbool.h>
+
+#include "pw_boot/boot.h"
+#include "pw_boot_cortex_m/boot.h"
+#include "stm32f7xx.h"
+
+// Default handler to insert into the ARMv7-M vector table (below).
+// This function exists for convenience. If a device isn't doing what you
+// expect, it might have hit a fault and ended up here.
+static void DefaultFaultHandler(void) {
+  while (true) {
+    // Wait for debugger to attach.
+  }
+}
+
+// This is the device's interrupt vector table. It's not referenced in any
+// code because the platform (STM32F7xx) expects this table to be present at the
+// beginning of flash. The exact address is specified in the pw_boot_armv7m
+// configuration as part of the target config.
+//
+// For more information, see ARMv7-M Architecture Reference Manual DDI 0403E.b
+// section B1.5.3.
+
+// This typedef is for convenience when building the vector table. With the
+// exception of SP_main (0th entry in the vector table), all the entries of the
+// vector table are function pointers.
+typedef void (*InterruptHandler)(void);
+
+// This is the timer interrupt handler implemented by the stm32cubef7 timer
+// template.
+void TIM6_DAC_IRQHandler(void);
+
+// Interrupt handlers critical for OS operation.
+void SVC_Handler(void);
+void PendSV_Handler(void);
+void SysTick_Handler(void);
+
+PW_KEEP_IN_SECTION(".vector_table")
+const InterruptHandler vector_table[] = {
+    // The starting location of the stack pointer.
+    // This address is NOT an interrupt handler/function pointer, it is simply
+    // the address that the main stack pointer should be initialized to. The
+    // value is reinterpret casted because it needs to be in the vector table.
+    [0] = (InterruptHandler)(&pw_boot_stack_high_addr),
+
+    // Reset handler, dictates how to handle reset interrupt. This is the
+    // address that the Program Counter (PC) is initialized to at boot.
+    [1] = pw_boot_Entry,
+
+    // NMI handler.
+    [2] = DefaultFaultHandler,
+    // HardFault handler.
+    [3] = DefaultFaultHandler,
+    // 4-6: Specialized fault handlers.
+    // 7-10: Reserved.
+    // SVCall handler.
+    [11] = SVC_Handler,
+    // DebugMon handler.
+    [12] = DefaultFaultHandler,
+    // 13: Reserved.
+    // PendSV handler.
+    [14] = PendSV_Handler,
+    // SysTick handler.
+    [15] = SysTick_Handler,
+    // stm32f7xx_hal sys-tick handler.
+    [TIM6_DAC_IRQn + 16] = TIM6_DAC_IRQHandler,
+};
diff --git a/third_party/freertos/BUILD.gn b/third_party/freertos/BUILD.gn
index 564656a..d4be44c 100644
--- a/third_party/freertos/BUILD.gn
+++ b/third_party/freertos/BUILD.gn
@@ -37,6 +37,15 @@
   visibility = [ ":*" ]
 }
 
+config("cm7_public_config") {
+  include_dirs = [
+    "$dir_freertos/include",
+    "$dir_freertos/portable/GCC/ARM_CM7/r0p1",
+  ]
+  cflags = [ "-Wno-cast-qual" ]
+  visibility = [ ":*" ]
+}
+
 config("cm33_public_config") {
   include_dirs = [
     "$dir_freertos/include",
@@ -79,6 +88,16 @@
   ]
 }
 
+pw_source_set("arm_cm7_freertos_port") {
+  public_configs = [ ":cm7_public_config" ]
+  deps = [ "$pw_third_party_freertos_CONFIG" ]
+  public = []
+  sources = [
+    "$dir_freertos/portable/GCC/ARM_CM7/r0p1/port.c",
+    "$dir_freertos/portable/MemMang/heap_4.c",
+  ]
+}
+
 pw_source_set("arm_cm33_ntz_freertos_port") {
   public_configs = [
     ":cm33_public_config",