diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..74ebf7962142a961ff317000ca46a44fa36b210a
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "firmware/rust1/embassy"]
+	path = firmware/rust1/embassy
+	url = https://github.com/embassy-rs/embassy
diff --git a/firmware/rust1/.cargo/config.toml b/firmware/rust1/.cargo/config.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2ee6fcb00cf26a3356f191adf02d543d9c795129
--- /dev/null
+++ b/firmware/rust1/.cargo/config.toml
@@ -0,0 +1,8 @@
+[target.'cfg(all(target_arch = "arm", target_os = "none"))']
+runner = "probe-rs-cli run --chip RP2040"
+
+[build]
+target = "thumbv6m-none-eabi"        # Cortex-M0 and Cortex-M0+
+
+[env]
+DEFMT_LOG = "debug"
diff --git a/firmware/rust1/Cargo.toml b/firmware/rust1/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..ffeb69f15bf6e5e8aa0bb24ee71b8552bac32e39
--- /dev/null
+++ b/firmware/rust1/Cargo.toml
@@ -0,0 +1,50 @@
+[package]
+edition = "2021"
+name = "embassy-rp-examples"
+version = "0.1.0"
+license = "MIT OR Apache-2.0"
+
+
+[dependencies]
+embassy-embedded-hal = { version = "0.1.0", path = "../../embassy-embedded-hal", features = ["defmt"] }
+embassy-sync = { version = "0.2.0", path = "../../embassy-sync", features = ["defmt"] }
+embassy-executor = { version = "0.2.0", path = "../../embassy-executor", features = ["arch-cortex-m", "executor-thread", "executor-interrupt", "defmt", "integrated-timers"] }
+embassy-time = { version = "0.1.0", path = "../../embassy-time", features = ["nightly", "unstable-traits", "defmt", "defmt-timestamp-uptime"] }
+embassy-rp = { version = "0.1.0", path = "../../embassy-rp", features = ["defmt", "unstable-traits", "nightly", "unstable-pac", "time-driver", "critical-section-impl"] }
+embassy-usb = { version = "0.1.0", path = "../../embassy-usb", features = ["defmt"] }
+embassy-net = { version = "0.1.0", path = "../../embassy-net", features = ["defmt", "nightly", "tcp", "dhcpv4", "medium-ethernet"] }
+embassy-futures = { version = "0.1.0", path = "../../embassy-futures" }
+embassy-usb-logger = { version = "0.1.0", path = "../../embassy-usb-logger" }
+embassy-lora = { version = "0.1.0", path = "../../embassy-lora", features = ["time", "defmt"] }
+lora-phy = { version = "1" }
+lorawan-device = { version = "0.10.0", default-features = false, features = ["async", "external-lora-phy"] }
+lorawan = { version = "0.7.3", default-features = false, features = ["default-crypto"] }
+
+defmt = "0.3"
+defmt-rtt = "0.4"
+fixed = "1.23.1"
+fixed-macro = "1.2"
+
+#cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] }
+cortex-m = { version = "0.7.6", features = ["inline-asm"] }
+cortex-m-rt = "0.7.0"
+panic-probe = { version = "0.3", features = ["print-defmt"] }
+futures = { version = "0.3.17", default-features = false, features = ["async-await", "cfg-target-has-atomic", "unstable"] }
+display-interface-spi = "0.4.1"
+embedded-graphics = "0.7.1"
+st7789 = "0.6.1"
+display-interface = "0.4.1"
+byte-slice-cast = { version = "1.2.0", default-features = false }
+smart-leds = "0.3.0"
+
+embedded-hal-1 = { package = "embedded-hal", version = "=1.0.0-alpha.10" }
+embedded-hal-async = "0.2.0-alpha.1"
+embedded-io = { version = "0.4.0", features = ["async", "defmt"] }
+embedded-storage = { version = "0.3" }
+static_cell = "1.0.0"
+log = "0.4"
+pio-proc = "0.2"
+pio = "0.2.1"
+
+[profile.release]
+debug = true
diff --git a/firmware/rust1/build.rs b/firmware/rust1/build.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3f915f931254e27ef7a904d5e572f753d1b9f57a
--- /dev/null
+++ b/firmware/rust1/build.rs
@@ -0,0 +1,36 @@
+//! This build script copies the `memory.x` file from the crate root into
+//! a directory where the linker can always find it at build time.
+//! For many projects this is optional, as the linker always searches the
+//! project root directory -- wherever `Cargo.toml` is. However, if you
+//! are using a workspace or have a more complicated build setup, this
+//! build script becomes required. Additionally, by requesting that
+//! Cargo re-run the build script whenever `memory.x` is changed,
+//! updating `memory.x` ensures a rebuild of the application with the
+//! new memory settings.
+
+use std::env;
+use std::fs::File;
+use std::io::Write;
+use std::path::PathBuf;
+
+fn main() {
+    // Put `memory.x` in our output directory and ensure it's
+    // on the linker search path.
+    let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
+    File::create(out.join("memory.x"))
+        .unwrap()
+        .write_all(include_bytes!("memory.x"))
+        .unwrap();
+    println!("cargo:rustc-link-search={}", out.display());
+
+    // By default, Cargo will re-run a build script whenever
+    // any file in the project changes. By specifying `memory.x`
+    // here, we ensure the build script is only re-run when
+    // `memory.x` is changed.
+    println!("cargo:rerun-if-changed=memory.x");
+
+    println!("cargo:rustc-link-arg-bins=--nmagic");
+    println!("cargo:rustc-link-arg-bins=-Tlink.x");
+    println!("cargo:rustc-link-arg-bins=-Tlink-rp.x");
+    println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
+}
diff --git a/firmware/rust1/embassy b/firmware/rust1/embassy
new file mode 160000
index 0000000000000000000000000000000000000000..dec75474d5fd82dd6abe25647f0e221c2266dda2
--- /dev/null
+++ b/firmware/rust1/embassy
@@ -0,0 +1 @@
+Subproject commit dec75474d5fd82dd6abe25647f0e221c2266dda2
diff --git a/firmware/rust1/memory.x b/firmware/rust1/memory.x
new file mode 100644
index 0000000000000000000000000000000000000000..aba861aae94a050c6f3c12b401161ff21f37af0c
--- /dev/null
+++ b/firmware/rust1/memory.x
@@ -0,0 +1,5 @@
+MEMORY {
+    BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100
+    FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
+    RAM   : ORIGIN = 0x20000000, LENGTH = 256K
+}
\ No newline at end of file
diff --git a/firmware/rust1/rust-toolchain.toml b/firmware/rust1/rust-toolchain.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2301ddc8d45048796745508ac27f6f835074759a
--- /dev/null
+++ b/firmware/rust1/rust-toolchain.toml
@@ -0,0 +1,14 @@
+# Before upgrading check that everything is available on all tier1 targets here:
+# https://rust-lang.github.io/rustup-components-history
+[toolchain]
+channel = "nightly-2023-04-18"
+components = [ "rust-src", "rustfmt", "llvm-tools-preview" ]
+targets = [
+    "thumbv7em-none-eabi",
+    "thumbv7m-none-eabi",
+    "thumbv6m-none-eabi",
+    "thumbv7em-none-eabihf",
+    "thumbv8m.main-none-eabihf",
+    "riscv32imac-unknown-none-elf",
+    "wasm32-unknown-unknown",
+]
diff --git a/firmware/rust1/src/bin/adc.rs b/firmware/rust1/src/bin/adc.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4202fd394c74e7fc5183c13c199ee3f9ccb8f432
--- /dev/null
+++ b/firmware/rust1/src/bin/adc.rs
@@ -0,0 +1,38 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::adc::{Adc, Config};
+use embassy_rp::interrupt;
+use embassy_time::{Duration, Timer};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    let irq = interrupt::take!(ADC_IRQ_FIFO);
+    let mut adc = Adc::new(p.ADC, irq, Config::default());
+
+    let mut p26 = p.PIN_26;
+    let mut p27 = p.PIN_27;
+    let mut p28 = p.PIN_28;
+
+    loop {
+        let level = adc.read(&mut p26).await;
+        info!("Pin 26 ADC: {}", level);
+        let level = adc.read(&mut p27).await;
+        info!("Pin 27 ADC: {}", level);
+        let level = adc.read(&mut p28).await;
+        info!("Pin 28 ADC: {}", level);
+        let temp = adc.read_temperature().await;
+        info!("Temp: {} degrees", convert_to_celsius(temp));
+        Timer::after(Duration::from_secs(1)).await;
+    }
+}
+
+fn convert_to_celsius(raw_temp: u16) -> f32 {
+    // According to chapter 4.9.5. Temperature Sensor in RP2040 datasheet
+    27.0 - (raw_temp as f32 * 3.3 / 4096.0 - 0.706) / 0.001721 as f32
+}
diff --git a/firmware/rust1/src/bin/blinky.rs b/firmware/rust1/src/bin/blinky.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7aa36a19fa4652195c415ccf319a7d98321808f8
--- /dev/null
+++ b/firmware/rust1/src/bin/blinky.rs
@@ -0,0 +1,26 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::gpio;
+use embassy_time::{Duration, Timer};
+use gpio::{Level, Output};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    let mut led = Output::new(p.PIN_25, Level::Low);
+
+    loop {
+        info!("led on!");
+        led.set_high();
+        Timer::after(Duration::from_secs(1)).await;
+
+        info!("led off!");
+        led.set_low();
+        Timer::after(Duration::from_secs(1)).await;
+    }
+}
diff --git a/firmware/rust1/src/bin/button.rs b/firmware/rust1/src/bin/button.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c5422c616defadf4fb90a1311863fb5edd7d5225
--- /dev/null
+++ b/firmware/rust1/src/bin/button.rs
@@ -0,0 +1,22 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use embassy_executor::Spawner;
+use embassy_rp::gpio::{Input, Level, Output, Pull};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    let button = Input::new(p.PIN_28, Pull::Up);
+    let mut led = Output::new(p.PIN_25, Level::Low);
+
+    loop {
+        if button.is_high() {
+            led.set_high();
+        } else {
+            led.set_low();
+        }
+    }
+}
diff --git a/firmware/rust1/src/bin/flash.rs b/firmware/rust1/src/bin/flash.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8d6b379f419c97b03414876dd3a5a78fd2e53dc4
--- /dev/null
+++ b/firmware/rust1/src/bin/flash.rs
@@ -0,0 +1,89 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::flash::{ERASE_SIZE, FLASH_BASE};
+use embassy_rp::peripherals::FLASH;
+use embassy_time::{Duration, Timer};
+use {defmt_rtt as _, panic_probe as _};
+
+const ADDR_OFFSET: u32 = 0x100000;
+const FLASH_SIZE: usize = 2 * 1024 * 1024;
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    info!("Hello World!");
+
+    // add some delay to give an attached debug probe time to parse the
+    // defmt RTT header. Reading that header might touch flash memory, which
+    // interferes with flash write operations.
+    // https://github.com/knurling-rs/defmt/pull/683
+    Timer::after(Duration::from_millis(10)).await;
+
+    let mut flash = embassy_rp::flash::Flash::<_, FLASH_SIZE>::new(p.FLASH);
+    erase_write_sector(&mut flash, 0x00);
+
+    multiwrite_bytes(&mut flash, ERASE_SIZE as u32);
+
+    loop {}
+}
+
+fn multiwrite_bytes(flash: &mut embassy_rp::flash::Flash<'_, FLASH, FLASH_SIZE>, offset: u32) {
+    info!(">>>> [multiwrite_bytes]");
+    let mut read_buf = [0u8; ERASE_SIZE];
+    defmt::unwrap!(flash.read(ADDR_OFFSET + offset, &mut read_buf));
+
+    info!("Addr of flash block is {:x}", ADDR_OFFSET + offset + FLASH_BASE as u32);
+    info!("Contents start with {=[u8]}", read_buf[0..4]);
+
+    defmt::unwrap!(flash.erase(ADDR_OFFSET + offset, ADDR_OFFSET + offset + ERASE_SIZE as u32));
+
+    defmt::unwrap!(flash.read(ADDR_OFFSET + offset, &mut read_buf));
+    info!("Contents after erase starts with {=[u8]}", read_buf[0..4]);
+    if read_buf.iter().any(|x| *x != 0xFF) {
+        defmt::panic!("unexpected");
+    }
+
+    defmt::unwrap!(flash.write(ADDR_OFFSET + offset, &[0x01]));
+    defmt::unwrap!(flash.write(ADDR_OFFSET + offset + 1, &[0x02]));
+    defmt::unwrap!(flash.write(ADDR_OFFSET + offset + 2, &[0x03]));
+    defmt::unwrap!(flash.write(ADDR_OFFSET + offset + 3, &[0x04]));
+
+    defmt::unwrap!(flash.read(ADDR_OFFSET + offset, &mut read_buf));
+    info!("Contents after write starts with {=[u8]}", read_buf[0..4]);
+    if &read_buf[0..4] != &[0x01, 0x02, 0x03, 0x04] {
+        defmt::panic!("unexpected");
+    }
+}
+
+fn erase_write_sector(flash: &mut embassy_rp::flash::Flash<'_, FLASH, FLASH_SIZE>, offset: u32) {
+    info!(">>>> [erase_write_sector]");
+    let mut buf = [0u8; ERASE_SIZE];
+    defmt::unwrap!(flash.read(ADDR_OFFSET + offset, &mut buf));
+
+    info!("Addr of flash block is {:x}", ADDR_OFFSET + offset + FLASH_BASE as u32);
+    info!("Contents start with {=[u8]}", buf[0..4]);
+
+    defmt::unwrap!(flash.erase(ADDR_OFFSET + offset, ADDR_OFFSET + offset + ERASE_SIZE as u32));
+
+    defmt::unwrap!(flash.read(ADDR_OFFSET + offset, &mut buf));
+    info!("Contents after erase starts with {=[u8]}", buf[0..4]);
+    if buf.iter().any(|x| *x != 0xFF) {
+        defmt::panic!("unexpected");
+    }
+
+    for b in buf.iter_mut() {
+        *b = 0xDA;
+    }
+
+    defmt::unwrap!(flash.write(ADDR_OFFSET + offset, &buf));
+
+    defmt::unwrap!(flash.read(ADDR_OFFSET + offset, &mut buf));
+    info!("Contents after write starts with {=[u8]}", buf[0..4]);
+    if buf.iter().any(|x| *x != 0xDA) {
+        defmt::panic!("unexpected");
+    }
+}
diff --git a/firmware/rust1/src/bin/gpio_async.rs b/firmware/rust1/src/bin/gpio_async.rs
new file mode 100644
index 0000000000000000000000000000000000000000..52d13a9d5556e5bc91ee32c8e2552e27c7483be7
--- /dev/null
+++ b/firmware/rust1/src/bin/gpio_async.rs
@@ -0,0 +1,39 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::gpio;
+use embassy_time::{Duration, Timer};
+use gpio::{Input, Level, Output, Pull};
+use {defmt_rtt as _, panic_probe as _};
+
+/// This example shows how async gpio can be used with a RP2040.
+///
+/// It requires an external signal to be manually triggered on PIN 16. For
+/// example, this could be accomplished using an external power source with a
+/// button so that it is possible to toggle the signal from low to high.
+///
+/// This example will begin with turning on the LED on the board and wait for a
+/// high signal on PIN 16. Once the high event/signal occurs the program will
+/// continue and turn off the LED, and then wait for 2 seconds before completing
+/// the loop and starting over again.
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    let mut led = Output::new(p.PIN_25, Level::Low);
+    let mut async_input = Input::new(p.PIN_16, Pull::None);
+
+    loop {
+        info!("wait_for_high. Turn on LED");
+        led.set_high();
+
+        async_input.wait_for_high().await;
+
+        info!("done wait_for_high. Turn off LED");
+        led.set_low();
+
+        Timer::after(Duration::from_secs(2)).await;
+    }
+}
diff --git a/firmware/rust1/src/bin/gpout.rs b/firmware/rust1/src/bin/gpout.rs
new file mode 100644
index 0000000000000000000000000000000000000000..236a653ac6a6fb75a0bde278bfa726bb24fb91a9
--- /dev/null
+++ b/firmware/rust1/src/bin/gpout.rs
@@ -0,0 +1,34 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::clocks;
+use embassy_time::{Duration, Timer};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+
+    let gpout3 = clocks::Gpout::new(p.PIN_25);
+    gpout3.set_div(1000, 0);
+    gpout3.enable();
+
+    loop {
+        gpout3.set_src(clocks::GpoutSrc::CLK_SYS);
+        info!(
+            "Pin 25 is now outputing CLK_SYS/1000, should be toggling at {}",
+            gpout3.get_freq()
+        );
+        Timer::after(Duration::from_secs(2)).await;
+
+        gpout3.set_src(clocks::GpoutSrc::CLK_REF);
+        info!(
+            "Pin 25 is now outputing CLK_REF/1000, should be toggling at {}",
+            gpout3.get_freq()
+        );
+        Timer::after(Duration::from_secs(2)).await;
+    }
+}
diff --git a/firmware/rust1/src/bin/i2c_async.rs b/firmware/rust1/src/bin/i2c_async.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d1a2e3cd759e7d1c8b5112a5e79e56fbcd71e964
--- /dev/null
+++ b/firmware/rust1/src/bin/i2c_async.rs
@@ -0,0 +1,102 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::i2c::{self, Config};
+use embassy_rp::interrupt;
+use embassy_time::{Duration, Timer};
+use embedded_hal_async::i2c::I2c;
+use {defmt_rtt as _, panic_probe as _};
+
+#[allow(dead_code)]
+mod mcp23017 {
+    pub const ADDR: u8 = 0x20; // default addr
+
+    macro_rules! mcpregs {
+        ($($name:ident : $val:expr),* $(,)?) => {
+            $(
+                pub const $name: u8 = $val;
+            )*
+
+            pub fn regname(reg: u8) -> &'static str {
+                match reg {
+                    $(
+                        $val => stringify!($name),
+                    )*
+                    _ => panic!("bad reg"),
+                }
+            }
+        }
+    }
+
+    // These are correct for IOCON.BANK=0
+    mcpregs! {
+        IODIRA: 0x00,
+        IPOLA: 0x02,
+        GPINTENA: 0x04,
+        DEFVALA: 0x06,
+        INTCONA: 0x08,
+        IOCONA: 0x0A,
+        GPPUA: 0x0C,
+        INTFA: 0x0E,
+        INTCAPA: 0x10,
+        GPIOA: 0x12,
+        OLATA: 0x14,
+        IODIRB: 0x01,
+        IPOLB: 0x03,
+        GPINTENB: 0x05,
+        DEFVALB: 0x07,
+        INTCONB: 0x09,
+        IOCONB: 0x0B,
+        GPPUB: 0x0D,
+        INTFB: 0x0F,
+        INTCAPB: 0x11,
+        GPIOB: 0x13,
+        OLATB: 0x15,
+    }
+}
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+
+    let sda = p.PIN_14;
+    let scl = p.PIN_15;
+    let irq = interrupt::take!(I2C1_IRQ);
+
+    info!("set up i2c ");
+    let mut i2c = i2c::I2c::new_async(p.I2C1, scl, sda, irq, Config::default());
+
+    use mcp23017::*;
+
+    info!("init mcp23017 config for IxpandO");
+    // init - a outputs, b inputs
+    i2c.write(ADDR, &[IODIRA, 0x00]).await.unwrap();
+    i2c.write(ADDR, &[IODIRB, 0xff]).await.unwrap();
+    i2c.write(ADDR, &[GPPUB, 0xff]).await.unwrap(); // pullups
+
+    let mut val = 1;
+    loop {
+        let mut portb = [0];
+
+        i2c.write_read(mcp23017::ADDR, &[GPIOB], &mut portb).await.unwrap();
+        info!("portb = {:02x}", portb[0]);
+        i2c.write(mcp23017::ADDR, &[GPIOA, val | portb[0]]).await.unwrap();
+        val = val.rotate_left(1);
+
+        // get a register dump
+        info!("getting register dump");
+        let mut regs = [0; 22];
+        i2c.write_read(ADDR, &[0], &mut regs).await.unwrap();
+        // always get the regdump but only display it if portb'0 is set
+        if portb[0] & 1 != 0 {
+            for (idx, reg) in regs.into_iter().enumerate() {
+                info!("{} => {:02x}", regname(idx as u8), reg);
+            }
+        }
+
+        Timer::after(Duration::from_millis(100)).await;
+    }
+}
diff --git a/firmware/rust1/src/bin/i2c_blocking.rs b/firmware/rust1/src/bin/i2c_blocking.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7623e33c83730cb8080ad19e3b9bd7a74b5a567e
--- /dev/null
+++ b/firmware/rust1/src/bin/i2c_blocking.rs
@@ -0,0 +1,70 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::i2c::{self, Config};
+use embassy_time::{Duration, Timer};
+use embedded_hal_1::i2c::I2c;
+use {defmt_rtt as _, panic_probe as _};
+
+#[allow(dead_code)]
+mod mcp23017 {
+    pub const ADDR: u8 = 0x20; // default addr
+
+    pub const IODIRA: u8 = 0x00;
+    pub const IPOLA: u8 = 0x02;
+    pub const GPINTENA: u8 = 0x04;
+    pub const DEFVALA: u8 = 0x06;
+    pub const INTCONA: u8 = 0x08;
+    pub const IOCONA: u8 = 0x0A;
+    pub const GPPUA: u8 = 0x0C;
+    pub const INTFA: u8 = 0x0E;
+    pub const INTCAPA: u8 = 0x10;
+    pub const GPIOA: u8 = 0x12;
+    pub const OLATA: u8 = 0x14;
+    pub const IODIRB: u8 = 0x01;
+    pub const IPOLB: u8 = 0x03;
+    pub const GPINTENB: u8 = 0x05;
+    pub const DEFVALB: u8 = 0x07;
+    pub const INTCONB: u8 = 0x09;
+    pub const IOCONB: u8 = 0x0B;
+    pub const GPPUB: u8 = 0x0D;
+    pub const INTFB: u8 = 0x0F;
+    pub const INTCAPB: u8 = 0x11;
+    pub const GPIOB: u8 = 0x13;
+    pub const OLATB: u8 = 0x15;
+}
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+
+    let sda = p.PIN_14;
+    let scl = p.PIN_15;
+
+    info!("set up i2c ");
+    let mut i2c = i2c::I2c::new_blocking(p.I2C1, scl, sda, Config::default());
+
+    use mcp23017::*;
+
+    info!("init mcp23017 config for IxpandO");
+    // init - a outputs, b inputs
+    i2c.write(ADDR, &[IODIRA, 0x00]).unwrap();
+    i2c.write(ADDR, &[IODIRB, 0xff]).unwrap();
+    i2c.write(ADDR, &[GPPUB, 0xff]).unwrap(); // pullups
+
+    let mut val = 0xaa;
+    loop {
+        let mut portb = [0];
+
+        i2c.write(mcp23017::ADDR, &[GPIOA, val]).unwrap();
+        i2c.write_read(mcp23017::ADDR, &[GPIOB], &mut portb).unwrap();
+
+        info!("portb = {:02x}", portb[0]);
+        val = !val;
+
+        Timer::after(Duration::from_secs(1)).await;
+    }
+}
diff --git a/firmware/rust1/src/bin/lora_lorawan.rs b/firmware/rust1/src/bin/lora_lorawan.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a9c84bf95c9403d9dcaa9a5714a41f306bdfc7e7
--- /dev/null
+++ b/firmware/rust1/src/bin/lora_lorawan.rs
@@ -0,0 +1,80 @@
+//! This example runs on the Raspberry Pi Pico with a Waveshare board containing a Semtech Sx1262 radio.
+//! It demonstrates LoRaWAN join functionality.
+#![no_std]
+#![no_main]
+#![macro_use]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_lora::iv::GenericSx126xInterfaceVariant;
+use embassy_lora::LoraTimer;
+use embassy_rp::gpio::{Input, Level, Output, Pin, Pull};
+use embassy_rp::spi::{Config, Spi};
+use embassy_time::Delay;
+use lora_phy::mod_params::*;
+use lora_phy::sx1261_2::SX1261_2;
+use lora_phy::LoRa;
+use lorawan::default_crypto::DefaultFactory as Crypto;
+use lorawan_device::async_device::lora_radio::LoRaRadio;
+use lorawan_device::async_device::{region, Device, JoinMode};
+use {defmt_rtt as _, panic_probe as _};
+
+const LORAWAN_REGION: region::Region = region::Region::EU868; // warning: set this appropriately for the region
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+
+    let miso = p.PIN_12;
+    let mosi = p.PIN_11;
+    let clk = p.PIN_10;
+    let spi = Spi::new(p.SPI1, clk, mosi, miso, p.DMA_CH0, p.DMA_CH1, Config::default());
+
+    let nss = Output::new(p.PIN_3.degrade(), Level::High);
+    let reset = Output::new(p.PIN_15.degrade(), Level::High);
+    let dio1 = Input::new(p.PIN_20.degrade(), Pull::None);
+    let busy = Input::new(p.PIN_2.degrade(), Pull::None);
+
+    let iv = GenericSx126xInterfaceVariant::new(nss, reset, dio1, busy, None, None).unwrap();
+
+    let mut delay = Delay;
+
+    let lora = {
+        match LoRa::new(
+            SX1261_2::new(BoardType::RpPicoWaveshareSx1262, spi, iv),
+            true,
+            &mut delay,
+        )
+        .await
+        {
+            Ok(l) => l,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    let radio = LoRaRadio::new(lora);
+    let region: region::Configuration = region::Configuration::new(LORAWAN_REGION);
+    let mut device: Device<_, Crypto, _, _> = Device::new(region, radio, LoraTimer::new(), embassy_rp::clocks::RoscRng);
+
+    defmt::info!("Joining LoRaWAN network");
+
+    // TODO: Adjust the EUI and Keys according to your network credentials
+    match device
+        .join(&JoinMode::OTAA {
+            deveui: [0, 0, 0, 0, 0, 0, 0, 0],
+            appeui: [0, 0, 0, 0, 0, 0, 0, 0],
+            appkey: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+        })
+        .await
+    {
+        Ok(()) => defmt::info!("LoRaWAN network joined"),
+        Err(err) => {
+            info!("Radio error = {}", err);
+            return;
+        }
+    };
+}
diff --git a/firmware/rust1/src/bin/lora_p2p_receive.rs b/firmware/rust1/src/bin/lora_p2p_receive.rs
new file mode 100644
index 0000000000000000000000000000000000000000..250419202889f5f78967891366838edb6ab89bae
--- /dev/null
+++ b/firmware/rust1/src/bin/lora_p2p_receive.rs
@@ -0,0 +1,115 @@
+//! This example runs on the Raspberry Pi Pico with a Waveshare board containing a Semtech Sx1262 radio.
+//! It demonstrates LORA P2P receive functionality in conjunction with the lora_p2p_send example.
+#![no_std]
+#![no_main]
+#![macro_use]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_lora::iv::GenericSx126xInterfaceVariant;
+use embassy_rp::gpio::{Input, Level, Output, Pin, Pull};
+use embassy_rp::spi::{Config, Spi};
+use embassy_time::{Delay, Duration, Timer};
+use lora_phy::mod_params::*;
+use lora_phy::sx1261_2::SX1261_2;
+use lora_phy::LoRa;
+use {defmt_rtt as _, panic_probe as _};
+
+const LORA_FREQUENCY_IN_HZ: u32 = 903_900_000; // warning: set this appropriately for the region
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+
+    let miso = p.PIN_12;
+    let mosi = p.PIN_11;
+    let clk = p.PIN_10;
+    let spi = Spi::new(p.SPI1, clk, mosi, miso, p.DMA_CH0, p.DMA_CH1, Config::default());
+
+    let nss = Output::new(p.PIN_3.degrade(), Level::High);
+    let reset = Output::new(p.PIN_15.degrade(), Level::High);
+    let dio1 = Input::new(p.PIN_20.degrade(), Pull::None);
+    let busy = Input::new(p.PIN_2.degrade(), Pull::None);
+
+    let iv = GenericSx126xInterfaceVariant::new(nss, reset, dio1, busy, None, None).unwrap();
+
+    let mut delay = Delay;
+
+    let mut lora = {
+        match LoRa::new(
+            SX1261_2::new(BoardType::RpPicoWaveshareSx1262, spi, iv),
+            false,
+            &mut delay,
+        )
+        .await
+        {
+            Ok(l) => l,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    let mut debug_indicator = Output::new(p.PIN_25, Level::Low);
+
+    let mut receiving_buffer = [00u8; 100];
+
+    let mdltn_params = {
+        match lora.create_modulation_params(
+            SpreadingFactor::_10,
+            Bandwidth::_250KHz,
+            CodingRate::_4_8,
+            LORA_FREQUENCY_IN_HZ,
+        ) {
+            Ok(mp) => mp,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    let rx_pkt_params = {
+        match lora.create_rx_packet_params(4, false, receiving_buffer.len() as u8, true, false, &mdltn_params) {
+            Ok(pp) => pp,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    match lora
+        .prepare_for_rx(&mdltn_params, &rx_pkt_params, None, true, false, 0, 0x00ffffffu32)
+        .await
+    {
+        Ok(()) => {}
+        Err(err) => {
+            info!("Radio error = {}", err);
+            return;
+        }
+    };
+
+    loop {
+        receiving_buffer = [00u8; 100];
+        match lora.rx(&rx_pkt_params, &mut receiving_buffer).await {
+            Ok((received_len, _rx_pkt_status)) => {
+                if (received_len == 3)
+                    && (receiving_buffer[0] == 0x01u8)
+                    && (receiving_buffer[1] == 0x02u8)
+                    && (receiving_buffer[2] == 0x03u8)
+                {
+                    info!("rx successful");
+                    debug_indicator.set_high();
+                    Timer::after(Duration::from_secs(5)).await;
+                    debug_indicator.set_low();
+                } else {
+                    info!("rx unknown packet");
+                }
+            }
+            Err(err) => info!("rx unsuccessful = {}", err),
+        }
+    }
+}
diff --git a/firmware/rust1/src/bin/lora_p2p_send.rs b/firmware/rust1/src/bin/lora_p2p_send.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3a0544b1762525b07783cc354010f9d688f51961
--- /dev/null
+++ b/firmware/rust1/src/bin/lora_p2p_send.rs
@@ -0,0 +1,103 @@
+//! This example runs on the Raspberry Pi Pico with a Waveshare board containing a Semtech Sx1262 radio.
+//! It demonstrates LORA P2P send functionality.
+#![no_std]
+#![no_main]
+#![macro_use]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_lora::iv::GenericSx126xInterfaceVariant;
+use embassy_rp::gpio::{Input, Level, Output, Pin, Pull};
+use embassy_rp::spi::{Config, Spi};
+use embassy_time::Delay;
+use lora_phy::mod_params::*;
+use lora_phy::sx1261_2::SX1261_2;
+use lora_phy::LoRa;
+use {defmt_rtt as _, panic_probe as _};
+
+const LORA_FREQUENCY_IN_HZ: u32 = 903_900_000; // warning: set this appropriately for the region
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+
+    let miso = p.PIN_12;
+    let mosi = p.PIN_11;
+    let clk = p.PIN_10;
+    let spi = Spi::new(p.SPI1, clk, mosi, miso, p.DMA_CH0, p.DMA_CH1, Config::default());
+
+    let nss = Output::new(p.PIN_3.degrade(), Level::High);
+    let reset = Output::new(p.PIN_15.degrade(), Level::High);
+    let dio1 = Input::new(p.PIN_20.degrade(), Pull::None);
+    let busy = Input::new(p.PIN_2.degrade(), Pull::None);
+
+    let iv = GenericSx126xInterfaceVariant::new(nss, reset, dio1, busy, None, None).unwrap();
+
+    let mut delay = Delay;
+
+    let mut lora = {
+        match LoRa::new(
+            SX1261_2::new(BoardType::RpPicoWaveshareSx1262, spi, iv),
+            false,
+            &mut delay,
+        )
+        .await
+        {
+            Ok(l) => l,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    let mdltn_params = {
+        match lora.create_modulation_params(
+            SpreadingFactor::_10,
+            Bandwidth::_250KHz,
+            CodingRate::_4_8,
+            LORA_FREQUENCY_IN_HZ,
+        ) {
+            Ok(mp) => mp,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    let mut tx_pkt_params = {
+        match lora.create_tx_packet_params(4, false, true, false, &mdltn_params) {
+            Ok(pp) => pp,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    match lora.prepare_for_tx(&mdltn_params, 20, false).await {
+        Ok(()) => {}
+        Err(err) => {
+            info!("Radio error = {}", err);
+            return;
+        }
+    };
+
+    let buffer = [0x01u8, 0x02u8, 0x03u8];
+    match lora.tx(&mdltn_params, &mut tx_pkt_params, &buffer, 0xffffff).await {
+        Ok(()) => {
+            info!("TX DONE");
+        }
+        Err(err) => {
+            info!("Radio error = {}", err);
+            return;
+        }
+    };
+
+    match lora.sleep(&mut delay).await {
+        Ok(()) => info!("Sleep successful"),
+        Err(err) => info!("Sleep unsuccessful = {}", err),
+    }
+}
diff --git a/firmware/rust1/src/bin/lora_p2p_send_multicore.rs b/firmware/rust1/src/bin/lora_p2p_send_multicore.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5585606d85da99038b9cb48aa68580ed675d4d4a
--- /dev/null
+++ b/firmware/rust1/src/bin/lora_p2p_send_multicore.rs
@@ -0,0 +1,139 @@
+//! This example runs on the Raspberry Pi Pico with a Waveshare board containing a Semtech Sx1262 radio.
+//! It demonstrates LORA P2P send functionality using the second core, with data provided by the first core.
+#![no_std]
+#![no_main]
+#![macro_use]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Executor;
+use embassy_executor::_export::StaticCell;
+use embassy_lora::iv::GenericSx126xInterfaceVariant;
+use embassy_rp::gpio::{AnyPin, Input, Level, Output, Pin, Pull};
+use embassy_rp::multicore::{spawn_core1, Stack};
+use embassy_rp::peripherals::SPI1;
+use embassy_rp::spi::{Async, Config, Spi};
+use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
+use embassy_sync::channel::Channel;
+use embassy_time::{Delay, Duration, Timer};
+use lora_phy::mod_params::*;
+use lora_phy::sx1261_2::SX1261_2;
+use lora_phy::LoRa;
+use {defmt_rtt as _, panic_probe as _};
+
+static mut CORE1_STACK: Stack<4096> = Stack::new();
+static EXECUTOR0: StaticCell<Executor> = StaticCell::new();
+static EXECUTOR1: StaticCell<Executor> = StaticCell::new();
+static CHANNEL: Channel<CriticalSectionRawMutex, [u8; 3], 1> = Channel::new();
+
+const LORA_FREQUENCY_IN_HZ: u32 = 903_900_000; // warning: set this appropriately for the region
+
+#[cortex_m_rt::entry]
+fn main() -> ! {
+    let p = embassy_rp::init(Default::default());
+
+    let miso = p.PIN_12;
+    let mosi = p.PIN_11;
+    let clk = p.PIN_10;
+    let spi = Spi::new(p.SPI1, clk, mosi, miso, p.DMA_CH0, p.DMA_CH1, Config::default());
+
+    let nss = Output::new(p.PIN_3.degrade(), Level::High);
+    let reset = Output::new(p.PIN_15.degrade(), Level::High);
+    let dio1 = Input::new(p.PIN_20.degrade(), Pull::None);
+    let busy = Input::new(p.PIN_2.degrade(), Pull::None);
+
+    let iv = GenericSx126xInterfaceVariant::new(nss, reset, dio1, busy, None, None).unwrap();
+
+    spawn_core1(p.CORE1, unsafe { &mut CORE1_STACK }, move || {
+        let executor1 = EXECUTOR1.init(Executor::new());
+        executor1.run(|spawner| unwrap!(spawner.spawn(core1_task(spi, iv))));
+    });
+
+    let executor0 = EXECUTOR0.init(Executor::new());
+    executor0.run(|spawner| unwrap!(spawner.spawn(core0_task())));
+}
+
+#[embassy_executor::task]
+async fn core0_task() {
+    info!("Hello from core 0");
+    loop {
+        CHANNEL.send([0x01u8, 0x02u8, 0x03u8]).await;
+        Timer::after(Duration::from_millis(60 * 1000)).await;
+    }
+}
+
+#[embassy_executor::task]
+async fn core1_task(
+    spi: Spi<'static, SPI1, Async>,
+    iv: GenericSx126xInterfaceVariant<Output<'static, AnyPin>, Input<'static, AnyPin>>,
+) {
+    info!("Hello from core 1");
+    let mut delay = Delay;
+
+    let mut lora = {
+        match LoRa::new(
+            SX1261_2::new(BoardType::RpPicoWaveshareSx1262, spi, iv),
+            false,
+            &mut delay,
+        )
+        .await
+        {
+            Ok(l) => l,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    let mdltn_params = {
+        match lora.create_modulation_params(
+            SpreadingFactor::_10,
+            Bandwidth::_250KHz,
+            CodingRate::_4_8,
+            LORA_FREQUENCY_IN_HZ,
+        ) {
+            Ok(mp) => mp,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    let mut tx_pkt_params = {
+        match lora.create_tx_packet_params(4, false, true, false, &mdltn_params) {
+            Ok(pp) => pp,
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        }
+    };
+
+    loop {
+        let buffer: [u8; 3] = CHANNEL.recv().await;
+        match lora.prepare_for_tx(&mdltn_params, 20, false).await {
+            Ok(()) => {}
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        };
+
+        match lora.tx(&mdltn_params, &mut tx_pkt_params, &buffer, 0xffffff).await {
+            Ok(()) => {
+                info!("TX DONE");
+            }
+            Err(err) => {
+                info!("Radio error = {}", err);
+                return;
+            }
+        };
+
+        match lora.sleep(&mut delay).await {
+            Ok(()) => info!("Sleep successful"),
+            Err(err) => info!("Sleep unsuccessful = {}", err),
+        }
+    }
+}
diff --git a/firmware/rust1/src/bin/multicore.rs b/firmware/rust1/src/bin/multicore.rs
new file mode 100644
index 0000000000000000000000000000000000000000..376b2b61ebbdcb0cd384b8c8948ec96b0777813f
--- /dev/null
+++ b/firmware/rust1/src/bin/multicore.rs
@@ -0,0 +1,60 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Executor;
+use embassy_executor::_export::StaticCell;
+use embassy_rp::gpio::{Level, Output};
+use embassy_rp::multicore::{spawn_core1, Stack};
+use embassy_rp::peripherals::PIN_25;
+use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
+use embassy_sync::channel::Channel;
+use embassy_time::{Duration, Timer};
+use {defmt_rtt as _, panic_probe as _};
+
+static mut CORE1_STACK: Stack<4096> = Stack::new();
+static EXECUTOR0: StaticCell<Executor> = StaticCell::new();
+static EXECUTOR1: StaticCell<Executor> = StaticCell::new();
+static CHANNEL: Channel<CriticalSectionRawMutex, LedState, 1> = Channel::new();
+
+enum LedState {
+    On,
+    Off,
+}
+
+#[cortex_m_rt::entry]
+fn main() -> ! {
+    let p = embassy_rp::init(Default::default());
+    let led = Output::new(p.PIN_25, Level::Low);
+
+    spawn_core1(p.CORE1, unsafe { &mut CORE1_STACK }, move || {
+        let executor1 = EXECUTOR1.init(Executor::new());
+        executor1.run(|spawner| unwrap!(spawner.spawn(core1_task(led))));
+    });
+
+    let executor0 = EXECUTOR0.init(Executor::new());
+    executor0.run(|spawner| unwrap!(spawner.spawn(core0_task())));
+}
+
+#[embassy_executor::task]
+async fn core0_task() {
+    info!("Hello from core 0");
+    loop {
+        CHANNEL.send(LedState::On).await;
+        Timer::after(Duration::from_millis(100)).await;
+        CHANNEL.send(LedState::Off).await;
+        Timer::after(Duration::from_millis(400)).await;
+    }
+}
+
+#[embassy_executor::task]
+async fn core1_task(mut led: Output<'static, PIN_25>) {
+    info!("Hello from core 1");
+    loop {
+        match CHANNEL.recv().await {
+            LedState::On => led.set_high(),
+            LedState::Off => led.set_low(),
+        }
+    }
+}
diff --git a/firmware/rust1/src/bin/multiprio.rs b/firmware/rust1/src/bin/multiprio.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2f79ba49e6199044f3da66eaca49fe4056507ff5
--- /dev/null
+++ b/firmware/rust1/src/bin/multiprio.rs
@@ -0,0 +1,152 @@
+//! This example showcases how to create multiple Executor instances to run tasks at
+//! different priority levels.
+//!
+//! Low priority executor runs in thread mode (not interrupt), and uses `sev` for signaling
+//! there's work in the queue, and `wfe` for waiting for work.
+//!
+//! Medium and high priority executors run in two interrupts with different priorities.
+//! Signaling work is done by pending the interrupt. No "waiting" needs to be done explicitly, since
+//! when there's work the interrupt will trigger and run the executor.
+//!
+//! Sample output below. Note that high priority ticks can interrupt everything else, and
+//! medium priority computations can interrupt low priority computations, making them to appear
+//! to take significantly longer time.
+//!
+//! ```not_rust
+//!     [med] Starting long computation
+//!     [med] done in 992 ms
+//!         [high] tick!
+//! [low] Starting long computation
+//!     [med] Starting long computation
+//!         [high] tick!
+//!         [high] tick!
+//!     [med] done in 993 ms
+//!     [med] Starting long computation
+//!         [high] tick!
+//!         [high] tick!
+//!     [med] done in 993 ms
+//! [low] done in 3972 ms
+//!     [med] Starting long computation
+//!         [high] tick!
+//!         [high] tick!
+//!     [med] done in 993 ms
+//! ```
+//!
+//! For comparison, try changing the code so all 3 tasks get spawned on the low priority executor.
+//! You will get an output like the following. Note that no computation is ever interrupted.
+//!
+//! ```not_rust
+//!         [high] tick!
+//!     [med] Starting long computation
+//!     [med] done in 496 ms
+//! [low] Starting long computation
+//! [low] done in 992 ms
+//!     [med] Starting long computation
+//!     [med] done in 496 ms
+//!         [high] tick!
+//! [low] Starting long computation
+//! [low] done in 992 ms
+//!         [high] tick!
+//!     [med] Starting long computation
+//!     [med] done in 496 ms
+//!         [high] tick!
+//! ```
+//!
+
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use core::mem;
+
+use cortex_m::peripheral::NVIC;
+use cortex_m_rt::entry;
+use defmt::{info, unwrap};
+use embassy_rp::executor::{Executor, InterruptExecutor};
+use embassy_rp::interrupt;
+use embassy_rp::pac::Interrupt;
+use embassy_time::{Duration, Instant, Timer, TICK_HZ};
+use static_cell::StaticCell;
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::task]
+async fn run_high() {
+    loop {
+        info!("        [high] tick!");
+        Timer::after(Duration::from_ticks(673740)).await;
+    }
+}
+
+#[embassy_executor::task]
+async fn run_med() {
+    loop {
+        let start = Instant::now();
+        info!("    [med] Starting long computation");
+
+        // Spin-wait to simulate a long CPU computation
+        cortex_m::asm::delay(125_000_000); // ~1 second
+
+        let end = Instant::now();
+        let ms = end.duration_since(start).as_ticks() * 1000 / TICK_HZ;
+        info!("    [med] done in {} ms", ms);
+
+        Timer::after(Duration::from_ticks(53421)).await;
+    }
+}
+
+#[embassy_executor::task]
+async fn run_low() {
+    loop {
+        let start = Instant::now();
+        info!("[low] Starting long computation");
+
+        // Spin-wait to simulate a long CPU computation
+        cortex_m::asm::delay(250_000_000); // ~2 seconds
+
+        let end = Instant::now();
+        let ms = end.duration_since(start).as_ticks() * 1000 / TICK_HZ;
+        info!("[low] done in {} ms", ms);
+
+        Timer::after(Duration::from_ticks(82983)).await;
+    }
+}
+
+static EXECUTOR_HIGH: InterruptExecutor = InterruptExecutor::new();
+static EXECUTOR_MED: InterruptExecutor = InterruptExecutor::new();
+static EXECUTOR_LOW: StaticCell<Executor> = StaticCell::new();
+
+#[interrupt]
+unsafe fn SWI_IRQ_1() {
+    EXECUTOR_HIGH.on_interrupt()
+}
+
+#[interrupt]
+unsafe fn SWI_IRQ_0() {
+    EXECUTOR_MED.on_interrupt()
+}
+
+#[entry]
+fn main() -> ! {
+    info!("Hello World!");
+
+    let _p = embassy_rp::init(Default::default());
+    let mut nvic: NVIC = unsafe { mem::transmute(()) };
+
+    // High-priority executor: SWI_IRQ_1, priority level 2
+    unsafe { nvic.set_priority(Interrupt::SWI_IRQ_1, 2 << 6) };
+    info!("bla: {}", NVIC::get_priority(Interrupt::SWI_IRQ_1));
+    let spawner = EXECUTOR_HIGH.start(Interrupt::SWI_IRQ_1);
+    unwrap!(spawner.spawn(run_high()));
+
+    // Medium-priority executor: SWI_IRQ_0, priority level 3
+    unsafe { nvic.set_priority(Interrupt::SWI_IRQ_0, 3 << 6) };
+    info!("bla: {}", NVIC::get_priority(Interrupt::SWI_IRQ_0));
+    let spawner = EXECUTOR_MED.start(Interrupt::SWI_IRQ_0);
+    unwrap!(spawner.spawn(run_med()));
+
+    // Low priority executor: runs in thread mode, using WFE/SEV
+    let executor = EXECUTOR_LOW.init(Executor::new());
+    executor.run(|spawner| {
+        unwrap!(spawner.spawn(run_low()));
+    });
+}
diff --git a/firmware/rust1/src/bin/pio_async.rs b/firmware/rust1/src/bin/pio_async.rs
new file mode 100644
index 0000000000000000000000000000000000000000..12484e88242f3e8654285f9a052bfebbf049522f
--- /dev/null
+++ b/firmware/rust1/src/bin/pio_async.rs
@@ -0,0 +1,122 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+use defmt::info;
+use embassy_embedded_hal::SetConfig;
+use embassy_executor::Spawner;
+use embassy_rp::peripherals::PIO0;
+use embassy_rp::pio::{Common, Config, Irq, Pio, PioPin, ShiftDirection, StateMachine};
+use embassy_rp::relocate::RelocatedProgram;
+use fixed::traits::ToFixed;
+use fixed_macro::types::U56F8;
+use {defmt_rtt as _, panic_probe as _};
+
+fn setup_pio_task_sm0<'a>(pio: &mut Common<'a, PIO0>, sm: &mut StateMachine<'a, PIO0, 0>, pin: impl PioPin) {
+    // Setup sm0
+
+    // Send data serially to pin
+    let prg = pio_proc::pio_asm!(
+        ".origin 16",
+        "set pindirs, 1",
+        ".wrap_target",
+        "out pins,1 [19]",
+        ".wrap",
+    );
+
+    let relocated = RelocatedProgram::new(&prg.program);
+    let mut cfg = Config::default();
+    cfg.use_program(&pio.load_program(&relocated), &[]);
+    let out_pin = pio.make_pio_pin(pin);
+    cfg.set_out_pins(&[&out_pin]);
+    cfg.set_set_pins(&[&out_pin]);
+    cfg.clock_divider = (U56F8!(125_000_000) / 20 / 200).to_fixed();
+    cfg.shift_out.auto_fill = true;
+    sm.set_config(&cfg);
+}
+
+#[embassy_executor::task]
+async fn pio_task_sm0(mut sm: StateMachine<'static, PIO0, 0>) {
+    sm.set_enable(true);
+
+    let mut v = 0x0f0caffa;
+    loop {
+        sm.tx().wait_push(v).await;
+        v ^= 0xffff;
+        info!("Pushed {:032b} to FIFO", v);
+    }
+}
+
+fn setup_pio_task_sm1<'a>(pio: &mut Common<'a, PIO0>, sm: &mut StateMachine<'a, PIO0, 1>) {
+    // Setupm sm1
+
+    // Read 0b10101 repeatedly until ISR is full
+    let prg = pio_proc::pio_asm!(".origin 8", "set x, 0x15", ".wrap_target", "in x, 5 [31]", ".wrap",);
+
+    let relocated = RelocatedProgram::new(&prg.program);
+    let mut cfg = Config::default();
+    cfg.use_program(&pio.load_program(&relocated), &[]);
+    cfg.clock_divider = (U56F8!(125_000_000) / 2000).to_fixed();
+    cfg.shift_in.auto_fill = true;
+    cfg.shift_in.direction = ShiftDirection::Right;
+    sm.set_config(&cfg);
+}
+
+#[embassy_executor::task]
+async fn pio_task_sm1(mut sm: StateMachine<'static, PIO0, 1>) {
+    sm.set_enable(true);
+    loop {
+        let rx = sm.rx().wait_pull().await;
+        info!("Pulled {:032b} from FIFO", rx);
+    }
+}
+
+fn setup_pio_task_sm2<'a>(pio: &mut Common<'a, PIO0>, sm: &mut StateMachine<'a, PIO0, 2>) {
+    // Setup sm2
+
+    // Repeatedly trigger IRQ 3
+    let prg = pio_proc::pio_asm!(
+        ".origin 0",
+        ".wrap_target",
+        "set x,10",
+        "delay:",
+        "jmp x-- delay [15]",
+        "irq 3 [15]",
+        ".wrap",
+    );
+    let relocated = RelocatedProgram::new(&prg.program);
+    let mut cfg = Config::default();
+    cfg.use_program(&pio.load_program(&relocated), &[]);
+    cfg.clock_divider = (U56F8!(125_000_000) / 2000).to_fixed();
+    sm.set_config(&cfg);
+}
+
+#[embassy_executor::task]
+async fn pio_task_sm2(mut irq: Irq<'static, PIO0, 3>, mut sm: StateMachine<'static, PIO0, 2>) {
+    sm.set_enable(true);
+    loop {
+        irq.wait().await;
+        info!("IRQ trigged");
+    }
+}
+
+#[embassy_executor::main]
+async fn main(spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    let pio = p.PIO0;
+
+    let Pio {
+        mut common,
+        irq3,
+        mut sm0,
+        mut sm1,
+        mut sm2,
+        ..
+    } = Pio::new(pio);
+
+    setup_pio_task_sm0(&mut common, &mut sm0, p.PIN_0);
+    setup_pio_task_sm1(&mut common, &mut sm1);
+    setup_pio_task_sm2(&mut common, &mut sm2);
+    spawner.spawn(pio_task_sm0(sm0)).unwrap();
+    spawner.spawn(pio_task_sm1(sm1)).unwrap();
+    spawner.spawn(pio_task_sm2(irq3, sm2)).unwrap();
+}
diff --git a/firmware/rust1/src/bin/pio_dma.rs b/firmware/rust1/src/bin/pio_dma.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7f85288bfa67e653d83f19aba27c751d10093479
--- /dev/null
+++ b/firmware/rust1/src/bin/pio_dma.rs
@@ -0,0 +1,80 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+use defmt::info;
+use embassy_embedded_hal::SetConfig;
+use embassy_executor::Spawner;
+use embassy_futures::join::join;
+use embassy_rp::pio::{Config, Pio, ShiftConfig, ShiftDirection};
+use embassy_rp::relocate::RelocatedProgram;
+use embassy_rp::Peripheral;
+use fixed::traits::ToFixed;
+use fixed_macro::types::U56F8;
+use {defmt_rtt as _, panic_probe as _};
+
+fn swap_nibbles(v: u32) -> u32 {
+    let v = (v & 0x0f0f_0f0f) << 4 | (v & 0xf0f0_f0f0) >> 4;
+    let v = (v & 0x00ff_00ff) << 8 | (v & 0xff00_ff00) >> 8;
+    (v & 0x0000_ffff) << 16 | (v & 0xffff_0000) >> 16
+}
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    let pio = p.PIO0;
+    let Pio {
+        mut common,
+        sm0: mut sm,
+        ..
+    } = Pio::new(pio);
+
+    let prg = pio_proc::pio_asm!(
+        ".origin 0",
+        "set pindirs,1",
+        ".wrap_target",
+        "set y,7",
+        "loop:",
+        "out x,4",
+        "in x,4",
+        "jmp y--, loop",
+        ".wrap",
+    );
+
+    let relocated = RelocatedProgram::new(&prg.program);
+    let mut cfg = Config::default();
+    cfg.use_program(&common.load_program(&relocated), &[]);
+    cfg.clock_divider = (U56F8!(125_000_000) / U56F8!(10_000)).to_fixed();
+    cfg.shift_in = ShiftConfig {
+        auto_fill: true,
+        threshold: 32,
+        direction: ShiftDirection::Left,
+    };
+    cfg.shift_out = ShiftConfig {
+        auto_fill: true,
+        threshold: 32,
+        direction: ShiftDirection::Right,
+    };
+
+    sm.set_config(&cfg);
+    sm.set_enable(true);
+
+    let mut dma_out_ref = p.DMA_CH0.into_ref();
+    let mut dma_in_ref = p.DMA_CH1.into_ref();
+    let mut dout = [0x12345678u32; 29];
+    for i in 1..dout.len() {
+        dout[i] = (dout[i - 1] & 0x0fff_ffff) * 13 + 7;
+    }
+    let mut din = [0u32; 29];
+    loop {
+        let (rx, tx) = sm.rx_tx();
+        join(
+            tx.dma_push(dma_out_ref.reborrow(), &dout),
+            rx.dma_pull(dma_in_ref.reborrow(), &mut din),
+        )
+        .await;
+        for i in 0..din.len() {
+            assert_eq!(din[i], swap_nibbles(dout[i]));
+        }
+        info!("Swapped {} words", dout.len());
+    }
+}
diff --git a/firmware/rust1/src/bin/pio_hd44780.rs b/firmware/rust1/src/bin/pio_hd44780.rs
new file mode 100644
index 0000000000000000000000000000000000000000..088fd56495affaeed5bde09e5d2a2e02db50750e
--- /dev/null
+++ b/firmware/rust1/src/bin/pio_hd44780.rs
@@ -0,0 +1,235 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use core::fmt::Write;
+
+use embassy_embedded_hal::SetConfig;
+use embassy_executor::Spawner;
+use embassy_rp::dma::{AnyChannel, Channel};
+use embassy_rp::peripherals::PIO0;
+use embassy_rp::pio::{Config, Direction, FifoJoin, Pio, PioPin, ShiftConfig, ShiftDirection, StateMachine};
+use embassy_rp::pwm::{self, Pwm};
+use embassy_rp::relocate::RelocatedProgram;
+use embassy_rp::{into_ref, Peripheral, PeripheralRef};
+use embassy_time::{Duration, Instant, Timer};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    // this test assumes a 2x16 HD44780 display attached as follow:
+    //   rs  = PIN0
+    //   rw  = PIN1
+    //   e   = PIN2
+    //   db4 = PIN3
+    //   db5 = PIN4
+    //   db6 = PIN5
+    //   db7 = PIN6
+    // additionally a pwm signal for a bias voltage charge pump is provided on pin 15,
+    // allowing direct connection of the display to the RP2040 without level shifters.
+    let p = embassy_rp::init(Default::default());
+
+    let _pwm = Pwm::new_output_b(p.PWM_CH7, p.PIN_15, {
+        let mut c = pwm::Config::default();
+        c.divider = 125.into();
+        c.top = 100;
+        c.compare_b = 50;
+        c
+    });
+
+    let mut hd = HD44780::new(
+        p.PIO0, p.DMA_CH3, p.PIN_0, p.PIN_1, p.PIN_2, p.PIN_3, p.PIN_4, p.PIN_5, p.PIN_6,
+    )
+    .await;
+
+    loop {
+        struct Buf<const N: usize>([u8; N], usize);
+        impl<const N: usize> Write for Buf<N> {
+            fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
+                for b in s.as_bytes() {
+                    if self.1 >= N {
+                        return Err(core::fmt::Error);
+                    }
+                    self.0[self.1] = *b;
+                    self.1 += 1;
+                }
+                Ok(())
+            }
+        }
+        let mut buf = Buf([0; 16], 0);
+        write!(buf, "up {}s", Instant::now().as_micros() as f32 / 1e6).unwrap();
+        hd.add_line(&buf.0[0..buf.1]).await;
+        Timer::after(Duration::from_secs(1)).await;
+    }
+}
+
+pub struct HD44780<'l> {
+    dma: PeripheralRef<'l, AnyChannel>,
+    sm: StateMachine<'l, PIO0, 0>,
+
+    buf: [u8; 40],
+}
+
+impl<'l> HD44780<'l> {
+    pub async fn new(
+        pio: impl Peripheral<P = PIO0> + 'l,
+        dma: impl Peripheral<P = impl Channel> + 'l,
+        rs: impl PioPin,
+        rw: impl PioPin,
+        e: impl PioPin,
+        db4: impl PioPin,
+        db5: impl PioPin,
+        db6: impl PioPin,
+        db7: impl PioPin,
+    ) -> HD44780<'l> {
+        into_ref!(dma);
+
+        let Pio {
+            mut common,
+            mut irq0,
+            mut sm0,
+            ..
+        } = Pio::new(pio);
+
+        // takes command words (<wait:24> <command:4> <0:4>)
+        let prg = pio_proc::pio_asm!(
+            r#"
+                .side_set 1 opt
+                .origin 20
+
+                loop:
+                    out x,     24
+                delay:
+                    jmp x--,   delay
+                    out pins,  4     side 1
+                    out null,  4     side 0
+                    jmp !osre, loop
+                irq 0
+            "#,
+        );
+
+        let rs = common.make_pio_pin(rs);
+        let rw = common.make_pio_pin(rw);
+        let e = common.make_pio_pin(e);
+        let db4 = common.make_pio_pin(db4);
+        let db5 = common.make_pio_pin(db5);
+        let db6 = common.make_pio_pin(db6);
+        let db7 = common.make_pio_pin(db7);
+
+        sm0.set_pin_dirs(Direction::Out, &[&rs, &rw, &e, &db4, &db5, &db6, &db7]);
+
+        let relocated = RelocatedProgram::new(&prg.program);
+        let mut cfg = Config::default();
+        cfg.use_program(&common.load_program(&relocated), &[&e]);
+        cfg.clock_divider = 125u8.into();
+        cfg.set_out_pins(&[&db4, &db5, &db6, &db7]);
+        cfg.shift_out = ShiftConfig {
+            auto_fill: true,
+            direction: ShiftDirection::Left,
+            threshold: 32,
+        };
+        cfg.fifo_join = FifoJoin::TxOnly;
+        sm0.set_config(&cfg);
+
+        sm0.set_enable(true);
+        // init to 8 bit thrice
+        sm0.tx().push((50000 << 8) | 0x30);
+        sm0.tx().push((5000 << 8) | 0x30);
+        sm0.tx().push((200 << 8) | 0x30);
+        // init 4 bit
+        sm0.tx().push((200 << 8) | 0x20);
+        // set font and lines
+        sm0.tx().push((50 << 8) | 0x20);
+        sm0.tx().push(0b1100_0000);
+
+        irq0.wait().await;
+        sm0.set_enable(false);
+
+        // takes command sequences (<rs:1> <count:7>, data...)
+        // many side sets are only there to free up a delay bit!
+        let prg = pio_proc::pio_asm!(
+            r#"
+                .origin 27
+                .side_set 1
+
+                .wrap_target
+                pull     side 0
+                out  x 1 side 0 ; !rs
+                out  y 7 side 0 ; #data - 1
+
+                ; rs/rw to e: >= 60ns
+                ; e high time: >= 500ns
+                ; e low time: >= 500ns
+                ; read data valid after e falling: ~5ns
+                ; write data hold after e falling: ~10ns
+
+                loop:
+                    pull                 side 0
+                    jmp  !x       data   side 0
+                command:
+                    set  pins     0b00   side 0
+                    jmp  shift           side 0
+                data:
+                    set  pins     0b01   side 0
+                shift:
+                    out  pins     4      side 1 [9]
+                    nop                  side 0 [9]
+                    out  pins     4      side 1 [9]
+                    mov  osr      null   side 0 [7]
+                    out  pindirs  4      side 0
+                    set  pins     0b10   side 0
+                busy:
+                    nop                  side 1 [9]
+                    jmp  pin      more   side 0 [9]
+                    mov  osr      ~osr   side 1 [9]
+                    nop                  side 0 [4]
+                    out  pindirs  4      side 0
+                    jmp  y--      loop   side 0
+                .wrap
+                more:
+                    nop                  side 1 [9]
+                    jmp busy             side 0 [9]
+            "#
+        );
+
+        let relocated = RelocatedProgram::new(&prg.program);
+        let mut cfg = Config::default();
+        cfg.use_program(&common.load_program(&relocated), &[&e]);
+        cfg.clock_divider = 8u8.into(); // ~64ns/insn
+        cfg.set_jmp_pin(&db7);
+        cfg.set_set_pins(&[&rs, &rw]);
+        cfg.set_out_pins(&[&db4, &db5, &db6, &db7]);
+        cfg.shift_out.direction = ShiftDirection::Left;
+        cfg.fifo_join = FifoJoin::TxOnly;
+        sm0.set_config(&cfg);
+
+        sm0.set_enable(true);
+
+        // display on and cursor on and blinking, reset display
+        sm0.tx().dma_push(dma.reborrow(), &[0x81u8, 0x0f, 1]).await;
+
+        Self {
+            dma: dma.map_into(),
+            sm: sm0,
+            buf: [0x20; 40],
+        }
+    }
+
+    pub async fn add_line(&mut self, s: &[u8]) {
+        // move cursor to 0:0, prepare 16 characters
+        self.buf[..3].copy_from_slice(&[0x80, 0x80, 15]);
+        // move line 2 up
+        self.buf.copy_within(22..38, 3);
+        // move cursor to 1:0, prepare 16 characters
+        self.buf[19..22].copy_from_slice(&[0x80, 0xc0, 15]);
+        // file line 2 with spaces
+        self.buf[22..38].fill(0x20);
+        // copy input line
+        let len = s.len().min(16);
+        self.buf[22..22 + len].copy_from_slice(&s[0..len]);
+        // set cursor to 1:15
+        self.buf[38..].copy_from_slice(&[0x80, 0xcf]);
+
+        self.sm.tx().dma_push(self.dma.reborrow(), &self.buf).await;
+    }
+}
diff --git a/firmware/rust1/src/bin/pwm.rs b/firmware/rust1/src/bin/pwm.rs
new file mode 100644
index 0000000000000000000000000000000000000000..69d315553af8f95d1d0178dc26231a837e438b78
--- /dev/null
+++ b/firmware/rust1/src/bin/pwm.rs
@@ -0,0 +1,27 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_embedded_hal::SetConfig;
+use embassy_executor::Spawner;
+use embassy_rp::pwm::{Config, Pwm};
+use embassy_time::{Duration, Timer};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+
+    let mut c: Config = Default::default();
+    c.top = 0x8000;
+    c.compare_b = 8;
+    let mut pwm = Pwm::new_output_b(p.PWM_CH4, p.PIN_25, c.clone());
+
+    loop {
+        info!("current LED duty cycle: {}/32768", c.compare_b);
+        Timer::after(Duration::from_secs(1)).await;
+        c.compare_b = c.compare_b.rotate_left(4);
+        pwm.set_config(&c);
+    }
+}
diff --git a/firmware/rust1/src/bin/spi.rs b/firmware/rust1/src/bin/spi.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a830a17a2998523ac5a903f2d71cb502b723fbb0
--- /dev/null
+++ b/firmware/rust1/src/bin/spi.rs
@@ -0,0 +1,43 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::spi::Spi;
+use embassy_rp::{gpio, spi};
+use gpio::{Level, Output};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    info!("Hello World!");
+
+    // Example for resistive touch sensor in Waveshare Pico-ResTouch
+
+    let miso = p.PIN_12;
+    let mosi = p.PIN_11;
+    let clk = p.PIN_10;
+    let touch_cs = p.PIN_16;
+
+    // create SPI
+    let mut config = spi::Config::default();
+    config.frequency = 2_000_000;
+    let mut spi = Spi::new_blocking(p.SPI1, clk, mosi, miso, config);
+
+    // Configure CS
+    let mut cs = Output::new(touch_cs, Level::Low);
+
+    loop {
+        cs.set_low();
+        let mut buf = [0x90, 0x00, 0x00, 0xd0, 0x00, 0x00];
+        spi.blocking_transfer_in_place(&mut buf).unwrap();
+        cs.set_high();
+
+        let x = (buf[1] as u32) << 5 | (buf[2] as u32) >> 3;
+        let y = (buf[4] as u32) << 5 | (buf[5] as u32) >> 3;
+
+        info!("touch: {=u32} {=u32}", x, y);
+    }
+}
diff --git a/firmware/rust1/src/bin/spi_async.rs b/firmware/rust1/src/bin/spi_async.rs
new file mode 100644
index 0000000000000000000000000000000000000000..671a9caaf94d8aa65c8176107532c3992c3c679c
--- /dev/null
+++ b/firmware/rust1/src/bin/spi_async.rs
@@ -0,0 +1,29 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::spi::{Config, Spi};
+use embassy_time::{Duration, Timer};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    info!("Hello World!");
+
+    let miso = p.PIN_12;
+    let mosi = p.PIN_11;
+    let clk = p.PIN_10;
+
+    let mut spi = Spi::new(p.SPI1, clk, mosi, miso, p.DMA_CH0, p.DMA_CH1, Config::default());
+
+    loop {
+        let tx_buf = [1_u8, 2, 3, 4, 5, 6];
+        let mut rx_buf = [0_u8; 6];
+        spi.transfer(&mut rx_buf, &tx_buf).await.unwrap();
+        info!("{:?}", rx_buf);
+        Timer::after(Duration::from_secs(1)).await;
+    }
+}
diff --git a/firmware/rust1/src/bin/spi_display.rs b/firmware/rust1/src/bin/spi_display.rs
new file mode 100644
index 0000000000000000000000000000000000000000..85a19ce07282a15a46b640adf712037cbf12eb1e
--- /dev/null
+++ b/firmware/rust1/src/bin/spi_display.rs
@@ -0,0 +1,308 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use core::cell::RefCell;
+
+use defmt::*;
+use embassy_embedded_hal::shared_bus::blocking::spi::SpiDeviceWithConfig;
+use embassy_executor::Spawner;
+use embassy_rp::gpio::{Level, Output};
+use embassy_rp::spi;
+use embassy_rp::spi::{Blocking, Spi};
+use embassy_sync::blocking_mutex::raw::NoopRawMutex;
+use embassy_sync::blocking_mutex::Mutex;
+use embassy_time::Delay;
+use embedded_graphics::image::{Image, ImageRawLE};
+use embedded_graphics::mono_font::ascii::FONT_10X20;
+use embedded_graphics::mono_font::MonoTextStyle;
+use embedded_graphics::pixelcolor::Rgb565;
+use embedded_graphics::prelude::*;
+use embedded_graphics::primitives::{PrimitiveStyleBuilder, Rectangle};
+use embedded_graphics::text::Text;
+use st7789::{Orientation, ST7789};
+use {defmt_rtt as _, panic_probe as _};
+
+use crate::my_display_interface::SPIDeviceInterface;
+use crate::touch::Touch;
+
+const DISPLAY_FREQ: u32 = 64_000_000;
+const TOUCH_FREQ: u32 = 200_000;
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    info!("Hello World!");
+
+    let bl = p.PIN_13;
+    let rst = p.PIN_15;
+    let display_cs = p.PIN_9;
+    let dcx = p.PIN_8;
+    let miso = p.PIN_12;
+    let mosi = p.PIN_11;
+    let clk = p.PIN_10;
+    let touch_cs = p.PIN_16;
+    //let touch_irq = p.PIN_17;
+
+    // create SPI
+    let mut display_config = spi::Config::default();
+    display_config.frequency = DISPLAY_FREQ;
+    display_config.phase = spi::Phase::CaptureOnSecondTransition;
+    display_config.polarity = spi::Polarity::IdleHigh;
+    let mut touch_config = spi::Config::default();
+    touch_config.frequency = TOUCH_FREQ;
+    touch_config.phase = spi::Phase::CaptureOnSecondTransition;
+    touch_config.polarity = spi::Polarity::IdleHigh;
+
+    let spi: Spi<'_, _, Blocking> = Spi::new_blocking(p.SPI1, clk, mosi, miso, touch_config.clone());
+    let spi_bus: Mutex<NoopRawMutex, _> = Mutex::new(RefCell::new(spi));
+
+    let display_spi = SpiDeviceWithConfig::new(&spi_bus, Output::new(display_cs, Level::High), display_config);
+    let touch_spi = SpiDeviceWithConfig::new(&spi_bus, Output::new(touch_cs, Level::High), touch_config);
+
+    let mut touch = Touch::new(touch_spi);
+
+    let dcx = Output::new(dcx, Level::Low);
+    let rst = Output::new(rst, Level::Low);
+    // dcx: 0 = command, 1 = data
+
+    // Enable LCD backlight
+    let _bl = Output::new(bl, Level::High);
+
+    // display interface abstraction from SPI and DC
+    let di = SPIDeviceInterface::new(display_spi, dcx);
+
+    // create driver
+    let mut display = ST7789::new(di, rst, 240, 320);
+
+    // initialize
+    display.init(&mut Delay).unwrap();
+
+    // set default orientation
+    display.set_orientation(Orientation::Landscape).unwrap();
+
+    display.clear(Rgb565::BLACK).unwrap();
+
+    let raw_image_data = ImageRawLE::new(include_bytes!("../../assets/ferris.raw"), 86);
+    let ferris = Image::new(&raw_image_data, Point::new(34, 68));
+
+    // Display the image
+    ferris.draw(&mut display).unwrap();
+
+    let style = MonoTextStyle::new(&FONT_10X20, Rgb565::GREEN);
+    Text::new(
+        "Hello embedded_graphics \n + embassy + RP2040!",
+        Point::new(20, 200),
+        style,
+    )
+    .draw(&mut display)
+    .unwrap();
+
+    loop {
+        if let Some((x, y)) = touch.read() {
+            let style = PrimitiveStyleBuilder::new().fill_color(Rgb565::BLUE).build();
+
+            Rectangle::new(Point::new(x - 1, y - 1), Size::new(3, 3))
+                .into_styled(style)
+                .draw(&mut display)
+                .unwrap();
+        }
+    }
+}
+
+/// Driver for the XPT2046 resistive touchscreen sensor
+mod touch {
+    use embedded_hal_1::spi::{Operation, SpiDevice};
+
+    struct Calibration {
+        x1: i32,
+        x2: i32,
+        y1: i32,
+        y2: i32,
+        sx: i32,
+        sy: i32,
+    }
+
+    const CALIBRATION: Calibration = Calibration {
+        x1: 3880,
+        x2: 340,
+        y1: 262,
+        y2: 3850,
+        sx: 320,
+        sy: 240,
+    };
+
+    pub struct Touch<SPI: SpiDevice> {
+        spi: SPI,
+    }
+
+    impl<SPI> Touch<SPI>
+    where
+        SPI: SpiDevice,
+    {
+        pub fn new(spi: SPI) -> Self {
+            Self { spi }
+        }
+
+        pub fn read(&mut self) -> Option<(i32, i32)> {
+            let mut x = [0; 2];
+            let mut y = [0; 2];
+            self.spi
+                .transaction(&mut [
+                    Operation::Write(&[0x90]),
+                    Operation::Read(&mut x),
+                    Operation::Write(&[0xd0]),
+                    Operation::Read(&mut y),
+                ])
+                .unwrap();
+
+            let x = (u16::from_be_bytes(x) >> 3) as i32;
+            let y = (u16::from_be_bytes(y) >> 3) as i32;
+
+            let cal = &CALIBRATION;
+
+            let x = ((x - cal.x1) * cal.sx / (cal.x2 - cal.x1)).clamp(0, cal.sx);
+            let y = ((y - cal.y1) * cal.sy / (cal.y2 - cal.y1)).clamp(0, cal.sy);
+            if x == 0 && y == 0 {
+                None
+            } else {
+                Some((x, y))
+            }
+        }
+    }
+}
+
+mod my_display_interface {
+    use display_interface::{DataFormat, DisplayError, WriteOnlyDataCommand};
+    use embedded_hal_1::digital::OutputPin;
+    use embedded_hal_1::spi::SpiDeviceWrite;
+
+    /// SPI display interface.
+    ///
+    /// This combines the SPI peripheral and a data/command pin
+    pub struct SPIDeviceInterface<SPI, DC> {
+        spi: SPI,
+        dc: DC,
+    }
+
+    impl<SPI, DC> SPIDeviceInterface<SPI, DC>
+    where
+        SPI: SpiDeviceWrite,
+        DC: OutputPin,
+    {
+        /// Create new SPI interface for communciation with a display driver
+        pub fn new(spi: SPI, dc: DC) -> Self {
+            Self { spi, dc }
+        }
+    }
+
+    impl<SPI, DC> WriteOnlyDataCommand for SPIDeviceInterface<SPI, DC>
+    where
+        SPI: SpiDeviceWrite,
+        DC: OutputPin,
+    {
+        fn send_commands(&mut self, cmds: DataFormat<'_>) -> Result<(), DisplayError> {
+            // 1 = data, 0 = command
+            self.dc.set_low().map_err(|_| DisplayError::DCError)?;
+
+            send_u8(&mut self.spi, cmds).map_err(|_| DisplayError::BusWriteError)?;
+            Ok(())
+        }
+
+        fn send_data(&mut self, buf: DataFormat<'_>) -> Result<(), DisplayError> {
+            // 1 = data, 0 = command
+            self.dc.set_high().map_err(|_| DisplayError::DCError)?;
+
+            send_u8(&mut self.spi, buf).map_err(|_| DisplayError::BusWriteError)?;
+            Ok(())
+        }
+    }
+
+    fn send_u8<T: SpiDeviceWrite>(spi: &mut T, words: DataFormat<'_>) -> Result<(), T::Error> {
+        match words {
+            DataFormat::U8(slice) => spi.write(slice),
+            DataFormat::U16(slice) => {
+                use byte_slice_cast::*;
+                spi.write(slice.as_byte_slice())
+            }
+            DataFormat::U16LE(slice) => {
+                use byte_slice_cast::*;
+                for v in slice.as_mut() {
+                    *v = v.to_le();
+                }
+                spi.write(slice.as_byte_slice())
+            }
+            DataFormat::U16BE(slice) => {
+                use byte_slice_cast::*;
+                for v in slice.as_mut() {
+                    *v = v.to_be();
+                }
+                spi.write(slice.as_byte_slice())
+            }
+            DataFormat::U8Iter(iter) => {
+                let mut buf = [0; 32];
+                let mut i = 0;
+
+                for v in iter.into_iter() {
+                    buf[i] = v;
+                    i += 1;
+
+                    if i == buf.len() {
+                        spi.write(&buf)?;
+                        i = 0;
+                    }
+                }
+
+                if i > 0 {
+                    spi.write(&buf[..i])?;
+                }
+
+                Ok(())
+            }
+            DataFormat::U16LEIter(iter) => {
+                use byte_slice_cast::*;
+                let mut buf = [0; 32];
+                let mut i = 0;
+
+                for v in iter.map(u16::to_le) {
+                    buf[i] = v;
+                    i += 1;
+
+                    if i == buf.len() {
+                        spi.write(&buf.as_byte_slice())?;
+                        i = 0;
+                    }
+                }
+
+                if i > 0 {
+                    spi.write(&buf[..i].as_byte_slice())?;
+                }
+
+                Ok(())
+            }
+            DataFormat::U16BEIter(iter) => {
+                use byte_slice_cast::*;
+                let mut buf = [0; 64];
+                let mut i = 0;
+                let len = buf.len();
+
+                for v in iter.map(u16::to_be) {
+                    buf[i] = v;
+                    i += 1;
+
+                    if i == len {
+                        spi.write(&buf.as_byte_slice())?;
+                        i = 0;
+                    }
+                }
+
+                if i > 0 {
+                    spi.write(&buf[..i].as_byte_slice())?;
+                }
+
+                Ok(())
+            }
+            _ => unimplemented!(),
+        }
+    }
+}
diff --git a/firmware/rust1/src/bin/uart.rs b/firmware/rust1/src/bin/uart.rs
new file mode 100644
index 0000000000000000000000000000000000000000..05177a6b4e27d064093885fb186552af60c9d675
--- /dev/null
+++ b/firmware/rust1/src/bin/uart.rs
@@ -0,0 +1,20 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use embassy_executor::Spawner;
+use embassy_rp::uart;
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    let config = uart::Config::default();
+    let mut uart = uart::Uart::new_with_rtscts_blocking(p.UART0, p.PIN_0, p.PIN_1, p.PIN_3, p.PIN_2, config);
+    uart.blocking_write("Hello World!\r\n".as_bytes()).unwrap();
+
+    loop {
+        uart.blocking_write("hello there!\r\n".as_bytes()).unwrap();
+        cortex_m::asm::delay(1_000_000);
+    }
+}
diff --git a/firmware/rust1/src/bin/uart_buffered_split.rs b/firmware/rust1/src/bin/uart_buffered_split.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a8a6822742c449d5b5bf8260cd97f60e49804e4d
--- /dev/null
+++ b/firmware/rust1/src/bin/uart_buffered_split.rs
@@ -0,0 +1,57 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_executor::_export::StaticCell;
+use embassy_rp::interrupt;
+use embassy_rp::peripherals::UART0;
+use embassy_rp::uart::{BufferedUart, BufferedUartRx, Config};
+use embassy_time::{Duration, Timer};
+use embedded_io::asynch::{Read, Write};
+use {defmt_rtt as _, panic_probe as _};
+
+macro_rules! singleton {
+    ($val:expr) => {{
+        type T = impl Sized;
+        static STATIC_CELL: StaticCell<T> = StaticCell::new();
+        let (x,) = STATIC_CELL.init(($val,));
+        x
+    }};
+}
+
+#[embassy_executor::main]
+async fn main(spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    let (tx_pin, rx_pin, uart) = (p.PIN_0, p.PIN_1, p.UART0);
+
+    let irq = interrupt::take!(UART0_IRQ);
+    let tx_buf = &mut singleton!([0u8; 16])[..];
+    let rx_buf = &mut singleton!([0u8; 16])[..];
+    let uart = BufferedUart::new(uart, irq, tx_pin, rx_pin, tx_buf, rx_buf, Config::default());
+    let (rx, mut tx) = uart.split();
+
+    unwrap!(spawner.spawn(reader(rx)));
+
+    info!("Writing...");
+    loop {
+        let data = [
+            1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
+            29, 30, 31,
+        ];
+        info!("TX {:?}", data);
+        tx.write_all(&data).await.unwrap();
+        Timer::after(Duration::from_secs(1)).await;
+    }
+}
+
+#[embassy_executor::task]
+async fn reader(mut rx: BufferedUartRx<'static, UART0>) {
+    info!("Reading...");
+    loop {
+        let mut buf = [0; 31];
+        rx.read_exact(&mut buf).await.unwrap();
+        info!("RX {:?}", buf);
+    }
+}
diff --git a/firmware/rust1/src/bin/uart_unidir.rs b/firmware/rust1/src/bin/uart_unidir.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4119a309fbb13495b5388481ae108bfca573b6a0
--- /dev/null
+++ b/firmware/rust1/src/bin/uart_unidir.rs
@@ -0,0 +1,49 @@
+//! test TX-only and RX-only UARTs. You need to connect GPIO0 to GPIO5 for
+//! this to work
+
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_rp::interrupt;
+use embassy_rp::peripherals::UART1;
+use embassy_rp::uart::{Async, Config, UartRx, UartTx};
+use embassy_time::{Duration, Timer};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+
+    let mut uart_tx = UartTx::new(p.UART0, p.PIN_0, p.DMA_CH0, Config::default());
+    let uart_rx = UartRx::new(
+        p.UART1,
+        p.PIN_5,
+        interrupt::take!(UART1_IRQ),
+        p.DMA_CH1,
+        Config::default(),
+    );
+
+    unwrap!(spawner.spawn(reader(uart_rx)));
+
+    info!("Writing...");
+    loop {
+        let data = [1u8, 2, 3, 4, 5, 6, 7, 8];
+        info!("TX {:?}", data);
+        uart_tx.write(&data).await.unwrap();
+        Timer::after(Duration::from_secs(1)).await;
+    }
+}
+
+#[embassy_executor::task]
+async fn reader(mut rx: UartRx<'static, UART1, Async>) {
+    info!("Reading...");
+    loop {
+        // read a total of 4 transmissions (32 / 8) and then print the result
+        let mut buf = [0; 32];
+        rx.read(&mut buf).await.unwrap();
+        info!("RX {:?}", buf);
+    }
+}
diff --git a/firmware/rust1/src/bin/usb_ethernet.rs b/firmware/rust1/src/bin/usb_ethernet.rs
new file mode 100644
index 0000000000000000000000000000000000000000..66a6ed4d05639ff6a51cc89848c288139dd34154
--- /dev/null
+++ b/firmware/rust1/src/bin/usb_ethernet.rs
@@ -0,0 +1,151 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_net::tcp::TcpSocket;
+use embassy_net::{Stack, StackResources};
+use embassy_rp::usb::Driver;
+use embassy_rp::{interrupt, peripherals};
+use embassy_usb::class::cdc_ncm::embassy_net::{Device, Runner, State as NetState};
+use embassy_usb::class::cdc_ncm::{CdcNcmClass, State};
+use embassy_usb::{Builder, Config, UsbDevice};
+use embedded_io::asynch::Write;
+use static_cell::StaticCell;
+use {defmt_rtt as _, panic_probe as _};
+
+type MyDriver = Driver<'static, peripherals::USB>;
+
+macro_rules! singleton {
+    ($val:expr) => {{
+        type T = impl Sized;
+        static STATIC_CELL: StaticCell<T> = StaticCell::new();
+        let (x,) = STATIC_CELL.init(($val,));
+        x
+    }};
+}
+
+const MTU: usize = 1514;
+
+#[embassy_executor::task]
+async fn usb_task(mut device: UsbDevice<'static, MyDriver>) -> ! {
+    device.run().await
+}
+
+#[embassy_executor::task]
+async fn usb_ncm_task(class: Runner<'static, MyDriver, MTU>) -> ! {
+    class.run().await
+}
+
+#[embassy_executor::task]
+async fn net_task(stack: &'static Stack<Device<'static, MTU>>) -> ! {
+    stack.run().await
+}
+
+#[embassy_executor::main]
+async fn main(spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+
+    // Create the driver, from the HAL.
+    let irq = interrupt::take!(USBCTRL_IRQ);
+    let driver = Driver::new(p.USB, irq);
+
+    // Create embassy-usb Config
+    let mut config = Config::new(0xc0de, 0xcafe);
+    config.manufacturer = Some("Embassy");
+    config.product = Some("USB-Ethernet example");
+    config.serial_number = Some("12345678");
+    config.max_power = 100;
+    config.max_packet_size_0 = 64;
+
+    // Required for Windows support.
+    config.composite_with_iads = true;
+    config.device_class = 0xEF;
+    config.device_sub_class = 0x02;
+    config.device_protocol = 0x01;
+
+    // Create embassy-usb DeviceBuilder using the driver and config.
+    let mut builder = Builder::new(
+        driver,
+        config,
+        &mut singleton!([0; 256])[..],
+        &mut singleton!([0; 256])[..],
+        &mut singleton!([0; 256])[..],
+        &mut singleton!([0; 128])[..],
+    );
+
+    // Our MAC addr.
+    let our_mac_addr = [0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC];
+    // Host's MAC addr. This is the MAC the host "thinks" its USB-to-ethernet adapter has.
+    let host_mac_addr = [0x88, 0x88, 0x88, 0x88, 0x88, 0x88];
+
+    // Create classes on the builder.
+    let class = CdcNcmClass::new(&mut builder, singleton!(State::new()), host_mac_addr, 64);
+
+    // Build the builder.
+    let usb = builder.build();
+
+    unwrap!(spawner.spawn(usb_task(usb)));
+
+    let (runner, device) = class.into_embassy_net_device::<MTU, 4, 4>(singleton!(NetState::new()), our_mac_addr);
+    unwrap!(spawner.spawn(usb_ncm_task(runner)));
+
+    let config = embassy_net::Config::Dhcp(Default::default());
+    //let config = embassy_net::Config::Static(embassy_net::StaticConfig {
+    //    address: Ipv4Cidr::new(Ipv4Address::new(10, 42, 0, 61), 24),
+    //    dns_servers: Vec::new(),
+    //    gateway: Some(Ipv4Address::new(10, 42, 0, 1)),
+    //});
+
+    // Generate random seed
+    let seed = 1234; // guaranteed random, chosen by a fair dice roll
+
+    // Init network stack
+    let stack = &*singleton!(Stack::new(device, config, singleton!(StackResources::<2>::new()), seed));
+
+    unwrap!(spawner.spawn(net_task(stack)));
+
+    // And now we can use it!
+
+    let mut rx_buffer = [0; 4096];
+    let mut tx_buffer = [0; 4096];
+    let mut buf = [0; 4096];
+
+    loop {
+        let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
+        socket.set_timeout(Some(embassy_net::SmolDuration::from_secs(10)));
+
+        info!("Listening on TCP:1234...");
+        if let Err(e) = socket.accept(1234).await {
+            warn!("accept error: {:?}", e);
+            continue;
+        }
+
+        info!("Received connection from {:?}", socket.remote_endpoint());
+
+        loop {
+            let n = match socket.read(&mut buf).await {
+                Ok(0) => {
+                    warn!("read EOF");
+                    break;
+                }
+                Ok(n) => n,
+                Err(e) => {
+                    warn!("read error: {:?}", e);
+                    break;
+                }
+            };
+
+            info!("rxd {:02x}", &buf[..n]);
+
+            match socket.write_all(&buf[..n]).await {
+                Ok(()) => {}
+                Err(e) => {
+                    warn!("write error: {:?}", e);
+                    break;
+                }
+            };
+        }
+    }
+}
diff --git a/firmware/rust1/src/bin/usb_logger.rs b/firmware/rust1/src/bin/usb_logger.rs
new file mode 100644
index 0000000000000000000000000000000000000000..52417a02e0c018e7198549b14079681d4f2daea4
--- /dev/null
+++ b/firmware/rust1/src/bin/usb_logger.rs
@@ -0,0 +1,30 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use embassy_executor::Spawner;
+use embassy_rp::interrupt;
+use embassy_rp::peripherals::USB;
+use embassy_rp::usb::Driver;
+use embassy_time::{Duration, Timer};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::task]
+async fn logger_task(driver: Driver<'static, USB>) {
+    embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver);
+}
+
+#[embassy_executor::main]
+async fn main(spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    let irq = interrupt::take!(USBCTRL_IRQ);
+    let driver = Driver::new(p.USB, irq);
+    spawner.spawn(logger_task(driver)).unwrap();
+
+    let mut counter = 0;
+    loop {
+        counter += 1;
+        log::info!("Tick {}", counter);
+        Timer::after(Duration::from_secs(1)).await;
+    }
+}
diff --git a/firmware/rust1/src/bin/usb_serial.rs b/firmware/rust1/src/bin/usb_serial.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8160a18753fd355c5b8a0f9e0a08b1af89f119a5
--- /dev/null
+++ b/firmware/rust1/src/bin/usb_serial.rs
@@ -0,0 +1,101 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::{info, panic};
+use embassy_executor::Spawner;
+use embassy_futures::join::join;
+use embassy_rp::interrupt;
+use embassy_rp::usb::{Driver, Instance};
+use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
+use embassy_usb::driver::EndpointError;
+use embassy_usb::{Builder, Config};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    info!("Hello there!");
+
+    let p = embassy_rp::init(Default::default());
+
+    // Create the driver, from the HAL.
+    let irq = interrupt::take!(USBCTRL_IRQ);
+    let driver = Driver::new(p.USB, irq);
+
+    // Create embassy-usb Config
+    let mut config = Config::new(0xc0de, 0xcafe);
+    config.manufacturer = Some("Embassy");
+    config.product = Some("USB-serial example");
+    config.serial_number = Some("12345678");
+    config.max_power = 100;
+    config.max_packet_size_0 = 64;
+
+    // Required for windows compatibility.
+    // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help
+    config.device_class = 0xEF;
+    config.device_sub_class = 0x02;
+    config.device_protocol = 0x01;
+    config.composite_with_iads = true;
+
+    // Create embassy-usb DeviceBuilder using the driver and config.
+    // It needs some buffers for building the descriptors.
+    let mut device_descriptor = [0; 256];
+    let mut config_descriptor = [0; 256];
+    let mut bos_descriptor = [0; 256];
+    let mut control_buf = [0; 64];
+
+    let mut state = State::new();
+
+    let mut builder = Builder::new(
+        driver,
+        config,
+        &mut device_descriptor,
+        &mut config_descriptor,
+        &mut bos_descriptor,
+        &mut control_buf,
+    );
+
+    // Create classes on the builder.
+    let mut class = CdcAcmClass::new(&mut builder, &mut state, 64);
+
+    // Build the builder.
+    let mut usb = builder.build();
+
+    // Run the USB device.
+    let usb_fut = usb.run();
+
+    // Do stuff with the class!
+    let echo_fut = async {
+        loop {
+            class.wait_connection().await;
+            info!("Connected");
+            let _ = echo(&mut class).await;
+            info!("Disconnected");
+        }
+    };
+
+    // Run everything concurrently.
+    // If we had made everything `'static` above instead, we could do this using separate tasks instead.
+    join(usb_fut, echo_fut).await;
+}
+
+struct Disconnected {}
+
+impl From<EndpointError> for Disconnected {
+    fn from(val: EndpointError) -> Self {
+        match val {
+            EndpointError::BufferOverflow => panic!("Buffer overflow"),
+            EndpointError::Disabled => Disconnected {},
+        }
+    }
+}
+
+async fn echo<'d, T: Instance + 'd>(class: &mut CdcAcmClass<'d, Driver<'d, T>>) -> Result<(), Disconnected> {
+    let mut buf = [0; 64];
+    loop {
+        let n = class.read_packet(&mut buf).await?;
+        let data = &buf[..n];
+        info!("data: {:x}", data);
+        class.write_packet(data).await?;
+    }
+}
diff --git a/firmware/rust1/src/bin/watchdog.rs b/firmware/rust1/src/bin/watchdog.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ece5cfe38200b235ef7b85cd954f46583d1e6732
--- /dev/null
+++ b/firmware/rust1/src/bin/watchdog.rs
@@ -0,0 +1,48 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::info;
+use embassy_executor::Spawner;
+use embassy_rp::gpio;
+use embassy_rp::watchdog::*;
+use embassy_time::{Duration, Timer};
+use gpio::{Level, Output};
+use {defmt_rtt as _, panic_probe as _};
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    let p = embassy_rp::init(Default::default());
+    info!("Hello world!");
+
+    let mut watchdog = Watchdog::new(p.WATCHDOG);
+    let mut led = Output::new(p.PIN_25, Level::Low);
+
+    // Set the LED high for 2 seconds so we know when we're about to start the watchdog
+    led.set_high();
+    Timer::after(Duration::from_secs(2)).await;
+
+    // Set to watchdog to reset if it's not fed within 1.05 seconds, and start it
+    watchdog.start(Duration::from_millis(1_050));
+    info!("Started the watchdog timer");
+
+    // Blink once a second for 5 seconds, feed the watchdog timer once a second to avoid a reset
+    for _ in 1..=5 {
+        led.set_low();
+        Timer::after(Duration::from_millis(500)).await;
+        led.set_high();
+        Timer::after(Duration::from_millis(500)).await;
+        info!("Feeding watchdog");
+        watchdog.feed();
+    }
+
+    info!("Stopped feeding, device will reset in 1.05 seconds");
+    // Blink 10 times per second, not feeding the watchdog.
+    // The processor should reset in 1.05 seconds.
+    loop {
+        led.set_low();
+        Timer::after(Duration::from_millis(100)).await;
+        led.set_high();
+        Timer::after(Duration::from_millis(100)).await;
+    }
+}
diff --git a/firmware/rust1/src/bin/ws2812-pio.rs b/firmware/rust1/src/bin/ws2812-pio.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d7c4742d89ac7bf147cc5b46c35fd258a1bae07f
--- /dev/null
+++ b/firmware/rust1/src/bin/ws2812-pio.rs
@@ -0,0 +1,129 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+use defmt::*;
+use embassy_embedded_hal::SetConfig;
+use embassy_executor::Spawner;
+use embassy_rp::pio::{Common, Config, FifoJoin, Instance, Pio, PioPin, ShiftConfig, ShiftDirection, StateMachine};
+use embassy_rp::relocate::RelocatedProgram;
+use embassy_time::{Duration, Timer};
+use fixed_macro::fixed;
+use smart_leds::RGB8;
+use {defmt_rtt as _, panic_probe as _};
+pub struct Ws2812<'d, P: Instance, const S: usize> {
+    sm: StateMachine<'d, P, S>,
+}
+
+impl<'d, P: Instance, const S: usize> Ws2812<'d, P, S> {
+    pub fn new(mut pio: Common<'d, P>, mut sm: StateMachine<'d, P, S>, pin: impl PioPin) -> Self {
+        // Setup sm0
+
+        // prepare the PIO program
+        let side_set = pio::SideSet::new(false, 1, false);
+        let mut a: pio::Assembler<32> = pio::Assembler::new_with_side_set(side_set);
+
+        const T1: u8 = 2; // start bit
+        const T2: u8 = 5; // data bit
+        const T3: u8 = 3; // stop bit
+        const CYCLES_PER_BIT: u32 = (T1 + T2 + T3) as u32;
+
+        let mut wrap_target = a.label();
+        let mut wrap_source = a.label();
+        let mut do_zero = a.label();
+        a.set_with_side_set(pio::SetDestination::PINDIRS, 1, 0);
+        a.bind(&mut wrap_target);
+        // Do stop bit
+        a.out_with_delay_and_side_set(pio::OutDestination::X, 1, T3 - 1, 0);
+        // Do start bit
+        a.jmp_with_delay_and_side_set(pio::JmpCondition::XIsZero, &mut do_zero, T1 - 1, 1);
+        // Do data bit = 1
+        a.jmp_with_delay_and_side_set(pio::JmpCondition::Always, &mut wrap_target, T2 - 1, 1);
+        a.bind(&mut do_zero);
+        // Do data bit = 0
+        a.nop_with_delay_and_side_set(T2 - 1, 0);
+        a.bind(&mut wrap_source);
+
+        let prg = a.assemble_with_wrap(wrap_source, wrap_target);
+        let mut cfg = Config::default();
+
+        // Pin config
+        let out_pin = pio.make_pio_pin(pin);
+
+        let relocated = RelocatedProgram::new(&prg);
+        cfg.use_program(&pio.load_program(&relocated), &[&out_pin]);
+
+        // Clock config, measured in kHz to avoid overflows
+        // TODO CLOCK_FREQ should come from embassy_rp
+        let clock_freq = fixed!(125_000: U24F8);
+        let ws2812_freq = fixed!(800: U24F8);
+        let bit_freq = ws2812_freq * CYCLES_PER_BIT;
+        cfg.clock_divider = clock_freq / bit_freq;
+
+        // FIFO config
+        cfg.fifo_join = FifoJoin::TxOnly;
+        cfg.shift_out = ShiftConfig {
+            auto_fill: true,
+            threshold: 24,
+            direction: ShiftDirection::Left,
+        };
+
+        sm.set_config(&cfg);
+        sm.set_enable(true);
+
+        Self { sm }
+    }
+
+    pub async fn write(&mut self, colors: &[RGB8]) {
+        for color in colors {
+            let word = (u32::from(color.g) << 24) | (u32::from(color.r) << 16) | (u32::from(color.b) << 8);
+            self.sm.tx().wait_push(word).await;
+        }
+    }
+}
+
+/// Input a value 0 to 255 to get a color value
+/// The colours are a transition r - g - b - back to r.
+fn wheel(mut wheel_pos: u8) -> RGB8 {
+    wheel_pos = 255 - wheel_pos;
+    if wheel_pos < 85 {
+        return (255 - wheel_pos * 3, 0, wheel_pos * 3).into();
+    }
+    if wheel_pos < 170 {
+        wheel_pos -= 85;
+        return (0, wheel_pos * 3, 255 - wheel_pos * 3).into();
+    }
+    wheel_pos -= 170;
+    (wheel_pos * 3, 255 - wheel_pos * 3, 0).into()
+}
+
+#[embassy_executor::main]
+async fn main(_spawner: Spawner) {
+    info!("Start");
+    let p = embassy_rp::init(Default::default());
+
+    let Pio { common, sm0, .. } = Pio::new(p.PIO0);
+
+    // This is the number of leds in the string. Helpfully, the sparkfun thing plus and adafruit
+    // feather boards for the 2040 both have one built in.
+    const NUM_LEDS: usize = 1;
+    let mut data = [RGB8::default(); NUM_LEDS];
+
+    // For the thing plus, use pin 8
+    // For the feather, use pin 16
+    let mut ws2812 = Ws2812::new(common, sm0, p.PIN_8);
+
+    // Loop forever making RGB values and pushing them out to the WS2812.
+    loop {
+        for j in 0..(256 * 5) {
+            debug!("New Colors:");
+            for i in 0..NUM_LEDS {
+                data[i] = wheel((((i * 256) as u16 / NUM_LEDS as u16 + j as u16) & 255) as u8);
+                debug!("R: {} G: {} B: {}", data[i].r, data[i].g, data[i].b);
+            }
+            ws2812.write(&data).await;
+
+            Timer::after(Duration::from_micros(5)).await;
+        }
+    }
+}