SchOS/cmd/pcitest/main.c
Nexoniarz cb3d28e3d6 docs: README, plan, guides and the site describe what is built now
README: rewritten around what runs (Linux software, Xorg, apk, Linux's drivers), the honest gaps, measured numbers.
plan/: 1.0.md is the finish line, where each clause of the acceptance test stands, and the register (one failing
clause: a root that survives a reboot); built plans (foundations, hybrid, alpine, xorg) and the decided-against one
(wayland) are gone, their content lives in architecture/; roadmap.md is only what is still open.
guides: building/layout/testing/adding-a-driver (Linux code may be used). history: four entries (ls -lR, TLB
shootdowns, MDIO 0x02460246, netlink MSG_TRUNC). Website: cards, stack, download page, docs index.
Code comments that pointed at deleted plan docs point at the architecture docs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 08:08:05 +01:00

267 lines
9.7 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// pcitest -- prove that a driver could live out here.
//
// docs/architecture/devices.md mechanisms 2 and 4, exercised end to end from a process:
// enumerate the bus, claim a device, map its registers, read them, and get
// memory the device could be pointed at.
//
// It is deliberately a test and not a driver. The question it answers is "does
// the mechanism work", which has to be answered before the first real eviction
// -- otherwise a virtio-gpu that fails in userspace has two candidate causes
// and no way to tell them apart.
//
// pcitest list the bus
// pcitest <index> claim that device, map BAR0, read it, allocate DMA
// pcitest <index> x the same, then assert an interrupt and exit WITHOUT
// acking it -- which is what a driver that crashed looks
// like from the kernel's side, and the case that used to
// leave a level-triggered line asserted with no handler
#include "core/types.h"
#include "uapi/user.h"
#include "uapi/fcntl.h"
#include "uapi/posix.h"
#include "dev/pciuser.h"
static void
puts(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void *)s, n);
}
static void
puthex(unsigned long long v, int digits)
{
static const char h[] = "0123456789abcdef";
char b[17];
for (int i = digits - 1; i >= 0; i--) { b[i] = h[v & 0xF]; v >>= 4; }
write(1, b, digits);
}
static void
putn(int v)
{
char b[12];
int i = 0;
if (v < 0) { write(1, "-", 1); v = -v; }
if (v == 0) { write(1, "0", 1); return; }
while (v > 0) { b[i++] = '0' + (v % 10); v /= 10; }
while (i-- > 0) write(1, &b[i], 1);
}
static int
num(const char *s)
{
int v = 0;
while (*s >= '0' && *s <= '9') v = v * 10 + (*s++ - '0');
return v;
}
// What the class byte means, for the handful that matter here. Not a full
// table: a listing that says "class 02" is only marginally better than one
// that says nothing, and a full one is 200 lines of data nobody reads.
static const char *
class_name(unsigned char c)
{
switch (c) {
case 0x01: return "storage";
case 0x02: return "network";
case 0x03: return "display";
case 0x04: return "multimedia";
case 0x06: return "bridge";
case 0x0c: return "serial bus";
default: return "other";
}
}
int
main(int argc, char *argv[])
{
int fd = open("/dev/pci", O_RDWR);
if (fd < 0) {
puts("pcitest: cannot open /dev/pci\n");
exit();
}
int count = 0;
if (ioctl(fd, PCIIOC_COUNT, &count) < 0) {
puts("pcitest: PCIIOC_COUNT failed\n");
exit();
}
if (argc == 1) {
puts(" # bdf vendor:device irq class bm driver\n");
for (int i = 0; i < count; i++) {
struct pciuser_info in;
in.index = (unsigned int)i;
if (ioctl(fd, PCIIOC_INFO, &in) < 0)
continue;
// Bus mastering, straight out of the command register.
//
// Worth a column of its own because it is the one bit that says
// whether a device can still touch memory, and the whole lifecycle
// turns on it: claiming a device sets it, releasing one clears it,
// and a driver that died leaves a device that must not be left
// reading buffers the kernel is about to hand to somebody else.
struct pciuser_cfg cfg;
cfg.index = (unsigned int)i;
cfg.offset = 0x04; // PCI_COMMAND
cfg.value = 0;
// Three states, not two. A read that failed is not the same as a
// bit that is clear, and printing them the same way is how a
// column stops being evidence -- which is exactly what happened
// the first time this was run.
const char *bm = "? ";
if (ioctl(fd, PCIIOC_CFG, &cfg) == 0)
bm = (cfg.value & 0x0004) ? "on " : "-- ";
puts(" "); putn(i); puts(" ");
puthex(in.bus, 2); puts(":"); puthex(in.slot, 2);
puts("."); puthex(in.func, 1); puts(" ");
puthex(in.vendor, 4); puts(":"); puthex(in.device, 4); puts(" ");
if (in.irq == 0xFF) puts(" - "); else { putn(in.irq); puts(" "); }
puts(class_name(in.class));
puts(" ");
puts(bm);
puts(in.driver[0] ? in.driver : "-");
puts("\n");
}
puts("\npcitest <n> to claim one and read its registers\n");
exit();
}
int idx = num(argv[1]);
struct pciuser_info in;
in.index = (unsigned int)idx;
if (ioctl(fd, PCIIOC_INFO, &in) < 0) {
puts("pcitest: no device "); putn(idx); puts("\n");
exit();
}
puts("device "); putn(idx); puts(": ");
puthex(in.vendor, 4); puts(":"); puthex(in.device, 4);
puts(" ("); puts(class_name(in.class)); puts(")");
if (in.driver[0]) { puts(", held by "); puts(in.driver); }
puts("\n");
if (ioctl(fd, PCIIOC_CLAIM, &idx) < 0) {
// EBUSY is the expected answer for anything the kernel drives, and it
// is the right answer until a driver can be told to let go. Saying so
// beats a bare failure.
puts(" claim refused -- the kernel is driving it, or someone else is\n");
exit();
}
puts(" claimed\n");
// The first memory BAR. A device with none is not one a driver reaches
// this way, and saying that is more useful than mapping nothing.
int b = -1;
for (int i = 0; i < PCIUSER_MAXBAR; i++)
if (in.bar[i].base && (in.bar[i].flags & PCIUSER_BAR_MMIO)) { b = i; break; }
if (b < 0) {
puts(" no memory BAR to map\n");
exit();
}
puts(" BAR"); putn(b); puts(" at 0x"); puthex(in.bar[b].base, 16);
puts(", "); putn((int)(in.bar[b].size / 1024)); puts(" KB\n");
unsigned long long len = in.bar[b].size;
if (len > 4096) len = 4096; // a page is enough to prove the point
volatile unsigned int *regs =
(volatile unsigned int *)mmap(0, len, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, in.bar[b].base);
if (regs == 0 || (long)regs == -1) {
puts(" mmap of the BAR failed\n");
exit();
}
puts(" mapped at 0x"); puthex((unsigned long long)regs, 16); puts("\n");
puts(" regs[0..3] =");
for (int i = 0; i < 4; i++) {
puts(" 0x");
puthex(regs[i], 8);
}
puts("\n");
// All-ones across the board means the read did not reach the device: an
// unmapped PCI read returns 0xFFFFFFFF rather than faulting, so this is
// the difference between "mapped the wrong thing" and "it works", and it
// is worth checking rather than eyeballing.
if (regs[0] == 0xFFFFFFFFu && regs[1] == 0xFFFFFFFFu) {
puts(" all ones -- the mapping does not reach the device\n");
} else {
puts(" MMIO reaches the device\n");
}
struct pciuser_dma dma;
dma.size = 8192;
if (ioctl(fd, PCIIOC_DMA, &dma) < 0) {
puts(" DMA allocation failed\n");
exit();
}
puts(" DMA 8 KB at virtual 0x"); puthex(dma.uaddr, 16);
puts(", physical 0x"); puthex(dma.paddr, 16); puts("\n");
// Write through the mapping and read it back: proves the memory is really
// there and really writable, which a physical address alone does not.
volatile unsigned int *buf = (volatile unsigned int *)dma.uaddr;
buf[0] = 0x5C405; buf[1] = 0xD1DD1E;
if (buf[0] == 0x5C405u && buf[1] == 0xD1DD1Eu)
puts(" DMA buffer reads back what was written\n");
else
puts(" DMA buffer did not read back correctly\n");
// Mechanism 3. Ask for the device's interrupts, then make it interrupt.
//
// The e1000 family has an Interrupt Cause Set register at 0x00C8: writing
// a cause bit asserts the line exactly as the hardware would, which is the
// only way to test interrupt delivery without traffic arriving from
// somewhere. Reading ICR at 0x00C0 clears it, which is the ack the kernel
// cannot do for us.
if (in.vendor != 0x8086 || in.class != 0x02) {
puts("\nmechanisms 2 and 4 work: a driver could live out here.\n");
puts("(interrupts untested -- that part knows only Intel NICs)\n");
exit();
}
if (ioctl(fd, PCIIOC_IRQ, &idx) < 0) {
puts(" interrupts refused -- the line is shared with a kernel driver\n");
puts("\nmechanisms 2 and 4 work: a driver could live out here.\n");
exit();
}
puts(" interrupts routed here (line "); putn(in.irq); puts(")\n");
volatile unsigned int *IMS = &regs[0x00D0 / 4]; // interrupt mask set
volatile unsigned int *ICS = &regs[0x00C8 / 4]; // interrupt cause set
volatile unsigned int *ICR = &regs[0x00C0 / 4]; // cause read, clears
*IMS = 0x00000004; // LSC: link status change, a cause with no traffic
(void)*ICR; // clear anything already pending
*ICS = 0x00000004; // assert it
// Crash simulation: assert the cause and leave, without reading or
// acking. The line is asserted and this process is about to stop existing.
if (argc > 2 && argv[2][0] == 'x') {
puts(" interrupt asserted; exiting WITHOUT acking (simulated crash)\n");
exit();
}
unsigned long long n = 0;
if (read(fd, &n, sizeof n) != (int)sizeof n) {
puts(" read() did not return an interrupt\n");
exit();
}
puts(" interrupt delivered, count = "); putn((int)n); puts("\n");
unsigned int cause = *ICR; // ack at the device
puts(" ICR = 0x"); puthex(cause, 8); puts("\n");
write(fd, &n, sizeof n); // re-arm
puts("\nall three mechanisms work: a driver could live out here.\n");
exit();
}