最近需要读取内存条的 SPD 信息,于是研究了一下,在 CloverBootloader 项目中【参考1】找到了一份代码。经过修改后分析出来一个独立的代码, 实体机上测试如下:
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/DebugLib.h>
#include <Library/ShellCEntryLib.h>
#include <Protocol/PciIo.h>
#include <IndustryStandard/Pci22.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/IoLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include "memvendors.h"
#include "spd.h"
// Intel SMB reg offsets
#define SMBHSTSTS 0
#define SMBHSTCNT 2
#define SMBHSTCMD 3
#define SMBHSTADD 4
#define SMBHSTDAT 5
#define SMBHSTDAT1 6
#define SBMBLKDAT 7
// MCP and nForce SMB reg offsets
#define SMBHPRTCL_NV 0 /* protocol, PEC */
#define SMBHSTSTS_NV 1 /* status */
#define SMBHSTADD_NV 2 /* address */
#define SMBHSTCMD_NV 3 /* command */
#define SMBHSTDAT_NV 4 /* 32 data registers */
//
// XMP memory profile
#define SPD_XMP_SIG1 176
#define SPD_XMP_SIG1_VALUE 0x0C
#define SPD_XMP_SIG2 177
#define SPD_XMP_SIG2_VALUE 0x4A
#define SPD_XMP_PROFILES 178
#define SPD_XMP_VERSION 179
#define SPD_XMP_PROF1_DIVISOR 180
#define SPD_XMP_PROF1_DIVIDEND 181
#define SPD_XMP_PROF2_DIVISOR 182
#define SPD_XMP_PROF2_DIVIDEND 183
#define SPD_XMP_PROF1_RATIO 186
#define SPD_XMP_PROF2_RATIO 221
#define SPD_XMP20_SIG1 0x180
#define SPD_XMP20_SIG2 0x181
#define SPD_XMP20_PROFILES 0x182
#define SPD_XMP20_VERSION 0x183
/* 0x189 */
#define SPD_XMP20_PROF1_MINCYCLE 0x18C
#define SPD_XMP20_PROF1_FINEADJUST 0x1AF
/* 0x1B8 */
#define SPD_XMP20_PROF2_MINCYCLE 0x1BB
#define SPD_XMP20_PROF2_FINEADJUST 0x1DE
/* 0x1E7 */
BOOLEAN smbIntel;
UINT8 smbPage;
/** Read one byte from i2c, used for reading SPD */
UINT8 smb_read_byte(UINT32 base, UINT8 adr, UINT16 cmd)
{
// INTN l1, h1, l2, h2;
UINT64 t;
UINT8 page;
UINT8 c;
// UINT8 p;
if (smbIntel)
{
IoWrite8(base + SMBHSTSTS, 0x1f); // reset SMBus Controller (set busy)
IoWrite8(base + SMBHSTDAT, 0xff);
t=0;
while ( IoRead8(base + SMBHSTSTS) & 0x01) // wait until host is not busy
{
if (t > 5)
{
gBS->Stall(1000000UL);
Print(L"host is busy for too long for byte %2hhX:%d!\n", adr, cmd);
return 0xFF; // break
}
t++;
}
page = (cmd >> 8) & 1;
if (page != smbPage)
{
// p = 0xFF;
IoWrite8(base + SMBHSTCMD, 0x00);
IoWrite8(base + SMBHSTADD, 0x6C + (page << 1)); // Set SPD Page Address
#if 0
IoWrite8(base + SMBHSTCNT, 0x48); // Start + Byte Data Write
// Don't use "Byte Data Write" because status goes from 0x41 (Busy) -> 0x44 (Error)
#else
IoWrite8(base + SMBHSTCNT, 0x40); // Start + Quick Write
// status goes from 0x41 (Busy) -> 0x42 (Completed)
#endif
smbPage = page;
t=0;
while (!( (c=IoRead8(base + SMBHSTSTS)) & 0x02)) // wait until command finished
{
gBS->Stall(1000000UL);
if (c & 4)
{
Print(L"spd page change error for byte %2hhX:%d!\n", adr, cmd);
break;
}
if (t > 5)
{
Print(L"spd page change taking too long for byte %2hhX:%d!\n", adr, cmd);
break; // break after 5ms
}
t++;
}
return smb_read_byte(base, adr, cmd);
}
// p = 0xFF;
IoWrite8(base + SMBHSTCMD, (UINT8)(cmd & 0xFF)); // SMBus uses 8 bit commands
IoWrite8(base + SMBHSTADD, (adr << 1) | 0x01 ); // read from spd
IoWrite8(base + SMBHSTCNT, 0x48 ); // Start + Byte Data Read
// status goes from 0x41 (Busy) -> 0x42 (Completed) or 0x44 (Error)
t=0;
while (!( (c=IoRead8(base + SMBHSTSTS)) & 0x02)) // wait until command finished
{
gBS->Stall(1000000UL);
if (c & 4)
{
// This alway happens when trying to read the memory type (cmd 2) of an empty slot
// Print(L"spd byte read error for byte %2hhX:%d!\n", adr, cmd);
break;
}
if (t > 5)
{
// if (cmd != 2)
Print(L"spd byte read taking too long for byte %2hhX:%d!\n", adr, cmd);
break; // break after 5ms
}
t++;
}
return IoRead8(base + SMBHSTDAT);
}
else
{
}
}
/* SPD i2c read optimization: prefetch only what we need, read non prefetcheable bytes on the fly */
#define READ_SPD(spd, base, slot, x) spd[x] = smb_read_byte(base, 0x50 + slot, x)
/** Read from smbus the SPD content and interpret it for detecting memory attributes */
STATIC void read_smb(EFI_PCI_IO_PROTOCOL *PciIo, UINT16 vid, UINT16 did)
{
// EFI_STATUS Status;
UINT8 i;// spd_size, spd_type;
UINT8 j;
UINT32 base, mmio, hostc;
UINT16 Command;
//RAM_SLOT_INFO* slot;
//XBool fullBanks;
UINT8* spdbuf;
// UINT16 vid, did;
UINT8 TotalSlotsCount;
smbPage = 0; // valid pages are 0 and 1; assume the first page (page 0) is already selected
// vid = gPci->Hdr.VendorId;
// did = gPci->Hdr.DeviceId;
/*Status = */
PciIo->Pci.Read (
PciIo,
EfiPciIoWidthUint16,
PCI_COMMAND_OFFSET,
1,
&Command
);
Command |= 1;
/*Status = */
PciIo->Pci.Write (
PciIo,
EfiPciIoWidthUint16,
PCI_COMMAND_OFFSET,
1,
&Command
);
Print(L"SMBus CmdReg: 0x%X\n", Command);
/*Status = */
PciIo->Pci.Read (
PciIo,
EfiPciIoWidthUint32,
0x10,
1,
&mmio
);
if (vid == 0x8086)
{
/*Status = */PciIo->Pci.Read (
PciIo,
EfiPciIoWidthUint32,
0x20,
1,
&base
);
base &= 0xFFFE;
smbIntel = TRUE;
}
else
{
/*Status = */PciIo->Pci.Read (
PciIo,
EfiPciIoWidthUint32,
0x24, // iobase offset 0x24 on MCP
1,
&base
);
base &= 0xFFFC;
smbIntel = FALSE;
}
/*Status = */PciIo->Pci.Read (
PciIo,
EfiPciIoWidthUint32,
0x40,
1,
&hostc
);
Print(L"Scanning SMBus [%04X:%04X], mmio: 0x%X, ioport: 0x%X, hostc: 0x%X\n",
vid, did, mmio, base, hostc);
// needed at least for laptops
//fullBanks = (gDMI->MemoryModules == gDMI->CntMemorySlots);
spdbuf = AllocateZeroPool(MAX_SPD_SIZE);
TotalSlotsCount = 8; //MAX_RAM_SLOTS; -- spd can read only 8 slots
Print(L"Slots to scan [%d]...\n", TotalSlotsCount);
for (i = 0; i < TotalSlotsCount; i++)
{
//<==
ZeroMem(spdbuf, MAX_SPD_SIZE);
READ_SPD(spdbuf, base, i, SPD_MEMORY_TYPE);
if (spdbuf[SPD_MEMORY_TYPE] == 0xFF)
{
//Print(L"SPD[%d]: Empty\n", i);
continue;
}
else if (spdbuf[SPD_MEMORY_TYPE] == 0)
{
// First 0x40 bytes of DDR4 spd second page is 0. Maybe we need to change page, so do that and retry.
Print(L"SPD[%d]: Got invalid type %d @0x%X. Will set page and retry.\n", i, spdbuf[SPD_MEMORY_TYPE], 0x50 + i);
smbPage = 0xFF; // force page to be set
READ_SPD(spdbuf, base, i, SPD_MEMORY_TYPE);
}
// Copy spd data into buffer
Print(L"SPD[%d]: Type %d @0x%X\n", i, spdbuf[SPD_MEMORY_TYPE], 0x50 + i);
for (j=0;j<16;j++) {
READ_SPD(spdbuf, base, i, j);
Print(L"%X ",spdbuf[j]);
}
Print(L"\n");
} // for
if (smbPage != 0)
{
READ_SPD(spdbuf, base, 0, 0); // force first page when we're done
}
}
void ScanSPD()
{
EFI_STATUS Status;
EFI_HANDLE *HandleBuffer = NULL;
EFI_PCI_IO_PROTOCOL *PciIo = NULL;
UINTN HandleCount;
UINTN Index;
PCI_TYPE00 gPci;
// Scan PCI handles
Status = gBS->LocateHandleBuffer (
ByProtocol,
&gEfiPciIoProtocolGuid,
NULL,
&HandleCount,
&HandleBuffer
);
if (!EFI_ERROR(Status))
{
for (Index = 0; Index < HandleCount; ++Index)
{
Status = gBS->HandleProtocol(HandleBuffer[Index], &gEfiPciIoProtocolGuid, (void **)&PciIo);
if (!EFI_ERROR(Status))
{
// Read PCI BUS
//PciIo->GetLocation (PciIo, &Segment, &Bus, &Device, &Function);
Status = PciIo->Pci.Read (
PciIo,
EfiPciIoWidthUint32,
0,
sizeof (gPci) / sizeof (UINT32),
&gPci
);
//SmBus controller has class = 0x0c0500
if ((gPci.Hdr.ClassCode[2] == 0x0c) && (gPci.Hdr.ClassCode[1] == 5)
&& (gPci.Hdr.ClassCode[0] == 0) && (gPci.Hdr.VendorId == 0x8086 || gPci.Hdr.VendorId == 0x10DE))
{
Print(L"SMBus device : %04hX %04hX class=%02X%02X%02X status=%r\n",
gPci.Hdr.VendorId,
gPci.Hdr.DeviceId,
gPci.Hdr.ClassCode[2],
gPci.Hdr.ClassCode[1],
gPci.Hdr.ClassCode[0],
Status
);
read_smb(PciIo, gPci.Hdr.VendorId, gPci.Hdr.DeviceId);
}
}
}
}
}
/**
UEFI application entry point which has an interface similar to a
standard C main function.
The ShellCEntryLib library instance wrappers the actual UEFI application
entry point and calls this ShellAppMain function.
@param ImageHandle The image handle of the UEFI Application.
@param SystemTable A pointer to the EFI System Table.
@retval 0 The application exited normally.
@retval Other An error occurred.
**/
INTN
EFIAPI
ShellAppMain (
IN UINTN Argc,
IN CHAR16 **Argv
)
{
ScanSPD();
return EFI_SUCCESS;
}
完整代码和EFI 下载:
参考:
1. https://github.com/CloverHackyColor/CloverBootloader/releases
能否支持 DDR5 SPD ?
应该可以,SMBUS部分 每次都没有什么改动
请问这个有什么spec可以参考吗?
https://www.jedec.org/
这个网站有官网的 SPD Spec
请问有没有对于on board memory读取SPD的方法呢?
目前没有的。
請問 SmbusHcProtocol->Execute 的 SlaveAddress 有辦法帶入 0xA0 嗎?
看 source code 他這個變數的長度會被限制在 7 bits, 因此會被自動轉成 0x20, 會回傳 EFI_DEVICE_ERROR.
I2C 设备7Bit地址如果是 Addr, 那么读的时候需要使用 Addr<<1+1, 写的地址是 Addr<<1+0. 不知道你问的是不是这个?
大大晚上好,謝謝您的回覆,已解決位置的問題了。
另外想請問一下… 該如何進行 page 的切換?
本人用的是 DDR5 的 memory,
在 RU 底下直接選擇 SMBUS 讀取 SPD 後不知道該如何切換 Page, 有上網找了些資料但仍看不太懂,請問您知道該如何操作嗎?
不好意思,这个没研究过
请问I3C怎么读取SPD?
不好意思,还没有接触过。