1. 前言
随着数字化时代的发展,数字数据的量越来越大,相应的数据存储的需求也越来越大,存储设备产业也是蓬勃发展。存储设备产业中,发展最为迅猛的则是固态存储(Solid State Storage,SSS)。数字化时代,海量的数据,需要海量的存储设备。可以说,固态存储设备是数字化时代最重要的基础设施。
为了解决发现的Bug,安全漏洞,或者为了提升性能,固态存储设备也有升级其固件的需求。固态存储设备种类繁多,有可以随身携带的U盘、TF卡,也有手机中的eMMC,UFS,还有电脑中的固态硬盘(SSD),更有各种云背后的分布式存储系统中大量使用的各种固态存储设备。不同应用场景,其使用方式也不同,那么相应的升级方式也有可能不同。本文尝试全面介绍一下不同应用场景下的固态存储设备的固件升级方案。
2. 固态存储设备
固态存储设备的主要构成为存储介质,控制芯片和一些外置电子元器件。固态存储设备的主要存储介质为非易失性存储器,其主要为NAND Flash。控制芯片的主要作用是连接NAND Flash和主机存储接口,并管理存储在NAND Flash上的数据。
主机存储接口不同,应用的场景也不同。其主要分为:
● USB接口,主要用于移动存储;
● SD接口,主要用于小型电子设备,如相机,监控设备等;
● eMMC接口,主要用于手机、平板和一些嵌入式设备,如智能电视,车机等;
● UFS接口,主要用于手机;
● SATA接口,主要用于个人电脑;
● PCIe接口,主要用于对性能有更高要求的企业级存储,如分布式存储,云存储等。
3. 升级方案
存储设备是否支持固件升级,需要两个方面的支持,存储设备固件算法支持升级,并且有升级工具。
3.1. 固件算法
存储设备升级成功,或者万一在升级过程中断电,都不能影响存储设备上之前存放的用户数据。升级算法设计要点:
● 用户数据的正确性,即L2P表(存储设备逻辑到NAND Flash物理地址映射表)不能发生改变。
○ FTL算法设计的时候,需要将L2P表等影响用户数据的算法数据结构和其他算法数据结构分块存储。因为升级固件,必须重新写入算法二进制文件和重新配置Boot信息,因为NAND Flash的块必须擦除了才能写,所以固件二进制文件和Boot信息必须和L2P表分块存储。
● 异常行为的安全性,即升级过程中,发生异常,重新上电时,能够恢复原有的状态。
○ 升级的操作过程是日志型的,每一步操作都有记录,只有最后升级成功,并且有CheckSum校验机制来保证操作的完整性。
○ 升级算法的设计,新旧算法都保留,每次升级成功之后,将新算法头中索引加1,这样每次启动后,检测到两个算法,并通过CheckSum校验算法的完整性,最后比较算法头中的索引值,启动索引较大的算法。
3.2. 升级流程
3.2.1. 移动存储设备
移动存储设备U盘/TF卡,基本都可以通过USB接口接入电脑,电脑的操作系统主要包括Windows和Linux。所以针对移动存储设备,主要考虑制作系统软件来应对此类升级。为了减少软件开发工作,建议使用跨平台方案,保证最大限度的复用代码。为了更好地操作底层API,并且有效率的开发软件,采用C++作为软件开发语言,选用Qt作为跨平台的开发框架,并且都采用g++编译器。无论是在Linux平台,还是在Windows平台,升级的基本流程基本是相同的。如下图:
抽象出Linux和Windows下不同点,统一抽象的接口,然后复用宏WIN32区分不同系统,主要包括:
- 设备的标识,Windows下以盘符(E:\,F:\等)作为标识,Linux下则以设备路径(/dev/sdb, /dev/sdc等)作为标识。
- 文件路径,Windows下以反斜杠\作为分隔符,Linux下则以斜杠/作为分隔符。
- 和设备通信,和USB设备通信,数据层是采用SCSI协议,传输层采用的USB协议。应用软件直接采用SCSI协议与设备进行通信即可。
a. Windows层的通信主要代码:
// Open device
char szLetter[] = "\\.\G:;
HANDLE hDev = CreateFile((LPCSTR)_devFile, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// Transport CMD to device
if (!DeviceIoControl(hDev,
IOCTL_SCSI_PASS_THROUGH,
pt,
sizeof(buf),
buf,
sizeof(buf),
&bytes,
NULL)) {
printf("IOCTL failed %d\n", GetLastError());
}
// Close device
CloseHandle(hDev);
b. Linux下通信的主要代码:
// Open device
int fd = open("/dev/sde", O_RDWR);
// Transport CMD
unsigned char buff[1024] = {0};
unsigned char inq_cmd[] = {WRITE_10, 00, 0, 0, 0, 0, 0, 0, 0x2, 0};
unsigned char sense[32] = {0};
struct sg_io_hdr io_hdr = {};
io_hdr.interface_id = 'S';
io_hdr.cmdp = inq_cmd;
io_hdr.cmd_len = sizeof(inq_cmd);
io_hdr.dxferp = buff;
io_hdr.dxfer_len = 32;
io_hdr.dxfer_direction = SG_DXFER_TO_DEV;
io_hdr.sbp = sense;
io_hdr.mx_sb_len = sizeof(sense);
io_hdr.timeout = 5000;
ioctl(fd, SG_IO, &io_hdr);
// Close device
close(fd);
3.2.2. eMMC/UFS
eMMC/UFS主要用于手机、平板和一些嵌入式设备,手机和平板基本是Andriod系统,嵌入式也多是Andriod和Linux。而Andriod的内核也基本上就是Linux内核了。Andriod因为安全机制,不允许应用程序直接与存储储备通信,所有的数据传输都是加密的,所以Andriod也不方便通过应用程序来进行升级固件。
无论是Andriod还是嵌入式Linux,都是通过u-boot来启动系统的。u-boot中都已经携带有eMMC/UFS的驱动代码,并且u-boot中都会初始化eMMC/UFS,然后读取存放在eMMC/UFS上的系统进行引导启动。所以在u-boot中进行固件升级,不仅可以绕开Andriod的安全机制,升级方式也可以与Linux统一。
eMMC自从协议4.0版之后,协议提出统一的固件更新规则FFU(Field Firmware Update,现场固件更新)。其基本流程为Host发送命令进入FFU模式,通过写命令将固件bin文件写入设备存储在NAND Flash相应位置,然后Host发送激活命令、或硬件复位或者断上电操作,就可以完成固件更新。
其协议流程为:
u-boot流程,修改完u-boot之后,编译成二进制文件,然后与Andriod文件一起推送到手机终端,等待用户升级系统过程中,先完成对eMMC/UFS的升级。
// 在mmc_inti之后执行如下流程
mmc_set_clock(mmc, mmc->tran_speed,MMC_CLK_ENABLE);
mmc_switch(mmc, FFU_MODE);
mmc_write(mmc, addr, buff, size);
mmc_switch(mmc, NORMAL_MODE);
mmc_power_cycle(mmc);
UFS协议继承了eMMC的FFU,其流程基本一样,只不过在激活新固件时,简化了eMMC原有的方式,只支持HW Reset或Power Cycle。
3.2.3. SATA SSD
SATA接口的SSD主要用于个人电脑。如果SSD作为电脑的从盘(非操作系统盘),则可以直接使用系统软件。如果SSD作为电脑的主盘(操作系统盘),由于操作系统的限制,无法直接与主盘通信。操作系统有MAC、Windows、Linux等,而且cpu内核也有x86/x86-64/Arm等,如果要编译系统软件,会有很多版本。有没有一种通用的方法兼容所有情况呢?
为了解决主盘无法直接升级,需要另外接入系统,让主盘作为从盘。
3.2.3.1. UEFI应用
当前电脑系统的启动都是通过UEFI来引导启动操作系统的,可以考虑编写UEFI应用程序,通过UEFI程序来完成对SSD固件的更新。
3.2.3.2. WinPE应用
现在安装系统,都是使用U盘来完成系统,先在U盘中安装一个Win PE的启动系统来引导安装操作系统。Win PE是一个简化版的Windows系统,可以运行基本的Windows应用程序。这种方式可以不用管电脑原来是什么操作系统,只需要针对不同CPU制作不同的Win PE启动盘即可。
● 升级工具:
SATA接口的SSD使用ATA协议来通信,并且兼容SCSI的通用命令。
可以通过SCSI的3种操作码,来配置3种ATA通信协议。
主要通信代码:
SCSI_PASS_THROUGH spt = {0};
spt.Length = sizeof(SCSI_PASS_THROUGH);
spt.TimeOutValue = 2;
spt.CdbLength = 12; // 16,32
spt.Cdb[0] = 0xA1; // 0x85,0x7F
spt.Cdb[1] = 3 << 1;
memcpy(&spt.Cdb[3], aptex.CurrentTaskFile, 8);
ret = DeviceIoControl(handle, IOCTL_SCSI_PASS_THROUGH,
&spt, sizeof(SCSI_PASS_THROUGH), NULL, 0, &nRet, NULL);
也可以直接通过ATA协议来与设备通信,其主要代码:
ATA_PASS_THROUGH_EX aptex = {0};
aptex.Length = sizeof(ATA_PASS_THROUGH_EX);
aptex.TimeOutValue = 2;
aptex.CurrentTaskFile[6] = 0xEF;
aptex.CurrentTaskFile[0] = 0x05;
aptex.CurrentTaskFile[1] = 0x80;
ret = DeviceIoControl(handle, IOCTL_ATA_PASS_THROUGH, &aptex,
sizeof(ATA_PASS_THROUGH_EX), NULL, 0, &nRet, NULL);
● WinPE系统盘
- 制作WinPE需要安装ADK,不同版本WinPE对应不同的ADK,下载合适的ADK版本安装。
- 以管理员身份启动“部署和映像工具环境” 。
- 运行“copype”以创建 Windows PE 文件的工作副本。
// 提取64位的WinPE资源文件
copype amd64 C:\WinPE_amd64
// 提取32位的WinPE资源文件
copype x86 C:\WinPE_x86 - 提取镜像中文件
- 在WinPE中添加固件更新工具。
把fw_update_tool.exe放在mout\Program Files目录。 - 启动WinPE之后自动执行升级工具。
wpeinit
cd …
cd “Program Files\FWupdateTool”
FWupdateTool.exe - 提交修改
将相关的修改提交到新的winpe.wim中,并卸载所有提取的文件。
Dism /unmount-Wim /MountDir:C:\winpe_x86\mount /Commit - 拷贝Winpe.wim至IOS目录
copy winpe.wim C:\winpe_x86\ISO\sources\boot.wim /y - 生成镜像文件
oscdimg -n -bC:\winpe_x86\etfsboot.com C:\winpe_x86\iso C:\winpe.iso - 利用Ultraiso写入镜像
利用Ultraiso打开Winpe.iso,然后菜单选择启动->写入硬盘映像,选择指定U盘,点击确认,等待完成,winpe启动盘即制作完成。
也可以使用命令行制作WinPE系统:
set PEPath=C:\win10PE_x86
call "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\DandISetEnv.bat"
if exist %PEPath% rmdir /s /q %PEPath%
call CopyPE.cmd x86 %PEPath%
Dism /Mount-Image /ImageFile:%PEPath%\media\sources\boot.wim /index:1 /MountDir:%PEPath%\mount
pause
Dism /Unmount-Image /MountDir:%PEPath%\mount /commit
rem MakeWinPEMedia /ISO %PEPath% %PEPath%\Win10PE_x86.ISO
MakeWinPEMedia /ufd %PEPath% g:
3.2.4. PCIe SSD
PCIe接口的SSD主要用高性能场景,如高价个人电脑,还有云存储、企业分布式存储等。PCIe接口的SSD采用NVMe通信协议,协议中也有规定固件升级流程,和FFU流程大同小异,下载固件,激活固件。
● Windows下的实现流程:
BOOL DeviceStorageFirmwareUpgrade(int _nPhyNo, BYTE _slotID)
{
QString strDeviceName = QString("%1%2").arg("\\\\.\\Physicaldrive").arg(_nPhyNo);
HANDLE deviceHandle = CreateFile(strDeviceName.toStdWString().data(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING,
nullptr
);
if (deviceHandle == INVALID_HANDLE_VALUE)
{
return FALSE;
}
// Setup header of firmware download data structure.
const DWORD dwBuffSize = FIELD_OFFSET(STORAGE_HW_FIRMWARE_DOWNLOAD, ImageBuffer) + 4*1024;
QScopedPointer<char> scopeBuff(new char[dwBuffSize]());
PSTORAGE_HW_FIRMWARE_DOWNLOAD firmwareDownload = reinterpret_cast<PSTORAGE_HW_FIRMWARE_DOWNLOAD>(scopeBuff.get());
firmwareDownload->Version = sizeof(STORAGE_HW_FIRMWARE_DOWNLOAD);
firmwareDownload->Size = dwBuffSize;
firmwareDownload->Flags = STORAGE_HW_FIRMWARE_REQUEST_FLAG_CONTROLLER;
firmwareDownload->Slot = 0x11;
// Open image file and download it to controller.
ULONGLONG imageBufferLength = dwBuffSize - FIELD_OFFSET(STORAGE_HW_FIRMWARE_DOWNLOAD, ImageBuffer);
QString strFilePath = ":/libra_cpu01_sysfw.bin";
QFile file(strFilePath);
if (!file.open(QIODevice::ReadOnly))
{
qDebug()<<"Open "<<strFilePath<<" failed.";
return FALSE;
}
ULONG imageOffset = 0;
BOOL moreToDownload = TRUE;
while (moreToDownload)
{
RtlZeroMemory(firmwareDownload->ImageBuffer, imageBufferLength);
qint64 readLength = file.read(reinterpret_cast<char*>(firmwareDownload->ImageBuffer), static_cast<qint64>(imageBufferLength));
if (readLength == 0)
{
file.close();
break;
}
firmwareDownload->Offset = imageOffset;
firmwareDownload->BufferSize = min(imageBufferLength, static_cast<ULONG>(readLength));
ULONG returnedLength = 0;
BOOL result = DeviceIoControl(deviceHandle,
IOCTL_STORAGE_FIRMWARE_DOWNLOAD,
scopeBuff.get(),
dwBuffSize,
scopeBuff.get(),
dwBuffSize,
&returnedLength,
nullptr);
if (!result)
{
return FALSE;
}
imageOffset += static_cast<ULONG>(firmwareDownload->BufferSize);
}
// Activate the newly downloaded image.
RtlZeroMemory(scopeBuff.get(), dwBuffSize);
PSTORAGE_HW_FIRMWARE_ACTIVATE firmwareActivate = reinterpret_cast<PSTORAGE_HW_FIRMWARE_ACTIVATE>(scopeBuff.get());
firmwareActivate->Version = sizeof(STORAGE_HW_FIRMWARE_ACTIVATE);
firmwareActivate->Size = sizeof(STORAGE_HW_FIRMWARE_ACTIVATE);
firmwareActivate->Slot = _slotID;
firmwareActivate->Flags = STORAGE_HW_FIRMWARE_REQUEST_FLAG_CONTROLLER;
// activate firmware
ULONG returnedLength = 0;
BOOL result = DeviceIoControl(deviceHandle,
IOCTL_STORAGE_FIRMWARE_ACTIVATE,
scopeBuff.get(),
dwBuffSize,
scopeBuff.get(),
dwBuffSize,
&returnedLength,
nullptr
);
return result;
}
● Linux下有开源工具NVME-cli,可以直接使用:
> nvme fw-download /dev/nvme0 -f allBinary.bin nvme fw-commit /dev/nvme0
> -s 2 -a 1
> # 参数-s代表slot。标准定义SSD支持7个slot,slot 1 是只读权限,用于存放出厂固件,slot 2和3 可用于固件下载。
> # 参数-a代表不同的升级方法,常用的有两个。001b(向指定slot下载固件,需要reset后完成激活操作);
> # 011b(向指定slot下载固件,激活立即生效,固件升级完成)
4. 参考资料
- NVM Express TM Revision 1.4
- Information technology -SCSI / ATA Translation - 5 (SAT-5)
- ATA Command Pass-Through
- Universal Serial Bus Mass Storage Class UFI Command Specification
- Universal Flash Storage (UFS)Version 3.1
- Embedded Multi-Media Card (e•MMC) Electrical Standard (5.1)
- https://github.com/linux-nvme/nvme-cli/
- https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-ioctl_storage_firmware_activate
- https://learn.microsoft.com/zh-cn/windows-hardware/manufacture/desktop/winpe-intro?view=windows-11
- https://github.com/u-boot/u-boot