Skip to content

NIC Driver — AMD am79c973

Part of the Network Stack.
Header: include/drivers/amd_am79c973.h
Source: src/drivers/amd_am79c973.cc

The AMD am79c973 driver is the lowest layer of the DracOS network stack. It talks directly to the emulated PCnet NIC exposed by QEMU (-net nic,model=pcnet) and is responsible for getting raw bytes onto and off of the wire. Everything above this layer — Ethernet, ARP, IPv4, ICMP — is entirely software. This is where the software meets the hardware.


Hardware interface

The am79c973 is a PCI device. The driver discovers it through the PCI subsystem and is passed a PeripheralComponentInterconnectDeviceDescriptor on construction, which carries the I/O port base address and interrupt line.

All register access goes through I/O ports, not memory-mapped I/O. The driver opens five port objects at construction time:

Port offset Width Purpose
+0x00 16-bit MAC address bytes 0–1
+0x02 16-bit MAC address bytes 2–3
+0x04 16-bit MAC address bytes 4–5
+0x10 32-bit Register Data Port (RDP)
+0x12 16-bit Register Address Port (RAP)
+0x14 16-bit Reset port
+0x16 16-bit Bus Control Register Data Port (BDP)

To read or write a CSR or BCR, the driver first writes the register index to RAP, then reads or writes RDP (for CSRs) or BDP (for BCRs). This is the standard LANCE register access protocol.


Initialization sequence

The constructor follows the am79c973 initialization sequence mandated by the datasheet:

1. Read the MAC address

MAC0 = MACAddress0Port.Read();
MAC2 = MACAddress2Port.Read();
MAC4 = MACAddress4Port.Read();

The three 16-bit reads give bytes 0–5 of the hardware MAC. They are stored directly in initBlock.physicalAddress as a 48-bit value.

2. Force 32-bit mode

registerAddressPort.Write(20);          // select BCR20
busControlRegisterDataPort.Write(0x102); // SSIZE32=1, SWSTYLE=2

BCR20 must be set before issuing STOP. SWSTYLE=2 selects the 32-bit software style, which changes the layout of the buffer descriptor ring entries and is required for correct operation in 32-bit protected mode.

3. Reset the card

registerAddressPort.Write(0);   // select CSR0
registerDataPort.Write(0x04);   // STOP bit

Writing STOP halts any in-progress DMA and puts the card into a known idle state.

4. Build the InitializationBlock

The InitializationBlock is a structure the card reads from memory during its init phase:

struct InitializationBlock {
    uint16_t mode;                  // 0x0000 = normal, 0x8000 = promiscuous
    unsigned sendBufferCount  : 4;  // log2 of send ring size
    unsigned reserved1        : 4;
    unsigned recvBufferCount  : 4;  // log2 of recv ring size
    unsigned reserved2        : 4;
    uint64_t physicalAddress;       // 48-bit MAC
    uint16_t reserved3;
    uint64_t logicalAddress;        // multicast filter / IP storage
    uint32_t recvBufferDescrAddress;
    uint32_t sendBufferDescrAddress;
};

Key fields set at construction:

initBlock.mode            = 0x0000;
initBlock.numSendBuffers  = 3;       // 2^3 = 8 send descriptors
initBlock.numRecvBuffers  = 3;       // 2^3 = 8 receive descriptors
initBlock.physicalAddress = MAC;
initBlock.logicalAddress  = 0;

5. Align and wire up the descriptor rings

The am79c973 requires descriptor rings to be aligned to 16-byte boundaries. Since the kernel heap does not guarantee alignment, the driver over-allocates by 15 bytes and manually aligns:

sendBufferDescr = (BufferDescriptor*)(
    ((uint32_t)(&sendBufferDescrMemory) + 15) & ~0xFu
);

Each BufferDescriptor entry holds a buffer address, a flags word, a second flags word, and the available byte count:

struct BufferDescriptor {
    uint32_t address;
    uint32_t flags;
    uint32_t flags2;
    uint32_t avail;
};

Send descriptors are initialized with flags = 0x7FF | 0xF000 and cleared flags2. Receive descriptors are initialized with flags = 0x80000000 | 0xF7FF (owned by NIC, max buffer size) and cleared flags2.

6. Program the card with the init block address

registerAddressPort.Write(1);
registerDataPort.Write((uint32_t)(&initBlock) & 0xFFFF);

registerAddressPort.Write(2);
registerDataPort.Write(((uint32_t)(&initBlock) >> 16) & 0xFFFF);

CSR1 takes the low 16 bits of the init block's physical address; CSR2 takes the high 16 bits. The card will DMA-read the init block when INIT is asserted in CSR0.


Activation

Activate() is called after construction (typically from the DriverManager) to start the card. It performs three CSR writes in order:

CSR0 ← 0x41   (INIT | IENA)     trigger initialization, enable interrupts
CSR4 ← temp | 0xC00             enable APAD_XMT (auto-pad short packets)
CSR3 ← csr3 & ~(MISSM | MERRM)  unmask missed-frame and memory-error interrupts
CSR0 ← 0x42   (STRT | IENA)     start the card, keep interrupts enabled

After the STRT write the card is live: it will DMA frames into the receive ring and raise interrupts.


Send path

caller → Send(buffer, size)
           ├─ clamp size to 1518 bytes
           ├─ select next send descriptor (currentSendBuffer % 8)
           ├─ copy payload into sendBuffer[descriptor] from end (device quirk)
           ├─ set descriptor flags:
           │    flags = 0x8300F000 | ((-size) & 0xFFF)
           │    ↑ OWN=1 (NIC owns), STP=1, ENP=1, BCNT = two's complement of size
           └─ write CSR0 ← 0x48  (TDMD | IENA)  kick transmit

The (-size) & 0xFFF encoding is the am79c973's way of expressing buffer length: it stores the two's complement of the byte count in the low 12 bits of the flags word.

The send ring index advances unconditionally. If all 8 slots are NIC-owned (burst scenario), the oldest slot is overwritten. There is no backpressure.


Receive path

Receive() is called from HandleInterrupt() whenever CSR0 reports a receive event. It loops over the receive ring collecting CPU-owned descriptors:

for each descriptor where (flags & 0x80000000) == 0:   // CPU owns
    if not error and STP+ENP set:                      // single-buffer packet
        size = flags & 0xFFF
        if size > 64: size -= 4                        // strip FCS
        call handler->OnRawDataReceived(buffer, size)
        if handler returns true:
            Send(buffer, size)                         // in-place reply
    re-arm descriptor:
        flags2 = 0
        flags  = 0x8000F7FF                            // hand back to NIC
    advance currentRecvBuffer

The STP (start of packet) and ENP (end of packet) bits both set on the same descriptor indicates a single-buffer, non-fragmented frame — the only case handled. Multi-buffer frames (jumbo or fragmented) are silently skipped.


Interrupt handler

uint32_t amd_am79c973::HandleInterrupt(uint32_t esp) {
    registerAddressPort.Write(0);
    uint32_t csr0 = registerDataPort.Read();

    if (csr0 & 0x8000) /* ERR  */ ...
    if (csr0 & 0x2000) /* CERR */ ...  // collision
    if (csr0 & 0x1000) /* MISS */ ...  // missed frame
    if (csr0 & 0x0800) /* MERR */ ...  // memory error
    if (csr0 & 0x0400) /* RINT */  Receive();
    if (csr0 & 0x0200) /* TINT */  ...  // transmit done

    registerAddressPort.Write(0);
    registerDataPort.Write(csr0);   // write back to acknowledge
    return esp;
}

Writing CSR0 back clears the interrupt flags (RC bits). The interrupt line is shared via the PCI interrupt mechanism and is registered with the InterruptManager at construction using dev->interrupt + interrupts->HardwareInterruptOffset().


RawDataHandler interface

The driver has no knowledge of Ethernet framing or upper protocols. It communicates upward through a single abstract interface:

class RawDataHandler {
  amd_am79c973* backend;
public:
  RawDataHandler(amd_am79c973* backend);
  virtual ~RawDataHandler();

  virtual bool OnRawDataReceived(uint8_t* buffer, uint32_t size);
  void Send(uint8_t* buffer, uint32_t size);
};

The EtherFrameProvider inherits from RawDataHandler and registers itself with the NIC at construction. The Send method on RawDataHandler is a thin wrapper that calls amd_am79c973::Send directly, so upper layers can inject frames without holding a direct pointer to the NIC.

The return value of OnRawDataReceived is the in-place reply signal: if it returns true, the NIC driver calls Send(buffer, size) on the same buffer before returning. This is the mechanism by which ARP replies and ICMP echo replies are transmitted without a separate outbound call from the upper layer.


IP address storage

The driver stores a 32-bit IP address in initBlock.logicalAddress. The hardware uses this field as a multicast filter; DracOS repurposes it for IP storage since multicast is not used. Upper layers retrieve the IP via GetIPAddress() and set it via SetIPAddress(uint32_t).


Invariants and assumptions

  • Single NIC. There is no multi-interface support; one global amd_am79c973 instance is created in kernelMain.
  • MTU clamp. Payloads longer than 1518 bytes are silently truncated in Send.
  • I/O port only. No memory-mapped register access. Correct for QEMU's PCnet model.
  • Ring overwrite. The send ring does not stall; a full ring overwrites the oldest slot. This is safe under the current single-threaded, interrupt-driven model but would need a guard under preemptive multitasking.
  • Single-buffer frames only. Multi-buffer (STP without ENP, or ENP without STP) receive descriptors are skipped without logging.
  • Endianness. MAC and IP values stored in the init block are in the byte order written by the port reads. Callers are responsible for byte-swapping if needed.