326 lines
7.8 KiB
C
326 lines
7.8 KiB
C
/*
|
|
* Copyright (c) 2023 Enphase Energy
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include "eth_ivshmem_priv.h"
|
|
|
|
#include <zephyr/arch/cpu.h>
|
|
#include <zephyr/cache.h>
|
|
#include <zephyr/kernel.h>
|
|
|
|
#include <stdatomic.h>
|
|
#include <string.h>
|
|
|
|
/* These defines must match on the peer */
|
|
#define ETH_IVSHMEM_VRING_ALIGNMENT 64
|
|
#define ETH_IVSHMEM_FRAME_SIZE(len) ROUND_UP(18 + (len), L1_CACHE_BYTES)
|
|
|
|
#define VRING_FLUSH(x) sys_cache_data_flush_range(&(x), sizeof(x))
|
|
#define VRING_INVALIDATE(x) sys_cache_data_invd_range(&(x), sizeof(x))
|
|
|
|
static int calc_vring_size(
|
|
size_t section_size, uint16_t *vring_desc_len,
|
|
uint32_t *vring_header_size);
|
|
static uint32_t tx_buffer_advance(uint32_t max_len, uint32_t *position, uint32_t *len);
|
|
static int tx_clean_used(struct eth_ivshmem_queue *q);
|
|
static int get_rx_avail_desc_idx(struct eth_ivshmem_queue *q, uint16_t *avail_desc_idx);
|
|
|
|
int eth_ivshmem_queue_init(
|
|
struct eth_ivshmem_queue *q, uintptr_t tx_shmem,
|
|
uintptr_t rx_shmem, size_t shmem_section_size)
|
|
{
|
|
memset(q, 0, sizeof(*q));
|
|
|
|
uint16_t vring_desc_len;
|
|
uint32_t vring_header_size;
|
|
int res = calc_vring_size(shmem_section_size, &vring_desc_len, &vring_header_size);
|
|
|
|
if (res != 0) {
|
|
return res;
|
|
}
|
|
|
|
q->desc_max_len = vring_desc_len;
|
|
q->vring_data_max_len = shmem_section_size - vring_header_size;
|
|
q->vring_header_size = vring_header_size;
|
|
q->tx.shmem = (void *)tx_shmem;
|
|
q->rx.shmem = (void *)rx_shmem;
|
|
|
|
/* Init vrings */
|
|
vring_init(&q->tx.vring, vring_desc_len, q->tx.shmem, ETH_IVSHMEM_VRING_ALIGNMENT);
|
|
vring_init(&q->rx.vring, vring_desc_len, q->rx.shmem, ETH_IVSHMEM_VRING_ALIGNMENT);
|
|
|
|
/* Swap "used" pointers.
|
|
* This is done so that each peer only ever writes to its output section,
|
|
* while maintaining vring code consistency elsewhere in this file.
|
|
*/
|
|
struct vring_used *tmp_used = q->tx.vring.used;
|
|
|
|
q->tx.vring.used = q->rx.vring.used;
|
|
q->rx.vring.used = tmp_used;
|
|
|
|
eth_ivshmem_queue_reset(q);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void eth_ivshmem_queue_reset(struct eth_ivshmem_queue *q)
|
|
{
|
|
q->tx.desc_head = 0;
|
|
q->tx.desc_len = 0;
|
|
q->tx.data_head = 0;
|
|
q->tx.data_tail = 0;
|
|
q->tx.data_len = 0;
|
|
q->tx.avail_idx = 0;
|
|
q->tx.used_idx = 0;
|
|
q->tx.pending_data_head = 0;
|
|
q->tx.pending_data_len = 0;
|
|
q->rx.avail_idx = 0;
|
|
q->rx.used_idx = 0;
|
|
|
|
memset(q->tx.shmem, 0, q->vring_header_size);
|
|
|
|
/* Init TX ring descriptors */
|
|
for (unsigned int i = 0; i < q->tx.vring.num - 1; i++) {
|
|
q->tx.vring.desc[i].next = i + 1;
|
|
}
|
|
q->tx.vring.desc[q->tx.vring.num - 1].next = 0;
|
|
}
|
|
|
|
int eth_ivshmem_queue_tx_get_buff(struct eth_ivshmem_queue *q, void **data, size_t len)
|
|
{
|
|
/* Clean used TX buffers */
|
|
int res = tx_clean_used(q);
|
|
|
|
if (res != 0) {
|
|
return res;
|
|
}
|
|
|
|
if (q->tx.desc_len >= q->desc_max_len) {
|
|
return -ENOBUFS;
|
|
}
|
|
|
|
uint32_t head = q->tx.data_head;
|
|
uint32_t consumed_len = len;
|
|
uint32_t new_head = tx_buffer_advance(q->vring_data_max_len, &head, &consumed_len);
|
|
|
|
if (q->vring_data_max_len - q->tx.data_len < consumed_len) {
|
|
return -ENOBUFS;
|
|
}
|
|
|
|
struct vring_desc *tx_desc = &q->tx.vring.desc[q->tx.desc_head];
|
|
|
|
tx_desc->addr = q->vring_header_size + head;
|
|
tx_desc->len = len;
|
|
tx_desc->flags = 0;
|
|
VRING_FLUSH(*tx_desc);
|
|
|
|
*data = (uint8_t *)q->tx.shmem + q->vring_header_size + head;
|
|
|
|
q->tx.pending_data_head = new_head;
|
|
q->tx.pending_data_len = q->tx.data_len + consumed_len;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int eth_ivshmem_queue_tx_commit_buff(struct eth_ivshmem_queue *q)
|
|
{
|
|
/* Ensure that a TX buffer is pending */
|
|
if (q->tx.pending_data_len == 0) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
uint16_t desc_head = q->tx.desc_head;
|
|
|
|
q->tx.desc_len++;
|
|
q->tx.desc_head = (q->tx.desc_head + 1) % q->desc_max_len;
|
|
|
|
q->tx.data_head = q->tx.pending_data_head;
|
|
q->tx.data_len = q->tx.pending_data_len;
|
|
|
|
q->tx.vring.avail->ring[q->tx.avail_idx % q->desc_max_len] = desc_head;
|
|
|
|
VRING_FLUSH(q->tx.vring.avail->ring[q->tx.avail_idx % q->desc_max_len]);
|
|
atomic_thread_fence(memory_order_seq_cst);
|
|
|
|
q->tx.avail_idx++;
|
|
q->tx.vring.avail->idx = q->tx.avail_idx;
|
|
|
|
VRING_FLUSH(q->tx.vring.avail->idx);
|
|
|
|
q->tx.pending_data_len = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int eth_ivshmem_queue_rx(struct eth_ivshmem_queue *q, const void **data, size_t *len)
|
|
{
|
|
*data = NULL;
|
|
*len = 0;
|
|
|
|
uint16_t avail_desc_idx;
|
|
int res = get_rx_avail_desc_idx(q, &avail_desc_idx);
|
|
|
|
if (res != 0) {
|
|
return res;
|
|
}
|
|
|
|
struct vring_desc *desc = &q->rx.vring.desc[avail_desc_idx];
|
|
|
|
VRING_INVALIDATE(*desc);
|
|
|
|
uint64_t offset = desc->addr - q->vring_header_size;
|
|
uint32_t rx_len = desc->len;
|
|
|
|
if (offset > q->vring_data_max_len ||
|
|
rx_len > q->vring_data_max_len ||
|
|
offset > q->vring_data_max_len - rx_len) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
*data = (uint8_t *)q->rx.shmem + q->vring_header_size + offset;
|
|
*len = desc->len;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int eth_ivshmem_queue_rx_complete(struct eth_ivshmem_queue *q)
|
|
{
|
|
uint16_t avail_desc_idx;
|
|
int res = get_rx_avail_desc_idx(q, &avail_desc_idx);
|
|
|
|
if (res != 0) {
|
|
return res;
|
|
}
|
|
|
|
uint16_t used_idx = q->rx.used_idx % q->desc_max_len;
|
|
|
|
q->rx.used_idx++;
|
|
q->rx.vring.used->ring[used_idx].id = avail_desc_idx;
|
|
q->rx.vring.used->ring[used_idx].len = 1;
|
|
VRING_FLUSH(q->rx.vring.used->ring[used_idx]);
|
|
atomic_thread_fence(memory_order_seq_cst);
|
|
|
|
q->rx.vring.used->idx = q->rx.used_idx;
|
|
VRING_FLUSH(q->rx.vring.used->idx);
|
|
atomic_thread_fence(memory_order_seq_cst);
|
|
|
|
q->rx.avail_idx++;
|
|
vring_avail_event(&q->rx.vring) = q->rx.avail_idx;
|
|
VRING_FLUSH(vring_avail_event(&q->rx.vring));
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Calculates the vring descriptor length and header size.
|
|
* This must match what is calculated by the peer.
|
|
*/
|
|
static int calc_vring_size(
|
|
size_t section_size, uint16_t *vring_desc_len,
|
|
uint32_t *vring_header_size)
|
|
{
|
|
static const int eth_min_mtu = 68;
|
|
uint32_t header_size;
|
|
int16_t desc_len;
|
|
|
|
for (desc_len = 4096; desc_len > 32; desc_len >>= 1) {
|
|
header_size = vring_size(desc_len, ETH_IVSHMEM_VRING_ALIGNMENT);
|
|
header_size = ROUND_UP(header_size, ETH_IVSHMEM_VRING_ALIGNMENT);
|
|
if (header_size < section_size / 8) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (header_size > section_size) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
uint32_t vring_data_size = section_size - header_size;
|
|
|
|
if (vring_data_size < 4 * eth_min_mtu) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
*vring_desc_len = desc_len;
|
|
*vring_header_size = header_size;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint32_t tx_buffer_advance(uint32_t max_len, uint32_t *position, uint32_t *len)
|
|
{
|
|
uint32_t aligned_len = ETH_IVSHMEM_FRAME_SIZE(*len);
|
|
uint32_t contiguous_len = max_len - *position;
|
|
|
|
*len = aligned_len;
|
|
if (aligned_len > contiguous_len) {
|
|
/* Wrap back to zero */
|
|
*position = 0;
|
|
*len += contiguous_len;
|
|
}
|
|
|
|
return *position + aligned_len;
|
|
}
|
|
|
|
static int tx_clean_used(struct eth_ivshmem_queue *q)
|
|
{
|
|
while (true) {
|
|
VRING_INVALIDATE(q->tx.vring.used->idx);
|
|
if (q->tx.used_idx == q->tx.vring.used->idx) {
|
|
break;
|
|
}
|
|
|
|
struct vring_used_elem *used = &q->tx.vring.used->ring[
|
|
q->tx.used_idx % q->desc_max_len];
|
|
|
|
atomic_thread_fence(memory_order_seq_cst);
|
|
VRING_INVALIDATE(*used);
|
|
|
|
if (used->id >= q->desc_max_len || used->len != 1) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
struct vring_desc *desc = &q->tx.vring.desc[used->id];
|
|
|
|
uint64_t offset = desc->addr - q->vring_header_size;
|
|
uint32_t len = desc->len;
|
|
|
|
uint32_t tail = q->tx.data_tail;
|
|
uint32_t consumed_len = len;
|
|
uint32_t new_tail = tx_buffer_advance(q->vring_data_max_len, &tail, &consumed_len);
|
|
|
|
if (consumed_len > q->tx.data_len ||
|
|
offset != tail) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
q->tx.data_tail = new_tail;
|
|
q->tx.data_len -= consumed_len;
|
|
q->tx.desc_len--;
|
|
q->tx.used_idx++;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int get_rx_avail_desc_idx(struct eth_ivshmem_queue *q, uint16_t *avail_desc_idx)
|
|
{
|
|
atomic_thread_fence(memory_order_seq_cst);
|
|
VRING_INVALIDATE(q->rx.vring.avail->idx);
|
|
|
|
uint16_t avail_idx = q->rx.vring.avail->idx;
|
|
|
|
if (avail_idx == q->rx.avail_idx) {
|
|
return -EWOULDBLOCK;
|
|
}
|
|
|
|
VRING_INVALIDATE(q->rx.vring.avail->ring[q->rx.avail_idx % q->desc_max_len]);
|
|
*avail_desc_idx = q->rx.vring.avail->ring[q->rx.avail_idx % q->desc_max_len];
|
|
if (*avail_desc_idx >= q->desc_max_len) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|