MS DOS下读取RAM SPD EEPROM失败,求代码问题排查与解决
你的16位DOS应用读取SPD EEPROM得到垃圾数据,核心问题出在SMBus基址获取方式错误、操作流程不符合SMBus协议、状态寄存器检查逻辑错误这几个方面,以下是具体分析和修正方案:
一、代码中的关键问题
1. 硬编码SMBus控制器PCI地址,兼容性极差
你直接使用0x8000FB20作为PCI配置空间地址,这是假设SMBus控制器固定在总线0、设备23(0x17)、功能0的偏移0x20位置,但不同主板的SMBus控制器PCI位置差异极大,硬编码会导致绝大多数主板无法正确获取基址。
另外,PCI基址寄存器(BAR)是32位的,你仅保留低16位的操作虽然在实模式下勉强可行,但没有先验证设备是否存在,直接读取的地址可能无效。
2. SMBus读操作流程不符合协议规范
SPD EEPROM遵循SMBus协议,单个字节读取的标准流程是:
等待总线空闲 → 发送起始条件 → 发送设备写地址(如0xA0)并等待ACK → 发送读取偏移地址并等待ACK → 发送重复起始条件 → 发送设备读地址(如0xA1)并等待ACK → 读取数据字节 → 发送NACK → 发送停止条件
你的代码直接硬写端口值(如outportb(base+0x02, 0x48)),没有处理ACK校验、起始/停止条件,完全依赖固定延时,这种写法仅适用于特定型号控制器的私有命令,不具备通用性,且极易出错。
3. WaitReady函数检查错误的状态位
不同SMBus控制器的状态寄存器定义不同,以常见的Intel ICH系列为例,状态寄存器(偏移0x00)的BUSY位是bit6(0x40),而你检查的是bit0(0x01,通常是中断标志位),导致等待的不是总线空闲状态,操作可能在总线忙时执行,直接产生垃圾数据。
4. 固定延时不合理且不可靠
你用int 15h ah=86h延时20ms,远超过SMBus单字节操作所需的时间(100kHz总线速度下,单字节操作仅需约100μs),且固定延时无法适配不同控制器的操作速度,可能导致操作未完成就读取数据,或浪费不必要的时间。
二、修正方案
1. 动态枚举SMBus控制器基址
遍历PCI总线,找到设备类为0x0C(串行总线控制器)、子类为0x05(SMBus控制器)的设备,读取其BAR0寄存器获取IO基址:
uint16_t FindSMBusBase() { for (int bus = 0; bus < 256; bus++) { for (int dev = 0; dev < 32; dev++) { for (int func = 0; func < 8; func++) { uint32_t addr = 0x80000000 | (bus << 16) | (dev << 11) | (func << 8) | 0x00; uint16_t vendorDevId; __asm { mov eax, addr mov dx, 0xCF8 out dx, eax mov dx, 0xCFC in ax, dx mov [vendorDevId], ax } if (vendorDevId == 0xFFFF) continue; // 设备不存在 uint16_t classSubclass; addr = 0x80000000 | (bus << 16) | (dev << 11) | (func << 8) | 0x08; __asm { mov eax, addr mov dx, 0xCF8 out dx, eax mov dx, 0xCFC in ax, dx mov [classSubclass], ax } BYTE subclass = (classSubclass >> 8) & 0xFF; BYTE class = classSubclass & 0xFF; if (class == 0x0C && subclass == 0x05) { uint16_t bar0; addr = 0x80000000 | (bus << 16) | (dev << 11) | (func << 8) | 0x10; __asm { mov eax, addr mov dx, 0xCF8 out dx, eax mov dx, 0xCFC in ax, dx mov [bar0], ax } return bar0 & 0xFFFE; // 清除IO空间标志位(最低位) } } } } return 0; // 未找到SMBus控制器 }
2. 修正WaitReady函数的状态检查
以Intel ICH系列为例,修改为检查BUSY位:
bool WaitReady(WORD base) { BYTE status; do { __asm { mov dx, base in al, dx mov [status], al } } while ((status & 0x40) != 0); // 等待BUSY位(bit6)清零 return true; }
3. 遵循SMBus协议实现读操作
以下是基于Intel ICH系列控制器的标准单字节读取实现(包含ACK校验):
bool ReadSPD(WORD base, BYTE offset, BYTE devAddr, BYTE *data) { if (!WaitReady(base)) return false; // 发送起始条件+写设备地址 outportb(base + 0x02, 0x48); // START + WRITE outportb(base + 0x03, devAddr); // 设备写地址(如0xA0) WaitReady(base); BYTE status = inportb(base); if ((status & 0x02) == 0) return false; // 未收到ACK,操作失败 // 发送偏移地址 outportb(base + 0x03, offset); WaitReady(base); status = inportb(base); if ((status & 0x02) == 0) return false; // 发送重复起始条件+读设备地址 outportb(base + 0x02, 0x4C); // START + READ outportb(base + 0x03, devAddr | 0x01); // 设备读地址(如0xA1) WaitReady(base); status = inportb(base); if ((status & 0x02) == 0) return false; // 读取数据+发送NACK+停止条件 outportb(base + 0x02, 0x0C); // READ + NACK + STOP WaitReady(base); *data = inportb(base + 0x05); return true; }
4. 修改main函数逻辑
使用动态枚举的基址,增加错误处理:
void main() { uint16_t smbusBase = FindSMBusBase(); if (smbusBase == 0) { print("SMBus controller not found!\n"); return; } BYTE value; for (int i = 0; i < 256; i++) { if (ReadSPD(smbusBase, i, 0xA0, &value)) { print("%02X ", value); if ((i + 1) % 16 == 0) print("\n"); } else { print("Error reading offset %02X\n", i); } } }
三、额外注意事项
- 不同品牌的SMBus控制器(如AMD、VIA)寄存器定义可能不同,需根据对应主板的芯片组手册调整操作逻辑;
- 实模式下需确保编译器支持32位汇编指令(如使用Turbo C++ 3.0及以上版本,并开启386支持);
- SPD EEPROM的设备地址可能为
0xA0或0xA2,若读取失败可尝试切换地址。
内容的提问来源于stack exchange,提问作者photonepoch421

