usb: initial bulk-only transport implementation

This commit is contained in:
chemicstry 2023-01-20 21:48:40 +02:00
parent 825b67101b
commit 7515369984
10 changed files with 675 additions and 0 deletions

View File

@ -1,3 +1,4 @@
pub mod cdc_acm; pub mod cdc_acm;
pub mod cdc_ncm; pub mod cdc_ncm;
pub mod hid; pub mod hid;
pub mod msc;

View File

@ -0,0 +1,57 @@
pub mod subclass;
pub mod transport;
use core::marker::PhantomData;
use crate::driver::Driver;
use crate::types::InterfaceNumber;
use crate::Builder;
/// USB Mass Storage Class ID
///
/// Section 4.3 [USB Bulk Only Transport Spec](https://www.usb.org/document-library/mass-storage-bulk-only-10)
pub const USB_CLASS_MSC: u8 = 0x08;
/// Command set used by the MSC interface.
///
/// Reported in `bInterfaceSubclass` field.
///
/// Section 2 [USB Mass Storage Class Overview](https://www.usb.org/document-library/mass-storage-class-specification-overview-14)
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum MscSubclass {
/// SCSI command set not reported. De facto use
ScsiCommandSetNotReported = 0x00,
/// Allocated by USB-IF for RBC. RBC is defined outside of USB
Rbc = 0x01,
/// Allocated by USB-IF for MMC-5. MMC-5 is defined outside of USB
Mmc5Atapi = 0x02,
/// Specifies how to interface Floppy Disk Drives to USB
Ufi = 0x04,
/// Allocated by USB-IF for SCSI. SCSI standards are defined outside of USB
ScsiTransparentCommandSet = 0x06,
/// LSDFS specifies how host has to negotiate access before trying SCSI
LsdFs = 0x07,
/// Allocated by USB-IF for IEEE 1667. IEEE 1667 is defined outside of USB
Ieee1667 = 0x08,
/// Specific to device vendor. De facto use
VendorSpecific = 0xFF,
}
/// Transport protocol of the MSC interface.
///
/// Reported in `bInterfaceProtocol` field.
///
/// Section 3 [USB Mass Storage Class Overview](https://www.usb.org/document-library/mass-storage-class-specification-overview-14)
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum MscProtocol {
/// USB Mass Storage Class Control/Bulk/Interrupt (CBI) Transport (with command completion interrupt)
CbiWithCCInterrupt = 0x00,
/// USB Mass Storage Class Control/Bulk/Interrupt (CBI) Transport (with no command completion interrupt)
CbiNoCCInterrupt = 0x01,
/// USB Mass Storage Class Bulk-Only (BBB) Transport
BulkOnlyTransport = 0x50,
/// Allocated by USB-IF for UAS. UAS is defined outside of USB
Uas = 0x62,
/// Specific to device vendor. De facto use
VendorSpecific = 0xFF,
}

View File

@ -0,0 +1 @@
pub mod scsi;

View File

@ -0,0 +1,88 @@
use core::mem::{size_of, size_of_val};
use embassy_usb_driver::Direction;
/// Signature that identifies this packet as CBW
pub const CBW_SIGNATURE: u32 = 0x43425355;
/// A wrapper that identifies a command sent from the host to the
/// device on the OUT endpoint. Describes the data transfer IN or OUT
/// that should happen immediatly after this wrapper is received.
/// Little Endian
#[repr(packed)]
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct CommandBlockWrapper {
/// Signature that identifies this packet as CBW
/// Must contain 0x43425355
pub signature: u32,
/// Tag sent by the host. Must be echoed back to host in tag
/// field of the command status wrapper sent after the command
/// has been executed/rejected. Host uses it to positively
/// associate a CSW with the corresponding CBW
pub tag: u32,
/// Number of bytes of data that the host expects to receive on
/// the IN or OUT endpoint (as indicated by the direction field)
/// during the execution of this command. If this field is zero,
/// must respond directly with CSW
pub data_transfer_length: u32,
/// Direction of transfer initiated by this command.
/// 0b0XXXXXXX = OUT from host to device
/// 0b1XXXXXXX = IN from device to host
/// X bits are obsolete or reserved
pub direction: u8,
/// The device Logical Unit Number (LUN) to which the command is
/// for. For devices that don't support multiple LUNs the host will
/// set this field to zero.
/// Devices that don't support multiple LUNS must not ignore this
/// field and apply all commands to LUN 0, [see General Problems with Commands](http://janaxelson.com/device_errors.htm)
pub lun: u8,
/// The number of valid bytes in data field
pub data_length: u8,
/// The command set specific data for this command
pub data: [u8; 16],
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum CommandBlockWrapperDeserializeError {
BufferTooShort,
InvalidSignature,
InvalidDirection,
InvalidDataLength,
}
impl CommandBlockWrapper {
pub fn from_bytes(buf: &[u8]) -> Result<CommandBlockWrapper, CommandBlockWrapperDeserializeError> {
if buf.len() < size_of::<Self>() {
return Err(CommandBlockWrapperDeserializeError::BufferTooShort);
}
let cbw = unsafe { core::ptr::read(buf.as_ptr() as *const Self) };
if cbw.signature != CBW_SIGNATURE {
return Err(CommandBlockWrapperDeserializeError::InvalidSignature);
}
if cbw.direction & 0b01111111 != 0 {
return Err(CommandBlockWrapperDeserializeError::InvalidDirection);
}
if cbw.data_length as usize > size_of_val(&cbw.data) {
return Err(CommandBlockWrapperDeserializeError::InvalidDataLength);
}
Ok(cbw)
}
pub fn dir(&self) -> Direction {
if self.direction == 0x80 {
Direction::In
} else {
Direction::Out
}
}
pub fn data(&self) -> &[u8] {
&self.data[..self.data_length as usize]
}
}

View File

@ -0,0 +1,66 @@
use core::mem::size_of;
use core::ptr::copy_nonoverlapping;
/// Signature that identifies this packet as CSW
pub const CSW_SIGNATURE: u32 = 0x53425355;
/// The status of a command
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum CommandStatus {
/// Ok, command completed successfully
CommandOk = 0x00,
/// Error, command failed
CommandError = 0x01,
/// Fatal device error, reset required
PhaseError = 0x02,
}
/// A wrapper that identifies a command sent from the host to the
/// device on the OUT endpoint. Describes the data transfer IN or OUT
/// that should happen immediatly after this wrapper is received.
/// Little Endian
#[repr(packed)]
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub struct CommandStatusWrapper {
/// Signature that identifies this packet as CSW
/// Must contain 0x53425355
pub signature: u32,
/// Tag that matches this CSW back to the CBW that initiated it.
/// Must be copied from CBW tag field. Host uses it to positively
/// associate a CSW with the corresponding CBW
pub tag: u32,
/// Difference between the expected data length from CSW.data_transfer_length
/// and the the actual amount of data sent or received. Cannot be greater
/// than data_transfer_length. Non-zero for an OUT (host to device) transfer
/// likely means there was an error whereas non-zero on IN (device to host) may
/// mean the host allocated enough space for an extended/complete result but
/// a shorter result was sent.
pub data_residue: u32,
/// The status of the command
/// 0x00 = Command succeeded
/// 0x01 = Command failed
/// 0x02 = Phase error. Causes the host to perform a reset recovery on the
/// device. This indicates the device state machine has got messed up
/// or similar unrecoverable condition. Processing further CBWs without
/// a reset gives indeterminate results.
pub status: u8,
}
impl CommandStatusWrapper {
pub fn new(tag: u32, data_residue: u32, status: CommandStatus) -> Self {
Self {
signature: CSW_SIGNATURE,
tag,
data_residue,
status: status as _,
}
}
pub fn to_bytes<'d>(&self, buf: &'d mut [u8]) -> &'d [u8] {
let len = size_of::<Self>();
assert!(buf.len() >= len);
unsafe { copy_nonoverlapping(self as *const _ as *const u8, buf.as_mut_ptr(), len) }
&buf[..len]
}
}

View File

@ -0,0 +1,268 @@
pub mod command_block_wrapper;
pub mod command_status_wrapper;
use core::mem::{size_of, MaybeUninit};
use embassy_usb_driver::{Direction, Endpoint, EndpointError, EndpointIn, EndpointOut};
use self::command_block_wrapper::CommandBlockWrapper;
use self::command_status_wrapper::{CommandStatus, CommandStatusWrapper};
use super::{CommandError, CommandSetHandler, DataPipeError, DataPipeIn, DataPipeOut};
use crate::class::msc::{MscProtocol, MscSubclass, USB_CLASS_MSC};
use crate::control::{ControlHandler, InResponse, OutResponse, Request, RequestType};
use crate::driver::Driver;
use crate::types::InterfaceNumber;
use crate::Builder;
const REQ_GET_MAX_LUN: u8 = 0xFE;
const REQ_BULK_ONLY_RESET: u8 = 0xFF;
pub struct State {
control: MaybeUninit<Control>,
}
impl Default for State {
fn default() -> Self {
Self {
control: MaybeUninit::uninit(),
}
}
}
pub struct Control {
max_lun: u8,
}
impl ControlHandler for Control {
fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> InResponse<'a> {
match (req.request_type, req.request) {
(RequestType::Class, REQ_GET_MAX_LUN) => {
debug!("REQ_GET_MAX_LUN");
buf[0] = self.max_lun;
InResponse::Accepted(&buf[..1])
}
(RequestType::Class, REQ_BULK_ONLY_RESET) => {
debug!("REQ_BULK_ONLY_RESET");
InResponse::Accepted(&[])
}
_ => InResponse::Rejected,
}
}
}
pub struct BulkOnlyTransport<'d, D: Driver<'d>, C: CommandSetHandler> {
msc_if: InterfaceNumber,
read_ep: D::EndpointOut,
write_ep: D::EndpointIn,
max_packet_size: u16,
handler: C,
}
impl<'d, D: Driver<'d>, C: CommandSetHandler> BulkOnlyTransport<'d, D, C> {
pub fn new(
builder: &mut Builder<'d, D>,
state: &'d mut State,
subclass: MscSubclass,
max_packet_size: u16,
max_lun: u8,
handler: C,
) -> Self {
assert!(max_lun < 16, "BulkOnlyTransport supports maximum 16 LUNs");
let control = state.control.write(Control { max_lun });
let mut func = builder.function(USB_CLASS_MSC, subclass as _, MscProtocol::BulkOnlyTransport as _);
// Control interface
let mut iface = func.interface();
iface.handler(control);
let msc_if = iface.interface_number();
let mut alt = iface.alt_setting(USB_CLASS_MSC, subclass as _, MscProtocol::BulkOnlyTransport as _);
let read_ep = alt.endpoint_bulk_out(max_packet_size);
let write_ep = alt.endpoint_bulk_in(max_packet_size);
Self {
msc_if,
read_ep,
write_ep,
max_packet_size,
handler,
}
}
async fn receive_control_block_wrapper(&mut self) -> CommandBlockWrapper {
let mut cbw_buf = [0u8; size_of::<CommandBlockWrapper>()];
loop {
// CBW is always sent at a packet boundary and is a short packet of 31 bytes
match self.read_ep.read(&mut cbw_buf).await {
Ok(len) => {
if len != cbw_buf.len() {
error!("Invalid CBW length");
}
match CommandBlockWrapper::from_bytes(&cbw_buf) {
Ok(cbw) => return cbw,
Err(e) => {
error!("Invalid CBW: {:?}", e);
}
}
}
Err(e) => match e {
EndpointError::BufferOverflow => {
error!("Host sent too long CBW");
}
EndpointError::Disabled => self.read_ep.wait_enabled().await,
},
};
}
}
async fn send_csw(&mut self, csw: CommandStatusWrapper) {
let mut csw_buf = [0u8; size_of::<CommandStatusWrapper>()];
match self.write_ep.write(csw.to_bytes(&mut csw_buf)).await {
Ok(_) => {}
Err(e) => error!("error sending CSW: {:?}", e),
}
}
async fn handle_command_out(&mut self, cbw: CommandBlockWrapper) -> CommandStatusWrapper {
let mut pipe_out = BulkOnlyTransportDataPipeOut {
ep: &mut self.read_ep,
data_residue: cbw.data_transfer_length as _,
max_packet_size: self.max_packet_size,
last_packet_full: true,
};
let status = match self.handler.command_out(cbw.lun, cbw.data(), &mut pipe_out).await {
Ok(_) => CommandStatus::CommandOk,
Err(e) => match e {
CommandError::PipeError(e) => {
error!("data pipe error: {:?}", e);
CommandStatus::PhaseError
}
CommandError::CommandError => CommandStatus::CommandError,
},
};
CommandStatusWrapper::new(cbw.tag, pipe_out.data_residue, status)
}
async fn handle_command_in(&mut self, cbw: CommandBlockWrapper) -> CommandStatusWrapper {
let mut pipe_in = BulkOnlyTransportDataPipeIn {
ep: &mut self.write_ep,
data_residue: cbw.data_transfer_length as _,
max_packet_size: self.max_packet_size,
last_packet_full: true,
};
let status = match self.handler.command_in(cbw.lun, cbw.data(), &mut pipe_in).await {
Ok(_) => match pipe_in.finalize().await {
Ok(_) => CommandStatus::CommandOk,
Err(e) => {
error!("Error finalizing data pipe: {:?}", e);
CommandStatus::PhaseError
}
},
Err(e) => match e {
CommandError::PipeError(e) => {
error!("data pipe error: {:?}", e);
CommandStatus::PhaseError
}
CommandError::CommandError => CommandStatus::CommandError,
},
};
CommandStatusWrapper::new(cbw.tag, pipe_in.data_residue, status)
}
pub async fn run(&mut self) {
loop {
let cbw = self.receive_control_block_wrapper().await;
trace!("received CBW");
let csw = match cbw.dir() {
Direction::Out => {
trace!("handle_command_out");
self.handle_command_out(cbw).await
}
Direction::In => {
trace!("handle_command_in");
self.handle_command_in(cbw).await
}
};
trace!("sending CSW");
self.send_csw(csw).await;
}
}
}
pub struct BulkOnlyTransportDataPipeIn<'d, E: EndpointIn> {
ep: &'d mut E,
// requested transfer size minus already transfered bytes
data_residue: u32,
max_packet_size: u16,
last_packet_full: bool,
}
impl<'d, E: EndpointIn> DataPipeIn for BulkOnlyTransportDataPipeIn<'d, E> {
async fn write(&mut self, buf: &[u8]) -> Result<(), DataPipeError> {
if !self.last_packet_full {
return Err(DataPipeError::TransferFinalized);
}
for chunk in buf.chunks(self.max_packet_size.into()) {
if self.data_residue < chunk.len() as _ {
return Err(DataPipeError::TransferSizeExceeded);
}
self.ep.write(chunk).await?;
self.data_residue -= chunk.len() as u32;
self.last_packet_full = chunk.len() == self.max_packet_size.into();
}
Ok(())
}
}
impl<'d, E: EndpointIn> BulkOnlyTransportDataPipeIn<'d, E> {
async fn finalize(&mut self) -> Result<(), DataPipeError> {
// Send ZLP only if last packet was full and transfer size was not exhausted
if self.last_packet_full && self.data_residue != 0 {
self.ep.write(&[]).await?;
}
Ok(())
}
}
pub struct BulkOnlyTransportDataPipeOut<'d, E: EndpointOut> {
ep: &'d mut E,
// requested transfer size minus already transfered bytes
data_residue: u32,
max_packet_size: u16,
last_packet_full: bool,
}
impl<'d, E: EndpointOut> DataPipeOut for BulkOnlyTransportDataPipeOut<'d, E> {
async fn read(&mut self, buf: &mut [u8]) -> Result<(), DataPipeError> {
if !self.last_packet_full {
return Err(DataPipeError::TransferFinalized);
}
for chunk in buf.chunks_mut(self.max_packet_size.into()) {
if self.data_residue < chunk.len() as _ {
return Err(DataPipeError::TransferSizeExceeded);
}
self.ep.read(chunk).await?;
self.data_residue -= chunk.len() as u32;
self.last_packet_full = chunk.len() == self.max_packet_size.into();
}
Ok(())
}
}

View File

@ -0,0 +1,56 @@
use embassy_usb_driver::EndpointError;
pub mod bulk_only;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum DataPipeError {
/// Exceeded the host requested transfer size
TransferSizeExceeded,
/// Transfer was finalized by sending a short (non-full) packet
TransferFinalized,
/// USB driver endpoint error
EndpointError(EndpointError),
}
impl From<EndpointError> for DataPipeError {
fn from(e: EndpointError) -> Self {
Self::EndpointError(e)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum CommandError {
PipeError(DataPipeError),
CommandError,
}
/// A pipe that allows [CommandSetHandler] to write command-specific data.
pub trait DataPipeIn {
/// Sends data to host.
///
/// Must be called only once or in lengths multiple of maximum USB packet size.
/// Otherwise, incomplete USB packet is interpreted as end of transfer.
async fn write(&mut self, buf: &[u8]) -> Result<(), DataPipeError>;
}
/// A pipe that allows [CommandSetHandler] to read command-specific data.
pub trait DataPipeOut {
/// Receives data to host.
///
/// Must be called only once or in lengths multiple of maximum USB packet size.
/// Otherwise, incomplete USB packet is interpreted as end of transfer.
async fn read(&mut self, buf: &mut [u8]) -> Result<(), DataPipeError>;
}
/// Implemented by mass storage subclasses (i.e. SCSI).
///
/// This trait is tailored to bulk-only transport and may require changes for other transports.
pub trait CommandSetHandler {
/// Handles command where data is sent to device.
async fn command_out(&mut self, lun: u8, cmd: &[u8], pipe: &mut impl DataPipeOut) -> Result<(), CommandError>;
/// Handles command where data is sent to host.
async fn command_in(&mut self, lun: u8, cmd: &[u8], pipe: &mut impl DataPipeIn) -> Result<(), CommandError>;
}

View File

@ -1,5 +1,6 @@
#![no_std] #![no_std]
#![feature(type_alias_impl_trait)] #![feature(type_alias_impl_trait)]
#![feature(async_fn_in_trait)]
// This mod MUST go first, so that the others see its macros. // This mod MUST go first, so that the others see its macros.
pub(crate) mod fmt; pub(crate) mod fmt;

View File

@ -0,0 +1,137 @@
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
#![feature(async_fn_in_trait)]
use defmt::{panic, *};
use embassy_executor::Spawner;
use embassy_stm32::time::mhz;
use embassy_stm32::usb_otg::{Driver, Instance};
use embassy_stm32::{interrupt, Config};
use embassy_usb::class::cdc_acm::{CdcAcmClass, State};
use embassy_usb::class::msc::transport::bulk_only::BulkOnlyTransport;
use embassy_usb::class::msc::transport::CommandSetHandler;
use embassy_usb::class::msc::MscSubclass;
use embassy_usb::driver::EndpointError;
use embassy_usb::Builder;
use futures::future::join;
use {defmt_rtt as _, panic_probe as _};
struct CommandSet {}
impl CommandSetHandler for CommandSet {
async fn command_out(
&mut self,
lun: u8,
cmd: &[u8],
pipe: &mut impl embassy_usb::class::msc::transport::DataPipeOut,
) -> Result<(), embassy_usb::class::msc::transport::CommandError> {
info!("CMD_OUT: {:?}", cmd);
Ok(())
}
async fn command_in(
&mut self,
lun: u8,
cmd: &[u8],
pipe: &mut impl embassy_usb::class::msc::transport::DataPipeIn,
) -> Result<(), embassy_usb::class::msc::transport::CommandError> {
info!("CMD_IN: {:?}", cmd);
Ok(())
}
}
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
info!("Hello World!");
let mut config = Config::default();
config.rcc.pll48 = true;
config.rcc.sys_ck = Some(mhz(48));
let p = embassy_stm32::init(config);
// Create the driver, from the HAL.
let irq = interrupt::take!(OTG_FS);
let mut ep_out_buffer = [0u8; 256];
let driver = Driver::new_fs(p.USB_OTG_FS, irq, p.PA12, p.PA11, &mut ep_out_buffer);
// Create embassy-usb Config
let mut config = embassy_usb::Config::new(0xc0de, 0xcafe);
config.manufacturer = Some("Embassy");
config.product = Some("USB-serial example");
config.serial_number = Some("12345678");
// Required for windows compatiblity.
// https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help
config.device_class = 0xEF;
config.device_sub_class = 0x02;
config.device_protocol = 0x01;
config.composite_with_iads = true;
// Create embassy-usb DeviceBuilder using the driver and config.
// It needs some buffers for building the descriptors.
let mut device_descriptor = [0; 256];
let mut config_descriptor = [0; 256];
let mut bos_descriptor = [0; 256];
let mut control_buf = [0; 64];
let mut state = Default::default();
let mut builder = Builder::new(
driver,
config,
&mut device_descriptor,
&mut config_descriptor,
&mut bos_descriptor,
&mut control_buf,
None,
);
let mut msc = BulkOnlyTransport::new(
&mut builder,
&mut state,
MscSubclass::ScsiTransparentCommandSet,
64,
0,
CommandSet {},
);
// Build the builder.
let mut usb = builder.build();
// Run the USB device.
let usb_fut = usb.run();
// Do stuff with the class!
let echo_fut = async {
loop {
msc.run().await;
}
};
// Run everything concurrently.
// If we had made everything `'static` above instead, we could do this using separate tasks instead.
join(usb_fut, echo_fut).await;
}
struct Disconnected {}
impl From<EndpointError> for Disconnected {
fn from(val: EndpointError) -> Self {
match val {
EndpointError::BufferOverflow => panic!("Buffer overflow"),
EndpointError::Disabled => Disconnected {},
}
}
}
async fn echo<'d, T: Instance + 'd>(class: &mut CdcAcmClass<'d, Driver<'d, T>>) -> Result<(), Disconnected> {
let mut buf = [0; 64];
loop {
let n = class.read_packet(&mut buf).await?;
let data = &buf[..n];
info!("data: {:x}", data);
class.write_packet(data).await?;
}
}