Waiting for any non-zero duration currently waits for at least one timer cycle (10 ms), which is especially expensive during early boot. The current workaround of simply reading the status 14 times seems really suspicious although the osdev wiki documents it, but let's see how well it works on real hardware, it's probably good enough. Try to determine the initial selected drive to save one drive selection.
343 lines
9 KiB
C++
343 lines
9 KiB
C++
/*
|
|
* Copyright (c) 2011-2016, 2022 Jonas 'Sortie' Termansen.
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
* disk/ata/hba.cpp
|
|
* Driver for ATA.
|
|
*/
|
|
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <timespec.h>
|
|
|
|
#include <sortix/clock.h>
|
|
|
|
#include <sortix/kernel/clock.h>
|
|
#include <sortix/kernel/interlock.h>
|
|
#include <sortix/kernel/interrupt.h>
|
|
#include <sortix/kernel/ioctx.h>
|
|
#include <sortix/kernel/ioport.h>
|
|
#include <sortix/kernel/kthread.h>
|
|
#include <sortix/kernel/log.h>
|
|
#include <sortix/kernel/panic.h>
|
|
#include <sortix/kernel/pci.h>
|
|
#include <sortix/kernel/time.h>
|
|
|
|
#include "../node.h"
|
|
|
|
#include "hba.h"
|
|
#include "port.h"
|
|
#include "registers.h"
|
|
|
|
namespace Sortix {
|
|
namespace ATA {
|
|
|
|
static unsigned long AllocateDiskNumber()
|
|
{
|
|
static unsigned long next_disk_number = 0;
|
|
return InterlockedIncrement(&next_disk_number).o;
|
|
}
|
|
|
|
static void sleep_400_nanoseconds(uint16_t port_base)
|
|
{
|
|
// TODO: The clock granularity of 10 ms slows down the early boot.
|
|
#if 0
|
|
struct timespec delay = timespec_make(0, 400);
|
|
Clock* clock = Time::GetClock(CLOCK_BOOTTIME);
|
|
clock->SleepDelay(delay);
|
|
#else
|
|
for ( int i = 0; i < 14; i++ )
|
|
inport8(port_base + REG_STATUS);
|
|
#endif
|
|
}
|
|
|
|
Channel::Channel()
|
|
{
|
|
hw_lock = KTHREAD_MUTEX_INITIALIZER;
|
|
interrupt_registered = false;
|
|
drives[0] = NULL;
|
|
drives[1] = NULL;
|
|
}
|
|
|
|
Channel::~Channel()
|
|
{
|
|
if ( interrupt_registered )
|
|
Interrupt::UnregisterHandler(interrupt_index, &interrupt_registration);
|
|
// TODO: Destroy all the ports?
|
|
}
|
|
|
|
void Channel::LogF(const char* format, ...)
|
|
{
|
|
// TODO: Print this line in an atomic manner.
|
|
const char* cdesc = channel_index == 0 ? "primary" : "secondary";
|
|
Log::PrintF("ata: pci 0x%X: %s channel: ", devaddr, cdesc);
|
|
va_list ap;
|
|
va_start(ap, format);
|
|
Log::PrintFV(format, ap);
|
|
va_end(ap);
|
|
Log::PrintF("\n");
|
|
}
|
|
|
|
void Channel::SelectDrive(unsigned int drive_index) // hw_lock locked
|
|
{
|
|
if ( current_drive == drive_index )
|
|
return;
|
|
|
|
#if 0
|
|
// TODO: Perhaps not do this here. This appears to time out on boot on real
|
|
// hardware where there is no drive there.
|
|
if ( !wait_inport8_clear(port_base + REG_STATUS,
|
|
STATUS_BUSY | STATUS_DATAREADY, false, 10000 /*ms*/) )
|
|
{
|
|
LogF("error: timed out waiting for idle");
|
|
// TODO: This can fail!
|
|
//return errno = EIO, false;
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
uint8_t value = 0xA0 | (drive_index << 4);
|
|
outport8(port_base + REG_DRIVE_SELECT, value);
|
|
//outport8(port_control, value); // TODO: Or is it port_control we use?
|
|
|
|
sleep_400_nanoseconds(port_base);
|
|
|
|
// TODO: Do we need to wait for non-busy now? Can this operation fail?
|
|
|
|
current_drive = drive_index;
|
|
}
|
|
|
|
void Channel::OnInterrupt()
|
|
{
|
|
// Check whether the interrupt came from this channel.
|
|
uint8_t status = inport8(busmaster_base + BUSMASTER_REG_STATUS);
|
|
if ( !(status & BUSMASTER_STATUS_INTERRUPT_PENDING) )
|
|
return;
|
|
|
|
if ( status & BUSMASTER_STATUS_DMA_FAILURE )
|
|
{
|
|
// TODO: What do we do here?
|
|
}
|
|
|
|
// Clear interrupt flag.
|
|
// TODO: This filters away BUSMASTER_STATUS_DMA and
|
|
// BUSMASTER_STATUS_DMA_FAILURE and might be a bug?
|
|
//status &= 0xF8 | BUSMASTER_STATUS_DMA;
|
|
status |= BUSMASTER_STATUS_INTERRUPT_PENDING;
|
|
outport8(busmaster_base + BUSMASTER_REG_STATUS, status);
|
|
|
|
if ( current_drive < 2 && drives[current_drive] )
|
|
drives[current_drive]->OnInterrupt();
|
|
}
|
|
|
|
void Channel__OnInterrupt(struct interrupt_context*, void* context)
|
|
{
|
|
((Channel*) context)->OnInterrupt();
|
|
}
|
|
|
|
static
|
|
void FixDefaultDeviceBars(pcibar_t* basebar, pcibar_t* ctrlbar, uint8_t* irq,
|
|
unsigned int channel_index, uint8_t interface)
|
|
{
|
|
bool compatibility = interface == 0x00 || interface == 0x02;
|
|
|
|
if ( compatibility )
|
|
*irq = channel_index == 0 ? Interrupt::IRQ14 : Interrupt::IRQ15;
|
|
|
|
if ( compatibility ||
|
|
basebar->addr_raw == 0 ||
|
|
(basebar->is_iospace() && !ctrlbar->ioaddr()) )
|
|
{
|
|
uint16_t ioport = channel_index == 0 ? 0x1F0 : 0x170;
|
|
basebar->addr_raw = ioport | PCIBAR_TYPE_IOSPACE;
|
|
basebar->size_raw = 8;
|
|
}
|
|
|
|
if ( compatibility ||
|
|
ctrlbar->addr_raw == 0 ||
|
|
(ctrlbar->is_iospace() && !ctrlbar->ioaddr()) )
|
|
{
|
|
uint16_t ioport = channel_index == 0 ? 0x3F4 : 0x374;
|
|
ctrlbar->addr_raw = ioport | PCIBAR_TYPE_IOSPACE;
|
|
ctrlbar->size_raw = 4; // TODO: This is just a guess.
|
|
}
|
|
}
|
|
|
|
bool Channel::Initialize(Ref<Descriptor> dev, const char* devpath)
|
|
{
|
|
uint8_t prog_if = PCI::Read8(devaddr, PCIFIELD_PROG_IF);
|
|
uint8_t interface = (prog_if >> (channel_index * 2)) & 0x3;
|
|
pcibar_t basebar = PCI::GetBAR(devaddr, 2 * channel_index + 0);
|
|
pcibar_t ctrlbar = PCI::GetBAR(devaddr, 2 * channel_index + 1);
|
|
pcibar_t busmasterbar = PCI::GetBAR(devaddr, 4);
|
|
interrupt_index = PCI::SetupInterruptLine(devaddr);
|
|
FixDefaultDeviceBars(&basebar, &ctrlbar, &interrupt_index, channel_index, interface);
|
|
|
|
if ( !interrupt_index )
|
|
{
|
|
LogF("error: cannot determine interrupt line");
|
|
return errno = EINVAL, false;
|
|
}
|
|
|
|
if ( !basebar.is_iospace() )
|
|
{
|
|
LogF("ignoring: non-iospace base BAR");
|
|
return errno = EINVAL, false;
|
|
}
|
|
|
|
if ( basebar.size() < 8 )
|
|
{
|
|
LogF("ignoring: too small base BAR");
|
|
return errno = EINVAL, false;
|
|
}
|
|
|
|
if ( !ctrlbar.is_iospace() )
|
|
{
|
|
LogF("ignoring: non-iospace control BAR");
|
|
return errno = EINVAL, false;
|
|
}
|
|
|
|
if ( ctrlbar.size() < 4 )
|
|
{
|
|
LogF("ignoring: too small control BAR");
|
|
return errno = EINVAL, false;
|
|
}
|
|
|
|
if ( !busmasterbar.is_iospace() )
|
|
{
|
|
LogF("ignoring: non-iospace bus master BAR");
|
|
return errno = EINVAL, false;
|
|
}
|
|
|
|
if ( busmasterbar.size() < 16 )
|
|
{
|
|
LogF("ignoring: too small bus master BAR");
|
|
return errno = EINVAL, false;
|
|
}
|
|
|
|
port_base = basebar.addr();
|
|
if ( inport8(port_base + REG_STATUS) == 0xFF )
|
|
return errno = ENODEV, false; // Non-existent.
|
|
|
|
// TODO: Ensure this is the correct logic.
|
|
port_control = ctrlbar.addr() + 2;
|
|
|
|
busmaster_base = busmasterbar.addr() + 8 * channel_index;
|
|
|
|
current_drive = (inport8(port_base + REG_DRIVE_SELECT) >> 4) & 1;
|
|
|
|
for ( unsigned int i = 0; i < 2; i++ )
|
|
{
|
|
drives[i] = NULL;
|
|
|
|
ScopedLock lock(&hw_lock);
|
|
|
|
SelectDrive(i);
|
|
|
|
// TODO: May we do this before sending an IDENTITY command?
|
|
uint8_t status = inport8(port_base + REG_STATUS);
|
|
if ( status == 0 )
|
|
continue; // Non-existent.
|
|
|
|
const char* name = i == 0 ? "master" : "slave";
|
|
|
|
drives[i] = new Port(this, i);
|
|
if ( !drives[i] )
|
|
{
|
|
LogF("error: failed to allocate %s drive", name);
|
|
continue;
|
|
}
|
|
|
|
if ( !drives[i]->Initialize() )
|
|
{
|
|
delete drives[i];
|
|
drives[i] = NULL;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
interrupt_registration.handler = Channel__OnInterrupt;
|
|
interrupt_registration.context = this;
|
|
Interrupt::RegisterHandler(interrupt_index, &interrupt_registration);
|
|
interrupt_registered = true;
|
|
|
|
for ( unsigned int i = 0; i < 2; i++ )
|
|
{
|
|
if ( !drives[i] )
|
|
continue;
|
|
if ( !drives[i]->FinishInitialize() )
|
|
{
|
|
// TODO: Gracefully destroy the drive here?
|
|
// TODO: Unsafe with respect to interrupt handler.
|
|
delete drives[i];
|
|
drives[i] = NULL;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
for ( unsigned int i = 0; i < 2; i++ )
|
|
{
|
|
if ( !drives[i] )
|
|
continue;
|
|
unsigned long number = AllocateDiskNumber();
|
|
char name[3 + sizeof(unsigned long) * 3];
|
|
snprintf(name, sizeof(name), "ata%lu", number);
|
|
Ref<PortNode> node(new PortNode(drives[i], 0, 0, 0660, dev->dev, 0));
|
|
if ( !node )
|
|
PanicF("Unable to allocate memory for %s/%s inode", devpath, name);
|
|
ioctx_t ctx; SetupKernelIOCtx(&ctx);
|
|
if ( LinkInodeInDir(&ctx, dev, name, node) != 0 )
|
|
PanicF("Unable to link %s/%s to ATA driver inode", devpath, name);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
HBA::HBA(uint32_t devaddr)
|
|
{
|
|
this->devaddr = devaddr;
|
|
}
|
|
|
|
HBA::~HBA()
|
|
{
|
|
}
|
|
|
|
bool HBA::InitializeChannel(Ref<Descriptor> dev, const char* devpath,
|
|
unsigned int channel_index)
|
|
{
|
|
channels[channel_index].devaddr = devaddr;
|
|
channels[channel_index].hba = this;
|
|
channels[channel_index].channel_index = channel_index;
|
|
return channels[channel_index].Initialize(dev, devpath);
|
|
}
|
|
|
|
bool HBA::Initialize(Ref<Descriptor> dev, const char* devpath)
|
|
{
|
|
uint16_t dev_pci_command_cur = PCI::Read16(devaddr, PCIFIELD_COMMAND);
|
|
uint16_t dev_pci_command_new = dev_pci_command_cur;
|
|
dev_pci_command_new &= ~PCIFIELD_COMMAND_INTERRUPT_DISABLE;
|
|
dev_pci_command_new |= PCIFIELD_COMMAND_IO_SPACE;
|
|
dev_pci_command_new |= PCIFIELD_COMMAND_BUS_MASTER;
|
|
if ( dev_pci_command_cur != dev_pci_command_new )
|
|
PCI::Write16(devaddr, PCIFIELD_COMMAND, dev_pci_command_new);
|
|
|
|
InitializeChannel(dev, devpath, 0);
|
|
InitializeChannel(dev, devpath, 1);
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace ATA
|
|
} // namespace Sortix
|