dev/block: Add NVMe driver
This commit is contained in:
@@ -221,7 +221,6 @@ uacpi_status uacpi_kernel_handle_firmware_request(uacpi_firmware_request* req) {
|
||||
|
||||
struct acpi_irq {
|
||||
struct irq_handler handler;
|
||||
struct irq_line* line;
|
||||
uacpi_interrupt_handler func;
|
||||
uacpi_handle ctx;
|
||||
};
|
||||
@@ -254,7 +253,7 @@ uacpi_status uacpi_kernel_install_interrupt_handler(
|
||||
if (!acpi_irq)
|
||||
return UACPI_STATUS_OUT_OF_MEMORY;
|
||||
acpi_irq->handler.raise = acpi_irq_raise;
|
||||
acpi_irq->line = line;
|
||||
acpi_irq->handler.line = line;
|
||||
acpi_irq->func = handler;
|
||||
acpi_irq->ctx = ctx;
|
||||
|
||||
@@ -268,7 +267,7 @@ uacpi_status uacpi_kernel_install_interrupt_handler(
|
||||
|
||||
uacpi_status uacpi_kernel_uninstall_interrupt_handler(uacpi_interrupt_handler handler, uacpi_handle irq_handle) {
|
||||
struct acpi_irq* acpi_irq = irq_handle;
|
||||
struct irq_line* line = acpi_irq->line;
|
||||
struct irq_line* line = acpi_irq->handler.line;
|
||||
irq_line_detach(line, &acpi_irq->handler);
|
||||
if (list_is_empty(&line->handlers) && line->mask)
|
||||
line->mask(line);
|
||||
|
||||
@@ -6,6 +6,6 @@ kernel_sources += files(
|
||||
'ram.c',
|
||||
)
|
||||
|
||||
if kconfig.get('CONFIG_NVME_CORE', 'n') != 'n'
|
||||
if kconfig.get('CONFIG_NVME', 'n') != 'n'
|
||||
subdir('nvme')
|
||||
endif
|
||||
|
||||
+2
-11
@@ -1,13 +1,4 @@
|
||||
menu "NVMe Support"
|
||||
|
||||
config NVME_CORE
|
||||
bool
|
||||
default y
|
||||
|
||||
config NVME_PCI
|
||||
bool "NVMe via PCI transport"
|
||||
config NVME
|
||||
bool "NVMe block devices"
|
||||
default y
|
||||
depends on PCI
|
||||
depends on NVME_CORE
|
||||
|
||||
endmenu
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#include <zinnia/mmio.h>
|
||||
#include "nvme.h"
|
||||
|
||||
void nvme_rw_cmd_write(const struct nvme_rw_cmd* cmd, struct nvme_sqe* sqe) {
|
||||
uint32_t cdw0 = mmio_field(NVME_SQE_OPC, cmd->do_write ? NVME_CMD_WRITE : NVME_CMD_READ) |
|
||||
mmio_field(NVME_SQE_CID, cmd->cid) | mmio_field(NVME_SQE_PSDT, 0); // We always want to use PRP for this.
|
||||
|
||||
uint32_t cdw10 = mmio_field(NVME_RW_SLBA_LOW, (uint32_t)cmd->start_lba);
|
||||
|
||||
uint32_t cdw11 = mmio_field(NVME_RW_SLBA_HIGH, (uint32_t)(cmd->start_lba >> 32));
|
||||
|
||||
uint32_t cdw12 = mmio_field(NVME_RW_NLB, (uint16_t)(cmd->num_lbas - 1));
|
||||
uint32_t cdw13 = 0;
|
||||
uint32_t cdw14 = 0;
|
||||
uint32_t cdw15 = 0;
|
||||
|
||||
mmio_reg_write(sqe, NVME_SQE_NSID, cmd->nsid);
|
||||
mmio_reg_write(sqe, NVME_SQE_DPTR0, cmd->prp1);
|
||||
mmio_reg_write(sqe, NVME_SQE_DPTR1, cmd->prp2);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW0, cdw0);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW10, cdw10);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW11, cdw11);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW12, cdw12);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW13, cdw13);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW14, cdw14);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW15, cdw15);
|
||||
}
|
||||
|
||||
void nvme_flush_cmd_write(const struct nvme_flush_cmd* cmd, struct nvme_sqe* sqe) {
|
||||
uint32_t cdw0 = mmio_field(NVME_SQE_OPC, NVME_CMD_FLUSH) | mmio_field(NVME_SQE_CID, cmd->cid);
|
||||
|
||||
mmio_reg_write(sqe, NVME_SQE_NSID, cmd->nsid);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW0, cdw0);
|
||||
}
|
||||
|
||||
void nvme_create_cq_cmd_write(const struct nvme_create_cq_cmd* cmd, struct nvme_sqe* sqe) {
|
||||
uint32_t cdw0 = mmio_field(NVME_SQE_OPC, NVME_ADMIN_CREATE_CQ) |
|
||||
mmio_field(NVME_SQE_PSDT, 0); // We always want to use PRP for this.
|
||||
|
||||
uint32_t cdw10 = mmio_field(NVME_CREATE_CQ_QSIZE, (uint16_t)(cmd->depth - 1)) |
|
||||
mmio_field(NVME_CREATE_CQ_QID, (uint16_t)cmd->qid);
|
||||
|
||||
uint32_t cdw11 = mmio_field(NVME_CREATE_CQ_IV, cmd->irq_vector) |
|
||||
mmio_field(NVME_CREATE_CQ_IEN, cmd->irqs_enabled ? 1 : 0) |
|
||||
mmio_field(NVME_CREATE_CQ_PC, 1); // Our buffer is physically contiguous.
|
||||
|
||||
mmio_reg_write(sqe, NVME_SQE_DPTR0, cmd->cq_addr);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW0, cdw0);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW10, cdw10);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW11, cdw11);
|
||||
}
|
||||
|
||||
void nvme_create_sq_cmd_write(const struct nvme_create_sq_cmd* cmd, struct nvme_sqe* sqe) {
|
||||
uint32_t cdw0 = mmio_field(NVME_SQE_OPC, NVME_ADMIN_CREATE_SQ) |
|
||||
mmio_field(NVME_SQE_PSDT, 0); // We always want to use PRP for this.
|
||||
|
||||
uint32_t cdw10 = mmio_field(NVME_CREATE_SQ_QSIZE, (uint16_t)(cmd->depth - 1)) |
|
||||
mmio_field(NVME_CREATE_SQ_QID, (uint16_t)cmd->qid);
|
||||
|
||||
uint32_t cdw11 = mmio_field(NVME_CREATE_SQ_CQID, (uint16_t)cmd->cqid) |
|
||||
mmio_field(NVME_CREATE_SQ_QPRIO, 0) | // Priority is the highest.
|
||||
mmio_field(NVME_CREATE_SQ_PC, 1); // Our buffer is physically contiguous.
|
||||
|
||||
mmio_reg_write(sqe, NVME_SQE_DPTR0, cmd->sq_addr);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW0, cdw0);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW10, cdw10);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW11, cdw11);
|
||||
}
|
||||
|
||||
void nvme_identify_cmd_write(const struct nvme_identify_cmd* cmd, struct nvme_sqe* sqe) {
|
||||
uint32_t cdw0 = mmio_field(NVME_SQE_OPC, NVME_ADMIN_IDENTIFY) |
|
||||
mmio_field(NVME_SQE_PSDT, 0); // We always want to use PRP for this.
|
||||
|
||||
uint32_t cdw10 = mmio_field(NVME_IDENTIFY_CNTID, cmd->controller_id) | mmio_field(NVME_IDENTIFY_CNS, cmd->cns);
|
||||
|
||||
mmio_reg_write(sqe, NVME_SQE_DPTR0, cmd->buffer);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW0, cdw0);
|
||||
mmio_reg_write(sqe, NVME_SQE_NSID, cmd->nsid);
|
||||
mmio_reg_write(sqe, NVME_SQE_CDW10, cdw10);
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#define pr_fmt(fmt) "nvme: " fmt
|
||||
|
||||
#include <zinnia/alloc.h>
|
||||
#include <zinnia/clock.h>
|
||||
#include <zinnia/mmio.h>
|
||||
#include <zinnia/print.h>
|
||||
#include <zinnia/utils.h>
|
||||
#include <arch/mem.h>
|
||||
#include <string.h>
|
||||
#include "nvme.h"
|
||||
|
||||
constexpr size_t ADMIN_QUEUE_SIZE = 32;
|
||||
constexpr size_t IO_QUEUE_SIZE_MAX = 1024;
|
||||
constexpr size_t IDENTIFY_SIZE = 4096;
|
||||
constexpr size_t NS_LIST_LEN = 1024;
|
||||
|
||||
constexpr mmio_reg8 ID_MDTS = {77, MMIO_LE};
|
||||
constexpr mmio_reg32 ID_NN = {516, MMIO_LE};
|
||||
constexpr mmio_reg64 ID_NSZE = {0, MMIO_LE};
|
||||
constexpr mmio_reg8 ID_FLBAS = {26, MMIO_LE};
|
||||
constexpr mmio_field8 ID_LBAF_IDX = {ID_FLBAS, 0, 4};
|
||||
constexpr mmio_reg32 ID_LBAF0 = {128, MMIO_LE};
|
||||
constexpr mmio_field32 ID_LBADS = {ID_LBAF0, 16, 8};
|
||||
|
||||
size_t nvme_ctrl_max_transfer_bytes(struct nvme_ctrl* ctrl) {
|
||||
const size_t cap = 512 * arch_mem_page_size();
|
||||
if (ctrl->mdts != 0)
|
||||
return MIN(ctrl->mdts, cap);
|
||||
return cap;
|
||||
}
|
||||
|
||||
errno_t nvme_ctrl_new_pci(void* regs, struct dma_tag* dma_tag, struct nvme_ctrl** out) {
|
||||
// Read controller version.
|
||||
uint32_t vs = mmio_reg_read(regs, NVME_REG_VS);
|
||||
log("NVMe controller version %u.%u.%u\n",
|
||||
mmio_field_get(NVME_VS_MJR, vs),
|
||||
mmio_field_get(NVME_VS_MNR, vs),
|
||||
mmio_field_get(NVME_VS_TER, vs));
|
||||
|
||||
uint64_t cap = mmio_reg_read(regs, NVME_REG_CAP);
|
||||
|
||||
// Check if our host page size is supported.
|
||||
const size_t mps_max = (size_t)1 << (mmio_field_get(NVME_CAP_MPSMAX, cap) + 12);
|
||||
const size_t mps_min = (size_t)1 << (mmio_field_get(NVME_CAP_MPSMIN, cap) + 12);
|
||||
const size_t page_size = arch_mem_page_size();
|
||||
if (page_size < mps_min || page_size > mps_max) {
|
||||
log_err("Host page size is not supported on this NVMe!\n");
|
||||
return ENOTSUP;
|
||||
}
|
||||
|
||||
struct nvme_ctrl* ctrl = mem_alloc(sizeof(*ctrl), 0);
|
||||
if (!ctrl)
|
||||
return ENOMEM;
|
||||
|
||||
ctrl->regs = regs;
|
||||
ctrl->dma_tag = dma_tag;
|
||||
ctrl->doorbell_stride = (size_t)4 << mmio_field_get(NVME_CAP_DSTRD, cap);
|
||||
ctrl->mps_min = mps_min;
|
||||
ctrl->mps = __builtin_ctzl(page_size) - 12;
|
||||
mutex_init(&ctrl->admin_lock);
|
||||
spin_init(&ctrl->io_lock);
|
||||
atomic_init(&ctrl->ns_count, 0);
|
||||
|
||||
*out = ctrl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sets the CC.EN flag.
|
||||
static errno_t set_status(struct nvme_ctrl* ctrl, bool enable) {
|
||||
uint64_t cap = mmio_reg_read(ctrl->regs, NVME_REG_CAP);
|
||||
// CAP.TO is in 500ms units.
|
||||
const uint64_t timeout = MAX((uint64_t)mmio_field_get(NVME_CAP_TO, cap), 1) * 500'000'000;
|
||||
const uint64_t deadline = clock_get_elapsed_ns() + timeout;
|
||||
|
||||
uint32_t cc = mmio_reg_read(ctrl->regs, NVME_REG_CC);
|
||||
cc &= ~mmio_field(NVME_CC_EN, UINT32_MAX);
|
||||
cc |= mmio_field(NVME_CC_EN, enable ? 1 : 0);
|
||||
mmio_reg_write(ctrl->regs, NVME_REG_CC, cc);
|
||||
|
||||
while (true) {
|
||||
uint32_t csts = mmio_reg_read(ctrl->regs, NVME_REG_CSTS);
|
||||
|
||||
if (mmio_field_get(NVME_CSTS_CFS, csts) != 0)
|
||||
return EIO;
|
||||
|
||||
bool ready = mmio_field_get(NVME_CSTS_RDY, csts) != 0;
|
||||
if (ready == enable)
|
||||
break;
|
||||
|
||||
if (clock_get_elapsed_ns() >= deadline)
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static errno_t admin_cmd(struct nvme_queue* queue, const struct nvme_sqe* sqe, struct nvme_completion* out) {
|
||||
errno_t status = nvme_queue_submit_cmd(queue, sqe);
|
||||
if (status)
|
||||
return status;
|
||||
return nvme_queue_next_completion(queue, out);
|
||||
}
|
||||
|
||||
static errno_t identify(struct nvme_queue* queue, const struct nvme_identify_cmd* cmd) {
|
||||
struct nvme_sqe sqe = {};
|
||||
nvme_identify_cmd_write(cmd, &sqe);
|
||||
struct nvme_completion comp;
|
||||
errno_t status = admin_cmd(queue, &sqe, &comp);
|
||||
if (status)
|
||||
return status;
|
||||
if (!nvme_status_is_success(comp.status)) {
|
||||
log_err("Identification failed with status %x\n", comp.status);
|
||||
return EIO;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
errno_t nvme_ctrl_reset(struct nvme_ctrl* ctrl, bool irqs_enabled, uint16_t io_irq_vector) {
|
||||
// Disable the controller.
|
||||
errno_t status = set_status(ctrl, false);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
uint64_t cap = mmio_reg_read(ctrl->regs, NVME_REG_CAP);
|
||||
|
||||
const size_t queue_depth = MIN((size_t)mmio_field_get(NVME_CAP_MQES, cap) + 1, IO_QUEUE_SIZE_MAX);
|
||||
|
||||
// Create an admin queue first so we can create more queues.
|
||||
struct nvme_queue* admin_queue;
|
||||
status = nvme_queue_new(ctrl->dma_tag, ctrl->regs, ctrl->doorbell_stride, 0, ADMIN_QUEUE_SIZE, &admin_queue);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
// Set the admin queue sizes.
|
||||
uint32_t aqa = 0;
|
||||
aqa |= mmio_field(NVME_AQA_ACQS, admin_queue->depth - 1);
|
||||
aqa |= mmio_field(NVME_AQA_ASQS, admin_queue->depth - 1);
|
||||
mmio_reg_write(ctrl->regs, NVME_REG_AQA, aqa);
|
||||
|
||||
// Set the addresses to the admin submission and completion queue.
|
||||
mmio_reg_write(ctrl->regs, NVME_REG_ASQ, admin_queue->sq.addr);
|
||||
mmio_reg_write(ctrl->regs, NVME_REG_ACQ, admin_queue->cq.addr);
|
||||
|
||||
uint32_t cc = mmio_reg_read(ctrl->regs, NVME_REG_CC);
|
||||
cc &=
|
||||
~(mmio_field(NVME_CC_MPS, UINT32_MAX) | mmio_field(NVME_CC_IOCQES, UINT32_MAX) |
|
||||
mmio_field(NVME_CC_IOSQES, UINT32_MAX));
|
||||
|
||||
cc |= mmio_field(NVME_CC_MPS, ctrl->mps);
|
||||
cc |= mmio_field(NVME_CC_IOCQES, __builtin_ctzl(sizeof(struct nvme_cqe)));
|
||||
cc |= mmio_field(NVME_CC_IOSQES, __builtin_ctzl(sizeof(struct nvme_sqe)));
|
||||
|
||||
mmio_reg_write(ctrl->regs, NVME_REG_CC, cc);
|
||||
|
||||
// Re-enable the controller.
|
||||
status = set_status(ctrl, true);
|
||||
if (status)
|
||||
goto fail_admin;
|
||||
|
||||
struct nvme_io_queue* io_queue;
|
||||
status =
|
||||
nvme_io_queue_new(ctrl->dma_tag, ctrl->regs, ctrl->doorbell_stride, 1, queue_depth, !irqs_enabled, &io_queue);
|
||||
if (status)
|
||||
goto fail_admin;
|
||||
|
||||
struct nvme_sqe sqe = {};
|
||||
struct nvme_completion comp;
|
||||
nvme_create_cq_cmd_write(
|
||||
&(struct nvme_create_cq_cmd){
|
||||
.cq_addr = io_queue->cq.addr,
|
||||
.depth = io_queue->depth,
|
||||
.qid = io_queue->queue_id,
|
||||
.irqs_enabled = irqs_enabled,
|
||||
.irq_vector = irqs_enabled ? io_irq_vector : 0,
|
||||
},
|
||||
&sqe
|
||||
);
|
||||
status = admin_cmd(admin_queue, &sqe, &comp);
|
||||
if (!status && !nvme_status_is_success(comp.status))
|
||||
status = EIO;
|
||||
if (status)
|
||||
goto fail_io;
|
||||
|
||||
sqe = (struct nvme_sqe){};
|
||||
nvme_create_sq_cmd_write(
|
||||
&(struct nvme_create_sq_cmd){
|
||||
.sq_addr = io_queue->sq.addr,
|
||||
.depth = io_queue->depth,
|
||||
.qid = io_queue->queue_id,
|
||||
.cqid = io_queue->queue_id,
|
||||
},
|
||||
&sqe
|
||||
);
|
||||
status = admin_cmd(admin_queue, &sqe, &comp);
|
||||
if (!status && !nvme_status_is_success(comp.status))
|
||||
status = EIO;
|
||||
if (status)
|
||||
goto fail_io;
|
||||
|
||||
mutex_lock(&ctrl->admin_lock);
|
||||
struct nvme_queue* old_admin = ctrl->admin_queue;
|
||||
ctrl->admin_queue = admin_queue;
|
||||
mutex_unlock(&ctrl->admin_lock);
|
||||
if (old_admin)
|
||||
nvme_queue_free(old_admin);
|
||||
|
||||
spin_lock(&ctrl->io_lock);
|
||||
ctrl->io_queue = io_queue;
|
||||
spin_unlock(&ctrl->io_lock);
|
||||
|
||||
return 0;
|
||||
|
||||
fail_io:
|
||||
nvme_io_queue_free(io_queue);
|
||||
fail_admin:
|
||||
nvme_queue_free(admin_queue);
|
||||
return status;
|
||||
}
|
||||
|
||||
static void trim_ascii_end(char* str, size_t len) {
|
||||
while (len > 0 &&
|
||||
(str[len - 1] == ' ' || str[len - 1] == '\t' || str[len - 1] == '\n' || str[len - 1] == '\r' ||
|
||||
str[len - 1] == '\f'))
|
||||
len--;
|
||||
str[len] = '\0';
|
||||
}
|
||||
|
||||
errno_t nvme_ctrl_identify(struct nvme_ctrl* ctrl) {
|
||||
struct dma_buffer identify_buffer;
|
||||
errno_t status = dma_buffer_alloc(ctrl->dma_tag, IDENTIFY_SIZE, &identify_buffer);
|
||||
if (status)
|
||||
return ENOMEM;
|
||||
|
||||
mutex_lock(&ctrl->admin_lock);
|
||||
if (!ctrl->admin_queue) {
|
||||
status = ENXIO;
|
||||
goto out;
|
||||
}
|
||||
|
||||
status = identify(
|
||||
ctrl->admin_queue,
|
||||
&(struct nvme_identify_cmd){
|
||||
.buffer = identify_buffer.addr,
|
||||
.controller_id = 0, // TODO: Use a real number here
|
||||
.cns = 1,
|
||||
.nsid = 0,
|
||||
}
|
||||
);
|
||||
// Check if the identification was successful.
|
||||
if (status)
|
||||
goto out;
|
||||
|
||||
dma_buffer_sync(&identify_buffer, 0, IDENTIFY_SIZE, DMA_SYNC_POSTREAD);
|
||||
|
||||
// Read the model strings.
|
||||
const uint8_t* view = identify_buffer.virt;
|
||||
memcpy(ctrl->serial, view + 4, sizeof(ctrl->serial) - 1);
|
||||
memcpy(ctrl->model, view + 24, sizeof(ctrl->model) - 1);
|
||||
memcpy(ctrl->revision, view + 64, sizeof(ctrl->revision) - 1);
|
||||
trim_ascii_end(ctrl->serial, sizeof(ctrl->serial) - 1);
|
||||
trim_ascii_end(ctrl->model, sizeof(ctrl->model) - 1);
|
||||
trim_ascii_end(ctrl->revision, sizeof(ctrl->revision) - 1);
|
||||
log("Model \"%s\"\n", ctrl->model);
|
||||
|
||||
atomic_store(&ctrl->ns_count, mmio_reg_read(identify_buffer.virt, ID_NN), ATOMIC_RELEASE);
|
||||
|
||||
uint8_t mdts = mmio_reg_read(identify_buffer.virt, ID_MDTS);
|
||||
if (mdts != 0 && mdts < sizeof(size_t) * 8)
|
||||
ctrl->mdts = ((size_t)1 << mdts) * ctrl->mps_min;
|
||||
|
||||
out:
|
||||
mutex_unlock(&ctrl->admin_lock);
|
||||
dma_buffer_free(&identify_buffer);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Scans all existing namespaces.
|
||||
errno_t nvme_ctrl_scan_namespaces(struct nvme_ctrl* ctrl, nvme_ns_vec_t* out) {
|
||||
vec_init(out);
|
||||
|
||||
uint32_t* ns_list = mem_alloc(NS_LIST_LEN * sizeof(uint32_t), 0);
|
||||
if (!ns_list)
|
||||
return ENOMEM;
|
||||
|
||||
struct dma_buffer identify_buffer;
|
||||
errno_t status = dma_buffer_alloc(ctrl->dma_tag, IDENTIFY_SIZE, &identify_buffer);
|
||||
if (status) {
|
||||
mem_free(ns_list);
|
||||
return ENOMEM;
|
||||
}
|
||||
|
||||
mutex_lock(&ctrl->admin_lock);
|
||||
struct nvme_queue* aq = ctrl->admin_queue;
|
||||
if (!aq) {
|
||||
status = ENXIO;
|
||||
goto out;
|
||||
}
|
||||
|
||||
uint32_t nsid = 0;
|
||||
while (true) {
|
||||
const struct nvme_identify_cmd admin_ident_cmd = {
|
||||
.buffer = identify_buffer.addr,
|
||||
.controller_id = 0,
|
||||
.cns = 2,
|
||||
.nsid = nsid,
|
||||
};
|
||||
status = identify(aq, &admin_ident_cmd);
|
||||
|
||||
if (status)
|
||||
goto out;
|
||||
|
||||
dma_buffer_sync(&identify_buffer, 0, IDENTIFY_SIZE, DMA_SYNC_POSTREAD);
|
||||
|
||||
// Parse the identifier list until we hit an namespace value of 0.
|
||||
size_t ns_list_len = 0;
|
||||
const uint32_t* raw_list = identify_buffer.virt;
|
||||
while (ns_list_len < NS_LIST_LEN && raw_list[ns_list_len] != 0) {
|
||||
ns_list[ns_list_len] = raw_list[ns_list_len];
|
||||
ns_list_len++;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ns_list_len; i++) {
|
||||
const uint32_t id = ns_list[i];
|
||||
const struct nvme_identify_cmd ns_ident_cmd = {
|
||||
.buffer = identify_buffer.addr,
|
||||
.controller_id = 0,
|
||||
.cns = 0,
|
||||
.nsid = id,
|
||||
};
|
||||
status = identify(aq, &ns_ident_cmd);
|
||||
|
||||
if (status)
|
||||
goto out;
|
||||
|
||||
dma_buffer_sync(&identify_buffer, 0, IDENTIFY_SIZE, DMA_SYNC_POSTREAD);
|
||||
|
||||
const uint64_t lba_count = mmio_reg_read(identify_buffer.virt, ID_NSZE);
|
||||
|
||||
// To get the block size of a namespace, we need to read a descriptor (see LBAF0-15).
|
||||
// Get the format descriptor index used by this namespace.
|
||||
const uint8_t lba_format_idx = mmio_field_get(ID_LBAF_IDX, mmio_reg_read(identify_buffer.virt, ID_FLBAS));
|
||||
|
||||
const mmio_reg32 lbaf = {ID_LBAF0.offset + lba_format_idx * sizeof(uint32_t), MMIO_LE};
|
||||
const uint32_t lba_format = mmio_reg_read(identify_buffer.virt, lbaf);
|
||||
|
||||
uint8_t lba_shift = mmio_field_get(ID_LBADS, lba_format);
|
||||
// If LBADS doesn't contain a non-zero value, we need to assume a default. Use 512 bytes here.
|
||||
if (lba_shift == 0)
|
||||
lba_shift = 9;
|
||||
|
||||
struct nvme_ns* ns;
|
||||
status = nvme_ns_new(ctrl, id, lba_shift, lba_count, nvme_ctrl_max_transfer_bytes(ctrl), &ns);
|
||||
if (status)
|
||||
goto out;
|
||||
status = vec_push(out, ns);
|
||||
if (status) {
|
||||
blockdev_unref(&ns->blk);
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no 0 in the buffer, there's more entries than can fit in `identify_buffer`.
|
||||
if (ns_list_len < NS_LIST_LEN)
|
||||
break;
|
||||
nsid = ns_list[ns_list_len - 1];
|
||||
}
|
||||
|
||||
out:
|
||||
mutex_unlock(&ctrl->admin_lock);
|
||||
dma_buffer_free(&identify_buffer);
|
||||
mem_free(ns_list);
|
||||
if (status) {
|
||||
vec_foreach(out, i) {
|
||||
blockdev_unref(&vec_elem_get(out, i)->blk);
|
||||
}
|
||||
vec_destroy(out);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
struct nvme_io_queue* nvme_ctrl_io_queue(struct nvme_ctrl* ctrl) {
|
||||
spin_lock(&ctrl->io_lock);
|
||||
struct nvme_io_queue* queue = ctrl->io_queue;
|
||||
spin_unlock(&ctrl->io_lock);
|
||||
return queue;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#define pr_fmt(fmt) "nvme: " fmt
|
||||
|
||||
#include <zinnia/fdt.h>
|
||||
#include <zinnia/module.h>
|
||||
#include <zinnia/pci.h>
|
||||
#include <zinnia/print.h>
|
||||
#include <zinnia/utils.h>
|
||||
#include <config.h>
|
||||
|
||||
#if CONFIG_NVME_PCI
|
||||
#include "nvme_pci.h"
|
||||
#endif
|
||||
|
||||
struct nvme_dev {
|
||||
#if CONFIG_NVME_PCI
|
||||
struct pci_device pci_dev;
|
||||
#endif
|
||||
};
|
||||
|
||||
errno_t nvme_init(struct module* mod) {
|
||||
errno_t status = 0;
|
||||
|
||||
#if CONFIG_NVME_PCI
|
||||
status = pci_driver_register(&nvme_driver);
|
||||
#endif
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
errno_t nvme_exit(struct module* mod) {
|
||||
// TODO: Unregister
|
||||
return 0;
|
||||
}
|
||||
|
||||
MODULE_ENTRY(nvme_init);
|
||||
MODULE_EXIT(nvme_exit);
|
||||
|
||||
MODULE_DESC("NVMe Block Devices");
|
||||
MODULE_AUTHOR("Marvin Friedrich");
|
||||
MODULE_LICENSE("ISC");
|
||||
MODULE_VERSION("1.0");
|
||||
@@ -1,12 +1,13 @@
|
||||
nvme_sources = files('core.c')
|
||||
|
||||
if kconfig.get('CONFIG_NVME_PCI', 'n') == 'y'
|
||||
nvme_sources += files('pci.c')
|
||||
endif
|
||||
|
||||
modules += {
|
||||
'nvme': {
|
||||
'modular': kconfig.get('CONFIG_NVME_CORE') == 'm',
|
||||
'sources': nvme_sources,
|
||||
'modular': kconfig.get('CONFIG_NVME') == 'm',
|
||||
'sources': files(
|
||||
'command.c',
|
||||
'controller.c',
|
||||
'namespace.c',
|
||||
'pci.c',
|
||||
'prp.c',
|
||||
'queue.c',
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#define pr_fmt(fmt) "nvme: " fmt
|
||||
|
||||
#include <zinnia/alloc.h>
|
||||
#include <zinnia/clock.h>
|
||||
#include <zinnia/print.h>
|
||||
#include <zinnia/utils.h>
|
||||
#include <stdckdint.h>
|
||||
#include "nvme.h"
|
||||
|
||||
static errno_t nvme_ns_submit(struct blockdev* blk, struct bio* bio) {
|
||||
struct nvme_ns* ns = containerof(blk, struct nvme_ns, blk);
|
||||
|
||||
if (bio->op != BIO_FLUSH) {
|
||||
uint64_t end_lba;
|
||||
if (ckd_add(&end_lba, bio->lba, (uint64_t)bio->num_lbas)) {
|
||||
bio_complete(bio, EOVERFLOW, 0);
|
||||
return 0;
|
||||
}
|
||||
if (end_lba > ns->lba_count) {
|
||||
bio_complete(bio, bio->op == BIO_READ ? 0 : ENOSPC, 0);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
struct nvme_io_queue* queue = nvme_ctrl_io_queue(ns->ctrl);
|
||||
if (!queue) {
|
||||
bio_complete(bio, EIO, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t bytes = 0;
|
||||
for (size_t i = 0; i < bio->num_segs; i++)
|
||||
bytes += bio->segs[i].len;
|
||||
|
||||
_Atomic(bool) done = false;
|
||||
const bool poll = queue->poll;
|
||||
|
||||
uint16_t cid = nvme_io_queue_submit(
|
||||
queue,
|
||||
bio,
|
||||
(struct nvme_rw_cmd){
|
||||
.prp1 = 0,
|
||||
.prp2 = 0,
|
||||
.cid = 0,
|
||||
.do_write = bio->op == BIO_WRITE,
|
||||
.start_lba = bio->lba,
|
||||
.num_lbas = bio->num_lbas,
|
||||
.bytes = bytes,
|
||||
.control = 0,
|
||||
.ds_mgmt = 0,
|
||||
.ref_tag = 0,
|
||||
.app_tag = 0,
|
||||
.app_mask = 0,
|
||||
.nsid = ns->nsid,
|
||||
},
|
||||
poll ? &done : nullptr
|
||||
);
|
||||
|
||||
// If we're not interrupt-driven, poll the IO completion queue.
|
||||
if (poll) {
|
||||
const uint64_t deadline = clock_get_elapsed_ns() + NVME_COMPLETION_TIMEOUT_NS;
|
||||
while (!atomic_load(&done, ATOMIC_ACQUIRE)) {
|
||||
nvme_io_queue_drain(queue);
|
||||
if (atomic_load(&done, ATOMIC_ACQUIRE))
|
||||
break;
|
||||
if (clock_get_elapsed_ns() >= deadline && nvme_io_queue_abandon(queue, cid, bio)) {
|
||||
bio_complete(bio, EIO, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void nvme_ns_destroy(struct blockdev* blk) {
|
||||
mem_free(containerof(blk, struct nvme_ns, blk));
|
||||
}
|
||||
|
||||
static const struct blockdev_ops nvme_ns_ops = {
|
||||
.submit = nvme_ns_submit,
|
||||
.destroy = nvme_ns_destroy,
|
||||
};
|
||||
|
||||
errno_t nvme_ns_new(
|
||||
struct nvme_ctrl* ctrl,
|
||||
uint32_t nsid,
|
||||
uint8_t lba_shift,
|
||||
uint64_t lba_count,
|
||||
size_t max_transfer_bytes,
|
||||
struct nvme_ns** out
|
||||
) {
|
||||
log("Namespace %u: LBA size %zu bytes, %zu MBs total\n",
|
||||
nsid,
|
||||
(size_t)1 << lba_shift,
|
||||
(size_t)(lba_count << lba_shift) / 1024 / 1024);
|
||||
|
||||
struct nvme_ns* ns = mem_alloc(sizeof(*ns), 0);
|
||||
if (!ns)
|
||||
return ENOMEM;
|
||||
|
||||
blockdev_init(&ns->blk, &nvme_ns_ops, NVME_MAJOR, nsid, (size_t)1 << lba_shift, lba_count);
|
||||
ns->blk.limits = (struct blockdev_limits){
|
||||
.max_lbas = MAX(max_transfer_bytes >> lba_shift, 1),
|
||||
.max_segs = 512,
|
||||
};
|
||||
ns->ctrl = ctrl;
|
||||
ns->nsid = nsid;
|
||||
ns->lba_shift = lba_shift;
|
||||
ns->lba_count = lba_count;
|
||||
ns->max_transfer_bytes = max_transfer_bytes;
|
||||
|
||||
*out = ns;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zinnia/atomic.h>
|
||||
#include <zinnia/blockdev.h>
|
||||
#include <zinnia/dma.h>
|
||||
#include <zinnia/errno.h>
|
||||
#include <zinnia/event.h>
|
||||
#include <zinnia/mutex.h>
|
||||
#include <zinnia/pci.h>
|
||||
#include <zinnia/spin.h>
|
||||
#include <zinnia/vec.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "spec.h"
|
||||
|
||||
constexpr uint32_t NVME_MAJOR = 159;
|
||||
constexpr uint64_t NVME_COMPLETION_TIMEOUT_NS = 30'000'000'000;
|
||||
|
||||
// An entry in the completion queue.
|
||||
struct nvme_completion {
|
||||
uint32_t result;
|
||||
uint16_t sq_head;
|
||||
uint16_t sq_id;
|
||||
uint16_t cmd_id;
|
||||
bool phase_tag;
|
||||
uint16_t status;
|
||||
};
|
||||
|
||||
static inline bool nvme_status_is_success(uint16_t status) {
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
struct nvme_rw_cmd {
|
||||
uint64_t prp1;
|
||||
uint64_t prp2;
|
||||
uint16_t cid;
|
||||
bool do_write;
|
||||
uint64_t start_lba;
|
||||
size_t num_lbas;
|
||||
size_t bytes;
|
||||
uint16_t control;
|
||||
uint32_t ds_mgmt;
|
||||
uint32_t ref_tag;
|
||||
uint16_t app_tag;
|
||||
uint16_t app_mask;
|
||||
uint32_t nsid;
|
||||
};
|
||||
|
||||
struct nvme_flush_cmd {
|
||||
uint16_t cid;
|
||||
uint32_t nsid;
|
||||
};
|
||||
|
||||
struct nvme_create_cq_cmd {
|
||||
dma_addr_t cq_addr;
|
||||
size_t depth;
|
||||
size_t qid;
|
||||
bool irqs_enabled;
|
||||
uint16_t irq_vector;
|
||||
};
|
||||
|
||||
struct nvme_create_sq_cmd {
|
||||
dma_addr_t sq_addr;
|
||||
size_t depth;
|
||||
size_t qid;
|
||||
size_t cqid;
|
||||
};
|
||||
|
||||
struct nvme_identify_cmd {
|
||||
dma_addr_t buffer;
|
||||
uint16_t controller_id;
|
||||
uint8_t cns;
|
||||
uint32_t nsid;
|
||||
};
|
||||
|
||||
void nvme_rw_cmd_write(const struct nvme_rw_cmd* cmd, struct nvme_sqe* sqe);
|
||||
void nvme_flush_cmd_write(const struct nvme_flush_cmd* cmd, struct nvme_sqe* sqe);
|
||||
void nvme_create_cq_cmd_write(const struct nvme_create_cq_cmd* cmd, struct nvme_sqe* sqe);
|
||||
void nvme_create_sq_cmd_write(const struct nvme_create_sq_cmd* cmd, struct nvme_sqe* sqe);
|
||||
void nvme_identify_cmd_write(const struct nvme_identify_cmd* cmd, struct nvme_sqe* sqe);
|
||||
|
||||
struct nvme_queue {
|
||||
// Amount of queue entries.
|
||||
size_t depth;
|
||||
size_t doorbells_offset;
|
||||
size_t doorbell_stride;
|
||||
void* regs;
|
||||
// Physical buffer for the completion queue.
|
||||
struct dma_buffer cq;
|
||||
// The index of the current completion queue entry.
|
||||
size_t cq_head;
|
||||
// Determines whether a completion queue entry is new.
|
||||
uint8_t cq_phase;
|
||||
// Physical buffer for the submission queue.
|
||||
struct dma_buffer sq;
|
||||
// The index of the current submission queue entry.
|
||||
size_t sq_tail;
|
||||
};
|
||||
|
||||
errno_t nvme_queue_new(
|
||||
struct dma_tag* tag,
|
||||
void* regs,
|
||||
size_t doorbell_stride,
|
||||
size_t queue_id,
|
||||
size_t depth,
|
||||
struct nvme_queue** out
|
||||
);
|
||||
void nvme_queue_free(struct nvme_queue* queue);
|
||||
errno_t nvme_queue_submit_cmd(struct nvme_queue* queue, const struct nvme_sqe* sqe);
|
||||
errno_t nvme_queue_next_completion(struct nvme_queue* queue, struct nvme_completion* out);
|
||||
|
||||
struct nvme_inflight {
|
||||
bool active;
|
||||
struct bio* bio;
|
||||
size_t lbas;
|
||||
struct dma_map* map;
|
||||
struct dma_buffer prps;
|
||||
_Atomic(bool)* done;
|
||||
};
|
||||
|
||||
struct nvme_io_queue {
|
||||
size_t queue_id;
|
||||
size_t depth;
|
||||
size_t doorbells_offset;
|
||||
size_t doorbell_stride;
|
||||
void* regs;
|
||||
bool poll;
|
||||
struct dma_tag* tag;
|
||||
struct dma_buffer cq;
|
||||
struct dma_buffer sq;
|
||||
struct spinlock sq_lock;
|
||||
size_t sq_tail;
|
||||
struct spinlock cq_lock;
|
||||
size_t cq_head;
|
||||
uint8_t cq_phase;
|
||||
struct spinlock cmds_lock;
|
||||
struct nvme_inflight* slots;
|
||||
uint16_t* free;
|
||||
size_t num_free;
|
||||
struct event slots_free;
|
||||
};
|
||||
|
||||
errno_t nvme_io_queue_new(
|
||||
struct dma_tag* tag,
|
||||
void* regs,
|
||||
size_t doorbell_stride,
|
||||
size_t queue_id,
|
||||
size_t depth,
|
||||
bool poll,
|
||||
struct nvme_io_queue** out
|
||||
);
|
||||
void nvme_io_queue_free(struct nvme_io_queue* queue);
|
||||
uint16_t nvme_io_queue_submit(
|
||||
struct nvme_io_queue* queue,
|
||||
struct bio* bio,
|
||||
struct nvme_rw_cmd cmd,
|
||||
_Atomic(bool)* done
|
||||
);
|
||||
bool nvme_io_queue_abandon(struct nvme_io_queue* queue, uint16_t cid, struct bio* bio);
|
||||
size_t nvme_io_queue_drain(struct nvme_io_queue* queue);
|
||||
|
||||
errno_t nvme_build_prps(
|
||||
struct dma_tag* tag,
|
||||
struct bio* bio,
|
||||
struct nvme_inflight* slot,
|
||||
uint64_t* prp1,
|
||||
uint64_t* prp2
|
||||
);
|
||||
void nvme_release_prps(struct nvme_inflight* slot);
|
||||
|
||||
struct nvme_ns;
|
||||
|
||||
struct nvme_ctrl {
|
||||
void* regs;
|
||||
struct dma_tag* dma_tag;
|
||||
size_t doorbell_stride; // The amount of bytes between two doorbells.
|
||||
size_t mps_min;
|
||||
uint8_t mps;
|
||||
|
||||
char serial[20 + 1]; // Serial number of the controller.
|
||||
char model[40 + 1]; // Model name of the controller.
|
||||
char revision[8 + 1]; // Revision of the controller's firmware.
|
||||
|
||||
struct nvme_queue* admin_queue; // Queue to submit admin commands to.
|
||||
mutex_t admin_lock;
|
||||
|
||||
struct nvme_io_queue* io_queue; // Queue to submit IO commands to.
|
||||
struct spinlock io_lock;
|
||||
|
||||
_Atomic(uint32_t) ns_count; // Amount of namespaces.
|
||||
size_t mdts; // Maximum data transfer size.
|
||||
};
|
||||
|
||||
typedef vec(struct nvme_ns*) nvme_ns_vec_t;
|
||||
|
||||
errno_t nvme_ctrl_new_pci(void* regs, struct dma_tag* dma_tag, struct nvme_ctrl** out);
|
||||
size_t nvme_ctrl_max_transfer_bytes(struct nvme_ctrl* ctrl);
|
||||
errno_t nvme_ctrl_reset(struct nvme_ctrl* ctrl, bool irqs_enabled, uint16_t io_irq_vector);
|
||||
errno_t nvme_ctrl_identify(struct nvme_ctrl* ctrl);
|
||||
errno_t nvme_ctrl_scan_namespaces(struct nvme_ctrl* ctrl, nvme_ns_vec_t* out);
|
||||
struct nvme_io_queue* nvme_ctrl_io_queue(struct nvme_ctrl* ctrl);
|
||||
|
||||
struct nvme_ns {
|
||||
struct blockdev blk;
|
||||
struct nvme_ctrl* ctrl;
|
||||
uint32_t nsid;
|
||||
uint8_t lba_shift;
|
||||
uint64_t lba_count;
|
||||
size_t max_transfer_bytes;
|
||||
};
|
||||
|
||||
errno_t nvme_ns_new(
|
||||
struct nvme_ctrl* ctrl,
|
||||
uint32_t nsid,
|
||||
uint8_t lba_shift,
|
||||
uint64_t lba_count,
|
||||
size_t max_transfer_bytes,
|
||||
struct nvme_ns** out
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#pragma once
|
||||
|
||||
extern struct pci_driver nvme_driver;
|
||||
+136
-4
@@ -1,13 +1,128 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#define pr_fmt(fmt) "nvme: " fmt
|
||||
|
||||
#include <zinnia/alloc.h>
|
||||
#include <zinnia/atomic.h>
|
||||
#include <zinnia/errno.h>
|
||||
#include <zinnia/irq.h>
|
||||
#include <zinnia/module.h>
|
||||
#include <zinnia/pci.h>
|
||||
#include <zinnia/pmap.h>
|
||||
#include <zinnia/print.h>
|
||||
#include <zinnia/utils.h>
|
||||
#include <zinnia/virt.h>
|
||||
#include <zinnia/vma.h>
|
||||
#include <arch/mem.h>
|
||||
#include "nvme.h"
|
||||
|
||||
static _Atomic(size_t) nvme_counter;
|
||||
|
||||
struct nvme_irq_handler {
|
||||
struct irq_handler handler;
|
||||
struct nvme_ctrl* controller;
|
||||
};
|
||||
|
||||
static enum irq_status nvme_irq_raise(struct irq_handler* self) {
|
||||
struct nvme_irq_handler* handler = containerof(self, struct nvme_irq_handler, handler);
|
||||
struct nvme_io_queue* queue = nvme_ctrl_io_queue(handler->controller);
|
||||
if (queue)
|
||||
nvme_io_queue_drain(queue);
|
||||
return IRQ_STATUS_HANDLED;
|
||||
}
|
||||
|
||||
static errno_t map_bar(struct pci_bar* bar, void** out) {
|
||||
const size_t page_size = arch_mem_page_size();
|
||||
const size_t len = ALIGN_UP(bar->size, page_size);
|
||||
uintptr_t vaddr = vm_alloc(len);
|
||||
if (!vaddr)
|
||||
return ENOMEM;
|
||||
|
||||
for (size_t i = 0; i < len; i += page_size) {
|
||||
errno_t status = pmap_map(&kernel_vma.pmap, vaddr + i, bar->address + i, PTE_READ | PTE_WRITE, CACHE_MMIO);
|
||||
if (status)
|
||||
return status;
|
||||
}
|
||||
|
||||
*out = (void*)vaddr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static errno_t nvme_pci_probe(struct pci_device* dev, const struct pci_match* match) {
|
||||
pci_log(dev, "Detected controller \n");
|
||||
pci_log(dev, "Probing NVMe device\n");
|
||||
|
||||
return 0;
|
||||
// Enable MMIO decoding and DMA. We only support MSI-X or polling, so keep legacy INTx disabled.
|
||||
uint16_t cmd = pci_read16(dev, PCI_COMMAND);
|
||||
pci_write16(dev, PCI_COMMAND, cmd | PCI_COMMAND_MEM | PCI_COMMAND_MASTER | PCI_COMMAND_INTX_DISABLE);
|
||||
|
||||
struct irq_line* irq_line = nullptr;
|
||||
if (pci_setup_msix(dev, &irq_line)) {
|
||||
pci_log_warn(dev, "NVMe MSI-X setup failed, falling back to polling completions\n");
|
||||
irq_line = nullptr;
|
||||
}
|
||||
|
||||
struct pci_bar bar;
|
||||
errno_t status = pci_bar_get(dev, 0, &bar);
|
||||
if (status)
|
||||
return ENXIO;
|
||||
if (bar.type != PCI_BAR_MMIO32 && bar.type != PCI_BAR_MMIO64)
|
||||
return ENXIO;
|
||||
|
||||
void* regs;
|
||||
status = map_bar(&bar, ®s);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
struct nvme_ctrl* controller;
|
||||
status = nvme_ctrl_new_pci(regs, dev->dma_tag, &controller);
|
||||
if (status) {
|
||||
pci_log_err(dev, "Failed to probe controller: %i\n", status);
|
||||
return ENODEV;
|
||||
}
|
||||
|
||||
if (irq_line) {
|
||||
struct nvme_irq_handler* handler = mem_alloc(sizeof(*handler), 0);
|
||||
if (!handler)
|
||||
return ENOMEM;
|
||||
handler->handler.raise = nvme_irq_raise;
|
||||
handler->controller = controller;
|
||||
irq_line_attach(irq_line, &handler->handler);
|
||||
if (irq_line->unmask)
|
||||
irq_line->unmask(irq_line);
|
||||
}
|
||||
|
||||
// Reset the controller to initialize all queues and other structures.
|
||||
status = nvme_ctrl_reset(controller, irq_line != nullptr, 0);
|
||||
if (status) {
|
||||
pci_log_err(dev, "Failed to reset controller: %i\n", status);
|
||||
return ENODEV;
|
||||
}
|
||||
|
||||
status = nvme_ctrl_identify(controller);
|
||||
if (status) {
|
||||
pci_log_err(dev, "Failed to identify controller: %i\n", status);
|
||||
return ENODEV;
|
||||
}
|
||||
|
||||
nvme_ns_vec_t namespaces;
|
||||
status = nvme_ctrl_scan_namespaces(controller, &namespaces);
|
||||
if (status) {
|
||||
pci_log_err(dev, "Failed to identify controller: %i\n", status);
|
||||
return ENODEV;
|
||||
}
|
||||
|
||||
const size_t nvme_id = atomic_fetch_add(&nvme_counter, 1, ATOMIC_SEQ_CST);
|
||||
vec_foreach(&namespaces, i) {
|
||||
struct nvme_ns* ns = vec_elem_get(&namespaces, i);
|
||||
char path[32];
|
||||
ksnprintf(path, sizeof(path), "nvme%zun%u", nvme_id, ns->nsid);
|
||||
status = blockdev_register(path, &ns->blk);
|
||||
if (status)
|
||||
break;
|
||||
}
|
||||
vec_destroy(&namespaces);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
static const struct pci_match nvme_match[] = {
|
||||
@@ -17,9 +132,26 @@ static const struct pci_match nvme_match[] = {
|
||||
|
||||
MATCH_TABLE("pci", nvme_match);
|
||||
|
||||
struct pci_driver nvme_driver = {
|
||||
.name = "nvme_pci",
|
||||
static struct pci_driver nvme_driver = {
|
||||
.name = "nvme",
|
||||
.match = nvme_match,
|
||||
.num_matches = ARRAY_SIZE(nvme_match),
|
||||
.probe = nvme_pci_probe,
|
||||
};
|
||||
|
||||
errno_t nvme_init(struct module* mod) {
|
||||
return pci_driver_register(&nvme_driver);
|
||||
}
|
||||
|
||||
errno_t nvme_exit(struct module* mod) {
|
||||
// TODO: Unregister
|
||||
return 0;
|
||||
}
|
||||
|
||||
MODULE_ENTRY(nvme_init);
|
||||
MODULE_EXIT(nvme_exit);
|
||||
|
||||
MODULE_DESC("NVMe Block Devices");
|
||||
MODULE_AUTHOR("Marvin Friedrich");
|
||||
MODULE_LICENSE("ISC");
|
||||
MODULE_VERSION("1.0");
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#include <zinnia/utils.h>
|
||||
#include <arch/mem.h>
|
||||
#include "nvme.h"
|
||||
|
||||
static size_t count_pages(struct dma_map* map, size_t page_size) {
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < map->num_segs; i++) {
|
||||
const struct dma_seg* seg = &map->segs[i];
|
||||
if (seg->len == 0)
|
||||
continue;
|
||||
const dma_addr_t end = seg->addr + seg->len;
|
||||
for (dma_addr_t page = ALIGN_DOWN(seg->addr, page_size); page < end; page += page_size)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void fill_pages(struct dma_map* map, size_t page_size, uint64_t* out) {
|
||||
bool first_page = true;
|
||||
size_t idx = 0;
|
||||
for (size_t i = 0; i < map->num_segs; i++) {
|
||||
const struct dma_seg* seg = &map->segs[i];
|
||||
if (seg->len == 0)
|
||||
continue;
|
||||
const dma_addr_t end = seg->addr + seg->len;
|
||||
for (dma_addr_t page = ALIGN_DOWN(seg->addr, page_size); page < end; page += page_size) {
|
||||
if (first_page)
|
||||
first_page = false;
|
||||
else
|
||||
out[idx++] = page;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errno_t nvme_build_prps(
|
||||
struct dma_tag* tag,
|
||||
struct bio* bio,
|
||||
struct nvme_inflight* slot,
|
||||
uint64_t* prp1,
|
||||
uint64_t* prp2
|
||||
) {
|
||||
const size_t page_size = arch_mem_page_size();
|
||||
slot->map = nullptr;
|
||||
slot->prps = (struct dma_buffer){};
|
||||
|
||||
if (bio->num_segs == 0)
|
||||
return EIO;
|
||||
|
||||
errno_t status = dma_map_create(tag, SIZE_MAX, bio->num_segs, SIZE_MAX, 0, &slot->map);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
status = dma_map_load_bio(slot->map, bio);
|
||||
if (status)
|
||||
goto fail;
|
||||
if (slot->map->num_segs == 0) {
|
||||
status = EIO;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
dma_map_sync(slot->map, 0, slot->map->mapped_len, bio->op == BIO_WRITE ? DMA_SYNC_PREWRITE : DMA_SYNC_PREREAD);
|
||||
|
||||
*prp1 = slot->map->segs[0].addr;
|
||||
|
||||
// Enumerate the base of every page the transfer touches in order.
|
||||
const size_t pages = count_pages(slot->map, page_size);
|
||||
const size_t rest_pages = pages ? pages - 1 : 0;
|
||||
|
||||
if (rest_pages == 0) {
|
||||
*prp2 = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (rest_pages == 1) {
|
||||
uint64_t second;
|
||||
fill_pages(slot->map, page_size, &second);
|
||||
*prp2 = second;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We don't want to chain across pages.
|
||||
const size_t entries_per_page = page_size / sizeof(uint64_t);
|
||||
if (rest_pages > entries_per_page) {
|
||||
status = ENOMEM;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
status = dma_buffer_alloc(tag, page_size, &slot->prps);
|
||||
if (status)
|
||||
goto fail;
|
||||
|
||||
fill_pages(slot->map, page_size, slot->prps.virt);
|
||||
dma_buffer_sync(&slot->prps, 0, rest_pages * sizeof(uint64_t), DMA_SYNC_PREWRITE);
|
||||
|
||||
*prp2 = slot->prps.addr;
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
nvme_release_prps(slot);
|
||||
return status;
|
||||
}
|
||||
|
||||
void nvme_release_prps(struct nvme_inflight* slot) {
|
||||
if (slot->map) {
|
||||
dma_map_sync(
|
||||
slot->map,
|
||||
0,
|
||||
slot->map->mapped_len,
|
||||
slot->bio && slot->bio->op == BIO_WRITE ? DMA_SYNC_POSTWRITE : DMA_SYNC_POSTREAD
|
||||
);
|
||||
dma_map_unload(slot->map);
|
||||
dma_map_destroy(slot->map);
|
||||
slot->map = nullptr;
|
||||
}
|
||||
dma_buffer_free(&slot->prps);
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#define pr_fmt(fmt) "nvme: " fmt
|
||||
|
||||
#include <zinnia/alloc.h>
|
||||
#include <zinnia/clock.h>
|
||||
#include <zinnia/mmio.h>
|
||||
#include <zinnia/print.h>
|
||||
#include <zinnia/utils.h>
|
||||
#include <string.h>
|
||||
#include "nvme.h"
|
||||
|
||||
constexpr size_t DOORBELL_OFFSET = 0x1000;
|
||||
|
||||
static void ring_tail(void* regs, size_t doorbells_offset, size_t tail) {
|
||||
mmio_reg_write(regs, ((mmio_reg32){doorbells_offset, MMIO_LE}), (uint32_t)tail);
|
||||
}
|
||||
|
||||
static void ring_head(void* regs, size_t doorbells_offset, size_t doorbell_stride, size_t head) {
|
||||
mmio_reg_write(regs, ((mmio_reg32){doorbells_offset + doorbell_stride, MMIO_LE}), (uint32_t)head);
|
||||
}
|
||||
|
||||
// Creates a new submission and completion queue pair.
|
||||
errno_t nvme_queue_new(
|
||||
struct dma_tag* tag,
|
||||
void* regs,
|
||||
size_t doorbell_stride,
|
||||
size_t queue_id,
|
||||
size_t depth,
|
||||
struct nvme_queue** out
|
||||
) {
|
||||
const size_t align = 0x1000;
|
||||
const size_t sq_size = ALIGN_UP(depth * sizeof(struct nvme_sqe), align);
|
||||
const size_t cq_size = ALIGN_UP(depth * sizeof(struct nvme_cqe), align);
|
||||
|
||||
struct nvme_queue* queue = mem_alloc(sizeof(*queue), 0);
|
||||
if (!queue)
|
||||
return ENOMEM;
|
||||
|
||||
// Allocate memory the completion queue.
|
||||
errno_t status = dma_buffer_alloc(tag, cq_size, &queue->cq);
|
||||
if (status)
|
||||
goto fail;
|
||||
|
||||
// Allocate memory for the submission queue.
|
||||
status = dma_buffer_alloc(tag, sq_size, &queue->sq);
|
||||
if (status)
|
||||
goto fail;
|
||||
|
||||
// Calculate the offset of the doorbell registers. The stride is already precomputed here.
|
||||
queue->doorbells_offset = DOORBELL_OFFSET + (queue_id * 2 * doorbell_stride);
|
||||
queue->doorbell_stride = doorbell_stride;
|
||||
queue->depth = depth;
|
||||
queue->regs = regs;
|
||||
queue->cq_head = 0;
|
||||
queue->cq_phase = 1; // When the controller is enabled, the first phase is 1.
|
||||
queue->sq_tail = 0;
|
||||
|
||||
*out = queue;
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
nvme_queue_free(queue);
|
||||
return status;
|
||||
}
|
||||
|
||||
void nvme_queue_free(struct nvme_queue* queue) {
|
||||
dma_buffer_free(&queue->cq);
|
||||
dma_buffer_free(&queue->sq);
|
||||
mem_free(queue);
|
||||
}
|
||||
|
||||
// Submits a command to this queue.
|
||||
errno_t nvme_queue_submit_cmd(struct nvme_queue* queue, const struct nvme_sqe* sqe) {
|
||||
struct nvme_sqe* entries = queue->sq.virt;
|
||||
memcpy(&entries[queue->sq_tail], sqe, sizeof(*sqe));
|
||||
dma_buffer_sync(&queue->sq, queue->sq_tail * sizeof(*sqe), sizeof(*sqe), DMA_SYNC_PREWRITE);
|
||||
|
||||
queue->sq_tail++;
|
||||
if (queue->sq_tail == queue->depth)
|
||||
queue->sq_tail = 0;
|
||||
|
||||
// Notify the controller of the new tail index.
|
||||
ring_tail(queue->regs, queue->doorbells_offset, queue->sq_tail);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Reads the next completion entry from the queue.
|
||||
errno_t nvme_queue_next_completion(struct nvme_queue* queue, struct nvme_completion* out) {
|
||||
struct nvme_cqe* entry = &((struct nvme_cqe*)queue->cq.virt)[queue->cq_head];
|
||||
|
||||
// Wait until the phase for this entry has changed.
|
||||
uint32_t dw3;
|
||||
const uint64_t deadline = clock_get_elapsed_ns() + NVME_COMPLETION_TIMEOUT_NS;
|
||||
while (true) {
|
||||
dma_buffer_sync(&queue->cq, queue->cq_head * sizeof(*entry), sizeof(*entry), DMA_SYNC_POSTREAD);
|
||||
dw3 = mmio_reg_read(entry, NVME_CQE_DW3);
|
||||
|
||||
// The controller will flip the phase bit of the current entry when writing.
|
||||
if (mmio_field_get(NVME_CQE_PHASE_TAG, dw3) == queue->cq_phase)
|
||||
break;
|
||||
|
||||
if (clock_get_elapsed_ns() >= deadline)
|
||||
return ETIMEDOUT;
|
||||
}
|
||||
|
||||
// Then, read the rest of the completion queue entry.
|
||||
uint32_t dw0 = mmio_reg_read(entry, NVME_CQE_DW0);
|
||||
uint32_t dw2 = mmio_reg_read(entry, NVME_CQE_DW2);
|
||||
|
||||
*out = (struct nvme_completion){
|
||||
.result = dw0,
|
||||
.sq_head = mmio_field_get(NVME_CQE_SQ_HEAD, dw2),
|
||||
.sq_id = mmio_field_get(NVME_CQE_SQ_IDENT, dw2),
|
||||
.cmd_id = mmio_field_get(NVME_CQE_CID, dw3),
|
||||
.status = mmio_field_get(NVME_CQE_STATUS, dw3),
|
||||
.phase_tag = mmio_field_get(NVME_CQE_PHASE_TAG, dw3) != 0,
|
||||
};
|
||||
|
||||
queue->cq_head++;
|
||||
if (queue->cq_head == queue->depth) {
|
||||
queue->cq_head = 0;
|
||||
queue->cq_phase ^= 1; // Flip the phase bit.
|
||||
}
|
||||
|
||||
// Notify the controller of the new head index.
|
||||
ring_head(queue->regs, queue->doorbells_offset, queue->doorbell_stride, queue->cq_head);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
errno_t nvme_io_queue_new(
|
||||
struct dma_tag* tag,
|
||||
void* regs,
|
||||
size_t doorbell_stride,
|
||||
size_t queue_id,
|
||||
size_t depth,
|
||||
bool poll,
|
||||
struct nvme_io_queue** out
|
||||
) {
|
||||
const size_t align = 0x1000;
|
||||
const size_t sq_size = ALIGN_UP(depth * sizeof(struct nvme_sqe), align);
|
||||
const size_t cq_size = ALIGN_UP(depth * sizeof(struct nvme_cqe), align);
|
||||
|
||||
struct nvme_io_queue* queue = mem_alloc(sizeof(*queue), 0);
|
||||
if (!queue)
|
||||
return ENOMEM;
|
||||
|
||||
errno_t status = dma_buffer_alloc(tag, cq_size, &queue->cq);
|
||||
if (status)
|
||||
goto fail;
|
||||
|
||||
status = dma_buffer_alloc(tag, sq_size, &queue->sq);
|
||||
if (status)
|
||||
goto fail;
|
||||
|
||||
const size_t slots = depth - 1;
|
||||
queue->slots = mem_alloc(slots * sizeof(*queue->slots), 0);
|
||||
queue->free = mem_alloc(slots * sizeof(*queue->free), 0);
|
||||
if (!queue->slots || !queue->free) {
|
||||
status = ENOMEM;
|
||||
goto fail;
|
||||
}
|
||||
for (size_t i = 0; i < slots; i++)
|
||||
queue->free[i] = slots - 1 - i;
|
||||
queue->num_free = slots;
|
||||
|
||||
queue->queue_id = queue_id;
|
||||
queue->depth = depth;
|
||||
queue->doorbells_offset = DOORBELL_OFFSET + (queue_id * 2 * doorbell_stride);
|
||||
queue->doorbell_stride = doorbell_stride;
|
||||
queue->regs = regs;
|
||||
queue->poll = poll;
|
||||
queue->tag = tag;
|
||||
queue->sq_tail = 0;
|
||||
queue->cq_head = 0;
|
||||
queue->cq_phase = 1;
|
||||
spin_init(&queue->sq_lock);
|
||||
spin_init(&queue->cq_lock);
|
||||
spin_init(&queue->cmds_lock);
|
||||
event_init(&queue->slots_free);
|
||||
|
||||
*out = queue;
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
nvme_io_queue_free(queue);
|
||||
return status;
|
||||
}
|
||||
|
||||
void nvme_io_queue_free(struct nvme_io_queue* queue) {
|
||||
dma_buffer_free(&queue->cq);
|
||||
dma_buffer_free(&queue->sq);
|
||||
mem_free(queue->slots);
|
||||
mem_free(queue->free);
|
||||
mem_free(queue);
|
||||
}
|
||||
|
||||
static uint16_t acquire_slot(struct nvme_io_queue* queue) {
|
||||
while (true) {
|
||||
spin_lock(&queue->cmds_lock);
|
||||
if (queue->num_free) {
|
||||
uint16_t cid = queue->free[--queue->num_free];
|
||||
spin_unlock(&queue->cmds_lock);
|
||||
return cid;
|
||||
}
|
||||
spin_unlock(&queue->cmds_lock);
|
||||
|
||||
if (queue->poll) {
|
||||
nvme_io_queue_drain(queue);
|
||||
continue;
|
||||
}
|
||||
|
||||
nvme_io_queue_drain(queue);
|
||||
event_reset(&queue->slots_free);
|
||||
spin_lock(&queue->cmds_lock);
|
||||
bool empty = queue->num_free == 0;
|
||||
spin_unlock(&queue->cmds_lock);
|
||||
if (empty)
|
||||
event_wait(&queue->slots_free);
|
||||
}
|
||||
}
|
||||
|
||||
static void release_slot(struct nvme_io_queue* queue, uint16_t cid) {
|
||||
spin_lock(&queue->cmds_lock);
|
||||
queue->free[queue->num_free++] = cid;
|
||||
spin_unlock(&queue->cmds_lock);
|
||||
}
|
||||
|
||||
static void write_sqe(struct nvme_io_queue* queue, const struct nvme_sqe* sqe) {
|
||||
spin_lock(&queue->sq_lock);
|
||||
struct nvme_sqe* entries = queue->sq.virt;
|
||||
memcpy(&entries[queue->sq_tail], sqe, sizeof(*sqe));
|
||||
dma_buffer_sync(&queue->sq, queue->sq_tail * sizeof(*sqe), sizeof(*sqe), DMA_SYNC_PREWRITE);
|
||||
queue->sq_tail++;
|
||||
if (queue->sq_tail == queue->depth)
|
||||
queue->sq_tail = 0;
|
||||
ring_tail(queue->regs, queue->doorbells_offset, queue->sq_tail);
|
||||
spin_unlock(&queue->sq_lock);
|
||||
}
|
||||
|
||||
uint16_t nvme_io_queue_submit(
|
||||
struct nvme_io_queue* queue,
|
||||
struct bio* bio,
|
||||
struct nvme_rw_cmd cmd,
|
||||
_Atomic(bool)* done
|
||||
) {
|
||||
const uint16_t cid = acquire_slot(queue);
|
||||
struct nvme_inflight* slot = &queue->slots[cid];
|
||||
|
||||
struct nvme_sqe sqe = {};
|
||||
if (bio->op == BIO_FLUSH) {
|
||||
slot->map = nullptr;
|
||||
slot->prps = (struct dma_buffer){};
|
||||
nvme_flush_cmd_write(&(struct nvme_flush_cmd){.cid = cid, .nsid = cmd.nsid}, &sqe);
|
||||
} else {
|
||||
errno_t status = nvme_build_prps(queue->tag, bio, slot, &cmd.prp1, &cmd.prp2);
|
||||
if (status) {
|
||||
release_slot(queue, cid);
|
||||
bio_complete(bio, EIO, 0);
|
||||
if (done)
|
||||
atomic_store(done, true, ATOMIC_RELEASE);
|
||||
return cid;
|
||||
}
|
||||
cmd.cid = cid;
|
||||
nvme_rw_cmd_write(&cmd, &sqe);
|
||||
}
|
||||
|
||||
spin_lock(&queue->cmds_lock);
|
||||
slot->active = true;
|
||||
slot->bio = bio;
|
||||
slot->lbas = bio->num_lbas;
|
||||
slot->done = done;
|
||||
spin_unlock(&queue->cmds_lock);
|
||||
|
||||
write_sqe(queue, &sqe);
|
||||
return cid;
|
||||
}
|
||||
|
||||
bool nvme_io_queue_abandon(struct nvme_io_queue* queue, uint16_t cid, struct bio* bio) {
|
||||
spin_lock(&queue->cmds_lock);
|
||||
struct nvme_inflight* slot = &queue->slots[cid];
|
||||
bool abandoned = slot->active && slot->bio == bio;
|
||||
if (abandoned) {
|
||||
slot->bio = nullptr;
|
||||
slot->done = nullptr;
|
||||
}
|
||||
spin_unlock(&queue->cmds_lock);
|
||||
return abandoned;
|
||||
}
|
||||
|
||||
size_t nvme_io_queue_drain(struct nvme_io_queue* queue) {
|
||||
spin_lock(&queue->cq_lock);
|
||||
size_t completed = 0;
|
||||
|
||||
while (true) {
|
||||
struct nvme_cqe* entry = &((struct nvme_cqe*)queue->cq.virt)[queue->cq_head];
|
||||
dma_buffer_sync(&queue->cq, queue->cq_head * sizeof(*entry), sizeof(*entry), DMA_SYNC_POSTREAD);
|
||||
uint32_t dw3 = mmio_reg_read(entry, NVME_CQE_DW3);
|
||||
if (mmio_field_get(NVME_CQE_PHASE_TAG, dw3) != queue->cq_phase)
|
||||
break;
|
||||
|
||||
uint16_t cid = mmio_field_get(NVME_CQE_CID, dw3);
|
||||
uint16_t status = mmio_field_get(NVME_CQE_STATUS, dw3);
|
||||
|
||||
queue->cq_head++;
|
||||
if (queue->cq_head == queue->depth) {
|
||||
queue->cq_head = 0;
|
||||
queue->cq_phase ^= 1;
|
||||
}
|
||||
|
||||
struct nvme_inflight inflight = {};
|
||||
spin_lock(&queue->cmds_lock);
|
||||
if (cid < queue->depth - 1 && queue->slots[cid].active) {
|
||||
inflight = queue->slots[cid];
|
||||
queue->slots[cid].active = false;
|
||||
queue->free[queue->num_free++] = cid;
|
||||
}
|
||||
spin_unlock(&queue->cmds_lock);
|
||||
|
||||
if (inflight.active) {
|
||||
nvme_release_prps(&inflight);
|
||||
if (inflight.bio) {
|
||||
if (nvme_status_is_success(status))
|
||||
bio_complete(inflight.bio, 0, inflight.lbas);
|
||||
else
|
||||
bio_complete(inflight.bio, EIO, 0);
|
||||
}
|
||||
if (inflight.done)
|
||||
atomic_store(inflight.done, true, ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
completed++;
|
||||
}
|
||||
|
||||
if (completed > 0)
|
||||
ring_head(queue->regs, queue->doorbells_offset, queue->doorbell_stride, queue->cq_head);
|
||||
|
||||
spin_unlock(&queue->cq_lock);
|
||||
|
||||
if (completed > 0)
|
||||
event_set(&queue->slots_free);
|
||||
|
||||
return completed;
|
||||
}
|
||||
@@ -1,3 +1,152 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zinnia/compiler.h>
|
||||
#include <zinnia/mmio.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Submission queue entry.
|
||||
struct [[__packed]] nvme_sqe {
|
||||
uint32_t cdw0;
|
||||
|
||||
uint32_t nsid; // Namespace Identifier
|
||||
uint32_t cdw2;
|
||||
uint32_t cdw3;
|
||||
|
||||
uint64_t mptr; // Metadata Pointer
|
||||
|
||||
uint64_t dptr0; // Data Pointer 0
|
||||
uint64_t dptr1; // Data Pointer 1
|
||||
|
||||
uint32_t cdw10;
|
||||
uint32_t cdw11;
|
||||
uint32_t cdw12;
|
||||
uint32_t cdw13;
|
||||
uint32_t cdw14;
|
||||
uint32_t cdw15;
|
||||
};
|
||||
static_assert(sizeof(struct nvme_sqe) == 0x40);
|
||||
|
||||
constexpr mmio_reg32 NVME_SQE_CDW0 = {offsetof(struct nvme_sqe, cdw0), MMIO_LE};
|
||||
constexpr mmio_field32 NVME_SQE_CID = {NVME_SQE_CDW0, 16, 16}; // Command Identifier
|
||||
constexpr mmio_field32 NVME_SQE_PSDT = {NVME_SQE_CDW0, 14, 2}; // PRP or SGL for Data Transfer
|
||||
constexpr mmio_field32 NVME_SQE_FUSE = {NVME_SQE_CDW0, 8, 2}; // Fused Operation
|
||||
constexpr mmio_field32 NVME_SQE_OPC = {NVME_SQE_CDW0, 0, 8}; // Opcode
|
||||
|
||||
constexpr mmio_reg32 NVME_SQE_NSID = {offsetof(struct nvme_sqe, nsid), MMIO_LE}; // Namespace Identifier
|
||||
constexpr mmio_reg64 NVME_SQE_MPTR = {offsetof(struct nvme_sqe, mptr), MMIO_LE}; // Metadata Pointer
|
||||
constexpr mmio_reg64 NVME_SQE_DPTR0 = {offsetof(struct nvme_sqe, dptr0), MMIO_LE}; // Data Pointer 0
|
||||
constexpr mmio_reg64 NVME_SQE_DPTR1 = {offsetof(struct nvme_sqe, dptr1), MMIO_LE}; // Data Pointer 1
|
||||
|
||||
constexpr mmio_reg32 NVME_SQE_CDW10 = {offsetof(struct nvme_sqe, cdw10), MMIO_LE};
|
||||
constexpr mmio_reg32 NVME_SQE_CDW11 = {offsetof(struct nvme_sqe, cdw11), MMIO_LE};
|
||||
constexpr mmio_reg32 NVME_SQE_CDW12 = {offsetof(struct nvme_sqe, cdw12), MMIO_LE};
|
||||
constexpr mmio_reg32 NVME_SQE_CDW13 = {offsetof(struct nvme_sqe, cdw13), MMIO_LE};
|
||||
constexpr mmio_reg32 NVME_SQE_CDW14 = {offsetof(struct nvme_sqe, cdw14), MMIO_LE};
|
||||
constexpr mmio_reg32 NVME_SQE_CDW15 = {offsetof(struct nvme_sqe, cdw15), MMIO_LE};
|
||||
|
||||
constexpr uint8_t NVME_CREATE_CQ_OPCODE = 0x05;
|
||||
constexpr mmio_field32 NVME_CREATE_CQ_QSIZE = {NVME_SQE_CDW10, 16, 16}; // Queue Size
|
||||
constexpr mmio_field32 NVME_CREATE_CQ_QID = {NVME_SQE_CDW10, 0, 16}; // Queue Identifier
|
||||
constexpr mmio_field32 NVME_CREATE_CQ_IV = {NVME_SQE_CDW11, 16, 16}; // Interrupt Vector
|
||||
constexpr mmio_field32 NVME_CREATE_CQ_IEN = {NVME_SQE_CDW11, 1, 1}; // Interrupts Enabled
|
||||
constexpr mmio_field32 NVME_CREATE_CQ_PC = {NVME_SQE_CDW11, 0, 1}; // Physically Contiguous
|
||||
|
||||
constexpr uint8_t NVME_CREATE_SQ_OPCODE = 0x01;
|
||||
constexpr mmio_field32 NVME_CREATE_SQ_QSIZE = {NVME_SQE_CDW10, 16, 16}; // Queue Size
|
||||
constexpr mmio_field32 NVME_CREATE_SQ_QID = {NVME_SQE_CDW10, 0, 16}; // Queue Identifier
|
||||
constexpr mmio_field32 NVME_CREATE_SQ_CQID = {NVME_SQE_CDW11, 16, 16}; // Completion Queue Identifier
|
||||
constexpr mmio_field32 NVME_CREATE_SQ_QPRIO = {NVME_SQE_CDW11, 1, 2}; // Queue Priority
|
||||
constexpr mmio_field32 NVME_CREATE_SQ_PC = {NVME_SQE_CDW11, 0, 1}; // Physically Contiguous
|
||||
|
||||
constexpr mmio_field32 NVME_IDENTIFY_CNTID = {NVME_SQE_CDW10, 16, 16}; // Controller Identifier
|
||||
constexpr mmio_field32 NVME_IDENTIFY_CNS = {NVME_SQE_CDW10, 0, 8}; // Controller or Namespace Structure
|
||||
|
||||
constexpr mmio_field32 NVME_RW_SLBA_LOW = {NVME_SQE_CDW10, 0, 32}; // Starting LBA Lower Bits
|
||||
constexpr mmio_field32 NVME_RW_SLBA_HIGH = {NVME_SQE_CDW11, 0, 32}; // Starting LBA Higher Bits
|
||||
constexpr mmio_field32 NVME_RW_LR = {NVME_SQE_CDW12, 31, 1}; // Limited Retry
|
||||
constexpr mmio_field32 NVME_RW_FUA = {NVME_SQE_CDW12, 30, 1}; // Force Unit Access
|
||||
constexpr mmio_field32 NVME_RW_PRINFO = {NVME_SQE_CDW12, 26, 4}; // Protection Information Field
|
||||
constexpr mmio_field32 NVME_RW_NLB = {NVME_SQE_CDW12, 0, 16}; // Number of Logical Blocks
|
||||
|
||||
struct [[__packed]] nvme_cqe {
|
||||
uint32_t dw0;
|
||||
uint32_t dw1;
|
||||
uint32_t dw2;
|
||||
uint32_t dw3;
|
||||
};
|
||||
static_assert(sizeof(struct nvme_cqe) == 0x10);
|
||||
|
||||
constexpr mmio_reg32 NVME_CQE_DW0 = {offsetof(struct nvme_cqe, dw0), MMIO_LE};
|
||||
constexpr mmio_reg32 NVME_CQE_DW2 = {offsetof(struct nvme_cqe, dw2), MMIO_LE};
|
||||
constexpr mmio_field32 NVME_CQE_SQ_IDENT = {NVME_CQE_DW2, 16, 16};
|
||||
constexpr mmio_field32 NVME_CQE_SQ_HEAD = {NVME_CQE_DW2, 0, 16};
|
||||
constexpr mmio_reg32 NVME_CQE_DW3 = {offsetof(struct nvme_cqe, dw3), MMIO_LE};
|
||||
constexpr mmio_field32 NVME_CQE_STATUS = {NVME_CQE_DW3, 17, 15};
|
||||
constexpr mmio_field32 NVME_CQE_PHASE_TAG = {NVME_CQE_DW3, 16, 1};
|
||||
constexpr mmio_field32 NVME_CQE_CID = {NVME_CQE_DW3, 0, 16};
|
||||
|
||||
constexpr mmio_reg64 NVME_REG_CAP = {0x00, MMIO_LE}; // Controller Capabilities
|
||||
constexpr mmio_field64 NVME_CAP_MPSMAX = {NVME_REG_CAP, 52, 4}; // Memory Page Size Maximum
|
||||
constexpr mmio_field64 NVME_CAP_MPSMIN = {NVME_REG_CAP, 48, 4}; // Memory Page Size Minimum
|
||||
constexpr mmio_field64 NVME_CAP_BPS = {NVME_REG_CAP, 45, 1}; // Boot Partition Support
|
||||
constexpr mmio_field64 NVME_CAP_CSS = {NVME_REG_CAP, 37, 8}; // Command Sets Supported
|
||||
constexpr mmio_field64 NVME_CAP_NSSRS = {NVME_REG_CAP, 36, 1}; // NVM Subsystem Reset Supported
|
||||
constexpr mmio_field64 NVME_CAP_DSTRD = {NVME_REG_CAP, 32, 4}; // Doorbell Stride
|
||||
constexpr mmio_field64 NVME_CAP_TO = {NVME_REG_CAP, 24, 8}; // Timeout
|
||||
constexpr mmio_field64 NVME_CAP_AMS = {NVME_REG_CAP, 17, 2}; // Arbitration Mechanism Supported
|
||||
constexpr mmio_field64 NVME_CAP_CQR = {NVME_REG_CAP, 16, 1}; // Contiguous Queues Required
|
||||
constexpr mmio_field64 NVME_CAP_MQES = {NVME_REG_CAP, 0, 16}; // Maximum Queue Entries Supported
|
||||
|
||||
constexpr mmio_reg32 NVME_REG_VS = {0x08, MMIO_LE}; // Version
|
||||
constexpr mmio_field32 NVME_VS_MJR = {NVME_REG_VS, 16, 16}; // Major Version Number
|
||||
constexpr mmio_field32 NVME_VS_MNR = {NVME_REG_VS, 8, 8}; // Minor Version Number
|
||||
constexpr mmio_field32 NVME_VS_TER = {NVME_REG_VS, 0, 8}; // Tertiary Version Number
|
||||
|
||||
constexpr mmio_reg32 NVME_REG_INTMS = {0x0C, MMIO_LE}; // Interrupt Mask Set
|
||||
constexpr mmio_reg32 NVME_REG_INTMC = {0x10, MMIO_LE}; // Interrupt Mask Clear
|
||||
|
||||
constexpr mmio_reg32 NVME_REG_CC = {0x14, MMIO_LE}; // Controller Configuration
|
||||
constexpr mmio_field32 NVME_CC_IOCQES = {NVME_REG_CC, 20, 4}; // I/O Completion Queue Entry Size
|
||||
constexpr mmio_field32 NVME_CC_IOSQES = {NVME_REG_CC, 16, 4}; // I/O Submission Queue Entry Size
|
||||
constexpr mmio_field32 NVME_CC_SHN = {NVME_REG_CC, 14, 2}; // Shutdown Notification
|
||||
constexpr mmio_field32 NVME_CC_AMS = {NVME_REG_CC, 11, 3}; // Arbitration Mechanism Selected
|
||||
constexpr mmio_field32 NVME_CC_MPS = {NVME_REG_CC, 7, 4}; // Memory Page Size
|
||||
constexpr mmio_field32 NVME_CC_CSS = {NVME_REG_CC, 4, 3}; // I/O Command Set Selected
|
||||
constexpr mmio_field32 NVME_CC_EN = {NVME_REG_CC, 0, 1}; // Enable
|
||||
|
||||
constexpr mmio_reg32 NVME_REG_CSTS = {0x1C, MMIO_LE}; // Controller Status
|
||||
constexpr mmio_field32 NVME_CSTS_PP = {NVME_REG_CSTS, 5, 1}; // Processing Paused
|
||||
constexpr mmio_field32 NVME_CSTS_NSSRO = {NVME_REG_CSTS, 4, 1}; // NVM Subsystem Reset Occurred
|
||||
constexpr mmio_field32 NVME_CSTS_SHST = {NVME_REG_CSTS, 2, 2}; // Shutdown Status
|
||||
constexpr mmio_field32 NVME_CSTS_CFS = {NVME_REG_CSTS, 1, 1}; // Controller Fatal Status
|
||||
constexpr mmio_field32 NVME_CSTS_RDY = {NVME_REG_CSTS, 0, 1}; // Ready
|
||||
|
||||
constexpr mmio_reg32 NVME_REG_AQA = {0x24, MMIO_LE}; // Admin Queue Attributes
|
||||
|
||||
constexpr mmio_field32 NVME_AQA_ACQS = {NVME_REG_AQA, 0, 12}; // Admin Completion Queue Size
|
||||
constexpr mmio_field32 NVME_AQA_ASQS = {NVME_REG_AQA, 16, 12}; // Admin Submission Queue Size
|
||||
|
||||
constexpr mmio_reg64 NVME_REG_ASQ = {0x28, MMIO_LE}; // Admin Submission Queue
|
||||
constexpr mmio_reg64 NVME_REG_ACQ = {0x30, MMIO_LE}; // Admin Completion Queue
|
||||
|
||||
// Generic Command Set
|
||||
enum nvme_cmd : uint8_t {
|
||||
NVME_CMD_FLUSH = 0x00,
|
||||
NVME_CMD_WRITE = 0x01,
|
||||
NVME_CMD_READ = 0x02,
|
||||
};
|
||||
|
||||
// Admin Command Set
|
||||
enum nvme_admin_cmd : uint8_t {
|
||||
NVME_ADMIN_DELETE_SQ = 0x00,
|
||||
NVME_ADMIN_CREATE_SQ = 0x01,
|
||||
NVME_ADMIN_DELETE_CQ = 0x04,
|
||||
NVME_ADMIN_CREATE_CQ = 0x05,
|
||||
NVME_ADMIN_IDENTIFY = 0x06,
|
||||
NVME_ADMIN_ABORT = 0x08,
|
||||
NVME_ADMIN_SET_FEATURES = 0x09,
|
||||
NVME_ADMIN_GET_FEATURES = 0x0A,
|
||||
};
|
||||
|
||||
+121
@@ -4,11 +4,18 @@
|
||||
|
||||
#include <zinnia/alloc.h>
|
||||
#include <zinnia/init.h>
|
||||
#include <zinnia/mmio.h>
|
||||
#include <zinnia/module.h>
|
||||
#include <zinnia/pci.h>
|
||||
#include <zinnia/pmap.h>
|
||||
#include <zinnia/print.h>
|
||||
#include <zinnia/spin.h>
|
||||
#include <zinnia/utils.h>
|
||||
#include <zinnia/vec.h>
|
||||
#include <zinnia/virt.h>
|
||||
#include <zinnia/vma.h>
|
||||
#include <arch/irq.h>
|
||||
#include <arch/mem.h>
|
||||
#include <string.h>
|
||||
|
||||
static struct spinlock access_lock = {};
|
||||
@@ -170,6 +177,119 @@ uint8_t pci_cap_find(struct pci_device* dev, uint8_t id) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
constexpr uint32_t PCI_MSI_CTRL_ENABLE = 1 << 16;
|
||||
constexpr uint32_t PCI_MSI_CTRL_MME = 0x7 << 20;
|
||||
constexpr uint32_t PCI_MSI_CTRL_64BIT = 1 << 23;
|
||||
constexpr uint32_t PCI_MSIX_CTRL_FUNCTION_MASK = 1 << 30;
|
||||
constexpr uint32_t PCI_MSIX_CTRL_ENABLE = 1u << 31;
|
||||
|
||||
static void msix_set_state(struct pci_device* dev, uint8_t cap, bool status) {
|
||||
uint32_t ctrl = pci_read32(dev, cap);
|
||||
ctrl &= ~(PCI_MSIX_CTRL_ENABLE | PCI_MSIX_CTRL_FUNCTION_MASK);
|
||||
if (status)
|
||||
ctrl |= PCI_MSIX_CTRL_ENABLE;
|
||||
pci_write32(dev, cap, ctrl);
|
||||
}
|
||||
|
||||
errno_t pci_setup_msix(struct pci_device* dev, struct irq_line** out) {
|
||||
// message address + data + vector control
|
||||
constexpr size_t ENTRY_SIZE = 8 + 4 + 4;
|
||||
|
||||
const uint8_t cap = pci_cap_find(dev, PCI_CAP_MSIX);
|
||||
if (cap == 0)
|
||||
return ENODEV;
|
||||
|
||||
const uint32_t table = pci_read32(dev, cap + 4);
|
||||
const uint8_t bir = table & 0x7;
|
||||
const size_t table_offset = table & ~0x7;
|
||||
|
||||
pci_log(dev, "Using MSI-X for this device, table at BAR%hhu + %#zx\n", bir, table_offset);
|
||||
|
||||
// Map the first MSI-X table entry (16 bytes).
|
||||
struct pci_bar bar;
|
||||
errno_t status = pci_bar_get(dev, bir, &bar);
|
||||
if (status)
|
||||
return ENODEV;
|
||||
if (bar.type != PCI_BAR_MMIO32 && bar.type != PCI_BAR_MMIO64)
|
||||
return EINVAL;
|
||||
|
||||
const size_t page_size = arch_mem_page_size();
|
||||
const phys_t table_phys = bar.address + table_offset;
|
||||
const phys_t page_base = ALIGN_DOWN(table_phys, page_size);
|
||||
const size_t page_offset = table_phys - page_base;
|
||||
const size_t map_len = ALIGN_UP(page_offset + ENTRY_SIZE, page_size);
|
||||
|
||||
uintptr_t vaddr = vm_alloc(map_len);
|
||||
if (!vaddr)
|
||||
return ENOMEM;
|
||||
for (size_t i = 0; i < map_len; i += page_size) {
|
||||
status = pmap_map(&kernel_vma.pmap, vaddr + i, page_base + i, PTE_READ | PTE_WRITE, CACHE_MMIO);
|
||||
if (status)
|
||||
return status;
|
||||
}
|
||||
void* entry = (void*)(vaddr + page_offset);
|
||||
|
||||
struct msi_line* msi;
|
||||
status = arch_irq_alloc_msi(&msi);
|
||||
if (status)
|
||||
return status;
|
||||
irq_line_program(&msi->line, IRQ_TRIGGER_EDGE, IRQ_POLARITY_HIGH);
|
||||
|
||||
// Write entry 0.
|
||||
const uint64_t msg_addr = msi->msg_addr(msi);
|
||||
const uint32_t msg_data = msi->msg_data(msi);
|
||||
mmio_write32(entry + 0x00, (uint32_t)msg_addr); // Address lo
|
||||
mmio_write32(entry + 0x04, (uint32_t)(msg_addr >> 32)); // Address hi
|
||||
mmio_write32(entry + 0x08, msg_data); // Data
|
||||
mmio_write32(entry + 0x0C, 0); // Vector control
|
||||
|
||||
// Enable MSI-X on the device.
|
||||
msix_set_state(dev, cap, true);
|
||||
|
||||
*out = &msi->line;
|
||||
return 0;
|
||||
}
|
||||
|
||||
errno_t pci_setup_msi(struct pci_device* dev, struct irq_line** out) {
|
||||
uint8_t cap = pci_cap_find(dev, PCI_CAP_MSI);
|
||||
if (cap == 0)
|
||||
return ENODEV;
|
||||
|
||||
pci_log(dev, "Using MSI for this device\n");
|
||||
|
||||
struct msi_line* msi;
|
||||
errno_t status = arch_irq_alloc_msi(&msi);
|
||||
if (status)
|
||||
return status;
|
||||
irq_line_program(&msi->line, IRQ_TRIGGER_EDGE, IRQ_POLARITY_HIGH);
|
||||
|
||||
const uint64_t addr = msi->msg_addr(msi);
|
||||
const uint16_t data = msi->msg_data(msi);
|
||||
|
||||
uint32_t ctrl = pci_read32(dev, cap);
|
||||
pci_write32(dev, cap + 0x04, (uint32_t)addr);
|
||||
uint16_t data_off = 0x08;
|
||||
if (ctrl & PCI_MSI_CTRL_64BIT) {
|
||||
pci_write32(dev, cap + 0x08, (uint32_t)(addr >> 32));
|
||||
data_off = 0x0C;
|
||||
}
|
||||
pci_write32(dev, cap + data_off, data);
|
||||
|
||||
ctrl &= ~PCI_MSI_CTRL_MME;
|
||||
ctrl |= PCI_MSI_CTRL_ENABLE;
|
||||
pci_write32(dev, cap, ctrl);
|
||||
|
||||
*out = &msi->line;
|
||||
return 0;
|
||||
}
|
||||
|
||||
errno_t pci_setup_irq(struct pci_device* dev, struct irq_line** out) {
|
||||
errno_t status = pci_setup_msix(dev, out);
|
||||
if (status == ENODEV)
|
||||
return pci_setup_msi(dev, out);
|
||||
return status;
|
||||
}
|
||||
|
||||
static void pci_device_name(const struct pci_device* dev, char buf[static 18]) {
|
||||
ksnprintf(buf, 18, "pci-" PCI_ADDR_FMT, PCI_ADDR_ARGS(&dev->address));
|
||||
}
|
||||
@@ -192,6 +312,7 @@ static bool scan_function(struct pci_access* access, struct pci_address addr) {
|
||||
|
||||
dev->address = addr;
|
||||
dev->access = access;
|
||||
dev->dma_tag = &dma_default_tag;
|
||||
dev->id.vendor_id = vendor_id;
|
||||
dev->id.device_id = device_id;
|
||||
dev->class.prog_if = pci_read8(dev, PCI_PROG_IF);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zinnia/errno.h>
|
||||
#include <zinnia/iovec.h>
|
||||
#include <zinnia/types.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef uint64_t dma_addr_t;
|
||||
|
||||
constexpr dma_addr_t DMA_ADDR_MAX = UINT64_MAX;
|
||||
|
||||
struct bio;
|
||||
|
||||
struct dma_tag;
|
||||
|
||||
struct dma_ops {
|
||||
errno_t (*map)(struct dma_tag* tag, phys_t phys, size_t len, dma_addr_t* out);
|
||||
void (*unmap)(struct dma_tag* tag, dma_addr_t addr, size_t len);
|
||||
};
|
||||
|
||||
struct dma_tag {
|
||||
const struct dma_ops* ops;
|
||||
void* priv;
|
||||
dma_addr_t max_addr;
|
||||
};
|
||||
|
||||
extern struct dma_tag dma_default_tag;
|
||||
|
||||
void dma_tag_subregion(struct dma_tag* parent, dma_addr_t max_addr, struct dma_tag* out);
|
||||
|
||||
struct dma_seg {
|
||||
dma_addr_t addr;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
struct dma_buffer {
|
||||
struct dma_tag* tag;
|
||||
phys_t phys;
|
||||
dma_addr_t addr;
|
||||
void* virt;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
errno_t dma_buffer_alloc(struct dma_tag* tag, size_t len, struct dma_buffer* out);
|
||||
void dma_buffer_free(struct dma_buffer* buf);
|
||||
|
||||
struct dma_map {
|
||||
struct dma_tag* tag;
|
||||
size_t size;
|
||||
size_t max_segs;
|
||||
size_t max_seg_size;
|
||||
size_t boundary;
|
||||
size_t mapped_len;
|
||||
size_t num_segs;
|
||||
struct dma_seg segs[];
|
||||
};
|
||||
|
||||
enum dma_sync {
|
||||
DMA_SYNC_PREREAD = 1 << 0,
|
||||
DMA_SYNC_POSTREAD = 1 << 1,
|
||||
DMA_SYNC_PREWRITE = 1 << 2,
|
||||
DMA_SYNC_POSTWRITE = 1 << 3,
|
||||
};
|
||||
|
||||
errno_t dma_map_create(
|
||||
struct dma_tag* tag,
|
||||
size_t size,
|
||||
size_t max_segs,
|
||||
size_t max_seg_size,
|
||||
size_t boundary,
|
||||
struct dma_map** out
|
||||
);
|
||||
void dma_map_destroy(struct dma_map* map);
|
||||
|
||||
errno_t dma_map_load(struct dma_map* map, void* buf, size_t len);
|
||||
errno_t dma_map_load_bio(struct dma_map* map, struct bio* bio);
|
||||
errno_t dma_map_load_iter(struct dma_map* map, const iovec_iter_t* iter);
|
||||
void dma_map_unload(struct dma_map* map);
|
||||
|
||||
void dma_map_sync(struct dma_map* map, size_t offset, size_t len, enum dma_sync ops);
|
||||
void dma_buffer_sync(struct dma_buffer* buf, size_t offset, size_t len, enum dma_sync ops);
|
||||
@@ -5,13 +5,16 @@
|
||||
#include <zinnia/compiler.h>
|
||||
#include <zinnia/errno.h>
|
||||
#include <zinnia/list.h>
|
||||
#include <zinnia/spin.h>
|
||||
#include <zinnia/types.h>
|
||||
#include <zinnia/workqueue.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum irq_status : uint8_t {
|
||||
IRQ_STATUS_IGNORED,
|
||||
IRQ_STATUS_HANDLED,
|
||||
IRQ_STATUS_WAKE_THREAD,
|
||||
};
|
||||
|
||||
enum irq_polarity : uint8_t {
|
||||
@@ -29,9 +32,17 @@ enum irq_mode : uint8_t {
|
||||
IRQ_MODE_MASKABLE = 1 << 1,
|
||||
};
|
||||
|
||||
struct irq_line;
|
||||
|
||||
struct irq_handler {
|
||||
enum irq_status (*raise)(struct irq_handler* self);
|
||||
void (*thread)(struct irq_handler* self);
|
||||
list_link(struct irq_handler*) next;
|
||||
|
||||
struct irq_line* line;
|
||||
struct workqueue* wq;
|
||||
struct work work;
|
||||
bool holds_mask;
|
||||
};
|
||||
|
||||
struct irq_line {
|
||||
@@ -43,6 +54,9 @@ struct irq_line {
|
||||
bool is_busy;
|
||||
list_head(struct irq_handler*) handlers;
|
||||
enum irq_mode mode;
|
||||
enum irq_trigger_mode trigger;
|
||||
struct spinlock thread_lock;
|
||||
size_t threads_pending;
|
||||
};
|
||||
|
||||
struct msi_line {
|
||||
@@ -62,6 +76,7 @@ void irq_line_program(struct irq_line* line, enum irq_trigger_mode trigger, enum
|
||||
void irq_line_raise(struct irq_line* line);
|
||||
void irq_line_attach(struct irq_line* line, struct irq_handler* handler);
|
||||
void irq_line_detach(struct irq_line* line, struct irq_handler* handler);
|
||||
errno_t irq_line_attach_threaded(struct irq_line* line, struct irq_handler* handler, const char* name);
|
||||
|
||||
void irq_lock();
|
||||
void irq_unlock();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <zinnia/alloc.h>
|
||||
#include <zinnia/compiler.h>
|
||||
#include <zinnia/dma.h>
|
||||
#include <zinnia/errno.h>
|
||||
#include <zinnia/initgraph.h>
|
||||
#include <zinnia/print.h>
|
||||
@@ -106,6 +107,7 @@ struct pci_device {
|
||||
struct pci_access* access;
|
||||
uint8_t header_type;
|
||||
struct pci_driver* driver;
|
||||
struct dma_tag* dma_tag;
|
||||
};
|
||||
|
||||
uint8_t pci_read8(struct pci_device* dev, uint16_t offset);
|
||||
@@ -137,6 +139,12 @@ uint8_t pci_cap_find(struct pci_device* dev, uint8_t id);
|
||||
constexpr uint16_t PCI_ANY_ID = 0xffff;
|
||||
constexpr uint8_t PCI_ANY_CLASS = 0xff;
|
||||
|
||||
struct irq_line;
|
||||
|
||||
errno_t pci_setup_msix(struct pci_device* dev, struct irq_line** out);
|
||||
errno_t pci_setup_msi(struct pci_device* dev, struct irq_line** out);
|
||||
errno_t pci_setup_irq(struct pci_device* dev, struct irq_line** out);
|
||||
|
||||
// Matches a device by ID and class.
|
||||
struct pci_match {
|
||||
uint16_t vendor_id;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <zinnia/list.h>
|
||||
#include <zinnia/percpu.h>
|
||||
#include <zinnia/print.h>
|
||||
#include <zinnia/sched.h>
|
||||
#include <zinnia/utils.h>
|
||||
#include <arch/cpu.h>
|
||||
#include <arch/irq.h>
|
||||
|
||||
@@ -39,9 +41,43 @@ void irq_unlock() {
|
||||
void irq_line_program(struct irq_line* line, enum irq_trigger_mode trigger, enum irq_polarity polarity) {
|
||||
irq_lock();
|
||||
line->mode = line->set_config(line, trigger, polarity);
|
||||
line->trigger = trigger;
|
||||
irq_unlock();
|
||||
}
|
||||
|
||||
static bool holds_mask_while_threaded(struct irq_line* line) {
|
||||
return line->trigger == IRQ_TRIGGER_LEVEL && (line->mode & IRQ_MODE_MASKABLE) && line->mask && line->unmask;
|
||||
}
|
||||
|
||||
static void wake_thread(struct irq_line* line, struct irq_handler* handler) {
|
||||
if (holds_mask_while_threaded(line)) {
|
||||
spin_lock(&line->thread_lock);
|
||||
if (!handler->holds_mask) {
|
||||
handler->holds_mask = true;
|
||||
if (line->threads_pending++ == 0)
|
||||
line->mask(line);
|
||||
}
|
||||
spin_unlock(&line->thread_lock);
|
||||
}
|
||||
|
||||
work_queue(handler->wq, &handler->work);
|
||||
}
|
||||
|
||||
static void irq_thread_work(struct work* work) {
|
||||
struct irq_handler* handler = containerof(work, struct irq_handler, work);
|
||||
struct irq_line* line = handler->line;
|
||||
|
||||
handler->thread(handler);
|
||||
|
||||
spin_lock(&line->thread_lock);
|
||||
if (handler->holds_mask) {
|
||||
handler->holds_mask = false;
|
||||
if (--line->threads_pending == 0)
|
||||
line->unmask(line);
|
||||
}
|
||||
spin_unlock(&line->thread_lock);
|
||||
}
|
||||
|
||||
void irq_line_raise(struct irq_line* line) {
|
||||
assert(!arch_irq_get_state(), "Raised an IRQ line with interrupts enabled");
|
||||
|
||||
@@ -51,8 +87,13 @@ void irq_line_raise(struct irq_line* line) {
|
||||
bool claimed = false;
|
||||
struct irq_handler* handler;
|
||||
list_foreach(handler, &line->handlers, next) {
|
||||
if (handler->raise(handler) == IRQ_STATUS_HANDLED)
|
||||
enum irq_status status = handler->raise ? handler->raise(handler) : IRQ_STATUS_WAKE_THREAD;
|
||||
if (status == IRQ_STATUS_WAKE_THREAD && handler->wq) {
|
||||
wake_thread(line, handler);
|
||||
claimed = true;
|
||||
} else if (status != IRQ_STATUS_IGNORED) {
|
||||
claimed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!claimed)
|
||||
@@ -83,3 +124,21 @@ void irq_line_detach(struct irq_line* line, struct irq_handler* handler) {
|
||||
while (__atomic_load_n(&line->is_busy, __ATOMIC_ACQUIRE))
|
||||
arch_cpu_pause();
|
||||
}
|
||||
|
||||
errno_t irq_line_attach_threaded(struct irq_line* line, struct irq_handler* handler, const char* name) {
|
||||
if (!handler->thread)
|
||||
return EINVAL;
|
||||
|
||||
char task_name[TASK_NAME_MAX];
|
||||
ksnprintf(task_name, sizeof(task_name), "irq/%s", name);
|
||||
|
||||
handler->line = line;
|
||||
work_init(&handler->work, irq_thread_work);
|
||||
handler->holds_mask = false;
|
||||
errno_t status = workqueue_create(task_name, SCHED_PRIO_IRQ, &handler->wq);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
irq_line_attach(line, handler);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
// SPDX-License-Identifier: ISC
|
||||
|
||||
#include <zinnia/alloc.h>
|
||||
#include <zinnia/atomic.h>
|
||||
#include <zinnia/blockdev.h>
|
||||
#include <zinnia/dma.h>
|
||||
#include <zinnia/percpu.h>
|
||||
#include <zinnia/pmap.h>
|
||||
#include <zinnia/sched.h>
|
||||
#include <zinnia/utils.h>
|
||||
#include <zinnia/vma.h>
|
||||
#include <arch/mem.h>
|
||||
|
||||
static errno_t dma_noop_map(struct dma_tag*, phys_t phys, size_t, dma_addr_t* out) {
|
||||
*out = phys;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void dma_noop_unmap(struct dma_tag*, dma_addr_t, size_t) {
|
||||
// Does nothing.
|
||||
}
|
||||
|
||||
static const struct dma_ops dma_noop_ops = {
|
||||
.map = dma_noop_map,
|
||||
.unmap = dma_noop_unmap,
|
||||
};
|
||||
|
||||
struct dma_tag dma_default_tag = {
|
||||
.ops = &dma_noop_ops,
|
||||
.max_addr = DMA_ADDR_MAX,
|
||||
};
|
||||
|
||||
void dma_tag_subregion(struct dma_tag* parent, dma_addr_t max_addr, struct dma_tag* out) {
|
||||
*out = *parent;
|
||||
out->max_addr = MIN(parent->max_addr, max_addr);
|
||||
}
|
||||
|
||||
static errno_t tag_alloc_flags(struct dma_tag* tag, enum alloc_flags* out) {
|
||||
if (tag->max_addr == DMA_ADDR_MAX)
|
||||
*out = 0;
|
||||
else if (tag->max_addr >= UINT32_MAX)
|
||||
*out = ALLOC_MEM32;
|
||||
else if (tag->max_addr >= ((dma_addr_t)1 << 20) - 1)
|
||||
*out = ALLOC_MEM20;
|
||||
else
|
||||
return EINVAL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
errno_t dma_buffer_alloc(struct dma_tag* tag, size_t len, struct dma_buffer* out) {
|
||||
if (len == 0)
|
||||
return EINVAL;
|
||||
|
||||
enum alloc_flags flags;
|
||||
errno_t status = tag_alloc_flags(tag, &flags);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
const size_t num_pages = ROUND_UP(len, arch_mem_page_size());
|
||||
phys_t phys;
|
||||
status = mem_phys_alloc(num_pages, flags, &phys);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
const size_t size = num_pages * arch_mem_page_size();
|
||||
dma_addr_t addr;
|
||||
status = tag->ops->map(tag, phys, size, &addr);
|
||||
if (status) {
|
||||
mem_phys_free(phys, num_pages);
|
||||
return status;
|
||||
}
|
||||
if (addr + size - 1 > tag->max_addr) {
|
||||
tag->ops->unmap(tag, addr, size);
|
||||
mem_phys_free(phys, num_pages);
|
||||
return ENOMEM;
|
||||
}
|
||||
|
||||
*out = (struct dma_buffer){
|
||||
.tag = tag,
|
||||
.phys = phys,
|
||||
.addr = addr,
|
||||
.virt = HHDM_PTR(phys),
|
||||
.len = len,
|
||||
};
|
||||
return 0;
|
||||
}
|
||||
|
||||
void dma_buffer_free(struct dma_buffer* buf) {
|
||||
if (buf->virt == nullptr)
|
||||
return;
|
||||
|
||||
const size_t num_pages = ROUND_UP(buf->len, arch_mem_page_size());
|
||||
buf->tag->ops->unmap(buf->tag, buf->addr, num_pages * arch_mem_page_size());
|
||||
mem_phys_free(buf->phys, num_pages);
|
||||
*buf = (struct dma_buffer){0};
|
||||
}
|
||||
|
||||
errno_t dma_map_create(
|
||||
struct dma_tag* tag,
|
||||
size_t size,
|
||||
size_t max_segs,
|
||||
size_t max_seg_size,
|
||||
size_t boundary,
|
||||
struct dma_map** out
|
||||
) {
|
||||
if (max_segs == 0 || max_seg_size == 0)
|
||||
return EINVAL;
|
||||
if (boundary & (boundary - 1))
|
||||
return EINVAL;
|
||||
|
||||
struct dma_map* map = mem_alloc(sizeof(*map) + max_segs * sizeof(struct dma_seg), 0);
|
||||
if (map == nullptr)
|
||||
return ENOMEM;
|
||||
|
||||
map->tag = tag;
|
||||
map->size = size;
|
||||
map->max_segs = max_segs;
|
||||
map->max_seg_size = max_seg_size;
|
||||
map->boundary = boundary;
|
||||
map->mapped_len = 0;
|
||||
map->num_segs = 0;
|
||||
*out = map;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void dma_map_destroy(struct dma_map* map) {
|
||||
mem_free(map);
|
||||
}
|
||||
|
||||
static errno_t map_add_range(struct dma_map* map, phys_t phys, size_t len) {
|
||||
if (len == 0)
|
||||
return 0;
|
||||
|
||||
dma_addr_t addr;
|
||||
errno_t status = map->tag->ops->map(map->tag, phys, len, &addr);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
if (addr + len - 1 > map->tag->max_addr) {
|
||||
map->tag->ops->unmap(map->tag, addr, len);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
while (len > 0) {
|
||||
size_t chunk = MIN(len, map->max_seg_size);
|
||||
if (map->boundary) {
|
||||
const dma_addr_t limit = ALIGN_DOWN(addr, map->boundary) + map->boundary;
|
||||
chunk = MIN(chunk, limit - addr);
|
||||
}
|
||||
|
||||
struct dma_seg* last = map->num_segs ? &map->segs[map->num_segs - 1] : nullptr;
|
||||
if (last && last->addr + last->len == addr && last->len + chunk <= map->max_seg_size &&
|
||||
(!map->boundary || ALIGN_DOWN(last->addr, map->boundary) == ALIGN_DOWN(addr + chunk - 1, map->boundary))) {
|
||||
last->len += chunk;
|
||||
} else {
|
||||
if (map->num_segs == map->max_segs) {
|
||||
map->tag->ops->unmap(map->tag, addr, len);
|
||||
return EFBIG;
|
||||
}
|
||||
map->segs[map->num_segs++] = (struct dma_seg){.addr = addr, .len = chunk};
|
||||
}
|
||||
|
||||
map->mapped_len += chunk;
|
||||
addr += chunk;
|
||||
len -= chunk;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static errno_t map_add_virt(struct dma_map* map, struct pmap* pmap, uintptr_t vaddr, size_t len) {
|
||||
const size_t page_size = arch_mem_page_size();
|
||||
while (len > 0) {
|
||||
const size_t offset = vaddr % page_size;
|
||||
const size_t chunk = MIN(len, page_size - offset);
|
||||
|
||||
phys_t phys;
|
||||
if (pmap_extract(pmap, vaddr - offset, &phys))
|
||||
return EFAULT;
|
||||
|
||||
errno_t status = map_add_range(map, phys + offset, chunk);
|
||||
if (status)
|
||||
return status;
|
||||
|
||||
vaddr += chunk;
|
||||
len -= chunk;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static errno_t map_load_finish(struct dma_map* map, errno_t status) {
|
||||
if (status)
|
||||
dma_map_unload(map);
|
||||
return status;
|
||||
}
|
||||
|
||||
errno_t dma_map_load(struct dma_map* map, void* buf, size_t len) {
|
||||
if (map->num_segs || len > map->size)
|
||||
return EINVAL;
|
||||
|
||||
return map_load_finish(map, map_add_virt(map, &kernel_vma.pmap, (uintptr_t)buf, len));
|
||||
}
|
||||
|
||||
errno_t dma_map_load_bio(struct dma_map* map, struct bio* bio) {
|
||||
if (map->num_segs)
|
||||
return EINVAL;
|
||||
|
||||
size_t total = 0;
|
||||
for (size_t i = 0; i < bio->num_segs; i++)
|
||||
total += bio->segs[i].len;
|
||||
if (total > map->size)
|
||||
return EINVAL;
|
||||
|
||||
errno_t status = 0;
|
||||
for (size_t i = 0; i < bio->num_segs && !status; i++)
|
||||
status = map_add_range(map, bio->segs[i].addr, bio->segs[i].len);
|
||||
|
||||
return map_load_finish(map, status);
|
||||
}
|
||||
|
||||
errno_t dma_map_load_iter(struct dma_map* map, const iovec_iter_t* iter) {
|
||||
if (map->num_segs || iter->total_len - iter->total_offset > map->size)
|
||||
return EINVAL;
|
||||
|
||||
struct pmap* pmap = &kernel_vma.pmap;
|
||||
if (iter->user)
|
||||
pmap = &percpu_get()->sched.current->space->pmap;
|
||||
|
||||
errno_t status = 0;
|
||||
size_t offset = iter->current_offset;
|
||||
for (size_t i = iter->current_idx; i < iter->iovecs_len && !status; i++) {
|
||||
const iovec_t* iov = &iter->iovecs[i];
|
||||
if (offset >= iov->len) {
|
||||
offset = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uintptr_t base = iov->base + offset;
|
||||
const size_t len = iov->len - offset;
|
||||
offset = 0;
|
||||
|
||||
if (iter->user && (!arch_mem_addr_is_user(base) || !arch_mem_addr_is_user(base + len - 1))) {
|
||||
status = EFAULT;
|
||||
break;
|
||||
}
|
||||
|
||||
status = map_add_virt(map, pmap, base, len);
|
||||
}
|
||||
|
||||
return map_load_finish(map, status);
|
||||
}
|
||||
|
||||
void dma_map_unload(struct dma_map* map) {
|
||||
for (size_t i = 0; i < map->num_segs; i++)
|
||||
map->tag->ops->unmap(map->tag, map->segs[i].addr, map->segs[i].len);
|
||||
map->num_segs = 0;
|
||||
map->mapped_len = 0;
|
||||
}
|
||||
|
||||
static void dma_sync(enum dma_sync ops) {
|
||||
if (ops & (DMA_SYNC_PREREAD | DMA_SYNC_PREWRITE))
|
||||
atomic_fence(ATOMIC_RELEASE);
|
||||
if (ops & (DMA_SYNC_POSTREAD | DMA_SYNC_POSTWRITE))
|
||||
atomic_fence(ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
||||
void dma_map_sync(struct dma_map* map, size_t offset, size_t len, enum dma_sync ops) {
|
||||
dma_sync(ops);
|
||||
}
|
||||
|
||||
void dma_buffer_sync(struct dma_buffer* buf, size_t offset, size_t len, enum dma_sync ops) {
|
||||
dma_sync(ops);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
kernel_sources += files(
|
||||
'amap.c',
|
||||
'dma.c',
|
||||
'init.c',
|
||||
'page.c',
|
||||
'pmm.c',
|
||||
|
||||
Reference in New Issue
Block a user