blob: 23ff3d23ab28f5b7c6203979c4cf388fdd9a1c06 [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 <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <ddk/io-buffer.h>
#include <ddk/mmio-buffer.h>
#include <ddk/phys-iter.h>
#include <ddk/protocol/pci.h>
#include <ddk/protocol/pci-lib.h>
#include <assert.h>
#include <hw/pci.h>
#include <zircon/listnode.h>
#include <zircon/syscalls.h>
#include <zircon/types.h>
#include <zircon/assert.h>
#include <pretty/hexdump.h>
#include <lib/sync/completion.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <threads.h>
#include <unistd.h>
#include "ahci.h"
#include "sata.h"
#define ahci_read(reg) pcie_read32(reg)
#define ahci_write(reg, val) pcie_write32(reg, val)
#define HI32(val) (((val) >> 32) & 0xffffffff)
#define LO32(val) ((val) & 0xffffffff)
#define PAGE_MASK (PAGE_SIZE - 1ull)
// port is implemented by the controller
#define AHCI_PORT_FLAG_IMPLEMENTED (1u << 0)
// a device is present on port
#define AHCI_PORT_FLAG_PRESENT (1u << 1)
// port is paused (no queued transactions will be processed)
// until pending transactions are done
#define AHCI_PORT_FLAG_SYNC_PAUSED (1u << 2)
//clang-format on
typedef struct ahci_port {
int nr; // 0-based
int flags;
sata_devinfo_t devinfo;
ahci_port_reg_t* regs;
ahci_cl_t* cl;
ahci_fis_t* fis;
ahci_ct_t* ct[AHCI_MAX_COMMANDS];
mtx_t lock;
list_node_t txn_list;
io_buffer_t buffer;
uint32_t running; // bitmask of running commands
uint32_t completed; // bitmask of completed commands
sata_txn_t* commands[AHCI_MAX_COMMANDS]; // commands in flight
sata_txn_t* sync; // FLUSH command in flight
} ahci_port_t;
struct ahci_device {
zx_device_t* zxdev;
ahci_hba_t* regs;
mmio_buffer_t mmio;
pci_protocol_t pci;
zx_handle_t irq_handle;
thrd_t irq_thread;
zx_handle_t bti_handle;
thrd_t worker_thread;
sync_completion_t worker_completion;
thrd_t watchdog_thread;
sync_completion_t watchdog_completion;
uint32_t cap;
// TODO(ZX-1641): lazily allocate these
ahci_port_t ports[AHCI_MAX_PORTS];
};
static inline zx_status_t ahci_wait_for_clear(const volatile uint32_t* reg, uint32_t mask,
zx_time_t timeout) {
int i = 0;
zx_time_t start_time = zx_clock_get_monotonic();
do {
if (!(ahci_read(reg) & mask)) return ZX_OK;
usleep(10 * 1000);
i++;
} while (zx_clock_get_monotonic() - start_time < timeout);
return ZX_ERR_TIMED_OUT;
}
static inline zx_status_t ahci_wait_for_set(const volatile uint32_t* reg, uint32_t mask,
zx_time_t timeout) {
int i = 0;
zx_time_t start_time = zx_clock_get_monotonic();
do {
if (ahci_read(reg) & mask) return ZX_OK;
usleep(10 * 1000);
i++;
} while (zx_clock_get_monotonic() - start_time < timeout);
return ZX_ERR_TIMED_OUT;
}
static bool ahci_port_valid(ahci_device_t* dev, int portnr) {
if (portnr >= AHCI_MAX_PORTS) {
return false;
}
ahci_port_t* port = &dev->ports[portnr];
int flags = AHCI_PORT_FLAG_IMPLEMENTED | AHCI_PORT_FLAG_PRESENT;
return (port->flags & flags) == flags;
}
static void ahci_port_disable(ahci_port_t* port) {
uint32_t cmd = ahci_read(&port->regs->cmd);
if (!(cmd & AHCI_PORT_CMD_ST)) return;
cmd &= ~AHCI_PORT_CMD_ST;
ahci_write(&port->regs->cmd, cmd);
zx_status_t status = ahci_wait_for_clear(&port->regs->cmd, AHCI_PORT_CMD_CR, 500 * 1000 * 1000);
if (status) {
zxlogf(ERROR, "ahci.%d: port disable timed out\n", port->nr);
}
}
static void ahci_port_enable(ahci_port_t* port) {
uint32_t cmd = ahci_read(&port->regs->cmd);
if (cmd & AHCI_PORT_CMD_ST) return;
if (!(cmd & AHCI_PORT_CMD_FRE)) {
zxlogf(ERROR, "ahci.%d: cannot enable port without FRE enabled\n", port->nr);
return;
}
zx_status_t status = ahci_wait_for_clear(&port->regs->cmd, AHCI_PORT_CMD_CR, 500 * 1000 * 1000);
if (status) {
zxlogf(ERROR, "ahci.%d: dma engine still running when enabling port\n", port->nr);
}
cmd |= AHCI_PORT_CMD_ST;
ahci_write(&port->regs->cmd, cmd);
}
static void ahci_port_reset(ahci_port_t* port) {
// disable port
ahci_port_disable(port);
// clear error
ahci_write(&port->regs->serr, ahci_read(&port->regs->serr));
// wait for device idle
zx_status_t status = ahci_wait_for_clear(&port->regs->tfd, AHCI_PORT_TFD_BUSY | AHCI_PORT_TFD_DATA_REQUEST, 1000 * 1000 * 1000);
if (status < 0) {
// if busy is not cleared, do a full comreset
zxlogf(SPEW, "ahci.%d: timed out waiting for port idle, resetting\n", port->nr);
// v1.3.1, 10.4.2 port reset
uint32_t sctl = AHCI_PORT_SCTL_IPM_ACTIVE | AHCI_PORT_SCTL_IPM_PARTIAL | AHCI_PORT_SCTL_DET_INIT;
ahci_write(&port->regs->sctl, sctl);
usleep(1000);
sctl = ahci_read(&port->regs->sctl);
sctl &= ~AHCI_PORT_SCTL_DET_MASK;
ahci_write(&port->regs->sctl, sctl);
}
// enable port
ahci_port_enable(port);
// wait for device detect
status = ahci_wait_for_set(&port->regs->ssts, AHCI_PORT_SSTS_DET_PRESENT, 1llu * 1000 * 1000 * 1000);
if ((driver_get_log_flags() & DDK_LOG_SPEW) && (status < 0)) {
zxlogf(SPEW, "ahci.%d: no device detected\n", port->nr);
}
// clear error
ahci_write(&port->regs->serr, ahci_read(&port->regs->serr));
}
static bool ahci_port_cmd_busy(ahci_port_t* port, int slot) {
// a command slot is busy if a transaction is in flight or pending to be completed
return ((ahci_read(&port->regs->sact) | ahci_read(&port->regs->ci)) & (1u << slot)) ||
(port->commands[slot] != NULL) ||
(port->running & (1u << slot)) || (port->completed & (1u << slot));
}
static bool cmd_is_read(uint8_t cmd) {
if (cmd == SATA_CMD_READ_DMA ||
cmd == SATA_CMD_READ_DMA_EXT ||
cmd == SATA_CMD_READ_FPDMA_QUEUED) {
return true;
} else {
return false;
}
}
static bool cmd_is_write(uint8_t cmd) {
if (cmd == SATA_CMD_WRITE_DMA ||
cmd == SATA_CMD_WRITE_DMA_EXT ||
cmd == SATA_CMD_WRITE_FPDMA_QUEUED) {
return true;
} else {
return false;
}
}
static bool cmd_is_queued(uint8_t cmd) {
return (cmd == SATA_CMD_READ_FPDMA_QUEUED) || (cmd == SATA_CMD_WRITE_FPDMA_QUEUED);
}
static void ahci_port_complete_txn(ahci_device_t* dev, ahci_port_t* port, zx_status_t status) {
mtx_lock(&port->lock);
uint32_t active = ahci_read(&port->regs->sact); // Transactions active in hardware.
uint32_t running = port->running; // Transactions tagged as running.
// Transactions active in hardware but not tagged as running.
uint32_t unaccounted = active & ~running;
// Remove transactions that have been completed by the watchdog.
unaccounted &= ~port->completed;
// assert if a command slot without an outstanding transaction is active.
ZX_DEBUG_ASSERT(unaccounted == 0);
// Transactions tagged as running but completed by hardware.
uint32_t done = running & ~active;
port->completed |= done;
mtx_unlock(&port->lock);
// hit the worker thread to complete commands
sync_completion_signal(&dev->worker_completion);
}
static zx_status_t ahci_do_txn(ahci_device_t* dev, ahci_port_t* port, int slot, sata_txn_t* txn) {
assert(slot < AHCI_MAX_COMMANDS);
assert(!ahci_port_cmd_busy(port, slot));
uint64_t offset_vmo = txn->bop.rw.offset_vmo * port->devinfo.block_size;
uint64_t bytes = txn->bop.rw.length * port->devinfo.block_size;
size_t pagecount = ((offset_vmo & (PAGE_SIZE - 1)) + bytes + (PAGE_SIZE - 1)) /
PAGE_SIZE;
zx_paddr_t pages[AHCI_MAX_PAGES];
if (pagecount > AHCI_MAX_PAGES) {
zxlogf(SPEW, "ahci.%d: txn %p too many pages (%zd)\n", port->nr, txn, pagecount);
return ZX_ERR_INVALID_ARGS;
}
zx_handle_t vmo = txn->bop.rw.vmo;
bool is_write = cmd_is_write(txn->cmd);
uint32_t options = is_write ? ZX_BTI_PERM_READ : ZX_BTI_PERM_WRITE;
zx_handle_t pmt;
zx_status_t st = zx_bti_pin(dev->bti_handle, options, vmo, offset_vmo & ~PAGE_MASK,
pagecount * PAGE_SIZE, pages, pagecount, &pmt);
if (st != ZX_OK) {
zxlogf(SPEW, "ahci.%d: failed to pin pages, err = %d\n", port->nr, st);
return st;
}
txn->pmt = pmt;
phys_iter_buffer_t physbuf = {
.phys = pages,
.phys_count = pagecount,
.length = bytes,
.vmo_offset = offset_vmo,
};
phys_iter_t iter;
phys_iter_init(&iter, &physbuf, AHCI_PRD_MAX_SIZE);
uint8_t cmd = txn->cmd;
uint8_t device = txn->device;
uint64_t lba = txn->bop.rw.offset_dev;
uint64_t count = txn->bop.rw.length;
// use queued command if available
if (dev->cap & AHCI_CAP_NCQ) {
if (cmd == SATA_CMD_READ_DMA_EXT) {
cmd = SATA_CMD_READ_FPDMA_QUEUED;
} else if (cmd == SATA_CMD_WRITE_DMA_EXT) {
cmd = SATA_CMD_WRITE_FPDMA_QUEUED;
}
}
// build the command
ahci_cl_t* cl = port->cl + slot;
// don't clear the cl since we set up ctba/ctbau at init
cl->prdtl_flags_cfl = 0;
cl->cfl = 5; // 20 bytes
cl->w = is_write ? 1 : 0;
cl->prdbc = 0;
memset(port->ct[slot], 0, sizeof(ahci_ct_t));
uint8_t* cfis = port->ct[slot]->cfis;
cfis[0] = 0x27; // host-to-device
cfis[1] = 0x80; // command
cfis[2] = cmd;
cfis[7] = device;
// some commands have lba/count fields
if (cmd == SATA_CMD_READ_DMA_EXT ||
cmd == SATA_CMD_WRITE_DMA_EXT) {
cfis[4] = lba & 0xff;
cfis[5] = (lba >> 8) & 0xff;
cfis[6] = (lba >> 16) & 0xff;
cfis[8] = (lba >> 24) & 0xff;
cfis[9] = (lba >> 32) & 0xff;
cfis[10] = (lba >> 40) & 0xff;
cfis[12] = count & 0xff;
cfis[13] = (count >> 8) & 0xff;
} else if (cmd_is_queued(cmd)) {
cfis[4] = lba & 0xff;
cfis[5] = (lba >> 8) & 0xff;
cfis[6] = (lba >> 16) & 0xff;
cfis[8] = (lba >> 24) & 0xff;
cfis[9] = (lba >> 32) & 0xff;
cfis[10] = (lba >> 40) & 0xff;
cfis[3] = count & 0xff;
cfis[11] = (count >> 8) & 0xff;
cfis[12] = (slot << 3) & 0xff; // tag
cfis[13] = 0; // normal priority
}
cl->prdtl = 0;
ahci_prd_t* prd = (ahci_prd_t*)((void*)port->ct[slot] + sizeof(ahci_ct_t));
size_t length;
zx_paddr_t paddr;
for (;;) {
length = phys_iter_next(&iter, &paddr);
if (length == 0) {
break;
} else if (length > AHCI_PRD_MAX_SIZE) {
zxlogf(ERROR, "ahci.%d: chunk size > %zu is unsupported\n", port->nr, length);
return ZX_ERR_NOT_SUPPORTED;;
} else if (cl->prdtl == AHCI_MAX_PRDS) {
zxlogf(ERROR, "ahci.%d: txn with more than %d chunks is unsupported\n",
port->nr, cl->prdtl);
return ZX_ERR_NOT_SUPPORTED;
}
prd->dba = LO32(paddr);
prd->dbau = HI32(paddr);
prd->dbc = ((length - 1) & (AHCI_PRD_MAX_SIZE - 1)); // 0-based byte count
cl->prdtl += 1;
prd += 1;
}
port->running |= (1u << slot);
port->commands[slot] = txn;
zxlogf(SPEW, "ahci.%d: do_txn txn %p (%c) offset 0x%" PRIx64 " length 0x%" PRIx64
" slot %d prdtl %u\n",
port->nr, txn, cl->w ? 'w' : 'r', lba, count, slot, cl->prdtl);
prd = (ahci_prd_t*)((void*)port->ct[slot] + sizeof(ahci_ct_t));
if (driver_get_log_flags() & DDK_LOG_SPEW) {
for (uint i = 0; i < cl->prdtl; i++) {
zxlogf(SPEW, "%04u: dbau=0x%08x dba=0x%08x dbc=0x%x\n",
i, prd->dbau, prd->dba, prd->dbc);
prd += 1;
}
}
// start command
if (cmd_is_queued(cmd)) {
ahci_write(&port->regs->sact, (1u << slot));
}
ahci_write(&port->regs->ci, (1u << slot));
// set the watchdog
// TODO: general timeout mechanism
txn->timeout = zx_clock_get_monotonic() + ZX_SEC(1);
sync_completion_signal(&dev->watchdog_completion);
return ZX_OK;
}
static zx_status_t ahci_port_initialize(ahci_device_t* dev, ahci_port_t* port) {
uint32_t cmd = ahci_read(&port->regs->cmd);
if (cmd & (AHCI_PORT_CMD_ST | AHCI_PORT_CMD_FRE | AHCI_PORT_CMD_CR | AHCI_PORT_CMD_FR)) {
zxlogf(ERROR, "ahci.%d: port busy\n", port->nr);
return ZX_ERR_UNAVAILABLE;
}
// allocate memory for the command list, FIS receive area, command table and PRDT
size_t ct_prd_sz = sizeof(ahci_ct_t) + sizeof(ahci_prd_t) * AHCI_MAX_PRDS;
size_t ct_prd_padding = 0x80 - (ct_prd_sz & (0x80 - 1)); // 128-byte aligned
size_t mem_sz = sizeof(ahci_fis_t) + sizeof(ahci_cl_t) * AHCI_MAX_COMMANDS
+ (ct_prd_sz + ct_prd_padding) * AHCI_MAX_COMMANDS;
zx_status_t status = io_buffer_init(&port->buffer, dev->bti_handle,
mem_sz, IO_BUFFER_RW | IO_BUFFER_CONTIG);
if (status < 0) {
zxlogf(ERROR, "ahci.%d: error %d allocating dma memory\n", port->nr, status);
return status;
}
zx_paddr_t mem_phys = io_buffer_phys(&port->buffer);
void* mem = io_buffer_virt(&port->buffer);
// clear memory area
// order is command list (1024-byte aligned)
// FIS receive area (256-byte aligned)
// command table + PRDT (127-byte aligned)
memset(mem, 0, mem_sz);
// command list
ahci_write(&port->regs->clb, LO32(mem_phys));
ahci_write(&port->regs->clbu, HI32(mem_phys));
mem_phys += sizeof(ahci_cl_t) * AHCI_MAX_COMMANDS;
port->cl = mem;
mem += sizeof(ahci_cl_t) * AHCI_MAX_COMMANDS;
// FIS receive area
ahci_write(&port->regs->fb, LO32(mem_phys));
ahci_write(&port->regs->fbu, HI32(mem_phys));
mem_phys += sizeof(ahci_fis_t);
port->fis = mem;
mem += sizeof(ahci_fis_t);
// command table, followed by PRDT
for (int i = 0; i < AHCI_MAX_COMMANDS; i++) {
port->cl[i].ctba = LO32(mem_phys);
port->cl[i].ctbau = HI32(mem_phys);
mem_phys += ct_prd_sz + ct_prd_padding;
port->ct[i] = mem;
mem += ct_prd_sz + ct_prd_padding;
}
// clear port interrupts
ahci_write(&port->regs->is, ahci_read(&port->regs->is));
// clear error
ahci_write(&port->regs->serr, ahci_read(&port->regs->serr));
// spin up
cmd |= AHCI_PORT_CMD_SUD;
ahci_write(&port->regs->cmd, cmd);
// activate link
cmd &= ~AHCI_PORT_CMD_ICC_MASK;
cmd |= AHCI_PORT_CMD_ICC_ACTIVE;
ahci_write(&port->regs->cmd, cmd);
// enable FIS receive
cmd |= AHCI_PORT_CMD_FRE;
ahci_write(&port->regs->cmd, cmd);
return ZX_OK;
}
static void ahci_enable_ahci(ahci_device_t* dev) {
uint32_t ghc = ahci_read(&dev->regs->ghc);
if (ghc & AHCI_GHC_AE) return;
for (int i = 0; i < 5; i++) {
ghc |= AHCI_GHC_AE;
ahci_write(&dev->regs->ghc, ghc);
ghc = ahci_read(&dev->regs->ghc);
if (ghc & AHCI_GHC_AE) return;
usleep(10 * 1000);
}
}
static void ahci_hba_reset(ahci_device_t* dev) {
// AHCI 1.3: Software may perform an HBA reset prior to initializing the controller
uint32_t ghc = ahci_read(&dev->regs->ghc);
ghc |= AHCI_GHC_AE;
ahci_write(&dev->regs->ghc, ghc);
ghc |= AHCI_GHC_HR;
ahci_write(&dev->regs->ghc, ghc);
// reset should complete within 1 second
zx_status_t status = ahci_wait_for_clear(&dev->regs->ghc, AHCI_GHC_HR, 1000 * 1000 * 1000);
if (status) {
zxlogf(ERROR, "ahci: hba reset timed out\n");
}
}
void ahci_set_devinfo(ahci_device_t* device, int portnr, sata_devinfo_t* devinfo) {
ZX_DEBUG_ASSERT(ahci_port_valid(device, portnr));
ahci_port_t* port = &device->ports[portnr];
memcpy(&port->devinfo, devinfo, sizeof(port->devinfo));
}
void ahci_queue(ahci_device_t* device, int portnr, sata_txn_t* txn) {
ZX_DEBUG_ASSERT(ahci_port_valid(device, portnr));
ahci_port_t* port = &device->ports[portnr];
zxlogf(SPEW, "ahci.%d: queue_txn txn %p offset_dev 0x%" PRIx64 " length 0x%x\n",
port->nr, txn, txn->bop.rw.offset_dev, txn->bop.rw.length);
// reset the physical address
txn->pmt = ZX_HANDLE_INVALID;
// put the cmd on the queue
mtx_lock(&port->lock);
list_add_tail(&port->txn_list, &txn->node);
// hit the worker thread
sync_completion_signal(&device->worker_completion);
mtx_unlock(&port->lock);
}
static void ahci_release(void* ctx) {
// FIXME - join threads created by this driver
ahci_device_t* device = ctx;
mmio_buffer_release(&device->mmio);
zx_handle_close(device->irq_handle);
zx_handle_close(device->bti_handle);
free(device);
}
// worker thread
static int ahci_worker_thread(void* arg) {
ahci_device_t* dev = (ahci_device_t*)arg;
ahci_port_t* port;
sata_txn_t* txn;
for (;;) {
// iterate all the ports and run or complete commands
for (int i = 0; i < AHCI_MAX_PORTS; i++) {
port = &dev->ports[i];
mtx_lock(&port->lock);
if (!ahci_port_valid(dev, i)) {
goto next;
}
// complete commands first
while (port->completed) {
unsigned slot = 32 - __builtin_clz(port->completed) - 1;
txn = port->commands[slot];
if (txn == NULL) {
// Transaction was completed by watchdog.
} else {
mtx_unlock(&port->lock);
if (txn->pmt != ZX_HANDLE_INVALID) {
zx_pmt_unpin(txn->pmt);
}
zxlogf(SPEW, "ahci.%d: complete txn %p\n", port->nr, txn);
block_complete(txn, ZX_OK);
mtx_lock(&port->lock);
}
port->completed &= ~(1u << slot);
port->running &= ~(1u << slot);
port->commands[slot] = NULL;
// resume the port if paused for sync and no outstanding transactions
if ((port->flags & AHCI_PORT_FLAG_SYNC_PAUSED) && !port->running) {
port->flags &= ~AHCI_PORT_FLAG_SYNC_PAUSED;
if (port->sync) {
sata_txn_t* sop = port->sync;
port->sync = NULL;
mtx_unlock(&port->lock);
block_complete(sop, ZX_OK);
mtx_lock(&port->lock);
}
}
}
if (port->flags & AHCI_PORT_FLAG_SYNC_PAUSED) {
goto next;
}
// process queued txns
for (;;) {
txn = list_peek_head_type(&port->txn_list, sata_txn_t, node);
if (!txn) {
break;
}
// find a free command tag
int max = MIN(port->devinfo.max_cmd, (int)((dev->cap >> 8) & 0x1f));
int i = 0;
for (i = 0; i <= max; i++) {
if (!ahci_port_cmd_busy(port, i)) break;
}
if (i > max) {
break;
}
list_delete(&txn->node);
if (BLOCK_OP(txn->bop.command) == BLOCK_OP_FLUSH) {
if (port->running) {
ZX_DEBUG_ASSERT(port->sync == NULL);
// pause the port if FLUSH command
port->flags |= AHCI_PORT_FLAG_SYNC_PAUSED;
port->sync = txn;
} else {
// complete immediately if nothing in flight
mtx_unlock(&port->lock);
block_complete(txn, ZX_OK);
mtx_lock(&port->lock);
}
} else {
// run the transaction
zx_status_t st = ahci_do_txn(dev, port, i, txn);
// complete the transaction with if it failed during processing
if (st != ZX_OK) {
mtx_unlock(&port->lock);
block_complete(txn, st);
mtx_lock(&port->lock);
continue;
}
}
}
next:
mtx_unlock(&port->lock);
}
// wait here until more commands are queued, or a port becomes idle
sync_completion_wait(&dev->worker_completion, ZX_TIME_INFINITE);
sync_completion_reset(&dev->worker_completion);
}
return 0;
}
static int ahci_watchdog_thread(void* arg) {
ahci_device_t* dev = (ahci_device_t*)arg;
for (;;) {
bool idle = true;
for (int i = 0; i < AHCI_MAX_PORTS; i++) {
ahci_port_t* port = &dev->ports[i];
if (!ahci_port_valid(dev, i)) {
continue;
}
mtx_lock(&port->lock);
zx_time_t now = zx_clock_get_monotonic();
uint32_t pending = port->running & ~port->completed;
sata_txn_t* failed_txn[AHCI_MAX_COMMANDS];
static_assert(AHCI_MAX_COMMANDS >= 32,
"Failed TXN insufficiently sized to handle all commmand");
while (pending) {
idle = false;
unsigned slot = 32 - __builtin_clz(pending) - 1;
failed_txn[slot] = NULL;
sata_txn_t* txn = port->commands[slot];
pending &= ~(1u << slot);
if (!txn) {
zxlogf(ERROR, "ahci: command %u pending but txn is NULL\n", slot);
continue;
}
if (txn->timeout >= now) {
continue;
}
// Check whether this is a real timeout.
uint32_t active = ahci_read(&port->regs->sact);
if ((active & (1u << slot)) == 0) {
// Command is no longer active, it has completed but not yet serviced by
// IRQ thread. Get the time this event happened, compare to time
// watchdog loop started to determine whether it has blocked for too
// long.
zx_time_t looptime = zx_clock_get_monotonic() - now;
zxlogf(ERROR,
"ahci: spurious watchdog timeout port %u txn %p, time in watchdog = %lu\n",
port->nr, txn, looptime);
} else {
// time out
zxlogf(ERROR, "ahci: txn time out on port %d txn %p\n", port->nr, txn);
port->running &= ~(1u << slot);
port->completed |= (1u << slot);
port->commands[slot] = NULL;
failed_txn[slot] = txn;
}
}
mtx_unlock(&port->lock);
for (uint32_t i = 0; i < countof(failed_txn); i++) {
if (failed_txn[i] != NULL) {
block_complete(failed_txn[i], ZX_ERR_TIMED_OUT);
}
}
}
// no need to run the watchdog if there are no active xfers
sync_completion_wait(&dev->watchdog_completion, idle ? ZX_TIME_INFINITE : 5ULL * 1000 * 1000 * 1000);
sync_completion_reset(&dev->watchdog_completion);
}
return 0;
}
// irq handler:
static void ahci_port_irq(ahci_device_t* dev, int nr) {
ahci_port_t* port = &dev->ports[nr];
// clear interrupt
uint32_t int_status = ahci_read(&port->regs->is);
ahci_write(&port->regs->is, int_status);
if (int_status & AHCI_PORT_INT_PRC) { // PhyRdy change
uint32_t serr = ahci_read(&port->regs->serr);
ahci_write(&port->regs->serr, serr & ~0x1);
}
if (int_status & AHCI_PORT_INT_ERROR) { // error
zxlogf(ERROR, "ahci.%d: error is=0x%08x\n", nr, int_status);
ahci_port_complete_txn(dev, port, ZX_ERR_INTERNAL);
} else if (int_status) {
ahci_port_complete_txn(dev, port, ZX_OK);
}
}
static int ahci_irq_thread(void* arg) {
ahci_device_t* dev = (ahci_device_t*)arg;
zx_status_t status;
for (;;) {
status = zx_interrupt_wait(dev->irq_handle, NULL);
if (status) {
zxlogf(ERROR, "ahci: error %d waiting for interrupt\n", status);
continue;
}
// mask hba interrupts while interrupts are being handled
uint32_t ghc = ahci_read(&dev->regs->ghc);
ahci_write(&dev->regs->ghc, ghc & ~AHCI_GHC_IE);
// handle interrupt for each port
uint32_t is = ahci_read(&dev->regs->is);
ahci_write(&dev->regs->is, is);
for (int i = 0; is && i < AHCI_MAX_PORTS; i++) {
if (is & 0x1) {
ahci_port_irq(dev, i);
}
is >>= 1;
}
// unmask hba interrupts
ghc = ahci_read(&dev->regs->ghc);
ahci_write(&dev->regs->ghc, ghc | AHCI_GHC_IE);
}
return 0;
}
// implement device protocol:
static zx_protocol_device_t ahci_device_proto = {
.version = DEVICE_OPS_VERSION,
.release = ahci_release,
};
extern zx_protocol_device_t ahci_port_device_proto;
static int ahci_init_thread(void* arg) {
ahci_device_t* dev = (ahci_device_t*)arg;
// reset
ahci_hba_reset(dev);
// enable ahci mode
ahci_enable_ahci(dev);
dev->cap = ahci_read(&dev->regs->cap);
// count number of ports
uint32_t port_map = ahci_read(&dev->regs->pi);
// initialize ports
zx_status_t status;
ahci_port_t* port;
for (int i = 0; i < AHCI_MAX_PORTS; i++) {
port = &dev->ports[i];
port->nr = i;
if (!(port_map & (1u << i))) continue; // port not implemented
port->flags = AHCI_PORT_FLAG_IMPLEMENTED;
port->regs = &dev->regs->ports[i];
list_initialize(&port->txn_list);
status = ahci_port_initialize(dev, port);
if (status) goto fail;
}
// clear hba interrupts
ahci_write(&dev->regs->is, ahci_read(&dev->regs->is));
// enable hba interrupts
uint32_t ghc = ahci_read(&dev->regs->ghc);
ghc |= AHCI_GHC_IE;
ahci_write(&dev->regs->ghc, ghc);
// this part of port init happens after enabling interrupts in ghc
for (int i = 0; i < AHCI_MAX_PORTS; i++) {
port = &dev->ports[i];
if (!(port->flags & AHCI_PORT_FLAG_IMPLEMENTED)) continue;
// enable port
ahci_port_enable(port);
// enable interrupts
ahci_write(&port->regs->ie, AHCI_PORT_INT_MASK);
// reset port
ahci_port_reset(port);
// FIXME proper layering?
if (ahci_read(&port->regs->ssts) & AHCI_PORT_SSTS_DET_PRESENT) {
port->flags |= AHCI_PORT_FLAG_PRESENT;
if (ahci_read(&port->regs->sig) == AHCI_PORT_SIG_SATA) {
sata_bind(dev, dev->zxdev, port->nr);
}
}
}
return ZX_OK;
fail:
free(dev->ports);
return status;
}
// implement driver object:
static zx_status_t ahci_bind(void* ctx, zx_device_t* dev) {
// map resources and initialize the device
ahci_device_t* device = calloc(1, sizeof(ahci_device_t));
if (!device) {
zxlogf(ERROR, "ahci: out of memory\n");
return ZX_ERR_NO_MEMORY;
}
if (device_get_protocol(dev, ZX_PROTOCOL_PCI, &device->pci)) {
free(device);
return ZX_ERR_NOT_SUPPORTED;
}
// map register window
zx_status_t status = pci_map_bar_buffer(&device->pci,
5u,
ZX_CACHE_POLICY_UNCACHED_DEVICE,
&device->mmio);
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: error %d mapping register window\n", status);
goto fail;
}
device->regs = device->mmio.vaddr;
zx_pcie_device_info_t config;
status = pci_get_device_info(&device->pci, &config);
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: error getting config information\n");
goto fail;
}
if (config.sub_class != 0x06 && config.base_class == 0x01) { // SATA
status = ZX_ERR_NOT_SUPPORTED;
zxlogf(ERROR, "ahci: device class 0x%x unsupported!\n", config.sub_class);
goto fail;
}
// FIXME intel devices need to set SATA port enable at config + 0x92
// ahci controller is bus master
status = pci_enable_bus_master(&device->pci, true);
if (status < 0) {
zxlogf(ERROR, "ahci: error %d in enable bus master\n", status);
goto fail;
}
// Query and configure IRQ modes by trying MSI first and falling back to
// legacy if necessary.
uint32_t irq_cnt;
zx_pci_irq_mode_t irq_mode = ZX_PCIE_IRQ_MODE_MSI;
status = pci_query_irq_mode(&device->pci, ZX_PCIE_IRQ_MODE_MSI, &irq_cnt);
if (status == ZX_ERR_NOT_SUPPORTED) {
status = pci_query_irq_mode(&device->pci, ZX_PCIE_IRQ_MODE_LEGACY, &irq_cnt);
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: neither MSI nor legacy interrupts are supported\n");
goto fail;
} else {
irq_mode = ZX_PCIE_IRQ_MODE_LEGACY;
}
}
if (irq_cnt == 0) {
zxlogf(ERROR, "ahci: no interrupts available\n");
status = ZX_ERR_NO_RESOURCES;
goto fail;
}
zxlogf(INFO, "ahci: using %s interrupt\n", (irq_mode == ZX_PCIE_IRQ_MODE_MSI) ? "MSI" : "legacy");
status = pci_set_irq_mode(&device->pci, irq_mode, 1);
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: error %d setting irq mode\n", status);
goto fail;
}
// get bti handle
status = pci_get_bti(&device->pci, 0, &device->bti_handle);
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: error %d getting bti handle\n", status);
goto fail;
}
// get irq handle
status = pci_map_interrupt(&device->pci, 0, &device->irq_handle);
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: error %d getting irq handle\n", status);
goto fail;
}
// start irq thread
int ret = thrd_create_with_name(&device->irq_thread, ahci_irq_thread, device, "ahci-irq");
if (ret != thrd_success) {
zxlogf(ERROR, "ahci: error %d in irq thread create\n", ret);
goto fail;
}
// start watchdog thread
device->watchdog_completion = SYNC_COMPLETION_INIT;
thrd_create_with_name(&device->watchdog_thread, ahci_watchdog_thread, device, "ahci-watchdog");
// start worker thread (for iotxn queue)
device->worker_completion = SYNC_COMPLETION_INIT;
ret = thrd_create_with_name(&device->worker_thread, ahci_worker_thread, device, "ahci-worker");
if (ret != thrd_success) {
zxlogf(ERROR, "ahci: error %d in worker thread create\n", ret);
goto fail;
}
// add the device for the controller
device_add_args_t args = {
.version = DEVICE_ADD_ARGS_VERSION,
.name = "ahci",
.ctx = device,
.ops = &ahci_device_proto,
.flags = DEVICE_ADD_NON_BINDABLE,
};
status = device_add(dev, &args, &device->zxdev);
if (status != ZX_OK) {
zxlogf(ERROR, "ahci: error %d in device_add\n", status);
goto fail;
}
// initialize controller and detect devices
thrd_t t;
ret = thrd_create_with_name(&t, ahci_init_thread, device, "ahci-init");
if (ret != thrd_success) {
zxlogf(ERROR, "ahci: error %d in init thread create\n", status);
goto fail;
}
return ZX_OK;
fail:
// FIXME unmap, and join any threads created above
free(device);
return status;
}
static zx_driver_ops_t ahci_driver_ops = {
.version = DRIVER_OPS_VERSION,
.bind = ahci_bind,
};
// clang-format off
ZIRCON_DRIVER_BEGIN(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)