USB SPOR 工具 MFC 开发问题集

1 Sector = 0.5 K B= 512Byte

2 Sector = 1 KB

128 Sector = 64 KB

1. SPOR的整体架构

主线程:循环写 → 被打断 → 校验 → 再循环写
掉电线程:随机时刻打断写 → 掉电 →  保持掉电2秒 → 上电 → 交回主线程进行校验

1.1 整体逻辑

上电 → 主线程连续写入
掉电线程 1~5 秒内随机到断电
不管主线程写到哪,直接断
上电 → 主线程校验上一次写入段
校验成功 → 继续连续写入

日志输出:
[通道0] 第1轮 | 写入起始LBA: 0     | 长度: 156
[通道0] 执行掉电
[通道0] 执行上电 ⚡
[通道0] 开始校验 | 起始LBA: 0     | 长度: 156
[通道0] 第1轮 校验成功 ✅

[通道0] 第2轮 | 写入起始LBA: 156   | 长度: 273
[通道0] 执行掉电
[通道0] 执行上电 ⚡
[通道0] 开始校验 | 起始LBA: 156   | 长度: 273
[通道0] 第2轮 校验成功 ✅

1.2 写入的数据模型

每个扇区的数据都是 测试次数+LBA

每一个扇区(512Byte)结构:
【0~3字节】  SPOR 轮次
【4~511字节】 全是当前这个扇区的 LBA 地址(重复填充)
✅ 扇区 0:SPOR 次数 + LBA0
✅ 扇区 1:SPOR 次数 + LBA1
✅ 扇区 2:SPOR 次数 + LBA2

2. SPOR测试过程打开日志文件导致异常中断

原因:文件没有共享权限,当文件被占用 or 多线程写日志没有同步就会出现这个问题

流程:文件独占 + 错误写入 + 无锁 + 错误判断 → 打开log就触发异常

  1. 文件被记事本占用
  2. 你用独占模式打开 → 失败
  3. 失败后逻辑继续执行
  4. 多线程同时写 → 放大问题,出现异常中断

2.1 创建文件时没有设置共享模式

// 未设置共享模式:默认情况下是独占访问(不允许其他程序打开)
MyFile.Open(pszlogName, CFile::modeReadWrite)
    
// 设置共享模式:
CFile file;
file.Open(pszlogName,CFile::modeWrite | CFile::modeCreate | CFile::modeNoTruncate)

MFC CFile 默认规则:

  1. 只要用了CFile::modeWrite 或 CFile::modeCreate系统会自动加上 FILE_SHARE_READ | FILE_SHARE_WRITE 共享权限!
  2. 只用 CFile::modeReadWrite 没有共享权限
代码 共享权限 多线程安全 是否适合 SPOR 测试
modeReadWrite ❌ 无 不安全,会锁文件 ❌ 不推荐
modeWrite / modeCreate 安全 推荐

2.2 多线程写入日志文件时无锁 SPOR 容易出问题

一般来说主线程 + 掉电线程 → 同时写log,如果没有任何锁容易出现文件指针竞争、写入交错甚至崩溃

static CRITICAL_SECTION logLock;
EnterCriticalSection(&logLock);
LeaveCriticalSection(&logLock);

2.3 Windows 记事本打开日志输出乱码问题

问题:日志输出使用 SublimeText 能正常打开不会乱码,使用Window11系统自带的记事本打开出现乱码

SublimeText:宽容 → 自动猜编码 → 正常显示
记事本:严格 → 解析失败 → 乱码

编辑器 行为
Sublime 自动猜编码(容错强)
Notepad 严格按 BOM + 编码解析
// 只需要在调用File.Open之后增加
if (file.GetLength() == 0)
{
    WORD bom = 0xFEFF;  // UTF-16 LE BOM
    file.Write(&bom, sizeof(bom));
}

3. 使用 GDI/DC 日志输出至 UI 界面问题

如果使用 GDI/DC 进行日志输出至UI界面,很容易犯的两个典型坑:

  1. GetDC 没有 ReleaseDC(会耗尽GDI资源)
  2. 递归 + 高频调用

3.1 最直接的问题:GetDC 没有 ReleaseDC(会耗尽GDI资源)

CDC *pDC = pListBox->GetDC();
...
pDC->SelectObject(pOldFont);

// 但没有释放
pListBox->ReleaseDC(pDC);
❗结果:SPOR长时间跑:
每次调用 → GetDC → 不释放
→ GDI Handle 持续增长
→ 达到上限(~10k)
→ SelectObject / GetTextExtent 崩

3.2 第二个隐患:递归 + 高频调用

if(len != strLen)
{
    ListBoxShowInfo(pListBox, infoStr.Right(strLen - len));
}
👉 问题:
长字符串 → 多次递归
每一层都:
GetDC
SelectObject
在SPOR日志高频输出下
👉 GDI压力被放大

3.3 ListBox本身也有上限

Win32 ListBox:

  • 默认最多 ~32767 条(早期)
  • 实际受内存/GDI影响

4. 了解什么是 GDI(顺便复习一下)

GDI 是 Graphics Device Interface(图形设备接口),是 Windows 提供的一套底层绘图系统,负责把“画图操作”真正画到屏幕、打印机等设备上。

GDI = Windows 负责画界面的底层引擎

CDC *pDC = pListBox->GetDC();
CFont *pOldFont = pDC->SelectObject(pFont);
CSize sz = pDC->GetTextExtent(infoStr, len);
// 👉 这些全部都是 GDI操作
API 本质
GetDC 获取绘图上下文(画布)
SelectObject 选择字体/画笔
GetTextExtent 计算文本尺寸
ReleaseDC 释放资源

4.1 GDI 的核心概念(必须理解的)

1️⃣ DC(Device Context)

可以理解为:画布 + 画图工具集合

👉 你要画任何东西,都必须先拿到 DC:

CDC *pDC = GetDC();

2️⃣ GDI对象

比如:

  • 字体(CFont)
  • 画笔(Pen)
  • 画刷(Brush)
  • 位图(Bitmap)

👉 使用前必须:

SelectObject

3️⃣ GDI资源是有限的(重点)

Windows 每个进程:

GDI对象上限 ≈ 10,000(典型值)

4.2 典型错误模式(刚好踩了)

❌ 错误1:GetDC 不释放

pListBox->GetDC();
// 没有 ReleaseDC

❌ 错误2:频繁GDI操作

循环里疯狂 GetTextExtent

❌ 错误3:多线程用GDI

👉 GDI本身不是线程安全的

4.3 正确使用方式(标准写法)

CDC *pDC = pListBox->GetDC();
if (pDC)
{
    CFont *pOldFont = pDC->SelectObject(pFont);
    CSize sz = pDC->GetTextExtent(infoStr);
    pDC->SelectObject(pOldFont);
    pListBox->ReleaseDC(pDC);   // ★必须
}

5. Windows 的设备插拔通知导致 PC 卡顿

当进行 SPOR 测试时:

掉电 → USB断开 → 上电 → 重新枚举

Windows 会触发:

Shell Hardware Detection (ShellHWDetection)

它会做:

  • 自动播放检测
  • 设备变化广播
  • Explorer刷新
  • 弹提示

👉 SPOR 高频触发 → 系统消息风暴 → UI卡顿

5.1 手动禁用 Shell Hardware Detection (ShellHWDetection)

❗注意:👉 测试前停 → 测试后恢复

桌面我的电脑 → 右键找到“管理”→ 服务 → Shell Hardware Detection (ShellHWDetection)停止Shell HardWare Detection

5.2 程序里代码控制 ShellHWDetection

启动SPOR工具时停止 ShellHWDetection,退出SPOR工具时恢复启动 ShellHWDetection

必须要用管理员模式运行 SPOR 工具才能控制 ShellHWDetection,不然权限不够

#include <windows.h>
#include <winsvc.h>   // ★关键:服务控制API定义在这里
#pragma comment(lib, "Advapi32.lib")
👉 加在 .cpp 顶部即可

BOOL CDeviceOpreationDlg::StopShellHWDetection()
{
	SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
	if (!hSCM) return FALSE;

	SC_HANDLE hService = OpenService(
		hSCM,
		_T("ShellHWDetection"),
		SERVICE_STOP | SERVICE_QUERY_STATUS
		);

	if (!hService)
	{
		CloseServiceHandle(hSCM);
		return FALSE;
	}

	SERVICE_STATUS status;
	ControlService(hService, SERVICE_CONTROL_STOP, &status);

	// 等待停止
	for (int i = 0; i < 20; i++)
	{
		QueryServiceStatus(hService, &status);
		if (status.dwCurrentState == SERVICE_STOPPED)
			break;
		Sleep(200);
	}

	CloseServiceHandle(hService);
	CloseServiceHandle(hSCM);

	return TRUE;
}
BOOL CDeviceOpreationDlg::StartShellHWDetection()
{
	SC_HANDLE hSCM = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
	if (!hSCM) return FALSE;

	SC_HANDLE hService = OpenService(
		hSCM,
		_T("ShellHWDetection"),
		SERVICE_START | SERVICE_QUERY_STATUS
		);

	if (!hService)
	{
		CloseServiceHandle(hSCM);
		return FALSE;
	}

	StartService(hService, 0, NULL);

	SERVICE_STATUS status;
	for (int i = 0; i < 20; i++)
	{
		QueryServiceStatus(hService, &status);
		if (status.dwCurrentState == SERVICE_RUNNING)
			break;
		Sleep(200);
	}

	CloseServiceHandle(hService);
	CloseServiceHandle(hSCM);

	return TRUE;
}

6. 多设备并发 SPOR 触发系统层竞争

6.1 原因

6.1.1 为什么单盘OK,双盘就不行

当两个设备同时:

掉电 → 上电 → 枚举 → 挂载 → 打开句柄

Windows 内部发生:

1. USB Hub 复位(可能是同一个Hub)
2. PnP 设备枚举(串行/锁)
3. 磁盘类驱动初始化
4. 卷管理器挂载(MountMgr)
5. 文件系统锁

👉 两个盘一起,会出现:

✔ 枚举延迟
✔ 盘符暂时不存在
✔ 设备 busy
✔ 句柄被系统占用
✔ CreateFile 返回失败
6.1.2 为什么“线程独立”仍然会冲突

假设并发是这样:

线程A:USB1 掉电 → 上电 → CreateFile
线程B:USB2 掉电 → 上电 → CreateFile

但 Windows 内部实际是:

USB Hub(共享)
  ↓
USB Host Controller(共享)
  ↓
PnP Manager(全局锁/串行化)
  ↓
Disk.sys / ClassPnP(共享队列)
  ↓
MountMgr(盘符分配,强串行)

👉 关键点:

这些路径不是 per-device 独立的,而是“全局共享 + 有锁”

所以会出现:

  • 一个设备正在枚举,另一个设备的 I/O 被阻塞
  • MountMgr 正在处理盘符,另一个设备 CreateFile 失败
  • 设备节点还没 ready,但已经访问了

典型竞态如下:

T0:USB1 上电 → 开始枚举
T1:USB2 上电 → 也开始枚举
T2:系统正在处理 USB1(锁住)
T3:线程B 去 CreateFile(USB2) → 设备还没 ready → FAIL
6.1.3 容易忽略的点

❗盘符不是稳定资源,盘符分配是动态的

可能发生:

  • 上电瞬间没有盘符
  • 盘符被延迟挂载
  • 两个设备竞争 MountMgr

👉 这也会导致:

ERROR_FILE_NOT_FOUND / ERROR_PATH_NOT_FOUND
6.1.4 正确的解决思路(不是简单加锁)

✅ 1. CreateFile 必须变成“状态等待”

不是:

失败 → return

而是:

失败 → 等待设备ready → 重试

多设备要更激进一点

// 建议:5~10秒窗口
for (int i = 0; i < 100; i++)
{
    ...
    Sleep(100);
}

但笔者测试过似乎并没有完全解决这个问题,只是降低了发生竞争的概率,应该还有其他解决方案,暂时未探讨!

6.2 CreateFile 失败不直接返回 NULL 或错误

❗核心思想:CreateFile失败 ≠ 错误,而是“设备暂时不可用”

HANDLE CDeviceOpreationDlg::GetDiskHandle(CString diskName)
{
    CString path;
    path.Format(_T("\\\\.\\%s"), diskName);

    HANDLE hDev = INVALID_HANDLE_VALUE;
    DWORD err = 0;

    // 等待最长 5 秒(多设备必须加长)
    for (int i = 0; i < 50; i++)
    {
        hDev = CreateFile(
            path,
            GENERIC_READ | GENERIC_WRITE,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            NULL);

        if (hDev != INVALID_HANDLE_VALUE)
        {
            return hDev;  // 成功
        }

        err = GetLastError();

        // 关键:这些错误是“正常的”
        if (err == ERROR_ACCESS_DENIED ||
            err == ERROR_NOT_READY ||
            err == ERROR_FILE_NOT_FOUND ||
            err == ERROR_PATH_NOT_FOUND ||
            err == ERROR_SHARING_VIOLATION)
        {
            Sleep(100);   // 等设备稳定
            continue;
        }

        // 其他异常才认为是真失败
        break;
    }

    CString str;
    str.Format(_T("GetDiskHandle fail! err=%d disk=%s"), err, diskName);
    Infomation_Deal(ONLY_SAVE, str);

    return INVALID_HANDLE_VALUE;
}

6.3 GPT 给出的方案

这个问题没有“彻底消除冲突”的办法,只能工程化地规避系统瓶颈。你现在的冲突点在三层:

USB Host / Hub(硬件共享)
→ PnP / 枚举(内核串行)
→ MountMgr / 文件系统(全局资源)

所以思路不是“让它们完全并发”,而是:

把“破坏性操作(掉电/上电/枚举)串行化”,把“数据IO”并行化

下面给你一套可落地的分层方案(按收益排序)。


6.3.1 核心方案:掉电调度器(必须做)

❗问题本质

多个设备 → 同时掉电 → 同时上电 → 同时枚举

👉 这一步直接把:

USB + PnP + MountMgr 全部打爆

✅ 解决:全局掉电仲裁(推荐)

CRITICAL_SECTION g_SporPowerLock;

掉电线程:

EnterCriticalSection(&g_SporPowerLock);
// 掉电
AXxxxDev.ControlDevicePower(cmdHdev, FALSE);
Sleep(...);

// 上电
AXxxxDev.ControlDevicePower(cmdHdev, TRUE);
Sleep(...);

LeaveCriticalSection(&g_SporPowerLock);

✔ 效果

✔ 掉电不会同时发生
✔ 枚举不会冲突
✔ CreateFile 成功率大幅提升

⚠️ 注意

锁住的是“掉电过程”,不是整个测试

IO仍然可以并发。


6.3.2 进阶方案:时间片调度(比锁更优雅)

比起“硬锁”,更推荐:

✅ 分时调度(工业常用)

设备0:0~5秒窗口掉电
设备1:5~10秒窗口掉电
设备2:10~15秒窗口掉电

实现:

int base = channel * 5000;
Sleep(base + rand() % 5000);

✔ 优势

✔ 无锁
✔ 不会完全串行
✔ 更接近真实随机SPOR

6.3.3 设备Ready检测(必须补)

✅ 正确做法:以“设备可打开”为准

HANDLE hDev;
for (int i = 0; i < 100; i++)
{
    hDev = CreateFile(...);
    if (hDev != INVALID_HANDLE_VALUE)
        break;
    Sleep(100);
}

✔ 本质

CreateFile成功 = 设备ready(唯一可信信号)

6.3.4 避免盘符竞争(强烈建议)

问题:

盘符由 MountMgr 分配 → 强串行资源

✅ 改成:

\\.\PhysicalDriveX

✔ 优势

✔ 不依赖文件系统
✔ 枚举更早可访问
✔ 多设备更稳定

6.3.5 降低系统干扰

✅ 必做

测试前:

关闭 ShellHWDetection 服务

否则:

每次上电 → 系统扫描 → 抢占设备

6.3.6 I/O层容错(必须做)

✅ 正确:

if (!ReadSectorDataFromUDisk(...))
{
    // 重试几次
    for (int retry = 0; retry < 5; retry++)
    {
        Sleep(50);
        if (ReadSectorDataFromUDisk(...))
            break;
    }
}

✔ 原因

设备刚ready → 短时间IO不稳定(正常现象)

6.3.7 硬件层优化(很多人忽略)

如果你真要跑多设备:


❗不要用同一个 USB Hub

笔记本/前面板USB口 → 很可能同一个Hub ❌

✅ 推荐

✔ 不同Root Hub(主板后置口)
✔ PCIe USB扩展卡
✔ 工业USB HUB(带独立控制器)

6.3.8 终极方案(量产级)

引入一个SPOR调度状态机

状态:
IDLE
WRITE
WAIT_POWER_SLOT
POWER_DOWN
POWER_UP
WAIT_READY
VERIFY

统一调度:

任何时刻:
只允许1个设备处于 POWER_DOWN / POWER_UP

6.3.9 一句话解决方案

掉电串行化 + IO并行化 + CreateFile等待机制


6.3.10、最小可落地组合(建议先做)

先改这三点,就能明显稳定:

① 掉电加全局锁(或错峰)
② CreateFile失败重试(5~10秒)
③ Read/Write失败重试

如果你后面要扩到:

4路 / 8路 / 16路 SPOR

那就必须上:

调度器 + 状态机 + 设备管理层

否则不是“偶现问题”,而是“必然不稳定”。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐