blob: e764a54c598223dadfa647d4603fd60e4f274910 [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 <inttypes.h>
#include <lib/zx/clock.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <unistd.h>
#include <zircon/assert.h>
#include <zircon/listnode.h>
#include <zircon/syscalls.h>
#include <zircon/types.h>
#include <mutex>
#include <fbl/alloc_checker.h>
#include "pci-bus.h"
#include "sata.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) {
FDF_LOG(ERROR, "HBA reset timed out");
}
return status;
}
zx_status_t Controller::SetDevInfo(uint32_t portnr, SataDeviceInfo* 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, SataTransaction* txn) {
ZX_DEBUG_ASSERT(portnr < AHCI_MAX_PORTS);
Port* port = &ports_[portnr];
zx_status_t status = port->Queue(txn);
if (status == ZX_OK) {
FDF_LOG(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 loop
worker_event_completion_.Signal();
} else {
FDF_LOG(INFO, "ahci.%u: Failed to queue txn %p: %s", port->num(), txn,
zx_status_get_string(status));
// TODO: close transaction.
}
}
void Controller::PrepareStop(fdf::PrepareStopCompleter completer) {
Shutdown();
completer(zx::ok());
}
bool Controller::ShouldExit() {
std::lock_guard<std::mutex> lock(lock_);
return shutdown_;
}
void 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;
}
// Wait here until more commands are queued, or a port becomes idle.
worker_event_completion_.Wait();
worker_event_completion_.Reset();
}
}
void Controller::IrqLoop() {
for (;;) {
zx_status_t status = bus_->InterruptWait();
if (status != ZX_OK) {
if (!ShouldExit()) {
FDF_LOG(ERROR, "Error waiting for interrupt: %s", zx_status_get_string(status));
}
return;
}
// 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 loop to complete commands
worker_event_completion_.Signal();
}
}
is >>= 1;
}
// unmask hba interrupts
ghc = RegRead(kHbaGlobalHostControl);
RegWrite(kHbaGlobalHostControl, ghc | AHCI_GHC_IE);
}
}
// implement device protocol:
zx_status_t Controller::Init() {
zx_status_t status;
if ((status = LaunchIrqAndWorkerDispatchers()) != ZX_OK) {
FDF_LOG(ERROR, "Failed to start controller irq and worker threads: %s",
zx_status_get_string(status));
return status;
}
// reset
HbaReset();
// enable ahci mode
AhciEnable();
const uint32_t capabilities = RegRead(kHbaCapabilities);
const bool use_command_queue = capabilities & AHCI_CAP_NCQ;
const uint32_t max_command_tag = (capabilities >> 8) & 0x1f;
inspect_node_ = inspector_.GetRoot().CreateChild(kDriverName);
inspect_node_.RecordBool("native_command_queuing", use_command_queue);
inspect_node_.RecordUint("max_command_tag", max_command_tag);
// count number of ports
uint32_t port_map = RegRead(kHbaPortsImplemented);
// initialize ports
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, max_command_tag);
if (status != ZX_OK) {
FDF_LOG(ERROR, "Failed to configure port %u: %s", i, zx_status_get_string(status));
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->port_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_device_present(true);
if (port->RegRead(kPortSignature) == AHCI_PORT_SIG_SATA) {
zx::result<std::unique_ptr<SataDevice>> device =
SataDevice::Bind(this, port->num(), use_command_queue);
if (device.is_error()) {
FDF_LOG(ERROR, "Failed to add SATA device at port %u: %s", port->num(),
device.status_string());
return device.status_value();
}
sata_devices_.push_back(*std::move(device));
}
}
}
return ZX_OK;
}
zx_status_t Controller::LaunchIrqAndWorkerDispatchers() {
auto dispatcher = fdf::SynchronizedDispatcher::Create(
fdf::SynchronizedDispatcher::Options::kAllowSyncCalls, "ahci-irq",
[&](fdf_dispatcher_t*) { irq_shutdown_completion_.Signal(); });
if (dispatcher.is_error()) {
FDF_LOG(ERROR, "Failed to create irq dispatcher: %s",
zx_status_get_string(dispatcher.status_value()));
return dispatcher.status_value();
}
irq_dispatcher_ = *std::move(dispatcher);
zx_status_t status = async::PostTask(irq_dispatcher_.async_dispatcher(), [this] { IrqLoop(); });
if (status != ZX_OK) {
FDF_LOG(ERROR, "Error creating irq loop: %s", zx_status_get_string(status));
return status;
}
worker_shutdown_.store(false);
dispatcher = fdf::SynchronizedDispatcher::Create(
fdf::SynchronizedDispatcher::Options::kAllowSyncCalls, "ahci-worker",
[&](fdf_dispatcher_t*) { worker_shutdown_.store(true); });
if (dispatcher.is_error()) {
FDF_LOG(ERROR, "Failed to create dispatcher: %s",
zx_status_get_string(dispatcher.status_value()));
return dispatcher.status_value();
}
worker_dispatcher_ = *std::move(dispatcher);
status = async::PostTask(worker_dispatcher_.async_dispatcher(), [this] { WorkerLoop(); });
if (status != ZX_OK) {
FDF_LOG(ERROR, "Error creating worker loop: %s", zx_status_get_string(status));
return status;
}
return ZX_OK;
}
void Controller::Shutdown() {
{
std::lock_guard<std::mutex> lock(lock_);
shutdown_ = true;
}
if (worker_dispatcher_.get() && !worker_shutdown_.load()) {
worker_dispatcher_.ShutdownAsync();
// TODO(https://fxbug.dev/42061061): This driver may be missing a watchdog-like capability to
// check in on in-flight device requests to detect timeouts. The polling here implements the
// watchdog only for the shutdown case, but a more general solution may be needed.
while (!worker_shutdown_.load()) {
worker_event_completion_.Signal();
zx::nanosleep(zx::deadline_after(zx::msec(10)));
}
}
// Signal the interrupt loop to exit.
bus_->InterruptCancel();
if (irq_dispatcher_.get() && !irq_shutdown_completion_.signaled()) {
irq_dispatcher_.ShutdownAsync();
irq_shutdown_completion_.Wait();
}
}
zx::result<std::unique_ptr<Bus>> Controller::CreateBus() {
auto pci_client_end = incoming()->Connect<fuchsia_hardware_pci::Service::Device>("pci");
if (!pci_client_end.is_ok()) {
FDF_LOG(ERROR, "Failed to connect to PCI device service: %s", pci_client_end.status_string());
return pci_client_end.take_error();
}
auto pci = fidl::WireSyncClient(*std::move(pci_client_end));
fbl::AllocChecker ac;
auto bus = fbl::make_unique_checked<PciBus>(&ac, std::move(pci));
if (!ac.check()) {
FDF_LOG(ERROR, "Failed to allocate memory for bus.");
return zx::error(ZX_ERR_NO_MEMORY);
}
return zx::ok(std::move(bus));
}
zx::result<> Controller::Start() {
parent_node_.Bind(std::move(node()));
if (AHCI_PAGE_SIZE != zx_system_get_page_size()) {
FDF_LOG(ERROR, "System page size of %u does not match expected page size of %u\n",
zx_system_get_page_size(), AHCI_PAGE_SIZE);
return zx::error(ZX_ERR_INTERNAL);
}
zx::result<std::unique_ptr<Bus>> bus = CreateBus();
if (bus.is_error()) {
return bus.take_error();
}
bus_ = *std::move(bus);
zx_status_t status = bus_->Configure();
if (status != ZX_OK) {
FDF_LOG(ERROR, "Failed to configure host bus");
return zx::error(status);
}
auto inspect_sink = incoming()->Connect<fuchsia_inspect::InspectSink>();
if (inspect_sink.is_error() || !inspect_sink->is_valid()) {
FDF_LOG(ERROR, "Failed to connect to inspect sink: %s", inspect_sink.status_string());
return inspect_sink.take_error();
}
exposed_inspector_.emplace(inspect::ComponentInspector(
dispatcher(), {.inspector = inspector(), .client_end = std::move(inspect_sink.value())}));
auto [controller_client_end, controller_server_end] =
fidl::Endpoints<fuchsia_driver_framework::NodeController>::Create();
auto [node_client_end, node_server_end] =
fidl::Endpoints<fuchsia_driver_framework::Node>::Create();
node_controller_.Bind(std::move(controller_client_end));
root_node_.Bind(std::move(node_client_end));
fidl::Arena arena;
const auto args =
fuchsia_driver_framework::wire::NodeAddArgs::Builder(arena).name(arena, name()).Build();
auto result =
parent_node_->AddChild(args, std::move(controller_server_end), std::move(node_server_end));
if (!result.ok()) {
FDF_LOG(ERROR, "Failed to add child: %s", result.status_string());
return zx::error(result.status());
}
status = Init();
if (status != ZX_OK) {
FDF_LOG(ERROR, "Driver initialization failed: %s", zx_status_get_string(status));
return zx::error(status);
}
return zx::ok();
}
} // namespace ahci