blob: f00b392cb501cec93071cdc6308ef6ca36a1a1d7 [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "controller.h"
#include <fuchsia/hardware/pci/c/banjo.h>
#include <inttypes.h>
#include <lib/ddk/debug.h>
#include <lib/ddk/device.h>
#include <lib/ddk/driver.h>
#include <lib/ddk/io-buffer.h>
#include <lib/ddk/phys-iter.h>
#include <lib/device-protocol/pci.h>
#include <lib/zx/clock.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <threads.h>
#include <unistd.h>
#include <zircon/assert.h>
#include <zircon/listnode.h>
#include <zircon/syscalls.h>
#include <zircon/types.h>
#include <fbl/alloc_checker.h>
#include <fbl/auto_lock.h>
#include "pci-bus.h"
#include "sata.h"
#include "src/devices/block/drivers/ahci/ahci_bind.h"
namespace ahci {
//clang-format on
// TODO(sron): Check return values from bus_->RegRead() and RegWrite().
// Handle properly for buses that may by unplugged at runtime.
uint32_t Controller::RegRead(size_t offset) {
uint32_t val = 0;
bus_->RegRead(offset, &val);
return val;
}
zx_status_t Controller::RegWrite(size_t offset, uint32_t val) {
return bus_->RegWrite(offset, val);
}
void Controller::AhciEnable() {
uint32_t ghc = RegRead(kHbaGlobalHostControl);
if (ghc & AHCI_GHC_AE)
return;
for (int i = 0; i < 5; i++) {
ghc |= AHCI_GHC_AE;
RegWrite(kHbaGlobalHostControl, ghc);
ghc = RegRead(kHbaGlobalHostControl);
if (ghc & AHCI_GHC_AE)
return;
usleep(10 * 1000);
}
}
zx_status_t Controller::HbaReset() {
// AHCI 1.3: Software may perform an HBA reset prior to initializing the controller
uint32_t ghc = RegRead(kHbaGlobalHostControl);
ghc |= AHCI_GHC_AE;
RegWrite(kHbaGlobalHostControl, ghc);
ghc |= AHCI_GHC_HR;
RegWrite(kHbaGlobalHostControl, ghc);
// reset should complete within 1 second
zx_status_t status = bus_->WaitForClear(kHbaGlobalHostControl, AHCI_GHC_HR, zx::sec(1));
if (status) {
zxlogf(ERROR, "ahci: hba reset timed out");
}
return status;
}
zx_status_t Controller::SetDevInfo(uint32_t portnr, sata_devinfo_t* devinfo) {
if (portnr >= AHCI_MAX_PORTS) {
return ZX_ERR_OUT_OF_RANGE;
}
ports_[portnr].SetDevInfo(devinfo);
return ZX_OK;
}
void Controller::Queue(uint32_t portnr, sata_txn_t* txn) {
ZX_DEBUG_ASSERT(portnr < AHCI_MAX_PORTS);
Port* port = &ports_[portnr];
zx_status_t status = port->Queue(txn);
if (status == ZX_OK) {
zxlogf(TRACE, "ahci.%u: queue txn %p offset_dev 0x%" PRIx64 " length 0x%x", port->num(), txn,
txn->bop.rw.offset_dev, txn->bop.rw.length);
// hit the worker thread
sync_completion_signal(&worker_completion_);
} else {
zxlogf(INFO, "ahci.%u: failed to queue txn %p: %d", port->num(), txn, status);
// TODO: close transaction.
}
}
Controller::~Controller() {}
void Controller::Release(void* ctx) {
Controller* controller = static_cast<Controller*>(ctx);
controller->Shutdown();
delete controller;
}
bool Controller::ShouldExit() {
fbl::AutoLock lock(&lock_);
return threads_should_exit_;
}
// worker thread
int Controller::WorkerLoop() {
Port* port;
for (;;) {
// iterate all the ports and run or complete commands
bool port_active = false;
for (uint32_t i = 0; i < AHCI_MAX_PORTS; i++) {
port = &ports_[i];
// Complete commands first.
bool txns_in_progress = port->Complete();
// Process queued txns.
bool txns_added = port->ProcessQueued();
port_active = txns_in_progress || txns_added;
}
// Exit only when there are no more transactions in flight.
if ((!port_active) && ShouldExit()) {
return 0;
}
// Wait here until more commands are queued, or a port becomes idle.
sync_completion_wait(&worker_completion_, ZX_TIME_INFINITE);
sync_completion_reset(&worker_completion_);
}
}
// irq handler:
int Controller::IrqLoop() {
for (;;) {
zx_status_t status = bus_->InterruptWait();
if (status != ZX_OK) {
if (!ShouldExit()) {
zxlogf(ERROR, "ahci: error %d waiting for interrupt", status);
}
return 0;
}
// mask hba interrupts while interrupts are being handled
uint32_t ghc = RegRead(kHbaGlobalHostControl);
RegWrite(kHbaGlobalHostControl, ghc & ~AHCI_GHC_IE);
// handle interrupt for each port
uint32_t is = RegRead(kHbaInterruptStatus);
RegWrite(kHbaInterruptStatus, is);
for (uint32_t i = 0; is && i < AHCI_MAX_PORTS; i++) {
if (is & 0x1) {
bool txn_handled = ports_[i].HandleIrq();
if (txn_handled) {
// hit the worker thread to complete commands
sync_completion_signal(&worker_completion_);
}
}
is >>= 1;
}
// unmask hba interrupts
ghc = RegRead(kHbaGlobalHostControl);
RegWrite(kHbaGlobalHostControl, ghc | AHCI_GHC_IE);
}
}
// implement device protocol:
zx_protocol_device_t ahci_device_proto = []() {
zx_protocol_device_t device = {};
device.version = DEVICE_OPS_VERSION;
device.release = Controller::Release;
return device;
}();
int Controller::InitScan() {
// reset
HbaReset();
// enable ahci mode
AhciEnable();
cap_ = RegRead(kHbaCapabilities);
// count number of ports
uint32_t port_map = RegRead(kHbaPortsImplemented);
// initialize ports
zx_status_t status;
for (uint32_t i = 0; i < AHCI_MAX_PORTS; i++) {
if (!(port_map & (1u << i)))
continue; // port not implemented
status = ports_[i].Configure(i, bus_.get(), kHbaPorts, cap_);
if (status != ZX_OK) {
return status;
}
}
// clear hba interrupts
RegWrite(kHbaInterruptStatus, RegRead(kHbaInterruptStatus));
// enable hba interrupts
uint32_t ghc = RegRead(kHbaGlobalHostControl);
ghc |= AHCI_GHC_IE;
RegWrite(kHbaGlobalHostControl, ghc);
// this part of port init happens after enabling interrupts in ghc
for (uint32_t i = 0; i < AHCI_MAX_PORTS; i++) {
Port* port = &ports_[i];
if (!(port->is_implemented()))
continue;
// enable port
port->Enable();
// enable interrupts
port->RegWrite(kPortInterruptEnable, AHCI_PORT_INT_MASK);
// reset port
port->Reset();
// FIXME proper layering?
if (port->RegRead(kPortSataStatus) & AHCI_PORT_SSTS_DET_PRESENT) {
port->set_present(true);
if (port->RegRead(kPortSignature) == AHCI_PORT_SIG_SATA) {
sata_bind(this, zxdev_, port->num());
}
}
}
return ZX_OK;
}
zx_status_t Controller::Create(zx_device_t* parent, std::unique_ptr<Controller>* con_out) {
fbl::AllocChecker ac;
std::unique_ptr<Bus> bus(new (&ac) PciBus());
if (!ac.check()) {
zxlogf(ERROR, "ahci: out of memory");
return ZX_ERR_NO_MEMORY;
}
return CreateWithBus(parent, std::move(bus), con_out);
}
zx_status_t Controller::CreateWithBus(zx_device_t* parent, std::unique_ptr<Bus> bus,
std::unique_ptr<Controller>* con_out) {
fbl::AllocChecker ac;
std::unique_ptr<Controller> controller(new (&ac) Controller());
if (!ac.check()) {
zxlogf(ERROR, "ahci: out of memory");
return ZX_ERR_NO_MEMORY;
}
zx_status_t status = bus->Configure(parent);
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: failed to configure host bus");
return status;
}
controller->bus_ = std::move(bus);
*con_out = std::move(controller);
return ZX_OK;
}
zx_status_t Controller::LaunchThreads() {
zx_status_t status = irq_thread_.CreateWithName(IrqThread, this, "ahci-irq");
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: error %d creating irq thread", status);
return status;
}
status = worker_thread_.CreateWithName(WorkerThread, this, "ahci-worker");
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: error %d creating worker thread", status);
return status;
}
return ZX_OK;
}
void Controller::Shutdown() {
{
fbl::AutoLock lock(&lock_);
threads_should_exit_ = true;
}
// Signal the worker thread.
sync_completion_signal(&worker_completion_);
worker_thread_.Join();
// Signal the interrupt thread to exit.
bus_->InterruptCancel();
irq_thread_.Join();
}
// implement driver object:
zx_status_t ahci_bind(void* ctx, zx_device_t* parent) {
std::unique_ptr<Controller> controller;
zx_status_t status = Controller::Create(parent, &controller);
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: failed to create ahci controller (%d)", status);
return status;
}
if ((status = controller->LaunchThreads()) != ZX_OK) {
zxlogf(ERROR, "ahci: failed to start controller threads (%d)", status);
return status;
}
// add the device for the controller
device_add_args_t args = {};
args.version = DEVICE_ADD_ARGS_VERSION;
args.name = "ahci";
args.ctx = controller.get();
args.ops = &ahci_device_proto;
args.flags = DEVICE_ADD_NON_BINDABLE;
status = device_add(parent, &args, controller->zxdev_ptr());
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: error %d in device_add", status);
controller->Shutdown();
return status;
}
// initialize controller and detect devices
thrd_t t;
int ret = thrd_create_with_name(&t, Controller::InitThread, controller.get(), "ahci-init");
if (ret != thrd_success) {
zxlogf(ERROR, "ahci: error %d in init thread create", status);
// This is an error in that no devices will be found, but the AHCI controller is enabled.
// Not returning an error, but the controller should be removed.
// TODO: handle this better in upcoming init cleanup CL.
}
// Controller is retained by device_add().
controller.release();
return ZX_OK;
}
constexpr zx_driver_ops_t ahci_driver_ops = []() {
zx_driver_ops_t driver = {};
driver.version = DRIVER_OPS_VERSION;
driver.bind = ahci_bind;
return driver;
}();
} // namespace ahci
ZIRCON_DRIVER(ahci, ahci::ahci_driver_ops, "zircon", "0.1");