diff --git a/src/main.rs b/src/main.rs
index 9354cafb09bdeb7d9e81b9bfa33ca6d1ffb4c4a8..6c670af3c66c7ddf62f18fd4bd01d04d9a7a4147 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,10 +12,12 @@ mod packet;
 #[rtic::app(device = stm32f4xx_hal::pac, peripherals = true)]
 mod app {
     use hal::block;
+    use hal::dma;
     use hal::gpio::{self, Speed};
-    use hal::pac::{SPI1, TIM5, USART1};
+    use hal::pac::{self, SPI1, TIM5, USART1};
     use hal::prelude::*;
     use hal::serial::{Config, Event, Serial};
+    use hal::spi;
     use hal::spi::{Mode, Phase, Polarity, Spi};
     use hal::timer::Delay;
     use rf4463::config::RADIO_CONFIG_500_2;
@@ -28,6 +30,13 @@ mod app {
     // in # packets
     const BUFFER_LEN: usize = 50;
 
+    // in bytes
+    // explicitly make this smaller than a packet so that we don't buffer things in the tx DMA for a while
+    const PI_RX_BUFFER_LEN: usize = 256;
+
+    // in bytes
+    const PI_TX_BUFFER_LEN: usize = 1;
+
     const MODE: Mode = Mode {
         polarity: Polarity::IdleLow,
         phase: Phase::CaptureOnFirstTransition,
@@ -41,21 +50,33 @@ mod app {
         Delay<TIM5, 1000000>,
     >;
 
+    type RxTransfer = dma::Transfer<
+        dma::StreamX<pac::DMA1, 3>,
+        0,
+        spi::Rx<pac::SPI2>,
+        dma::PeripheralToMemory,
+        &'static mut [u8; PI_RX_BUFFER_LEN],
+    >;
+
+    type TxTransfer = dma::Transfer<
+        dma::StreamX<pac::DMA1, 4>,
+        0,
+        spi::Tx<pac::SPI2>,
+        dma::MemoryToPeripheral,
+        &'static mut [u8; PI_TX_BUFFER_LEN],
+    >;
+
     #[derive(Debug)]
     enum SlaveCmd {
-        BufferStatus,
         SendPacket,
         GetTemp,
-        Sync,
     }
 
     impl SlaveCmd {
         pub fn from_u8(i: u8) -> Option<Self> {
             match i {
-                0x00 => Some(Self::BufferStatus),
                 0x01 => Some(Self::SendPacket),
                 0x02 => Some(Self::GetTemp),
-                0x03 => Some(Self::Sync),
                 _ => None,
             }
         }
@@ -72,13 +93,18 @@ mod app {
         radio: Radio,
         radio_temp: f32,
         tx_buf: ConstGenericRingBuffer<Packet, BUFFER_LEN>,
+
+        pi_tx: TxTransfer,
+        other_tx_buf: Option<&'static mut [u8; PI_TX_BUFFER_LEN]>,
     }
 
     #[local]
     struct Local {
         radio_irq: gpio::Pin<'B', 2, gpio::Input>,
-        usart: Serial<USART1>,
         state: SlaveState,
+
+        pi_rx: RxTransfer,
+        other_rx_buf: Option<&'static mut [u8; PI_RX_BUFFER_LEN]>,
     }
 
     #[init]
@@ -99,6 +125,7 @@ mod app {
         let mut sys_cfg = ctx.device.SYSCFG.constrain();
 
         // setup 4463 spi
+
         let mosi = gpioa.pa7.into_alternate().speed(Speed::VeryHigh);
         let miso = gpioa.pa6.into_alternate().speed(Speed::VeryHigh);
         let sclk = gpioa.pa5.into_alternate().speed(Speed::VeryHigh);
@@ -116,36 +143,78 @@ mod app {
         radio_irq.enable_interrupt(&mut ctx.device.EXTI);
         radio_irq.trigger_on_edge(&mut ctx.device.EXTI, gpio::Edge::Falling);
 
-        // setup Pi UART
-        let tx = gpioa.pa9;
-        let rx = gpioa.pa10;
-
-        let mut usart = Serial::new(
-            ctx.device.USART1,
-            (tx, rx),
-            Config::default().baudrate(921_600.bps()),
-            &clocks,
-        )
-        .unwrap();
-
-        usart.listen(Event::Rxne);
+        // setup Pi SPI
+        let pi_mosi = gpiob.pb15.internal_resistor(gpio::Pull::Up);
+        let pi_miso = gpiob.pb14.internal_resistor(gpio::Pull::Down);
+        let pi_sclk = gpiob.pb13.internal_resistor(gpio::Pull::Down);
+
+        // TODO nss?
+
+        let mut pi_spi = ctx
+            .device
+            .SPI2
+            .spi_slave((pi_sclk, pi_miso, pi_mosi, None), MODE);
+        pi_spi.set_internal_nss(false);
+
+        let (tx, rx) = pi_spi.use_dma().txrx();
+
+        let streams = dma::StreamsTuple::new(ctx.device.DMA1);
+
+        let tx_buffer =
+            cortex_m::singleton!(: [u8; PI_TX_BUFFER_LEN] = [0x02; PI_TX_BUFFER_LEN]).unwrap();
+        let other_tx_buf =
+            Some(cortex_m::singleton!(: [u8; PI_TX_BUFFER_LEN] = [0u8; PI_TX_BUFFER_LEN]).unwrap());
+
+        let mut pi_tx = dma::Transfer::init_memory_to_peripheral(
+            streams.4,
+            tx,
+            tx_buffer,
+            None,
+            dma::config::DmaConfig::default().memory_increment(false),
+        );
+
+        pi_tx.start(|_tx| {});
+
+        let rx_buffer =
+            cortex_m::singleton!(: [u8; PI_RX_BUFFER_LEN] = [0u8; PI_RX_BUFFER_LEN]).unwrap();
+        let other_rx_buf =
+            Some(cortex_m::singleton!(: [u8; PI_RX_BUFFER_LEN] = [0u8; PI_RX_BUFFER_LEN]).unwrap());
+
+        let mut pi_rx = dma::Transfer::init_peripheral_to_memory(
+            streams.3,
+            rx,
+            rx_buffer,
+            None,
+            dma::config::DmaConfig::default()
+                .memory_increment(true)
+                .fifo_enable(true)
+                .fifo_error_interrupt(true)
+                .transfer_complete_interrupt(true),
+        );
+
+        pi_rx.start(|_rx| {});
 
         (
             Shared {
                 radio,
                 radio_temp: 0.0,
                 tx_buf: ConstGenericRingBuffer::new(),
+
+                pi_tx,
+                other_tx_buf,
             },
             Local {
                 radio_irq,
-                usart,
                 state: SlaveState::Idle,
+
+                pi_rx,
+                other_rx_buf,
             },
             init::Monotonics(),
         )
     }
 
-    #[idle(shared=[radio, tx_buf, radio_temp])]
+    #[idle(shared=[radio, tx_buf, radio_temp, pi_tx, other_tx_buf])]
     fn idle(mut ctx: idle::Context) -> ! {
         let mut i = 0;
         let mut iterations_since_last_packet = 0;
@@ -176,6 +245,15 @@ mod app {
                     }
                 }
 
+                // update our SPI bus to say if we have free space or not
+                let b = if ctx.shared.tx_buf.lock(|b| b.len() >= b.capacity() - 2) {
+                    0x01 // not enough space for packet
+                } else {
+                    0x02 // space for packet
+                };
+
+                set_spi_tx_byte(b, &mut ctx.shared.pi_tx, &mut ctx.shared.other_tx_buf);
+
                 iterations_since_last_packet = 0;
             } else {
                 iterations_since_last_packet += 1;
@@ -194,43 +272,74 @@ mod app {
         }
     }
 
-    #[task(binds = USART1, priority = 2, local = [state, usart], shared = [tx_buf, radio_temp])]
-    fn usart1(mut ctx: usart1::Context) {
-        let state = ctx.local.state;
-        let usart = ctx.local.usart;
-        let mut buf = ctx.shared.tx_buf;
+    #[task(binds = DMA1_STREAM3, priority = 2, local = [state, pi_rx, other_rx_buf], shared = [tx_buf, pi_tx, other_tx_buf])]
+    fn pi_spi_recv(mut ctx: pi_spi_recv::Context) {
+        let rx = ctx.local.pi_rx;
+
+        let (old_buf, _) = rx
+            .next_transfer(ctx.local.other_rx_buf.take().unwrap())
+            .unwrap();
+
+        for b in old_buf.iter() {
+            handle_incoming_byte(
+                *b,
+                ctx.local.state,
+                &mut ctx.shared.tx_buf,
+                &mut ctx.shared.pi_tx,
+                &mut ctx.shared.other_tx_buf,
+            );
+        }
+
+        *ctx.local.other_rx_buf = Some(old_buf);
 
-        let x = block!(usart.read()).unwrap();
+        rx.clear_transfer_complete_interrupt();
+        rx.clear_fifo_error_interrupt();
+    }
+
+    #[task(binds = EXTI2, shared = [radio], local = [radio_irq])]
+    fn radio_irq(mut ctx: radio_irq::Context) {
+        ctx.shared.radio.lock(|r| r.interrupt()).unwrap();
 
+        let irq = ctx.local.radio_irq;
+        if irq.is_high() {
+            irq.clear_interrupt_pending_bit();
+        }
+    }
+
+    fn handle_incoming_byte<
+        T: rtic::Mutex<T = ConstGenericRingBuffer<Packet, BUFFER_LEN>>,
+        A: rtic::Mutex<T = TxTransfer>,
+        B: rtic::Mutex<T = Option<&'static mut [u8; PI_TX_BUFFER_LEN]>>,
+    >(
+        x: u8,
+        state: &mut SlaveState,
+        buf: &mut T,
+        tx: &mut A,
+        other_tx: &mut B,
+    ) {
         match state {
             SlaveState::Idle => {
                 if let Some(cmd) = SlaveCmd::from_u8(x) {
                     match cmd {
-                        SlaveCmd::BufferStatus => {
-                            if buf.lock(|b| b.is_full()) {
-                                block!(usart.write(0x01)).unwrap(); // not enough space for a new packet
-                            } else {
-                                block!(usart.write(0x02)).unwrap(); // safe to send a packet
-                            }
-                        }
-
                         SlaveCmd::SendPacket => {
-                            if buf.lock(|b| b.len() >= b.capacity() - 1) {
-                                block!(usart.write(0x01)).unwrap(); // not enough space for a new packet
+                            let b = if buf.lock(|b| b.len() >= b.capacity() - 2) {
+                                0x01 // not enough space for packet
                             } else {
-                                block!(usart.write(0x02)).unwrap(); // safe to send a packet
-                            }
+                                0x02 // space for packet
+                            };
+
+                            set_spi_tx_byte(b, tx, other_tx);
 
                             *state = SlaveState::RecvPacket(Packet::default());
                         }
 
                         SlaveCmd::GetTemp => {
-                            let temp = ctx.shared.radio_temp.lock(|x| *x);
-                            usart.bwrite_all(&temp.to_le_bytes()).unwrap();
-                        }
+                            todo!();
 
-                        SlaveCmd::Sync => {
-                            usart.bwrite_all(&[0x03]).unwrap();
+                            /*
+                                        let temp = ctx.shared.radio_temp.lock(|x| *x);
+                                        usart.bwrite_all(&temp.to_le_bytes()).unwrap();
+                            */
                         }
                     }
                 }
@@ -247,13 +356,23 @@ mod app {
         };
     }
 
-    #[task(binds = EXTI2, shared = [radio], local = [radio_irq])]
-    fn radio_irq(mut ctx: radio_irq::Context) {
-        ctx.shared.radio.lock(|r| r.interrupt()).unwrap();
-
-        let irq = ctx.local.radio_irq;
-        if irq.is_high() {
-            irq.clear_interrupt_pending_bit();
-        }
+    fn set_spi_tx_byte<
+        A: rtic::Mutex<T = TxTransfer>,
+        B: rtic::Mutex<T = Option<&'static mut [u8; PI_TX_BUFFER_LEN]>>,
+    >(
+        b: u8,
+        tx: &mut A,
+        other_tx: &mut B,
+    ) {
+        other_tx.lock(|t| t.as_mut().unwrap()[0] = b);
+
+        other_tx.lock(|t| {
+            let (tx, _) =
+                tx.lock(|tx_transfer| tx_transfer.next_transfer(t.take().unwrap()).unwrap());
+
+            t.replace(tx);
+        });
     }
 }
+
+// TODO need an interrupt to clear TX errors