storage: AHCI, GPT/MBR partitions, and a disk a PC can boot
q35 stopped with "no root filesystem" because its disk is SATA, like every PC since 2008, and SchOS only had IDE. And SchFS could only sit on a whole raw device, so nothing could boot from a disk that also carried an ESP. - include/blkdev.h, kernel/dev/blkdev.c: a registry of block devices found at probe time, numbered from 4. A disk's GPT (or MBR) is read when it registers, and each partition becomes a device whose block 0 is the partition's first sector. SchFS needs no format change to live in a partition; a Linux SchFS driver will see the same bytes on /dev/sdX2. SchFS has its own GPT type: 47138FA9-B79C-47A6-9F7D-AC1F7F572D5E. - AHCI driver, written from the AHCI 1.3.1 spec: BIOS handoff, COMRESET when the link is down, IDENTIFY, 48-bit DMA read/write, FLUSH. Completion is polled, so the root disk does not depend on interrupt routing. - USB sticks also register as usb0, so their partitions are found. - The root probe tries registered devices between USB and IDE, SchFS-typed partitions first. - make disk / tools/posix/mkdisk.sh: a GPT image with a 64 MB ESP and a SchFS partition, ready to dd to a stick or a disk. Tested in QEMU q35: the old two-disk setup roots on sata1; disk.img as the only SATA disk roots on sata0p2; disk.img as a USB stick (firmware boots from it) roots on usb0p2. Writes survive sync in each case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
This commit is contained in:
parent
7e8d5e2821
commit
d7352c0db0
10 changed files with 1027 additions and 17 deletions
12
Makefile
12
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
49
include/blkdev.h
Normal file
49
include/blkdev.h
Normal file
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
334
kernel/dev/blkdev.c
Normal file
334
kernel/dev/blkdev.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
468
kernel/drivers/storage/ahci/ahci.c
Normal file
468
kernel/drivers/storage/ahci/ahci.c
Normal file
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
73
tools/posix/mkdisk.sh
Normal file
73
tools/posix/mkdisk.sh
Normal file
|
|
@ -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 <esp-dir> <schfs.img> <output.img>
|
||||
#
|
||||
# A GUID partition table with two entries:
|
||||
#
|
||||
# 1 EFI system partition FAT, holding <esp-dir> (the loader and kernel)
|
||||
# 2 SchFS <schfs.img>, 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" <<EOF
|
||||
start=$esp_start, size=$esp_sectors, type=$ESP_TYPE, name="EFI system"
|
||||
start=$vol_start, size=$vol_sectors, type=$SCHFS_TYPE, name="SchOS"
|
||||
EOF
|
||||
|
||||
# The ESP, built apart and copied in.
|
||||
dd if=/dev/zero of="$out.esp" bs=1M count=$esp_mb 2>/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"
|
||||
Loading…
Reference in a new issue