sonata: ethernet driver now uses SPI block CS lines

The SPI block now has built-in chip select outputs; one no longer has to
rely on a seperate, and possibly shared, GPIO block.

Note, the lock around the CS line changes is likely superflous now, but
has been left in for the time being.

Co-authored-by: Alex Jones <alex.jones@lowrisc.org>
diff --git a/sdk/include/platform/sunburst/platform-ethernet.hh b/sdk/include/platform/sunburst/platform-ethernet.hh
new file mode 100644
index 0000000..64a4f95
--- /dev/null
+++ b/sdk/include/platform/sunburst/platform-ethernet.hh
@@ -0,0 +1,795 @@
+#pragma once
+#include <array>
+#include <cheri.hh>
+#include <cstddef>
+#include <cstdint>
+#include <debug.hh>
+#include <futex.h>
+#include <interrupt.h>
+#include <locks.hh>
+#include <optional>
+#include <platform/concepts/ethernet.hh>
+#include <platform/sunburst/platform-spi.hh>
+#include <thread.h>
+#include <type_traits>
+
+DECLARE_AND_DEFINE_INTERRUPT_CAPABILITY(EthernetInterruptCapability,
+                                        InterruptName::EthernetInterrupt,
+                                        true,
+                                        true);
+
+/**
+ * The driver for KSZ8851 SPI Ethernet MAC.
+ */
+class Ksz8851Ethernet
+{
+	/**
+	 * 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 = true;
+
+	/**
+	 * Maxmium size of a single Ethernet frame.
+	 */
+	static constexpr uint16_t MaxFrameSize = 1500;
+
+	/**
+	 * Helper for conditional debug logs and assertions.
+	 */
+	using Debug = ConditionalDebug<DebugEthernet, "Ethernet driver">;
+
+	/**
+	 * Helper for conditional debug logs and assertions for dropped frames.
+	 */
+	using DebugFrameDrops =
+	  ConditionalDebug<DebugDroppedFrames, "Ethernet driver">;
+
+	/**
+	 * Import the Capability helper from the CHERI namespace.
+	 */
+	template<typename T>
+	using Capability = CHERI::Capability<T>;
+
+	/**
+	 * SPI commands
+	 */
+	enum class SpiCommand : uint8_t
+	{
+		ReadRegister  = 0b00,
+		WriteRegister = 0b01,
+		// DMA in this context means that the Ethernet MAC is DMA directly
+		// from the SPI interface into its internal buffer, so it takes single
+		// SPI transaction for the entire frame. It is unrelated to whether
+		// SPI driver uses PIO or DMA for the SPI transaction.
+		ReadDma  = 0b10,
+		WriteDma = 0b11,
+	};
+
+	/**
+	 * The location of registers
+	 */
+	enum class RegisterOffset : uint8_t
+	{
+		ChipConfiguration = 0x08,
+		MacAddressLow     = 0x10,
+		MacAdressMiddle   = 0x12,
+		MacAddressHigh    = 0x14,
+		OnChipBusControl  = 0x20,
+		EepromControl     = 0x22,
+		MemoryBistInfo    = 0x24,
+		GlobalReset       = 0x26,
+
+		/* Wakeup frame registers omitted */
+
+		TransmitControl               = 0x70,
+		TransmitStatus                = 0x72,
+		ReceiveControl1               = 0x74,
+		ReceiveControl2               = 0x76,
+		TransmitQueueMemoryInfo       = 0x78,
+		ReceiveFrameHeaderStatus      = 0x7C,
+		ReceiveFrameHeaderByteCount   = 0x7E,
+		TransmitQueueCommand          = 0x80,
+		ReceiveQueueCommand           = 0x82,
+		TransmitFrameDataPointer      = 0x84,
+		ReceiveFrameDataPointer       = 0x86,
+		ReceiveDurationTimerThreshold = 0x8C,
+		ReceiveDataByteCountThreshold = 0x8E,
+		InterruptEnable               = 0x90,
+		InterruptStatus               = 0x92,
+		ReceiveFrameCountThreshold    = 0x9c,
+		TransmitNextTotalFrameSize    = 0x9E,
+
+		/* MAC address hash table registers omitted */
+
+		FlowControlLowWatermark               = 0xB0,
+		FlowControlHighWatermark              = 0xB2,
+		FlowControlOverrunWatermark           = 0xB4,
+		ChipIdEnable                          = 0xC0,
+		ChipGlobalControl                     = 0xC6,
+		IndirectAccessControl                 = 0xC8,
+		IndirectAccessDataLow                 = 0xD0,
+		IndirectAccessDataHigh                = 0xD2,
+		PowerManagementEventControl           = 0xD4,
+		GoSleepWakeUp                         = 0xD4,
+		PhyReset                              = 0xD4,
+		Phy1MiiBasicControl                   = 0xE4,
+		Phy1MiiBasicStatus                    = 0xE6,
+		Phy1IdLow                             = 0xE8,
+		Phy1High                              = 0xEA,
+		Phy1AutoNegotiationAdvertisement      = 0xEC,
+		Phy1AutoNegotiationLinkPartnerAbility = 0xEE,
+		Phy1SpecialControlStatus              = 0xF4,
+		Port1Control                          = 0xF6,
+		Port1Status                           = 0xF8,
+	};
+
+	using MACAddress = std::array<uint8_t, 6>;
+
+	/**
+	 * Flag bits of the TransmitControl register.
+	 */
+	enum [[clang::flag_enum]] TransmitControl : uint16_t{
+	  TransmitEnable                 = 1 << 0,
+	  TransmitCrcEnable              = 1 << 1,
+	  TransmitPaddingEnable          = 1 << 2,
+	  TransmitFlowControlEnable      = 1 << 3,
+	  FlushTransmitQueue             = 1 << 4,
+	  TransmitChecksumGenerationIp   = 1 << 5,
+	  TransmitChecksumGenerationTcp  = 1 << 6,
+	  TransmitChecksumGenerationIcmp = 1 << 9,
+	};
+
+	/**
+	 * Flag bits of the ReceiveControl1 register.
+	 */
+	enum [[clang::flag_enum]] ReceiveControl1 : uint16_t{
+	  ReceiveEnable                                        = 1 << 0,
+	  ReceiveInverseFilter                                 = 1 << 1,
+	  ReceiveAllEnable                                     = 1 << 4,
+	  ReceiveUnicastEnable                                 = 1 << 5,
+	  ReceiveMulticastEnable                               = 1 << 6,
+	  ReceiveBroadcastEnable                               = 1 << 7,
+	  ReceiveMulticastAddressFilteringWithMacAddressEnable = 1 << 8,
+	  ReceiveErrorFrameEnable                              = 1 << 9,
+	  ReceiveFlowControlEnable                             = 1 << 10,
+	  ReceivePhysicalAddressFilteringWithMacAddressEnable  = 1 << 11,
+	  ReceiveIpFrameChecksumCheckEnable                    = 1 << 12,
+	  ReceiveTcpFrameChecksumCheckEnable                   = 1 << 13,
+	  ReceiveUdpFrameChecksumCheckEnable                   = 1 << 14,
+	  FlushReceiveQueue                                    = 1 << 15,
+	};
+
+	/**
+	 * Flag bits of the ReceiveControl2 register.
+	 */
+	enum [[clang::flag_enum]] ReceiveControl2 : uint16_t{
+	  ReceiveSourceAddressFiltering            = 1 << 0,
+	  ReceiveIcmpFrameChecksumEnable           = 1 << 1,
+	  UdpLiteFrameEnable                       = 1 << 2,
+	  ReceiveIpv4Ipv6UdpFrameChecksumEqualZero = 1 << 3,
+	  ReceiveIpv4Ipv6FragmentFramePass         = 1 << 4,
+	  DataBurst4Bytes                          = 0b000 << 5,
+	  DataBurst8Bytes                          = 0b001 << 5,
+	  DataBurst16Bytes                         = 0b010 << 5,
+	  DataBurst32Bytes                         = 0b011 << 5,
+	  DataBurstSingleFrame                     = 0b100 << 5,
+	};
+
+	/**
+	 * Flag bits of the ReceiveFrameHeaderStatus register.
+	 */
+	enum [[clang::flag_enum]] ReceiveFrameHeaderStatus : uint16_t{
+	  ReceiveCrcError                = 1 << 0,
+	  ReceiveRuntFrame               = 1 << 1,
+	  ReceiveFrameTooLong            = 1 << 2,
+	  ReceiveFrameType               = 1 << 3,
+	  ReceiveMiiError                = 1 << 4,
+	  ReceiveUnicastFrame            = 1 << 5,
+	  ReceiveMulticastFrame          = 1 << 6,
+	  ReceiveBroadcastFrame          = 1 << 7,
+	  ReceiveUdpFrameChecksumStatus  = 1 << 10,
+	  ReceiveTcpFrameChecksumStatus  = 1 << 11,
+	  ReceiveIpFrameChecksumStatus   = 1 << 12,
+	  ReceiveIcmpFrameChecksumStatus = 1 << 13,
+	  ReceiveFrameValid              = 1 << 15,
+	};
+
+	/**
+	 * Flag bits of the ReceiveQueueCommand register.
+	 */
+	enum [[clang::flag_enum]] ReceiveQueueCommand : uint16_t{
+	  ReleaseReceiveErrorFrame            = 1 << 0,
+	  StartDmaAccess                      = 1 << 3,
+	  AutoDequeueReceiveQueueFrameEnable  = 1 << 4,
+	  ReceiveFrameCountThresholdEnable    = 1 << 5,
+	  ReceiveDataByteCountThresholdEnable = 1 << 6,
+	  ReceiveDurationTimerThresholdEnable = 1 << 7,
+	  ReceiveIpHeaderTwoByteOffsetEnable  = 1 << 9,
+	  ReceiveFrameCountThresholdStatus    = 1 << 10,
+	  ReceiveDataByteCountThresholdstatus = 1 << 11,
+	  ReceiveDurationTimerThresholdStatus = 1 << 12,
+	};
+
+	/**
+	 * Flag bits of the TransmitQueueCommand register.
+	 */
+	enum [[clang::flag_enum]] TransmitQueueCommand : uint16_t{
+	  ManualEnqueueTransmitQueueFrameEnable = 1 << 0,
+	  TransmitQueueMemoryAvailableMonitor   = 1 << 1,
+	  AutoEnqueueTransmitQueueFrameEnable   = 1 << 2,
+	};
+
+	/**
+	 * Flag bits of the TransmitFrameDataPointer and ReceiveFrameDataPointer
+	 * register.
+	 */
+	enum [[clang::flag_enum]] FrameDataPointer : uint16_t{
+	  /**
+	   * When this bit is set, the frame data pointer register increments
+	   * automatically on accesses to the data register.
+	   */
+	  FrameDataPointerAutoIncrement = 1 << 14,
+	};
+
+	/**
+	 * Flags bits of the InterruptStatus and InterruptEnable registers.
+	 */
+	enum [[clang::flag_enum]] Interrupt : uint16_t{
+	  EnergyDetectInterrupt             = 1 << 2,
+	  LinkupDetectInterrupt             = 1 << 3,
+	  ReceiveMagicPacketDetectInterrupt = 1 << 4,
+	  ReceiveWakeupFrameDetectInterrupt = 1 << 5,
+	  TransmitSpaceAvailableInterrupt   = 1 << 6,
+	  ReceiveProcessStoppedInterrupt    = 1 << 7,
+	  TransmitProcessStoppedInterrupt   = 1 << 8,
+	  ReceiveOverrunInterrupt           = 1 << 11,
+	  ReceiveInterrupt                  = 1 << 13,
+	  TransmitInterrupt                 = 1 << 14,
+	  LinkChangeInterruptStatus         = 1 << 15,
+	};
+
+	/**
+	 * Flags bits of the Port1Control register.
+	 */
+	enum [[clang::flag_enum]] Port1Control : uint16_t{
+	  Advertised10BTHalfDuplexCapability  = 1 << 0,
+	  Advertised10BTFullDuplexCapability  = 1 << 1,
+	  Advertised100BTHalfDuplexCapability = 1 << 2,
+	  Advertised100BTFullDuplexCapability = 1 << 3,
+	  AdvertisedFlowControlCapability     = 1 << 4,
+	  ForceDuplex                         = 1 << 5,
+	  ForceSpeed                          = 1 << 6,
+	  AutoNegotiationEnable               = 1 << 7,
+	  ForceMDIX                           = 1 << 9,
+	  DisableAutoMDIMDIX                  = 1 << 10,
+	  RestartAutoNegotiation              = 1 << 13,
+	  TransmitterDisable                  = 1 << 14,
+	  LedOff                              = 1 << 15,
+	};
+
+	/**
+	 * Flags bits of the Port1Status register.
+	 */
+	enum [[clang::flag_enum]] Port1Status : uint16_t{
+	  Partner10BTHalfDuplexCapability  = 1 << 0,
+	  Partner10BTFullDuplexCapability  = 1 << 1,
+	  Partner100BTHalfDuplexCapability = 1 << 2,
+	  Partner100BTFullDuplexCapability = 1 << 3,
+	  PartnerFlowControlCapability     = 1 << 4,
+	  LinkGood                         = 1 << 5,
+	  AutoNegotiationDone              = 1 << 6,
+	  MDIXStatus                       = 1 << 7,
+	  OperationDuplex                  = 1 << 9,
+	  OperationSpeed                   = 1 << 10,
+	  PolarityReverse                  = 1 << 13,
+	  HPMDIX                           = 1 << 15,
+	};
+
+	/**
+	 * The futex used to wait for interrupts when packets are available to
+	 * receive.
+	 */
+	const uint32_t *receiveInterruptFutex;
+
+	/**
+	 * Read a register from the KSZ8851.
+	 */
+	[[nodiscard]] uint16_t register_read(RegisterOffset reg) const
+	{
+		// KSZ8851 command have the following format:
+		//
+		// First byte:
+		// +---------+-------------+-------------------+
+		// | 7     6 | 5         2 | 1               0 |
+		// +---------+-------------+-------------------+
+		// | Command | Byte Enable | Address (bit 7-6) |
+		// +---------+-------------+-------------------+
+		//
+		// Second byte (for register read/write only):
+		// +-------------------+--------+
+		// | 7               4 | 3    0 |
+		// +-------------------+--------+
+		// | Address (bit 5-2) | Unused |
+		// +-------------------+--------+
+		//
+		// Note that the access is 32-bit since bit 1 & 0 of the address is not
+		// included. KSZ8851 have 16-bit registers so byte enable is used to
+		// determine which register to access from the 32 bits specified by the
+		// address.
+		uint8_t addr       = static_cast<uint8_t>(reg);
+		uint8_t byteEnable = (addr & 0x2) == 0 ? 0b0011 : 0b1100;
+		uint8_t bytes[2];
+		bytes[0] = (static_cast<uint8_t>(SpiCommand::ReadRegister) << 6) |
+		           (byteEnable << 2) | (addr >> 6);
+		bytes[1] = (addr << 2) & 0b11110000;
+
+		spi()->chip_select_assert(true);
+		spi()->blocking_write(bytes, sizeof(bytes));
+		uint16_t val;
+		spi()->blocking_read(reinterpret_cast<uint8_t *>(&val), sizeof(val));
+		spi()->chip_select_assert(false);
+		return val;
+	}
+
+	/**
+	 * Write a register to KSZ8851.
+	 */
+	void register_write(RegisterOffset reg, uint16_t val) const
+	{
+		// See register_read for command format.
+		uint8_t addr       = static_cast<uint8_t>(reg);
+		uint8_t byteEnable = (addr & 0x2) == 0 ? 0b0011 : 0b1100;
+		uint8_t bytes[2];
+		bytes[0] = (static_cast<uint8_t>(SpiCommand::WriteRegister) << 6) |
+		           (byteEnable << 2) | (addr >> 6);
+		bytes[1] = (addr << 2) & 0b11110000;
+
+		spi()->chip_select_assert(true);
+		spi()->blocking_write(bytes, sizeof(bytes));
+		spi()->blocking_write(reinterpret_cast<uint8_t *>(&val), sizeof(val));
+		spi()->wait_idle();
+		spi()->chip_select_assert(false);
+	}
+
+	/**
+	 * Set bits in a KSZ8851 register.
+	 */
+	void register_set(RegisterOffset reg, uint16_t mask) const
+	{
+		uint16_t old = register_read(reg);
+		register_write(reg, old | mask);
+	}
+
+	/**
+	 * Clear bits in a KSZ8851 register.
+	 */
+	void register_clear(RegisterOffset reg, uint16_t mask) const
+	{
+		uint16_t old = register_read(reg);
+		register_write(reg, old & ~mask);
+	}
+
+	/**
+	 * Helper. Returns a pointer to the SPI device.
+	 */
+	[[nodiscard,
+	  gnu::always_inline]] Capability<volatile SonataSpi::EthernetMac>
+	spi() const
+	{
+		return MMIO_CAPABILITY(SonataSpi::EthernetMac, spi_ethmac);
+	}
+
+	/**
+	 * Number of frames yet to be received since last interrupt acknowledgement.
+	 */
+	uint16_t framesToProcess = 0;
+
+	/**
+	 * Mutex protecting transmitBuffer if send_frame is reentered.
+	 */
+	RecursiveMutex transmitBufferMutex;
+
+	/**
+	 * Buffer used by send_frame.
+	 */
+	std::unique_ptr<uint8_t[]> transmitBuffer;
+
+	/**
+	 * Mutex protecting receiveBuffer if receive_frame is called before a
+	 * previous returned frame is dropped.
+	 */
+	RecursiveMutex receiveBufferMutex;
+
+	/**
+	 * Lock to protect reads/writes to the SPI Chip Selects, which use the
+	 * same bits of the MMIO region and thus need to be protected.
+	 */
+	FlagLockPriorityInherited chipSelectLock;
+
+	/**
+	 * Buffer used by receive_frame.
+	 */
+	std::unique_ptr<uint8_t[]> receiveBuffer;
+
+	public:
+	/**
+	 * Initialise a reference to the Ethernet device.
+	 */
+	Ksz8851Ethernet()
+	{
+		transmitBuffer = std::make_unique<uint8_t[]>(MaxFrameSize);
+		receiveBuffer  = std::make_unique<uint8_t[]>(MaxFrameSize);
+
+		// Reset chip. It needs to be hold in reset for at least 10ms.
+		spi()->reset_assert(true);
+		thread_millisecond_wait(20);
+		spi()->reset_assert(false);
+
+		uint16_t chipId = register_read(RegisterOffset::ChipIdEnable);
+		Debug::log("Chip ID is {}", chipId);
+
+		// Check the chip ID. The last nibble is revision ID and can be ignored.
+		Debug::Assert((chipId & 0xFFF0) == 0x8870, "Unexpected Chip ID");
+
+		// This is the initialisation sequence suggested by the programmer's
+		// guide.
+		register_write(RegisterOffset::TransmitFrameDataPointer,
+		               FrameDataPointer::FrameDataPointerAutoIncrement);
+		register_write(RegisterOffset::TransmitControl,
+		               TransmitControl::TransmitCrcEnable |
+		                 TransmitControl::TransmitPaddingEnable |
+		                 TransmitControl::TransmitFlowControlEnable |
+		                 TransmitControl::TransmitChecksumGenerationIp |
+		                 TransmitControl::TransmitChecksumGenerationTcp |
+		                 TransmitControl::TransmitChecksumGenerationIcmp);
+		register_write(RegisterOffset::ReceiveFrameDataPointer,
+		               FrameDataPointer::FrameDataPointerAutoIncrement);
+		// Configure Receive Frame Threshold for one frame.
+		register_write(RegisterOffset::ReceiveFrameCountThreshold, 0x0001);
+		register_write(RegisterOffset::ReceiveControl1,
+		               ReceiveControl1::ReceiveUnicastEnable |
+		                 ReceiveControl1::ReceiveMulticastEnable |
+		                 ReceiveControl1::ReceiveBroadcastEnable |
+		                 ReceiveControl1::ReceiveFlowControlEnable |
+		                 ReceiveControl1::
+		                   ReceivePhysicalAddressFilteringWithMacAddressEnable |
+		                 ReceiveControl1::ReceiveIpFrameChecksumCheckEnable |
+		                 ReceiveControl1::ReceiveTcpFrameChecksumCheckEnable |
+		                 ReceiveControl1::ReceiveUdpFrameChecksumCheckEnable);
+		// The frame data burst field in this register controls how many data
+		// from a frame is read per DMA operation. The programmer's guide has a
+		// 4 byte burst, but to reduce SPI transactions and improve performance
+		// we choose to use single-frame data burst which reads the entire
+		// Ethernet frame in a single SPI DMA.
+		register_write(
+		  RegisterOffset::ReceiveControl2,
+		  ReceiveControl2::UdpLiteFrameEnable |
+		    ReceiveControl2::ReceiveIpv4Ipv6UdpFrameChecksumEqualZero |
+		    ReceiveControl2::ReceiveIpv4Ipv6FragmentFramePass |
+		    ReceiveControl2::DataBurstSingleFrame);
+		register_write(
+		  RegisterOffset::ReceiveQueueCommand,
+		  ReceiveQueueCommand::ReceiveFrameCountThresholdEnable |
+		    ReceiveQueueCommand::AutoDequeueReceiveQueueFrameEnable);
+
+		// Programmer's guide have a step to set the chip in half-duplex when
+		// negotiation failed, but we omit the step since non-switching hubs and
+		// half-duplex Ethernet is rarely used these days.
+
+		register_set(RegisterOffset::Port1Control,
+		             Port1Control::RestartAutoNegotiation);
+
+		// Configure Low Watermark to 6KByte available buffer space out of
+		// 12KByte (unit is 4 bytes).
+		register_write(RegisterOffset::FlowControlLowWatermark, 0x0600);
+		// Configure High Watermark to 4KByte available buffer space out of
+		// 12KByte (unit is 4 bytes).
+		register_write(RegisterOffset::FlowControlHighWatermark, 0x0400);
+
+		// Clear the interrupt status
+		register_write(RegisterOffset::InterruptStatus, 0xFFFF);
+		receiveInterruptFutex =
+		  interrupt_futex_get(STATIC_SEALED_VALUE(EthernetInterruptCapability));
+		// Enable Receive interrupt
+		register_write(RegisterOffset::InterruptEnable, ReceiveInterrupt);
+
+		// Enable QMU Transmit.
+		register_set(RegisterOffset::TransmitControl,
+		             TransmitControl::TransmitEnable);
+		// Enable QMU Receive.
+		register_set(RegisterOffset::ReceiveControl1,
+		             ReceiveControl1::ReceiveEnable);
+	}
+
+	Ksz8851Ethernet(const Ksz8851Ethernet &) = delete;
+	Ksz8851Ethernet(Ksz8851Ethernet &&)      = 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 {0x3a, 0x30, 0x25, 0x24, 0xfe, 0x7a};
+	}
+
+	void mac_address_set(MACAddress address = mac_address_default())
+	{
+		register_write(RegisterOffset::MacAddressHigh,
+		               (address[0] << 8) | address[1]);
+		register_write(RegisterOffset::MacAdressMiddle,
+		               (address[2] << 8) | address[3]);
+		register_write(RegisterOffset::MacAddressLow,
+		               (address[4] << 8) | address[5]);
+	}
+
+	uint32_t receive_interrupt_value()
+	{
+		return *receiveInterruptFutex;
+	}
+
+	int receive_interrupt_complete(Timeout *timeout,
+	                               uint32_t lastInterruptValue)
+	{
+		// If there are frames to process, do not enter wait.
+		if (framesToProcess)
+		{
+			return 0;
+		}
+
+		// Our interrupt is level-triggered; if a frame happens to arrive
+		// between `receive_frame` call and we marking interrupt as received,
+		// it will trigger again immediately after we acknowledge it.
+
+		// Acknowledge the interrupt in the scheduler.
+		interrupt_complete(STATIC_SEALED_VALUE(EthernetInterruptCapability));
+		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;
+	}
+
+	/**
+	 * Simple class representing a received Ethernet frame.
+	 */
+	class Frame
+	{
+		public:
+		uint16_t            length;
+		Capability<uint8_t> buffer;
+
+		private:
+		friend class Ksz8851Ethernet;
+		LockGuard<RecursiveMutex> guard;
+
+		Frame(LockGuard<RecursiveMutex> &&guard,
+		      Capability<uint8_t>         buffer,
+		      uint16_t                    length)
+		  : guard(std::move(guard)), buffer(buffer), length(length)
+		{
+		}
+	};
+
+	/**
+	 * Check the link status of the PHY.
+	 */
+	bool phy_link_status()
+	{
+		uint16_t status = register_read(RegisterOffset::Port1Status);
+		return (status & Port1Status::LinkGood) != 0;
+	}
+
+	std::optional<Frame> receive_frame()
+	{
+		LockGuard g{chipSelectLock};
+		if (framesToProcess == 0)
+		{
+			uint16_t isr = register_read(RegisterOffset::InterruptStatus);
+			if (!(isr & ReceiveInterrupt))
+			{
+				return std::nullopt;
+			}
+
+			// Acknowledge the interrupt
+			register_write(RegisterOffset::InterruptStatus, ReceiveInterrupt);
+
+			// Read number of frames pending.
+			// Note that this is only updated when we acknowledge the interrupt.
+			framesToProcess =
+			  register_read(RegisterOffset::ReceiveFrameCountThreshold) >> 8;
+		}
+
+		// Get number of frames pending
+		for (; framesToProcess; framesToProcess--)
+		{
+			uint16_t status =
+			  register_read(RegisterOffset::ReceiveFrameHeaderStatus);
+			uint16_t length =
+			  register_read(RegisterOffset::ReceiveFrameHeaderByteCount) &
+			  0xFFF;
+			bool valid =
+			  (status & ReceiveFrameValid) &&
+			  !(status &
+			    (ReceiveCrcError | ReceiveRuntFrame | ReceiveFrameTooLong |
+			     ReceiveMiiError | ReceiveUdpFrameChecksumStatus |
+			     ReceiveTcpFrameChecksumStatus | ReceiveIpFrameChecksumStatus |
+			     ReceiveIcmpFrameChecksumStatus));
+
+			if (!valid)
+			{
+				DebugFrameDrops::log("Dropping frame with status: {}", status);
+
+				drop_error_frame();
+				continue;
+			}
+
+			if (length == 0)
+			{
+				DebugFrameDrops::log("Dropping frame with zero length");
+
+				drop_error_frame();
+				continue;
+			}
+
+			// The DMA transfer to the Ethernet MAC must be a multiple of 4
+			// bytes.
+			uint16_t paddedLength = (length + 3) & ~0x3;
+			if (paddedLength > MaxFrameSize)
+			{
+				DebugFrameDrops::log("Dropping frame that is too large: {}",
+				                     length);
+
+				drop_error_frame();
+				continue;
+			}
+
+			Debug::log("Receiving frame of length {}", length);
+
+			LockGuard guard{receiveBufferMutex};
+
+			// Reset receive frame pointer to zero and start DMA transfer
+			// operation.
+			register_write(RegisterOffset::ReceiveFrameDataPointer,
+			               FrameDataPointer::FrameDataPointerAutoIncrement);
+			register_set(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
+
+			// Start receiving via SPI.
+			uint8_t cmd = static_cast<uint8_t>(SpiCommand::ReadDma) << 6;
+			spi()->chip_select_assert(true);
+			spi()->blocking_write(&cmd, 1);
+
+			// Initial words are ReceiveFrameHeaderStatus and
+			// ReceiveFrameHeaderByteCount which we have already know the value.
+			uint8_t dummy[8];
+			spi()->blocking_read(dummy, sizeof(dummy));
+
+			spi()->blocking_read(receiveBuffer.get(), paddedLength);
+
+			spi()->chip_select_assert(false);
+
+			register_clear(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
+			framesToProcess -= 1;
+
+			Capability<uint8_t> boundedBuffer{receiveBuffer.get()};
+			boundedBuffer.bounds().set_inexact(length);
+			// Remove all permissions except load.  This also removes global, so
+			// that this cannot be captured.
+			boundedBuffer.permissions() &=
+			  CHERI::PermissionSet{CHERI::Permission::Load};
+
+			return Frame{std::move(guard), boundedBuffer, length};
+		}
+
+		return std::nullopt;
+	}
+
+	/**
+	 * Send a packet.  This will block if no buffer space is available on
+	 * device.
+	 *
+	 * 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)
+	{
+		// The DMA transfer to the Ethernet MAC must be a multiple of 4 bytes.
+		uint16_t paddedLength = (length + 3) & ~0x3;
+		if (paddedLength > MaxFrameSize)
+		{
+			Debug::log("Frame size {} is larger than the maximum size", length);
+			return false;
+		}
+
+		LockGuard guard{transmitBufferMutex};
+
+		// 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.get(), buffer, length);
+		if (!check(transmitBuffer.get(), length))
+		{
+			return false;
+		}
+
+		LockGuard g{chipSelectLock};
+
+		// Wait for the transmit buffer to be available on the device side.
+		// This needs to include the header.
+		while ((register_read(RegisterOffset::TransmitQueueMemoryInfo) &
+		        0xFFF) < length + 4)
+		{
+		}
+
+		Debug::log("Sending frame of length {}", length);
+
+		// Start DMA transfer operation.
+		register_set(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
+
+		// Start sending via SPI.
+		uint8_t cmd = static_cast<uint8_t>(SpiCommand::WriteDma) << 6;
+		spi()->chip_select_assert(true);
+		spi()->blocking_write(&cmd, 1);
+
+		uint32_t header = static_cast<uint32_t>(length) << 16;
+		spi()->blocking_write(reinterpret_cast<uint8_t *>(&header),
+		                      sizeof(header));
+
+		spi()->blocking_write(transmitBuffer.get(), paddedLength);
+
+		spi()->wait_idle();
+		spi()->chip_select_assert(false);
+
+		// Stop QMU DMA transfer operation.
+		register_clear(RegisterOffset::ReceiveQueueCommand, StartDmaAccess);
+
+		// Enqueue the frame for transmission.
+		register_set(
+		  RegisterOffset::TransmitQueueCommand,
+		  TransmitQueueCommand::ManualEnqueueTransmitQueueFrameEnable);
+
+		return true;
+	}
+
+	private:
+	void drop_error_frame()
+	{
+		register_set(RegisterOffset::ReceiveQueueCommand,
+		             ReleaseReceiveErrorFrame);
+		// Wait for confirmation of frame release before attempting to process
+		// next frame.
+		while (register_read(RegisterOffset::ReceiveQueueCommand) &
+		       ReleaseReceiveErrorFrame)
+		{
+		}
+	}
+};
+
+using EthernetDevice = Ksz8851Ethernet;
+
+static_assert(EthernetAdaptor<EthernetDevice>);