/**@file
  ISA Floppy Driver
  1. Support two types diskette drive
     1.44M drive and 2.88M drive (and now only support 1.44M)
  2. Support two diskette drives
  3. Use DMA channel 2 to transfer data
  4. Do not use interrupt
  5. Support diskette change line signal and write protect

  The internal function for the floppy driver
  
Copyright (c) 2006 - 2007, Intel Corporation.<BR>
All rights reserved. This program and the accompanying materials
are licensed and made available under the terms and conditions of the BSD License
which accompanies this distribution.  The full text of the license may be found at
http://opensource.org/licenses/bsd-license.php

THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.

**/

#include "IsaFloppy.h"

/**

  Detect the floppy drive is presented or not
 
  @param  FdcDev FDC_BLK_IO_DEV * : A pointer to the Data Structure FDC_BLK_IO_DEV
  @retval EFI_SUCCESS    Drive is presented
  @retval EFI_NOT_FOUND  Drive is not presented

**/
EFI_STATUS
DiscoverFddDevice (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;

  FdcDev->BlkIo.Media = &FdcDev->BlkMedia;

  //
  // Call FddIndentify subroutine
  //
  Status = FddIdentify (FdcDev);
  if (EFI_ERROR (Status)) {
    return EFI_NOT_FOUND;
  }

  FdcDev->BlkIo.Reset               = FdcReset;
  FdcDev->BlkIo.FlushBlocks         = FddFlushBlocks;
  FdcDev->BlkIo.ReadBlocks          = FddReadBlocks;
  FdcDev->BlkIo.WriteBlocks         = FddWriteBlocks;
  FdcDev->BlkMedia.LogicalPartition = FALSE;
  FdcDev->BlkMedia.WriteCaching     = FALSE;

  return EFI_SUCCESS;
}

/**

  Do recalibrate  and see the drive is presented or not
  Set the media parameters
  
  @param FdcDev FDC_BLK_IO_DEV * : A pointer to the Data Structure FDC_BLK_IO_DEV
  @return the drive is presented or not

**/
EFI_STATUS
FddIdentify (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;

  //
  // Set Floppy Disk Controller's motor on
  //
  Status = MotorOn (FdcDev);
  if (EFI_ERROR (Status)) {
    return EFI_DEVICE_ERROR;
  }

  Status = Recalibrate (FdcDev);

  if (EFI_ERROR (Status)) {
    MotorOff (FdcDev);
    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }
  //
  // Set Media Parameter
  //
  FdcDev->BlkIo.Media->RemovableMedia = TRUE;
  FdcDev->BlkIo.Media->MediaPresent   = TRUE;
  //
  // investigate
  //
  FdcDev->BlkIo.Media->MediaId = 0;

  //
  // Check Media
  //
  Status = DisketChanged (FdcDev);

  if (Status == EFI_NO_MEDIA) {
    FdcDev->BlkIo.Media->MediaPresent = FALSE;
  } else if ((Status != EFI_MEDIA_CHANGED) &&
             (Status != EFI_SUCCESS)) {
    MotorOff (FdcDev);
    return Status;
  }

  //
  // Check Disk Write Protected
  //
  Status = SenseDrvStatus (FdcDev, 0);

  if (Status == EFI_WRITE_PROTECTED) {
    FdcDev->BlkIo.Media->ReadOnly = TRUE;
  } else if (Status == EFI_SUCCESS) {
    FdcDev->BlkIo.Media->ReadOnly = FALSE;
  } else {
    return EFI_DEVICE_ERROR;
  }

  MotorOff (FdcDev);

  //
  // Set Media Default Type
  //
  FdcDev->BlkIo.Media->BlockSize  = DISK_1440K_BYTEPERSECTOR;
  FdcDev->BlkIo.Media->LastBlock  = DISK_1440K_EOT * 2 * (DISK_1440K_MAXTRACKNUM + 1) - 1;

  return EFI_SUCCESS;
}

/**

  Reset the Floppy Logic Drive
  
  @param  FdcDev FDC_BLK_IO_DEV * : A pointer to the Data Structure FDC_BLK_IO_DEV
  
  @retval EFI_SUCCESS:    The Floppy Logic Drive is reset
  @retval EFI_DEVICE_ERROR: The Floppy Logic Drive is not functioning correctly and
                      can not be reset

**/
EFI_STATUS
FddReset (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  UINT8 data;
  UINT8 StatusRegister0;
  UINT8 PresentCylinderNumber;
  UINTN Index;

  //
  // Report reset progress code
  //
  REPORT_STATUS_CODE_WITH_DEVICE_PATH (
    EFI_PROGRESS_CODE,
    EFI_PERIPHERAL_REMOVABLE_MEDIA | EFI_P_PC_RESET,
    FdcDev->DevicePath
    );

  //
  // Reset specified Floppy Logic Drive according to FdcDev -> Disk
  // Set Digital Output Register(DOR) to do reset work
  //   bit0 & bit1 of DOR : Drive Select
  //   bit2 : Reset bit
  //   bit3 : DMA and Int bit
  // Reset : a "0" written to bit2 resets the FDC, this reset will remain
  //         active until
  //         a "1" is written to this bit.
  // Reset step 1:
  //         use bit0 & bit1 to  select the logic drive
  //         write "0" to bit2
  //
  data = 0x0;
  data = (UINT8) (data | (SELECT_DRV & FdcDev->Disk));
  FdcWritePort (FdcDev, FDC_REGISTER_DOR, data);

  //
  // wait some time,at least 120us
  //
  MicroSecondDelay (500);

  //
  // Reset step 2:
  //   write "1" to bit2
  //   write "1" to bit3 : enable DMA
  //
  data |= 0x0C;
  FdcWritePort (FdcDev, FDC_REGISTER_DOR, data);

  //
  // Experience value
  //
  MicroSecondDelay (2000);

  //
  // wait specified floppy logic drive is not busy
  //
  if (EFI_ERROR (FddWaitForBSYClear (FdcDev, 1))) {
    return EFI_DEVICE_ERROR;
  }
  //
  // Set the Transfer Data Rate
  //
  FdcWritePort (FdcDev, FDC_REGISTER_CCR, 0x0);

  //
  // Experience value
  //
  MicroSecondDelay (100);

  //
  // Issue Sense interrupt command for each drive (total 4 drives)
  //
  for (Index = 0; Index < 4; Index++) {
    if (EFI_ERROR (SenseIntStatus (FdcDev, &StatusRegister0, &PresentCylinderNumber))) {
      return EFI_DEVICE_ERROR;
    }
  }
  //
  // issue Specify command
  //
  if (EFI_ERROR (Specify (FdcDev))) {
    return EFI_DEVICE_ERROR;
  }

  return EFI_SUCCESS;
}

/**

  Turn the drive's motor on
  The drive's motor must be on before any command can be executed
  
  @param  FdcDev FDC_BLK_IO_DEV * : A pointer to the Data Structure FDC_BLK_IO_DEV
  
  @retval  EFI_SUCCESS:       Turn the drive's motor on successfully
  @retval  EFI_DEVICE_ERROR:    The drive is busy, so can not turn motor on
  @retval  EFI_INVALID_PARAMETER: Fail to Set timer(Cancel timer)

**/
EFI_STATUS
MotorOn (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;
  UINT8       data;

  //
  // Control of the floppy drive motors is a big pain. If motor is off, you have
  // to turn it on first. But you can not leave the motor on all the time, since
  // that would wear out the disk. On the other hand, if you turn the motor off
  // after each operation, the system performance will be awful. The compromise
  // used in this driver is to leave the motor on for 2 seconds after
  // each operation. If a new operation is started in that interval(2s),
  // the motor need not be turned on again. If no new operation is started,
  // a timer goes off and the motor is turned off
  //
  //
  // Cancel the timer
  //
  Status = gBS->SetTimer (FdcDev->Event, TimerCancel, 0);

  if (EFI_ERROR (Status)) {
    return EFI_INVALID_PARAMETER;
  }
  //
  // Get the motor status
  //
  data = FdcReadPort (FdcDev, FDC_REGISTER_DOR);

  if (((FdcDev->Disk == FDC_DISK0) && ((data & 0x10) == 0x10)) ||
      ((FdcDev->Disk == FDC_DISK1) && ((data & 0x21) == 0x21))
      ) {
    return EFI_SUCCESS;
  }
  //
  // The drive's motor is off, so need turn it on
  // first look at command and drive are busy or not
  //
  if (EFI_ERROR (FddWaitForBSYClear (FdcDev, 1))) {
    return EFI_DEVICE_ERROR;
  }
  //
  // for drive A: 1CH, drive B: 2DH
  //
  data = 0x0C;
  data = (UINT8) (data | (SELECT_DRV & FdcDev->Disk));
  if (FdcDev->Disk == FDC_DISK0) {
    //
    // drive A
    //
    data |= DRVA_MOTOR_ON;
  } else {
    //
    // drive B
    //
    data |= DRVB_MOTOR_ON;
  }

  FdcWritePort (FdcDev, FDC_REGISTER_DOR, data);

  //
  // Experience value
  //
  MicroSecondDelay (4000);

  return EFI_SUCCESS;
}

/**

  Set a Timer and when Timer goes off, turn the motor off
  
  
  @param  FdcDev FDC_BLK_IO_DEV * : A pointer to the Data Structure FDC_BLK_IO_DEV
  
  @retval  EFI_SUCCESS:       Set the Timer successfully
  @retval  EFI_INVALID_PARAMETER: Fail to Set the timer

**/
EFI_STATUS
MotorOff (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  //
  // Set the timer : 2s
  //
  return gBS->SetTimer (FdcDev->Event, TimerRelative, 20000000);
}

/**
  Detect the disk in the drive is changed or not
  
  
  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV
  
  @retval  EFI_SUCCESS:    No disk media change
  @retval  EFI_DEVICE_ERROR: Fail to do the recalibrate or seek operation
  @retval  EFI_NO_MEDIA:   No disk in the drive
  @retval  EFI_MEDIA_CHANGED:  There is a new disk in the drive
**/
EFI_STATUS
DisketChanged (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;
  UINT8       data;

  //
  // Check change line
  //
  data = FdcReadPort (FdcDev, FDC_REGISTER_DIR);

  //
  // Io delay
  //
  MicroSecondDelay (50);

  if ((data & DIR_DCL) == 0x80) {
    //
    // disk change line is active
    //
    if (FdcDev->PresentCylinderNumber != 0) {
      Status = Recalibrate (FdcDev);
    } else {
      Status = Seek (FdcDev, 0x30);
    }

    if (EFI_ERROR (Status)) {
      FdcDev->ControllerState->NeedRecalibrate = TRUE;
      return EFI_DEVICE_ERROR;
      //
      // Fail to do the seek or recalibrate operation
      //
    }

    data = FdcReadPort (FdcDev, FDC_REGISTER_DIR);

    //
    // Io delay
    //
    MicroSecondDelay (50);

    if ((data & DIR_DCL) == 0x80) {
      return EFI_NO_MEDIA;
    }

    return EFI_MEDIA_CHANGED;
  }

  return EFI_SUCCESS;
}

/**
  Do the Specify command, this command sets DMA operation
  and the initial values for each of the three internal
  times: HUT, SRT and HLT
  
  @param FdcDev    Pointer to instance of FDC_BLK_IO_DEV
  
  @retval  EFI_SUCCESS:    Execute the Specify command successfully
  @retval  EFI_DEVICE_ERROR: Fail to execute the command

**/
EFI_STATUS
Specify (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  FDD_SPECIFY_CMD Command;
  UINTN           Index;
  UINT8           *CommandPointer;

  ZeroMem (&Command, sizeof (FDD_SPECIFY_CMD));
  Command.CommandCode = SPECIFY_CMD;
  //
  // set SRT, HUT
  //
  Command.SrtHut = 0xdf;
  //
  // 0xdf;
  //
  // set HLT and DMA
  //
  Command.HltNd   = 0x02;

  CommandPointer  = (UINT8 *) (&Command);
  for (Index = 0; Index < sizeof (FDD_SPECIFY_CMD); Index++) {
    if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }

  return EFI_SUCCESS;
}

/**
  Set the head of floppy drive to track 0
 
  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV
  @retval EFI_SUCCESS:    Execute the Recalibrate operation successfully
  @retval EFI_DEVICE_ERROR: Fail to execute the Recalibrate operation

**/
EFI_STATUS
Recalibrate (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  FDD_COMMAND_PACKET2 Command;
  UINTN               Index;
  UINT8               StatusRegister0;
  UINT8               PresentCylinderNumber;
  UINT8               *CommandPointer;
  UINT8               Count;

  Count = 2;

  while (Count > 0) {
    ZeroMem (&Command, sizeof (FDD_COMMAND_PACKET2));
    Command.CommandCode = RECALIBRATE_CMD;
    //
    // drive select
    //
    if (FdcDev->Disk == FDC_DISK0) {
      Command.DiskHeadSel = 0;
      //
      // 0
      //
    } else {
      Command.DiskHeadSel = 1;
      //
      // 1
      //
    }

    CommandPointer = (UINT8 *) (&Command);
    for (Index = 0; Index < sizeof (FDD_COMMAND_PACKET2); Index++) {
      if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
        return EFI_DEVICE_ERROR;
      }
    }
    //
    // Experience value
    //
    MicroSecondDelay (250000);
    //
    // need modify according to 1.44M or 2.88M
    //
    if (EFI_ERROR (SenseIntStatus (FdcDev, &StatusRegister0, &PresentCylinderNumber))) {
      return EFI_DEVICE_ERROR;
    }

    if ((StatusRegister0 & 0xf0) == 0x20 && PresentCylinderNumber == 0) {
      FdcDev->PresentCylinderNumber             = 0;
      FdcDev->ControllerState->NeedRecalibrate  = FALSE;
      return EFI_SUCCESS;
    } else {
      Count--;
      if (Count == 0) {
        return EFI_DEVICE_ERROR;
      }
    }
  }
  //
  // end while
  //
  return EFI_SUCCESS;
}

/**
  Set the head of floppy drive to the new cylinder
  
  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV
  @param  Lba EFI_LBA     : The logic block address want to seek
  
  @retval  EFI_SUCCESS:    Execute the Seek operation successfully
  @retval  EFI_DEVICE_ERROR: Fail to execute the Seek operation

**/
EFI_STATUS
Seek (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN EFI_LBA         Lba
  )
{
  FDD_SEEK_CMD  Command;
  UINT8         EndOfTrack;
  UINT8         Head;
  UINT8         Cylinder;
  UINT8         StatusRegister0;
  UINT8         *CommandPointer;
  UINT8         PresentCylinderNumber;
  UINTN         Index;
  UINT8         DelayTime;

  if (FdcDev->ControllerState->NeedRecalibrate) {
    if (EFI_ERROR (Recalibrate (FdcDev))) {
      FdcDev->ControllerState->NeedRecalibrate = TRUE;
      return EFI_DEVICE_ERROR;
    }
  }

  EndOfTrack = DISK_1440K_EOT;
  //
  // Calculate cylinder based on Lba and EOT
  //
  Cylinder = (UINT8) ((UINTN) Lba / EndOfTrack / 2);

  //
  // if the destination cylinder is the present cylinder, unnecessary to do the
  // seek operation
  //
  if (FdcDev->PresentCylinderNumber == Cylinder) {
    return EFI_SUCCESS;
  }
  //
  // Calculate the head : 0 or 1
  //
  Head = (UINT8) ((UINTN) Lba / EndOfTrack % 2);

  ZeroMem (&Command, sizeof (FDD_SEEK_CMD));
  Command.CommandCode = SEEK_CMD;
  if (FdcDev->Disk == FDC_DISK0) {
    Command.DiskHeadSel = 0;
    //
    // 0
    //
  } else {
    Command.DiskHeadSel = 1;
    //
    // 1
    //
  }

  Command.DiskHeadSel = (UINT8) (Command.DiskHeadSel | (Head << 2));
  Command.NewCylinder = Cylinder;

  CommandPointer      = (UINT8 *) (&Command);
  for (Index = 0; Index < sizeof (FDD_SEEK_CMD); Index++) {
    if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }
  //
  // Io delay
  //
  MicroSecondDelay (100);

  //
  // Calculate waiting time
  //
  if (FdcDev->PresentCylinderNumber > Cylinder) {
    DelayTime = (UINT8) (FdcDev->PresentCylinderNumber - Cylinder);
  } else {
    DelayTime = (UINT8) (Cylinder - FdcDev->PresentCylinderNumber);
  }

  MicroSecondDelay ((DelayTime + 1) * 4000);

  if (EFI_ERROR (SenseIntStatus (FdcDev, &StatusRegister0, &PresentCylinderNumber))) {
    return EFI_DEVICE_ERROR;
  }

  if ((StatusRegister0 & 0xf0) == 0x20) {
    FdcDev->PresentCylinderNumber = Command.NewCylinder;
    return EFI_SUCCESS;
  } else {
    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }
}

/**
  Do the Sense Interrupt Status command, this command
  resets the interrupt signal
  
  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV
  @param  StatusRegister0 UINT8 *: Be used to save Status Register 0 read from FDC
  @param  PresentCylinderNumber  UINT8 *: Be used to save present cylinder number
                                    read from FDC
  
  @retval  EFI_SUCCESS:    Execute the Sense Interrupt Status command successfully
  @retval  EFI_DEVICE_ERROR: Fail to execute the command

**/
EFI_STATUS
SenseIntStatus (
  IN     FDC_BLK_IO_DEV  *FdcDev,
  IN OUT UINT8           *StatusRegister0,
  IN OUT UINT8           *PresentCylinderNumber
  )
{
  UINT8 command;

  command = SENSE_INT_STATUS_CMD;
  if (EFI_ERROR (DataOutByte (FdcDev, &command))) {
    return EFI_DEVICE_ERROR;
  }

  if (EFI_ERROR (DataInByte (FdcDev, StatusRegister0))) {
    return EFI_DEVICE_ERROR;
  }

  if (EFI_ERROR (DataInByte (FdcDev, PresentCylinderNumber))) {
    return EFI_DEVICE_ERROR;
  }

  return EFI_SUCCESS;
}

/**
  Do the Sense Drive Status command
  
  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV
  @param  Lba EFI_LBA     : Logic block address
  
  @retval  EFI_SUCCESS:    Execute the Sense Drive Status command successfully
  @retval  EFI_DEVICE_ERROR: Fail to execute the command
  @retval  EFI_WRITE_PROTECTED:The disk is write protected

**/
EFI_STATUS
SenseDrvStatus (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN EFI_LBA         Lba
  )
{
  FDD_COMMAND_PACKET2 Command;
  UINT8               Head;
  UINT8               EndOfTrack;
  UINTN               Index;
  UINT8               StatusRegister3;
  UINT8               *CommandPointer;

  //
  // Sense Drive Status command obtains drive status information,
  // it has not execution phase and goes directly to the result phase from the
  // command phase, Status Register 3 contains the drive status information
  //
  ZeroMem (&Command, sizeof (FDD_COMMAND_PACKET2));
  Command.CommandCode = SENSE_DRV_STATUS_CMD;

  if (FdcDev->Disk == FDC_DISK0) {
    Command.DiskHeadSel = 0;
  } else {
    Command.DiskHeadSel = 1;
  }

  EndOfTrack  = DISK_1440K_EOT;
  Head        = (UINT8) ((UINTN) Lba / EndOfTrack % 2);
  Command.DiskHeadSel = (UINT8) (Command.DiskHeadSel | (Head << 2));

  CommandPointer = (UINT8 *) (&Command);
  for (Index = 0; Index < sizeof (FDD_COMMAND_PACKET2); Index++) {
    if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }

  if (EFI_ERROR (DataInByte (FdcDev, &StatusRegister3))) {
    return EFI_DEVICE_ERROR;
  }
  //
  // Io delay
  //
  MicroSecondDelay (50);

  //
  // Check Status Register 3 to get drive status information
  //
  return CheckStatus3 (StatusRegister3);
}

/**
  Update the disk media properties and if necessary
                        reinstall Block I/O interface
 
  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV
  
  @retval  EFI_SUCCESS:    Do the operation successfully
  @retval  EFI_DEVICE_ERROR: Fail to the operation

**/
EFI_STATUS
DetectMedia (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;
  BOOLEAN     bReset;
  BOOLEAN     bReadOnlyLastTime;
  BOOLEAN     bMediaPresentLastTime;

  bReset                = FALSE;
  bReadOnlyLastTime     = FdcDev->BlkIo.Media->ReadOnly;
  bMediaPresentLastTime = FdcDev->BlkIo.Media->MediaPresent;

  //
  // Check disk change
  //
  Status = DisketChanged (FdcDev);

  if (Status == EFI_MEDIA_CHANGED) {
    FdcDev->BlkIo.Media->MediaId++;
    FdcDev->BlkIo.Media->MediaPresent = TRUE;
    bReset = TRUE;
  } else if (Status == EFI_NO_MEDIA) {
    FdcDev->BlkIo.Media->MediaPresent = FALSE;
  } else if (Status != EFI_SUCCESS) {
    MotorOff (FdcDev);
    return Status;
    //
    // EFI_DEVICE_ERROR
    //
  }

  if (FdcDev->BlkIo.Media->MediaPresent) {
    //
    // Check disk write protected
    //
    Status = SenseDrvStatus (FdcDev, 0);
    if (Status == EFI_WRITE_PROTECTED) {
      FdcDev->BlkIo.Media->ReadOnly = TRUE;
    } else {
      FdcDev->BlkIo.Media->ReadOnly = FALSE;
    }
  }

  if (FdcDev->BlkIo.Media->MediaPresent && (bReadOnlyLastTime != FdcDev->BlkIo.Media->ReadOnly)) {
    bReset = TRUE;
  }

  if (bMediaPresentLastTime != FdcDev->BlkIo.Media->MediaPresent) {
    bReset = TRUE;
  }

  if (bReset) {
    Status = gBS->ReinstallProtocolInterface (
                    FdcDev->Handle,
                    &gEfiBlockIoProtocolGuid,
                    &FdcDev->BlkIo,
                    &FdcDev->BlkIo
                    );

    if (EFI_ERROR (Status)) {
      return Status;
    }
  }

  return EFI_SUCCESS;
}

/**
  Set the data rate and so on
 
  @param  FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV

  @retval EFI_SUCCESS success to set the data rate
**/
EFI_STATUS
Setup (
  IN FDC_BLK_IO_DEV  *FdcDev
  )
{
  EFI_STATUS  Status;

  //
  // Set data rate 500kbs
  //
  FdcWritePort (FdcDev, FDC_REGISTER_CCR, 0x0);

  //
  // Io delay
  //
  MicroSecondDelay (50);

  Status = Specify (FdcDev);

  if (EFI_ERROR (Status)) {
    return EFI_DEVICE_ERROR;
  }

  return EFI_SUCCESS;
}

/**
  Read or Write a number of blocks in the same cylinder
 
  @param  FdcDev      A pointer to Data Structure FDC_BLK_IO_DEV
  @param  HostAddress device address 
  @param  Lba         The starting logic block address to read from on the device
  @param  NumberOfBlocks The number of block wanted to be read or write
  @param  Read        Operation type: read or write
  
  @retval EFI_SUCCESS Success operate

**/
EFI_STATUS
ReadWriteDataSector (
  IN  FDC_BLK_IO_DEV  *FdcDev,
  IN  VOID            *HostAddress,
  IN  EFI_LBA         Lba,
  IN  UINTN           NumberOfBlocks,
  IN  BOOLEAN         Read
  )
{
  EFI_STATUS                                    Status;
  FDD_COMMAND_PACKET1                           Command;
  FDD_RESULT_PACKET                             Result;
  UINTN                                         Index;
  UINTN                                         Times;
  UINT8                                         *CommandPointer;

  EFI_PHYSICAL_ADDRESS                          DeviceAddress;
  EFI_ISA_IO_PROTOCOL                           *IsaIo;
  UINTN                                         NumberofBytes;
  VOID                                          *Mapping;
  EFI_ISA_IO_PROTOCOL_OPERATION                 Operation;
  EFI_STATUS                                    Status1;
  UINT8                                         Channel;
  EFI_ISA_ACPI_RESOURCE                         *ResourceItem;
  UINT32                                        Attribute;

  Status = Seek (FdcDev, Lba);
  if (EFI_ERROR (Status)) {
    return EFI_DEVICE_ERROR;
  }
  //
  // Map Dma
  //
  IsaIo         = FdcDev->IsaIo;
  NumberofBytes = NumberOfBlocks * 512;
  if (Read == READ) {
    Operation = EfiIsaIoOperationSlaveWrite;
  } else {
    Operation = EfiIsaIoOperationSlaveRead;
  }

  ResourceItem  = IsaIo->ResourceList->ResourceItem;
  Index         = 0;
  while (ResourceItem[Index].Type != EfiIsaAcpiResourceEndOfList) {
    if (ResourceItem[Index].Type == EfiIsaAcpiResourceDma) {
      break;
    }

    Index++;
  }

  if (ResourceItem[Index].Type == EfiIsaAcpiResourceEndOfList) {
    return EFI_DEVICE_ERROR;
  }

  Channel   = (UINT8) IsaIo->ResourceList->ResourceItem[Index].StartRange;
  Attribute = IsaIo->ResourceList->ResourceItem[Index].Attribute;

  Status1 = IsaIo->Map (
                    IsaIo,
                    Operation,
                    Channel,
                    Attribute,
                    HostAddress,
                    &NumberofBytes,
                    &DeviceAddress,
                    &Mapping
                    );
  if (EFI_ERROR (Status1)) {
    return Status1;
  }

  //
  // Allocate Read or Write command packet
  //
  ZeroMem (&Command, sizeof (FDD_COMMAND_PACKET1));
  if (Read == READ) {
    Command.CommandCode = READ_DATA_CMD | CMD_MT | CMD_MFM | CMD_SK;
  } else {
    Command.CommandCode = WRITE_DATA_CMD | CMD_MT | CMD_MFM;
  }

  FillPara (FdcDev, Lba, &Command);

  //
  // Write command bytes to FDC
  //
  CommandPointer = (UINT8 *) (&Command);
  for (Index = 0; Index < sizeof (FDD_COMMAND_PACKET1); Index++) {
    if (EFI_ERROR (DataOutByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }
  //
  // wait for some time
  //
  Times = (STALL_1_SECOND / 50) + 1;
  do {
    if ((FdcReadPort (FdcDev, FDC_REGISTER_MSR) & 0xc0) == 0xc0) {
      break;
    }

    MicroSecondDelay (50);
    Times = Times - 1;
  } while (Times);

  if (Times == 0) {
    return EFI_TIMEOUT;
  }
  //
  // Read result bytes from FDC
  //
  CommandPointer = (UINT8 *) (&Result);
  for (Index = 0; Index < sizeof (FDD_RESULT_PACKET); Index++) {
    if (EFI_ERROR (DataInByte (FdcDev, CommandPointer++))) {
      return EFI_DEVICE_ERROR;
    }
  }
  //
  // Flush before Unmap
  //
  if (Read == READ) {
    Status1 = IsaIo->Flush (IsaIo);
    if (EFI_ERROR (Status1)) {
      return Status1;
    }
  }
  //
  // Unmap Dma
  //
  Status1 = IsaIo->Unmap (IsaIo, Mapping);
  if (EFI_ERROR (Status1)) {
    return Status1;
  }

  return CheckResult (&Result, FdcDev);
}

/**
  Fill in FDD command's parameter
  
  @param FdcDev   Pointer to instance of FDC_BLK_IO_DEV
  @param Lba      The starting logic block address to read from on the device
  @param Command  FDD command

**/
VOID
FillPara (
  IN  FDC_BLK_IO_DEV       *FdcDev,
  IN  EFI_LBA              Lba,
  IN  FDD_COMMAND_PACKET1  *Command
  )
{
  UINT8 EndOfTrack;

  //
  // Get EndOfTrack from the Para table
  //
  EndOfTrack = DISK_1440K_EOT;

  //
  // Fill the command parameter
  //
  if (FdcDev->Disk == FDC_DISK0) {
    Command->DiskHeadSel = 0;
  } else {
    Command->DiskHeadSel = 1;
  }

  Command->Cylinder = (UINT8) ((UINTN) Lba / EndOfTrack / 2);
  Command->Head     = (UINT8) ((UINTN) Lba / EndOfTrack % 2);
  Command->Sector   = (UINT8) ((UINT8) ((UINTN) Lba % EndOfTrack) + 1);
  Command->DiskHeadSel = (UINT8) (Command->DiskHeadSel | (Command->Head << 2));
  Command->Number     = DISK_1440K_NUMBER;
  Command->EndOfTrack = DISK_1440K_EOT;
  Command->GapLength  = DISK_1440K_GPL;
  Command->DataLength = DISK_1440K_DTL;
}

/**
  Read result byte from Data Register of FDC
  
  @param FdcDev  Pointer to instance of FDC_BLK_IO_DEV
  @param Pointer UINT8 *: Be used to save result byte read from FDC
  
  
  @retval  EFI_SUCCESS:    Read result byte from FDC successfully
  @retval  EFI_DEVICE_ERROR: The FDC is not ready to be read

**/
EFI_STATUS
DataInByte (
  IN     FDC_BLK_IO_DEV  *FdcDev,
  IN OUT UINT8           *Pointer
  )
{
  UINT8 data;

  //
  // wait for 1ms and detect the FDC is ready to be read
  //
  if (EFI_ERROR (FddDRQReady (FdcDev, DATA_IN, 1))) {
    return EFI_DEVICE_ERROR;
    //
    // is not ready
    //
  }

  data = FdcReadPort (FdcDev, FDC_REGISTER_DTR);

  //
  // Io delay
  //
  MicroSecondDelay (50);

  *Pointer = data;
  return EFI_SUCCESS;
}

/**
  Write command byte to Data Register of FDC
  
  @param FdcDev  Pointer to instance of FDC_BLK_IO_DEV
  @param Pointer Be used to save command byte written to FDC
  
  @retval  EFI_SUCCESS:    Write command byte to FDC successfully
  @retval  EFI_DEVICE_ERROR: The FDC is not ready to be written

**/
EFI_STATUS
DataOutByte (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN UINT8           *Pointer
  )
{
  UINT8 data;

  //
  // wait for 1ms and detect the FDC is ready to be written
  //
  if (EFI_ERROR (FddDRQReady (FdcDev, DATA_OUT, 1))) {
    return EFI_DEVICE_ERROR;
    //
    // is not ready
    //
  }

  data = *Pointer;

  FdcWritePort (FdcDev, FDC_REGISTER_DTR, data);

  //
  // Io delay
  //
  MicroSecondDelay (50);

  return EFI_SUCCESS;
}

/**
  Detect the specified floppy logic drive is busy or
  not within a period of time
  
  @param FdcDev           Indicate it is drive A or drive B
  @param TimeoutInSeconds the time period for waiting
  
  @retval EFI_SUCCESS:  The drive and command are not busy
  @retval EFI_TIMEOUT:  The drive or command is still busy after a period time that
                        set by TimeoutInSeconds

**/
EFI_STATUS
FddWaitForBSYClear (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN UINTN           TimeoutInSeconds
  )
{
  UINTN Delay;
  UINT8 StatusRegister;
  UINT8 Mask;

  //
  // How to determine drive and command are busy or not: by the bits of
  // Main Status Register
  // bit0: Drive 0 busy (drive A)
  // bit1: Drive 1 busy (drive B)
  // bit4: Command busy
  //
  //
  // set mask: for drive A set bit0 & bit4; for drive B set bit1 & bit4
  //
  Mask  = (UINT8) ((FdcDev->Disk == FDC_DISK0 ? MSR_DAB : MSR_DBB) | MSR_CB);

  Delay = ((TimeoutInSeconds * STALL_1_MSECOND) / 50) + 1;
  do {
    StatusRegister = FdcReadPort (FdcDev, FDC_REGISTER_MSR);
    if ((StatusRegister & Mask) == 0x00) {
      break;
      //
      // not busy
      //
    }

    MicroSecondDelay (50);
    Delay = Delay - 1;
  } while (Delay);

  if (Delay == 0) {
    return EFI_TIMEOUT;
  }

  return EFI_SUCCESS;
}

/**

  Routine Description:  Determine whether FDC is ready to write or read
  
  @param  FdcDev Pointer to instance of FDC_BLK_IO_DEV
  @param  Dio BOOLEAN:      Indicate the FDC is waiting to write or read
  @param  TimeoutInSeconds UINTN: The time period for waiting
  
  @retval EFI_SUCCESS:  FDC is ready to write or read
  @retval EFI_NOT_READY:  FDC is not ready within the specified time period

**/
EFI_STATUS
FddDRQReady (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN BOOLEAN         Dio,
  IN  UINTN          TimeoutInSeconds
  )
{
  UINTN Delay;
  UINT8 StatusRegister;
  UINT8 DataInOut;

  //
  // Before writing to FDC or reading from FDC, the Host must examine
  // the bit7(RQM) and bit6(DIO) of the Main Status Register.
  // That is to say:
  //  command bytes can not be written to Data Register
  //  unless RQM is 1 and DIO is 0
  //  result bytes can not be read from Data Register
  //  unless RQM is 1 and DIO is 1
  //
  DataInOut = (UINT8) (Dio << 6);
  //
  // in order to compare bit6
  //
  Delay = ((TimeoutInSeconds * STALL_1_MSECOND) / 50) + 1;
  do {
    StatusRegister = FdcReadPort (FdcDev, FDC_REGISTER_MSR);
    if ((StatusRegister & MSR_RQM) == MSR_RQM && (StatusRegister & MSR_DIO) == DataInOut) {
      break;
      //
      // FDC is ready
      //
    }

    MicroSecondDelay (50);
    //
    // Stall for 50 us
    //
    Delay = Delay - 1;
  } while (Delay);

  if (Delay == 0) {
    return EFI_NOT_READY;
    //
    // FDC is not ready within the specified time period
    //
  }

  return EFI_SUCCESS;
}

/**
  Set FDC control structure's attribute according to
  result 

  @param Result  Point to result structure
  @param FdcDev  FDC control structure

  @retval EFI_DEVICE_ERROR - GC_TODO: Add description for return value
  @retval EFI_DEVICE_ERROR - GC_TODO: Add description for return value
  @retval EFI_DEVICE_ERROR - GC_TODO: Add description for return value
  @retval EFI_SUCCESS - GC_TODO: Add description for return value

**/
EFI_STATUS
CheckResult (
  IN     FDD_RESULT_PACKET  *Result,
  IN OUT FDC_BLK_IO_DEV     *FdcDev
  )
{
  //
  // Check Status Register0
  //
  if ((Result->Status0 & STS0_IC) != IC_NT) {
    if ((Result->Status0 & STS0_SE) == 0x20) {
      //
      // seek error
      //
      FdcDev->ControllerState->NeedRecalibrate = TRUE;
    }

    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }
  //
  // Check Status Register1
  //
  if (Result->Status1 & (STS1_EN | STS1_DE | STS1_OR | STS1_ND | STS1_NW | STS1_MA)) {
    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }
  //
  // Check Status Register2
  //
  if (Result->Status2 & (STS2_CM | STS2_DD | STS2_WC | STS2_BC | STS2_MD)) {
    FdcDev->ControllerState->NeedRecalibrate = TRUE;
    return EFI_DEVICE_ERROR;
  }

  return EFI_SUCCESS;
}

/**
  Check the drive status information
  
  @param StatusRegister3  the value of Status Register 3
  
  @retval EFI_SUCCESS           The disk is not write protected
  @retval EFI_WRITE_PROTECTED:  The disk is write protected

**/
EFI_STATUS
CheckStatus3 (
  IN UINT8 StatusRegister3
  )
{
  if (StatusRegister3 & STS3_WP) {
    return EFI_WRITE_PROTECTED;
  }

  return EFI_SUCCESS;
}

/**
  Calculate the number of block in the same cylinder
  according to LBA
  
  @param FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV
  @param LBA EFI_LBA:      The starting logic block address
  @param NumberOfBlocks UINTN: The number of blocks
  
  @return The number of blocks in the same cylinder which the starting
        logic block address is LBA

**/
UINTN
GetTransferBlockCount (
  IN  FDC_BLK_IO_DEV  *FdcDev,
  IN  EFI_LBA         LBA,
  IN  UINTN           NumberOfBlocks
  )
{
  UINT8 EndOfTrack;
  UINT8 Head;
  UINT8 SectorsInTrack;

  //
  // Calculate the number of block in the same cylinder
  //
  EndOfTrack      = DISK_1440K_EOT;
  Head            = (UINT8) ((UINTN) LBA / EndOfTrack % 2);

  SectorsInTrack  = (UINT8) (EndOfTrack * (2 - Head) - (UINT8) ((UINTN) LBA % EndOfTrack));
  if (SectorsInTrack < NumberOfBlocks) {
    return SectorsInTrack;
  } else {
    return NumberOfBlocks;
  }
}

/**
  When the Timer(2s) off, turn the drive's motor off
  
  @param Event EFI_EVENT: Event(the timer) whose notification function is being
                     invoked
  @param Context VOID *:  Pointer to the notification function's context

**/
VOID
EFIAPI
FddTimerProc (
  IN EFI_EVENT  Event,
  IN VOID       *Context
  )
{
  FDC_BLK_IO_DEV  *FdcDev;
  UINT8           data;

  FdcDev = (FDC_BLK_IO_DEV *) Context;

  //
  // Get the motor status
  //
  data = FdcReadPort (FdcDev, FDC_REGISTER_DOR);

  if (((FdcDev->Disk == FDC_DISK0) && ((data & 0x10) != 0x10)) ||
      ((FdcDev->Disk == FDC_DISK1) && ((data & 0x21) != 0x21))
      ) {
    return ;
  }
  //
  // the motor is on, so need motor off
  //
  data = 0x0C;
  data = (UINT8) (data | (SELECT_DRV & FdcDev->Disk));
  FdcWritePort (FdcDev, FDC_REGISTER_DOR, data);
  MicroSecondDelay (500);
}

/**
  Read I/O port for FDC
 
  @param FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV
  @param Offset The offset address of port

**/
UINT8
FdcReadPort (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN UINT32          Offset
  )
{
  UINT8       Data;

  //
  // Call IsaIo
  //
  FdcDev->IsaIo->Io.Read (
                      FdcDev->IsaIo,
                      EfiIsaIoWidthUint8,
                      FdcDev->BaseAddress + Offset,
                      1,
                      &Data
                      );

  return Data;
}

/**
  Write I/O port for FDC
 
  @param FdcDev FDC_BLK_IO_DEV *: A pointer to Data Structure FDC_BLK_IO_DEV
  @param Offset The offset address of port
  @param Data   Value written to port
  
**/
VOID
FdcWritePort (
  IN FDC_BLK_IO_DEV  *FdcDev,
  IN UINT32          Offset,
  IN UINT8           Data
  )
{

  //
  // Call IsaIo
  //
  FdcDev->IsaIo->Io.Write (
                      FdcDev->IsaIo,
                      EfiIsaIoWidthUint8,
                      FdcDev->BaseAddress + Offset,
                      1,
                      &Data
                      );
}

