diff --git a/embassy-nrf/Cargo.toml b/embassy-nrf/Cargo.toml
index 5e69a8878..36c61c651 100644
--- a/embassy-nrf/Cargo.toml
+++ b/embassy-nrf/Cargo.toml
@@ -64,6 +64,7 @@ _gpio-p1 = []
 embassy = { version = "0.1.0", path = "../embassy" }
 embassy-macros = { version = "0.1.0", path = "../embassy-macros", features = ["nrf"]}
 embassy-hal-common = {version = "0.1.0", path = "../embassy-hal-common" }
+embassy-usb = {version = "0.1.0", path = "../embassy-usb" }
 
 embedded-hal-02 = { package = "embedded-hal", version = "0.2.6", features = ["unproven"] }
 embedded-hal-1 = { package = "embedded-hal", version = "1.0.0-alpha.7", git = "https://github.com/embassy-rs/embedded-hal", branch = "embassy2", optional = true}
@@ -80,8 +81,6 @@ rand_core = "0.6.3"
 fixed = "1.10.0"
 embedded-storage = "0.3.0"
 cfg-if = "1.0.0"
-nrf-usbd = {version = "0.1.1"}
-usb-device = "0.2.8"
 
 nrf52805-pac  = { version = "0.11.0", optional = true, features = [ "rt" ] }
 nrf52810-pac  = { version = "0.11.0", optional = true, features = [ "rt" ] }
diff --git a/embassy-nrf/src/usb.rs b/embassy-nrf/src/usb.rs
index deab94544..0f7d68d8c 100644
--- a/embassy-nrf/src/usb.rs
+++ b/embassy-nrf/src/usb.rs
@@ -1,43 +1,423 @@
 #![macro_use]
 
+use core::marker::PhantomData;
+use core::sync::atomic::{compiler_fence, Ordering};
+use core::task::Poll;
+use embassy::interrupt::InterruptExt;
+use embassy::time::{with_timeout, Duration};
+use embassy::util::Unborrow;
+use embassy::waitqueue::AtomicWaker;
+use embassy_hal_common::unborrow;
+use embassy_usb::driver::{self, ReadError, WriteError};
+use embassy_usb::types::{EndpointAddress, EndpointInfo, EndpointType, UsbDirection};
+use futures::future::poll_fn;
+use futures::Future;
+use pac::NVIC;
+
+pub use embassy_usb;
+
 use crate::interrupt::Interrupt;
 use crate::pac;
 
-use core::marker::PhantomData;
-use embassy::util::Unborrow;
-use nrf_usbd::{UsbPeripheral, Usbd};
-use usb_device::bus::UsbBusAllocator;
+static EP0_WAKER: AtomicWaker = AtomicWaker::new();
 
-pub use embassy_hal_common::usb::*;
-
-pub struct UsbBus<'d, T: Instance> {
+pub struct Driver<'d, T: Instance> {
     phantom: PhantomData<&'d mut T>,
+    alloc_in: Allocator,
+    alloc_out: Allocator,
 }
 
-unsafe impl<'d, T: Instance> UsbPeripheral for UsbBus<'d, T> {
-    // todo how to use T::regs
-    const REGISTERS: *const () = pac::USBD::ptr() as *const ();
-}
+impl<'d, T: Instance> Driver<'d, T> {
+    pub fn new(
+        _usb: impl Unborrow<Target = T> + 'd,
+        irq: impl Unborrow<Target = T::Interrupt> + 'd,
+    ) -> Self {
+        unborrow!(irq);
+        irq.set_handler(Self::on_interrupt);
+        irq.unpend();
+        irq.enable();
 
-impl<'d, T: Instance> UsbBus<'d, T> {
-    pub fn new(_usb: impl Unborrow<Target = T> + 'd) -> UsbBusAllocator<Usbd<UsbBus<'d, T>>> {
-        let r = T::regs();
-
-        r.intenset.write(|w| {
-            w.sof().set_bit();
-            w.usbevent().set_bit();
-            w.ep0datadone().set_bit();
-            w.ep0setup().set_bit();
-            w.usbreset().set_bit()
-        });
-
-        Usbd::new(UsbBus {
+        Self {
             phantom: PhantomData,
-        })
+            alloc_in: Allocator::new(),
+            alloc_out: Allocator::new(),
+        }
+    }
+
+    fn on_interrupt(_: *mut ()) {
+        let regs = T::regs();
+
+        if regs.events_ep0setup.read().bits() != 0 {
+            regs.intenclr.write(|w| w.ep0setup().clear());
+            EP0_WAKER.wake();
+        }
+        if regs.events_ep0datadone.read().bits() != 0 {
+            regs.intenclr.write(|w| w.ep0datadone().clear());
+            EP0_WAKER.wake();
+        }
+    }
+
+    fn set_stalled(ep_addr: EndpointAddress, stalled: bool) {
+        let regs = T::regs();
+
+        unsafe {
+            if ep_addr.index() == 0 {
+                regs.tasks_ep0stall
+                    .write(|w| w.tasks_ep0stall().bit(stalled));
+            } else {
+                regs.epstall.write(|w| {
+                    w.ep().bits(ep_addr.index() as u8 & 0b111);
+                    w.io().bit(ep_addr.is_in());
+                    w.stall().bit(stalled)
+                });
+            }
+        }
+
+        //if stalled {
+        //    self.busy_in_endpoints &= !(1 << ep_addr.index());
+        //}
+    }
+
+    fn is_stalled(ep_addr: EndpointAddress) -> bool {
+        let regs = T::regs();
+
+        let i = ep_addr.index();
+        match ep_addr.direction() {
+            UsbDirection::Out => regs.halted.epout[i].read().getstatus().is_halted(),
+            UsbDirection::In => regs.halted.epin[i].read().getstatus().is_halted(),
+        }
     }
 }
 
-unsafe impl embassy_hal_common::usb::USBInterrupt for crate::interrupt::USBD {}
+impl<'d, T: Instance> driver::Driver<'d> for Driver<'d, T> {
+    type EndpointOut = Endpoint<'d, T, Out>;
+    type EndpointIn = Endpoint<'d, T, In>;
+    type Bus = Bus<'d, T>;
+
+    fn alloc_endpoint_in(
+        &mut self,
+        ep_addr: Option<EndpointAddress>,
+        ep_type: EndpointType,
+        max_packet_size: u16,
+        interval: u8,
+    ) -> Result<Self::EndpointIn, driver::EndpointAllocError> {
+        let index = self
+            .alloc_in
+            .allocate(ep_addr, ep_type, max_packet_size, interval)?;
+        let ep_addr = EndpointAddress::from_parts(index, UsbDirection::In);
+        Ok(Endpoint {
+            _phantom: PhantomData,
+            info: EndpointInfo {
+                addr: ep_addr,
+                ep_type,
+                max_packet_size,
+                interval,
+            },
+        })
+    }
+
+    fn alloc_endpoint_out(
+        &mut self,
+        ep_addr: Option<EndpointAddress>,
+        ep_type: EndpointType,
+        max_packet_size: u16,
+        interval: u8,
+    ) -> Result<Self::EndpointOut, driver::EndpointAllocError> {
+        let index = self
+            .alloc_out
+            .allocate(ep_addr, ep_type, max_packet_size, interval)?;
+        let ep_addr = EndpointAddress::from_parts(index, UsbDirection::Out);
+        Ok(Endpoint {
+            _phantom: PhantomData,
+            info: EndpointInfo {
+                addr: ep_addr,
+                ep_type,
+                max_packet_size,
+                interval,
+            },
+        })
+    }
+
+    fn enable(self) -> Self::Bus {
+        let regs = T::regs();
+
+        errata::pre_enable();
+
+        regs.enable.write(|w| w.enable().enabled());
+
+        // Wait until the peripheral is ready.
+        while !regs.eventcause.read().ready().is_ready() {}
+        regs.eventcause.write(|w| w.ready().set_bit()); // Write 1 to clear.
+
+        errata::post_enable();
+
+        unsafe { NVIC::unmask(pac::Interrupt::USBD) };
+
+        // Enable the USB pullup, allowing enumeration.
+        regs.usbpullup.write(|w| w.connect().enabled());
+        info!("enabled");
+
+        Bus {
+            phantom: PhantomData,
+            alloc_in: self.alloc_in,
+            alloc_out: self.alloc_out,
+        }
+    }
+}
+
+pub struct Bus<'d, T: Instance> {
+    phantom: PhantomData<&'d mut T>,
+    alloc_in: Allocator,
+    alloc_out: Allocator,
+}
+
+impl<'d, T: Instance> driver::Bus for Bus<'d, T> {
+    #[inline]
+    fn reset(&mut self) {
+        let regs = T::regs();
+
+        // TODO: Initialize ISO buffers
+
+        // XXX this is not spec compliant; the endpoints should only be enabled after the device
+        // has been put in the Configured state. However, usb-device provides no hook to do that
+        unsafe {
+            regs.epinen.write(|w| w.bits(self.alloc_in.used.into()));
+            regs.epouten.write(|w| w.bits(self.alloc_out.used.into()));
+        }
+
+        for i in 1..8 {
+            let out_enabled = self.alloc_out.used & (1 << i) != 0;
+
+            // when first enabled, bulk/interrupt OUT endpoints will *not* receive data (the
+            // peripheral will NAK all incoming packets) until we write a zero to the SIZE
+            // register (see figure 203 of the 52840 manual). To avoid that we write a 0 to the
+            // SIZE register
+            if out_enabled {
+                regs.size.epout[i].reset();
+            }
+        }
+
+        //self.busy_in_endpoints = 0;
+    }
+
+    #[inline]
+    fn set_device_address(&mut self, _addr: u8) {
+        // Nothing to do, the peripheral handles this.
+    }
+
+    fn set_stalled(&mut self, ep_addr: EndpointAddress, stalled: bool) {
+        Driver::<T>::set_stalled(ep_addr, stalled)
+    }
+
+    fn is_stalled(&mut self, ep_addr: EndpointAddress) -> bool {
+        Driver::<T>::is_stalled(ep_addr)
+    }
+
+    #[inline]
+    fn suspend(&mut self) {
+        let regs = T::regs();
+        regs.lowpower.write(|w| w.lowpower().low_power());
+    }
+
+    #[inline]
+    fn resume(&mut self) {
+        let regs = T::regs();
+
+        errata::pre_wakeup();
+
+        regs.lowpower.write(|w| w.lowpower().force_normal());
+    }
+}
+
+pub enum Out {}
+pub enum In {}
+
+pub struct Endpoint<'d, T: Instance, Dir> {
+    _phantom: PhantomData<(&'d mut T, Dir)>,
+    info: EndpointInfo,
+}
+
+impl<'d, T: Instance, Dir> driver::Endpoint for Endpoint<'d, T, Dir> {
+    fn info(&self) -> &EndpointInfo {
+        &self.info
+    }
+
+    fn set_stalled(&self, stalled: bool) {
+        Driver::<T>::set_stalled(self.info.addr, stalled)
+    }
+
+    fn is_stalled(&self) -> bool {
+        Driver::<T>::is_stalled(self.info.addr)
+    }
+}
+
+impl<'d, T: Instance> driver::EndpointOut for Endpoint<'d, T, Out> {
+    type ReadFuture<'a>
+    where
+        Self: 'a,
+    = impl Future<Output = Result<usize, ReadError>> + 'a;
+
+    fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> Self::ReadFuture<'a> {
+        async move {
+            let regs = T::regs();
+
+            if buf.len() == 0 {
+                regs.tasks_ep0status.write(|w| unsafe { w.bits(1) });
+                return Ok(0);
+            }
+
+            // Wait for SETUP packet
+            regs.events_ep0setup.reset();
+            regs.intenset.write(|w| w.ep0setup().set());
+            poll_fn(|cx| {
+                EP0_WAKER.register(cx.waker());
+                if regs.events_ep0setup.read().bits() != 0 {
+                    Poll::Ready(())
+                } else {
+                    Poll::Pending
+                }
+            })
+            .await;
+            info!("got SETUP");
+
+            if buf.len() < 8 {
+                return Err(ReadError::BufferOverflow);
+            }
+
+            buf[0] = regs.bmrequesttype.read().bits() as u8;
+            buf[1] = regs.brequest.read().brequest().bits();
+            buf[2] = regs.wvaluel.read().wvaluel().bits();
+            buf[3] = regs.wvalueh.read().wvalueh().bits();
+            buf[4] = regs.windexl.read().windexl().bits();
+            buf[5] = regs.windexh.read().windexh().bits();
+            buf[6] = regs.wlengthl.read().wlengthl().bits();
+            buf[7] = regs.wlengthh.read().wlengthh().bits();
+
+            Ok(8)
+        }
+    }
+}
+
+impl<'d, T: Instance> driver::EndpointIn for Endpoint<'d, T, In> {
+    type WriteFuture<'a>
+    where
+        Self: 'a,
+    = impl Future<Output = Result<(), WriteError>> + 'a;
+
+    fn write<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteFuture<'a> {
+        async move {
+            info!("write: {:x}", buf);
+
+            let regs = T::regs();
+
+            let ptr = buf.as_ptr() as u32;
+            let len = buf.len() as u32;
+            regs.epin0.ptr.write(|w| unsafe { w.bits(ptr) });
+            regs.epin0.maxcnt.write(|w| unsafe { w.bits(len) });
+
+            regs.events_ep0datadone.reset();
+            regs.events_endepin[0].reset();
+
+            dma_start();
+
+            regs.tasks_startepin[0].write(|w| unsafe { w.bits(1) });
+            info!("write: waiting for endepin...");
+            while regs.events_endepin[0].read().bits() == 0 {}
+
+            dma_end();
+
+            info!("write: waiting for ep0datadone...");
+            regs.intenset.write(|w| w.ep0datadone().set());
+            let res = with_timeout(
+                Duration::from_millis(10),
+                poll_fn(|cx| {
+                    EP0_WAKER.register(cx.waker());
+                    if regs.events_ep0datadone.read().bits() != 0 {
+                        Poll::Ready(())
+                    } else {
+                        Poll::Pending
+                    }
+                }),
+            )
+            .await;
+
+            if res.is_err() {
+                // todo wrong error
+                return Err(driver::WriteError::BufferOverflow);
+            }
+
+            info!("write done");
+
+            Ok(())
+        }
+    }
+}
+
+fn dma_start() {
+    compiler_fence(Ordering::Release);
+}
+
+fn dma_end() {
+    compiler_fence(Ordering::Acquire);
+}
+
+struct Allocator {
+    used: u16,
+    // Buffers can be up to 64 Bytes since this is a Full-Speed implementation.
+    lens: [u8; 9],
+}
+
+impl Allocator {
+    fn new() -> Self {
+        Self {
+            used: 0,
+            lens: [0; 9],
+        }
+    }
+
+    fn allocate(
+        &mut self,
+        ep_addr: Option<EndpointAddress>,
+        ep_type: EndpointType,
+        max_packet_size: u16,
+        _interval: u8,
+    ) -> Result<usize, driver::EndpointAllocError> {
+        // Endpoint addresses are fixed in hardware:
+        // - 0x80 / 0x00 - Control        EP0
+        // - 0x81 / 0x01 - Bulk/Interrupt EP1
+        // - 0x82 / 0x02 - Bulk/Interrupt EP2
+        // - 0x83 / 0x03 - Bulk/Interrupt EP3
+        // - 0x84 / 0x04 - Bulk/Interrupt EP4
+        // - 0x85 / 0x05 - Bulk/Interrupt EP5
+        // - 0x86 / 0x06 - Bulk/Interrupt EP6
+        // - 0x87 / 0x07 - Bulk/Interrupt EP7
+        // - 0x88 / 0x08 - Isochronous
+
+        // Endpoint directions are allocated individually.
+
+        let alloc_index = match ep_type {
+            EndpointType::Isochronous => 8,
+            EndpointType::Control => 0,
+            EndpointType::Interrupt | EndpointType::Bulk => {
+                // Find rightmost zero bit in 1..=7
+                let ones = (self.used >> 1).trailing_ones() as usize;
+                if ones >= 7 {
+                    return Err(driver::EndpointAllocError);
+                }
+                ones + 1
+            }
+        };
+
+        if self.used & (1 << alloc_index) != 0 {
+            return Err(driver::EndpointAllocError);
+        }
+
+        self.used |= 1 << alloc_index;
+        self.lens[alloc_index] = max_packet_size as u8;
+
+        Ok(alloc_index)
+    }
+}
 
 pub(crate) mod sealed {
     use super::*;
@@ -63,3 +443,64 @@ macro_rules! impl_usb {
         }
     };
 }
+
+mod errata {
+
+    /// Writes `val` to `addr`. Used to apply Errata workarounds.
+    unsafe fn poke(addr: u32, val: u32) {
+        (addr as *mut u32).write_volatile(val);
+    }
+
+    /// Reads 32 bits from `addr`.
+    unsafe fn peek(addr: u32) -> u32 {
+        (addr as *mut u32).read_volatile()
+    }
+
+    pub fn pre_enable() {
+        // Works around Erratum 187 on chip revisions 1 and 2.
+        unsafe {
+            poke(0x4006EC00, 0x00009375);
+            poke(0x4006ED14, 0x00000003);
+            poke(0x4006EC00, 0x00009375);
+        }
+
+        pre_wakeup();
+    }
+
+    pub fn post_enable() {
+        post_wakeup();
+
+        // Works around Erratum 187 on chip revisions 1 and 2.
+        unsafe {
+            poke(0x4006EC00, 0x00009375);
+            poke(0x4006ED14, 0x00000000);
+            poke(0x4006EC00, 0x00009375);
+        }
+    }
+
+    pub fn pre_wakeup() {
+        // Works around Erratum 171 on chip revisions 1 and 2.
+
+        unsafe {
+            if peek(0x4006EC00) == 0x00000000 {
+                poke(0x4006EC00, 0x00009375);
+            }
+
+            poke(0x4006EC14, 0x000000C0);
+            poke(0x4006EC00, 0x00009375);
+        }
+    }
+
+    pub fn post_wakeup() {
+        // Works around Erratum 171 on chip revisions 1 and 2.
+
+        unsafe {
+            if peek(0x4006EC00) == 0x00000000 {
+                poke(0x4006EC00, 0x00009375);
+            }
+
+            poke(0x4006EC14, 0x00000000);
+            poke(0x4006EC00, 0x00009375);
+        }
+    }
+}
diff --git a/embassy-usb/Cargo.toml b/embassy-usb/Cargo.toml
new file mode 100644
index 000000000..dfdc8fbac
--- /dev/null
+++ b/embassy-usb/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "embassy-usb"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+embassy = { version = "0.1.0", path = "../embassy" }
+
+defmt = { version = "0.3", optional = true }
+log = { version = "0.4.14", optional = true }
+cortex-m = "0.7.3"
+num-traits = { version = "0.2.14", default-features = false }
diff --git a/embassy-usb/src/builder.rs b/embassy-usb/src/builder.rs
new file mode 100644
index 000000000..e92cc8ef2
--- /dev/null
+++ b/embassy-usb/src/builder.rs
@@ -0,0 +1,347 @@
+use super::descriptor::{BosWriter, DescriptorWriter};
+use super::driver::{Driver, EndpointAllocError};
+use super::types::*;
+use super::UsbDevice;
+
+#[derive(Debug, Copy, Clone)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[non_exhaustive]
+pub struct Config<'a> {
+    pub(crate) vendor_id: u16,
+    pub(crate) product_id: u16,
+
+    /// Device class code assigned by USB.org. Set to `0xff` for vendor-specific
+    /// devices that do not conform to any class.
+    ///
+    /// Default: `0x00` (class code specified by interfaces)
+    pub device_class: u8,
+
+    /// Device sub-class code. Depends on class.
+    ///
+    /// Default: `0x00`
+    pub device_sub_class: u8,
+
+    /// Device protocol code. Depends on class and sub-class.
+    ///
+    /// Default: `0x00`
+    pub device_protocol: u8,
+
+    /// Device release version in BCD.
+    ///
+    /// Default: `0x0010` ("0.1")
+    pub device_release: u16,
+
+    /// Maximum packet size in bytes for the control endpoint 0.
+    ///
+    /// Valid values are 8, 16, 32 and 64. There's generally no need to change this from the default
+    /// value of 8 bytes unless a class uses control transfers for sending large amounts of data, in
+    /// which case using a larger packet size may be more efficient.
+    ///
+    /// Default: 8 bytes
+    pub max_packet_size_0: u8,
+
+    /// Manufacturer name string descriptor.
+    ///
+    /// Default: (none)
+    pub manufacturer: Option<&'a str>,
+
+    /// Product name string descriptor.
+    ///
+    /// Default: (none)
+    pub product: Option<&'a str>,
+
+    /// Serial number string descriptor.
+    ///
+    /// Default: (none)
+    pub serial_number: Option<&'a str>,
+
+    /// Whether the device supports remotely waking up the host is requested.
+    ///
+    /// Default: `false`
+    pub supports_remote_wakeup: bool,
+
+    /// Configures the device as a composite device with interface association descriptors.
+    ///
+    /// If set to `true`, the following fields should have the given values:
+    ///
+    /// - `device_class` = `0xEF`
+    /// - `device_sub_class` = `0x02`
+    /// - `device_protocol` = `0x01`
+    pub composite_with_iads: bool,
+
+    /// Whether the device has its own power source.
+    ///
+    /// This should be set to `true` even if the device is sometimes self-powered and may not
+    /// always draw power from the USB bus.
+    ///
+    /// Default: `false`
+    ///
+    /// See also: `max_power`
+    pub self_powered: bool,
+
+    /// Maximum current drawn from the USB bus by the device, in milliamps.
+    ///
+    /// The default is 100 mA. If your device always uses an external power source and never draws
+    /// power from the USB bus, this can be set to 0.
+    ///
+    /// See also: `self_powered`
+    ///
+    /// Default: 100mA
+    /// Max: 500mA
+    pub max_power: u16,
+}
+
+impl<'a> Config<'a> {
+    pub fn new(vid: u16, pid: u16) -> Self {
+        Self {
+            device_class: 0x00,
+            device_sub_class: 0x00,
+            device_protocol: 0x00,
+            max_packet_size_0: 8,
+            vendor_id: vid,
+            product_id: pid,
+            device_release: 0x0010,
+            manufacturer: None,
+            product: None,
+            serial_number: None,
+            self_powered: false,
+            supports_remote_wakeup: false,
+            composite_with_iads: false,
+            max_power: 100,
+        }
+    }
+}
+
+/// Used to build new [`UsbDevice`]s.
+pub struct UsbDeviceBuilder<'d, D: Driver<'d>> {
+    config: Config<'d>,
+
+    bus: D,
+    next_interface_number: u8,
+    next_string_index: u8,
+
+    // TODO make not pub?
+    pub device_descriptor: DescriptorWriter<'d>,
+    pub config_descriptor: DescriptorWriter<'d>,
+    pub bos_descriptor: BosWriter<'d>,
+}
+
+impl<'d, D: Driver<'d>> UsbDeviceBuilder<'d, D> {
+    /// Creates a builder for constructing a new [`UsbDevice`].
+    pub fn new(
+        bus: D,
+        config: Config<'d>,
+        device_descriptor_buf: &'d mut [u8],
+        config_descriptor_buf: &'d mut [u8],
+        bos_descriptor_buf: &'d mut [u8],
+    ) -> Self {
+        // Magic values specified in USB-IF ECN on IADs.
+        if config.composite_with_iads
+            && (config.device_class != 0xEF
+                || config.device_sub_class != 0x02
+                || config.device_protocol != 0x01)
+        {
+            panic!("if composite_with_iads is set, you must set device_class = 0xEF, device_sub_class = 0x02, device_protocol = 0x01");
+        }
+
+        if config.max_power > 500 {
+            panic!("The maximum allowed value for `max_power` is 500mA");
+        }
+
+        match config.max_packet_size_0 {
+            8 | 16 | 32 | 64 => {}
+            _ => panic!("invalid max_packet_size_0, the allowed values are 8, 16, 32 or 64"),
+        }
+
+        let mut device_descriptor = DescriptorWriter::new(device_descriptor_buf);
+        let mut config_descriptor = DescriptorWriter::new(config_descriptor_buf);
+        let mut bos_descriptor = BosWriter::new(DescriptorWriter::new(bos_descriptor_buf));
+
+        device_descriptor.device(&config).unwrap();
+        config_descriptor.configuration(&config).unwrap();
+        bos_descriptor.bos().unwrap();
+
+        UsbDeviceBuilder {
+            bus,
+            config,
+            next_interface_number: 0,
+            next_string_index: 4,
+
+            device_descriptor,
+            config_descriptor,
+            bos_descriptor,
+        }
+    }
+
+    /// Creates the [`UsbDevice`] instance with the configuration in this builder.
+    pub fn build(mut self) -> UsbDevice<'d, D> {
+        self.config_descriptor.end_configuration();
+        self.bos_descriptor.end_bos();
+
+        UsbDevice::build(
+            self.bus,
+            self.config,
+            self.device_descriptor.into_buf(),
+            self.config_descriptor.into_buf(),
+            self.bos_descriptor.writer.into_buf(),
+        )
+    }
+
+    /// Allocates a new interface number.
+    pub fn alloc_interface(&mut self) -> InterfaceNumber {
+        let number = self.next_interface_number;
+        self.next_interface_number += 1;
+
+        InterfaceNumber::new(number)
+    }
+
+    /// Allocates a new string index.
+    pub fn alloc_string(&mut self) -> StringIndex {
+        let index = self.next_string_index;
+        self.next_string_index += 1;
+
+        StringIndex::new(index)
+    }
+
+    /// Allocates an in endpoint.
+    ///
+    /// This directly delegates to [`Driver::alloc_endpoint_in`], so see that method for details. In most
+    /// cases classes should call the endpoint type specific methods instead.
+    pub fn alloc_endpoint_in(
+        &mut self,
+        ep_addr: Option<EndpointAddress>,
+        ep_type: EndpointType,
+        max_packet_size: u16,
+        interval: u8,
+    ) -> Result<D::EndpointIn, EndpointAllocError> {
+        self.bus
+            .alloc_endpoint_in(ep_addr, ep_type, max_packet_size, interval)
+    }
+
+    /// Allocates an out endpoint.
+    ///
+    /// This directly delegates to [`Driver::alloc_endpoint_out`], so see that method for details. In most
+    /// cases classes should call the endpoint type specific methods instead.
+    pub fn alloc_endpoint_out(
+        &mut self,
+        ep_addr: Option<EndpointAddress>,
+        ep_type: EndpointType,
+        max_packet_size: u16,
+        interval: u8,
+    ) -> Result<D::EndpointOut, EndpointAllocError> {
+        self.bus
+            .alloc_endpoint_out(ep_addr, ep_type, max_packet_size, interval)
+    }
+
+    /// Allocates a control in endpoint.
+    ///
+    /// This crate implements the control state machine only for endpoint 0. If classes want to
+    /// support control requests in other endpoints, the state machine must be implemented manually.
+    /// This should rarely be needed by classes.
+    ///
+    /// # Arguments
+    ///
+    /// * `max_packet_size` - Maximum packet size in bytes. Must be one of 8, 16, 32 or 64.
+    ///
+    /// # Panics
+    ///
+    /// Panics if endpoint allocation fails, because running out of endpoints or memory is not
+    /// feasibly recoverable.
+    #[inline]
+    pub fn alloc_control_endpoint_in(&mut self, max_packet_size: u16) -> D::EndpointIn {
+        self.alloc_endpoint_in(None, EndpointType::Control, max_packet_size, 0)
+            .expect("alloc_ep failed")
+    }
+
+    /// Allocates a control out endpoint.
+    ///
+    /// This crate implements the control state machine only for endpoint 0. If classes want to
+    /// support control requests in other endpoints, the state machine must be implemented manually.
+    /// This should rarely be needed by classes.
+    ///
+    /// # Arguments
+    ///
+    /// * `max_packet_size` - Maximum packet size in bytes. Must be one of 8, 16, 32 or 64.
+    ///
+    /// # Panics
+    ///
+    /// Panics if endpoint allocation fails, because running out of endpoints or memory is not
+    /// feasibly recoverable.
+    #[inline]
+    pub fn alloc_control_endpoint_out(&mut self, max_packet_size: u16) -> D::EndpointOut {
+        self.alloc_endpoint_out(None, EndpointType::Control, max_packet_size, 0)
+            .expect("alloc_ep failed")
+    }
+
+    /// Allocates a bulk in endpoint.
+    ///
+    /// # Arguments
+    ///
+    /// * `max_packet_size` - Maximum packet size in bytes. Must be one of 8, 16, 32 or 64.
+    ///
+    /// # Panics
+    ///
+    /// Panics if endpoint allocation fails, because running out of endpoints or memory is not
+    /// feasibly recoverable.
+    #[inline]
+    pub fn alloc_bulk_endpoint_in(&mut self, max_packet_size: u16) -> D::EndpointIn {
+        self.alloc_endpoint_in(None, EndpointType::Bulk, max_packet_size, 0)
+            .expect("alloc_ep failed")
+    }
+
+    /// Allocates a bulk out endpoint.
+    ///
+    /// # Arguments
+    ///
+    /// * `max_packet_size` - Maximum packet size in bytes. Must be one of 8, 16, 32 or 64.
+    ///
+    /// # Panics
+    ///
+    /// Panics if endpoint allocation fails, because running out of endpoints or memory is not
+    /// feasibly recoverable.
+    #[inline]
+    pub fn alloc_bulk_endpoint_out(&mut self, max_packet_size: u16) -> D::EndpointOut {
+        self.alloc_endpoint_out(None, EndpointType::Bulk, max_packet_size, 0)
+            .expect("alloc_ep failed")
+    }
+
+    /// Allocates a bulk in endpoint.
+    ///
+    /// # Arguments
+    ///
+    /// * `max_packet_size` - Maximum packet size in bytes. Cannot exceed 64 bytes.
+    ///
+    /// # Panics
+    ///
+    /// Panics if endpoint allocation fails, because running out of endpoints or memory is not
+    /// feasibly recoverable.
+    #[inline]
+    pub fn alloc_interrupt_endpoint_in(
+        &mut self,
+        max_packet_size: u16,
+        interval: u8,
+    ) -> D::EndpointIn {
+        self.alloc_endpoint_in(None, EndpointType::Interrupt, max_packet_size, interval)
+            .expect("alloc_ep failed")
+    }
+
+    /// Allocates a bulk in endpoint.
+    ///
+    /// # Arguments
+    ///
+    /// * `max_packet_size` - Maximum packet size in bytes. Cannot exceed 64 bytes.
+    ///
+    /// # Panics
+    ///
+    /// Panics if endpoint allocation fails, because running out of endpoints or memory is not
+    /// feasibly recoverable.
+    #[inline]
+    pub fn alloc_interrupt_endpoint_out(
+        &mut self,
+        max_packet_size: u16,
+        interval: u8,
+    ) -> D::EndpointOut {
+        self.alloc_endpoint_out(None, EndpointType::Interrupt, max_packet_size, interval)
+            .expect("alloc_ep failed")
+    }
+}
diff --git a/embassy-usb/src/control.rs b/embassy-usb/src/control.rs
new file mode 100644
index 000000000..f1148ac76
--- /dev/null
+++ b/embassy-usb/src/control.rs
@@ -0,0 +1,134 @@
+use core::mem;
+
+use super::types::*;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub enum ParseError {
+    InvalidLength,
+}
+
+/// Control request type.
+#[repr(u8)]
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub enum RequestType {
+    /// Request is a USB standard request. Usually handled by
+    /// [`UsbDevice`](crate::device::UsbDevice).
+    Standard = 0,
+    /// Request is intended for a USB class.
+    Class = 1,
+    /// Request is vendor-specific.
+    Vendor = 2,
+    /// Reserved.
+    Reserved = 3,
+}
+
+/// Control request recipient.
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub enum Recipient {
+    /// Request is intended for the entire device.
+    Device = 0,
+    /// Request is intended for an interface. Generally, the `index` field of the request specifies
+    /// the interface number.
+    Interface = 1,
+    /// Request is intended for an endpoint. Generally, the `index` field of the request specifies
+    /// the endpoint address.
+    Endpoint = 2,
+    /// None of the above.
+    Other = 3,
+    /// Reserved.
+    Reserved = 4,
+}
+
+/// A control request read from a SETUP packet.
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct Request {
+    /// Direction of the request.
+    pub direction: UsbDirection,
+    /// Type of the request.
+    pub request_type: RequestType,
+    /// Recipient of the request.
+    pub recipient: Recipient,
+    /// Request code. The meaning of the value depends on the previous fields.
+    pub request: u8,
+    /// Request value. The meaning of the value depends on the previous fields.
+    pub value: u16,
+    /// Request index. The meaning of the value depends on the previous fields.
+    pub index: u16,
+    /// Length of the DATA stage. For control OUT transfers this is the exact length of the data the
+    /// host sent. For control IN transfers this is the maximum length of data the device should
+    /// return.
+    pub length: u16,
+}
+
+impl Request {
+    /// Standard USB control request Get Status
+    pub const GET_STATUS: u8 = 0;
+
+    /// Standard USB control request Clear Feature
+    pub const CLEAR_FEATURE: u8 = 1;
+
+    /// Standard USB control request Set Feature
+    pub const SET_FEATURE: u8 = 3;
+
+    /// Standard USB control request Set Address
+    pub const SET_ADDRESS: u8 = 5;
+
+    /// Standard USB control request Get Descriptor
+    pub const GET_DESCRIPTOR: u8 = 6;
+
+    /// Standard USB control request Set Descriptor
+    pub const SET_DESCRIPTOR: u8 = 7;
+
+    /// Standard USB control request Get Configuration
+    pub const GET_CONFIGURATION: u8 = 8;
+
+    /// Standard USB control request Set Configuration
+    pub const SET_CONFIGURATION: u8 = 9;
+
+    /// Standard USB control request Get Interface
+    pub const GET_INTERFACE: u8 = 10;
+
+    /// Standard USB control request Set Interface
+    pub const SET_INTERFACE: u8 = 11;
+
+    /// Standard USB control request Synch Frame
+    pub const SYNCH_FRAME: u8 = 12;
+
+    /// Standard USB feature Endpoint Halt for Set/Clear Feature
+    pub const FEATURE_ENDPOINT_HALT: u16 = 0;
+
+    /// Standard USB feature Device Remote Wakeup for Set/Clear Feature
+    pub const FEATURE_DEVICE_REMOTE_WAKEUP: u16 = 1;
+
+    pub(crate) fn parse(buf: &[u8]) -> Result<Request, ParseError> {
+        if buf.len() != 8 {
+            return Err(ParseError::InvalidLength);
+        }
+
+        let rt = buf[0];
+        let recipient = rt & 0b11111;
+
+        Ok(Request {
+            direction: rt.into(),
+            request_type: unsafe { mem::transmute((rt >> 5) & 0b11) },
+            recipient: if recipient <= 3 {
+                unsafe { mem::transmute(recipient) }
+            } else {
+                Recipient::Reserved
+            },
+            request: buf[1],
+            value: (buf[2] as u16) | ((buf[3] as u16) << 8),
+            index: (buf[4] as u16) | ((buf[5] as u16) << 8),
+            length: (buf[6] as u16) | ((buf[7] as u16) << 8),
+        })
+    }
+
+    /// Gets the descriptor type and index from the value field of a GET_DESCRIPTOR request.
+    pub fn descriptor_type_index(&self) -> (u8, u8) {
+        ((self.value >> 8) as u8, self.value as u8)
+    }
+}
diff --git a/embassy-usb/src/descriptor.rs b/embassy-usb/src/descriptor.rs
new file mode 100644
index 000000000..746c6b828
--- /dev/null
+++ b/embassy-usb/src/descriptor.rs
@@ -0,0 +1,387 @@
+use super::builder::Config;
+use super::{types::*, CONFIGURATION_VALUE, DEFAULT_ALTERNATE_SETTING};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub enum Error {
+    BufferFull,
+    InvalidState,
+}
+
+/// Standard descriptor types
+#[allow(missing_docs)]
+pub mod descriptor_type {
+    pub const DEVICE: u8 = 1;
+    pub const CONFIGURATION: u8 = 2;
+    pub const STRING: u8 = 3;
+    pub const INTERFACE: u8 = 4;
+    pub const ENDPOINT: u8 = 5;
+    pub const IAD: u8 = 11;
+    pub const BOS: u8 = 15;
+    pub const CAPABILITY: u8 = 16;
+}
+
+/// String descriptor language IDs.
+pub mod lang_id {
+    /// English (US)
+    ///
+    /// Recommended for use as the first language ID for compatibility.
+    pub const ENGLISH_US: u16 = 0x0409;
+}
+
+/// Standard capability descriptor types
+#[allow(missing_docs)]
+pub mod capability_type {
+    pub const WIRELESS_USB: u8 = 1;
+    pub const USB_2_0_EXTENSION: u8 = 2;
+    pub const SS_USB_DEVICE: u8 = 3;
+    pub const CONTAINER_ID: u8 = 4;
+    pub const PLATFORM: u8 = 5;
+}
+
+/// A writer for USB descriptors.
+pub struct DescriptorWriter<'a> {
+    buf: &'a mut [u8],
+    position: usize,
+    num_interfaces_mark: Option<usize>,
+    num_endpoints_mark: Option<usize>,
+    write_iads: bool,
+}
+
+impl<'a> DescriptorWriter<'a> {
+    pub(crate) fn new(buf: &'a mut [u8]) -> Self {
+        DescriptorWriter {
+            buf,
+            position: 0,
+            num_interfaces_mark: None,
+            num_endpoints_mark: None,
+            write_iads: false,
+        }
+    }
+
+    pub fn into_buf(self) -> &'a mut [u8] {
+        &mut self.buf[..self.position]
+    }
+
+    /// Gets the current position in the buffer, i.e. the number of bytes written so far.
+    pub fn position(&self) -> usize {
+        self.position
+    }
+
+    /// Writes an arbitrary (usually class-specific) descriptor.
+    pub fn write(&mut self, descriptor_type: u8, descriptor: &[u8]) -> Result<(), Error> {
+        let length = descriptor.len();
+
+        if (self.position + 2 + length) > self.buf.len() || (length + 2) > 255 {
+            return Err(Error::BufferFull);
+        }
+
+        self.buf[self.position] = (length + 2) as u8;
+        self.buf[self.position + 1] = descriptor_type;
+
+        let start = self.position + 2;
+
+        self.buf[start..start + length].copy_from_slice(descriptor);
+
+        self.position = start + length;
+
+        Ok(())
+    }
+
+    pub(crate) fn device(&mut self, config: &Config) -> Result<(), Error> {
+        self.write(
+            descriptor_type::DEVICE,
+            &[
+                0x10,
+                0x02,                     // bcdUSB 2.1
+                config.device_class,      // bDeviceClass
+                config.device_sub_class,  // bDeviceSubClass
+                config.device_protocol,   // bDeviceProtocol
+                config.max_packet_size_0, // bMaxPacketSize0
+                config.vendor_id as u8,
+                (config.vendor_id >> 8) as u8, // idVendor
+                config.product_id as u8,
+                (config.product_id >> 8) as u8, // idProduct
+                config.device_release as u8,
+                (config.device_release >> 8) as u8,    // bcdDevice
+                config.manufacturer.map_or(0, |_| 1),  // iManufacturer
+                config.product.map_or(0, |_| 2),       // iProduct
+                config.serial_number.map_or(0, |_| 3), // iSerialNumber
+                1,                                     // bNumConfigurations
+            ],
+        )
+    }
+
+    pub(crate) fn configuration(&mut self, config: &Config) -> Result<(), Error> {
+        self.num_interfaces_mark = Some(self.position + 4);
+
+        self.write_iads = config.composite_with_iads;
+
+        self.write(
+            descriptor_type::CONFIGURATION,
+            &[
+                0,
+                0,                   // wTotalLength
+                0,                   // bNumInterfaces
+                CONFIGURATION_VALUE, // bConfigurationValue
+                0,                   // iConfiguration
+                0x80 | if config.self_powered { 0x40 } else { 0x00 }
+                    | if config.supports_remote_wakeup {
+                        0x20
+                    } else {
+                        0x00
+                    }, // bmAttributes
+                (config.max_power / 2) as u8, // bMaxPower
+            ],
+        )
+    }
+
+    pub(crate) fn end_class(&mut self) {
+        self.num_endpoints_mark = None;
+    }
+
+    pub(crate) fn end_configuration(&mut self) {
+        let position = self.position as u16;
+        self.buf[2..4].copy_from_slice(&position.to_le_bytes());
+    }
+
+    /// Writes a interface association descriptor. Call from `UsbClass::get_configuration_descriptors`
+    /// before writing the USB class or function's interface descriptors if your class has more than
+    /// one interface and wants to play nicely with composite devices on Windows. If the USB device
+    /// hosting the class was not configured as composite with IADs enabled, calling this function
+    /// does nothing, so it is safe to call from libraries.
+    ///
+    /// # Arguments
+    ///
+    /// * `first_interface` - Number of the function's first interface, previously allocated with
+    ///   [`UsbDeviceBuilder::interface`](crate::bus::UsbDeviceBuilder::interface).
+    /// * `interface_count` - Number of interfaces in the function.
+    /// * `function_class` - Class code assigned by USB.org. Use `0xff` for vendor-specific devices
+    ///   that do not conform to any class.
+    /// * `function_sub_class` - Sub-class code. Depends on class.
+    /// * `function_protocol` - Protocol code. Depends on class and sub-class.
+    pub fn iad(
+        &mut self,
+        first_interface: InterfaceNumber,
+        interface_count: u8,
+        function_class: u8,
+        function_sub_class: u8,
+        function_protocol: u8,
+    ) -> Result<(), Error> {
+        if !self.write_iads {
+            return Ok(());
+        }
+
+        self.write(
+            descriptor_type::IAD,
+            &[
+                first_interface.into(), // bFirstInterface
+                interface_count,        // bInterfaceCount
+                function_class,
+                function_sub_class,
+                function_protocol,
+                0,
+            ],
+        )?;
+
+        Ok(())
+    }
+
+    /// Writes a interface descriptor.
+    ///
+    /// # Arguments
+    ///
+    /// * `number` - Interface number previously allocated with
+    ///   [`UsbDeviceBuilder::interface`](crate::bus::UsbDeviceBuilder::interface).
+    /// * `interface_class` - Class code assigned by USB.org. Use `0xff` for vendor-specific devices
+    ///   that do not conform to any class.
+    /// * `interface_sub_class` - Sub-class code. Depends on class.
+    /// * `interface_protocol` - Protocol code. Depends on class and sub-class.
+    pub fn interface(
+        &mut self,
+        number: InterfaceNumber,
+        interface_class: u8,
+        interface_sub_class: u8,
+        interface_protocol: u8,
+    ) -> Result<(), Error> {
+        self.interface_alt(
+            number,
+            DEFAULT_ALTERNATE_SETTING,
+            interface_class,
+            interface_sub_class,
+            interface_protocol,
+            None,
+        )
+    }
+
+    /// Writes a interface descriptor with a specific alternate setting and
+    /// interface string identifier.
+    ///
+    /// # Arguments
+    ///
+    /// * `number` - Interface number previously allocated with
+    ///   [`UsbDeviceBuilder::interface`](crate::bus::UsbDeviceBuilder::interface).
+    /// * `alternate_setting` - Number of the alternate setting
+    /// * `interface_class` - Class code assigned by USB.org. Use `0xff` for vendor-specific devices
+    ///   that do not conform to any class.
+    /// * `interface_sub_class` - Sub-class code. Depends on class.
+    /// * `interface_protocol` - Protocol code. Depends on class and sub-class.
+    /// * `interface_string` - Index of string descriptor describing this interface
+
+    pub fn interface_alt(
+        &mut self,
+        number: InterfaceNumber,
+        alternate_setting: u8,
+        interface_class: u8,
+        interface_sub_class: u8,
+        interface_protocol: u8,
+        interface_string: Option<StringIndex>,
+    ) -> Result<(), Error> {
+        if alternate_setting == DEFAULT_ALTERNATE_SETTING {
+            match self.num_interfaces_mark {
+                Some(mark) => self.buf[mark] += 1,
+                None => return Err(Error::InvalidState),
+            };
+        }
+
+        let str_index = interface_string.map_or(0, Into::into);
+
+        self.num_endpoints_mark = Some(self.position + 4);
+
+        self.write(
+            descriptor_type::INTERFACE,
+            &[
+                number.into(),       // bInterfaceNumber
+                alternate_setting,   // bAlternateSetting
+                0,                   // bNumEndpoints
+                interface_class,     // bInterfaceClass
+                interface_sub_class, // bInterfaceSubClass
+                interface_protocol,  // bInterfaceProtocol
+                str_index,           // iInterface
+            ],
+        )?;
+
+        Ok(())
+    }
+
+    /// Writes an endpoint descriptor.
+    ///
+    /// # Arguments
+    ///
+    /// * `endpoint` - Endpoint previously allocated with
+    ///   [`UsbDeviceBuilder`](crate::bus::UsbDeviceBuilder).
+    pub fn endpoint(&mut self, endpoint: &EndpointInfo) -> Result<(), Error> {
+        match self.num_endpoints_mark {
+            Some(mark) => self.buf[mark] += 1,
+            None => return Err(Error::InvalidState),
+        };
+
+        self.write(
+            descriptor_type::ENDPOINT,
+            &[
+                endpoint.addr.into(),   // bEndpointAddress
+                endpoint.ep_type as u8, // bmAttributes
+                endpoint.max_packet_size as u8,
+                (endpoint.max_packet_size >> 8) as u8, // wMaxPacketSize
+                endpoint.interval,                     // bInterval
+            ],
+        )?;
+
+        Ok(())
+    }
+
+    /// Writes a string descriptor.
+    pub(crate) fn string(&mut self, string: &str) -> Result<(), Error> {
+        let mut pos = self.position;
+
+        if pos + 2 > self.buf.len() {
+            return Err(Error::BufferFull);
+        }
+
+        self.buf[pos] = 0; // length placeholder
+        self.buf[pos + 1] = descriptor_type::STRING;
+
+        pos += 2;
+
+        for c in string.encode_utf16() {
+            if pos >= self.buf.len() {
+                return Err(Error::BufferFull);
+            }
+
+            self.buf[pos..pos + 2].copy_from_slice(&c.to_le_bytes());
+            pos += 2;
+        }
+
+        self.buf[self.position] = (pos - self.position) as u8;
+
+        self.position = pos;
+
+        Ok(())
+    }
+}
+
+/// A writer for Binary Object Store descriptor.
+pub struct BosWriter<'a> {
+    pub(crate) writer: DescriptorWriter<'a>,
+    num_caps_mark: Option<usize>,
+}
+
+impl<'a> BosWriter<'a> {
+    pub(crate) fn new(writer: DescriptorWriter<'a>) -> Self {
+        Self {
+            writer: writer,
+            num_caps_mark: None,
+        }
+    }
+
+    pub(crate) fn bos(&mut self) -> Result<(), Error> {
+        self.num_caps_mark = Some(self.writer.position + 4);
+        self.writer.write(
+            descriptor_type::BOS,
+            &[
+                0x00, 0x00, // wTotalLength
+                0x00, // bNumDeviceCaps
+            ],
+        )?;
+
+        self.capability(capability_type::USB_2_0_EXTENSION, &[0; 4])?;
+
+        Ok(())
+    }
+
+    /// Writes capability descriptor to a BOS
+    ///
+    /// # Arguments
+    ///
+    /// * `capability_type` - Type of a capability
+    /// * `data` - Binary data of the descriptor
+    pub fn capability(&mut self, capability_type: u8, data: &[u8]) -> Result<(), Error> {
+        match self.num_caps_mark {
+            Some(mark) => self.writer.buf[mark] += 1,
+            None => return Err(Error::InvalidState),
+        }
+
+        let mut start = self.writer.position;
+        let blen = data.len();
+
+        if (start + blen + 3) > self.writer.buf.len() || (blen + 3) > 255 {
+            return Err(Error::BufferFull);
+        }
+
+        self.writer.buf[start] = (blen + 3) as u8;
+        self.writer.buf[start + 1] = descriptor_type::CAPABILITY;
+        self.writer.buf[start + 2] = capability_type;
+
+        start += 3;
+        self.writer.buf[start..start + blen].copy_from_slice(data);
+        self.writer.position = start + blen;
+
+        Ok(())
+    }
+
+    pub(crate) fn end_bos(&mut self) {
+        self.num_caps_mark = None;
+        let position = self.writer.position as u16;
+        self.writer.buf[2..4].copy_from_slice(&position.to_le_bytes());
+    }
+}
diff --git a/embassy-usb/src/driver.rs b/embassy-usb/src/driver.rs
new file mode 100644
index 000000000..ed4edb576
--- /dev/null
+++ b/embassy-usb/src/driver.rs
@@ -0,0 +1,160 @@
+use core::future::Future;
+
+use super::types::*;
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct EndpointAllocError;
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+
+/// Operation is unsupported by the driver.
+pub struct Unsupported;
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+
+/// Errors returned by [`EndpointIn::write`]
+pub enum WriteError {
+    /// The packet is too long to fit in the
+    ///   transmission buffer. This is generally an error in the class implementation, because the
+    ///   class shouldn't provide more data than the `max_packet_size` it specified when allocating
+    ///   the endpoint.
+    BufferOverflow,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+
+/// Errors returned by [`EndpointOut::read`]
+pub enum ReadError {
+    /// The received packet is too long to
+    /// fit in `buf`. This is generally an error in the class implementation, because the class
+    /// should use a buffer that is large enough for the `max_packet_size` it specified when
+    /// allocating the endpoint.
+    BufferOverflow,
+}
+
+/// Driver for a specific USB peripheral. Implement this to add support for a new hardware
+/// platform.
+pub trait Driver<'a> {
+    type EndpointOut: EndpointOut + 'a;
+    type EndpointIn: EndpointIn + 'a;
+    type Bus: Bus + 'a;
+
+    /// Allocates an endpoint and specified endpoint parameters. This method is called by the device
+    /// and class implementations to allocate endpoints, and can only be called before
+    /// [`enable`](UsbBus::enable) is called.
+    ///
+    /// # Arguments
+    ///
+    /// * `ep_addr` - A static endpoint address to allocate. If Some, the implementation should
+    ///   attempt to return an endpoint with the specified address. If None, the implementation
+    ///   should return the next available one.
+    /// * `max_packet_size` - Maximum packet size in bytes.
+    /// * `interval` - Polling interval parameter for interrupt endpoints.
+    fn alloc_endpoint_out(
+        &mut self,
+        ep_addr: Option<EndpointAddress>,
+        ep_type: EndpointType,
+        max_packet_size: u16,
+        interval: u8,
+    ) -> Result<Self::EndpointOut, EndpointAllocError>;
+
+    fn alloc_endpoint_in(
+        &mut self,
+        ep_addr: Option<EndpointAddress>,
+        ep_type: EndpointType,
+        max_packet_size: u16,
+        interval: u8,
+    ) -> Result<Self::EndpointIn, EndpointAllocError>;
+
+    /// Enables and initializes the USB peripheral. Soon after enabling the device will be reset, so
+    /// there is no need to perform a USB reset in this method.
+    fn enable(self) -> Self::Bus;
+
+    /// Indicates that `set_device_address` must be called before accepting the corresponding
+    /// control transfer, not after.
+    ///
+    /// The default value for this constant is `false`, which corresponds to the USB 2.0 spec, 9.4.6
+    const QUIRK_SET_ADDRESS_BEFORE_STATUS: bool = false;
+}
+
+pub trait Bus {
+    /// Called when the host resets the device. This will be soon called after
+    /// [`poll`](crate::device::UsbDevice::poll) returns [`PollResult::Reset`]. This method should
+    /// reset the state of all endpoints and peripheral flags back to a state suitable for
+    /// enumeration, as well as ensure that all endpoints previously allocated with alloc_ep are
+    /// initialized as specified.
+    fn reset(&mut self);
+
+    /// Sets the device USB address to `addr`.
+    fn set_device_address(&mut self, addr: u8);
+
+    /// Sets or clears the STALL condition for an endpoint. If the endpoint is an OUT endpoint, it
+    /// should be prepared to receive data again. Only used during control transfers.
+    fn set_stalled(&mut self, ep_addr: EndpointAddress, stalled: bool);
+
+    /// Gets whether the STALL condition is set for an endpoint. Only used during control transfers.
+    fn is_stalled(&mut self, ep_addr: EndpointAddress) -> bool;
+
+    /// Causes the USB peripheral to enter USB suspend mode, lowering power consumption and
+    /// preparing to detect a USB wakeup event. This will be called after
+    /// [`poll`](crate::device::UsbDevice::poll) returns [`PollResult::Suspend`]. The device will
+    /// continue be polled, and it shall return a value other than `Suspend` from `poll` when it no
+    /// longer detects the suspend condition.
+    fn suspend(&mut self);
+
+    /// Resumes from suspend mode. This may only be called after the peripheral has been previously
+    /// suspended.
+    fn resume(&mut self);
+
+    /// Simulates a disconnect from the USB bus, causing the host to reset and re-enumerate the
+    /// device.
+    ///
+    /// The default implementation just returns `Unsupported`.
+    ///
+    /// # Errors
+    ///
+    /// * [`Unsupported`](crate::UsbError::Unsupported) - This UsbBus implementation doesn't support
+    ///   simulating a disconnect or it has not been enabled at creation time.
+    fn force_reset(&mut self) -> Result<(), Unsupported> {
+        Err(Unsupported)
+    }
+}
+
+pub trait Endpoint {
+    /// Get the endpoint address
+    fn info(&self) -> &EndpointInfo;
+
+    /// Sets or clears the STALL condition for an endpoint. If the endpoint is an OUT endpoint, it
+    /// should be prepared to receive data again.
+    fn set_stalled(&self, stalled: bool);
+
+    /// Gets whether the STALL condition is set for an endpoint.
+    fn is_stalled(&self) -> bool;
+
+    // TODO enable/disable?
+}
+
+pub trait EndpointOut: Endpoint {
+    type ReadFuture<'a>: Future<Output = Result<usize, ReadError>> + 'a
+    where
+        Self: 'a;
+
+    /// Reads a single packet of data from the endpoint, and returns the actual length of
+    /// the packet.
+    ///
+    /// This should also clear any NAK flags and prepare the endpoint to receive the next packet.
+    fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> Self::ReadFuture<'a>;
+}
+
+pub trait EndpointIn: Endpoint {
+    type WriteFuture<'a>: Future<Output = Result<(), WriteError>> + 'a
+    where
+        Self: 'a;
+
+    /// Writes a single packet of data to the endpoint.
+    fn write<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteFuture<'a>;
+}
diff --git a/embassy-usb/src/fmt.rs b/embassy-usb/src/fmt.rs
new file mode 100644
index 000000000..066970813
--- /dev/null
+++ b/embassy-usb/src/fmt.rs
@@ -0,0 +1,225 @@
+#![macro_use]
+#![allow(unused_macros)]
+
+#[cfg(all(feature = "defmt", feature = "log"))]
+compile_error!("You may not enable both `defmt` and `log` features.");
+
+macro_rules! assert {
+    ($($x:tt)*) => {
+        {
+            #[cfg(not(feature = "defmt"))]
+            ::core::assert!($($x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::assert!($($x)*);
+        }
+    };
+}
+
+macro_rules! assert_eq {
+    ($($x:tt)*) => {
+        {
+            #[cfg(not(feature = "defmt"))]
+            ::core::assert_eq!($($x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::assert_eq!($($x)*);
+        }
+    };
+}
+
+macro_rules! assert_ne {
+    ($($x:tt)*) => {
+        {
+            #[cfg(not(feature = "defmt"))]
+            ::core::assert_ne!($($x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::assert_ne!($($x)*);
+        }
+    };
+}
+
+macro_rules! debug_assert {
+    ($($x:tt)*) => {
+        {
+            #[cfg(not(feature = "defmt"))]
+            ::core::debug_assert!($($x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::debug_assert!($($x)*);
+        }
+    };
+}
+
+macro_rules! debug_assert_eq {
+    ($($x:tt)*) => {
+        {
+            #[cfg(not(feature = "defmt"))]
+            ::core::debug_assert_eq!($($x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::debug_assert_eq!($($x)*);
+        }
+    };
+}
+
+macro_rules! debug_assert_ne {
+    ($($x:tt)*) => {
+        {
+            #[cfg(not(feature = "defmt"))]
+            ::core::debug_assert_ne!($($x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::debug_assert_ne!($($x)*);
+        }
+    };
+}
+
+macro_rules! todo {
+    ($($x:tt)*) => {
+        {
+            #[cfg(not(feature = "defmt"))]
+            ::core::todo!($($x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::todo!($($x)*);
+        }
+    };
+}
+
+macro_rules! unreachable {
+    ($($x:tt)*) => {
+        {
+            #[cfg(not(feature = "defmt"))]
+            ::core::unreachable!($($x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::unreachable!($($x)*);
+        }
+    };
+}
+
+macro_rules! panic {
+    ($($x:tt)*) => {
+        {
+            #[cfg(not(feature = "defmt"))]
+            ::core::panic!($($x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::panic!($($x)*);
+        }
+    };
+}
+
+macro_rules! trace {
+    ($s:literal $(, $x:expr)* $(,)?) => {
+        {
+            #[cfg(feature = "log")]
+            ::log::trace!($s $(, $x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::trace!($s $(, $x)*);
+            #[cfg(not(any(feature = "log", feature="defmt")))]
+            let _ = ($( & $x ),*);
+        }
+    };
+}
+
+macro_rules! debug {
+    ($s:literal $(, $x:expr)* $(,)?) => {
+        {
+            #[cfg(feature = "log")]
+            ::log::debug!($s $(, $x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::debug!($s $(, $x)*);
+            #[cfg(not(any(feature = "log", feature="defmt")))]
+            let _ = ($( & $x ),*);
+        }
+    };
+}
+
+macro_rules! info {
+    ($s:literal $(, $x:expr)* $(,)?) => {
+        {
+            #[cfg(feature = "log")]
+            ::log::info!($s $(, $x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::info!($s $(, $x)*);
+            #[cfg(not(any(feature = "log", feature="defmt")))]
+            let _ = ($( & $x ),*);
+        }
+    };
+}
+
+macro_rules! warn {
+    ($s:literal $(, $x:expr)* $(,)?) => {
+        {
+            #[cfg(feature = "log")]
+            ::log::warn!($s $(, $x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::warn!($s $(, $x)*);
+            #[cfg(not(any(feature = "log", feature="defmt")))]
+            let _ = ($( & $x ),*);
+        }
+    };
+}
+
+macro_rules! error {
+    ($s:literal $(, $x:expr)* $(,)?) => {
+        {
+            #[cfg(feature = "log")]
+            ::log::error!($s $(, $x)*);
+            #[cfg(feature = "defmt")]
+            ::defmt::error!($s $(, $x)*);
+            #[cfg(not(any(feature = "log", feature="defmt")))]
+            let _ = ($( & $x ),*);
+        }
+    };
+}
+
+#[cfg(feature = "defmt")]
+macro_rules! unwrap {
+    ($($x:tt)*) => {
+        ::defmt::unwrap!($($x)*)
+    };
+}
+
+#[cfg(not(feature = "defmt"))]
+macro_rules! unwrap {
+    ($arg:expr) => {
+        match $crate::fmt::Try::into_result($arg) {
+            ::core::result::Result::Ok(t) => t,
+            ::core::result::Result::Err(e) => {
+                ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e);
+            }
+        }
+    };
+    ($arg:expr, $($msg:expr),+ $(,)? ) => {
+        match $crate::fmt::Try::into_result($arg) {
+            ::core::result::Result::Ok(t) => t,
+            ::core::result::Result::Err(e) => {
+                ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e);
+            }
+        }
+    }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub struct NoneError;
+
+pub trait Try {
+    type Ok;
+    type Error;
+    fn into_result(self) -> Result<Self::Ok, Self::Error>;
+}
+
+impl<T> Try for Option<T> {
+    type Ok = T;
+    type Error = NoneError;
+
+    #[inline]
+    fn into_result(self) -> Result<T, NoneError> {
+        self.ok_or(NoneError)
+    }
+}
+
+impl<T, E> Try for Result<T, E> {
+    type Ok = T;
+    type Error = E;
+
+    #[inline]
+    fn into_result(self) -> Self {
+        self
+    }
+}
diff --git a/embassy-usb/src/lib.rs b/embassy-usb/src/lib.rs
new file mode 100644
index 000000000..397db96c4
--- /dev/null
+++ b/embassy-usb/src/lib.rs
@@ -0,0 +1,342 @@
+#![no_std]
+#![feature(generic_associated_types)]
+
+// This mod MUST go first, so that the others see its macros.
+pub(crate) mod fmt;
+
+mod builder;
+mod control;
+pub mod descriptor;
+pub mod driver;
+pub mod types;
+
+use self::control::*;
+use self::descriptor::*;
+use self::driver::*;
+use self::types::*;
+
+pub use self::builder::Config;
+pub use self::builder::UsbDeviceBuilder;
+
+/// The global state of the USB device.
+///
+/// In general class traffic is only possible in the `Configured` state.
+#[repr(u8)]
+#[derive(PartialEq, Eq, Copy, Clone, Debug)]
+pub enum UsbDeviceState {
+    /// The USB device has just been created or reset.
+    Default,
+
+    /// The USB device has received an address from the host.
+    Addressed,
+
+    /// The USB device has been configured and is fully functional.
+    Configured,
+
+    /// The USB device has been suspended by the host or it has been unplugged from the USB bus.
+    Suspend,
+}
+
+/// The bConfiguration value for the not configured state.
+pub const CONFIGURATION_NONE: u8 = 0;
+
+/// The bConfiguration value for the single configuration supported by this device.
+pub const CONFIGURATION_VALUE: u8 = 1;
+
+/// The default value for bAlternateSetting for all interfaces.
+pub const DEFAULT_ALTERNATE_SETTING: u8 = 0;
+
+pub struct UsbDevice<'d, D: Driver<'d>> {
+    driver: D::Bus,
+    control_in: D::EndpointIn,
+    control_out: D::EndpointOut,
+
+    config: Config<'d>,
+    device_descriptor: &'d [u8],
+    config_descriptor: &'d [u8],
+    bos_descriptor: &'d [u8],
+
+    device_state: UsbDeviceState,
+    remote_wakeup_enabled: bool,
+    self_powered: bool,
+    pending_address: u8,
+}
+
+impl<'d, D: Driver<'d>> UsbDevice<'d, D> {
+    pub(crate) fn build(
+        mut driver: D,
+        config: Config<'d>,
+        device_descriptor: &'d [u8],
+        config_descriptor: &'d [u8],
+        bos_descriptor: &'d [u8],
+    ) -> Self {
+        let control_out = driver
+            .alloc_endpoint_out(
+                Some(0x00.into()),
+                EndpointType::Control,
+                config.max_packet_size_0 as u16,
+                0,
+            )
+            .expect("failed to alloc control endpoint");
+
+        let control_in = driver
+            .alloc_endpoint_in(
+                Some(0x80.into()),
+                EndpointType::Control,
+                config.max_packet_size_0 as u16,
+                0,
+            )
+            .expect("failed to alloc control endpoint");
+
+        // Enable the USB bus.
+        // This prevent further allocation by consuming the driver.
+        let driver = driver.enable();
+
+        Self {
+            driver,
+            config,
+            control_in,
+            control_out,
+            device_descriptor,
+            config_descriptor,
+            bos_descriptor,
+            device_state: UsbDeviceState::Default,
+            remote_wakeup_enabled: false,
+            self_powered: false,
+            pending_address: 0,
+        }
+    }
+
+    pub async fn run(&mut self) {
+        loop {
+            let mut buf = [0; 8];
+            let n = self.control_out.read(&mut buf).await.unwrap();
+            assert_eq!(n, 8);
+            let req = Request::parse(&buf).unwrap();
+            info!("setup request: {:x}", req);
+
+            // Now that we have properly parsed the setup packet, ensure the end-point is no longer in
+            // a stalled state.
+            self.control_out.set_stalled(false);
+
+            match req.direction {
+                UsbDirection::In => self.handle_control_in(req).await,
+                UsbDirection::Out => self.handle_control_out(req).await,
+            }
+        }
+    }
+
+    async fn write_chunked(&mut self, data: &[u8]) -> Result<(), driver::WriteError> {
+        for c in data.chunks(8) {
+            self.control_in.write(c).await?;
+        }
+        if data.len() % 8 == 0 {
+            self.control_in.write(&[]).await?;
+        }
+        Ok(())
+    }
+
+    async fn control_out_accept(&mut self, req: Request) {
+        info!("control out accept");
+        // status phase
+        // todo: cleanup
+        self.control_out.read(&mut []).await.unwrap();
+    }
+
+    async fn control_in_accept(&mut self, req: Request, data: &[u8]) {
+        info!("control accept {:x}", data);
+
+        let len = data.len().min(req.length as _);
+        if let Err(e) = self.write_chunked(&data[..len]).await {
+            info!("write_chunked failed: {:?}", e);
+        }
+
+        // status phase
+        // todo: cleanup
+        self.control_out.read(&mut []).await.unwrap();
+    }
+
+    async fn control_in_accept_writer(
+        &mut self,
+        req: Request,
+        f: impl FnOnce(&mut DescriptorWriter),
+    ) {
+        let mut buf = [0; 256];
+        let mut w = DescriptorWriter::new(&mut buf);
+        f(&mut w);
+        let pos = w.position();
+        self.control_in_accept(req, &buf[..pos]).await;
+    }
+
+    fn control_reject(&mut self, req: Request) {
+        self.control_out.set_stalled(true);
+    }
+
+    async fn handle_control_out(&mut self, req: Request) {
+        // TODO actually read the data if there's an OUT data phase.
+
+        const CONFIGURATION_NONE_U16: u16 = CONFIGURATION_NONE as u16;
+        const CONFIGURATION_VALUE_U16: u16 = CONFIGURATION_VALUE as u16;
+        const DEFAULT_ALTERNATE_SETTING_U16: u16 = DEFAULT_ALTERNATE_SETTING as u16;
+
+        match req.request_type {
+            RequestType::Standard => match (req.recipient, req.request, req.value) {
+                (
+                    Recipient::Device,
+                    Request::CLEAR_FEATURE,
+                    Request::FEATURE_DEVICE_REMOTE_WAKEUP,
+                ) => {
+                    self.remote_wakeup_enabled = false;
+                    self.control_out_accept(req).await;
+                }
+
+                (Recipient::Endpoint, Request::CLEAR_FEATURE, Request::FEATURE_ENDPOINT_HALT) => {
+                    //self.bus.set_stalled(((req.index as u8) & 0x8f).into(), false);
+                    self.control_out_accept(req).await;
+                }
+
+                (
+                    Recipient::Device,
+                    Request::SET_FEATURE,
+                    Request::FEATURE_DEVICE_REMOTE_WAKEUP,
+                ) => {
+                    self.remote_wakeup_enabled = true;
+                    self.control_out_accept(req).await;
+                }
+
+                (Recipient::Endpoint, Request::SET_FEATURE, Request::FEATURE_ENDPOINT_HALT) => {
+                    //self.bus.set_stalled(((req.index as u8) & 0x8f).into(), true);
+                    self.control_out_accept(req).await;
+                }
+
+                (Recipient::Device, Request::SET_ADDRESS, 1..=127) => {
+                    self.pending_address = req.value as u8;
+
+                    // on NRF the hardware auto-handles SET_ADDRESS.
+                    self.control_out_accept(req).await;
+                }
+
+                (Recipient::Device, Request::SET_CONFIGURATION, CONFIGURATION_VALUE_U16) => {
+                    self.device_state = UsbDeviceState::Configured;
+                    self.control_out_accept(req).await;
+                }
+
+                (Recipient::Device, Request::SET_CONFIGURATION, CONFIGURATION_NONE_U16) => {
+                    match self.device_state {
+                        UsbDeviceState::Default => {
+                            self.control_out_accept(req).await;
+                        }
+                        _ => {
+                            self.device_state = UsbDeviceState::Addressed;
+                            self.control_out_accept(req).await;
+                        }
+                    }
+                }
+
+                (Recipient::Interface, Request::SET_INTERFACE, DEFAULT_ALTERNATE_SETTING_U16) => {
+                    // TODO: do something when alternate settings are implemented
+                    self.control_out_accept(req).await;
+                }
+
+                _ => self.control_reject(req),
+            },
+            _ => self.control_reject(req),
+        }
+    }
+
+    async fn handle_control_in(&mut self, req: Request) {
+        match req.request_type {
+            RequestType::Standard => match (req.recipient, req.request) {
+                (Recipient::Device, Request::GET_STATUS) => {
+                    let mut status: u16 = 0x0000;
+                    if self.self_powered {
+                        status |= 0x0001;
+                    }
+                    if self.remote_wakeup_enabled {
+                        status |= 0x0002;
+                    }
+                    self.control_in_accept(req, &status.to_le_bytes()).await;
+                }
+
+                (Recipient::Interface, Request::GET_STATUS) => {
+                    let status: u16 = 0x0000;
+                    self.control_in_accept(req, &status.to_le_bytes()).await;
+                }
+
+                (Recipient::Endpoint, Request::GET_STATUS) => {
+                    let ep_addr: EndpointAddress = ((req.index as u8) & 0x8f).into();
+                    let mut status: u16 = 0x0000;
+                    if self.driver.is_stalled(ep_addr) {
+                        status |= 0x0001;
+                    }
+                    self.control_in_accept(req, &status.to_le_bytes()).await;
+                }
+
+                (Recipient::Device, Request::GET_DESCRIPTOR) => {
+                    self.handle_get_descriptor(req).await;
+                }
+
+                (Recipient::Device, Request::GET_CONFIGURATION) => {
+                    let status = match self.device_state {
+                        UsbDeviceState::Configured => CONFIGURATION_VALUE,
+                        _ => CONFIGURATION_NONE,
+                    };
+                    self.control_in_accept(req, &status.to_le_bytes()).await;
+                }
+
+                (Recipient::Interface, Request::GET_INTERFACE) => {
+                    // TODO: change when alternate settings are implemented
+                    let status = DEFAULT_ALTERNATE_SETTING;
+                    self.control_in_accept(req, &status.to_le_bytes()).await;
+                }
+                _ => self.control_reject(req),
+            },
+            _ => self.control_reject(req),
+        }
+    }
+
+    async fn handle_get_descriptor(&mut self, req: Request) {
+        let (dtype, index) = req.descriptor_type_index();
+        let config = self.config.clone();
+
+        match dtype {
+            descriptor_type::BOS => self.control_in_accept(req, self.bos_descriptor).await,
+            descriptor_type::DEVICE => self.control_in_accept(req, self.device_descriptor).await,
+            descriptor_type::CONFIGURATION => {
+                self.control_in_accept(req, self.config_descriptor).await
+            }
+            descriptor_type::STRING => {
+                if index == 0 {
+                    self.control_in_accept_writer(req, |w| {
+                        w.write(descriptor_type::STRING, &lang_id::ENGLISH_US.to_le_bytes())
+                            .unwrap();
+                    })
+                    .await
+                } else {
+                    let s = match index {
+                        1 => self.config.manufacturer,
+                        2 => self.config.product,
+                        3 => self.config.serial_number,
+                        _ => {
+                            let index = StringIndex::new(index);
+                            let lang_id = req.index;
+                            None
+                            //classes
+                            //    .iter()
+                            //    .filter_map(|cls| cls.get_string(index, lang_id))
+                            //    .nth(0)
+                        }
+                    };
+
+                    if let Some(s) = s {
+                        self.control_in_accept_writer(req, |w| w.string(s).unwrap())
+                            .await;
+                    } else {
+                        self.control_reject(req)
+                    }
+                }
+            }
+            _ => self.control_reject(req),
+        }
+    }
+}
diff --git a/embassy-usb/src/types.rs b/embassy-usb/src/types.rs
new file mode 100644
index 000000000..9d00e46cb
--- /dev/null
+++ b/embassy-usb/src/types.rs
@@ -0,0 +1,138 @@
+/// Direction of USB traffic. Note that in the USB standard the direction is always indicated from
+/// the perspective of the host, which is backward for devices, but the standard directions are used
+/// for consistency.
+///
+/// The values of the enum also match the direction bit used in endpoint addresses and control
+/// request types.
+#[repr(u8)]
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub enum UsbDirection {
+    /// Host to device (OUT)
+    Out = 0x00,
+    /// Device to host (IN)
+    In = 0x80,
+}
+
+impl From<u8> for UsbDirection {
+    fn from(value: u8) -> Self {
+        unsafe { core::mem::transmute(value & 0x80) }
+    }
+}
+
+/// USB endpoint transfer type. The values of this enum can be directly cast into `u8` to get the
+/// transfer bmAttributes transfer type bits.
+#[repr(u8)]
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub enum EndpointType {
+    /// Control endpoint. Used for device management. Only the host can initiate requests. Usually
+    /// used only endpoint 0.
+    Control = 0b00,
+    /// Isochronous endpoint. Used for time-critical unreliable data. Not implemented yet.
+    Isochronous = 0b01,
+    /// Bulk endpoint. Used for large amounts of best-effort reliable data.
+    Bulk = 0b10,
+    /// Interrupt endpoint. Used for small amounts of time-critical reliable data.
+    Interrupt = 0b11,
+}
+
+/// Type-safe endpoint address.
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct EndpointAddress(u8);
+
+impl From<u8> for EndpointAddress {
+    #[inline]
+    fn from(addr: u8) -> EndpointAddress {
+        EndpointAddress(addr)
+    }
+}
+
+impl From<EndpointAddress> for u8 {
+    #[inline]
+    fn from(addr: EndpointAddress) -> u8 {
+        addr.0
+    }
+}
+
+impl EndpointAddress {
+    const INBITS: u8 = UsbDirection::In as u8;
+
+    /// Constructs a new EndpointAddress with the given index and direction.
+    #[inline]
+    pub fn from_parts(index: usize, dir: UsbDirection) -> Self {
+        EndpointAddress(index as u8 | dir as u8)
+    }
+
+    /// Gets the direction part of the address.
+    #[inline]
+    pub fn direction(&self) -> UsbDirection {
+        if (self.0 & Self::INBITS) != 0 {
+            UsbDirection::In
+        } else {
+            UsbDirection::Out
+        }
+    }
+
+    /// Returns true if the direction is IN, otherwise false.
+    #[inline]
+    pub fn is_in(&self) -> bool {
+        (self.0 & Self::INBITS) != 0
+    }
+
+    /// Returns true if the direction is OUT, otherwise false.
+    #[inline]
+    pub fn is_out(&self) -> bool {
+        (self.0 & Self::INBITS) == 0
+    }
+
+    /// Gets the index part of the endpoint address.
+    #[inline]
+    pub fn index(&self) -> usize {
+        (self.0 & !Self::INBITS) as usize
+    }
+}
+
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct EndpointInfo {
+    pub addr: EndpointAddress,
+    pub ep_type: EndpointType,
+    pub max_packet_size: u16,
+    pub interval: u8,
+}
+
+/// A handle for a USB interface that contains its number.
+#[derive(Copy, Clone, Eq, PartialEq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InterfaceNumber(u8);
+
+impl InterfaceNumber {
+    pub(crate) fn new(index: u8) -> InterfaceNumber {
+        InterfaceNumber(index)
+    }
+}
+
+impl From<InterfaceNumber> for u8 {
+    fn from(n: InterfaceNumber) -> u8 {
+        n.0
+    }
+}
+
+/// A handle for a USB string descriptor that contains its index.
+#[derive(Copy, Clone, Eq, PartialEq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct StringIndex(u8);
+
+impl StringIndex {
+    pub(crate) fn new(index: u8) -> StringIndex {
+        StringIndex(index)
+    }
+}
+
+impl From<StringIndex> for u8 {
+    fn from(i: StringIndex) -> u8 {
+        i.0
+    }
+}
diff --git a/examples/nrf/Cargo.toml b/examples/nrf/Cargo.toml
index a704eb3bc..fb846b3a9 100644
--- a/examples/nrf/Cargo.toml
+++ b/examples/nrf/Cargo.toml
@@ -10,7 +10,8 @@ nightly = ["embassy-nrf/nightly", "embassy-nrf/unstable-traits"]
 
 [dependencies]
 embassy = { version = "0.1.0", path = "../../embassy", features = ["defmt", "defmt-timestamp-uptime"] }
-embassy-nrf = { version = "0.1.0", path = "../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote"] }
+embassy-nrf = { version = "0.1.0", path = "../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote", "unstable-pac"] }
+embassy-usb = { version = "0.1.0", path = "../../embassy-usb", features = ["defmt"] }
 
 defmt = "0.3"
 defmt-rtt = "0.3"
@@ -22,5 +23,3 @@ futures = { version = "0.3.17", default-features = false, features = ["async-awa
 rand = { version = "0.8.4", default-features = false }
 embedded-storage = "0.3.0"
 
-usb-device = "0.2"
-usbd-serial = "0.1.1"
diff --git a/examples/nrf/src/bin/usb/cdc_acm.rs b/examples/nrf/src/bin/usb/cdc_acm.rs
new file mode 100644
index 000000000..345d00389
--- /dev/null
+++ b/examples/nrf/src/bin/usb/cdc_acm.rs
@@ -0,0 +1,356 @@
+use core::convert::TryInto;
+use core::mem;
+use embassy_usb::driver::{Endpoint, EndpointIn, EndpointOut, ReadError, WriteError};
+use embassy_usb::{driver::Driver, types::*, UsbDeviceBuilder};
+
+/// This should be used as `device_class` when building the `UsbDevice`.
+pub const USB_CLASS_CDC: u8 = 0x02;
+
+const USB_CLASS_CDC_DATA: u8 = 0x0a;
+const CDC_SUBCLASS_ACM: u8 = 0x02;
+const CDC_PROTOCOL_NONE: u8 = 0x00;
+
+const CS_INTERFACE: u8 = 0x24;
+const CDC_TYPE_HEADER: u8 = 0x00;
+const CDC_TYPE_CALL_MANAGEMENT: u8 = 0x01;
+const CDC_TYPE_ACM: u8 = 0x02;
+const CDC_TYPE_UNION: u8 = 0x06;
+
+const REQ_SEND_ENCAPSULATED_COMMAND: u8 = 0x00;
+#[allow(unused)]
+const REQ_GET_ENCAPSULATED_COMMAND: u8 = 0x01;
+const REQ_SET_LINE_CODING: u8 = 0x20;
+const REQ_GET_LINE_CODING: u8 = 0x21;
+const REQ_SET_CONTROL_LINE_STATE: u8 = 0x22;
+
+/// Packet level implementation of a CDC-ACM serial port.
+///
+/// This class can be used directly and it has the least overhead due to directly reading and
+/// writing USB packets with no intermediate buffers, but it will not act like a stream-like serial
+/// port. The following constraints must be followed if you use this class directly:
+///
+/// - `read_packet` must be called with a buffer large enough to hold max_packet_size bytes, and the
+///   method will return a `WouldBlock` error if there is no packet to be read.
+/// - `write_packet` must not be called with a buffer larger than max_packet_size bytes, and the
+///   method will return a `WouldBlock` error if the previous packet has not been sent yet.
+/// - If you write a packet that is exactly max_packet_size bytes long, it won't be processed by the
+///   host operating system until a subsequent shorter packet is sent. A zero-length packet (ZLP)
+///   can be sent if there is no other data to send. This is because USB bulk transactions must be
+///   terminated with a short packet, even if the bulk endpoint is used for stream-like data.
+pub struct CdcAcmClass<'d, D: Driver<'d>> {
+    comm_if: InterfaceNumber,
+    comm_ep: D::EndpointIn,
+    data_if: InterfaceNumber,
+    read_ep: D::EndpointOut,
+    write_ep: D::EndpointIn,
+    line_coding: LineCoding,
+    dtr: bool,
+    rts: bool,
+}
+
+impl<'d, D: Driver<'d>> CdcAcmClass<'d, D> {
+    /// Creates a new CdcAcmClass with the provided UsbBus and max_packet_size in bytes. For
+    /// full-speed devices, max_packet_size has to be one of 8, 16, 32 or 64.
+    pub fn new(builder: &mut UsbDeviceBuilder<'d, D>, max_packet_size: u16) -> Self {
+        let comm_if = builder.alloc_interface();
+        let comm_ep = builder.alloc_interrupt_endpoint_in(8, 255);
+        let data_if = builder.alloc_interface();
+        let read_ep = builder.alloc_bulk_endpoint_out(max_packet_size);
+        let write_ep = builder.alloc_bulk_endpoint_in(max_packet_size);
+
+        builder
+            .config_descriptor
+            .iad(
+                comm_if,
+                2,
+                USB_CLASS_CDC,
+                CDC_SUBCLASS_ACM,
+                CDC_PROTOCOL_NONE,
+            )
+            .unwrap();
+
+        builder
+            .config_descriptor
+            .interface(comm_if, USB_CLASS_CDC, CDC_SUBCLASS_ACM, CDC_PROTOCOL_NONE)
+            .unwrap();
+
+        builder
+            .config_descriptor
+            .write(
+                CS_INTERFACE,
+                &[
+                    CDC_TYPE_HEADER, // bDescriptorSubtype
+                    0x10,
+                    0x01, // bcdCDC (1.10)
+                ],
+            )
+            .unwrap();
+
+        builder
+            .config_descriptor
+            .write(
+                CS_INTERFACE,
+                &[
+                    CDC_TYPE_ACM, // bDescriptorSubtype
+                    0x00,         // bmCapabilities
+                ],
+            )
+            .unwrap();
+
+        builder
+            .config_descriptor
+            .write(
+                CS_INTERFACE,
+                &[
+                    CDC_TYPE_UNION, // bDescriptorSubtype
+                    comm_if.into(), // bControlInterface
+                    data_if.into(), // bSubordinateInterface
+                ],
+            )
+            .unwrap();
+
+        builder
+            .config_descriptor
+            .write(
+                CS_INTERFACE,
+                &[
+                    CDC_TYPE_CALL_MANAGEMENT, // bDescriptorSubtype
+                    0x00,                     // bmCapabilities
+                    data_if.into(),           // bDataInterface
+                ],
+            )
+            .unwrap();
+
+        builder.config_descriptor.endpoint(comm_ep.info()).unwrap();
+
+        builder
+            .config_descriptor
+            .interface(data_if, USB_CLASS_CDC_DATA, 0x00, 0x00)
+            .unwrap();
+
+        builder.config_descriptor.endpoint(write_ep.info()).unwrap();
+        builder.config_descriptor.endpoint(read_ep.info()).unwrap();
+
+        CdcAcmClass {
+            comm_if,
+            comm_ep,
+            data_if,
+            read_ep,
+            write_ep,
+            line_coding: LineCoding {
+                stop_bits: StopBits::One,
+                data_bits: 8,
+                parity_type: ParityType::None,
+                data_rate: 8_000,
+            },
+            dtr: false,
+            rts: false,
+        }
+    }
+
+    /// Gets the maximum packet size in bytes.
+    pub fn max_packet_size(&self) -> u16 {
+        // The size is the same for both endpoints.
+        self.read_ep.info().max_packet_size
+    }
+
+    /// Gets the current line coding. The line coding contains information that's mainly relevant
+    /// for USB to UART serial port emulators, and can be ignored if not relevant.
+    pub fn line_coding(&self) -> &LineCoding {
+        &self.line_coding
+    }
+
+    /// Gets the DTR (data terminal ready) state
+    pub fn dtr(&self) -> bool {
+        self.dtr
+    }
+
+    /// Gets the RTS (request to send) state
+    pub fn rts(&self) -> bool {
+        self.rts
+    }
+
+    /// Writes a single packet into the IN endpoint.
+    pub async fn write_packet(&mut self, data: &[u8]) -> Result<(), WriteError> {
+        self.write_ep.write(data).await
+    }
+
+    /// Reads a single packet from the OUT endpoint.
+    pub async fn read_packet(&mut self, data: &mut [u8]) -> Result<usize, ReadError> {
+        self.read_ep.read(data).await
+    }
+
+    /// Gets the address of the IN endpoint.
+    pub(crate) fn write_ep_address(&self) -> EndpointAddress {
+        self.write_ep.info().addr
+    }
+}
+
+/*
+impl<B: UsbBus> UsbClass<B> for CdcAcmClass<'_, B> {
+    fn get_configuration_descriptors(&self, builder.config_descriptor: &mut Descriptorbuilder.config_descriptor) -> Result<()> {
+
+        Ok(())
+    }
+
+    fn reset(&mut self) {
+        self.line_coding = LineCoding::default();
+        self.dtr = false;
+        self.rts = false;
+    }
+
+    fn control_in(&mut self, xfer: ControlIn<B>) {
+        let req = xfer.request();
+
+        if !(req.request_type == control::RequestType::Class
+            && req.recipient == control::Recipient::Interface
+            && req.index == u8::from(self.comm_if) as u16)
+        {
+            return;
+        }
+
+        match req.request {
+            // REQ_GET_ENCAPSULATED_COMMAND is not really supported - it will be rejected below.
+            REQ_GET_LINE_CODING if req.length == 7 => {
+                xfer.accept(|data| {
+                    data[0..4].copy_from_slice(&self.line_coding.data_rate.to_le_bytes());
+                    data[4] = self.line_coding.stop_bits as u8;
+                    data[5] = self.line_coding.parity_type as u8;
+                    data[6] = self.line_coding.data_bits;
+
+                    Ok(7)
+                })
+                .ok();
+            }
+            _ => {
+                xfer.reject().ok();
+            }
+        }
+    }
+
+    fn control_out(&mut self, xfer: ControlOut<B>) {
+        let req = xfer.request();
+
+        if !(req.request_type == control::RequestType::Class
+            && req.recipient == control::Recipient::Interface
+            && req.index == u8::from(self.comm_if) as u16)
+        {
+            return;
+        }
+
+        match req.request {
+            REQ_SEND_ENCAPSULATED_COMMAND => {
+                // We don't actually support encapsulated commands but pretend we do for standards
+                // compatibility.
+                xfer.accept().ok();
+            }
+            REQ_SET_LINE_CODING if xfer.data().len() >= 7 => {
+                self.line_coding.data_rate =
+                    u32::from_le_bytes(xfer.data()[0..4].try_into().unwrap());
+                self.line_coding.stop_bits = xfer.data()[4].into();
+                self.line_coding.parity_type = xfer.data()[5].into();
+                self.line_coding.data_bits = xfer.data()[6];
+
+                xfer.accept().ok();
+            }
+            REQ_SET_CONTROL_LINE_STATE => {
+                self.dtr = (req.value & 0x0001) != 0;
+                self.rts = (req.value & 0x0002) != 0;
+
+                xfer.accept().ok();
+            }
+            _ => {
+                xfer.reject().ok();
+            }
+        };
+    }
+}
+
+ */
+
+/// Number of stop bits for LineCoding
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum StopBits {
+    /// 1 stop bit
+    One = 0,
+
+    /// 1.5 stop bits
+    OnePointFive = 1,
+
+    /// 2 stop bits
+    Two = 2,
+}
+
+impl From<u8> for StopBits {
+    fn from(value: u8) -> Self {
+        if value <= 2 {
+            unsafe { mem::transmute(value) }
+        } else {
+            StopBits::One
+        }
+    }
+}
+
+/// Parity for LineCoding
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum ParityType {
+    None = 0,
+    Odd = 1,
+    Event = 2,
+    Mark = 3,
+    Space = 4,
+}
+
+impl From<u8> for ParityType {
+    fn from(value: u8) -> Self {
+        if value <= 4 {
+            unsafe { mem::transmute(value) }
+        } else {
+            ParityType::None
+        }
+    }
+}
+
+/// Line coding parameters
+///
+/// This is provided by the host for specifying the standard UART parameters such as baud rate. Can
+/// be ignored if you don't plan to interface with a physical UART.
+pub struct LineCoding {
+    stop_bits: StopBits,
+    data_bits: u8,
+    parity_type: ParityType,
+    data_rate: u32,
+}
+
+impl LineCoding {
+    /// Gets the number of stop bits for UART communication.
+    pub fn stop_bits(&self) -> StopBits {
+        self.stop_bits
+    }
+
+    /// Gets the number of data bits for UART communication.
+    pub fn data_bits(&self) -> u8 {
+        self.data_bits
+    }
+
+    /// Gets the parity type for UART communication.
+    pub fn parity_type(&self) -> ParityType {
+        self.parity_type
+    }
+
+    /// Gets the data rate in bits per second for UART communication.
+    pub fn data_rate(&self) -> u32 {
+        self.data_rate
+    }
+}
+
+impl Default for LineCoding {
+    fn default() -> Self {
+        LineCoding {
+            stop_bits: StopBits::One,
+            data_bits: 8,
+            parity_type: ParityType::None,
+            data_rate: 8_000,
+        }
+    }
+}
diff --git a/examples/nrf/src/bin/usb/main.rs b/examples/nrf/src/bin/usb/main.rs
new file mode 100644
index 000000000..21ca2ba4f
--- /dev/null
+++ b/examples/nrf/src/bin/usb/main.rs
@@ -0,0 +1,53 @@
+#![no_std]
+#![no_main]
+#![feature(type_alias_impl_trait)]
+
+#[path = "../../example_common.rs"]
+mod example_common;
+
+mod cdc_acm;
+
+use core::mem;
+use defmt::*;
+use embassy::executor::Spawner;
+use embassy_nrf::interrupt;
+use embassy_nrf::pac;
+use embassy_nrf::usb::{self, Driver};
+use embassy_nrf::Peripherals;
+use embassy_usb::{Config, UsbDeviceBuilder};
+
+use crate::cdc_acm::CdcAcmClass;
+
+#[embassy::main]
+async fn main(_spawner: Spawner, p: Peripherals) {
+    let clock: pac::CLOCK = unsafe { mem::transmute(()) };
+    let power: pac::POWER = unsafe { mem::transmute(()) };
+
+    info!("Enabling ext hfosc...");
+    clock.tasks_hfclkstart.write(|w| unsafe { w.bits(1) });
+    while clock.events_hfclkstarted.read().bits() != 1 {}
+
+    info!("Waiting for vbus...");
+    while !power.usbregstatus.read().vbusdetect().is_vbus_present() {}
+    info!("vbus OK");
+
+    let irq = interrupt::take!(USBD);
+    let driver = Driver::new(p.USBD, irq);
+    let config = Config::new(0xc0de, 0xcafe);
+    let mut device_descriptor = [0; 256];
+    let mut config_descriptor = [0; 256];
+    let mut bos_descriptor = [0; 256];
+
+    let mut builder = UsbDeviceBuilder::new(
+        driver,
+        config,
+        &mut device_descriptor,
+        &mut config_descriptor,
+        &mut bos_descriptor,
+    );
+
+    let mut class = CdcAcmClass::new(&mut builder, 64);
+
+    let mut usb = builder.build();
+    usb.run().await;
+}
diff --git a/examples/nrf/src/bin/usb_uart.rs b/examples/nrf/src/bin/usb_uart.rs
deleted file mode 100644
index d283dccd1..000000000
--- a/examples/nrf/src/bin/usb_uart.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-#![no_std]
-#![no_main]
-#![feature(type_alias_impl_trait)]
-
-use defmt::{info, unwrap};
-use embassy::executor::Spawner;
-use embassy::interrupt::InterruptExt;
-use embassy::io::{AsyncBufReadExt, AsyncWriteExt};
-use embassy_nrf::usb::{State, Usb, UsbBus, UsbSerial};
-use embassy_nrf::{interrupt, Peripherals};
-use futures::pin_mut;
-use usb_device::device::{UsbDeviceBuilder, UsbVidPid};
-
-use defmt_rtt as _; // global logger
-use panic_probe as _; // print out panic messages
-
-#[embassy::main]
-async fn main(_spawner: Spawner, p: Peripherals) {
-    let mut rx_buffer = [0u8; 64];
-    // we send back input + cr + lf
-    let mut tx_buffer = [0u8; 66];
-
-    let usb_bus = UsbBus::new(p.USBD);
-
-    let serial = UsbSerial::new(&usb_bus, &mut rx_buffer, &mut tx_buffer);
-
-    let device = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd))
-        .manufacturer("Fake company")
-        .product("Serial port")
-        .serial_number("TEST")
-        .device_class(0x02)
-        .build();
-
-    let irq = interrupt::take!(USBD);
-    irq.set_priority(interrupt::Priority::P3);
-
-    let mut state = State::new();
-    let usb = unsafe { Usb::new(&mut state, device, serial, irq) };
-    pin_mut!(usb);
-
-    let (mut reader, mut writer) = usb.as_ref().take_serial_0();
-
-    info!("usb initialized!");
-
-    unwrap!(
-        writer
-            .write_all(b"\r\nInput returned upper cased on CR+LF\r\n")
-            .await
-    );
-
-    let mut buf = [0u8; 64];
-    loop {
-        let mut n = 0;
-
-        async {
-            loop {
-                let char = unwrap!(reader.read_byte().await);
-
-                // throw away, read more on cr, exit on lf
-                if char == b'\r' {
-                    continue;
-                } else if char == b'\n' {
-                    break;
-                }
-
-                buf[n] = char;
-                n += 1;
-
-                // stop if we're out of room
-                if n == buf.len() {
-                    break;
-                }
-            }
-        }
-        .await;
-
-        if n > 0 {
-            for char in buf[..n].iter_mut() {
-                // upper case
-                if 0x61 <= *char && *char <= 0x7a {
-                    *char &= !0x20;
-                }
-            }
-            unwrap!(writer.write_all(&buf[..n]).await);
-            unwrap!(writer.write_all(b"\r\n").await);
-            unwrap!(writer.flush().await);
-        }
-    }
-}
diff --git a/examples/nrf/src/bin/usb_uart_io.rs b/examples/nrf/src/bin/usb_uart_io.rs
deleted file mode 100644
index ef2629844..000000000
--- a/examples/nrf/src/bin/usb_uart_io.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-#![no_std]
-#![no_main]
-#![feature(type_alias_impl_trait)]
-
-use defmt::{info, unwrap};
-use embassy::executor::Spawner;
-use embassy::interrupt::InterruptExt;
-use embassy::io::{read_line, AsyncWriteExt};
-use embassy_nrf::usb::{State, Usb, UsbBus, UsbSerial};
-use embassy_nrf::{interrupt, Peripherals};
-use futures::pin_mut;
-use usb_device::device::{UsbDeviceBuilder, UsbVidPid};
-
-use defmt_rtt as _; // global logger
-use panic_probe as _; // print out panic messages
-
-#[embassy::main]
-async fn main(_spawner: Spawner, p: Peripherals) {
-    let mut rx_buffer = [0u8; 64];
-    // we send back input + cr + lf
-    let mut tx_buffer = [0u8; 66];
-
-    let usb_bus = UsbBus::new(p.USBD);
-
-    let serial = UsbSerial::new(&usb_bus, &mut rx_buffer, &mut tx_buffer);
-
-    let device = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd))
-        .manufacturer("Fake company")
-        .product("Serial port")
-        .serial_number("TEST")
-        .device_class(0x02)
-        .build();
-
-    let irq = interrupt::take!(USBD);
-    irq.set_priority(interrupt::Priority::P3);
-
-    let mut state = State::new();
-    let usb = unsafe { Usb::new(&mut state, device, serial, irq) };
-    pin_mut!(usb);
-
-    let (mut reader, mut writer) = usb.as_ref().take_serial_0();
-
-    info!("usb initialized!");
-
-    unwrap!(
-        writer
-            .write_all(b"\r\nInput returned upper cased on CR+LF\r\n")
-            .await
-    );
-
-    let mut buf = [0u8; 64];
-    loop {
-        let n = unwrap!(read_line(&mut reader, &mut buf).await);
-
-        for char in buf[..n].iter_mut() {
-            // upper case
-            if 0x61 <= *char && *char <= 0x7a {
-                *char &= !0x20;
-            }
-        }
-
-        unwrap!(writer.write_all(&buf[..n]).await);
-        unwrap!(writer.write_all(b"\r\n").await);
-        unwrap!(writer.flush().await);
-    }
-}