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¶
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¶
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:
Each BufferDescriptor entry holds a buffer address, a flags word, a second flags word,
and the available byte count:
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_am79c973instance is created inkernelMain. - 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.