From cfb76b1b946bf3d97100a999a2c5bbfb654c8806 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 16 Oct 2026 20:39:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(ide):=20PIO=20=E6=95=B0=E6=8D=AE=E5=B0=B1?= =?UTF-8?q?=E7=BB=AA=E5=8F=AA=E8=AE=A4=20DRQ=EF=BC=8C=E5=B9=B6=E5=9C=A8?= =?UTF-8?q?=E5=8F=91=E5=91=BD=E4=BB=A4=E5=89=8D/=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E5=90=8E=E6=B8=85=E6=8E=89=E6=AE=8B=E7=95=99=E6=89=87=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现场(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,无需三重编译。 --- VKernel/Kernel/drivers/storage/ide/ide.vp | 39 ++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/VKernel/Kernel/drivers/storage/ide/ide.vp b/VKernel/Kernel/drivers/storage/ide/ide.vp index dea9a1a..86e7263 100644 --- a/VKernel/Kernel/drivers/storage/ide/ide.vp +++ b/VKernel/Kernel/drivers/storage/ide/ide.vp @@ -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