blob: dc5392a8a481651124f4f6d3765cd1d4939aa898 [file] [log] [blame]
#pragma once
#include <array>
#include <cheri.hh>
#include <cstddef>
#include <cstdint>
#include <debug.hh>
#include <futex.h>
#include <interrupt.h>
#include <optional>
#include <platform/concepts/ethernet.hh>
#include <thread.h>
#include <type_traits>
DECLARE_AND_DEFINE_INTERRUPT_CAPABILITY(EthernetReceive,
EthernetReceiveInterrupt,
true,
true);
/**
* The driver for Kunyan Liu's custom Ethernet MAC for the Arty A7. This is
* intended to run at 10Mb/s. It provides two send and two receive buffers in
* shared SRAM.
*
* WARNING: This is currently evolving and is not yet stable.
*/
class KunyanEthernet
{
/**
* Flag set when we're debugging this driver.
*/
static constexpr bool DebugEthernet = false;
/**
* Flag set to log messages when frames are dropped.
*/
static constexpr bool DebugDroppedFrames = false;
/**
* Helper for conditional debug logs and assertions.
*/
using Debug = ConditionalDebug<DebugEthernet, "Ethernet driver">;
/**
* Import the Capability helper from the CHERI namespace.
*/
template<typename T>
using Capability = CHERI::Capability<T>;
/**
* The location of registers
*/
enum class RegisterOffset : size_t
{
/**
* MAC control register. Higher bits control filtering modes:
*
* 12: Drop frames with invalid CRC.
* 11: Allow incoming IPv6 multicast filtering
* 10: Allow incoming IPv4 multicast filtering
* 9: Allow incoming broadcast filtering
* 8: Allow incoming unicast filtering
*
*
* Write a 1 to reset the PHY. This takes 167 ms.
*/
MACControl = 0,
/**
* High 4 bytes of the MAC address. Byte 2 is in bits 31:24, byte 5 is
* in 7:0.
*/
MACAddressHigh = 4,
/**
* Low 2 bytes of the MAC address. Byte 1 in bits 15:8, byte 0 in 7:0.
*/
MACAddressLow = 8,
/**
* Scratch register, unused.
*/
Scratch = 12,
/**
* MDIO address, controls which PHY register will be read or written.
*/
MDIOAddress = 0x10,
/**
* MDIO Write, written to set the value to write to a PHY register.
*/
MDIODataWrite = 0x14,
/**
* MDIO Read, set by the device to the value of a PHY register in
* response to an MDIO read.
*/
MDIODataRead = 0x18,
/**
* MDIO control. Used to control and report status of MDIO
* transactions.
*/
MDIOControl = 0x1c,
/**
* Length of the frame written to the ping buffer for sending.
*/
TransmitFrameLengthPing = 0x20,
/**
* Transmit control for the ping buffer.
*/
TransmitControlPing = 0x24,
/**
* Length of the frame written to the pong buffer for sending.
*/
TransmitFrameLengthPong = 0x28,
/**
* Transmit control for the pong buffer.
*/
TransmitControlPong = 0x2c,
/**
* Receive control for the ping buffer.
*/
ReceiveControlPing = 0x30,
/**
* Receive control for the pong buffer.
*/
ReceiveControlPong = 0x34,
/**
* Interrupt enable. Write 1 to enable, 0 to disable interrupts for
* each direction. Bit 0 is transmit, bit 1 is receive.
*/
GlobalInterruptEnable = 0x38,
/**
* Current interrupt status. Bit 0 is transmit, bit 1 is receive.
*
* Write a 1 to each bit to clear. The device will keep the interrupt
* asserted until it is explicitly cleared.
*/
InterruptStatus = 0x3c,
/**
* Length of the frames in the ping and pong buffer, in the low and
* high 16 bits respectively.
*/
ReceiveFrameLength = 0x40,
/**
* Saturating counter of the number of frames received.
*/
ReceivedFramesCount = 0x44,
/**
* Saturating counter of the number of frames passed to software.
*/
ReceivedFramesPassedToSoftware = 0x48,
/**
* Saturating counter of the number of frames dropped because the
* receive buffers were both full.
*/
ReceiveDroppedFramesBuffersFull = 0x4c,
/**
* Saturating counter of the number of frames dropped because they had
* failed FCS checks.
*/
ReceiveDroppedFramesFCSFailed = 0x50,
/**
* Saturating counter of the number of frames dropped because the
* target address was invalid.
*/
ReceiveDroppedFramesInvalidAddress = 0x54,
};
using MACAddress = std::array<uint8_t, 6>;
/**
* MAC filtering modes.
*/
enum class FilterMode : uint32_t
{
/**
* Allow incoming frames without doing any address filtering.
*/
EnableAddressFiltering = 1 << 13,
/**
* Drop frames with invalid CRC.
*/
DropInvalidCRC = 1 << 12,
/**
* Allow incoming IPv6 multicast filtering
*/
AllowIPv6Multicast = 1 << 11,
/**
* Allow incoming IPv4 multicast filtering
*/
AllowIPv4Multicast = 1 << 10,
/**
* Allow incoming broadcast filtering
*/
AllowBroadcast = 1 << 9,
/**
* Allow incoming unicast filtering
*/
AllowUnicast = 1 << 8,
};
/**
* The futex used to wait for interrupts when packets are available to
* receive.
*/
const uint32_t *receiveInterruptFutex;
/**
* Counter for dropped frames, intended to be extensible to support
* different debugging registers.
*/
struct DroppedFrameCount
{
/**
* The old value of the counter.
*/
uint32_t droppedFrameCount[3];
/**
* Get a reference to one of the recorded counters.
*/
template<RegisterOffset Reg>
uint32_t &get();
template<>
uint32_t &get<RegisterOffset::ReceiveDroppedFramesBuffersFull>()
{
return droppedFrameCount[0];
}
template<>
uint32_t &get<RegisterOffset::ReceiveDroppedFramesFCSFailed>()
{
return droppedFrameCount[1];
}
template<>
uint32_t &get<RegisterOffset::ReceiveDroppedFramesInvalidAddress>()
{
return droppedFrameCount[2];
}
};
/**
* Empty class, for use with `std::conditional_t` to conditionally add a
* field.
*/
struct Empty
{
};
/**
* If we're building with debugging support for dropped-frame counters,
* reserve space for them.
*/
[[no_unique_address]] std::
conditional_t<DebugDroppedFrames, DroppedFrameCount, Empty>
droppedFrames;
/**
* Log a message if the dropped-frame counter identified by `Reg` has
* changed.
*/
template<RegisterOffset Reg, typename T = decltype(droppedFrames)>
void dropped_frames_log_if_changed(T &droppedFrames)
{
if constexpr (std::is_same_v<T, DroppedFrameCount>)
{
auto &lastCount = droppedFrames.template get<Reg>();
auto count = mmio_register<Reg>();
if (count != lastCount)
{
ConditionalDebug<true, "Ethernet driver">::log(
"Dropped frames in counter {}: {}", Reg, count);
lastCount = count;
}
}
}
/**
* Set the state of one of the MAC filtering modes. Returns the previous
* value of the mode.
*/
bool filter_mode_update(FilterMode mode, bool enable)
{
auto &macControl = mmio_register<RegisterOffset::MACControl>();
uint32_t modeBit = static_cast<uint32_t>(mode);
uint32_t oldMode = macControl;
Debug::log("Old filter mode {}: {}", mode, oldMode);
if (enable)
{
macControl = oldMode | modeBit;
}
else
{
macControl = oldMode & ~modeBit;
}
return oldMode | modeBit;
}
/**
* The PHY registers are accessed through the MDIO interface.
*/
enum class PHYRegister
{
BasicControlRegister = 0,
BasicStatusRegister = 1,
Identifier1 = 2,
Identifier2 = 3,
AutoNegotiationAdvertisement = 4,
AutoNegotiationLinkPartnerAbility = 5,
};
enum BufferID
{
Ping = 0,
Pong = 1,
};
bool receiveBufferInUse[2] = {false, false};
bool sendBufferInUse[2] = {false, false};
BufferID nextReceiveBuffer = BufferID::Ping;
constexpr static BufferID next_buffer_id(BufferID current)
{
return current == BufferID::Ping ? BufferID::Pong : BufferID::Ping;
}
/**
* Reset the PHY. This must be done on initialisation.
*/
void phy_reset()
{
auto &macControl = mmio_register<RegisterOffset::MACControl>();
macControl = 1;
// Wait for the PHY to reset.
thread_millisecond_wait(167);
// Initialise MDIO again after the PHY has been reset.
mmio_register<RegisterOffset::MDIOControl>() = 0x10;
// Wait to make sure MDIO initialisation has completed.
thread_microsecond_spin(1);
}
/**
* Helper. Returns a pointer to the device.
*/
[[nodiscard]] __always_inline Capability<volatile uint32_t>
mmio_region() const
{
return MMIO_CAPABILITY(uint32_t, kunyan_ethernet);
}
/**
* Helper. Returns a reference to a register in this device's MMIO region.
* The register is identified by a `RegisterOffset` value.
*/
template<RegisterOffset Offset>
[[nodiscard]] volatile uint32_t &mmio_register() const
{
auto reg = mmio_region();
reg.address() += static_cast<size_t>(Offset);
reg.bounds() = sizeof(uint32_t);
return *reg;
}
/**
* Poll the MDIO control register until a transaction is done.
*
* NOTE: This does no error handling. It can infinite loop if the device
* is broken. Normally, MDIO operations complete in fewer cycles than it
* takes to call this method and so it will return almost instantly.
*/
void mdio_wait_for_ready()
{
auto &mdioControl = mmio_register<RegisterOffset::MDIOControl>();
while (mdioControl & 1) {}
}
/**
* Start an MDIO transaction. This assumes that the MDIO control register
* and, for write operations, the MDIO data write register have been set up.
* A call to `mdio_wait_for_ready` is required for read transactions to
* ensure that the operation has completed before reading the MIDO data read
* register.
*/
void mdio_start_transaction()
{
auto &mdioControl = mmio_register<RegisterOffset::MDIOControl>();
Debug::Assert(((mdioControl & ((1 << 3) | 1)) == 0), "MDIO is busy");
// Write the MDIO enable bit and the start bit.
mdioControl = (1 << 3) | 1;
}
/**
* Returns a pointer to one of the receive buffers, identified by the buffer
* identifier (ping or pong).
*/
uint8_t *receive_buffer_pointer(BufferID index = BufferID::Ping)
{
auto buffer = mmio_region();
buffer.address() +=
static_cast<size_t>(index == BufferID::Ping ? 0x2000 : 0x2800);
buffer.bounds() = 0x800;
return const_cast<uint8_t *>(
reinterpret_cast<volatile uint8_t *>((buffer.get())));
}
/**
* Returns a pointer to one of the receive buffers, identified by the buffer
* identifier (ping or pong).
*/
uint8_t *transmit_buffer_pointer(BufferID index = BufferID::Ping)
{
auto buffer = mmio_region();
buffer.address() +=
static_cast<size_t>(index == BufferID::Ping ? 0x1000 : 0x1800);
buffer.bounds() = 0x800;
return const_cast<uint8_t *>(
reinterpret_cast<volatile uint8_t *>((buffer.get())));
}
/**
* Write a value to a PHY register via MDIO.
*/
void
mdio_write(uint8_t phyAddress, PHYRegister registerAddress, uint16_t data)
{
mdio_wait_for_ready();
auto &mdioAddress = mmio_register<RegisterOffset::MDIOAddress>();
auto &mdioWrite = mmio_register<RegisterOffset::MDIODataWrite>();
uint32_t writeCommand =
(0 << 10) | (phyAddress << 5) | uint32_t(registerAddress);
mdioAddress = writeCommand;
mdioWrite = data;
mdio_start_transaction();
}
/**
* Read a value from a PHY register via MDIO.
*/
uint16_t mdio_read(uint8_t phyAddress, PHYRegister registerAddress)
{
mdio_wait_for_ready();
auto &mdioAddress = mmio_register<RegisterOffset::MDIOAddress>();
uint32_t readCommand =
(1 << 10) | (phyAddress << 5) | uint8_t(registerAddress);
mdioAddress = readCommand;
mdio_start_transaction();
mdio_wait_for_ready();
return mmio_register<RegisterOffset::MDIODataRead>();
}
public:
/**
* Initialise a reference to the Ethernet device. This will check the ID
* registers to make sure that this is the kind of device that we're
* expecting.
*/
KunyanEthernet()
{
phy_reset();
// Check that this is the device that we're looking for
Debug::Assert(
[&]() { return mdio_read(1, PHYRegister::Identifier1) == 0x2000; },
"PHY identifier 1 is not 0x2000");
Debug::Assert(
[&]() {
return (mdio_read(1, PHYRegister::Identifier2) & 0xFFF0) ==
0x5c90;
},
"PHY identifier 1 is not 0x5c9x");
autonegotiation_enable();
filter_mode_update(FilterMode::DropInvalidCRC, true);
filter_mode_update(FilterMode::EnableAddressFiltering, false);
filter_mode_update(FilterMode::AllowUnicast, false);
filter_mode_update(FilterMode::AllowIPv4Multicast, false);
filter_mode_update(FilterMode::AllowIPv6Multicast, false);
receiveInterruptFutex =
interrupt_futex_get(STATIC_SEALED_VALUE(EthernetReceive));
// Enable receive interrupts
mmio_register<RegisterOffset::GlobalInterruptEnable>() = 0b10;
// Clear pending receive interrupts.
mmio_register<RegisterOffset::InterruptStatus>() = 0b10;
}
KunyanEthernet(const KunyanEthernet &) = delete;
KunyanEthernet(KunyanEthernet &&) = delete;
/**
* This device does not have a unique MAC address and so users must provide
* a locally administered MAC address if more than one device is present on
* the same network.
*/
static constexpr bool has_unique_mac_address()
{
return false;
}
static constexpr MACAddress mac_address_default()
{
return {0x60, 0x6A, 0x6A, 0x37, 0x47, 0x88};
}
void mac_address_set(MACAddress address = mac_address_default())
{
auto &macAddressHigh = mmio_register<RegisterOffset::MACAddressHigh>();
auto &macAddressLow = mmio_register<RegisterOffset::MACAddressLow>();
macAddressHigh = (address[2] << 24) | (address[3] << 16) |
(address[4] << 8) | address[5];
macAddressLow = (address[1] << 8) | address[0];
}
uint32_t receive_interrupt_value()
{
return *receiveInterruptFutex;
}
int receive_interrupt_complete(Timeout *timeout,
uint32_t lastInterruptValue)
{
// Clear the interrupt on the device.
auto &interruptStatus =
mmio_register<RegisterOffset::InterruptStatus>();
interruptStatus = 0b10;
// There's a small window in between finishing checking the receive
// buffers and clearing the interrupt where we could miss the
// interrupt. Check that the receive buffers are empty *after*
// reenabling the interrupt to ensure that we don't sleep in this
// period.
if (check_frame(BufferID::Ping) || check_frame(BufferID::Pong))
{
Debug::log("Packets already ready, not waiting for interrupt");
return 0;
}
// Acknowledge the interrupt in the scheduler.
interrupt_complete(STATIC_SEALED_VALUE(EthernetReceive));
if (*receiveInterruptFutex == lastInterruptValue)
{
Debug::log("Acknowledged interrupt, sleeping on futex {}",
receiveInterruptFutex);
return futex_timed_wait(
timeout, receiveInterruptFutex, lastInterruptValue);
}
Debug::log("Scheduler announces interrupt has fired");
return 0;
}
/**
* Enable autonegotiation on the PHY.
*
* FIXME: This blocks forever if the cable is disconnected!
*/
void autonegotiation_enable(uint8_t phyAddress = 1)
{
Debug::log("Starting autonegotiation");
// Advertise 802.3, 10Base-T full and half duplex
mdio_write(phyAddress,
PHYRegister::AutoNegotiationAdvertisement,
(1 << 5) | (1 << 6) | 1);
// Enable and restart autonegitiation
mdio_write(
phyAddress, PHYRegister::BasicControlRegister, (1 << 12) | (1 << 9));
Debug::log("Waiting for autonegotiation to complete");
uint32_t loops = 0;
while ((mdio_read(phyAddress, PHYRegister::BasicStatusRegister) &
(1 << 5)) == 0)
{
if ((loops++ & 0xfffff) == 0)
{
Debug::log(
"Waiting for autonegotiation to complete ({} loops): status: "
"{}",
loops,
mdio_read(phyAddress, PHYRegister::BasicStatusRegister));
}
yield();
}
Debug::Assert(
[&]() {
return (mdio_read(phyAddress, PHYRegister::BasicStatusRegister) &
(1 << 2)) != 0;
},
"Autonegotiation completed but link is down");
Debug::log("Autonegotiation complete, status: {}",
mdio_read(phyAddress, PHYRegister::BasicStatusRegister));
// Write 1 to clear the receive status.
mmio_register<RegisterOffset::ReceiveControlPing>() = 1;
mmio_register<RegisterOffset::ReceiveControlPong>() = 1;
}
/**
* Check the link status of the PHY.
*/
bool phy_link_status()
{
return (mdio_read(1, PHYRegister::BasicStatusRegister) & (1 << 2)) != 0;
}
std::optional<uint16_t> check_frame(BufferID index = BufferID::Ping)
{
// Debug::log("Checking for frame in buffer {}", index);
auto receiveControl =
index == BufferID::Ping
? mmio_register<RegisterOffset::ReceiveControlPing>()
: mmio_register<RegisterOffset::ReceiveControlPong>();
if ((receiveControl & 1) == 0)
{
return std::nullopt;
}
Debug::log("Buffer has a frame. Error? {}", receiveControl & 2);
Debug::log("Buffer length: {}", receiveControl >> 16);
return {receiveControl >> 16};
}
void complete_receive(BufferID index = BufferID::Ping)
{
Debug::log("Completing receive in buffer {}", index);
// This buffer is now free to receive another frame.
receiveBufferInUse[index] = false;
if (index == BufferID::Ping)
{
mmio_register<RegisterOffset::ReceiveControlPing>() = 1;
}
else
{
mmio_register<RegisterOffset::ReceiveControlPong>() = 1;
}
}
/**
* Class encapsulating a frame that has been received. This holds a
* reference to the internal state of the buffer and will mark the buffer
* as free when it is destroyed.
*/
class BufferedFrame
{
friend class KunyanEthernet;
KunyanEthernet &ethernetAdaptor;
BufferID owningBuffer;
private:
BufferedFrame(KunyanEthernet &ethernetAdaptor,
BufferID owningBuffer,
Capability<uint8_t> buffer,
uint16_t length)
: ethernetAdaptor(ethernetAdaptor),
owningBuffer(owningBuffer),
buffer(buffer),
length(length)
{
// Mark this buffer as in use and try to advance the next buffer
ethernetAdaptor.receiveBufferInUse[owningBuffer] = true;
if (!ethernetAdaptor.receiveBufferInUse[next_buffer_id(
ethernetAdaptor.nextReceiveBuffer)])
{
ethernetAdaptor.nextReceiveBuffer =
next_buffer_id(ethernetAdaptor.nextReceiveBuffer);
}
}
public:
uint16_t length;
Capability<uint8_t> buffer;
BufferedFrame(const BufferedFrame &) = delete;
BufferedFrame(BufferedFrame &&other)
: ethernetAdaptor(other.ethernetAdaptor),
owningBuffer(other.owningBuffer),
buffer(other.buffer),
length(other.length)
{
other.buffer = nullptr;
other.length = 0;
}
~BufferedFrame()
{
if (buffer != nullptr)
{
ethernetAdaptor.complete_receive(owningBuffer);
}
}
};
std::optional<BufferedFrame> receive_frame()
{
// To avoid processing the same packet twice, we must not use
// the current receive buffer if it is already in use. In that
// case, check the next receive buffer. If it is not in use,
// rotate the buffers. Otherwise, abandon.
if (receiveBufferInUse[nextReceiveBuffer])
{
if (!receiveBufferInUse[next_buffer_id(nextReceiveBuffer)])
{
// The next receive buffer is not in use. Rotate.
nextReceiveBuffer = next_buffer_id(nextReceiveBuffer);
}
else
{
// The next receive buffer is in use too. Abandon.
return std::nullopt;
}
}
auto maybeLength = check_frame(nextReceiveBuffer);
if (!maybeLength)
{
// The current receive buffer is empty. However, the next
// receive buffer, if not in use, may have a packet ready for
// us. Check that one as well.
if (!receiveBufferInUse[next_buffer_id(nextReceiveBuffer)])
{
// The next receive buffer is not in use. Check
// for packets there as well.
nextReceiveBuffer = next_buffer_id(nextReceiveBuffer);
maybeLength = check_frame(nextReceiveBuffer);
}
if (!maybeLength)
{
// None of the buffers has packets that we can
// process. Abandon.
return std::nullopt;
}
}
auto length = *maybeLength;
// Strip the FCS from the length.
length -= 4;
auto buffer = receive_buffer_pointer(nextReceiveBuffer);
Capability<uint8_t> boundedBuffer{buffer};
boundedBuffer.bounds() = length;
// Remove all permissions except load. This also removes global, so
// that this cannot be captured.
boundedBuffer.permissions() &=
CHERI::PermissionSet{CHERI::Permission::Load};
Debug::log("Received frame from buffer {}", nextReceiveBuffer);
return {{*this, nextReceiveBuffer, buffer, length}};
}
/**
* Send a packet. This is synchronous and will block until the packet has
* been sent. A better version would use the next available ping or pong
* buffer and return as soon as the send operation had been enqueued.
*
* The third argument is a callback that allows the caller to check the
* frame before it's sent but after it's copied into memory that isn't
* shared with other compartments.
*/
bool send_frame(const uint8_t *buffer, uint16_t length, auto &&check)
{
auto &transmitControl =
mmio_register<RegisterOffset::TransmitControlPing>();
// Spin waiting for the transmit buffer to be free.
while (transmitControl & 1) {}
// Write the frame to the transmit buffer.
auto transmitBuffer = transmit_buffer_pointer();
// We must check the frame pointer and its length. Although it
// is supplied by the firewall which is trusted, the firewall
// does not check the pointer which is coming from external
// untrusted components.
Timeout t{10};
if ((heap_claim_fast(&t, buffer) < 0) ||
(!CHERI::check_pointer<CHERI::PermissionSet{
CHERI::Permission::Load}>(buffer, length)))
{
return false;
}
memcpy(transmitBuffer, buffer, length);
if (!check(transmitBuffer, length))
{
return false;
}
// The Ethernet standard requires frames to be at least 60 bytes long.
// If we're asked to send anything shorter, pad it with zeroes.
// (It would be nice if the MAC did this automatically).
if (length < 60)
{
memset(transmitBuffer + length, 0, 60 - length);
length = 60;
}
// Write the length of the frame to the transmit length register.
mmio_register<RegisterOffset::TransmitFrameLengthPing>() = length;
// Start the transmit.
transmitControl = 1;
Debug::log("Sent frame, waiting for completion");
while (transmitControl & 1) {}
// Return if the frame was sent successfully.
Debug::log("Transmit control register: {}", transmitControl);
if ((transmitControl & 2) != 0)
{
Debug::log("Error sending frame");
}
return (transmitControl & 2) == 0;
}
/**
* If debugging dropped frames is enabled, log any counter values that have
* changed since the last call to this function.
*/
void dropped_frames_log_all_if_changed()
{
if constexpr (DebugDroppedFrames)
{
dropped_frames_log_if_changed<
RegisterOffset::ReceiveDroppedFramesBuffersFull>(droppedFrames);
dropped_frames_log_if_changed<
RegisterOffset::ReceiveDroppedFramesFCSFailed>(droppedFrames);
dropped_frames_log_if_changed<
RegisterOffset::ReceiveDroppedFramesInvalidAddress>(
droppedFrames);
}
}
void received_frames_log()
{
auto count = mmio_register<RegisterOffset::ReceivedFramesCount>();
ConditionalDebug<true, "Ethernet driver">::log("Received frames: {}",
count);
}
};
using EthernetDevice = KunyanEthernet;
static_assert(EthernetAdaptor<EthernetDevice>);