参考文档:UFI Command Specification、SCSI Reference Guide
CBW包中CB部分(0FH-1EH)的第一个字节是操作码,对于Mass Storage来说,使用的SCSI协议定义了其具体意义。常用的命令如下:
1. SCSI_TEST_UNIT_READY(0x00)
作用是查询设备是否ready。空闲的时候,主机就会发送test ready命令下来,这个命令不用回复和应答数据,如果已经Ready,只要回复个CSW即可。如果设备无法运行或处于需要主机操作以准备就绪的状态,设备应返回检查条件状态,检测键为NOT ready。
void scsiCmdTestUnitReady(uint8_t lun)
{
if(massMalGetStatus(lun) > 0)
{
scsiSetSenseData(gMscCBW.bLun, SCSI_NOT_READY, SCSI_ASC_MEDIUM_NOT_PRESENT);
mscBotSendCSW(MSC_BOT_CSW_CMD_FAILED, MSC_BOT_SEND_CSW_ENABLE);
mscBotAbort(MSC_DIR_IN);
return;
}
mscBotSendCSW(MSC_BOT_CSW_CMD_PASSED, MSC_BOT_SEND_CSW_ENABLE);
}
CSW的数据格式如下图:
前面4个字节0x53425355表示CSW的标志字,接下来的4个字节是Tag,然后是4个字节的dDataResidue,表示剩余数据长度,最后一个字节是Status。
2. SCSI_REQUEST_SENSE(0x03)
设备将指定LUN的SENSE数据发送给HOST。HOST发送的命令数据格式如下表:
Allocation Length: 指定HOST可以接收的Sense数据的最大字节数。如果这小于Sense数据的大小,设备将只返回请求的字节数。
返回的Sense数据的格式如下图所示:
uint8_t gScsiSenseData[] =
{
0x70, /*RespCode*/
0x00, /*SegmentNumber*/
SCSI_NO_SENSE, /* Sens_Key*/
0x00,
0x00,
0x00,
0x00, /*Information*/
0x0A, /*AdditionalSenseLength*/
0x00,
0x00,
0x00,
0x00, /*CmdInformation*/
SCSI_NO_SENSE, /*Asc*/
0x00, /*ASCQ*/
0x00, /*FRUC*/
0x00, /*TBD*/
0x00,
0x00 /*SenseKeySpecific*/
};
设置Sense数据的函数如下:
void scsiSetSenseData(uint8_t lun, uint8_t sensKey, uint8_t asc)
{
gScsiSenseData[2] = sensKey;
gScsiSenseData[12] = asc;
}
发送Sense数据:
void scsiCmdRequestSense(uint8_t lun)
{
uint8_t len;
len = (gMscCBW.CB[4] > sizeof(gScsiSenseData)) ? sizeof(gScsiSenseData) : gMscCBW.CB[4];
mscBotSendData(gScsiSenseData, len);
}
3. SCSI_FORMAT_UNIT(0x04)
HOST发送格式化单元命令。命令格式如下表:
FmtData必须为1,CmpList必须为0,Defect List Format必须为7. 如果不是这些值,Sense Key = ILLEGAL REQUEST,Sense Code = INVALID FIELD IN COMMAND PACKET。
Track Number: 指定要格式化的Track。此参数对HD和磁盘的单Track格式有效。
Interleave: 指定格式化会用到的交错功能。
0: 使用默认交错。对于USB-FDU,这是1:1。
1: 使用1:1的交错。
交错为1意味着连续的逻辑块将按连续的升序排列。
Parameter List Length: 参数列表中的字节数。HOST在发送FORMAT UNIT命令后,将参数列表发送到Bulk Out端点上的设备。对于FORMAT UNIT命令,长度为零不是错误。参数列表长度通常为4+8字节。
【Format Parameter List】
在发送命令包后,HOST应在Bulk Out端点输出格式参数列表。格式参数列表可能包括:
Defect List Header(4字节) + Format Descriptor(8字节)
FOV:Format Options Valid的缩写,格式化参数有效位,当DCRT = 1或Immediate = 1时,FOV设置为1.
Extend:一直为0
DCRT:Disable Certification的缩写,禁止证书。如果设置为1,设备不会认证此磁盘。对于USB Mass Storage,这个位设置为1.
Single Track:设置为1时,表示仅格式化Track Number指定的Track。只有在格式化兼容的HD或DD软盘时才需要该位。
Immediate:当设置为1时,该命令应立即返回状态。USB Mass Storage不支持这个设置,如果设置为1,则Sense Key = ILLEGAL REQUEST & Sense Code = ILLEGAL FIELD IN PARAMETER LIST。
Side:指定使用Single Track格式时哪一面格式化。如果Side = 1,Top Side被格式化;Side = 0, Bottom Side被格式化。
Defect List Length:值默认为8。如果不是8,Sense Key = ILLEGAL REQUEST & Sense Code = ILLEGAL FIELD IN PARAMETER LIST。
4. SCSI_INQUIRY(0x12)
INQUIRY命令请求将有关设备本身参数的信息发送到主机。
EVPD: Enable Vital Product Data的缩写,使能必不可少的产品数据。
Logical Unit Number: 有效值0-7.
Page Code:设备仅支持页面代码零(00h),即标准查询数据。
Allocation Length:指定要返回的查询数据的最大字节数。值为零不会导致错误。
标准INQUIRY数据格式:
Peripheral Device Type:标识当前连接到请求的逻辑单元的设备。
00h:直接存取设备
1Fh: none(没有连接到请求的逻辑单元的FDD)
RMB:Removable Media Bit的缩写,设置为1表示可移动磁盘。
ISO Version:为0
ECMA Version:为0
ANSI Version:为2,表示非标准。
Response Data Format:
Additional Length:指定参数的字节长度。
Vendor Identification:制造商信息,默认8字节长。
Product Identification:产品信息,默认16字节长。
命令实现的代码如下:
void scsiCmdInquiry(uint8_t lun)
{
uint8_t *buf;
uint16_t len;
if( (gMscCBW.CB[1] & 0x01) > 0)//EVPD > 0
{
buf = (uint8_t *)gScsiInquiryPage00Data;
len = SCSI_INQUIRYPAGE00_DATA_LEN;
}
else
{
buf = (uint8_t *)gScsiInquiryData;
len = (gMscCBW.CB[4] > SCSI_INQUIRY_DATA_LEN) ? SCSI_INQUIRY_DATA_LEN : gMscCBW.CB[4];
}
mscBotSendData(buf, len);
}
const uint8_t gScsiInquiryPage00Data[] =
{
0x00, /* PERIPHERAL QUALIFIER & PERIPHERAL DEVICE TYPE*/
0x00,
0x00,
0x00,
0x00 /* Supported Pages 00*/
};
const uint8_t gScsiInquiryData[] =
{
0x00, /* Direct Access Device */
0x80, /* RMB = 1: Removable Medium */
0x02, /* Version: No conformance claim to standard */
0x02,
31, /* Additional Length */
0x00, /* SCCS = 1: Storage Controller Component */
0x00,
0x00,
/* Vendor Identification */
'S', 'T', 'M', ' ', ' ', ' ', ' ', ' ',
/* Product Identification */
'R', 'A', 'M', ' ', 'D', 'i', 's', 'k',
' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
/* Product Revision Level */
'1', '.', '0', ' '
};
5. SCSI_START_STOP_UNIT(0x1B)
启用或禁用介质访问操作。
IMMED: 是否立即执行
Start: 1表示启用介质访问操作;0表示禁止。
LoEj: Load Eject,1表示当Start = 0时弹出介质;0表示不弹出介质。
这里不支持这个功能,直接返回PASSED状态。
void scsiCmdStartStopUnit(uint8_t lun)
{
mscBotSendCSW(MSC_BOT_CSW_CMD_PASSED, MSC_BOT_SEND_CSW_ENABLE);
}
6. SCSI_ALLOW_MEDIUM_REMOVAL(0x1E)
允许或禁用移除逻辑单元中的介质。
Prevent:0表示允许介质的移除;1表示禁止介质的移除。
如果设备不支持锁定功能,介质将始终处于解锁状态。在这种情况下,(Prevent=0)命令将成功完成,Sense Key = NO sense,而(Prevent=1)命令将导致错误,Sense Key = ILLEGAL REQUEST & Sense Code = ILLEGAL FIELD IN PARAMETER LIST。
此命令这里和SCSI_START_STOP_UNIT一样调用scsiCmdStartStopUnit,即不支持这个功能。
case SCSI_ALLOW_MEDIUM_REMOVAL:
scsiCmdStartStopUnit(gMscCBW.bLUN);
break;
7. SCSI_MODE_SENSE6(0x1A)SCSI_MODE_SENSE10(0x5A)
LLBA(仅10字节命令): Large LBA? 值为0.
DBD: Disable Block Descriptor的缩写。值为0.
Page Control:
0h(00b)= 当前值。设备将当前的参数给请求的page,然后返回该page。
1h(01b)= 可变参数值。指示程序可以更改哪些参数(1表示可更改的参数,0表示不可更改的参数)。
2h(10b)= 默认值。将默认的参数给请求的page。
3h(11b)= 保存的值。此选项仅对可保存的模式页有效。如果无法保存模式页,则返回检查条件。
Page Code: 指定要返回的模式页。指定3Fh请求所有模式页。在这种情况下,模式页应按升序页代码顺序返回,模式页00h除外。
SubPage Code: 指定库返回的模式子页。
Allocation Length: 指定要返回数据的最大字节数。
const uint8_t gScsiModeSense6data[] =
{
0x03,
0x00,
0x00,
0x00,
};
void scsiCmdModeSense6(uint8_t lun)
{
mscBotSendData((uint8_t *)gScsiModeSense6data, SCSI_MODE_SENSE6_DATA_LEN);
}
const uint8_t gScsiModeSense10data[] =
{
0x00,
0x06,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00
};
void scsiCmdModeSense10(uint8_t lun)
{
mscBotSendData((uint8_t *)gScsiModeSense10data, SCSI_MODE_SENSE10_DATA_LEN);
}
8. SCSI_READ_FORMAT_CAPACITIES(0x23)
READ FORMAT CAPACITIES命令是HOST请求在当前安装的介质上可以格式化的可能容量的列表。如果当前没有安装介质,UFI设备应返回设备可以格式化的最大容量。
设备返回容量列表给HOST,其结构如下:
容量列表分2种情况,
无介质的情况:Capacity List Header + Maximum Capacity Header(这里只考虑这种情况)
有介质的情况:Capacity List Header + Current Capacity Header + Formattable Capacity Descriptors
Capacity List Header:4个字节长度,前面3个字节保留,第4个自己表示容量列表的长度(不包括这4个字节)。
Current/Maximum Capacity Descriptor:描述了当前介质容量/设备支持的格式化最大容量
Number of Blocks:可寻址块的数量
Block Length:指定返回到主机的描述符的类型。
void scsiCmdReadFormatCapacity(uint8_t lun)
{
uint8_t readFormatCapacityData[] =
{
0x00,
0x00,
0x00,
0x08, /* Capacity List Length */
/* Block Count */
0,
0,
0,
0,
/* Block Length */
0x02,/* Descriptor Code: Formatted Media */
0,
0,
0
};
if(massMalGetStatus(lun) > 0)
{
scsiSetSenseData(gMscCBW.bLUN, SCSI_NOT_READY, SCSI_ASC_MEDIUM_NOT_PRESENT);
mscBotSendCSW(MSC_BOT_CSW_CMD_FAILED, MSC_BOT_SEND_CSW_ENABLE);
mscBotAbort(MSC_DIR_IN);
return;
}
readFormatCapacityData[4] = (uint8_t)(gMassMemory.blockCount[lun] >> 24);
readFormatCapacityData[5] = (uint8_t)(gMassMemory.blockCount[lun] >> 16);
readFormatCapacityData[6] = (uint8_t)(gMassMemory.blockCount[lun] >> 8);
readFormatCapacityData[7] = (uint8_t)(gMassMemory.blockCount[lun] >> 0);
readFormatCapacityData[9] = (uint8_t)(gMassMemory.blockSize[lun] >> 16);
readFormatCapacityData[10] = (uint8_t)(gMassMemory.blockSize[lun] >> 8);
readFormatCapacityData[11] = (uint8_t)(gMassMemory.blockSize[lun] >> 0);
mscBotSendData(readFormatCapacityData, sizeof(readFormatCapacityData));
}
9. SCSI_READ_CAPACITY10(0x25)
读取当前安装介质的容量。
RelAdr: 值为0.
Logical Block Address:值为0.
PMI: 值为0.
返回的容量数据结构:
void scsiCmdReadCapacity10(uint8_t lun)
{
uint8_t readCapacity10Data[] =
{
/* Last Logical Block */
0,
0,
0,
0,
/* Block Length */
0,
0,
0,
0
};
if(massMalGetStatus(lun) > 0)
{
scsiSetSenseData(gMscCBW.bLUN, SCSI_NOT_READY, SCSI_ASC_MEDIUM_NOT_PRESENT);
mscBotSendCSW(MSC_BOT_CSW_CMD_FAILED, MSC_BOT_SEND_CSW_ENABLE);
mscBotAbort(MSC_DIR_IN);
return;
}
readCapacity10Data[0] = (uint8_t)((gMassMemory.blockCount[lun] - 1) >> 24);
readCapacity10Data[1] = (uint8_t)((gMassMemory.blockCount[lun] - 1) >> 16);
readCapacity10Data[2] = (uint8_t)((gMassMemory.blockCount[lun] - 1) >> 8);
readCapacity10Data[3] = (uint8_t)(gMassMemory.blockCount[lun] - 1);
readCapacity10Data[4] = (uint8_t)(gMassMemory.blockSize[lun] >> 24);
readCapacity10Data[5] = (uint8_t)(gMassMemory.blockSize[lun] >> 16);
readCapacity10Data[6] = (uint8_t)(gMassMemory.blockSize[lun] >> 8);
readCapacity10Data[7] = (uint8_t)(gMassMemory.blockSize[lun]);
mscBotSendData(readCapacity10Data, sizeof(readCapacity10Data));
}
10. SCSI_READ10(0x28)
HOST从设备读取数据。
DPO,FUA,RelAdr:这3个参数都为0.
Logical Block Address:指定开始读取操作的逻辑块地址。
Transfer Length:指定读取的数据长度。
/* Calculate Logical Block Address */
gScsiInfo.LBA = (gMscCBW.CB[2] << 24) | (gMscCBW.CB[3] << 16) | (gMscCBW.CB[4] << 8) | gMscCBW.CB[5];
/* Calculate the Number of Blocks to transfer */
gScsiInfo.BlkLen = (gMscCBW.CB[7] << 8) | gMscCBW.CB[8];
读数据分2个阶段,如果这笔数据是第一次读入,则先判断是否地址和长度有效。
if(scsiAddressManagement(lun, SCSI_READ10, lba, blockNbr) == FALSE)
return;
然后判断数据方向,确认是IN则读入数据,并将状态改为DATA IN。如果方向错误则发送错误Sense。
if((gMscCBW.bmFlags & 0x80) != 0)
{
gMscBotState = MSC_BOT_DATA_IN;
massReadMemory(lun, lba, blockNbr);
}
else
{
mscBotAbort(MSC_DIR_BOTH);
scsiSetSenseData(gMscCBW.bLUN, SCSI_ILLEGAL_REQUEST, SCSI_ASC_INVALID_FIELED_IN_COMMAND);
mscBotSendCSW(MSC_BOT_CSW_CMD_FAILED, MSC_BOT_SEND_CSW_ENABLE);
}
第二个阶段是持续读入数据阶段,这是状态变量已经是DATA IN,只需要读入数据即可。
massReadMemory(lun, lba, blockNbr);
11. SCSI_WRITE10(0x2A)
HOST写数据给设备。
基本与READ10差不多。
12. SCSI_VERIFY10(0x2F)
CB[1]的Bit2具体什么意思没有查到资料,代码里面有判断这个bit是否为0,为0则表示正常。判断CBW的dDataLength是否为0,为0则表示数据已经读写完,只有读写完了才能有Verify命令。
void scsiCmdVerify10(uint8_t lun)
{
if(gMscCBW.dDataLength == 0 && (gMscCBW.CB[1] & SCSI_VERIFY_BLKVFY) == 0)
{
mscBotSendCSW(MSC_BOT_CSW_CMD_PASSED, MSC_BOT_SEND_CSW_ENABLE);
}
else
{
mscBotAbort(MSC_BOTH_DIR);
scsiSetSenseData(gMscCBW.bLUN, SCSI_ILLEGAL_REQUEST, SCSI_ASC_INVALID_FIELED_IN_COMMAND);
mscBotSendCSW(MSC_BOT_CSW_CMD_FAILED, MSC_BOT_SEND_CSW_DISABLE);
}
}