feat: e100e Ethernet driver, ramdisk reorganization, shell rewrite, web server/client, and more

This commit is contained in:
2026-02-19 01:18:11 +01:00
parent 1a5d943649
commit d355d376f9
63 changed files with 5368 additions and 614 deletions
+71 -1
View File
@@ -20,6 +20,9 @@
#include <Net/Icmp.hpp>
#include <Net/Socket.hpp>
#include <Net/ByteOrder.hpp>
#include <Net/NetConfig.hpp>
#include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp>
#include <Hal/MSR.hpp>
#include <Hal/GDT.hpp>
#include <Graphics/Cursor.hpp>
@@ -303,6 +306,45 @@ namespace Zenith {
Net::Socket::Close(fd, Sched::GetCurrentPid());
}
static int Sys_SendTo(int fd, const uint8_t* data, uint32_t len,
uint32_t destIp, uint16_t destPort) {
return Net::Socket::SendTo(fd, data, len, destIp, destPort, Sched::GetCurrentPid());
}
static int Sys_RecvFrom(int fd, uint8_t* buf, uint32_t maxLen,
uint32_t* srcIp, uint16_t* srcPort) {
return Net::Socket::RecvFrom(fd, buf, maxLen, srcIp, srcPort, Sched::GetCurrentPid());
}
static void Sys_GetNetCfg(NetCfg* out) {
if (out == nullptr) return;
out->ipAddress = Net::GetIpAddress();
out->subnetMask = Net::GetSubnetMask();
out->gateway = Net::GetGateway();
const uint8_t* mac = nullptr;
if (Drivers::Net::E1000::IsInitialized()) {
mac = Drivers::Net::E1000::GetMacAddress();
} else if (Drivers::Net::E1000E::IsInitialized()) {
mac = Drivers::Net::E1000E::GetMacAddress();
}
if (mac) {
for (int i = 0; i < 6; i++) out->macAddress[i] = mac[i];
} else {
for (int i = 0; i < 6; i++) out->macAddress[i] = 0;
}
out->_pad[0] = 0;
out->_pad[1] = 0;
}
static int Sys_SetNetCfg(const NetCfg* in) {
if (in == nullptr) return -1;
Net::SetIpAddress(in->ipAddress);
Net::SetSubnetMask(in->subnetMask);
Net::SetGateway(in->gateway);
return 0;
}
static void Sys_Reset() {
/*
Triple fault for now; TODO: implement UEFI runtime function for clean reboot.
@@ -320,6 +362,16 @@ namespace Zenith {
__builtin_unreachable();
}
// ---- File write/create ----
static int Sys_FWrite(int handle, const uint8_t* data, uint64_t offset, uint64_t size) {
return Fs::Vfs::VfsWrite(handle, data, offset, size);
}
static int Sys_FCreate(const char* path) {
return Fs::Vfs::VfsCreate(path);
}
// ---- Dispatch ----
extern "C" int64_t SyscallDispatch(SyscallFrame* frame) {
@@ -416,6 +468,24 @@ namespace Zenith {
case SYS_CLOSESOCK:
Sys_CloseSock((int)frame->arg1);
return 0;
case SYS_GETNETCFG:
Sys_GetNetCfg((NetCfg*)frame->arg1);
return 0;
case SYS_SETNETCFG:
return (int64_t)Sys_SetNetCfg((const NetCfg*)frame->arg1);
case SYS_SENDTO:
return (int64_t)Sys_SendTo((int)frame->arg1, (const uint8_t*)frame->arg2,
(uint32_t)frame->arg3, (uint32_t)frame->arg4,
(uint16_t)frame->arg5);
case SYS_RECVFROM:
return (int64_t)Sys_RecvFrom((int)frame->arg1, (uint8_t*)frame->arg2,
(uint32_t)frame->arg3, (uint32_t*)frame->arg4,
(uint16_t*)frame->arg5);
case SYS_FWRITE:
return (int64_t)Sys_FWrite((int)frame->arg1, (const uint8_t*)frame->arg2,
frame->arg3, frame->arg4);
case SYS_FCREATE:
return (int64_t)Sys_FCreate((const char*)frame->arg1);
default:
return -1;
}
@@ -442,7 +512,7 @@ namespace Zenith {
Hal::WriteMSR(Hal::IA32_FMASK, 0x200);
Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 37 syscalls)";
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 43 syscalls)";
}
}
+15
View File
@@ -48,8 +48,15 @@ namespace Zenith {
static constexpr uint64_t SYS_SEND = 34;
static constexpr uint64_t SYS_RECV = 35;
static constexpr uint64_t SYS_CLOSESOCK = 36;
static constexpr uint64_t SYS_GETNETCFG = 37;
static constexpr uint64_t SYS_SETNETCFG = 38;
static constexpr uint64_t SYS_SENDTO = 39;
static constexpr uint64_t SYS_RECVFROM = 40;
static constexpr uint64_t SYS_FWRITE = 41;
static constexpr uint64_t SYS_FCREATE = 42;
static constexpr int SOCK_TCP = 1;
static constexpr int SOCK_UDP = 2;
struct DateTime {
uint16_t Year;
@@ -75,6 +82,14 @@ namespace Zenith {
uint32_t maxProcesses;
};
struct NetCfg {
uint32_t ipAddress; // network byte order
uint32_t subnetMask; // network byte order
uint32_t gateway; // network byte order
uint8_t macAddress[6];
uint8_t _pad[2];
};
struct KeyEvent {
uint8_t scancode;
char ascii;
+763
View File
@@ -0,0 +1,763 @@
/*
* E1000E.cpp
* Intel I217/I218/I219 (E1000E) Ethernet driver
* Copyright (c) 2025 Daniel Hammer
*/
#include "E1000E.hpp"
#include <Pci/Pci.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Libraries/Memory.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Hal/Apic/IoApic.hpp>
using namespace Kt;
namespace Drivers::Net::E1000E {
// Supported device entry
struct DeviceEntry {
uint16_t DeviceId;
const char* Name;
};
// PCI vendor ID for Intel
static constexpr uint16_t VendorIntel = 0x8086;
// Device ID table for I217/I218/I219 series
static constexpr DeviceEntry g_deviceTable[] = {
// I217
{ 0x153A, "I217-LM" },
{ 0x153B, "I217-V" },
// I218
{ 0x155A, "I218-LM" },
{ 0x1559, "I218-V" },
{ 0x15A0, "I218-LM (2)" },
{ 0x15A1, "I218-V (2)" },
{ 0x15A2, "I218-LM (3)" },
{ 0x15A3, "I218-V (3)" },
// I219-LM variants
{ 0x156F, "I219-LM" },
{ 0x15B7, "I219-LM (2)" },
{ 0x15BB, "I219-LM (3)" },
{ 0x15BD, "I219-LM (4)" },
{ 0x15DF, "I219-LM (5)" },
{ 0x15E1, "I219-LM (6)" },
{ 0x15E3, "I219-LM (7)" },
{ 0x15D7, "I219-LM (8)" },
{ 0x0D4C, "I219-LM (9)" },
{ 0x0D4E, "I219-LM (10)" },
{ 0x0D53, "I219-LM (11)" },
{ 0x0D55, "I219-LM (12)" },
{ 0x0DC5, "I219-LM (13)" },
{ 0x0DC7, "I219-LM (14)" },
{ 0x1A1C, "I219-LM (15)" },
{ 0x1A1E, "I219-LM (16)" },
// I219-V variants
{ 0x1570, "I219-V" },
{ 0x15B8, "I219-V (2)" },
{ 0x15BC, "I219-V (3)" },
{ 0x15BE, "I219-V (4)" },
{ 0x15E0, "I219-V (5)" },
{ 0x15E2, "I219-V (6)" },
{ 0x15D6, "I219-V (7)" },
{ 0x15D8, "I219-V (8)" },
{ 0x0D4D, "I219-V (9)" },
{ 0x0D4F, "I219-V (10)" },
{ 0x0D54, "I219-V (11)" },
{ 0x0DC6, "I219-V (13)" },
{ 0x0DC8, "I219-V (14)" },
{ 0x1A1D, "I219-V (15)" },
{ 0x1A1F, "I219-V (16)" },
};
static constexpr uint32_t g_deviceTableSize = sizeof(g_deviceTable) / sizeof(g_deviceTable[0]);
// PCI config space offsets
static constexpr uint8_t PCI_REG_BAR0 = 0x10;
static constexpr uint8_t PCI_REG_BAR1 = 0x14;
static constexpr uint8_t PCI_REG_COMMAND = 0x04;
static constexpr uint8_t PCI_REG_INTERRUPT = 0x3C;
// PCI command register bits
static constexpr uint16_t PCI_CMD_BUS_MASTER = (1 << 2);
static constexpr uint16_t PCI_CMD_MEM_SPACE = (1 << 1);
static constexpr uint16_t PCI_CMD_INTX_DISABLE = (1 << 10);
// MSI configuration
static constexpr uint8_t MSI_IRQ = 24; // IRQ slot 24 = vector 56 (first MSI slot)
static constexpr uint32_t MSI_VECTOR = 56; // IRQ_VECTOR_BASE + MSI_IRQ
static constexpr uint32_t MSI_ADDR_BASE = 0xFEE00000; // Local APIC message address range
// Driver state
static bool g_initialized = false;
static bool g_pollingMode = false;
static volatile uint8_t* g_mmioBase = nullptr;
static uint8_t g_macAddress[6] = {};
static uint8_t g_irqLine = 0;
// Descriptor rings (physical addresses for DMA, virtual for CPU access)
static RxDescriptor* g_rxDescs = nullptr;
static TxDescriptor* g_txDescs = nullptr;
static uint64_t g_rxDescsPhys = 0;
static uint64_t g_txDescsPhys = 0;
// Packet buffers (virtual addresses)
static uint8_t* g_rxBuffers[RX_DESC_COUNT] = {};
static uint8_t* g_txBuffers[TX_DESC_COUNT] = {};
static uint64_t g_rxBuffersPhys[RX_DESC_COUNT] = {};
static uint64_t g_txBuffersPhys[TX_DESC_COUNT] = {};
// Current descriptor indices
static uint32_t g_rxTail = 0;
static uint32_t g_txTail = 0;
// Statistics
static uint64_t g_rxPacketCount = 0;
static uint64_t g_txPacketCount = 0;
// RX callback
static RxCallback g_rxCallback = nullptr;
// -------------------------------------------------------------------------
// Register access helpers
// -------------------------------------------------------------------------
static void WriteReg(uint32_t reg, uint32_t value) {
*(volatile uint32_t*)(g_mmioBase + reg) = value;
}
static uint32_t ReadReg(uint32_t reg) {
return *(volatile uint32_t*)(g_mmioBase + reg);
}
// -------------------------------------------------------------------------
// SW/FW semaphore (prevents conflicts with Intel Management Engine)
// -------------------------------------------------------------------------
static bool AcquireSwFwSync() {
// Step 1: Acquire the software semaphore (SWSM.SMBI)
bool gotSmbi = false;
for (int i = 0; i < 2000; i++) {
uint32_t swsm = ReadReg(REG_SWSM);
if (!(swsm & SWSM_SMBI)) {
WriteReg(REG_SWSM, swsm | SWSM_SMBI);
if (ReadReg(REG_SWSM) & SWSM_SMBI) {
gotSmbi = true;
break;
}
}
}
if (!gotSmbi) {
KernelLogStream(WARNING, "E1000E") << "Could not acquire SMBI, proceeding anyway";
return false;
}
// Step 2: Acquire the SW/FW semaphore (EXTCNF_CTRL.SW_OWN)
for (int i = 0; i < 2000; i++) {
uint32_t extcnf = ReadReg(REG_EXTCNF_CTRL);
if (!(extcnf & EXTCNF_CTRL_SWFLAG)) {
WriteReg(REG_EXTCNF_CTRL, extcnf | EXTCNF_CTRL_SWFLAG);
if (ReadReg(REG_EXTCNF_CTRL) & EXTCNF_CTRL_SWFLAG) {
return true;
}
}
}
// Failed to acquire SWFLAG — release SMBI
uint32_t swsm = ReadReg(REG_SWSM);
WriteReg(REG_SWSM, swsm & ~SWSM_SMBI);
KernelLogStream(WARNING, "E1000E") << "Could not acquire SWFLAG, proceeding anyway";
return false;
}
static void ReleaseSwFwSync() {
uint32_t extcnf = ReadReg(REG_EXTCNF_CTRL);
WriteReg(REG_EXTCNF_CTRL, extcnf & ~EXTCNF_CTRL_SWFLAG);
uint32_t swsm = ReadReg(REG_SWSM);
WriteReg(REG_SWSM, swsm & ~SWSM_SMBI);
}
// -------------------------------------------------------------------------
// PHY access via MDIC register
// -------------------------------------------------------------------------
static uint16_t PhyRead(uint32_t phyReg) {
// PHY address = 1 for internal PHY
uint32_t mdic = ((phyReg & 0x1F) << MDIC_REG_SHIFT)
| (1u << MDIC_PHY_SHIFT)
| MDIC_OP_READ;
WriteReg(REG_MDIC, mdic);
for (int i = 0; i < 200000; i++) {
mdic = ReadReg(REG_MDIC);
if (mdic & MDIC_READY) {
if (mdic & MDIC_ERROR) {
KernelLogStream(WARNING, "E1000E") << "PHY read error for reg " << base::hex << (uint64_t)phyReg;
return 0;
}
return (uint16_t)(mdic & MDIC_DATA_MASK);
}
}
KernelLogStream(WARNING, "E1000E") << "PHY read timeout for reg " << base::hex << (uint64_t)phyReg;
return 0;
}
static void PhyWrite(uint32_t phyReg, uint16_t value) {
uint32_t mdic = (uint32_t)value
| ((phyReg & 0x1F) << MDIC_REG_SHIFT)
| (1u << MDIC_PHY_SHIFT)
| MDIC_OP_WRITE;
WriteReg(REG_MDIC, mdic);
for (int i = 0; i < 200000; i++) {
mdic = ReadReg(REG_MDIC);
if (mdic & MDIC_READY) {
if (mdic & MDIC_ERROR) {
KernelLogStream(WARNING, "E1000E") << "PHY write error for reg " << base::hex << (uint64_t)phyReg;
}
return;
}
}
KernelLogStream(WARNING, "E1000E") << "PHY write timeout for reg " << base::hex << (uint64_t)phyReg;
}
// -------------------------------------------------------------------------
// PHY initialization
// -------------------------------------------------------------------------
static void InitPhy() {
// Reset the PHY
PhyWrite(PHY_CONTROL, PHY_CTRL_RESET);
// Wait for reset to complete
for (int i = 0; i < 100000; i++) {
uint16_t ctrl = PhyRead(PHY_CONTROL);
if (!(ctrl & PHY_CTRL_RESET)) {
break;
}
}
// Advertise 10/100/1000 Mbps capabilities
uint16_t anar = PhyRead(PHY_AUTONEG_ADV);
anar |= (1 << 5) | (1 << 6) | (1 << 7) | (1 << 8); // 10/100 HD/FD
PhyWrite(PHY_AUTONEG_ADV, anar);
// Advertise 1000BASE-T
uint16_t gbcr = PhyRead(PHY_1000T_CTRL);
gbcr |= (1 << 8) | (1 << 9); // 1000BASE-T HD/FD
PhyWrite(PHY_1000T_CTRL, gbcr);
// Enable and restart auto-negotiation
PhyWrite(PHY_CONTROL, PHY_CTRL_AUTONEG_EN | PHY_CTRL_RESTART_AN);
KernelLogStream(OK, "E1000E") << "PHY initialized, auto-negotiation started";
}
// -------------------------------------------------------------------------
// EEPROM access (e1000e encoding differs from e1000)
// -------------------------------------------------------------------------
static uint16_t EepromRead(uint8_t address) {
// E1000E: address shifted left by 2 (not 8), done bit at position 1 (not 4)
WriteReg(REG_EERD, ((uint32_t)address << 2) | 1);
uint32_t value;
for (int i = 0; i < 10000; i++) {
value = ReadReg(REG_EERD);
if (value & (1 << 1)) {
return (uint16_t)(value >> 16);
}
}
KernelLogStream(WARNING, "E1000E") << "EEPROM read timeout for address " << base::hex << (uint64_t)address;
return 0;
}
// -------------------------------------------------------------------------
// MAC address
// -------------------------------------------------------------------------
static void ReadMacAddress() {
// Try reading from RAL/RAH first
uint32_t ral = ReadReg(REG_RAL);
uint32_t rah = ReadReg(REG_RAH);
if (ral != 0) {
g_macAddress[0] = (uint8_t)(ral);
g_macAddress[1] = (uint8_t)(ral >> 8);
g_macAddress[2] = (uint8_t)(ral >> 16);
g_macAddress[3] = (uint8_t)(ral >> 24);
g_macAddress[4] = (uint8_t)(rah);
g_macAddress[5] = (uint8_t)(rah >> 8);
} else {
// Fallback: read from EEPROM
uint16_t word0 = EepromRead(0);
uint16_t word1 = EepromRead(1);
uint16_t word2 = EepromRead(2);
g_macAddress[0] = (uint8_t)(word0);
g_macAddress[1] = (uint8_t)(word0 >> 8);
g_macAddress[2] = (uint8_t)(word1);
g_macAddress[3] = (uint8_t)(word1 >> 8);
g_macAddress[4] = (uint8_t)(word2);
g_macAddress[5] = (uint8_t)(word2 >> 8);
}
// Write MAC back to RAL/RAH to ensure the filter is set
WriteReg(REG_RAL,
(uint32_t)g_macAddress[0] |
((uint32_t)g_macAddress[1] << 8) |
((uint32_t)g_macAddress[2] << 16) |
((uint32_t)g_macAddress[3] << 24));
WriteReg(REG_RAH,
(uint32_t)g_macAddress[4] |
((uint32_t)g_macAddress[5] << 8) |
(1u << 31)); // AV (Address Valid) bit
}
// -------------------------------------------------------------------------
// Allocate page-aligned DMA buffer, returns virtual address
// -------------------------------------------------------------------------
static uint8_t* AllocateDmaBuffer(uint64_t& outPhysAddr) {
void* virt = Memory::g_pfa->AllocateZeroed();
outPhysAddr = Memory::SubHHDM(virt);
return (uint8_t*)virt;
}
// -------------------------------------------------------------------------
// RX setup
// -------------------------------------------------------------------------
static void SetupRx() {
uint64_t descPhys;
g_rxDescs = (RxDescriptor*)AllocateDmaBuffer(descPhys);
g_rxDescsPhys = descPhys;
for (uint32_t i = 0; i < RX_DESC_COUNT; i++) {
g_rxBuffers[i] = AllocateDmaBuffer(g_rxBuffersPhys[i]);
// Allocate second page for larger buffer support
uint64_t secondPhys;
AllocateDmaBuffer(secondPhys);
g_rxDescs[i].BufferAddress = g_rxBuffersPhys[i];
g_rxDescs[i].Status = 0;
g_rxDescs[i].Length = 0;
g_rxDescs[i].Checksum = 0;
g_rxDescs[i].Errors = 0;
g_rxDescs[i].Special = 0;
}
WriteReg(REG_RDBAL, (uint32_t)(g_rxDescsPhys & 0xFFFFFFFF));
WriteReg(REG_RDBAH, (uint32_t)(g_rxDescsPhys >> 32));
WriteReg(REG_RDLEN, RX_DESC_COUNT * sizeof(RxDescriptor));
WriteReg(REG_RDH, 0);
WriteReg(REG_RDT, RX_DESC_COUNT - 1);
g_rxTail = RX_DESC_COUNT - 1;
uint32_t rctl = RCTL_EN | RCTL_BAM | RCTL_SECRC | RCTL_BSIZE_4096 | RCTL_BSEX;
WriteReg(REG_RCTL, rctl);
KernelLogStream(OK, "E1000E") << "RX ring configured: " << base::dec << (uint64_t)RX_DESC_COUNT << " descriptors";
}
// -------------------------------------------------------------------------
// TX setup
// -------------------------------------------------------------------------
static void SetupTx() {
uint64_t descPhys;
g_txDescs = (TxDescriptor*)AllocateDmaBuffer(descPhys);
g_txDescsPhys = descPhys;
for (uint32_t i = 0; i < TX_DESC_COUNT; i++) {
g_txBuffers[i] = AllocateDmaBuffer(g_txBuffersPhys[i]);
g_txDescs[i].BufferAddress = g_txBuffersPhys[i];
g_txDescs[i].Length = 0;
g_txDescs[i].Command = 0;
g_txDescs[i].Status = TXSTA_DD;
g_txDescs[i].ChecksumOffset = 0;
g_txDescs[i].ChecksumStart = 0;
g_txDescs[i].Special = 0;
}
WriteReg(REG_TDBAL, (uint32_t)(g_txDescsPhys & 0xFFFFFFFF));
WriteReg(REG_TDBAH, (uint32_t)(g_txDescsPhys >> 32));
WriteReg(REG_TDLEN, TX_DESC_COUNT * sizeof(TxDescriptor));
WriteReg(REG_TDH, 0);
WriteReg(REG_TDT, 0);
g_txTail = 0;
uint32_t tctl = TCTL_EN | TCTL_PSP
| (15u << TCTL_CT_SHIFT)
| (64u << TCTL_COLD_SHIFT);
WriteReg(REG_TCTL, tctl);
WriteReg(REG_TIPG, 10 | (10 << 10) | (10 << 20));
KernelLogStream(OK, "E1000E") << "TX ring configured: " << base::dec << (uint64_t)TX_DESC_COUNT << " descriptors";
}
// -------------------------------------------------------------------------
// MSI setup
// -------------------------------------------------------------------------
static void HandleInterrupt(uint8_t irq); // forward declaration
static bool SetupMsi(uint8_t bus, uint8_t dev, uint8_t func) {
uint8_t cap = Pci::FindCapability(bus, dev, func, Pci::PCI_CAP_MSI);
if (cap == 0) {
KernelLogStream(INFO, "E1000E") << "MSI capability not found";
return false;
}
KernelLogStream(INFO, "E1000E") << "MSI capability at offset " << base::hex << (uint64_t)cap;
// Read Message Control (cap+2)
uint16_t msgCtrl = Pci::LegacyRead16(bus, dev, func, cap + 2);
bool is64bit = (msgCtrl & (1 << 7)) != 0;
// Write Message Address (cap+4): BSP APIC ID 0, physical destination, fixed delivery
Pci::LegacyWrite32(bus, dev, func, cap + 4, MSI_ADDR_BASE);
// Write Message Data (vector number, edge-triggered, fixed delivery)
if (is64bit) {
// 64-bit: Upper Address at cap+8, Data at cap+12
Pci::LegacyWrite32(bus, dev, func, cap + 8, 0);
Pci::LegacyWrite16(bus, dev, func, cap + 12, MSI_VECTOR);
} else {
// 32-bit: Data at cap+8
Pci::LegacyWrite16(bus, dev, func, cap + 8, MSI_VECTOR);
}
// Enable MSI: set bit 0 (MSI Enable), clear bits 6:4 (single message)
msgCtrl &= ~(0x70); // Clear Multiple Message Enable (bits 6:4)
msgCtrl |= (1 << 0); // MSI Enable
Pci::LegacyWrite16(bus, dev, func, cap + 2, msgCtrl);
// Disable legacy INTx in PCI command register
uint16_t pciCmd = Pci::LegacyRead16(bus, dev, func, PCI_REG_COMMAND);
pciCmd |= PCI_CMD_INTX_DISABLE;
Pci::LegacyWrite16(bus, dev, func, PCI_REG_COMMAND, pciCmd);
// Register the interrupt handler for MSI vector
Hal::RegisterIrqHandler(MSI_IRQ, HandleInterrupt);
KernelLogStream(OK, "E1000E") << "MSI enabled: vector " << base::dec << (uint64_t)MSI_VECTOR
<< " (IRQ slot " << (uint64_t)MSI_IRQ << ")" << (is64bit ? " [64-bit]" : " [32-bit]");
return true;
}
// -------------------------------------------------------------------------
// Interrupt handler
// -------------------------------------------------------------------------
static void HandleInterrupt(uint8_t irq) {
(void)irq;
uint32_t icr = ReadReg(REG_ICR);
// Spurious IRQ guard
if (icr == 0) {
return;
}
if (icr & ICR_LSC) {
uint32_t status = ReadReg(REG_STATUS);
bool linkUp = (status & (1 << 1)) != 0;
KernelLogStream(INFO, "E1000E") << "Link status change: " << (linkUp ? "UP" : "DOWN");
}
if (icr & ICR_RXT0) {
while (true) {
uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT;
RxDescriptor& desc = g_rxDescs[nextIdx];
if (!(desc.Status & RXSTA_DD)) {
break;
}
uint16_t length = desc.Length;
g_rxPacketCount++;
if (g_rxCallback != nullptr) {
g_rxCallback(g_rxBuffers[nextIdx], length);
}
desc.Status = 0;
desc.Length = 0;
desc.Errors = 0;
g_rxTail = nextIdx;
WriteReg(REG_RDT, g_rxTail);
}
}
if (icr & (ICR_TXDW | ICR_TXQE)) {
// TX completion — nothing to do for now
}
}
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
void Initialize() {
KernelLogStream(INFO, "E1000E") << "Scanning for Intel e1000e NIC...";
auto& devices = Pci::GetDevices();
const Pci::PciDevice* foundDev = nullptr;
const char* foundName = nullptr;
for (uint64_t i = 0; i < devices.size(); i++) {
if (devices[i].VendorId != VendorIntel) {
continue;
}
for (uint32_t j = 0; j < g_deviceTableSize; j++) {
if (devices[i].DeviceId == g_deviceTable[j].DeviceId) {
foundDev = &devices[i];
foundName = g_deviceTable[j].Name;
break;
}
}
if (foundDev != nullptr) {
break;
}
}
if (foundDev == nullptr) {
KernelLogStream(WARNING, "E1000E") << "No e1000e NIC found";
return;
}
KernelLogStream(OK, "E1000E") << "Found " << foundName << " at PCI "
<< base::hex << (uint64_t)foundDev->Bus << ":"
<< (uint64_t)foundDev->Device << "." << (uint64_t)foundDev->Function;
// Read BAR0 (MMIO base address), check for 64-bit BAR
uint32_t bar0 = Pci::LegacyRead32(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_BAR0);
uint64_t mmioPhys = bar0 & 0xFFFFFFF0;
// Check if 64-bit BAR (type field bits 2:1 == 0b10)
if ((bar0 & 0x06) == 0x04) {
uint32_t bar1 = Pci::LegacyRead32(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_BAR1);
mmioPhys |= ((uint64_t)bar1 << 32);
}
KernelLogStream(INFO, "E1000E") << "BAR0 physical: " << base::hex << mmioPhys;
// Map the MMIO region (128KB = 32 pages)
constexpr uint64_t MmioSize = 0x20000;
for (uint64_t offset = 0; offset < MmioSize; offset += 0x1000) {
Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset));
}
g_mmioBase = (volatile uint8_t*)Memory::HHDM(mmioPhys);
// Enable bus mastering and memory space in PCI command register
uint16_t pciCmd = Pci::LegacyRead16(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_COMMAND);
pciCmd |= PCI_CMD_BUS_MASTER | PCI_CMD_MEM_SPACE;
Pci::LegacyWrite16(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_COMMAND, pciCmd);
KernelLogStream(OK, "E1000E") << "Bus mastering enabled";
// Read interrupt line from PCI config (used for legacy IRQ fallback)
g_irqLine = Pci::LegacyRead8(foundDev->Bus, foundDev->Device, foundDev->Function, PCI_REG_INTERRUPT);
KernelLogStream(INFO, "E1000E") << "PCI IRQ line: " << base::dec << (uint64_t)g_irqLine;
// --- ICH/PCH reset sequence ---
// 1. Disable interrupts, flush ICR
KernelLogStream(INFO, "E1000E") << "Disabling interrupts...";
WriteReg(REG_IMC, 0xFFFFFFFF);
ReadReg(REG_ICR);
// 2. Acquire SW/FW semaphore (non-fatal if it fails)
KernelLogStream(INFO, "E1000E") << "Acquiring semaphore...";
AcquireSwFwSync();
// 3. Reset the device
KernelLogStream(INFO, "E1000E") << "Resetting device...";
uint32_t ctrl = ReadReg(REG_CTRL);
WriteReg(REG_CTRL, ctrl | CTRL_RST);
// Post-reset settling delay (give hardware time before polling)
for (int i = 0; i < 100000; i++) {
asm volatile("" ::: "memory");
}
// Poll for reset completion
for (int i = 0; i < 10000; i++) {
if (!(ReadReg(REG_CTRL) & CTRL_RST)) {
break;
}
}
// 4. Release semaphore
ReleaseSwFwSync();
// 5. Disable interrupts again after reset
WriteReg(REG_IMC, 0xFFFFFFFF);
ReadReg(REG_ICR);
KernelLogStream(OK, "E1000E") << "Reset complete";
// Set link up: SLU on, but let auto-negotiation decide speed/duplex
ctrl = ReadReg(REG_CTRL);
ctrl |= CTRL_SLU;
ctrl &= ~CTRL_FRCSPD;
ctrl &= ~CTRL_FRCDPLX;
ctrl &= ~(1u << 3); // Clear LRST
ctrl &= ~(1u << 31); // Clear PHY_RST
ctrl &= ~(1u << 7); // Clear ILOS
WriteReg(REG_CTRL, ctrl);
// Initialize PHY
InitPhy();
// Read MAC address
ReadMacAddress();
KernelLogStream(OK, "E1000E") << "MAC: "
<< base::hex
<< (uint64_t)g_macAddress[0] << ":"
<< (uint64_t)g_macAddress[1] << ":"
<< (uint64_t)g_macAddress[2] << ":"
<< (uint64_t)g_macAddress[3] << ":"
<< (uint64_t)g_macAddress[4] << ":"
<< (uint64_t)g_macAddress[5];
// Zero out the Multicast Table Array (128 entries)
for (uint32_t i = 0; i < 128; i++) {
WriteReg(REG_MTA + (i * 4), 0);
}
// Set up RX and TX descriptor rings
SetupRx();
SetupTx();
// Three-tier interrupt strategy: MSI → legacy IRQ → polling
if (SetupMsi(foundDev->Bus, foundDev->Device, foundDev->Function)) {
// MSI configured — enable NIC interrupt causes
WriteReg(REG_IMS, ICR_RXT0 | ICR_TXDW | ICR_TXQE | ICR_LSC | ICR_RXDMT0);
} else if (g_irqLine != 0xFF) {
// Legacy IRQ fallback
KernelLogStream(INFO, "E1000E") << "Falling back to legacy IRQ " << base::dec << (uint64_t)g_irqLine;
Hal::RegisterIrqHandler(g_irqLine, HandleInterrupt);
Hal::IoApic::UnmaskIrq(Hal::IoApic::GetGsiForIrq(g_irqLine));
WriteReg(REG_IMS, ICR_RXT0 | ICR_TXDW | ICR_TXQE | ICR_LSC | ICR_RXDMT0);
} else {
// Polling as last resort
KernelLogStream(WARNING, "E1000E") << "No MSI or legacy IRQ available, using polling mode";
g_pollingMode = true;
}
g_initialized = true;
uint32_t status = ReadReg(REG_STATUS);
bool linkUp = (status & (1 << 1)) != 0;
KernelLogStream(OK, "E1000E") << "Initialization complete, link: " << (linkUp ? "UP" : "DOWN");
}
// -------------------------------------------------------------------------
// Polling: process received packets (with reentrancy guard)
// -------------------------------------------------------------------------
static bool g_polling = false;
static void PollRx() {
// Guard against reentrancy: RX callback can trigger ARP reply →
// Ethernet::Send → SendPacket → PollRx(). Two concurrent callers
// modifying g_rxTail / RDT would corrupt the descriptor ring.
if (g_polling) {
return;
}
g_polling = true;
while (true) {
uint32_t nextIdx = (g_rxTail + 1) % RX_DESC_COUNT;
RxDescriptor& desc = g_rxDescs[nextIdx];
if (!(desc.Status & RXSTA_DD)) {
break;
}
uint16_t length = desc.Length;
g_rxPacketCount++;
if (g_rxCallback != nullptr) {
g_rxCallback(g_rxBuffers[nextIdx], length);
}
desc.Status = 0;
desc.Length = 0;
desc.Errors = 0;
g_rxTail = nextIdx;
WriteReg(REG_RDT, g_rxTail);
}
g_polling = false;
}
bool SendPacket(const uint8_t* data, uint16_t length) {
if (!g_initialized || data == nullptr || length == 0 || length > 1518) {
return false;
}
TxDescriptor& desc = g_txDescs[g_txTail];
if (!(desc.Status & TXSTA_DD)) {
KernelLogStream(WARNING, "E1000E") << "TX ring full";
return false;
}
memcpy(g_txBuffers[g_txTail], data, length);
desc.BufferAddress = g_txBuffersPhys[g_txTail];
desc.Length = length;
desc.Command = TXCMD_EOP | TXCMD_IFCS | TXCMD_RS;
desc.Status = 0;
g_txTail = (g_txTail + 1) % TX_DESC_COUNT;
WriteReg(REG_TDT, g_txTail);
g_txPacketCount++;
return true;
}
const uint8_t* GetMacAddress() {
return g_macAddress;
}
bool IsInitialized() {
return g_initialized;
}
void SetRxCallback(RxCallback callback) {
g_rxCallback = callback;
}
void Poll() {
if (!g_initialized) {
return;
}
PollRx();
}
};
+155
View File
@@ -0,0 +1,155 @@
/*
* E1000E.hpp
* Intel I217/I218/I219 (E1000E) Ethernet driver
* Copyright (c) 2025 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Drivers::Net::E1000E {
// E1000E register offsets (memory-mapped via BAR0)
constexpr uint32_t REG_CTRL = 0x0000; // Device Control
constexpr uint32_t REG_STATUS = 0x0008; // Device Status
constexpr uint32_t REG_EERD = 0x0014; // EEPROM Read
constexpr uint32_t REG_CTRL_EXT = 0x0018; // Extended Device Control
constexpr uint32_t REG_MDIC = 0x0020; // MDI Control (PHY access)
constexpr uint32_t REG_ICR = 0x00C0; // Interrupt Cause Read
constexpr uint32_t REG_IMS = 0x00D0; // Interrupt Mask Set
constexpr uint32_t REG_IMC = 0x00D8; // Interrupt Mask Clear
constexpr uint32_t REG_RCTL = 0x0100; // Receive Control
constexpr uint32_t REG_TCTL = 0x0400; // Transmit Control
constexpr uint32_t REG_TIPG = 0x0410; // Transmit IPG
constexpr uint32_t REG_RDBAL = 0x2800; // RX Descriptor Base Low
constexpr uint32_t REG_RDBAH = 0x2804; // RX Descriptor Base High
constexpr uint32_t REG_RDLEN = 0x2808; // RX Descriptor Length
constexpr uint32_t REG_RDH = 0x2810; // RX Descriptor Head
constexpr uint32_t REG_RDT = 0x2818; // RX Descriptor Tail
constexpr uint32_t REG_TDBAL = 0x3800; // TX Descriptor Base Low
constexpr uint32_t REG_TDBAH = 0x3804; // TX Descriptor Base High
constexpr uint32_t REG_TDLEN = 0x3808; // TX Descriptor Length
constexpr uint32_t REG_TDH = 0x3810; // TX Descriptor Head
constexpr uint32_t REG_TDT = 0x3818; // TX Descriptor Tail
constexpr uint32_t REG_MTA = 0x5200; // Multicast Table Array (128 entries)
constexpr uint32_t REG_RAL = 0x5400; // Receive Address Low
constexpr uint32_t REG_RAH = 0x5404; // Receive Address High
constexpr uint32_t REG_EXTCNF_CTRL = 0x0F00; // Extended Configuration Control
constexpr uint32_t REG_SWSM = 0x5B50; // Software Semaphore
constexpr uint32_t REG_FWSM = 0x5B54; // Firmware Semaphore
// CTRL register bits
constexpr uint32_t CTRL_SLU = (1 << 6); // Set Link Up
constexpr uint32_t CTRL_FRCSPD = (1 << 11); // Force Speed
constexpr uint32_t CTRL_FRCDPLX = (1 << 12); // Force Duplex
constexpr uint32_t CTRL_RST = (1 << 26); // Device Reset
// MDIC register
constexpr uint32_t MDIC_DATA_MASK = 0x0000FFFF;
constexpr uint32_t MDIC_REG_SHIFT = 16; // PHY register address shift
constexpr uint32_t MDIC_PHY_SHIFT = 21; // PHY address shift
constexpr uint32_t MDIC_OP_READ = (2u << 26);
constexpr uint32_t MDIC_OP_WRITE = (1u << 26);
constexpr uint32_t MDIC_READY = (1u << 28);
constexpr uint32_t MDIC_ERROR = (1u << 30);
// PHY register addresses
constexpr uint32_t PHY_CONTROL = 0x00; // PHY Control Register
constexpr uint32_t PHY_STATUS = 0x01; // PHY Status Register
constexpr uint32_t PHY_AUTONEG_ADV = 0x04; // Auto-Negotiation Advertisement
constexpr uint32_t PHY_1000T_CTRL = 0x09; // 1000BASE-T Control
// PHY Control register bits
constexpr uint16_t PHY_CTRL_RESET = (1 << 15);
constexpr uint16_t PHY_CTRL_AUTONEG_EN = (1 << 12);
constexpr uint16_t PHY_CTRL_RESTART_AN = (1 << 9);
// Semaphore bits
constexpr uint32_t SWSM_SMBI = (1 << 0);
constexpr uint32_t SWSM_SWESMBI = (1 << 1);
constexpr uint32_t EXTCNF_CTRL_SWFLAG = (1 << 5);
// RCTL register bits
constexpr uint32_t RCTL_EN = (1 << 1); // Receiver Enable
constexpr uint32_t RCTL_SBP = (1 << 2); // Store Bad Packets
constexpr uint32_t RCTL_UPE = (1 << 3); // Unicast Promiscuous
constexpr uint32_t RCTL_MPE = (1 << 4); // Multicast Promiscuous
constexpr uint32_t RCTL_BAM = (1 << 15); // Broadcast Accept Mode
constexpr uint32_t RCTL_BSIZE_4096 = (3 << 16); // Buffer Size 4096 (with BSEX)
constexpr uint32_t RCTL_BSEX = (1 << 25); // Buffer Size Extension
constexpr uint32_t RCTL_SECRC = (1 << 26); // Strip Ethernet CRC
// TCTL register bits
constexpr uint32_t TCTL_EN = (1 << 1); // Transmit Enable
constexpr uint32_t TCTL_PSP = (1 << 3); // Pad Short Packets
constexpr uint32_t TCTL_CT_SHIFT = 4; // Collision Threshold shift
constexpr uint32_t TCTL_COLD_SHIFT = 12; // Collision Distance shift
// ICR (interrupt cause) bits
constexpr uint32_t ICR_TXDW = (1 << 0); // TX Descriptor Written Back
constexpr uint32_t ICR_TXQE = (1 << 1); // TX Queue Empty
constexpr uint32_t ICR_LSC = (1 << 2); // Link Status Change
constexpr uint32_t ICR_RXDMT0 = (1 << 4); // RX Descriptor Minimum Threshold
constexpr uint32_t ICR_RXO = (1 << 6); // Receiver Overrun
constexpr uint32_t ICR_RXT0 = (1 << 7); // Receiver Timer Interrupt
// TX descriptor command bits
constexpr uint8_t TXCMD_EOP = (1 << 0); // End Of Packet
constexpr uint8_t TXCMD_IFCS = (1 << 1); // Insert FCS/CRC
constexpr uint8_t TXCMD_RS = (1 << 3); // Report Status
// TX descriptor status bits
constexpr uint8_t TXSTA_DD = (1 << 0); // Descriptor Done
// RX descriptor status bits
constexpr uint8_t RXSTA_DD = (1 << 0); // Descriptor Done
constexpr uint8_t RXSTA_EOP = (1 << 1); // End Of Packet
// Descriptor ring sizes
constexpr uint32_t RX_DESC_COUNT = 32;
constexpr uint32_t TX_DESC_COUNT = 32;
constexpr uint32_t PACKET_BUFFER_SIZE = 8192;
// RX descriptor (legacy format, 16 bytes)
struct RxDescriptor {
uint64_t BufferAddress;
uint16_t Length;
uint16_t Checksum;
uint8_t Status;
uint8_t Errors;
uint16_t Special;
} __attribute__((packed));
// TX descriptor (legacy format, 16 bytes)
struct TxDescriptor {
uint64_t BufferAddress;
uint16_t Length;
uint8_t ChecksumOffset;
uint8_t Command;
uint8_t Status;
uint8_t ChecksumStart;
uint16_t Special;
} __attribute__((packed));
// Initialize the E1000E driver (scans PCI for the device)
void Initialize();
// Send a raw Ethernet frame
bool SendPacket(const uint8_t* data, uint16_t length);
// Get the MAC address (6 bytes)
const uint8_t* GetMacAddress();
// Check if the device was found and initialized
bool IsInitialized();
// RX callback type: called with (packet data, length)
using RxCallback = void(*)(const uint8_t* data, uint16_t length);
// Register a callback for received packets
void SetRxCallback(RxCallback callback);
// Poll for received packets (used when legacy IRQ is unavailable)
void Poll();
};
+107
View File
@@ -8,6 +8,7 @@
#include <Terminal/Terminal.hpp>
#include <Libraries/String.hpp>
#include <Libraries/Memory.hpp>
#include <Memory/Heap.hpp>
namespace Fs::Ramdisk {
@@ -105,6 +106,8 @@ namespace Fs::Ramdisk {
entry.isDirectory = (typeFlag == '5');
entry.size = size;
entry.capacity = size;
entry.heapAllocated = false;
// Data starts at next 512-byte block
entry.data = ptr + 512;
@@ -235,6 +238,110 @@ namespace Fs::Ramdisk {
return count;
}
int Write(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) {
if (handle < 0 || handle >= fileCount) return -1;
if (buffer == nullptr || size == 0) return 0;
FileEntry& entry = fileTable[handle];
if (entry.isDirectory) return -1;
uint64_t endOffset = offset + size;
// Copy-on-write: if data points into tar memory, copy to heap
if (!entry.heapAllocated) {
uint64_t newCap = entry.size;
if (endOffset > newCap) newCap = endOffset;
if (newCap < 256) newCap = 256;
// Round up to next power of 2 for growth
uint64_t rounded = 256;
while (rounded < newCap) rounded *= 2;
newCap = rounded;
uint8_t* newBuf = (uint8_t*)Memory::g_heap->Request(newCap);
if (newBuf == nullptr) return -1;
if (entry.data && entry.size > 0) {
memcpy(newBuf, entry.data, entry.size);
}
entry.data = newBuf;
entry.capacity = newCap;
entry.heapAllocated = true;
}
// Grow buffer if needed
if (endOffset > entry.capacity) {
uint64_t newCap = entry.capacity;
while (newCap < endOffset) newCap *= 2;
uint8_t* newBuf = (uint8_t*)Memory::g_heap->Request(newCap);
if (newBuf == nullptr) return -1;
if (entry.data && entry.size > 0) {
memcpy(newBuf, entry.data, entry.size);
}
Memory::g_heap->Free(entry.data);
entry.data = newBuf;
entry.capacity = newCap;
}
memcpy(entry.data + offset, buffer, size);
if (endOffset > entry.size) {
entry.size = endOffset;
}
return (int)size;
}
int Create(const char* path) {
if (path == nullptr) return -1;
if (fileCount >= MaxFiles) return -1;
// Normalize: skip leading '/'
if (path[0] == '/') path++;
// Check if file already exists
for (int i = 0; i < fileCount; i++) {
if (StrEqual(fileTable[i].name, path)) {
// File exists — truncate it
FileEntry& entry = fileTable[i];
if (!entry.heapAllocated) {
uint8_t* newBuf = (uint8_t*)Memory::g_heap->Request(256);
if (newBuf == nullptr) return -1;
entry.data = newBuf;
entry.capacity = 256;
entry.heapAllocated = true;
}
entry.size = 0;
entry.isDirectory = false;
return i;
}
}
// Create new file entry
FileEntry& entry = fileTable[fileCount];
int nameLen = 0;
while (nameLen < MaxNameLen - 1 && path[nameLen] != '\0') {
entry.name[nameLen] = path[nameLen];
nameLen++;
}
entry.name[nameLen] = '\0';
uint8_t* buf = (uint8_t*)Memory::g_heap->Request(256);
if (buf == nullptr) return -1;
entry.data = buf;
entry.size = 0;
entry.capacity = 256;
entry.isDirectory = false;
entry.heapAllocated = true;
return fileCount++;
}
int GetFileCount() {
return fileCount;
}
+4
View File
@@ -17,13 +17,17 @@ namespace Fs::Ramdisk {
char name[MaxNameLen];
uint8_t* data;
uint64_t size;
uint64_t capacity;
bool isDirectory;
bool heapAllocated;
};
void Initialize(void* moduleData, uint64_t moduleSize);
int Open(const char* path);
int Read(int handle, uint8_t* buffer, uint64_t offset, uint64_t size);
int Write(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size);
int Create(const char* path);
uint64_t GetSize(int handle);
void Close(int handle);
+40
View File
@@ -123,6 +123,46 @@ namespace Fs::Vfs {
entry.inUse = false;
}
int VfsWrite(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) {
if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return -1;
HandleEntry& entry = handleTable[handle];
if (driveTable[entry.driveNumber]->Write == nullptr) return -1;
return driveTable[entry.driveNumber]->Write(entry.localHandle, buffer, offset, size);
}
int VfsCreate(const char* path) {
int drive;
const char* localPath;
if (!ParsePath(path, drive, localPath)) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format for Create";
return -1;
}
if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered";
return -1;
}
if (driveTable[drive]->Create == nullptr) return -1;
int localHandle = driveTable[drive]->Create(localPath);
if (localHandle < 0) return -1;
int globalHandle = AllocHandle();
if (globalHandle < 0) {
Kt::KernelLogStream(Kt::ERROR, "VFS") << "No free handles";
return -1;
}
handleTable[globalHandle].inUse = true;
handleTable[globalHandle].driveNumber = drive;
handleTable[globalHandle].localHandle = localHandle;
return globalHandle;
}
int VfsReadDir(const char* path, const char** outNames, int maxEntries) {
int drive;
const char* localPath;
+4
View File
@@ -19,6 +19,8 @@ namespace Fs::Vfs {
uint64_t (*GetSize)(int handle);
void (*Close)(int handle);
int (*ReadDir)(const char* path, const char** outNames, int maxEntries);
int (*Write)(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size);
int (*Create)(const char* path);
};
void Initialize();
@@ -26,6 +28,8 @@ namespace Fs::Vfs {
int VfsOpen(const char* path);
int VfsRead(int handle, uint8_t* buffer, uint64_t offset, uint64_t size);
int VfsWrite(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size);
int VfsCreate(const char* path);
uint64_t VfsGetSize(int handle);
void VfsClose(int handle);
int VfsReadDir(const char* path, const char** outNames, int maxEntries);
+4 -4
View File
@@ -8,11 +8,11 @@
#include <cstdint>
namespace Hal {
// IRQ handler function type. The parameter is the IRQ number (0-23).
// IRQ handler function type. The parameter is the IRQ number (0-47).
using IrqHandler = void(*)(uint8_t irq);
// Number of IRQ lines supported (IOAPIC inputs)
constexpr int IRQ_COUNT = 24;
// Number of IRQ slots supported (0-23: legacy ISA via IOAPIC, 24-47: MSI)
constexpr int IRQ_COUNT = 48;
// IRQ vector base: hardware IRQs start at IDT vector 32
constexpr uint8_t IRQ_VECTOR_BASE = 32;
@@ -29,7 +29,7 @@ namespace Hal {
constexpr uint8_t IRQ_ATA1 = 14;
constexpr uint8_t IRQ_ATA2 = 15;
// Register a handler for the given IRQ number (0-23)
// Register a handler for the given IRQ number (0-47)
void RegisterIrqHandler(uint8_t irq, IrqHandler handler);
// Install IRQ stubs into the IDT and set up the dispatch table
+50 -2
View File
@@ -65,8 +65,8 @@ IrqCommon:
iretq
; Define stubs for IRQs 0..23 (vectors 32..55)
; This covers all standard ISA IRQs plus some extra IOAPIC inputs
; Define stubs for IRQs 0..47 (vectors 32..79)
; 0-23: legacy ISA IRQs via IOAPIC, 24-47: MSI vectors
IRQ_STUB 0
IRQ_STUB 1
IRQ_STUB 2
@@ -91,6 +91,30 @@ IRQ_STUB 20
IRQ_STUB 21
IRQ_STUB 22
IRQ_STUB 23
IRQ_STUB 24
IRQ_STUB 25
IRQ_STUB 26
IRQ_STUB 27
IRQ_STUB 28
IRQ_STUB 29
IRQ_STUB 30
IRQ_STUB 31
IRQ_STUB 32
IRQ_STUB 33
IRQ_STUB 34
IRQ_STUB 35
IRQ_STUB 36
IRQ_STUB 37
IRQ_STUB 38
IRQ_STUB 39
IRQ_STUB 40
IRQ_STUB 41
IRQ_STUB 42
IRQ_STUB 43
IRQ_STUB 44
IRQ_STUB 45
IRQ_STUB 46
IRQ_STUB 47
; Spurious interrupt handler (vector 0xFF) - do nothing, no EOI
global IrqStubSpurious
@@ -125,3 +149,27 @@ IrqStubTable:
dq IrqStub21
dq IrqStub22
dq IrqStub23
dq IrqStub24
dq IrqStub25
dq IrqStub26
dq IrqStub27
dq IrqStub28
dq IrqStub29
dq IrqStub30
dq IrqStub31
dq IrqStub32
dq IrqStub33
dq IrqStub34
dq IrqStub35
dq IrqStub36
dq IrqStub37
dq IrqStub38
dq IrqStub39
dq IrqStub40
dq IrqStub41
dq IrqStub42
dq IrqStub43
dq IrqStub44
dq IrqStub45
dq IrqStub46
dq IrqStub47
+9 -2
View File
@@ -34,6 +34,7 @@
#include <Drivers/PS2/Keyboard.hpp>
#include <Drivers/PS2/Mouse.hpp>
#include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp>
#include <Net/Net.hpp>
#include <CppLib/BoxUI.hpp>
#include <Graphics/Cursor.hpp>
@@ -153,6 +154,10 @@ extern "C" void kmain() {
Drivers::PS2::Mouse::Initialize();
Drivers::Net::E1000::Initialize();
if (!Drivers::Net::E1000::IsInitialized()) {
KernelLogStream(INFO, "Init") << "E1000 not found, trying E1000E...";
Drivers::Net::E1000E::Initialize();
}
Net::Initialize();
}
#endif
@@ -188,7 +193,9 @@ extern "C" void kmain() {
Fs::Ramdisk::Read,
Fs::Ramdisk::GetSize,
Fs::Ramdisk::Close,
Fs::Ramdisk::ReadDir
Fs::Ramdisk::ReadDir,
Fs::Ramdisk::Write,
Fs::Ramdisk::Create
};
Fs::Vfs::RegisterDrive(0, &ramdiskDriver);
@@ -198,7 +205,7 @@ extern "C" void kmain() {
Zenith::InitializeSyscalls();
Sched::Initialize();
Sched::Spawn("0:/shell.elf");
Sched::Spawn("0:/os/init.elf");
// Enable preemptive scheduling via the APIC timer
Timekeeping::EnableSchedulerTick();
+9 -2
View File
@@ -10,6 +10,7 @@
#include <Net/Ipv4.hpp>
#include <Net/NetConfig.hpp>
#include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -19,6 +20,12 @@ using namespace Kt;
namespace Net::Arp {
static const uint8_t* GetActiveNicMac() {
if (Drivers::Net::E1000::IsInitialized())
return Drivers::Net::E1000::GetMacAddress();
return Drivers::Net::E1000E::GetMacAddress();
}
// ARP cache entry
struct CacheEntry {
uint32_t Ip;
@@ -107,7 +114,7 @@ namespace Net::Arp {
reply.ProtocolAddrLen = 4;
reply.Operation = Htons(OP_REPLY);
memcpy(reply.SenderMac, Drivers::Net::E1000::GetMacAddress(), 6);
memcpy(reply.SenderMac, GetActiveNicMac(), 6);
reply.SenderIp = GetIpAddress();
memcpy(reply.TargetMac, pkt->SenderMac, 6);
reply.TargetIp = senderIp;
@@ -141,7 +148,7 @@ namespace Net::Arp {
req.ProtocolAddrLen = 4;
req.Operation = Htons(OP_REQUEST);
memcpy(req.SenderMac, Drivers::Net::E1000::GetMacAddress(), 6);
memcpy(req.SenderMac, GetActiveNicMac(), 6);
req.SenderIp = GetIpAddress();
memset(req.TargetMac, 0, 6);
req.TargetIp = targetIp;
+15 -2
View File
@@ -9,6 +9,7 @@
#include <Net/Arp.hpp>
#include <Net/Ipv4.hpp>
#include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -17,6 +18,18 @@ using namespace Kt;
namespace Net::Ethernet {
static const uint8_t* GetActiveNicMac() {
if (Drivers::Net::E1000::IsInitialized())
return Drivers::Net::E1000::GetMacAddress();
return Drivers::Net::E1000E::GetMacAddress();
}
static bool ActiveNicSend(const uint8_t* data, uint16_t length) {
if (Drivers::Net::E1000::IsInitialized())
return Drivers::Net::E1000::SendPacket(data, length);
return Drivers::Net::E1000E::SendPacket(data, length);
}
void Initialize() {
KernelLogStream(OK, "Net") << "Ethernet layer initialized";
}
@@ -30,14 +43,14 @@ namespace Net::Ethernet {
Header* hdr = (Header*)frame;
memcpy(hdr->DestMac, destMac, 6);
memcpy(hdr->SrcMac, Drivers::Net::E1000::GetMacAddress(), 6);
memcpy(hdr->SrcMac, GetActiveNicMac(), 6);
hdr->EtherType = Htons(etherType);
memcpy(frame + HEADER_SIZE, payload, payloadLen);
uint16_t totalLen = HEADER_SIZE + payloadLen;
return Drivers::Net::E1000::SendPacket(frame, totalLen);
return ActiveNicSend(frame, totalLen);
}
void OnFrameReceived(const uint8_t* data, uint16_t length) {
+9 -4
View File
@@ -14,6 +14,7 @@
#include <Net/Socket.hpp>
#include <Net/NetConfig.hpp>
#include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -22,8 +23,8 @@ using namespace Kt;
namespace Net {
void Initialize() {
if (!Drivers::Net::E1000::IsInitialized()) {
KernelLogStream(WARNING, "Net") << "E1000 not initialized, skipping network stack";
if (!Drivers::Net::E1000::IsInitialized() && !Drivers::Net::E1000E::IsInitialized()) {
KernelLogStream(WARNING, "Net") << "No NIC initialized, skipping network stack";
return;
}
@@ -36,8 +37,12 @@ namespace Net {
Tcp::Initialize();
Socket::Initialize();
// Hook E1000 RX to our Ethernet dispatcher
Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived);
// Hook the active NIC's RX to our Ethernet dispatcher
if (Drivers::Net::E1000::IsInitialized()) {
Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived);
} else {
Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived);
}
// Send a gratuitous ARP to announce ourselves on the network
Arp::SendRequest(GetIpAddress());
+185 -2
View File
@@ -1,19 +1,42 @@
/*
* Socket.cpp
* Socket descriptor table wrapping kernel TCP layer
* Socket descriptor table wrapping kernel TCP and UDP layers
* Copyright (c) 2025 Daniel Hammer
*/
#include "Socket.hpp"
#include <Net/Tcp.hpp>
#include <Net/Udp.hpp>
#include <Net/NetConfig.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
using namespace Kt;
namespace Net::Socket {
// ---- UDP socket state ----
static constexpr uint32_t UDP_RING_SIZE = 4096;
static constexpr int MAX_UDP_SOCKETS = 16;
struct UdpDgramHeader {
uint32_t SrcIp;
uint16_t SrcPort;
uint16_t DataLen;
};
struct UdpSocketState {
uint8_t Ring[UDP_RING_SIZE];
uint32_t Head;
uint32_t Tail;
uint32_t Count;
uint16_t LocalPort;
bool Active;
};
static UdpSocketState g_udpSockets[MAX_UDP_SOCKETS] = {};
static SocketEntry g_sockets[MAX_SOCKETS] = {};
static uint16_t g_nextEphemeralPort = 49152;
@@ -30,15 +53,78 @@ namespace Net::Socket {
return true;
}
static UdpSocketState* AllocUdpState() {
for (int i = 0; i < MAX_UDP_SOCKETS; i++) {
if (!g_udpSockets[i].Active) {
g_udpSockets[i].Active = true;
g_udpSockets[i].Head = 0;
g_udpSockets[i].Tail = 0;
g_udpSockets[i].Count = 0;
g_udpSockets[i].LocalPort = 0;
return &g_udpSockets[i];
}
}
return nullptr;
}
static void FreeUdpState(UdpSocketState* state) {
if (state) {
if (state->LocalPort != 0) {
Udp::Unbind(state->LocalPort);
}
state->Active = false;
}
}
static void UdpSocketDispatcher(uint32_t srcIp, uint16_t srcPort,
uint16_t dstPort,
const uint8_t* data, uint16_t length) {
for (int i = 0; i < MAX_UDP_SOCKETS; i++) {
if (g_udpSockets[i].Active && g_udpSockets[i].LocalPort == dstPort) {
UdpSocketState* st = &g_udpSockets[i];
uint32_t needed = sizeof(UdpDgramHeader) + length;
if (st->Count + needed > UDP_RING_SIZE) {
return; // drop if buffer full
}
// Enqueue header
UdpDgramHeader hdr;
hdr.SrcIp = srcIp;
hdr.SrcPort = srcPort;
hdr.DataLen = length;
const uint8_t* hdrBytes = (const uint8_t*)&hdr;
for (uint32_t j = 0; j < sizeof(UdpDgramHeader); j++) {
st->Ring[st->Tail] = hdrBytes[j];
st->Tail = (st->Tail + 1) % UDP_RING_SIZE;
}
// Enqueue payload
for (uint16_t j = 0; j < length; j++) {
st->Ring[st->Tail] = data[j];
st->Tail = (st->Tail + 1) % UDP_RING_SIZE;
}
st->Count += needed;
return;
}
}
}
// ---- Public API ----
void Initialize() {
for (int i = 0; i < MAX_SOCKETS; i++) {
g_sockets[i].Active = false;
}
for (int i = 0; i < MAX_UDP_SOCKETS; i++) {
g_udpSockets[i].Active = false;
}
KernelLogStream(OK, "Net") << "Socket table initialized";
}
int Create(int type, int pid) {
if (type != SOCK_TCP) return -1;
if (type != SOCK_TCP && type != SOCK_UDP) return -1;
for (int i = 0; i < MAX_SOCKETS; i++) {
if (!g_sockets[i].Active) {
@@ -46,7 +132,18 @@ namespace Net::Socket {
g_sockets[i].Type = type;
g_sockets[i].OwnerPid = pid;
g_sockets[i].TcpConn = nullptr;
g_sockets[i].UdpState = nullptr;
g_sockets[i].LocalPort = 0;
if (type == SOCK_UDP) {
UdpSocketState* us = AllocUdpState();
if (!us) {
g_sockets[i].Active = false;
return -1;
}
g_sockets[i].UdpState = us;
}
return i;
}
}
@@ -55,6 +152,7 @@ namespace Net::Socket {
int Connect(int fd, uint32_t ip, uint16_t port, int pid) {
if (!ValidFd(fd, pid)) return -1;
if (g_sockets[fd].Type != SOCK_TCP) return -1;
if (g_sockets[fd].TcpConn != nullptr) return -1;
uint16_t srcPort = AllocEphemeralPort();
@@ -70,11 +168,20 @@ namespace Net::Socket {
int Bind(int fd, uint16_t port, int pid) {
if (!ValidFd(fd, pid)) return -1;
g_sockets[fd].LocalPort = port;
if (g_sockets[fd].Type == SOCK_UDP) {
UdpSocketState* us = g_sockets[fd].UdpState;
if (!us) return -1;
us->LocalPort = port;
if (!Udp::Bind(port, UdpSocketDispatcher)) return -1;
}
return 0;
}
int Listen(int fd, int pid) {
if (!ValidFd(fd, pid)) return -1;
if (g_sockets[fd].Type != SOCK_TCP) return -1;
if (g_sockets[fd].LocalPort == 0) return -1;
if (g_sockets[fd].TcpConn != nullptr) return -1;
@@ -87,6 +194,7 @@ namespace Net::Socket {
int Accept(int fd, int pid) {
if (!ValidFd(fd, pid)) return -1;
if (g_sockets[fd].Type != SOCK_TCP) return -1;
if (g_sockets[fd].TcpConn == nullptr) return -1;
Tcp::Connection* clientConn = Tcp::Accept(g_sockets[fd].TcpConn);
@@ -99,6 +207,7 @@ namespace Net::Socket {
g_sockets[i].Type = SOCK_TCP;
g_sockets[i].OwnerPid = pid;
g_sockets[i].TcpConn = clientConn;
g_sockets[i].UdpState = nullptr;
g_sockets[i].LocalPort = g_sockets[fd].LocalPort;
return i;
}
@@ -111,22 +220,92 @@ namespace Net::Socket {
int Send(int fd, const uint8_t* data, uint32_t len, int pid) {
if (!ValidFd(fd, pid)) return -1;
if (g_sockets[fd].Type != SOCK_TCP) return -1;
if (g_sockets[fd].TcpConn == nullptr) return -1;
return Tcp::Send(g_sockets[fd].TcpConn, data, (uint16_t)len);
}
int Recv(int fd, uint8_t* buf, uint32_t maxLen, int pid) {
if (!ValidFd(fd, pid)) return -1;
if (g_sockets[fd].Type != SOCK_TCP) return -1;
if (g_sockets[fd].TcpConn == nullptr) return -1;
return Tcp::ReceiveNonBlocking(g_sockets[fd].TcpConn, buf, (uint16_t)maxLen);
}
int SendTo(int fd, const uint8_t* data, uint32_t len,
uint32_t destIp, uint16_t destPort, int pid) {
if (!ValidFd(fd, pid)) return -1;
if (g_sockets[fd].Type != SOCK_UDP) return -1;
UdpSocketState* us = g_sockets[fd].UdpState;
if (!us) return -1;
// Auto-bind ephemeral port if not already bound
if (us->LocalPort == 0) {
uint16_t ep = AllocEphemeralPort();
us->LocalPort = ep;
g_sockets[fd].LocalPort = ep;
if (!Udp::Bind(ep, UdpSocketDispatcher)) return -1;
}
if (!Udp::Send(destIp, us->LocalPort, destPort, data, (uint16_t)len)) {
return -1;
}
return (int)len;
}
int RecvFrom(int fd, uint8_t* buf, uint32_t maxLen,
uint32_t* srcIp, uint16_t* srcPort, int pid) {
if (!ValidFd(fd, pid)) return -1;
if (g_sockets[fd].Type != SOCK_UDP) return -1;
UdpSocketState* us = g_sockets[fd].UdpState;
if (!us) return -1;
if (us->Count < sizeof(UdpDgramHeader)) {
return -1; // no data available
}
// Dequeue header
UdpDgramHeader hdr;
uint8_t* hdrBytes = (uint8_t*)&hdr;
for (uint32_t j = 0; j < sizeof(UdpDgramHeader); j++) {
hdrBytes[j] = us->Ring[us->Head];
us->Head = (us->Head + 1) % UDP_RING_SIZE;
}
us->Count -= sizeof(UdpDgramHeader);
// Dequeue payload
uint16_t copyLen = hdr.DataLen;
if (copyLen > maxLen) copyLen = (uint16_t)maxLen;
for (uint16_t j = 0; j < copyLen; j++) {
buf[j] = us->Ring[us->Head];
us->Head = (us->Head + 1) % UDP_RING_SIZE;
}
// Skip remaining data if buffer was too small
for (uint16_t j = copyLen; j < hdr.DataLen; j++) {
us->Head = (us->Head + 1) % UDP_RING_SIZE;
}
us->Count -= hdr.DataLen;
if (srcIp) *srcIp = hdr.SrcIp;
if (srcPort) *srcPort = hdr.SrcPort;
return (int)copyLen;
}
void Close(int fd, int pid) {
if (!ValidFd(fd, pid)) return;
if (g_sockets[fd].TcpConn != nullptr) {
Tcp::Close(g_sockets[fd].TcpConn);
g_sockets[fd].TcpConn = nullptr;
}
if (g_sockets[fd].UdpState != nullptr) {
FreeUdpState(g_sockets[fd].UdpState);
g_sockets[fd].UdpState = nullptr;
}
g_sockets[fd].Active = false;
}
@@ -137,6 +316,10 @@ namespace Net::Socket {
Tcp::Close(g_sockets[i].TcpConn);
g_sockets[i].TcpConn = nullptr;
}
if (g_sockets[i].UdpState != nullptr) {
FreeUdpState(g_sockets[i].UdpState);
g_sockets[i].UdpState = nullptr;
}
g_sockets[i].Active = false;
}
}
+12 -1
View File
@@ -1,6 +1,6 @@
/*
* Socket.hpp
* Socket descriptor table for userspace TCP access
* Socket descriptor table for userspace TCP/UDP access
* Copyright (c) 2025 Daniel Hammer
*/
@@ -14,11 +14,14 @@ namespace Net::Socket {
static constexpr int SOCK_UDP = 2;
static constexpr int MAX_SOCKETS = 64;
struct UdpSocketState;
struct SocketEntry {
bool Active;
int Type;
int OwnerPid;
Tcp::Connection* TcpConn;
UdpSocketState* UdpState;
uint16_t LocalPort;
};
@@ -45,6 +48,14 @@ namespace Net::Socket {
// Receive data from a connected socket. Returns bytes received, 0 on close, or -1.
int Recv(int fd, uint8_t* buf, uint32_t maxLen, int pid);
// Send a UDP datagram to a specific destination. Returns bytes sent or -1.
int SendTo(int fd, const uint8_t* data, uint32_t len,
uint32_t destIp, uint16_t destPort, int pid);
// Receive a UDP datagram, returning source info. Returns bytes received or -1.
int RecvFrom(int fd, uint8_t* buf, uint32_t maxLen,
uint32_t* srcIp, uint16_t* srcPort, int pid);
// Close a socket.
void Close(int fd, int pid);
+1 -1
View File
@@ -61,7 +61,7 @@ namespace Net::Udp {
// Dispatch to bound callback
for (uint32_t i = 0; i < MAX_BINDINGS; i++) {
if (g_bindings[i].Active && g_bindings[i].Port == dstPort) {
g_bindings[i].Callback(srcIp, srcPort, payload, payloadLen);
g_bindings[i].Callback(srcIp, srcPort, dstPort, payload, payloadLen);
return;
}
}
+3 -1
View File
@@ -19,7 +19,9 @@ namespace Net::Udp {
} __attribute__((packed));
// Callback type for receiving UDP data
using RecvCallback = void(*)(uint32_t srcIp, uint16_t srcPort, const uint8_t* data, uint16_t length);
using RecvCallback = void(*)(uint32_t srcIp, uint16_t srcPort,
uint16_t dstPort,
const uint8_t* data, uint16_t length);
// Initialize the UDP subsystem
void Initialize();
+26
View File
@@ -223,6 +223,32 @@ namespace Pci {
return LegacyRead8(bus, device, function, (uint8_t)offset);
}
// -------------------------------------------------------------------------
// PCI capability list traversal
// -------------------------------------------------------------------------
uint8_t FindCapability(uint8_t bus, uint8_t device, uint8_t function, uint8_t capId) {
// Check Status register bit 4 (Capabilities List present)
uint16_t status = ReadConfig16(bus, device, function, RegStatus);
if (!(status & (1 << 4))) {
return 0;
}
// Read Capabilities Pointer (offset 0x34), mask to dword-aligned
uint8_t offset = ReadConfig8(bus, device, function, 0x34) & 0xFC;
// Walk the linked list (cap_id @ +0, next_ptr @ +1)
while (offset != 0) {
uint8_t id = ReadConfig8(bus, device, function, offset);
if (id == capId) {
return offset;
}
offset = ReadConfig8(bus, device, function, offset + 1) & 0xFC;
}
return 0;
}
// -------------------------------------------------------------------------
// PCI class code names
// -------------------------------------------------------------------------
+7
View File
@@ -60,6 +60,13 @@ namespace Pci {
void LegacyWrite16(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint16_t value);
void LegacyWrite32(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint32_t value);
// PCI capability IDs
constexpr uint8_t PCI_CAP_MSI = 0x05;
// Walk the PCI capability linked list for a given device.
// Returns the config-space offset of the capability, or 0 if not found.
uint8_t FindCapability(uint8_t bus, uint8_t device, uint8_t function, uint8_t capId);
// Class code name lookup
const char* GetClassName(uint8_t classCode, uint8_t subClass);
-1
View File
@@ -54,7 +54,6 @@ namespace Sched {
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys) {
int handle = Fs::Vfs::VfsOpen(vfsPath);
if (handle < 0) {
Kt::KernelLogStream(Kt::ERROR, "ELF") << "Failed to open " << vfsPath;
return 0;
}
+14 -4
View File
@@ -106,7 +106,6 @@ namespace Sched {
// Load ELF into the process's address space
uint64_t entry = ElfLoad(vfsPath, pml4Phys);
if (entry == 0) {
Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to load ELF: " << vfsPath;
return -1;
}
@@ -204,6 +203,13 @@ namespace Sched {
}
void Schedule() {
// Reclaim terminated process slots so they can be reused
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].state == ProcessState::Terminated) {
processTable[i].state = ProcessState::Free;
}
}
int next = -1;
int start = (currentPid >= 0) ? currentPid + 1 : 0;
@@ -311,9 +317,13 @@ namespace Sched {
}
bool IsAlive(int pid) {
if (pid < 0 || pid >= MaxProcesses) return false;
return processTable[pid].state == ProcessState::Ready
|| processTable[pid].state == ProcessState::Running;
for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].pid == pid) {
return processTable[i].state == ProcessState::Ready
|| processTable[i].state == ProcessState::Running;
}
}
return false;
}
}
+6 -1
View File
@@ -11,6 +11,7 @@
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Sched/Scheduler.hpp>
#include <Drivers/Net/E1000E.hpp>
using namespace Kt;
@@ -37,10 +38,14 @@ namespace Timekeeping {
static bool g_schedEnabled = false;
// Timer IRQ handler: increment tick count and drive scheduler
// Timer IRQ handler: increment tick count, poll NIC, and drive scheduler
static void TimerHandler(uint8_t) {
g_tickCount = g_tickCount + 1;
// In polling mode, drain the NIC's RX ring from timer context
// (equivalent to a real NIC IRQ handler, runs with interrupts disabled)
Drivers::Net::E1000E::Poll();
if (g_schedEnabled) {
Sched::Tick();
}