| // 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/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 <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/device.h> |
| #include <ddk/driver.h> |
| #include <ddk/io-buffer.h> |
| #include <ddk/phys-iter.h> |
| #include <ddk/protocol/pci.h> |
| #include <fbl/alloc_checker.h> |
| #include <fbl/auto_lock.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) { |
| 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 |
| |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(ahci, ahci::ahci_driver_ops, "zircon", "0.1", 4) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PCI), |
| BI_ABORT_IF(NE, BIND_PCI_CLASS, 0x01), |
| BI_ABORT_IF(NE, BIND_PCI_SUBCLASS, 0x06), |
| BI_MATCH_IF(EQ, BIND_PCI_INTERFACE, 0x01), |
| ZIRCON_DRIVER_END(ahci) |