Create pw_display_driver_st7789

Move the driver code out of pw_display_pico_st7789_spi and make
it platform agnostic.

This change introduces a new class, pw::display_driver::SPIHelper,
which is a temporary solution to the need to reconfigure the SPI
bus. This was needed because the ST7789 needs to switch between
8 and 16 bit mode for each display update. This will eventually
be removed when implementing a full fix for b/251033990, but this
is sufficient to keep the DisplayDriver platform independent
with minimal hacks.

Bug: none
Change-Id: I85917ac242074581615a0bb5ae260c2dd52a1822
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/experimental/+/119734
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
Commit-Queue: Chris Mumford <cmumford@google.com>
diff --git a/build_overrides/pigweed.gni b/build_overrides/pigweed.gni
index 6ce3c89..bc468ca 100644
--- a/build_overrides/pigweed.gni
+++ b/build_overrides/pigweed.gni
@@ -58,6 +58,9 @@
   dir_pw_display_driver_ili9341 =
       get_path_info("$dir_pigweed_experimental/pw_display_driver_ili9341",
                     "abspath")
+  dir_pw_display_driver_st7789 =
+      get_path_info("$dir_pigweed_experimental/pw_display_driver_st7789",
+                    "abspath")
   dir_pw_display_host_imgui =
       get_path_info(
           "$dir_pigweed_experimental/pw_graphics/pw_display_host_imgui",
diff --git a/pw_display_driver/public/pw_display_driver/spi_helper.h b/pw_display_driver/public/pw_display_driver/spi_helper.h
new file mode 100644
index 0000000..5678147
--- /dev/null
+++ b/pw_display_driver/public/pw_display_driver/spi_helper.h
@@ -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.
+#pragma once
+
+#include "pw_status/status.h"
+
+namespace pw::display_driver {
+
+// TODO(b/251033990): Move/refactor when eventual SPI config ability is added.
+class SPIHelper {
+ public:
+  virtual Status SetDataBits(uint8_t data_bits);
+};
+
+}  // namespace pw::display_driver
diff --git a/pw_display_driver_st7789/BUILD.gn b/pw_display_driver_st7789/BUILD.gn
new file mode 100644
index 0000000..93af99b
--- /dev/null
+++ b/pw_display_driver_st7789/BUILD.gn
@@ -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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_display_driver_st7789") {
+  public_configs = [ ":default_config" ]
+  public = [ "public/pw_display_driver_st7789/display_driver.h" ]
+  deps = [
+    "$dir_pw_log",
+    "$dir_pw_spin_delay",
+  ]
+  public_deps = [
+    "$dir_pw_digital_io",
+    "$dir_pw_display_driver:display_driver",
+    "$dir_pw_spi:device",
+  ]
+  sources = [ "display_driver.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/pw_display_driver_st7789/display_driver.cc b/pw_display_driver_st7789/display_driver.cc
new file mode 100644
index 0000000..6f9a6e0
--- /dev/null
+++ b/pw_display_driver_st7789/display_driver.cc
@@ -0,0 +1,216 @@
+// 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_driver_st7789/display_driver.h"
+
+#include <array>
+#include <cstddef>
+
+#include "pw_digital_io/digital_io.h"
+#include "pw_framebuffer/rgb565.h"
+#include "pw_spin_delay/delay.h"
+
+using pw::color::color_rgb565_t;
+using pw::digital_io::State;
+using pw::spi::ChipSelectBehavior;
+using pw::spi::Device;
+using std::array;
+using std::byte;
+
+namespace pw::display_driver {
+
+namespace {
+
+constexpr array<byte, 0> kEmptyArray;
+
+// ST7789 Display Registers
+// clang-format off
+#define ST7789_SWRESET  0x01
+#define ST7789_TEOFF    0x34
+#define ST7789_TEON     0x35
+#define ST7789_MADCTL   0x36
+#define ST7789_COLMOD   0x3A
+#define ST7789_GCTRL    0xB7
+#define ST7789_VCOMS    0xBB
+#define ST7789_LCMCTRL  0xC0
+#define ST7789_VDVVRHEN 0xC2
+#define ST7789_VRHS     0xC3
+#define ST7789_VDVS     0xC4
+#define ST7789_FRCTRL2  0xC6
+#define ST7789_PWCTRL1  0xD0
+#define ST7789_PORCTRL  0xB2
+#define ST7789_GMCTRP1  0xE0
+#define ST7789_GMCTRN1  0xE1
+#define ST7789_INVOFF   0x20
+#define ST7789_SLPOUT   0x11
+#define ST7789_DISPON   0x29
+#define ST7789_GAMSET   0x26
+#define ST7789_DISPOFF  0x28
+#define ST7789_RAMWR    0x2C
+#define ST7789_INVON    0x21
+#define ST7789_CASET    0x2A
+#define ST7789_RASET    0x2B
+
+// MADCTL Bits (See page 215: MADCTL (36h): Memory Data Access Control)
+#define ST7789_MADCTL_ROW_ORDER   0b10000000
+#define ST7789_MADCTL_COL_ORDER   0b01000000
+#define ST7789_MADCTL_SWAP_XY     0b00100000
+#define ST7789_MADCTL_SCAN_ORDER  0b00010000
+#define ST7789_MADCTL_RGB_BGR     0b00001000
+#define ST7789_MADCTL_HORIZ_ORDER 0b00000100
+// clang-format on
+
+}  // namespace
+
+DisplayDriverST7789::DisplayDriverST7789(const Config& config)
+    : data_cmd_gpio_(config.data_cmd_gpio),
+      reset_gpio_(config.reset_gpio),
+      spi_device_(config.spi_device),
+      spi_helper_(config.spi_helper),
+      screen_width_(config.screen_width),
+      screen_height_(config.screen_height) {}
+
+void DisplayDriverST7789::SetMode(Mode mode) {
+  // Set the D/CX pin to indicate data or command values.
+  if (mode == Mode::kData) {
+    data_cmd_gpio_.SetState(State::kActive);
+  } else {
+    data_cmd_gpio_.SetState(State::kInactive);
+  }
+}
+
+Status DisplayDriverST7789::WriteCommand(Device::Transaction& transaction,
+                                         const Command& command) {
+  SetMode(Mode::kCommand);
+  byte buff[1]{static_cast<byte>(command.command)};
+  auto s = transaction.Write(buff);
+  if (!s.ok())
+    return s;
+
+  SetMode(Mode::kData);
+  if (command.command_data.empty()) {
+    return OkStatus();
+  }
+  return transaction.Write(command.command_data);
+}
+
+Status DisplayDriverST7789::Init() {
+  auto transaction =
+      spi_device_.StartTransaction(ChipSelectBehavior::kPerWriteRead);
+
+  spi_helper_.SetDataBits(8);
+
+  WriteCommand(transaction, {ST7789_SWRESET, kEmptyArray});  // Software reset
+  pw::spin_delay::WaitMillis(150);
+
+  WriteCommand(transaction, {ST7789_TEON, kEmptyArray});
+  WriteCommand(transaction, {ST7789_COLMOD, array<byte, 1>{byte{0x05}}});
+
+  WriteCommand(transaction,
+               {ST7789_PORCTRL,
+                array<byte, 5>{
+                    byte{0x0c},
+                    byte{0x0c},
+                    byte{0x00},
+                    byte{0x33},
+                    byte{0x33},
+                }});
+  WriteCommand(transaction, {ST7789_LCMCTRL, array<byte, 1>{byte{0x2c}}});
+  WriteCommand(transaction, {ST7789_VDVVRHEN, array<byte, 1>{byte{0x01}}});
+  WriteCommand(transaction, {ST7789_VRHS, array<byte, 1>{byte{0x12}}});
+  WriteCommand(transaction, {ST7789_VDVS, array<byte, 1>{byte{0x20}}});
+  WriteCommand(transaction,
+               {ST7789_PWCTRL1,
+                array<byte, 2>{
+                    byte{0xa4},
+                    byte{0xa1},
+                }});
+  WriteCommand(transaction, {ST7789_FRCTRL2, array<byte, 1>{byte{0x0f}}});
+
+  WriteCommand(transaction, {ST7789_INVON, kEmptyArray});
+  WriteCommand(transaction, {ST7789_SLPOUT, kEmptyArray});
+  WriteCommand(transaction, {ST7789_DISPON, kEmptyArray});
+
+  // Landscape drawing Column Address Set
+  const uint16_t kMaxColumn = screen_width_ - 1;
+  WriteCommand(transaction,
+               {ST7789_CASET,
+                array<byte, 4>{
+                    byte{0x0},
+                    byte{0x0},
+                    byte{static_cast<uint8_t>(kMaxColumn >> 8)},
+                    byte{static_cast<uint8_t>(kMaxColumn & 0xff)},
+                }});
+
+  // Page Address Set
+  const uint16_t kMaxRow = screen_height_ - 1;
+  WriteCommand(transaction,
+               {ST7789_RASET,
+                array<byte, 4>{
+                    byte{0x0},
+                    byte{0x0},
+                    byte{static_cast<uint8_t>(kMaxRow >> 8)},
+                    byte{static_cast<uint8_t>(kMaxRow & 0xff)},
+                }});
+
+  uint8_t madctl = 0;
+  bool rotate_180 = false;
+
+  if (screen_width_ == 240 && screen_height_ == 240) {
+    // TODO: Figure out 240x240 square display MADCTL values for rotation.
+    madctl = ST7789_MADCTL_HORIZ_ORDER;
+  } else if (screen_width_ == 320 && screen_height_ == 240) {
+    madctl = ST7789_MADCTL_COL_ORDER;
+    if (rotate_180)
+      madctl = ST7789_MADCTL_ROW_ORDER;
+
+    madctl |= ST7789_MADCTL_SWAP_XY | ST7789_MADCTL_SCAN_ORDER;
+  }
+
+  WriteCommand(transaction, {ST7789_MADCTL, array<byte, 1>{byte{madctl}}});
+
+  pw::spin_delay::WaitMillis(50);
+
+  return OkStatus();
+}
+
+Status DisplayDriverST7789::Update(
+    pw::framebuffer::FramebufferRgb565* frame_buffer) {
+  // Let controller know a write is coming.
+  auto transaction =
+      spi_device_.StartTransaction(ChipSelectBehavior::kPerWriteRead);
+  PW_TRY(spi_helper_.SetDataBits(8));
+  PW_TRY(WriteCommand(transaction, {ST7789_RAMWR, kEmptyArray}));
+  PW_TRY(spi_helper_.SetDataBits(16));
+
+  // Write the pixel data.
+  const uint16_t* fb_data = frame_buffer->GetFramebufferData();
+  const int num_pixels = frame_buffer->GetWidth() * frame_buffer->GetHeight();
+  return transaction.Write(
+      ConstByteSpan(reinterpret_cast<const byte*>(fb_data), num_pixels));
+}
+
+Status DisplayDriverST7789::Reset() {
+  if (!reset_gpio_)
+    return Status::Unavailable();
+  auto s = reset_gpio_->SetStateInactive();
+  if (!s.ok())
+    return s;
+  pw::spin_delay::WaitMillis(100);
+  s = reset_gpio_->SetStateActive();
+  pw::spin_delay::WaitMillis(100);
+  return s;
+}
+
+}  // namespace pw::display_driver
diff --git a/pw_display_driver_st7789/public/pw_display_driver_st7789/display_driver.h b/pw_display_driver_st7789/public/pw_display_driver_st7789/display_driver.h
new file mode 100644
index 0000000..309725f
--- /dev/null
+++ b/pw_display_driver_st7789/public/pw_display_driver_st7789/display_driver.h
@@ -0,0 +1,76 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+
+#include "pw_digital_io/digital_io.h"
+#include "pw_display_driver/display_driver.h"
+#include "pw_display_driver/spi_helper.h"
+#include "pw_spi/device.h"
+
+namespace pw::display_driver {
+
+class DisplayDriverST7789 : public DisplayDriver {
+ public:
+  // DisplayDriverST7789 configuration parameters.
+  struct Config {
+    // The GPIO line to use when specifying data/command mode for the display
+    // controller.
+    pw::digital_io::DigitalOut& data_cmd_gpio;
+    // GPIO line to reset the display controller.
+    pw::digital_io::DigitalOut* reset_gpio;
+    // The SPI device to which the display controller is connected.
+    pw::spi::Device& spi_device;
+    SPIHelper& spi_helper;
+    int screen_width;
+    int screen_height;
+  };
+
+  DisplayDriverST7789(const Config& config);
+
+  // DisplayDriver implementation:
+  Status Init() override;
+  Status Update(pw::framebuffer::FramebufferRgb565* framebuffer);
+
+ private:
+  enum class Mode {
+    kData,
+    kCommand,
+  };
+
+  // A command and optional data to write to the ST7789.
+  struct Command {
+    uint8_t command;
+    ConstByteSpan command_data;
+  };
+
+  // Toggle the reset GPIO line to reset the display controller.
+  Status Reset();
+
+  // Set the command/data mode of the display controller.
+  void SetMode(Mode mode);
+  // Write the command to the display controller.
+  Status WriteCommand(pw::spi::Device::Transaction& transaction,
+                      const Command& command);
+
+  pw::digital_io::DigitalOut& data_cmd_gpio_;  // Pin to specify D/CX mode.
+  pw::digital_io::DigitalOut* reset_gpio_;     // Controller reset.
+  pw::spi::Device& spi_device_;  // SPI device connected to controller.
+  SPIHelper& spi_helper_;
+  const int screen_width_;
+  const int screen_height_;
+};
+
+}  // namespace pw::display_driver
diff --git a/pw_graphics/pw_display_pico_st7789_spi/BUILD.gn b/pw_graphics/pw_display_pico_st7789_spi/BUILD.gn
index 05f3180..ea58749 100644
--- a/pw_graphics/pw_display_pico_st7789_spi/BUILD.gn
+++ b/pw_graphics/pw_display_pico_st7789_spi/BUILD.gn
@@ -24,12 +24,21 @@
 
 pw_source_set("pw_display_pico_st7789_spi") {
   public_configs = [ ":backend_config" ]
+  public_deps = [
+    "$dir_pw_digital_io_pico",
+    "$dir_pw_display_driver_st7789",
+    "$dir_pw_spi_pico",
+    "$dir_pw_sync:borrow",
+    "$dir_pw_sync:mutex",
+  ]
   deps = [
     "$PICO_ROOT/src/common/pico_base",
     "$PICO_ROOT/src/common/pico_stdlib",
     "$PICO_ROOT/src/rp2_common/hardware_pwm",
     "$PICO_ROOT/src/rp2_common/hardware_spi",
     "$dir_pw_display:pw_display.facade",
+    "$dir_pw_log",
+    "$dir_pw_status",
   ]
   public = [ "public_overrides/pw_display/display_backend.h" ]
   sources = [ "display.cc" ]
diff --git a/pw_graphics/pw_display_pico_st7789_spi/display.cc b/pw_graphics/pw_display_pico_st7789_spi/display.cc
index 8b93588..842bcfc 100644
--- a/pw_graphics/pw_display_pico_st7789_spi/display.cc
+++ b/pw_graphics/pw_display_pico_st7789_spi/display.cc
@@ -18,179 +18,86 @@
 #include <cinttypes>
 #include <cstdint>
 
-#include "hardware/pwm.h"
-#include "hardware/spi.h"
-#include "pico/stdlib.h"
-#include "pw_color/color.h"
 #include "pw_display/display_backend.h"
-#include "pw_framebuffer/rgb565.h"
 
-using pw::color::color_rgb565_t;
-using pw::framebuffer::FramebufferRgb565;
+#define LIB_CMSIS_CORE 0
+#define LIB_PICO_STDIO_USB 0
+#define LIB_PICO_STDIO_SEMIHOSTING 0
+
+#include "hardware/gpio.h"
+#include "hardware/pwm.h"
+#include "pico/stdlib.h"
+#include "pw_log/log.h"
+#include "pw_status/try.h"
 
 namespace pw::display::backend {
 
 namespace {
 
-// ST7789 Display Registers
-#define ST7789_SWRESET 0x01
-#define ST7789_TEOFF 0x34
-#define ST7789_TEON 0x35
-#define ST7789_MADCTL 0x36
-#define ST7789_COLMOD 0x3A
-#define ST7789_GCTRL 0xB7
-#define ST7789_VCOMS 0xBB
-#define ST7789_LCMCTRL 0xC0
-#define ST7789_VDVVRHEN 0xC2
-#define ST7789_VRHS 0xC3
-#define ST7789_VDVS 0xC4
-#define ST7789_FRCTRL2 0xC6
-#define ST7789_PWCTRL1 0xD0
-#define ST7789_PORCTRL 0xB2
-#define ST7789_GMCTRP1 0xE0
-#define ST7789_GMCTRN1 0xE1
-#define ST7789_INVOFF 0x20
-#define ST7789_SLPOUT 0x11
-#define ST7789_DISPON 0x29
-#define ST7789_GAMSET 0x26
-#define ST7789_DISPOFF 0x28
-#define ST7789_RAMWR 0x2C
-#define ST7789_INVON 0x21
-#define ST7789_CASET 0x2A
-#define ST7789_RASET 0x2B
-
-// MADCTL Bits (See page 215: MADCTL (36h): Memory Data Access Control)
-#define ST7789_MADCTL_ROW_ORDER 0b10000000
-#define ST7789_MADCTL_COL_ORDER 0b01000000
-#define ST7789_MADCTL_SWAP_XY 0b00100000
-#define ST7789_MADCTL_SCAN_ORDER 0b00010000
-#define ST7789_MADCTL_RGB_BGR 0b00001000
-#define ST7789_MADCTL_HORIZ_ORDER 0b00000100
-
 // Pico Display Pack 2 Pins
 // https://shop.pimoroni.com/products/pico-display-pack-2-0
 // --------------------------------------------------------
 constexpr int BACKLIGHT_EN = 20;
-// spi0 Pins
+// Pico spi0 Pins
 #define SPI_PORT spi0
 constexpr int TFT_SCLK = 18;  // SPI0 SCK
 constexpr int TFT_MOSI = 19;  // SPI0 TX
-// Unconnected
-// const int TFT_MISO = 4;  // SPI0 RX
+// Unused
+// constexpr int TFT_MISO = 4;   // SPI0 RX
 constexpr int TFT_CS = 17;  // SPI0 CSn
-constexpr int TFT_DC = 16;  // GP16
+constexpr int TFT_DC = 16;  // GP10
 // Reset pin is connected to the Pico reset pin (RUN #30)
 // constexpr int TFT_RST = 19;
 
-// Pico Display Pack 2 Size
-constexpr int kDisplayWidth = 320;
-constexpr int kDisplayHeight = 240;
-constexpr int kDisplayDataSize = kDisplayWidth * kDisplayHeight;
+constexpr uint32_t kBaudRate = 62'500'000;
 
-uint16_t framebuffer_data[kDisplayDataSize];
-
-// Pico Enviro+ Pack Pins are the same as Display Pack 2
-// https://shop.pimoroni.com/products/pico-enviro-pack
-// --------------------------------------------------------
-
-// Pico Enviro+ Pack Size
-// constexpr int kDisplayWidth = 240;
-// constexpr int kDisplayHeight = 240;
-// constexpr int kDisplayDataSize = kDisplayWidth * kDisplayHeight;
-
-// PicoSystem
-// https://shop.pimoroni.com/products/picosystem
-// --------------------------------------------------------
-// constexpr int BACKLIGHT_EN = 12;
-// #define SPI_PORT spi0
-// constexpr int TFT_SCLK = 6;  // SPI0 SCK
-// constexpr int TFT_MOSI = 7;  // SPI0 TX
-// Unconnected
-// const int TFT_MISO = 4;  // SPI0 RX
-// constexpr int TFT_CS = 5;  // SPI0 CSn
-// constexpr int TFT_DC = 9;  // GP16
-// constexpr int TFT_RST = 4;
-
-// SPI Functions
-// TODO(tonymd): move to pw_spi
-inline void ChipSelectEnable() {
-  asm volatile("nop \n nop \n nop");
-  gpio_put(TFT_CS, 0);
-  asm volatile("nop \n nop \n nop");
-}
-
-inline void ChipSelectDisable() {
-  asm volatile("nop \n nop \n nop");
-  gpio_put(TFT_CS, 1);
-  asm volatile("nop \n nop \n nop");
-}
-
-inline void DataCommandEnable() {
-  asm volatile("nop \n nop \n nop");
-  gpio_put(TFT_DC, 0);
-  asm volatile("nop \n nop \n nop");
-}
-
-inline void DataCommandDisable() {
-  asm volatile("nop \n nop \n nop");
-  gpio_put(TFT_DC, 1);
-  asm volatile("nop \n nop \n nop");
-}
-
-void inline SPISendByte(uint8_t data) {
-  ChipSelectEnable();
-  DataCommandDisable();
-  spi_write_blocking(SPI_PORT, &data, 1);
-  ChipSelectDisable();
-}
-
-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();
-}
-
-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();
-}
-
-void inline SPISendCommand(uint8_t command,
-                           size_t data_length,
-                           const char* data) {
-  // 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();
-  spi_write_blocking(SPI_PORT, (const uint8_t*)data, data_length);
-
-  ChipSelectDisable();
-}
+constexpr pw::spi::Config kSpiConfig{
+    .polarity = pw::spi::ClockPolarity::kActiveHigh,
+    .phase = pw::spi::ClockPhase::kFallingEdge,
+    .bits_per_word = pw::spi::BitsPerWord(8),
+    .bit_order = pw::spi::BitOrder::kMsbFirst,
+};
 
 }  // namespace
 
-Display::Display() = default;
+SPIHelperST7789::SPIHelperST7789(pw::digital_io::DigitalOut& cs_pin)
+    : spi_chip_selector_(cs_pin),
+      spi_initiator_(SPI_PORT, kBaudRate),
+      borrowable_spi_initiator_(spi_initiator_, spi_initiator_mutex_),
+      spi_device_(borrowable_spi_initiator_, kSpiConfig, spi_chip_selector_) {}
+
+Status SPIHelperST7789::SetDataBits(uint8_t data_bits) {
+  PW_ASSERT(data_bits == 8 || data_bits == 16);
+  spi_set_format(SPI_PORT, data_bits, SPI_CPOL_1, SPI_CPHA_1, SPI_MSB_FIRST);
+  spi_initiator_.SetOverrideBitsPerWord(pw::spi::BitsPerWord(data_bits));
+  return OkStatus();
+}
+
+Status SPIHelperST7789::Init() {
+  uint actual_baudrate = spi_init(SPI_PORT, kBaudRate);
+  PW_LOG_DEBUG("Actual Baudrate: %u", actual_baudrate);
+
+  // Not currently used (not yet reading from display).
+  // gpio_set_function(TFT_MISO, GPIO_FUNC_SPI);
+  gpio_set_function(TFT_SCLK, GPIO_FUNC_SPI);
+  gpio_set_function(TFT_MOSI, GPIO_FUNC_SPI);
+
+  return OkStatus();
+}
+
+Display::Display()
+    : chip_selector_gpio_(TFT_CS),
+      data_cmd_gpio_(TFT_DC),
+      spi_helper_(chip_selector_gpio_),
+      driver_config_{
+          .data_cmd_gpio = data_cmd_gpio_,
+          .reset_gpio = nullptr,
+          .spi_device = spi_helper_.GetDevice(),
+          .spi_helper = spi_helper_,
+          .screen_width = kDisplayWidth,
+          .screen_height = kDisplayHeight,
+      },
+      display_driver_(driver_config_) {}
 
 Display::~Display() = default;
 
@@ -199,12 +106,7 @@
   // TODO: This should be a facade
   setup_default_uart();
 
-  uint actual_baudrate = spi_init(SPI_PORT, 62'500'000);
-  // NOTE: If the display isn't working try a slower SPI baudrate:
-  // uint actual_baudrate = spi_init(SPI_PORT, 31'250'000);
-
-  // Set 8 bit SPI writes.
-  spi_set_format(SPI_PORT, 8, (spi_cpol_t)1, (spi_cpha_t)1, SPI_MSB_FIRST);
+  InitGPIO();
 
   // Init backlight PWM
   pwm_config cfg = pwm_get_default_config();
@@ -214,142 +116,47 @@
   // Full Brightness
   pwm_set_gpio_level(BACKLIGHT_EN, 65535);
 
-  // Init Pico SPI
-  // gpio_set_function(TFT_MISO, GPIO_FUNC_SPI);  // Unused
-  gpio_set_function(TFT_SCLK, GPIO_FUNC_SPI);
-  gpio_set_function(TFT_MOSI, GPIO_FUNC_SPI);
+  PW_TRY(spi_helper_.Init());
+  PW_TRY(display_driver_.Init());
 
+  return OkStatus();
+}
+
+void Display::Update(pw::framebuffer::FramebufferRgb565& frame_buffer) {
+  display_driver_.Update(&frame_buffer);
+}
+
+Status Display::InitFramebuffer(
+    pw::framebuffer::FramebufferRgb565* framebuffer) {
+  framebuffer->SetFramebufferData(framebuffer_data_,
+                                  kDisplayWidth,
+                                  kDisplayHeight,
+                                  kDisplayWidth * sizeof(uint16_t));
+  return OkStatus();
+}
+
+void Display::InitGPIO() {
   gpio_init(TFT_CS);
   gpio_init(TFT_DC);
   // gpio_init(TFT_RST); // Unused
 
   gpio_set_dir(TFT_CS, GPIO_OUT);
   gpio_set_dir(TFT_DC, GPIO_OUT);
-  // gpio_set_dir(TFT_RST, GPIO_OUT);  // Unused
-  gpio_put(TFT_CS, 1);
-  gpio_put(TFT_DC, 0);
-  // gpio_put(TFT_RST, 0);  // Unused
 
-  // Init Display
-  SPISendCommand(ST7789_SWRESET);  // Software reset
-
-  sleep_ms(150);
-
-  SPISendCommand(ST7789_TEON);
-  SPISendCommand(ST7789_COLMOD, 1, "\x05");
-
-  SPISendCommand(ST7789_PORCTRL, 5, "\x0c\x0c\x00\x33\x33");
-  SPISendCommand(ST7789_LCMCTRL, 1, "\x2c");
-  SPISendCommand(ST7789_VDVVRHEN, 1, "\x01");
-  SPISendCommand(ST7789_VRHS, 1, "\x12");
-  SPISendCommand(ST7789_VDVS, 1, "\x20");
-  SPISendCommand(ST7789_PWCTRL1, 2, "\xa4\xa1");
-  SPISendCommand(ST7789_FRCTRL2, 1, "\x0f");
-
-  SPISendCommand(ST7789_INVON);
-  SPISendCommand(ST7789_SLPOUT);
-  SPISendCommand(ST7789_DISPON);
-
-  uint8_t madctl = 0;
-  bool rotate_180 = false;
-
-  if (kDisplayWidth == 240 && kDisplayHeight == 240) {
-    // Column Address Set
-    SPISendCommand(ST7789_CASET);
-    SPISendShort(0);
-    SPISendShort(kDisplayWidth - 1);
-    // Page Address Set
-    SPISendCommand(ST7789_RASET);
-    SPISendShort(0);
-    SPISendShort(kDisplayHeight - 1);
-    // TODO: Figure out 240x240 square display MADCTL values for rotation.
-    madctl = ST7789_MADCTL_HORIZ_ORDER;
-  } else if (kDisplayWidth == 320 && kDisplayHeight == 240) {
-    // Landscape drawing
-    // Column Address Set
-    SPISendCommand(ST7789_CASET);
-    SPISendShort(0);
-    SPISendShort(kDisplayWidth - 1);
-    // Page Address Set
-    SPISendCommand(ST7789_RASET);
-    SPISendShort(0);
-    SPISendShort(kDisplayHeight - 1);
-
-    madctl = ST7789_MADCTL_COL_ORDER;
-    if (rotate_180)
-      madctl = ST7789_MADCTL_ROW_ORDER;
-
-    madctl |= ST7789_MADCTL_SWAP_XY | ST7789_MADCTL_SCAN_ORDER;
-  }
-
-  SPISendCommand(ST7789_MADCTL, 1, (char*)&madctl);
-
-  sleep_ms(50);
-  return OkStatus();
+  chip_selector_gpio_.Enable();
+  data_cmd_gpio_.Enable();
 }
 
 int Display::GetWidth() const { return kDisplayWidth; }
 
 int Display::GetHeight() const { return kDisplayHeight; }
 
-void SendDisplayWriteCommand() {
-  uint8_t command = ST7789_RAMWR;
-  // Switch to 8 bit writes.
-  spi_set_format(SPI_PORT, 8, (spi_cpol_t)1, (spi_cpha_t)1, SPI_MSB_FIRST);
-  DataCommandEnable();
-  ChipSelectEnable();
-  spi_write_blocking(SPI_PORT, &command, 1);
-  DataCommandDisable();
-  // Switch to 16 bit writes.
-  spi_set_format(SPI_PORT, 16, (spi_cpol_t)1, (spi_cpha_t)1, SPI_MSB_FIRST);
-}
-
-void UpdatePixelDouble(pw::framebuffer::FramebufferRgb565* frame_buffer) {
-  SendDisplayWriteCommand();
-
-  uint16_t temp_row[kDisplayWidth];
-  color_rgb565_t* pixel_data = frame_buffer->GetFramebufferData();
-  for (int y = 0; y < frame_buffer->GetHeight(); y++) {
-    // Populate this row with each pixel repeated twice
-    for (int x = 0; x < frame_buffer->GetWidth(); x++) {
-      temp_row[x * 2] = pixel_data[y * frame_buffer->GetWidth() + x];
-      temp_row[(x * 2) + 1] = pixel_data[y * frame_buffer->GetHeight() + x];
-    }
-    // Send this row to the display twice.
-    spi_write16_blocking(SPI_PORT, temp_row, kDisplayWidth);
-    spi_write16_blocking(SPI_PORT, temp_row, kDisplayWidth);
-  }
-
-  ChipSelectDisable();
-}
-
-void Display::Update(FramebufferRgb565& frame_buffer) {
-  SendDisplayWriteCommand();
-
-  const uint16_t* pixel_data = frame_buffer.GetFramebufferData();
-  spi_write16_blocking(SPI_PORT, pixel_data, kDisplayDataSize);
-  // put the display back into data mode (high).
-  ChipSelectDisable();
-}
-
 bool Display::TouchscreenAvailable() const { return false; }
 
 bool Display::NewTouchEvent() { return false; }
 
 pw::coordinates::Vec3Int Display::GetTouchPoint() {
-  pw::coordinates::Vec3Int point;
-  point.x = 0;
-  point.y = 0;
-  point.z = 0;
-  return point;
-}
-
-Status Display::InitFramebuffer(FramebufferRgb565* framebuffer) {
-  framebuffer->SetFramebufferData(framebuffer_data,
-                                  kDisplayWidth,
-                                  kDisplayHeight,
-                                  kDisplayWidth * sizeof(uint16_t));
-  return OkStatus();
+  return pw::coordinates::Vec3Int{0, 0, 0};
 }
 
 }  // namespace pw::display::backend
diff --git a/pw_graphics/pw_display_pico_st7789_spi/public_overrides/pw_display/display_backend.h b/pw_graphics/pw_display_pico_st7789_spi/public_overrides/pw_display/display_backend.h
index 6cdfe15..144a125 100644
--- a/pw_graphics/pw_display_pico_st7789_spi/public_overrides/pw_display/display_backend.h
+++ b/pw_graphics/pw_display_pico_st7789_spi/public_overrides/pw_display/display_backend.h
@@ -13,10 +13,34 @@
 // the License.
 #pragma once
 
+#include "pw_digital_io_pico/digital_io.h"
 #include "pw_display/display.h"
+#include "pw_display_driver_st7789/display_driver.h"
+#include "pw_spi_pico/chip_selector.h"
+#include "pw_spi_pico/initiator.h"
+#include "pw_sync/borrow.h"
+#include "pw_sync/mutex.h"
 
 namespace pw::display::backend {
 
+class SPIHelperST7789 : public pw::display_driver::SPIHelper {
+ public:
+  SPIHelperST7789(pw::digital_io::DigitalOut& cs_pin);
+
+  Status Init();
+  pw::spi::Device& GetDevice() { return spi_device_; }
+
+  // pw::display_driver::SPIHelper implementation:
+  Status SetDataBits(uint8_t data_bits) override;
+
+ private:
+  pw::spi::PicoChipSelector spi_chip_selector_;
+  pw::spi::PicoInitiator spi_initiator_;
+  pw::sync::VirtualMutex spi_initiator_mutex_;
+  pw::sync::Borrowable<pw::spi::Initiator> borrowable_spi_initiator_;
+  pw::spi::Device spi_device_;
+};
+
 class Display : pw::display::Display {
  public:
   Display();
@@ -32,6 +56,21 @@
   bool TouchscreenAvailable() const override;
   bool NewTouchEvent() override;
   pw::coordinates::Vec3Int GetTouchPoint() override;
+
+ private:
+  constexpr static int kDisplayWidth = 320;
+  constexpr static int kDisplayHeight = 240;
+  constexpr static int kNumDisplayPixels = kDisplayWidth * kDisplayHeight;
+
+  void InitGPIO();
+  void InitSPI();
+
+  pw::digital_io::PicoDigitalOut chip_selector_gpio_;
+  pw::digital_io::PicoDigitalOut data_cmd_gpio_;
+  SPIHelperST7789 spi_helper_;
+  pw::display_driver::DisplayDriverST7789::Config driver_config_;
+  pw::display_driver::DisplayDriverST7789 display_driver_;
+  uint16_t framebuffer_data_[kNumDisplayPixels];
 };
 
 }  // namespace pw::display::backend
diff --git a/pw_spi_pico/initiator.cc b/pw_spi_pico/initiator.cc
index cff4e5f..8f4e5d8 100644
--- a/pw_spi_pico/initiator.cc
+++ b/pw_spi_pico/initiator.cc
@@ -76,6 +76,7 @@
   // TODO(b/251033990): Remove once changing SPI device config is added.
   desired_bits_per_word_ = bits_per_word;
   override_bits_per_word_ = true;
+  config_.bits_per_word = bits_per_word;
 }
 
 Status PicoInitiator::LazyInit() {