Merge pull request #897 from Daft-Freak/pico-presto

pico: Presto + QwST Pad
diff --git a/32blit-pico/CMakeLists.txt b/32blit-pico/CMakeLists.txt
index b4f893c..d2221d3 100644
--- a/32blit-pico/CMakeLists.txt
+++ b/32blit-pico/CMakeLists.txt
@@ -43,7 +43,7 @@
 )
 
 target_link_libraries(BlitHalPico INTERFACE
-    hardware_dma hardware_pio hardware_pwm hardware_spi
+    hardware_dma hardware_i2c hardware_pio hardware_pwm hardware_spi
     pico_multicore pico_stdlib pico_unique_id pico_rand
     tinyusb_device
     FatFsBlitAPI
@@ -115,7 +115,7 @@
 endif()
 
 if(BLIT_DISPLAY_DRIVER STREQUAL "picovision")
-    list(APPEND BLIT_BOARD_LIBRARIES hardware_i2c aps6404 swd_load)
+    list(APPEND BLIT_BOARD_LIBRARIES aps6404 swd_load)
 elseif(BLIT_DISPLAY_DRIVER STREQUAL "scanvideo")
     set(BLIT_REQUIRE_PICO_EXTRAS TRUE)
     set(BLIT_ENABLE_CORE1 TRUE)
@@ -147,6 +147,7 @@
 pico_generate_pio_header(BlitHalPico ${CMAKE_CURRENT_LIST_DIR}/dbi-spi.pio)
 pico_generate_pio_header(BlitHalPico ${CMAKE_CURRENT_LIST_DIR}/dbi-8bit.pio)
 pico_generate_pio_header(BlitHalPico ${CMAKE_CURRENT_LIST_DIR}/audio/i2s.pio)
+pico_generate_pio_header(BlitHalPico ${CMAKE_CURRENT_LIST_DIR}/display/dpi.pio)
 pico_generate_pio_header(BlitHalPico ${CMAKE_CURRENT_LIST_DIR}/spi.pio)
 
 # include picovision drivers
diff --git a/32blit-pico/board/pimoroni_picovision.h b/32blit-pico/board/pimoroni_picovision.h
index c8dde1c..092017d 100644
--- a/32blit-pico/board/pimoroni_picovision.h
+++ b/32blit-pico/board/pimoroni_picovision.h
@@ -3,6 +3,17 @@
 
 #define PIMORONI_PICOVISION
 
+// --- I2C ---
+#ifndef PICO_DEFAULT_I2C
+#define PICO_DEFAULT_I2C 1
+#endif
+#ifndef PICO_DEFAULT_I2C_SDA_PIN
+#define PICO_DEFAULT_I2C_SDA_PIN 6
+#endif
+#ifndef PICO_DEFAULT_I2C_SCL_PIN
+#define PICO_DEFAULT_I2C_SCL_PIN 7
+#endif
+
 #include "boards/pico_w.h"
 
 #endif
diff --git a/32blit-pico/board/pimoroni_picovision/config.h b/32blit-pico/board/pimoroni_picovision/config.h
index 4e3b304..3a24090 100644
--- a/32blit-pico/board/pimoroni_picovision/config.h
+++ b/32blit-pico/board/pimoroni_picovision/config.h
@@ -5,6 +5,8 @@
 
 #define DEFAULT_SCREEN_FORMAT PixelFormat::BGR555
 
+#define DEFAULT_I2C_CLOCK 400000
+
 // native
 #define SD_CLK  10
 #define SD_CMD  11
diff --git a/32blit-pico/board/pimoroni_presto/config.cmake b/32blit-pico/board/pimoroni_presto/config.cmake
new file mode 100644
index 0000000..88cd4fc
--- /dev/null
+++ b/32blit-pico/board/pimoroni_presto/config.cmake
@@ -0,0 +1,12 @@
+set(BLIT_BOARD_NAME "Presto")
+
+set(BLIT_BOARD_DEFINITIONS
+    PICO_DEFAULT_I2C=0
+    PICO_DEFAULT_I2C_SDA_PIN=40
+    PICO_DEFAULT_I2C_SCL_PIN=41
+)
+
+blit_driver(audio beep)
+blit_driver(display dpi)
+blit_driver(input tca9555)
+blit_driver(storage sd_spi)
diff --git a/32blit-pico/board/pimoroni_presto/config.h b/32blit-pico/board/pimoroni_presto/config.h
new file mode 100644
index 0000000..684264f
--- /dev/null
+++ b/32blit-pico/board/pimoroni_presto/config.h
@@ -0,0 +1,45 @@
+#pragma once
+
+// audio beep
+#define AUDIO_BEEP_PIN 43
+
+#define DISPLAY_WIDTH  240
+#define DISPLAY_HEIGHT 240
+
+#define DPI_DATA_PIN_BASE  1
+#define DPI_SYNC_PIN_BASE 19
+#define DPI_CLOCK_PIN     22
+
+#define DPI_MODE_CLOCK 15625000 // ish
+
+#define DPI_MODE_H_FRONT_PORCH     4
+#define DPI_MODE_H_SYNC_WIDTH     16
+#define DPI_MODE_H_BACK_PORCH     30
+#define DPI_MODE_H_ACTIVE_PIXELS 480
+
+#define DPI_MODE_V_FRONT_PORCH     5
+#define DPI_MODE_V_SYNC_WIDTH      8
+#define DPI_MODE_V_BACK_PORCH      5
+#define DPI_MODE_V_ACTIVE_LINES  480
+
+#define DPI_SPI_INIT spi1
+#define DPI_ST7701
+
+#define DPI_BIT_REVERSE
+
+#define LCD_CS_PIN        28
+#define LCD_DC_PIN        -1
+#define LCD_SCK_PIN       26
+#define LCD_MOSI_PIN      27
+#define LCD_BACKLIGHT_PIN 45
+#define LCD_RESET_PIN     44
+
+// spi
+#define SD_SCK  34
+#define SD_MOSI 35
+#define SD_MISO 36
+#define SD_CS   39
+
+#define PSRAM_CS_PIN 47
+
+#define DEFAULT_I2C_CLOCK 400000
diff --git a/32blit-pico/board/vgaboard/config.cmake b/32blit-pico/board/vgaboard/config.cmake
index cc4d244..977040e 100644
--- a/32blit-pico/board/vgaboard/config.cmake
+++ b/32blit-pico/board/vgaboard/config.cmake
@@ -1,12 +1,9 @@
 set(BLIT_BOARD_NAME "VGA Board")
 
-set(BLIT_BOARD_DEFINITIONS
-    PICO_SCANVIDEO_PLANE1_VARIABLE_FRAGMENT_DMA=1
-    PICO_SCANVIDEO_MAX_SCANLINE_BUFFER_WORDS=12
-)
-
 blit_driver(audio i2s)
-blit_driver(display scanvideo)
+blit_driver(display dpi)
 blit_driver(input usb_hid)
 blit_driver(storage sd_spi)
 blit_driver(usb host)
+
+set(BLIT_ENABLE_CORE1 TRUE)
diff --git a/32blit-pico/board/vgaboard/config.h b/32blit-pico/board/vgaboard/config.h
index fe227c4..79ee3a9 100644
--- a/32blit-pico/board/vgaboard/config.h
+++ b/32blit-pico/board/vgaboard/config.h
@@ -1,10 +1,8 @@
 #pragma once
 
-#ifndef ALLOW_HIRES
-#define ALLOW_HIRES 0 // disable by default, mode switching isn't supported
-#endif
-
 #define AUDIO_MAX_SAMPLE_UPDATE 64
+#define AUDIO_I2S_CLOCK_PIN_BASE 27
+#define AUDIO_I2S_DATA_PIN 26
 
 // spi
 #define SD_SCK   5
diff --git a/32blit-pico/display/dbi.cpp b/32blit-pico/display/dbi.cpp
index d04e1ed..4fa3d9d 100644
--- a/32blit-pico/display/dbi.cpp
+++ b/32blit-pico/display/dbi.cpp
@@ -2,6 +2,7 @@
 #include <math.h>
 
 #include "display.hpp"
+#include "display_commands.hpp"
 
 #include "hardware/clocks.h"
 #include "hardware/dma.h"
@@ -29,18 +30,6 @@
 static bool backlight_enabled = false;
 static uint32_t last_render = 0;
 
-enum MADCTL : uint8_t {
-  // writing to internal memory
-  ROW_ORDER   = 0b10000000,  // MY / y flip
-  COL_ORDER   = 0b01000000,  // MX / x flip
-  SWAP_XY     = 0b00100000,  // AKA "MV"
-
-  // scanning out from internal memory
-  SCAN_ORDER  = 0b00010000,
-  RGB         = 0b00001000,
-  HORIZ_ORDER = 0b00000100
-};
-
 static const uint8_t rotations[]{
   0,                                                                                // 0
   MADCTL::HORIZ_ORDER | MADCTL::SWAP_XY | MADCTL::COL_ORDER,                        // 90
@@ -48,29 +37,6 @@
   MADCTL::SCAN_ORDER | MADCTL::SWAP_XY | MADCTL::ROW_ORDER                          // 270
 };
 
-// standard commands
-enum MIPIDCS
-{
-  Nop              = 0x00,
-  SoftReset        = 0x01,
-  GetAddressMode   = 0x0B,
-  GetPixelFormat   = 0x0C,
-  EnterSleepMode   = 0x10,
-  ExitSleepMode    = 0x11,
-  ExitInvertMode   = 0x20,
-  EnterInvertMode  = 0x21,
-  DisplayOff       = 0x28,
-  DisplayOn        = 0x29,
-  SetColumnAddress = 0x2A,
-  SetRowAddress    = 0x2B,
-  WriteMemoryStart = 0x2C,
-  SetTearOff       = 0x34,
-  SetTearOn        = 0x35,
-  SetAddressMode   = 0x36,
-  SetPixelFormat   = 0x3A,
-  SetTearScanline  = 0x44,
-};
-
 enum ST7789Reg
 {
   RAMCTRL   = 0xB0,
diff --git a/32blit-pico/display/display_commands.hpp b/32blit-pico/display/display_commands.hpp
new file mode 100644
index 0000000..ca70ced
--- /dev/null
+++ b/32blit-pico/display/display_commands.hpp
@@ -0,0 +1,37 @@
+#pragma once
+#include <cstdint>
+
+// standard display command set
+enum MIPIDCS {
+  Nop              = 0x00,
+  SoftReset        = 0x01,
+  GetAddressMode   = 0x0B,
+  GetPixelFormat   = 0x0C,
+  EnterSleepMode   = 0x10,
+  ExitSleepMode    = 0x11,
+  ExitInvertMode   = 0x20,
+  EnterInvertMode  = 0x21,
+  DisplayOff       = 0x28,
+  DisplayOn        = 0x29,
+  SetColumnAddress = 0x2A,
+  SetRowAddress    = 0x2B,
+  WriteMemoryStart = 0x2C,
+  SetTearOff       = 0x34,
+  SetTearOn        = 0x35,
+  SetAddressMode   = 0x36,
+  SetPixelFormat   = 0x3A,
+  SetTearScanline  = 0x44,
+};
+
+
+enum MADCTL : uint8_t {
+  // writing to internal memory
+  ROW_ORDER   = 0b10000000,  // MY / y flip
+  COL_ORDER   = 0b01000000,  // MX / x flip
+  SWAP_XY     = 0b00100000,  // AKA "MV"
+
+  // scanning out from internal memory
+  SCAN_ORDER  = 0b00010000,
+  RGB         = 0b00001000,
+  HORIZ_ORDER = 0b00000100
+};
diff --git a/32blit-pico/display/dpi.cpp b/32blit-pico/display/dpi.cpp
new file mode 100644
index 0000000..cd3be7b
--- /dev/null
+++ b/32blit-pico/display/dpi.cpp
@@ -0,0 +1,591 @@
+#include "hardware/clocks.h"
+#include "hardware/dma.h"
+#include "hardware/gpio.h"
+#include "hardware/irq.h"
+#include "hardware/pio.h"
+#include "hardware/spi.h"
+#include "pico/binary_info.h"
+#include "pico/time.h"
+
+#include "display.hpp"
+#include "display_commands.hpp"
+
+#include "config.h"
+
+#include "dpi.pio.h"
+
+enum ST7701Reg {
+  // Command2_BK0
+  PVGAMCTRL = 0xB0,  // Positive Voltage Gamma Control
+  NVGAMCTRL = 0xB1,  // Negative Voltage Gamma Control
+  DGMEN = 0xB8,   // Digital Gamma Enable
+  DGMLUTR = 0xB9, // Digital Gamma LUT for Red
+  DGMLUTB = 0xBA, // Digital Gamma Lut for Blue
+  LNESET = 0xC0,  // Display Line Setting
+  PORCTRL = 0xC1, // Porch Control
+  INVSET = 0xC2,  // Inversion Selection & Frame Rate Control
+  RGBCTRL = 0xC3, // RGB Control
+  PARCTRL = 0xC5, // Partial Mode Control
+  SDIR = 0xC7,    // X-direction Control
+  PDOSET = 0xC8,  // Pseudo-Dot Inversion Diving Setting
+  COLCTRL = 0xCD, // Colour Control
+  SRECTRL = 0xE0, // Sunlight Readable Enhancement
+  NRCTRL = 0xE1,  // Noise Reduce Control
+  SECTRL = 0xE2,  // Sharpness Control
+  CCCTRL = 0xE3,  // Color Calibration Control
+  SKCTRL = 0xE4,  // Skin Tone Preservation Control
+  // Command2_BK1
+  VHRS = 0xB0,    // Vop amplitude
+  VCOMS = 0xB1,   // VCOM amplitude
+  VGHSS = 0xB2,   // VGH voltage
+  TESTCMD = 0xB3, // TEST command
+  VGLS = 0xB5,    // VGL voltage
+  VRHDV = 0xB6,   // VRH_DV voltage
+  PWCTRL1 = 0xB7, // Power Control 1
+  PWCTRL2 = 0xB8, // Power Control 2
+  PCLKS1 = 0xBA,  // Power pumping clock selection 1
+  PCLKS2 = 0xBC,  // Power pumping clock selection 2
+  PDR1 = 0xC1,    // Source pre_drive timing set 1
+  PDR2 = 0xC2,    // Source pre_drive timing set 2
+  // Command2_BK3
+  NVMEN = 0xC8,    // NVM enable
+  NVMSET = 0xCA,   // NVM manual control
+  PROMACT = 0xCC,  // NVM program active
+  // Other
+  CND2BKxSEL = 0xFF,
+};
+
+#ifndef DPI_DATA_PIN_BASE
+#define DPI_DATA_PIN_BASE 0
+#endif
+
+#ifndef DPI_SYNC_PIN_BASE
+#define DPI_SYNC_PIN_BASE 16
+#endif
+
+// mode (default to 640x480)
+#ifndef DPI_MODE_CLOCK
+#define DPI_MODE_CLOCK 25000000
+#endif
+
+#ifndef DPI_MODE_H_SYNC_POLARITY
+#define DPI_MODE_H_SYNC_POLARITY 0
+#endif
+#ifndef DPI_MODE_H_FRONT_PORCH
+#define DPI_MODE_H_FRONT_PORCH   16
+#endif
+#ifndef DPI_MODE_H_SYNC_WIDTH
+#define DPI_MODE_H_SYNC_WIDTH    96
+#endif
+#ifndef DPI_MODE_H_BACK_PORCH
+#define DPI_MODE_H_BACK_PORCH    48
+#endif
+#ifndef DPI_MODE_H_ACTIVE_PIXELS
+#define DPI_MODE_H_ACTIVE_PIXELS 640
+#endif
+
+#ifndef DPI_MODE_V_SYNC_POLARITY
+#define DPI_MODE_V_SYNC_POLARITY 0
+#endif
+#ifndef DPI_MODE_V_FRONT_PORCH
+#define DPI_MODE_V_FRONT_PORCH   10
+#endif
+#ifndef DPI_MODE_V_SYNC_WIDTH
+#define DPI_MODE_V_SYNC_WIDTH    2
+#endif
+#ifndef DPI_MODE_V_BACK_PORCH
+#define DPI_MODE_V_BACK_PORCH    33
+#endif
+#ifndef DPI_MODE_V_ACTIVE_LINES
+#define DPI_MODE_V_ACTIVE_LINES  480
+#endif
+
+static_assert(DPI_MODE_H_ACTIVE_PIXELS % DISPLAY_WIDTH == 0);
+static_assert(DPI_MODE_V_ACTIVE_LINES % DISPLAY_HEIGHT == 0);
+
+#define MODE_V_TOTAL_LINES  ( \
+  DPI_MODE_V_FRONT_PORCH + DPI_MODE_V_SYNC_WIDTH + \
+  DPI_MODE_V_BACK_PORCH  + DPI_MODE_V_ACTIVE_LINES \
+)
+
+// DMA logic
+
+#define DPI_DMA_CH_BASE 0
+#define DPI_NUM_DMA_CHANNELS 2
+
+static uint8_t cur_dma_ch = DPI_DMA_CH_BASE;
+
+static PIO pio = pio0;
+static uint8_t timing_sm, data_sm;
+static uint8_t data_program_offset;
+
+// pixel/line repeat
+static uint16_t line_width = 0;
+static uint8_t v_repeat = 0;
+static uint8_t new_v_repeat = 0;
+
+static uint data_scanline = DPI_NUM_DMA_CHANNELS;
+static uint timing_scanline = 0;
+static uint8_t timing_offset = 0;
+
+static bool started = false;
+static volatile bool do_render = true;
+static volatile bool need_mode_change = false;
+static uint8_t reconfigure_data_pio = 0;
+static uint8_t *cur_display_buffer = nullptr;
+
+static uint32_t active_line_timings[4];
+static uint32_t vblank_line_timings[4];
+static uint32_t vsync_line_timings[4];
+
+// assumes data SM is idle
+static inline void update_h_repeat() {
+  // update Y register
+  pio_sm_put(pio, data_sm, line_width - 1);
+  pio_sm_exec(pio, data_sm, pio_encode_out(pio_y, 32));
+
+  // patch loop delay for repeat
+  int h_repeat = DPI_MODE_H_ACTIVE_PIXELS / line_width;
+  auto delay = (h_repeat - 1) * 2;
+
+#ifdef DPI_BIT_REVERSE
+  auto offset = dpi_data_reversed_16_offset_data_loop_delay;
+  auto instr = dpi_data_reversed_16_program.instructions[offset];
+  delay *= 2;
+#else
+  auto offset = dpi_data_16_offset_data_loop_delay;
+  auto instr = dpi_data_16_program.instructions[offset];
+#endif
+
+  // need to add the program offset as it's a jump
+  pio->instr_mem[data_program_offset + offset] = (instr | pio_encode_delay(delay)) + data_program_offset;
+}
+
+static void __not_in_flash_func(dma_irq_handler)() {
+  // this only covers active lines
+
+  dma_channel_hw_t *ch = &dma_hw->ch[cur_dma_ch];
+  dma_hw->intr = 1u << cur_dma_ch;
+
+  if(cur_dma_ch + 1 == DPI_DMA_CH_BASE + DPI_NUM_DMA_CHANNELS)
+    cur_dma_ch = DPI_DMA_CH_BASE;
+  else
+    cur_dma_ch++;
+
+  if(data_scanline == DPI_MODE_V_ACTIVE_LINES) {
+    // new frame, swap buffers
+    data_scanline = 0;
+
+    if(!do_render) {
+      if(fb_double_buffer)
+        std::swap(blit::screen.data, cur_display_buffer);
+      do_render = true;
+    }
+
+    // set h/v shift
+    if(need_mode_change) {
+      if(line_width != cur_surf_info.bounds.w) {
+        reconfigure_data_pio = (ch - dma_hw->ch) + 1;
+        hw_clear_bits(&ch->al1_ctrl, DMA_CH0_CTRL_TRIG_EN_BITS); // clear enable so line 0 won't start
+      }
+
+      v_repeat = new_v_repeat;
+      line_width = cur_surf_info.bounds.w;
+
+      need_mode_change = false;
+    }
+  } else if(reconfigure_data_pio) {
+    // this should be the point where the last line finished (in vblank) and we would start line 0, but we disabled it
+    // reconfigure the PIO before re-enabling it
+    int prev_chan = reconfigure_data_pio - 1;
+
+    while(pio->sm[data_sm].addr != data_program_offset); // wait until we've returned to waiting for irq
+
+    update_h_repeat();
+
+    // resume
+    hw_set_bits(&dma_hw->ch[prev_chan].ctrl_trig, DMA_CH0_CTRL_TRIG_EN_BITS);
+    reconfigure_data_pio = 0;
+  }
+
+  // setup next line DMA
+  int display_line = data_scanline / v_repeat;
+  auto w = line_width;
+  auto fb_line_ptr = reinterpret_cast<uint16_t *>(cur_display_buffer) + display_line * w;
+
+  ch->read_addr = uintptr_t(fb_line_ptr);
+  ch->transfer_count = w / 2;
+
+  data_scanline++;
+}
+
+static void __not_in_flash_func(pio_timing_irq_handler)() {
+  while(!(pio->fstat & (1 << (PIO_FSTAT_TXFULL_LSB + timing_sm)))) {
+    if(timing_scanline >= DPI_MODE_V_FRONT_PORCH && timing_scanline < DPI_MODE_V_FRONT_PORCH + DPI_MODE_V_SYNC_WIDTH)
+      pio_sm_put(pio, timing_sm, vsync_line_timings[timing_offset]); // v sync
+    else if(timing_scanline < DPI_MODE_V_FRONT_PORCH + DPI_MODE_V_SYNC_WIDTH + DPI_MODE_V_BACK_PORCH)
+      pio_sm_put(pio, timing_sm, vblank_line_timings[timing_offset]); // v blank
+    else
+      pio_sm_put(pio, timing_sm, active_line_timings[timing_offset]); // active
+
+    if(++timing_offset == std::size(active_line_timings)) {
+      timing_offset = 0;
+
+      if(++timing_scanline == MODE_V_TOTAL_LINES)
+        timing_scanline = 0;
+    }
+  }
+}
+
+#ifdef DPI_SPI_INIT
+static void command(uint8_t reg, size_t len = 0, const char *data = nullptr) {
+  gpio_put(LCD_CS_PIN, 0);
+
+#if LCD_DC_PIN != -1
+  gpio_put(LCD_DC_PIN, 0); // command
+  spi_write_blocking(DPI_SPI_INIT, &reg, 1);
+
+  if(data) {
+    gpio_put(LCD_DC_PIN, 1); // data
+    spi_write_blocking(DPI_SPI_INIT, (const uint8_t *)data, len);
+  }
+#else
+  uint16_t v = reg;
+  spi_write16_blocking(DPI_SPI_INIT, &v, 1);
+
+  if(data) {
+    for(size_t i = 0; i < len; i++) {
+      v = data[i] | 0x100;
+      spi_write16_blocking(DPI_SPI_INIT, &v, 1);
+    }
+  }
+#endif
+
+  gpio_put(LCD_CS_PIN, 1);
+}
+#endif
+
+static void init_display_spi() {
+#ifdef DPI_SPI_INIT
+  spi_init(DPI_SPI_INIT, 1 * 1000 * 1000);
+  gpio_set_function(LCD_SCK_PIN, GPIO_FUNC_SPI);
+  gpio_set_function(LCD_MOSI_PIN, GPIO_FUNC_SPI);
+
+  // init CS
+  gpio_init(LCD_CS_PIN);
+  gpio_set_dir(LCD_CS_PIN, GPIO_OUT);
+  gpio_put(LCD_CS_PIN, 1);
+
+  bi_decl_if_func_used(bi_1pin_with_name(LCD_MOSI_PIN, "Display TX"));
+  bi_decl_if_func_used(bi_1pin_with_name(LCD_SCK_PIN, "Display SCK"));
+  bi_decl_if_func_used(bi_1pin_with_name(LCD_CS_PIN, "Display CS"));
+
+#if LCD_DC_PIN != -1
+  // init D/C
+  gpio_init(LCD_DC_PIN);
+  gpio_set_dir(LCD_DC_PIN, GPIO_OUT);
+
+  bi_decl_if_func_used(bi_1pin_with_name(LCD_DC_PIN, "Display D/C"));
+#else
+  // configure for 9 bit if no D/C pin
+  spi_set_format(DPI_SPI_INIT, 9, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);
+#endif
+
+#ifdef LCD_RESET_PIN
+  gpio_init(LCD_RESET_PIN);
+  gpio_set_dir(LCD_RESET_PIN, GPIO_OUT);
+
+  sleep_ms(15);
+  gpio_put(LCD_RESET_PIN, 1);
+  sleep_ms(15);
+
+  bi_decl_if_func_used(bi_1pin_with_name(LCD_RESET_PIN, "Display Reset"));
+#endif
+
+#ifdef DPI_ST7701
+  command(MIPIDCS::SoftReset);
+
+  sleep_ms(150);
+
+  // Commmand 2 BK0 - kinda a page select
+  command(ST7701Reg::CND2BKxSEL, 5, "\x77\x01\x00\x00\x10");
+
+  command(ST7701Reg::LNESET, 2, "\x3b\x00");   // (59 + 1) * 8 = 480 lines
+  command(ST7701Reg::PORCTRL, 2, "\x0d\x02");  // Display porch settings: 13 VBP, 2 VFP (these should not be changed)
+  command(ST7701Reg::INVSET, 2, "\x31\x01");
+  command(ST7701Reg::COLCTRL, 1, "\x08");      // LED polarity reversed
+  command(ST7701Reg::PVGAMCTRL, 16, "\x00\x11\x18\x0e\x11\x06\x07\x08\x07\x22\x04\x12\x0f\xaa\x31\x18");
+  command(ST7701Reg::NVGAMCTRL, 16, "\x00\x11\x19\x0e\x12\x07\x08\x08\x08\x22\x04\x11\x11\xa9\x32\x18");
+  command(ST7701Reg::RGBCTRL, 3, "\x80\x2e\x0e");  // HV mode, H and V back porch + sync
+
+
+  // Command 2 BK1 - Voltages and power and stuff
+  command(ST7701Reg::CND2BKxSEL, 5, "\x77\x01\x00\x00\x11");
+  command(ST7701Reg::VHRS, 1, "\x60");    // 4.7375v
+  command(ST7701Reg::VCOMS, 1, "\x32");   // 0.725v
+  command(ST7701Reg::VGHSS, 1, "\x07");   // 15v
+  command(ST7701Reg::TESTCMD, 1, "\x80"); // y tho?
+  command(ST7701Reg::VGLS, 1, "\x49");    // -10.17v
+  command(ST7701Reg::PWCTRL1, 1, "\x85"); // Middle/Min/Min bias
+  command(ST7701Reg::PWCTRL2, 1, "\x21"); // 6.6 / -4.6
+  command(ST7701Reg::PDR1, 1, "\x78");    // 1.6uS
+  command(ST7701Reg::PDR2, 1, "\x78");    // 6.4uS
+
+  // Begin Forbidden Knowledge
+  // This sequence is probably specific to TL040WVS03CT15-H1263A.
+  // It is not documented in the ST7701s datasheet.
+  // TODO: 👇 W H A T ! ? 👇
+  command(0xE0, 3, "\x00\x1b\x02");
+  command(0xE1, 11, "\x08\xa0\x00\x00\x07\xa0\x00\x00\x00\x44\x44");
+  command(0xE2, 12, "\x11\x11\x44\x44\xed\xa0\x00\x00\xec\xa0\x00\x00");
+  command(0xE3, 4, "\x00\x00\x11\x11");
+  command(0xE4, 2, "\x44\x44");
+  command(0xE5, 16, "\x0a\xe9\xd8\xa0\x0c\xeb\xd8\xa0\x0e\xed\xd8\xa0\x10\xef\xd8\xa0");
+  command(0xE6, 4, "\x00\x00\x11\x11");
+  command(0xE7, 2, "\x44\x44");
+  command(0xE8, 16, "\x09\xe8\xd8\xa0\x0b\xea\xd8\xa0\x0d\xec\xd8\xa0\x0f\xee\xd8\xa0");
+  command(0xEB, 7, "\x02\x00\xe4\xe4\x88\x00\x40");
+  command(0xEC, 2, "\x3c\x00");
+  command(0xED, 16, "\xab\x89\x76\x54\x02\xff\xff\xff\xff\xff\xff\x20\x45\x67\x98\xba");
+  command(0x36, 1, "\x00");
+
+  // Command 2 BK3
+  command(ST7701Reg::CND2BKxSEL, 5, "\x77\x01\x00\x00\x13");
+  command(0xE5, 1, "\xe4");
+  // End Forbidden Knowledge
+
+  command(ST7701Reg::CND2BKxSEL, 5, "\x77\x01\x00\x00\x00");
+
+  command(MIPIDCS::SetPixelFormat, 1, "\x66"); // (18bpp)
+
+  uint8_t madctl = MADCTL::RGB;
+  command(MIPIDCS::SetAddressMode, 1, (char *)&madctl);
+
+  command(MIPIDCS::EnterInvertMode);
+  sleep_ms(1);
+  command(MIPIDCS::ExitSleepMode);
+  sleep_ms(120);
+  command(MIPIDCS::DisplayOn);
+#endif
+#endif
+}
+
+void init_display() {
+  // send init commands if needed
+  init_display_spi();
+
+  // setup timing buffers
+  auto encode_timing = [](uint16_t instr, bool vsync, bool hsync, bool de, int delay) {
+    // instr needs sideset 0, but that's just a zero
+    return instr                                   << 16
+         | (delay - 3)                             <<  3 // two cycles from setup, one for the first loop iteration
+         //| (de ? 1 : 0) << 2 // TODO
+         | (vsync == DPI_MODE_V_SYNC_POLARITY ? 1 : 0) <<  1
+         | (hsync == DPI_MODE_H_SYNC_POLARITY ? 1 : 0) <<  0;
+  };
+
+  //                                     instr                           vbl    hbl    de     delay
+  active_line_timings[0] = encode_timing(pio_encode_nop(),               false, true,  false, DPI_MODE_H_SYNC_WIDTH);
+  active_line_timings[1] = encode_timing(pio_encode_nop(),               false, false, false, DPI_MODE_H_BACK_PORCH);
+  active_line_timings[2] = encode_timing(pio_encode_irq_set(false, 4),   false, false, true,  DPI_MODE_H_ACTIVE_PIXELS);
+  active_line_timings[3] = encode_timing(pio_encode_irq_clear(false, 4), false, false, false, DPI_MODE_H_FRONT_PORCH);
+
+  vblank_line_timings[0] = encode_timing(pio_encode_nop(),               false, true,  false, DPI_MODE_H_SYNC_WIDTH);
+  vblank_line_timings[1] = encode_timing(pio_encode_nop(),               false, false, false, DPI_MODE_H_BACK_PORCH);
+  vblank_line_timings[2] = encode_timing(pio_encode_nop(),               false, false, false, DPI_MODE_H_ACTIVE_PIXELS);
+  vblank_line_timings[3] = encode_timing(pio_encode_nop(),               false, false, false, DPI_MODE_H_FRONT_PORCH);
+
+  vsync_line_timings[0]  = encode_timing(pio_encode_nop(),               true,  true,  false, DPI_MODE_H_SYNC_WIDTH);
+  vsync_line_timings[1]  = encode_timing(pio_encode_nop(),               true,  false, false, DPI_MODE_H_BACK_PORCH);
+  vsync_line_timings[2]  = encode_timing(pio_encode_nop(),               true,  false, false, DPI_MODE_H_ACTIVE_PIXELS);
+  vsync_line_timings[3]  = encode_timing(pio_encode_nop(),               true,  false, false, DPI_MODE_H_FRONT_PORCH);
+
+  // setup timing program
+  int num_sync_pins = 2; // h/v sync
+  const int num_data_pins = 16; // assume 16-bit/565
+
+  int pio_offset = pio_add_program(pio, &dpi_timing_program);
+
+  // allocate data first so unassigned clock pin doesn't cause problems
+  data_sm = pio_claim_unused_sm(pio, true);
+  timing_sm = pio_claim_unused_sm(pio, true);
+
+  pio_sm_config cfg = dpi_timing_program_get_default_config(pio_offset);
+
+  const int clkdiv = clock_get_hz(clk_sys) / (DPI_MODE_CLOCK * 2);
+  assert(clock_get_hz(clk_sys) / clkdiv == DPI_MODE_CLOCK * 2);
+  sm_config_set_clkdiv_int_frac(&cfg, clkdiv, 0);
+
+  sm_config_set_out_shift(&cfg, false, true, 32);
+  sm_config_set_out_pins(&cfg, DPI_SYNC_PIN_BASE, num_sync_pins);
+  sm_config_set_fifo_join(&cfg, PIO_FIFO_JOIN_TX);
+#ifdef DPI_CLOCK_PIN
+  sm_config_set_sideset_pins(&cfg, DPI_CLOCK_PIN);
+#endif
+
+  pio_sm_init(pio, timing_sm, pio_offset, &cfg);
+
+  // setup data program
+
+#ifdef DPI_BIT_REVERSE
+  pio_offset = pio_add_program(pio, &dpi_data_reversed_16_program);
+
+  cfg = dpi_data_reversed_16_program_get_default_config(pio_offset);
+  assert(!(clkdiv & 1));
+  sm_config_set_clkdiv_int_frac(&cfg, clkdiv / 2, 0);
+#else
+  pio_offset = pio_add_program(pio, &dpi_data_16_program);
+
+  cfg = dpi_data_16_program_get_default_config(pio_offset);
+  sm_config_set_clkdiv_int_frac(&cfg, clkdiv, 0);
+#endif
+  sm_config_set_out_shift(&cfg, true, true, 32);
+  sm_config_set_in_shift(&cfg, false, false, 32);
+  sm_config_set_out_pins(&cfg, DPI_DATA_PIN_BASE, num_data_pins);
+  sm_config_set_fifo_join(&cfg, PIO_FIFO_JOIN_TX);
+
+  data_program_offset = pio_offset;
+
+  pio_sm_init(pio, data_sm, pio_offset, &cfg);
+
+  // init Y register
+  pio_sm_put(pio, data_sm, DPI_MODE_H_ACTIVE_PIXELS - 1);
+  pio_sm_exec(pio, data_sm, pio_encode_out(pio_y, 32));
+
+  // init pins
+  for(int i = 0; i < num_sync_pins; i++)
+    pio_gpio_init(pio, DPI_SYNC_PIN_BASE + i);
+
+  for(int i = 0; i < num_data_pins; i++)
+    pio_gpio_init(pio, DPI_DATA_PIN_BASE + i);
+
+  pio_sm_set_consecutive_pindirs(pio, timing_sm, DPI_SYNC_PIN_BASE, num_sync_pins, true);
+  pio_sm_set_consecutive_pindirs(pio, data_sm, DPI_DATA_PIN_BASE, num_data_pins, true);
+
+  bi_decl_if_func_used(bi_pin_mask_with_name(3 << DPI_SYNC_PIN_BASE, "Display Sync"));
+  bi_decl_if_func_used(bi_pin_mask_with_name(0xFFFF << DPI_DATA_PIN_BASE, "Display Data"));
+
+#ifdef DPI_CLOCK_PIN
+  pio_gpio_init(pio, DPI_CLOCK_PIN);
+  pio_sm_set_consecutive_pindirs(pio, timing_sm, DPI_CLOCK_PIN, 1, true);
+
+  bi_decl_if_func_used(bi_1pin_with_name(DPI_CLOCK_PIN, "Display Clock"));
+#endif
+
+  // setup PIO IRQ
+  pio_set_irq0_source_enabled(pio, pio_interrupt_source_t(pis_sm0_tx_fifo_not_full + timing_sm), true);
+  irq_set_exclusive_handler(pio_get_irq_num(pio, 0), pio_timing_irq_handler);
+  irq_set_enabled(pio_get_irq_num(pio, 0), true);
+
+  // setup data DMA
+  // chain channels in a loop
+  for(int i = 0; i < DPI_NUM_DMA_CHANNELS; i++) {
+    dma_channel_claim(DPI_DMA_CH_BASE + i);
+    dma_channel_config c;
+    c = dma_channel_get_default_config(DPI_DMA_CH_BASE + i);
+
+    int next_chan = i == (DPI_NUM_DMA_CHANNELS - 1) ? 0 : i + 1;
+
+    channel_config_set_chain_to(&c, DPI_DMA_CH_BASE + next_chan);
+    channel_config_set_dreq(&c, pio_get_dreq(pio, data_sm, true));
+
+    dma_channel_configure(
+      DPI_DMA_CH_BASE + i,
+      &c,
+      &pio->txf[data_sm],
+      cur_display_buffer,
+      DPI_MODE_H_ACTIVE_PIXELS,
+      false
+    );
+  }
+
+  const unsigned chan_mask = (1 << DPI_NUM_DMA_CHANNELS) - 1;
+
+  dma_hw->ints0 = (chan_mask << DPI_DMA_CH_BASE);
+  dma_hw->inte0 = (chan_mask << DPI_DMA_CH_BASE);
+  irq_set_exclusive_handler(DMA_IRQ_0, dma_irq_handler);
+  irq_set_enabled(DMA_IRQ_0, true);
+}
+
+void update_display(uint32_t time) {
+  if(do_render) {
+    blit::render(time);
+
+    // start dma/pio after first render
+    if(!started && blit::screen.data) {
+      started = true;
+      dma_channel_start(DPI_DMA_CH_BASE);
+      pio_set_sm_mask_enabled(pio, 1 << timing_sm | 1 << data_sm, true);
+    } else if(cur_surf_info.bounds.w != line_width || new_v_repeat != v_repeat) {
+      need_mode_change = true;
+    }
+    do_render = false;
+  }
+}
+
+void init_display_core1() {
+}
+
+void update_display_core1() {
+}
+
+bool display_render_needed() {
+  return do_render;
+}
+
+bool display_mode_supported(blit::ScreenMode new_mode, const blit::SurfaceTemplate &new_surf_template) {
+  if(new_surf_template.format != blit::PixelFormat::RGB565)
+    return false;
+
+  auto w = new_surf_template.bounds.w;
+  auto h = new_surf_template.bounds.h;
+
+  const int min_size = 96; // clamp smallest size
+
+  // width needs to be even
+  // allow a little rounding (it'll be filled with black)
+  int repeat = DPI_MODE_H_ACTIVE_PIXELS / w;
+  if(w < min_size || DPI_MODE_H_ACTIVE_PIXELS % w > repeat + 1 || (w & 1))
+    return false;
+
+  if(h < min_size || DPI_MODE_V_ACTIVE_LINES % h)
+    return false;
+
+  return true;
+}
+
+void display_mode_changed(blit::ScreenMode new_mode, blit::SurfaceTemplate &new_surf_template) {
+  auto display_buf_base = (uint8_t *)screen_fb;
+
+  // prevent buffer swap while we're doing this
+  do_render = true;
+
+  bool use_second_buf = fb_double_buffer && (!blit::screen.data || blit::screen.data == display_buf_base);
+  cur_display_buffer = use_second_buf ? display_buf_base + get_display_page_size() : display_buf_base;
+
+  // avoid resetting screen.data to first buffer, causing both buffers to be the same
+  if(fb_double_buffer && !use_second_buf)
+    new_surf_template.data = display_buf_base + get_display_page_size();
+
+  // set h/v repeat
+  new_v_repeat = DPI_MODE_V_ACTIVE_LINES / new_surf_template.bounds.h;
+
+  // check if we're actually changing scale
+  if(new_v_repeat == v_repeat && new_surf_template.bounds.w == line_width)
+    return;
+
+  // don't do it yet if already started
+  // (will set need_mode_change after next render)
+  if(started)
+    return;
+
+  v_repeat = new_v_repeat;
+  line_width = new_surf_template.bounds.w;
+
+  update_h_repeat();
+
+  // reconfigure DMA channels
+  // FIXME: update addr for 2nd+ line
+  for(int i = 0; i < DPI_NUM_DMA_CHANNELS; i++)
+    dma_channel_set_trans_count(DPI_DMA_CH_BASE + i, line_width / 2, false);
+}
diff --git a/32blit-pico/display/dpi.pio b/32blit-pico/display/dpi.pio
new file mode 100644
index 0000000..a846a23
--- /dev/null
+++ b/32blit-pico/display/dpi.pio
@@ -0,0 +1,38 @@
+; clock/sync generation, inspired by scanvideo
+.program dpi_timing
+.side_set 1 ; clock
+  out exec, 16       side 1 ; may set/clear IRQ
+
+  out x, 13          side 1 ; delay counter
+  out pins, 3        side 0 ; hsync/vsync/de bits
+
+delay_loop:
+  nop                side 1
+  jmp x-- delay_loop side 0
+
+
+.program dpi_data_16
+  wait irq, 4
+  mov x, y ; setup counter from pre-initialised Y
+
+data_loop:
+  out pins, 16
+PUBLIC data_loop_delay: ; patched to add delays
+  jmp x-- data_loop
+
+  mov pins, null
+
+; reversed bit order
+; needs to run at 2x clock
+.program dpi_data_reversed_16
+  wait irq, 4
+  mov x, y; setup counter from pre-initialised Y
+
+data_loop:
+  out isr, 16
+  in null, 16
+  mov pins, ::isr
+PUBLIC data_loop_delay: ; patched to add delays
+  jmp x-- data_loop
+
+  mov pins, null [1]
diff --git a/32blit-pico/display/picovision.cpp b/32blit-pico/display/picovision.cpp
index e8f5270..84583c4 100644
--- a/32blit-pico/display/picovision.cpp
+++ b/32blit-pico/display/picovision.cpp
@@ -211,7 +211,7 @@
       }
     } else {
       ram.wait_for_finish_blocking(); // make sure to always wait
-    
+
       for(unsigned i = 0; i < step; i += h_repeat) {
         for(int j = 0; j < h_repeat; j++)
           *ptr++ = *s;
@@ -310,7 +310,7 @@
 
   // write frame table
   uint frame_table_addr = 4 * 7;
-  
+
   for(int y = 0; y < height; y += buf_size) {
     int step = std::min(buf_size, height - y);
     for(int i = 0; i < step; i++) {
@@ -348,13 +348,7 @@
   gpio_put(RAM_SEL, 0);
   sleep_ms(100);
 
-  // i2c init
-  i2c_init(i2c1, 400000);
-  gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
-  gpio_pull_up(I2C_SDA);
-  gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
-  gpio_pull_up(I2C_SCL);
-
+  static_assert(i2c_default == i2c1);
   uint8_t resolution = 0; // 640x480
   uint8_t buf[2] = {I2C_REG_SET_RES, resolution};
   i2c_write_blocking(i2c1, I2C_ADDR, buf, 2, false);
@@ -369,14 +363,14 @@
 
     i++;
   }
-  
+
   return -1;
 }
 
 void update_display(uint32_t time) {
   if(!do_render)
     return;
-  
+
   blit::render(time);
 
   flush_batch();
diff --git a/32blit-pico/input/tca9555.cpp b/32blit-pico/input/tca9555.cpp
new file mode 100644
index 0000000..1030f86
--- /dev/null
+++ b/32blit-pico/input/tca9555.cpp
@@ -0,0 +1,77 @@
+#include <cstdio>
+
+#include "hardware/gpio.h"
+#include "hardware/i2c.h"
+
+#include "input.hpp"
+
+#include "config.h"
+
+#include "engine/api_private.hpp"
+#include "engine/input.hpp"
+
+#ifndef TCA9555_I2C
+#define TCA9555_I2C i2c_default
+#endif
+
+#ifndef TCA9555_ADDR
+#define TCA9555_ADDR 0x21
+#endif
+
+// QwST Pad
+#define TCA9555_LEFT_IO   2
+#define TCA9555_RIGHT_IO  3
+#define TCA9555_UP_IO     1
+#define TCA9555_DOWN_IO   4
+#define TCA9555_A_IO      14
+#define TCA9555_B_IO      12
+#define TCA9555_X_IO      15
+#define TCA9555_Y_IO      13
+#define TCA9555_START_IO  11
+#define TCA9555_SELECT_IO 5
+
+void init_input() {
+  // setup for reading
+  uint8_t port = 0;
+  i2c_write_blocking(TCA9555_I2C, TCA9555_ADDR, &port, 1, true);
+}
+
+void update_input() {
+  uint16_t gpio = 0;
+
+  i2c_read_blocking(TCA9555_I2C, TCA9555_ADDR, (uint8_t *)&gpio, 2, false);
+
+  uint32_t new_buttons = 0;
+
+  if(!(gpio & (1 << TCA9555_LEFT_IO)))
+    new_buttons |= blit::Button::DPAD_LEFT;
+
+  if(!(gpio & (1 << TCA9555_RIGHT_IO)))
+    new_buttons |= blit::Button::DPAD_RIGHT;
+
+  if(!(gpio & (1 << TCA9555_UP_IO)))
+    new_buttons |= blit::Button::DPAD_UP;
+
+  if(!(gpio & (1 << TCA9555_DOWN_IO)))
+    new_buttons |= blit::Button::DPAD_DOWN;
+
+  if(!(gpio & (1 << TCA9555_A_IO)))
+    new_buttons |= blit::Button::A;
+
+  if(!(gpio & (1 << TCA9555_B_IO)))
+    new_buttons |= blit::Button::B;
+
+  if(!(gpio & (1 << TCA9555_X_IO)))
+    new_buttons |= blit::Button::X;
+
+  if(!(gpio & (1 << TCA9555_Y_IO)))
+    new_buttons |= blit::Button::Y;
+
+  if(!(gpio & (1 << TCA9555_START_IO)))
+    new_buttons |= blit::Button::HOME;
+
+  if(!(gpio & (1 << TCA9555_SELECT_IO)))
+    new_buttons |= blit::Button::MENU;
+
+  blit::api_data.buttons = new_buttons;
+}
diff --git a/32blit-pico/main.cpp b/32blit-pico/main.cpp
index bda4c2f..aef3030 100644
--- a/32blit-pico/main.cpp
+++ b/32blit-pico/main.cpp
@@ -1,6 +1,7 @@
 #include <cstdio>
 
 #include "hardware/clocks.h"
+#include "hardware/i2c.h"
 #include "hardware/structs/rosc.h"
 #include "hardware/vreg.h"
 #include "hardware/timer.h"
@@ -200,6 +201,18 @@
 }
 #endif
 
+static void init_i2c() {
+  // multiple drivers need i2c, initialise it in one place if needed
+#ifdef DEFAULT_I2C_CLOCK
+  i2c_init(i2c_default, DEFAULT_I2C_CLOCK);
+  gpio_set_function(PICO_DEFAULT_I2C_SDA_PIN, GPIO_FUNC_I2C);
+  gpio_set_function(PICO_DEFAULT_I2C_SCL_PIN, GPIO_FUNC_I2C);
+
+  gpio_pull_up(PICO_DEFAULT_I2C_SDA_PIN);
+  gpio_pull_up(PICO_DEFAULT_I2C_SCL_PIN);
+#endif
+}
+
 int main() {
 #if OVERCLOCK_250
 #ifndef PICO_RP2350
@@ -215,6 +228,8 @@
   init_usb();
   stdio_init_all();
 
+  init_i2c();
+
   init_led();
   init_display();
   init_input();
diff --git a/32blit-pico/storage/sd_spi.cpp b/32blit-pico/storage/sd_spi.cpp
index d2f93f0..f0b2809 100644
--- a/32blit-pico/storage/sd_spi.cpp
+++ b/32blit-pico/storage/sd_spi.cpp
@@ -262,6 +262,15 @@
 
   // this will be called again it it fails
   if(!sd_io_initialised) {
+    int base = 0;
+
+#if SD_MOSI >= 32 || SD_MISO >= 32 || SD_SCK >= 32
+    // assumes anything else using this PIO can also deal with the base
+    static_assert(SD_MOSI >= 16 && SD_MISO >= 16 && SD_SCK >= 16);
+    pio_set_gpio_base(sd_pio, 16);
+    base = 16;
+#endif
+
     uint offset = pio_add_program(sd_pio, &spi_cpha0_program);
 
     sd_sm = pio_claim_unused_sm(sd_pio, true);
@@ -276,8 +285,8 @@
     sm_config_set_in_shift(&c, false, true, 8);
 
     // MOSI, SCK output are low, MISO is input
-    pio_sm_set_pins_with_mask(sd_pio, sd_sm, 0, (1u << SD_SCK) | (1u << SD_MOSI));
-    pio_sm_set_pindirs_with_mask(sd_pio, sd_sm, (1u << SD_SCK) | (1u << SD_MOSI), (1u << SD_SCK) | (1u << SD_MOSI) | (1u << SD_MISO));
+    pio_sm_set_pins_with_mask64(sd_pio, sd_sm, 0, (1ull << SD_SCK) | (1ull << SD_MOSI));
+    pio_sm_set_pindirs_with_mask64(sd_pio, sd_sm, (1ull << SD_SCK) | (1ull << SD_MOSI), (1ull << SD_SCK) | (1ull << SD_MOSI) | (1ull << SD_MISO));
     pio_gpio_init(sd_pio, SD_MOSI);
     pio_gpio_init(sd_pio, SD_MISO);
     pio_gpio_init(sd_pio, SD_SCK);
@@ -285,7 +294,7 @@
     gpio_pull_up(SD_MISO);
 
     // SPI is synchronous, so bypass input synchroniser to reduce input delay.
-    hw_set_bits(&sd_pio->input_sync_bypass, 1u << SD_MISO);
+    hw_set_bits(&sd_pio->input_sync_bypass, 1u << (SD_MISO - base));
 
     pio_sm_init(sd_pio, sd_sm, offset, &c);
     pio_sm_set_enabled(sd_pio, sd_sm, true);