rp2040: st7789 PixelPusher using PIO

- Add a SupportsResize function to pw_display_driver and
  pw_pixel_pusher.
- Added a PixelPusher for the rp2040 that uses PIO to write the
  framebuffer with 2x scaling (pixel doubling).
- common_pico.cc switched to use double buffering.
- Fix a missing pixel on the '8' character in font6x8.cc
- Add a kudzu board definition in targets/rp2040/board_configs.gni
- Add an OVERCLOCK_250 define for common_pico.cc rp2040

Change-Id: Ia8d98688d3d9c7191bfa465f414357868d4fc0c6
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/experimental/+/169450
Reviewed-by: Chris Mumford <cmumford@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Anthony DiGirolamo <tonymd@google.com>
diff --git a/applications/app_common_impl/BUILD.gn b/applications/app_common_impl/BUILD.gn
index 92e3f0a..d84a014 100644
--- a/applications/app_common_impl/BUILD.gn
+++ b/applications/app_common_impl/BUILD.gn
@@ -58,6 +58,14 @@
   ]
 }
 
+config("rp2040_pio_flags") {
+  cflags = [
+    "-DUSE_PIO=1",
+    # Overclock the rp2040 in common_pico.cc
+    # "-DOVERCLOCK_250=1",
+  ]
+}
+
 pw_source_set("stm32cube") {
   public_configs = [
     ":common_flags",
@@ -123,6 +131,7 @@
   "$PICO_ROOT/src/common/pico_stdlib",
   "$PICO_ROOT/src/rp2_common/hardware_pwm",
   "$PICO_ROOT/src/rp2_common/hardware_spi",
+  "$PICO_ROOT/src/rp2_common/hardware_vreg",
   "$dir_pigweed_experimental/applications/app_common:app_common.facade",
   "$dir_pw_digital_io_pico",
   "$dir_pw_display",
@@ -157,6 +166,22 @@
   remove_configs = [ "$dir_pw_build:strict_warnings" ]
 }
 
+pw_source_set("pico_st7789_pio") {
+  public_configs = [
+    ":common_flags",
+    ":spi_flags",
+    ":rp2040_pio_flags",
+  ]
+  cflags = [ "-DDISPLAY_TYPE_ST7789_PIO" ]
+  deps = _pico_common_deps
+  deps += [
+    "$dir_pw_display_driver_st7789",
+    "$dir_pw_pixel_pusher_rp2040_pio",
+  ]
+  sources = [ "common_pico.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
+
 pw_source_set("pico_st7735") {
   public_configs = [
     ":common_flags",
diff --git a/applications/app_common_impl/common_pico.cc b/applications/app_common_impl/common_pico.cc
index 8f1973a..c2c2cc5 100644
--- a/applications/app_common_impl/common_pico.cc
+++ b/applications/app_common_impl/common_pico.cc
@@ -20,9 +20,11 @@
 
 #include "hardware/gpio.h"
 #include "hardware/pwm.h"
+#include "hardware/vreg.h"
 #include "pico/stdlib.h"
 #include "pw_digital_io_pico/digital_io.h"
 #include "pw_log/log.h"
+#include "pw_pixel_pusher_rp2040_pio/pixel_pusher.h"
 #include "pw_spi_pico/chip_selector.h"
 #include "pw_spi_pico/initiator.h"
 #include "pw_sync/borrow.h"
@@ -37,6 +39,9 @@
 #elif defined(DISPLAY_TYPE_ST7789)
 #include "pw_display_driver_st7789/display_driver.h"
 using DisplayDriver = pw::display_driver::DisplayDriverST7789;
+#elif defined(DISPLAY_TYPE_ST7789_PIO)
+#include "pw_display_driver_st7789/display_driver.h"
+using DisplayDriver = pw::display_driver::DisplayDriverST7789;
 #else
 #error "Undefined display type"
 #endif
@@ -48,6 +53,7 @@
 using pw::framebuffer::Framebuffer;
 using pw::framebuffer::PixelFormat;
 using pw::framebuffer_pool::FramebufferPool;
+using pw::pixel_pusher::PixelPusherRp2040Pio;
 using pw::spi::Device;
 using pw::spi::Initiator;
 using pw::spi::PicoChipSelector;
@@ -73,7 +79,7 @@
 static_assert(DISPLAY_WIDTH > 0);
 static_assert(DISPLAY_HEIGHT > 0);
 
-constexpr uint16_t kDisplayScaleFactor = 1;
+constexpr uint16_t kDisplayScaleFactor = 2;
 constexpr uint16_t kFramebufferWidth =
     FRAMEBUFFER_WIDTH >= 0 ? FRAMEBUFFER_WIDTH / kDisplayScaleFactor
                            : DISPLAY_WIDTH / kDisplayScaleFactor;
@@ -116,8 +122,18 @@
 SpiValues s_spi_16_bit(kSpiConfig16Bit,
                        s_spi_chip_selector,
                        s_spi_initiator_mutex);
-uint16_t s_pixel_data[kNumPixels];
-const pw::Vector<void*, 1> s_pixel_buffers{s_pixel_data};
+
+#if USE_PIO
+PixelPusherRp2040Pio s_pixel_pusher(DISPLAY_DC_GPIO,
+                                    DISPLAY_CS_GPIO,
+                                    SPI_MOSI_GPIO,
+                                    SPI_CLOCK_GPIO,
+                                    DISPLAY_TE_GPIO,
+                                    pio0);
+#endif
+uint16_t s_pixel_data1[kNumPixels];
+uint16_t s_pixel_data2[kNumPixels];
+const pw::Vector<void*, 2> s_pixel_buffers{s_pixel_data1, s_pixel_data2};
 pw::framebuffer_pool::FramebufferPool s_fb_pool({
     .fb_addr = s_pixel_buffers,
     .dimensions = {kFramebufferWidth, kFramebufferHeight},
@@ -125,7 +141,7 @@
     .pixel_format = PixelFormat::RGB565,
 });
 DisplayDriver s_display_driver({
-  .data_cmd_gpio = s_display_dc_pin,
+  .data_cmd_gpio = s_display_dc_pin, .spi_cs_gpio = s_display_cs_pin,
 #if DISPLAY_RESET_GPIO != -1
   .reset_gpio = &s_display_reset_pin,
 #else
@@ -138,6 +154,9 @@
 #endif
   .spi_device_8_bit = s_spi_8_bit.device,
   .spi_device_16_bit = s_spi_16_bit.device,
+#if USE_PIO
+  .pixel_pusher = &s_pixel_pusher,
+#endif
 });
 Display s_display(s_display_driver, kDisplaySize, s_fb_pool);
 
@@ -163,6 +182,13 @@
 
 // static
 Status Common::Init() {
+#if OVERCLOCK_250
+  // Overvolt for a stable 250MHz on some RP2040s
+  vreg_set_voltage(VREG_VOLTAGE_1_20);
+  sleep_ms(10);
+  set_sys_clock_khz(250000, false);
+#endif
+
   // Initialize all of the present standard stdio types that are linked into the
   // binary.
   stdio_init_all();
@@ -193,7 +219,15 @@
   gpio_set_function(SPI_CLOCK_GPIO, GPIO_FUNC_SPI);
   gpio_set_function(SPI_MOSI_GPIO, GPIO_FUNC_SPI);
 
+#if USE_PIO
+  // Init the display before the pixel pusher.
+  s_display_driver.Init();
+  auto result = s_pixel_pusher.Init(s_fb_pool);
+  s_pixel_pusher.SetPixelDouble(true);
+  return result;
+#else
   return s_display_driver.Init();
+#endif
 }
 
 // static
diff --git a/build_overrides/pigweed.gni b/build_overrides/pigweed.gni
index 9dae5cc..cce6d5c 100644
--- a/build_overrides/pigweed.gni
+++ b/build_overrides/pigweed.gni
@@ -147,4 +147,7 @@
           "abspath")
   dir_pw_pixel_pusher =
       get_path_info("$dir_pigweed_experimental/pw_pixel_pusher", "abspath")
+  dir_pw_pixel_pusher_rp2040_pio =
+      get_path_info("$dir_pigweed_experimental/pw_pixel_pusher_rp2040_pio",
+                    "abspath")
 }
diff --git a/pw_display_driver/public/pw_display_driver/display_driver.h b/pw_display_driver/public/pw_display_driver/display_driver.h
index bc9cc4f..95d6283 100644
--- a/pw_display_driver/public/pw_display_driver/display_driver.h
+++ b/pw_display_driver/public/pw_display_driver/display_driver.h
@@ -51,6 +51,9 @@
   virtual uint16_t GetWidth() const = 0;
 
   virtual uint16_t GetHeight() const = 0;
+
+  // Display driver supports resizing during write.
+  virtual bool SupportsResize() const { return false; }
 };
 
 }  // namespace pw::display_driver
diff --git a/pw_display_driver_st7789/BUILD.gn b/pw_display_driver_st7789/BUILD.gn
index 93af99b..5800337 100644
--- a/pw_display_driver_st7789/BUILD.gn
+++ b/pw_display_driver_st7789/BUILD.gn
@@ -30,6 +30,7 @@
   public_deps = [
     "$dir_pw_digital_io",
     "$dir_pw_display_driver:display_driver",
+    "$dir_pw_pixel_pusher:pixel_pusher",
     "$dir_pw_spi:device",
   ]
   sources = [ "display_driver.cc" ]
diff --git a/pw_display_driver_st7789/display_driver.cc b/pw_display_driver_st7789/display_driver.cc
index c45990a..a635289 100644
--- a/pw_display_driver_st7789/display_driver.cc
+++ b/pw_display_driver_st7789/display_driver.cc
@@ -186,6 +186,14 @@
 void DisplayDriverST7789::WriteFramebuffer(Framebuffer frame_buffer,
                                            WriteCallback write_callback) {
   PW_ASSERT(frame_buffer.pixel_format() == PixelFormat::RGB565);
+  if (config_.pixel_pusher) {
+    // Write the pixel data.
+    config_.pixel_pusher->WriteFramebuffer(std::move(frame_buffer),
+                                           std::move(write_callback));
+    return;
+  }
+
+  // Write the frame_buffer using pw_spi.
   // Let controller know a write is coming.
   Status s;
   {
@@ -260,4 +268,9 @@
   return s;
 }
 
+bool DisplayDriverST7789::SupportsResize() const {
+  return config_.pixel_pusher != nullptr &&
+         config_.pixel_pusher->SupportsResize();
+}
+
 }  // 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
index 7000a8a..5f13d65 100644
--- 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
@@ -17,6 +17,7 @@
 
 #include "pw_digital_io/digital_io.h"
 #include "pw_display_driver/display_driver.h"
+#include "pw_pixel_pusher/pixel_pusher.h"
 #include "pw_spi/device.h"
 
 namespace pw::display_driver {
@@ -28,6 +29,7 @@
     // The GPIO line to use when specifying data/command mode for the display
     // controller.
     pw::digital_io::DigitalOut& data_cmd_gpio;
+    pw::digital_io::DigitalOut& spi_cs_gpio;
     // GPIO line to reset the display controller.
     pw::digital_io::DigitalOut* reset_gpio;
     pw::digital_io::DigitalIn* tear_effect_gpio;
@@ -39,6 +41,8 @@
     pw::spi::Device& spi_device_16_bit;
     uint16_t screen_width = 320;
     uint16_t screen_height = 240;
+    // The pixel pusher.
+    pw::pixel_pusher::PixelPusher* pixel_pusher = nullptr;
   };
 
   DisplayDriverST7789(const Config& config);
@@ -52,6 +56,7 @@
                   uint16_t col_idx) override;
   uint16_t GetWidth() const override { return config_.screen_width; }
   uint16_t GetHeight() const override { return config_.screen_height; }
+  bool SupportsResize() const override;
 
  private:
   enum class Mode {
diff --git a/pw_graphics/pw_display/display.cc b/pw_graphics/pw_display/display.cc
index 6264c17..317bc93 100644
--- a/pw_graphics/pw_display/display.cc
+++ b/pw_graphics/pw_display/display.cc
@@ -92,6 +92,7 @@
   return framebuffer_pool_.GetFramebuffer();
 }
 
+// This may be async
 Status Display::ReleaseFramebuffer(Framebuffer framebuffer) {
   pw::framebuffer_pool::FramebufferPool& fb_pool = framebuffer_pool_;
   auto write_cb = [&fb_pool](pw::framebuffer::Framebuffer fb, Status status) {
@@ -102,9 +103,14 @@
     return Status::InvalidArgument();
   if (framebuffer.size() != size_) {
 #if DISPLAY_RESIZE
-    Status result = UpdateNearestNeighbor(framebuffer);
-    write_cb(std::move(framebuffer), result);
-    return result;
+    if (display_driver_.SupportsResize()) {
+      display_driver_.WriteFramebuffer(std::move(framebuffer), write_cb);
+      return OkStatus();
+    } else {
+      Status result = UpdateNearestNeighbor(framebuffer);
+      write_cb(std::move(framebuffer), result);
+      return result;
+    }
 #endif
     // Rely on display driver's ability to support size mismatch. It is
     // expected to return an error if it cannot.
diff --git a/pw_graphics/pw_draw/public/pw_draw/font6x8.cc b/pw_graphics/pw_draw/public/pw_draw/font6x8.cc
index 3ee9e43..5a78595 100644
--- a/pw_graphics/pw_draw/public/pw_draw/font6x8.cc
+++ b/pw_graphics/pw_draw/public/pw_draw/font6x8.cc
@@ -260,7 +260,7 @@
     0b100010,
     0b100010,
     0b011100,
-    0b000010,
+    0b100010,
     0b100010,
     0b011100,
     0b000000,
diff --git a/pw_pixel_pusher/public/pw_pixel_pusher/pixel_pusher.h b/pw_pixel_pusher/public/pw_pixel_pusher/pixel_pusher.h
index d8edce9..6a6df0a 100644
--- a/pw_pixel_pusher/public/pw_pixel_pusher/pixel_pusher.h
+++ b/pw_pixel_pusher/public/pw_pixel_pusher/pixel_pusher.h
@@ -26,11 +26,14 @@
 
   virtual ~PixelPusher() = default;
 
+  // PixelPusher implementation:
   virtual Status Init(
       const pw::framebuffer_pool::FramebufferPool& framebuffer_pool) = 0;
 
   virtual void WriteFramebuffer(framebuffer::Framebuffer framebuffer,
                                 WriteCallback complete_callback) = 0;
+
+  virtual bool SupportsResize() const { return false; }
 };
 
 }  // namespace pw::pixel_pusher
diff --git a/pw_pixel_pusher_rp2040_pio/BUILD.gn b/pw_pixel_pusher_rp2040_pio/BUILD.gn
new file mode 100644
index 0000000..6b358d2
--- /dev/null
+++ b/pw_pixel_pusher_rp2040_pio/BUILD.gn
@@ -0,0 +1,41 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_pixel_pusher_rp2040_pio") {
+  public_configs = [ ":default_config" ]
+  deps = [
+    "$dir_pw_function",
+    "$dir_pw_log",
+  ]
+  public_deps = [
+    "$PICO_ROOT/src/rp2_common/hardware_dma",
+    "$PICO_ROOT/src/rp2_common/hardware_pio",
+    "$PICO_ROOT/src/rp2_common/hardware_spi",
+    "$dir_pw_digital_io",
+    "$dir_pw_framebuffer_pool",
+    "$dir_pw_pixel_pusher:pixel_pusher",
+    "$dir_pw_sync:binary_semaphore",
+  ]
+  sources = [ "pixel_pusher.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/pw_pixel_pusher_rp2040_pio/pixel_pusher.cc b/pw_pixel_pusher_rp2040_pio/pixel_pusher.cc
new file mode 100644
index 0000000..b4ce090
--- /dev/null
+++ b/pw_pixel_pusher_rp2040_pio/pixel_pusher.cc
@@ -0,0 +1,304 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_pixel_pusher_rp2040_pio/pixel_pusher.h"
+
+#include <array>
+#include <cstddef>
+
+#include "hardware/dma.h"
+#include "hardware/pio.h"
+#include "hardware/spi.h"
+#include "pw_digital_io/digital_io.h"
+#include "pw_pixel_pusher_rp2040_pio/st7789.pio.h"
+
+using pw::Status;
+using pw::framebuffer::Framebuffer;
+using pw::framebuffer_pool::FramebufferPool;
+
+namespace pw::pixel_pusher {
+
+namespace {
+
+Framebuffer s_framebuffer;
+Callback<void(Framebuffer, Status)> s_draw_callback;
+static uint dma_channel;
+static volatile int current_scanline = 240;
+static volatile int irq_fire_count;
+
+static void __isr irq_handler() {
+  irq_fire_count++;
+  // Write was active, just finished
+  if (dma_channel_get_irq0_status(dma_channel)) {
+    dma_channel_acknowledge_irq0(dma_channel);
+
+    // * 2 for pixel doubling
+    uint16_t fb_width = s_framebuffer.size().width * 2;
+    uint16_t fb_height = s_framebuffer.size().height * 2;
+
+    if (++current_scanline > fb_height / 2) {
+      // All scanlines written. This frame is done.
+      if (s_draw_callback != nullptr) {
+        s_draw_callback(std::move(s_framebuffer), pw::OkStatus());
+        s_draw_callback = nullptr;
+      }
+      return;
+    }
+
+    auto count =
+        current_scanline == (fb_height + 1) / 2 ? fb_width / 4 : fb_width / 2;
+
+    dma_channel_set_trans_count(dma_channel,
+                                /* trans_count= */ count,
+                                /* trigger= */ false);
+    dma_channel_set_read_addr(
+        dma_channel,
+        /* read_addr= */ static_cast<const uint16_t*>(s_framebuffer.data()) +
+            (current_scanline - 1) * (fb_width / 2),
+        /* trigger= */ true);
+  }
+}
+
+// PIO helpers
+static void pio_put_byte(PIO pio, uint sm, uint8_t b) {
+  while (pio_sm_is_tx_fifo_full(pio, sm))
+    ;
+  *(volatile uint8_t*)&pio->txf[sm] = b;
+}
+
+static void pio_wait(PIO pio, uint sm) {
+  uint32_t stall_mask = 1u << (PIO_FDEBUG_TXSTALL_LSB + sm);
+  pio->fdebug |= stall_mask;
+  while (!(pio->fdebug & stall_mask))
+    ;
+}
+
+}  // namespace
+
+PixelPusherRp2040Pio::PixelPusherRp2040Pio(
+    int dc_pin, int cs_pin, int dout_pin, int sck_pin, int te_pin, PIO pio)
+    : dc_pin_(dc_pin),
+      cs_pin_(cs_pin),
+      dout_pin_(dout_pin),
+      sck_pin_(sck_pin),
+      te_pin_(te_pin),
+      pio_(pio) {}
+PixelPusherRp2040Pio::~PixelPusherRp2040Pio() = default;
+
+Status PixelPusherRp2040Pio::Init(const FramebufferPool& framebuffer_pool) {
+  const FramebufferPool::BufferArray& buffers =
+      framebuffer_pool.GetBuffersForInit();
+  if (buffers.empty()) {
+    return Status::Internal();
+  }
+
+  // PIO Setup
+  pio_offset_ = pio_add_program(pio_, /* program= */ &st7789_raw_program);
+  pio_double_offset_ =
+      pio_add_program(pio_, /* program= */ &st7789_pixel_double_program);
+
+  pio_sm_ = pio_claim_unused_sm(pio_, true);
+
+  pio_sm_config pio_config = st7789_raw_program_get_default_config(pio_offset_);
+
+#if OVERCLOCK_250
+  sm_config_set_clkdiv(&pio_config, 2);  // Clock divide by 2 for 62.5MHz
+#endif
+
+  sm_config_set_out_shift(&pio_config,
+                          /* shift_right= */ false,
+                          /* autopull= */ true,
+                          /* pull_threshold= */ 8);
+  sm_config_set_out_pins(
+      &pio_config, /* out_base= */ dout_pin_, /* out_count= */ 1);
+  sm_config_set_fifo_join(&pio_config, /* pio_fifo_join= */ PIO_FIFO_JOIN_TX);
+  sm_config_set_sideset_pins(&pio_config, /* sideset_base= */ sck_pin_);
+
+  pio_gpio_init(pio_, /* pin= */ dout_pin_);
+  pio_gpio_init(pio_, /* pin= */ sck_pin_);
+  pio_sm_set_consecutive_pindirs(pio_,
+                                 pio_sm_,
+                                 /* pin_base= */ dout_pin_,
+                                 /* pin_count= */ 1,
+                                 /* is_out= */ true);
+  pio_sm_set_consecutive_pindirs(pio_,
+                                 pio_sm_,
+                                 /* pin_base= */ sck_pin_,
+                                 /* pin_count= */ 1,
+                                 /* is_out= */ true);
+
+  pio_sm_init(pio_, pio_sm_, /* initial_pc= */ pio_offset_, &pio_config);
+  pio_sm_set_enabled(pio_, pio_sm_, /* enabled= */ true);
+
+  // DMA Setup
+  dma_channel_ = dma_claim_unused_channel(true);
+  dma_channel_config config = dma_channel_get_default_config(dma_channel_);
+  channel_config_set_transfer_data_size(
+      &config, /* dma_channel_transfer_size= */ DMA_SIZE_16);
+  // DMA byte swapping: off
+  channel_config_set_bswap(&config, /* bswap= */ false);
+  // Set tranfer request signal to a dreq (data request).
+  channel_config_set_dreq(
+      &config,
+      /* dreq= */ pio_get_dreq(pio_, pio_sm_, /* is_tx= */ true));
+  dma_channel_configure(dma_channel_,
+                        &config,
+                        /* write_addr= */ &pio_->txf[pio_sm_],
+                        /* read_addr= */ NULL,    // framebuffer
+                        /* transfer_count= */ 0,  // width * height
+                        /* trigger= */ false);
+
+  irq_add_shared_handler(
+      /* num= */ DMA_IRQ_0,
+      /* handler= */ irq_handler,
+      /* order_priority= */ PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
+  irq_set_enabled(DMA_IRQ_0, /* enabled= */ true);
+
+  return OkStatus();
+}
+
+void PixelPusherRp2040Pio::WriteFramebuffer(
+    framebuffer::Framebuffer framebuffer,
+    Callback<void(framebuffer::Framebuffer, Status)> complete_callback) {
+  // If this should be non-blocking
+  // if (dma_channel_is_busy(dma_channel)) {
+  //   return;
+  // }
+
+  // If not vsync
+  while (DmaIsBusy()) {
+  }
+
+  dma_channel_wait_for_finish_blocking(dma_channel);
+
+  if (!write_mode_) {
+    SetupWriteFramebuffer();
+  }
+
+  PW_ASSERT(s_draw_callback == nullptr);
+  s_draw_callback = std::move(complete_callback);
+  s_framebuffer = std::move(framebuffer);
+  dma_channel = dma_channel_;
+
+  const uint16_t* fb_data = static_cast<const uint16_t*>(s_framebuffer.data());
+  int fb_width = s_framebuffer.size().width;
+  int fb_height = s_framebuffer.size().height;
+
+  if (pixel_double_enabled_) {
+    fb_width *= 2;
+    fb_height *= 2;
+    current_scanline = 0;
+    irq_fire_count = 0;
+    dma_channel_set_trans_count(dma_channel_, fb_width / 4, false);
+  } else {
+    dma_channel_set_trans_count(dma_channel_, fb_width * fb_height, false);
+  }
+
+  dma_channel_set_read_addr(dma_channel_, fb_data, true);
+}
+
+void PixelPusherRp2040Pio::SetPixelDouble(bool enabled) {
+  pixel_double_enabled_ = enabled;
+
+  if (pixel_double_enabled_) {
+    dma_channel_acknowledge_irq0(dma_channel);
+    dma_channel_set_irq0_enabled(dma_channel, true);
+  } else {
+    dma_channel_set_irq0_enabled(dma_channel, false);
+  }
+}
+
+bool PixelPusherRp2040Pio::DmaIsBusy() {
+  if (pixel_double_enabled_ &&
+      current_scanline <= (s_framebuffer.size().height * 2) / 2) {
+    return true;
+  }
+  return dma_channel_is_busy(dma_channel);
+}
+
+void PixelPusherRp2040Pio::Clear() {
+  if (!write_mode_)
+    SetupWriteFramebuffer();
+
+  int fb_width = s_framebuffer.size().width;
+  int fb_height = s_framebuffer.size().height;
+  if (pixel_double_enabled_) {
+    fb_width *= 2;
+    fb_height *= 2;
+  }
+
+  for (int i = 0; i < fb_width * fb_height; i++)
+    pio_sm_put_blocking(pio_, pio_sm_, 0);
+}
+
+bool PixelPusherRp2040Pio::VsyncCallback(gpio_irq_callback_t callback) {
+#if DISPLAY_TE_GPIO != -1
+  gpio_set_irq_enabled_with_callback(
+      te_pin_, GPIO_IRQ_EDGE_RISE, true, callback);
+  return true;
+#else
+  return false;
+#endif
+}
+
+void PixelPusherRp2040Pio::SetupWriteFramebuffer() {
+  pio_wait(pio_, pio_sm_);
+
+  gpio_put(cs_pin_, 0);
+
+  // Enter command mode.
+  gpio_put(dc_pin_, 0);
+  // Tell the display a framebuffer is coming next.
+  pio_put_byte(pio_, pio_sm_, 0x2C);  // ST7789_RAMWR
+  pio_wait(pio_, pio_sm_);
+
+  // Enter data mode.
+  gpio_put(dc_pin_, 1);
+
+  pio_sm_set_enabled(pio_, pio_sm_, false);
+  pio_sm_restart(pio_, pio_sm_);
+
+  if (pixel_double_enabled_) {
+    // Switch PIO to the pixel double program.
+    pio_sm_set_wrap(pio_,
+                    pio_sm_,
+                    pio_double_offset_ + st7789_pixel_double_wrap_target,
+                    pio_double_offset_ + st7789_pixel_double_wrap);
+
+    pio_->sm[pio_sm_].shiftctrl &=
+        ~(PIO_SM0_SHIFTCTRL_PULL_THRESH_BITS | PIO_SM0_SHIFTCTRL_AUTOPULL_BITS);
+
+    pio_sm_exec(pio_, pio_sm_, pio_encode_jmp(pio_double_offset_));
+
+    dma_channel_hw_addr(dma_channel_)->al1_ctrl &=
+        ~DMA_CH0_CTRL_TRIG_DATA_SIZE_BITS;
+    dma_channel_hw_addr(dma_channel_)->al1_ctrl |=
+        DMA_SIZE_32 << DMA_CH0_CTRL_TRIG_DATA_SIZE_LSB;
+  } else {
+    pio_->sm[pio_sm_].shiftctrl &= ~PIO_SM0_SHIFTCTRL_PULL_THRESH_BITS;
+    pio_->sm[pio_sm_].shiftctrl |= (16 << PIO_SM0_SHIFTCTRL_PULL_THRESH_LSB) |
+                                   PIO_SM0_SHIFTCTRL_AUTOPULL_BITS;
+
+    dma_channel_hw_addr(dma_channel_)->al1_ctrl &=
+        ~DMA_CH0_CTRL_TRIG_DATA_SIZE_BITS;
+    dma_channel_hw_addr(dma_channel_)->al1_ctrl |=
+        DMA_SIZE_16 << DMA_CH0_CTRL_TRIG_DATA_SIZE_LSB;
+  }
+
+  pio_sm_set_enabled(pio_, pio_sm_, true);
+
+  write_mode_ = true;
+}
+
+}  // namespace pw::pixel_pusher
diff --git a/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/pixel_pusher.h b/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/pixel_pusher.h
new file mode 100644
index 0000000..51e9f55
--- /dev/null
+++ b/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/pixel_pusher.h
@@ -0,0 +1,64 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <array>
+#include <cstddef>
+
+#include "hardware/dma.h"
+#include "hardware/irq.h"
+#include "hardware/pio.h"
+#include "hardware/spi.h"
+#include "pw_digital_io/digital_io.h"
+#include "pw_pixel_pusher/pixel_pusher.h"
+#include "pw_sync/binary_semaphore.h"
+
+namespace pw::pixel_pusher {
+
+class PixelPusherRp2040Pio : public PixelPusher {
+ public:
+  PixelPusherRp2040Pio(
+      int dc_pin, int cs_pin, int dout_pin, int sck_pin_, int te_pin, PIO pio);
+  ~PixelPusherRp2040Pio();
+
+  // PixelPusher implementation:
+  Status Init(
+      const pw::framebuffer_pool::FramebufferPool& framebuffer_pool) override;
+  void WriteFramebuffer(framebuffer::Framebuffer framebuffer,
+                        WriteCallback complete_callback) override;
+  bool SupportsResize() const override { return true; }
+
+  // RP2040 PIO Functions
+  void SetPixelDouble(bool enabled);
+  bool DmaIsBusy();
+  void SetupWriteFramebuffer();
+  void Clear();
+  bool VsyncCallback(gpio_irq_callback_t callback);
+
+ private:
+  bool pixel_double_enabled_ = false;
+  bool write_mode_ = false;
+  PIO pio_;
+  uint dma_channel_;
+  uint dc_pin_;
+  uint cs_pin_;
+  uint te_pin_;
+  uint dout_pin_;
+  uint sck_pin_;
+  uint pio_sm_;
+  uint pio_offset_;
+  uint pio_double_offset_;
+};
+
+}  // namespace pw::pixel_pusher
diff --git a/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/st7789.pio.h b/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/st7789.pio.h
new file mode 100644
index 0000000..f724347
--- /dev/null
+++ b/pw_pixel_pusher_rp2040_pio/public/pw_pixel_pusher_rp2040_pio/st7789.pio.h
@@ -0,0 +1,86 @@
+// -------------------------------------------------- //
+// This file is autogenerated by pioasm; do not edit! //
+// -------------------------------------------------- //
+
+#pragma once
+
+#if !PICO_NO_HARDWARE
+#include "hardware/pio.h"
+#endif
+
+// ---------- //
+// st7789_raw //
+// ---------- //
+
+#define st7789_raw_wrap_target 0
+#define st7789_raw_wrap 1
+
+static const uint16_t st7789_raw_program_instructions[] = {
+    //     .wrap_target
+    0x7001,  //  0: out    pins, 1         side 0
+    0xb842,  //  1: nop                    side 1
+             //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program st7789_raw_program = {
+    .instructions = st7789_raw_program_instructions,
+    .length = 2,
+    .origin = -1,
+};
+
+static inline pio_sm_config st7789_raw_program_get_default_config(uint offset) {
+  pio_sm_config c = pio_get_default_sm_config();
+  sm_config_set_wrap(
+      &c, offset + st7789_raw_wrap_target, offset + st7789_raw_wrap);
+  sm_config_set_sideset(&c, 2, true, false);
+  return c;
+}
+#endif
+
+// ------------------- //
+// st7789_pixel_double //
+// ------------------- //
+
+#define st7789_pixel_double_wrap_target 0
+#define st7789_pixel_double_wrap 16
+
+static const uint16_t st7789_pixel_double_program_instructions[] = {
+    //     .wrap_target
+    0x80a0,  //  0: pull   block
+    0xa027,  //  1: mov    x, osr
+    0x6050,  //  2: out    y, 16
+    0x7001,  //  3: out    pins, 1         side 0
+    0x18e3,  //  4: jmp    !osre, 3        side 1
+    0xa0e1,  //  5: mov    osr, x
+    0x6070,  //  6: out    null, 16
+    0x7001,  //  7: out    pins, 1         side 0
+    0x18e7,  //  8: jmp    !osre, 7        side 1
+    0xa0e2,  //  9: mov    osr, y
+    0x6070,  // 10: out    null, 16
+    0x7001,  // 11: out    pins, 1         side 0
+    0x18eb,  // 12: jmp    !osre, 11       side 1
+    0xa0e2,  // 13: mov    osr, y
+    0x6070,  // 14: out    null, 16
+    0x7001,  // 15: out    pins, 1         side 0
+    0x18ef,  // 16: jmp    !osre, 15       side 1
+             //     .wrap
+};
+
+#if !PICO_NO_HARDWARE
+static const struct pio_program st7789_pixel_double_program = {
+    .instructions = st7789_pixel_double_program_instructions,
+    .length = 17,
+    .origin = -1,
+};
+
+static inline pio_sm_config st7789_pixel_double_program_get_default_config(
+    uint offset) {
+  pio_sm_config c = pio_get_default_sm_config();
+  sm_config_set_wrap(&c,
+                     offset + st7789_pixel_double_wrap_target,
+                     offset + st7789_pixel_double_wrap);
+  sm_config_set_sideset(&c, 2, true, false);
+  return c;
+}
+#endif
diff --git a/targets/rp2040/BUILD.gn b/targets/rp2040/BUILD.gn
index dfb037c..10f7072 100644
--- a/targets/rp2040/BUILD.gn
+++ b/targets/rp2040/BUILD.gn
@@ -60,7 +60,7 @@
     # Merge in the app_common_BACKEND and various application settings
     # which are defined in board_configs.gni.
 
-    forward_variables_from(board_config_st7789, "*")
+    forward_variables_from(board_config_kudzu, "*")
 
     # forward_variables_from(board_config_st7735, "*")
     # forward_variables_from(board_config_ili9341, "*")
@@ -78,6 +78,7 @@
     pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv7m"
     pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
         "$dir_pw_sync_freertos:interrupt_spin_lock"
+    pw_sync_BINARY_SEMAPHORE_BACKEND = "$dir_pw_sync_freertos:binary_semaphore"
     pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
     pw_sync_COUNTING_SEMAPHORE_BACKEND =
         "$dir_pw_sync_freertos:counting_semaphore"
diff --git a/targets/rp2040/board_configs.gni b/targets/rp2040/board_configs.gni
index e114ecc..9cb5d12 100644
--- a/targets/rp2040/board_configs.gni
+++ b/targets/rp2040/board_configs.gni
@@ -63,3 +63,18 @@
   pw_app_common_SPI_MOSI_GPIO = "19"
   pw_app_common_SPI_CLOCK_GPIO = "18"
 }
+
+board_config_kudzu = {
+  app_common_BACKEND =
+      "$dir_pigweed_experimental/applications/app_common_impl:pico_st7789_pio"
+  pw_app_common_DISPLAY_WIDTH = "320"
+  pw_app_common_DISPLAY_HEIGHT = "240"
+  pw_app_common_BACKLIGHT_GPIO = "20"
+  pw_app_common_DISPLAY_TE_GPIO = "21"
+  pw_app_common_DISPLAY_CS_GPIO = "17"
+  pw_app_common_DISPLAY_DC_GPIO = "16"
+  pw_app_common_DISPLAY_RESET_GPIO = "-1"
+  pw_app_common_SPI_MISO_GPIO = "-1"
+  pw_app_common_SPI_MOSI_GPIO = "19"
+  pw_app_common_SPI_CLOCK_GPIO = "18"
+}