Files
zinnia/dev/block/nvme/queue.c
T
2026-10-29 01:50:52 +01:00

347 lines
10 KiB
C

// 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;
}