diff --git a/Makefile b/Makefile index 930e947..824fbf2 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \ panic.o uaccess.o qr.o time.o ksym.o klog.o sbuf.o kmem.o) KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ - bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \ + bio.o blkdev.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \ pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \ dbg.o netuser.o audio.o) @@ -47,6 +47,7 @@ KERN_DRIVERS += $(BUILD)/kernel/drivers/net/r8169/r8169.o KERN_DRIVERS += $(BUILD)/kernel/drivers/net/rtl8139/rtl8139.o KERN_DRIVERS += $(BUILD)/kernel/drivers/audio/ac97.o KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o +KERN_DRIVERS += $(BUILD)/kernel/drivers/storage/ahci/ahci.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/core/usb.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hid/usb_hid.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o @@ -736,6 +737,13 @@ iso: schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI sysroot-install SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@ +# A partitioned disk -- GPT, an ESP and a SchFS partition -- which is what a +# real machine boots from: dd it to a USB stick or a disk. See +# tools/posix/mkdisk.sh. `make qemu-disk` boots it on q35 as the only disk. +disk: $(BUILD)/disk.img +$(BUILD)/disk.img: esp $(BUILD)/schfs.img tools/posix/mkdisk.sh + sh tools/posix/mkdisk.sh $(BUILD)/esp $(BUILD)/schfs.img $@ + # Boot the ISO the way a machine booting off a disc would: no hard disk at # all, so a kernel that still secretly depends on one fails here rather than # in someone's optical drive. @@ -1505,7 +1513,7 @@ qrtest: $(QRTEST) qrcheck: $(QRTEST) $(PYTHON) tools/python/qrcheck.py -.PHONY: all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \ +.PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \ schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \ schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ logo qrtest qrcheck crashqr usb-ids ovmf esp uefi diff --git a/docs/architecture/boot.md b/docs/architecture/boot.md index b76b26a..1deb93e 100644 --- a/docs/architecture/boot.md +++ b/docs/architecture/boot.md @@ -447,12 +447,28 @@ what makes a loop over candidates legal at all. |---|---|---| | 1 | `RAMDISK_DEV` | only in a `RAMDISK_ROOT` build, where it is an explicit choice | | 2 | `USBDISK_DEV` | a stick that is plugged in was plugged in on purpose | -| 3 | IDE 1 | what the old constant named | -| 4 | IDE 0 | usually the ESP, so usually not SchFS | +| 3 | registered partitions typed SchFS | labelled that way on purpose | +| 4 | every other registered device | whole disks, untyped and MBR partitions | +| 5 | IDE 1 | what the old constant named | +| 6 | IDE 0 | usually the ESP, so usually not SchFS | + +The **registered** devices are the ones found at probe time rather than known +at build time, numbered from 4 (`include/blkdev.h`): a SATA disk behind AHCI +(`sata0`), the USB stick seen a second time (`usb0`), and every partition on +either (`sata0p2`, `usb0p2`). When a disk registers, its GPT is read, or its MBR +if it has no GPT. Each partition then becomes a device whose block 0 is the +partition's first sector, so SchFS in a partition and SchFS on a whole disk are +the same bytes. The ESP is skipped because it is FAT. + +SchFS's GPT partition type is `47138FA9-B79C-47A6-9F7D-AC1F7F572D5E`. +`make disk` builds `build/disk.img` (`tools/posix/mkdisk.sh`), a GPT disk with a +64 MB ESP and a SchFS partition of that type. `dd` it to a stick or a disk, or +give it to QEMU's q35 as the only disk. Both boot, the root comes up as +`sata0p2` or `usb0p2`, and the q35 run is the same path a PC takes. `iderw()` *panics* on an absent drive rather than failing, so each candidate is -checked with `ide_disk_present()` / `usb_storage_present()` first. That is the -whole reason those predicates exist. +checked with `ide_disk_present()` / `usb_storage_present()` / +`blkdev_present()` first. That is the whole reason those predicates exist. When nothing matches, the panic names what it looked at, because "no root filesystem" with no list is the least useful thing a kernel can say at the one @@ -590,9 +606,10 @@ in the one process that runs first. stall, which is the wrong order for finding out. - **`/proc/bootmode` is read once, at start.** A program that wants to be quieter or louder later has nowhere to say so, and nothing re-reads it. -- **The root device is compiled in.** Booting from a USB disk needs the root - device to be selectable, which it is not; the USB mass-storage driver works - and a superblock reads back through `bread()`, but nothing can mount it. +- **One USB disk, found at boot.** The mass-storage driver drives the first + stick and no other, and a stick plugged in later is not seen. See + [usb.md](usb.md). +- **No NVMe yet.** SATA (AHCI) and IDE are the only fixed disks. - **`make ramroot` is the only way to boot with no data disk.** It links a filesystem image into the kernel binary, which costs boot time and carries a licensing constraint — see [../licensing.md](../licensing.md) §4. diff --git a/include/blkdev.h b/include/blkdev.h new file mode 100644 index 0000000..c55e188 --- /dev/null +++ b/include/blkdev.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +// Block devices that are found rather than assumed. +// +// The first four device numbers are fixed and always were: IDE 0 and 1, the +// ramdisk, the first USB stick. Everything since -- a SATA disk behind AHCI, +// an NVMe namespace, and every partition on any of them -- registers here at +// probe time and is handed the next free number from BLKDEV_FIRST. +// +// A partition is a block device in its own right, with block 0 at its first +// sector. That is the whole of partition support from a filesystem's side: +// SchFS on /dev/sata0p2 sees a volume that starts at zero, exactly as it would +// on a whole disk, and needs to know nothing about GPT -- which is also what +// a Linux driver for SchFS will see when it is handed /dev/sda2. + +#define BLKDEV_FIRST 4 +#define NBLKDEV 16 // so device numbers stay below SCHFS_DEV (64) + +// The GPT partition type of a SchFS volume, as it is written and printed: +// 47138FA9-B79C-47A6-9F7D-AC1F7F572D5E. sgdisk and fdisk accept it as is. +// Generated for SchFS on 2026-10-22; nothing else uses it. +#define SCHFS_GPT_TYPE "47138FA9-B79C-47A6-9F7D-AC1F7F572D5E" + +// What a driver supplies. `rw` moves `count` of the device's own sectors at +// `lba` to or from `buf`, a kernel address; it is never asked for more than +// BSIZE bytes at once. Returns 0, or -1 if the device refused. +struct blkdev_ops { + int (*rw)(void *priv, uint64 lba, uint32 count, void *buf, int write); + int (*flush)(void *priv); // 0 when there is nothing to flush +}; + +// Register a whole disk. Its partition table, if it has one, is read here and +// every partition registered after it. Returns the disk's device number, or +// -1 if the table is full. +int blkdev_register(const char *name, const struct blkdev_ops *ops, + void *priv, uint64 nsectors, uint32 secsize); + +int blkdev_present(uint dev); +void blkdev_rw(struct buf *b); +int blkdev_flush(uint dev); + +// For the root probe: the registered devices in order, partitions after the +// disk they are on. Returns the device number of the i'th, or -1 past the end. +int blkdev_nth(int i); +const char *blkdev_name(uint dev); +int blkdev_is_schfs(uint dev); // a partition typed SCHFS_GPT_TYPE + +int blkdev_info(char *buf, int cap); // /proc/partitions diff --git a/kernel/dev/bio.c b/kernel/dev/bio.c index 924d1fe..c7d300b 100644 --- a/kernel/dev/bio.c +++ b/kernel/dev/bio.c @@ -27,6 +27,7 @@ #include "buf.h" #include "x86.h" #include "fsstat.h" +#include "blkdev.h" struct fsstat fsstat; @@ -86,6 +87,12 @@ bdevrw_inner(struct buf *b) usb_storage_rw(b); return; } + // SATA, NVMe and every partition: registered at probe time rather than + // known at build time. See include/blkdev.h. + if(b->dev >= BLKDEV_FIRST){ + blkdev_rw(b); + return; + } iderw(b); } @@ -99,6 +106,8 @@ bdevflush(uint dev) return 0; // memory has no volatile stage below it if(dev == USBDISK_DEV) return usb_storage_flush(); + if(dev >= BLKDEV_FIRST) + return blkdev_flush(dev); return ideflush(dev); } diff --git a/kernel/dev/blkdev.c b/kernel/dev/blkdev.c new file mode 100644 index 0000000..ac3a785 --- /dev/null +++ b/kernel/dev/blkdev.c @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// The block device registry, and the partition tables on what is in it. +// See include/blkdev.h. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "fs.h" +#include "buf.h" +#include "blkdev.h" + +struct blkdev { + int used; + char name[16]; + const struct blkdev_ops *ops; + void *priv; + uint64 start; // first sector, in the disk's own sectors + uint64 nsectors; + uint32 secsize; + int parent; // the disk's slot for a partition, -1 for a disk + int schfs; // GPT says this is a SchFS volume +}; + +static struct blkdev devs[NBLKDEV]; +static struct spinlock blklock; +static int blkinit; + +static struct blkdev * +lookup(uint dev) +{ + if (dev < BLKDEV_FIRST || dev >= BLKDEV_FIRST + NBLKDEV) + return 0; + struct blkdev *d = &devs[dev - BLKDEV_FIRST]; + return d->used ? d : 0; +} + +static int +add(const char *name, const struct blkdev_ops *ops, void *priv, + uint64 start, uint64 nsectors, uint32 secsize, int parent, int schfs) +{ + if (!blkinit) { + initlock(&blklock, "blkdev"); + blkinit = 1; + } + acquire(&blklock); + for (int i = 0; i < NBLKDEV; i++) { + if (devs[i].used) + continue; + struct blkdev *d = &devs[i]; + safestrcpy(d->name, name, sizeof(d->name)); + d->ops = ops; + d->priv = priv; + d->start = start; + d->nsectors = nsectors; + d->secsize = secsize; + d->parent = parent; + d->schfs = schfs; + __sync_synchronize(); + d->used = 1; + release(&blklock); + return BLKDEV_FIRST + i; + } + release(&blklock); + cprintf("blkdev: no slot for %s; it will not be usable\n", name); + return -1; +} + +// --- partition tables ------------------------------------------------------- + +static uint32 le32(const uchar *p) { return p[0] | p[1] << 8 | p[2] << 16 | (uint32)p[3] << 24; } +static uint64 le64(const uchar *p) { return le32(p) | (uint64)le32(p + 4) << 32; } + +// A GUID as GPT stores it: the first three fields little-endian, the last two +// as bytes. Compared against the printed form, so the constant in blkdev.h is +// the one a person types into sgdisk. +static int +guid_is(const uchar *g, const char *text) +{ + static const int order[16] = { 3,2,1,0, 5,4, 7,6, 8,9, 10,11,12,13,14,15 }; + uchar want[16]; + int n = 0; + + for (const char *p = text; *p && n < 32; p++) { + int v; + if (*p == '-') + continue; + if (*p >= '0' && *p <= '9') v = *p - '0'; + else if (*p >= 'a' && *p <= 'f') v = *p - 'a' + 10; + else if (*p >= 'A' && *p <= 'F') v = *p - 'A' + 10; + else return 0; + if (n & 1) want[n / 2] |= v; else want[n / 2] = v << 4; + n++; + } + for (int i = 0; i < 16; i++) + if (g[order[i]] != want[i]) + return 0; + return 1; +} + +static int +guid_zero(const uchar *g) +{ + for (int i = 0; i < 16; i++) + if (g[i]) + return 0; + return 1; +} + +static void +partname(char *out, const char *disk, int n) +{ + int i = 0; + for (; disk[i] && i < 12; i++) + out[i] = disk[i]; + out[i++] = 'p'; + if (n >= 10) + out[i++] = '0' + n / 10; + out[i++] = '0' + n % 10; + out[i] = 0; +} + +static int +readsec(struct blkdev *d, uint64 lba, uchar *buf) +{ + return d->ops->rw(d->priv, lba, 1, buf, 0); +} + +#define ESP_TYPE "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + +// The GUID partition table: the header in sector 1, and its entries wherever +// it says. Only the primary copy is read -- a disk whose primary is damaged +// gets reported as having no partitions rather than half of some. +// +// The EFI system partition is skipped. It is FAT, nothing here can mount it, +// and registering it would only give the root probe one more thing to try. +static int +scan_gpt(int disk, uchar *sec) +{ + struct blkdev *d = &devs[disk]; + int found = 0; + + if (readsec(d, 1, sec) < 0 || memcmp(sec, "EFI PART", 8) != 0) + return -1; + + uint64 elba = le64(sec + 72); + uint32 count = le32(sec + 80); + uint32 esize = le32(sec + 84); + if (esize < 128 || esize > d->secsize || (esize & (esize - 1)) || + count > 256 || elba < 2 || elba >= d->nsectors) { + cprintf("blkdev: %s: GPT header makes no sense; ignoring it\n", d->name); + return -1; + } + + uint32 per = d->secsize / esize; + for (uint32 i = 0; i < count; i++) { + if (i % per == 0 && readsec(d, elba + i / per, sec) < 0) + break; + uchar *e = sec + (i % per) * esize; + if (guid_zero(e) || guid_is(e, ESP_TYPE)) + continue; + uint64 first = le64(e + 32), last = le64(e + 40); + if (first == 0 || last < first || last >= d->nsectors) + continue; + char name[16]; + partname(name, d->name, i + 1); + int schfs = guid_is(e, SCHFS_GPT_TYPE); + if (add(name, d->ops, d->priv, first, last - first + 1, d->secsize, + disk, schfs) >= 0) { + kinfo("blkdev: %s: %d MB%s\n", name, + (int)(((last - first + 1) * d->secsize) >> 20), + schfs ? ", SchFS" : ""); + found++; + } + } + return found; +} + +// The PC partition table, for disks that have one instead. Primary entries +// only; a protective entry (0xEE) means the disk is GPT and was read above. +static void +scan_mbr(int disk, uchar *sec) +{ + struct blkdev *d = &devs[disk]; + + if (readsec(d, 0, sec) < 0 || sec[510] != 0x55 || sec[511] != 0xAA) + return; + for (int i = 0; i < 4; i++) { + uchar *e = sec + 446 + i * 16; + uint32 type = e[4], first = le32(e + 8), n = le32(e + 12); + // Empty, protective, extended (which would need its own chain read), and + // FAT/ESP types: none of them can be SchFS. + if (type == 0 || type == 0xEE || type == 0x05 || type == 0x0F || + type == 0xEF || n == 0 || first + (uint64)n > d->nsectors) + continue; + char name[16]; + partname(name, d->name, i + 1); + if (add(name, d->ops, d->priv, first, n, d->secsize, disk, 0) >= 0) + kinfo("blkdev: %s: %d MB, MBR type %x\n", name, + (int)(((uint64)n * d->secsize) >> 20), type); + } +} + +int +blkdev_register(const char *name, const struct blkdev_ops *ops, void *priv, + uint64 nsectors, uint32 secsize) +{ + if (secsize == 0 || secsize > BSIZE || BSIZE % secsize) + return -1; + int dev = add(name, ops, priv, 0, nsectors, secsize, -1, 0); + if (dev < 0) + return -1; + + uchar *sec = (uchar *)kalloc(); + if (sec) { + if (scan_gpt(dev - BLKDEV_FIRST, sec) < 0) + scan_mbr(dev - BLKDEV_FIRST, sec); + kfree((char *)sec); + } + return dev; +} + +// --- I/O --------------------------------------------------------------------- + +int +blkdev_present(uint dev) +{ + return lookup(dev) != 0; +} + +// Same contract as iderw(): fill b->data and set B_VALID on a read, write it +// out and clear B_DIRTY on a write. A failed transfer panics, for the reason +// usb_storage_rw() gives: bread() has no way to report one, and a buffer that +// silently holds the wrong bytes is how a filesystem is corrupted. +void +blkdev_rw(struct buf *b) +{ + struct blkdev *d = lookup(b->dev); + int write = (b->flags & B_DIRTY) != 0; + + if (!holdingsleep(&b->lock)) + panic("blkdev_rw: buf not locked"); + if (!d) + panic("blkdev_rw: no such device"); + + uint32 per = BSIZE / d->secsize; + uint64 lba = b->blockno * per; + if (lba + per > d->nsectors) + panic("blkdev_rw: block out of range"); + + if (d->ops->rw(d->priv, d->start + lba, per, b->data, write) < 0) { + cprintf("blkdev: %s: %s of block %d failed\n", d->name, + write ? "write" : "read", (int)b->blockno); + panic("blkdev_rw: transfer failed"); + } + b->flags |= B_VALID; + b->flags &= ~B_DIRTY; +} + +int +blkdev_flush(uint dev) +{ + struct blkdev *d = lookup(dev); + if (!d || !d->ops->flush) + return 0; + return d->ops->flush(d->priv); +} + +int +blkdev_nth(int i) +{ + for (int s = 0; s < NBLKDEV; s++) + if (devs[s].used && i-- == 0) + return BLKDEV_FIRST + s; + return -1; +} + +const char * +blkdev_name(uint dev) +{ + struct blkdev *d = lookup(dev); + return d ? d->name : "?"; +} + +int +blkdev_is_schfs(uint dev) +{ + struct blkdev *d = lookup(dev); + return d ? d->schfs : 0; +} + +// --- /proc/partitions -------------------------------------------------------- + +static int +put(char *buf, int cap, int off, const char *s) +{ + while (*s && off < cap) + buf[off++] = *s++; + return off; +} + +static int +putu(char *buf, int cap, int off, uint64 v) +{ + char t[24]; + int n = 0; + do { t[n++] = '0' + v % 10; v /= 10; } while (v); + while (n && off < cap) + buf[off++] = t[--n]; + return off; +} + +int +blkdev_info(char *buf, int cap) +{ + int off = put(buf, cap, 0, "dev name MB type\n"); + for (int s = 0; s < NBLKDEV; s++) { + struct blkdev *d = &devs[s]; + if (!d->used) + continue; + off = putu(buf, cap, off, BLKDEV_FIRST + s); + off = put(buf, cap, off, " "); + off = put(buf, cap, off, d->name); + for (int pad = 12 - strlen(d->name); pad > 0; pad--) + off = put(buf, cap, off, " "); + off = putu(buf, cap, off, (d->nsectors * d->secsize) >> 20); + off = put(buf, cap, off, d->parent < 0 ? " disk" : + d->schfs ? " schfs" : " part"); + off = put(buf, cap, off, "\n"); + } + return off; +} diff --git a/kernel/drivers/registry.c b/kernel/drivers/registry.c index 72c8449..8513338 100644 --- a/kernel/drivers/registry.c +++ b/kernel/drivers/registry.c @@ -35,6 +35,7 @@ extern const struct pci_driver e1000_driver; extern const struct pci_driver ac97_driver; extern const struct pci_driver r8169_driver; extern const struct pci_driver rtl8139_driver; +extern const struct pci_driver ahci_driver; // Order matters here in one respect: virtio-gpu is offered the bus before the // VBE framebuffer driver, so that on a machine with both, the device with real @@ -48,6 +49,7 @@ static const struct pci_driver *drivers[] = { &r8169_driver, &rtl8139_driver, &xhci_driver, + &ahci_driver, &ac97_driver, }; diff --git a/kernel/drivers/storage/ahci/ahci.c b/kernel/drivers/storage/ahci/ahci.c new file mode 100644 index 0000000..0b5b743 --- /dev/null +++ b/kernel/drivers/storage/ahci/ahci.c @@ -0,0 +1,468 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// AHCI: SATA disks, written from the Serial ATA AHCI 1.3.1 specification. +// +// This is how every PC since about 2008 attaches its internal disk, and the +// IDE driver beside it is how almost none of them do. Under QEMU's q35 the +// boot disk is here, which is why that machine used to stop with "no root +// filesystem" while the very same image booted on i440fx. +// +// The model is small. The HBA has up to 32 ports; each has a command list of +// up to 32 slots, each slot points at a command table, and the table holds +// the ATA command as a register FIS plus a scatter list of where the data +// goes. Issue a command by setting its bit in PxCI; the HBA clears the bit +// when the command is done. +// +// One slot per port and **completion by polling**, deliberately. A disk is +// on the path to a shell, and polling PxCI is the one way of finishing a +// command that does not depend on interrupt routing -- which on real hardware +// without an AML interpreter is the part that cannot be trusted (see +// kernel/dev/irq.c). A SATA command completes in tens of microseconds to a +// few milliseconds, so the cost is that, spent spinning, and it is yielded +// once a wait goes long. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "x86.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "fs.h" +#include "file.h" +#include "proc.h" +#include "pci.h" +#include "dma.h" +#include "blkdev.h" + +// --- HBA registers (section 3.1) --------------------------------------------- + +#define HBA_CAP 0x00 +#define CAP_S64A (1u << 31) // 64-bit DMA addresses +#define CAP_SSS (1u << 27) // staggered spin-up: ports start spun down +#define HBA_GHC 0x04 +#define GHC_AE (1u << 31) // AHCI enable +#define GHC_HR (1u << 0) // HBA reset +#define HBA_PI 0x0C // ports implemented +#define HBA_VS 0x10 +#define HBA_CAP2 0x24 +#define CAP2_BOH (1u << 0) // BIOS/OS handoff supported +#define HBA_BOHC 0x28 +#define BOHC_BOS (1u << 0) // the BIOS owns the HBA +#define BOHC_OOS (1u << 1) // the OS asks for it +#define BOHC_BB (1u << 4) // BIOS busy + +// --- port registers (section 3.3), at 0x100 + port * 0x80 -------------------- + +#define PX_CLB 0x00 +#define PX_CLBU 0x04 +#define PX_FB 0x08 +#define PX_FBU 0x0C +#define PX_IS 0x10 +#define PX_IE 0x14 +#define PX_CMD 0x18 +#define CMD_ST (1u << 0) // start processing the command list +#define CMD_SUD (1u << 1) // spin up +#define CMD_POD (1u << 2) // power on +#define CMD_FRE (1u << 4) // FIS receive enable +#define CMD_FR (1u << 14) // FIS receive running +#define CMD_CR (1u << 15) // command list running +#define PX_TFD 0x20 +#define TFD_ERR 0x01 +#define TFD_DRQ 0x08 +#define TFD_BSY 0x80 +#define PX_SIG 0x24 +#define SIG_ATA 0x00000101 +#define SIG_ATAPI 0xEB140101 +#define PX_SSTS 0x28 +#define PX_SCTL 0x2C +#define PX_SERR 0x30 +#define PX_CI 0x38 + +#define IS_TFES (1u << 30) // task file error + +// --- ATA --------------------------------------------------------------------- + +#define ATA_IDENTIFY 0xEC +#define ATA_READ_DMA_EXT 0x25 +#define ATA_WRITE_DMA_EXT 0x35 +#define ATA_FLUSH_EXT 0xEA + +#define FIS_H2D 0x27 + +// One command header (section 4.2.2). +struct cmdhdr { + uint16 flags; // CFL in 4:0, A 5, W 6, P 7, R 8, B 9, C 10 + uint16 prdtl; // scatter entries in the table + uint32 prdbc; // bytes transferred, written by the HBA + uint32 ctba, ctbau; // the command table, 128-byte aligned + uint32 rsv[4]; +}; + +struct prd { + uint32 dba, dbau; + uint32 rsv; + uint32 dbc; // byte count - 1 in 21:0, interrupt-on-completion 31 +}; + +// The command table (section 4.2.3): the FIS, an ATAPI command we never send, +// and one scatter entry -- every transfer here is one bounce buffer. +struct cmdtbl { + uint8 cfis[64]; + uint8 acmd[16]; + uint8 rsv[48]; + struct prd prdt[1]; +}; + +#define AHCI_MAX_PORTS 32 +#define AHCI_MAX_DISKS 8 + +struct ahci_port { + volatile uint8 *r; // this port's registers + struct cmdhdr *cl; // command list, 1 KB + uint8 *fis; // received FIS area, 256 bytes + struct cmdtbl *ct; // the one command table + uint8 *xfer; // bounce buffer, BSIZE + uint64 nsectors; + uint32 secsize; + struct sleeplock lock; // one command at a time on this port + char name[8]; + int dev; +}; + +static struct ahci_port disks[AHCI_MAX_DISKS]; +static int ndisks; +static int nhba; + +static inline uint32 rd(volatile uint8 *b, int o) { return *(volatile uint32 *)(b + o); } +static inline void wr(volatile uint8 *b, int o, uint32 v) { *(volatile uint32 *)(b + o) = v; } + +// Spin until (reg & mask) == want, for at most `ms`. Returns 0 or -1. +static int +waitreg(volatile uint8 *b, int o, uint32 mask, uint32 want, int ms) +{ + for (int i = 0; i < ms * 100; i++) { + if ((rd(b, o) & mask) == want) + return 0; + microdelay(10); + } + return (rd(b, o) & mask) == want ? 0 : -1; +} + +// Stop the port's engines, which is the precondition for touching CLB and FB. +// Section 10.1.2: clear ST and wait for CR, clear FRE and wait for FR. +static int +port_stop(volatile uint8 *r) +{ + wr(r, PX_CMD, rd(r, PX_CMD) & ~CMD_ST); + if (waitreg(r, PX_CMD, CMD_CR, 0, 500) < 0) + return -1; + wr(r, PX_CMD, rd(r, PX_CMD) & ~CMD_FRE); + return waitreg(r, PX_CMD, CMD_FR, 0, 500); +} + +// The link, if any. DET = 3 is "device present and communication established" +// and IPM = 1 is "active". Firmware normally leaves a link up; when it has not, +// a COMRESET (section 10.4.2) brings it up, and a port with nothing on it +// simply never answers. +static int +port_link(volatile uint8 *r) +{ + uint32 ssts = rd(r, PX_SSTS); + if ((ssts & 0xF) == 3) + return 1; + + wr(r, PX_SCTL, (rd(r, PX_SCTL) & ~0xFu) | 1); // DET = 1: COMRESET + microdelay(2000); // at least 1 ms + wr(r, PX_SCTL, rd(r, PX_SCTL) & ~0xFu); + for (int i = 0; i < 100; i++) { // up to 1 s for the link + if ((rd(r, PX_SSTS) & 0xF) == 3) + return 1; + microdelay(10000); + } + return 0; +} + +// Issue the command in slot 0 and wait for it. `buf` is BSIZE bytes of the +// port's bounce buffer, which is where the data goes or comes from. +static int +port_exec(struct ahci_port *p, uint8 cmd, uint64 lba, uint32 count, + uint32 bytes, int write) +{ + volatile uint8 *r = p->r; + struct cmdtbl *t = p->ct; + + // Wait out anything still busy from before. A port that stays busy is one + // the drive has wedged; failing is better than issuing into it. + if (waitreg(r, PX_TFD, TFD_BSY | TFD_DRQ, 0, 1000) < 0) + return -1; + + memset(t, 0, sizeof(*t)); + uint8 *f = t->cfis; + f[0] = FIS_H2D; + f[1] = 0x80; // C: this is a command, not a control + f[2] = cmd; + f[4] = lba; f[5] = lba >> 8; f[6] = lba >> 16; + f[7] = 0x40; // LBA mode + f[8] = lba >> 24; f[9] = lba >> 32; f[10] = lba >> 40; + f[12] = count; f[13] = count >> 8; + + struct cmdhdr *h = &p->cl[0]; + h->flags = 5 | (write ? (1 << 6) : 0); // a register FIS is five dwords + h->prdbc = 0; + if (bytes) { + uint64 pa = dma_phys(p->xfer); + t->prdt[0].dba = (uint32)pa; + t->prdt[0].dbau = (uint32)(pa >> 32); + t->prdt[0].dbc = bytes - 1; + h->prdtl = 1; + } else { + h->prdtl = 0; + } + + wr(r, PX_IS, 0xFFFFFFFF); + __sync_synchronize(); + wr(r, PX_CI, 1); + + // Spin for the first few milliseconds, which is when nearly every command + // finishes; after that give the processor away between looks. yield() only + // from a process -- the root probe and IDENTIFY run before there is one. + uint64 start = rdtsc(), limit = tsc_hz * 10; // ten seconds + while (rd(r, PX_CI) & 1) { + if (rd(r, PX_IS) & IS_TFES) + break; + uint64 now = rdtsc() - start; + if (now > limit) { + cprintf("ahci: %s: command %x timed out\n", p->name, cmd); + return -1; + } + if (now > tsc_hz / 200 && myproc()) + yield(); + else + asm volatile("pause"); + } + + if ((rd(r, PX_IS) & IS_TFES) || (rd(r, PX_TFD) & TFD_ERR)) { + cprintf("ahci: %s: command %x failed, tfd %x\n", p->name, cmd, + rd(r, PX_TFD)); + return -1; + } + return 0; +} + +static int +ahci_rw(void *priv, uint64 lba, uint32 count, void *buf, int write) +{ + struct ahci_port *p = priv; + uint32 bytes = count * p->secsize; + int rc; + + if (bytes > BSIZE) + return -1; + acquiresleep(&p->lock); + if (write) + memmove(p->xfer, buf, bytes); + rc = port_exec(p, write ? ATA_WRITE_DMA_EXT : ATA_READ_DMA_EXT, + lba, count, bytes, write); + if (rc == 0 && !write) + memmove(buf, p->xfer, bytes); + releasesleep(&p->lock); + return rc; +} + +static int +ahci_flush(void *priv) +{ + struct ahci_port *p = priv; + acquiresleep(&p->lock); + int rc = port_exec(p, ATA_FLUSH_EXT, 0, 0, 0, 0); + releasesleep(&p->lock); + return rc; +} + +static const struct blkdev_ops ahci_ops = { + .rw = ahci_rw, + .flush = ahci_flush, +}; + +// IDENTIFY DEVICE's model string: forty characters, byte-swapped in pairs, +// padded with spaces. +static void +model_of(const uint16 *id, char *out, int cap) +{ + int n = 0; + for (int w = 27; w < 47 && n + 2 < cap; w++) { + out[n++] = id[w] >> 8; + out[n++] = id[w] & 0xFF; + } + while (n > 0 && out[n - 1] == ' ') + n--; + out[n] = 0; +} + +// Bring one port up and, if a SATA disk answers on it, register it. +static void +port_init(volatile uint8 *hba, int port, uint32 cap) +{ + volatile uint8 *r = hba + 0x100 + port * 0x80; + + if (ndisks >= AHCI_MAX_DISKS) + return; + if (port_stop(r) < 0) { + cprintf("ahci: port %d will not stop; skipping it\n", port); + return; + } + + // Power and spin up first: with staggered spin-up the drive is not even + // turning until SUD is set, and its link never comes up until it is. + if (cap & CAP_SSS) + wr(r, PX_CMD, rd(r, PX_CMD) | CMD_POD | CMD_SUD); + + if (!port_link(r)) + return; // nothing plugged in + + struct ahci_port *p = &disks[ndisks]; + memset(p, 0, sizeof(*p)); + p->r = r; + p->cl = dma_alloc(1024, 1024); + p->fis = dma_alloc(256, 256); + p->ct = dma_alloc(sizeof(struct cmdtbl), 128); + p->xfer = dma_alloc(BSIZE, 4096); + if (!p->cl || !p->fis || !p->ct || !p->xfer) { + cprintf("ahci: no DMA memory for port %d\n", port); + return; + } + // Without S64A the HBA cannot see above 4 GB. The DMA arena is below it + // (see include/dma.h), so this holds, and says so if that ever changes. + if (!(cap & CAP_S64A) && dma_phys(p->xfer) + BSIZE > 0xFFFFFFFFULL) { + cprintf("ahci: DMA memory above 4 GB and the HBA is 32-bit\n"); + return; + } + memset(p->cl, 0, 1024); + memset(p->fis, 0, 256); + + uint64 cl = dma_phys(p->cl), fb = dma_phys(p->fis), ct = dma_phys(p->ct); + p->cl[0].ctba = (uint32)ct; + p->cl[0].ctbau = (uint32)(ct >> 32); + wr(r, PX_CLB, (uint32)cl); + wr(r, PX_CLBU, (uint32)(cl >> 32)); + wr(r, PX_FB, (uint32)fb); + wr(r, PX_FBU, (uint32)(fb >> 32)); + + wr(r, PX_SERR, 0xFFFFFFFF); // write-1-to-clear + wr(r, PX_IS, 0xFFFFFFFF); + wr(r, PX_IE, 0); // polled; see the top of the file + wr(r, PX_CMD, rd(r, PX_CMD) | CMD_FRE); + + // The drive's first D2H FIS sets the signature; until then it reads all + // ones. ATAPI (an optical drive) and port multipliers are not disks here. + if (waitreg(r, PX_TFD, TFD_BSY | TFD_DRQ, 0, 3000) < 0) { + cprintf("ahci: port %d: the drive stays busy\n", port); + return; + } + uint32 sig = rd(r, PX_SIG); + if (sig != SIG_ATA) { + if (sig == SIG_ATAPI) + kinfo("ahci: port %d: ATAPI device, not used\n", port); + return; + } + + wr(r, PX_CMD, rd(r, PX_CMD) | CMD_ST); + + p->name[0] = 's'; p->name[1] = 'a'; p->name[2] = 't'; p->name[3] = 'a'; + p->name[4] = '0' + ndisks; p->name[5] = 0; + initsleeplock(&p->lock, "ahci"); + p->secsize = 512; + + if (port_exec(p, ATA_IDENTIFY, 0, 0, 512, 0) < 0) { + cprintf("ahci: port %d: IDENTIFY failed\n", port); + return; + } + uint16 *id = (uint16 *)p->xfer; + if (!(id[83] & (1 << 10))) { + cprintf("ahci: port %d: no 48-bit LBA; not supported\n", port); + return; + } + p->nsectors = (uint64)id[100] | (uint64)id[101] << 16 | + (uint64)id[102] << 32 | (uint64)id[103] << 48; + // A logical sector larger than 512 (word 106 bit 12, size in 117-118 as + // words). Rare, and handled: the block layer works in BSIZE and asks for + // BSIZE / secsize of them. + if ((id[106] & 0xC000) == 0x4000 && (id[106] & (1 << 12))) + p->secsize = 2 * ((uint32)id[117] | (uint32)id[118] << 16); + if (p->secsize < 512 || p->secsize > BSIZE || BSIZE % p->secsize) { + cprintf("ahci: port %d: sector size %d not supported\n", port, p->secsize); + return; + } + + char model[44]; + model_of(id, model, sizeof(model)); + ndisks++; + kinfo("ahci: %s: %s, %d MB, port %d\n", p->name, model, + (int)((p->nsectors * p->secsize) >> 20), port); + + p->dev = blkdev_register(p->name, &ahci_ops, p, p->nsectors, p->secsize); +} + +// Take the HBA from the firmware if it is holding it (section 10.6). Most +// firmware lets go at ExitBootServices; the specification says to ask, and +// some does not let go until asked. +static void +bios_handoff(volatile uint8 *hba) +{ + if (!(rd(hba, HBA_CAP2) & CAP2_BOH)) + return; + wr(hba, HBA_BOHC, rd(hba, HBA_BOHC) | BOHC_OOS); + if (waitreg(hba, HBA_BOHC, BOHC_BOS, 0, 25) < 0 || + (rd(hba, HBA_BOHC) & BOHC_BB)) { + // BIOS busy: it is allowed two seconds to finish what it is doing. + waitreg(hba, HBA_BOHC, BOHC_BB | BOHC_BOS, 0, 2000); + } +} + +static int +ahci_probe(struct pci_dev *d) +{ + if (d->prog_if != 0x01) + return -1; // a RAID or IDE-mode controller + if (!d->bar[5].base || d->bar[5].is_io) { + cprintf("ahci: no ABAR; not driving this controller\n"); + return -1; + } + + pci_enable(d, PCI_CMD_MEMORY | PCI_CMD_BUS_MASTER); + volatile uint8 *hba = ioremap(d->bar[5].base, + d->bar[5].size ? d->bar[5].size : 0x1100); + if (!hba) + return -1; + + bios_handoff(hba); + + // AHCI mode, with interrupts off at the HBA. No HBA reset: firmware has + // already brought the links up, and resetting throws that away for a + // second or more of COMRESETs per port to get it back. + wr(hba, HBA_GHC, (rd(hba, HBA_GHC) | GHC_AE) & ~(1u << 1)); // IE off + + uint32 cap = rd(hba, HBA_CAP), pi = rd(hba, HBA_PI); + uint32 vs = rd(hba, HBA_VS); + kinfo("ahci: controller %x:%x, AHCI %d.%d, %d ports\n", + d->vendor, d->device, vs >> 16, (vs >> 8) & 0xFF, (cap & 0x1F) + 1); + + for (int port = 0; port < AHCI_MAX_PORTS; port++) + if (pi & (1u << port)) + port_init(hba, port, cap); + + nhba++; + return 0; +} + +static const struct pci_match ahci_match[] = { + { PCI_ANY, PCI_ANY, 0x01, 0x06 }, // mass storage, SATA + { 0, 0, 0, 0 }, +}; + +const struct pci_driver ahci_driver = { + .name = "ahci", + .devname = "sata", + .match = ahci_match, + .probe = ahci_probe, +}; diff --git a/kernel/drivers/usb/storage/usb_storage.c b/kernel/drivers/usb/storage/usb_storage.c index e500273..589abbc 100644 --- a/kernel/drivers/usb/storage/usb_storage.c +++ b/kernel/drivers/usb/storage/usb_storage.c @@ -25,6 +25,7 @@ #include "buf.h" #include "dma.h" #include "usb.h" +#include "blkdev.h" #define CBW_SIGNATURE 0x43425355 // "USBC" #define CSW_SIGNATURE 0x53425355 // "USBS" @@ -332,6 +333,40 @@ usb_storage_dump(void) brelse(b); } +// The same disk, as the block device registry sees it: whole, as "usb0", and +// with its partitions read and registered behind it. A stick written with +// tools/posix/mkdisk.sh -- or by any installer -- keeps SchFS in a partition +// next to its ESP, and the fixed USBDISK_DEV, which is the whole medium, can +// never see it there. +static int +ums_blk_rw(void *priv, uint64 lba, uint32 count, void *buf, int write) +{ + int n; + (void)priv; + if(count * SECTOR_SIZE > BSIZE || lba + count > ums.nsectors) + return -1; + acquiresleep(&ums.lock); + if(write) + memmove(ums.xfer, buf, count * SECTOR_SIZE); + n = scsi_rw((uint32)lba, count, ums.xfer, write); + if(n == (int)(count * SECTOR_SIZE) && !write) + memmove(buf, ums.xfer, count * SECTOR_SIZE); + releasesleep(&ums.lock); + return n == (int)(count * SECTOR_SIZE) ? 0 : -1; +} + +static int +ums_blk_flush(void *priv) +{ + (void)priv; + return usb_storage_flush(); +} + +static const struct blkdev_ops ums_blk_ops = { + .rw = ums_blk_rw, + .flush = ums_blk_flush, +}; + // --------------------------------------------------------------------------- static int @@ -407,6 +442,7 @@ storage_probe(struct usb_dev *dev) cprintf("usb-storage: %s %s, %d sectors (%d MB)\n", vendor, product, ums.nsectors, (int)((uint64)ums.nsectors * SECTOR_SIZE / (1024 * 1024))); + blkdev_register("usb0", &ums_blk_ops, 0, ums.nsectors, SECTOR_SIZE); return 0; fail: diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index 0a19da2..63416b3 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -14,6 +14,7 @@ #include "file.h" #include "posix.h" #include "errno.h" +#include "blkdev.h" // Verify myproc()->fs_base offset matches the hardcoded 0x688 in trapasm.S. _Static_assert(__builtin_offsetof(struct proc, fs_base) == 0x18, @@ -884,6 +885,8 @@ rootdev_usable(uint dev) return ramdisk_present(); if (dev == USBDISK_DEV) return usb_storage_present(); + if (dev >= BLKDEV_FIRST) + return blkdev_present(dev); // iderw() panics on an absent drive rather than failing, so a probe has to // ask first. That is the whole reason ide_disk_present() exists. return ide_disk_present((int)dev); @@ -892,16 +895,27 @@ rootdev_usable(uint dev) static void rootdev_mount(void) { - static const struct { uint dev; const char *what; } cand[] = { + // The fixed devices, with the registered ones -- SATA, NVMe, and every + // partition on them -- between USB and IDE. A partition GPT types as SchFS + // goes ahead of the rest of the registry, because it was labelled that way + // on purpose; the others are probed too, so a volume in an untyped or MBR + // partition, or on a whole unpartitioned disk, still boots. + struct { uint dev; const char *what; } cand[4 + 2 * NBLKDEV]; + int ncand = 0; #ifdef RAMDISK_ROOT - { RAMDISK_DEV, "ramdisk" }, + cand[ncand].dev = RAMDISK_DEV; cand[ncand++].what = "ramdisk"; #endif - { USBDISK_DEV, "USB disk" }, - { 1, "IDE disk 1" }, - { 0, "IDE disk 0" }, - }; + cand[ncand].dev = USBDISK_DEV; cand[ncand++].what = "USB disk"; + for (int pass = 0; pass < 2; pass++) + for (int i = 0, d; (d = blkdev_nth(i)) >= 0; i++) + if (blkdev_is_schfs(d) == !pass) { + cand[ncand].dev = d; + cand[ncand++].what = blkdev_name(d); + } + cand[ncand].dev = 1; cand[ncand++].what = "IDE disk 1"; + cand[ncand].dev = 0; cand[ncand++].what = "IDE disk 0"; - for (int i = 0; i < (int)NELEM(cand); i++) { + for (int i = 0; i < ncand; i++) { if (!rootdev_usable(cand[i].dev)) continue; schfs_kmount(cand[i].dev); @@ -922,7 +936,7 @@ rootdev_mount(void) // which is how the first version of this message came out as // `%-11s device 1596400`. See print_int() in kernel/core/console.c. cprintf("root: no SchFS volume on any candidate device. Looked at:\n"); - for (int i = 0; i < (int)NELEM(cand); i++) + for (int i = 0; i < ncand; i++) cprintf(" device %d, %s: %s\n", cand[i].dev, cand[i].what, rootdev_usable(cand[i].dev) ? "present, but no SchFS on it" : "not present"); diff --git a/tools/posix/mkdisk.sh b/tools/posix/mkdisk.sh new file mode 100644 index 0000000..cf2259c --- /dev/null +++ b/tools/posix/mkdisk.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Build a partitioned SchOS disk: the image a real machine boots from. +# +# tools/posix/mkdisk.sh +# +# A GUID partition table with two entries: +# +# 1 EFI system partition FAT, holding (the loader and kernel) +# 2 SchFS , byte for byte, typed +# 47138FA9-B79C-47A6-9F7D-AC1F7F572D5E +# +# This is the layout every UEFI machine expects: the firmware finds the ESP by +# its type and runs EFI/BOOT/BOOTX64.EFI from it, and the kernel finds the root +# by scanning the same table -- the SchFS type first, anything else after (see +# rootdev_mount() in kernel/proc/proc.c). Write it to a USB stick or a disk +# with dd and it boots; attach it to QEMU's q35 as its only disk and it boots +# the same way. +# +# The SchFS partition holds the volume unchanged. A filesystem addresses +# blocks from the start of whatever it is on, and the kernel hands a partition +# to it as a device whose block 0 is the partition's first sector -- so the +# image mkschfs writes for a whole disk is already the right content for a +# partition, and so is anything a Linux SchFS driver is given as /dev/sdX2. +# +# Needs sfdisk (util-linux) and mtools. Nothing here needs root or loop +# devices: the FAT is built in a file and copied in at its offset. + +set -eu + +esp=$1 +vol=$2 +out=$3 + +SCHFS_TYPE=47138FA9-B79C-47A6-9F7D-AC1F7F572D5E +ESP_TYPE=C12A7328-F81F-11D2-BA4B-00A0C93EC93B + +for t in sfdisk mformat mcopy; do + command -v $t >/dev/null || { echo "mkdisk: needs $t" >&2; exit 1; } +done + +# Sizes in 512-byte sectors. The ESP is 64 MB, which leaves room for the +# kernel to grow by an order of magnitude; partitions start on 1 MB +# boundaries, which is what every partitioning tool does and what flash +# media erase in. +esp_mb=64 +esp_start=2048 +esp_sectors=$((esp_mb * 2048)) +vol_bytes=$(wc -c < "$vol") +vol_start=$((esp_start + esp_sectors)) +vol_sectors=$(( (vol_bytes + 511) / 512 )) +# 1 MB at the end for the backup GPT, rounded the same way. +total=$((vol_start + vol_sectors + 2048)) + +rm -f "$out" "$out.esp" +dd if=/dev/zero of="$out" bs=512 count=0 seek=$total 2>/dev/null + +sfdisk --quiet --label gpt "$out" </dev/null +mformat -i "$out.esp" -F -v SCHOS :: +mcopy -i "$out.esp" -s "$esp"/* :: +dd if="$out.esp" of="$out" bs=512 seek=$esp_start conv=notrunc 2>/dev/null +rm -f "$out.esp" + +dd if="$vol" of="$out" bs=512 seek=$vol_start conv=notrunc 2>/dev/null + +echo "mkdisk: $out -- ESP ${esp_mb} MB, SchFS $((vol_bytes >> 20)) MB"