1
0
Fork 0
mirror of https://github.com/CTCaer/hekate.git synced 2024-10-30 15:31:45 +00:00
hekate/bdk/usb/usb_gadget_ums.c
CTCaer 4b7e1f699d ums/xusb: do not allow multiple CBW requests
On XUSB do not allow multiple requests for CBW to be done.
This avoids an issue with some XHCI controllers and OS combos (e.g. ASMedia and Linux/Mac OS) which confuse that and concatenate an old CBW request with another write request (SCSI Write) and create a babble error (transmit overflow).
2021-08-28 17:09:38 +03:00

1950 lines
53 KiB
C

/*
* USB Gadget UMS driver for Tegra X1
*
* Copyright (c) 2003-2008 Alan Stern
* Copyright (c) 2009 Samsung Electronics
* Author: Michal Nazarewicz <m.nazarewicz@samsung.com>
* Copyright (c) 2019-2021 CTCaer
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <usb/usbd.h>
#include <gfx_utils.h>
#include <soc/hw_init.h>
#include <soc/t210.h>
#include <storage/nx_sd.h>
#include <storage/sdmmc.h>
#include <storage/sdmmc_driver.h>
#include <utils/btn.h>
#include <utils/sprintf.h>
#include <utils/util.h>
#include <memory_map.h>
//#define DPRINTF(...) gfx_printf(__VA_ARGS__)
#define DPRINTF(...)
#define UMS_MAX_LUN 1 // Only 1 disk/partition for now.
#define USB_BULK_CB_WRAP_LEN 31
#define USB_BULK_CB_SIG 0x43425355 // USBC.
#define USB_BULK_IN_FLAG 0x80
#define USB_BULK_CS_WRAP_LEN 13
#define USB_BULK_CS_SIG 0x53425355 // USBS.
#define USB_STATUS_PASS 0
#define USB_STATUS_FAIL 1
#define USB_STATUS_PHASE_ERROR 2
#define UMS_DISK_LBA_SHIFT 9
#define UMS_DISK_LBA_SIZE (1 << UMS_DISK_LBA_SHIFT)
#define UMS_DISK_MAX_IO_TRANSFER_64K (USB_EP_BUFFER_MAX_SIZE >> UMS_DISK_LBA_SHIFT)
#define UMS_DISK_MAX_IO_TRANSFER_32K (UMS_DISK_MAX_IO_TRANSFER_64K / 2)
#define UMS_SCSI_TRANSFER_512K (0x80000 >> UMS_DISK_LBA_SHIFT)
#define UMS_EP_OUT_MAX_XFER (USB_EP_BULK_OUT_MAX_XFER)
// Length of a SCSI Command Data Block.
#define SCSI_MAX_CMD_SZ 16
// SCSI device types
#define SCSI_TYPE_DISK 0x00
// SCSI commands.
#define SC_FORMAT_UNIT 0x04
#define SC_INQUIRY 0x12
#define SC_LOG_SENSE 0x4D
#define SC_MODE_SELECT_6 0x15
#define SC_MODE_SELECT_10 0x55
#define SC_MODE_SENSE_6 0x1A
#define SC_MODE_SENSE_10 0x5A
#define SC_PREVENT_ALLOW_MEDIUM_REMOVAL 0x1E
#define SC_READ_6 0x08
#define SC_READ_10 0x28
#define SC_READ_12 0xA8
#define SC_READ_CAPACITY 0x25
#define SC_READ_FORMAT_CAPACITIES 0x23
#define SC_READ_HEADER 0x44
#define SC_READ_TOC 0x43
#define SC_RELEASE 0x17
#define SC_REQUEST_SENSE 0x03
#define SC_RESERVE 0x16
#define SC_SEND_DIAGNOSTIC 0x1D
#define SC_START_STOP_UNIT 0x1B
#define SC_SYNCHRONIZE_CACHE 0x35
#define SC_TEST_UNIT_READY 0x00
#define SC_VERIFY 0x2F
#define SC_WRITE_6 0x0A
#define SC_WRITE_10 0x2A
#define SC_WRITE_12 0xAA
// SCSI Sense Key/Additional Sense Code/ASC Qualifier values.
#define SS_NO_SENSE 0x0
#define SS_COMMUNICATION_FAILURE 0x40800
#define SS_INVALID_COMMAND 0x52000
#define SS_INVALID_FIELD_IN_CDB 0x52400
#define SS_LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE 0x52100
#define SS_MEDIUM_NOT_PRESENT 0x23A00
#define SS_MEDIUM_REMOVAL_PREVENTED 0x55302
#define SS_NOT_READY_TO_READY_TRANSITION 0x62800
#define SS_RESET_OCCURRED 0x62900
#define SS_SAVING_PARAMETERS_NOT_SUPPORTED 0x53900
#define SS_UNRECOVERED_READ_ERROR 0x31100
#define SS_WRITE_ERROR 0x30C02
#define SS_WRITE_PROTECTED 0x72700
#define SK(x) ((u8) ((x) >> 16)) // Sense Key byte, etc.
#define ASC(x) ((u8) ((x) >> 8))
#define ASCQ(x) ((u8) (x))
enum ums_state {
UMS_STATE_NORMAL = 0,
UMS_STATE_ABORT_BULK_OUT,
UMS_STATE_PROTOCOL_RESET,
UMS_STATE_EXIT,
UMS_STATE_TERMINATED
};
enum ums_result {
UMS_RES_OK = 0,
UMS_RES_IO_ERROR = -5,
UMS_RES_TIMEOUT = -3,
UMS_RES_PROT_FATAL = -4,
UMS_RES_INVALID_ARG = -22
};
enum data_direction {
DATA_DIR_UNKNOWN = 0,
DATA_DIR_FROM_HOST,
DATA_DIR_TO_HOST,
DATA_DIR_NONE
};
enum buffer_state {
BUF_STATE_EMPTY = 0,
BUF_STATE_FULL,
BUF_STATE_BUSY
};
typedef struct _bulk_recv_pkt_t {
u32 Signature; // 'USBC'.
u32 Tag; // Unique per command id.
u32 DataTransferLength; // Size of the data.
u8 Flags; // Direction in bit 7.
u8 Lun; // LUN (normally 0).
u8 Length; // Of the CDB, <= SCSI_MAX_CMD_SZ.
u8 CDB[16]; // Command Data Block.
} bulk_recv_pkt_t;
typedef struct _bulk_send_pkt_t {
u32 Signature; // 'USBS'.
u32 Tag; // Same as original command.
u32 Residue; // Amount not transferred.
u8 Status;
} bulk_send_pkt_t;
typedef struct _logical_unit_t
{
sdmmc_t *sdmmc;
sdmmc_storage_t *storage;
u32 num_sectors;
u32 offset;
int unmounted;
u32 ro;
u32 type;
u32 partition;
u32 removable;
u32 prevent_medium_removal;
u32 info_valid;
u32 sense_data;
u32 sense_data_info;
u32 unit_attention_data;
} logical_unit_t;
typedef struct _bulk_ctxt_t {
u32 bulk_in;
int bulk_in_status;
u32 bulk_in_length;
u32 bulk_in_length_actual;
u8 *bulk_in_buf;
enum buffer_state bulk_in_buf_state;
u32 bulk_out;
int bulk_out_status;
u32 bulk_out_length;
u32 bulk_out_length_actual;
int bulk_out_ignore;
u8 *bulk_out_buf;
enum buffer_state bulk_out_buf_state;
} bulk_ctxt_t;
typedef struct _usbd_gadget_ums_t {
bulk_ctxt_t bulk_ctxt;
u32 cmnd_size;
u8 cmnd[SCSI_MAX_CMD_SZ];
u32 lun_idx; // lun index
logical_unit_t lun;
enum ums_state state; // For exception handling.
enum data_direction data_dir;
u32 data_size;
u32 data_size_from_cmnd;
u32 tag;
u32 residue;
u32 usb_amount_left;
bool cbw_req_queued;
u32 phase_error;
u32 short_packet_received;
int thread_wakeup_needed;
int can_stall;
u32 timeouts;
bool xusb;
void (*system_maintenance)(bool);
void *label;
void (*set_text)(void *, const char *);
} usbd_gadget_ums_t;
static usb_ops_t usb_ops;
static inline void put_array_le_to_be16(u16 val, void *p)
{
u8 *_p = p;
_p[0] = val >> 8;
_p[1] = val;
}
static inline void put_array_le_to_be32(u32 val, void *p)
{
u8 *_p = p;
_p[0] = val >> 24;
_p[1] = val >> 16;
_p[2] = val >> 8;
_p[3] = val;
}
static inline u16 get_array_be_to_le16(const void *p)
{
const u8 *_p = p;
u16 val = _p[0] << 8 | _p[1];
return val;
}
static inline u32 get_array_be_to_le24(const void *p)
{
const u8 *_p = p;
u32 val = (_p[0] << 16) | (_p[1] << 8) | _p[2];
return val;
}
static inline u32 get_array_be_to_le32(const void *p)
{
const u8 *_p = p;
u32 val = (_p[0] << 24) | (_p[1] << 16) | (_p[2] << 8) | _p[3];
return val;
}
static void raise_exception(usbd_gadget_ums_t *ums, enum ums_state new_state)
{
/* Do nothing if a higher-priority exception is already in progress.
* If a lower-or-equal priority exception is in progress, preempt it
* and notify the main thread by sending it a signal. */
if (ums->state <= new_state) {
ums->state = new_state;
ums->thread_wakeup_needed = 1;
}
}
static void ums_handle_ep0_ctrl(usbd_gadget_ums_t *ums)
{
if (usb_ops.usbd_handle_ep0_ctrl_setup())
raise_exception(ums, UMS_STATE_PROTOCOL_RESET);
}
static int ums_wedge_bulk_in_endpoint(usbd_gadget_ums_t *ums)
{
/* usbd_set_ep_wedge(bulk_ctxt->bulk_in); */
return UMS_RES_OK;
}
static int ums_set_stall(u32 ep)
{
usb_ops.usbd_set_ep_stall(ep, USB_EP_CFG_STALL);
return UMS_RES_OK;
}
static int ums_clear_stall(u32 ep)
{
usb_ops.usbd_set_ep_stall(ep, USB_EP_CFG_CLEAR);
return UMS_RES_OK;
}
static void ums_flush_endpoint(u32 ep)
{
if (usb_ops.usbd_flush_endpoint)
usb_ops.usbd_flush_endpoint(ep);
}
static void _ums_transfer_start(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt, u32 ep, u32 sync_timeout)
{
if (ep == bulk_ctxt->bulk_in)
{
bulk_ctxt->bulk_in_status = usb_ops.usb_device_ep1_in_write(
bulk_ctxt->bulk_in_buf, bulk_ctxt->bulk_in_length,
&bulk_ctxt->bulk_in_length_actual, sync_timeout);
if (bulk_ctxt->bulk_in_status == USB_ERROR_XFER_ERROR)
{
ums->set_text(ums->label, "#FFDD00 Error:# EP IN transfer!");
ums_flush_endpoint(bulk_ctxt->bulk_in);
}
else if (bulk_ctxt->bulk_in_status == USB2_ERROR_XFER_NOT_ALIGNED)
ums->set_text(ums->label, "#FFDD00 Error:# EP IN Buffer not aligned!");
if (sync_timeout)
bulk_ctxt->bulk_in_buf_state = BUF_STATE_EMPTY;
}
else
{
bulk_ctxt->bulk_out_status = usb_ops.usb_device_ep1_out_read(
bulk_ctxt->bulk_out_buf, bulk_ctxt->bulk_out_length,
&bulk_ctxt->bulk_out_length_actual, sync_timeout);
if (bulk_ctxt->bulk_out_status == USB_ERROR_XFER_ERROR)
{
ums->set_text(ums->label, "#FFDD00 Error:# EP OUT transfer!");
ums_flush_endpoint(bulk_ctxt->bulk_out);
}
else if (bulk_ctxt->bulk_out_status == USB2_ERROR_XFER_NOT_ALIGNED)
ums->set_text(ums->label, "#FFDD00 Error:# EP OUT Buffer not aligned!");
if (sync_timeout)
bulk_ctxt->bulk_out_buf_state = BUF_STATE_FULL;
}
}
static void _ums_transfer_out_big_read(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
bulk_ctxt->bulk_out_status = usb_ops.usb_device_ep1_out_read_big(
bulk_ctxt->bulk_out_buf, bulk_ctxt->bulk_out_length,
&bulk_ctxt->bulk_out_length_actual);
if (bulk_ctxt->bulk_out_status == USB_ERROR_XFER_ERROR)
{
ums->set_text(ums->label, "#FFDD00 Error:# EP OUT transfer!");
ums_flush_endpoint(bulk_ctxt->bulk_out);
}
bulk_ctxt->bulk_out_buf_state = BUF_STATE_FULL;
}
static void _ums_transfer_finish(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt, u32 ep, u32 sync_timeout)
{
if (ep == bulk_ctxt->bulk_in)
{
bulk_ctxt->bulk_in_status = usb_ops.usb_device_ep1_in_writing_finish(
&bulk_ctxt->bulk_in_length_actual, sync_timeout);
if (bulk_ctxt->bulk_in_status == USB_ERROR_XFER_ERROR)
{
ums->set_text(ums->label, "#FFDD00 Error:# EP IN transfer!");
ums_flush_endpoint(bulk_ctxt->bulk_in);
}
bulk_ctxt->bulk_in_buf_state = BUF_STATE_EMPTY;
}
else
{
bulk_ctxt->bulk_out_status = usb_ops.usb_device_ep1_out_reading_finish(
&bulk_ctxt->bulk_out_length_actual, sync_timeout);
if (bulk_ctxt->bulk_out_status == USB_ERROR_XFER_ERROR)
{
ums->set_text(ums->label, "#FFDD00 Error:# EP OUT transfer!");
ums_flush_endpoint(bulk_ctxt->bulk_out);
}
bulk_ctxt->bulk_out_buf_state = BUF_STATE_FULL;
}
}
static void _ums_reset_buffer(bulk_ctxt_t *bulk_ctxt, u32 ep)
{
if (ep == bulk_ctxt->bulk_in)
bulk_ctxt->bulk_in_buf = (u8 *)USB_EP_BULK_IN_BUF_ADDR;
else
bulk_ctxt->bulk_out_buf = (u8 *)USB_EP_BULK_OUT_BUF_ADDR;
}
/*
* The following are old data based on max 64KB SCSI transfers.
* The endpoint xfer is actually 41.2 MB/s and SD card max 39.2 MB/s, with higher SCSI
* transfers, but the concurrency still helps and increases speeds by 20%.
*
* Concurrency of the SDMMC and USB xfers is very important with no cache.
* The worst offender being the SD card. We are already limited by bus, so
* concurrency helps minimize the SDMMC overhead.
* Max achieved bulk endpoint rate on a Tegra X1 and USB2.0 is 39.4 MB/s.
*
* USB bulk endpoint raw max transfer rate:
* 39.4MB/S - SCSI 128KB.
* 38.2MB/s - SCSI 64KB.
*
* 128 KB, 64 KB, 32 KB, 16 KB, 8 KB - Internal SDMMC I\O Sizes
* -------------------------------------------------------------------------------------
* eMMC - Toshiba - 4MB reads: 314.8 MB/s:
* 225.9 MB/s, 168.6 MB/s, 114.7 MB/s, 86.4 MB/s, 50.3 MB/s - RAW SDMMC.
* 33.5 MB/s, 31.9 MB/s, 29.3 MB/s, 27.1 MB/s, 22.1 MB/s - SCSI 128KB, No concurrency.
* 33.5 MB/s, 35.3 MB/s, 36.3 MB/s, 37.3 MB/s, 37.8 MB/s - SCSI 128KB, Concurrency.
* --.- --/-, 31.1 MB/s, 28.7 MB/s, 26.5 MB/s, 21.7 MB/s - SCSI 64KB, No concurrency.
* --.- --/-, 31.1 MB/s, 32.7 MB/s, 34.4 MB/s, 35.0 MB/s - SCSI 64KB, Concurrency.
*
* SD Card - Samsung Evo+ 128GB - 4MB reads: 91.6 MB/s:
* 72.6 MB/s, 62.8 MB/s, 47.4 MB/s, 31.1 MB/s, 18.5 MB/s - RAW SDMMC.
* 25.5 MB/s, 24.2 MB/s, 21.5 MB/s, 17.4 MB/s, 12.6 MB/s - SCSI 128KB, No concurrency.
* 25.5 MB/s, 30.0 MB/s, 32.6 MB/s, 28.3 MB/s, 18.0 MB/s - SCSI 128KB, Concurrency.
* --.- --/-, 23.8 MB/s, 21.2 MB/s, 17.1 MB/s, 12.5 MB/s - SCSI 64KB, No concurrency.
* --.- --/-, 23.8 MB/s, 27.2 MB/s, 25.8 MB/s, 17.5 MB/s - SCSI 64KB, Concurrency.
*/
static int _scsi_read(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
u32 lba_offset;
bool first_read = true;
u8 *sdmmc_buf = (u8 *)SDXC_BUF_ALIGNED;
// Get the starting LBA and check that it's not too big.
if (ums->cmnd[0] == SC_READ_6)
lba_offset = get_array_be_to_le24(&ums->cmnd[1]);
else
{
lba_offset = get_array_be_to_le32(&ums->cmnd[2]);
// We allow DPO and FUA bypass cache bits, but we don't use them.
if ((ums->cmnd[1] & ~0x18) != 0)
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
}
if (lba_offset >= ums->lun.num_sectors)
{
ums->set_text(ums->label, "#FF8000 Warn:# Read - Out of range! Host notified.");
ums->lun.sense_data = SS_LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE;
return UMS_RES_INVALID_ARG;
}
// Check that request data size is not 0.
u32 amount_left = ums->data_size_from_cmnd >> UMS_DISK_LBA_SHIFT;
if (!amount_left)
return UMS_RES_IO_ERROR; // No default reply.
// Limit IO transfers based on request for faster concurrent reads.
u32 max_io_transfer = (amount_left >= UMS_SCSI_TRANSFER_512K) ?
UMS_DISK_MAX_IO_TRANSFER_64K : UMS_DISK_MAX_IO_TRANSFER_32K;
while (true)
{
// Max io size and end sector limits.
u32 amount = MIN(amount_left, max_io_transfer);
amount = MIN(amount, ums->lun.num_sectors - lba_offset);
// Check if it is a read past the end sector.
if (!amount)
{
ums->lun.sense_data = SS_LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE;
ums->lun.sense_data_info = lba_offset;
ums->lun.info_valid = 1;
bulk_ctxt->bulk_in_length = 0;
bulk_ctxt->bulk_in_buf_state = BUF_STATE_FULL;
break;
}
// Do the SDMMC read.
if (!sdmmc_storage_read(ums->lun.storage, ums->lun.offset + lba_offset, amount, sdmmc_buf))
amount = 0;
// Wait for the async USB transfer to finish.
if (!first_read)
_ums_transfer_finish(ums, bulk_ctxt, bulk_ctxt->bulk_in, USB_XFER_SYNCED);
lba_offset += amount;
amount_left -= amount;
ums->residue -= amount << UMS_DISK_LBA_SHIFT;
bulk_ctxt->bulk_in_length = amount << UMS_DISK_LBA_SHIFT;
bulk_ctxt->bulk_in_buf_state = BUF_STATE_FULL;
bulk_ctxt->bulk_in_buf = sdmmc_buf;
// If an error occurred, report it and its position.
if (!amount)
{
ums->set_text(ums->label, "#FFDD00 Error:# SDMMC Read!");
ums->lun.sense_data = SS_UNRECOVERED_READ_ERROR;
ums->lun.sense_data_info = lba_offset;
ums->lun.info_valid = 1;
break;
}
// Last SDMMC read. Last part will be sent by the finish reply function.
if (!amount_left)
break;
// Start the USB transfer.
_ums_transfer_start(ums, bulk_ctxt, bulk_ctxt->bulk_in, USB_XFER_START);
first_read = false;
// Increment our buffer to read new data.
sdmmc_buf += amount << UMS_DISK_LBA_SHIFT;
}
return UMS_RES_IO_ERROR; // No default reply.
}
/*
* Writes are another story.
* Tests showed that big writes are faster than concurrent 32K usb reads + writes.
* The only thing that can help here is caching the writes. But for the simplicity
* of this implementation it will not be implemented yet.
*/
static int _scsi_write(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
static char txt_buf[256];
u32 amount_left_to_req, amount_left_to_write;
u32 usb_lba_offset, lba_offset;
u32 amount;
if (ums->lun.ro)
{
ums->set_text(ums->label, "#FF8000 Warn:# Write - Read only! Host notified.");
ums->lun.sense_data = SS_WRITE_PROTECTED;
return UMS_RES_INVALID_ARG;
}
if (ums->cmnd[0] == SC_WRITE_6)
lba_offset = get_array_be_to_le24(&ums->cmnd[1]);
else
{
lba_offset = get_array_be_to_le32(&ums->cmnd[2]);
// We allow DPO and FUA bypass cache bits. We only implement FUA by performing synchronous output.
if (ums->cmnd[1] & ~0x18)
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
}
// Check that starting LBA is not past the end sector offset.
if (lba_offset >= ums->lun.num_sectors)
{
ums->set_text(ums->label, "#FF8000 Warn:# Write - Out of range! Host notified.");
ums->lun.sense_data = SS_LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE;
return UMS_RES_INVALID_ARG;
}
// Carry out the file writes.
usb_lba_offset = lba_offset;
amount_left_to_req = ums->data_size_from_cmnd;
amount_left_to_write = ums->data_size_from_cmnd;
while (amount_left_to_write > 0)
{
// Queue a request for more data from the host.
if (amount_left_to_req > 0)
{
// Limit write to max supported read from EP OUT.
amount = MIN(amount_left_to_req, UMS_EP_OUT_MAX_XFER);
if (usb_lba_offset >= ums->lun.num_sectors)
{
ums->set_text(ums->label, "#FFDD00 Error:# Write - Past last sector!");
ums->lun.sense_data = SS_LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE;
ums->lun.sense_data_info = usb_lba_offset;
ums->lun.info_valid = 1;
break;
}
// Get the next buffer.
usb_lba_offset += amount >> UMS_DISK_LBA_SHIFT;
ums->usb_amount_left -= amount;
amount_left_to_req -= amount;
bulk_ctxt->bulk_out_length = amount;
_ums_transfer_out_big_read(ums, bulk_ctxt);
}
if (bulk_ctxt->bulk_out_buf_state == BUF_STATE_FULL)
{
bulk_ctxt->bulk_out_buf_state = BUF_STATE_EMPTY;
// Did something go wrong with the transfer?.
if (bulk_ctxt->bulk_out_status != 0)
{
ums->lun.sense_data = SS_COMMUNICATION_FAILURE;
ums->lun.sense_data_info = lba_offset;
ums->lun.info_valid = 1;
s_printf(txt_buf, "#FFDD00 Error:# Write - Comm failure %d!", bulk_ctxt->bulk_out_status);
ums->set_text(ums->label, txt_buf);
break;
}
amount = bulk_ctxt->bulk_out_length_actual;
if ((ums->lun.num_sectors - lba_offset) < (amount >> UMS_DISK_LBA_SHIFT))
{
DPRINTF("write %X @ %X beyond end %X\n", amount, lba_offset, ums->lun.num_sectors);
amount = (ums->lun.num_sectors - lba_offset) << UMS_DISK_LBA_SHIFT;
}
/*
* Don't accept excess data. The spec doesn't say
* what to do in this case. We'll ignore the error.
*/
amount = MIN(amount, bulk_ctxt->bulk_out_length);
// Don't write a partial block.
amount -= (amount & 511);
if (amount == 0)
goto empty_write;
// Perform the write.
if (!sdmmc_storage_write(ums->lun.storage, ums->lun.offset + lba_offset,
amount >> UMS_DISK_LBA_SHIFT, (u8 *)bulk_ctxt->bulk_out_buf))
amount = 0;
DPRINTF("file write %X @ %X\n", amount, lba_offset);
lba_offset += amount >> UMS_DISK_LBA_SHIFT;
amount_left_to_write -= amount;
ums->residue -= amount;
// If an error occurred, report it and its position.
if (!amount)
{
ums->set_text(ums->label, "#FFDD00 Error:# SDMMC Write!");
ums->lun.sense_data = SS_WRITE_ERROR;
ums->lun.sense_data_info = lba_offset;
ums->lun.info_valid = 1;
break;
}
empty_write:
// Did the host decide to stop early?
if (bulk_ctxt->bulk_out_length_actual < bulk_ctxt->bulk_out_length)
{
ums->set_text(ums->label, "#FFDD00 Error:# Empty Write!");
ums->short_packet_received = 1;
break;
}
}
}
return UMS_RES_IO_ERROR; // No default reply.
}
static int _scsi_verify(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
// Check that start LBA is past the end sector offset.
u32 lba_offset = get_array_be_to_le32(&ums->cmnd[2]);
if (lba_offset >= ums->lun.num_sectors)
{
ums->set_text(ums->label, "#FF8000 Warn:# Verif - Out of range! Host notified.");
ums->lun.sense_data = SS_LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE;
return UMS_RES_INVALID_ARG;
}
// We allow DPO but we don't implement it. Check that nothing else is enabled.
if (ums->cmnd[1] & ~0x10)
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
u32 verification_length = get_array_be_to_le16(&ums->cmnd[7]);
if (verification_length == 0)
return UMS_RES_IO_ERROR; // No default reply.
u32 amount;
while (verification_length > 0)
{
// Limit to EP buffer size and end sector offset.
amount = MIN(verification_length, USB_EP_BUFFER_MAX_SIZE >> UMS_DISK_LBA_SHIFT);
amount = MIN(amount, ums->lun.num_sectors - lba_offset);
if (amount == 0) {
ums->lun.sense_data = SS_LOGICAL_BLOCK_ADDRESS_OUT_OF_RANGE;
ums->lun.sense_data_info = lba_offset;
ums->lun.info_valid = 1;
break;
}
if (!sdmmc_storage_read(ums->lun.storage, ums->lun.offset + lba_offset, amount, bulk_ctxt->bulk_in_buf))
amount = 0;
DPRINTF("File read %X @ %X\n", amount, lba_offset);
if (!amount)
{
ums->set_text(ums->label, "#FFDD00 Error:# File verify!");
ums->lun.sense_data = SS_UNRECOVERED_READ_ERROR;
ums->lun.sense_data_info = lba_offset;
ums->lun.info_valid = 1;
break;
}
lba_offset += amount;
verification_length -= amount;
}
return UMS_RES_OK;
}
static int _scsi_inquiry(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
u8 *buf = (u8 *)bulk_ctxt->bulk_in_buf;
memset(buf, 0, 36);
// Enable Vital Product Data (EVPD) and Unit Serial Number.
if (ums->cmnd[1] == 1 && ums->cmnd[2] == 0x80)
{
buf[0] = 0;
buf[1] = ums->cmnd[2];
buf[2] = 0;
buf[3] = 20; // Additional length.
buf += 4;
s_printf((char *)buf, "%04X%s",
ums->lun.storage->cid.serial, ums->lun.type == MMC_SD ? " SD " : " eMMC ");
switch (ums->lun.partition)
{
case 0:
strcpy((char *)buf + strlen((char *)buf), "RAW");
break;
case EMMC_GPP + 1:
s_printf((char *)buf + strlen((char *)buf), "GPP");
break;
case EMMC_BOOT0 + 1:
s_printf((char *)buf + strlen((char *)buf), "BOOT0");
break;
case EMMC_BOOT1 + 1:
s_printf((char *)buf + strlen((char *)buf), "BOOT1");
break;
}
for (u32 i = strlen((char *)buf); i < 20; i++)
buf[i] = ' ';
return 24;
}
else /* if (ums->cmnd[1] == 0 && ums->cmnd[2] == 0) */ // Standard inquiry.
{
buf[0] = SCSI_TYPE_DISK;
buf[1] = ums->lun.removable ? 0x80 : 0;
buf[2] = 6; // ANSI INCITS 351-2001 (SPC-2).////////SPC2: 4, SPC4: 6
buf[3] = 2; // SCSI-2 INQUIRY data format.
buf[4] = 31; // Additional length.
// buf5-7: No special options.
// Vendor ID. Max 8 chars.
buf += 8;
strcpy((char *)buf, "hekate");
// Product ID. Max 16 chars.
buf += 8;
switch (ums->lun.partition)
{
case 0:
s_printf((char *)buf, "%s", "SD RAW");
break;
case EMMC_GPP + 1:
s_printf((char *)buf, "%s%s",
ums->lun.type == MMC_SD ? "SD " : "eMMC ", "GPP");
break;
case EMMC_BOOT0 + 1:
s_printf((char *)buf, "%s%s",
ums->lun.type == MMC_SD ? "SD " : "eMMC ", "BOOT0");
break;
case EMMC_BOOT1 + 1:
s_printf((char *)buf, "%s%s",
ums->lun.type == MMC_SD ? "SD " : "eMMC ", "BOOT1");
break;
}
// Rev ID. Max 4 chars.
buf += 16;
strcpy((char *)buf, "1.00");
return 36;
}
}
static int _scsi_request_sense(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
u8 *buf = (u8 *)bulk_ctxt->bulk_in_buf;
u32 sd, sdinfo;
int valid;
sd = ums->lun.sense_data;
sdinfo = ums->lun.sense_data_info;
valid = ums->lun.info_valid << 7;
ums->lun.sense_data = SS_NO_SENSE;
ums->lun.sense_data_info = 0;
ums->lun.info_valid = 0;
memset(buf, 0, 18);
buf[0] = valid | 0x70; // Valid, current error.
buf[2] = SK(sd);
put_array_le_to_be32(sdinfo, &buf[3]); // Sense information.
buf[7] = 18 - 8; // Additional sense length.
buf[12] = ASC(sd);
buf[13] = ASCQ(sd);
return 18;
}
static int _scsi_read_capacity(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
u8 *buf = (u8 *)bulk_ctxt->bulk_in_buf;
u32 lba = get_array_be_to_le32(&ums->cmnd[2]);
int pmi = ums->cmnd[8];
// Check the PMI and LBA fields.
if (pmi > 1 || (pmi == 0 && lba != 0))
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
put_array_le_to_be32(ums->lun.num_sectors - 1, &buf[0]); // Max logical block.
put_array_le_to_be32(UMS_DISK_LBA_SIZE, &buf[4]); // Block length.
return 8;
}
static int _scsi_log_sense(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
u8 *buf = (u8 *)bulk_ctxt->bulk_in_buf;
u8 *buf0 = buf;
bool valid_page = false;
u8 pc = ums->cmnd[2] >> 6;
u8 page_code = ums->cmnd[2] & 0x3F;
u8 sub_page_code = ums->cmnd[3];
if (ums->cmnd[1] & 1)
{
ums->lun.sense_data = SS_SAVING_PARAMETERS_NOT_SUPPORTED;
return UMS_RES_INVALID_ARG;
}
if (pc != 1) // Current cumulative values.
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
memset(buf, 0, 8);
if (page_code == 0x00 && !sub_page_code) // Supported pages.
{
valid_page = true;
buf[0] = 0x00; // Page code.
buf += 4;
buf[0] = 0x00; // Page 0.
buf[1] = 0x0D; // Page 1.
buf += 2;
}
else if (page_code == 0x0d && !sub_page_code) // Temperature.
{
valid_page = true;
buf[0] = 0x0D;
buf += 4;
put_array_le_to_be16(0, &buf[0]); // Param code.
buf[2] = 1; // Param control byte.
buf[3] = 2; // Param length.
buf[4] = 0; // Reserved.
buf[5] = 35; // Temperature (C) current (PCB here).
put_array_le_to_be16(0, &buf[6]); // PARAMETER CODE
buf[8] = 1; // Param control byte.
buf[9] = 2; // Param length.
buf[10] = 0; // Reserved.
buf[11] = 60; // Temperature (C) reference.
buf += 12;
}
// Check that a valid page mode data length was requested.
u32 len = buf - buf0;
if (!valid_page)
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
put_array_le_to_be16(len - 4, &buf0[2]);
return len;
}
static int _scsi_mode_sense(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
u8 *buf = (u8 *)bulk_ctxt->bulk_in_buf;
u8 *buf0 = buf;
bool valid_page = false;
u8 pc = ums->cmnd[2] >> 6;
u8 page_code = ums->cmnd[2] & 0x3F;
bool changeable_values = pc == 1;
bool all_pages = page_code == 0x3F;
if ((ums->cmnd[1] & ~0x08) != 0) // Mask away DBD.
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
if (pc == 3)
{
ums->lun.sense_data = SS_SAVING_PARAMETERS_NOT_SUPPORTED;
return UMS_RES_INVALID_ARG;
}
/* Write the mode parameter header. Fixed values are: default
* medium type, no cache control (DPOFUA), and no block descriptors.
* The only variable value is the WriteProtect bit. We will fill in
* the mode data length later. */
memset(buf, 0, 8);
if (ums->cmnd[0] == SC_MODE_SENSE_6)
{
buf[2] = (ums->lun.ro ? 0x80 : 0x00); // WP, DPOFUA.
buf += 4;
}
else // SC_MODE_SENSE_10.
{
buf[3] = (ums->lun.ro ? 0x80 : 0x00); // WP, DPOFUA.
buf += 8;
}
// The only page we support is the Caching page.
// What about x1C
if (page_code == 0x08 || all_pages)
{
valid_page = true;
buf[0] = 0x08; // Page code.
buf[1] = 18; // Page length.
memset(buf + 2, 0, 18); // Set all parameters to 0.
// None of the fields are changeable.
if (!changeable_values)
{
// Write Cache enable, Read Cache not disabled, Multiplication Factor off.
buf[2] = 0x04;
// Multiplication Factor is disabled, so all values below are 1x LBA.
put_array_le_to_be16(0xFFFF, &buf[4]); // Disable Prefetch if >32MB.
put_array_le_to_be16(0x0000, &buf[6]); // Minimum Prefetch 0MB.
put_array_le_to_be16(0xFFFF, &buf[8]); // Maximum Prefetch 32MB.
put_array_le_to_be16(0xFFFF, &buf[10]); // Maximum Prefetch ceiling 32MB.
}
buf += 20;
}
// Check that a valid page mode data length was requested.
u32 len = buf - buf0;
if (!valid_page)
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
// Store the mode data length.
if (ums->cmnd[0] == SC_MODE_SENSE_6)
buf0[0] = len - 1;
else
put_array_le_to_be16(len - 2, buf0);
return len;
}
static int _scsi_start_stop(usbd_gadget_ums_t *ums)
{
int loej, start;
if (!ums->lun.removable)
{
ums->lun.sense_data = SS_INVALID_COMMAND;
return UMS_RES_INVALID_ARG;
}
else if ((ums->cmnd[1] & ~0x01) != 0 || // Mask away Immed.
(ums->cmnd[4] & ~0x03) != 0) // Mask LoEj, Start.
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
loej = ums->cmnd[4] & 0x02;
start = ums->cmnd[4] & 0x01;
// We do not support re-mounting.
if (start)
{
if (ums->lun.unmounted)
{
ums->lun.sense_data = SS_MEDIUM_NOT_PRESENT;
return UMS_RES_INVALID_ARG;
}
return UMS_RES_OK;
}
// Check if we are allowed to unload the media.
if (ums->lun.prevent_medium_removal)
{
ums->set_text(ums->label, "#C7EA46 Status:# Unload attempt prevented");
ums->lun.sense_data = SS_MEDIUM_REMOVAL_PREVENTED;
return UMS_RES_INVALID_ARG;
}
if (!loej)
return UMS_RES_OK;
// Unmount means we exit UMS because of ejection.
ums->lun.unmounted = 1;
return UMS_RES_OK;
}
static int _scsi_prevent_allow_removal(usbd_gadget_ums_t *ums)
{
int prevent;
if (!ums->lun.removable)
{
ums->lun.sense_data = SS_INVALID_COMMAND;
return UMS_RES_INVALID_ARG;
}
prevent = ums->cmnd[4] & 0x01;
if ((ums->cmnd[4] & ~0x01) != 0) // Mask away Prevent.
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
// Notify for possible unmounting?
// Normally we sync here but we do synced writes to SDMMC.
if (ums->lun.prevent_medium_removal && !prevent)
;
ums->lun.prevent_medium_removal = prevent;
return UMS_RES_OK;
}
static int _scsi_read_format_capacities(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
u8 *buf = (u8 *)bulk_ctxt->bulk_in_buf;
buf[0] = buf[1] = buf[2] = 0;
buf[3] = 8; // Only the Current/Maximum Capacity Descriptor.
buf += 4;
put_array_le_to_be32(ums->lun.num_sectors, &buf[0]); // Number of blocks.
put_array_le_to_be32(UMS_DISK_LBA_SIZE, &buf[4]); // Block length.
buf[4] = 0x02; // Current capacity.
return 12;
}
// Check whether the command is properly formed and whether its data size
// and direction agree with the values we already have.
static int _ums_check_scsi_cmd(usbd_gadget_ums_t *ums, u32 cmnd_size,
enum data_direction data_dir, u32 mask, int needs_medium)
{
//const char dirletter[4] = {'u', 'o', 'i', 'n'};
DPRINTF("SCSI command: %X; Dc=%d, D%c=%X; Hc=%d, H%c=%X\n",
ums->cmnd[0], cmnd_size, dirletter[(int)ums->data_dir],
ums->data_size_from_cmnd, ums->cmnd_size,
dirletter[(int)data_dir], ums->data_size);
// We can't reply if we don't know the direction and size.
if (ums->data_size_from_cmnd == 0)
data_dir = DATA_DIR_NONE;
// This is a phase error but we continue and only transfer as much we can.
if (ums->data_size < ums->data_size_from_cmnd)
{
ums->data_size_from_cmnd = ums->data_size;
ums->phase_error = 1;
}
ums->residue = ums->data_size;
ums->usb_amount_left = ums->data_size;
if (ums->data_dir != data_dir && ums->data_size_from_cmnd > 0)
{
ums->phase_error = 1;
return UMS_RES_INVALID_ARG;
}
// Cmd length verification.
if (cmnd_size != ums->cmnd_size)
{
// Special case workaround for Windows and Xbox 360.
if (cmnd_size <= ums->cmnd_size)
cmnd_size = ums->cmnd_size;
else
{
ums->phase_error = 1;
return UMS_RES_INVALID_ARG;
}
}
// check that LUN ums->cmnd[1] >> 5 is 0 because of only one.
if (ums->cmnd[0] != SC_REQUEST_SENSE)
{
ums->lun.sense_data = SS_NO_SENSE;
ums->lun.sense_data_info = 0;
ums->lun.info_valid = 0;
}
// If a unit attention condition exists, only INQUIRY and REQUEST SENSE
// commands are allowed.
if (ums->lun.unit_attention_data != SS_NO_SENSE && ums->cmnd[0] != SC_INQUIRY &&
ums->cmnd[0] != SC_REQUEST_SENSE)
{
ums->lun.sense_data = ums->lun.unit_attention_data;
ums->lun.unit_attention_data = SS_NO_SENSE;
return UMS_RES_INVALID_ARG;
}
// Check that only command bytes listed in the mask are set.
ums->cmnd[1] &= 0x1F; // Mask away the LUN.
for (u32 i = 1; i < cmnd_size; ++i)
{
if (ums->cmnd[i] && !(mask & BIT(i)))
{
ums->lun.sense_data = SS_INVALID_FIELD_IN_CDB;
return UMS_RES_INVALID_ARG;
}
}
// If the medium isn't mounted and the command needs to access it, return an error.
if (ums->lun.unmounted && needs_medium)
{
ums->lun.sense_data = SS_MEDIUM_NOT_PRESENT;
return UMS_RES_INVALID_ARG;
}
return UMS_RES_OK;
}
static int _ums_parse_scsi_cmd(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
u32 len;
int reply = UMS_RES_INVALID_ARG;
ums->phase_error = 0;
ums->short_packet_received = 0;
switch (ums->cmnd[0])
{
case SC_INQUIRY:
ums->data_size_from_cmnd = ums->cmnd[4];
u32 mask = (1<<4);
if (ums->cmnd[1] == 1 && ums->cmnd[2] == 0x80) // Inquiry S/N.
mask = (1<<1) | (1<<2) | (1<<4);
reply = _ums_check_scsi_cmd(ums, 6, DATA_DIR_TO_HOST, mask, 0);
if (reply == 0)
reply = _scsi_inquiry(ums, bulk_ctxt);
break;
case SC_LOG_SENSE:
ums->data_size_from_cmnd = get_array_be_to_le16(&ums->cmnd[7]);
reply = _ums_check_scsi_cmd(ums, 10, DATA_DIR_TO_HOST, (1<<1) | (1<<2) | (3<<7), 0);
if (reply == 0)
reply = _scsi_log_sense(ums, bulk_ctxt);
break;
case SC_MODE_SELECT_6:
ums->data_size_from_cmnd = ums->cmnd[4];
reply = _ums_check_scsi_cmd(ums, 6, DATA_DIR_FROM_HOST, (1<<1) | (1<<4), 0);
if (reply == 0)
{
// We don't support MODE SELECT.
ums->lun.sense_data = SS_INVALID_COMMAND;
reply = UMS_RES_INVALID_ARG;
}
break;
case SC_MODE_SELECT_10:
ums->data_size_from_cmnd = get_array_be_to_le16(&ums->cmnd[7]);
reply = _ums_check_scsi_cmd(ums, 10, DATA_DIR_FROM_HOST, (1<<1) | (3<<7), 0);
if (reply == 0)
{
// We don't support MODE SELECT.
ums->lun.sense_data = SS_INVALID_COMMAND;
reply = UMS_RES_INVALID_ARG;
}
break;
case SC_MODE_SENSE_6:
ums->data_size_from_cmnd = ums->cmnd[4];
reply = _ums_check_scsi_cmd(ums, 6, DATA_DIR_TO_HOST, (1<<1) | (1<<2) | (1<<4), 0);
if (reply == 0)
reply = _scsi_mode_sense(ums, bulk_ctxt);
break;
case SC_MODE_SENSE_10:
ums->data_size_from_cmnd = get_array_be_to_le16(&ums->cmnd[7]);
reply = _ums_check_scsi_cmd(ums, 10, DATA_DIR_TO_HOST, (1<<1) | (1<<2) | (3<<7), 0);
if (reply == 0)
reply = _scsi_mode_sense(ums, bulk_ctxt);
break;
case SC_PREVENT_ALLOW_MEDIUM_REMOVAL:
ums->data_size_from_cmnd = 0;
reply = _ums_check_scsi_cmd(ums, 6, DATA_DIR_NONE, (1<<4), 0);
if (reply == 0)
reply = _scsi_prevent_allow_removal(ums);
break;
case SC_READ_6:
len = ums->cmnd[4];
ums->data_size_from_cmnd = (len == 0 ? 256 : len) << UMS_DISK_LBA_SHIFT;
reply = _ums_check_scsi_cmd(ums, 6, DATA_DIR_TO_HOST, (7<<1) | (1<<4), 1);
if (reply == 0)
reply = _scsi_read(ums, bulk_ctxt);
break;
case SC_READ_10:
ums->data_size_from_cmnd = get_array_be_to_le16(&ums->cmnd[7]) << UMS_DISK_LBA_SHIFT;
reply = _ums_check_scsi_cmd(ums, 10, DATA_DIR_TO_HOST, (1<<1) | (0xf<<2) | (3<<7), 1);
if (reply == 0)
reply = _scsi_read(ums, bulk_ctxt);
break;
case SC_READ_12:
ums->data_size_from_cmnd = get_array_be_to_le32(&ums->cmnd[6]) << UMS_DISK_LBA_SHIFT;
reply = _ums_check_scsi_cmd(ums, 12, DATA_DIR_TO_HOST, (1<<1) | (0xf<<2) | (0xf<<6), 1);
if (reply == 0)
reply = _scsi_read(ums, bulk_ctxt);
break;
case SC_READ_CAPACITY:
ums->data_size_from_cmnd = 8;
reply = _ums_check_scsi_cmd(ums, 10, DATA_DIR_TO_HOST, (0xf<<2) | (1<<8), 1);
if (reply == 0)
reply = _scsi_read_capacity(ums, bulk_ctxt);
break;
case SC_READ_FORMAT_CAPACITIES:
ums->data_size_from_cmnd = get_array_be_to_le16(&ums->cmnd[7]);
reply = _ums_check_scsi_cmd(ums, 10, DATA_DIR_TO_HOST, (3<<7), 1);
if (reply == 0)
reply = _scsi_read_format_capacities(ums, bulk_ctxt);
break;
case SC_REQUEST_SENSE:
ums->data_size_from_cmnd = ums->cmnd[4];
reply = _ums_check_scsi_cmd(ums, 6, DATA_DIR_TO_HOST, (1<<4), 0);
if (reply == 0)
reply = _scsi_request_sense(ums, bulk_ctxt);
break;
case SC_START_STOP_UNIT:
ums->data_size_from_cmnd = 0;
reply = _ums_check_scsi_cmd(ums, 6, DATA_DIR_NONE, (1<<1) | (1<<4), 0);
if (reply == 0)
reply = _scsi_start_stop(ums);
break;
case SC_SYNCHRONIZE_CACHE:
ums->data_size_from_cmnd = 0;
reply = _ums_check_scsi_cmd(ums, 10, DATA_DIR_NONE, (0xf<<2) | (3<<7), 1);
if (reply == 0)
reply = 0; // Don't bother
break;
case SC_TEST_UNIT_READY:
ums->data_size_from_cmnd = 0;
reply = _ums_check_scsi_cmd(ums, 6, DATA_DIR_NONE, 0, 1);
break;
// This command is used by Windows. We support a minimal version and BytChk must be 0.
case SC_VERIFY:
ums->data_size_from_cmnd = 0;
reply = _ums_check_scsi_cmd(ums, 10, DATA_DIR_NONE, (1<<1) | (0xf<<2) | (3<<7), 1);
if (reply == 0)
reply = _scsi_verify(ums, bulk_ctxt);
break;
case SC_WRITE_6:
len = ums->cmnd[4];
ums->data_size_from_cmnd = (len == 0 ? 256 : len) << UMS_DISK_LBA_SHIFT;
reply = _ums_check_scsi_cmd(ums, 6, DATA_DIR_FROM_HOST, (7<<1) | (1<<4), 1);
if (reply == 0)
reply = _scsi_write(ums, bulk_ctxt);
break;
case SC_WRITE_10:
ums->data_size_from_cmnd = get_array_be_to_le16(&ums->cmnd[7]) << UMS_DISK_LBA_SHIFT;
reply = _ums_check_scsi_cmd(ums, 10, DATA_DIR_FROM_HOST, (1<<1) | (0xf<<2) | (3<<7), 1);
if (reply == 0)
reply = _scsi_write(ums, bulk_ctxt);
break;
case SC_WRITE_12:
ums->data_size_from_cmnd = get_array_be_to_le32(&ums->cmnd[6]) << UMS_DISK_LBA_SHIFT;
reply = _ums_check_scsi_cmd(ums, 12, DATA_DIR_FROM_HOST, (1<<1) | (0xf<<2) | (0xf<<6), 1);
if (reply == 0)
reply = _scsi_write(ums, bulk_ctxt);
break;
// Mandatory commands that we don't implement. No need.
case SC_READ_HEADER:
case SC_READ_TOC:
case SC_FORMAT_UNIT:
case SC_RELEASE:
case SC_RESERVE:
case SC_SEND_DIAGNOSTIC:
default:
ums->data_size_from_cmnd = 0;
reply = _ums_check_scsi_cmd(ums, ums->cmnd_size, DATA_DIR_UNKNOWN, 0xFF, 0);
if (reply == 0)
{
ums->lun.sense_data = SS_INVALID_COMMAND;
reply = UMS_RES_INVALID_ARG;
}
break;
}
if (reply == UMS_RES_INVALID_ARG)
reply = 0; // Error reply length.
// Set up reply buffer for finish_reply(). Otherwise it's already set.
if (reply >= 0 && ums->data_dir == DATA_DIR_TO_HOST)
{
reply = MIN((u32)reply, ums->data_size_from_cmnd);
bulk_ctxt->bulk_in_length = reply;
bulk_ctxt->bulk_in_buf_state = BUF_STATE_FULL;
ums->residue -= reply;
}
return UMS_RES_OK;
}
static int pad_with_zeros(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
bulk_ctxt->bulk_in_buf_state = BUF_STATE_EMPTY; // For the first iteration.
u32 current_len_to_keep = bulk_ctxt->bulk_in_length;
ums->usb_amount_left = current_len_to_keep + ums->residue;
while (ums->usb_amount_left > 0)
{
u32 nsend = MIN(ums->usb_amount_left, USB_EP_BUFFER_MAX_SIZE);
memset(bulk_ctxt->bulk_in_buf + current_len_to_keep, 0, nsend - current_len_to_keep);
bulk_ctxt->bulk_in_length = nsend;
_ums_transfer_start(ums, bulk_ctxt, bulk_ctxt->bulk_in, USB_XFER_SYNCED_DATA);
ums->usb_amount_left -= nsend;
current_len_to_keep = 0;
}
return UMS_RES_OK;
}
static int throw_away_data(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
if (bulk_ctxt->bulk_out_buf_state != BUF_STATE_EMPTY || ums->usb_amount_left > 0)
{
// Try to submit another request if we need one.
if (bulk_ctxt->bulk_out_buf_state == BUF_STATE_EMPTY && ums->usb_amount_left > 0)
{
u32 amount = MIN(ums->usb_amount_left, USB_EP_BUFFER_MAX_SIZE);
bulk_ctxt->bulk_out_length = amount;
_ums_transfer_start(ums, bulk_ctxt, bulk_ctxt->bulk_out, USB_XFER_SYNCED_DATA);
ums->usb_amount_left -= amount;
return UMS_RES_OK;
}
// Throw away the data in a filled buffer.
if (bulk_ctxt->bulk_out_buf_state == BUF_STATE_FULL)
bulk_ctxt->bulk_out_buf_state = BUF_STATE_EMPTY;
// A short packet or an error ends everything.
if (bulk_ctxt->bulk_out_length_actual != bulk_ctxt->bulk_out_length ||
bulk_ctxt->bulk_out_status != USB_RES_OK)
{
raise_exception(ums, UMS_STATE_ABORT_BULK_OUT);
return UMS_RES_PROT_FATAL;
}
}
return UMS_RES_OK;
}
static int finish_reply(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
int rc = UMS_RES_OK;
switch (ums->data_dir) {
case DATA_DIR_NONE:
break; // Nothing to send.
// If this is a CB or CBI with an unknown command, we mustn't
// try to send or receive any data. Stall if we can and wait reset.
case DATA_DIR_UNKNOWN:
if (ums->can_stall)
{
ums_set_stall(bulk_ctxt->bulk_out);
rc = ums_set_stall(bulk_ctxt->bulk_in);
ums->set_text(ums->label, "#FFDD00 Error:# Direction unknown. Stalled both EP!");
} // Else do nothing.
break;
// All but the last buffer of data have already been sent.
case DATA_DIR_TO_HOST:
if (ums->data_size)
{
// If there's no residue, simply send the last buffer.
if (!ums->residue)
{
_ums_transfer_start(ums, bulk_ctxt, bulk_ctxt->bulk_in, USB_XFER_SYNCED_DATA);
/* For Bulk-only, if we're allowed to stall then send the
* short packet and halt the bulk-in endpoint. If we can't
* stall, pad out the remaining data with 0's. */
}
else if (ums->can_stall)
{
_ums_transfer_start(ums, bulk_ctxt, bulk_ctxt->bulk_in, USB_XFER_SYNCED_DATA);
rc = ums_set_stall(bulk_ctxt->bulk_in);
ums->set_text(ums->label, "#FFDD00 Error:# Residue. Stalled EP IN!");
}
else
rc = pad_with_zeros(ums, bulk_ctxt);
}
// In case we used SDMMC transfer, reset the buffer address.
_ums_reset_buffer(bulk_ctxt, bulk_ctxt->bulk_in);
break;
// We have processed all we want from the data the host has sent.
// There may still be outstanding bulk-out requests.
case DATA_DIR_FROM_HOST:
if (ums->residue)
{
if (ums->short_packet_received) // Did the host stop sending unexpectedly early?
{
raise_exception(ums, UMS_STATE_ABORT_BULK_OUT);
rc = UMS_RES_PROT_FATAL;
}
else // We can't stall. Read in the excess data and throw it away.
rc = throw_away_data(ums, bulk_ctxt);
}
break;
}
return rc;
}
/*
* Medium ejection heuristics.
*
* Windows:
* Uses Start/Stop Unit. Only Stop with LoEj. Observed ONLY on very specific windows machines.
* Uses Prevent/Allow Medium Removal. (For big reads and ANY write.) //////Except trivial writes. Needs check with prefetch ON
* Sends Test Unit Ready every 1s at idle. (Needs 1 EP Timeout protection: 2s)
* Does not send data when ejects. In the case it does,
* it loops into Request Sense and Test Unit Ready when ejects.
* Line always at SE0 and only goes in J-State when it ejects.
*
* Linux:
* Uses Start/Stop Unit. Stops with LoEj when Media prevention is off.
* Uses Prevent/Allow Medium Removal. (For big read and any write.)
* Sends Test Unit Ready every 2s at idle. (Needs 2 EP Timeouts protection: 4s)
* Loops into Request Sense and Test Unit Ready when ejects.
* Line always at SE0.
*
* Mac OS:
* Uses Start/Stop. Stops with LoEj when Allow Medium Removal is enabled.
* Uses Prevent/Allow Medium Removal. (Properly. Enables at mount and only disables it when ejects.)
* Does not send Test Unit Ready at idle. But Prevent Medium Removal is enabled.
* Loops into Request Sense and Test Unit Ready when ejects.
* Line always at SE0.
*/
static int received_cbw(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
// Was this a real packet? Should it be ignored?
if (bulk_ctxt->bulk_out_status || bulk_ctxt->bulk_out_ignore || ums->lun.unmounted)
{
if (bulk_ctxt->bulk_out_status || ums->lun.unmounted)
{
DPRINTF("USB: EP timeout (%d)\n", bulk_ctxt->bulk_out_status);
// In case we disconnected, exit UMS.
// Raise timeout if removable and didn't got a unit ready command inside 4s.
if (bulk_ctxt->bulk_out_status == USB2_ERROR_XFER_EP_DISABLED ||
(bulk_ctxt->bulk_out_status == USB_ERROR_TIMEOUT && ums->lun.removable && !ums->lun.prevent_medium_removal))
{
if (bulk_ctxt->bulk_out_status == USB_ERROR_TIMEOUT)
{
if (usb_ops.usb_device_get_port_in_sleep())
{
ums->set_text(ums->label, "#C7EA46 Status:# EP in sleep");
ums->timeouts += 14;
}
else if (!ums->xusb) // Timeout only on USB2.
{
ums->timeouts += 4;
DPRINTF("USB: EP removable\n");
}
}
else
{
gfx_printf("USB: EP disabled\n");
msleep(500);
ums->timeouts += 4;
}
}
if (ums->lun.unmounted)
{
ums->set_text(ums->label, "#C7EA46 Status:# Medium unmounted");
ums->timeouts++;
if (!bulk_ctxt->bulk_out_status)
ums->timeouts += 3;
}
if (ums->timeouts > 20)
raise_exception(ums, UMS_STATE_EXIT);
}
if (bulk_ctxt->bulk_out_status || bulk_ctxt->bulk_out_ignore)
return UMS_RES_INVALID_ARG;
}
// Clear request flag to allow a new one to be queued.
ums->cbw_req_queued = false;
// Is the CBW valid?
bulk_recv_pkt_t *cbw = (bulk_recv_pkt_t *)bulk_ctxt->bulk_out_buf;
if (bulk_ctxt->bulk_out_length_actual != USB_BULK_CB_WRAP_LEN || cbw->Signature != USB_BULK_CB_SIG)
{
gfx_printf("USB: invalid CBW: len %X sig 0x%X\n", bulk_ctxt->bulk_out_length_actual, cbw->Signature);
/*
* The Bulk-only spec says we MUST stall the IN endpoint
* (6.6.1), so it's unavoidable. It also says we must
* retain this state until the next reset, but there's
* no way to tell the controller driver it should ignore
* Clear-Feature(HALT) requests.
*
* We aren't required to halt the OUT endpoint; instead
* we can simply accept and discard any data received
* until the next reset.
*/
ums_wedge_bulk_in_endpoint(ums);
bulk_ctxt->bulk_out_ignore = 1;
return UMS_RES_INVALID_ARG;
}
// Is the CBW meaningful?
if (cbw->Lun >= UMS_MAX_LUN || cbw->Flags & ~USB_BULK_IN_FLAG ||
cbw->Length == 0 || cbw->Length > SCSI_MAX_CMD_SZ)
{
gfx_printf("USB: non-meaningful CBW: lun = %X, flags = 0x%X, cmdlen %X\n",
cbw->Lun, cbw->Flags, cbw->Length);
/* We can do anything we want here, so let's stall the
* bulk pipes if we are allowed to. */
if (ums->can_stall)
{
ums_set_stall(bulk_ctxt->bulk_out);
ums_set_stall(bulk_ctxt->bulk_in);
ums->set_text(ums->label, "#FFDD00 Error:# CBW unknown - Stalled both EP!");
}
return UMS_RES_INVALID_ARG;
}
// Save the command for later.
ums->cmnd_size = cbw->Length;
memcpy(ums->cmnd, cbw->CDB, ums->cmnd_size);
if (cbw->Flags & USB_BULK_IN_FLAG)
ums->data_dir = DATA_DIR_TO_HOST;
else
ums->data_dir = DATA_DIR_FROM_HOST;
ums->data_size = cbw->DataTransferLength;
if (ums->data_size == 0)
ums->data_dir = DATA_DIR_NONE;
ums->lun_idx = cbw->Lun;
ums->tag = cbw->Tag;
if (!ums->lun.unmounted)
ums->timeouts = 0;
return UMS_RES_OK;
}
static int get_next_command(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
int rc = UMS_RES_OK;
/* Wait for the next buffer to become available */
// while (bulk_ctxt->bulk_out_buf_state != BUF_STATE_EMPTY)
// {
// //wait irq.
// }
bulk_ctxt->bulk_out_length = USB_BULK_CB_WRAP_LEN;
// Queue a request to read a Bulk-only CBW.
if (!ums->cbw_req_queued)
_ums_transfer_start(ums, bulk_ctxt, bulk_ctxt->bulk_out, USB_XFER_SYNCED_CMD);
else
_ums_transfer_finish(ums, bulk_ctxt, bulk_ctxt->bulk_out, USB_XFER_SYNCED_CMD);
/*
* On XUSB do not allow multiple requests for CBW to be done.
* This avoids an issue with some XHCI controllers and OS combos (e.g. ASMedia and Linux/Mac OS)
* which confuse that and concatenate an old CBW request with another write request (SCSI Write)
* and create a babble error (transmit overflow).
*/
if (ums->xusb)
ums->cbw_req_queued = true;
/* We will drain the buffer in software, which means we
* can reuse it for the next filling. No need to advance
* next_buffhd_to_fill. */
/* Wait for the CBW to arrive */
// while (bulk_ctxt->bulk_out_buf_state != BUF_STATE_FULL)
// {
// //wait irq.
// }
rc = received_cbw(ums, bulk_ctxt);
bulk_ctxt->bulk_out_buf_state = BUF_STATE_EMPTY;
return rc;
}
static void send_status(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
u8 status = USB_STATUS_PASS;
u32 sd = ums->lun.sense_data;
if (ums->phase_error)
{
ums->set_text(ums->label, "#FFDD00 Error:# Phase-error!");
status = USB_STATUS_PHASE_ERROR;
sd = SS_INVALID_COMMAND;
}
else if (sd != SS_NO_SENSE)
{
DPRINTF("USB: CMD fail\n");
status = USB_STATUS_FAIL;
DPRINTF("USB: Sense: SK x%02X, ASC x%02X, ASCQ x%02X; info x%X\n",
SK(sd), ASC(sd), ASCQ(sd), ums->lun.sense_data_info);
}
// Store and send the Bulk-only CSW.
bulk_send_pkt_t *csw = (bulk_send_pkt_t *)bulk_ctxt->bulk_in_buf;
csw->Signature = USB_BULK_CS_SIG;
csw->Tag = ums->tag;
csw->Residue = ums->residue;
csw->Status = status;
bulk_ctxt->bulk_in_length = USB_BULK_CS_WRAP_LEN;
_ums_transfer_start(ums, bulk_ctxt, bulk_ctxt->bulk_in, USB_XFER_SYNCED_CMD);
}
static void handle_exception(usbd_gadget_ums_t *ums, bulk_ctxt_t *bulk_ctxt)
{
enum ums_state old_state;
// Clear out the controller's fifos.
ums_flush_endpoint(bulk_ctxt->bulk_in);
ums_flush_endpoint(bulk_ctxt->bulk_out);
/* Reset the I/O buffer states and pointers, the SCSI
* state, and the exception. Then invoke the handler. */
bulk_ctxt->bulk_in_buf_state = BUF_STATE_EMPTY;
bulk_ctxt->bulk_out_buf_state = BUF_STATE_EMPTY;
old_state = ums->state;
if (old_state != UMS_STATE_ABORT_BULK_OUT)
{
ums->lun.prevent_medium_removal = 0;
ums->lun.sense_data = SS_NO_SENSE;
ums->lun.unit_attention_data = SS_NO_SENSE;
ums->lun.sense_data_info = 0;
ums->lun.info_valid = 0;
}
ums->state = UMS_STATE_NORMAL;
// Carry out any extra actions required for the exception.
switch (old_state)
{
case UMS_STATE_NORMAL:
break;
case UMS_STATE_ABORT_BULK_OUT:
send_status(ums, bulk_ctxt);
break;
case UMS_STATE_PROTOCOL_RESET:
/* In case we were forced against our will to halt a
* bulk endpoint, clear the halt now. (The SuperH UDC
* requires this.) */
if (bulk_ctxt->bulk_out_ignore)
{
bulk_ctxt->bulk_out_ignore = 0;
ums_clear_stall(bulk_ctxt->bulk_in);
}
ums->lun.unit_attention_data = SS_RESET_OCCURRED;
break;
case UMS_STATE_EXIT:
ums->state = UMS_STATE_TERMINATED; // Stop the thread.
break;
default:
break;
}
}
static inline void _system_maintainance(usbd_gadget_ums_t *ums)
{
static u32 timer_dram = 0;
static u32 timer_status_bar = 0;
u32 time = get_tmr_ms();
if (timer_status_bar < time)
{
ums->system_maintenance(true);
timer_status_bar = get_tmr_ms() + 30000;
}
else if (timer_dram < time)
{
minerva_periodic_training();
timer_dram = get_tmr_ms() + EMC_PERIODIC_TRAIN_MS;
}
}
int usb_device_gadget_ums(usb_ctxt_t *usbs)
{
int res = 0;
sdmmc_t sdmmc;
sdmmc_storage_t storage;
usbd_gadget_ums_t ums = {0};
// Get USB Controller ops.
if (hw_get_chip_id() == GP_HIDREV_MAJOR_T210)
usb_device_get_ops(&usb_ops);
else
{
ums.xusb = true;
xusb_device_get_ops(&usb_ops);
}
usbs->set_text(usbs->label, "#C7EA46 Status:# Started USB");
if (usb_ops.usb_device_init())
{
usb_ops.usbd_end(false, true);
return 1;
}
ums.state = UMS_STATE_NORMAL;
ums.can_stall = 0;
ums.bulk_ctxt.bulk_in = USB_EP_BULK_IN;
ums.bulk_ctxt.bulk_in_buf = (u8 *)USB_EP_BULK_IN_BUF_ADDR;
ums.bulk_ctxt.bulk_out = USB_EP_BULK_OUT;
ums.bulk_ctxt.bulk_out_buf = (u8 *)USB_EP_BULK_OUT_BUF_ADDR;
// Set LUN parameters.
ums.lun.ro = usbs->ro;
ums.lun.type = usbs->type;
ums.lun.partition = usbs->partition;
ums.lun.offset = usbs->offset;
ums.lun.removable = 1; // Always removable to force OSes to use prevent media removal.
ums.lun.unit_attention_data = SS_RESET_OCCURRED;
// Set system functions
ums.label = usbs->label;
ums.set_text = usbs->set_text;
ums.system_maintenance = usbs->system_maintenance;
ums.set_text(ums.label, "#C7EA46 Status:# Mounting disk");
// Initialize sdmmc.
if (usbs->type == MMC_SD)
{
sd_end();
sd_mount();
sd_unmount();
ums.lun.sdmmc = &sd_sdmmc;
ums.lun.storage = &sd_storage;
}
else
{
ums.lun.sdmmc = &sdmmc;
ums.lun.storage = &storage;
sdmmc_storage_init_mmc(ums.lun.storage, ums.lun.sdmmc, SDMMC_BUS_WIDTH_8, SDHCI_TIMING_MMC_HS400);
sdmmc_storage_set_mmc_partition(ums.lun.storage, ums.lun.partition - 1);
}
ums.set_text(ums.label, "#C7EA46 Status:# Waiting for connection");
// Initialize Control Endpoint.
if (usb_ops.usb_device_enumerate(USB_GADGET_UMS))
goto error;
ums.set_text(ums.label, "#C7EA46 Status:# Waiting for LUN");
if (usb_ops.usb_device_class_send_max_lun(0)) // One device for now.
goto error;
ums.set_text(ums.label, "#C7EA46 Status:# Started UMS");
if (usbs->sectors)
ums.lun.num_sectors = usbs->sectors;
else
ums.lun.num_sectors = ums.lun.storage->sec_cnt;
do
{
// Do DRAM training and update system tasks.
_system_maintainance(&ums);
// Check for force unmount button combo.
if (btn_read_vol() == (BTN_VOL_UP | BTN_VOL_DOWN))
{
// Check if we are allowed to unload the media.
if (ums.lun.prevent_medium_removal)
ums.set_text(ums.label, "#C7EA46 Status:# Unload attempt prevented");
else
break;
}
if (ums.state != UMS_STATE_NORMAL)
{
handle_exception(&ums, &ums.bulk_ctxt);
continue;
}
ums_handle_ep0_ctrl(&ums);
if (get_next_command(&ums, &ums.bulk_ctxt) || (ums.state > UMS_STATE_NORMAL))
continue;
ums_handle_ep0_ctrl(&ums);
if (_ums_parse_scsi_cmd(&ums, &ums.bulk_ctxt) || (ums.state > UMS_STATE_NORMAL))
continue;
ums_handle_ep0_ctrl(&ums);
if (finish_reply(&ums, &ums.bulk_ctxt) || (ums.state > UMS_STATE_NORMAL))
continue;
send_status(&ums, &ums.bulk_ctxt);
} while (ums.state != UMS_STATE_TERMINATED);
if (ums.lun.prevent_medium_removal)
ums.set_text(ums.label, "#FFDD00 Error:# Disk unsafely ejected");
else
ums.set_text(ums.label, "#C7EA46 Status:# Disk ejected");
goto exit;
error:
ums.set_text(ums.label, "#FFDD00 Error:# Timed out or canceled!");
res = 1;
exit:
if (ums.lun.type == MMC_EMMC)
sdmmc_storage_end(ums.lun.storage);
usb_ops.usbd_end(true, false);
return res;
}