[FREELDR:HWIDE] Improve support for ATAPI devices (#8560)

- Adjust DRQ timeout to fix some slow ATAPI devices.
- Add a fix for devices that clear BSY before raising the DRQ bit.
- Enable use of 32-bit I/O on Xbox.

CORE-17977 CORE-16216
This commit is contained in:
Dmitry Borisov
2026-01-15 19:50:51 +06:00
committed by GitHub
parent f49eccef01
commit a55ceaf834
3 changed files with 129 additions and 109 deletions

View File

@@ -2,7 +2,7 @@
* PROJECT: FreeLoader
* LICENSE: MIT (https://spdx.org/licenses/MIT)
* PURPOSE: ATA/ATAPI programmed I/O driver.
* COPYRIGHT: Copyright 2019-2025 Dmitry Borisov (di.sean@protonmail.com)
* COPYRIGHT: Copyright 2019-2026 Dmitry Borisov <di.sean@protonmail.com>
*/
/* INCLUDES *******************************************************************/
@@ -38,21 +38,6 @@ static PHW_DEVICE_UNIT AtapUnits[CHANNEL_MAX_CHANNELS * CHANNEL_MAX_DEVICES];
/* PRIVATE FUNCTIONS **********************************************************/
#if defined(ATA_SUPPORT_32_BIT_IO)
static
inline
BOOLEAN
AtapIs32BitIoSupported(
_In_ PHW_DEVICE_UNIT DeviceUnit)
{
#if defined(ATA_ALWAYS_DO_32_BIT_IO)
return TRUE;
#else
return !!(DeviceUnit->P.Flags & ATA_DEVICE_FLAG_IO32);
#endif
}
#endif
static
VOID
AtapSelectDevice(
@@ -69,11 +54,12 @@ AtapSelectDevice(
}
static
BOOLEAN
AtapWaitForNotBusy(
UCHAR
AtapWait(
_In_ PIDE_REGISTERS Registers,
_In_range_(>, 0) ULONG Timeout,
_Out_opt_ PUCHAR Result)
_In_ UCHAR Mask,
_In_ UCHAR Value)
{
UCHAR IdeStatus;
ULONG i;
@@ -83,12 +69,8 @@ AtapWaitForNotBusy(
for (i = 0; i < Timeout; ++i)
{
IdeStatus = ATA_READ(Registers->Status);
if (!(IdeStatus & IDE_STATUS_BUSY))
{
if (Result)
*Result = IdeStatus;
return TRUE;
}
if ((IdeStatus & Mask) == Value)
break;
if (IdeStatus == 0xFF)
break;
@@ -96,34 +78,7 @@ AtapWaitForNotBusy(
StallExecutionProcessor(10);
}
if (Result)
*Result = IdeStatus;
return FALSE;
}
static
BOOLEAN
AtapWaitForIdle(
_In_ PIDE_REGISTERS Registers,
_Out_ PUCHAR Result)
{
UCHAR IdeStatus;
ULONG i;
for (i = 0; i < ATA_TIME_DRQ_CLEAR; ++i)
{
IdeStatus = ATA_READ(Registers->Status);
if (!(IdeStatus & (IDE_STATUS_DRQ | IDE_STATUS_BUSY)))
{
*Result = IdeStatus;
return TRUE;
}
StallExecutionProcessor(2);
}
*Result = IdeStatus;
return FALSE;
return IdeStatus;
}
static
@@ -133,40 +88,14 @@ AtapSendCdb(
_In_ PATA_DEVICE_REQUEST Request)
{
#if defined(ATA_SUPPORT_32_BIT_IO)
if (AtapIs32BitIoSupported(DeviceUnit))
{
ATA_WRITE_BLOCK_32(DeviceUnit->Registers.Data,
Request->Cdb,
DeviceUnit->CdbSize / sizeof(USHORT));
}
else
ATA_WRITE_BLOCK_32(DeviceUnit->Registers.Data,
Request->Cdb,
DeviceUnit->CdbSize / sizeof(USHORT));
#else
ATA_WRITE_BLOCK_16(DeviceUnit->Registers.Data,
Request->Cdb,
DeviceUnit->CdbSize);
#endif
{
ATA_WRITE_BLOCK_16(DeviceUnit->Registers.Data,
Request->Cdb,
DeviceUnit->CdbSize);
}
/*
* In polled mode (interrupts disabled)
* the NEC CDR-260 drive clears BSY before updating the interrupt reason register.
* As a workaround, we will wait for the phase change.
*/
if (DeviceUnit->P.Flags & ATA_DEVICE_IS_NEC_CDR260)
{
ULONG i;
ATA_IO_WAIT();
for (i = 0; i < ATA_TIME_PHASE_CHANGE; ++i)
{
UCHAR InterruptReason = ATA_READ(DeviceUnit->Registers.InterruptReason);
if (InterruptReason != ATAPI_INT_REASON_COD)
break;
StallExecutionProcessor(10);
}
}
}
static
@@ -178,7 +107,7 @@ AtapPioDataIn(
ByteCount = min(ByteCount, DeviceUnit->BytesToTransfer);
#if defined(ATA_SUPPORT_32_BIT_IO)
if (AtapIs32BitIoSupported(DeviceUnit))
if (!(ByteCount & (sizeof(ULONG) - 1)))
{
ATA_READ_BLOCK_32(DeviceUnit->Registers.Data,
(PULONG)DeviceUnit->DataBuffer,
@@ -233,15 +162,37 @@ AtapSoftwareReset(
StallExecutionProcessor(20);
}
static
VOID
AtapDrainDeviceBuffer(
_In_ PIDE_REGISTERS Registers)
{
UCHAR IdeStatus;
ULONG i;
/* Try to clear the DRQ indication */
for (i = 0; i < 0x10000 / sizeof(USHORT); ++i)
{
IdeStatus = ATA_READ(Registers->Status);
if (!(IdeStatus & IDE_STATUS_DRQ))
break;
READ_PORT_USHORT((PUSHORT)(ULONG_PTR)Registers->Data);
}
}
static
BOOLEAN
AtapPerformSoftwareReset(
_In_ PHW_DEVICE_UNIT DeviceUnit)
{
PIDE_REGISTERS Registers = &DeviceUnit->Registers;
UCHAR IdeStatus;
ERR("Reset device at %X:%u\n", Registers->Data, DeviceUnit->DeviceNumber);
AtapDrainDeviceBuffer(Registers);
/* Perform a software reset */
AtapSoftwareReset(Registers);
@@ -253,7 +204,8 @@ AtapPerformSoftwareReset(
}
/* Now wait for busy to clear */
if (!AtapWaitForNotBusy(Registers, ATA_TIME_BUSY_RESET, NULL))
IdeStatus = AtapWait(Registers, ATA_TIME_BUSY_RESET, IDE_STATUS_BUSY, 0);
if (IdeStatus & IDE_STATUS_BUSY)
return FALSE;
return TRUE;
@@ -272,6 +224,23 @@ AtapProcessAtapiRequest(
InterruptReason &= ATAPI_INT_REASON_MASK;
InterruptReason |= IdeStatus & IDE_STATUS_DRQ;
/*
* The NEC CDR-C251 drive is not fully ATAPI-compliant
* and clears BSY before raising the DRQ bit and updating the interrupt reason register.
* As a workaround, we will wait a bit more in the case the valid IR is not quite there yet.
*/
if ((InterruptReason == ATAPI_INT_REASON_COD) || (InterruptReason == ATAPI_INT_REASON_IO))
{
IdeStatus = AtapWait(&DeviceUnit->Registers,
ATA_TIME_DRQ_ASSERT,
IDE_STATUS_DRQ,
IDE_STATUS_DRQ);
InterruptReason = ATA_READ(DeviceUnit->Registers.InterruptReason);
InterruptReason &= ATAPI_INT_REASON_MASK;
InterruptReason |= IdeStatus & IDE_STATUS_DRQ;
}
switch (InterruptReason)
{
case ATAPI_INT_REASON_AWAIT_CDB:
@@ -341,8 +310,20 @@ AtapProcessAtaRequest(
/* Read command */
if (Request->DataBuffer)
{
/*
* The NEC CDR-C251 drive clears BSY before raising the DRQ bit
* while processing the ATAPI identify command.
* Give the device a chance to assert that bit.
*/
if (!(IdeStatus & IDE_STATUS_DRQ))
return ATA_STATUS_RESET;
{
IdeStatus = AtapWait(&DeviceUnit->Registers,
ATA_TIME_DRQ_ASSERT,
IDE_STATUS_DRQ,
IDE_STATUS_DRQ);
if (!(IdeStatus & IDE_STATUS_DRQ))
return ATA_STATUS_RESET;
}
/* Read the next data block */
AtapPioDataIn(DeviceUnit, DeviceUnit->DrqByteCount);
@@ -351,7 +332,11 @@ AtapProcessAtaRequest(
return ATA_STATUS_PENDING;
/* All data has been transferred, wait for DRQ to clear */
if (!AtapWaitForIdle(&DeviceUnit->Registers, &IdeStatus))
IdeStatus = AtapWait(&DeviceUnit->Registers,
ATA_TIME_DRQ_CLEAR,
IDE_STATUS_BUSY | IDE_STATUS_DRQ,
0);
if (IdeStatus & (IDE_STATUS_BUSY | IDE_STATUS_DRQ))
return ATA_STATUS_RESET;
if (IdeStatus & (IDE_STATUS_ERROR | IDE_STATUS_DEVICE_FAULT))
@@ -363,16 +348,49 @@ AtapProcessAtaRequest(
}
static
BOOLEAN
UCHAR
AtapProcessRequest(
_In_ PHW_DEVICE_UNIT DeviceUnit,
_In_ PATA_DEVICE_REQUEST Request,
_In_ UCHAR IdeStatus)
{
if (Request->Flags & REQUEST_FLAG_PACKET_COMMAND)
return AtapProcessAtapiRequest(DeviceUnit, Request, IdeStatus);
else
UCHAR AtaStatus;
if (!(Request->Flags & REQUEST_FLAG_PACKET_COMMAND))
return AtapProcessAtaRequest(DeviceUnit, Request, IdeStatus);
AtaStatus = AtapProcessAtapiRequest(DeviceUnit, Request, IdeStatus);
/*
* In polled mode (interrupts disabled), the NEC CDR-260 drive behaves quite differently
* that other devices. This drive does not raise BSY immediately
* in response to a CDB or buffer write. The status and interrupt reason registers
* remain invalid and unchanged for the time of media access.
* As a workaround, we will wait for the phase change.
*/
if ((DeviceUnit->P.Flags & ATA_DEVICE_IS_NEC_CDR260) && (AtaStatus == ATA_STATUS_PENDING))
{
UCHAR OldInterruptReason, NewInterruptReason;
ULONG i;
OldInterruptReason = ATA_READ(DeviceUnit->Registers.InterruptReason);
/* Set a long timeout on purpose */
for (i = ATA_TIME_BUSY_POLL; i > 0; i--)
{
NewInterruptReason = ATA_READ(DeviceUnit->Registers.InterruptReason);
if (NewInterruptReason != OldInterruptReason)
break;
StallExecutionProcessor(10);
}
if (i == 0)
{
AtaStatus = ATA_STATUS_RESET;
}
}
return AtaStatus;
}
static
@@ -438,14 +456,15 @@ AtapSendCommand(
_In_ PHW_DEVICE_UNIT DeviceUnit,
_In_ PATA_DEVICE_REQUEST Request)
{
UCHAR AtaStatus;
UCHAR IdeStatus, AtaStatus;
DeviceUnit->BytesToTransfer = Request->DataTransferLength;
DeviceUnit->DataBuffer = Request->DataBuffer;
/* Select the device */
AtapSelectDevice(&DeviceUnit->Registers, DeviceUnit->DeviceNumber);
if (!AtapWaitForNotBusy(&DeviceUnit->Registers, ATA_TIME_BUSY_SELECT, NULL))
IdeStatus = AtapWait(&DeviceUnit->Registers, ATA_TIME_BUSY_SELECT, IDE_STATUS_BUSY, 0);
if (IdeStatus & IDE_STATUS_BUSY)
return ATA_STATUS_RETRY;
/* Always disable interrupts, otherwise it may lead to problems in underlying BIOS firmware */
@@ -458,11 +477,10 @@ AtapSendCommand(
while (TRUE)
{
UCHAR IdeStatus;
ATA_IO_WAIT();
if (!AtapWaitForNotBusy(&DeviceUnit->Registers, ATA_TIME_BUSY_POLL, &IdeStatus))
IdeStatus = AtapWait(&DeviceUnit->Registers, ATA_TIME_BUSY_POLL, IDE_STATUS_BUSY, 0);
if (IdeStatus & IDE_STATUS_BUSY)
return ATA_STATUS_RESET;
AtaStatus = AtapProcessRequest(DeviceUnit, Request, IdeStatus);
@@ -555,10 +573,6 @@ AtapIssueCommand(
return FALSE;
}
/* Turn off various things and retry the command */
DeviceUnit->MultiSectorTransfer = 0;
DeviceUnit->P.Flags &= ~ATA_DEVICE_FLAG_IO32;
if (!AtapPerformSoftwareReset(DeviceUnit))
return FALSE;
@@ -738,7 +752,8 @@ AtapIsDevicePresent(
if (ATA_READ(Registers->ByteCountHigh) != 0xAA)
return FALSE;
if (!AtapWaitForNotBusy(Registers, ATA_TIME_BUSY_ENUM, &IdeStatus))
IdeStatus = AtapWait(&DeviceUnit->Registers, ATA_TIME_BUSY_ENUM, IDE_STATUS_BUSY, 0);
if (IdeStatus & (IDE_STATUS_BUSY | IDE_STATUS_DRQ))
{
ERR("Device %X:%u is busy %02x\n", Registers->Data, DeviceUnit->DeviceNumber, IdeStatus);
@@ -1090,7 +1105,7 @@ AtapAtaInitDevice(
else
{
/* Using CHS addressing mode */
TotalSectors = Cylinders * Heads * SectorsPerTrack;
TotalSectors = (ULONG64)Cylinders * Heads * SectorsPerTrack;
}
if (TotalSectors == 0)
@@ -1221,6 +1236,7 @@ AtaReadLogicalSectors(
PHW_DEVICE_UNIT Unit = (PHW_DEVICE_UNIT)DeviceUnit;
ATA_DEVICE_REQUEST Request = { 0 };
ASSERT(Unit);
ASSERT((SectorNumber + SectorCount) <= Unit->P.TotalSectors);
ASSERT(SectorCount != 0);

View File

@@ -2,7 +2,7 @@
* PROJECT: FreeLoader
* LICENSE: MIT (https://spdx.org/licenses/MIT)
* PURPOSE: Private header for ATA/ATAPI programmed I/O driver.
* COPYRIGHT: Copyright 2019-2025 Dmitry Borisov (di.sean@protonmail.com)
* COPYRIGHT: Copyright 2019-2026 Dmitry Borisov <di.sean@protonmail.com>
*/
#pragma once
@@ -28,6 +28,8 @@
#if defined(SARCH_XBOX)
/* It's safe to enable the multiple mode */
#define ATA_ENABLE_MULTIPLE_MODE
/* nVidia PCI IDE controllers have a 32-bit data port */
#define ATA_SUPPORT_32_BIT_IO
#endif
/* Delay of 400ns */
@@ -69,9 +71,12 @@ typedef ULONG_PTR IDE_REG;
#define ATA_TIME_BUSY_ENUM 100 ///< 1 ms
#define ATA_TIME_BUSY_RESET 1000000 ///< 10 s
#define ATA_TIME_RESET_SELECT 200000 ///< 2 s
#define ATA_TIME_DRQ_CLEAR 100 ///< 200 us
#define ATA_TIME_DRQ_CLEAR 1000 ///< 10 ms
#define ATA_TIME_PHASE_CHANGE 100 ///< 1 ms
/* We keep the value as small as possible, since large timeout can break our error handling */
#define ATA_TIME_DRQ_ASSERT 15 ///< 150 us
#define ATA_WRITE(Port, Value) \
WRITE_PORT_UCHAR((PUCHAR)(ULONG_PTR)(Port), (Value))

View File

@@ -2,7 +2,7 @@
* PROJECT: FreeLoader
* LICENSE: MIT (https://spdx.org/licenses/MIT)
* PURPOSE: ATA/ATAPI programmed I/O driver header file.
* COPYRIGHT: Copyright 2019-2025 Dmitry Borisov (di.sean@protonmail.com)
* COPYRIGHT: Copyright 2019-2026 Dmitry Borisov <di.sean@protonmail.com>
*/
#pragma once
@@ -31,7 +31,6 @@ typedef struct _DEVICE_UNIT
#define ATA_DEVICE_LBA 0x00000002
#define ATA_DEVICE_LBA48 0x00000004
#define ATA_DEVICE_IS_NEC_CDR260 0x00000008
#define ATA_DEVICE_FLAG_IO32 0x00000010
} DEVICE_UNIT, *PDEVICE_UNIT;
/* FUNCTIONS ******************************************************************/