1725年,出现了打孔纸,通过纸上的孔来描述图案,用来控制纺织机织造图案,后来打孔纸还用来存储电报信息。一直到1928年,发明了磁带,开始用磁带,磁棒记录信息,其是利用电与磁之间的相互转换来记录声音电信号和输出声音电信号。1932年,发明了碰鼓存储器,也就是现在的机械硬盘的前身,都是通过电磁原理实现的。
此时,存储器的换算单位1Kilobyte,就是1000byte,遵循国际单位进制。
1941年,世界上第一台电子计算机出现,其计算和存储都是采用传统的10进制。1944年,冯·诺依曼提出了2进制计算和存储的理论,1946年,世界上第一台电子2进制计算机产生。因为其计算和存储的原理是2进制,为了方便计算机计算,其容量计算也有相应修改。如:1Kilobyte=1024byte。
此后,存储行业依然延续1KB=1000Byte的换算规则至今天。这就导致存储器上标注的容量比在手机、电脑上显示的容量少。
除了电磁技术的存储设备,人们对电、光、声、磁等多种物理现象进行探索,发展出了各种技术路径的存储设备,光盘、碟盘、软盘等等。
后来随着半导体的发展,基于半导体特性也发明了许多存储设备。
随机存储器(Random Access Memory,RAM),RAM是一种可读/写存储器,其特点是存储器的任何一个存储单元的内容都可以随机存取,而且存取时间与存储单元的物理位置无关。RAM有两大类,静态RAM(StaticRAM/SRAM)和动态RAM(Dynamic RAM/DRAM)。
只读存储器(Read Only Memory,ROM)只读存储器是能对其存储的内容读出,而不能对其随便重新写入的存储器。只读存储器,传统的指EPROM,EEPROM,Nor Flash等。现在是泛指,一般指那些存储启动代码(包括操作系统),如:甚至包括装的操作系统的eMMC,UFS甚至机械硬盘和SSD。
固态存储设备(Solid State Storage,SSS)是相对原有的机械硬盘而言,固态存储设备是基于Flash介质的存储设备,其不需要任何机械部分。固态存储设备依据不同的用途,会有不同的表现形式。
例如用于便携存储的SD/TF卡,其有专门的SD传输协议以及相应的物理接口,如下图:
用于便携的即插即用的还有U盘,其是基于USB传输协议,其接口有很多形式,有如下图常用的Type-A接口,也有Type-C接口的,还有MiniUSB接口的。
固态存储器还有用于嵌入式设备的,如eMMC和UFS。其也有配套的eMMC传输协议及接口,如下图。UFS是eMMC的升级,升级了传输协议,速度更快。
固态存储器还常用于PC,称其为SSD(Solid State Disk),根据其接口协议不同,有SATA接口的SSD,还有NVMe协议接口的SSD。
U盘是最为简单的固态存储设备。其主要构成有3部分,如下图,1为控制芯片,2为USB物理接口,3为存储介绍NAND Flash。
Nand Flash由于其特性,存在bit出错,那么其需要ECC纠错机制来保证其稳定性。Nand Flash的擦写读,都有特殊的时序接口要求,并且NandFlash上的块并不是每一个块都好的。
一个2GB大小的U盘,接入电脑,显示的容量为2GB,是连续的可用容量,这个容量通称为逻辑容量。其对应的Nand Flash,可能有2.3GB,由于其中有一些Bad Block,剔除坏块之后,其最终的可用容量只有2.2GB。
前面讲到Nand Flash由于其特性,一段时间后由于电子逃逸,导致存在bit出错的可能性,增加ECC纠错机制,可以大幅延长固态存储设备的生命周期。不同的存储设备采用不同的ECC纠错算法,U盘,由于成本低,对可靠性要求更你,所以其一般采用相对简单的BCH纠错算法。BCH根据检验位数据长度的不同,一般可以纠错1~72bit/DMA(DMA为MCU单次传输数据模块,其大小可以为512至2048BYTE)。如果使用72bit/512B的纠错机制后,可以达到最大的纠错效果。但是这种纠错机制需要的校验位也最大,需要256bit(32B)的空间来存储校验位。那么2GB的可用空间,其需要的校验位空间为128MB。如果使用72bit/1024B的纠错机制,由校验空间减半为64MB。
FTL是Flash translation layer的英文缩写,FTL是一种软件中间层,最初是由intel提出的,用于将闪存模拟成为虚拟块设备,从而能够在闪存上实现FAT等等块设备类文件系统。FTL包含了地址映射,垃圾回收,损耗均衡等等几个方面的内容。从下图可以看出,FTL集成了坏块管理和ECC纠错算法,另外还有低层驱动接口(也即对Nand Flash的操作抽象为通用的接口供FTL使用,这样无论是SLC还是TLC,不同的驱动去实现详细的访问,并提供相同的抽象接口给FTL使用即可)。
从文件系统层下来的地址(逻辑地址)可能是连续的,逻辑地址上的数据最终是要写入Nand Flash上的,也即逻辑地址要与Nand Flash上的地址(物理地址有一个对应关系)。逻辑地址对应哪个物理地址,物理地址上哪些是坏块地址,需要跳过,这个过程类似文件系统中的FAT表对应实际的数据表。这个映射表需要占有一定的空间,根据算法及容量不同,占用的空间也不同。例如,2GB的可用逻辑容量,可能需要128MB的映射表。此时如果使用72bit/512B纠错算法,那么还需要128MB的校验位空间,而实际只有2.2GM的物理可用容量就不够了。所以此时只能选择72b/1024B的纠错算法。
算法开销,除了映射表之外,还有磨损平衡(Wear Leveling)。如果用户一直只有前面512MB,如果没有磨损平衡,前面512MB用的次数过多坏了,后面的1.5GB还没有动过,这明显是不合理的。所以需要一个磨损平衡,在使用的过程中,动态调整逻辑与物理的对应关系,不使用固定的对应关系。
例如,有Trim机制,文件系统除了所有文件,此时,NandFlash不可能立即去清除所有存储的数据,因为擦除所有的块可能需要几十秒钟,这是非常影响使用即时性的。用户清除完所有文件之后,可能用来存储一个小文件,然后就去做其他事了。那么FTL校测到一定的时间内,没有用户操作,FTL就会自动一个碎片整理,垃圾回收(Garbage Collection)操作,去擦除掉前面Trim指令标记的所有坏块。这样的机后,可以为用户提供更好的使用体验。
MCU,即微控制器,即我们常说的控制芯片。MCU主要做什么呢?USB控制芯片,要完成USB协议的管理,如差分信号线的处理,USB中断的处理等。
芯片是由许多门电路构成的,它可以同时运行许多逻辑。假如MCU不支持乘法指针,纯软件就只能用加法来实现乘法,这样时间复杂度大幅提升。因为MCU是并行的,它可以同时执行多个加法计算,这样MCU执行乘法的时间就可以做到和加法一样快,一个时钟周期就执行完。同样,ECC纠错算法比较复杂,如果用软件来实现,效率非常代,所以ECC算法也是集成到芯片当中,另外还有一些排序算法,芯片也的提供,这样可以更好的提升嵌入式固件的执行效率。
操作Flash,不同的操作对应不同的时序,一则不方便,二则效率也不高,MCU也将Flash的操作封装好了,提供一些接口给固件使用。
可不可以所有事情都由MCU自己实现,不用固件呢?当然可以。不过,MCU流片一次非常贵,每次更改的验证成本非常高。所以MCU稳定了之后,就不再改动。如果全部由MCU来做,那么就不能有改动了,那么来了新的Flash,MCU也不支持,新的容量也不支持。甚至发现了Bug,也没办法升级,这明显是不合理的。所以此时需要固件(也可以理解为MCU上的操作系统代码),用来完成一些变化的任务,来支持不同的容量,针对不同型号的Nand Flash,甚至是升级固件修复Bug。
U盘接入电脑,MCU就通过USB接口马上通电了,其内部的Boot机制(类似电脑的Bios系统),就会去Nand Flash固定位置读取固件,然后运行。与此同时,电脑主板上的USB Host控制器检测到有USB设备接入,就会触发操作系统中的USB驱动,USB驱动就会开始通过USB协议向USB设备发送最基本的信息询问接入的设备的信息。MCU固件会提前配置好此设备的容量,类型(Disk),序列号等信息。MCU就会读取这些信息通过USB协议回给Host Controler,USB驱动然后去Host Controler的空间中读取相关信息,然后就知道了U盘的相关信息,然后就会在系统中显示U盘设备,并显示逻辑盘符卷标等。
用户写文件,通过USB驱动再通过Host Controler再通过USB线到达U盘,U盘MCU解析USB协议中的逻辑地址,数据和命令,知道是写命令。MCU通知固件FTL,说有写操作来了。FTL就根据P2L(Physical To Logical)表,将对应逻辑地址的数据写入到对应的物理地址上(即Flash上)。读操作也类似。
这样,U盘就完成了一个上盘识别,写读存储的基本过程。
物理接口只是使用的场景不同,并不影响其功能。不同的协议,其内部的线路会有不同。但是高协议都兼容低协议。
USB的传输协议有4种,此处主要针对大容量存储设备的Bluk传输协议而言。
传输层协议主要作用传输过程中信息的校验,标识信息类型,包括差分信号的处理等。在USB 3.1GEN2时,使用了非常好的线材防止频率过高时的信号干预,然后将频率提升到最大,并且再增加了一组输入输出信号线,直接让传输速度相比前一代提升了十几倍。
但是由于PN结本身的有容性物理特性,现有的半导体频率理论最大可以达到10G,实验室最大达到过8G。频率已经提升到顶了,再想提提升传输速度,那就只有扩宽路面了,单车道可以增加为双车道。USB3.2 Gen 2x2协议,就增加为双通道,直接将速度提升一倍。
在USB数据通信的过程中,总线上传输的并不是所有都是有效信息,还要包括诸如同步信号、类型标识、校验码、握手信号等各种协议信息。因此实际数据传输的速率根本没有可能达到总线传输的极限速度480 Mbps。
传输层为了提升信号传输效率,例如原有是1K有效数据配置一次协议信息,现在可以修改为128K数据配置一次信息,这样会降低安全性,传输过程中如果出错过多,影响性能。为此USB协议升级的过程中,对线材的要求特别高,为了尽量减少传输过程中的信号干扰导致的出错。
随着PCI-E协议的发展,USB也紧跟其后提出了USB4,其在上一代的基础上,提供双链路,速度再翻一倍,并且兼容之前的接口,而且支持100W的快充。这让USB在PCIE协议一路狂飙之后,依然能够保持外设的最佳接口选择方案。
USB协议名变来变去,最新版已经不再按协议来分别,而是直接按速度和功率来区分。
上层协议主要是定义设备是否支持热拔插,是否支持合并指令,如是否支持NCQ,USB3.2增加了UASP协议,开始支持NCQ。
针对USB存储设备,上应用层是无法直接使用Bulk传输协议,只能以SCSI命令集的方式来传递命令,地址和数据。因为早期的电脑存储设备都是使用的SCSI协议,应用层也是使用的SCSI命令集方式来传递数据。后来的ATA接口,USB接口的存储设备,为了兼容上层应用程序,都默认使用SCSI命令集来传递数据。这样在应用层看业,无论底层使用的是USB存储设备,还是ATA存储设备,甚至是SCSI存储设备,应用层都是一样的。
MMC和UFS都是面向移动端闪存的标准,其中eMMC全称为“embedded MulTI Media Card”,即嵌入式的多媒体存储卡,eMMC从eMMC4.3一路发展到4.4、4.5直到现在的5.0、5.1,传输速度也从50MB/s一路狂飙到200MB/s直到现在eMMC5.1的600MB/s,不过实际使用中读取速率都会低于低于理论值,比如搭载eMMC5.1的手机一般读取速度在250MB/s。不过对于eMMC来说600MB/s已经是极限,通过并行接口提升接口速率越发困难。为什么并行接口速度反倒速度上不去呢?这是因为并行接口需要有一个同步信号来保证各信号之间的一致,但是随着频率的提升,不同信号线之间为了保持同步,这个开销太大,这严格限制了频率的进一步提升,限制了速度。
口串行提供了更高速率的可能,这就是后来的UFS标准。UFS使用高速串行接口替代了并行接口,改用了全双工方式,收发数据可以同时进行。相比eMMC,JEDEC发布了全新的USF 2.0标准在接口速率上提升不少,全新的USF 2.0有两个版本,其中UFS 2.0 HS-G2的理论带宽为5.8Gbps,大约740MB/s,更快速的UFS 2.1 HS-G3的理论带宽更是达到了11.6Gbps,约1.5GB/s。
2018年1月的时候,UFS 3.0问世,有了全新标准;在高性能、低功耗及速度提升上有了显著的变化。UFS支持双通道的双向读写,UFS 3.0的接口带宽最高速率可达23.2Gbps,就是2.9GB/s;UFS3.0的单通道速率是UFS2.1的两倍有余,可以说,UFS3.0是UFS2.1的升级版。
UFS 4.0继续扩展通道,速度可以达到UFS 3.0的两倍。
SATA是主流的大容量存储设备的接口和传输层协议。
2003年1月7日SATA 1.0推出,为第一代SATA接口,传输速度为1.5Gbit/s;2004年SATA 2.0正式推出,符合ATA-7规范,传输速度为1.5Gbit/s;2009年5月26日SATA 3.0完成最终规格发布,相比上代速率提升至6Gbit/s。后面还推出了SATA 3.1、SATA 3.2、SATA 3.3,但速率上并没有提升,只是新增了一些功能。SATA的频率已经提升到最高了,如果不推行多通道策略,速度上已经没有竞争力了。目前来看,SATA是无法竞争过PCIE+NVMe,估计SATA协议已经放弃升级了。
SATA设备上级使用的是AHCI。AHCI(Serial ATA Advanced Host Controller Interface)串行ATA高级主控接口/高级主机控制器接口),是在Intel的指导下,由多家公司联合研发的接口标准,它允许存储驱动程序启用高级串行 ATA 功能,如本机命令队列和热插拔。SATA接口(Serial ATA),即串行ATA。它是一种电脑总线,主要功能是用作主板和大量存储设备(如硬盘及光盘驱动器)之间的数据传输。SATA总线使用嵌入式时钟信号,具备了更强的纠错能力,数据传输采用差分信号,所以SATA相比IDE虽然数据线更少,但是传输的频率却高非常多,这就导致SATA的数据传输速度比IDE更快。早期的主板需要在Bios中手动设计选择AHCI模式。我们在设备器中展开IDE ATA/ATAPI控制器,找到AHCI相关字样,即表示已经启用AHCI模式,如下图。SATA的数据线总共有7根,2根差分写信号线、2根差分读信号线还有3根地线。AHCI 是一个 PCI 类的设备,作为系统内存和 SATA 设备之间的数据移动引擎。
应用层同样兼容SCSI命令集,另外,也可以发送ATA命令集。为了更好的兼容应用程序,常用命令集一般都是使用SCSI命令,SATA驱动会在底层将SCSI命令转换为对应的ATA命令。
PCIe直接连接到CPU,这样应用程序访问存储设备不用经过内存中转,速度更快。
PCIe标准接口,U.2, M.2均可以,它们也只是形式要求上不同。各自有各的优缺点。
M.2的问世终结了mSATA接口,它同时可以支持SATA和NVMe两种协议,目前M.2 SSD以后者更为常见,具备体积小、性能强的特点。
U.2接口在企业级和数据中心固态硬盘当中更为常见,它和SATA硬盘一样属于2.5寸硬盘,但厚度相对更大,可以堆叠更多的Nand Flash,这样读写速度也更快。
一般的主板上是没有U.2接口的,一般需要用一个PCIE转U.2的转换板来适配。如下图:
多通道的接口,其数据线相应的也会增加。
上述PCIE接口与PC主板上的PCIe的插槽相应
PCIe传输协议是走得最快的,它将传输频率提高到最,然后传输过程中的CRC校验开销也尽量节省,再者不停地增加多通道。如PCI-E x1/x4/x8/x16/x32。将来会不会有x64, x128甚至更多也说不准。
上层协议使用NVMe,主要支持热拔插,NCQ等功能。
NVMe应用层同样支持SCIS命令集,NVMe命令集更少,并且也兼容SCSI命令集。
针对USB存储设备,Windows没有提供通过USB Bulk传输协议的接口。LibUSB是一个跨平台的开源项目,可以获取USB device list,还有各种描述符。还可以直接通过Bulk传输直接与USB设备通信。LibUSB需要配合一个libUSB0.sys的驱动,即可以自行构造一个SCSI传输的内容包,通过Bulk进行传输,也可以直接通过Bulk传输自定义消息,这需要USB设备配合来使用。
SCSI命令可以通过DeviceIoControl设置IOCTL_SCSI_PASS_THROUGH命令码来发送。通过SCSI完成的Write,Read和Win32API的WriteFile和ReadFile效果一样。
if ( DeviceIoControl(
IOCTL_SCSI_PASS_THROUGH,
&sptwb,
sizeof(SCSI_PASS_THROUGH_WITH_BUFFERS),
&sptwb,
sizeof(SCSI_PASS_THROUGH_WITH_BUFFERS),
&returned
) && (sptwb.Spt.ScsiStatus == 0) )
针对SATA设备,可以直接使用SCSI命令来完成一些基本的操作。针对一些特殊的ATA命令,也可以通SCSI 中提供的附加命令发送ATA命令。另外,还可以通过IOCTL_ATA_PASS_THROUGH_DIRECT来直接发送ATA命令。
bool AtaCommand(ATA_REGISTER ®, READWRITE read_write, bool dma, BYTE * buf, JCSIZE secs)
{
LOG_STACK_TRACE();
LOG_TRACE(_T("Send ata cmd: %02X, %02X, %02X, %02X, %02X, %02X, %02X")
, reg.feature, reg.sec_count, reg.lba_low, reg.lba_mid, reg.lba_hi, reg.device, reg.command);
JCASSERT(m_dev);
JCSIZE buf_len = secs * SECTOR_SIZE;
// 申请一个4KB对齐的内存。基本思想:申请一块内存比原来多4KB的空间,
// 然后将指针移到4KB对齐的位置。移动最多不会超过4KB。
static const JCSIZE ALIGNE_SIZE = 4096; // Aligne to 4KB
stdext::auto_array<BYTE> _buf(buf_len + 2 * ALIGNE_SIZE); //
#if 1 // aligne
BYTE * __buf = (BYTE*)_buf;
JCSIZE align_mask = (~ALIGNE_SIZE) + 1;
BYTE * align_buf = (BYTE*)((UINT)(__buf) & align_mask) + ALIGNE_SIZE; // 4KBB aligne
#else
BYTE * align_buf = buf;
#endif
JCSIZE hdr_size = sizeof(ATA_PASS_THROUGH_DIRECT);
ATA_PASS_THROUGH_DIRECT hdr_in;
memset(&hdr_in, 0, hdr_size);
hdr_in.DataTransferLength = buf_len;
hdr_in.DataBuffer = align_buf;
if (write == read_write)
{
hdr_in.AtaFlags = ATA_FLAGS_DATA_OUT;
if (buf_len > 0) memcpy_s(align_buf, buf_len, buf, buf_len);
}
else
{
hdr_in.AtaFlags = ATA_FLAGS_DATA_IN;
}
if (dma) hdr_in.AtaFlags |= ATA_FLAGS_USE_DMA;
else hdr_in.AtaFlags |= ATA_FLAGS_NO_MULTIPLE;
hdr_in.AtaFlags |= ATA_FLAGS_DRDY_REQUIRED;
hdr_in.Length = hdr_size;
hdr_in.TimeOutValue = 5;
memcpy_s(hdr_in.CurrentTaskFile, 8, ®, 8);
DWORD readsize = 0;
BOOL br = DeviceIoControl(m_dev, IOCTL_ATA_PASS_THROUGH_DIRECT, &hdr_in, hdr_size, &hdr_in, hdr_size, &readsize, NULL);
if (!br)
{
stdext::CWin32Exception err(GetLastError(), _T("failure on ata command "));
LOG_ERROR( err.WhatT() );
}
memcpy_s(®, 8, hdr_in.CurrentTaskFile, 8);
if ( (read == read_write) && (buf_len > 0) ) memcpy_s(buf, buf_len, align_buf, buf_len);
LOG_DEBUG(_T("Returned ata cmd: %02X, %02X"), reg.error, reg.status);
if (!br) THROW_WIN32_ERROR(_T("failure in invoking ata command."));
return true;
}
PCIe设备,出现没多久,基本操作也可以通过SCSI来完成。早期,一些特殊的操作,如Identify、SMART信息等,还没有形成完成的统一。微软没有提供直接的相关操作方法。所以大家各行一套,有的通过SCSI_MINI_PROT驱动来完成,也有通过SCSI的Security Protocol(扩展命令)来完成。到了Win10之后,微软提供了通用的NVMe驱动StorNVMe,并提供了一套标准的相关操作,查询各种信息用IOCTL_STORAGE_QUERY_PROPERTY,升级固件用IOCTL_STORAGE_FIRMWARE_DOWNLOAD,发私有命令用IOCTL_STORAGE_PROTOCOL_COMMAND。
YSNVMe::PSTORAGE_PROTOCOL_COMMAND pProtocolCmd = NULL;
YSNVMe::PNVME_COMMAND pCmd = NULL;
BYTE* bTmpBuf = NULL;
DWORD dwTmpBufSize = 0;
DWORD dwRtnLength = 0;
dwTmpBufSize = FIELD_OFFSET(YSNVMe::STORAGE_PROTOCOL_COMMAND, Command) + STORAGE_PROTOCOL_COMMAND_LENGTH_NVME + sizeof(YSNVMe::NVME_ERROR_INFO_LOG) + srtCommandSet.dwDataLength;
bTmpBuf = (BYTE *)::malloc((size_t)dwTmpBufSize);
if(bTmpBuf == NULL)
return False;
if(srtCommandSet.ulTimeout == 0)
srtCommandSet.ulTimeout = 10;
::ZeroMemory(bTmpBuf, dwTmpBufSize);
pProtocolCmd = (YSNVMe::PSTORAGE_PROTOCOL_COMMAND)bTmpBuf;
pProtocolCmd->Version = STORAGE_PROTOCOL_STRUCTURE_VERSION;
pProtocolCmd->Length = sizeof(YSNVMe::STORAGE_PROTOCOL_COMMAND);
pProtocolCmd->ProtocolType = YSNVMe::ProtocolTypeNvme;
pProtocolCmd->Flags = STORAGE_PROTOCOL_COMMAND_FLAG_ADAPTER_REQUEST;
pProtocolCmd->CommandLength = STORAGE_PROTOCOL_COMMAND_LENGTH_NVME;
pProtocolCmd->ErrorInfoLength = sizeof(YSNVMe::NVME_ERROR_INFO_LOG);
pProtocolCmd->TimeOutValue = srtCommandSet.ulTimeout;
pProtocolCmd->ErrorInfoOffset = FIELD_OFFSET(YSNVMe::STORAGE_PROTOCOL_COMMAND, Command) + STORAGE_PROTOCOL_COMMAND_LENGTH_NVME;
pProtocolCmd->CommandSpecific = STORAGE_PROTOCOL_SPECIFIC_NVME_ADMIN_COMMAND;
if(srtCommandSet.bProtocolField == 2)
{
pProtocolCmd->DataFromDeviceTransferLength = srtCommandSet.dwDataLength;
pProtocolCmd->DataFromDeviceBufferOffset = pProtocolCmd->ErrorInfoOffset + pProtocolCmd->ErrorInfoLength;
}
else if(srtCommandSet.bProtocolField == 3)
{
pProtocolCmd->DataToDeviceTransferLength = srtCommandSet.dwDataLength;
pProtocolCmd->DataToDeviceBufferOffset = pProtocolCmd->ErrorInfoOffset + pProtocolCmd->ErrorInfoLength;
::CopyMemory(((PCHAR)pProtocolCmd + pProtocolCmd->DataToDeviceBufferOffset), bIOBuf, srtCommandSet.dwDataLength);
}
pCmd = (YSNVMe::PNVME_COMMAND)pProtocolCmd->Command;
pCmd->CDW0.OPC = srtCommandSet.bOpcode;
pCmd->NSID = srtCommandSet.dwNSID;
pCmd->u.GENERAL.CDW10 = srtCommandSet.dw10;
pCmd->u.GENERAL.CDW11 = srtCommandSet.dw11;
pCmd->u.GENERAL.CDW12 = srtCommandSet.dw12;
pCmd->u.GENERAL.CDW13 = srtCommandSet.dw13;
pCmd->u.GENERAL.CDW14 = srtCommandSet.dw14;
pCmd->u.GENERAL.CDW15 = srtCommandSet.dw15;
boRtnStatus = ::DeviceIoControl(hDeviceIOCTL, IOCTL_STORAGE_PROTOCOL_COMMAND, bTmpBuf, dwTmpBufSize, bTmpBuf, dwTmpBufSize, &dwRtnLength, NULL);
if(boRtnStatus && (srtCommandSet.bProtocolField == 2))
::CopyMemory(bIOBuf, ((PCHAR)pProtocolCmd + pProtocolCmd->DataToDeviceBufferOffset) + 0xD0, srtCommandSet.dwDataLength);
::free(bTmpBuf);