pw_display: Facade backend implementations and demo

- pw_display
  - pw_display_null
  - pw_display_host_imgui
  - pw_display_pico_ili9341
  - pw_display_stm32f429i_disc1_stm32cube_ili9341
  - pw_display_teensy_ili9341
- pw_touchscreen
  - pw_touchscreen_null
  - pw_touchscreen_teensy_xpt2046
  - pw_touchscreen_teensy_stmpe610

Change-Id: I898efd067ad1b424a5789033091c2267216d8e19
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/experimental/+/70224
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Reviewed-by: Armando Montanez <amontanez@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 153cbd4..ec4fdae 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -41,6 +41,9 @@
     "//pw_color:tests_run(//targets/host:host_debug_tests)",
     "//pw_draw:tests_run(//targets/host:host_debug_tests)",
     "//pw_framebuffer:tests_run(//targets/host:host_debug_tests)",
+
+    # See //applications/pw_lcd_display_host_imgui/README.md for instructions.
+    "//applications/terminal_display:all(//targets/host:host_debug)",
   ]
 }
 
@@ -50,14 +53,20 @@
     deps = [ ":arduino_tests(//targets/arduino:arduino_debug_tests)" ]
 
     if (pw_arduino_build_CORE_NAME == "teensy") {
-      deps += []
+      deps += [
+        # See //pw_lcd_display_teensy_ili9341/README.md for instructions.
+        "//applications/terminal_display:all(//targets/arduino:arduino_debug)",
+      ]
     }
   }
 }
 
 group("pico") {
   if (PICO_SRC_DIR != "") {
-    deps = [ ":pico_tests(//targets/rp2040)" ]
+    deps = [
+      ":pico_tests(//targets/rp2040)",
+      "//applications/terminal_display:all(//targets/rp2040)",
+    ]
   }
 }
 
@@ -133,6 +142,7 @@
     deps += [
       ":applications_tests(//targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube_debug)",
       "//applications/blinky:blinky(//targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube_debug)",
+      "//applications/terminal_display:all(//targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube_debug)",
     ]
 
     # STMicroelectronics STM32F439ZI-Nucleo applications steps.
diff --git a/applications/terminal_display/BUILD.gn b/applications/terminal_display/BUILD.gn
new file mode 100644
index 0000000..5f058ce
--- /dev/null
+++ b/applications/terminal_display/BUILD.gn
@@ -0,0 +1,56 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_tokenizer/database.gni")
+import("$dir_pw_unit_test/test.gni")
+
+group("all") {
+  deps = [ ":terminal_demo" ]
+
+  # Build tokenizer_database for elf binaries only
+  if (host_os == "linux" || current_toolchain != "//targets/host:host_debug") {
+    deps += [ ":tokenizer_database" ]
+  }
+}
+
+pw_executable("terminal_demo") {
+  sources = [ "main.cc" ]
+  deps = [
+    "$dir_pw_color",
+    "$dir_pw_coordinates",
+    "$dir_pw_display",
+    "$dir_pw_draw",
+    "$dir_pw_framebuffer",
+    "$dir_pw_log",
+    "$dir_pw_random",
+    "$dir_pw_string",
+    "$dir_pw_touchscreen",
+    "//pw_board_led",
+    "//pw_spin_delay",
+  ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+
+  if (pw_build_EXECUTABLE_TARGET_TYPE == "arduino_executable") {
+    ldflags = [ "-Wl,--print-memory-usage" ]
+  }
+}
+
+pw_tokenizer_database("tokenizer_database") {
+  database = "tokenizer_database.csv"
+  targets = [ ":terminal_demo" ]
+}
diff --git a/applications/terminal_display/main.cc b/applications/terminal_display/main.cc
new file mode 100644
index 0000000..e1b649a
--- /dev/null
+++ b/applications/terminal_display/main.cc
@@ -0,0 +1,316 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include <cstdint>
+#include <forward_list>
+
+// #define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
+
+#include "pw_board_led/led.h"
+#include "pw_color/color.h"
+#include "pw_color/colors_pico8.h"
+#include "pw_coordinates/vec2.h"
+#include "pw_coordinates/vec_int.h"
+#include "pw_display/display.h"
+#include "pw_draw/draw.h"
+#include "pw_draw/font6x8.h"
+#include "pw_draw/pigweed_farm.h"
+#include "pw_framebuffer/rgb565.h"
+#include "pw_log/log.h"
+#include "pw_spin_delay/delay.h"
+#include "pw_string/string_builder.h"
+#include "pw_touchscreen/touchscreen.h"
+
+using pw::color::colors_pico8_rgb565;
+
+namespace {
+
+typedef bool (*bool_function_pointer)();
+typedef pw::coordinates::Vec3Int (*vec3int_function_pointer)();
+pw::framebuffer::FramebufferRgb565 frame_buffer = FramebufferRgb565();
+
+const uint8_t banner[] = {
+    127, 128, 128, 128, 128, 128, 129, 32,  32,  128, 127, 32,  32,  129, 128,
+    128, 128, 127, 32,  32,  127, 128, 32,  32,  32,  32,  127, 128, 32,  127,
+    127, 128, 128, 128, 128, 127, 32,  127, 127, 128, 128, 128, 128, 127, 32,
+    127, 127, 128, 128, 128, 128, 129, 10,  127, 128, 127, 32,  32,  128, 127,
+    32,  127, 128, 127, 32,  128, 128, 127, 32,  132, 128, 127, 32,  127, 128,
+    127, 32,  128, 32,  127, 128, 32,  32,  127, 128, 32,  32,  32,  132, 32,
+    32,  127, 128, 32,  32,  32,  132, 32,  32,  127, 128, 32,  32,  132, 128,
+    130, 10,  127, 128, 129, 129, 129, 128, 127, 32,  127, 128, 127, 32,  128,
+    127, 127, 32,  129, 129, 127, 32,  127, 128, 127, 32,  128, 32,  127, 128,
+    32,  32,  127, 128, 128, 128, 32,  32,  32,  32,  127, 128, 128, 128, 32,
+    32,  32,  32,  127, 128, 32,  32,  32,  128, 130, 10,  127, 128, 132, 32,
+    32,  32,  32,  32,  127, 128, 127, 32,  127, 128, 32,  32,  32,  128, 127,
+    32,  127, 128, 127, 32,  128, 32,  127, 128, 32,  32,  127, 128, 32,  32,
+    32,  129, 32,  32,  127, 128, 32,  32,  32,  129, 32,  32,  127, 128, 32,
+    32,  129, 128, 130, 10,  127, 128, 32,  32,  32,  32,  32,  32,  127, 128,
+    127, 32,  127, 127, 128, 128, 128, 132, 32,  32,  32,  127, 128, 127, 132,
+    127, 128, 127, 32,  127, 127, 128, 128, 128, 128, 127, 32,  127, 127, 128,
+    128, 128, 128, 127, 32,  127, 127, 128, 128, 128, 128, 132, 10,  0,
+};
+
+}  // namespace
+
+int main() {
+  uint32_t frame_start_millis = pw::spin_delay::Millis();
+  uint32_t frames = 0;
+  int frames_per_second = 0;
+
+  uint32_t time_start_delta = 0;
+  uint32_t delta_time = 30000;  // Initial guess
+  float delta_seconds = delta_time * 0.000001;
+
+  uint32_t time_start_button_check = pw::spin_delay::Micros();
+  uint32_t delta_button_check = pw::spin_delay::Micros();
+
+  uint32_t time_start_game_logic = 0;
+  uint32_t delta_game_logic = 0;
+
+  uint32_t time_start_draw_screen = 0;
+  uint32_t delta_screen_draw = 0;
+
+  uint32_t time_start_screen_spi_update = 0;
+  uint32_t delta_screen_spi_update = 0;
+
+  pw::StringBuffer<64> string_buffer;
+
+  pw::board_led::Init();
+  pw::board_led::TurnOn();
+  pw::spin_delay::WaitMillis(500);
+  pw::board_led::TurnOff();
+
+  PW_LOG_INFO("pw::display::Init()");
+  uint16_t* ifb = pw::display::GetInternalFramebuffer();
+  if (ifb != NULL) {
+    frame_buffer.SetFramebufferData(ifb, 320, 240);
+  }
+  frame_buffer.SetPenColor(0x0726);
+  pw::draw::Fill(&frame_buffer);
+
+  pw::display::Init();
+  pw::touchscreen::Init();
+
+  // Touchscreen Functions
+  // Default to using pw::touchscreen
+  bool_function_pointer touch_screen_available_func =
+      &pw::touchscreen::Available;
+  vec3int_function_pointer get_touch_screen_point_func =
+      &pw::touchscreen::GetTouchPoint;
+  bool_function_pointer new_touch_event_func = &pw::touchscreen::NewTouchEvent;
+  // Check if pw::display implements touchscreen functions
+  if (pw::display::TouchscreenAvailable()) {
+    touch_screen_available_func = &pw::display::TouchscreenAvailable;
+    get_touch_screen_point_func = &pw::display::GetTouchPoint;
+    new_touch_event_func = &pw::display::NewTouchEvent;
+  }
+
+  bool touch_screen_exists = (*touch_screen_available_func)();
+
+  Vec2 screen_center =
+      Vec2(pw::display::GetWidth() / 2, pw::display::GetHeight() / 2);
+  Vec2 touch_location;
+  Vec2 touch_location_from_origin;
+  float touch_location_angle;
+  float touch_location_length;
+
+  while (1) {
+    time_start_delta = pw::spin_delay::Micros();
+
+    // Input Update Phase
+    bool new_touch_event = false;
+
+    time_start_button_check = pw::spin_delay::Micros();
+
+    if (touch_screen_exists && (*new_touch_event_func)()) {
+      pw::coordinates::Vec3Int point = (*get_touch_screen_point_func)();
+      if (point.z > 0) {
+        new_touch_event = true;
+        touch_location = Vec2(point.x, point.y);
+        touch_location_from_origin = touch_location - screen_center;
+        touch_location_angle = touch_location_from_origin.angle();
+        touch_location_length = touch_location_from_origin.length();
+
+        // touch_location_angle will fall within the range [-pi, pi]
+        // Map it to [0, 2*pi] instead.
+        if (touch_location_angle < 0)
+          touch_location_angle = kTwoPi + touch_location_angle;
+        // Map to ship angle.
+        touch_location_angle = kTwoPi - touch_location_angle;
+
+        PW_LOG_DEBUG("Touch: x:%d, y:%d, z:%d, angle:%f, degrees:%f length:%f",
+                     point.x,
+                     point.y,
+                     point.z,
+                     touch_location_angle,
+                     degrees(touch_location_angle),
+                     touch_location_length);
+      }
+    }
+
+    delta_button_check = pw::spin_delay::Micros() - time_start_button_check;
+
+    // Game Logic Phase
+    time_start_game_logic = pw::spin_delay::Micros();
+
+    delta_game_logic = pw::spin_delay::Micros() - time_start_game_logic;
+
+    // Draw Phase
+    time_start_draw_screen = pw::spin_delay::Micros();
+
+    frame_buffer.SetPenColor(0);
+    pw::draw::Fill(&frame_buffer);
+
+    pw::draw::TextArea text_area(&frame_buffer, &font6x8);
+
+    text_area.DrawCharacter('\n', 0, 1, colors_pico8_rgb565[COLOR_DARK_BLUE]);
+
+    // Draw the Pigweed "ASCII" banner.
+    int i = 0;
+    while (banner[i] > 0) {
+      text_area.DrawCharacter(banner[i], colors_pico8_rgb565[COLOR_PINK]);
+      i++;
+    }
+
+    for (int c = font6x8.starting_character; c <= font6x8.ending_character;
+         c++) {
+      if (c % 16 == 0) {
+        text_area.DrawCharacter('\n');
+      }
+      frame_buffer.SetPenColor(colors_pico8_rgb565[c % 16]);
+      text_area.DrawCharacter(c);
+    }
+
+    text_area.DrawCharacter('\n');
+    text_area.DrawCharacter('\n');
+    text_area.DrawTestFontSheet(
+        32, text_area.cursor_x, text_area.cursor_y, 0xFFFF);
+
+    int sprite_pos_x = 10;
+    int sprite_pos_y = 180;
+    int sprite_scale = 4;
+    int border_size = 8;
+
+    // Draw the dark blue border
+    pw::draw::DrawRectWH(
+        &frame_buffer,
+        sprite_pos_x - border_size,
+        sprite_pos_y - border_size,
+        pigweed_farm_sprite_sheet.width * sprite_scale + (border_size * 2),
+        pigweed_farm_sprite_sheet.height * sprite_scale + (border_size * 2),
+        colors_pico8_rgb565[COLOR_DARK_BLUE],
+        true);
+
+    // Shrink the border
+    border_size = 4;
+
+    // Draw the light blue background
+    pw::draw::DrawRectWH(
+        &frame_buffer,
+        sprite_pos_x - border_size,
+        sprite_pos_y - border_size,
+        pigweed_farm_sprite_sheet.width * sprite_scale + (border_size * 2),
+        pigweed_farm_sprite_sheet.height * sprite_scale + (border_size * 2),
+        colors_pico8_rgb565[COLOR_BLUE],
+        true);
+
+    // Draw the Sun
+    frame_buffer.SetPenColor(colors_pico8_rgb565[COLOR_ORANGE]);
+    pw::draw::DrawCircle(
+        &frame_buffer,
+        sprite_pos_x + (pigweed_farm_sprite_sheet.width * sprite_scale) - 32,
+        sprite_pos_y,
+        20,
+        true);
+    frame_buffer.SetPenColor(colors_pico8_rgb565[COLOR_YELLOW]);
+    pw::draw::DrawCircle(
+        &frame_buffer,
+        sprite_pos_x + (pigweed_farm_sprite_sheet.width * sprite_scale) - 32,
+        sprite_pos_y,
+        18,
+        true);
+
+    // Draw the farm sprite's shadow
+    pigweed_farm_sprite_sheet.current_index = 1;
+    pw::draw::DrawSprite(&frame_buffer,
+                         sprite_pos_x + 2,
+                         sprite_pos_y + 2,
+                         &pigweed_farm_sprite_sheet,
+                         4);
+
+    // Draw the farm sprite
+    pigweed_farm_sprite_sheet.current_index = 0;
+    pw::draw::DrawSprite(&frame_buffer,
+                         sprite_pos_x,
+                         sprite_pos_y,
+                         &pigweed_farm_sprite_sheet,
+                         4);
+
+    // Print FPS Stats
+    int current_line = 210;
+    int font_height = 9;
+    string_buffer.clear();
+    string_buffer.Format("%.2f (%d) FPS",
+                         1.0 / (((float)delta_time) / 1000 / 1000),
+                         frames_per_second);
+    // pw::display::DrawText(0, current_line, COLOR_WHITE,
+    // string_buffer.data());
+
+    current_line += font_height;
+    string_buffer.clear();
+    string_buffer.Format("Loop Time: %u (microseconds) = ", delta_time);
+    // pw::display::DrawText(0, current_line, COLOR_WHITE,
+    // string_buffer.data());
+
+    current_line += font_height;
+    string_buffer.clear();
+    string_buffer.Format("  input:%u + logic:%u + draw:%u + SPI:%u",
+                         delta_button_check,
+                         delta_game_logic,
+                         delta_screen_draw,
+                         delta_screen_spi_update);
+    // pw::display::DrawText(0, current_line, COLOR_WHITE,
+    // string_buffer.data());
+
+    delta_screen_draw = pw::spin_delay::Micros() - time_start_draw_screen;
+
+    // SPI Send Phase
+    time_start_screen_spi_update = pw::spin_delay::Micros();
+
+    pw::display::Update(&frame_buffer);
+
+    delta_screen_spi_update =
+        pw::spin_delay::Micros() - time_start_screen_spi_update;
+
+    // FPS Count Update
+    delta_time = pw::spin_delay::Micros() - time_start_delta;
+    delta_seconds = delta_time * 0.000001;
+
+    frames++;
+    if (pw::spin_delay::Millis() > frame_start_millis + 1000) {
+      PW_LOG_INFO("%.2f (%d) FPS",
+                  1.0 / (((float)delta_time) / 1000 / 1000),
+                  frames_per_second);
+      PW_LOG_INFO("  input:%u + logic:%u + draw:%u + SPI:%u",
+                  delta_button_check,
+                  delta_game_logic,
+                  delta_screen_draw,
+                  delta_screen_spi_update);
+
+      frames_per_second = frames;
+      frames = 0;
+
+      frame_start_millis = pw::spin_delay::Millis();
+    }
+  }
+}
diff --git a/applications/terminal_display/tokenizer_database.csv b/applications/terminal_display/tokenizer_database.csv
new file mode 100644
index 0000000..1257f1c
--- /dev/null
+++ b/applications/terminal_display/tokenizer_database.csv
@@ -0,0 +1,9 @@
+2b4c99c4,          ,"■msg♦  input:%u + logic:%u + draw:%u + SPI:%u■module♦■file♦applications/terminal_display/main.cc"
+5c6aec26,          ,"■msg♦starfield main()■module♦■file♦applications/starfield_display/main.cc"
+6b150272,          ,"■msg♦%.2f (%d) FPS■module♦■file♦applications/terminal_display/main.cc"
+b0deea6b,          ,"■msg♦Touch: x:%d, y:%d, z:%d, angle:%f, degrees:%f length:%f■module♦■file♦applications/starfield_display/main.cc"
+b758cad4,          ,"■msg♦Touch: x:%d, y:%d, z:%d, angle:%f, degrees:%f length:%f■module♦■file♦applications/terminal_display/main.cc"
+bc3cf216,          ,"■msg♦pw::display::Init()■module♦■file♦applications/terminal_display/main.cc"
+bc9b17ad,          ,"■msg♦pw::display::Init()■module♦■file♦applications/starfield_display/main.cc"
+c9c46909,          ,"■msg♦%.2f (%d) FPS■module♦■file♦applications/starfield_display/main.cc"
+fa6107af,          ,"■msg♦  input:%u + logic:%u + draw:%u + SPI:%u■module♦■file♦applications/starfield_display/main.cc"
diff --git a/build_overrides/pigweed.gni b/build_overrides/pigweed.gni
index cfdfc3f..c7d0c75 100644
--- a/build_overrides/pigweed.gni
+++ b/build_overrides/pigweed.gni
@@ -33,6 +33,16 @@
   dir_pw_color = get_path_info("//pw_color", "abspath")
   dir_pw_coordinates = get_path_info("//pw_coordinates", "abspath")
   dir_pw_display = get_path_info("//pw_display", "abspath")
+  dir_pw_display_host_imgui =
+      get_path_info("//pw_display_host_imgui", "abspath")
+  dir_pw_display_null = get_path_info("//pw_display_null", "abspath")
+  dir_pw_display_pico_ili9341 =
+      get_path_info("//pw_display_pico_ili9341", "abspath")
+  dir_pw_display_stm32f429i_disc1_stm32cube_ili9341 =
+      get_path_info("//pw_display_stm32f429i_disc1_stm32cube_ili9341",
+                    "abspath")
+  dir_pw_display_teensy_ili9341 =
+      get_path_info("//pw_display_teensy_ili9341", "abspath")
   dir_pw_draw = get_path_info("//pw_draw", "abspath")
   dir_pw_framebuffer = get_path_info("//pw_framebuffer", "abspath")
   dir_pw_spin_delay = get_path_info("//pw_spin_delay", "abspath")
@@ -45,4 +55,9 @@
   dir_pw_spin_delay_stm32cube =
       get_path_info("//pw_spin_delay_stm32cube", "abspath")
   dir_pw_touchscreen = get_path_info("//pw_touchscreen", "abspath")
+  dir_pw_touchscreen_null = get_path_info("//pw_touchscreen_null", "abspath")
+  dir_pw_touchscreen_teensy_stmpe610 =
+      get_path_info("//pw_touchscreen_teensy_stmpe610", "abspath")
+  dir_pw_touchscreen_teensy_xpt2046 =
+      get_path_info("//pw_touchscreen_teensy_xpt2046", "abspath")
 }
diff --git a/pw_display_host_imgui/BUILD.gn b/pw_display_host_imgui/BUILD.gn
new file mode 100644
index 0000000..67d67a7
--- /dev/null
+++ b/pw_display_host_imgui/BUILD.gn
@@ -0,0 +1,90 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("imgui.gni")
+
+if (dir_pw_third_party_imgui != "") {
+  config("imgui") {
+    include_dirs = [
+      "$dir_pw_third_party_imgui",
+      "$dir_pw_third_party_imgui/backends",
+      "$dir_pw_third_party_imgui/examples/libs/glfw",
+    ]
+
+    if (host_os == "linux") {
+      libs = [
+        # pkg-config --static --libs gl
+        "GL",
+
+        # pkg-config --static --libs glfw3
+        "glfw",
+        "rt",
+        "m",
+        "dl",
+      ]
+      lib_dirs = [
+        "/usr/lib/x86_64-linux-gnu",
+        "/usr/lib",
+      ]
+    } else if (host_os == "mac") {
+      # TODO(tonymd) Test Mac build
+      ldflags = [
+        "-framework OpenGL",
+        "-framework Cocoa",
+        "-framework IOKit",
+        "-framework CoreVideo",
+      ]
+      libs = [ "glfw3" ]
+      lib_dirs = [
+        "$dir_pw_third_party_glfw/lib-universal",
+        "/usr/local/lib",
+        "/opt/local/lib",
+      ]
+      include_dirs += [
+        "$dir_pw_third_party_glfw/include",
+        "/usr/local/include",
+        "/opt/local/include",
+      ]
+    } else if (host_os == "win") {
+      libs = [
+        "glfw3",
+        "gdi32",
+        "opengl32",
+        "imm32",
+      ]
+      include_dirs += [ "$dir_pw_third_party_glfw/include" ]
+      lib_dirs = [ "$dir_pw_third_party_glfw/lib-mingw-w64" ]
+    }
+  }
+
+  _imgui_sources = [
+    "$dir_pw_third_party_imgui/backends/imgui_impl_glfw.cpp",
+    "$dir_pw_third_party_imgui/backends/imgui_impl_opengl3.cpp",
+    "$dir_pw_third_party_imgui/imgui.cpp",
+    "$dir_pw_third_party_imgui/imgui_demo.cpp",
+    "$dir_pw_third_party_imgui/imgui_draw.cpp",
+    "$dir_pw_third_party_imgui/imgui_tables.cpp",
+    "$dir_pw_third_party_imgui/imgui_widgets.cpp",
+  ]
+
+  pw_source_set("pw_display_host_imgui") {
+    configs = [ ":imgui" ]
+    deps = [ "$dir_pw_display:pw_display.facade" ]
+    sources = [ "display.cc" ] + _imgui_sources
+    remove_configs = [ "$dir_pw_build:strict_warnings" ]
+  }
+}
diff --git a/pw_display_host_imgui/README.md b/pw_display_host_imgui/README.md
new file mode 100644
index 0000000..50410c8
--- /dev/null
+++ b/pw_display_host_imgui/README.md
@@ -0,0 +1,38 @@
+# pw_display_host_imgui
+
+## Setup Instructions
+
+1. Install [ImGui](https://github.com/ocornut/imgui) and [glfw](https://www.glfw.org/).
+
+   ```
+   pw package install imgui
+   pw package install glfw
+   ```
+
+2. Install host OS requiremets (only required for Linux) shown below.
+
+## ImGui Requirements
+
+### Linux
+
+- Debian / Ubuntu
+
+  ```sh
+  sudo apt install libglfw3-dev libglfw3
+  ```
+
+- Arch Linux
+
+  ```sh
+  sudo pacman -S glfw-x11
+  ```
+
+3. Compile with:
+
+```
+gn gen out --args="
+dir_pw_third_party_imgui=\"$PWD/.environment/packages/imgui\"
+"
+ninja -C out host
+./out/host_debug/obj/applications/terminal_display/bin/terminal_demo
+```
diff --git a/pw_display_host_imgui/display.cc b/pw_display_host_imgui/display.cc
new file mode 100644
index 0000000..2d6e60a
--- /dev/null
+++ b/pw_display_host_imgui/display.cc
@@ -0,0 +1,338 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// LCD Facade using imgui running on a host machine.
+// Much of this code is from the imgui example:
+// https://github.com/ocornut/imgui/tree/master/examples/example_glfw_opengl3
+// As well as the wiki page:
+// https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples
+#include "pw_display/display.h"
+
+#if defined(IMGUI_IMPL_OPENGL_ES2)
+#include <GLES2/gl2.h>
+#endif
+#include <GLFW/glfw3.h>  // Will drag system OpenGL headers
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <cinttypes>
+#include <cstdint>
+
+#include "imgui.h"
+#include "imgui_impl_glfw.h"
+#include "imgui_impl_opengl3.h"
+#include "pw_color/color.h"
+#include "pw_color/colors_pico8.h"
+#include "pw_coordinates/vec_int.h"
+#include "pw_display/display.h"
+#include "pw_framebuffer/rgb565.h"
+
+namespace {
+
+constexpr int kDisplayWidth = 320;
+constexpr int kDisplayHeight = 240;
+constexpr int kDisplayDataSize = kDisplayWidth * kDisplayHeight;
+
+uint16_t internal_framebuffer[kDisplayDataSize];
+
+// OpenGL texture data.
+GLuint lcd_pixel_data[kDisplayDataSize];
+
+// imgui state
+bool show_demo_window = true;
+bool show_another_window = false;
+ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
+GLuint lcd_texture = 0;
+GLFWwindow* window;
+int lcd_texture_display_scale = 2;
+int old_lcd_texture_display_scale = 0;
+bool lcd_texture_display_mode_nearest = true;
+bool old_lcd_texture_display_mode_nearest = true;
+
+void CleanupAndExit() {
+  ImGui_ImplOpenGL3_Shutdown();
+  ImGui_ImplGlfw_Shutdown();
+  ImGui::DestroyContext();
+
+  glfwDestroyWindow(window);
+  glfwTerminate();
+  exit(0);
+}
+
+void _SetTexturePixel(
+    GLuint x, GLuint y, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
+  // Calculate target color
+  GLuint target_color;
+  GLubyte* colors = (GLubyte*)&target_color;
+  colors[0] = r;
+  colors[1] = g;
+  colors[2] = b;
+  colors[3] = a;
+  lcd_pixel_data[y * kDisplayWidth + x] = target_color;
+}
+
+void _SetTexturePixel(GLuint x, GLuint y, uint8_t r, uint8_t g, uint8_t b) {
+  _SetTexturePixel(x, y, r, g, b, 255);
+}
+
+void _SetTexturePixel(GLuint x, GLuint y, color_rgb565_t rgb565) {
+  ColorRGBA c(rgb565);
+  _SetTexturePixel(x, y, c.r, c.g, c.b, 255);
+}
+
+void UpdateLcdTexture(FramebufferRgb565* frame_buffer) {
+  // Copy frame_buffer into lcd_pixel_data
+  for (GLuint x = 0; x < kDisplayWidth; x++) {
+    for (GLuint y = 0; y < kDisplayHeight; y++) {
+      color_rgb565_t c = frame_buffer->GetPixel(x, y);
+      _SetTexturePixel(x, y, c);
+    }
+  }
+
+  // Set current texture
+  glBindTexture(GL_TEXTURE_2D, lcd_texture);
+  // Update texture
+  glTexSubImage2D(GL_TEXTURE_2D,
+                  0,
+                  0,
+                  0,
+                  kDisplayWidth,
+                  kDisplayHeight,
+                  GL_RGBA,
+                  GL_UNSIGNED_BYTE,
+                  lcd_pixel_data);
+  // Unbind texture
+  glBindTexture(GL_TEXTURE_2D, NULL);
+}
+
+void SetupLcdTexture(GLuint* out_texture) {
+  // Create a OpenGL texture identifier
+  GLuint image_texture;
+  glGenTextures(1, &image_texture);
+  glBindTexture(GL_TEXTURE_2D, image_texture);
+
+  // Setup filtering parameters for display
+  GLuint display_mode =
+      lcd_texture_display_mode_nearest ? GL_NEAREST : GL_LINEAR;
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, display_mode);
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, display_mode);
+  glTexParameteri(GL_TEXTURE_2D,
+                  GL_TEXTURE_WRAP_S,
+                  GL_CLAMP_TO_EDGE);  // This is required on WebGL for non
+                                      // power-of-two textures
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);  // Same
+
+  // Upload pixels into texture
+#if defined(GL_UNPACK_ROW_LENGTH) && !defined(__EMSCRIPTEN__)
+  glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+#endif
+  glTexImage2D(GL_TEXTURE_2D,
+               0,
+               GL_RGBA,
+               kDisplayWidth,
+               kDisplayHeight,
+               0,
+               GL_RGBA,
+               GL_UNSIGNED_BYTE,
+               lcd_pixel_data);
+
+  glBindTexture(GL_TEXTURE_2D, NULL);
+
+  *out_texture = image_texture;
+}
+
+static void glfw_error_callback(int error, const char* description) {
+  fprintf(stderr, "Glfw Error %d: %s\n", error, description);
+}
+
+}  // namespace
+
+namespace pw::display {
+
+uint16_t* GetInternalFramebuffer() { return internal_framebuffer; }
+
+int GetWidth() { return kDisplayWidth; }
+int GetHeight() { return kDisplayHeight; }
+
+void Init() {
+  // Setup window
+  glfwSetErrorCallback(glfw_error_callback);
+  if (!glfwInit())
+    return;
+
+    // Decide GL+GLSL versions
+#if defined(IMGUI_IMPL_OPENGL_ES2)
+  // GL ES 2.0 + GLSL 100
+  const char* glsl_version = "#version 100";
+  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
+  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
+  glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
+#elif defined(__APPLE__)
+  // GL 3.2 + GLSL 150
+  const char* glsl_version = "#version 150";
+  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
+  glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 3.2+ only
+  glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // Required on Mac
+#else
+  // GL 3.0 + GLSL 130
+  const char* glsl_version = "#version 130";
+  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
+  // glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 3.2+
+  // only glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 3.0+ only
+#endif
+
+  // Create window with graphics context
+  window = glfwCreateWindow(1280, 720, "pw_display", NULL, NULL);
+  if (window == NULL)
+    return;
+  glfwMakeContextCurrent(window);
+  glfwSwapInterval(1);  // Enable vsync
+
+  // Setup Dear ImGui context
+  IMGUI_CHECKVERSION();
+  ImGui::CreateContext();
+  ImGuiIO& io = ImGui::GetIO();
+  (void)io;
+  // io.Fonts->AddFontFromFileTTF("NotoSans-Regular.ttf", 32.0);
+
+  // Enable Keyboard Controls
+  io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
+  // Enable Gamepad Controls
+  // io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
+
+  // Setup Dear ImGui style
+  ImGui::StyleColorsDark();
+  // ImGui::StyleColorsLight();
+  // ImGui::StyleColorsClassic();
+
+  // Setup Platform/Renderer backends
+  ImGui_ImplGlfw_InitForOpenGL(window, true);
+  ImGui_ImplOpenGL3_Init(glsl_version);
+
+  SetupLcdTexture(&lcd_texture);
+}
+
+void Update(pw::framebuffer::FramebufferRgb565* frame_buffer) {
+  if (old_lcd_texture_display_mode_nearest !=
+      lcd_texture_display_mode_nearest) {
+    old_lcd_texture_display_mode_nearest = lcd_texture_display_mode_nearest;
+    SetupLcdTexture(&lcd_texture);
+  }
+  UpdateLcdTexture(frame_buffer);
+
+  // Poll and handle events (inputs, window resize, etc.)
+  glfwPollEvents();
+
+  // Start the Dear ImGui frame
+  ImGui_ImplOpenGL3_NewFrame();
+  ImGui_ImplGlfw_NewFrame();
+  ImGui::NewFrame();
+
+  // 1. Show the big demo window (Most of the sample code is in
+  // ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear
+  // ImGui!).
+  if (show_demo_window) {
+    ImGui::ShowDemoWindow(&show_demo_window);
+  }
+
+  // 2. Show a simple window that we create ourselves. We use a Begin/End pair
+  // to created a named window.
+  {
+    static float f = 0.0f;
+    static int counter = 0;
+
+    ImGui::Begin("Hello, world!");  // Create a window called "Hello, world!"
+                                    // and append into it.
+
+    ImGui::Text("This is some useful text.");  // Display some text (you can use
+                                               // a format strings too)
+    ImGui::Checkbox(
+        "Demo Window",
+        &show_demo_window);  // Edit bools storing our window open/close state
+    ImGui::Checkbox("Another Window", &show_another_window);
+
+    ImGui::SliderFloat("float",
+                       &f,
+                       0.0f,
+                       1.0f);  // Edit 1 float using a slider from 0.0f to 1.0f
+    ImGui::ColorEdit3(
+        "clear color",
+        (float*)&clear_color);  // Edit 3 floats representing a color
+
+    if (ImGui::Button("Button"))  // Buttons return true when clicked (most
+                                  // widgets return true when edited/activated)
+      counter++;
+    ImGui::SameLine();
+    ImGui::Text("counter = %d", counter);
+
+    ImGui::Text("Application average %.3f ms/frame (%.1f FPS)",
+                1000.0f / ImGui::GetIO().Framerate,
+                ImGui::GetIO().Framerate);
+    ImGui::End();
+
+    ImGui::Begin("Display");
+    ImGui::Text("Pixel Size = %d x %d", kDisplayWidth, kDisplayHeight);
+    ImGui::Checkbox("Nearest neighbor", &lcd_texture_display_mode_nearest);
+    ImGui::SliderInt("Integer Scaling", &lcd_texture_display_scale, 1, 10);
+    ImGui::Image((void*)(intptr_t)lcd_texture,
+                 ImVec2(lcd_texture_display_scale * kDisplayWidth,
+                        lcd_texture_display_scale * kDisplayHeight));
+    ImGui::End();
+  }
+
+  // 3. Show another simple window.
+  if (show_another_window) {
+    ImGui::Begin(
+        "Another Window",
+        &show_another_window);  // Pass a pointer to our bool variable (the
+                                // window will have a closing button that will
+                                // clear the bool when clicked)
+    ImGui::Text("Hello from another window!");
+    if (ImGui::Button("Close Me"))
+      show_another_window = false;
+    ImGui::End();
+  }
+
+  // Rendering
+  ImGui::Render();
+  int display_w, display_h;
+  glfwGetFramebufferSize(window, &display_w, &display_h);
+  glViewport(0, 0, display_w, display_h);
+  glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w);
+  glClear(GL_COLOR_BUFFER_BIT);
+  ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
+
+  glfwSwapBuffers(window);
+
+  if (glfwWindowShouldClose(window)) {
+    CleanupAndExit();
+  }
+}
+
+bool TouchscreenAvailable() { return false; }
+
+bool NewTouchEvent() { return false; }
+
+pw::coordinates::Vec3Int GetTouchPoint() {
+  pw::coordinates::Vec3Int point;
+  point.x = 0;
+  point.y = 0;
+  point.z = 0;
+  return point;
+}
+
+}  // namespace pw::display
diff --git a/pw_display_host_imgui/glfw.gni b/pw_display_host_imgui/glfw.gni
new file mode 100644
index 0000000..d73111f
--- /dev/null
+++ b/pw_display_host_imgui/glfw.gni
@@ -0,0 +1,19 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+declare_args() {
+  # Location of glfw download.
+  # Install with: pw package install glfw
+  dir_pw_third_party_glfw = ""
+}
diff --git a/pw_display_host_imgui/imgui.gni b/pw_display_host_imgui/imgui.gni
new file mode 100644
index 0000000..63b41c4
--- /dev/null
+++ b/pw_display_host_imgui/imgui.gni
@@ -0,0 +1,19 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+declare_args() {
+  # Location of imgui checkout.
+  # Install with: pw package install imgui
+  dir_pw_third_party_imgui = ""
+}
diff --git a/pw_display_null/BUILD.gn b/pw_display_null/BUILD.gn
new file mode 100644
index 0000000..7b02e55
--- /dev/null
+++ b/pw_display_null/BUILD.gn
@@ -0,0 +1,23 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_display_null") {
+  deps = [ "$dir_pw_display:pw_display.facade" ]
+  sources = [ "display.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/pw_display_null/display.cc b/pw_display_null/display.cc
new file mode 100644
index 0000000..f565d5b
--- /dev/null
+++ b/pw_display_null/display.cc
@@ -0,0 +1,48 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_display/display.h"
+
+#include <cinttypes>
+
+namespace pw::display {
+
+namespace {
+
+uint16_t internal_framebuffer[320 * 240];
+
+}  // namespace
+
+void Init() {}
+
+int GetWidth() { return 320; }
+int GetHeight() { return 240; }
+
+uint16_t* GetInternalFramebuffer() { return internal_framebuffer; }
+
+void Update(pw::framebuffer::FramebufferRgb565* frame_buffer) {}
+
+bool TouchscreenAvailable() { return false; }
+
+bool NewTouchEvent() { return false; }
+
+pw::coordinates::Vec3Int GetTouchPoint() {
+  pw::coordinates::Vec3Int point;
+  point.x = 0;
+  point.y = 0;
+  point.z = 0;
+  return point;
+}
+
+}  // namespace pw::display
diff --git a/pw_display_pico_ili9341/BUILD.gn b/pw_display_pico_ili9341/BUILD.gn
new file mode 100644
index 0000000..2c05f48
--- /dev/null
+++ b/pw_display_pico_ili9341/BUILD.gn
@@ -0,0 +1,29 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_display_pico_ili9341") {
+  deps = [
+    "$PICO_ROOT/src/common/pico_base",
+    "$PICO_ROOT/src/common/pico_stdlib",
+    "$PICO_ROOT/src/rp2_common/hardware_spi",
+    "$dir_pw_display:pw_display.facade",
+  ]
+  sources = [ "display.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/pw_display_pico_ili9341/display.cc b/pw_display_pico_ili9341/display.cc
new file mode 100644
index 0000000..188669f
--- /dev/null
+++ b/pw_display_pico_ili9341/display.cc
@@ -0,0 +1,339 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_display/display.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <cinttypes>
+#include <cstdint>
+
+#include "hardware/spi.h"
+#include "pico/stdlib.h"
+#include "pw_color/color.h"
+#include "pw_framebuffer/rgb565.h"
+
+namespace pw::display {
+
+namespace {
+
+#define SPI_PORT spi0
+
+#define ILI9341_MADCTL 0x36
+#define MADCTL_MY 0x80
+#define MADCTL_MX 0x40
+#define MADCTL_MV 0x20
+#define MADCTL_ML 0x10
+#define MADCTL_RGB 0x00
+#define MADCTL_BGR 0x08
+#define MADCTL_MH 0x04
+
+#define ILI9341_PIXEL_FORMAT_SET 0x3A
+
+constexpr int TFT_DC = 18;   // stm32f429i-disc1: PD13
+constexpr int TFT_CS = 5;    // stm32f429i-disc1: PC2
+constexpr int TFT_RST = 19;  // stm32f429i-disc1: NRST
+constexpr int TFT_MOSI = 3;  // stm32f429i-disc1: PF9
+constexpr int TFT_SCLK = 2;  // stm32f429i-disc1: PF7
+constexpr int TFT_MISO = 4;  // stm32f429i-disc1: PF8
+
+constexpr int kDisplayWidth = 320;
+constexpr int kDisplayHeight = 240;
+constexpr int kDisplayDataSize = kDisplayWidth * kDisplayHeight;
+uint16_t internal_framebuffer[kDisplayDataSize];
+
+// SPI Functions
+// TODO(tonymd): move to pw_spi
+static inline void ChipSelectEnable() {
+  asm volatile("nop \n nop \n nop");
+  gpio_put(TFT_CS, 0);
+  asm volatile("nop \n nop \n nop");
+}
+
+static inline void ChipSelectDisable() {
+  asm volatile("nop \n nop \n nop");
+  gpio_put(TFT_CS, 1);
+  asm volatile("nop \n nop \n nop");
+}
+
+static inline void DataCommandEnable() {
+  asm volatile("nop \n nop \n nop");
+  gpio_put(TFT_DC, 0);
+  asm volatile("nop \n nop \n nop");
+}
+
+static inline void DataCommandDisable() {
+  asm volatile("nop \n nop \n nop");
+  gpio_put(TFT_DC, 1);
+  asm volatile("nop \n nop \n nop");
+}
+
+static void inline SPISendByte(uint8_t data) {
+  ChipSelectEnable();
+  DataCommandDisable();
+  spi_write_blocking(SPI_PORT, &data, 1);
+  ChipSelectDisable();
+}
+
+static void inline SPISendShort(uint16_t data) {
+  ChipSelectEnable();
+  DataCommandDisable();
+
+  uint8_t shortBuffer[2];
+
+  shortBuffer[0] = (uint8_t)(data >> 8);
+  shortBuffer[1] = (uint8_t)data;
+
+  spi_write_blocking(SPI_PORT, shortBuffer, 2);
+
+  ChipSelectDisable();
+}
+
+static void inline SPISendCommand(uint8_t command) {
+  // set data/command to command mode (low).
+  DataCommandEnable();
+
+  ChipSelectEnable();
+
+  // send the command to the display.
+  spi_write_blocking(SPI_PORT, &command, 1);
+
+  // put the display back into data mode (high).
+  DataCommandDisable();
+
+  ChipSelectDisable();
+}
+
+}  // namespace
+
+void Init() {
+  stdio_init_all();
+  uint actual_baudrate = spi_init(SPI_PORT, 31250000);
+  spi_set_format(SPI_PORT, 8, (spi_cpol_t)1, (spi_cpha_t)1, SPI_MSB_FIRST);
+
+  // Init pico SPI
+  printf("Actual Baudrate: %i\n", actual_baudrate);
+  gpio_set_function(TFT_MISO, GPIO_FUNC_SPI);
+  gpio_set_function(TFT_SCLK, GPIO_FUNC_SPI);
+  gpio_set_function(TFT_MOSI, GPIO_FUNC_SPI);
+
+  gpio_init(TFT_CS);
+  gpio_init(TFT_DC);
+  gpio_init(TFT_RST);
+
+  gpio_set_dir(TFT_CS, GPIO_OUT);
+  gpio_set_dir(TFT_DC, GPIO_OUT);
+  gpio_set_dir(TFT_RST, GPIO_OUT);
+  gpio_put(TFT_CS, 1);
+  gpio_put(TFT_DC, 0);
+  gpio_put(TFT_RST, 0);
+
+  // Init Display
+  ChipSelectEnable();
+
+  // Toggle Reset pin
+  gpio_put(TFT_RST, 0);
+  sleep_ms(100);
+  gpio_put(TFT_RST, 1);
+  sleep_ms(100);
+
+  // ILI9341 Init Sequence:
+  SPISendCommand(0xEF);
+  SPISendByte(0x03);
+  SPISendByte(0x80);
+  SPISendByte(0x02);
+
+  // ?
+  SPISendCommand(0xCF);
+  SPISendByte(0x00);
+  SPISendByte(0xC1);
+  SPISendByte(0x30);
+
+  // ?
+  SPISendCommand(0xED);
+  SPISendByte(0x64);
+  SPISendByte(0x03);
+  SPISendByte(0x12);
+  SPISendByte(0x81);
+
+  // ?
+  SPISendCommand(0xE8);
+  SPISendByte(0x85);
+  SPISendByte(0x00);
+  SPISendByte(0x78);
+
+  // ?
+  SPISendCommand(0xCB);
+  SPISendByte(0x39);
+  SPISendByte(0x2C);
+  SPISendByte(0x00);
+  SPISendByte(0x34);
+  SPISendByte(0x02);
+
+  // ?
+  SPISendCommand(0xF7);
+  SPISendByte(0x20);
+
+  // ?
+  SPISendCommand(0xEA);
+  SPISendByte(0x00);
+  SPISendByte(0x00);
+
+  // Power control
+  SPISendCommand(0xC0);
+  SPISendByte(0x23);
+
+  // Power control
+  SPISendCommand(0xC1);
+  SPISendByte(0x10);
+
+  // VCM control
+  SPISendCommand(0xC5);
+  SPISendByte(0x3e);
+  SPISendByte(0x28);
+
+  // VCM control
+  SPISendCommand(0xC7);
+  SPISendByte(0x86);
+
+  SPISendCommand(ILI9341_MADCTL);
+  // Rotation
+  // SPISendByte(MADCTL_MX | MADCTL_BGR); // 0
+  // SPISendByte(MADCTL_MV | MADCTL_BGR); // 1 landscape
+  // SPISendByte(MADCTL_MY | MADCTL_BGR); // 2
+  SPISendByte(MADCTL_MX | MADCTL_MY | MADCTL_MV | MADCTL_BGR);  // 3 landscape
+
+  SPISendCommand(ILI9341_PIXEL_FORMAT_SET);
+  SPISendByte(0x55);  // 16 bits / pixel
+  // SPISendByte(0x36);  // 18 bits / pixel
+
+  // Frame Control (Normal Mode)
+  SPISendCommand(0xB1);
+  SPISendByte(0x00);  // division ratio
+  SPISendByte(0x1F);  // 61 Hz
+  // SPISendByte(0x1B);  // 70 Hz - default
+  // SPISendByte(0x18);  // 79 Hz
+  // SPISendByte(0x10);  // 119 Hz
+
+  // Display Function Control
+  SPISendCommand(0xB6);
+  SPISendByte(0x08);
+  SPISendByte(0x82);
+  SPISendByte(0x27);
+
+  // Gamma Function Disable?
+  SPISendCommand(0xF2);
+  SPISendByte(0x00);
+
+  // Gamma Set
+  SPISendCommand(0x26);
+  SPISendByte(0x01);
+
+  // Positive Gamma Correction
+  SPISendCommand(0xE0);
+  SPISendByte(0x0F);
+  SPISendByte(0x31);
+  SPISendByte(0x2B);
+  SPISendByte(0x0C);
+  SPISendByte(0x0E);
+  SPISendByte(0x08);
+  SPISendByte(0x4E);
+  SPISendByte(0xF1);
+  SPISendByte(0x37);
+  SPISendByte(0x07);
+  SPISendByte(0x10);
+  SPISendByte(0x03);
+  SPISendByte(0x0E);
+  SPISendByte(0x09);
+  SPISendByte(0x00);
+
+  // Negative Gamma Correction
+  SPISendCommand(0xE1);
+  SPISendByte(0x00);
+  SPISendByte(0x0E);
+  SPISendByte(0x14);
+  SPISendByte(0x03);
+  SPISendByte(0x11);
+  SPISendByte(0x07);
+  SPISendByte(0x31);
+  SPISendByte(0xC1);
+  SPISendByte(0x48);
+  SPISendByte(0x08);
+  SPISendByte(0x0F);
+  SPISendByte(0x0C);
+  SPISendByte(0x31);
+  SPISendByte(0x36);
+  SPISendByte(0x0F);
+
+  // Exit Sleep
+  SPISendCommand(0x11);
+
+  sleep_ms(100);
+
+  // Display On
+  SPISendCommand(0x29);
+
+  sleep_ms(100);
+
+  // Normal display mode on
+  SPISendCommand(0x13);
+
+  // Setup drawing full framebuffers
+
+  // Landscape drawing
+  // Column Address Set
+  SPISendCommand(0x2A);
+  SPISendShort(0);
+  SPISendShort(319);
+  // Page Address Set
+  SPISendCommand(0x2B);
+  SPISendShort(0);
+  SPISendShort(239);
+
+  sleep_ms(10);
+
+  SPISendCommand(0x2C);
+
+  ChipSelectEnable();
+  DataCommandDisable();
+
+  sleep_ms(10);
+
+  // SPI writes from here out use 16 data bits (for drawing the framebuffer).
+  spi_set_format(SPI_PORT, 16, (spi_cpol_t)1, (spi_cpha_t)1, SPI_MSB_FIRST);
+}
+
+int GetWidth() { return kDisplayWidth; }
+int GetHeight() { return kDisplayHeight; }
+
+uint16_t* GetInternalFramebuffer() { return internal_framebuffer; }
+
+void Update(pw::framebuffer::FramebufferRgb565* frame_buffer) {
+  spi_write16_blocking(SPI_PORT, internal_framebuffer, kDisplayDataSize);
+}
+
+bool TouchscreenAvailable() { return false; }
+
+bool NewTouchEvent() { return false; }
+
+pw::coordinates::Vec3Int GetTouchPoint() {
+  pw::coordinates::Vec3Int point;
+  point.x = 0;
+  point.y = 0;
+  point.z = 0;
+  return point;
+}
+
+}  // namespace pw::display
diff --git a/pw_display_stm32f429i_disc1_stm32cube_ili9341/BUILD.gn b/pw_display_stm32f429i_disc1_stm32cube_ili9341/BUILD.gn
new file mode 100644
index 0000000..713d569
--- /dev/null
+++ b/pw_display_stm32f429i_disc1_stm32cube_ili9341/BUILD.gn
@@ -0,0 +1,26 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_display_stm32f429i_disc1_stm32cube_ili9341") {
+  deps = [
+    "$dir_pw_display:pw_display.facade",
+    "$dir_pw_third_party/stm32cube",
+  ]
+  sources = [ "display.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/pw_display_stm32f429i_disc1_stm32cube_ili9341/display.cc b/pw_display_stm32f429i_disc1_stm32cube_ili9341/display.cc
new file mode 100644
index 0000000..20225f9
--- /dev/null
+++ b/pw_display_stm32f429i_disc1_stm32cube_ili9341/display.cc
@@ -0,0 +1,392 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_display/display.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <cinttypes>
+#include <cstdint>
+
+#include "pw_color/color.h"
+#include "pw_framebuffer/rgb565.h"
+#include "stm32cube/stm32cube.h"
+#include "stm32f4xx_hal.h"
+#include "stm32f4xx_hal_spi.h"
+
+namespace pw::display {
+
+namespace {
+
+SPI_HandleTypeDef hspi5;
+
+#define HSPI_INSTANCE &hspi5
+
+// CHIP SELECT PIN AND PORT
+#define LCD_CS_PORT GPIOC
+#define LCD_CS_PIN GPIO_PIN_2
+
+// DATA COMMAND PIN AND PORT
+#define LCD_DC_PORT GPIOD
+#define LCD_DC_PIN GPIO_PIN_13
+
+#define ILI9341_MADCTL 0x36
+#define MADCTL_MY 0x80
+#define MADCTL_MX 0x40
+#define MADCTL_MV 0x20
+#define MADCTL_ML 0x10
+#define MADCTL_RGB 0x00
+#define MADCTL_BGR 0x08
+#define MADCTL_MH 0x04
+
+#define ILI9341_PIXEL_FORMAT_SET 0x3A
+
+constexpr int kDisplayWidth = 320;
+constexpr int kDisplayHeight = 240;
+constexpr int kDisplayDataSize = kDisplayWidth * kDisplayHeight;
+
+uint16_t internal_framebuffer[kDisplayDataSize];
+
+// SPI Functions
+// TODO(tonymd): move to pw_spi
+static inline void ChipSelectEnable() {
+  HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET);
+}
+
+static inline void ChipSelectDisable() {
+  HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET);
+}
+
+static inline void DataCommandEnable() {
+  HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET);
+}
+
+static inline void DataCommandDisable() {
+  HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET);
+}
+
+static void inline SPISendByte(uint8_t data) {
+  ChipSelectEnable();
+  DataCommandDisable();
+  HAL_SPI_Transmit(HSPI_INSTANCE, &data, 1, 1);
+  ChipSelectDisable();
+}
+
+static void inline SPISendShort(uint16_t data) {
+  ChipSelectEnable();
+  DataCommandDisable();
+
+  uint8_t shortBuffer[2];
+
+  shortBuffer[0] = (uint8_t)(data >> 8);
+  shortBuffer[1] = (uint8_t)data;
+
+  HAL_SPI_Transmit(HSPI_INSTANCE, shortBuffer, 2, 1);
+
+  ChipSelectDisable();
+}
+
+static void inline SPISendCommand(uint8_t command) {
+  // set data/command to command mode (low).
+  DataCommandEnable();
+
+  ChipSelectEnable();
+
+  // send the command to the display.
+  HAL_SPI_Transmit(HSPI_INSTANCE, &command, 1, 1);
+
+  // put the display back into data mode (high).
+  DataCommandDisable();
+
+  ChipSelectDisable();
+}
+
+void MX_GPIO_Init(void) {
+  GPIO_InitTypeDef GPIO_InitStruct = {0};
+
+  // GPIO Ports Clock Enable
+  __HAL_RCC_GPIOC_CLK_ENABLE();
+  __HAL_RCC_GPIOF_CLK_ENABLE();
+  __HAL_RCC_GPIOH_CLK_ENABLE();
+  __HAL_RCC_GPIOA_CLK_ENABLE();
+  __HAL_RCC_GPIOB_CLK_ENABLE();
+  __HAL_RCC_GPIOG_CLK_ENABLE();
+  __HAL_RCC_GPIOE_CLK_ENABLE();
+  __HAL_RCC_GPIOD_CLK_ENABLE();
+
+  HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET);
+
+  GPIO_InitStruct.Pin = LCD_CS_PIN;
+  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
+  GPIO_InitStruct.Pull = GPIO_NOPULL;
+  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
+  HAL_GPIO_Init(LCD_CS_PORT, &GPIO_InitStruct);
+
+  HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET);
+
+  GPIO_InitStruct.Pin = LCD_DC_PIN;
+  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
+  GPIO_InitStruct.Pull = GPIO_NOPULL;
+  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
+  HAL_GPIO_Init(LCD_DC_PORT, &GPIO_InitStruct);
+
+  // Reset pin not connected
+
+  __HAL_RCC_SPI5_CLK_ENABLE();
+
+  // SPI5 GPIO Configuration:
+  // PF7 SPI5_SCK
+  // PF8 SPI5_MISO
+  // PF9 SPI5_MOSI
+  GPIO_InitStruct.Pin = GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9;
+  GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+  GPIO_InitStruct.Pull = GPIO_NOPULL;
+  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
+  GPIO_InitStruct.Alternate = GPIO_AF5_SPI5;
+  HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
+}
+
+void MX_SPI5_Init(void) {
+  hspi5.Instance = SPI5;
+  hspi5.Init.Mode = SPI_MODE_MASTER;
+  hspi5.Init.Direction = SPI_DIRECTION_2LINES;
+  hspi5.Init.DataSize = SPI_DATASIZE_8BIT;
+  hspi5.Init.CLKPolarity = SPI_POLARITY_LOW;
+  hspi5.Init.CLKPhase = SPI_PHASE_1EDGE;
+  hspi5.Init.NSS = SPI_NSS_SOFT;
+  hspi5.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
+  hspi5.Init.FirstBit = SPI_FIRSTBIT_MSB;
+  hspi5.Init.TIMode = SPI_TIMODE_DISABLE;
+  hspi5.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
+  hspi5.Init.CRCPolynomial = 7;
+  HAL_SPI_Init(&hspi5);
+}
+
+}  // namespace
+
+void Init() {
+  MX_GPIO_Init();
+  MX_SPI5_Init();
+  // CS OFF
+  HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET);
+
+  // Init Display
+  ChipSelectEnable();
+
+  // ILI9341 Init Sequence:
+
+  // ?
+  SPISendCommand(0xEF);
+  SPISendByte(0x03);
+  SPISendByte(0x80);
+  SPISendByte(0x02);
+
+  // ?
+  SPISendCommand(0xCF);
+  SPISendByte(0x00);
+  SPISendByte(0xC1);
+  SPISendByte(0x30);
+
+  // ?
+  SPISendCommand(0xED);
+  SPISendByte(0x64);
+  SPISendByte(0x03);
+  SPISendByte(0x12);
+  SPISendByte(0x81);
+
+  // ?
+  SPISendCommand(0xE8);
+  SPISendByte(0x85);
+  SPISendByte(0x00);
+  SPISendByte(0x78);
+
+  // ?
+  SPISendCommand(0xCB);
+  SPISendByte(0x39);
+  SPISendByte(0x2C);
+  SPISendByte(0x00);
+  SPISendByte(0x34);
+  SPISendByte(0x02);
+
+  // ?
+  SPISendCommand(0xF7);
+  SPISendByte(0x20);
+
+  // ?
+  SPISendCommand(0xEA);
+  SPISendByte(0x00);
+  SPISendByte(0x00);
+
+  // Power control
+  SPISendCommand(0xC0);
+  SPISendByte(0x23);
+
+  // Power control
+  SPISendCommand(0xC1);
+  SPISendByte(0x10);
+
+  // VCM control
+  SPISendCommand(0xC5);
+  SPISendByte(0x3e);
+  SPISendByte(0x28);
+
+  // VCM control
+  SPISendCommand(0xC7);
+  SPISendByte(0x86);
+
+  SPISendCommand(ILI9341_MADCTL);
+  // Rotation
+  // SPISendByte(MADCTL_MX | MADCTL_BGR); // 0
+  // SPISendByte(MADCTL_MV | MADCTL_BGR); // 1 landscape
+  // SPISendByte(MADCTL_MY | MADCTL_BGR); // 2
+  SPISendByte(MADCTL_MX | MADCTL_MY | MADCTL_MV | MADCTL_BGR);  // 3 landscape
+
+  SPISendCommand(ILI9341_PIXEL_FORMAT_SET);
+  SPISendByte(0x55);  // 16 bits / pixel
+  // SPISendByte(0x36);  // 18 bits / pixel
+
+  // Frame Control (Normal Mode)
+  SPISendCommand(0xB1);
+  SPISendByte(0x00);  // division ratio
+  SPISendByte(0x1F);  // 61 Hz
+  // SPISendByte(0x1B);  // 70 Hz - default
+  // SPISendByte(0x18);  // 79 Hz
+  // SPISendByte(0x10);  // 119 Hz
+
+  // Display Function Control
+  SPISendCommand(0xB6);
+  SPISendByte(0x08);
+  SPISendByte(0x82);
+  SPISendByte(0x27);
+
+  // Gamma Function Disable?
+  SPISendCommand(0xF2);
+  SPISendByte(0x00);
+
+  // Gamma Set
+  SPISendCommand(0x26);
+  SPISendByte(0x01);
+
+  // Positive Gamma Correction
+  SPISendCommand(0xE0);
+  SPISendByte(0x0F);
+  SPISendByte(0x31);
+  SPISendByte(0x2B);
+  SPISendByte(0x0C);
+  SPISendByte(0x0E);
+  SPISendByte(0x08);
+  SPISendByte(0x4E);
+  SPISendByte(0xF1);
+  SPISendByte(0x37);
+  SPISendByte(0x07);
+  SPISendByte(0x10);
+  SPISendByte(0x03);
+  SPISendByte(0x0E);
+  SPISendByte(0x09);
+  SPISendByte(0x00);
+
+  // Negative Gamma Correction
+  SPISendCommand(0xE1);
+  SPISendByte(0x00);
+  SPISendByte(0x0E);
+  SPISendByte(0x14);
+  SPISendByte(0x03);
+  SPISendByte(0x11);
+  SPISendByte(0x07);
+  SPISendByte(0x31);
+  SPISendByte(0xC1);
+  SPISendByte(0x48);
+  SPISendByte(0x08);
+  SPISendByte(0x0F);
+  SPISendByte(0x0C);
+  SPISendByte(0x31);
+  SPISendByte(0x36);
+  SPISendByte(0x0F);
+
+  // Exit Sleep
+  SPISendCommand(0x11);
+
+  HAL_Delay(100);
+
+  // Display On
+  SPISendCommand(0x29);
+
+  HAL_Delay(100);
+
+  // Normal display mode on
+  SPISendCommand(0x13);
+
+  // Setup drawing full framebuffers
+
+  // Landscape drawing
+  // Column Address Set
+  SPISendCommand(0x2A);
+  SPISendShort(0);
+  SPISendShort(319);
+  // Page Address Set
+  SPISendCommand(0x2B);
+  SPISendShort(0);
+  SPISendShort(239);
+  SPISendCommand(0x2C);
+
+  ChipSelectEnable();
+  DataCommandDisable();
+
+  // SPI writes from here out use 16 data bits (for drawing the framebuffer).
+  hspi5.Instance = SPI5;
+  hspi5.Init.Mode = SPI_MODE_MASTER;
+  hspi5.Init.Direction = SPI_DIRECTION_2LINES;
+  hspi5.Init.DataSize = SPI_DATASIZE_16BIT;
+  hspi5.Init.CLKPolarity = SPI_POLARITY_LOW;
+  hspi5.Init.CLKPhase = SPI_PHASE_1EDGE;
+  hspi5.Init.NSS = SPI_NSS_SOFT;
+  hspi5.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
+  hspi5.Init.FirstBit = SPI_FIRSTBIT_MSB;
+  hspi5.Init.TIMode = SPI_TIMODE_DISABLE;
+  hspi5.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
+  hspi5.Init.CRCPolynomial = 7;
+  HAL_SPI_Init(&hspi5);
+}
+
+int GetWidth() { return kDisplayWidth; }
+int GetHeight() { return kDisplayHeight; }
+
+uint16_t* GetInternalFramebuffer() { return internal_framebuffer; }
+
+void Update(pw::framebuffer::FramebufferRgb565* frame_buffer) {
+  // Send 10 rows at a time
+  for (int i = 0; i < 24; i++) {
+    HAL_SPI_Transmit(HSPI_INSTANCE,
+                     (uint8_t*)&internal_framebuffer[320 * (10 * i)],
+                     320 * 10,
+                     // TODO(tonymd): Figure out what the timeout should be.
+                     // 100 here works for 10 rows of 320 pixels.
+                     100);
+  }
+}
+
+bool TouchscreenAvailable() { return false; }
+
+bool NewTouchEvent() { return false; }
+
+pw::coordinates::Vec3Int GetTouchPoint() {
+  pw::coordinates::Vec3Int point;
+  point.x = 0;
+  point.y = 0;
+  point.z = 0;
+  return point;
+}
+
+}  // namespace pw::display
diff --git a/pw_display_teensy_ili9341/BUILD.gn b/pw_display_teensy_ili9341/BUILD.gn
new file mode 100644
index 0000000..942c9e8
--- /dev/null
+++ b/pw_display_teensy_ili9341/BUILD.gn
@@ -0,0 +1,70 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_build/target_types.gni")
+
+if (pw_arduino_build_CORE_PATH != "") {
+  # Common library arguments to the arduino_builder tool.
+  _library_args = [
+    "--library-path",
+    rebase_path(
+        "$pw_arduino_build_CORE_PATH/teensy/hardware/teensy/avr/libraries"),
+    rebase_path("libraries"),
+    "--library-names",
+
+    # Arduino Core Libaries. The c/c++ files for these should only be included
+    # once in any given pw_executable.
+    "SPI",
+    "Wire",
+
+    # Additional libraries
+    # Requires SPI+Wire
+    "Adafruit_BusIO-1.7.1",
+
+    # Requires Adafruit_BusIO
+    "Adafruit-GFX-Library-1.10.4",
+
+    # Requires Adafruit-GFX
+    "ILI9341_t3n-a9a6ff5d78b1207ae7fd394f44e83e322863e55f",
+  ]
+
+  _library_c_files = exec_script(
+          arduino_builder_script,
+          arduino_show_command_args + _library_args + [ "--library-c-files" ],
+          "list lines")
+
+  _library_cpp_files = exec_script(
+          arduino_builder_script,
+          arduino_show_command_args + _library_args + [ "--library-cpp-files" ],
+          "list lines")
+
+  _library_include_dirs =
+      exec_script(arduino_builder_script,
+                  arduino_show_command_args + _library_args +
+                      [ "--library-include-dirs" ],
+                  "list lines")
+
+  pw_source_set("pw_display_teensy_ili9341") {
+    deps = [
+      "$dir_pw_display:pw_display.facade",
+      "$dir_pw_third_party/arduino:arduino_core_sources",
+    ]
+    sources = [ "display.cc" ] + _library_c_files + _library_cpp_files
+    include_dirs = _library_include_dirs
+    remove_configs = [ "$dir_pw_build:strict_warnings" ]
+  }
+}
diff --git a/pw_display_teensy_ili9341/README.md b/pw_display_teensy_ili9341/README.md
new file mode 100644
index 0000000..34b3626
--- /dev/null
+++ b/pw_display_teensy_ili9341/README.md
@@ -0,0 +1,15 @@
+# pw_display_teensy_ili9341
+
+## Setup Instructions
+
+1. Install the Teensyduino core and set the required GN args.
+
+   ```sh
+   pw package install teensy
+   ```
+
+2. Edit `//targets/arduino/target_toolchains.gni` and set:
+
+   ```
+   pw_display_BACKEND = "$dir_pw_display_teensy_ili9341"
+   ```
diff --git a/pw_display_teensy_ili9341/display.cc b/pw_display_teensy_ili9341/display.cc
new file mode 100644
index 0000000..28db61c
--- /dev/null
+++ b/pw_display_teensy_ili9341/display.cc
@@ -0,0 +1,81 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_display/display.h"
+
+#include <Arduino.h>
+
+#include <cinttypes>
+#include <cstdint>
+
+#include "ILI9341_t3n.h"
+#include "pw_color/color.h"
+#include "pw_framebuffer/rgb565.h"
+
+namespace pw::display {
+
+namespace {
+
+constexpr int TFT_DC = 10;    // stm32f429i-disc1: PD13
+constexpr int TFT_CS = 9;     // stm32f429i-disc1: PC2
+constexpr int TFT_RST = 8;    // stm32f429i-disc1: NRST
+constexpr int TFT_MOSI = 11;  // stm32f429i-disc1: PF9
+constexpr int TFT_SCLK = 13;  // stm32f429i-disc1: PF7
+constexpr int TFT_MISO = 12;  // stm32f429i-disc1: PF8
+
+constexpr int kDisplayWidth = 320;
+constexpr int kDisplayHeight = 240;
+constexpr int kDisplayDataSize = kDisplayWidth * kDisplayHeight;
+
+ILI9341_t3n tft = ILI9341_t3n(TFT_CS, TFT_DC, TFT_RST);
+uint16_t internal_framebuffer[kDisplayDataSize];
+
+}  // namespace
+
+uint16_t* GetInternalFramebuffer() { return internal_framebuffer; }
+
+void Init() {
+  for (int i = 0; i < 320 * 240; i++) {
+    internal_framebuffer[i] = 0x0726;
+  }
+  // SPI Clock: 40MHz writes & 20MHz reads.
+  // Lower write speed to 30MHz if the display doesn't work.
+  tft.begin(30000000u, 2000000);
+  tft.setFrameBuffer(internal_framebuffer);
+  tft.useFrameBuffer(1);
+  tft.setRotation(3);
+  tft.setCursor(0, 0);
+  tft.updateScreen();
+}
+
+int GetWidth() { return tft.width(); }
+int GetHeight() { return tft.height(); }
+
+void Update(pw::framebuffer::FramebufferRgb565* frame_buffer) {
+  tft.updateScreen();
+}
+
+bool TouchscreenAvailable() { return false; }
+
+bool NewTouchEvent() { return false; }
+
+pw::coordinates::Vec3Int GetTouchPoint() {
+  pw::coordinates::Vec3Int point;
+  point.x = 0;
+  point.y = 0;
+  point.z = 0;
+  return point;
+}
+
+}  // namespace pw::display
diff --git a/pw_touchscreen_null/BUILD.gn b/pw_touchscreen_null/BUILD.gn
new file mode 100644
index 0000000..d3730e3
--- /dev/null
+++ b/pw_touchscreen_null/BUILD.gn
@@ -0,0 +1,22 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_touchscreen_null") {
+  deps = [ "$dir_pw_touchscreen:pw_touchscreen.facade" ]
+  sources = [ "touchscreen.cc" ]
+}
diff --git a/pw_touchscreen_null/touchscreen.cc b/pw_touchscreen_null/touchscreen.cc
new file mode 100644
index 0000000..951e60a
--- /dev/null
+++ b/pw_touchscreen_null/touchscreen.cc
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_touchscreen/touchscreen.h"
+
+#include <cinttypes>
+
+#include "pw_coordinates/vec_int.h"
+
+namespace pw::touchscreen {
+
+void Init() {}
+
+bool Available() { return false; }
+
+bool NewTouchEvent() { return false; }
+
+pw::coordinates::Vec3Int GetTouchPoint() {
+  pw::coordinates::Vec3Int point;
+  point.x = 0;
+  point.y = 0;
+  point.z = 0;
+  return point;
+}
+
+}  // namespace pw::touchscreen
diff --git a/pw_touchscreen_teensy_stmpe610/BUILD.gn b/pw_touchscreen_teensy_stmpe610/BUILD.gn
new file mode 100644
index 0000000..116ee92
--- /dev/null
+++ b/pw_touchscreen_teensy_stmpe610/BUILD.gn
@@ -0,0 +1,64 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_build/target_types.gni")
+
+if (pw_arduino_build_CORE_PATH != "") {
+  # Get c/c++ files for Adafruit_STMPE610 Arduino Library
+  _library_args = [
+    "--library-path",
+    rebase_path(
+        "$pw_arduino_build_CORE_PATH/teensy/hardware/teensy/avr/libraries"),
+    "--library-names",
+
+    "Adafruit_STMPE610",
+  ]
+  _library_c_files = exec_script(
+          arduino_builder_script,
+          arduino_show_command_args + _library_args + [ "--library-c-files" ],
+          "list lines")
+  _library_cpp_files = exec_script(
+          arduino_builder_script,
+          arduino_show_command_args + _library_args + [ "--library-cpp-files" ],
+          "list lines")
+
+  # Get include directories
+  _library_include_only_args = [
+    "--library-path",
+    rebase_path(
+        "$pw_arduino_build_CORE_PATH/teensy/hardware/teensy/avr/libraries"),
+    "--library-names",
+    "SPI",
+    "Wire",
+    "Adafruit_STMPE610",
+  ]
+  _library_include_dirs =
+      exec_script(arduino_builder_script,
+                  arduino_show_command_args + _library_include_only_args +
+                      [ "--library-include-dirs" ],
+                  "list lines")
+  pw_source_set("pw_touchscreen_teensy_stmpe610") {
+    deps = [
+      "$dir_pw_log",
+      "$dir_pw_third_party/arduino:arduino_core_sources",
+      "$dir_pw_touchscreen:pw_touchscreen.facade",
+    ]
+    sources = [ "touchscreen.cc" ] + _library_c_files + _library_cpp_files
+    include_dirs = _library_include_dirs
+    remove_configs = [ "$dir_pw_build:strict_warnings" ]
+  }
+}
diff --git a/pw_touchscreen_teensy_stmpe610/touchscreen.cc b/pw_touchscreen_teensy_stmpe610/touchscreen.cc
new file mode 100644
index 0000000..1b72f12
--- /dev/null
+++ b/pw_touchscreen_teensy_stmpe610/touchscreen.cc
@@ -0,0 +1,81 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_touchscreen/touchscreen.h"
+
+#include <Arduino.h>
+#include <SPI.h>
+#include <Wire.h>
+
+#include <cinttypes>
+
+#include "Adafruit_STMPE610.h"
+#include "pw_coordinates/vec_int.h"
+#include "pw_log/log.h"
+
+namespace pw::touchscreen {
+namespace {
+
+constexpr int kTouchscreenMinX = 288;
+constexpr int kTouchscreenMaxX = 3715;
+constexpr int kTouchscreenMinY = 350;
+constexpr int kTouchscreenMaxY = 3800;
+
+constexpr int ScreenPixelWidth = 320;
+constexpr int ScreenPixelHeight = 240;
+
+// I2C Usage
+Adafruit_STMPE610 touch_screen = Adafruit_STMPE610();
+
+// I2C Pins;
+//   SCL: Teensy Pin 19, stm32f429i-disc1 PA8
+//   SDA: Teensy Pin 18, stm32f429i-disc1 PC9
+//   INT: stm32f429i-disc1 PA15
+//     Note: No interrupt support using the Adafruit_STMPE610 library.
+
+// Hardware SPI Usage:
+//   Adafruit_STMPE610 touch_screen Adafruit_STMPE610(uint8_t cs);
+// Software SPI Usage:
+//   Adafruit_STMPE610 touch_screen Adafruit_STMPE610(uint8_t cspin,
+//                                                    uint8_t mosipin,
+//                                                    uint8_t misopin,
+//                                                    uint8_t clkpin);
+
+}  // namespace
+
+void Init() { touch_screen.begin(); }
+
+bool Available() { return true; }
+
+bool NewTouchEvent() { return touch_screen.touched(); }
+
+pw::coordinates::Vec3Int GetTouchPoint() {
+  pw::coordinates::Vec3Int point;
+  uint16_t x, y;
+  uint8_t z;
+  touch_screen.readData(&x, &y, &z);
+  point.x = map(x, kTouchscreenMinX, kTouchscreenMaxX, 0, ScreenPixelWidth);
+  point.y = map(y, kTouchscreenMinY, kTouchscreenMaxY, 0, ScreenPixelHeight);
+  point.z = z;
+  PW_LOG_DEBUG("Touch: x:%d, y:%d, z:%d, x:%d, y:%d, z:%d",
+               (int)x,
+               (int)y,
+               (int)z,
+               point.x,
+               point.y,
+               point.z);
+  return point;
+}
+
+}  // namespace pw::touchscreen
diff --git a/pw_touchscreen_teensy_xpt2046/BUILD.gn b/pw_touchscreen_teensy_xpt2046/BUILD.gn
new file mode 100644
index 0000000..195a603
--- /dev/null
+++ b/pw_touchscreen_teensy_xpt2046/BUILD.gn
@@ -0,0 +1,64 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_build/target_types.gni")
+
+if (pw_arduino_build_CORE_PATH != "") {
+  # Get c/c++ files for XPT2046_Touchscreen Arduino Library
+  _library_args = [
+    "--library-path",
+    rebase_path(
+        "$pw_arduino_build_CORE_PATH/teensy/hardware/teensy/avr/libraries"),
+    "--library-names",
+
+    # Requires SPI.h
+    "XPT2046_Touchscreen",
+  ]
+  _library_c_files = exec_script(
+          arduino_builder_script,
+          arduino_show_command_args + _library_args + [ "--library-c-files" ],
+          "list lines")
+  _library_cpp_files = exec_script(
+          arduino_builder_script,
+          arduino_show_command_args + _library_args + [ "--library-cpp-files" ],
+          "list lines")
+
+  # Get include directories for SPI and XPT2046_Touchscreen
+  _library_include_only_args = [
+    "--library-path",
+    rebase_path(
+        "$pw_arduino_build_CORE_PATH/teensy/hardware/teensy/avr/libraries"),
+    "--library-names",
+    "SPI",
+    "XPT2046_Touchscreen",
+  ]
+  _library_include_dirs =
+      exec_script(arduino_builder_script,
+                  arduino_show_command_args + _library_include_only_args +
+                      [ "--library-include-dirs" ],
+                  "list lines")
+
+  pw_source_set("pw_touchscreen_teensy_xpt2046") {
+    deps = [
+      "$dir_pw_third_party/arduino:arduino_core_sources",
+      "$dir_pw_touchscreen:pw_touchscreen.facade",
+    ]
+    sources = [ "touchscreen.cc" ] + _library_c_files + _library_cpp_files
+    include_dirs = _library_include_dirs
+    remove_configs = [ "$dir_pw_build:strict_warnings" ]
+  }
+}
diff --git a/pw_touchscreen_teensy_xpt2046/touchscreen.cc b/pw_touchscreen_teensy_xpt2046/touchscreen.cc
new file mode 100644
index 0000000..d144ad1
--- /dev/null
+++ b/pw_touchscreen_teensy_xpt2046/touchscreen.cc
@@ -0,0 +1,55 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_touchscreen/touchscreen.h"
+
+#include <Arduino.h>
+
+#include <cinttypes>
+
+#include "XPT2046_Touchscreen.h"
+#include "pw_coordinates/vec_int.h"
+
+namespace pw::touchscreen {
+namespace {
+
+constexpr int TS_CS = 7;
+constexpr int kTouchscreenMinX = 288;
+constexpr int kTouchscreenMaxX = 3715;
+constexpr int kTouchscreenMinY = 350;
+constexpr int kTouchscreenMaxY = 3800;
+
+constexpr int ScreenPixelWidth = 320;
+constexpr int ScreenPixelHeight = 240;
+
+XPT2046_Touchscreen touch_screen(TS_CS);
+
+}  // namespace
+
+void Init() { touch_screen.begin(); }
+
+bool Available() { return true; }
+
+bool NewTouchEvent() { return touch_screen.touched(); }
+
+pw::coordinates::Vec3Int GetTouchPoint() {
+  pw::coordinates::Vec3Int point;
+  TS_Point p = touch_screen.getPoint();
+  point.x = map(p.x, kTouchscreenMinX, kTouchscreenMaxX, 0, ScreenPixelWidth);
+  point.y = map(p.y, kTouchscreenMinY, kTouchscreenMaxY, 0, ScreenPixelHeight);
+  point.z = p.z;
+  return point;
+}
+
+}  // namespace pw::touchscreen
diff --git a/targets/arduino/target_toolchains.gni b/targets/arduino/target_toolchains.gni
index 5bf5b81..2a6cb86 100644
--- a/targets/arduino/target_toolchains.gni
+++ b/targets/arduino/target_toolchains.gni
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
 # use this file except in compliance with the License. You may obtain a copy of
@@ -38,6 +38,20 @@
 
       # Configure backend for pw_spin_delay
       pw_spin_delay_BACKEND = "$dir_pw_spin_delay_arduino"
+
+      # Configure backend for pw_display
+      # pw_display_null does nothing.
+      pw_display_BACKEND = "$dir_pw_display_null"
+
+      # See //pw_display_teensy_ili9341/README.md for instructions.
+      # pw_display_BACKEND = "$dir_pw_display_teensy_ili9341"
+
+      # Configure backend for pw_touchscreen
+      pw_touchscreen_BACKEND = "$dir_pw_touchscreen_null"
+
+      # These backends are for Teensy core builds.
+      # pw_touchscreen_BACKEND = "$dir_pw_touchscreen_teensy_xpt2046"
+      # pw_touchscreen_BACKEND = "$dir_pw_touchscreen_teensy_stmpe610"
     }
   }
 
diff --git a/targets/common_backends.gni b/targets/common_backends.gni
index 00c4edf..8dbabe5 100644
--- a/targets/common_backends.gni
+++ b/targets/common_backends.gni
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
 # use this file except in compliance with the License. You may obtain a copy of
@@ -20,12 +20,12 @@
   pw_assert_BACKEND = dir_pw_assert_basic
 
   # Configure the pw_log facade for Base64 tokenized logging.
-  pw_log_BACKEND = dir_pw_log_tokenized
-  pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND =
-      "$dir_pw_log_tokenized:base64_over_hdlc"
+  # pw_log_BACKEND = dir_pw_log_tokenized
+  # pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND =
+  #     "$dir_pw_log_tokenized:base64_over_hdlc"
 
   # Alternately, configure pw_log for plain text logging
-  # pw_log_BACKEND = dir_pw_log_basic
+  pw_log_BACKEND = dir_pw_log_basic
 
   # Path to the nanopb installation. Defaults to included git module.
   dir_pw_third_party_nanopb = "//third_party/nanopb"
diff --git a/targets/host/target_toolchains.gni b/targets/host/target_toolchains.gni
index c27aabc..439bda7 100644
--- a/targets/host/target_toolchains.gni
+++ b/targets/host/target_toolchains.gni
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
 # use this file except in compliance with the License. You may obtain a copy of
@@ -16,6 +16,7 @@
 
 import("//targets/common_backends.gni")
 import("$dir_pigweed/targets/host/target_toolchains.gni")
+import("$dir_pw_display_host_imgui/imgui.gni")
 import("$dir_pw_protobuf_compiler/proto.gni")
 import("$dir_pw_third_party/nanopb/nanopb.gni")
 
@@ -49,6 +50,18 @@
       # Configure backend for pw_sys_io facade.
       pw_sys_io_BACKEND = dir_pw_sys_io_stdio
 
+      # Configure backend for pw_display
+      # pw_display_null does nothing.
+      pw_display_BACKEND = "$dir_pw_display_null"
+
+      # pw_display_host_imgui uses imgui and opengl3
+      # See //pw_display_host_imgui/README.md for instructions.
+      if (dir_pw_third_party_imgui != "") {
+        pw_display_BACKEND = "$dir_pw_display_host_imgui"
+      }
+
+      pw_touchscreen_BACKEND = "$dir_pw_touchscreen_null"
+
       pw_board_led_BACKEND = dir_pw_board_led_host
       pw_spin_delay_BACKEND = dir_pw_spin_delay_host
     }
diff --git a/targets/rp2040/BUILD.gn b/targets/rp2040/BUILD.gn
index ede06be..6f761f4 100644
--- a/targets/rp2040/BUILD.gn
+++ b/targets/rp2040/BUILD.gn
@@ -76,6 +76,16 @@
     # Configure backend for pw_spin_delay
     pw_spin_delay_BACKEND = "$dir_pw_spin_delay_pico"
 
+    # Configure backend for pw_display
+    # pw_display_null does nothing.
+    pw_display_BACKEND = "$dir_pw_display_null"
+
+    # pw_display_pico_ili9341 is for a ILI9341 screen connected over SPI.
+    # pw_display_BACKEND = "$dir_pw_display_pico_ili9341"
+
+    # Configure backend for pw_touchscreen
+    pw_touchscreen_BACKEND = "$dir_pw_touchscreen_null"
+
     current_cpu = "arm"
     current_os = ""
   }
diff --git a/targets/stm32f429i_disc1_stm32cube/target_toolchains.gni b/targets/stm32f429i_disc1_stm32cube/target_toolchains.gni
index f8c9e61..c3dac58 100644
--- a/targets/stm32f429i_disc1_stm32cube/target_toolchains.gni
+++ b/targets/stm32f429i_disc1_stm32cube/target_toolchains.gni
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
 # use this file except in compliance with the License. You may obtain a copy of
@@ -90,6 +90,11 @@
 
   pw_spin_delay_BACKEND = dir_pw_spin_delay_stm32cube
 
+  pw_display_BACKEND = dir_pw_display_stm32f429i_disc1_stm32cube_ili9341
+
+  # Configure backend for pw_touchscreen
+  pw_touchscreen_BACKEND = "$dir_pw_touchscreen_null"
+
   dir_pw_third_party_stm32cube = dir_pw_third_party_stm32cube_f4
   pw_third_party_stm32cube_PRODUCT = "STM32F429xx"
   pw_third_party_stm32cube_CONFIG =