fix(ide): PIO 数据就绪只认 DRQ,并在发命令前/失败后清掉残留扇区

现场(serial.log 113-136):winread FAIL abs=8192 res=1,紧接着读根目录扇区 8192 读回 ELF 文件头 ⇒ 目录项全是垃圾 ⇒ Hello/Terminal/Gargantua 三个 spawn 连锁失败,之后又自行恢复(间歇)。两个成因:① waitDataReady 把 DRDY 当作数据就绪(DRDY 空闲时恒为 1),在 BSY 刚清、DRQ 未置的窗口里误判成功,随后从 0x1F0 读到上一次传输的残留;② 一旦某次读提前失败返回、数据没被取走(DRQ 仍置位),ATA 规定设备会忽略命令块寄存器写入 ⇒ LBA 保留旧值 ⇒ 后续每次读都拿到同一块残留缓冲,形成连锁污染。改法:waitDataReady 改为必须 !BSY && DRQ;新增 flushStaleData(),在 readSector/writeSector 发命令前以及失败返回前把残留扇区取走丢弃。未改 includes,无需三重编译。
This commit is contained in:
TermiNexus committed 2026-10-16 20:39:56 +08:00
1 parent 4d0f29d533
commit cfb76b1b94
1 file changed
+35 -4
+35 -4
View File
@@ -37,10 +37,32 @@ def waitDataReady(baseport: uint16) -> static IDError:
status: uint8 = asm.inb(baseport + 7)
if status & IDE_STATUS_ERR: return IDError.IDE_ERR_DEVICE_ERROR
if status & IDE_STATUS_DF: return IDError.IDE_ERR_DEVICE_FAILURE
if not status & IDE_STATUS_BSY and (status & IDE_STATUS_DRQ or status & IDE_STATUS_DRDY):
# PIO 数据就绪的唯一标志是 **DRQ**。DRDY 只表示设备在位/就绪,空闲时一直是 1;
# 拿它当"数据到了"会在 BSY 刚清、DRQ 未置的窗口里误判成功,随后从 0x1F0 读到
# 上一次传输的残留(现场:读目录扇区 8192 读回 ELF 文件头)。
if not status & IDE_STATUS_BSY and status & IDE_STATUS_DRQ:
return IDError.IDE_ERR_NONE
return IDError.IDE_ERR_TIMEOUT
# 残留数据缓冲:flushStaleData 用它把设备里待取的一扇区读掉(PIO 单线程,模块级静态即可)。
_stale_buf: [512]uint8
def flushStaleData():
"""把设备里残留的一扇区数据取走丢弃。
为什么必须有:ATA 规定设备"有数据待取"(DRQ 置位)时会忽略命令块寄存器写入。
若上一次读提前失败返回、数据没被取走,后续 0x1F2..0x1F7 的写全部被忽略 ⇒ LBA 保留
旧值 ⇒ 下一次读拿到上一次的残留缓冲(现场:读根目录扇区 8192 读回 ELF 头,目录项
全是垃圾 ⇒ Hello/Terminal/Gargantua 三个 spawn 连锁失败)。
"""
if not (asm.inb(IDE_baseport_PRIMARY + 7) & IDE_STATUS_DRQ):
return
p: uint16* = uint16*(&_stale_buf[0])
for _k in range(256):
*p = asm.inw(0x1F0)
# p 是 i16*:+1 表示前进 1 个元素 = 2 字节,256 次恰好一扇区
p = p + 1
def init() -> int:
global _ide_initialized
if _ide_initialized: return IDError.IDE_ERR_NONE
@@ -68,7 +90,10 @@ def readSector(lba: uint32, buffer: rawptr) -> int:
if buffer is None: return IDError.IDE_ERR_BUFFER_NULL
err: IDError = waitReady(IDE_baseport_PRIMARY)
if err != IDError.IDE_ERR_NONE:
flushStaleData()
return err
# 命令块寄存器能被接受的前提是设备**没有待取数据**:DRQ 若仍置位,写会被忽略。
flushStaleData()
asm.outb(0x3F6, 0x00)
asm.io_wait()
# 命令块寄存器**连写之间必须插 I/O 延时**:QEMU 的 IDE 是逐条采样寄存器的,缺延时
@@ -88,7 +113,8 @@ def readSector(lba: uint32, buffer: rawptr) -> int:
asm.io_wait()
err = waitDataReady(IDE_baseport_PRIMARY)
if err != IDError.IDE_ERR_NONE:
st: uint8 = asm.inb(0x1F7)
# 失败后必须把设备里可能的残留数据取走,否则后续每一次读都会被"命令寄存器写被忽略"污染。
flushStaleData()
return err
st2: uint8 = asm.inb(0x1F7)
# 读完再核一次 ERR:置位说明这次数据无效,不能当成功返回(否则上层会拿到错内容)
@@ -107,7 +133,10 @@ def readSector(lba: uint32, buffer: rawptr) -> int:
def writeSector(lba: uint32, buffer: const rawptr) -> int:
if buffer is None: return IDError.IDE_ERR_BUFFER_NULL
err: IDError = waitReady(IDE_baseport_PRIMARY)
if err != IDError.IDE_ERR_NONE: return err
if err != IDError.IDE_ERR_NONE:
flushStaleData()
return err
flushStaleData()
asm.outb(0x3F6, 0x00)
asm.io_wait()
# 与 readSector 同理:命令块寄存器连写之间必须插 I/O 延时,否则 QEMU 只采样到部分寄存器
@@ -125,7 +154,9 @@ def writeSector(lba: uint32, buffer: const rawptr) -> int:
asm.outb(0x1F7, 0x30)
asm.io_wait()
err = waitDataReady(IDE_baseport_PRIMARY)
if err != IDError.IDE_ERR_NONE: return err
if err != IDError.IDE_ERR_NONE:
flushStaleData()
return err
buf: uint16* = uint16*(buffer)
for j in range(256):
val: uint16 = *buf