usb: initial bulk-only transport implementation
This commit is contained in:
parent
825b67101b
commit
7515369984
@ -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;
|
||||||
|
57
embassy-usb/src/class/msc/mod.rs
Normal file
57
embassy-usb/src/class/msc/mod.rs
Normal 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,
|
||||||
|
}
|
1
embassy-usb/src/class/msc/subclass/mod.rs
Normal file
1
embassy-usb/src/class/msc/subclass/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod scsi;
|
0
embassy-usb/src/class/msc/subclass/scsi/mod.rs
Normal file
0
embassy-usb/src/class/msc/subclass/scsi/mod.rs
Normal 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]
|
||||||
|
}
|
||||||
|
}
|
@ -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]
|
||||||
|
}
|
||||||
|
}
|
268
embassy-usb/src/class/msc/transport/bulk_only/mod.rs
Normal file
268
embassy-usb/src/class/msc/transport/bulk_only/mod.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
56
embassy-usb/src/class/msc/transport/mod.rs
Normal file
56
embassy-usb/src/class/msc/transport/mod.rs
Normal 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>;
|
||||||
|
}
|
@ -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;
|
||||||
|
137
examples/stm32f4/src/bin/usb_msc.rs
Normal file
137
examples/stm32f4/src/bin/usb_msc.rs
Normal 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?;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user