blob: 6a2ff5f58f7c91861fa62e80f91e9473d9861180 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ethernet.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <ddk/io-buffer.h>
#include <ddk/protocol/ethernet.h>
#include <zircon/assert.h>
#include <zircon/status.h>
#include <zircon/types.h>
#include <fbl/algorithm.h>
#include <fbl/alloc_checker.h>
#include <fbl/auto_call.h>
#include <fbl/auto_lock.h>
#include <fbl/unique_ptr.h>
#include <pretty/hexdump.h>
#include <virtio/net.h>
#include <virtio/virtio.h>
#include "ring.h"
#include "trace.h"
// Enables/disables debugging info
#define LOCAL_TRACE 0
namespace virtio {
namespace {
// Specifies how many packets can fit in each of the receive and transmit
// backlogs.
const size_t kBacklog = 32;
// Specifies the maximum transfer unit we support and the maximum layer 1
// Ethernet packet header length.
const size_t kVirtioMtu = 1500;
const size_t kL1EthHdrLen = 26;
// Other constants determined by the values above and the memory architecture.
// The goal here is to allocate single-page I/O buffers.
const size_t kFrameSize = sizeof(virtio_net_hdr_t) + kL1EthHdrLen + kVirtioMtu;
const size_t kFramesInBuf = PAGE_SIZE / kFrameSize;
const size_t kNumIoBufs = fbl::roundup(kBacklog * 2, kFramesInBuf) / kFramesInBuf;
const uint16_t kRxId = 0u;
const uint16_t kTxId = 1u;
// Strictly for convenience...
typedef struct vring_desc desc_t;
// Device bridge helpers
void virtio_net_unbind(void* ctx) {
virtio::EthernetDevice* eth = static_cast<virtio::EthernetDevice*>(ctx);
eth->Unbind();
}
void virtio_net_release(void* ctx) {
fbl::unique_ptr<virtio::EthernetDevice> eth(static_cast<virtio::EthernetDevice*>(ctx));
eth->Release();
}
zx_protocol_device_t kDeviceOps = {
DEVICE_OPS_VERSION,
nullptr, // get_protocol
nullptr, // open
nullptr, // openat
nullptr, // close
virtio_net_unbind,
virtio_net_release,
nullptr, // read
nullptr, // write
nullptr, // iotxn_queue
nullptr, // get_size
nullptr, // ioctl
nullptr, // suspend
nullptr, // resume
};
// Protocol bridge helpers
zx_status_t virtio_net_query(void* ctx, uint32_t options, ethmac_info_t* info) {
virtio::EthernetDevice* eth = static_cast<virtio::EthernetDevice*>(ctx);
return eth->Query(options, info);
}
void virtio_net_stop(void* ctx) {
virtio::EthernetDevice* eth = static_cast<virtio::EthernetDevice*>(ctx);
eth->Stop();
}
zx_status_t virtio_net_start(void* ctx, ethmac_ifc_t* ifc, void* cookie) {
virtio::EthernetDevice* eth = static_cast<virtio::EthernetDevice*>(ctx);
return eth->Start(ifc, cookie);
}
void virtio_net_send(void* ctx, uint32_t options, void* data, size_t length) {
virtio::EthernetDevice* eth = static_cast<virtio::EthernetDevice*>(ctx);
eth->Send(options, data, length);
}
ethmac_protocol_ops_t kProtoOps = {
virtio_net_query, virtio_net_stop, virtio_net_start, virtio_net_send,
nullptr, // queue_rx
nullptr, // queue_tx
};
// I/O buffer helpers
zx_status_t InitBuffers(fbl::unique_ptr<io_buffer_t[]>* out) {
zx_status_t rc;
fbl::AllocChecker ac;
fbl::unique_ptr<io_buffer_t[]> bufs(new (&ac) io_buffer_t[kNumIoBufs]);
if (!ac.check()) {
VIRTIO_ERROR("out of memory!\n");
return ZX_ERR_NO_MEMORY;
}
memset(bufs.get(), 0, sizeof(io_buffer_t) * kNumIoBufs);
size_t buf_size = kFrameSize * kFramesInBuf;
for (uint16_t id = 0; id < kNumIoBufs; ++id) {
if ((rc = io_buffer_init(&bufs[id], buf_size, IO_BUFFER_RW)) != ZX_OK) {
VIRTIO_ERROR("failed to allocate I/O buffers: %s\n", zx_status_get_string(rc));
return rc;
}
}
*out = fbl::move(bufs);
return ZX_OK;
}
void ReleaseBuffers(fbl::unique_ptr<io_buffer_t[]> bufs) {
if (!bufs) {
return;
}
for (size_t i = 0; i < kNumIoBufs; ++i) {
if (io_buffer_is_valid(&bufs[i])) {
io_buffer_release(&bufs[i]);
}
}
}
// Frame access helpers
zx_off_t GetFrame(io_buffer_t** bufs, uint16_t ring_id, uint16_t desc_id) {
uint16_t i = static_cast<uint16_t>(desc_id + ring_id * kBacklog);
*bufs = &((*bufs)[i / kFramesInBuf]);
return (i % kFramesInBuf) * kFrameSize;
}
void* GetFrameVirt(io_buffer_t* bufs, uint16_t ring_id, uint16_t desc_id) {
zx_off_t offset = GetFrame(&bufs, ring_id, desc_id);
uintptr_t vaddr = reinterpret_cast<uintptr_t>(io_buffer_virt(bufs));
return reinterpret_cast<void*>(vaddr + offset);
}
zx_paddr_t GetFramePhys(io_buffer_t* bufs, uint16_t ring_id, uint16_t desc_id) {
zx_off_t offset = GetFrame(&bufs, ring_id, desc_id);
return io_buffer_phys(bufs) + offset;
}
virtio_net_hdr_t* GetFrameHdr(io_buffer_t* bufs, uint16_t ring_id, uint16_t desc_id) {
return reinterpret_cast<virtio_net_hdr_t*>(GetFrameVirt(bufs, ring_id, desc_id));
}
uint8_t* GetFrameData(io_buffer_t* bufs, uint16_t ring_id, uint16_t desc_id) {
uintptr_t vaddr = reinterpret_cast<uintptr_t>(GetFrameHdr(bufs, ring_id, desc_id));
return reinterpret_cast<uint8_t*>(vaddr + sizeof(virtio_net_hdr_t));
}
} // namespace
EthernetDevice::EthernetDevice(zx_device_t* bus_device)
: Device(bus_device), rx_(this), tx_(this), bufs_(nullptr), unkicked_(0), ifc_(nullptr),
cookie_(nullptr) {
LTRACE_ENTRY;
// VirtIO spec 1.0, section 4.1.4.8
bar0_size_ = VIRTIO_PCI_CONFIG_OFFSET_NOMSI + sizeof(config_);
}
EthernetDevice::~EthernetDevice() {
LTRACE_ENTRY;
}
zx_status_t EthernetDevice::Init() {
LTRACE_ENTRY;
zx_status_t rc;
if (mtx_init(&state_lock_, mtx_plain) != thrd_success ||
mtx_init(&tx_lock_, mtx_plain) != thrd_success) {
return ZX_ERR_NO_RESOURCES;
}
fbl::AutoLock lock(&state_lock_);
// Reset the device and read our configuration
Reset();
CopyDeviceConfig(&config_, sizeof(config_));
LTRACEF("mac %02x:%02x:%02x:%02x:%02x:%02x\n", config_.mac[0], config_.mac[1], config_.mac[2],
config_.mac[3], config_.mac[4], config_.mac[5]);
LTRACEF("status %u\n", config_.status);
LTRACEF("max_virtqueue_pairs %u\n", config_.max_virtqueue_pairs);
// Ack and set the driver status bit
StatusAcknowledgeDriver();
// TODO(aarongreen): Check features bits and ack/nak them
// Plan to clean up unless everything goes right.
auto cleanup = fbl::MakeAutoCall([this]() { Release(); });
// Allocate I/O buffers and virtqueues.
uint16_t num_descs = static_cast<uint16_t>(kBacklog & 0xffff);
if ((rc = InitBuffers(&bufs_)) != ZX_OK || (rc = rx_.Init(kRxId, num_descs)) != ZX_OK ||
(rc = tx_.Init(kTxId, num_descs)) != ZX_OK) {
VIRTIO_ERROR("failed to allocate virtqueue: %s\n", zx_status_get_string(rc));
return rc;
}
// Associate the I/O buffers with the virtqueue descriptors
desc_t* desc = nullptr;
uint16_t id;
// For rx buffers, we queue a bunch of "reads" from the network that
// complete when packets arrive.
for (uint16_t i = 0; i < num_descs; ++i) {
desc = rx_.AllocDescChain(1, &id);
desc->addr = GetFramePhys(bufs_.get(), kRxId, id);
desc->len = kFrameSize;
desc->flags |= VRING_DESC_F_WRITE;
LTRACE_DO(virtio_dump_desc(desc));
rx_.SubmitChain(id);
}
// For tx buffers, we hold onto them until we need to send a packet.
for (uint16_t id = 0; id < num_descs; ++id) {
desc = tx_.DescFromIndex(id);
desc->addr = GetFramePhys(bufs_.get(), kTxId, id);
desc->len = 0;
desc->flags &= static_cast<uint16_t>(~VRING_DESC_F_WRITE);
LTRACE_DO(virtio_dump_desc(desc));
}
// Start the interrupt thread and set the driver OK status
StartIrqThread();
// Initialize the zx_device and publish us
device_add_args_t args;
memset(&args, 0, sizeof(args));
args.version = DEVICE_ADD_ARGS_VERSION;
args.name = "virtio-net";
args.ctx = this;
args.ops = &kDeviceOps;
args.proto_id = ZX_PROTOCOL_ETHERMAC;
args.proto_ops = &kProtoOps;
if ((rc = device_add(bus_device_, &args, &device_)) != ZX_OK) {
VIRTIO_ERROR("failed to add device: %s\n", zx_status_get_string(rc));
return rc;
}
// Give the rx buffers to the host
rx_.Kick();
// Woohoo! Driver should be ready.
cleanup.cancel();
StatusDriverOK();
return ZX_OK;
}
void EthernetDevice::Release() {
LTRACE_ENTRY;
fbl::AutoLock lock(&state_lock_);
ReleaseLocked();
}
void EthernetDevice::ReleaseLocked() {
ifc_ = nullptr;
ReleaseBuffers(fbl::move(bufs_));
Device::Release();
}
void EthernetDevice::IrqRingUpdate() {
LTRACE_ENTRY;
// Lock to prevent changes to ifc_.
{
fbl::AutoLock lock(&state_lock_);
if (!ifc_) {
return;
}
// Ring::IrqRingUpdate will call this lambda on each rx buffer filled by
// the underlying device since the last IRQ.
// Thread safety analysis is explicitly disabled as clang isn't able to determine that the
// state_lock_ is held when the lambda invoked.
rx_.IrqRingUpdate([this](vring_used_elem* used_elem) TA_NO_THREAD_SAFETY_ANALYSIS {
uint16_t id = static_cast<uint16_t>(used_elem->id & 0xffff);
desc_t* desc = rx_.DescFromIndex(id);
// Transitional driver does not merge rx buffers.
assert(used_elem->len < desc->len);
uint8_t* data = GetFrameData(bufs_.get(), kRxId, id);
size_t len = used_elem->len - sizeof(virtio_net_hdr_t);
LTRACEF("Receiving %zu bytes:\n", len);
LTRACE_DO(hexdump8_ex(data, len, 0));
// Pass the data up the stack to the generic Ethernet driver
ifc_->recv(cookie_, data, len, 0);
assert((desc->flags & VRING_DESC_F_NEXT) == 0);
LTRACE_DO(virtio_dump_desc(desc));
rx_.FreeDesc(id);
});
}
// Now recycle the rx buffers. As in Init(), this means queuing a bunch of
// "reads" from the network that will complete when packets arrive.
desc_t* desc = nullptr;
uint16_t id;
bool need_kick = false;
while ((desc = rx_.AllocDescChain(1, &id))) {
desc->len = kFrameSize;
rx_.SubmitChain(id);
need_kick = true;
}
// If we have re-queued any rx buffers, poke the virtqueue to pick them up.
if (need_kick) {
rx_.Kick();
}
}
void EthernetDevice::IrqConfigChange() {
LTRACE_ENTRY;
fbl::AutoLock lock(&state_lock_);
if (!ifc_) {
return;
}
// Re-read our configuration
CopyDeviceConfig(&config_, sizeof(config_));
ifc_->status(cookie_, (config_.status & VIRTIO_NET_S_LINK_UP) ? ETH_STATUS_ONLINE : 0);
}
zx_status_t EthernetDevice::Query(uint32_t options, ethmac_info_t* info) {
LTRACE_ENTRY;
if (options) {
return ZX_ERR_INVALID_ARGS;
}
fbl::AutoLock lock(&state_lock_);
if (info) {
// TODO(aarongreen): Add info->features = GetFeatures();
info->mtu = kVirtioMtu;
memcpy(info->mac, config_.mac, sizeof(info->mac));
}
return ZX_OK;
}
void EthernetDevice::Stop() {
LTRACE_ENTRY;
fbl::AutoLock lock(&state_lock_);
ifc_ = nullptr;
}
zx_status_t EthernetDevice::Start(ethmac_ifc_t* ifc, void* cookie) {
LTRACE_ENTRY;
if (!ifc) {
return ZX_ERR_INVALID_ARGS;
}
fbl::AutoLock lock(&state_lock_);
if (!bufs_ || ifc_) {
return ZX_ERR_BAD_STATE;
}
ifc_ = ifc;
cookie_ = cookie;
ifc_->status(cookie_, (config_.status & VIRTIO_NET_S_LINK_UP) ? ETH_STATUS_ONLINE : 0);
return ZX_OK;
}
void EthernetDevice::Send(uint32_t options, void* data, size_t length) {
LTRACE_ENTRY;
// First, validate the packet
if (!data || length > sizeof(virtio_net_hdr_t) + kVirtioMtu) {
LTRACEF("dropping packet; invalid packet\n");
return;
}
fbl::AutoLock lock(&tx_lock_);
// Flush outstanding descriptors. Ring::IrqRingUpdate will call this lambda
// on each sent tx_buffer, allowing us to reclaim them.
auto flush = [this](vring_used_elem* used_elem) {
uint16_t id = static_cast<uint16_t>(used_elem->id & 0xffff);
desc_t* desc = tx_.DescFromIndex(id);
assert((desc->flags & VRING_DESC_F_NEXT) == 0);
LTRACE_DO(virtio_dump_desc(desc));
tx_.FreeDesc(id);
};
// Grab a free descriptor
uint16_t id;
desc_t* desc = tx_.AllocDescChain(1, &id);
if (!desc) {
tx_.IrqRingUpdate(flush);
desc = tx_.AllocDescChain(1, &id);
}
if (!desc) {
LTRACEF("dropping packet; out of descriptors\n");
return;
}
// Add the data to be sent
void* tx_hdr = GetFrameHdr(bufs_.get(), kTxId, id);
memset(tx_hdr, 0, sizeof(virtio_net_hdr_t));
void* tx_buf = GetFrameData(bufs_.get(), kTxId, id);
memcpy(tx_buf, data, length);
desc->len = static_cast<uint32_t>(sizeof(virtio_net_hdr_t) + length);
// Submit the descriptor and notify the back-end.
LTRACE_DO(virtio_dump_desc(desc));
LTRACEF("Sending %zu bytes:\n", length);
LTRACE_DO(hexdump8_ex(tx_buf, length, 0));
tx_.SubmitChain(id);
++unkicked_;
if ((options & ETHMAC_TX_OPT_MORE) == 0 || unkicked_ > kBacklog / 2) {
tx_.Kick();
unkicked_ = 0;
}
}
} // namespace virtio