【内存测试】06-WPF 读取 SMBIOS 实现内存规格自动检测

【内存测试】06-WPF 读取 SMBIOS 实现内存规格自动检测

系列:《WPF 产线功能测试实战》第六篇
关键词 :WPF C# SMBIOS P/Invoke 内存检测 产线测试 .NET Framework Windows API 硬件信息读取 DIMM


一、前言与应用场景

在产线测试中,内存(RAM)的规格验证是不可或缺的一环。一台出厂设备的内存大小是否符合订单规格(8GB/16GB/32GB)?内存频率是否达到设计标准(4800MHz/5600MHz)?每个 DIMM 插槽是否都能被系统识别?这些问题如果靠人工目测,既费时又容易出错。

本文介绍如何在 WPF 中通过 Windows SMBIOS 固件接口自动读取内存规格,并与预设的配置参数进行对比,实现全自动的内存规格验证。Demo 中用本地 JSON 文件替代生产线 MES 系统,任何人都可以直接跑起来调试。


二、基础概念科普

什么是 SMBIOS?

SMBIOS(System Management BIOS)是 BIOS 向操作系统暴露硬件信息的标准接口,由 DMTF 组织制定。你可以把它理解成一张"硬件出生证明"------主板上的 BIOS 在系统启动时,把 CPU、内存、主板、硬盘等硬件的规格信息写入一块内存区域,操作系统和应用程序可以随时来读取。

Windows 提供了 GetSystemFirmwareTable 这个 API,让普通应用程序也能读到这块数据。在 C# 中,通过 P/Invoke(让托管代码调用 Windows 原生 C API 的桥梁机制)来调用它。

Type 17:内存设备信息结构

SMBIOS 数据由若干个"结构"组成,每种结构有固定的类型编号(Type)。Type 17 = Memory Device,就是我们需要的内存信息,每个 DIMM 插槽对应一条 Type 17 记录,哪怕该槽是空的也会有一条。

Type 17 中与我们相关的字段:

字段偏移 字段名 说明
0Ch Size 内存大小,值 < 32768 时单位为 MB,位 15=1 时单位为 KB
15h Speed 内存速度(MHz),0xFFFF 表示未知
10h DeviceLocator 插槽名称的字符串索引(如 "ChannelA-DIMM0")

内存大小字段的位运算小技巧

SMBIOS Type 17 的 Size 字段是一个 ushort(2字节无符号整数),其中最高位(bit 15)决定单位:

复制代码
bit15 = 0 → 低15位 = 内存大小(MB)
bit15 = 1 → 低15位 = 内存大小(KB),需 & 0x7FFF 清除高位

这种节省字节的设计是上世纪 BIOS 规范的遗产,用一个字段同时表达单位和数值。

为什么用本地 JSON 替代 MES?

生产线工具通常从 MES(制造执行系统)获取当前产品应有的内存规格,这样可以自动适配不同 SKU。但在 Demo 和开发调试阶段,不需要依赖 MES 服务器,因此改用一个 JSON 配置文件,让开发者自行填写期望的内存大小和频率,方便本地调试和演示。


三、方案设计与架构

测试流程

MVVM 职责分层

层 文件 职责
View MainWindow.xaml 显示测试状态和结果,按钮触发
ViewModel MainVM.cs 编排三步测试,更新绑定属性
工具类 SmbiosReader.cs 封装 P/Invoke,解析 Type 17
配置 MemoryConfig.cs 读写本地 JSON 配置文件

核心依赖

本 Demo 只需 Newtonsoft.Json(NuGet 安装)用于解析配置文件,不依赖任何其他第三方库。SMBIOS 读取完全通过 Windows 原生 API 实现。


四、核心代码详解

4.1 P/Invoke 声明与 SMBIOS 数据结构

csharp 复制代码
// GetSystemFirmwareTable:向 Windows 请求固件表数据
// FirmwareTableProviderSignature = 0x52534D42 对应 ASCII "RSMB"
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint GetSystemFirmwareTable(
    uint FirmwareTableProviderSignature,
    uint FirmwareTableID,
    IntPtr pFirmwareTableBuffer,
    uint BufferSize);

// SMBIOS 数据块的头部(固定5字节)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct RawSMBIOSData
{
    public byte Used20CallingMethod;
    public byte SMBIOSMajorVersion;
    public byte SMBIOSMinorVersion;
    public byte DmiRevision;
    public uint Length;             // 后续 SMBIOS 表的字节长度
}

// Type 17 Memory Device 结构(精简,只映射到我们需要的字段)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct MemoryDeviceInfo
{
    public byte Type;               // 固定为 17
    public byte Length;             // 本结构的格式化区域长度
    public ushort Handle;
    public ushort PhysicalArrayHandle;
    public ushort ErrorInfoHandle;
    public ushort TotalWidth;
    public ushort DataWidth;
    public ushort Size;             // 0Ch:内存大小,见位运算说明
    public byte FormFactor;
    public byte DeviceSet;
    public byte DeviceLocator;      // 10h:插槽名称字符串的索引
    public byte BankLocator;
    public byte MemoryType;
    public ushort TypeDetail;
    public ushort Speed;            // 15h:内存频率(MHz)
}

Pack = 1 告诉运行时按 1 字节对齐,与 SMBIOS 规范一致,不能省略,否则字段偏移会错位。

4.2 获取内存大小(累加所有插槽)

csharp 复制代码
public double GetTotalMemorySizeGB()
{
    const uint RSMB = 0x52534D42;
    uint bufferSize = GetSystemFirmwareTable(RSMB, 0, IntPtr.Zero, 0); // 先查大小
    if (bufferSize == 0) return 0;

    IntPtr buffer = Marshal.AllocHGlobal((int)bufferSize);
    try
    {
        GetSystemFirmwareTable(RSMB, 0, buffer, bufferSize);

        RawSMBIOSData header = Marshal.PtrToStructure<RawSMBIOSData>(buffer);
        IntPtr current = IntPtr.Add(buffer, Marshal.SizeOf<RawSMBIOSData>());
        IntPtr end = IntPtr.Add(current, (int)header.Length);

        double totalGB = 0;
        while (current.ToInt64() < end.ToInt64())
        {
            var info = Marshal.PtrToStructure<MemoryDeviceInfo>(current);
            if (info.Type == 17 && info.Size != 0 && info.Size != 0xFFFF)
            {
                // bit15=0 → MB;bit15=1 → KB(低15位才是数值)
                double mb = (info.Size < 32768)
                    ? info.Size
                    : (info.Size & 0x7FFF) / 1024.0;
                totalGB += mb / 1024.0;
            }
            current = SkipToNext(current, end, info.Length);
            if (current == IntPtr.Zero) break;
        }
        return Math.Round(totalGB, 0); // 四舍五入到整数 GB
    }
    finally { Marshal.FreeHGlobal(buffer); }
}

为什么要累加? 一台 16GB 内存的机器可能由两条 8GB 组成(双通道),SMBIOS 里会有两条 Type 17 记录,必须全部加起来才是总量。

4.3 获取内存频率

频率的读取逻辑比大小简单------取第一个有效的 Type 17 记录的 Speed 字段即可(同一台机器的内存条通常频率相同):

csharp 复制代码
public double GetMemorySpeedMHz()
{
    // ... 同上,获取 buffer 并解析头部 ...
    while (current.ToInt64() < end.ToInt64())
    {
        var info = Marshal.PtrToStructure<MemoryDeviceInfo>(current);
        if (info.Type == 17 && info.Speed != 0 && info.Speed != 0xFFFF)
            return info.Speed; // 直接返回第一个有效值(MHz)
        current = SkipToNext(current, end, info.Length);
        if (current == IntPtr.Zero) break;
    }
    return 0;
}

4.4 遍历 DIMM 插槽,读取 Device Locator 字符串

为什么要测这一项?

前两步(大小 + 频率)验证的是内存的总量规格 ,但它们存在一个盲区:只要总量对上,单个插槽出问题不会被发现。

举个真实场景:一台设计为双通道 16GB 的笔记本(两个槽各插 8GB),如果其中一个槽的焊接虚焊或接触不良,BIOS 可能仍然能凑出 16GB 的总量(有时会 fallback 到单槽),但内存控制器其实只认出了一个槽。这种情况下,大小测试通过,性能却只有单通道,出厂后用户会明显感受到内存带宽不足。

Device Locator 是 BIOS 在初始化时为每个已识别插槽写入的物理位置标签(例如 ChannelA-DIMM0、ChannelB-DIMM0)。BIOS 能写出完整的 Device Locator,意味着该槽完成了初始化握手------内存颗粒被控制器正确识别,SPD 信息(内存条自带的参数 EEPROM)也被成功读取。若某个槽的 Device Locator 为空或不可读,则说明该槽在 BIOS POST 阶段就出了问题,属于硬件级故障。

简而言之:大小 + 频率 验证规格是否符合订单,Device Locator 验证每个物理插槽是否被硬件层面真正识别------两者缺一不可,覆盖的失效模式不同。


Type 17 的 DeviceLocator 字段存的是一个字符串索引 ,不是字符串本身。真正的字符串在结构的格式化区域之后,按 index 顺序排列,以 \0 分隔,整个字符串段以双 \0 结束:

csharp 复制代码
private string GetSMBIOSString(IntPtr structPtr, byte structLength, byte index)
{
    if (index == 0) return string.Empty;
    IntPtr strPtr = IntPtr.Add(structPtr, structLength); // 跳过格式化区域
    int cur = 1;
    while (Marshal.ReadByte(strPtr) != 0) // 未到双'\0'结尾
    {
        if (cur == index) return ReadNullTermString(strPtr);
        while (Marshal.ReadByte(strPtr) != 0) strPtr = IntPtr.Add(strPtr, 1);
        strPtr = IntPtr.Add(strPtr, 1); // 跳过当前字符串的'\0'
        cur++;
    }
    return string.Empty;
}

4.5 跳转到下一个 SMBIOS 结构

每个 SMBIOS 结构由"格式化区域(固定长度)+ 字符串区域(变长)"组成,遍历时需要同时跳过这两部分:

csharp 复制代码
private IntPtr SkipToNext(IntPtr ptr, IntPtr end, byte structLength)
{
    ptr = IntPtr.Add(ptr, structLength); // 跳过格式化区域
    // 跳过字符串区域,直到出现连续两个 '\0'
    while (ptr.ToInt64() < end.ToInt64() - 1)
    {
        if (Marshal.ReadByte(ptr) == 0 && Marshal.ReadByte(IntPtr.Add(ptr, 1)) == 0)
            return IntPtr.Add(ptr, 2);
        ptr = IntPtr.Add(ptr, 1);
    }
    return IntPtr.Zero;
}

五、完整可运行 Demo

Demo 项目结构

复制代码
MemoryTestDemo/
├── App.xaml
├── MainWindow.xaml          ← 测试结果 UI
├── MainWindow.xaml.cs       ← 代码后置(DataContext 绑定)
├── MainVM.cs                ← 测试主逻辑(ViewModel)
├── SmbiosReader.cs          ← SMBIOS 封装工具类
├── MemoryConfig.cs          ← JSON 配置文件读写
└── memory_config.json       ← 用户可编辑的期望规格(自动生成)

NuGet 依赖 :Newtonsoft.Json(Install-Package Newtonsoft.Json)


memory_config.json(首次运行自动生成,可手动修改)

json 复制代码
{
  "ExpectedMemorySizeGB": 16,
  "ExpectedMemorySpeedMHz": 4800
}

修改 ExpectedMemorySizeGB 和 ExpectedMemorySpeedMHz 两个值即可适配不同机型规格。


MemoryConfig.cs

csharp 复制代码
using System.IO;
using System.Text;
using Newtonsoft.Json;

namespace MemoryTestDemo
{
    public class MemoryConfig
    {
        public double ExpectedMemorySizeGB { get; set; } = 16;
        public double ExpectedMemorySpeedMHz { get; set; } = 4800;

        private static readonly string ConfigPath =
            Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "memory_config.json");

        public static MemoryConfig Load()
        {
            if (!File.Exists(ConfigPath))
            {
                var def = new MemoryConfig();
                def.Save(); // 首次运行:写出默认配置供用户参考
                return def;
            }
            string json = File.ReadAllText(ConfigPath, Encoding.UTF8);
            return JsonConvert.DeserializeObject<MemoryConfig>(json) ?? new MemoryConfig();
        }

        public void Save()
        {
            string json = JsonConvert.SerializeObject(this, Formatting.Indented);
            File.WriteAllText(ConfigPath, json, Encoding.UTF8);
        }
    }
}

SmbiosReader.cs

csharp 复制代码
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;

namespace MemoryTestDemo
{
    public class SmbiosReader
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern uint GetSystemFirmwareTable(
            uint FirmwareTableProviderSignature, uint FirmwareTableID,
            IntPtr pFirmwareTableBuffer, uint BufferSize);

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        private struct RawSMBIOSData
        {
            public byte Used20CallingMethod, SMBIOSMajorVersion, SMBIOSMinorVersion, DmiRevision;
            public uint Length;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        private struct MemoryDeviceInfo
        {
            public byte Type, Length;
            public ushort Handle, PhysicalArrayHandle, ErrorInfoHandle,
                          TotalWidth, DataWidth, Size;
            public byte FormFactor, DeviceSet, DeviceLocator, BankLocator, MemoryType;
            public ushort TypeDetail, Speed;
        }

        private const uint RSMB = 0x52534D42;

        private IntPtr _buffer;
        private IntPtr _tableStart;
        private IntPtr _tableEnd;

        public bool Init()
        {
            uint size = GetSystemFirmwareTable(RSMB, 0, IntPtr.Zero, 0);
            if (size == 0) return false;
            _buffer = Marshal.AllocHGlobal((int)size);
            GetSystemFirmwareTable(RSMB, 0, _buffer, size);
            var hdr = Marshal.PtrToStructure<RawSMBIOSData>(_buffer);
            _tableStart = IntPtr.Add(_buffer, Marshal.SizeOf<RawSMBIOSData>());
            _tableEnd = IntPtr.Add(_tableStart, (int)hdr.Length);
            return true;
        }

        public void Free() { if (_buffer != IntPtr.Zero) Marshal.FreeHGlobal(_buffer); }

        public double GetTotalMemorySizeGB()
        {
            double total = 0;
            ForEachType17(info =>
            {
                if (info.Size == 0 || info.Size == 0xFFFF) return;
                double mb = (info.Size < 32768) ? info.Size : (info.Size & 0x7FFF) / 1024.0;
                total += mb / 1024.0;
            });
            return Math.Round(total, 0);
        }

        public double GetMemorySpeedMHz()
        {
            double speed = 0;
            ForEachType17(info =>
            {
                if (speed > 0) return; // 只取第一个有效值
                if (info.Speed != 0 && info.Speed != 0xFFFF) speed = info.Speed;
            });
            return speed;
        }

        public Dictionary<int, string> GetDimmDeviceLocators()
        {
            var result = new Dictionary<int, string>();
            int idx = 0;
            IntPtr current = _tableStart;
            while (current.ToInt64() < _tableEnd.ToInt64())
            {
                byte type = Marshal.ReadByte(current);
                byte len = Marshal.ReadByte(IntPtr.Add(current, 1));
                if (type == 17)
                {
                    var info = Marshal.PtrToStructure<MemoryDeviceInfo>(current);
                    string loc = GetSMBIOSString(current, len, info.DeviceLocator);
                    result[++idx] = loc;
                }
                current = SkipToNext(current, _tableEnd, len);
                if (current == IntPtr.Zero) break;
            }
            return result;
        }

        private void ForEachType17(Action<MemoryDeviceInfo> action)
        {
            IntPtr current = _tableStart;
            while (current.ToInt64() < _tableEnd.ToInt64())
            {
                byte type = Marshal.ReadByte(current);
                byte len = Marshal.ReadByte(IntPtr.Add(current, 1));
                if (type == 17) action(Marshal.PtrToStructure<MemoryDeviceInfo>(current));
                current = SkipToNext(current, _tableEnd, len);
                if (current == IntPtr.Zero) break;
            }
        }

        private IntPtr SkipToNext(IntPtr ptr, IntPtr end, byte structLength)
        {
            ptr = IntPtr.Add(ptr, structLength);
            while (ptr.ToInt64() < end.ToInt64() - 1)
            {
                if (Marshal.ReadByte(ptr) == 0 && Marshal.ReadByte(IntPtr.Add(ptr, 1)) == 0)
                    return IntPtr.Add(ptr, 2);
                ptr = IntPtr.Add(ptr, 1);
            }
            return IntPtr.Zero;
        }

        private string GetSMBIOSString(IntPtr structPtr, byte structLength, byte index)
        {
            if (index == 0) return string.Empty;
            IntPtr p = IntPtr.Add(structPtr, structLength);
            int cur = 1;
            while (Marshal.ReadByte(p) != 0)
            {
                if (cur == index) return ReadNullTermStr(p);
                while (Marshal.ReadByte(p) != 0) p = IntPtr.Add(p, 1);
                p = IntPtr.Add(p, 1);
                cur++;
            }
            return string.Empty;
        }

        private string ReadNullTermStr(IntPtr ptr)
        {
            int len = 0;
            while (Marshal.ReadByte(ptr, len) != 0) len++;
            if (len == 0) return string.Empty;
            byte[] bytes = new byte[len];
            Marshal.Copy(ptr, bytes, 0, len);
            return Encoding.ASCII.GetString(bytes);
        }
    }
}

MainVM.cs

csharp 复制代码
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows.Media;

namespace MemoryTestDemo
{
    public class MainVM : INotifyPropertyChanged
    {
        private string _statusText = "点击"开始测试"";
        private Brush _statusColor = Brushes.Gray;
        private string _logText = string.Empty;

        public string StatusText { get => _statusText; set { _statusText = value; OnChanged(); } }
        public Brush StatusColor { get => _statusColor; set { _statusColor = value; OnChanged(); } }
        public string LogText { get => _logText; set { _logText = value; OnChanged(); } }

        private readonly SmbiosReader _reader = new SmbiosReader();
        private readonly StringBuilder _log = new StringBuilder();

        public async Task StartTestAsync()
        {
            _log.Clear();
            LogText = string.Empty;

            if (!_reader.Init())
            {
                SetStatus("无法读取 SMBIOS 数据", Brushes.Red);
                return;
            }

            try
            {
                // 加载本地 JSON 配置(期望规格)
                MemoryConfig config = MemoryConfig.Load();
                Log($"期望内存大小:{config.ExpectedMemorySizeGB} GB");
                Log($"期望内存频率:{config.ExpectedMemorySpeedMHz} MHz");
                Log("---");

                // Step 1:内存大小检测
                SetStatus("正在检测内存大小...", Brushes.Yellow);
                double actualSize = _reader.GetTotalMemorySizeGB();
                Log($"SMBIOS 读取内存大小:{actualSize} GB");
                if (actualSize <= 0) { SetStatus("无法从 SMBIOS 读取内存大小", Brushes.Red); return; }
                if (actualSize != config.ExpectedMemorySizeGB)
                {
                    SetStatus($"内存大小不符!期望 {config.ExpectedMemorySizeGB}GB,实际 {actualSize}GB", Brushes.Red);
                    return;
                }
                Log("✔ 内存大小检测通过");

                // Step 2:内存频率检测
                SetStatus("正在检测内存频率...", Brushes.Yellow);
                double actualSpeed = _reader.GetMemorySpeedMHz();
                Log($"SMBIOS 读取内存频率:{actualSpeed} MHz");
                if (actualSpeed <= 0) { SetStatus("无法从 SMBIOS 读取内存频率", Brushes.Red); return; }
                if (actualSpeed != config.ExpectedMemorySpeedMHz)
                {
                    SetStatus($"内存频率不符!期望 {config.ExpectedMemorySpeedMHz}MHz,实际 {actualSpeed}MHz", Brushes.Red);
                    return;
                }
                Log("✔ 内存频率检测通过");

                // Step 3:DIMM 插槽 Device Locator 检测
                SetStatus("正在检测 DIMM 插槽...", Brushes.Yellow);
                var slots = _reader.GetDimmDeviceLocators();
                if (slots.Count == 0) { SetStatus("未检测到任何内存插槽信息", Brushes.Red); return; }
                bool allValid = true;
                foreach (var kv in slots)
                {
                    if (string.IsNullOrWhiteSpace(kv.Value))
                    {
                        Log($"插槽 {kv.Key}:Device Locator 读取失败");
                        allValid = false;
                    }
                    else
                    {
                        Log($"插槽 {kv.Key}:{kv.Value}");
                    }
                }
                if (!allValid) { SetStatus("部分插槽 Device Locator 无法读取", Brushes.Red); return; }
                Log("✔ 所有 DIMM 插槽检测通过");

                SetStatus($"全部通过!内存 {actualSize}GB @ {actualSpeed}MHz", Brushes.YellowGreen);
            }
            finally
            {
                _reader.Free();
            }
        }

        private void SetStatus(string text, Brush color)
        {
            StatusText = text;
            StatusColor = color;
            Log(text);
        }

        private void Log(string msg)
        {
            _log.AppendLine(msg);
            LogText = _log.ToString();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnChanged([CallerMemberName] string name = "") =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

MainWindow.xaml

xml 复制代码
<Window x:Class="MemoryTestDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="内存规格检测 Demo" Height="420" Width="520"
        Background="#1E1E2E">
    <Grid Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!-- 测试状态 -->
        <TextBlock Grid.Row="0" Text="{Binding StatusText}"
                   Foreground="{Binding StatusColor}"
                   FontSize="18" FontWeight="Bold"
                   TextWrapping="Wrap" Margin="0,0,0,12"/>

        <!-- 提示:JSON 配置路径 -->
        <TextBlock Grid.Row="1"
                   Text="📄 期望规格配置文件:memory_config.json(与 exe 同目录,可手动编辑)"
                   Foreground="#888" FontSize="11" Margin="0,0,0,8"/>

        <!-- 日志输出 -->
        <ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
            <TextBox Text="{Binding LogText, Mode=OneWay}"
                     Background="#12121C" Foreground="#CDD6F4"
                     FontFamily="Consolas" FontSize="12"
                     IsReadOnly="True" TextWrapping="Wrap"
                     BorderThickness="0"/>
        </ScrollViewer>

        <!-- 开始按钮 -->
        <Button Grid.Row="3" Content="开始测试" Margin="0,12,0,0"
                Height="36" FontSize="14"
                Background="#89B4FA" Foreground="#1E1E2E"
                BorderThickness="0"
                Click="StartButton_Click"/>
    </Grid>
</Window>

MainWindow.xaml.cs

csharp 复制代码
using System.Windows;

namespace MemoryTestDemo
{
    public partial class MainWindow : Window
    {
        private readonly MainVM _vm = new MainVM();

        public MainWindow()
        {
            InitializeComponent();
            DataContext = _vm;
        }

        private async void StartButton_Click(object sender, RoutedEventArgs e)
        {
            await _vm.StartTestAsync();
        }
    }
}

运行效果说明

  1. 首次运行 :程序在 exe 同目录自动生成 memory_config.json,默认值为 16GB / 4800MHz

  2. 修改配置 :用记事本打开 memory_config.json,将数值改为你机器的实际规格

  3. 点击"开始测试":程序依次检测大小 → 频率 → 插槽,全部通过显示绿色,任一不符显示红色并说明原因

  4. 典型输出(通过):

    期望内存大小:16 GB
    期望内存频率:4800 MHz

    SMBIOS 读取内存大小:16 GB
    ✔ 内存大小检测通过
    SMBIOS 读取内存频率:4800 MHz
    ✔ 内存频率检测通过
    插槽 1:ChannelA-DIMM0
    ✔ 所有 DIMM 插槽检测通过
    全部通过!内存 16GB @ 4800MHz


六、总结与扩展

本文核心要点

  • SMBIOS 是读取硬件规格的最可靠途径,无需额外安装驱动,通过 GetSystemFirmwareTable P/Invoke 调用
  • Type 17 中 Size 字段的单位由 bit15 决定,0xFFFF 表示未知------解析时必须处理这两种边界情况
  • Device Locator 字段存的是字符串索引而非字符串本身,需要从结构末尾的字符串区域按索引逐个读取
  • 本地 JSON 配置 替代 MES 让 Demo 完全独立可运行,同时也可以作为离线测试场景的解决方案

可以扩展的方向

  • 内存类型识别 :解析 Type 17 的 MemoryType 字段(0x1A = DDR5,0x18 = DDR4),加入类型校验
  • 制造商/SN 读取 :解析 Manufacturer、SerialNumber 字符串字段,用于追溯物料来源
  • WMI 方案对比 :Win32_PhysicalMemory 也能获取类似信息,但 SMBIOS 直读更底层、速度更快
  • 多 SKU 支持:JSON 配置扩展为数组,工具启动时让用户选择当前机型对应的配置项

如有问题欢迎评论区交流。觉得有用的话点个赞和关注下,系列会持续更新 😃

相关推荐
蜗牛互联网2 天前
OLMo-core 3的token gerrymandering提醒:MoE路由要按时间窗验收
java·人工智能·wpf
LateFrames2 天前
WPF 升级选型:用 .NET 8 还是 .NET 10
.net·wpf·.net8·.net10
samble3 天前
从零搭建灌装监控系统(十):生产追溯,条码触发自动记录
c#·wpf·mvvm·modbus·工业控制
SamChan905 天前
PDF页面旋转与CropBox陷阱:译文回填位置总是错位的根因实测
python·ai·pdf·wpf
SamChan906 天前
PDF翻译时页眉页脚总在捣乱?跨页重复文本块的检测与过滤实测
人工智能·python·ai·pdf·wpf
samble13 天前
从零搭建灌装监控系统(四):深色主题与样式系统,ResourceDictionary 统一管理
c#·wpf·mvvm·样式·工业控制
_Jonas13 天前
.net framework开发环境正式环境配置文件的切换
.net·wpf
dalong1015 天前
WPF:Modbus 状态监控
wpf·modbus
He BianGu16 天前
【WPF-VisionMaster】机器视觉通用平台V5.0版本发行说明
opencv·c#·wpf·halcon·机器视觉·visionmaster
fogota19 天前
Emoji与Segoe MDL2 Assets的比较
c#·wpf