1. SCSI命令块包装器 (CBW) - USB存储设备通信核心
cpp
// SCSI Command Block Wrapper (CBW)
// 用途:在USB大容量存储协议中封装SCSI命令,用于发起数据传输请求
#pragma pack(push, 1) // 确保结构体按1字节对齐,避免编译器插入填充字节
typedef struct {
// 签名,必须为0x43425355 ('USBC' in little-endian)
// 验证数据包的合法性
uint32_t dCBWSignature;
// 命令标签,主机生成的唯一标识符
// 用于匹配CBW和对应的CSW响应
uint32_t dCBWTag;
// 本次命令要传输的数据总长度(字节数)
// 0表示无数据传输,比如TEST UNIT READY命令
uint32_t dCBWDataTransferLength;
// 传输方向标志位
// Bit 7: 0=主机到设备(OUT/写),1=设备到主机(IN/读)
// Bit 6-0: 保留,必须为0
uint8_t bmCBWFlags;
// 逻辑单元号(LUN),指定目标设备中的逻辑单元
// 对于单LUN设备通常为0
uint8_t bCBWLUN;
// CBWCB字段的有效长度(字节数),1-16
// 表示后面CBWCB数组中实际包含的命令字节数
uint8_t bCBWCBLength;
// SCSI命令块,包含实际的SCSI命令
// 第一个字节是操作码,后面是参数
uint8_t CBWCB[16];
} ScsiCBW;
#pragma pack(pop) // 恢复之前的对齐设置
实际用法:
cpp
// 创建一个INQUIRY命令的CBW
ScsiCBW cbw;
cbw.dCBWSignature = 0x43425355; // 'USBC'
cbw.dCBWTag = 0x00000001; // 唯一标签
cbw.dCBWDataTransferLength = 36; // INQUIRY返回36字节数据
cbw.bmCBWFlags = 0x80; // 设备到主机(读操作)
cbw.bCBWLUN = 0; // 第一个LUN
cbw.bCBWCBLength = 6; // INQUIRY命令长度6字节
// 设置INQUIRY命令 (0x12是INQUIRY操作码)
cbw.CBWCB[0] = 0x12; // INQUIRY opcode
cbw.CBWCB[1] = 0x00; // EVPD=0, CmdDt=0
cbw.CBWCB[2] = 0x00; // Page code
cbw.CBWCB[3] = 0x00; // Allocation length MSB
cbw.CBWCB[4] = 36; // Allocation length LSB (36字节)
cbw.CBWCB[5] = 0x00; // Control
2. SCSI命令状态包装器 (CSW) - 命令执行结果
cpp
// SCSI Command Status Wrapper (CSW)
// 用途:设备在完成CBW处理后返回的状态信息
#pragma pack(push, 1)
typedef struct {
// 签名,必须为0x53425355 ('USBS' in little-endian)
uint32_t dCSWSignature;
// 必须与对应CBW中的dCBWTag相同
// 用于主机确认这是哪个命令的响应
uint32_t dCSWTag;
// 未传输的数据字节数
// dCBWDataTransferLength - 实际传输的字节数
// 0表示所有数据已成功传输
uint32_t dCSWDataResidue;
// 命令执行状态:
// 0x00 = 命令通过 (PASSED)
// 0x01 = 命令失败 (FAILED)
// 0x02 = 阶段错误 (PHASE ERROR)
// 其他值保留
uint8_t bCSWStatus;
} ScsiCSW;
#pragma pack(pop)
实际用法示例:
cpp
// 检查CSW状态
ScsiCSW csw;
if (csw.dCSWSignature != 0x53425355) {
// 签名错误,数据包损坏
handleError("Invalid CSW signature");
}
if (csw.dCSWTag != expectedTag) {
// 标签不匹配,可能是乱序响应
handleError("Tag mismatch");
}
switch (csw.bCSWStatus) {
case 0x00: // PASSED
printf("Command completed successfully\n");
break;
case 0x01: // FAILED
printf("Command failed. Residue: %u bytes\n", csw.dCSWDataResidue);
// 此时应发送REQUEST SENSE获取详细错误信息
break;
case 0x02: // PHASE ERROR
printf("Phase error occurred\n");
break;
default:
printf("Unknown status: 0x%02X\n", csw.bCSWStatus);
}
3. SCSI查询数据 (INQUIRY) - 设备识别信息
cpp
// SCSI Inquiry Data Structure
// 用途:响应INQUIRY命令,提供设备基本信息
#pragma pack(push, 1)
typedef struct {
// 外设限定符 (3 bits):描述设备状态
// 0x00 = 设备连接且支持此LUN
// 0x01 = 设备连接但不支持此LUN
// 0x03 = 无法提供设备信息
// 0x07 = 无设备连接
uint8_t PeripheralQualifier : 3;
// 外设设备类型 (5 bits):SCSI设备类型代码
// 0x00 = 直接访问设备 (磁盘)
// 0x01 = 顺序访问设备 (磁带)
// 0x05 = CD/DVD设备
// 0x07 = 光存储设备
// 0x0E = 精简块设备 (RBC)
// 0x0F = 无LUN映射
uint8_t PeripheralDeviceType : 5;
// 可移动介质位:1=可移动,0=不可移动
uint8_t RMB : 1;
uint8_t Reserved1 : 7;
// SCSI标准版本:
// 0x00 = 无标准遵循
// 0x03 = SPC-2 (SCSI Primary Commands-2)
// 0x04 = SPC-3
// 0x05 = SPC-4
// 0x06 = SPC-5
uint8_t Version;
// 响应数据格式:应为0x02 (遵循SPC标准)
uint8_t ResponseDataFormat;
// 附加数据长度:后续数据的字节数
uint8_t AdditionalLength;
// 以下为SCSI支持的功能标志位:
uint8_t SCCS : 1; // 1=支持SCC(存储控制器命令)
uint8_t ACC : 1; // 1=支持寻址异常控制
uint8_t TPGS : 2; // 目标端口组支持:00=无,01=隐式,10=显式,11=两者
uint8_t ThreePC : 1; // 1=支持第三方复制
uint8_t Protect : 1; // 1=支持保护信息
uint8_t BQue : 1; // 1=支持基本队列
uint8_t EncServ : 1; // 1=支持加密服务
uint8_t MultiP : 1; // 1=支持多端口
uint8_t MChngr : 1; // 1=介质更换器存在
uint8_t Obsolete : 2; // 已废弃
uint8_t Addr16 : 1; // 1=支持16位SCSI地址
uint8_t Obsolete2 : 2;
uint8_t WBus16 : 1; // 1=支持16位宽总线
uint8_t Sync : 1; // 1=支持同步传输
uint8_t Linked : 1; // 1=支持链接命令
uint8_t CmdQue : 1; // 1=支持命令队列
uint8_t SftRe : 1; // 1=支持软重置
// 厂商标识 (8字节),ASCII码,右对齐,空格填充
// 如:"TOSHIBA "、"SEAGATE "
uint8_t VendorIdentification[8];
// 产品标识 (16字节),ASCII码,左对齐,空格填充
// 如:"MK3252GSX "
uint8_t ProductIdentification[16];
// 产品修订版本 (4字节),ASCII码,左对齐,空格填充
// 如:"FH00"
uint8_t ProductRevisionLevel[4];
} ScsiInquiryData;
#pragma pack(pop)
实际用法示例:
cpp
ScsiInquiryData inquiryData;
// 解析设备类型
const char* deviceTypeStr;
switch (inquiryData.PeripheralDeviceType) {
case 0x00: deviceTypeStr = "Direct-access block device (disk)"; break;
case 0x05: deviceTypeStr = "CD/DVD device"; break;
case 0x07: deviceTypeStr = "Optical memory device"; break;
case 0x0E: deviceTypeStr = "Reduced block device (RBC)"; break;
default: deviceTypeStr = "Unknown device type"; break;
}
printf("Device Type: %s (0x%02X)\n", deviceTypeStr, inquiryData.PeripheralDeviceType);
printf("Removable: %s\n", inquiryData.RMB ? "Yes" : "No");
// 提取字符串信息(需要去除填充的空格)
char vendor[9] = {0};
char product[17] = {0};
char revision[5] = {0};
memcpy(vendor, inquiryData.VendorIdentification, 8);
memcpy(product, inquiryData.ProductIdentification, 16);
memcpy(revision, inquiryData.ProductRevisionLevel, 4);
// 去除尾部空格
vendor[8] = product[16] = revision[4] = '\0';
trimTrailingSpaces(vendor);
trimTrailingSpaces(product);
trimTrailingSpaces(revision);
printf("Vendor: %s\n", vendor);
printf("Product: %s\n", product);
printf("Revision: %s\n", revision);
4. SCSI读取容量数据 - 设备容量信息
cpp
// SCSI Read Capacity Data Structure
// 用途:响应READ CAPACITY命令,报告设备大小
#pragma pack(push, 1)
typedef struct {
// 最后一个逻辑块的地址(LBA)
// 设备容量 = (LogicalBlockAddress + 1) 个逻辑块
// 例如:如果值为0x0000FFFF,表示有0x00010000个逻辑块
uint32_t LogicalBlockAddress;
// 每个逻辑块的字节数
// 通常是512, 1024, 2048, 4096等值
// 与LogicalBlockAddress一起计算总容量
uint32_t BlockLengthInBytes;
} ScsiReadCapacityData;
#pragma pack(pop)
实际用法示例:
cpp
ScsiReadCapacityData capacityData;
// 计算设备总容量
uint64_t totalBlocks = (uint64_t)capacityData.LogicalBlockAddress + 1;
uint64_t totalBytes = totalBlocks * capacityData.BlockLengthInBytes;
printf("Last LBA: 0x%08X (%u)\n",
capacityData.LogicalBlockAddress, capacityData.LogicalBlockAddress);
printf("Block Size: %u bytes\n", capacityData.BlockLengthInBytes);
printf("Total Capacity: %llu bytes\n", totalBytes);
// 转换为更友好的显示格式
const char* sizeUnit;
double displaySize;
if (totalBytes >= 1099511627776LL) { // 1 TiB
displaySize = totalBytes / 1099511627776.0;
sizeUnit = "TiB";
} else if (totalBytes >= 1073741824) { // 1 GiB
displaySize = totalBytes / 1073741824.0;
sizeUnit = "GiB";
} else if (totalBytes >= 1048576) { // 1 MiB
displaySize = totalBytes / 1048576.0;
sizeUnit = "MiB";
} else {
displaySize = totalBytes / 1024.0;
sizeUnit = "KiB";
}
printf("Formatted: %.2f %s\n", displaySize, sizeUnit);
5. SCSI感知数据 - 错误详细信息
cpp
// SCSI Sense Data Structure
// 用途:响应REQUEST SENSE命令,提供命令失败的具体原因
#pragma pack(push, 1)
typedef struct {
// 响应代码 (高4位):
// 0x70 = 当前错误 (fixed格式)
// 0x71 = 可纠正错误 (fixed格式)
// 0x72 = 当前错误 (descriptor格式)
// 0x73 = 可纠正错误 (descriptor格式)
uint8_t ResponseCode;
uint8_t Obsolete; // 已废弃,通常为0
// 感知键 (4 bits):主要的错误类别
// 0x00 = NO SENSE (无错误)
// 0x01 = RECOVERED ERROR (已恢复错误)
// 0x02 = NOT READY (设备未就绪)
// 0x03 = MEDIUM ERROR (介质错误)
// 0x04 = HARDWARE ERROR (硬件错误)
// 0x05 = ILLEGAL REQUEST (非法请求)
// 0x06 = UNIT ATTENTION (单元注意)
// 0x07 = DATA PROTECT (数据保护)
// 0x08 = BLANK CHECK (空白检查)
// 0x09 = VENDOR SPECIFIC (厂商特定)
// 0x0A = COPY ABORTED (复制中止)
// 0x0B = ABORTED COMMAND (命令中止)
// 0x0C = EQUAL (相等)
// 0x0D = VOLUME OVERFLOW (卷溢出)
// 0x0E = MISCOMPARE (不匹配)
// 0x0F = RESERVED (保留)
uint8_t SenseKey : 4;
uint8_t Reserved1 : 1;
// ILI (Incorrect Length Indicator):长度不正确指示
// 1=请求的长度与实际传输长度不符
uint8_t ILI : 1;
// EOM (End Of Medium):介质结束
uint8_t EOM : 1;
// File Mark:文件标记
uint8_t FileMark : 1;
// 信息字段:与错误相关的附加信息
uint8_t Information[4];
// 附加感知长度:后续字节数
uint8_t AdditionalSenseLength;
// 命令特定信息:与具体命令相关的信息
uint8_t CommandSpecificInformation[4];
// 附加感知代码 (ASC):详细错误代码
// 与SenseKey一起精确定位错误
// 例如:0x3A = MEDIUM NOT PRESENT (介质不存在)
uint8_t AdditionalSenseCode;
// 附加感知代码限定符 (ASCQ):ASC的进一步限定
uint8_t AdditionalSenseCodeQualifier;
// 可更换单元代码:标识发生错误的部件
uint8_t FieldReplaceableUnitCode;
// 感知键特定信息:SenseKey相关的附加信息
uint8_t SenseKeySpecific[3];
} ScsiSenseData;
#pragma pack(pop)
实际用法示例:
cpp
ScsiSenseData senseData;
// 解析感知键
const char* senseKeyStr;
switch (senseData.SenseKey) {
case 0x00: senseKeyStr = "NO SENSE"; break;
case 0x01: senseKeyStr = "RECOVERED ERROR"; break;
case 0x02: senseKeyStr = "NOT READY"; break;
case 0x03: senseKeyStr = "MEDIUM ERROR"; break;
case 0x04: senseKeyStr = "HARDWARE ERROR"; break;
case 0x05: senseKeyStr = "ILLEGAL REQUEST"; break;
case 0x06: senseKeyStr = "UNIT ATTENTION"; break;
case 0x07: senseKeyStr = "DATA PROTECT"; break;
case 0x08: senseKeyStr = "BLANK CHECK"; break;
case 0x0B: senseKeyStr = "ABORTED COMMAND"; break;
default: senseKeyStr = "Unknown sense key"; break;
}
printf("Sense Key: 0x%01X (%s)\n", senseData.SenseKey, senseKeyStr);
printf("ASC: 0x%02X, ASCQ: 0x%02X\n",
senseData.AdditionalSenseCode,
senseData.AdditionalSenseCodeQualifier);
// 检查特定错误
if (senseData.SenseKey == 0x02 && // NOT READY
senseData.AdditionalSenseCode == 0x3A) { // MEDIUM NOT PRESENT
printf("Error: Medium not present - please insert media\n");
} else if (senseData.SenseKey == 0x05 && // ILLEGAL REQUEST
senseData.AdditionalSenseCode == 0x20) { // INVALID COMMAND OPERATION CODE
printf("Error: Invalid command - unsupported operation\n");
}
6. SCSI命令结构 - 通用命令表示
cpp
// SCSI Command Structure
// 用途:通用SCSI命令表示,可用于构建各种操作
typedef struct {
uint8_t opcode; // 操作码,决定命令类型
uint8_t flags; // 命令特定标志
uint32_t lba; // 逻辑块地址(起始位置)
uint16_t transfer_length; // 传输的块数或字节数
uint8_t control; // 控制字节
} ScsiCommand;
常见操作码示例:
cpp
// 常用SCSI操作码
#define SCSI_OP_TEST_UNIT_READY 0x00
#define SCSI_OP_REQUEST_SENSE 0x03
#define SCSI_OP_INQUIRY 0x12
#define SCSI_OP_MODE_SELECT 0x55
#define SCSI_OP_MODE_SENSE 0x5A
#define SCSI_OP_READ_6 0x08 // 6字节命令格式
#define SCSI_OP_READ_10 0x28 // 10字节命令格式
#define SCSI_OP_READ_12 0xA8 // 12字节命令格式
#define SCSI_OP_READ_16 0x88 // 16字节命令格式
#define SCSI_OP_WRITE_6 0x0A
#define SCSI_OP_WRITE_10 0x2A
#define SCSI_OP_WRITE_12 0xAA
#define SCSI_OP_WRITE_16 0x8A
#define SCSI_OP_READ_CAPACITY 0x25 // READ CAPACITY (10)
#define SCSI_OP_READ_CAPACITY_16 0x9E // READ CAPACITY (16)
// 构建READ(10)命令示例
ScsiCommand buildReadCommand(uint32_t startLBA, uint16_t blockCount) {
ScsiCommand cmd;
cmd.opcode = 0x28; // READ(10)
cmd.flags = 0x00; // 通常为0
cmd.lba = startLBA;
cmd.transfer_length = blockCount;
cmd.control = 0x00; // 控制字节
return cmd;
}
// 构建WRITE(10)命令示例
ScsiCommand buildWriteCommand(uint32_t startLBA, uint16_t blockCount) {
ScsiCommand cmd;
cmd.opcode = 0x2A; // WRITE(10)
cmd.flags = 0x00;
cmd.lba = startLBA;
cmd.transfer_length = blockCount;
cmd.control = 0x00;
return cmd;
}
7. SCSI请求类 - 命令请求封装
cpp
// SCSI Request Structure
class ScsiRequest {
public:
ScsiRequest() : m_lun(), m_command(), m_data() {}
// 获取LUN(逻辑单元号)
const ScsiLun& lun() const { return m_lun; }
void setLun(const ScsiLun& lun) { m_lun = lun; }
// 获取SCSI命令
const ScsiCommand& command() const { return m_command; }
void setCommand(const ScsiCommand& command) { m_command = command; }
// 获取/设置传输数据
// 对于写操作:包含要写入的数据
// 对于读操作:接收读取的数据
const std::vector<uint8_t>& data() const { return m_data; }
std::vector<uint8_t>& data() { return m_data; }
void setData(const std::vector<uint8_t>& data) { m_data = data; }
private:
ScsiLun m_lun; // 目标逻辑单元
ScsiCommand m_command; // SCSI命令
std::vector<uint8_t> m_data; // 关联的数据缓冲区
};
实际用法示例:
cpp
// 创建INQUIRY请求
ScsiRequest createInquiryRequest() {
ScsiRequest request;
// 设置LUN
ScsiLun lun;
lun.lun = 0; // 第一个逻辑单元
request.setLun(lun);
// 设置INQUIRY命令
ScsiCommand cmd;
cmd.opcode = 0x12; // INQUIRY opcode
cmd.flags = 0x00;
cmd.lba = 0;
cmd.transfer_length = 36; // 标准INQUIRY数据长度
cmd.control = 0x00;
request.setCommand(cmd);
// 预留数据缓冲区用于接收响应
request.data().resize(36);
return request;
}
// 创建读取请求
ScsiRequest createReadRequest(uint32_t startLBA, uint16_t blockCount, uint32_t blockSize) {
ScsiRequest request;
ScsiLun lun;
lun.lun = 0;
request.setLun(lun);
ScsiCommand cmd;
cmd.opcode = 0x28; // READ(10)
cmd.flags = 0x00;
cmd.lba = startLBA;
cmd.transfer_length = blockCount;
cmd.control = 0x00;
request.setCommand(cmd);
// 预留缓冲区用于接收读取的数据
size_t dataSize = blockCount * blockSize;
request.data().resize(dataSize);
return request;
}
// 创建写入请求
ScsiRequest createWriteRequest(uint32_t startLBA,
const std::vector<uint8_t>& writeData,
uint32_t blockSize) {
ScsiRequest request;
ScsiLun lun;
lun.lun = 0;
request.setLun(lun);
ScsiCommand cmd;
cmd.opcode = 0x2A; // WRITE(10)
cmd.flags = 0x00;
cmd.lba = startLBA;
cmd.transfer_length = writeData.size() / blockSize;
cmd.control = 0x00;
request.setCommand(cmd);
// 设置要写入的数据
request.setData(writeData);
return request;
}
总结表格
| 结构体 | 主要用途 | 关键字段 | 应用场景 |
|---|---|---|---|
| ScsiCBW | USB存储命令封装 | dCBWSignature, bmCBWFlags, CBWCB | USB大容量存储设备通信 |
| ScsiCSW | 命令状态返回 | dCSWTag, bCSWStatus | 验证命令执行结果 |
| ScsiInquiryData | 设备信息查询 | PeripheralDeviceType, VendorIdentification | 设备识别和兼容性检查 |
| ScsiReadCapacityData | 容量报告 | LogicalBlockAddress, BlockLengthInBytes | 计算存储容量 |
| ScsiSenseData | 错误报告 | SenseKey, ASC, ASCQ | 错误诊断和恢复 |
| ScsiCommand | 通用命令 | opcode, lba, transfer_length | 构建各种SCSI操作 |
| ScsiRequest | 请求封装 | lun(), command(), data() | 应用程序层命令管理 |
这些数据结构构成了SCSI协议栈的核心,在你的光存储项目中:
-
CBW/CSW 用于USB接口通信
-
InquiryData 识别光存储设备类型
-
ReadCapacityData 确定存储空间
-
SenseData 处理写入/读取错误
-
ScsiRequest 封装你的激光器/DMD控制命令