diff --git a/Cargo.lock b/Cargo.lock
index bc8866ca69a7cf7b1d323bcb35c950eda27f72c9..c87e0125df2c7164cf8a89e0843babc049345169 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -270,6 +270,7 @@ dependencies = [
  "rf4463",
  "ringbuffer",
  "stm32f4xx-hal",
+ "systick-monotonic",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 91a85a69c1857ef4b220040e2d055a3b58a9e233..567f884876b60bf51488e4e84a8bb8883a5c19ed 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,4 +22,5 @@ panic-semihosting = "0.6"
 rf4463 = { path = "../rf4463" }
 cortex-m-rtic = { version = "1.1.4" }
 ringbuffer = { version = "0.13.0", default_features = false }
+systick-monotonic = "1.0.1"
 #panic-reset = "0.1.1"
diff --git a/src/main.rs b/src/main.rs
index 9354cafb09bdeb7d9e81b9bfa33ca6d1ffb4c4a8..d87ee617c10314de8f468d2596832e1c8265dfd2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,26 +8,38 @@ extern crate panic_semihosting;
 extern crate stm32f4xx_hal as hal;
 
 mod packet;
+mod spi_data;
 
-#[rtic::app(device = stm32f4xx_hal::pac, peripherals = true)]
+#[rtic::app(device = stm32f4xx_hal::pac, peripherals = true, dispatchers = [USART6])]
 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};
     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;
     use rf4463::Rf4463;
     use ringbuffer::{ConstGenericRingBuffer, RingBuffer, RingBufferRead, RingBufferWrite};
+    use systick_monotonic::*;
 
     use crate::packet::Packet;
     use crate::packet::PACKET_LEN;
+    use crate::spi_data::{self, SpiData};
 
     // 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 = spi_data::LEN;
+
+    const SYS_CLK: u32 = 100_000_000;
+
     const MODE: Mode = Mode {
         polarity: Polarity::IdleLow,
         phase: Phase::CaptureOnFirstTransition,
@@ -41,21 +53,31 @@ 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,
             }
         }
@@ -70,17 +92,25 @@ mod app {
     #[shared]
     struct Shared {
         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]>,
+        spi_data: SpiData,
     }
 
     #[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]>,
     }
 
+    #[monotonic(binds = SysTick, default = true)]
+    type Tonic = Systick<1000>;
+
     #[init]
     fn init(mut ctx: init::Context) -> (Shared, Local, init::Monotonics) {
         let rcc = ctx.device.RCC.constrain();
@@ -92,12 +122,18 @@ mod app {
         let clocks = rcc
             .cfgr
             .use_hse(25.MHz())
-            .sysclk(100.MHz())
+            .sysclk(SYS_CLK.Hz())
             .pclk1(48.MHz())
             .pclk2(48.MHz())
             .freeze();
         let mut sys_cfg = ctx.device.SYSCFG.constrain();
 
+        // setup our fake reset pin (temporary)
+        let mut reset_pin = gpiob.pb12.into_pull_up_input();
+        reset_pin.make_interrupt_source(&mut sys_cfg);
+        reset_pin.enable_interrupt(&mut ctx.device.EXTI);
+        reset_pin.trigger_on_edge(&mut ctx.device.EXTI, gpio::Edge::Rising);
+
         // setup 4463 spi
         let mosi = gpioa.pa7.into_alternate().speed(Speed::VeryHigh);
         let miso = gpioa.pa6.into_alternate().speed(Speed::VeryHigh);
@@ -116,38 +152,93 @@ 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;
+        // 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 spi_data = SpiData::new(0.0, true);
+        let tx_buffer = cortex_m::singleton!(: [u8; PI_TX_BUFFER_LEN] = spi_data.into()).unwrap();
+        let other_tx_buf =
+            Some(cortex_m::singleton!(: [u8; PI_TX_BUFFER_LEN] = spi_data.into()).unwrap());
+
+        let mut pi_tx = dma::Transfer::init_memory_to_peripheral(
+            streams.4,
+            tx,
+            tx_buffer,
+            None,
+            dma::config::DmaConfig::default()
+                .memory_increment(true)
+                .fifo_enable(true)
+                .fifo_error_interrupt(true),
+        );
+
+        // Enable circular mode
+        // safety: probably?
+        unsafe {
+            let dma1 = &*pac::DMA1::ptr();
+            dma1.st[4].cr.modify(|_, w| w.circ().set_bit());
+        }
+
+        pi_tx.start(|_tx| {});
 
-        let mut usart = Serial::new(
-            ctx.device.USART1,
-            (tx, rx),
-            Config::default().baudrate(921_600.bps()),
-            &clocks,
-        )
-        .unwrap();
+        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| {});
 
-        usart.listen(Event::Rxne);
+        // start our radio temp loop
+        get_radio_temp::spawn_after(1u64.secs()).unwrap();
 
         (
             Shared {
                 radio,
-                radio_temp: 0.0,
                 tx_buf: ConstGenericRingBuffer::new(),
+
+                pi_tx,
+                other_tx_buf,
+
+                spi_data,
             },
             Local {
                 radio_irq,
-                usart,
                 state: SlaveState::Idle,
+
+                pi_rx,
+                other_rx_buf,
             },
-            init::Monotonics(),
+            init::Monotonics(Systick::new(ctx.core.SYST, SYS_CLK)),
         )
     }
 
-    #[idle(shared=[radio, tx_buf, radio_temp])]
+    #[idle(shared=[radio, tx_buf, pi_tx, other_tx_buf, spi_data])]
     fn idle(mut ctx: idle::Context) -> ! {
-        let mut i = 0;
         let mut iterations_since_last_packet = 0;
         loop {
             if let Some(pkt) = ctx.shared.tx_buf.lock(|buf| buf.dequeue()) {
@@ -176,61 +267,116 @@ mod app {
                     }
                 }
 
+                // update our SPI bus to say if we have free space or not
+                let free = ctx.shared.tx_buf.lock(|b| b.len() < b.capacity() - 2);
+                let mut new_spi_data = ctx.shared.spi_data.lock(|sd| *sd);
+                new_spi_data.set_free(free);
+
+                set_spi_tx(
+                    &mut ctx.shared.pi_tx,
+                    &mut ctx.shared.other_tx_buf,
+                    &mut ctx.shared.spi_data,
+                    new_spi_data,
+                );
+
                 iterations_since_last_packet = 0;
             } else {
                 iterations_since_last_packet += 1;
             }
+        }
+    }
 
-            i += 1;
+    #[task(shared = [radio, spi_data, pi_tx, other_tx_buf])]
+    fn get_radio_temp(mut ctx: get_radio_temp::Context) {
+        // get temp every second
 
-            // get temp every 500 packets, or whenever when idle
-            if i >= 500 {
-                i = 0;
+        if let Ok(new_temp) = ctx.shared.radio.lock(|r| r.get_temp()) {
+            let mut new_spi_data = ctx.shared.spi_data.lock(|sd| *sd);
+            new_spi_data.set_temp(new_temp);
 
-                if let Ok(new_temp) = ctx.shared.radio.lock(|r| r.get_temp()) {
-                    ctx.shared.radio_temp.lock(|cur_temp| *cur_temp = new_temp);
-                }
-            }
+            set_spi_tx(
+                &mut ctx.shared.pi_tx,
+                &mut ctx.shared.other_tx_buf,
+                &mut ctx.shared.spi_data,
+                new_spi_data,
+            );
         }
+
+        get_radio_temp::spawn_after(1u64.secs()).unwrap();
+    }
+
+    #[task(binds = DMA1_STREAM3, priority = 2, local = [state, pi_rx, other_rx_buf], shared = [tx_buf, pi_tx, other_tx_buf, spi_data])]
+    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,
+                &mut ctx.shared.spi_data,
+            );
+        }
+
+        *ctx.local.other_rx_buf = Some(old_buf);
+
+        rx.clear_transfer_complete_interrupt();
+        rx.clear_fifo_error_interrupt();
     }
 
-    #[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_STREAM4, priority = 2, shared = [pi_tx])]
+    fn pi_spi_tx(mut ctx: pi_spi_tx::Context) {
+        ctx.shared.pi_tx.lock(|tx| tx.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();
+        }
+    }
 
-        let x = block!(usart.read()).unwrap();
+    // temporary. Eventually we'll want the pi to be connected directly to the reset pin
+    #[task(binds = EXTI15_10, priority = 5)]
+    fn reset(_: reset::Context) {
+        cortex_m::peripheral::SCB::sys_reset();
+    }
 
+    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]>>,
+        C: rtic::Mutex<T = SpiData>,
+    >(
+        x: u8,
+        state: &mut SlaveState,
+        buf: &mut T,
+        tx: &mut A,
+        other_tx: &mut B,
+        spi_data: &mut C,
+    ) {
         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
-                            } else {
-                                block!(usart.write(0x02)).unwrap(); // safe to send a packet
-                            }
+                            let free = buf.lock(|b| b.len() < b.capacity() - 2);
 
-                            *state = SlaveState::RecvPacket(Packet::default());
-                        }
+                            let mut new_spi_data = spi_data.lock(|sd| *sd);
+                            new_spi_data.set_free(free);
 
-                        SlaveCmd::GetTemp => {
-                            let temp = ctx.shared.radio_temp.lock(|x| *x);
-                            usart.bwrite_all(&temp.to_le_bytes()).unwrap();
-                        }
+                            set_spi_tx(tx, other_tx, spi_data, new_spi_data);
 
-                        SlaveCmd::Sync => {
-                            usart.bwrite_all(&[0x03]).unwrap();
+                            *state = SlaveState::RecvPacket(Packet::default());
                         }
                     }
                 }
@@ -247,13 +393,32 @@ 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<
+        A: rtic::Mutex<T = TxTransfer>,
+        B: rtic::Mutex<T = Option<&'static mut [u8; PI_TX_BUFFER_LEN]>>,
+        C: rtic::Mutex<T = SpiData>,
+    >(
+        tx: &mut A,
+        other_tx: &mut B,
+        spi_data: &mut C,
+        new_spi_data: SpiData,
+    ) {
+        let bytes: [u8; spi_data::LEN] = new_spi_data.into();
+
+        // if the new data is equal to the old data, do nothing
+        if spi_data.lock(|sd| <[u8; spi_data::LEN]>::from(*sd)) == bytes {
+            return;
         }
+
+        spi_data.lock(|sd| *sd = new_spi_data);
+
+        other_tx.lock(|t| t.as_mut().unwrap().clone_from_slice(&bytes));
+
+        other_tx.lock(|t| {
+            let (tx, _) =
+                tx.lock(|tx_transfer| tx_transfer.next_transfer(t.take().unwrap()).unwrap());
+
+            t.replace(tx);
+        });
     }
 }
diff --git a/src/spi_data.rs b/src/spi_data.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4c2655bb20ca054c77d346a86d1b971492741a20
--- /dev/null
+++ b/src/spi_data.rs
@@ -0,0 +1,43 @@
+// Represents the data that we repeatedly put on the SPI bus
+pub const LEN: usize = 7;
+
+#[derive(Copy, Clone, Debug)]
+pub struct SpiData {
+    temp: f32,
+    free_space: bool,
+}
+
+impl SpiData {
+    pub fn new(temp: f32, free_space: bool) -> Self {
+        Self { temp, free_space }
+    }
+
+    pub fn set_temp(&mut self, temp: f32) {
+        self.temp = temp
+    }
+
+    pub fn set_free(&mut self, free_space: bool) {
+        self.free_space = free_space;
+    }
+
+    pub fn free_space(&self) -> bool {
+        self.free_space
+    }
+}
+
+// There's probably a better way to do this
+// if the temp happens to have some 0xE5 or 0xAA/0x55 in there, it might be possible to cause problems?
+// could just use 7 bits per byte and have the extra bit cleared in the data, and set in the pre/post-amble
+impl From<SpiData> for [u8; LEN] {
+    fn from(data: SpiData) -> Self {
+        let free = if data.free_space { 0xAA } else { 0x55 };
+
+        let temp = data.temp.to_le_bytes();
+
+        let mut out = [0xE5, free, 0, 0, 0, 0, 0xE5];
+
+        out[2..6].copy_from_slice(&temp);
+
+        out
+    }
+}