Compare commits
129
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67702fa250 | ||
|
|
8a38540312 | ||
|
|
54635eecf5 | ||
|
|
184d90c2c6 | ||
|
|
a32eed877d | ||
|
|
347a041d85 | ||
|
|
53e42837e0 | ||
|
|
f52c591f5e | ||
|
|
77d2e22ed1 | ||
|
|
a64a01a6a9 | ||
|
|
35e8921de8 | ||
|
|
3fb9d5936a | ||
|
|
452080e997 | ||
|
|
5b63a841ba | ||
|
|
4b9507bd59 | ||
|
|
7dec1b0767 | ||
|
|
45b8fd8614 | ||
|
|
dd22bfbc48 | ||
|
|
6e60daed6a | ||
|
|
446f655c69 | ||
|
|
a785efa4a3 | ||
|
|
767a2a9a7c | ||
|
|
dd044fb115 | ||
|
|
3ec14509a0 | ||
|
|
1f2c60b3ec | ||
|
|
d71a5f25d3 | ||
|
|
2a0f17ae86 | ||
|
|
9ef61a0844 | ||
|
|
688b9101e8 | ||
|
|
1d7ba814dc | ||
|
|
8aba86b4ce | ||
|
|
a0c83f4b3f | ||
|
|
1ea48ed5d6 | ||
|
|
dfc7d6a609 | ||
|
|
77a3ccd33d | ||
|
|
07da27dc39 | ||
|
|
d89657d0a4 | ||
|
|
849b4b62d4 | ||
|
|
8589bf713b | ||
|
|
738f6aa697 | ||
|
|
01e56e3f36 | ||
|
|
d5d15cefcb | ||
|
|
fd96a35eb9 | ||
|
|
e3fe3f3f45 | ||
|
|
60da667b42 | ||
|
|
36145e623b | ||
|
|
565415327d | ||
|
|
bf6bdb389d | ||
|
|
0628944b15 | ||
|
|
e6d0bb7ef0 | ||
|
|
5ca804d827 | ||
|
|
a299363b59 | ||
|
|
d8dd62c639 | ||
|
|
d106b6e8dc | ||
|
|
af2c766f42 | ||
|
|
d26262bf56 | ||
|
|
10b89c06ff | ||
|
|
a2a05d0b3d | ||
|
|
75d62660b0 | ||
|
|
a53c2b0193 | ||
|
|
bf481c080c | ||
|
|
3a78dcab3f | ||
|
|
470f93a83d | ||
|
|
7798706b41 | ||
|
|
ad40de03c2 | ||
|
|
d8778b4b70 | ||
|
|
79d859a111 | ||
|
|
37fb09f75e | ||
|
|
34ebeb968d | ||
|
|
3cc1d38dd0 | ||
|
|
36e804b848 | ||
|
|
be83a42d42 | ||
|
|
650a1b1595 | ||
|
|
d8c55c6f2f | ||
|
|
2ebfb0c3b0 | ||
|
|
888eaa74e1 | ||
|
|
ed76cbbc79 | ||
|
|
140229b88d | ||
|
|
cb2379fd06 | ||
|
|
1665b239b0 | ||
|
|
70ed0337f8 | ||
|
|
116b8f6c41 | ||
|
|
77901bbba6 | ||
|
|
78582d24d2 | ||
|
|
1cdffe21b1 | ||
|
|
4df90bc212 | ||
|
|
713e77354b | ||
|
|
abb7b1b634 | ||
|
|
f5f0e15769 | ||
|
|
8652b4a724 | ||
|
|
e8233127c7 | ||
|
|
88e92254e9 | ||
|
|
5725d35e5b | ||
|
|
8c95525793 | ||
|
|
5bba5d3363 | ||
|
|
80b72db676 | ||
|
|
aa0c97353a | ||
|
|
dd93204b44 | ||
|
|
c7b17aaa0e | ||
|
|
d7a154a596 | ||
|
|
1bf91115dd | ||
|
|
65244e3103 | ||
|
|
75ccfff171 | ||
|
|
2a583d55a8 | ||
|
|
d218d93f79 | ||
|
|
a5fe63c1dd | ||
|
|
6b3ae0c997 | ||
|
|
59104dd988 | ||
|
|
e499f500c3 | ||
|
|
2ab0d129a2 | ||
|
|
d702d2e9ae | ||
|
|
3ec2d1828a | ||
|
|
21657943a8 | ||
|
|
e612d948d2 | ||
|
|
4ef21fa083 | ||
|
|
125a3b4993 | ||
|
|
e7c7e7b94c | ||
|
|
a581712b09 | ||
|
|
8eb4210251 | ||
|
|
56110b0019 | ||
|
|
afbf10f7fc | ||
|
|
b61b7775b9 | ||
|
|
be81394be3 | ||
|
|
47610e8ee2 | ||
|
|
193fd71a50 | ||
|
|
1c2b3ae64d | ||
|
|
d19a0ae38d | ||
|
|
3d1de37d0e | ||
|
|
ceacc6b514 |
@@ -0,0 +1,16 @@
|
||||
# EditorConfig: https://editorconfig.org/
|
||||
# Follows the Zig style guide: https://ziglang.org/documentation/0.16.0/#Style-Guide
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.zig]
|
||||
# "Line length: aim for 100; use common sense."
|
||||
max_line_length = 100
|
||||
@@ -0,0 +1 @@
|
||||
*.zig text eol=lf
|
||||
@@ -4,3 +4,6 @@ zig-out/
|
||||
|
||||
# JetBrains IDE
|
||||
.idea/
|
||||
|
||||
.claude/
|
||||
.github/
|
||||
@@ -2,12 +2,31 @@
|
||||
Codename: Shodan
|
||||
Version: 1
|
||||
|
||||
A small operating system, written from scratch in Zig — a bootloader (`boot/`)
|
||||
and a microkernel (`system/kernel/`), sharing a neutral handoff contract (`system/danos.zig`).
|
||||
It boots x86-64 via UEFI, and so far has a framebuffer console, a physical frame
|
||||
allocator, its own paging with W^X permissions, interrupt/exception handling, a
|
||||
LAPIC timer, a kernel heap, a fixed-priority preemptive scheduler, and in-kernel IPC
|
||||
channels. See [`docs/`](docs/README.md) for how each piece works.
|
||||
A small resilient operating system, written from scratch in Zig.
|
||||
|
||||
## Zen of DanOS:
|
||||
|
||||
- Resilient Micro-Kernel Architecture.
|
||||
- Every process run in an isolated user space not kernel space.
|
||||
- Processes cannot take down the entire OS with it when they die or is killed
|
||||
- Stable public runtime library, private OS ABI.
|
||||
- Keeps a stable runtime for user space processes between OS versions (great for backwards compatibility)
|
||||
- Allows the underlying OS to be changed without effecting applications
|
||||
- Provides a boundary to enable compatibility between OS's e.g. POSIX, MUSL etc
|
||||
- Drivers are just isolated processes in user space.
|
||||
- Thin binaries that can be restarted like applications.
|
||||
- Useful during driver development.
|
||||
- Drivers can claim MMIO / ports
|
||||
- Driver resources (e.g. IRQ/Port/MMIO) claims are automatically cleaned up if the driver dies or is killed
|
||||
- Drivers can also hook into the process lifecyle to clean up or reset hardware
|
||||
- No legacy to deal with
|
||||
- Zig code uses a clean coding style (Zen of Zig)
|
||||
- Favor reading code over writing code.
|
||||
- No magic numbers.
|
||||
- No shortend names unless its for ABI compatibility or acronyms
|
||||
- Inter-Process Communication (IPC)
|
||||
- Publish and subscribe to Asynchronous Messages
|
||||
- Talk to services and processes synchronously
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -30,8 +49,10 @@ channels. See [`docs/`](docs/README.md) for how each piece works.
|
||||
zig build
|
||||
```
|
||||
|
||||
Produces the UEFI bootloader (`zig-out/bin/BOOTX64.efi`) and the kernel ELF
|
||||
(`zig-out/bin/kernel`).
|
||||
Produces a FHS-shaped `zig-out/` that *is* the danos filesystem and the boot volume:
|
||||
the UEFI bootloader at `zig-out/EFI/BOOT/BOOTX64.efi`, the kernel at
|
||||
`zig-out/system/kernel`, init at `zig-out/system/services/init`, drivers under
|
||||
`zig-out/system/drivers/`, and the initial-ramdisk at `zig-out/boot/`.
|
||||
|
||||
## Run
|
||||
|
||||
@@ -58,9 +79,13 @@ straight into CI.
|
||||
|
||||
## Documentation
|
||||
|
||||
Design notes explaining the *why* behind the code live in
|
||||
Design notes explaining *why* behind the code live in
|
||||
[`docs/`](docs/README.md) — start with [`docs/README.md`](docs/README.md).
|
||||
|
||||
For the hardware needed to run DanOS — minimum specs plus a plain-language guide
|
||||
matching Intel/AMD CPU generations by name — see
|
||||
[`docs/system-requirements.md`](docs/system-requirements.md).
|
||||
|
||||
## Logo
|
||||
|
||||
San Serif Text "Dan OS" with a black karate belt around it.
|
||||
|
||||
+41
-39
@@ -1,22 +1,24 @@
|
||||
const std = @import("std");
|
||||
const uefi = std.os.uefi;
|
||||
const elf = std.elf;
|
||||
const danos = @import("danos");
|
||||
const BootInformation = danos.BootInformation;
|
||||
const boot_handoff = @import("boot-handoff");
|
||||
const BootInformation = boot_handoff.BootInformation;
|
||||
const GraphicsOutput = uefi.protocol.GraphicsOutput;
|
||||
const EdidActive = uefi.protocol.edid.Active;
|
||||
const MemoryMapSlice = uefi.tables.MemoryMapSlice;
|
||||
|
||||
/// Name of the kernel ELF on the boot volume (installed to the ESP root by
|
||||
/// build.zig). UEFI wants a UTF-16, null-terminated path.
|
||||
const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("kernel");
|
||||
// The boot volume is the FHS-shaped zig-out (see build.zig / docs/README.md), so the
|
||||
// loader reads each artifact from its addressed FHS path. UEFI paths use backslashes;
|
||||
// the FAT driver walks the components itself, so no per-directory dance is needed.
|
||||
|
||||
/// Path of the init program on the boot volume (UEFI paths use backslashes;
|
||||
/// the FAT driver walks the components itself, so no directory dance needed).
|
||||
const init_file_name = std.unicode.utf8ToUtf16LeStringLiteral("sbin\\init");
|
||||
/// The kernel image: /system/kernel.
|
||||
const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\kernel");
|
||||
|
||||
/// Path of the initrd image on the boot volume (the VFS server + drivers).
|
||||
const initrd_file_name = std.unicode.utf8ToUtf16LeStringLiteral("initrd.img");
|
||||
/// The init program: /system/services/init.
|
||||
const init_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\services\\init");
|
||||
|
||||
/// The initial-ramdisk (the VFS server + drivers), in /boot.
|
||||
const initial_ramdisk_file_name = std.unicode.utf8ToUtf16LeStringLiteral("boot\\initial-ramdisk.img");
|
||||
|
||||
/// Physical page size, and the sentinel UEFI uses to seek to end-of-file.
|
||||
const page_size = 4096;
|
||||
@@ -27,7 +29,7 @@ pub fn main() uefi.Status {
|
||||
// report the reason (boot services are still up) and park the machine so the
|
||||
// message stays on screen.
|
||||
boot() catch |err| {
|
||||
log("\r\ndanos: boot failed: ");
|
||||
log("\r\nEFI: boot failed: ");
|
||||
logBytes(@errorName(err));
|
||||
log("\r\n");
|
||||
while (true) asm volatile ("hlt");
|
||||
@@ -43,7 +45,7 @@ fn boot() !noreturn {
|
||||
var boot_information: BootInformation = .{
|
||||
// A missing GOP (a headless machine) is not fatal — hand the kernel a
|
||||
// "no framebuffer" descriptor (base 0) and let it log to serial instead.
|
||||
.framebuffer = queryFramebuffer(bs) catch danos.Framebuffer{
|
||||
.framebuffer = queryFramebuffer(bs) catch boot_handoff.Framebuffer{
|
||||
.base = 0,
|
||||
.width = 0,
|
||||
.height = 0,
|
||||
@@ -61,16 +63,16 @@ fn boot() !noreturn {
|
||||
|
||||
const entry = try loadKernel(bs, &boot_information);
|
||||
|
||||
// Best effort: a volume without sbin/init still boots (kernel-only).
|
||||
// Best effort: a volume without /system/services/init still boots (kernel-only).
|
||||
loadInit(bs, &boot_information) catch |err| {
|
||||
log("danos: no sbin/init (");
|
||||
log("EFI: no /system/services/init (");
|
||||
logBytes(@errorName(err));
|
||||
log(") - booting without user space\r\n");
|
||||
};
|
||||
|
||||
// Best effort: the initrd (VFS server + drivers) is optional too.
|
||||
loadInitrd(bs, &boot_information) catch |err| {
|
||||
log("danos: no initrd (");
|
||||
// Best effort: the initial_ramdisk (VFS server + drivers) is optional too.
|
||||
loadInitialRamdisk(bs, &boot_information) catch |err| {
|
||||
log("EFI: no initial_ramdisk (");
|
||||
logBytes(@errorName(err));
|
||||
log(")\r\n");
|
||||
};
|
||||
@@ -82,7 +84,7 @@ fn boot() !noreturn {
|
||||
// the map and exiting would invalidate the map key.
|
||||
const cr3 = try buildBootstrapTables(bs, &boot_information);
|
||||
|
||||
log("danos: kernel loaded, exiting boot services\r\n");
|
||||
log("EFI: kernel loaded, exiting boot services\r\n");
|
||||
boot_information.memory_map = try exitBootServices(bs);
|
||||
|
||||
// Switch onto our tables and jump to the kernel in one uninterruptible step.
|
||||
@@ -98,7 +100,7 @@ const Resolution = struct { width: u32, height: u32 };
|
||||
|
||||
/// Switch the GPU to the monitor's native resolution (when we can determine it)
|
||||
/// and read the resulting graphics mode into our own framebuffer description.
|
||||
fn queryFramebuffer(bs: *uefi.tables.BootServices) !danos.Framebuffer {
|
||||
fn queryFramebuffer(bs: *uefi.tables.BootServices) !boot_handoff.Framebuffer {
|
||||
// Enumerate the handles carrying the Graphics Output Protocol. We go through
|
||||
// handles (rather than locateProtocol) so we can also ask them for their EDID,
|
||||
// which is what tells us the panel's native resolution.
|
||||
@@ -130,7 +132,7 @@ fn queryFramebuffer(bs: *uefi.tables.BootServices) !danos.Framebuffer {
|
||||
|
||||
/// Map a GOP pixel format to ours. bit_mask / blt_only have no linear 32bpp
|
||||
/// layout we can paint into, so they're rejected.
|
||||
fn pixelFormat(fmt: GraphicsOutput.PixelFormat) !danos.PixelFormat {
|
||||
fn pixelFormat(fmt: GraphicsOutput.PixelFormat) !boot_handoff.PixelFormat {
|
||||
return switch (fmt) {
|
||||
.red_green_blue_reserved_8_bit_per_color => .rgbx,
|
||||
.blue_green_red_reserved_8_bit_per_color => .bgrx,
|
||||
@@ -233,7 +235,7 @@ fn loadKernel(bs: *uefi.tables.BootServices, boot_information: *BootInformation)
|
||||
// the first set of real page tables and switches CR3 before jumping in. They
|
||||
// carry: an identity map of low RAM (so the loader's own code/stack executing
|
||||
// the switch stays valid, and the low-linked kernel keeps working during the
|
||||
// staged move), a physmap at danos.physmap_base (the kernel's permanent way to
|
||||
// staged move), a physmap at boot_handoff.physmap_base (the kernel's permanent way to
|
||||
// reach physical memory), and 4 KiB mappings of any higher-half kernel segment.
|
||||
// The kernel later builds its own precise tables (paging.init) and abandons
|
||||
// these; they leak as reserved LoaderData (~a handful of frames).
|
||||
@@ -306,7 +308,7 @@ fn buildBootstrapTables(bs: *uefi.tables.BootServices, boot_information: *const
|
||||
var address: u64 = 0;
|
||||
while (address < 4 * gib) : (address += 2 << 20) {
|
||||
try pool.map2M(pml4, address, address); // identity
|
||||
try pool.map2M(pml4, danos.physicalToVirtual(address), address); // physmap
|
||||
try pool.map2M(pml4, boot_handoff.physicalToVirtual(address), address); // physmap
|
||||
}
|
||||
|
||||
// A framebuffer above the 4 GiB window needs its own identity + physmap
|
||||
@@ -317,7 +319,7 @@ fn buildBootstrapTables(bs: *uefi.tables.BootServices, boot_information: *const
|
||||
const fb_end = fb.base + @as(u64, fb.pitch) * fb.height;
|
||||
while (p < fb_end) : (p += 2 << 20) {
|
||||
try pool.map2M(pml4, p, p);
|
||||
try pool.map2M(pml4, danos.physicalToVirtual(p), p);
|
||||
try pool.map2M(pml4, boot_handoff.physicalToVirtual(p), p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,7 +328,7 @@ fn buildBootstrapTables(bs: *uefi.tables.BootServices, boot_information: *const
|
||||
// (and 4 KiB-mapping them would collide with the 2 MiB identity leaves), so
|
||||
// only map segments that actually live in the higher half.
|
||||
for (boot_information.kernel_segments[0..boot_information.kernel_segment_count]) |seg| {
|
||||
if (seg.virtual < danos.kernel_virt_base) continue;
|
||||
if (seg.virtual < boot_handoff.kernel_virt_base) continue;
|
||||
var off: u64 = 0;
|
||||
while (off < seg.pages * page_size) : (off += page_size) {
|
||||
try pool.map4K(pml4, seg.virtual + off, seg.physical + off);
|
||||
@@ -387,21 +389,21 @@ fn loadFile(bs: *uefi.tables.BootServices, name: [*:0]const u16) ![]u8 {
|
||||
return image[0..size];
|
||||
}
|
||||
|
||||
/// Ferry the init program (sbin/init) to the kernel. The kernel does the ELF
|
||||
/// Ferry the init program (/system/services/init) to the kernel. The kernel does the ELF
|
||||
/// loading itself (into ring-3 mappings) — the loader just carries the bytes.
|
||||
fn loadInit(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||
const image = try loadFile(bs, init_file_name);
|
||||
boot_information.init_base = @intFromPtr(image.ptr);
|
||||
boot_information.init_len = image.len;
|
||||
log("danos: sbin/init loaded\r\n");
|
||||
log("EFI: /system/services/init loaded\r\n");
|
||||
}
|
||||
|
||||
/// Ferry the initrd (the VFS server + drivers) to the kernel, same as init.
|
||||
fn loadInitrd(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||
const image = try loadFile(bs, initrd_file_name);
|
||||
boot_information.initrd_base = @intFromPtr(image.ptr);
|
||||
boot_information.initrd_len = image.len;
|
||||
log("danos: initrd loaded\r\n");
|
||||
/// Ferry the initial_ramdisk (the VFS server + drivers) to the kernel, same as init.
|
||||
fn loadInitialRamdisk(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||
const image = try loadFile(bs, initial_ramdisk_file_name);
|
||||
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
||||
boot_information.initial_ramdisk_len = image.len;
|
||||
log("EFI: initial_ramdisk loaded\r\n");
|
||||
}
|
||||
|
||||
/// Validate the ELF, copy every PT_LOAD segment to its physical address, and
|
||||
@@ -461,14 +463,14 @@ fn loadElf(bs: *uefi.tables.BootServices, image: []u8, boot_information: *BootIn
|
||||
/// neutral form. Allocating the buffers can itself change the map (invalidating
|
||||
/// the key), so retry until it takes. Both buffers are LoaderData, which survives
|
||||
/// the exit, so the returned map stays valid for the kernel.
|
||||
fn exitBootServices(bs: *uefi.tables.BootServices) !danos.MemoryMap {
|
||||
fn exitBootServices(bs: *uefi.tables.BootServices) !boot_handoff.MemoryMap {
|
||||
var attempts: usize = 0;
|
||||
while (attempts < 8) : (attempts += 1) {
|
||||
const info = try bs.getMemoryMapInfo();
|
||||
// Spare descriptors to absorb the growth from the allocations below.
|
||||
const cap = info.len + 8;
|
||||
const map_buffer = try bs.allocatePool(.loader_data, cap * info.descriptor_size);
|
||||
const regions_buffer = try bs.allocatePool(.loader_data, cap * @sizeOf(danos.MemoryRegion));
|
||||
const regions_buffer = try bs.allocatePool(.loader_data, cap * @sizeOf(boot_handoff.MemoryRegion));
|
||||
const map = bs.getMemoryMap(map_buffer) catch {
|
||||
_ = bs.freePool(map_buffer.ptr) catch {};
|
||||
_ = bs.freePool(regions_buffer.ptr) catch {};
|
||||
@@ -490,8 +492,8 @@ fn exitBootServices(bs: *uefi.tables.BootServices) !danos.MemoryMap {
|
||||
/// into `out` (sized for at least `map.info.len` regions). Adjacent regions of
|
||||
/// the same kind are coalesced. This is the loader's job precisely so the kernel
|
||||
/// never sees UEFI's vocabulary — the same seam the framebuffer already uses.
|
||||
fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
|
||||
const regions: [*]danos.MemoryRegion = @ptrCast(@alignCast(out.ptr));
|
||||
fn convertMemoryMap(map: MemoryMapSlice, out: []u8) boot_handoff.MemoryMap {
|
||||
const regions: [*]boot_handoff.MemoryRegion = @ptrCast(@alignCast(out.ptr));
|
||||
// We're about to call boot-services memory `usable`, but our own stack lives
|
||||
// in it and the kernel starts out running on it. Keep the region holding the
|
||||
// current stack pointer reserved so it's never handed out.
|
||||
@@ -508,14 +510,14 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
|
||||
if (d.number_of_pages == 0) continue;
|
||||
var kind = classify(d);
|
||||
// The descriptor we're executing on stays reserved (see rsp above).
|
||||
const region_end = d.physical_start + d.number_of_pages * danos.page_size;
|
||||
const region_end = d.physical_start + d.number_of_pages * page_size;
|
||||
if (kind == .usable and rsp >= d.physical_start and rsp < region_end) kind = .reserved;
|
||||
|
||||
// Coalesce with the previous region if it's the same kind and contiguous.
|
||||
if (count > 0) {
|
||||
const previous = ®ions[count - 1];
|
||||
if (previous.kind == kind and
|
||||
previous.base + previous.pages * danos.page_size == d.physical_start)
|
||||
previous.base + previous.pages * page_size == d.physical_start)
|
||||
{
|
||||
previous.pages += d.number_of_pages;
|
||||
continue;
|
||||
@@ -542,7 +544,7 @@ fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
|
||||
/// ever the firmware's (the one live piece, our stack, is reserved by the caller).
|
||||
/// Anything unrecognised is `reserved` — the safe default; our own LoaderData (the
|
||||
/// kernel image and these buffers) lands there and stays reserved.
|
||||
fn classify(d: *const uefi.tables.MemoryDescriptor) danos.MemoryKind {
|
||||
fn classify(d: *const uefi.tables.MemoryDescriptor) boot_handoff.MemoryKind {
|
||||
if (!d.attribute.wb) return .mmio;
|
||||
return switch (d.type) {
|
||||
.conventional_memory, .boot_services_code, .boot_services_data => .usable,
|
||||
|
||||
@@ -58,6 +58,9 @@ fn addUserBinary(
|
||||
b: *std.Build,
|
||||
target: std.Build.ResolvedTarget,
|
||||
runtime_module: *std.Build.Module,
|
||||
mmio_module: *std.Build.Module,
|
||||
xkeyboard_config_module: *std.Build.Module,
|
||||
acpi_ids_module: *std.Build.Module,
|
||||
name: []const u8,
|
||||
root: []const u8,
|
||||
) *std.Build.Step.Compile {
|
||||
@@ -74,6 +77,14 @@ fn addUserBinary(
|
||||
.stack_protector = false,
|
||||
.imports = &.{
|
||||
.{ .name = "runtime", .module = runtime_module },
|
||||
// Typed volatile MMIO + memory barriers, for drivers. See library/mmio/.
|
||||
.{ .name = "mmio", .module = mmio_module },
|
||||
// Keyboard layouts (keycode + modifiers -> keysym/character), available
|
||||
// to any program that wants it. See library/xkeyboard-config/.
|
||||
.{ .name = "xkeyboard-config", .module = xkeyboard_config_module },
|
||||
// ACPI/PnP hardware-ID registry, so drivers name devices
|
||||
// (HardwareId.ps2_keyboard) instead of magic "_HID" strings.
|
||||
.{ .name = "acpi-ids", .module = acpi_ids_module },
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -91,11 +102,62 @@ pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// Shared handoff definitions (BootInformation, Framebuffer, ...). No target is set,
|
||||
// so the module inherits the target of whichever binary imports it — the
|
||||
// freestanding kernel or the UEFI bootloader.
|
||||
const danos_module = b.addModule("danos", .{
|
||||
.root_source_file = b.path("system/danos.zig"),
|
||||
// The three shared contracts, each with its own audience so every import
|
||||
// declares which one it speaks (no target is set, so each inherits the target of
|
||||
// whichever binary imports it). See docs/coding-standards.md.
|
||||
// boot-handoff : loader <-> kernel (BootInformation, framebuffer, VM layout)
|
||||
// abi : kernel <-> runtime, core (SystemCall, mmap prot flags, page_size)
|
||||
// device-abi : kernel <-> user, devices (DeviceDescriptor, DeviceClass, ...)
|
||||
const boot_handoff_module = b.addModule("boot-handoff", .{
|
||||
.root_source_file = b.path("system/boot-handoff.zig"),
|
||||
});
|
||||
const abi_module = b.addModule("abi", .{
|
||||
.root_source_file = b.path("system/abi.zig"),
|
||||
});
|
||||
// The devices sub-project's public interface (the flat wire types), exposed as
|
||||
// its own module like vfs-protocol — importable by user space, unlike the
|
||||
// kernel-internal device model it also feeds (system/devices/device-model.zig).
|
||||
const device_abi_module = b.addModule("device-abi", .{
|
||||
.root_source_file = b.path("system/devices/device-abi.zig"),
|
||||
});
|
||||
// PCI class-code decoding (class/subclass/prog-IF -> names). Pure reference data,
|
||||
// shared by kernel discovery (the device-tree dump) and any user-space PCI tool.
|
||||
const pci_class_module = b.addModule("pci-class", .{
|
||||
.root_source_file = b.path("system/devices/pci-class.zig"),
|
||||
});
|
||||
// ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device
|
||||
// nodes. Also shared reference data.
|
||||
// The AML interpreter, a build module so the ring-3 acpi service can run the
|
||||
// same parser the kernel does (docs/discovery.md — the shared AML module).
|
||||
// Pure Zig, no kernel imports — one source, two builds.
|
||||
const aml_module = b.addModule("aml", .{
|
||||
.root_source_file = b.path("system/devices/aml/aml.zig"),
|
||||
});
|
||||
|
||||
const acpi_ids_module = b.addModule("acpi-ids", .{
|
||||
.root_source_file = b.path("system/devices/acpi-ids.zig"),
|
||||
});
|
||||
|
||||
// The USB device-framework wire ABI (chapter-9 set-up packets, standard +
|
||||
// class requests, descriptors) and the USB class-code taxonomy — the flat
|
||||
// reference the xHCI bus driver, the USB class drivers, and the device
|
||||
// manager's identity matcher all share. Pure data, like pci-class/acpi-ids.
|
||||
const usb_abi_module = b.addModule("usb-abi", .{
|
||||
.root_source_file = b.path("system/devices/usb-abi.zig"),
|
||||
});
|
||||
const usb_ids_module = b.addModule("usb-ids", .{
|
||||
.root_source_file = b.path("system/devices/usb-ids.zig"),
|
||||
});
|
||||
// The USB transfer protocol: what a USB class driver says to the xHCI bus
|
||||
// driver to drive its device (open / control / interrupt / bulk). A protocol
|
||||
// module like vfs-protocol, shared by the bus driver and every class driver.
|
||||
const usb_transfer_protocol_module = b.addModule("usb-transfer-protocol", .{
|
||||
.root_source_file = b.path("system/drivers/usb-xhci-bus/usb-transfer-protocol.zig"),
|
||||
});
|
||||
// The block-device protocol: read/write of fixed-size blocks, spoken between a
|
||||
// filesystem and a block driver (usb-storage). A protocol module like the rest.
|
||||
const block_protocol_module = b.addModule("block-protocol", .{
|
||||
.root_source_file = b.path("system/services/block/protocol.zig"),
|
||||
});
|
||||
|
||||
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
|
||||
@@ -111,7 +173,8 @@ pub fn build(b: *std.Build) void {
|
||||
const architecture_module = b.addModule("architecture", .{
|
||||
.root_source_file = b.path("system/kernel/architecture/x86_64/cpu.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "danos", .module = danos_module }, // paging uses the shared BootInformation/memory-map types
|
||||
.{ .name = "boot-handoff", .module = boot_handoff_module }, // paging uses BootInformation/memory-map + physicalToVirtual
|
||||
.{ .name = "abi", .module = abi_module }, // paging works in page_size units
|
||||
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus, ist_stack_size, timer_hz
|
||||
},
|
||||
});
|
||||
@@ -130,7 +193,11 @@ pub fn build(b: *std.Build) void {
|
||||
const platform_module = b.addModule("platform", .{
|
||||
.root_source_file = b.path("system/devices/platform.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "danos", .module = danos_module }, // BootInformation (carries the ACPI RSDP)
|
||||
.{ .name = "boot-handoff", .module = boot_handoff_module }, // BootInformation (carries the ACPI RSDP), physicalToVirtual
|
||||
.{ .name = "abi", .module = abi_module }, // acpi.zig works in page_size units
|
||||
.{ .name = "device-abi", .module = device_abi_module }, // device-model's DeviceClass/ResourceKind live here
|
||||
.{ .name = "pci-class", .module = pci_class_module }, // decode PCI class codes in the device dump
|
||||
.{ .name = "acpi-ids", .module = acpi_ids_module }, // decode ACPI _HID names in the device dump
|
||||
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus (the discovery pool)
|
||||
},
|
||||
});
|
||||
@@ -144,23 +211,75 @@ pub fn build(b: *std.Build) void {
|
||||
.root_source_file = b.path("system/services/vfs/protocol.zig"),
|
||||
});
|
||||
|
||||
// The user-space runtime library (a nascent libc): system_call wrappers, the
|
||||
// C-convention heap, IPC helpers, the process start shim. Compiled into every
|
||||
// user binary (see addUserBinary), so it inherits each exe's `.large` code
|
||||
// model — do NOT set a target/code_model here. It imports `danos` for the
|
||||
// shared SystemCall numbers and `vfs-protocol` for the file API.
|
||||
// The input wire protocol: the input service's public interface, exposed as its own
|
||||
// module the same way vfs-protocol is. Shared by the input service, the runtime's
|
||||
// `input` helper (subscribe/publish), and every source and subscriber.
|
||||
const input_protocol_module = b.addModule("input-protocol", .{
|
||||
.root_source_file = b.path("system/services/input/protocol.zig"),
|
||||
});
|
||||
|
||||
// The danos-native user-space runtime: system_call wrappers, the C-convention
|
||||
// heap, IPC helpers, the process start shim, device access. This is the stable
|
||||
// application ABI; POSIX compatibility is a separate library on top (see below).
|
||||
// Compiled into every user binary (see addUserBinary), so it inherits each exe's
|
||||
// `.large` code model — do NOT set a target/code_model here. It imports `abi`
|
||||
// for the shared SystemCall numbers / mmap flags, `device-abi` for the device
|
||||
// types its `device` helper wraps, and re-exports `vfs-protocol` for the VFS
|
||||
// server. It never touches `boot-handoff` — user space has no business with the
|
||||
// loader↔kernel handoff.
|
||||
const runtime_module = b.addModule("runtime", .{
|
||||
.root_source_file = b.path("library/runtime/runtime.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "danos", .module = danos_module },
|
||||
.{ .name = "abi", .module = abi_module },
|
||||
.{ .name = "device-abi", .module = device_abi_module },
|
||||
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
||||
.{ .name = "input-protocol", .module = input_protocol_module },
|
||||
},
|
||||
});
|
||||
|
||||
// The initrd container format, shared by the kernel (unpacks it) and the
|
||||
// build-time packer tools/mkinitrd.zig (produces it). No dependencies.
|
||||
const initrd_module = b.addModule("initrd", .{
|
||||
.root_source_file = b.path("system/initrd.zig"),
|
||||
// The device-manager protocol: hello + (M18.2) tree reports, exposed as its
|
||||
// own module like the other protocol modules. Imported through the runtime.
|
||||
const device_manager_protocol_module = b.addModule("device-manager-protocol", .{
|
||||
.root_source_file = b.path("system/services/device-manager/device-manager-protocol.zig"),
|
||||
});
|
||||
runtime_module.addImport("device-manager-protocol", device_manager_protocol_module);
|
||||
// The USB transfer protocol, so runtime.usb (the class-driver client) can speak
|
||||
// it, the way runtime.input speaks the input protocol.
|
||||
runtime_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module);
|
||||
// The block protocol, so runtime.block (the block-device client) can speak it.
|
||||
runtime_module.addImport("block-protocol", block_protocol_module);
|
||||
|
||||
// The power protocol: system power's domain-named surface (docs/power.md).
|
||||
const power_protocol_module = b.addModule("power-protocol", .{
|
||||
.root_source_file = b.path("system/services/power/protocol.zig"),
|
||||
});
|
||||
runtime_module.addImport("power-protocol", power_protocol_module);
|
||||
|
||||
// Typed volatile MMIO register access + memory-ordering barriers, for drivers on
|
||||
// top of an mmio_map grant. Depends only on `builtin` (arch-conditional barriers);
|
||||
// no target set, so it inherits each driver's. See library/mmio/mmio.zig.
|
||||
const mmio_module = b.addModule("mmio", .{
|
||||
.root_source_file = b.path("library/mmio/mmio.zig"),
|
||||
});
|
||||
|
||||
// Keyboard layouts compiled from the X11 xkeyboard-config database into native Zig
|
||||
// (keycode + modifiers -> keysym/character). The `layouts` tables are generated by
|
||||
// tools/make-xkeyboard-config.py; `xkeyboard-config` is the hand-written API over them.
|
||||
// No target set, so each inherits its importer's. See library/xkeyboard-config/.
|
||||
const xkb_layouts_module = b.addModule("layouts", .{
|
||||
.root_source_file = b.path("library/xkeyboard-config/generated/layouts.zig"),
|
||||
});
|
||||
const xkeyboard_config_module = b.addModule("xkeyboard-config", .{
|
||||
.root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"),
|
||||
.imports = &.{
|
||||
.{ .name = "layouts", .module = xkb_layouts_module },
|
||||
},
|
||||
});
|
||||
|
||||
// The initial_ramdisk container format, shared by the kernel (unpacks it) and the
|
||||
// build-time packer tools/make-initial-ramdisk.py (produces it). No dependencies.
|
||||
const initial_ramdisk_module = b.addModule("initial-ramdisk", .{
|
||||
.root_source_file = b.path("system/initial-ramdisk.zig"),
|
||||
});
|
||||
|
||||
// Compile-time configuration the kernel reads as `@import("build_options")`. The
|
||||
@@ -183,7 +302,7 @@ pub fn build(b: *std.Build) void {
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "kernel",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("system/kernel/main.zig"),
|
||||
.root_source_file = b.path("system/kernel/kernel.zig"),
|
||||
.target = kernel_target,
|
||||
.optimize = optimize,
|
||||
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
|
||||
@@ -193,12 +312,14 @@ pub fn build(b: *std.Build) void {
|
||||
.stack_check = false, // stack-probe calls have no runtime to land in
|
||||
.stack_protector = false,
|
||||
.imports = &.{
|
||||
.{ .name = "danos", .module = danos_module },
|
||||
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
||||
.{ .name = "abi", .module = abi_module },
|
||||
.{ .name = "device-abi", .module = device_abi_module },
|
||||
.{ .name = "architecture", .module = architecture_module },
|
||||
.{ .name = "platform", .module = platform_module },
|
||||
.{ .name = "parameters", .module = parameters_module },
|
||||
.{ .name = "build_options", .module = build_options_module },
|
||||
.{ .name = "initrd", .module = initrd_module },
|
||||
.{ .name = "initial-ramdisk", .module = initial_ramdisk_module },
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -214,43 +335,165 @@ pub fn build(b: *std.Build) void {
|
||||
// (.text at 1 MiB), which the loader allocates and copies into.
|
||||
exe.image_base = 0xFFFFFFFF80100000;
|
||||
|
||||
b.installArtifact(exe);
|
||||
// Everything installs into a FHS-shaped zig-out: it IS the danos filesystem *and*
|
||||
// the boot volume. Each binary lands at its addressed, leaf-collapsed path — the
|
||||
// kernel at zig-out/system/kernel (from system/kernel/kernel.zig), init at
|
||||
// zig-out/system/services/init, and so on (see docs/README.md). The bootloader
|
||||
// then loads these FHS paths off the volume.
|
||||
const kernel_install = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = "system" } } });
|
||||
b.getInstallStep().dependOn(&kernel_install.step);
|
||||
|
||||
// --- /sbin/init: the first user-space program ---
|
||||
// --- init: the first user-space program (a system service) ---
|
||||
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
|
||||
// linked into the kernel's user region against the `runtime` runtime library, and
|
||||
// started in ring 3 by the kernel's user-ELF loader.
|
||||
const init_exe = addUserBinary(b, kernel_target, runtime_module, "init", "system/services/init/init.zig");
|
||||
b.installArtifact(init_exe);
|
||||
const init_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig");
|
||||
const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
|
||||
b.getInstallStep().dependOn(&init_install.step);
|
||||
|
||||
// --- initrd: a bundle of extra user binaries (VFS server + drivers) ---
|
||||
// --- initial_ramdisk: a bundle of extra user binaries (VFS server + drivers) ---
|
||||
// Each is built by the same user-binary recipe, then packed into one image by
|
||||
// the host-side mkinitrd tool. The bootloader ferries the image to the kernel,
|
||||
// which unpacks it and spawns each program (system/initrd.zig).
|
||||
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, "vfs", "system/services/vfs/vfs.zig");
|
||||
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
||||
const hpetd_exe = addUserBinary(b, kernel_target, runtime_module, "hpetd", "system/drivers/hpetd/hpetd.zig");
|
||||
const busd_exe = addUserBinary(b, kernel_target, runtime_module, "busd", "system/drivers/busd/busd.zig");
|
||||
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
|
||||
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
|
||||
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig");
|
||||
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
||||
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
||||
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
||||
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
||||
const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig");
|
||||
// The xHCI bus driver builds chapter-9 requests and decodes descriptors from
|
||||
// usb-abi, and reports each interface's (class,subclass,protocol) identity via
|
||||
// usb-ids.packTriple.
|
||||
usb_xhci_bus_exe.root_module.addImport("usb-abi", usb_abi_module);
|
||||
usb_xhci_bus_exe.root_module.addImport("usb-ids", usb_ids_module);
|
||||
usb_xhci_bus_exe.root_module.addImport("usb-transfer-protocol", usb_transfer_protocol_module);
|
||||
// The USB HID class drivers: keyboard and mouse. They own no hardware — each
|
||||
// opens its device through runtime.usb (the transfer protocol) and publishes to
|
||||
// the input service. They build chapter-9 class requests from usb-abi.
|
||||
const usb_hid_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-keyboard", "system/drivers/usb-hid/keyboard.zig");
|
||||
usb_hid_keyboard_exe.root_module.addImport("usb-abi", usb_abi_module);
|
||||
const usb_hid_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-mouse", "system/drivers/usb-hid/mouse.zig");
|
||||
usb_hid_mouse_exe.root_module.addImport("usb-abi", usb_abi_module);
|
||||
// The USB mass-storage class driver: opens its device via runtime.usb, drives it
|
||||
// with Bulk-Only Transport + SCSI, and serves the block protocol under `.block`.
|
||||
const usb_storage_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-storage", "system/drivers/usb-storage/usb-storage.zig");
|
||||
usb_storage_exe.root_module.addImport("block-protocol", block_protocol_module);
|
||||
// The FAT filesystem server: mounts the block device and serves it into the VFS
|
||||
// at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively.
|
||||
const fat_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig");
|
||||
const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig");
|
||||
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
|
||||
// The PCI bus driver decodes each function's class triple to human names in its
|
||||
// boot log (class/subclass/prog-IF), so pull in the shared pci-class reference.
|
||||
pci_bus_exe.root_module.addImport("pci-class", pci_class_module);
|
||||
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
||||
// what the driver-restart scenario drives the crash-loop cap with.
|
||||
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
|
||||
const device_list_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig");
|
||||
// The discovery service: one swappable process per firmware
|
||||
// (docs/discovery.md), bundled under the neutral ramdisk name
|
||||
// "discovery" so the device manager never learns which firmware it is on.
|
||||
// x86 boots describe hardware with ACPI; the Raspberry Pis hand over a
|
||||
// flattened device tree — the aarch64 target flips the default when it
|
||||
// lands (docs/arm.md). Both are placeholders until M20.1 (acpi) and the
|
||||
// ARM bring-up (fdt).
|
||||
const Discovery = enum { acpi, fdt };
|
||||
const discovery = b.option(Discovery, "discovery", "Which discovery service fills the ramdisk's 'discovery' slot (default: acpi)") orelse Discovery.acpi;
|
||||
const discovery_source: []const u8 = switch (discovery) {
|
||||
.acpi => "system/services/acpi/acpi.zig",
|
||||
.fdt => "system/services/fdt/fdt.zig",
|
||||
};
|
||||
const discovery_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source);
|
||||
if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module);
|
||||
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
||||
// Names the xHCI PCI class triple from the shared taxonomy instead of a bare 0x0C0330.
|
||||
device_manager_exe.root_module.addImport("pci-class", pci_class_module);
|
||||
// The manager matches reported USB interfaces by their (class,subclass,protocol)
|
||||
// triple (usbDriverForIdentity), built from the named usb-ids codes.
|
||||
device_manager_exe.root_module.addImport("usb-ids", usb_ids_module);
|
||||
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
||||
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
||||
const input_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig");
|
||||
const input_source_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.zig");
|
||||
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig");
|
||||
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
||||
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig");
|
||||
const log_flush_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "log-flush", "system/services/log-flush/log-flush.zig");
|
||||
|
||||
// Pack the user binaries into the initrd image with the host-side Python tool
|
||||
// Pack the user binaries into the initial_ramdisk image with the host-side Python tool
|
||||
// (the container format is trivial, and Python sidesteps std API churn). Args:
|
||||
// mkinitrd.py <out> [<name> <file>]... — one name/file pair per binary.
|
||||
// make-initial-ramdisk.py <out> [<name> <file>]... — one name/file pair per binary.
|
||||
const mk_run = b.addSystemCommand(&.{"python3"});
|
||||
mk_run.addFileArg(b.path("tools/mkinitrd.py"));
|
||||
const initrd_img = mk_run.addOutputFileArg("initrd.img");
|
||||
mk_run.addFileArg(b.path("tools/make-initial-ramdisk.py"));
|
||||
const initial_ramdisk_img = mk_run.addOutputFileArg("initial-ramdisk.img");
|
||||
mk_run.addArg("vfs");
|
||||
mk_run.addFileArg(vfs_exe.getEmittedBin());
|
||||
mk_run.addArg("vfs-test");
|
||||
mk_run.addFileArg(vfstest_exe.getEmittedBin());
|
||||
mk_run.addArg("hpetd");
|
||||
mk_run.addFileArg(hpetd_exe.getEmittedBin());
|
||||
mk_run.addArg("busd");
|
||||
mk_run.addFileArg(busd_exe.getEmittedBin());
|
||||
mk_run.addArg("ps2-bus");
|
||||
mk_run.addFileArg(ps2_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("ps2-keyboard");
|
||||
mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin());
|
||||
mk_run.addArg("ps2-mouse");
|
||||
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
|
||||
mk_run.addArg("usb-xhci-bus");
|
||||
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("usb-hid-keyboard");
|
||||
mk_run.addFileArg(usb_hid_keyboard_exe.getEmittedBin());
|
||||
mk_run.addArg("usb-hid-mouse");
|
||||
mk_run.addFileArg(usb_hid_mouse_exe.getEmittedBin());
|
||||
mk_run.addArg("usb-storage");
|
||||
mk_run.addFileArg(usb_storage_exe.getEmittedBin());
|
||||
mk_run.addArg("fat");
|
||||
mk_run.addFileArg(fat_exe.getEmittedBin());
|
||||
mk_run.addArg("fat-test");
|
||||
mk_run.addFileArg(fat_test_exe.getEmittedBin());
|
||||
mk_run.addArg("pci-bus");
|
||||
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("crash-test");
|
||||
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
||||
mk_run.addArg("device-list");
|
||||
mk_run.addFileArg(device_list_exe.getEmittedBin());
|
||||
mk_run.addArg("discovery");
|
||||
mk_run.addFileArg(discovery_exe.getEmittedBin());
|
||||
mk_run.addArg("device-manager");
|
||||
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
||||
mk_run.addArg("input");
|
||||
mk_run.addFileArg(input_exe.getEmittedBin());
|
||||
mk_run.addArg("input-source");
|
||||
mk_run.addFileArg(input_source_exe.getEmittedBin());
|
||||
mk_run.addArg("input-test");
|
||||
mk_run.addFileArg(input_test_exe.getEmittedBin());
|
||||
mk_run.addArg("args-echo");
|
||||
mk_run.addFileArg(args_echo_exe.getEmittedBin());
|
||||
mk_run.addArg("process-test");
|
||||
mk_run.addFileArg(process_test_exe.getEmittedBin());
|
||||
mk_run.addArg("log-flush");
|
||||
mk_run.addFileArg(log_flush_exe.getEmittedBin());
|
||||
|
||||
// Install the image to zig-out/bin (so the QEMU test harness picks it up like
|
||||
// the other binaries). The run-x86-64 ESP install is added below.
|
||||
const initrd_install = b.addInstallFile(initrd_img, "bin/initrd.img");
|
||||
b.getInstallStep().dependOn(&initrd_install.step);
|
||||
// Also install the packed binaries to their FHS homes, so zig-out is a true image
|
||||
// of the filesystem — even though at boot they arrive inside the initial-ramdisk.
|
||||
for ([_]struct { *std.Build.Step.Compile, []const u8 }{
|
||||
.{ vfs_exe, "system/services" },
|
||||
.{ device_manager_exe, "system/services" },
|
||||
.{ input_exe, "system/services" },
|
||||
.{ ps2_bus_exe, "system/drivers" },
|
||||
.{ ps2_keyboard_exe, "system/drivers" },
|
||||
.{ ps2_mouse_exe, "system/drivers" },
|
||||
.{ usb_xhci_bus_exe, "system/drivers" },
|
||||
.{ usb_hid_keyboard_exe, "system/drivers" },
|
||||
.{ usb_hid_mouse_exe, "system/drivers" },
|
||||
.{ usb_storage_exe, "system/drivers" },
|
||||
.{ fat_exe, "system/services" },
|
||||
.{ log_flush_exe, "system/services" },
|
||||
}) |entry| {
|
||||
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
||||
b.getInstallStep().dependOn(&step.step);
|
||||
}
|
||||
|
||||
// The initial-ramdisk itself installs to /boot (with the loaders).
|
||||
const initial_ramdisk_install = b.addInstallFile(initial_ramdisk_img, "boot/initial-ramdisk.img");
|
||||
b.getInstallStep().dependOn(&initial_ramdisk_install.step);
|
||||
|
||||
// Boot methods live in boot/, one per way of getting the kernel running.
|
||||
// Each is its own binary/entry (a loader is built for its own target); today
|
||||
@@ -265,12 +508,46 @@ pub fn build(b: *std.Build) void {
|
||||
}),
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "danos", .module = danos_module },
|
||||
// The bootloader speaks only the handoff contract — never the user ABI.
|
||||
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
b.installArtifact(efiexe);
|
||||
// UEFI firmware requires the removable-media loader at exactly \EFI\BOOT\BOOTX64.efi,
|
||||
// so that path is fixed by the firmware (it is /boot's EFI stub, conceptually).
|
||||
const efi_install = b.addInstallArtifact(efiexe, .{ .dest_dir = .{ .override = .{ .custom = "EFI/BOOT" } } });
|
||||
b.getInstallStep().dependOn(&efi_install.step);
|
||||
|
||||
// --- danos-usb.img: the bootable FAT32 USB image ---
|
||||
// Format a real FAT32 image (the in-repo Python builder, no external tools)
|
||||
// holding exactly what the firmware and bootloader need off the ESP: the EFI
|
||||
// stub, the kernel, init, and the initial-ramdisk. QEMU presents this image as
|
||||
// a USB mass-storage device the guest boots from (see run-x86-64 and the test
|
||||
// harness), and the danos fat driver mounts the same image at /mnt/usb.
|
||||
const mk_fat = b.addSystemCommand(&.{"python3"});
|
||||
mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
||||
const fat_image = mk_fat.addOutputFileArg("danos-usb.img");
|
||||
mk_fat.addArg("64"); // MiB
|
||||
mk_fat.addArg("EFI/BOOT/BOOTX64.efi");
|
||||
mk_fat.addFileArg(efiexe.getEmittedBin());
|
||||
mk_fat.addArg("system/kernel");
|
||||
mk_fat.addFileArg(exe.getEmittedBin());
|
||||
mk_fat.addArg("system/services/init");
|
||||
mk_fat.addFileArg(init_exe.getEmittedBin());
|
||||
mk_fat.addArg("boot/initial-ramdisk.img");
|
||||
mk_fat.addFileArg(initial_ramdisk_img);
|
||||
const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
|
||||
b.getInstallStep().dependOn(&fat_image_install.step);
|
||||
|
||||
// `zig build check-fat-image` — validate the produced image is a real FAT32
|
||||
// with the EFI stub present (the builder's own --verify, no external tools).
|
||||
const check_fat = b.addSystemCommand(&.{"python3"});
|
||||
check_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
||||
check_fat.addArg("--verify");
|
||||
check_fat.addFileArg(fat_image);
|
||||
const check_fat_step = b.step("check-fat-image", "Verify the FAT32 USB image is valid and bootable");
|
||||
check_fat_step.dependOn(&check_fat.step);
|
||||
|
||||
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
|
||||
// Firmware lives in different places per OS/distro, so probe the known
|
||||
@@ -301,21 +578,8 @@ pub fn build(b: *std.Build) void {
|
||||
"/usr/local/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Intel)
|
||||
});
|
||||
|
||||
// Assemble an EFI System Partition layout: esp/EFI/BOOT/BOOTX64.efi
|
||||
const efi_install = b.addInstallArtifact(efiexe, .{
|
||||
.dest_dir = .{ .override = .{ .custom = "esp/EFI/BOOT" } },
|
||||
});
|
||||
// The bootloader loads the kernel by name from the volume root, so drop the
|
||||
// kernel ELF at esp/kernel.
|
||||
const kernel_install = b.addInstallArtifact(exe, .{
|
||||
.dest_dir = .{ .override = .{ .custom = "esp" } },
|
||||
});
|
||||
// The bootloader loads init from sbin/init on the same volume.
|
||||
const init_install = b.addInstallArtifact(init_exe, .{
|
||||
.dest_dir = .{ .override = .{ .custom = "esp/sbin" } },
|
||||
});
|
||||
// ...and the initrd (VFS server + drivers) from the volume root.
|
||||
const initrd_esp_install = b.addInstallFile(initrd_img, "esp/initrd.img");
|
||||
// The FHS zig-out (installed above) *is* the boot volume — no separate ESP to
|
||||
// assemble. QEMU presents it to the guest as a FAT drive below.
|
||||
|
||||
// The firmware needs to write NVRAM, so give it a writable copy of the vars.
|
||||
const vars_copy = b.addSystemCommand(&.{ "cp", "-f", ovmf_vars });
|
||||
@@ -323,6 +587,19 @@ pub fn build(b: *std.Build) void {
|
||||
|
||||
const run_efi = b.addSystemCommand(&.{
|
||||
"qemu-system-x86_64",
|
||||
"-device",
|
||||
"qemu-xhci,id=xhci",
|
||||
"-device",
|
||||
"usb-mouse,bus=xhci.0",
|
||||
"-device",
|
||||
"usb-kbd,bus=xhci.0",
|
||||
// "-usb",
|
||||
// "-device",
|
||||
// "usb-ehci,id=ehci",
|
||||
// "-device",
|
||||
// "usb-tablet,bus=usb-bus.0",
|
||||
// "-device",
|
||||
// "usb-mouse,bus=ehci.0",
|
||||
"-machine",
|
||||
"q35",
|
||||
"-m",
|
||||
@@ -332,10 +609,13 @@ pub fn build(b: *std.Build) void {
|
||||
});
|
||||
run_efi.addArg("-drive");
|
||||
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
||||
// Present the ESP directory to the guest as a FAT drive.
|
||||
// Boot off the FAT32 USB image: a mass-storage device on the same xHCI bus as
|
||||
// the keyboard and mouse. OVMF finds \EFI\BOOT\BOOTX64.efi on it and boots.
|
||||
run_efi.addArg("-drive");
|
||||
run_efi.addPrefixedFileArg("if=none,id=bootusb,format=raw,file=", fat_image);
|
||||
run_efi.addArgs(&.{
|
||||
"-drive",
|
||||
b.fmt("format=raw,file=fat:rw:{s}/esp", .{b.install_path}),
|
||||
"-device",
|
||||
"usb-storage,bus=xhci.0,drive=bootusb,removable=on,bootindex=0",
|
||||
"-net",
|
||||
"none",
|
||||
// Emulated display advertising 1280x720 as its native (EDID preferred)
|
||||
@@ -346,16 +626,19 @@ pub fn build(b: *std.Build) void {
|
||||
"-device",
|
||||
"VGA,edid=on,xres=1280,yres=720",
|
||||
});
|
||||
// Always capture the guest's serial0 (the kernel's machine-readable log) to a
|
||||
// timestamped file under zig-out, so each run leaves its own log behind.
|
||||
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ b.install_path, timestamp(b) });
|
||||
// Capture the guest's serial0 (danos's machine-readable log) to the qemu-test
|
||||
// scratch area — a dev/host artifact, kept out of the FHS boot volume we mount.
|
||||
// (/var/log/system is reserved for the kernel's own logging system later.) One
|
||||
// timestamped file per run.
|
||||
const log_dir = b.fmt("{s}/qemu-test", .{b.install_path});
|
||||
const make_log_dir = b.addSystemCommand(&.{ "mkdir", "-p", log_dir });
|
||||
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ log_dir, timestamp(b) });
|
||||
run_efi.addArgs(&.{ "-serial", b.fmt("file:{s}", .{serial_log}) });
|
||||
run_efi.step.dependOn(&efi_install.step);
|
||||
run_efi.step.dependOn(&kernel_install.step);
|
||||
run_efi.step.dependOn(&init_install.step);
|
||||
run_efi.step.dependOn(&initrd_esp_install.step);
|
||||
// The whole FHS zig-out must be installed (and the scratch dir created) before we mount it.
|
||||
run_efi.step.dependOn(b.getInstallStep());
|
||||
run_efi.step.dependOn(&make_log_dir.step);
|
||||
|
||||
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/run-x86-64-serial0-<timestamp>.log");
|
||||
const run_efi_step = b.step("run-x86-64", "Boot the x86-64 kernel in QEMU (UEFI/OVMF); serial0 is logged to zig-out/qemu-test/run-x86-64-serial0-<timestamp>.log");
|
||||
run_efi_step.dependOn(&run_efi.step);
|
||||
|
||||
// const run_cmd = b.addRunArtifact(exe);
|
||||
@@ -368,18 +651,73 @@ pub fn build(b: *std.Build) void {
|
||||
// }
|
||||
|
||||
// Tests run on the host. The kernel and bootloader target freestanding/UEFI
|
||||
// and can't be executed natively, so only the shared module is unit-tested
|
||||
// here (compiled for the host rather than inheriting a freestanding target).
|
||||
// and can't be executed natively, so only the shared contracts are unit-tested
|
||||
// here (compiled for the host rather than inheriting a freestanding target) —
|
||||
// which also compile-checks that the three-way split stays self-consistent.
|
||||
const test_step = b.step("test", "Run tests");
|
||||
for ([_][]const u8{
|
||||
"system/boot-handoff.zig",
|
||||
"system/abi.zig",
|
||||
"system/devices/device-abi.zig",
|
||||
"system/devices/pci-class.zig", // class/subclass/prog-IF name decoding
|
||||
"system/devices/acpi-ids.zig", // _HID name decoding
|
||||
"system/devices/aml/aml.zig", // AML parse + interpret, incl. Notify dispatch (M21)
|
||||
"system/devices/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings
|
||||
"system/devices/usb-ids.zig", // class/subclass/protocol code assignments
|
||||
"library/mmio/mmio.zig", // barriers assemble + registers round-trip
|
||||
"system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine
|
||||
"system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly
|
||||
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
|
||||
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
|
||||
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
|
||||
"system/services/vfs/path.zig", // mount-prefix path matching
|
||||
"system/services/vfs/protocol.zig", // NodeKind / DirectoryEntry sizes + op values
|
||||
"system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection
|
||||
"system/services/fat/engine.zig", // FAT read/write over a RAM-backed image
|
||||
}) |root| {
|
||||
const mod_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("system/danos.zig"),
|
||||
.root_source_file = b.path(root),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
|
||||
}
|
||||
|
||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
||||
// The xkeyboard-config keymap tests need its generated `layouts` import wired, so they
|
||||
// don't fit the plain loop above. Its keycode->character assertions are the end-to-end
|
||||
// proof that the xkb-data -> generator -> Zig-lookup pipeline is correct.
|
||||
const xkb_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "layouts", .module = xkb_layouts_module },
|
||||
},
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(xkb_tests).step);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_mod_tests.step);
|
||||
// runtime.time's Instant/Duration arithmetic. time.zig pulls in system.zig (the
|
||||
// syscall wrappers), which needs the `abi` module, so it doesn't fit the plain
|
||||
// loop above.
|
||||
const time_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("library/runtime/time.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "abi", .module = abi_module },
|
||||
},
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(time_tests).step);
|
||||
|
||||
// Convenience: `zig build gen-xkeyboard-config` regenerates the layout tables from the
|
||||
// vendored data (offline). `fetch` (the network step) stays a manual script run.
|
||||
const gen_xkb = b.addSystemCommand(&.{ "python3", "tools/make-xkeyboard-config.py", "generate" });
|
||||
const gen_xkb_step = b.step("gen-xkeyboard-config", "Regenerate library/xkeyboard-config/generated from the vendored data");
|
||||
gen_xkb_step.dependOn(&gen_xkb.step);
|
||||
}
|
||||
|
||||
+89
-18
@@ -45,10 +45,31 @@ rather than restate it. Roughly in the order things happen at runtime:
|
||||
until its hardware interrupts it**. The claim is the capability; `irq_ack` is the
|
||||
unmask.
|
||||
14. **[driver-model.md](driver-model.md) — buses, classes and host controllers.** How
|
||||
real driver stacks factor into three shapes, how families share code, and the
|
||||
proposed ABI for the three primitives still missing (capability passing, DMA +
|
||||
memory barriers, MSI).
|
||||
15. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
real driver stacks factor into three shapes and how families share code. The
|
||||
three primitives it proposed are long since built (M13 capability passing,
|
||||
M14 DMA + barriers, M15 MSI), and the driver *contract* on top of them —
|
||||
hello, supervision, restart — is built too (device-manager.md, M18).
|
||||
15. **[process-management.md](process-management.md) — process management.** The
|
||||
microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the
|
||||
supervision link as the kill authority, and child-exit notifications over the
|
||||
same endpoints IRQs arrive on.
|
||||
16. **[process-lifecycle.md](process-lifecycle.md) — the process lifecycle.** Built
|
||||
(M17): signals over IPC as the one lifecycle vocabulary every process speaks — the
|
||||
POSIX.1-1990 words with message delivery instead of stack hijack, the stable
|
||||
`runtime.process` interface, exit reasons, published exit events any stateful
|
||||
service can subscribe to (the VFS releasing dead clients' handles), and the two
|
||||
iron rules (cleanup is the kernel's job; kill is not a signal).
|
||||
17. **[device-manager.md](device-manager.md) — the device manager.** Built (M18,
|
||||
through the app surface): the
|
||||
tree, the matcher, and the supervisor. Tree structure lives in the manager,
|
||||
authority stays in the kernel; bus drivers report what they see; drivers are
|
||||
restarted through the lifecycle vocabulary — the plan that turns
|
||||
[resilience.md](resilience.md)'s restart goal into increments.
|
||||
18. **[input.md](input.md) — the input module.** Broadcasting input events (keyboard,
|
||||
mouse, joystick): why a synchronous rendezvous can't fan out to many listeners, the
|
||||
asynchronous `ipc_send` primitive built to fix it, and the per-device subscribe/publish
|
||||
service layered on top.
|
||||
19. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
||||
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||
|
||||
Start with the north star:
|
||||
@@ -61,9 +82,19 @@ Start with the north star:
|
||||
- **[resilience.md](resilience.md) — resilience.** A design note (not built yet) on
|
||||
fault isolation + live restart — the reincarnation-server + capability model that
|
||||
makes "if I break it, I can restart it" real. danos's core motivation.
|
||||
- **[zig-self-hosting.md](zig-self-hosting.md) — running Zig on danos.** A design note
|
||||
(not built yet) on making danos a real Zig target (`-target x86_64-danos`) and
|
||||
eventually running the compiler on it. The key realisation: Zig 0.16 reduces an OS
|
||||
port to **one seam** (`std.os.danos`), so we build `runtime.os` (→ that seam) plus a
|
||||
thin `runtime.fs`, retire the `posix` shim, and follow a phased path to
|
||||
`zig build-exe hello.zig` running on danos — **not** Linux-ABI emulation.
|
||||
|
||||
Cutting across all of these:
|
||||
|
||||
- **[system-requirements.md](system-requirements.md) — system requirements.** The
|
||||
hardware needed to run danos: minimum specs (UEFI x86-64, ACPI, PCIe ECAM,
|
||||
xHCI, ~128 MiB RAM) grounded in what the boot path actually assumes, plus a
|
||||
plain-language guide matching Intel/AMD CPU generations by name.
|
||||
- **[arch.md](arch.md) — the architecture split.** How CPU-specific code is kept
|
||||
behind a build-time `arch` module so the generic kernel never names x86_64,
|
||||
leaving room for other systems (e.g. an AArch64 Raspberry Pi) later.
|
||||
@@ -75,7 +106,16 @@ Cutting across all of these:
|
||||
when to build it, and how to keep it architecture-agnostic.
|
||||
- **[acpi.md](acpi.md) — finding the ACPI tables.** The concrete x86 locator chain:
|
||||
how the loader captures the **RSDP**, hands its physical address across in `BootInfo`,
|
||||
and how the platform derives the **RSDT/XSDT** from it and walks the SDTs.
|
||||
and how the platform derives the **RSDT/XSDT** from it and walks the SDTs — plus the
|
||||
live event side (the SCI, the power button, GPE/Notify) the ring-3 acpi service runs.
|
||||
- **[power.md](power.md) — the power service.** System power as a domain-named
|
||||
service: button/lid/battery events published to subscribers, and init's orderly
|
||||
shutdown composing the [lifecycle](process-lifecycle.md) stop sequence with an ACPI
|
||||
S5 write. Firmware-neutral — a PSCI backend drops in on ARM.
|
||||
- **[timers.md](timers.md) — timers and time.** The ring-3 surface for reading the
|
||||
clock and waiting: why `now()` is a syscall rather than a service, and the one-shot
|
||||
timer notification (`timer_bind`) that gives supervisors a timed wait — built on the
|
||||
LAPIC heartbeat and calibrated TSC of [device-interrupts.md](device-interrupts.md).
|
||||
- **[smp.md](smp.md) — multiple cores.** A design/research note on how microkernels
|
||||
(L4, seL4) handle SMP — big kernel lock vs per-CPU vs multikernel — and how the
|
||||
right choice depends on whether danos is chasing real-time or resilience.
|
||||
@@ -125,33 +165,64 @@ reach it *by module name*, never by a path into its files. The source tree delib
|
||||
what you see under `system/` in the source is what a running danos represents under
|
||||
`/system`.
|
||||
|
||||
**A sub-project is addressed by its directory; its entry point repeats the directory's
|
||||
name.** `system/services/init/` contains `init.zig` (its root), and produces a binary
|
||||
addressed as **`system/services/init`** — the repeated leaf resolves away:
|
||||
|
||||
| Source (root file) | Addressed as (module / binary / FHS path) |
|
||||
|----------------------------------------|--------------------------------------------|
|
||||
| `system/services/init/init.zig` | `system/services/init` → `/system/services/init` |
|
||||
| `system/drivers/ps2-bus/ps2-bus.zig` | `system/drivers/ps2-bus` → `/system/drivers/ps2-bus` |
|
||||
| `library/runtime/runtime.zig` | `library/runtime` (the `runtime` module) |
|
||||
|
||||
In **source**, a sub-project is a directory so it can hold many files — the entry is
|
||||
`init/init.zig`, beside it `vfs/vfs-test.zig`, `vfs/protocol.zig`, and so on. When
|
||||
**addressed or installed**, that collapses to the single canonical path: the `init`
|
||||
binary installs to `/system/services/init` (a file at that path), not
|
||||
`/system/services/init/init`. The repeated leaf exists only in source; the directory is
|
||||
the identity, the entry file is its implementation. (Same idea as a Go package being its
|
||||
directory, or a macOS `.app` bundle addressed by the bundle, not the executable within.)
|
||||
A sub-project's extra files are reached through the module, never as separate paths.
|
||||
|
||||
```
|
||||
system/ → /system danos's own internals (the self-representation)
|
||||
danos.zig the kernel↔user ABI contract (the `danos` module)
|
||||
parameters.zig initrd.zig shared contracts
|
||||
boot-handoff.zig the loader↔kernel contract (the `boot-handoff` module)
|
||||
abi.zig the private kernel↔runtime syscall ABI (the `abi` module)
|
||||
parameters.zig initial-ramdisk.zig shared contracts
|
||||
kernel/ IPC, memory, scheduling, the private syscall dispatch
|
||||
architecture/x86_64/ the `architecture` module (never named by generic code)
|
||||
devices/ the device model /system/devices reflects (+ aml/)
|
||||
drivers/ hpetd/ busd/ one sub-project per driver → /system/drivers
|
||||
services/ init/ vfs/ system servers → /system/services (vfs/ holds
|
||||
device-abi.zig the device wire types (the `device-abi` module)
|
||||
drivers/ hpet/ bus/ one sub-project per driver → /system/drivers
|
||||
services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds
|
||||
vfs.zig, vfs-test.zig, protocol.zig)
|
||||
library/ → /lib the runtime library (the stable application ABI)
|
||||
library/ → /lib libraries, one sub-directory each
|
||||
runtime/ the danos-native runtime + file API (fs) — the stable application ABI
|
||||
boot/ → /boot the loaders
|
||||
tools/ test/ host-side build + QEMU test harness
|
||||
```
|
||||
|
||||
A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns
|
||||
the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the runtime's
|
||||
file layer imports by name. `usb`/`block` drivers will expose their protocols the same
|
||||
way.
|
||||
file API (`runtime.fs`) imports by name. `usb`/`block` drivers expose their protocols the
|
||||
same way.
|
||||
|
||||
There is **no POSIX/C compatibility layer today**: danos programs do file I/O through the
|
||||
danos-native `runtime.fs` (open/read/write/list over the VFS). A hand-rolled POSIX shim
|
||||
(`library/posix/`) was retired as premature — the real POSIX/C surface will come later
|
||||
from the `std.os.danos` seam (and, eventually, musl) when danos becomes a Zig target (see
|
||||
[zig-self-hosting.md](zig-self-hosting.md)). When it does, the foreign-ABI naming
|
||||
exception in [coding-standards.md](coding-standards.md) applies to that seam.
|
||||
|
||||
## Source map
|
||||
|
||||
| Area | Code |
|
||||
|------|------|
|
||||
| Boot methods (one per way of booting the kernel) | `boot/` — `efi.zig` (UEFI) → `BOOTX64.efi` |
|
||||
| Kernel entry, panic, bring-up | `system/kernel/main.zig` |
|
||||
| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, `Syscall`, ABI) | `system/danos.zig` |
|
||||
| Kernel entry, panic, bring-up | `system/kernel/kernel.zig` |
|
||||
| Loader↔kernel handoff (`BootInfo`, `Framebuffer`, `MemoryMap`, VM layout) | `system/boot-handoff.zig` |
|
||||
| Private kernel↔runtime syscall ABI (`SystemCall`, mmap prot flags, `page_size`) — the runtime speaks it, not apps | `system/abi.zig` |
|
||||
| Device wire types (`DeviceDescriptor`, `DeviceClass`, …) | `system/devices/device-abi.zig` |
|
||||
| Physical frame allocator | `system/kernel/pmm.zig` |
|
||||
| Kernel heap (`std.mem.Allocator`) | `system/kernel/heap.zig` |
|
||||
| Scheduler (fixed-priority preemptive; blocking, wait queues) | `system/kernel/scheduler.zig` |
|
||||
@@ -159,14 +230,14 @@ way.
|
||||
| IPC channels between kernel threads (message passing) | `system/kernel/ipc.zig` |
|
||||
| IPC endpoints: cross-address-space call/reply, handles, notifications | `system/kernel/ipc-synchronous.zig` |
|
||||
| User processes: ELF loading, address spaces, the syscall table | `system/kernel/process.zig` |
|
||||
| Device tree + claim capability + `device_register` containment | `system/kernel/device-service.zig` |
|
||||
| Device tree + claim capability + `device_register` containment | `system/kernel/devices-broker.zig` |
|
||||
| IRQ-as-IPC: routing a device interrupt to a driver's endpoint | `system/kernel/irq.zig` |
|
||||
| Hardware discovery (ACPI/device tree) behind one neutral device model | `system/devices/` |
|
||||
| Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` |
|
||||
| In-kernel test cases | `system/kernel/tests.zig` |
|
||||
| Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `system/kernel/architecture/x86_64/` |
|
||||
| Runtime library (`runtime`): syscall wrappers, heap, stdio, IPC, device access — the stable application ABI | `library/runtime/` |
|
||||
| System services (init, the VFS server + its `protocol` module) | `system/services/` |
|
||||
| Device drivers, one sub-project each (`hpetd` leaf driver, `busd` bus driver) | `system/drivers/` |
|
||||
| danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access, the file API (`fs`) — the stable application ABI | `library/runtime/` |
|
||||
| System services (init, the VFS server + `protocol`, the device-manager) | `system/services/` |
|
||||
| Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` |
|
||||
| Build + `run-x86-64` (QEMU/OVMF) | `build.zig` |
|
||||
| QEMU integration test harness | `test/qemu_test.py` |
|
||||
|
||||
+57
-3
@@ -16,7 +16,7 @@ the RSDT's address is a field *inside* the RSDP. The platform follows that point
|
||||
UEFI configuration table
|
||||
│ the loader reads the RSDP's physical address
|
||||
▼
|
||||
BootInfo.acpi_rsdp (u64, in the shared `danos` module) system/danos.zig
|
||||
BootInfo.acpi_rsdp (u64, in the loader↔kernel handoff) system/boot-handoff.zig
|
||||
│ the kernel forwards the whole BootInfo
|
||||
▼
|
||||
platform.discover(boot_info, …) system/devices/platform.zig
|
||||
@@ -44,7 +44,7 @@ the [memory map](memory-map.md).
|
||||
|
||||
The loader can't just call the device module: the bootloader binary and the kernel
|
||||
binary are compiled separately, and **the loader isn't linked against the `platform`
|
||||
module at all** (it imports only the shared `danos` module). So instead of a call, it
|
||||
module at all** (it imports only the `boot-handoff` contract). So instead of a call, it
|
||||
deposits a value in the handoff struct:
|
||||
|
||||
```zig
|
||||
@@ -107,12 +107,66 @@ firmware-agnostic [device model](discovery.md) gets populated; this note stops a
|
||||
part that answers "where are the tables?" — everything past the RSDP is just following
|
||||
more pointers the tables themselves provide.
|
||||
|
||||
## ACPI events: the SCI, the power button, and GPEs (M21)
|
||||
|
||||
The tables above are static description; ACPI is also a *live* channel. Hardware
|
||||
raises the **SCI** (System Control Interrupt) — one shared, level-triggered line
|
||||
whose vector the FADT names — and the OS reads status registers to learn what
|
||||
happened: a fixed event like the power button, or a **General-Purpose Event**
|
||||
(GPE) whose handler is an AML method. Since [discovery](discovery.md) moved AML
|
||||
to ring 3, the event side lives there too, in the same **acpi service** — the
|
||||
device discoverer and the event source are one process, because both need the
|
||||
namespace and the port grant.
|
||||
|
||||
**The kernel hands the service what it needs and no more.** Reading PM1 event
|
||||
blocks and GPE blocks requires the FADT, which the kernel already parses for its
|
||||
own `\_S5` poweroff. Rather than re-parse, the kernel appends the **FADT as one
|
||||
more memory resource** on the `acpi-tables` node; the service tells it apart
|
||||
from the AML blob resources by signature — the FADT keeps its intact `"FACP"`
|
||||
header, while the blob resources are header-stripped bytecode that starts with
|
||||
no signature. The kernel's own FADT parse is untouched; the service reads the
|
||||
PM1 *event* blocks (which the kernel never parsed — it only needs PM1 *control*
|
||||
for `\_S5`) and the GPE0/GPE1 blocks straight from its copy. The **SCI itself**
|
||||
arrives as the node's one `len == 1` irq resource (distinct from the broad
|
||||
`[0, 256)` window that covers children's legacy lines), which is how the service
|
||||
finds the line to `irq_bind`.
|
||||
|
||||
With those in hand the service enables ACPI mode (only if `SCI_EN` is clear —
|
||||
some firmwares boot with it already set), sets `PWRBTN_EN`, and on each SCI:
|
||||
|
||||
- **The power button** is a *fixed* event: a set `PWRBTN_STS` bit in PM1 status.
|
||||
The handler clears it (write-1-to-clear), logs the press, and publishes a
|
||||
[`power`](power.md) `power_button` event to subscribers.
|
||||
- **GPEs** are the general path: for each set-and-enabled GPE bit `n`, the
|
||||
service evaluates its `\_GPE._L%02X` (level) or `_E%02X` (edge) handler
|
||||
method, drains the **Notify** queue that method produced, maps each notified
|
||||
device to an event (battery, AC, lid, or a generic `notify` with its code),
|
||||
and clears the status bit. A missing handler method is clear-and-log, not an
|
||||
error. Making GPEs work required teaching the interpreter one opcode it never
|
||||
handled — `Notify` (`0x86`) — which it now folds into a bounded queue drained
|
||||
per evaluation; everything else a handler needs (field access, control flow,
|
||||
method calls) was already proven by the ring-3 `_STA`/`_CRS` work.
|
||||
|
||||
**How this is tested.** QEMU cannot raise GPEs deterministically on this config,
|
||||
so GPE/Notify correctness is proven by **host unit tests** — hand-encoded AML
|
||||
with a `Notify` inside a method body, run under `zig build test`. The QEMU
|
||||
`power-button` scenario proves the fixed-event path end to end: a QMP
|
||||
`system_powerdown` injects a real ACPI power-button press, and the service's SCI
|
||||
handler must log it. Battery/AC/lid and the embedded controller's `_Qxx` queries
|
||||
are interface-complete but validated on real hardware later.
|
||||
|
||||
The service surface these events are *published on* — subscription, the event
|
||||
vocabulary, and orderly shutdown — is the power service, [power.md](power.md).
|
||||
|
||||
## Related
|
||||
|
||||
- [efi.md](efi.md) — the loader that captures the RSDP before `ExitBootServices`.
|
||||
- [memory-map.md](memory-map.md) — the same loader-captures / kernel-consumes seam, and
|
||||
the ACPI-reclaim memory the RSDP lives in.
|
||||
- [discovery.md](discovery.md) — the broader (still-evolving) plan for turning these
|
||||
tables into one neutral device model shared with the ARM device-tree path.
|
||||
tables into one neutral device model shared with the ARM device-tree path, and how
|
||||
ACPI enumeration and events moved to the ring-3 acpi service.
|
||||
- [power.md](power.md) — the domain-named power service the ACPI event side publishes
|
||||
to (button, lid, battery) and its orderly-shutdown path into S5.
|
||||
- [arch.md](arch.md) — why the kernel reaches the device code through a `platform`
|
||||
module and never names ACPI directly.
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ There are really two independent questions, and it's worth not conflating them:
|
||||
|
||||
The kernel entry point `_start` currently still lives in the generic `main.zig` as
|
||||
a thin trampoline into `kmain`. It's arch-adjacent (its calling convention is
|
||||
x86_64 [SysV](sysv.md), via the shared `danos.kernel_abi`), but it's three lines
|
||||
x86_64 [SysV](sysv.md), via the shared `system.kernel_abi`), but it's three lines
|
||||
and mostly generic, so it stays put for now. When AArch64 arrives — where entry means setting
|
||||
up a stack and reading a device-tree pointer from a register — the entry work will
|
||||
be substantial and per-arch, and *that* is when we extract an entry interface into
|
||||
|
||||
+75
-15
@@ -6,7 +6,7 @@ Conventions for danos source. The overriding one, from which most of the rest fo
|
||||
> abbreviation is an acronym.**
|
||||
|
||||
`interruptDispatch`, not `intDisp`. `message_len`, not `message_len` (`msg` expands, `len`
|
||||
is a Zig idiom — see the exceptions). `device_service`, not `device_service`. `scheduler`, not
|
||||
is a Zig idiom — see the exceptions). `devices_broker`, not `devices_broker`. `scheduler`, not
|
||||
`sched`. The cost of a longer name is paid once, at the keyboard; the cost of a
|
||||
cryptic one is paid every time the code is read, by everyone who reads it. In a
|
||||
microkernel whose whole argument is that a human can hold each piece in their head,
|
||||
@@ -58,13 +58,25 @@ abbreviation, expand it.
|
||||
|
||||
Three, and only three.
|
||||
|
||||
1. **Foreign ABI names are spelled exactly as the ABI spells them.** A function that
|
||||
*is* the C or POSIX interface keeps its name: `fopen`, `fwrite`, `fread`, `malloc`,
|
||||
`calloc`, `realloc`, `free`, `memcpy`, `mmap`, `munmap`, `open`, `read`, `write`,
|
||||
`close`, `lseek`, `stat`, `errno`. We don't get to rename `fwrite` to
|
||||
`fileWrite` — it wouldn't be `fwrite` any more. This also covers the syscall
|
||||
*wrappers* that exist to match those names. It does **not** license inventing new
|
||||
abbreviated names in that style.
|
||||
1. **Foreign ABI names are spelled exactly as the ABI spells them — but only inside
|
||||
the layer that *is* that ABI.** A function that *is* the C or POSIX interface keeps
|
||||
its name: `fopen`, `fwrite`, `fread`, `malloc`, `calloc`, `realloc`, `free`,
|
||||
`memcpy`, `mmap`, `munmap`, `open`, `read`, `write`, `close`, `lseek`, `stat`,
|
||||
`errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be
|
||||
`fwrite` any more.
|
||||
|
||||
**This exception is scoped to a file that *is* a foreign ABI, and nothing else.**
|
||||
danos has no such file today: the old `library/posix/` compatibility shim was retired
|
||||
once its callers moved to the danos-native `runtime.fs`, since a hand-rolled POSIX
|
||||
layer is premature until danos actually needs it (see
|
||||
[zig-self-hosting.md](zig-self-hosting.md)). The exception will apply again to the
|
||||
`std.os.danos` seam when danos becomes a real Zig target — that module *is* the C-ABI
|
||||
`system` interface, so it keeps `open`/`read`/`errno`/`O_CREAT`. **Everywhere else,
|
||||
Zig/danos naming applies with no exception**: a concept POSIX also has gets a danos
|
||||
name — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create`
|
||||
flag, not `O_CREAT`; the boundary is where `stat`→`status` and `O_CREAT`→`create` get
|
||||
mapped. (The `syscall` *wrappers* elsewhere are not an exception — they wrap the
|
||||
private danos ABI, so they use danos names.)
|
||||
|
||||
2. **Zig idioms are spelled the way Zig spells them.** Three names are the language's,
|
||||
not ours, and are left alone:
|
||||
@@ -83,11 +95,12 @@ Three, and only three.
|
||||
keep `i`; a coordinate may be `x`, `y`. The moment the scope is big enough that the
|
||||
letter's meaning isn't obvious on sight, give it a real name. When in doubt, name it.
|
||||
|
||||
4. **Established Unix filesystem and program conventions.** Top-level directories keep
|
||||
their conventional names — `src`, `lib`, `sbin`, `bin`, `docs` — as do daemon
|
||||
programs by their `d` suffix (`hpetd`, `busd`, following `sshd`/`httpd`). These are
|
||||
names a Unix reader already knows; expanding them fights the convention rather than
|
||||
serving it.
|
||||
That's all — no Unix-abbreviation exception. The source directories are full words
|
||||
(`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver
|
||||
lives in `system/drivers/` and a service in `system/services/`, so the *location*
|
||||
already says what it is. Encoding the role in the name too (`busd`, `vfsd`) is
|
||||
redundant — the program is just `ps2-bus`, `vfs`. Don't put in a name what its directory
|
||||
already tells you.
|
||||
|
||||
## A note on collisions
|
||||
|
||||
@@ -118,15 +131,46 @@ Within those spelling rules, follow Zig's own conventions:
|
||||
|
||||
- **Types** — `PascalCase`: `DeviceDescriptor`, `Endpoint`, `WaitQueue`.
|
||||
- **Functions** — `camelCase`: `mapUserDeviceInto`, `notifyFromIsr`.
|
||||
- **Variables, fields, constants** — `snake_case`: `message_length`, `device_service`,
|
||||
- **Variables, fields, constants** — `snake_case`: `message_length`, `devices_broker`,
|
||||
`notify_badge_bit`.
|
||||
|
||||
**File names are `kebab-case`.** A file named for a multi-word thing hyphenates it:
|
||||
`device-tree.zig`, `ipc-synchronous.zig`, `vfs-protocol.zig`, `device-service.zig`. A
|
||||
`device-tree.zig`, `ipc-synchronous.zig`, `vfs-protocol.zig`, `devices-broker.zig`. A
|
||||
single word or acronym needs no hyphen: `scheduler.zig`, `paging.zig`, `apic.zig`,
|
||||
`idt.zig`. (The module *alias* a file is imported under still follows the code
|
||||
conventions above — `snake_case` — because it's an identifier, not a filename.)
|
||||
|
||||
**A sub-project's entry point repeats its directory's name** — `init/init.zig`,
|
||||
`runtime/runtime.zig`, `ps2-bus/ps2-bus.zig` — and the sub-project is addressed by the
|
||||
*directory* (`system/services/init`, `library/runtime`), with the repeated leaf
|
||||
resolving away. See the repository-layout section of [README.md](README.md).
|
||||
|
||||
## Named values, not magic numbers
|
||||
|
||||
The naming rule has a twin: **a value with meaning gets a name, too.** The same
|
||||
principle drives both — a reader should never have to leave the code to understand it.
|
||||
An abbreviated *name* forces a reader to guess; a bare *number* forces them worse, out
|
||||
to a spec or a header or a comment three files away, to learn what the value even *is*.
|
||||
If `0x0C` is the PCI serial-bus class, the code says `BaseClass.serial_bus`, not `0x0C`;
|
||||
if `0x04` is the ACPI IRQ resource descriptor, it says `SmallResourceType.irq`, not
|
||||
`0x04`. The number is an implementation detail of the name — recorded once, where the
|
||||
name is defined, and never spelled again at a use site.
|
||||
|
||||
**Prefer an `enum`** when the values form a set (device classes, AML opcodes, resource
|
||||
descriptor types, states): the type then also says *which* set a value belongs to, and
|
||||
the compiler rejects a value from the wrong one. A lone `pub const` with a descriptive
|
||||
name suffices for a one-off (`const large_descriptor_bit = 0x80`). Reach for the enum
|
||||
the moment code elsewhere compares against, packs, or produces the value — a packed PCI
|
||||
class triple is written from named parts (`.serial_bus`, `.usb`, `.xhci`), never as
|
||||
`0x0C_03_30` under a comment that decodes the bytes.
|
||||
|
||||
The exceptions are the numbers that carry no hidden meaning: `0` and `1` as plain zero
|
||||
and one, an index step, a field width, a bit shift. `x + 1`, `buffer[0]`, and `<< 8`
|
||||
need no christening — there is nothing to look up. The test is exactly the naming test:
|
||||
*would a reader have to look this up to know what it means?* If yes, name it. This is
|
||||
what `opcodes.zig`'s `*_opcode` constants, `acpi-ids`'s `HardwareId`, and `pci-class`'s
|
||||
class enums already are — reference data defined once and named everywhere it is used.
|
||||
|
||||
## Why acronyms are the line
|
||||
|
||||
Because an acronym has no letters to restore. `MMIO` doesn't become "memory mapped
|
||||
@@ -135,3 +179,19 @@ input output" in code — that expansion is what the acronym *is for*. But `msg`
|
||||
test for "is this an abbreviation I must expand" is simply: *is there a longer word this
|
||||
is a clipped form of?* If yes, write the word. If it's an initialism standing in for a
|
||||
phrase, leave it.
|
||||
|
||||
## Zen of Zig
|
||||
|
||||
* Communicate intent precisely.
|
||||
* Edge cases matter.
|
||||
* Favor reading code over writing code.
|
||||
* Only one obvious way to do things.
|
||||
* Runtime crashes are better than bugs.
|
||||
* Compile errors are better than runtime crashes.
|
||||
* Incremental improvements.
|
||||
* Avoid local maximums.
|
||||
* Reduce the amount one must remember.
|
||||
* Focus on code rather than style.
|
||||
* Resource allocation may fail; resource deallocation must succeed.
|
||||
* Memory is a resource.
|
||||
* Together we serve the users.
|
||||
|
||||
@@ -5,10 +5,10 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o
|
||||
## Directory structure
|
||||
|
||||
| Path | Description |
|
||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| / | Primary hierarchy root and root directory of the entire file system hierarchy. |
|
||||
| /bin | Essential command binaries that need to be available in single-user mode, including to bring up the system or repair it, for all users (e.g., cat, ls, cp). |
|
||||
| /boot | Boot loader files (e.g., EFI, initrd.img ). |
|
||||
| /boot | Boot loader files (e.g., EFI, initial-ramdisk.img ). |
|
||||
| /dev | POSIX Device files (e.g., /dev/null, /dev/disk0, /dev/tty, /dev/random). |
|
||||
| /etc | Host-specific system-wide configuration files. |
|
||||
| /home | Users' home directories, containing saved files, personal settings, etc. |
|
||||
@@ -17,7 +17,7 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o
|
||||
| /srv | Site-specific data served by this system, such as data and scripts for web servers, data offered by FTP servers, and repositories for version control systems |
|
||||
| /system | DanOS operating system files (similar idea to C:\Windows). A true representation of danos — its layout mirrors the source tree, so `/system` is what danos *is*. |
|
||||
| /system/devices | danos virtual device tree e.g. similar to /sys on linux but with danos device tree conventions (the structures in the devices module) |
|
||||
| /system/drivers | driver binaries, one sub-project each (e.g. /system/drivers/hpetd) |
|
||||
| /system/drivers | driver binaries, one sub-project each (e.g. /system/drivers/pci-bus, /system/drivers/ps2-bus) |
|
||||
| /system/services | system-service binaries — the VFS server, init, and other user-mode servers (e.g. /system/services/vfs, /system/services/init) |
|
||||
| /system/kernel | the kernel image |
|
||||
| /tmp | Directory for temporary files (see also /var/tmp). Often not preserved between system reboots and may be severely size-restricted. |
|
||||
@@ -29,7 +29,7 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o
|
||||
POSIX specifies the long format of the ls command to represent the Unix file type as the first letter for an entry.
|
||||
|
||||
| type | symbol | Description |
|
||||
|-------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|-------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| regular | - | An ordinary file holding an uninterpreted byte stream. Reads and writes are positional, and the file grows on demand (e.g., a binary in /bin, a config file in /etc). |
|
||||
| directory | d | A container mapping names to other files. It may only be modified through directory operations, never written to directly. |
|
||||
| symbolic link | l | A file whose contents are a path that is resolved in its place. The target need not exist, and may cross mount points. |
|
||||
@@ -61,17 +61,18 @@ to the driver in the order written, and a read consumes what is there. Terminals
|
||||
serial lines, keyboards and mice are all of this shape. These are the natural first
|
||||
device nodes in danos, because a character driver needs nothing the kernel doesn't
|
||||
already provide — it claims its device, maps its registers with `mmio_map`, and blocks
|
||||
on `replyWait` for either an interrupt or a client request. `system/drivers/hpetd/hpetd.zig` is already
|
||||
that program, minus the client half.
|
||||
on `replyWait` for either an interrupt or a client request. `system/drivers/ps2-bus/ps2-bus.zig`
|
||||
is already that program, minus the file-node client half.
|
||||
|
||||
The obstacle is not the file type, it is which hardware a ring-3 driver can actually
|
||||
drive. Port I/O is unavailable to user space — the TSS I/O permission bitmap is absent
|
||||
and IOPL is never raised — so `in`/`out` from a driver is a #GP. That excludes the
|
||||
16550 UART at `0x3F8` and PS/2 at `0x60`/`0x64`, which is to say it excludes the
|
||||
obvious implementations of `/dev/tty`, `/dev/ttyS0` and a keyboard node. Until either
|
||||
port I/O grants or a memory-mapped UART exist, serial output stays a kernel service
|
||||
reached through the `write` system call rather than a file. A memory-mapped device such
|
||||
as the framebuffer has no such problem and is the more likely first real entry here.
|
||||
The obstacle was never the file type; it is which hardware a ring-3 driver can reach.
|
||||
Direct `in`/`out` from user space is still a #GP (no TSS I/O bitmap, IOPL never raised),
|
||||
but a driver no longer needs it: **`io_read`/`io_write`** grant port access the same way
|
||||
`mmio_map` grants memory — gated by `device_claim` and the device's discovered `io_port`
|
||||
resource. So the 16550 UART at `0x3F8` and the PS/2 controller at `0x60`/`0x64` (and thus
|
||||
`/dev/ttyS0` and a keyboard node) are now writable as ordinary ring-3 drivers; the
|
||||
low-rate legacy hardware that needs port I/O is fine with a syscall per access. A
|
||||
memory-mapped device such as the framebuffer, needing no port I/O at all, remains the
|
||||
easiest first entry.
|
||||
|
||||
### Block devices
|
||||
|
||||
@@ -79,20 +80,23 @@ A block device is addressed in fixed-size blocks and, unlike a character device,
|
||||
layer above is free to buffer, reorder, coalesce and retry requests against it. Disks
|
||||
and other persistent storage are the whole population of this class.
|
||||
|
||||
**danos cannot host a block driver at all today,** and the reason is worth stating
|
||||
plainly because it is not a matter of unwritten code. Every storage controller worth
|
||||
naming is a bus master: it is programmed by handing it the physical address of a
|
||||
descriptor ring and left to read and write memory on its own. A ring-3 driver cannot
|
||||
build such a ring, because `mmap` returns writeback-cached, physically discontiguous
|
||||
pages and never discloses their physical address. Nor should it be allowed to: a device
|
||||
programmed with an arbitrary physical address writes to arbitrary physical memory, and
|
||||
page tables do not sit between a device and RAM — an IOMMU does. Granting a DMA-capable
|
||||
device to a driver process, with no IOMMU programmed, is equivalent to granting ring 0,
|
||||
which would forfeit the isolation that motivates user-space drivers in the first place.
|
||||
A block driver is now **writable, but not yet memory-safe.** Every storage controller
|
||||
worth naming is a bus master: it is programmed by handing it the physical address of a
|
||||
descriptor ring and left to read and write memory on its own. That ring is exactly what
|
||||
**`dma_alloc`** now provides — physically contiguous, pinned, uncacheable, with its
|
||||
physical address disclosed — and **`/lib/mmio`**'s barriers order the descriptor writes
|
||||
against the doorbell, and **`msi_bind`** delivers completions. So an AHCI or NVMe driver
|
||||
can be written today (the M14/M15 work in [driver-model.md](driver-model.md); the earlier
|
||||
"cannot host a block driver at all" is no longer true).
|
||||
|
||||
Block devices therefore wait on DMA-capable memory, memory barriers, and VT-d/DMAR —
|
||||
the M14–M16 work in [driver-model.md](driver-model.md). A ramdisk over the initrd is
|
||||
the one block-shaped thing implementable now, and it needs no driver process.
|
||||
What is *not* yet true is that it is safe. A device programmed with an arbitrary physical
|
||||
address writes to arbitrary physical memory, and page tables do not sit between a device
|
||||
and RAM — an IOMMU does. The IOMMU is now *detected* (M16), but no translation domains
|
||||
are programmed, so granting a DMA-capable device to a driver process is still equivalent
|
||||
to granting ring 0. Until per-device domains confine a driver's DMA to the buffers it
|
||||
`dma_alloc`'d, a block driver works but forfeits the isolation that motivates user-space
|
||||
drivers — enforcement is the next step, and lands with that first driver. A ramdisk over
|
||||
the initial ramdisk remains the one block-shaped thing that needs no driver process at all.
|
||||
|
||||
### Pseudo-devices
|
||||
|
||||
|
||||
@@ -78,6 +78,40 @@ preemption and wakeups (1 ms granularity); the **TSC** is the resolution you rea
|
||||
time at. Making `sleep` itself sub-millisecond would take a tickless one-shot
|
||||
timer — a later step.
|
||||
|
||||
### Is the TSC trustworthy? Invariant, and synchronized
|
||||
|
||||
A cycle counter is only a valid *clock* if two things hold, and danos checks both,
|
||||
because they decide whether we read time with a cheap `rdtsc` or fall back to the HPET.
|
||||
|
||||
**Invariant.** An old TSC counted core clock cycles, so it sped up and slowed down with
|
||||
frequency scaling — useless as wall time. Modern CPUs (all of danos's targets) provide an
|
||||
**invariant TSC**: a constant rate across P/C-states that never stops. The guarantee is a
|
||||
CPUID bit — leaf `0x80000007`, EDX bit 8 — on both Intel *and* AMD. danos reads it in
|
||||
`calibrate`, and a TSC that doesn't advertise it is not used as the clocksource. AMD is
|
||||
why this matters in practice: it doesn't populate the Intel leaf `0x15` that enumerates
|
||||
the TSC *frequency*, so danos already measures AMD's rate against the HPET — but a
|
||||
measured frequency without the invariance guarantee is not enough.
|
||||
|
||||
**Synchronized.** Each core has its own TSC. Even invariant ones can start at different
|
||||
values (a second socket, some firmware), so a thread migrating from a core reading
|
||||
`1_000_000` to one reading `999_000` would see time jump *backward*. danos runs a **warp
|
||||
check** as each application processor comes online (`checkWarpSource`, adapted from
|
||||
Linux's): the waking core and the BSP hammer a shared "highest seen" TSC under a lock,
|
||||
and if either ever reads below it, the cores' TSCs are skewed. It's pairwise because APs
|
||||
come up one at a time ([smp.md](smp.md)).
|
||||
|
||||
**The fallback.** When the TSC fails either test — non-invariant (a bare VM such as the
|
||||
default qemu64), or warped between cores — danos moves the monotonic clock onto the
|
||||
**HPET** main counter: one fixed-rate counter, so it can neither skew between cores nor
|
||||
drift with frequency. It costs a memory-mapped read instead of a register read, but it
|
||||
keeps time *accurate*, which is the whole point. The switch preserves the current value,
|
||||
so the clock never jumps. The boot log names the outcome:
|
||||
|
||||
```
|
||||
/system/kernel: clocksource tsc (TSC invariant: yes, synchronized: yes) # real Intel/AMD
|
||||
/system/kernel: clocksource hpet (TSC invariant: no, synchronized: yes) # a bare VM (TCG)
|
||||
```
|
||||
|
||||
## Two kinds of vector, one dispatch
|
||||
|
||||
The IDT now installs gates `0-47`: the 32 exceptions plus the device range. Every
|
||||
@@ -156,10 +190,11 @@ spinning in unrelated code — is the whole mechanism working end to end.
|
||||
|
||||
## What's next (not done here)
|
||||
|
||||
- **The keyboard**: the PS/2 controller is port-mapped (`0x60`/`0x64`), and ring 3
|
||||
has no port I/O yet, so the first *input* device is blocked on either an I/O
|
||||
permission bitmap or `io_in`/`io_out` syscalls ([drivers.md](drivers.md)).
|
||||
- **MSI/MSI-X**: per-device vectors, edge-triggered and unshared, which retire the
|
||||
I/O APIC's mask/ack cycle and its 24-GSI ceiling.
|
||||
- **The keyboard**: the PS/2 controller is port-mapped (`0x60`/`0x64`), and port I/O is
|
||||
now available to ring 3 via the claim-gated `io_read`/`io_write` syscalls
|
||||
([drivers.md](drivers.md)) — so the first *input* device is unblocked; it just needs
|
||||
writing (claim the controller, `irq_bind` GSI 1, read scancodes from `0x60`).
|
||||
- **MSI-X**: `msi_bind` gives one per-device edge-triggered vector (M15); MSI-X's
|
||||
multi-vector table (many queues per device, e.g. NVMe) is the remaining extension.
|
||||
- **The LAPIC's own page** is still mapped writeback-cacheable like the rest of the
|
||||
identity map. QEMU tolerates it; real hardware wants it uncacheable.
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# The device manager
|
||||
|
||||
**Status: the protocol and supervision are built** (M18.1, 2026-07-13): `hello`
|
||||
with its deadline, supervised spawn, restart with backoff, and the crash-loop
|
||||
cap are in — usb-xhci-bus is the first conforming driver, and the
|
||||
`driver-restart` scenario proves fault → backoff → re-claim → cap end to end.
|
||||
Tree reports are built too (M18.2, 2026-07-13): the xHCI driver scans its
|
||||
root-hub ports and reports each connected device (`child_added`); the manager
|
||||
mirrors them and prunes a dead reporter's children, and the `usb-report`
|
||||
scenario proves report → prune → respawn → re-report. The application surface is built (M18.3, 2026-07-13):
|
||||
`enumerate` and `subscribe` over IPC, with `device-list` as the first client —
|
||||
the manager is now the one answer to "what devices exist" for applications.
|
||||
The primitives underneath are real ([process-management.md](process-management.md):
|
||||
spawn/supervise/kill/exit-notification; [driver-model.md](driver-model.md): the device
|
||||
table as a capability system; [drivers.md](drivers.md): claim/map/IRQ), and the first
|
||||
per-device driver spawn works (the device manager matches the xHCI controller by PCI
|
||||
class and spawns `usb-xhci-bus` with the device id as argv[1]). This document designs
|
||||
the rest: the device manager as **the tree, the matcher, and the supervisor** — the
|
||||
policy process that turns [resilience.md](resilience.md)'s restart goal into practice
|
||||
for drivers.
|
||||
|
||||
How processes stop, reload, and report their deaths is deliberately **not** in this
|
||||
document: that is the universal lifecycle every danos process speaks —
|
||||
[process-lifecycle.md](process-lifecycle.md), signals over IPC and the stable
|
||||
`runtime.process` interface. The device manager is that design's first serious
|
||||
customer, not its owner. Its own protocol contains nothing lifecycle-shaped; a
|
||||
driver is stopped, health-checked, and buried exactly like any other process.
|
||||
|
||||
## The tree: structure in the manager, authority in the kernel
|
||||
|
||||
The device tree is two things fused: *information* (what exists, how it nests) and
|
||||
*authority* (a descriptor is a licence to map physical memory). They separate:
|
||||
|
||||
- The **kernel keeps the capability system** — device, I/O-port, and interrupt
|
||||
claims, resource containment on `device_register`, the
|
||||
`mmio_map`/`irq_bind`/`msi_bind` gates — and **cleans all of it up when a process
|
||||
dies** (settled; it is increment 1 of
|
||||
[process-lifecycle.md](process-lifecycle.md)). The three invariants in
|
||||
[driver-model.md](driver-model.md) stay exactly where they are. A device manager
|
||||
that could mint MMIO mappings by its own say-so would be a second kernel, and a
|
||||
buggy one would un-earn everything the microkernel bought.
|
||||
- The **device manager owns the tree as data** — identity, topology, naming, driver
|
||||
matching, hotplug events, and being the one process everything else asks about
|
||||
devices. Firmware discovery seeds it (today via the kernel's snapshot); **bus
|
||||
drivers grow it** by reporting what they see; applications query and watch it.
|
||||
`device_enumerate` fades to a manager-internal (then deleted) seam.
|
||||
|
||||
Long-term, discovery itself leaves the kernel — but not *into* the manager. PCI
|
||||
enumeration is a **pci-bus driver**: the manager spawns it against the host bridge
|
||||
(already a device with the ECAM window as a resource), it scans, it reports functions
|
||||
like any bus reports children. ACPI becomes an **acpi service** that interprets the
|
||||
tables and reports the namespace. The manager only orchestrates and merges. Moving
|
||||
AML interpretation out of ring 0 is its own project on its own track; nothing here
|
||||
depends on when it lands. (It landed: [discovery.md](discovery.md), M19–M20.)
|
||||
|
||||
`device_register` is **idempotent on exact match**: a re-registration with an
|
||||
identical (parent, class, identity, resources) tuple returns the existing id
|
||||
instead of appending a duplicate. The kernel table has no unregister, so without
|
||||
this a restarted registering bus would re-report its children as fresh nodes on
|
||||
every respawn. Idempotence is what makes restart-and-re-report sound for *every*
|
||||
reporting bus — pci-bus, the acpi service, a future fdt service — not just one,
|
||||
and it is why supervision (below) can prune a dead bus's subtree and trust the
|
||||
restarted instance to rebuild exactly the same ids.
|
||||
|
||||
## The protocol
|
||||
|
||||
A `device-manager-protocol` module (the vfs-protocol pattern): extern-struct
|
||||
messages, a version in the handshake, reserved fields everywhere. The manager is a
|
||||
well-known endpoint (`ipc.register(.device_manager)`); the badge tells it who is
|
||||
talking; the same endpoint receives its children's exit notifications — one loop,
|
||||
one world.
|
||||
|
||||
| Direction | Message | Purpose |
|
||||
|---|---|---|
|
||||
| driver → manager | `hello { version, role, device_id }` | confirms the argv assignment, starts the deadline clock |
|
||||
| bus → manager | `child_added { parent, identity, resources }` | one node the bus discovered |
|
||||
| bus → manager | `child_removed { id }` | unplug, or the bus lost it |
|
||||
| app → manager | `enumerate` | snapshot of the tree (read-only) |
|
||||
| app → manager | `subscribe` | receive published add/remove events |
|
||||
|
||||
`hello` is the one deadline the manager enforces itself: spawned and silent past the
|
||||
deadline means wrong binary, wrong protocol version, or wedged before main — apply
|
||||
the stop sequence and the restart policy. Everything else lifecycle-shaped
|
||||
(terminate, the common `ping` liveness call, exit reasons) arrives through
|
||||
[process-lifecycle.md](process-lifecycle.md)'s vocabulary, not this protocol.
|
||||
|
||||
Assignment stays argv (`usb-xhci-bus <device id>`) for now — simple, and it works.
|
||||
The step after `hello` exists is delegation: the manager claims (or is granted) the
|
||||
devices and passes the claim to the driver over IPC (the M13 capability-transfer
|
||||
mechanism), replacing first-come-first-served `device_claim` with policy. Identity in
|
||||
`child_added` is per-bus: PCI children carry the class triple (`pci_class`, as the
|
||||
xHCI match already uses); USB children carry the (class, subclass, protocol) triple
|
||||
from usb-ids.zig — each bus's native language, decoded by the shared ids modules.
|
||||
|
||||
## Supervision and restart
|
||||
|
||||
Every driver is spawned with the manager's exit endpoint (`spawnSupervised` — built).
|
||||
On a death notification:
|
||||
|
||||
1. **Read the reason** ([process-lifecycle.md](process-lifecycle.md) increment 2).
|
||||
Clean exit → it meant to; don't restart. Fault or missed `hello` deadline →
|
||||
restart with **backoff**, and a crash-loop cap (three fast deaths → mark failed,
|
||||
stop respawning, log loudly; a later `reload` to the manager can retry).
|
||||
2. **Prune the subtree** the dead bus driver reported. Its children describe
|
||||
protocol state (xHCI slot ids, transfer rings) that died with the process;
|
||||
keeping the nodes would be keeping a lie. Watchers receive `child_removed` — the
|
||||
input service losing, then regaining, a keyboard is the *honest* description of
|
||||
what happened. The restarted instance rediscovers and re-reports.
|
||||
3. **The claim is already free** because the kernel released it at death — the
|
||||
restarted instance claims the same controller and comes up.
|
||||
|
||||
Who supervises the supervisor: **init** (PID 1), which already supervises the
|
||||
services it starts. If the manager dies, drivers keep running (they hold their
|
||||
claims; the kernel doesn't care who their supervisor was — though their exit
|
||||
notifications now dangle harmlessly). The restarted manager re-learns the world:
|
||||
kernel snapshot, then a re-`hello` round — drivers answer a broadcast or are stopped
|
||||
and respawned. Full state handoff is deliberately not attempted.
|
||||
|
||||
## Thin drivers, class protocols
|
||||
|
||||
The [driver-model.md](driver-model.md) three-shape split, restated as processes:
|
||||
|
||||
- A **bus driver** (usb-xhci-bus) owns its controller — claim, MMIO, IRQ/MSI, DMA
|
||||
rings — and offers a *transfer* protocol ("submit a control transfer to device N",
|
||||
built from the usb-abi request constructors) plus tree reports to the manager.
|
||||
- A **class driver** (usb-hid, usb-storage) owns nothing: it is matched to a reported
|
||||
child by its identity triple, speaks the bus's transfer protocol downward and its
|
||||
service's protocol upward — HID reports to the input service, blocks to the block
|
||||
service. It works unchanged over any controller.
|
||||
- **Services** (input, display, block) aggregate class drivers and face applications.
|
||||
|
||||
Each arrow is a protocol module. The manager routes none of the data plane — it
|
||||
introduces the parties (matching), supervises them (lifecycle), and gets out of the
|
||||
way.
|
||||
|
||||
## Increments
|
||||
|
||||
Increments 1–4 are the lifecycle prerequisites and live in
|
||||
[process-lifecycle.md](process-lifecycle.md) (claim cleanup on death, exit reasons,
|
||||
published exit events, signals + `runtime.process`). On top of those:
|
||||
|
||||
5. **device-manager-protocol**: `hello`, supervised spawn with restart policy;
|
||||
usb-xhci-bus becomes the first conforming driver.
|
||||
6. **Tree reports**: `child_added`/`child_removed`; the manager mirrors; xHCI reports
|
||||
the mouse and keyboard QEMU already hangs off it.
|
||||
7. **App surface**: `enumerate`/`subscribe` over IPC; `device_enumerate` retreats
|
||||
to a manager-internal seam.
|
||||
8. **Discovery migration** — DONE (M19–M20, 2026-07-13): enumeration moved to
|
||||
ring 3 as swappable per-firmware discoverers — the pci-bus driver (M19) then
|
||||
the acpi service (M20), see [discovery.md](discovery.md); the kernel seeds
|
||||
only the host bridge and the acpi-tables node. Matching moved with it:
|
||||
`child_added` grew a `device_id` (the kernel-registered id, `no_device` for
|
||||
unregistered leaves like USB ports) and a firmware `hid`, and the manager now
|
||||
matches drivers from those **reports** rather than its boot-time snapshot. The
|
||||
PCI arm flipped in M19.3, the ACPI arm (ps2-bus matched from `_HID`) in M20.3
|
||||
— each in a single phase so no device is ever matched from both sources at
|
||||
once. The acpi service reports only the non-PCI `_HID` devices, since pci-bus
|
||||
already reports PCI functions (M20.2).
|
||||
|
||||
## Settled questions (2026-07-12)
|
||||
|
||||
- **Stateful buses**: pruning the subtree on bus-driver death is right for USB. A
|
||||
future storage bus with in-flight writes wants drain-before-terminate — which is
|
||||
exactly the `deadline_ms` parameter `stop()` already has; a per-driver deadline
|
||||
is one value in the manager's policy table when such a bus arrives. No design
|
||||
change.
|
||||
- **Manager death**: drivers survive the manager; the restarted manager re-learns
|
||||
the world (above). Checkpointing driver state with the manager is deferred until
|
||||
something demonstrates the need.
|
||||
- **Matching stays code until the third bus.** `driverFor`/`pciDriverFor` are
|
||||
honest at two bus types; the third triggers the manifest (a driver declares what
|
||||
it binds: a PCI class triple, a USB class triple, an ACPI `_HID`).
|
||||
@@ -167,3 +167,81 @@ free; discovery on x86 is partly about *finding* what ARM just tells you.
|
||||
- [ipc.md](ipc.md) — the channels that interrupts-as-messages and the device manager
|
||||
will ride on.
|
||||
- [vision.md](vision.md) — why drivers belong in isolated user space at all.
|
||||
|
||||
## Update (M19.3, 2026-07-13): PCI enumeration left the kernel
|
||||
|
||||
The kernel now seeds only the `pci_host_bridge` node (ECAM window, MMIO
|
||||
apertures derived from the memory map's holes, bus range, and the 16-bit I/O
|
||||
window). The per-function walk moved to the ring-3 `pci-bus` driver
|
||||
([device-manager.md](device-manager.md)): it claims the bridge, repeats the
|
||||
ECAM scan through its mmio grant, and `device_register`s what it finds, which
|
||||
the device manager mirrors and matches. The ACPI namespace walk follows in M20;
|
||||
the static tables (MADT, HPET, MCFG, FADT + `\\_S5`) stay kernel-side.
|
||||
|
||||
## Update (M20.3, 2026-07-13): ACPI enumeration left the kernel too
|
||||
|
||||
The kernel no longer folds the AML namespace's Device objects into the device
|
||||
tree. It still parses the *static* tables (MADT for SMP, HPET for the tick, MCFG
|
||||
for the host bridge, FADT) and still builds the AML namespace — but only to read
|
||||
the `\\_S5` sleep type for poweroff. Device discovery is the ring-3 **acpi
|
||||
service** ([device-manager.md](device-manager.md)): it claims the `acpi-tables`
|
||||
node the kernel publishes (the AML blobs, a broad io_port grant, the SCI),
|
||||
re-parses the same blobs with the shared AML module, evaluates `_STA`/`_CRS`,
|
||||
and registers + reports each `_HID` device — the device manager matches drivers
|
||||
(ps2-bus) from those reports. With M19's pci-bus driver, discovery now runs
|
||||
entirely in user space; the kernel seeds only the host bridge and the
|
||||
acpi-tables node.
|
||||
|
||||
## Discovery is a swappable process per firmware (M19–M20)
|
||||
|
||||
Moving PCI and ACPI enumeration out of ring 0 was not just a relocation — it
|
||||
made discovery **firmware-neutral by construction**, which is the whole reason
|
||||
to do it before the second architecture rather than after. Everything at and
|
||||
above the [device-manager](device-manager.md) protocol — descriptors,
|
||||
containment, reports, matching, supervision — is generic and may never become
|
||||
x86-specific. Discovery is the single firmware-specific piece, and it is
|
||||
isolated as **one swappable process per firmware**:
|
||||
|
||||
- **x86** boots describe hardware with ACPI, so the discoverer is the **acpi
|
||||
service** ([acpi.md](acpi.md)): it claims the `acpi-tables` node and runs AML.
|
||||
- **The Raspberry Pis** hand over a flattened device tree, so the discoverer is
|
||||
an **fdt service**: it claims a `devicetree-blob` node and walks the tree —
|
||||
pure data, no bytecode, so it needs neither a port grant nor an interpreter,
|
||||
strictly simpler than ACPI. (A placeholder until the [aarch64](arm.md)
|
||||
bring-up fills it in.)
|
||||
|
||||
The device manager spawns the discoverer under the **neutral ramdisk name
|
||||
`discovery`** and never learns which firmware it is on; the build's
|
||||
`-Ddiscovery=acpi|fdt` option fills that slot (x86 defaults to `acpi`, the
|
||||
aarch64 target flips the default when it lands). The manager owns the device
|
||||
tree as *data* and touches no hardware, ever — firmware bytecode runs only
|
||||
inside the crashable, supervised discoverer, so an AML fault can never take
|
||||
down the supervisor.
|
||||
|
||||
Two consequences of neutrality bind on later work:
|
||||
|
||||
- **Cross-firmware surfaces are named by domain, not firmware.** System power is
|
||||
a [`power`](power.md) protocol, not an "ACPI events" protocol: on x86 the acpi
|
||||
service registers it, on ARM a PSCI/mailbox service registers the same
|
||||
`ServiceId.power`, and subscribers never learn the difference.
|
||||
- **Identity must widen before the fdt service exists.** `DeviceDescriptor`'s
|
||||
8-byte `hid` holds an EISA id but cannot hold an FDT `compatible` string
|
||||
(`"brcm,bcm2835-aux-uart"`); the identity field grows before the ARM path can
|
||||
report a real node.
|
||||
|
||||
Two supporting decisions keep the kernel's remaining slice honest:
|
||||
|
||||
- **The AML interpreter is a shared build module**, compiled into both the
|
||||
kernel and the acpi service — one source, two builds, no fork. The kernel
|
||||
links it for the `\_S5` poweroff evaluation, the service links it for
|
||||
everything else, and the `acpi-parse` test asserts the two produce the same
|
||||
device count across the ring-3 move.
|
||||
- **Bridge apertures come from the firmware memory map, not AML.** Registered
|
||||
PCI functions carry BAR resources, and `device_register` containment demands
|
||||
the bridge own windows that cover them. Those apertures are derived
|
||||
kernel-side from the boot memory map's MMIO holes (regions that are neither
|
||||
RAM nor tables) — mechanical, AML-free, and available at boot regardless of
|
||||
what later moved to user space. The acpi service's authority is likewise
|
||||
exactly one node: the `acpi-tables` node, whose broad io_port grant is the
|
||||
documented trust boundary for the one process allowed to run firmware
|
||||
bytecode.
|
||||
|
||||
+92
-29
@@ -32,19 +32,19 @@ plain bus driver with no controller — a USB hub — is also a real thing.
|
||||
|
||||
## The device table is the spine
|
||||
|
||||
danos already has the right central structure. `system/kernel/device-service.zig` holds a table of
|
||||
danos already has the right central structure. `system/kernel/devices-broker.zig` holds a table of
|
||||
`DeviceDesc`, each with a parent, a class, and a set of resources. Firmware discovery
|
||||
seeds it ([discovery.md](discovery.md)); `device_register` grows it.
|
||||
|
||||
Three invariants make it a capability system rather than a directory:
|
||||
|
||||
1. **A claim is exclusive.** `device_claim(id)` succeeds once. Everything downstream —
|
||||
`mmio_map`, `irq_bind`, `device_register` — checks `device_service.ownerOf(id) == me`.
|
||||
`mmio_map`, `irq_bind`, `device_register` — checks `devices_broker.ownerOf(id) == me`.
|
||||
2. **A descriptor is a licence to map physical memory.** Whoever claims a device may
|
||||
map its `.memory` resources and bind its `.irq` resources. This is why
|
||||
`device_register` cannot be a free-for-all.
|
||||
3. **Therefore: containment.** Every resource of a registered child must lie inside a
|
||||
resource of the same kind on its parent (`device_service.contains`). A bus driver can only
|
||||
resource of the same kind on its parent (`devices_broker.contains`). A bus driver can only
|
||||
ever *subdivide* what it already holds. Without this, `device_register` would be a
|
||||
syscall named "map any physical page you like."
|
||||
|
||||
@@ -57,8 +57,9 @@ is not an address window. Discovery is trusted; user space is not.
|
||||
|
||||
### What a bus driver looks like
|
||||
|
||||
`system/drivers/busd/busd.zig` is the smallest honest one. Its "bus" is the HPET's register block and
|
||||
its "devices" are the block's comparators:
|
||||
danos ships no demo bus driver — the real ones are `pci-bus`, `ps2-bus`, and
|
||||
`usb-xhci-bus`. The smallest *honest* shape, illustrated here with an HPET register block
|
||||
as the "bus" and its comparators as the "devices", is:
|
||||
|
||||
```zig
|
||||
_ = dev.claim(bus.id); // 1. own the bus
|
||||
@@ -78,8 +79,8 @@ for (0..n) |i| { // 3. publish each child
|
||||
|
||||
Each child is left **unclaimed**, which is the handoff: a comparator driver can now
|
||||
`device_claim` one and `mmio_map` it, and will see only its own 0x20-byte window. A child
|
||||
whose window escapes the bus is refused — `busd` asserts that, and the `bus` test
|
||||
asserts the kernel's table upholds it.
|
||||
whose window escapes the bus is refused; the in-kernel `containment` test asserts the
|
||||
kernel's table upholds that ([drivers.md](drivers.md)).
|
||||
|
||||
A USB device has *no* resources at all: `resource_count = 0`, because it's addressed
|
||||
through its controller, not by MMIO. That case is allowed and is the common one.
|
||||
@@ -99,21 +100,21 @@ danos already has one of each: `library/runtime/device.zig` is a logic module,
|
||||
and its clients. The pattern generalises directly:
|
||||
|
||||
```
|
||||
lib/
|
||||
rt.zig module "rt" — syscalls, heap, ipc, dev, stdio
|
||||
mmio.zig module "mmio" — volatile register access + barriers [M14]
|
||||
library/
|
||||
runtime/ module "runtime" — syscalls, heap, ipc, device, stdio
|
||||
mmio/ module "mmio" — volatile register access + barriers [M14]
|
||||
bus/
|
||||
pci.zig module "pci" — ECAM, BAR decode, capability walk
|
||||
usb.zig module "usb" — descriptors, control transfers, hubs
|
||||
pci/ module "pci" — ECAM, BAR decode, capability walk
|
||||
usb/ module "usb" — descriptors, control transfers, hubs
|
||||
proto/
|
||||
vfs.zig module "proto.vfs" (today: system/services/vfs/protocol.zig)
|
||||
block.zig module "proto.block"
|
||||
hid.zig module "proto.hid"
|
||||
vfs/ module "vfs-protocol" (today: system/services/vfs/protocol.zig)
|
||||
block/ module "block-protocol"
|
||||
hid/ module "hid-protocol"
|
||||
|
||||
sbin/
|
||||
xhcid.zig HCD + bus driver imports rt, pci, usb, mmio
|
||||
usbhid.zig class driver imports rt, usb, proto.hid
|
||||
blockd.zig class driver imports rt, proto.block
|
||||
system/drivers/ one sub-project each → /system/drivers (no `d` suffix)
|
||||
xhci/ HCD + bus driver imports runtime, pci, usb, mmio
|
||||
usb-hid/ class driver imports runtime, usb, hid-protocol
|
||||
block/ class driver imports runtime, block-protocol
|
||||
```
|
||||
|
||||
The only build change needed: [`addUserBinary`](build.zig) currently takes exactly one
|
||||
@@ -132,15 +133,60 @@ If a class driver needs `mmio`, it has become an HCD and should be one.
|
||||
- **M11** — `irq_bind` / `irq_ack`. IRQ delivered as an IPC notification; mask before
|
||||
EOI; `irq_ack` is the unmask.
|
||||
- **M12** — `parent` in `DeviceDesc`, `device_register` with resource containment.
|
||||
- **M13** — capability passing. `ipc_call` / `ipc_reply_wait` grew a `send_cap` argument
|
||||
and a `received_cap` return (r8): an endpoint travels with a message, installed into
|
||||
the receiver's handle table (shared, refcount-bumped — a copy, not a move). A full
|
||||
table fails `-ENOSPC` and does not half-deliver. This is the "open" primitive — a bus
|
||||
driver mints a per-device endpoint and hands it to a class driver. The runtime exposes
|
||||
`callCap` and `replyWait(..., send_cap)`; no class driver consumes it yet.
|
||||
- **M14** — DMA memory + the memory-ordering layer. `/lib/mmio` gives drivers typed
|
||||
volatile access and `mb`/`rmb`/`wmb` (per-arch); `dma_alloc`/`dma_free` grant
|
||||
physically-contiguous, pinned, uncacheable, reclaim-on-teardown buffers with the
|
||||
physical address exposed (`pmm.allocContiguous`, a DMA arena, `mapUserDmaInto`).
|
||||
`dma_below_4g` caps the address for legacy engines; `dma_write_combining` is accepted
|
||||
but falls back to coherent until PAT is programmed. The bus drivers use `/lib/mmio`;
|
||||
no DMA driver consumes `dma_alloc` yet.
|
||||
- **M15** — interrupts for PCI devices, the MSI half. Discovery now gives every PCI
|
||||
function its 4 KiB ECAM config space as resource 0 (unblocking the capability walk
|
||||
with no new syscall), and `msi_bind(device_id, endpoint) -> address, data` allocates a
|
||||
per-device edge-triggered vector, delivered as an IPC notification with no mask and no
|
||||
ack cycle. Legacy INTx (`_PRT` parsing + shared lines) is deliberately skipped — MSI
|
||||
is the real answer. QEMU's HPET has no MSI, so delivery is proven with a self-IPI; the
|
||||
first PCI driver is the first real consumer.
|
||||
- **Port I/O** — `io_read`/`io_write(device_id, resource_index, offset, width[, value])`:
|
||||
a claimed device's `io_port` resource lets a driver read/write its ports, gated exactly
|
||||
like `mmio_map` gates memory (direct ring-3 `in`/`out` stays a #GP). This is what makes
|
||||
a PS/2 or 16550 driver possible; the low-rate legacy hardware that needs it is fine with
|
||||
a syscall per access. `io_port` resources were recorded by discovery and ignored — now
|
||||
they're used.
|
||||
- **M16 (detection)** — the IOMMU is now *found*: discovery parses the ACPI DMAR table,
|
||||
maps the first VT-d unit, and reads its version + capabilities (`iommu_present` in the
|
||||
platform info). This is detection only — **no translation domains are programmed, so
|
||||
DMA is still unprotected** (the caveat below). Enforcement lands with the first DMA
|
||||
driver, which is what there is to protect and test against. Proven in the `iommu` test,
|
||||
booted with an emulated `intel-iommu`.
|
||||
- **`system_spawn`** — a user-space supervisor starts a driver:
|
||||
`system_spawn(name, arguments)` loads a binary bundled in the initial-ramdisk as a
|
||||
fresh ring-3 process; `name` becomes the child's argv[0] and the optional
|
||||
NUL-separated `arguments` blob its argv[1..], delivered on a SysV entry stack
|
||||
([sysv.md](sysv.md)). This is what
|
||||
turned the device manager from "log the match" into "run the driver": the kernel now
|
||||
spawns only `init`, `init` spawns the services, and the **device-manager** discovers
|
||||
the hardware and spawns each driver ([drivers.md](drivers.md)). Ungated for now — a
|
||||
spawn capability is future work.
|
||||
|
||||
So: **bus drivers work now.** HCDs and class drivers do not. Here is exactly why, and
|
||||
exactly what would fix it.
|
||||
So: **bus drivers work now, and they're started by the device manager, not the kernel.**
|
||||
HCDs and class drivers do not work yet. Here is exactly why, and exactly what would fix it.
|
||||
|
||||
---
|
||||
|
||||
# Proposed ABI
|
||||
|
||||
## M13 — capability passing, for class drivers
|
||||
## M13 — capability passing, for class drivers ✅ done
|
||||
|
||||
*Implemented as described below (see "What exists today"). The signatures landed
|
||||
verbatim: `send_cap` in r9, `received_cap` returned in r8, `-ENOSPC` on a full receiver
|
||||
table with no delivery. The rest of this section is the original design note.*
|
||||
|
||||
**The blocker.** A class driver has to reach *its* device. Today the only way to find
|
||||
an endpoint is the name registry: `ipc_register(service_id, h)` / `ipc_lookup(id)`,
|
||||
@@ -178,7 +224,12 @@ const dev_ep = ipc.callCap(h, // ... mint a per-device endpoint,
|
||||
// now dev_ep is a private channel to that one device
|
||||
```
|
||||
|
||||
## M14 — DMA memory and the memory-ordering contract, for HCDs
|
||||
## M14 — DMA memory and the memory-ordering contract, for HCDs ✅ done
|
||||
|
||||
*Implemented: `/lib/mmio` (typed volatile access + `mb`/`rmb`/`wmb`, per-arch) and
|
||||
`dma_alloc`/`dma_free` (contiguous, pinned, uncacheable, reclaim-on-teardown, physical
|
||||
address exposed). `dma_write_combining` still falls back to coherent — real WC needs
|
||||
PAT, a small follow-up. The rest of this section is the original design note.*
|
||||
|
||||
**The blocker.** An HCD is a DMA-engine programmer. It needs a descriptor ring the
|
||||
device can read, which means memory that is (a) physically contiguous, (b) at a
|
||||
@@ -243,12 +294,17 @@ condition. Build the abstraction while there is one caller to fix.
|
||||
(Zig note: `@fence` was **removed in 0.16**. Use `@atomicRmw(..., .seq_cst)` for a full
|
||||
barrier, or per-arch inline asm — which is what `library/mmio.zig` should hide.)
|
||||
|
||||
## M15 — interrupts for PCI devices
|
||||
## M15 — interrupts for PCI devices ✅ done (MSI)
|
||||
|
||||
*Implemented the MSI half: ECAM config space per PCI function (resource 0) and
|
||||
`msi_bind` (per-device edge-triggered vector, delivered as a notification). Legacy INTx
|
||||
`_PRT` parsing is skipped on purpose. `msi_bind` returns (address, data) as two values
|
||||
rather than an out-struct. The rest of this section is the original design note.*
|
||||
|
||||
**The blocker, and it's a hard one.** No PCI device can take an interrupt today.
|
||||
[`addBars`](system/devices/acpi.zig) records `.memory` and `.io_port` BARs and never an
|
||||
`.irq`; there is no `_PRT` parsing anywhere in the tree. `hpetd` only works because the
|
||||
HPET advertises its own routing options in its own registers — a privilege no ordinary
|
||||
`.irq`; there is no `_PRT` parsing anywhere in the tree. The HPET is the one exception —
|
||||
it advertises its own interrupt routing in its own registers, a privilege no ordinary
|
||||
device has.
|
||||
|
||||
**The fix, in two halves.**
|
||||
@@ -271,10 +327,17 @@ which means **discovery should give each `pci_device` a `.memory` resource for i
|
||||
4 KiB ECAM slot**. That's a small change to `parseMcfg` and it unblocks the whole
|
||||
capability walk (MSI, MSI-X, PCIe extended caps) without any new syscall.
|
||||
|
||||
Note QEMU's HPET reports `Tn_FSB_INT_DEL_CAP = 0` — no MSI — so `hpetd` can never
|
||||
Note QEMU's HPET reports `Tn_FSB_INT_DEL_CAP = 0` — no MSI — so an HPET timer could never
|
||||
exercise this path. The first MSI driver will be the first PCI driver.
|
||||
|
||||
## M16 — the IOMMU, and the honest caveat
|
||||
## M16 — the IOMMU, and the honest caveat ◑ detection done, enforcement pending
|
||||
|
||||
*The IOMMU is now detected (DMAR parsed, VT-d unit mapped and read — see the `iommu`
|
||||
test), but **enforcement is not built**: no translation domains are programmed, so the
|
||||
caveat below still holds in full. Detection can't be taken further usefully until there
|
||||
is a DMA driver to protect and QEMU's `intel-iommu` to test the protection against —
|
||||
building the per-device domains alongside that first driver is both the natural order
|
||||
and the only way to verify them. The rest of this section is the original caveat.*
|
||||
|
||||
Everything above is capability-gated at the *CPU*. None of it is gated at the *device*.
|
||||
A driver that can program a bus-mastering engine can make that device write to any
|
||||
@@ -291,7 +354,7 @@ gap should be named rather than implied.
|
||||
|
||||
`M13` (capability passing) is independent of `M14`/`M15` and is the cheapest. It
|
||||
unlocks class drivers, which are the shape with no hardware requirements at all — you
|
||||
could write a real one against `busd`'s comparators tomorrow.
|
||||
could write a real one against any device a bus driver publishes tomorrow.
|
||||
|
||||
`M14` and `M15` together unlock the first HCD. `M14`'s barrier layer is worth landing
|
||||
on its own regardless: it's small, obviously correct, and stops every future driver
|
||||
|
||||
+130
-54
@@ -20,9 +20,55 @@ them for itself:
|
||||
A driver is, in one sentence, *a process that sleeps until its device has something to
|
||||
say.*
|
||||
|
||||
## How a driver gets started: discover, match, spawn
|
||||
|
||||
Nothing in the kernel decides that the PCI host bridge needs the `pci-bus` driver — that
|
||||
is policy, and policy lives in user space. Boot brings user space up as a three-level
|
||||
supervision hierarchy, each level owning one job:
|
||||
|
||||
```
|
||||
kernel ──spawns──► init (PID 1) ──spawns──► device-manager ──spawns──► pci-bus
|
||||
| | |
|
||||
spawns only init, the service supervisor: the driver supervisor: enumerates
|
||||
publishes the starts the system /system/devices, matches each device
|
||||
initial-ramdisk services (vfs, the to a driver, and system_spawn's it
|
||||
so user space can device-manager). Its
|
||||
system_spawn from it list is init policy.
|
||||
```
|
||||
|
||||
The kernel launches exactly one process — `init` — and hands it nothing but the raw
|
||||
ability to start more (`system_spawn(name, arguments)`, which loads a binary bundled
|
||||
in the initial-ramdisk as a fresh ring-3 process — `name` becoming its argv[0],
|
||||
the optional arguments its argv[1..], on a SysV entry stack, see sysv.md). Everything else is a user-space decision:
|
||||
|
||||
- **init** ([system/services/init](system/services/init/init.zig)) is the **service
|
||||
supervisor**. It spawns the system services danos brings up at boot — today `vfs` and
|
||||
the `device-manager` — from a small list. Drivers are deliberately *not* its job.
|
||||
- **device-manager** ([system/services/device-manager](system/services/device-manager/device-manager.zig))
|
||||
is the **driver supervisor**. It does the three steps a monolithic kernel would do in
|
||||
its probe path, entirely from ring 3:
|
||||
1. **Discover** — `device_enumerate` snapshots the device table the kernel built from
|
||||
ACPI/PCI ([discovery](discovery.md)).
|
||||
2. **Match** — for each device it looks up a driver by `DeviceClass`. The match policy
|
||||
is a table (`driverFor`): today a static `timer → hpet` map; a fuller system reads
|
||||
what each driver *binds* (a manifest under `/system/drivers`, or the driver
|
||||
describing its own match).
|
||||
3. **Spawn** — `system_spawn(driver_name, arguments)` starts the matched driver (the
|
||||
arguments can carry *which* device it matched), which then claims
|
||||
its device and runs the event loop below.
|
||||
|
||||
So "how is a driver discovered and configured" has two halves: **discovery** is the
|
||||
kernel's device table, read by anyone; **configuration** is two user-space policies —
|
||||
init's service list and the device-manager's match table. Both are hardcoded in their
|
||||
respective programs today; the natural next step is to move them into `/etc` (see the
|
||||
milestone notes in [driver-model.md](driver-model.md)). `system_spawn` is currently
|
||||
ungated — any process may spawn any bundled binary — because there is no spawn
|
||||
capability yet.
|
||||
|
||||
## The capability: claim before touch
|
||||
|
||||
The five driver syscalls (`system/danos.zig`, dispatched in `system/kernel/process.zig`):
|
||||
The driver syscall numbers (`system/abi.zig`) with the device types they carry
|
||||
(`system/devices/device-abi.zig`), dispatched in `system/kernel/process.zig`:
|
||||
|
||||
| # | Call | Meaning |
|
||||
|---|------|---------|
|
||||
@@ -40,7 +86,7 @@ memory; if `irq_bind` took a GSI, any process could bind the keyboard's line and
|
||||
silently intercept it. Instead the kernel checks two things (`process.ownedGsi`, and
|
||||
the same check at the top of `sysMmioMap`):
|
||||
|
||||
- `device_service.ownerOf(dev_id) == me` — you claimed it, and claims are exclusive
|
||||
- `devices_broker.ownerOf(dev_id) == me` — you claimed it, and claims are exclusive
|
||||
- the resource at `res_idx` is of the right *kind* — `memory` for `mmio_map`, `irq`
|
||||
for `irq_bind`
|
||||
|
||||
@@ -138,14 +184,18 @@ Two properties worth knowing:
|
||||
|
||||
## A whole driver
|
||||
|
||||
`system/drivers/hpetd/hpetd.zig` is ~150 lines and does all of it. The shape:
|
||||
A minimal leaf driver is only ~150 lines and does all of it. danos ships **no such
|
||||
example binary** — the driver model is proven by the real drivers (`pci-bus`, `ps2-bus`,
|
||||
`usb-xhci-bus`), and a teaching example belongs here, in the docs, rather than as a
|
||||
compiled program nobody runs. Illustrated with a hypothetical HPET timer driver, the
|
||||
shape is:
|
||||
|
||||
```zig
|
||||
const hpet = findHpet(buf) orelse return; // device_enumerate, look for
|
||||
// class=timer with memory + irq
|
||||
_ = dev.claim(hpet.dev_id); // the capability
|
||||
const base = dev.mmioMap(hpet.dev_id, hpet.mmio).?;
|
||||
const endpoint = ipc.createEndpoint().?;
|
||||
const endpoint = ipc.createIpcEndpoint().?;
|
||||
|
||||
// program the hardware over the mapping we were just handed
|
||||
reg(base, 0x100).* = level | int_enb | (hpet.gsi << 9); // timer 0 config
|
||||
@@ -163,8 +213,8 @@ while (...) {
|
||||
}
|
||||
```
|
||||
|
||||
The HPET is a good first driver for a reason that isn't obvious. Its *counter* is a
|
||||
clocksource — the only way to use it is to read it, so it proved `mmio_map` without
|
||||
The HPET makes a good illustration for a reason that isn't obvious. Its *counter* is a
|
||||
clocksource — the only way to use it is to read it, so it exercises `mmio_map` without
|
||||
needing interrupts at all. Its *comparators* are a clockevent, and can be configured
|
||||
**level-triggered** (`Tn_INT_TYPE_CNF`), which asserts a bit in `GENERAL_INT_STATUS`
|
||||
that the driver must write-1-to-clear. That's a genuine deassert step, so the full
|
||||
@@ -206,9 +256,10 @@ bus driver may only ever subdivide what it already owns.
|
||||
A device with **no resources** is legal and common. A USB device is reached through its
|
||||
controller, not by MMIO, so it gets `resource_count = 0`.
|
||||
|
||||
See [`system/drivers/busd/busd.zig`](../system/drivers/busd/busd.zig) for a complete one, and
|
||||
[driver-model.md](driver-model.md) for how bus drivers, class drivers and host
|
||||
controller drivers fit together.
|
||||
See [`system/drivers/pci-bus/pci-bus.zig`](../system/drivers/pci-bus/pci-bus.zig) for a
|
||||
real one — it claims a PCI host bridge, maps its ECAM window, and publishes each function
|
||||
it finds as a child — and [driver-model.md](driver-model.md) for how bus drivers, class
|
||||
drivers and host controller drivers fit together.
|
||||
|
||||
## What the kernel does not do for you
|
||||
|
||||
@@ -222,26 +273,24 @@ controller drivers fit together.
|
||||
|
||||
Worth knowing before you write the second driver:
|
||||
|
||||
- **Ring 3 has no port I/O.** The TSS I/O permission bitmap is absent
|
||||
(`tss.zig`: `iomap_base = @sizeOf(Tss)`), and IOPL is never raised, so `in`/`out`
|
||||
from a driver is a #GP. That rules out a user-space 16550 UART (`0x3F8`), PS/2
|
||||
(`0x60`/`0x64`), and legacy PCI config (`0xCF8`/`0xCFC`). Everything must be MMIO.
|
||||
`io_port` resources are recorded by discovery and then ignored.
|
||||
Several things this list used to warn about are now available (see
|
||||
[driver-model.md](driver-model.md)): **port I/O** (`io_read`/`io_write`, claim-gated by
|
||||
the device's `io_port` resource — direct ring-3 `in`/`out` is still a #GP, so a PS/2 or
|
||||
16550 driver goes through these), **DMA memory** (`dma_alloc`: contiguous, pinned,
|
||||
uncacheable, physical address exposed), and **memory barriers** (`/lib/mmio`'s
|
||||
`mb`/`rmb`/`wmb`). What remains:
|
||||
|
||||
- **Page granularity.** `mmio_map` rounds to 4 KiB. Two devices sharing a page means
|
||||
granting one grants the other. A `device_register`ed child's *resource* can be narrower
|
||||
than a page, but its *mapping* can't.
|
||||
- **No DMA memory.** `mmap` gives you writeback-cached, non-contiguous pages and never
|
||||
tells you their physical address, so you cannot build a descriptor ring. Any driver
|
||||
for a bus-mastering device is blocked on this.
|
||||
- **No memory barriers.** There are none in the tree, and `volatile` is not one — it
|
||||
won't stop the compiler sinking an ordinary store (your DMA descriptor) past a
|
||||
volatile MMIO store (your doorbell). On x86 you mostly get away with it; on ARM you
|
||||
will not. See [driver-model.md](driver-model.md#m14).
|
||||
- **DMA is not contained.** A driver that can program a bus-mastering device can make
|
||||
that device write to *any* physical address — page tables don't sit between a device
|
||||
and RAM; an IOMMU does. Until VT-d/DMAR is programmed, `device_claim` on a DMA-capable
|
||||
device is effectively equivalent to granting ring 0. This is the largest gap between
|
||||
the design's promise and what it delivers.
|
||||
and RAM; an IOMMU does. The IOMMU is now *detected* (M16), but no translation domains
|
||||
are programmed, so `device_claim` on a DMA-capable device is still effectively
|
||||
equivalent to granting ring 0. This is the largest gap between the design's promise and
|
||||
what it delivers; enforcement lands with the first DMA driver.
|
||||
- **No `dev_release`.** A claim is never dropped (only IRQ/MSI bindings are, on exit), so
|
||||
a device stays owned for the life of its driver — which blocks restart.
|
||||
- **One endpoint per GSI**, so shared legacy PCI INTx lines can't be split between two
|
||||
drivers. MSI/MSI-X — one vector per device, edge-triggered, unshared — is the real
|
||||
answer, and QEMU's HPET doesn't offer it (`Tn_FSB_INT_DEL_CAP = 0`).
|
||||
@@ -269,51 +318,78 @@ Worth knowing before you write the second driver:
|
||||
|
||||
## Verifying it
|
||||
|
||||
The `hpet` test spawns `hpetd` from the initrd and watches the serial log. The driver
|
||||
prints `hpetd: ok` only after being woken five times, and its loop's only exit is
|
||||
through `replyWait` returning a notification — it cannot reach that line by polling.
|
||||
No demo driver ships to prove this end to end; the *real* drivers do, so the tests
|
||||
target them and the kernel primitives directly:
|
||||
|
||||
The last check doesn't trust the driver's self-report at all: the kernel reads the I/O
|
||||
APIC redirection entry back and asserts the line really is routed to a device vector,
|
||||
really is level-triggered, and really was left unmasked by the driver's final
|
||||
`irq_ack`.
|
||||
- **`device-manager`** — boots only the device manager, which discovers the PCI host
|
||||
bridge, matches `pci-bus`, and `system_spawn`s it. The test reads kernel state — the
|
||||
process table and the device tree — to confirm pci-bus came up and registered the
|
||||
functions it enumerated: the whole discover → match → spawn → driver-up chain.
|
||||
- **`acpi-ps2`** — a user-space driver (`ps2-bus`) is woken by its device's IRQ,
|
||||
delivered as an IPC notification, and attaches the keyboard: IRQ-as-IPC, end to end.
|
||||
- **`pci-scan`** — a user-space driver (`pci-bus`) maps its device's MMIO (the ECAM
|
||||
window) and walks it: `mmio_map`, end to end.
|
||||
- **`containment`** — the kernel refuses a `device_register` whose child window escapes
|
||||
the parent's grant (else it would be a syscall for mapping arbitrary memory), while an
|
||||
identical re-register stays idempotent. Asserted in-kernel, straight against the broker.
|
||||
- **`irqfree`** — the teardown path. Binds two owners to one shared endpoint, releases
|
||||
one, and reads the I/O APIC back: the departing owner's line is masked, the sibling's
|
||||
is not. That second half is why bindings are keyed on the owning *task* and not on the
|
||||
endpoint pointer — endpoints are shared, so releasing "everything pointing at this
|
||||
endpoint" would silently mask a live driver's device.
|
||||
- **`iopass`** — the `device_grant` teardown rule, so destroying a driver's address
|
||||
space never returns MMIO frames to the RAM pool.
|
||||
|
||||
```
|
||||
$ python3 test/qemu_test.py hpet irqfree iopass
|
||||
hpet ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||
$ python3 test/qemu_test.py device-manager acpi-ps2 pci-scan containment irqfree iopass
|
||||
device-manager ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||
acpi-ps2 ... PASS
|
||||
pci-scan ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||
containment ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||
irqfree ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||
iopass ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||
```
|
||||
|
||||
Two companions cover what `hpetd` can't, because it never exits:
|
||||
|
||||
- **`irqfree`** — the teardown path. Binds two owners to one shared endpoint, releases
|
||||
one, and reads the I/O APIC back: the departing owner's line is masked, the sibling's
|
||||
is not. That second half is why bindings are keyed on the owning *task* and not on
|
||||
the endpoint pointer — endpoints are shared, so releasing "everything pointing at
|
||||
this endpoint" would silently mask a live driver's device.
|
||||
- **`iopass`** — the `device_grant` teardown rule, so destroying a driver's address
|
||||
space never returns MMIO frames to the RAM pool.
|
||||
|
||||
## What's next (not done here)
|
||||
|
||||
The big ones — capability passing (class drivers), DMA + barriers and MSI (host
|
||||
controller drivers), and the IOMMU — have proposed signatures in
|
||||
[driver-model.md](driver-model.md). Smaller items:
|
||||
The big driver-model pieces — capability passing (class drivers), DMA + barriers, MSI,
|
||||
and IOMMU detection — are **now done** ([driver-model.md](driver-model.md), M13–M16), as
|
||||
is **port I/O** (`io_read`/`io_write`, the claim-gated syscalls that make a PS/2 or 16550
|
||||
driver possible). What's left is IOMMU *enforcement* (per-device domains — it waits on
|
||||
the first DMA driver to protect and test against) and these smaller items:
|
||||
|
||||
- **Port I/O grants**, so a PS/2 or 16550 driver is possible: either a per-device TSS
|
||||
I/O permission bitmap swapped on context switch, or `io_in`/`io_out` syscalls gated
|
||||
by the same claim. The legacy devices that need it are all low-rate, so the syscall
|
||||
is likely fast enough.
|
||||
- **Releasing a claim.** There is no `dev_release`, and `device_service` never drops a claim on
|
||||
- **Releasing a claim.** There is no `dev_release`, and `devices_broker` never drops a claim on
|
||||
exit — only IRQ bindings are released. A dead driver's device stays owned forever,
|
||||
which blocks restart.
|
||||
- **Unregistering children.** `device_register` only appends. A USB device that is
|
||||
unplugged cannot be removed, and a bus driver in a loop can exhaust the 64-entry
|
||||
table.
|
||||
- **Restart.** A driver that dies should release its claim, have its device quiesced,
|
||||
and be respawned by a supervisor. Some pieces (`releaseIrqs`, `device_grant`
|
||||
teardown, the claim table) exist; the policy doesn't.
|
||||
- **Restart.** A supervisor that *spawns* drivers now exists — the device-manager starts
|
||||
them with `system_spawn` — but a supervisor that *restarts* them does not. A driver that
|
||||
dies should release its claim, have its device quiesced, and be respawned; today nothing
|
||||
notices the death. Some pieces (`releaseIrqs`, `device_grant` teardown, the claim table)
|
||||
exist, and `dev_release` (below) is the missing mechanism; the restart policy is the
|
||||
resilience track ([resilience.md](resilience.md)).
|
||||
- **Interrupt priority / threaded IRQ latency.** `notifyFromIsr` enqueues the woken
|
||||
driver but doesn't preempt (`wakeLocked` deliberately leaves that to the caller), so
|
||||
a woken driver waits for the next scheduling point.
|
||||
|
||||
## The driver contract (M17–M18)
|
||||
|
||||
Claiming and mapping is half of being a danos driver; the other half is the
|
||||
**lifecycle and protocol contract**, and the runtime makes it nearly free:
|
||||
|
||||
- Build on `runtime.service.run` — one replyWait loop folding protocol
|
||||
requests, signals, and notifications into callbacks. The harness answers the
|
||||
universal zero-length ping and turns `terminate` into a clean exit for you
|
||||
([process-lifecycle.md](process-lifecycle.md)).
|
||||
- A driver spawned with an assignment (its device id as argv[1]) sends the
|
||||
versioned `hello` to the device manager inside the deadline, and a **bus**
|
||||
driver reports what it discovers with `child_added`
|
||||
([device-manager.md](device-manager.md); usb-xhci-bus is the reference
|
||||
implementation).
|
||||
- Crash freely — that is the design. The kernel releases your claims, IRQ
|
||||
bindings, and MSI vectors at death; the manager reads your exit reason,
|
||||
prunes what you reported, restarts you with backoff, and your fresh instance
|
||||
re-claims and re-reports. Never depend on your own cleanup running
|
||||
(iron rule 1).
|
||||
|
||||
+23
-18
@@ -20,14 +20,17 @@ UEFI boots by looking for a FAT-formatted partition called the **EFI System
|
||||
Partition (ESP)** and running a file at a well-known fallback path:
|
||||
|
||||
```
|
||||
esp/EFI/BOOT/BOOTX64.efi <- the "removable media" default for x86-64
|
||||
EFI/BOOT/BOOTX64.efi <- the "removable media" default for x86-64
|
||||
```
|
||||
|
||||
That's exactly the layout `build.zig` assembles. It builds `boot/efi.zig` for the
|
||||
`uefi` target, installs it to `esp/EFI/BOOT/BOOTX64.efi`, and drops the kernel ELF
|
||||
at `esp/kernel`. The `run-x86-64` step then points QEMU at OVMF (UEFI firmware for
|
||||
virtual machines) and presents that `esp/` directory to the guest as a FAT drive.
|
||||
The firmware finds `BOOTX64.efi` and runs it — that's our `main()`.
|
||||
The boot volume is the **FHS-shaped `zig-out`** itself (see the repository-layout note
|
||||
in [README.md](README.md)): `build.zig` installs `boot/efi.zig` (built for the `uefi`
|
||||
target) to `zig-out/EFI/BOOT/BOOTX64.efi` — the one path UEFI firmware fixes — and lays
|
||||
the rest out by FHS path: the kernel at `zig-out/system/kernel`, init at
|
||||
`zig-out/system/services/init`, the initial-ramdisk at `zig-out/boot/`. The
|
||||
`run-x86-64` step points QEMU at OVMF (UEFI firmware for virtual machines) and presents
|
||||
`zig-out` to the guest as a FAT drive. The firmware finds `BOOTX64.efi` and runs it —
|
||||
that's our `main()`, which then loads the kernel and init from their FHS paths.
|
||||
|
||||
## Boot services: the firmware's API
|
||||
|
||||
@@ -83,9 +86,9 @@ All of this *must* happen now, because after exit there's no GOP to ask. (See
|
||||
|
||||
- Use the **LoadedImage** protocol to discover which device we booted from, then
|
||||
**SimpleFileSystem** to open that volume.
|
||||
- Open the file named `danos`, seek to the end to learn its size, rewind, and read
|
||||
the whole ELF into a firmware-allocated pool buffer. (`read` may return short, so
|
||||
we loop.)
|
||||
- Open the kernel ELF at its FHS path (`system\kernel`), seek to the end to learn its
|
||||
size, rewind, and read the whole ELF into a firmware-allocated pool buffer. (`read`
|
||||
may return short, so we loop.)
|
||||
- Parse the ELF: validate the `\x7fELF` magic and the `x86_64` machine type, then
|
||||
walk the program headers. For every `PT_LOAD` segment we:
|
||||
- reserve the exact physical pages it's linked at (`p_paddr`) via
|
||||
@@ -121,7 +124,7 @@ entirely ours.
|
||||
### 4. Jump to the kernel
|
||||
|
||||
```zig
|
||||
const kernel: *const fn (*const BootInfo) callconv(danos.kernel_abi) noreturn =
|
||||
const kernel: *const fn (*const BootInfo) callconv(boot_handoff.kernel_abi) noreturn =
|
||||
@ptrFromInt(entry);
|
||||
kernel(&boot_info);
|
||||
```
|
||||
@@ -139,17 +142,19 @@ kernel is freestanding and uses the **SysV AMD64** convention (first argument in
|
||||
read garbage.
|
||||
|
||||
So both sides pin the convention explicitly to SysV via the shared
|
||||
`danos.kernel_abi` (defined in `system/danos.zig`). The loader's function-pointer type
|
||||
and the kernel's `_start` both reference it, so the pointer lands in the register
|
||||
the kernel expects. This is the whole reason `kernel_abi` lives in the shared
|
||||
`danos` module: it's a contract both binaries must agree on. See
|
||||
`boot_handoff.kernel_abi` (defined in `system/boot-handoff.zig`). The loader's
|
||||
function-pointer type and the kernel's `_start` both reference it, so the pointer lands
|
||||
in the register the kernel expects. This is the whole reason `kernel_abi` lives in the
|
||||
shared `boot-handoff` module: it's a contract both binaries must agree on. See
|
||||
[sysv.md](sysv.md) for what "SysV" means and where else it shows up.
|
||||
|
||||
## The handoff contract
|
||||
|
||||
The loader and kernel are two *separate* binaries built for two different targets,
|
||||
so everything they exchange must have an identically-defined memory layout. That's
|
||||
what `system/danos.zig` provides — imported by both as the `danos` module:
|
||||
what `system/boot-handoff.zig` provides — imported by both as the `boot-handoff` module.
|
||||
It is *only* the handoff: the kernel↔user ABI (`system/abi.zig`) and the device types
|
||||
(`system/devices/device-abi.zig`) are separate contracts the bootloader never sees.
|
||||
|
||||
- `BootInfo` — the top-level struct passed to the kernel (currently just the
|
||||
framebuffer; this is where future handoff data like the memory map will go).
|
||||
@@ -164,13 +169,13 @@ the loader writes are the bytes the kernel reads.
|
||||
```
|
||||
power on
|
||||
-> UEFI firmware initialises hardware
|
||||
-> finds esp/EFI/BOOT/BOOTX64.efi, runs it (our efi.zig main)
|
||||
-> finds EFI/BOOT/BOOTX64.efi on the FHS volume, runs it (our efi.zig main)
|
||||
-> grab boot services
|
||||
-> queryFramebuffer (via GOP: EDID native res, setMode, describe fb)
|
||||
-> loadKernel (read danos ELF, load PT_LOAD segments to 0x100000)
|
||||
-> loadKernel (read system/kernel ELF, load PT_LOAD segments to 0x100000)
|
||||
-> exitBootServices (retry until the memory-map key holds)
|
||||
-> jump to e_entry, boot_info pointer in RDI
|
||||
-> kernel _start (system/kernel/main.zig: framebuffer console, then halt)
|
||||
-> kernel _start (system/kernel/kernel.zig: framebuffer console, then halt)
|
||||
```
|
||||
|
||||
Bottom line: **UEFI's job is to give us a CPU, memory, and a framebuffer, then
|
||||
|
||||
@@ -8,7 +8,7 @@ natural unit because that's the granularity the CPU's paging hardware maps — a
|
||||
it is the primitive everything above it stands on: page tables, the kernel heap,
|
||||
per-process memory all ultimately ask the frame allocator for pages.
|
||||
|
||||
It's **generic kernel code**: it operates on the neutral `danos.MemoryRegion`
|
||||
It's **generic kernel code**: it operates on the neutral `system.MemoryRegion`
|
||||
array, so there's no UEFI in it and nothing architecture-specific beyond the 4 KiB
|
||||
page. (Contrast [arch.md](arch.md), which is where CPU-specific code lives.)
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ exactly what `Console.pixel` does:
|
||||
self.rowPtr(y)[x] = color; // system/kernel/console.zig
|
||||
```
|
||||
|
||||
Our `Framebuffer` struct (`system/danos.zig`) is the four facts you need to
|
||||
Our `Framebuffer` struct (`system/boot-handoff.zig`) is the four facts you need to
|
||||
address it:
|
||||
|
||||
| Field | Meaning |
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ treats the call:
|
||||
signature for a kernel entry point — the bootloader jumps in and nothing ever
|
||||
jumps back out.
|
||||
|
||||
You can see the chain in `system/kernel/main.zig`: `_start` is `noreturn`, it calls
|
||||
You can see the chain in `system/kernel/kernel.zig`: `_start` is `noreturn`, it calls
|
||||
`kmain` which is `noreturn`, which ends by calling `arch.halt()` which is
|
||||
`noreturn`. The "never returns" property is threaded all the way down.
|
||||
|
||||
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
# The input module: broadcasting input events
|
||||
|
||||
A keyboard driver has one keystroke and *many* programs that might want it — a shell, a
|
||||
window server, a logger. None of them owns the hardware, and the driver should not know
|
||||
who is listening. So between the drivers and the listeners sits the **input service**
|
||||
(`system/services/input/`): drivers **publish** events to it, programs **subscribe**, and
|
||||
it fans each event out to every interested subscriber. It is an ordinary ring-3 process
|
||||
reached over IPC, like the [VFS server](../system/services/vfs/vfs.zig) — no kernel knows
|
||||
what a key is.
|
||||
|
||||
## One service, several device classes
|
||||
|
||||
The service carries three device classes today — **keyboard**, **mouse**, and
|
||||
**joystick/gamepad** — and is built to take more
|
||||
([protocol.zig](../system/services/input/protocol.zig)). Each class has its own typed
|
||||
event:
|
||||
|
||||
- `KeyEvent` — `key_down`/`key_up` (physical make/break) and `key_press` (a character was
|
||||
produced, carrying the Unicode scalar); plus a layout-independent `keycode` and a
|
||||
`modifiers` bitmask.
|
||||
- `MouseEvent` — relative `motion` (`dx`/`dy`), `button_down`/`button_up`, and `scroll`.
|
||||
- `JoystickEvent` — `axis` moves (a signed value on a `control` index) and
|
||||
`button_down`/`button_up`.
|
||||
|
||||
All three travel in one **`InputEvent` envelope** tagged with a `DeviceKind`, so the
|
||||
fan-out is a single code path and a subscriber can take a mix of classes on one stream.
|
||||
Decode an envelope with `asKeyboard()` / `asMouse()` / `asJoystick()` (each returns null
|
||||
unless the tag matches). A subscriber names the classes it wants with a **`device_mask`**,
|
||||
and the service routes each event only to subscribers whose mask includes its class — so a
|
||||
mouse-only listener never wakes for keystrokes.
|
||||
|
||||
## Why this needed a new kernel primitive
|
||||
|
||||
The interesting part is delivery, and it runs straight into the shape of danos IPC.
|
||||
[ipc.md](ipc.md) describes a **synchronous rendezvous**: a server holds exactly one
|
||||
pending reply (`Task.ipc_client`) and *must* answer it on its next `replyWait`. Two
|
||||
consequences decide the whole design:
|
||||
|
||||
1. **You cannot block N subscribers waiting for "the next event".** A server can hold only
|
||||
one caller at a time, so the natural "subscriber calls `next_event()` and blocks" API
|
||||
is impossible for more than one subscriber. Delivery therefore has to be **push** — the
|
||||
service reaching out to subscribers — not pull.
|
||||
|
||||
2. **A synchronous push can hang the whole service.** If the service delivered with
|
||||
`ipc_call`, it would block until each subscriber replied. `ipc_call` has no timeout, and
|
||||
the kernel does **not** wake a caller parked on a *dead* peer's endpoint (it only fails a
|
||||
peer that was mid-reply — see [process.zig](../system/kernel/process.zig)
|
||||
`releaseTaskResourcesLocked`). One subscriber that exits mid-delivery would wedge input
|
||||
for everyone. That is the opposite of the resilience the microkernel is for.
|
||||
|
||||
The fix is the asynchronous send that [ipc.md](ipc.md) had already earmarked as future
|
||||
work ("asynchronous / buffered send … for notifications between servers"):
|
||||
|
||||
```
|
||||
ipc_send(handle, message_ptr, message_len) -> 0 / -errno
|
||||
```
|
||||
|
||||
`ipc_send` copies a small payload into the endpoint's **bounded queue** and wakes a
|
||||
receiver, then returns immediately — it never blocks and so can never hang on a dead or
|
||||
slow subscriber. The receiver picks it up through the same `replyWait` it already runs:
|
||||
the wake arrives as a **buffered message** — `notify_badge_bit | notify_message_bit` set in
|
||||
the badge (distinguishing it from a bare IRQ/child-exit notification), the sender's task id
|
||||
in the low bits, and the payload in the receive buffer, with no reply owed. The queue holds
|
||||
16 messages per endpoint; a full queue **drops the oldest**, because a buffered message is
|
||||
discrete data, not a coalescing "level" like an interrupt. See
|
||||
[ipc-synchronous.zig](../system/kernel/ipc-synchronous.zig) (`sendLocked`, `popPost`, and
|
||||
the `replyWait` receive loop).
|
||||
|
||||
This is the async counterpart of `ipc_call`, and the input service is its first consumer.
|
||||
|
||||
## How the pieces fit
|
||||
|
||||
```
|
||||
keyboard/mouse driver, input-source input service subscriber(s)
|
||||
----------------------------------- ------------- -------------
|
||||
connectSource(); loop: replyWait: subscribeKeyboard()/…All:
|
||||
publishKeyboardEvent(k) ─ ipc_call ─▶ publish → broadcast: createIpcEndpoint()
|
||||
publishMouseEvent(m) for each sub whose callCap(subscribe,
|
||||
publishJoystickEvent(j) mask matches event.device: send_cap = ep,
|
||||
ipc_send(sub_ep) ──────▶ device_mask)
|
||||
reply ok loop: next()
|
||||
subscribe → store {ep cap, └─ replyWait(ep)
|
||||
task id, device_mask} → InputEvent
|
||||
```
|
||||
|
||||
- A **subscriber** calls `input.subscribe(mask)` — or a typed helper: `subscribeKeyboard()`,
|
||||
`subscribeMouse()`, `subscribeJoystick()` (one class, `next()` returns the decoded event),
|
||||
or `subscribeAll()` (every class, `next()` returns a tagged `InputEvent`)
|
||||
([library/runtime/input.zig](../library/runtime/input.zig)). It creates its own endpoint
|
||||
and hands it to the service as a **capability** (M13 capability passing — the input
|
||||
service is that feature's first real user), along with its `device_mask`. Then it loops on
|
||||
`next()`, a `replyWait` on that endpoint returning each pushed event.
|
||||
- A **source** (a keyboard, mouse, or joystick driver) calls `input.connectSource()` and the
|
||||
method for its class: `publishKeyboardEvent`, `publishMouseEvent`, or
|
||||
`publishJoystickEvent`. Publishing is a short synchronous `ipc_call` the service answers at
|
||||
once; the service's own fan-out is asynchronous, so publishing never blocks on a slow
|
||||
subscriber.
|
||||
- The **service** ([input.zig](../system/services/input/input.zig)) keeps a small subscriber
|
||||
table (endpoint handle + owning task id + `device_mask`). On `publish` it `ipc_send`s the
|
||||
event to every subscriber whose mask includes the event's device class. On `subscribe` it
|
||||
stores the passed capability and mask and, as housekeeping, prunes any slot whose owning
|
||||
process has exited (checked against `process_enumerate`) — not for correctness (an async
|
||||
send to an orphaned endpoint is harmless) but to reclaim the slot.
|
||||
|
||||
Publisher and subscriber must be **separate processes**: a single thread that both
|
||||
published and serviced its own subscription would deadlock (its `publish` call blocks until
|
||||
the service delivers to its endpoint, which only the same thread could receive).
|
||||
|
||||
## Status and follow-ups
|
||||
|
||||
- **The keyboard is real.** The `ps2-bus` driver owns PNP0303, which carries *both* the
|
||||
0x60/0x64 ports and IRQ1, so reading the hardware lives in the bus, not in
|
||||
[keyboard.zig](../system/drivers/ps2-bus/keyboard.zig): the bus binds IRQ1 and, on each
|
||||
interrupt, drains port 0x60, routing every byte by the status register's
|
||||
auxiliary-output bit to whichever child driver **attached** for that device (an
|
||||
`AttachRequest` to the well-known `ps2_bus` service, carrying the child's endpoint as a
|
||||
capability; the bytes then arrive as asynchronous `ForwardedByte` messages, so the IRQ
|
||||
path never blocks on a child). The keyboard driver decodes the stream — scancode **set 2**,
|
||||
what the keyboard sends with the 8042's legacy translation off, decoded by
|
||||
[scancode.zig](../system/drivers/ps2-bus/scancode.zig) into USB HID usage keycodes with
|
||||
make/break, typematic-repeat, and modifier tracking (host-tested under `zig build test`) —
|
||||
and publishes real `key_down`/`key_press`/`key_up` events.
|
||||
- **Keycode → character** is wired in: the keyboard driver fills a `key_press` event's
|
||||
`character` through [`library/xkeyboard-config`](../library/xkeyboard-config/README.md)
|
||||
(`xkb.map(layout, keycode, mods)` → keysym + Unicode character), synthesizing the ASCII
|
||||
control characters for Enter/Tab/Backspace/Escape, whose keysyms map to no Unicode. The
|
||||
layout defaults to `us`; the bus can pass another as the driver's argv[2] — the seam for
|
||||
a future settings source.
|
||||
- **The mouse is real too.** IRQ12 is enumerated on the auxiliary device's own ACPI node
|
||||
(PNP0F13), so the bus claims that node alongside the controller and routes both IRQs to
|
||||
its one endpoint, acking whichever line the notification's badge names.
|
||||
[mouse.zig](../system/drivers/ps2-bus/mouse.zig) attaches the way the keyboard does and
|
||||
assembles the forwarded bytes with
|
||||
[mouse-packet.zig](../system/drivers/ps2-bus/mouse-packet.zig) (three-byte stream-mode
|
||||
packets: sync/overflow handling, nine-bit movement, screen-convention `dy` — host-tested
|
||||
under `zig build test`) into `button_down`/`button_up` transitions and `motion` events.
|
||||
**Follow-up:** the IntelliMouse magic-knock for a scroll wheel (four-byte packets) and
|
||||
`scroll` events. The hardware-free `input-source` still rotates through all three classes
|
||||
synthetically (including a joystick, which has no driver yet) via the
|
||||
`input.synthetic*Event` helpers.
|
||||
- **Drop-oldest under overflow** is a defined loss; the 16-slot ring absorbs normal bursts.
|
||||
Real backpressure/flow-control is future work.
|
||||
- **`publish` is unauthenticated** — any process may publish, consistent with the current
|
||||
bring-up trust model (see [driver-model.md](driver-model.md)). A source capability is
|
||||
future work.
|
||||
|
||||
## Verifying it
|
||||
|
||||
The `input` case (`python3 test/qemu_test.py input`, in
|
||||
[tests.zig](../system/kernel/tests.zig) `inputTest`) boots the real kernel and spawns the
|
||||
service, the synthetic source (which cycles keyboard, mouse, and joystick events), and a
|
||||
subscriber that took all three classes. It passes only when the subscriber heartbeats
|
||||
`input-test: ok` — proof that an event travelled source → service → subscriber over IPC,
|
||||
exercising `ipc_send`, capability-passing subscription, and per-device routing. Each
|
||||
serial line names the class received, so the log shows all three arriving on one stream.
|
||||
|
||||
## See also
|
||||
|
||||
- [ipc.md](ipc.md) — the synchronous rendezvous and the notification path `ipc_send` extends.
|
||||
- [syscall.md](syscall.md) — the system-call surface, including `ipc_send`.
|
||||
- [driver-model.md](driver-model.md) — class drivers, capability passing (M13), the trust model.
|
||||
+15
-4
@@ -80,11 +80,22 @@ inline). `build.zig` adds `isr.s` to the arch module.
|
||||
## Reporting a fault
|
||||
|
||||
`isr_common` calls `exceptionHandler`, which forwards to a swappable `on_fault`
|
||||
hook. The generic kernel installs a reporter (`onException` in `main.zig`) that
|
||||
prints, in red, the exception name and vector, the error code, the faulting RIP
|
||||
hook. The generic kernel installs a reporter (`onException` in `kernel.zig`) that
|
||||
prints the exception name and vector, the error code, the faulting RIP
|
||||
and RSP, and — for a page fault (#PF, vector 14) — the faulting address from
|
||||
**CR2**. Then it halts. There's no fault *recovery* yet, so every exception is
|
||||
terminal; the point is that it's now **visible** instead of a silent reset.
|
||||
**CR2**. What happens next depends on where the fault came from:
|
||||
|
||||
- **User mode (CPL 3): kill the process, keep the machine.** The kernel is intact
|
||||
(the CPU trapped onto the task's kernel stack), so the faulting process is
|
||||
killed — address space, IRQ bindings, and IPC handles reclaimed; a client it
|
||||
owed a reply to is failed with `-EPEER` — and the core reschedules. A crashing
|
||||
driver takes itself down, never the OS. This is fault recovery step 2 of
|
||||
[resilience.md](resilience.md). NMI, double fault, and machine check are
|
||||
excluded: they report machine trouble regardless of what was running.
|
||||
- **Kernel mode: halt this core.** The trusted base itself is broken, so there is
|
||||
nothing safe to kill; the fault is still *contained* to the core (an
|
||||
application-processor fault leaves the rest of the system running), and the
|
||||
report makes it **visible** instead of a silent reset.
|
||||
|
||||
The hook is set before `arch.init()` in `kmain`, so a fault during setup is still
|
||||
caught.
|
||||
|
||||
+25
-1
@@ -95,6 +95,30 @@ This is what makes a user-space driver possible at all, and it's the subject of
|
||||
every capability is either well-known (the registry) or inherited — there's no way
|
||||
to delegate one.
|
||||
- **Asynchronous / buffered send** for the cases where a rendezvous is the wrong
|
||||
shape (logging, notifications between servers).
|
||||
shape (logging, notifications between servers). *Landed as `ipc_send`* — a
|
||||
non-blocking post to an endpoint's bounded payload queue, delivered through
|
||||
`reply_wait` as a buffered message (badge bit `notify_message_bit`). Built for, and
|
||||
first used by, the [input service](input.md)'s keyboard-event broadcast, where a
|
||||
synchronous push would let one dead subscriber hang the fan-out. A full queue drops
|
||||
the oldest (discrete messages, not a coalescing level like the notification ring).
|
||||
- **A bounded reply.** `MSG_MAX` is 256 bytes and the copy runs under the big kernel
|
||||
lock; a bulk transfer wants shared pages, not a copy.
|
||||
|
||||
## Lifecycle conventions over IPC (M17)
|
||||
|
||||
Three conventions from [process-lifecycle.md](process-lifecycle.md) ride the
|
||||
notification mechanism:
|
||||
|
||||
- **Signals** arrive as notifications on the endpoint a process nominated with
|
||||
`signal_bind` (`runtime.process.bindSignals`): badge = the signal bit plus the
|
||||
coalesced pending mask (`runtime.process.signalsFrom` decodes). Statements,
|
||||
never questions; no payload, no reply.
|
||||
- **One-shot timers** (`timer_bind`, `runtime.system.timerOnce`) land as a
|
||||
timer-bit notification — the timed wait: a service arms a deadline and keeps
|
||||
serving, instead of blocking in sleep.
|
||||
- **The universal ping**: a **zero-length request is the liveness probe**,
|
||||
answered with a zero-length reply by the service harness itself
|
||||
(`runtime.service.run`). No protocol's requests start at length zero, so the
|
||||
encoding cannot collide, and a wedged service simply fails to answer — which
|
||||
is the diagnosis. Deep health ("can I reach my hardware?") stays a per-service
|
||||
protocol message.
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ screen. `console.write` is a no-op when the firmware gave us no framebuffer.
|
||||
A framebuffer is not guaranteed — a headless server exposes no UEFI Graphics Output
|
||||
Protocol. That used to be *fatal* (the loader failed the boot). Now the loader hands
|
||||
over a "no framebuffer" descriptor (`base == 0`) rather than failing, and
|
||||
`Framebuffer.present()` (in `system/danos.zig`) gates every on-screen path. A headless,
|
||||
`Framebuffer.present()` (in `system/boot-handoff.zig`) gates every on-screen path. A headless,
|
||||
serial-less machine boots and runs correctly — it just goes quiet.
|
||||
|
||||
## Last-resort channels (no text output at all)
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ danos's own neutral format, and the kernel only ever sees that.**
|
||||
|
||||
## The neutral format
|
||||
|
||||
Defined in `system/danos.zig`, the shared loader↔kernel contract:
|
||||
Defined in `system/boot-handoff.zig`, the shared loader↔kernel contract:
|
||||
|
||||
```zig
|
||||
pub const MemoryKind = enum(u32) {
|
||||
@@ -121,7 +121,7 @@ The kernel receives a plain array and reads it with zero UEFI knowledge:
|
||||
|
||||
```zig
|
||||
const mm = boot_info.memory_map;
|
||||
const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len];
|
||||
const regions = @as([*]const system.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len];
|
||||
for (regions) |r| {
|
||||
if (r.kind == .usable) usable_pages += r.pages;
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ address to the low load address in its bootstrap tables and jumps in). The entir
|
||||
alongside a **physmap** — a straight window onto all of physical memory at
|
||||
`physmap_base + phys`. Wherever the kernel needs to touch a physical address (a
|
||||
page-table frame, an ACPI table, a device register), it adds that constant:
|
||||
`danos.physToVirt(phys)`. The layout constants live in `system/danos.zig`:
|
||||
`system.physToVirt(phys)`. The layout constants live in `system/boot-handoff.zig`:
|
||||
|
||||
| region | virtual base | PML4 slot |
|
||||
|--------|--------------|-----------|
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# The power service: events and shutdown
|
||||
|
||||
A laptop lid closes, a battery drains, someone presses the power button — and
|
||||
several parts of the system might care: a session manager dims the screen, a
|
||||
logger notes it, and ultimately *something* has to turn the machine off. None of
|
||||
them owns the hardware that reported the event, and the reporter should not know
|
||||
who is listening. So system power is a **service**: an event source **publishes**
|
||||
button/lid/battery/AC events, interested processes **subscribe**, and one
|
||||
privileged caller — init — can ask it to power the machine off. It is the same
|
||||
publish/subscribe shape as the [input service](input.md), applied to power.
|
||||
|
||||
## Why a service, and why it is named for the domain, not the firmware
|
||||
|
||||
Where the events come from is firmware-specific — on x86 they ride the ACPI SCI
|
||||
([acpi.md](acpi.md)); on a Raspberry Pi they would come from PSCI or a mailbox.
|
||||
What subscribers want is not: *the lid closed* means the same thing regardless of
|
||||
who noticed. So the surface is **domain-named**. There is a `power-protocol`
|
||||
module and a well-known `ServiceId.power = 5`; on x86 the **acpi service**
|
||||
registers it, and on ARM a PSCI/mailbox service will register the *same* id.
|
||||
Subscribers call `runtime.ipc.lookup(.power)` and never learn which firmware they
|
||||
are on — the neutrality the whole [discovery](discovery.md) migration exists to
|
||||
preserve, carried one layer up into a running-system surface.
|
||||
|
||||
This is why the protocol is `power`, not "ACPI events": naming a cross-firmware
|
||||
surface after one firmware would leak x86 into code the ARM port must reuse
|
||||
unchanged.
|
||||
|
||||
## The protocol
|
||||
|
||||
The `power-protocol` module ([system/services/power/protocol.zig](../system/services/power/protocol.zig))
|
||||
follows the vfs-protocol pattern — extern-struct messages, a version, reserved
|
||||
fields. Three operations:
|
||||
|
||||
| Direction | Operation | Purpose |
|
||||
|---|---|---|
|
||||
| subscriber → service | `subscribe` | receive published events; the subscriber's endpoint rides as the call's **capability** (the input/device-manager pattern) |
|
||||
| init → service | `shutdown` | orderly shutdown's last step: enter S5 (soft off) |
|
||||
| service → subscriber | `event` | a published `EventMessage`, delivered as a buffered message (never sent *to* the service) |
|
||||
|
||||
Events are published, not polled: like the input service, the service holds
|
||||
subscriber endpoints as capabilities and `ipc_send`s each event as a buffered
|
||||
message, so a slow or dead subscriber can never wedge the source. The event
|
||||
vocabulary is hardware-neutral:
|
||||
|
||||
- `power_button` — the button was pressed (a fixed ACPI event on x86).
|
||||
- `lid`, `ac`, `battery` — the named GPE-driven events.
|
||||
- `notify` — a device notification that maps to none of the above; its `code`
|
||||
(the ACPI `Notify` argument) and the notifying device's `hid` say which device
|
||||
and what happened.
|
||||
|
||||
An `EventMessage` carries the `event` tag plus `code` and an 8-byte `hid`, so a
|
||||
generic `notify` is fully described without a second round trip.
|
||||
|
||||
**`shutdown` is authority, not information.** It is the only operation that
|
||||
*does* something irreversible, so it is gated: the contract is that only init
|
||||
(PID 1) may request it, because init is the process that has already run the stop
|
||||
sequence over everything else. The acpi service implements this as a **soft
|
||||
gate** — it honors `shutdown` only from a process that is a *subscriber*, and
|
||||
init is the one subscriber. That stands in for "only the system supervisor may
|
||||
power off" without hard-coding a pid, so it still holds under tests where PID 1
|
||||
is not init.
|
||||
|
||||
## Orderly shutdown
|
||||
|
||||
Powering off cleanly is where the power service, the [process
|
||||
lifecycle](process-lifecycle.md), and [ACPI events](acpi.md) compose. init
|
||||
already supervises the services it starts; for shutdown it runs **one event loop
|
||||
over one endpoint** that carries three things at once: its children's exit
|
||||
notifications, the lifecycle **signals** it can receive (`terminate`), and the
|
||||
**power events** it subscribes to — plus a re-arming heartbeat timer proving PID
|
||||
1 is alive. (init subscribes with retries, because the power service registers
|
||||
`.power` well after init starts; a missing power service is not fatal — a
|
||||
`terminate` signal drives the same path.)
|
||||
|
||||
On a `power_button` event or a `terminate` signal, init:
|
||||
|
||||
1. logs that it is shutting down,
|
||||
2. runs the standard stop sequence — `runtime.process.stop(child, deadline,
|
||||
endpoint)` — over its children **in reverse spawn order**, so the VFS stops
|
||||
last (other services may flush through it), each child getting the
|
||||
*terminate → deadline → kill* escalation from
|
||||
[process-lifecycle.md](process-lifecycle.md), and
|
||||
3. requests `.power` `shutdown`.
|
||||
|
||||
The service then enters **S5** (soft off) by writing `SLP_TYP | SLP_EN` to the
|
||||
PM1 control register(s) from ring 3, mirroring the kernel's own
|
||||
`system/devices/power.zig` `sleepValue`. If the write returns instead of powering
|
||||
the machine off, it logs loudly so a test fails rather than hangs.
|
||||
|
||||
**No new system call was needed for S5.** The broad io_port grant on the
|
||||
`acpi-tables` node ([discovery.md](discovery.md)) already put the PM1 control
|
||||
ports in the acpi service's hands, so writing S5 from ring 3 is something it
|
||||
could physically already do; formalizing it as a protocol operation added a
|
||||
contract, not authority. The kernel keeps `power.zig` for its own test paths and
|
||||
panic-time poweroff, where no user space is available to ask.
|
||||
|
||||
## Verifying it
|
||||
|
||||
Two QEMU scenarios exercise the path, both injecting a real ACPI power-button
|
||||
press via QMP `system_powerdown` (there is no other deterministic power event on
|
||||
this config):
|
||||
|
||||
- `power-button` proves the source: the acpi service's SCI handler logs the
|
||||
press and publishes `power_button` (the ACPI half is in [acpi.md](acpi.md)).
|
||||
- `orderly-shutdown` proves the whole composition: button → init logs shutting
|
||||
down → children stopped → the service enters S5 → QEMU exits. The ordered
|
||||
regex is the proof, and QEMU's self-exit through S5 is the pass.
|
||||
|
||||
## Scope
|
||||
|
||||
Interface-complete but validated on real hardware (the author's laptop) later,
|
||||
because QEMU does not emulate them: battery `_BST`/`_BIF` evaluation beyond the
|
||||
interface stubs, lid and AC events, and the embedded controller's `_Qxx`
|
||||
queries. Deliberately out of scope for now: reboot over the power protocol, S3
|
||||
sleep, per-device D-states (a future lifecycle-vocabulary extension, since
|
||||
"suspend" has the shape of a signal every driver must answer and has no consumer
|
||||
until laptop sleep), and thermal zones.
|
||||
|
||||
## See also
|
||||
|
||||
- [acpi.md](acpi.md) — where the events come from on x86: the SCI, the power
|
||||
button fixed event, and GPE/Notify dispatch in the acpi service.
|
||||
- [discovery.md](discovery.md) — why the surface is domain-named, and the
|
||||
firmware neutrality that makes a PSCI backend drop-in on ARM.
|
||||
- [process-lifecycle.md](process-lifecycle.md) — the stop sequence
|
||||
(`terminate → deadline → kill`) and signals init composes into shutdown.
|
||||
- [device-manager.md](device-manager.md) — the supervision model init mirrors for
|
||||
its own children.
|
||||
@@ -0,0 +1,327 @@
|
||||
# Process lifecycle: signals over IPC
|
||||
|
||||
**Status: increments 1–4 built** (2026-07-12): claim release on death, exit
|
||||
reasons, published exit events, and signals + one-shot timers + the service
|
||||
harness are all in — the interface below is as-built. The primitives underneath
|
||||
predate this design ([process-management.md](process-management.md):
|
||||
spawn, the supervision link, kill, child-exit notifications); this document designs
|
||||
the layer above them — the standard vocabulary a danos process speaks about its own
|
||||
life, and the stable `runtime.process` interface that carries it. Nothing here is
|
||||
device- or driver-specific: a driver, the VFS, and a user application all stop,
|
||||
reload, and die the same way. The device manager is simply this design's first
|
||||
serious customer ([device-manager.md](device-manager.md)).
|
||||
|
||||
**"POSIX" in this document means the concepts, never the letter of the standard.**
|
||||
danos borrows the ideas and the hard-won lessons (what SIGTERM *means*, why SIGPIPE
|
||||
was a mistake) without inheriting the mechanism, the API, or the names. The naming
|
||||
rule is danos's own and it is strict: plain words that communicate intent
|
||||
(`terminate`, `reload`, `exited`) and the IPC vocabulary the system already speaks
|
||||
(`bind`, `subscribe`, `publish`, `endpoint`) — never `SIG*`, never a second word for
|
||||
a concept that already has one. Literal POSIX arrives later and lives elsewhere: the
|
||||
`std.os.danos` seam that makes danos a Zig target, and eventually a **musl-based C
|
||||
layer** on the same native surface (see [zig-self-hosting.md](zig-self-hosting.md)) —
|
||||
musl's syscall surface retargeted at danos system calls and IPC protocols (files onto
|
||||
the VFS protocol, `sigaction`/`wait` onto this lifecycle, sockets onto whatever
|
||||
networking becomes). Ported programs see POSIX; the system underneath never does.
|
||||
|
||||
## Why a standard vocabulary
|
||||
|
||||
A supervisor can only manage processes it has never heard of if "please exit" means
|
||||
the same thing to all of them. That is the one thing POSIX signals got deeply right:
|
||||
`SIGTERM` means the same thing to nginx and to a five-line script, which is why
|
||||
process supervision on Unix (init systems, container runtimes) is possible at all.
|
||||
danos wants that property from day one, because supervision-and-restart is the
|
||||
system's core motivation ([resilience.md](resilience.md)).
|
||||
|
||||
What POSIX got wrong — for a system like this — is the **delivery mechanism**:
|
||||
asynchronous control-flow hijack. A Unix handler runs on a stolen stack at an
|
||||
arbitrary instruction boundary, which is why the async-signal-safe function list
|
||||
exists, why `errno` must be saved, and why the canonical signal bug is a SIGTERM
|
||||
handler innocently calling `printf` mid-`malloc`. That entire bug class comes from
|
||||
the mechanism, not the vocabulary, and none of it is worth importing.
|
||||
|
||||
A microkernel already has the right channel: **a signal is a message.** QNX delivers
|
||||
POSIX signals over its message passing; seL4 has notification objects; Erlang turned
|
||||
"death is a message to whoever linked" into a reliability philosophy. danos has
|
||||
already done it once without naming it: a child's death arrives as a notification
|
||||
badge on the supervisor's endpoint — the microkernel's SIGCHLD, the IRQ-as-IPC
|
||||
pattern reused. Signals are the same pattern reused a third time.
|
||||
|
||||
## The mechanism
|
||||
|
||||
- **`signal_bind(endpoint)`** — a process nominates the endpoint its signals arrive
|
||||
on, exactly as `irq_bind` nominates where a device's interrupts land. The runtime
|
||||
does this at startup for any program that opts in.
|
||||
- **`process_signal(id, signal)`** — posts the signal as an asynchronous
|
||||
notification to the target's bound endpoint: badge = `notify_badge_bit |
|
||||
notify_signal_bit | pending signals`. Non-blocking for the sender, always.
|
||||
- **Pending signals coalesce** in a per-process bitmask until the target next waits
|
||||
— exactly like interrupt notifications, and exactly POSIX's own semantics for
|
||||
non-realtime signals (two pending SIGTERMs are one SIGTERM). The bitmask *is* the
|
||||
design: signals carry no payload. Anything with a payload is a protocol message.
|
||||
- **Authority**: the supervisor may signal its children — the same link that is
|
||||
already the kill authority. A process may signal itself. Anything broader waits
|
||||
for transferable process handles.
|
||||
- **No binding, no problem**: a process that never calls `signal_bind` is not
|
||||
broken — its signals pend unread and only `process_kill` works on it. Simple
|
||||
programs stay simple; the vocabulary is opt-in, the kill authority is not.
|
||||
|
||||
Because delivery is a message into the process's own event loop, there is no
|
||||
async-signal-safe list in danos: a handler is ordinary code running at a point the
|
||||
process chose. The bug class is gone by construction, not by discipline.
|
||||
|
||||
## The vocabulary: POSIX.1-1990, sorted honestly
|
||||
|
||||
The full 1990 set, and what each becomes. Two intrinsically problematic cases get a
|
||||
defense below the table.
|
||||
|
||||
| POSIX.1-1990 | danos disposition | Notes |
|
||||
|---|---|---|
|
||||
| SIGTERM | signal `terminate` | finish up and exit; the supervisor's polite half |
|
||||
| SIGHUP | signal `reload` | re-read configuration / re-scan |
|
||||
| SIGINT | signal `interrupt` | interactive interrupt; meaningful once a console can send it, in the vocabulary now so numbering is stable |
|
||||
| SIGQUIT | signal `quit` | as SIGINT, without the core-dump baggage |
|
||||
| SIGALRM | signal `alarm` | timer expiry as a message; the Unix SIGALRM+`longjmp` timeout hacks are impossible here. In the vocabulary, unbuilt: no consumer yet, and when one appears it is runtime sugar over the existing timer — zero kernel work |
|
||||
| SIGUSR1, SIGUSR2 | signals `user_1`, `user_2` | service-defined |
|
||||
| SIGCHLD | **already exists** — the exit notification | the badge carries the child id, dodging the classic coalescing bug (Unix code must loop `waitpid`) |
|
||||
| SIGKILL | `process_kill` — kernel mechanism | its definition is "cannot be handled"; it was never really a signal |
|
||||
| SIGABRT | exit reason `abort` | `abort()` is synchronous self-termination, not an event |
|
||||
| SIGSEGV, SIGILL, SIGFPE | exit reasons, **never delivered** | see below |
|
||||
| SIGPIPE | **an error return**, not a signal | see below |
|
||||
| SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU, SIGCONT | deferred | job control needs terminals, sessions, and process groups; stop/continue is scheduler territory |
|
||||
|
||||
**The fault signals (SIGSEGV, SIGILL, SIGFPE) are intrinsically wrong for messages.**
|
||||
They are *synchronous* — raised at a specific faulting instruction, not "sometime
|
||||
soon". A message cannot be delivered to a process whose next instruction re-faults;
|
||||
it never reaches its event loop to read it. POSIX only makes fault handlers "work"
|
||||
via the async hijack (run the handler *instead of* the instruction), and even there,
|
||||
returning from a SIGSEGV handler without curing the cause is undefined behavior.
|
||||
danos's architecture already has the better answer: fault → the kernel kills the
|
||||
process ([resilience.md](resilience.md) step 2, built) → the supervisor reads the
|
||||
reason → restart. Recovery is restart, not a handler. This is also truer to the 1990
|
||||
standard than handling is: the standard's default action for all three was
|
||||
"terminate the process".
|
||||
|
||||
**SIGPIPE deserves special contempt.** Its default kills a process that writes to a
|
||||
closed pipe — which is why "the whole server died because one client disconnected"
|
||||
is roughly every network daemon's first production bug, and why every mature codebase
|
||||
contains the same fix: ignore SIGPIPE, handle the `EPIPE` error return. danos made
|
||||
the right choice natively already — a reply owed to a dead peer fails with `-EPEER`.
|
||||
Errors from operations are error returns from those operations. The posix layer can
|
||||
synthesize SIGPIPE for ported code that expects it.
|
||||
|
||||
### Statements, not questions
|
||||
|
||||
A signal and a protocol message both travel over IPC — the difference is the
|
||||
**contract**, not the transport. danos IPC has two primitives, both already in
|
||||
daily use: the **asynchronous notification** (a badge — bits that coalesce into a
|
||||
pending mask; the sender never blocks; no payload, *no reply path*; how IRQs and
|
||||
exit events arrive) and the **synchronous call** (a rendezvous — payload both
|
||||
ways, the caller waits for the reply; how VFS requests work). A signal is the
|
||||
first kind: a *statement*. `terminate` wants no reply — the exit notification is
|
||||
its acknowledgement.
|
||||
|
||||
A health probe is the second kind: a *question*, worthless without its answer —
|
||||
and the answer's absence within a deadline is the very thing being measured.
|
||||
Asked as a signal it has no reply channel (a coalescing bit can't carry an answer,
|
||||
and the authority rule forbids a child signalling its supervisor back); asked as a
|
||||
call, the timeout-is-the-diagnosis semantics come free. So there is no `health`
|
||||
signal. Liveness is the common **`ping`**: a reserved request every harness-run
|
||||
service answers automatically on its main endpoint — still free for the service
|
||||
author, still one obvious way — and a supervisor's probe is a `ping` call with a
|
||||
deadline.
|
||||
|
||||
## The two iron rules
|
||||
|
||||
1. **Cleanup is the kernel's job.** A process can die with no warning — fault,
|
||||
kill, power. Correctness must never depend on a `terminate` handler running. On
|
||||
any death the kernel releases the address space, IPC handles, IRQ bindings, and
|
||||
owed replies (built), and must also release **device, I/O-port, and interrupt
|
||||
claims and MSI vectors** (the known gap in
|
||||
[process-management.md](process-management.md); increment 1). A signal handler is
|
||||
for *graceful* work — flushing, deregistering, saving — never for *necessary*
|
||||
work.
|
||||
2. **Kill is not a signal, and exit reasons are load-bearing.** The standard stop
|
||||
sequence is *terminate → deadline → `process_kill`*; the unhandleable kill stays
|
||||
a kernel mechanism. And a supervisor deciding whether to restart must know *how*
|
||||
the child died: clean exit (meant to — don't restart), fault (restart with
|
||||
backoff), killed (the supervisor did it). The exit notification today carries
|
||||
only the id; it grows a reason. Restart policy cannot be written without it.
|
||||
|
||||
## Who learns of a death
|
||||
|
||||
A death has three audiences, and conflating them is how systems end up with either
|
||||
zombie state or privileged snooping:
|
||||
|
||||
1. **The supervisor** — gets the exit notification on the endpoint it gave at spawn
|
||||
(built), which grows the `ExitReason` (increment 2). The supervisor is the only
|
||||
audience that needs the *reason*, because it is the only one deciding whether to
|
||||
restart.
|
||||
2. **The peer owed a reply** — already built: a client that dies mid-request fails
|
||||
the server's reply with `-EPEER`; a server that dies fails its waiting clients
|
||||
the same way. This covers the *synchronous* case only.
|
||||
3. **The subscribers** — the new piece, and it is the input service's
|
||||
publish/subscribe shape ([input.md](input.md)) applied to exits. A stateful
|
||||
service accumulates per-client state across many requests: the VFS holds a dead
|
||||
client's open file handles, the input service holds its subscriptions, a future
|
||||
network stack holds its sockets. None of these are the client's supervisor, and
|
||||
none learn anything from a failed reply if the client simply never calls again.
|
||||
So the kernel **publishes every exit** to whoever subscribed:
|
||||
`process_subscribe(endpoint)` adds a subscriber, and each death posts a
|
||||
notification to every subscriber (badge = `notify_exit_bit | process id` — the
|
||||
same encoding supervisors already decode, the IRQ-as-IPC pattern once more). The
|
||||
subscriber filters for ids it holds state for and releases what the dead client
|
||||
held. Correlating is free of bookkeeping: an IPC sender's badge already *is* its
|
||||
task id (`runtime.ipc.Received`), so the id a service has been keying client
|
||||
state by all along is the id the exit event carries.
|
||||
|
||||
Subscription, not broadcast-to-everyone: only processes that asked receive
|
||||
events, the kernel keeps a bounded subscriber table, and delivery is the same
|
||||
non-blocking coalescing notification as everything else — a dying process never
|
||||
waits on its mourners. Subscribing is ungated, like `process_enumerate`: what is
|
||||
running (and dying) is not a secret between cooperating processes. Subscribers
|
||||
do not receive the exit reason — the VFS does not care *why* the client died.
|
||||
|
||||
This is the service-side mirror of iron rule 1: **a service must never depend on
|
||||
its clients cleaning up after themselves.** Handle release on client death is the
|
||||
service's job, triggered by the published exit event — never by a courtesy
|
||||
"closing now" message that a crashed client will never send.
|
||||
|
||||
## The stable interface: `runtime.process`
|
||||
|
||||
`runtime.process` already owns what a process receives at birth (`Init`, the
|
||||
argv contract). It grows to own the other end of life.
|
||||
|
||||
**The runtime is the stable interface; the numbers are not.** danos applications do
|
||||
not make system calls — they call the runtime library, and the system-call numbers,
|
||||
notification bits, and signal bit positions beneath it are a **private kernel ↔
|
||||
runtime contract** that may change at any time (settled 2026-07-12). This is why
|
||||
the runtime exists. Today kernel and runtime ship from one tree in one image, so
|
||||
"stability" is simply building them together. When driver binaries start shipping
|
||||
as separately-versioned applications — the whole point of the restart design — the
|
||||
binary's embedded runtime version becomes compatibility metadata (the same idea as
|
||||
the protocol version in the device manager's `hello`), and the kernel refuses what
|
||||
it cannot serve. Signals therefore need no reserved numbering scheme: the enum
|
||||
below is vocabulary, not ABI.
|
||||
|
||||
```zig
|
||||
/// The signal vocabulary. The value is the bit position in the pending mask — a
|
||||
/// private kernel/runtime detail, free to change while they ship together.
|
||||
pub const Signal = enum(u5) {
|
||||
terminate = 0, // SIGTERM: finish up and exit
|
||||
reload = 1, // SIGHUP: re-read configuration
|
||||
interrupt = 2, // SIGINT
|
||||
quit = 3, // SIGQUIT
|
||||
alarm = 4, // SIGALRM
|
||||
user_1 = 5, // SIGUSR1
|
||||
user_2 = 6, // SIGUSR2
|
||||
};
|
||||
|
||||
/// A decoded pending mask: the coalesced set of signals a notification delivered.
|
||||
pub const SignalSet = struct {
|
||||
pending: u32,
|
||||
pub fn has(set: SignalSet, signal: Signal) bool { ... }
|
||||
pub fn iterate(set: SignalSet) Iterator { ... }
|
||||
};
|
||||
|
||||
/// Nominate `endpoint` as this process's signal endpoint (signal_bind). The
|
||||
/// runtime's service harness calls this; a bare program may call it directly and
|
||||
/// fold signals into its own replyWait loop.
|
||||
pub fn bindSignals(endpoint: usize) bool { ... }
|
||||
|
||||
/// Decode a received badge into signals, or null if the badge is not a signal
|
||||
/// notification (mirrors ipc.Received.isChildExit).
|
||||
pub fn signalsFrom(badge: usize) ?SignalSet { ... }
|
||||
|
||||
/// Send `signal` to process `id`. Supervisor-gated, like kill; non-blocking.
|
||||
pub fn sendSignal(id: u32, signal: Signal) bool { ... }
|
||||
|
||||
/// The standard stop sequence: terminate, wait up to `deadline_ms` for the exit
|
||||
/// notification, then process_kill. The one call a supervisor needs.
|
||||
pub fn stop(id: u32, deadline_ms: u64) void { ... }
|
||||
|
||||
/// Subscribe `endpoint` to published exit events (process_subscribe). Every
|
||||
/// process death posts an asynchronous notification: badge = notify_exit_bit |
|
||||
/// process id — the same encoding a supervisor's exit notification uses, decoded
|
||||
/// by the same ipc.Received helpers. For stateful services: release what the dead
|
||||
/// client held (file handles, subscriptions, sockets). Ungated, like
|
||||
/// process_enumerate.
|
||||
pub fn subscribeExits(endpoint: usize) bool { ... }
|
||||
|
||||
/// How a process ended — queried after the exit notification (the kernel records
|
||||
/// it first, so the two never race). What restart policy reads. (Built in M17.2.)
|
||||
pub const ExitReason = enum(u8) {
|
||||
exited, // returned from main / clean exit
|
||||
aborted, // abort() — deliberate self-termination (SIGABRT's ghost; reserved)
|
||||
segmentation_fault, // SIGSEGV's ghost
|
||||
illegal_instruction, // SIGILL's ghost
|
||||
arithmetic_fault, // SIGFPE's ghost
|
||||
protection_fault, // general protection fault
|
||||
fault, // any other CPU exception
|
||||
killed, // process_kill
|
||||
};
|
||||
```
|
||||
|
||||
Two deliberate absences. There is no `mask`/`block` API — a process that is not
|
||||
ready for a signal simply has not waited on its endpoint yet; the pending mask *is*
|
||||
the blocked set. And there is no per-signal handler registration at this layer —
|
||||
dispatch is the process's own `switch` over `SignalSet`, or the service harness's
|
||||
callbacks (`on_terminate`, `on_reload`) for programs that want defaults.
|
||||
|
||||
### The service harness
|
||||
|
||||
`runtime.service` owns the `replyWait` loop and folds every event source — signals,
|
||||
child exits, protocol messages — into callbacks, with the vocabulary's defaults:
|
||||
`terminate` returns from the loop (clean exit), the common `ping` is answered automatically,
|
||||
`reload` is ignored unless overridden. One loop, no locking, nothing reentrant. A
|
||||
service author writes domain logic; the lifecycle contract is satisfied by the
|
||||
harness. A process that bypasses the harness and ignores its signals meets the
|
||||
deadline-then-kill escalation — you cannot force a process to implement an
|
||||
interface, but you can make compliance free and non-compliance fatal.
|
||||
|
||||
### The musl layer later
|
||||
|
||||
The POSIX C layer is a **musl port**: musl's arch/syscall layer retargeted so that
|
||||
what musl believes are kernel syscalls become danos runtime calls and IPC — `open`
|
||||
and `read` onto the VFS protocol, `kill`/`sigaction`/`waitpid` onto this document's
|
||||
vocabulary, `exit` onto the runtime's exit path. `sigaction` handlers registered
|
||||
through it are invoked by the runtime's loop when the signal message arrives —
|
||||
synchronous underneath, async-looking to ported code, delivered at wait boundaries
|
||||
the way most Unix programs already experience signals (at syscalls). No stack hijack
|
||||
ever happens, `SA_RESTART` semantics come free because nothing was interrupted, and
|
||||
SIGPIPE can be synthesized from `-EPEER` for the programs that expect it. C programs
|
||||
get POSIX; danos-native programs never pay for it.
|
||||
|
||||
## Increments
|
||||
|
||||
1. **Kernel: release device/port/IRQ claims and MSI vectors on death** — the
|
||||
cleanup half of iron rule 1, and the prerequisite for any restart story. Test:
|
||||
kill a claiming driver, spawn it again, the claim succeeds.
|
||||
2. **Exit reason in the death notification** (`ExitReason` above).
|
||||
3. **Exit events**: `process_subscribe` in the kernel (bounded subscriber table,
|
||||
publishes on every death), `runtime.process.subscribeExits`; the VFS becomes the
|
||||
first subscriber — releasing a dead client's handles is its proof test.
|
||||
4. **Signals**: `signal_bind` + `process_signal` + the pending mask in the kernel;
|
||||
`runtime.process` grows the interface above; the service harness handles
|
||||
`terminate` and answers the common `ping`; `stop()` for supervisors.
|
||||
|
||||
[device-manager.md](device-manager.md) builds directly on all four.
|
||||
|
||||
## Settled questions (2026-07-12)
|
||||
|
||||
- **Signal numbering is not ABI**: the runtime is the stable interface; the numbers
|
||||
beneath it are a private kernel ↔ runtime contract (see "The stable interface").
|
||||
- **Liveness is a `ping` call, not a signal**: signals are statements, questions
|
||||
are synchronous calls (see "Statements, not questions"). A service wanting *deep*
|
||||
health ("can I reach my hardware?") defines its own protocol message on top.
|
||||
- **Process handles: deferred.** Pids + the supervisor gate cover everything
|
||||
planned; transferable handles (Fuchsia-style, delegating signalling without
|
||||
delegating kill) wait for the capability table to grow types beyond endpoints.
|
||||
- **`alarm`: in the vocabulary, unbuilt.** No consumer yet; when one appears it is
|
||||
runtime sugar over the existing timer (arm a timer that posts your own signal) —
|
||||
zero kernel work, so deferring costs nothing.
|
||||
- **Subscription granularity: all exits**, subscriber-side filtering — one
|
||||
subscription per service, a bounded kernel table. Per-id subscriptions only if
|
||||
event volume ever matters (hundreds of processes, not before).
|
||||
- **Client identity across the exit boundary: no convention needed** — an IPC
|
||||
sender's badge already is its task id (see "Who learns of a death").
|
||||
@@ -0,0 +1,120 @@
|
||||
# Process Management
|
||||
|
||||
How danos lists, supervises, and kills processes — the microkernel answer to
|
||||
`ps`, `kill`, and `SIGCHLD`/`wait`.
|
||||
|
||||
## Why system calls, not `/proc`
|
||||
|
||||
Unix systems sit on a spectrum. Classic BSD/macOS list processes through
|
||||
syscalls (`sysctl(KERN_PROC)`) and kill through `kill(2)`; Linux renders the
|
||||
process table as `/proc` for *reading* but still kills through a syscall; Plan 9
|
||||
made the file tree the whole interface (`echo kill > /proc/n/ctl`). Microkernels
|
||||
mostly abandon ambient PIDs: Minix and QNX route everything through a user-space
|
||||
process-manager server, and Fuchsia/seL4 control processes only through handles.
|
||||
|
||||
danos rules out `/proc` **as the primitive**: here a `/proc` would be served by
|
||||
the VFS server — a user process — which would put the VFS in the path of process
|
||||
control. If the VFS (or anything under it) hangs, nothing could be listed or
|
||||
killed, *including the hung VFS*. The control plane for processes must not
|
||||
depend on a process. So the primitives are kernel system calls; a read-only
|
||||
`/proc` rendering can be layered on later, and a POSIX-style process-manager
|
||||
server can be built *from* these primitives when one is needed.
|
||||
|
||||
## The three primitives
|
||||
|
||||
### `process_enumerate(buffer, maximum) -> total`
|
||||
|
||||
A snapshot of the task table into a caller buffer of `abi.ProcessDescriptor`
|
||||
(id, supervisor, state, priority, name) — the exact shape of
|
||||
`device_enumerate`, so `ps` is a user program over a snapshot, not a kernel
|
||||
service. The total may exceed what fit; call again with a larger buffer. Kernel
|
||||
tasks are included with an empty name — an honest listing shows the idle tasks
|
||||
too. Ungated and read-only: what is running is not a secret between cooperating
|
||||
bring-up processes.
|
||||
|
||||
### `system_spawn(..., exit_endpoint) -> child id`, and the supervision link
|
||||
|
||||
`system_spawn` records the caller as the child's **supervisor** and returns the
|
||||
child's process id (ids are monotonic, never reused — a stale id can only miss).
|
||||
That link is the kill authority: it answers "who may kill process 7?" without
|
||||
inventing users or permissions, the same way a device *claim* is the capability
|
||||
for `mmio_map`. It composes with the supervision hierarchy the device manager
|
||||
already forms: init supervises the services it starts, the device manager
|
||||
supervises the drivers it matches. (A transferable process *handle* — Fuchsia
|
||||
style — can replace the id once the handle table grows types beyond endpoints.)
|
||||
|
||||
`exit_endpoint` (a handle, or `abi.no_cap`) is the supervisor's death-watch: when
|
||||
the child ends — clean exit, CPU fault, or `process_kill` — the kernel posts an
|
||||
asynchronous notification to that endpoint, exactly like a bound IRQ. The badge
|
||||
carries `abi.notify_badge_bit | abi.notify_exit_bit | child_id`, so one endpoint
|
||||
supervises many children and can even share with IRQ notifications. This is the
|
||||
microkernel's SIGCHLD: no new mechanism, just the IRQ-as-IPC pattern reused, and
|
||||
a supervisor's event loop (`ipc.replyWait`) already knows how to receive it. The
|
||||
child holds a reference to the endpoint from birth, so the notification cannot
|
||||
dangle even if the supervisor dies first.
|
||||
|
||||
### `process_kill(id) -> 0 / -ESRCH / -EPERM`
|
||||
|
||||
Only the supervisor may kill; kernel tasks are not killable processes. Like a
|
||||
signal, delivery is prompt but asynchronous — 0 means the kill is accepted and
|
||||
irrevocable; the exit notification confirms completion.
|
||||
|
||||
## How a kill lands (the kernel mechanics)
|
||||
|
||||
Everything below runs under the big kernel lock, where task states cannot move.
|
||||
|
||||
- **Target ready or blocked** (not on any core): reaped on the killer's own
|
||||
call. The reap releases what death always releases (IRQ bindings first, then
|
||||
a client the target still owed a reply to is failed with `-EPEER`, IPC handles
|
||||
closed, the exit notification posted last) — plus the unlinking only a
|
||||
*remote* death needs: out of the ready queue, out of an endpoint's sender FIFO
|
||||
(`Task.ipc_wait_endpoint`), out of a receive wait queue (`Task.wait_queue`),
|
||||
and out of any server's owed-reply slot, so nothing ever dequeues a dangling
|
||||
pointer. Destroying the address space is safe because no core can have it
|
||||
loaded: every switch away from a task loads the next task's tables.
|
||||
- **Target running on another core**: it cannot be torn down mid-instruction,
|
||||
so it is condemned (`Task.kill_pending`) and dies at whichever comes first:
|
||||
- its next **system_call entry** — checked before dispatch, so a condemned
|
||||
process cannot spawn, claim, or message anything on its way out;
|
||||
- its core's next **timer tick** — but only when the task is not inside one
|
||||
of its own system calls (`Task.in_system_call`): the tick may have
|
||||
interrupted kernel code mid-operation, where teardown would leak whatever
|
||||
the operation held. User-mode execution is always a safe kill point. The
|
||||
tick-time terminate abandons the interrupt frame exactly like the fault
|
||||
path (the LAPIC is acknowledged before the tick hook runs);
|
||||
- any core's tick finding it **blocked or ready** (it entered a syscall and
|
||||
parked after being condemned) — reaped by the same remote-reap path.
|
||||
|
||||
A pure user-mode spin loop that never makes a system call therefore dies
|
||||
within one tick; nothing a process does can outrun the kill.
|
||||
|
||||
The scheduler stays below the process layer: finishing a kill (IRQ bindings,
|
||||
handles, the notification) is called *up* through two hooks process.zig
|
||||
registers at boot (`terminate_current_hook`, `reap_task_hook`), mirroring how
|
||||
the architecture layer calls up into `tick`.
|
||||
|
||||
## Known gaps (bring-up honesty)
|
||||
|
||||
- ~~Device claims are not released on death~~ Closed (M17.1): every path out of a
|
||||
process releases its device claims alongside its IRQ and MSI bindings
|
||||
(`releaseTaskResourcesLocked`), so a restarted driver can claim its hardware
|
||||
again — the cleanup half of [process-lifecycle.md](process-lifecycle.md)'s iron
|
||||
rule 1. The `claim-release` test proves the kill → release → re-claim cycle.
|
||||
- Kernel stacks of dead tasks are leaked, as on every exit path (no reaper yet).
|
||||
- ~~There is no exit status in the notification~~ Closed (M17.2): the kernel
|
||||
records how every process ends — exited, a fault class, or killed — before it
|
||||
posts the exit notification, and the supervisor reads it with
|
||||
`process_exit_reason` (`runtime.process.exitReason`). This is the input to
|
||||
restart policy ([process-lifecycle.md](process-lifecycle.md)); an exit *code*
|
||||
for the clean case can still ride alongside later.
|
||||
- Enumerate writes through the caller's raw pointer under the bring-up trust
|
||||
model, like `device_enumerate` (an unmapped page is a self-DoS, not an
|
||||
isolation break).
|
||||
|
||||
## Tests
|
||||
|
||||
`process-list` (enumerate), `process-kill` (kernel-level kill paths, refusals,
|
||||
notifications), `supervision` (the whole user-side surface via the process-test
|
||||
service: spawn supervised → enumerate → kill blocked and spinning children →
|
||||
notifications → gone), `claim-release` (a killed claim-holder's device is
|
||||
claimable again). See test/qemu_test.py.
|
||||
+16
-3
@@ -1,6 +1,17 @@
|
||||
# Resilience: fault isolation and live restart
|
||||
|
||||
A design/research note, not built yet. This is the property danos is really chasing:
|
||||
Steps 1–4 of the ordering below are **built** (M17–M18, 2026-07-13): user-mode
|
||||
isolation; fault → kill the process → keep the core (`onException`; the
|
||||
`fault-recovery` test); the supervisor notification **with exit reasons**
|
||||
([process-lifecycle.md](process-lifecycle.md) — clean exit, fault class, or
|
||||
killed, recorded before the notice posts); and the **restart policy itself**
|
||||
([device-manager.md](device-manager.md)): the device manager supervises every
|
||||
driver, restarts crashes with backoff, caps crash loops, and re-claims work
|
||||
because the kernel releases a dead process's claims. The `driver-restart` and
|
||||
`usb-report` scenarios prove kill → release → respawn → re-claim → re-report
|
||||
end to end. What remains of this document's ladder is scope, not mechanism:
|
||||
more of the system moved into restartable processes (the discovery migration,
|
||||
[discovery.md](discovery.md), is the next rung). This is the property danos is really chasing:
|
||||
**if a part of the OS breaks, isolate it, and re-initialise it — without rebooting.**
|
||||
A crashed driver gets restarted; a wedged service gets killed and brought back. It's
|
||||
the reason the [microkernel](vision.md) shape was chosen, and it's a *separate* goal
|
||||
@@ -111,9 +122,11 @@ Honest boundaries:
|
||||
## Suggested ordering
|
||||
|
||||
1. **User mode + address-space isolation** — the shared prerequisite (also on the
|
||||
path for everything else).
|
||||
path for everything else). **Done.**
|
||||
2. **Kernel: fault → kill process → notify.** Turn today's "halt on fault" into
|
||||
"confine to the process and report it."
|
||||
"confine to the process and report it." **Done** (the kill and reclaim; the
|
||||
supervisor notification waits for step 3's supervisor). A killed server's
|
||||
pending client is unblocked with `-EPEER` rather than hung.
|
||||
3. **A minimal supervisor server** that can (re)start a process.
|
||||
4. **Resource cleanup on death** — reclaim memory/MMIO/IPC/IRQ, via caps or a grant
|
||||
table.
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ next lands.
|
||||
[scheduling.md](scheduling.md#affinity-pinning-a-task-to-a-core)). The `affinity`
|
||||
test confirms a pinned task never migrates. This is the mechanism the fault-on-AP
|
||||
test rides on, and the *explicit-affinity* real-time-predictable model.
|
||||
- **Right-sized footprint** — the per-CPU ceiling (`danos.max_cpus`, one constant
|
||||
- **Right-sized footprint** — the per-CPU ceiling (`system.max_cpus`, one constant
|
||||
shared by discovery, the scheduler, and the per-core GDT/TSS) is generous (128), but
|
||||
the *large* per-core resources — the kernel and IST (double-fault) stacks — are
|
||||
**heap-allocated at bring-up**, only for cores that actually come online. Only the
|
||||
|
||||
@@ -47,6 +47,8 @@ Everything else---including`read()`,`write()`,`malloc()`, and`fork()`---will run
|
||||
- **What it does:**Used strictly by your background user-space servers (like your disk driver or filesystem). It sends a reply to the last client that called it, and immediately puts the server to sleep until the next request arrives.[[1](https://news.ycombinator.com/item?id=33078441)]
|
||||
3. **`Yield()`/`Thread_Ctrl()`**
|
||||
- **What it does:**Allows a thread to voluntarily give up its CPU time slice, or allows a root task to spawn/kill threads.
|
||||
4. **`ipc_send(endpoint, message_buffer)`(Asynchronous Send)**
|
||||
- **What it does:**Posts a small payload to an endpoint's bounded queue and returns *without* blocking — no rendezvous, no reply. The receiver picks it up through the same `IPC_ReplyWait`, as a buffered message. It is the async counterpart of `IPC_Call`, for one-to-many broadcasts where a synchronous rendezvous would let one dead or slow receiver hang the sender. The [input service](input.md) — keyboard-event fan-out — is its first user. A full queue drops the oldest message (a buffered message is discrete data, unlike a coalescing interrupt notification).
|
||||
|
||||
* * * * *
|
||||
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# System Requirements
|
||||
|
||||
Minimum and recommended hardware for running danos. Every requirement below is
|
||||
grounded in what the current code actually assumes at boot — this is a
|
||||
description of the real target, not an aspirational one.
|
||||
|
||||
## Summary
|
||||
|
||||
danos targets a **modern UEFI x86-64 PC with ACPI and PCIe**. The practical
|
||||
minimum is:
|
||||
|
||||
- 64-bit x86-64 CPU with SSE2, APIC, and `syscall`/`sysret`
|
||||
- UEFI firmware (no BIOS / legacy boot)
|
||||
- ACPI tables: MADT, MCFG, FADT
|
||||
- PCIe with an ECAM (MMConfig) window
|
||||
- **128 MiB RAM** (target); see [Memory](#memory) for the breakdown
|
||||
- USB via **xHCI only**
|
||||
|
||||
There is no support for legacy BIOS boot, x2APIC, port-IO PCI configuration, or
|
||||
any USB host controller other than xHCI.
|
||||
|
||||
## Plain-language hardware guide
|
||||
|
||||
If you don't want to cross-reference chipset datasheets, here's roughly what era
|
||||
of PC works. These are **guidance based on when the required features became
|
||||
standard**, not a list of tested machines — the authoritative rules are in the
|
||||
technical sections below.
|
||||
|
||||
The feature that sets the floor is **built-in xHCI USB** (danos supports no other
|
||||
USB controller) combined with **UEFI firmware**. Both became standard on
|
||||
mainstream desktops and laptops around **2012**.
|
||||
|
||||
| | Known-good baseline | Comfortable recommendation |
|
||||
|---|---|---|
|
||||
| **Intel** | 3rd-gen Core "Ivy Bridge" (2012) with a 7-series "Panther Point" chipset — Intel's first chipset with xHCI built in | 6th-gen Core "Skylake" (2015) or newer |
|
||||
| **AMD** | A-series "Llano" APU with an A75 FCH (2011) — the industry's first chipset with built-in xHCI | Any AM4 platform, i.e. Ryzen (2017) or newer |
|
||||
|
||||
**AMD is not behind Intel here — it was first.** AMD's A75 FCH shipped with
|
||||
native xHCI in April 2011, about a year *ahead* of Intel's 7-series (2012); AMD
|
||||
was the first vendor to earn USB-IF certification for chipset-level USB 3.0. The
|
||||
two "comfortable recommendation" dates differ only because they name convenient,
|
||||
long-supported product lines (Skylake, Ryzen) — not because of any USB
|
||||
capability gap. Every AMD desktop platform from the A75 FCH (2011) and FM2/AM3+
|
||||
era onward has built-in xHCI, and any of them qualifies as a baseline.
|
||||
|
||||
Older 64-bit machines (e.g. Intel Core 2, Nehalem, Sandy Bridge) meet the CPU
|
||||
requirements but typically **lack built-in xHCI and/or ship with BIOS instead of
|
||||
UEFI**, so they are not supported.
|
||||
|
||||
### Matching your CPU by name
|
||||
|
||||
If you know your chip's marketing name or codename, find it here. Everything from
|
||||
the **Supported** rows down works; the **Too old** row does not.
|
||||
|
||||
**Intel Core** (the "-lake"/"-bridge"/"-well" codenames):
|
||||
|
||||
| Status | Generation | Codename(s) | Year |
|
||||
|---|---|---|---|
|
||||
| Too old | 2nd gen | Sandy Bridge | 2011 |
|
||||
| Supported (baseline) | 3rd gen | Ivy Bridge | 2012 |
|
||||
| Supported | 4th–5th gen | Haswell, Broadwell | 2013–2014 |
|
||||
| **Recommended** | 6th–9th gen | **Skylake**, Kaby Lake, Coffee Lake | 2015–2018 |
|
||||
| Recommended | 10th–11th gen | Comet Lake, Ice Lake, Tiger Lake, Rocket Lake | 2019–2021 |
|
||||
| Recommended | 12th gen+ | Alder Lake, Raptor Lake | 2021–2023 |
|
||||
| Recommended | Core Ultra | Meteor Lake, Arrow Lake, Lunar Lake | 2023+ |
|
||||
|
||||
**AMD:**
|
||||
|
||||
| Status | Family | Codename(s) | Year |
|
||||
|---|---|---|---|
|
||||
| Supported (baseline) | A-series APU (A75/A85 FCH) | Llano, Trinity, Richland, Kaveri | 2011–2014 |
|
||||
| Supported | FX (AM3+) | Bulldozer, Piledriver | 2011–2012 |
|
||||
| **Recommended** | **Ryzen** 1000–5000 (AM4) | Summit/Pinnacle Ridge, Matisse, Vermeer (Zen–Zen 3) | 2017–2020 |
|
||||
| Recommended | Ryzen 7000+ (AM5) | Raphael, Granite Ridge (Zen 4 / Zen 5) | 2022+ |
|
||||
| Recommended | Threadripper / EPYC | Zen and later | 2017+ |
|
||||
|
||||
(These map generations to the era their platforms shipped built-in xHCI + UEFI;
|
||||
they are guidance, not a tested-hardware list.)
|
||||
|
||||
**Two caveats that matter regardless of CPU:**
|
||||
|
||||
- **Firmware must be UEFI.** Many 2011-era machines could do either UEFI or
|
||||
legacy BIOS — danos needs it set to UEFI. There is no BIOS boot path.
|
||||
- **Input is PS/2 only, for now.** danos does not yet support USB
|
||||
keyboards/mice. This is fine on most **laptops** (their built-in keyboards are
|
||||
wired to a PS/2-style i8042 controller) but means a **desktop with only USB
|
||||
ports** currently has no usable keyboard. USB HID input is planned.
|
||||
|
||||
Virtual machines are the easiest way to meet every requirement: QEMU (with OVMF/
|
||||
UEFI, a `qemu-xhci` controller, and the default Q35 machine type), or any
|
||||
hypervisor configured for UEFI firmware and an xHCI USB controller.
|
||||
|
||||
## CPU / architecture
|
||||
|
||||
| Requirement | Detail | Source |
|
||||
|---|---|---|
|
||||
| **x86-64, 64-bit only** | Kernel and loader are built exclusively for `x86_64`; the loader rejects any non-x86-64 kernel ELF (`error.WrongArchitecture`). | `build.zig:285`, `boot/efi.zig:418` |
|
||||
| **Long mode + PAE + NX** | AP trampoline sets `CR4.PAE`, `EFER.LME`, `EFER.NXE`; NX is used in kernel page-table entries. | `system/kernel/architecture/x86_64/trampoline.s:62` |
|
||||
| **SSE / SSE2** | Baseline: the compiler emits SSE for ordinary struct copies. Trampoline enables `CR4.OSFXSR` + `OSXMMEXCPT` and clears `CR0.EM`. | `build.zig:282`, `trampoline.s:62` |
|
||||
| **`syscall` / `sysret`** | Primary user↔kernel entry path. `EFER.SCE` enabled; `STAR`/`LSTAR`/`SFMASK` programmed per core. (`int 0x80` exists as a parallel gate.) | `architecture/x86_64/per-cpu.zig:59`, `isr.s:169` |
|
||||
| **Local APIC (xAPIC)** | LAPIC accessed via MMIO at `0xFEE00000`. LAPIC ID read as a `u8` — classic xAPIC. **x2APIC is not supported** (no MSR path). | `apic.zig:62`, `apic.zig:414` |
|
||||
| **CPUID + RDTSC** | CPUID leaf `0x15` for TSC frequency; RDTSC is the monotonic clock. | `apic.zig:279`, `apic.zig:84` |
|
||||
| **SMP (optional)** | Multi-core supported via INIT–SIPI–SIPI; ceiling `maximum_cpus = 128`. Single core is fine. Cores beyond the ceiling are parked. | `system/parameters.zig:16`, `apic.zig:144` |
|
||||
|
||||
## Firmware / boot
|
||||
|
||||
- **UEFI only.** A custom UEFI application loader is installed to
|
||||
`\EFI\BOOT\BOOTX64.efi`. There is **no BIOS, multiboot, or limine** path. The
|
||||
loader tolerates UEFI Class-3 machines with no legacy PIC/PIT.
|
||||
(`build.zig:464`, `boot/efi.zig`)
|
||||
- **ACPI is the hardware-discovery mechanism.** The RSDP is taken from the UEFI
|
||||
configuration table (ACPI 2.0 GUID preferred, 1.0 fallback). Without a valid
|
||||
RSDP there is **no device discovery** — no SMP, no IOAPIC routing, no PCI/USB.
|
||||
(`efi.zig:578`, `boot-handoff.zig:144`)
|
||||
- **Required ACPI tables:** MADT (interrupt topology), MCFG (PCIe ECAM base),
|
||||
FADT (power / PM timer). Optionally consumed: HPET, DMAR, SPCR.
|
||||
(`system/devices/acpi.zig:3`)
|
||||
- The loader reads `/system/kernel`, `/system/services/init`, and
|
||||
`/boot/initial-ramdisk.img` off the FAT boot volume. The kernel can boot
|
||||
"kernel-only" without init or the ramdisk. (`efi.zig:14`, `efi.zig:66`)
|
||||
|
||||
## Interrupt controller
|
||||
|
||||
- **Local APIC + I/O APIC required.** I/O APIC base, GSI base, and MADT
|
||||
interrupt-source overrides come from ACPI. (`cpu.zig:365`, `apic.zig:119`)
|
||||
- **MSI supported** — edge-triggered, keyed by vector, no I/O APIC mask cycle.
|
||||
Vector window 33–46, timer on 32, spurious on 47. (`system/kernel/irq.zig:70`,
|
||||
`cpu.zig:397`)
|
||||
- The legacy 8259 PIC is remapped and masked **only if present** (MADT
|
||||
`PCAT_COMPAT`); it is not required. (`apic.zig:103`)
|
||||
|
||||
## PCI / PCIe
|
||||
|
||||
- **PCIe with ECAM (MMConfig) required.** The PCI bus driver maps the host
|
||||
bridge's ECAM window (1 MiB config space per bus) and computes config
|
||||
addresses directly. **There is no legacy CF8/CFC port-IO config path** — the
|
||||
driver bails if the bridge exposes no ECAM window. The ECAM base comes from
|
||||
the ACPI MCFG table. (`system/drivers/pci-bus/pci-bus.zig:41`, `acpi.zig:6`)
|
||||
|
||||
## USB
|
||||
|
||||
- **xHCI only.** The sole USB driver is `usb-xhci-bus`, and the device manager
|
||||
binds it strictly to PCI prog-IF `0x30` (xHCI). UHCI / OHCI / EHCI exist only
|
||||
as report strings with no driver behind them — **USB 1.x/2.0-only controllers
|
||||
are not supported.** (`system/drivers/usb-xhci-bus/`,
|
||||
`system/services/device-manager/device-manager.zig:34`)
|
||||
- USB input (keyboard/mouse over HID) is future work; the current input stack is
|
||||
PS/2. See [Buses & devices](#buses--devices).
|
||||
|
||||
## Timers
|
||||
|
||||
Calibration prefers, in order: (1) CPUID leaf `0x15` TSC frequency, (2) HPET,
|
||||
(3) ACPI PM timer (3.579545 MHz, from FADT), (4) legacy PIT. Any one suffices —
|
||||
HPET/PM-timer/PIT are optional fallbacks when CPUID `0x15` is absent.
|
||||
(`apic.zig:180`)
|
||||
|
||||
- **TSC** — monotonic high-resolution clock.
|
||||
- **LAPIC timer** — scheduler heartbeat, periodic at `timer_hz = 1000 Hz`.
|
||||
(`parameters.zig:39`)
|
||||
|
||||
## Memory
|
||||
|
||||
**Target: 128 MiB RAM.** The system uses 4 KiB pages and a bitmap physical-frame
|
||||
allocator built from the firmware memory map. There is no hardcoded minimum-RAM
|
||||
constant — the allocator only panics if there is no usable region, or none large
|
||||
enough to hold its own bitmap. (`system/kernel/pmm.zig:13`, `pmm.zig:77`)
|
||||
|
||||
Where the budget goes:
|
||||
|
||||
| Consumer | Size | Source |
|
||||
|---|---|---|
|
||||
| Kernel heap (cap, grown one page at a time) | up to **64 MiB** | `system/kernel/heap.zig:26` |
|
||||
| Kernel stack, per CPU | 16 KiB | `parameters.zig:26` |
|
||||
| IST stack, per CPU | 16 KiB | `parameters.zig:36` |
|
||||
| User stack, per task | 8 pages / 32 KiB | `parameters.zig:32` |
|
||||
| Max concurrent tasks | 32 | `parameters.zig:23` |
|
||||
| Boot page-table pool | 64 frames / 256 KiB | `efi.zig:299` |
|
||||
|
||||
The 64 MiB heap cap plus kernel image, per-CPU stacks, task stacks, the frame
|
||||
bitmap, and DMA-contiguous allocations fit comfortably within 128 MiB on a
|
||||
single- or low-core-count machine. Very high core counts (toward the 128-CPU
|
||||
ceiling) add per-CPU stack overhead and push toward more RAM.
|
||||
|
||||
**Note on the 4 GiB physmap:** the loader identity-maps and physmaps the low
|
||||
4 GiB of address space with 2 MiB leaves. This is *virtual address* reach, not a
|
||||
RAM requirement — RAM above 4 GiB simply needs an extra mapping window and is not
|
||||
needed to boot. (`efi.zig:305`)
|
||||
|
||||
Virtual-memory layout (`boot-handoff.zig:47`):
|
||||
|
||||
| Region | Base |
|
||||
|---|---|
|
||||
| User space | `0x0000_7000_0000_0000` |
|
||||
| Kernel heap | `0xFFFF_8000_0000_0000` |
|
||||
| Physmap | `0xFFFF_8800_0000_0000` |
|
||||
| Kernel image | `0xFFFF_FFFF_8000_0000` |
|
||||
|
||||
## Buses & devices
|
||||
|
||||
Buses with real drivers today:
|
||||
|
||||
- **PCIe** via ECAM (`pci-bus`)
|
||||
- **xHCI USB** (`usb-xhci-bus`)
|
||||
- **PS/2** keyboard + mouse (`ps2-bus`) — the current input stack
|
||||
- **Serial UART** (16550/16450), configured from the ACPI SPCR table
|
||||
|
||||
**No storage driver exists yet.** AHCI / NVMe / IDE are named for reporting only;
|
||||
there is no block-device driver. Persistent storage is future work.
|
||||
|
||||
## IOMMU
|
||||
|
||||
**Detection only; enforcement deferred.** The ACPI DMAR table is parsed for the
|
||||
first VT-d DRHD unit and its capabilities are exposed via `PlatformInfo`
|
||||
(`iommu_present`, `iommu_base`, `iommu_version`). No DMA-remapping tables are
|
||||
programmed and no translation is enforced. An IOMMU is therefore **not required**
|
||||
and does not currently constrain devices. (`system/devices/acpi.zig:96`)
|
||||
|
||||
## What is explicitly NOT supported
|
||||
|
||||
- Legacy BIOS / multiboot / limine boot
|
||||
- 32-bit x86
|
||||
- x2APIC
|
||||
- Legacy port-IO (CF8/CFC) PCI configuration
|
||||
- Non-xHCI USB (UHCI / OHCI / EHCI)
|
||||
- Machines without ACPI (no device discovery)
|
||||
- Persistent storage (no AHCI / NVMe / IDE driver yet)
|
||||
- USB HID input (PS/2 only for now)
|
||||
+35
-3
@@ -1,6 +1,6 @@
|
||||
# SysV: the kernel's calling convention
|
||||
|
||||
Several places in danos say "the kernel is SysV" — most visibly `system/danos.zig`:
|
||||
Several places in danos say "the kernel is SysV" — most visibly `system/boot-handoff.zig`:
|
||||
|
||||
```zig
|
||||
pub const kernel_abi: std.builtin.CallingConvention = .{ .x86_64_sysv = .{} };
|
||||
@@ -59,12 +59,44 @@ danos's two binaries default to different conventions:
|
||||
When the loader jumps to the kernel passing the `BootInfo` pointer, both sides have
|
||||
to agree *which register that pointer lands in*. Left to their defaults, the loader
|
||||
would place it in RCX while the kernel looked in RDI — and the kernel would read
|
||||
garbage. So both sides reference the same `danos.kernel_abi` (SysV): the loader's
|
||||
garbage. So both sides reference the same `system.kernel_abi` (SysV): the loader's
|
||||
function-pointer type and the kernel's `_start` both carry
|
||||
`callconv(danos.kernel_abi)`, and the pointer reliably arrives in RDI. That is the
|
||||
`callconv(system.kernel_abi)`, and the pointer reliably arrives in RDI. That is the
|
||||
whole reason `kernel_abi` lives in the shared contract — see [efi.md](efi.md) for
|
||||
the handoff it governs.
|
||||
|
||||
## The process-entry stack (argc/argv)
|
||||
|
||||
The SysV ABI also fixes what a *fresh process* finds on its stack — and danos
|
||||
follows it, so its own runtime and any future C libc read arguments the same way.
|
||||
At the first user instruction, `rsp` is 16-byte aligned and points at (addresses
|
||||
growing upward):
|
||||
|
||||
```
|
||||
rsp → argc u64
|
||||
argv[0] … argv[argc-1] pointers into the strings area below
|
||||
NULL argv terminator
|
||||
NULL envp terminator (no environment yet)
|
||||
{AT_PAGESZ, page size} auxiliary vector
|
||||
{AT_NULL, 0} auxiliary-vector terminator
|
||||
argv string bytes NUL-terminated
|
||||
───────────────────────── stack top (stack_top_virtual)
|
||||
```
|
||||
|
||||
The kernel builds this block at the top of the process's stack — 8 pages (32 KiB,
|
||||
`parameters.user_stack_pages`) mapped RW+NX below a fixed top, with the page below
|
||||
them left unmapped as a **guard**, so a stack overflow faults (killing only that
|
||||
process) instead of silently corrupting the image
|
||||
(`buildEntryStack` in `system/kernel/process.zig`); `argv[0]` is always the path
|
||||
or initial-ramdisk name the process was spawned as, and `system_spawn`'s optional
|
||||
argument blob becomes `argv[1..]`. The runtime's `_start`
|
||||
(`library/runtime/start.zig`) hands the block to `rt_start`, which builds a
|
||||
`runtime.process.Init` from it and passes that to the program's `main`
|
||||
(`pub fn main(init: runtime.process.Init)`; a parameterless `main()` is also
|
||||
accepted). A C runtime's `crt0` would walk
|
||||
the identical layout unmodified — that's the compatibility being bought. The
|
||||
`args` test proves the round trip.
|
||||
|
||||
## Where else it surfaces
|
||||
|
||||
- **The red zone → `red_zone = false`.** `build.zig` disables the red zone for the
|
||||
|
||||
+3
-2
@@ -8,8 +8,9 @@ without a human staring at the screen.
|
||||
There are two layers:
|
||||
|
||||
- **Host unit tests** (`zig build test`) — for pure, platform-independent logic in
|
||||
the shared `danos` module (the handoff layout in `system/danos.zig`). These compile
|
||||
for the host and run natively.
|
||||
the shared contracts (`system/boot-handoff.zig`, `system/abi.zig`,
|
||||
`system/devices/device-abi.zig`), which also compile-checks the three-way split
|
||||
stays self-consistent. These compile for the host and run natively.
|
||||
- **QEMU integration tests** (`python3 test/qemu_test.py`) — boot the real kernel
|
||||
and check its behaviour. This is the interesting part.
|
||||
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
# Timers and time
|
||||
|
||||
Two different needs hide under the word "timer", and danos keeps them apart:
|
||||
|
||||
- **Reading the clock** — *what time is it?* A read of a free-running counter.
|
||||
- **Waiting** — *wake me in N milliseconds*, or *notify me when a deadline passes.*
|
||||
|
||||
Both are answered by the **kernel**, because the kernel already owns a timer: it has
|
||||
to, to preempt tasks. The LAPIC heartbeat and the calibrated TSC that back all of this
|
||||
are built in [device-interrupts.md](device-interrupts.md); the scheduler's blocking and
|
||||
wait queues are in [scheduling.md](scheduling.md). This page is about the surface a
|
||||
ring-3 program actually uses, and one deliberate absence: **there is no user-space time
|
||||
service.**
|
||||
|
||||
## Why time is a syscall, not a service
|
||||
|
||||
The tempting microkernel move is to put a timer *driver* in user space and have
|
||||
applications ask it for the time over IPC. For a **monotonic clock that is wrong** —
|
||||
reading `now()` should never cost an IPC round trip. The kernel is already holding the
|
||||
answer: it computes the current time every time it schedules, from the TSC, in a couple
|
||||
of instructions. Surfacing that as a system call is pure mechanism; routing it through a
|
||||
message to another process would be slower *and* redundant, and a device like the HPET
|
||||
(uncacheable MMIO reads) is a particularly bad thing to read on every `now()`.
|
||||
|
||||
This is the same conclusion every serious system reaches: Linux and Zircon read the
|
||||
counter in the vDSO, L4 exposes a clock field in a shared kernel page, seL4 reads the
|
||||
cycle counter directly. None of them make a clock read an IPC. danos makes it a syscall.
|
||||
|
||||
That "from the TSC" hides a portability question, because the TSC is only a valid clock
|
||||
when the CPU guarantees it is *invariant* and when every core's TSC is *synchronized*.
|
||||
danos checks both — the invariant-TSC CPUID bit (`0x80000007` EDX[8], set on Intel and
|
||||
AMD), and a cross-core "warp" check as the cores come up — and falls back to the HPET
|
||||
counter when either fails. So `now()` stays accurate on a real Intel box, a real AMD box,
|
||||
and inside a VM alike; only the source behind it differs. The mechanism is in
|
||||
[device-interrupts.md](device-interrupts.md).
|
||||
|
||||
So the timer hardware lives in the kernel, and there is **no `hpet` driver and no time
|
||||
server** to consume. (An earlier HPET driver existed only to *demonstrate* the driver
|
||||
model; that role now lives in [drivers.md](drivers.md), as documentation.) The one place
|
||||
a user-space time service *is* justified — **wall-clock / calendar time** — is discussed
|
||||
at the end; it is deliberately not built yet.
|
||||
|
||||
## The three system calls
|
||||
|
||||
Time and waiting are three entries in the small syscall table ([syscall.md](syscall.md)):
|
||||
|
||||
- **`clock` (#23)** → monotonic nanoseconds since boot. It only moves forward. Not
|
||||
wall-clock: no date, no timezone. Backed by `architecture.nanos()` (TSC, scaled with a
|
||||
128-bit intermediate so a long uptime can't overflow) — a few nanoseconds of
|
||||
resolution, and just an `rdtsc` plus a multiply.
|
||||
- **`sleep` (#3)** → block the caller for N milliseconds. The scheduler records a wake
|
||||
deadline and the tick sweep wakes it (`scheduler.sleep`).
|
||||
- **`timer_bind` (#31)** → arm a one-shot timer that, after N milliseconds, posts a
|
||||
**timer notification** to an IPC endpoint. Unlike `sleep` it does **not** block: a
|
||||
service can keep answering messages on the same endpoint while a deadline is pending.
|
||||
This is the timed wait that stop-sequence escalation, hello deadlines, and restart
|
||||
backoff are built from ([process-lifecycle.md](process-lifecycle.md),
|
||||
[device-manager.md](device-manager.md)).
|
||||
|
||||
The kernel's own scheduling timer (the LAPIC, vector 32) is never exposed to user space;
|
||||
programs read the TSC through `clock` and get timed wakeups through `sleep`/`timer_bind`,
|
||||
both riding the scheduler tick.
|
||||
|
||||
## `runtime.time` — the generic interface
|
||||
|
||||
Applications don't call the syscalls directly; they use `runtime.time`
|
||||
(`library/runtime/time.zig`), a thin `Instant`/`Duration` layer over them — an ergonomic
|
||||
front door, not new mechanism.
|
||||
|
||||
```zig
|
||||
const time = @import("runtime").time;
|
||||
|
||||
const start = time.now(); // Instant — monotonic
|
||||
doWork();
|
||||
const took = start.elapsed(); // Duration
|
||||
time.sleep(time.Duration.fromMillis(5)); // block ~5 ms
|
||||
|
||||
// A deadline delivered as a notification, so a service keeps serving meanwhile:
|
||||
_ = time.after(endpoint, time.Duration.fromMillis(200));
|
||||
```
|
||||
|
||||
- `Duration` is nanoseconds under the hood, with `fromNanos/fromMicros/fromMillis/
|
||||
fromSeconds` and `asNanos/asMillis`. `ceilMillis` rounds *up* to the kernel's
|
||||
millisecond granularity, so a sub-millisecond `sleep` never rounds down to zero and
|
||||
returns early. All arithmetic saturates rather than wraps.
|
||||
- `Instant` is a point on the monotonic clock: `since`, `elapsed`, `plus`, `reached` —
|
||||
built for deadline loops (`while (!deadline.reached()) …`).
|
||||
- `now()` / `monotonicNanos()` wrap `clock`. `available()` reports whether the clock is
|
||||
calibrated at all (the kernel returns 0 until the TSC frequency is known, so a caller
|
||||
that needs real time can treat 0 as "unavailable" rather than assume it advances).
|
||||
- `sleep(d)` wraps `sleep`; `spin(d)` busy-polls `now()` for the sub-millisecond delays
|
||||
the millisecond tick can't express; `after(endpoint, d)` wraps `timer_bind`.
|
||||
|
||||
The raw wrappers (`system.clock`, `system.sleep`, `system.timerOnce`) stay in
|
||||
`library/runtime/system.zig`; `runtime.time` is the layer meant for everyday use.
|
||||
|
||||
## Wall-clock time (not built)
|
||||
|
||||
Everything above is **monotonic**: elapsed time since boot, perfect for timeouts and
|
||||
measurement, useless for "what is the date?" Calendar time — a real-time clock, time
|
||||
zones, leap seconds — is genuinely a **user-space** concern, and it *is* the case a time
|
||||
service is for. It would be backed by an **RTC** driver (the CMOS real-time clock), not
|
||||
the HPET, and exposed as a `CLOCK_REALTIME`-style service alongside the monotonic
|
||||
syscall. It is deferred until something needs it; the monotonic clock the kernel already
|
||||
owns covers every current use.
|
||||
|
||||
## Verifying it
|
||||
|
||||
`runtime.time`'s `Instant`/`Duration` arithmetic has unit tests that run on the host:
|
||||
|
||||
```
|
||||
$ zig build test # includes library/runtime/time.zig
|
||||
```
|
||||
|
||||
End to end, the proof the clock is real is that it *advances*: read `now()`, `sleep` a
|
||||
`Duration`, read `now()` again, and the second reading is later — the kernel's timer
|
||||
driving a ring-3 program with no service in between.
|
||||
+2
-2
@@ -84,13 +84,13 @@ interrupts](interrupts.md), a [calibrated timer + ns clock](device-interrupts.md
|
||||
in-kernel [IPC channels](ipc.md), SMP (all cores scheduling, with affinity), a
|
||||
**higher-half kernel** with a physmap, and **user space**: per-process address
|
||||
spaces, `syscall`/`sysret` with the `swapgs` discipline, a user-ELF loader, and
|
||||
`/sbin/init` — a real user ELF built from `sbin/`, running at CPL 3 as PID 1 on its
|
||||
`/system/services/init` — a real user ELF built from `system/services/init/`, running at CPL 3 as PID 1 on its
|
||||
own page tables — plus a [test harness](testing.md).
|
||||
|
||||
- **Isolation track** — **user mode + address-space isolation**. *Done: a
|
||||
higher-half kernel with a physmap (the low half is user space), per-process
|
||||
address spaces with CR3 switched on context switch, the `swapgs` discipline,
|
||||
`syscall`/`sysret`, a user-ELF loader, and `/sbin/init` running as a real
|
||||
`syscall`/`sysret`, a user-ELF loader, and `/system/services/init` running as a real
|
||||
preemptive ring-3 process (PID 1). Remaining polish: an address-space/stack
|
||||
reaper for exited tasks, SMAP + fault-recovering copy-in/out, the real IPC
|
||||
syscalls (IPC_Call/IPC_ReplyWait — they arrive with the second user server),
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
# Running Zig on danos: the self-hosting roadmap
|
||||
|
||||
A design note (not built yet) on the path to making danos a **real Zig target** — a
|
||||
target you can name (`-target x86_64-danos`) and, eventually, run the Zig compiler
|
||||
itself on. It is forward-looking, like [vision.md](vision.md): it sets a direction
|
||||
and the decisions that follow from it, so the code we write now bends toward it
|
||||
instead of away.
|
||||
|
||||
This note deliberately does **not** cover a text editor or terminal. Those are
|
||||
easier (single-process, I/O-bound) and fall out of the early phases here almost for
|
||||
free; the hard, shaping problem is the standard-library surface, so that is what
|
||||
this roadmap is about.
|
||||
|
||||
The analysis behind it was done against **Zig 0.16** (the pinned toolchain). Zig's
|
||||
standard library moves between releases — especially the parts described here — so
|
||||
treat upstream references as "the shape in 0.16.x," and expect to re-check them on a
|
||||
toolchain bump.
|
||||
|
||||
## The win condition
|
||||
|
||||
danos runs the Zig compiler when a bare
|
||||
|
||||
```
|
||||
zig build-exe hello.zig
|
||||
```
|
||||
|
||||
completes **on danos** and produces a runnable danos binary. Note the milestone is
|
||||
`build-exe`, not `zig build`: the `zig build` runner spawns child processes (the
|
||||
build steps), which needs a whole process-control surface danos does not have yet.
|
||||
A single `build-exe` needs none of that (see Phase 3). Reaching `build-exe` is
|
||||
"self-hosting"; reaching `zig build` is a later, separate lift.
|
||||
|
||||
### Non-goals
|
||||
|
||||
- **No Linux syscall/ABI emulation.** danos will not implement the Linux `syscall`
|
||||
interface so that stock `x86_64-linux` binaries run. That is a permanent
|
||||
compatibility treadmill and it inverts the microkernel design — explicitly out.
|
||||
- **No musl port yet.** A musl libc port is a reasonable *later* effort (it unlocks
|
||||
the C ecosystem), but it is not on the critical path to Zig-on-danos, and it is
|
||||
deferred. The roadmap below is arranged so the work still pays off if musl ever
|
||||
happens (see "The same surface, twice").
|
||||
- **Editor/terminal are out of scope for this note** (they are downstream of Phase 1).
|
||||
|
||||
**On FFI.** Foreign-function interop splits the same way as the doors below. Zig-level
|
||||
and C-ABI-*exposing* FFI (`extern`, `callconv(.c)`, C-ABI structs) work on a real target
|
||||
immediately — and the `std.os.danos` seam is C-ABI-shaped by construction, so it is
|
||||
FFI-friendly from the start. *Consuming* C libraries (`@cImport`, linking archives) is
|
||||
the part that needs a libc + headers, i.e. the deferred musl door. So an eventual FFI
|
||||
need reinforces keeping that door open; it does not change the plan.
|
||||
|
||||
## The realization that shapes everything: 0.16 gives us *one* seam
|
||||
|
||||
The instinct "to target Zig we'd have to reimplement all the `std` namespaces" was
|
||||
how older Zig worked. Zig 0.16 (post-"writergate") is far kinder:
|
||||
|
||||
- **`std.fs` is essentially gone.** It is now path helpers plus deprecated aliases;
|
||||
there is no `std.fs.File`, `std.fs.Dir`, or `std.fs.cwd()`. File and directory
|
||||
work goes through **`std.Io`** — a single runtime **vtable** (`Io.zig`) of
|
||||
function pointers handed to `main` as `std.process.Init.io`. `std.Io.File` and
|
||||
`std.Io.Dir` are thin forwarders to that vtable. `Io.zig` and the `fs` shim carry
|
||||
**zero** per-OS branches.
|
||||
- **`std.posix` is one generic body** parameterised over a single `system` module.
|
||||
With no libc, `system` resolves **per target OS**: `.linux => std.os.linux`,
|
||||
`.plan9 => std.os.plan9`, and so on. The generic `std.posix.read`/`write`/`open`
|
||||
bodies are just `system.read(...)` plus an errno switch — *identical for every
|
||||
OS*. The only variable is what `system` binds to.
|
||||
- **`std.os.<tag>`** (e.g. `std/os/linux.zig`) is therefore the real porting seam: a
|
||||
low-level, C-ABI-shaped module of `read/write/open/close/lseek/mmap/clock/exit/…`
|
||||
plus an `errno` enum and the constant tables (`O_*`, `CLOCK_*`, `S_*`).
|
||||
|
||||
Put together: **to port danos we write `std.os.danos` once** — the ~30-operation
|
||||
seam — and the whole `std.posix` / `std.fs` / `std.Io` tower above it lights up
|
||||
generically, because none of it branches on the OS. That is a dramatically smaller
|
||||
and more contained target than "reimplement the namespaces."
|
||||
|
||||
## Three doors, and why we take the first
|
||||
|
||||
| Door | What it is | Verdict |
|
||||
|------|-----------|---------|
|
||||
| **1. Implement the std seam** (`std.os.danos`) | Write the ~30-op `system` module over danos's native ABI + VFS; the generic std tower lights up. | **Take this.** The only door that touches neither C nor the Linux ABI. |
|
||||
| **2. Port musl** | Port musl libc to danos, link Zig against it. | Defer. Good later for the *C* ecosystem; barely helps *Zig* (std only uses libc on the libc-linked path). |
|
||||
| **3. Emulate the Linux ABI** | Implement Linux syscalls so stock linux binaries run. | Reject. Bottomless compatibility treadmill; against the design. |
|
||||
|
||||
### The same surface, twice
|
||||
|
||||
Doors 1 and 2 are the **same native surface at different layers**. `std.posix.read`
|
||||
is `system.read(...)` + an errno switch *regardless of OS* — the only question is
|
||||
whether `system` is **`std.os.danos` (Zig)** or **musl (C)**. Either way, the set of
|
||||
danos-facing operations you must implement is the *same* ~30 ops, all bottoming out
|
||||
in danos's native syscalls + the VFS/FAT server.
|
||||
|
||||
So the runtime work below is **not throwaway** if musl ever happens: you are building
|
||||
the danos-native implementations of that surface either way. Door 1 just packages
|
||||
them as Zig; a future musl re-uses the identical kernel/VFS operations underneath. The
|
||||
two symmetries worth keeping in mind: doors 1 and 2 converge at the **top** (identical
|
||||
POSIX surface); doors 2 and 3 converge at the **bottom** (unmodified musl needs the
|
||||
Linux syscall ABI). Door 1 is the only one that avoids both C and Linux.
|
||||
|
||||
### A fork is table stakes — for any door
|
||||
|
||||
`std.Target.Os.Tag` is a **closed enum** baked into the compiler binary *and* into
|
||||
the `std` linked with every program; `-target x86_64-danos` resolves through it. So
|
||||
adding `danos` as a name requires patching and rebuilding the compiler — even the
|
||||
musl door needs this. "Fork Zig" is therefore not an extra cost unique to door 1; it
|
||||
is the price of admission for *any* real target. What door 1 adds on top is small and
|
||||
localised (below).
|
||||
|
||||
## The architecture decision: `runtime.os` + `runtime.fs`, and retire `posix`
|
||||
|
||||
danos already has the right split ([the private-ABI boundary](../README.md)): the
|
||||
kernel exposes a minimal syscall ABI ([syscall.md](syscall.md)); the **`runtime`**
|
||||
library is the stable, danos-native application ABI. What this roadmap adds:
|
||||
|
||||
- **`runtime.os` — the seam.** A C-ABI-shaped module of the ~30 operations
|
||||
(`read/write/open/close/lseek/mmap/munmap/clock/exit/…`) + an errno enum + the
|
||||
constant tables, each backed by danos's native syscalls and the VFS. **Structure it
|
||||
to mirror `std/os/linux.zig`.** This is the load-bearing, *non-throwaway* artifact:
|
||||
when we fork Zig, `runtime.os` is copy-pasted (near-verbatim) into `std.os.danos`.
|
||||
- **`runtime.fs` — the thin native file API** danos programs use *today*, layered
|
||||
over `runtime.os`. It is also the concrete backing for the `std.Io` vtable's
|
||||
file-write entry once we're a real target, which is why program stdout, diagnostics,
|
||||
and file writes should all be *decided once at that seam* rather than as bespoke
|
||||
per-call helpers (see "How this informs decisions now").
|
||||
|
||||
**Do not hand-mirror the high-level std namespaces.** `std.fs`/`std.Io`/`std.process`
|
||||
are generic and OS-agnostic; once `std.os.danos` exists and we fork, upstream *gives*
|
||||
them to danos for free. Hand-writing `runtime.std.fs` to imitate them would be
|
||||
redundant the day the fork works, and it would chase a moving target (0.16's `std.Io`
|
||||
is large and still shifting). Build the seam well; take the tower for free.
|
||||
|
||||
**Why not a library called `std`?** Because `@import("std")` resolves to the
|
||||
compiler-provided standard library; a user module named `std` would *shadow* it for
|
||||
anything that imports it that way. That is the real reason the seam lives *inside* a
|
||||
forked std as `std/os/danos.zig`, not as a `runtime.std` library — and why danos's end
|
||||
state (`@import("std")` just working, and knowing danos) is the most natively Zig it can
|
||||
be. `runtime.os` is only the interim staging ground: developed against the stock
|
||||
toolchain so Phase 1 need not wait on the fork, then promoted near-verbatim into the
|
||||
fork's `std/os/danos.zig`.
|
||||
|
||||
### Retire `library/posix`
|
||||
|
||||
The `posix` compatibility layer (`unistd`, `stdio`) was the right instinct too early.
|
||||
Its whole value is POSIX *spellings* for POSIX software — and danos has no POSIX
|
||||
software; every current caller is danos-native code that could use `runtime.fs`
|
||||
directly. The real POSIX story arrives later and from elsewhere (musl, or upstream
|
||||
`std`'s own posix over `std.os.danos`), which supersedes a hand-rolled shim. So it is
|
||||
premature abstraction that adds a "which layer do I use?" fork with no payoff yet.
|
||||
|
||||
Its footprint is tiny: **five** call sites, all `unistd` file operations —
|
||||
`system/services/fat/fat.zig` (`mount`), the `vfs-test` and `fat-test` clients, and
|
||||
(from the boot-log work) `init.zig` and `log-flush.zig`. `stdio.zig` is dead — nothing
|
||||
imports it. The plan: build `runtime.fs`, migrate those five to it, delete
|
||||
`library/posix/`, and drop the `posix` module from `build.zig`'s `addUserBinary`.
|
||||
|
||||
## Where danos stands: coverage vs. the gaps
|
||||
|
||||
What the seam needs, and what danos already provides:
|
||||
|
||||
| std need | danos today | Gap |
|
||||
|----------|-------------|-----|
|
||||
| open / read / write / close / lseek | VFS (via the current `unistd`, → `runtime.fs`) | none — repackage |
|
||||
| directory read (`getdents`) | VFS `readdir` | none — repackage |
|
||||
| mmap / munmap | native syscalls ([abi.zig](../system/abi.zig)) | none |
|
||||
| page allocator | over `mmap`, via `root.os.heap.page_allocator` override | ~30-line hook |
|
||||
| monotonic clock | `clock` syscall | none |
|
||||
| args / argv | SysV entry stack ([sysv.md](sysv.md)), `runtime.process.Init` | none |
|
||||
| stdout / stderr | `debug_write` today | wire fd 1/2 to a console **byte** stream |
|
||||
| mkdir / unlink / rename / truncate | done — engine + VFS + `runtime.fs` (Phase 2) | — |
|
||||
| stat fields | `{size, kind, mtime}` | **mode / inode** still missing (cache validity) |
|
||||
| wall-clock / realtime | done — `wall_clock` syscall (CMOS RTC, Phase 2d) | — |
|
||||
| **environment variables** | `Init` has no env field | missing (can start empty) |
|
||||
| **cwd / chdir** | paths are absolute or bare | missing (no cwd anchor) |
|
||||
| **entropy / random** | — | missing (needed behind `vtable.random`) |
|
||||
| process spawn + exit status | `system_spawn` starts a *named ramdisk binary*; `ExitReason` is a *category* | no exec-of-path, no numeric `WEXITSTATUS` |
|
||||
| threads | one thread per process | avoided via `-fsingle-threaded` (below) |
|
||||
| symlinks | `NodeKind` has the tag; unimplemented | low priority |
|
||||
|
||||
The clustering is clear: reads and memory are basically done; the real work is
|
||||
**filesystem mutation + richer stat + wall-clock**, and a few small seam pieces
|
||||
(page-allocator hook, stdio bytes, entropy). Process spawning and threads are
|
||||
side-stepped entirely for a single `build-exe`.
|
||||
|
||||
## The roadmap
|
||||
|
||||
### Phase 0 — Make `danos` a real target
|
||||
|
||||
**Host, target, self-host — keep the three roles straight.** The *host* is where the
|
||||
compiler runs (your mac + linux dev machines); the *target* is what it emits (`danos`);
|
||||
and eventually danos becomes a host too (self-hosting — the win condition). So the move
|
||||
is: fork the compiler, build it **for** your dev hosts, and teach it to **cross-compile
|
||||
to** danos. You already do this — danos is cross-compiled `freestanding` from your dev
|
||||
host today; Phase 0 swaps that `freestanding` target for a real `x86_64-danos` one, which
|
||||
is what unlocks the native `std`.
|
||||
|
||||
**Why a compiler fork, not just a `--zig-lib-dir` override.** `std.Target.Os.Tag` is a
|
||||
*closed enum compiled into the compiler binary*, so `-target x86_64-danos` will not even
|
||||
parse unless the compiler itself knows the tag. Overriding the std lib directory alone
|
||||
cannot add a target — and there is no libc-only shortcut (a future musl needs the same
|
||||
patch). The only alternative, staying on `freestanding` + hand-shims, is exactly the
|
||||
non-native feel we are leaving: `@import("std")` there is stubbed, not real.
|
||||
|
||||
**The fork.** Clone `ziglang/zig` at the pinned 0.16 tag; build it with a stock
|
||||
same-version `zig` (`zig build` in the tree — a standard, LLVM-pulling, roughly one-time
|
||||
build); point danos's `build.zig`/CI at the resulting binary. Four localised patches:
|
||||
|
||||
- add `danos` to `std.Target.Os.Tag`, in the "no version range" group alongside
|
||||
plan9/serenity;
|
||||
- add `danos` to the freestanding/other **no-op `_start` list** in `std`'s `start.zig`,
|
||||
so std does *not* emit its own System-V `_start` — danos keeps owning the entry shim
|
||||
and `Init`/argv construction it already builds ([sysv.md](sysv.md));
|
||||
- wire the `system` selector `.danos => std.os.danos` in `std.posix`;
|
||||
- add `std/os/danos.zig` — **the seam itself**, promoted near-verbatim from the
|
||||
`runtime.os` developed first in Phase 1 (against the stock toolchain, so the fork is
|
||||
not a prerequisite for starting).
|
||||
|
||||
This is the fork treadmill we accept once. Keep the patch set tiny and `else`-friendly,
|
||||
pin to one 0.16.x, and rebase on point releases.
|
||||
|
||||
### Phase 1 — `runtime.os` read-side + allocator + stdio + cwd; retire `posix`
|
||||
|
||||
Author `runtime.os` (→ `std.os.danos`): the `errno` enum, the constant tables, and
|
||||
the C-convention `read / write / open / openat / close / lseek / mmap / munmap /
|
||||
exit`, each returning result-or-`-errno`. Most backing already exists (VFS + native
|
||||
mmap + clock).
|
||||
|
||||
- Provide `page_allocator` via `root.os.heap.page_allocator` (a thin override over
|
||||
danos `mmap`). This sits **outside** the `std.Io` vtable, so it is wired separately.
|
||||
- Wire fd 0/1/2 to a console **byte** stream (today output only reaches `debug_write`;
|
||||
input is structured `InputEvent` IPC — a byte tty is a new, small thing in both
|
||||
directions).
|
||||
- Add a `getcwd`/`chdir` anchor so `std.fs.cwd()`-style resolution has something to
|
||||
resolve against.
|
||||
- Build `runtime.fs` over `runtime.os`; migrate the five `posix` callers to it; delete
|
||||
`library/posix/` and drop its build module.
|
||||
|
||||
After Phase 1, the surface an editor or terminal needs (open/read/write/close/lseek/
|
||||
readdir/isatty/args/exit) exists. Those are downstream and out of scope here.
|
||||
|
||||
### Phase 2 — Filesystem mutation + real stat (the compiler's cache tower)
|
||||
|
||||
danos's biggest genuine gap, and the correctness-critical one:
|
||||
|
||||
- Add **mkdir / unlink / rename / truncate** to *both* the VFS wire protocol
|
||||
([protocol.zig](../system/services/vfs/protocol.zig)) and the FAT engine
|
||||
([engine.zig](../system/services/fat/engine.zig)), then expose them via `runtime.os`.
|
||||
- Extend `stat` beyond `{size, kind}` to carry **mtime + inode + mode** — `std`'s file
|
||||
stat needs them for build-cache validity — which in turn needs **wall-clock** time
|
||||
(danos is monotonic-only today; an RTC/time service is the dependency).
|
||||
|
||||
Because `std.fs`/`std.Io` have no per-OS branches, finishing this in `runtime.os`
|
||||
lights up the whole file tower for the compiler at once. Environment can stay an empty
|
||||
map until the kernel populates a non-empty `envp`.
|
||||
|
||||
**Status — Phase 2 complete.** `truncate` (O_TRUNC, closing the boot-log stale-tail
|
||||
bug), `mkdir`, `unlink`, and `rename` are all wired through the FAT engine, the VFS
|
||||
protocol + router, and `runtime.fs` (`makeDirectory` / `remove` / `rename`) —
|
||||
host-tested and QEMU-tested (`fat-mutations` + `fat-rename` make a directory, write+read
|
||||
a file in it, rename it, then remove it through the mount). `removeFile` and `rename`
|
||||
are LFN-aware; `rename` is same-directory + 8.3 (cross-directory and long-name-
|
||||
preserving rename are noted limitations). Wall-clock is now a kernel syscall
|
||||
(`wall_clock`, a CMOS-RTC read anchored to the monotonic clock), and the FAT engine
|
||||
stamps and reports **mtime** — `stat` / `runtime.fs.Attributes` carry a real
|
||||
modification time (the `fat-mtime` case reads it back within seconds of the host clock).
|
||||
The remaining `stat` fields, `mode`/`inode`, are deferred (not needed until the
|
||||
compiler's cache layer wants them). **Everything past here is gated on Phase 0 (the
|
||||
fork):** the `runtime.os` seam, `cwd`, stdio-as-fds, and the compiler bring-up.
|
||||
|
||||
### Phase 3 — Single-threaded, self-linked compiler bring-up
|
||||
|
||||
Build the compiler with **two load-bearing flags**:
|
||||
|
||||
- **`-fsingle-threaded`** removes `std.Thread` entirely — `Thread.spawn` is a hard
|
||||
compile error under it, and `std.Io`'s threaded backend runs inline. danos being
|
||||
one-thread-per-process is therefore **not** a blocker. Parallel codegen is a
|
||||
throughput optimisation, not a correctness requirement.
|
||||
- **`-fno-llvm -fno-lld`** keeps codegen and linking **in-process** (the self-hosted
|
||||
x86-64 backend + self-linker), so a single `build-exe` **never forks a child**. That
|
||||
is what lets us defer the entire spawn/exec/wait surface.
|
||||
|
||||
Then supply the few remaining seam pieces: `now` (wrap the danos clock), an entropy
|
||||
source behind `vtable.random` (`randomSecure` can alias it initially — low volume, for
|
||||
temp-file names and hashmap seeds), and the Phase-2 mkdir/rename/unlink for cache dir
|
||||
trees and atomic temp-then-rename output.
|
||||
|
||||
**Explicitly deferred** (not on the `build-exe` path): child-process spawn/exec (only
|
||||
`zig build` and external tools need it), `std.Thread`, `fsync` (FAT is write-through
|
||||
today), symlinks, and musl.
|
||||
|
||||
## Risks and gotchas
|
||||
|
||||
- **The std-fork rebase treadmill is the main ongoing cost.** A new OS tag touches the
|
||||
same broad file set plan9/serenity touch (hundreds of `native_os` sites, plus
|
||||
"unsupported OS" `@compileError` dead-ends a new tag must be routed around), and the
|
||||
entire `std.Io` layer is new in 0.16 and still moving. Stay pinned to one 0.16.x,
|
||||
keep additions localised and `else`-friendly. Watch the closed-enum gotcha: adding
|
||||
`danos` to `Os.Tag` can break existing *exhaustive* switches that lack an `else`, so
|
||||
expect to touch switch sites beyond the ones you implement.
|
||||
- **Single-threaded is load-bearing.** The "no `std.Thread`" simplification rests
|
||||
entirely on `-fsingle-threaded`. If a dependency or flag flips threading back on, you
|
||||
inherit an unescapable compile error (no root-hook exists) — the only outs are a full
|
||||
thread-impl fork or linking libc for pthreads. Keep `single_threaded` asserted end to
|
||||
end.
|
||||
- **In-process linking is load-bearing.** Reaching the compiler without fork/exec
|
||||
depends on `-fno-llvm -fno-lld`. The moment you shell out to LLD/`ld`, you need the
|
||||
full `spawn`/`wait` surface — the hardest microkernel piece — and danos's
|
||||
`system_spawn` only starts a *named ramdisk binary*, not exec of an arbitrary path.
|
||||
Verify the self-hosted backend covers the target output before assuming child
|
||||
processes are optional.
|
||||
- **The shim cannot host the compiler.** danos's current `runtime`/`posix` is fine for
|
||||
danos's *own* native programs, but the compiler `import`s *upstream* `std`, which on
|
||||
a non-target hits the void `system` stub. So the compiler forces the real target
|
||||
(Phase 0's fork). Do not over-invest in extending the hand-shim for compiler
|
||||
purposes; put that effort into `runtime.os` + the VFS/FAT operations, which both the
|
||||
fork *and* a future musl consume.
|
||||
- **`"w"`/`O_CREAT` does not truncate — a silent-corruption bug on this road.** The FAT
|
||||
engine's `writeFile` only *grows* `node.size`, so overwriting a shorter file leaves
|
||||
trailing garbage. Harmless for the boot log today, but for a compiler it means
|
||||
**corrupt `.o`/cache files that look like nondeterministic compiler bugs.** Land
|
||||
`truncate` (Phase 2) before the compiler ever writes cache.
|
||||
- **Exit status is categorical, not numeric.** `process_exit_reason` returns an
|
||||
`ExitReason` *category*, not a numeric code (`WEXITSTATUS`). Fine while spawn is
|
||||
stubbed; the day `zig build` or external tools arrive, plan a kernel exit-record
|
||||
extension — do not let it surprise you.
|
||||
|
||||
## How this informs decisions now
|
||||
|
||||
Two current decisions fall out of this roadmap:
|
||||
|
||||
1. **The `runtime.fs` / `std.Io` question resolves at the vtable seam.** Because 0.16
|
||||
routes *all* output through the `std.Io` vtable's file-write entry, and stdout/stderr
|
||||
are just `File`s with well-known handles, build `runtime.fs` (and the console stdout)
|
||||
as the concrete backing for that entry — not as a bespoke `std.Io.Writer`-only shim.
|
||||
Decide it once, at the seam, and program stdout, diagnostics, and file writes all
|
||||
flow through the same danos VFS/console path.
|
||||
2. **The boot-log `truncate` caveat is now fixed** (Phase 2a). It was the same
|
||||
`writeFile`-only-grows gap that on the self-hosting road would corrupt build output;
|
||||
`engine.truncate` + an O_TRUNC open flag now free the old chain so a shorter rewrite
|
||||
leaves no stale tail, and the boot-log flush opens with it.
|
||||
|
||||
## Related
|
||||
|
||||
- [vision.md](vision.md) — the north star this serves.
|
||||
- [syscall.md](syscall.md) — the kernel↔runtime ABI `runtime.os` is built on.
|
||||
- [sysv.md](sysv.md) — the entry stack (`argc/argv/envp/auxv`) danos already constructs.
|
||||
- [ipc.md](ipc.md) — the IPC the VFS/FAT operations travel over.
|
||||
- [danos-file-system-hierarchy-FSH.md](danos-file-system-hierarchy-FSH.md) — the
|
||||
filesystem layout the file surface serves.
|
||||
- [coding-standards.md](coding-standards.md) — danos naming (why the compat spellings
|
||||
are confined, and now retired).
|
||||
@@ -0,0 +1,80 @@
|
||||
//! /lib/mmio — typed volatile MMIO register access, plus the memory-ordering
|
||||
//! barriers a device driver needs. Used by drivers on top of an `mmio_map` grant.
|
||||
//!
|
||||
//! **`volatile` is not a barrier.** In Zig it means only: don't elide this access, and
|
||||
//! don't reorder it against *other volatile* accesses. It says nothing about ordinary
|
||||
//! stores — the DMA descriptor you just filled in write-back RAM — which the compiler
|
||||
//! (and, on weakly-ordered hardware, the CPU) may freely move past a volatile MMIO
|
||||
//! write. The canonical bug:
|
||||
//!
|
||||
//! ring[i] = descriptor; // ordinary store to WB RAM
|
||||
//! doorbell.* = i; // volatile store to UC MMIO
|
||||
//! // nothing orders these; the device can read a stale descriptor
|
||||
//!
|
||||
//! Put a `wmb()` between them. The barriers lower per-architecture — which is the whole
|
||||
//! reason they are a named primitive and not scattered `asm volatile`:
|
||||
//!
|
||||
//! x86_64 aarch64
|
||||
//! mb() mfence dsb sy
|
||||
//! rmb() lfence dsb ld
|
||||
//! wmb() sfence dsb st
|
||||
//!
|
||||
//! x86 is forgiving (TSO + strong-uncacheable MMIO), so a compiler barrier usually
|
||||
//! suffices; ARM is not, and ARM is the win condition (docs/vision.md) — so the
|
||||
//! abstraction exists now, while there is one caller (hpet) to get right. See
|
||||
//! docs/driver-model.md (M14) for the full ordering contract.
|
||||
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// Read a register of type `T` at absolute virtual address `addr` — a location inside
|
||||
/// a device's `mmio_map` grant. `volatile`: never elided, never reordered against
|
||||
/// another volatile access.
|
||||
pub inline fn read(comptime T: type, addr: usize) T {
|
||||
return @as(*const volatile T, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
/// Write `value` of type `T` to the register at absolute virtual address `addr`.
|
||||
pub inline fn write(comptime T: type, addr: usize, value: T) void {
|
||||
@as(*volatile T, @ptrFromInt(addr)).* = value;
|
||||
}
|
||||
|
||||
/// Full barrier: all loads and stores before it are globally visible before any after
|
||||
/// it. Use when an MMIO write must complete before a following read.
|
||||
pub inline fn mb() void {
|
||||
switch (builtin.target.cpu.arch) {
|
||||
.x86_64 => asm volatile ("mfence" ::: .{ .memory = true }),
|
||||
.aarch64 => asm volatile ("dsb sy" ::: .{ .memory = true }),
|
||||
else => @compileError("mmio.mb: unsupported architecture"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read barrier: loads before it complete before loads after it. Use after an IRQ
|
||||
/// wake, before reading what the device wrote to shared memory.
|
||||
pub inline fn rmb() void {
|
||||
switch (builtin.target.cpu.arch) {
|
||||
.x86_64 => asm volatile ("lfence" ::: .{ .memory = true }),
|
||||
.aarch64 => asm volatile ("dsb ld" ::: .{ .memory = true }),
|
||||
else => @compileError("mmio.rmb: unsupported architecture"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write barrier: stores before it become visible before stores after it. Use between
|
||||
/// filling a DMA descriptor in RAM and ringing the device's doorbell.
|
||||
pub inline fn wmb() void {
|
||||
switch (builtin.target.cpu.arch) {
|
||||
.x86_64 => asm volatile ("sfence" ::: .{ .memory = true }),
|
||||
.aarch64 => asm volatile ("dsb st" ::: .{ .memory = true }),
|
||||
else => @compileError("mmio.wmb: unsupported architecture"),
|
||||
}
|
||||
}
|
||||
|
||||
test "barriers emit and registers round-trip through a RAM cell" {
|
||||
// The barriers must at least assemble for the host arch; ordering can't be unit
|
||||
// tested, but a missing/mistyped mnemonic is caught here.
|
||||
wmb();
|
||||
rmb();
|
||||
mb();
|
||||
var cell: u64 = 0;
|
||||
write(u64, @intFromPtr(&cell), 0xDEAD_BEEF);
|
||||
try @import("std").testing.expectEqual(@as(u64, 0xDEAD_BEEF), read(u64, @intFromPtr(&cell)));
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Block-device client: the helper a filesystem uses to read and write a block
|
||||
//! device (a USB stick, via usb-storage) without hand-rolling the block-protocol
|
||||
//! IPC. Layered over `ipc` and the shared `block-protocol` wire format, like
|
||||
//! `runtime.usb` over the transfer protocol.
|
||||
//!
|
||||
//! Transfers name a caller-owned DMA buffer by physical address (from
|
||||
//! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size
|
||||
//! limit — the same handoff usb-storage uses toward the controller.
|
||||
|
||||
const std = @import("std");
|
||||
const ipc = @import("ipc.zig");
|
||||
const system = @import("system.zig");
|
||||
const protocol = @import("block-protocol");
|
||||
|
||||
pub const Geometry = struct { block_size: u32, block_count: u64 };
|
||||
|
||||
pub const Device = struct {
|
||||
endpoint: ipc.Handle,
|
||||
|
||||
/// The device's block size and total block count.
|
||||
pub fn geometry(self: Device) ?Geometry {
|
||||
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.geometry), .lba = 0, .count = 0, .physical = 0 };
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return null;
|
||||
if (n < protocol.reply_size) return null;
|
||||
const result = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
|
||||
if (result.status != 0) return null;
|
||||
return .{ .block_size = result.block_size, .block_count = result.block_count };
|
||||
}
|
||||
|
||||
/// Read `count` blocks starting at `lba` into the DMA buffer at `physical`.
|
||||
pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool {
|
||||
return self.transfer(.read, lba, count, physical);
|
||||
}
|
||||
|
||||
/// Write `count` blocks starting at `lba` from the DMA buffer at `physical`.
|
||||
pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool {
|
||||
return self.transfer(.write, lba, count, physical);
|
||||
}
|
||||
|
||||
fn transfer(self: Device, operation: protocol.Operation, lba: u64, count: u32, physical: u64) bool {
|
||||
var request = protocol.Request{ .operation = @intFromEnum(operation), .lba = lba, .count = count, .physical = physical };
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return false;
|
||||
if (n < protocol.reply_size) return false;
|
||||
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Look up the block device, retrying generously while the USB storage chain
|
||||
/// (controller reset, enumeration, mass-storage bring-up) comes up.
|
||||
pub fn open() ?Device {
|
||||
// Patient: the whole USB storage chain (firmware discovery, xHCI reset and
|
||||
// enumeration, mass-storage bring-up) must complete first, which can take
|
||||
// tens of seconds under emulation.
|
||||
var attempts: usize = 0;
|
||||
while (attempts < 1200) : (attempts += 1) {
|
||||
if (ipc.lookup(.block)) |handle| return .{ .endpoint = handle };
|
||||
system.sleep(50);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -3,13 +3,15 @@
|
||||
//! ownership of its hardware; the claim is the capability the kernel checks before
|
||||
//! mapping registers or routing an IRQ.
|
||||
|
||||
const danos = @import("danos");
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const device_abi = @import("device-abi");
|
||||
const sc = @import("system-call.zig");
|
||||
|
||||
pub const DeviceDescriptor = danos.DeviceDescriptor;
|
||||
pub const ResourceDescriptor = danos.ResourceDescriptor;
|
||||
pub const DeviceClass = danos.DeviceClass;
|
||||
pub const ResourceKind = danos.ResourceKind;
|
||||
pub const DeviceDescriptor = device_abi.DeviceDescriptor;
|
||||
pub const ResourceDescriptor = device_abi.ResourceDescriptor;
|
||||
pub const DeviceClass = device_abi.DeviceClass;
|
||||
pub const ResourceKind = device_abi.ResourceKind;
|
||||
|
||||
inline fn failed(r: usize) bool {
|
||||
return r > ~@as(usize, 0) - 4095;
|
||||
@@ -33,7 +35,11 @@ pub fn mmioMap(device_id: u64, resource_index: u64) ?usize {
|
||||
}
|
||||
|
||||
/// `DeviceDescriptor.parent` for a device with no parent.
|
||||
pub const no_parent = danos.no_parent;
|
||||
pub const no_parent = device_abi.no_parent;
|
||||
|
||||
/// `DeviceDescriptor.pci_class` for a device that is not a PCI function. Set this on
|
||||
/// descriptors passed to `register` unless the child really is one.
|
||||
pub const no_pci_class = device_abi.no_pci_class;
|
||||
|
||||
/// Publish `descriptor` as a child of `parent_id`, which this process must have claimed.
|
||||
/// Returns the new device id. The child is left unclaimed, so whichever driver owns
|
||||
@@ -65,3 +71,61 @@ pub fn irqBind(device_id: u64, resource_index: u64, endpoint: usize) bool {
|
||||
pub fn irqAck(device_id: u64, resource_index: u64) bool {
|
||||
return !failed(sc.systemCall2(.irq_ack, device_id, resource_index));
|
||||
}
|
||||
|
||||
/// The Message-Signalled Interrupt address/data a driver programs into its device's
|
||||
/// MSI capability. The device raises the interrupt by writing `data` to `address`.
|
||||
pub const Msi = struct { address: u64, data: u32 };
|
||||
|
||||
/// Set up MSI for a claimed device: the kernel allocates a per-device edge-triggered
|
||||
/// vector, binds it to `endpoint` (delivered like `irqBind`, but with no mask and no
|
||||
/// `irqAck` cycle), and returns the (address, data) to write into the device's MSI
|
||||
/// capability — found by mmio_mapping the device's ECAM config space (resource 0) and
|
||||
/// walking its capability list. Returns null on failure. Two return values (address in
|
||||
/// rax, data in rdx), so a hand-written stub.
|
||||
pub fn msiBind(device_id: u64, endpoint: usize) ?Msi {
|
||||
var rax: usize = undefined;
|
||||
var rdx: usize = undefined;
|
||||
asm volatile ("syscall"
|
||||
: [rax] "={rax}" (rax),
|
||||
[rdx] "={rdx}" (rdx),
|
||||
: [n] "{rax}" (@intFromEnum(abi.SystemCall.msi_bind)),
|
||||
[a0] "{rdi}" (device_id),
|
||||
[a1] "{rsi}" (endpoint),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
if (failed(rax)) return null;
|
||||
return .{ .address = rax, .data = @intCast(rdx) };
|
||||
}
|
||||
|
||||
/// Read `width` bytes (1, 2, or 4) from a port in a claimed device's `io_port`
|
||||
/// resource, at byte `offset` within it. Ring 3 has no direct `in`/`out`, so a legacy
|
||||
/// driver (PS/2, 16550 UART) reaches its ports through this claim-gated call — each
|
||||
/// access is a syscall, which is fine for the low-rate hardware that needs it. Returns
|
||||
/// null if the capability check fails (device not claimed, wrong resource, out of
|
||||
/// range). A device that decodes no data returns all-ones, which is a valid value, not
|
||||
/// a failure.
|
||||
pub fn ioRead(device_id: u64, resource_index: u64, offset: u64, width: u8) ?u32 {
|
||||
const r = sc.systemCall4(.io_read, device_id, resource_index, offset, width);
|
||||
return if (failed(r)) null else @intCast(r);
|
||||
}
|
||||
|
||||
/// Write `value` (its low `width` bytes, 1/2/4) to a port in a claimed device's
|
||||
/// `io_port` resource, at byte `offset`. Same capability gate as `ioRead`.
|
||||
pub fn ioWrite(device_id: u64, resource_index: u64, offset: u64, width: u8, value: u32) bool {
|
||||
return !failed(sc.systemCall5(.io_write, device_id, resource_index, offset, width, value));
|
||||
}
|
||||
|
||||
/// Find DeviceDescription by hid
|
||||
///
|
||||
/// Utility function for driver development
|
||||
pub fn findDeviceDescriptorByHid(buffer: []DeviceDescriptor, hid_needle: []const u8) ?DeviceDescriptor {
|
||||
const total = enumerate(buffer);
|
||||
const n = @min(total, buffer.len);
|
||||
for (@as([]DeviceDescriptor, buffer[0..n])) |d| {
|
||||
const hid_haystack = d.hid[0..@intCast(d.hid_len)];
|
||||
if (std.mem.eql(u8, hid_haystack, hid_needle)) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//! User-space DMA memory: `dma_alloc` / `dma_free`. A driver that programs a
|
||||
//! bus-mastering engine needs a descriptor ring the device can read — memory that is
|
||||
//! physically contiguous, at a physical address the driver knows, uncacheable, and
|
||||
//! pinned. `mmap` gives none of those; this does. Pair it with the barriers in
|
||||
//! `/lib/mmio` (fill the ring, `wmb()`, ring the doorbell). See docs/driver-model.md.
|
||||
|
||||
const abi = @import("abi");
|
||||
const sc = @import("system-call.zig");
|
||||
|
||||
/// Allocation flags. `coherent` (uncacheable) is the portable default; the rest are
|
||||
/// opt-in for specific hardware — see `abi`.
|
||||
pub const coherent: usize = abi.dma_coherent;
|
||||
pub const write_combining: usize = abi.dma_write_combining;
|
||||
pub const below_4g: usize = abi.dma_below_4g;
|
||||
|
||||
/// A DMA allocation: the `virtual` address the CPU touches, and the `physical` address
|
||||
/// to program into the device's descriptor-ring / base registers.
|
||||
pub const Region = struct {
|
||||
virtual: usize,
|
||||
physical: usize,
|
||||
};
|
||||
|
||||
inline fn failed(r: usize) bool {
|
||||
return r > ~@as(usize, 0) - 4095;
|
||||
}
|
||||
|
||||
/// Allocate `len` bytes of DMA-capable memory with `flags` (e.g. `coherent`, or
|
||||
/// `coherent | below_4g`). Returns the virtual/physical pair, or null on failure. Two
|
||||
/// return values — the virtual address in rax, the physical address in rdx — so it
|
||||
/// needs a hand-written stub.
|
||||
pub fn alloc(len: usize, flags: usize) ?Region {
|
||||
var rax: usize = undefined;
|
||||
var rdx: usize = undefined; // out: physical address
|
||||
asm volatile ("syscall"
|
||||
: [rax] "={rax}" (rax),
|
||||
[rdx] "={rdx}" (rdx),
|
||||
: [n] "{rax}" (@intFromEnum(abi.SystemCall.dma_alloc)),
|
||||
[a0] "{rdi}" (len),
|
||||
[a1] "{rsi}" (flags),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
if (failed(rax)) return null;
|
||||
return .{ .virtual = rax, .physical = rdx };
|
||||
}
|
||||
|
||||
/// Release a region from a prior `alloc` (`virtual` and the same `len`).
|
||||
pub fn free(virtual: usize, len: usize) void {
|
||||
_ = sc.systemCall2(.dma_free, virtual, len);
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
//! runtime.fs — the danos-native file API. A program opens, reads, writes, and
|
||||
//! lists files served by the user-space VFS (system/services/vfs), each call
|
||||
//! marshalling a vfs-protocol request over IPC. This is the danos-native layer
|
||||
//! danos programs use directly; it is also where the file operations that later
|
||||
//! become `std.os.danos` are staged (see docs/zig-self-hosting.md). It replaces
|
||||
//! the old POSIX `unistd` shim — a compatibility spelling danos does not need yet.
|
||||
//!
|
||||
//! Handles are *values*, not entries in a global descriptor table: a `File` /
|
||||
//! `Directory` owns its VFS node id and (for files) a byte offset. So there is no
|
||||
//! per-process fd limit and no shared table to synchronise — the danos-native
|
||||
//! shape, unlike the POSIX fd model the old shim emulated.
|
||||
|
||||
const std = @import("std");
|
||||
const ipc = @import("ipc.zig");
|
||||
const protocol = @import("vfs-protocol");
|
||||
|
||||
/// The kind of a filesystem node — re-exported so a caller need not import the
|
||||
/// wire protocol.
|
||||
pub const Kind = protocol.NodeKind;
|
||||
|
||||
/// A node's metadata (the answer to a status request).
|
||||
pub const Attributes = struct {
|
||||
size: u64,
|
||||
kind: Kind,
|
||||
/// Modification time — Unix epoch seconds, UTC. 0 if the filesystem has none.
|
||||
mtime: u64 = 0,
|
||||
};
|
||||
|
||||
// Map a wire `NodeKind` value to the enum, defaulting anything unrecognised to
|
||||
// `.regular` (the server is trusted, but a value outside the enum would be
|
||||
// illegal to `@enumFromInt` directly).
|
||||
fn kindFromWire(value: u32) Kind {
|
||||
return switch (value) {
|
||||
@intFromEnum(Kind.directory) => .directory,
|
||||
@intFromEnum(Kind.character_device) => .character_device,
|
||||
@intFromEnum(Kind.block_device) => .block_device,
|
||||
@intFromEnum(Kind.symbolic_link) => .symbolic_link,
|
||||
@intFromEnum(Kind.fifo) => .fifo,
|
||||
@intFromEnum(Kind.socket) => .socket,
|
||||
else => .regular,
|
||||
};
|
||||
}
|
||||
|
||||
/// How to open a path.
|
||||
pub const OpenOptions = struct {
|
||||
/// Create the file if it does not exist.
|
||||
create: bool = false,
|
||||
/// Open a directory node (for listing) rather than a file.
|
||||
directory: bool = false,
|
||||
/// Truncate an existing file to zero length on open (O_TRUNC) — replace its
|
||||
/// contents rather than overwriting in place.
|
||||
truncate: bool = false,
|
||||
|
||||
fn wireFlags(self: OpenOptions) u32 {
|
||||
var f: u32 = 0;
|
||||
if (self.create) f |= protocol.create;
|
||||
if (self.directory) f |= protocol.directory;
|
||||
if (self.truncate) f |= protocol.truncate;
|
||||
return f;
|
||||
}
|
||||
};
|
||||
|
||||
// The VFS server endpoint, looked up once by well-known id and cached.
|
||||
var vfs_handle: ipc.Handle = 0;
|
||||
var vfs_resolved = false;
|
||||
fn vfs() ?ipc.Handle {
|
||||
if (!vfs_resolved) {
|
||||
vfs_handle = ipc.lookup(.vfs) orelse return null;
|
||||
vfs_resolved = true;
|
||||
}
|
||||
return vfs_handle;
|
||||
}
|
||||
|
||||
const Result = struct { reply: protocol.Reply, payload: []u8 };
|
||||
|
||||
// One request/reply round trip: [Request header][send payload] -> VFS ->
|
||||
// [Reply header][receive payload]. The receive payload lands in `out`.
|
||||
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
||||
const h = vfs() orelse return null;
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const slen = @min(send.len, protocol.maximum_payload);
|
||||
@memcpy(message[protocol.request_size..][0..slen], send[0..slen]);
|
||||
|
||||
var rbuf: [protocol.message_maximum]u8 = undefined;
|
||||
const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null;
|
||||
if (n < protocol.reply_size) return null;
|
||||
const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]);
|
||||
const rpl = @min(n - protocol.reply_size, out.len);
|
||||
@memcpy(out[0..rpl], rbuf[protocol.reply_size..][0..rpl]);
|
||||
return .{ .reply = reply, .payload = out[0..rpl] };
|
||||
}
|
||||
|
||||
/// An open file: a VFS node plus a byte cursor. Read and write advance the cursor.
|
||||
pub const File = struct {
|
||||
node: u64,
|
||||
offset: u64 = 0,
|
||||
|
||||
/// Read up to `buffer.len` bytes at the current offset; returns the count, or
|
||||
/// null on error.
|
||||
pub fn read(self: *File, buffer: []u8) ?usize {
|
||||
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
|
||||
const request = protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
||||
const r = transact(request, &.{}, buffer) orelse return null;
|
||||
if (r.reply.status != 0) return null;
|
||||
self.offset += r.reply.len;
|
||||
return r.reply.len;
|
||||
}
|
||||
|
||||
/// Write `data` at the current offset; returns the count written. A single
|
||||
/// call is capped at the VFS payload size, so the return may be short — use
|
||||
/// `writeAll` to write the whole slice. Null on error.
|
||||
pub fn write(self: *File, data: []const u8) ?usize {
|
||||
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
|
||||
const request = protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
||||
const r = transact(request, data[0..want], &.{}) orelse return null;
|
||||
if (r.reply.status != 0) return null;
|
||||
self.offset += r.reply.len;
|
||||
return r.reply.len;
|
||||
}
|
||||
|
||||
/// Write all of `data`, looping past the per-call payload cap. Returns the
|
||||
/// total written, or null if a write failed before any progress.
|
||||
pub fn writeAll(self: *File, data: []const u8) ?usize {
|
||||
var written: usize = 0;
|
||||
while (written < data.len) {
|
||||
const n = self.write(data[written..]) orelse return if (written == 0) null else written;
|
||||
if (n == 0) return written; // no forward progress; stop rather than spin
|
||||
written += n;
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
/// Move the read/write cursor to an absolute byte position.
|
||||
pub fn seekTo(self: *File, position: u64) void {
|
||||
self.offset = position;
|
||||
}
|
||||
|
||||
/// This file's metadata.
|
||||
pub fn attributes(self: *File) ?Attributes {
|
||||
const request = protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
||||
var buffer: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
||||
const r = transact(request, &.{}, &buffer) orelse return null;
|
||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null;
|
||||
const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]);
|
||||
return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
|
||||
}
|
||||
|
||||
/// Release the VFS's open handle for this file.
|
||||
pub fn close(self: *File) void {
|
||||
const request = protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
||||
_ = transact(request, &.{}, &.{});
|
||||
}
|
||||
};
|
||||
|
||||
/// Open (or create, with `.create`) `path`. Returns the open file, or null.
|
||||
pub fn open(path: []const u8, options: OpenOptions) ?File {
|
||||
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = options.wireFlags() };
|
||||
const r = transact(request, path, &.{}) orelse return null;
|
||||
if (r.reply.status != 0) return null;
|
||||
return .{ .node = r.reply.node };
|
||||
}
|
||||
|
||||
/// A path's metadata without keeping it open (open -> status -> close).
|
||||
pub fn attributes(path: []const u8) ?Attributes {
|
||||
var file = open(path, .{}) orelse return null;
|
||||
defer file.close();
|
||||
return file.attributes();
|
||||
}
|
||||
|
||||
/// Whether `path` resolves — handy as a readiness check (e.g. waiting for a mount
|
||||
/// to come up before writing to it).
|
||||
pub fn exists(path: []const u8) bool {
|
||||
return attributes(path) != null;
|
||||
}
|
||||
|
||||
/// One entry returned by `Directory.next`.
|
||||
pub const Entry = struct {
|
||||
kind: Kind = .regular,
|
||||
size: u64 = 0,
|
||||
name_buffer: [64]u8 = undefined,
|
||||
name_len: usize = 0,
|
||||
|
||||
pub fn name(self: *const Entry) []const u8 {
|
||||
return self.name_buffer[0..self.name_len];
|
||||
}
|
||||
};
|
||||
|
||||
/// An open directory being listed, cursor-advanced by `next`.
|
||||
pub const Directory = struct {
|
||||
node: u64,
|
||||
cursor: u64 = 0,
|
||||
|
||||
/// Fill `entry` with the next directory entry; false at end of directory or
|
||||
/// on error.
|
||||
pub fn next(self: *Directory, entry: *Entry) bool {
|
||||
const request = protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 };
|
||||
var buffer: [protocol.message_maximum]u8 = undefined;
|
||||
const r = transact(request, &.{}, &buffer) orelse return false;
|
||||
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
|
||||
if (r.payload.len < protocol.directory_entry_size) return false;
|
||||
const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
|
||||
entry.kind = kindFromWire(header.kind);
|
||||
entry.size = header.size;
|
||||
const source = r.payload[protocol.directory_entry_size..];
|
||||
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
|
||||
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
|
||||
entry.name_len = nlen;
|
||||
self.cursor += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Release the VFS's open handle for this directory.
|
||||
pub fn close(self: *Directory) void {
|
||||
var f = File{ .node = self.node };
|
||||
f.close();
|
||||
}
|
||||
};
|
||||
|
||||
/// Open `path` as a directory for listing. Returns null if it isn't one / on error.
|
||||
pub fn openDirectory(path: []const u8) ?Directory {
|
||||
const file = open(path, .{ .directory = true }) orelse return null;
|
||||
return .{ .node = file.node };
|
||||
}
|
||||
|
||||
// A path-based request that returns only a status (mkdir, unlink).
|
||||
fn pathOperation(operation: protocol.Operation, path: []const u8) bool {
|
||||
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = 0 };
|
||||
const r = transact(request, path, &.{}) orelse return false;
|
||||
return r.reply.status == 0;
|
||||
}
|
||||
|
||||
/// Create a directory at `path` (its parent must already exist). Returns true on
|
||||
/// success. Only works under a mounted filesystem that supports directories.
|
||||
pub fn makeDirectory(path: []const u8) bool {
|
||||
return pathOperation(.mkdir, path);
|
||||
}
|
||||
|
||||
/// Remove the file at `path`. Returns true on success. Directories are refused
|
||||
/// (a separate directory-removal would have to check emptiness).
|
||||
pub fn remove(path: []const u8) bool {
|
||||
return pathOperation(.unlink, path);
|
||||
}
|
||||
|
||||
/// Rename `old_path` to `new_path`. Both must be in the same directory (same-
|
||||
/// directory, 8.3-name rename only for now). Returns true on success.
|
||||
pub fn rename(old_path: []const u8, new_path: []const u8) bool {
|
||||
const total = old_path.len + 1 + new_path.len;
|
||||
if (total > protocol.maximum_payload) return false;
|
||||
var payload: [protocol.maximum_payload]u8 = undefined;
|
||||
@memcpy(payload[0..old_path.len], old_path);
|
||||
payload[old_path.len] = 0;
|
||||
@memcpy(payload[old_path.len + 1 ..][0..new_path.len], new_path);
|
||||
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
||||
const r = transact(request, payload[0..total], &.{}) orelse return false;
|
||||
return r.reply.status == 0;
|
||||
}
|
||||
|
||||
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
||||
/// the VFS then routes everything under `target` to that backend. This is the one
|
||||
/// call that hands the VFS a capability (the backend endpoint). Returns true on
|
||||
/// success.
|
||||
pub fn mount(target: []const u8, backend: ipc.Handle) bool {
|
||||
const h = vfs() orelse return false;
|
||||
const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 };
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const tlen = @min(target.len, protocol.maximum_payload);
|
||||
@memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]);
|
||||
var rbuf: [protocol.message_maximum]u8 = undefined;
|
||||
const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return false;
|
||||
if (result.len < protocol.reply_size) return false;
|
||||
return std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0;
|
||||
}
|
||||
@@ -13,10 +13,10 @@
|
||||
//! lock and larger alignments come when user programs gain threads.
|
||||
|
||||
const std = @import("std");
|
||||
const danos = @import("danos");
|
||||
const system = @import("system.zig");
|
||||
const abi = @import("abi");
|
||||
const system_calls = @import("system.zig");
|
||||
|
||||
const page_size = danos.page_size;
|
||||
const page_size = abi.page_size;
|
||||
|
||||
/// A block header, at the start of every block; while free it also links the
|
||||
/// free list via `next`.
|
||||
@@ -46,8 +46,8 @@ fn payloadOf(block: *Block) [*]u8 {
|
||||
/// grants usually are adjacent). Returns false if the kernel is out of memory.
|
||||
fn grow(minimum_bytes: usize) bool {
|
||||
const bytes = alignUp(@max(minimum_bytes, chunk), page_size);
|
||||
const ret = system.mmap(bytes, system.PROT_READ | system.PROT_WRITE);
|
||||
if (system.mmapFailed(ret)) return false;
|
||||
const ret = system_calls.mmap(bytes, system_calls.PROT_READ | system_calls.PROT_WRITE);
|
||||
if (system_calls.mmapFailed(ret)) return false;
|
||||
|
||||
const block: *Block = @ptrFromInt(ret);
|
||||
block.size = bytes;
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
//! User-space input helpers: the client and publisher sides of the input service, so a
|
||||
//! program listening for input events — or a driver broadcasting them — doesn't hand-roll
|
||||
//! the IPC. Layered over `ipc` (endpoints, capability passing, `send`) and the shared
|
||||
//! `input-protocol` wire format, the way `device.zig` layers over the raw `device_*` calls.
|
||||
//! See system/services/input/input.zig.
|
||||
//!
|
||||
//! The service carries several device classes (keyboard, mouse, joystick/gamepad). A
|
||||
//! **source** publishes its class with the matching method:
|
||||
//! var source = input.connectSource() orelse return;
|
||||
//! _ = source.publishKeyboardEvent(.{ .kind = ..., .keycode = ..., ... });
|
||||
//! _ = source.publishMouseEvent(.{ ... });
|
||||
//! _ = source.publishJoystickEvent(.{ ... });
|
||||
//!
|
||||
//! A **subscriber** either takes one class with a typed helper —
|
||||
//! var keys = input.subscribeKeyboard() orelse return;
|
||||
//! while (true) { const key = keys.next() orelse continue; ... }
|
||||
//! — or takes several at once and inspects the tagged envelope:
|
||||
//! var listener = input.subscribeAll() orelse return;
|
||||
//! while (true) {
|
||||
//! const event = listener.next() orelse continue;
|
||||
//! if (event.asKeyboard()) |k| { ... } else if (event.asMouse()) |m| { ... }
|
||||
//! }
|
||||
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const ipc = @import("ipc.zig");
|
||||
const system = @import("system.zig");
|
||||
const protocol = @import("input-protocol");
|
||||
|
||||
pub const DeviceKind = protocol.DeviceKind;
|
||||
pub const InputEvent = protocol.InputEvent;
|
||||
pub const KeyEvent = protocol.KeyEvent;
|
||||
pub const MouseEvent = protocol.MouseEvent;
|
||||
pub const JoystickEvent = protocol.JoystickEvent;
|
||||
pub const EventKind = protocol.EventKind;
|
||||
pub const MouseEventKind = protocol.MouseEventKind;
|
||||
pub const JoystickEventKind = protocol.JoystickEventKind;
|
||||
pub const Keycode = protocol.Keycode;
|
||||
|
||||
/// Interest masks re-exported so a caller can `subscribe(input.device_keyboard |
|
||||
/// input.device_mouse)`.
|
||||
pub const device_keyboard = protocol.device_keyboard;
|
||||
pub const device_mouse = protocol.device_mouse;
|
||||
pub const device_joystick = protocol.device_joystick;
|
||||
pub const device_all = protocol.device_all;
|
||||
|
||||
/// Look up the input service, retrying while it is still coming up. Both a subscriber and
|
||||
/// a source race the service's registration at boot, so both wait for it here rather than
|
||||
/// failing. Returns the service endpoint handle, or null if it never appears.
|
||||
fn lookupService() ?ipc.Handle {
|
||||
var attempts: usize = 0;
|
||||
while (attempts < 100) : (attempts += 1) {
|
||||
if (ipc.lookup(.input)) |handle| return handle;
|
||||
system.sleep(50);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- subscribing ------------------------------------------------------------
|
||||
|
||||
/// A subscription to the input service: our own endpoint, which the service pushes events
|
||||
/// to. `next` returns each event as a tagged `InputEvent`; use `asKeyboard`/`asMouse`/
|
||||
/// `asJoystick` to decode. Created with `subscribe`/`subscribeAll`; for a single device
|
||||
/// class prefer the typed helpers (`subscribeKeyboard`, ...), which return decoded events.
|
||||
pub const Subscriber = struct {
|
||||
/// The endpoint the service delivers events to (created and owned by us; its handle
|
||||
/// was handed to the service as a capability at subscribe time).
|
||||
endpoint: ipc.Handle,
|
||||
receive: [protocol.event_size]u8 = undefined,
|
||||
|
||||
/// Block until the next event is pushed, and return it. Events arrive as asynchronous
|
||||
/// buffered messages (`ipc_send` from the service), so nothing is owed in reply — the
|
||||
/// empty reply this issues is a harmless no-op. Returns null for any non-event wake-up
|
||||
/// (there should be none), so callers can loop.
|
||||
pub fn next(self: *Subscriber) ?InputEvent {
|
||||
const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, null);
|
||||
if (!got.isMessage() or got.len < protocol.event_size) return null;
|
||||
return std.mem.bytesToValue(InputEvent, self.receive[0..protocol.event_size]);
|
||||
}
|
||||
};
|
||||
|
||||
/// Subscribe to the input classes named in `device_mask` (an OR of `device_*`, or
|
||||
/// `device_all`). Creates an endpoint for the service to push to and hands it over as a
|
||||
/// capability. Returns a `Subscriber` to loop `next` on, or null on failure.
|
||||
pub fn subscribe(device_mask: u32) ?Subscriber {
|
||||
const service = lookupService() orelse return null;
|
||||
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
||||
|
||||
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.subscribe), .device_mask = device_mask };
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const result = ipc.callCap(service, std.mem.asBytes(&request), &reply, endpoint) catch return null;
|
||||
if (result.len < protocol.reply_size) return null;
|
||||
if (std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status != 0) return null;
|
||||
return .{ .endpoint = endpoint };
|
||||
}
|
||||
|
||||
/// Subscribe to every input class (keyboard, mouse, joystick) on one stream.
|
||||
pub fn subscribeAll() ?Subscriber {
|
||||
return subscribe(device_all);
|
||||
}
|
||||
|
||||
/// A subscriber filtered to keyboard events, whose `next` returns a decoded `KeyEvent`.
|
||||
pub const KeyboardSubscriber = struct {
|
||||
inner: Subscriber,
|
||||
pub fn next(self: *KeyboardSubscriber) ?KeyEvent {
|
||||
return (self.inner.next() orelse return null).asKeyboard();
|
||||
}
|
||||
};
|
||||
|
||||
/// A subscriber filtered to mouse events, whose `next` returns a decoded `MouseEvent`.
|
||||
pub const MouseSubscriber = struct {
|
||||
inner: Subscriber,
|
||||
pub fn next(self: *MouseSubscriber) ?MouseEvent {
|
||||
return (self.inner.next() orelse return null).asMouse();
|
||||
}
|
||||
};
|
||||
|
||||
/// A subscriber filtered to joystick/gamepad events, whose `next` returns a decoded
|
||||
/// `JoystickEvent`.
|
||||
pub const JoystickSubscriber = struct {
|
||||
inner: Subscriber,
|
||||
pub fn next(self: *JoystickSubscriber) ?JoystickEvent {
|
||||
return (self.inner.next() orelse return null).asJoystick();
|
||||
}
|
||||
};
|
||||
|
||||
/// Subscribe to keyboard events only; `next` returns decoded `KeyEvent`s.
|
||||
pub fn subscribeKeyboard() ?KeyboardSubscriber {
|
||||
return .{ .inner = subscribe(device_keyboard) orelse return null };
|
||||
}
|
||||
|
||||
/// Subscribe to mouse events only; `next` returns decoded `MouseEvent`s.
|
||||
pub fn subscribeMouse() ?MouseSubscriber {
|
||||
return .{ .inner = subscribe(device_mouse) orelse return null };
|
||||
}
|
||||
|
||||
/// Subscribe to joystick/gamepad events only; `next` returns decoded `JoystickEvent`s.
|
||||
pub fn subscribeJoystick() ?JoystickSubscriber {
|
||||
return .{ .inner = subscribe(device_joystick) orelse return null };
|
||||
}
|
||||
|
||||
// --- publishing -------------------------------------------------------------
|
||||
|
||||
/// A connection to the input service for a source (a keyboard/mouse/joystick driver) that
|
||||
/// publishes events. Each `publish*Event` is a short synchronous call the service answers
|
||||
/// at once; its own fan-out to subscribers is asynchronous, so publishing never blocks on
|
||||
/// a slow subscriber.
|
||||
pub const Publisher = struct {
|
||||
service: ipc.Handle,
|
||||
|
||||
fn publish(self: Publisher, event: InputEvent) bool {
|
||||
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.publish), .event = event };
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const len = ipc.call(self.service, std.mem.asBytes(&request), &reply) catch return false;
|
||||
if (len < protocol.reply_size) return false;
|
||||
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
|
||||
}
|
||||
|
||||
/// Broadcast a keyboard event to every subscriber that took keyboard events.
|
||||
pub fn publishKeyboardEvent(self: Publisher, event: KeyEvent) bool {
|
||||
return self.publish(InputEvent.fromKeyboard(event));
|
||||
}
|
||||
/// Broadcast a mouse event to every subscriber that took mouse events.
|
||||
pub fn publishMouseEvent(self: Publisher, event: MouseEvent) bool {
|
||||
return self.publish(InputEvent.fromMouse(event));
|
||||
}
|
||||
/// Broadcast a joystick/gamepad event to every subscriber that took joystick events.
|
||||
pub fn publishJoystickEvent(self: Publisher, event: JoystickEvent) bool {
|
||||
return self.publish(InputEvent.fromJoystick(event));
|
||||
}
|
||||
};
|
||||
|
||||
/// Connect to the input service as an event source, waiting for it to come up. Returns a
|
||||
/// `Publisher`, or null if the service never registered.
|
||||
pub fn connectSource() ?Publisher {
|
||||
return .{ .service = lookupService() orelse return null };
|
||||
}
|
||||
|
||||
// --- synthetic scaffolding --------------------------------------------------
|
||||
|
||||
/// Synthetic key events, shared by the demo source and the keyboard driver's placeholder
|
||||
/// stream while real scancode decoding is still a follow-up. `step` rolls through A..E,
|
||||
/// emitting for each key a `key_down`, then a `key_press` carrying the character, then a
|
||||
/// `key_up`. Scaffolding, not wire protocol — hence it lives with the helpers.
|
||||
pub fn syntheticKeyEvent(step: usize) KeyEvent {
|
||||
const Key = struct { code: Keycode, character: u32 };
|
||||
const keys = [_]Key{
|
||||
.{ .code = .a, .character = 'A' },
|
||||
.{ .code = .b, .character = 'B' },
|
||||
.{ .code = .c, .character = 'C' },
|
||||
.{ .code = .d, .character = 'D' },
|
||||
.{ .code = .e, .character = 'E' },
|
||||
};
|
||||
const key = keys[(step / 3) % keys.len];
|
||||
return switch (step % 3) {
|
||||
0 => .{ .kind = @intFromEnum(EventKind.key_down), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
|
||||
1 => .{ .kind = @intFromEnum(EventKind.key_press), .keycode = @intFromEnum(key.code), .character = key.character, .modifiers = 0 },
|
||||
else => .{ .kind = @intFromEnum(EventKind.key_up), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/// Synthetic mouse events (placeholder until real PS/2 packet decoding). `step` alternates
|
||||
/// a small diagonal motion with a left-button click.
|
||||
pub fn syntheticMouseEvent(step: usize) MouseEvent {
|
||||
return switch (step % 3) {
|
||||
0 => .{ .kind = @intFromEnum(MouseEventKind.motion), .button = 0, .dx = 1, .dy = 1, .scroll_x = 0, .scroll_y = 0, .buttons = 0 },
|
||||
1 => .{ .kind = @intFromEnum(MouseEventKind.button_down), .button = protocol.mouse_button_left, .dx = 0, .dy = 0, .scroll_x = 0, .scroll_y = 0, .buttons = protocol.mouse_button_left },
|
||||
else => .{ .kind = @intFromEnum(MouseEventKind.button_up), .button = protocol.mouse_button_left, .dx = 0, .dy = 0, .scroll_x = 0, .scroll_y = 0, .buttons = 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/// Synthetic joystick/gamepad events (placeholder until a real controller driver). `step`
|
||||
/// sweeps axis 0 and toggles button 0.
|
||||
pub fn syntheticJoystickEvent(step: usize) JoystickEvent {
|
||||
return switch (step % 3) {
|
||||
0 => .{ .kind = @intFromEnum(JoystickEventKind.axis), .control = 0, .value = 16384, .buttons = 0 },
|
||||
1 => .{ .kind = @intFromEnum(JoystickEventKind.button_down), .control = 0, .value = 0, .buttons = 1 },
|
||||
else => .{ .kind = @intFromEnum(JoystickEventKind.button_up), .control = 0, .value = 0, .buttons = 0 },
|
||||
};
|
||||
}
|
||||
+124
-24
@@ -3,7 +3,7 @@
|
||||
//! reached this way. The server side (`replyWait`, which returns two values) is
|
||||
//! added with the first server binary.
|
||||
|
||||
const danos = @import("danos");
|
||||
const abi = @import("abi");
|
||||
const sc = @import("system-call.zig");
|
||||
|
||||
/// A small-int handle into the calling process's handle table.
|
||||
@@ -24,71 +24,171 @@ inline fn failed(r: usize) bool {
|
||||
}
|
||||
|
||||
/// Create a new endpoint owned by this process; returns its handle.
|
||||
pub fn createEndpoint() ?Handle {
|
||||
const r = sc.systemCall0(.create_endpoint);
|
||||
pub fn createIpcEndpoint() ?Handle {
|
||||
const r = sc.systemCall0(.create_ipc_endpoint);
|
||||
return if (failed(r)) null else r;
|
||||
}
|
||||
|
||||
/// Publish endpoint `h` under a well-known service id so other processes find it.
|
||||
pub fn register(id: danos.ServiceId, h: Handle) bool {
|
||||
pub fn register(id: abi.ServiceId, h: Handle) bool {
|
||||
return !failed(sc.systemCall2(.ipc_register, @intFromEnum(id), h));
|
||||
}
|
||||
|
||||
/// Find the endpoint published under `id`, installing a handle to it in this
|
||||
/// process.
|
||||
pub fn lookup(id: danos.ServiceId) ?Handle {
|
||||
pub fn lookup(id: abi.ServiceId) ?Handle {
|
||||
const r = sc.systemCall1(.ipc_lookup, @intFromEnum(id));
|
||||
return if (failed(r)) null else r;
|
||||
}
|
||||
|
||||
pub const CallError = error{Failed};
|
||||
|
||||
/// The result of a capability-passing `callCap`: the reply length, and the handle of
|
||||
/// an endpoint the server sent back (e.g. a per-device channel), or null.
|
||||
pub const Reply = struct {
|
||||
len: usize,
|
||||
cap: ?Handle,
|
||||
};
|
||||
|
||||
/// Send `message` to endpoint `h` and block until the server replies into `reply`,
|
||||
/// optionally handing the server a capability (`send_cap`) and receiving one back.
|
||||
/// This is the class-driver "open" primitive: call a bus with `send_cap = null`, get a
|
||||
/// private per-device endpoint back in `.cap`. Two return values (reply length in rax,
|
||||
/// received handle in r8) need a hand-written stub — r8 is read-write (in: reply
|
||||
/// capacity, arg #4; out: the received handle).
|
||||
pub fn callCap(h: Handle, message: []const u8, reply: []u8, send_cap: ?Handle) CallError!Reply {
|
||||
var rax: usize = undefined;
|
||||
var r8: usize = reply.len; // in: reply capacity (arg #4); out: received capability handle
|
||||
asm volatile ("syscall"
|
||||
: [rax] "={rax}" (rax),
|
||||
[r8] "+{r8}" (r8),
|
||||
: [n] "{rax}" (@intFromEnum(abi.SystemCall.ipc_call)),
|
||||
[a0] "{rdi}" (h),
|
||||
[a1] "{rsi}" (@intFromPtr(message.ptr)),
|
||||
[a2] "{rdx}" (message.len),
|
||||
[a3] "{r10}" (@intFromPtr(reply.ptr)),
|
||||
[a5] "{r9}" (send_cap orelse abi.no_cap),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
if (failed(rax)) return error.Failed;
|
||||
return .{ .len = rax, .cap = if (r8 == abi.no_cap) null else r8 };
|
||||
}
|
||||
|
||||
/// Send `message` to endpoint `h` and block until the server replies into `reply`.
|
||||
/// Returns the reply length.
|
||||
/// Returns the reply length. The common case: no capability passed either way.
|
||||
pub fn call(h: Handle, message: []const u8, reply: []u8) CallError!usize {
|
||||
const r = sc.systemCall5(.ipc_call, h, @intFromPtr(message.ptr), message.len, @intFromPtr(reply.ptr), reply.len);
|
||||
return if (failed(r)) error.Failed else r;
|
||||
return (try callCap(h, message, reply, null)).len;
|
||||
}
|
||||
|
||||
/// Post `message` to endpoint `h`'s asynchronous queue and return immediately — no
|
||||
/// rendezvous, no reply, no blocking. The receiver picks it up through `replyWait` as a
|
||||
/// buffered message (`Received.isMessage`). Unlike `call`, this **cannot hang on a dead
|
||||
/// or slow peer**, which is why a broadcaster (the input service) delivers events this
|
||||
/// way. The payload must fit an endpoint slot (64 bytes); a full queue drops the oldest
|
||||
/// message. Returns false on failure (bad handle, oversized payload, bad buffer).
|
||||
pub fn send(h: Handle, message: []const u8) bool {
|
||||
return !failed(sc.systemCall3(.ipc_send, h, @intFromPtr(message.ptr), message.len));
|
||||
}
|
||||
|
||||
/// Set in `Received.badge` when what arrived is an asynchronous notification — a
|
||||
/// bound device interrupt — rather than a client's message. The low bits carry the
|
||||
/// GSI. See `isNotification`.
|
||||
pub const notify_badge_bit: u64 = danos.notify_badge_bit;
|
||||
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
||||
|
||||
/// The result of a `replyWait`: the request length and the sender's badge (a
|
||||
/// task id, or an IRQ notification if the high bit is set).
|
||||
/// Set alongside `notify_badge_bit` when the notification is a **signal** — the
|
||||
/// lifecycle vocabulary of docs/process-lifecycle.md, delivered to the endpoint
|
||||
/// nominated with `process.bindSignals`. Decode with `process.signalsFrom`.
|
||||
pub const notify_signal_bit: u64 = abi.notify_signal_bit;
|
||||
|
||||
/// Set alongside `notify_badge_bit` when the notification is a **one-shot timer**
|
||||
/// landing (`system.timerOnce`).
|
||||
pub const notify_timer_bit: u64 = abi.notify_timer_bit;
|
||||
|
||||
/// Set alongside `notify_badge_bit` when the notification is a **child-exit
|
||||
/// notice** — a process this one spawned (with an exit endpoint) has ended —
|
||||
/// rather than a device interrupt. The low bits carry the child's process id.
|
||||
pub const notify_exit_bit: u64 = abi.notify_exit_bit;
|
||||
|
||||
/// Set alongside `notify_badge_bit` when the wake-up is a **buffered message** — a payload
|
||||
/// posted with `send` (`ipc_send`) — rather than a bare device interrupt or child-exit
|
||||
/// notice. The payload is in the `replyWait` receive buffer (`Received.len` bytes); the
|
||||
/// low bits of the badge carry the sender's task id. See `Received.isMessage`.
|
||||
pub const notify_message_bit: u64 = abi.notify_message_bit;
|
||||
|
||||
/// The result of a `replyWait`: the request length, the sender's badge (a task id, or
|
||||
/// an IRQ notification if the high bit is set), and any capability the request carried.
|
||||
pub const Received = struct {
|
||||
len: usize,
|
||||
badge: u64,
|
||||
cap: ?Handle,
|
||||
|
||||
/// True if this wake-up was a device interrupt, not a client request. A driver's
|
||||
/// event loop branches on this; there is no reply owed on the notification path.
|
||||
/// True if this wake-up was an asynchronous notification (a device interrupt
|
||||
/// or a child-exit notice), not a client request. An event loop branches on
|
||||
/// this; there is no reply owed on the notification path.
|
||||
pub fn isNotification(self: Received) bool {
|
||||
return self.badge & notify_badge_bit != 0;
|
||||
}
|
||||
|
||||
/// The interrupt source (a GSI), meaningful only when `isNotification`.
|
||||
/// True if this wake-up tells of a supervised child's end — the notification
|
||||
/// requested by passing an exit endpoint to `system.spawnSupervised`.
|
||||
pub fn isChildExit(self: Received) bool {
|
||||
return self.isNotification() and self.badge & notify_exit_bit != 0;
|
||||
}
|
||||
|
||||
/// True if this wake-up is a **buffered message** posted with `send` (`ipc_send`):
|
||||
/// there is a payload in the receive buffer (`self.len` bytes) and no reply is owed.
|
||||
/// The subscriber side of a broadcast branches on this.
|
||||
pub fn isMessage(self: Received) bool {
|
||||
return self.isNotification() and self.badge & notify_message_bit != 0;
|
||||
}
|
||||
|
||||
/// The task id of whoever posted a buffered message, meaningful only when
|
||||
/// Whether this arrival is a signal notification — decode the set with
|
||||
/// `process.signalsFrom(badge)`.
|
||||
pub fn isSignal(self: Received) bool {
|
||||
return self.isNotification() and self.badge & notify_signal_bit != 0;
|
||||
}
|
||||
|
||||
/// Whether this arrival is a one-shot timer landing (`system.timerOnce`).
|
||||
pub fn isTimer(self: Received) bool {
|
||||
return self.isNotification() and self.badge & notify_timer_bit != 0;
|
||||
}
|
||||
|
||||
/// `isMessage`. (The badge's low bits, with the three high marker bits masked off.)
|
||||
pub fn senderTaskId(self: Received) u32 {
|
||||
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit | notify_message_bit));
|
||||
}
|
||||
|
||||
/// The interrupt source (a GSI), meaningful only when `isNotification` and
|
||||
/// not `isChildExit`.
|
||||
pub fn source(self: Received) u64 {
|
||||
return self.badge & ~notify_badge_bit;
|
||||
}
|
||||
|
||||
/// The ended child's process id, meaningful only when `isChildExit`.
|
||||
pub fn childProcessId(self: Received) u32 {
|
||||
return @intCast(self.badge & ~(notify_badge_bit | notify_exit_bit));
|
||||
}
|
||||
};
|
||||
|
||||
/// Server side of IPC_ReplyWait: deliver `reply` to the client last received (if
|
||||
/// any), then block until the next request arrives in `receive`. Returns its length
|
||||
/// and the sender badge. This system_call returns two values — the length in rax and
|
||||
/// the badge in rdx — so it needs a hand-written stub: rdx is a read-write
|
||||
/// operand (input = reply length, arg #3; output = badge).
|
||||
pub fn replyWait(h: Handle, reply: []const u8, receive: []u8) Received {
|
||||
/// Server side of IPC_ReplyWait: deliver `reply` to the client last received (if any,
|
||||
/// optionally handing it `send_cap`), then block until the next request arrives in
|
||||
/// `receive`. Returns its length, the sender badge, and any capability the request
|
||||
/// carried (in `.cap`). Three return values — length in rax, badge in rdx, received
|
||||
/// handle in r8 — so it needs a hand-written stub: rdx is read-write (in: reply length,
|
||||
/// arg #3; out: badge) and r8 is read-write (in: receive capacity, arg #4; out: handle).
|
||||
pub fn replyWait(h: Handle, reply: []const u8, receive: []u8, send_cap: ?Handle) Received {
|
||||
var rax: usize = undefined;
|
||||
var rdx: usize = reply.len; // in: reply_len (arg #3 -> rdx); out: badge
|
||||
var rdx: usize = reply.len; // in: reply_len (arg #3); out: badge
|
||||
var r8: usize = receive.len; // in: receive capacity (arg #4); out: received capability handle
|
||||
asm volatile ("syscall"
|
||||
: [rax] "={rax}" (rax),
|
||||
[rdx] "+{rdx}" (rdx),
|
||||
: [n] "{rax}" (@intFromEnum(danos.SystemCall.ipc_reply_wait)),
|
||||
[r8] "+{r8}" (r8),
|
||||
: [n] "{rax}" (@intFromEnum(abi.SystemCall.ipc_reply_wait)),
|
||||
[a0] "{rdi}" (h),
|
||||
[a1] "{rsi}" (@intFromPtr(reply.ptr)),
|
||||
[a3] "{r10}" (@intFromPtr(receive.ptr)),
|
||||
[a4] "{r8}" (receive.len),
|
||||
[a5] "{r9}" (send_cap orelse abi.no_cap),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
return .{ .len = rax, .badge = rdx };
|
||||
return .{ .len = rax, .badge = rdx, .cap = if (r8 == abi.no_cap) null else r8 };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! Process-level runtime types: what a user program receives at entry (`Init`,
|
||||
//! the argv contract) and the process end of the lifecycle
|
||||
//! (docs/process-lifecycle.md) — today the exit reason a supervisor reads to
|
||||
//! decide restart; signals and the stop sequence land here with M17.4. Mirrors
|
||||
//! the spirit of `std.process.Init.Minimal` in danos terms — std's `Args` holds
|
||||
//! no data on freestanding targets, so the type is danos's own.
|
||||
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const sc = @import("system-call.zig");
|
||||
const ipc = @import("ipc.zig");
|
||||
const system = @import("system.zig");
|
||||
|
||||
/// Everything a program receives at entry. Passed to
|
||||
/// `pub fn main(init: runtime.process.Init)`; programs that need nothing keep
|
||||
/// `pub fn main() void`. An `environment` field is added here once the kernel
|
||||
/// passes a non-empty envp (today it is always empty — see docs/sysv.md).
|
||||
pub const Init = struct {
|
||||
arguments: Arguments,
|
||||
};
|
||||
|
||||
/// The process arguments (argc/argv), parsed from the kernel-built System V
|
||||
/// entry block. The bytes live in the entry block at the top of the stack page,
|
||||
/// NUL-terminated, valid for the process's lifetime.
|
||||
pub const Arguments = struct {
|
||||
/// argc — at least 1: argument 0 is the path or name this binary was
|
||||
/// spawned as.
|
||||
count: usize,
|
||||
/// The argv pointers in the entry block (NULL-terminated after `count`
|
||||
/// entries).
|
||||
vector: [*]const [*:0]const u8,
|
||||
|
||||
/// Argument `index` (0 = the program's own path/name), or null if out of
|
||||
/// range.
|
||||
pub fn get(arguments: Arguments, index: usize) ?[:0]const u8 {
|
||||
if (index >= arguments.count) return null;
|
||||
return std.mem.span(arguments.vector[index]);
|
||||
}
|
||||
|
||||
pub fn iterate(arguments: Arguments) Iterator {
|
||||
return .{ .arguments = arguments };
|
||||
}
|
||||
|
||||
pub const Iterator = struct {
|
||||
arguments: Arguments,
|
||||
index: usize = 0,
|
||||
|
||||
pub fn next(iterator: *Iterator) ?[:0]const u8 {
|
||||
const argument = iterator.arguments.get(iterator.index) orelse return null;
|
||||
iterator.index += 1;
|
||||
return argument;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/// How a process ended — what a supervisor's restart policy reads: a clean exit
|
||||
/// meant to stop, a fault wants a restart with backoff, killed means the
|
||||
/// supervisor did it itself (docs/process-lifecycle.md).
|
||||
pub const ExitReason = abi.ExitReason;
|
||||
|
||||
/// How dead child `id` ended. Ask after the exit notification arrives — the
|
||||
/// kernel records the reason before it posts the notification, so this never
|
||||
/// races it. Returns null for an id that never lived, is still alive, was
|
||||
/// evicted from the kernel's bounded record, or is not this process's child
|
||||
/// (the same authority gate as `kill`).
|
||||
pub fn exitReason(id: u32) ?ExitReason {
|
||||
const r = sc.systemCall1(.process_exit_reason, id);
|
||||
if (r > ~@as(usize, 0) - 4095) return null; // a wrapped -errno
|
||||
return @enumFromInt(r);
|
||||
}
|
||||
|
||||
/// The signal vocabulary (docs/process-lifecycle.md): POSIX's concepts, danos's
|
||||
/// names, message delivery. A signal is a one-way coalescing statement — never a
|
||||
/// question (liveness is the zero-length ping call) and never kill (that is
|
||||
/// `system.kill`, unhandleable by definition).
|
||||
pub const Signal = abi.Signal;
|
||||
|
||||
/// The coalesced set of signals one notification delivered: two pending
|
||||
/// terminates arrive as one. Decode a received badge with `signalsFrom`.
|
||||
pub const SignalSet = struct {
|
||||
pending: u32,
|
||||
|
||||
pub fn has(set: SignalSet, signal: Signal) bool {
|
||||
return set.pending & (@as(u32, 1) << @intFromEnum(signal)) != 0;
|
||||
}
|
||||
};
|
||||
|
||||
/// Nominate `endpoint` as this process's signal endpoint. Signals posted while
|
||||
/// unbound have pended; they are delivered immediately on bind, coalesced.
|
||||
pub fn bindSignals(endpoint: usize) bool {
|
||||
return sc.systemCall1(.signal_bind, endpoint) == 0;
|
||||
}
|
||||
|
||||
/// Decode a received badge into the signals it delivered, or null if it is not
|
||||
/// a signal notification.
|
||||
pub fn signalsFrom(badge: u64) ?SignalSet {
|
||||
if (badge & abi.notify_badge_bit == 0 or badge & abi.notify_signal_bit == 0) return null;
|
||||
return .{ .pending = @truncate(badge & ~(abi.notify_badge_bit | abi.notify_signal_bit)) };
|
||||
}
|
||||
|
||||
/// Post `signal` to child `id` (or to yourself). Supervisor-gated, like kill;
|
||||
/// non-blocking, always — a statement, not a conversation.
|
||||
pub fn sendSignal(id: u32, signal: Signal) bool {
|
||||
return sc.systemCall2(.process_signal, id, @intFromEnum(signal)) == 0;
|
||||
}
|
||||
|
||||
/// The standard stop sequence (docs/process-lifecycle.md): terminate, wait up to
|
||||
/// `deadline_ms` for the exit notification on `exit_endpoint` (the endpoint the
|
||||
/// child was spawned with), then kill. Any *other* notifications arriving on
|
||||
/// that endpoint while stopping are consumed and dropped — a supervisor with
|
||||
/// concurrent traffic implements the same sequence inside its own event loop
|
||||
/// (arm `system.timerOnce`, keep serving) instead of calling this.
|
||||
pub fn stop(id: u32, deadline_ms: u64, exit_endpoint: usize) void {
|
||||
_ = sendSignal(id, .terminate);
|
||||
_ = system.timerOnce(exit_endpoint, deadline_ms);
|
||||
var receive: [8]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(exit_endpoint, &.{}, &receive, null);
|
||||
if (got.isChildExit() and got.childProcessId() == id) return;
|
||||
if (got.isTimer()) break; // the deadline passed first — escalate
|
||||
}
|
||||
_ = system.kill(id);
|
||||
while (true) {
|
||||
const got = ipc.replyWait(exit_endpoint, &.{}, &receive, null);
|
||||
if (got.isChildExit() and got.childProcessId() == id) return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe `endpoint` to published exit events: every process death posts an
|
||||
/// asynchronous notification with the same badge encoding as a supervisor's exit
|
||||
/// notice (decode with `ipc.Received.isChildExit`/`childProcessId`). For stateful
|
||||
/// services: release what the dead client held — file handles, subscriptions —
|
||||
/// because a service must never depend on clients cleaning up after themselves
|
||||
/// (docs/process-lifecycle.md). Ungated, like `system.processes`.
|
||||
pub fn subscribeExits(endpoint: usize) bool {
|
||||
return sc.systemCall1(.process_subscribe, endpoint) == 0;
|
||||
}
|
||||
@@ -8,23 +8,58 @@
|
||||
//! const runtime = @import("runtime");
|
||||
//! pub const panic = runtime.panic;
|
||||
//! comptime { _ = &runtime.start._start; } // pull the entry shim in
|
||||
//! and a `pub fn main() void`.
|
||||
//! and a `pub fn main() void` or `pub fn main(init: runtime.process.Init) void`
|
||||
//! (arguments arrive via `init`).
|
||||
|
||||
pub const system = @import("system.zig");
|
||||
/// Monotonic time, delays, and deadlines over the kernel clock/sleep/timer syscalls
|
||||
/// — an `Instant`/`Duration` front door, no time service (docs/timers.md).
|
||||
pub const time = @import("time.zig");
|
||||
pub const heap = @import("heap.zig");
|
||||
pub const ipc = @import("ipc.zig");
|
||||
pub const start = @import("start.zig");
|
||||
/// The VFS wire protocol (shared with the VFS server).
|
||||
pub const vfs_protocol = @import("vfs-protocol");
|
||||
|
||||
/// The device-manager protocol: hello + tree reports (docs/device-manager.md).
|
||||
pub const device_manager_protocol = @import("device-manager-protocol");
|
||||
|
||||
/// The power protocol: events (button, lid, battery) + shutdown (docs/power.md).
|
||||
pub const power_protocol = @import("power-protocol");
|
||||
/// Keyboard-event listening (subscribe/next) and broadcasting (publish), over the input
|
||||
/// service. See library/runtime/input.zig and system/services/input/.
|
||||
pub const input = @import("input.zig");
|
||||
/// The input wire protocol (shared with the input service and its clients).
|
||||
pub const input_protocol = @import("input-protocol");
|
||||
/// POSIX-style file API: open/read/write/lseek/stat/close.
|
||||
pub const unistd = @import("unistd.zig");
|
||||
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
|
||||
pub const stdio = @import("stdio.zig");
|
||||
/// Device access for drivers: enumerate/claim/mmioMap.
|
||||
pub const device = @import("device.zig");
|
||||
/// DMA-capable memory for drivers: contiguous, pinned, uncacheable buffers.
|
||||
pub const dma = @import("dma.zig");
|
||||
|
||||
/// USB class-driver client: open a device on the xHCI bus and drive it
|
||||
/// (control / interrupt / bulk transfers). See library/runtime/usb.zig.
|
||||
pub const usb = @import("usb.zig");
|
||||
|
||||
/// Block-device client: read/write a block device (a USB stick, via
|
||||
/// usb-storage). See library/runtime/block.zig.
|
||||
pub const block = @import("block.zig");
|
||||
|
||||
/// The danos-native file API (open/read/write/list over the user-space VFS) — the
|
||||
/// layer danos programs use directly, and where the operations that later become
|
||||
/// `std.os.danos` are staged. See docs/zig-self-hosting.md.
|
||||
pub const fs = @import("fs.zig");
|
||||
|
||||
/// Re-exported so a user binary can `pub const panic = runtime.panic;`.
|
||||
pub const panic = start.panic;
|
||||
|
||||
/// Process entry types: the `Init` handed to `main`, and its `Arguments`.
|
||||
pub const process = @import("process.zig");
|
||||
|
||||
/// The service harness: one replyWait loop folding requests, signals, and
|
||||
/// notifications into callbacks (docs/process-lifecycle.md).
|
||||
pub const service = @import("service.zig");
|
||||
|
||||
/// The heap as a `std.mem.Allocator`, for Zig `std` containers in user code.
|
||||
pub const allocator = heap.allocator;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! The service harness (docs/process-lifecycle.md): one replyWait loop that
|
||||
//! folds protocol requests, signals, and subscribed notifications into
|
||||
//! callbacks — so the lifecycle contract ("answers ping, exits on terminate")
|
||||
//! is satisfied by construction and a service author writes domain logic only.
|
||||
//! Nothing is asynchronous inside the process: a callback runs at a point the
|
||||
//! loop chose, never on a hijacked stack — the whole reason signals are
|
||||
//! messages.
|
||||
//!
|
||||
//! The liveness probe: a **zero-length request is the universal ping**, answered
|
||||
//! with a zero-length reply by the harness itself. No protocol's requests start
|
||||
//! at length zero, so the encoding cannot collide, and there is nothing for a
|
||||
//! service author to implement — a wedged service simply fails to answer, which
|
||||
//! is the diagnosis (see docs/ipc.md).
|
||||
|
||||
const abi = @import("abi");
|
||||
const ipc = @import("ipc.zig");
|
||||
const process = @import("process.zig");
|
||||
|
||||
pub const Callbacks = struct {
|
||||
/// Called once with the service's endpoint before the loop starts — the
|
||||
/// place to subscribe to exit events, bind IRQs, or announce readiness.
|
||||
/// Return false to abort startup (the process exits).
|
||||
init: ?*const fn (endpoint: ipc.Handle) bool = null,
|
||||
/// One protocol request from `sender` (a task id): write the reply into
|
||||
/// `reply`, return its length. `capability` is the handle the request
|
||||
/// carried, if any (M13 cap passing — how a subscriber hands over its
|
||||
/// endpoint). The zero-length ping never reaches this.
|
||||
on_message: *const fn (message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize,
|
||||
/// A notification that is not a signal — a subscribed exit event, a bound
|
||||
/// IRQ, a timer landing. The raw badge; decode with the ipc helpers.
|
||||
on_notification: ?*const fn (badge: u64) void = null,
|
||||
/// The reload signal. Default: ignored.
|
||||
on_reload: ?*const fn () void = null,
|
||||
/// The terminate signal, called before the loop returns. The clean exit is
|
||||
/// the return itself — never put *necessary* work here (iron rule 1: a kill
|
||||
/// arrives with no warning; this is for graceful extras only).
|
||||
on_terminate: ?*const fn () void = null,
|
||||
/// Publish the endpoint under a well-known service id at startup.
|
||||
service: ?abi.ServiceId = null,
|
||||
};
|
||||
|
||||
/// Run the service: create and (optionally) register the endpoint, bind signals
|
||||
/// to it, call `init`, then serve until `terminate` arrives — at which point the
|
||||
/// loop returns and main's return is the clean exit the supervisor reads as
|
||||
/// `ExitReason.exited`. `maximum_message` sizes the receive and reply buffers
|
||||
/// (a service passes its protocol's message maximum).
|
||||
pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
|
||||
const endpoint = ipc.createIpcEndpoint() orelse return;
|
||||
if (callbacks.service) |id| {
|
||||
if (!ipc.register(id, endpoint)) return;
|
||||
}
|
||||
_ = process.bindSignals(endpoint);
|
||||
if (callbacks.init) |initialise| {
|
||||
if (!initialise(endpoint)) return;
|
||||
}
|
||||
|
||||
var reply_buffer: [maximum_message]u8 = undefined;
|
||||
var reply_len: usize = 0;
|
||||
var receive: [maximum_message]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||
if (got.isNotification()) {
|
||||
reply_len = 0; // nothing owed for a notification
|
||||
if (process.signalsFrom(got.badge)) |signals| {
|
||||
if (signals.has(.reload)) {
|
||||
if (callbacks.on_reload) |onReload| onReload();
|
||||
}
|
||||
if (signals.has(.terminate)) {
|
||||
if (callbacks.on_terminate) |onTerminate| onTerminate();
|
||||
return; // the loop's return IS the clean exit
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (callbacks.on_notification) |onNotification| onNotification(got.badge);
|
||||
continue;
|
||||
}
|
||||
if (got.len == 0) {
|
||||
reply_len = 0; // the universal ping: a zero-length reply, from the harness
|
||||
continue;
|
||||
}
|
||||
reply_len = callbacks.on_message(receive[0..got.len], &reply_buffer, got.senderTaskId(), got.cap);
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,78 @@
|
||||
|
||||
const std = @import("std");
|
||||
const system = @import("system.zig");
|
||||
const process = @import("process.zig");
|
||||
|
||||
/// The kernel enters at `_start` with rsp 16-aligned, but a SystemV function expects
|
||||
/// rsp ≡ 8 (mod 16) on entry (as if reached by `call`). The `call` below pushes
|
||||
/// the 8-byte return address, satisfying the ABI before any Zig frame runs; the
|
||||
/// `ud2` is a safety net if `rt_start` ever returns.
|
||||
/// The kernel enters at `_start` with rsp 16-aligned, pointing at the System V
|
||||
/// process-entry block it built: argc, argv pointers, NULL, envp terminator, the
|
||||
/// auxiliary vector, then the strings (see system/kernel/process.zig,
|
||||
/// `buildEntryStack`). Capture that address in rdi — the first SysV argument —
|
||||
/// before `call` disturbs the stack; the call's pushed return address also puts
|
||||
/// rsp ≡ 8 (mod 16), satisfying the ABI before any Zig frame runs. The `ud2` is a
|
||||
/// safety net if `rt_start` ever returns.
|
||||
pub export fn _start() callconv(.naked) noreturn {
|
||||
asm volatile (
|
||||
\\mov %%rsp, %%rdi
|
||||
\\call rt_start
|
||||
\\ud2
|
||||
);
|
||||
}
|
||||
|
||||
/// The first Zig frame. The heap is lazy (first alloc grows it), so there is no
|
||||
/// runtime init to order here — just hand control to the program's `main`.
|
||||
export fn rt_start() callconv(.c) noreturn {
|
||||
/// The first Zig frame, entered with `stack` pointing at the kernel-built entry
|
||||
/// block. Build the `process.Init` from it and dispatch to the program's `main`,
|
||||
/// whose signature is inspected at comptime. The heap is lazy (first alloc grows
|
||||
/// it), so there is no other runtime init to order here.
|
||||
export fn rt_start(stack: [*]const u64) callconv(.c) noreturn {
|
||||
const init: process.Init = .{ .arguments = .{
|
||||
.count = stack[0],
|
||||
.vector = @ptrCast(stack + 1),
|
||||
} };
|
||||
system.exit(callMain(init));
|
||||
}
|
||||
|
||||
/// Comptime-dispatch on root.main's signature, in the spirit of std's start.zig:
|
||||
/// zero parameters or one `process.Init`; returns void, noreturn, u8, !void, or !u8.
|
||||
fn callMain(init: process.Init) u8 {
|
||||
const root = @import("root"); // the user binary's root source file
|
||||
root.main();
|
||||
system.exit(0);
|
||||
const main_information = @typeInfo(@TypeOf(root.main)).@"fn";
|
||||
|
||||
const call_arguments = switch (main_information.params.len) {
|
||||
0 => .{},
|
||||
1 => arguments: {
|
||||
const Parameter = main_information.params[0].type orelse
|
||||
@compileError("main's parameter must be runtime.process.Init (not anytype)");
|
||||
if (Parameter != process.Init)
|
||||
@compileError("main's parameter must be runtime.process.Init, found " ++ @typeName(Parameter));
|
||||
break :arguments .{init};
|
||||
},
|
||||
else => @compileError("main takes no parameters or a single runtime.process.Init"),
|
||||
};
|
||||
|
||||
const ReturnType = main_information.return_type.?;
|
||||
switch (@typeInfo(ReturnType)) {
|
||||
.noreturn => @call(.auto, root.main, call_arguments),
|
||||
.void => {
|
||||
@call(.auto, root.main, call_arguments);
|
||||
return 0;
|
||||
},
|
||||
.int => {
|
||||
if (ReturnType != u8)
|
||||
@compileError("main's integer return type must be u8, found " ++ @typeName(ReturnType));
|
||||
return @call(.auto, root.main, call_arguments);
|
||||
},
|
||||
.error_union => {
|
||||
const payload = @call(.auto, root.main, call_arguments) catch |err| {
|
||||
var buffer: [128]u8 = undefined;
|
||||
const line = std.fmt.bufPrint(&buffer, "main returned error: {s}\n", .{@errorName(err)}) catch "main returned an error\n";
|
||||
_ = system.write(line);
|
||||
return 1; // distinct from panic's 127
|
||||
};
|
||||
if (@TypeOf(payload) == void) return 0;
|
||||
if (@TypeOf(payload) == u8) return payload;
|
||||
@compileError("main's error-union payload must be void or u8, found " ++ @typeName(@TypeOf(payload)));
|
||||
},
|
||||
else => @compileError("main must return void, noreturn, u8, !void, or !u8, found " ++ @typeName(ReturnType)),
|
||||
}
|
||||
}
|
||||
|
||||
/// No runtime to unwind into — report a panic as a nonzero exit code.
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
//! A small C stdio layer over the POSIX-style file API (unistd.zig). Unbuffered
|
||||
//! for now — each fread/fwrite is one VFS round trip; an internal buffer (fewer
|
||||
//! IPC calls) is a later optimisation. Both a Zig-callable API and `extern "C"`
|
||||
//! symbols are provided, so Zig and future C programs share it.
|
||||
|
||||
const std = @import("std");
|
||||
const unistd = @import("unistd.zig");
|
||||
const heap = @import("heap.zig");
|
||||
|
||||
pub const SEEK_SET = unistd.SEEK_SET;
|
||||
pub const SEEK_CURRENT = unistd.SEEK_CURRENT;
|
||||
pub const SEEK_END = unistd.SEEK_END;
|
||||
|
||||
/// A C `FILE`: an fd plus sticky end-of-file / error flags. Allocated on the
|
||||
/// heap; `fclose` frees it.
|
||||
pub const FILE = extern struct {
|
||||
fd: i32,
|
||||
eof: c_int = 0,
|
||||
err: c_int = 0,
|
||||
};
|
||||
|
||||
fn flagsFor(mode: []const u8) u32 {
|
||||
if (mode.len == 0) return 0;
|
||||
return switch (mode[0]) {
|
||||
'w', 'a' => unistd.O_CREAT,
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Open `path` in `mode` ("r"/"w"/"a", '+' ignored for now). Returns null on error.
|
||||
pub fn fopen(path: []const u8, mode: []const u8) ?*FILE {
|
||||
const fd = unistd.open(path, flagsFor(mode));
|
||||
if (fd < 0) return null;
|
||||
const f = heap.allocator().create(FILE) catch {
|
||||
unistd.close(fd);
|
||||
return null;
|
||||
};
|
||||
f.* = .{ .fd = fd };
|
||||
if (mode.len > 0 and mode[0] == 'a') _ = unistd.lseek(fd, 0, unistd.SEEK_END);
|
||||
return f;
|
||||
}
|
||||
|
||||
pub fn fclose(f: *FILE) c_int {
|
||||
unistd.close(f.fd);
|
||||
heap.allocator().destroy(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Read `size*nmemb` bytes; returns the number of whole items read.
|
||||
pub fn fread(buffer: []u8, size: usize, nmemb: usize, f: *FILE) usize {
|
||||
const total = size * nmemb;
|
||||
if (total == 0) return 0;
|
||||
const n = unistd.read(f.fd, buffer[0..@min(buffer.len, total)]);
|
||||
if (n <= 0) {
|
||||
f.eof = 1;
|
||||
return 0;
|
||||
}
|
||||
return @as(usize, @intCast(n)) / size;
|
||||
}
|
||||
|
||||
/// Write `size*nmemb` bytes; returns the number of whole items written.
|
||||
pub fn fwrite(data: []const u8, size: usize, nmemb: usize, f: *FILE) usize {
|
||||
const total = @min(data.len, size * nmemb);
|
||||
if (total == 0) return 0;
|
||||
const n = unistd.write(f.fd, data[0..total]);
|
||||
if (n <= 0) {
|
||||
f.err = 1;
|
||||
return 0;
|
||||
}
|
||||
return @as(usize, @intCast(n)) / size;
|
||||
}
|
||||
|
||||
pub fn fseek(f: *FILE, off: i64, whence: u32) c_int {
|
||||
f.eof = 0;
|
||||
return if (unistd.lseek(f.fd, off, whence) < 0) -1 else 0;
|
||||
}
|
||||
|
||||
pub fn ftell(f: *FILE) i64 {
|
||||
return unistd.lseek(f.fd, 0, unistd.SEEK_CURRENT);
|
||||
}
|
||||
|
||||
pub fn rewind(f: *FILE) void {
|
||||
_ = fseek(f, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
pub fn feof(f: *FILE) c_int {
|
||||
return f.eof;
|
||||
}
|
||||
|
||||
pub fn ferror(f: *FILE) c_int {
|
||||
return f.err;
|
||||
}
|
||||
|
||||
pub fn fputs(s: []const u8, f: *FILE) c_int {
|
||||
return if (unistd.write(f.fd, s) < 0) -1 else 0;
|
||||
}
|
||||
|
||||
pub fn fputc(c: u8, f: *FILE) c_int {
|
||||
const b = [_]u8{c};
|
||||
return if (unistd.write(f.fd, &b) == 1) c else -1;
|
||||
}
|
||||
|
||||
pub fn fgetc(f: *FILE) c_int {
|
||||
var b: [1]u8 = undefined;
|
||||
const n = unistd.read(f.fd, &b);
|
||||
if (n <= 0) {
|
||||
f.eof = 1;
|
||||
return -1; // EOF
|
||||
}
|
||||
return b[0];
|
||||
}
|
||||
|
||||
// Real `extern "C"` symbols (fopen/fread/fseek/...) — with a C-string signature
|
||||
// distinct from the Zig slice API above — land with the first C program, wired
|
||||
// via @export so they don't collide with these Zig names.
|
||||
@@ -6,8 +6,8 @@
|
||||
//! Note argument #3 goes in **r10, not rcx** — rcx is unavailable across the
|
||||
//! instruction, so the kernel reads the 4th argument from r10.
|
||||
|
||||
const danos = @import("danos");
|
||||
const SystemCall = danos.SystemCall;
|
||||
const abi = @import("abi");
|
||||
const SystemCall = abi.SystemCall;
|
||||
|
||||
pub inline fn systemCall0(n: SystemCall) usize {
|
||||
return asm volatile ("syscall"
|
||||
@@ -19,34 +19,49 @@ pub inline fn systemCall0(n: SystemCall) usize {
|
||||
pub inline fn systemCall1(n: SystemCall, a0: usize) usize {
|
||||
return asm volatile ("syscall"
|
||||
: [ret] "={rax}" (-> usize),
|
||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0),
|
||||
: [n] "{rax}" (@intFromEnum(n)),
|
||||
[a0] "{rdi}" (a0),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
}
|
||||
|
||||
pub inline fn systemCall2(n: SystemCall, a0: usize, a1: usize) usize {
|
||||
return asm volatile ("syscall"
|
||||
: [ret] "={rax}" (-> usize),
|
||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1),
|
||||
: [n] "{rax}" (@intFromEnum(n)),
|
||||
[a0] "{rdi}" (a0),
|
||||
[a1] "{rsi}" (a1),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
}
|
||||
|
||||
pub inline fn systemCall3(n: SystemCall, a0: usize, a1: usize, a2: usize) usize {
|
||||
return asm volatile ("syscall"
|
||||
: [ret] "={rax}" (-> usize),
|
||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), [a2] "{rdx}" (a2),
|
||||
: [n] "{rax}" (@intFromEnum(n)),
|
||||
[a0] "{rdi}" (a0),
|
||||
[a1] "{rsi}" (a1),
|
||||
[a2] "{rdx}" (a2),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
}
|
||||
|
||||
pub inline fn systemCall4(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize) usize {
|
||||
return asm volatile ("syscall"
|
||||
: [ret] "={rax}" (-> usize),
|
||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), [a2] "{rdx}" (a2), [a3] "{r10}" (a3),
|
||||
: [n] "{rax}" (@intFromEnum(n)),
|
||||
[a0] "{rdi}" (a0),
|
||||
[a1] "{rsi}" (a1),
|
||||
[a2] "{rdx}" (a2),
|
||||
[a3] "{r10}" (a3),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
}
|
||||
|
||||
pub inline fn systemCall5(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize) usize {
|
||||
return asm volatile ("syscall"
|
||||
: [ret] "={rax}" (-> usize),
|
||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0), [a1] "{rsi}" (a1), [a2] "{rdx}" (a2), [a3] "{r10}" (a3), [a4] "{r8}" (a4),
|
||||
: [n] "{rax}" (@intFromEnum(n)),
|
||||
[a0] "{rdi}" (a0),
|
||||
[a1] "{rsi}" (a1),
|
||||
[a2] "{rdx}" (a2),
|
||||
[a3] "{r10}" (a3),
|
||||
[a4] "{r8}" (a4),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
}
|
||||
|
||||
+118
-5
@@ -1,15 +1,20 @@
|
||||
//! Typed system_call surface for user space — thin wrappers over the raw `system_call`
|
||||
//! stubs, one per kernel call. Numbers come from `danos.SystemCall`, the single
|
||||
//! stubs, one per kernel call. Numbers come from `abi.SystemCall`, the single
|
||||
//! source of truth shared with the kernel dispatcher.
|
||||
|
||||
const danos = @import("danos");
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const sc = @import("system-call.zig");
|
||||
|
||||
/// `mmap` protection flags (matching the usual C bit values). Grants are always
|
||||
/// readable+writable today; the kernel does not yet honour finer prot.
|
||||
pub const PROT_READ: usize = danos.prot_read;
|
||||
pub const PROT_WRITE: usize = danos.prot_write;
|
||||
pub const PROT_EXEC: usize = danos.prot_exec;
|
||||
pub const PROT_READ: usize = abi.prot_read;
|
||||
pub const PROT_WRITE: usize = abi.prot_write;
|
||||
pub const PROT_EXEC: usize = abi.prot_exec;
|
||||
|
||||
/// One `processes` entry — re-exported from the shared ABI so a user program can
|
||||
/// declare its snapshot buffer without importing `abi` itself.
|
||||
pub const ProcessDescriptor = abi.ProcessDescriptor;
|
||||
|
||||
/// Give up the rest of this quantum.
|
||||
pub fn yield() void {
|
||||
@@ -27,12 +32,120 @@ pub fn sleep(ms: usize) void {
|
||||
_ = sc.systemCall1(.sleep, ms);
|
||||
}
|
||||
|
||||
/// Arm a one-shot timer: after `ms` milliseconds the kernel posts a timer
|
||||
/// notification (`ipc.Received.isTimer`) to `endpoint`. The timed wait of
|
||||
/// docs/process-lifecycle.md — a service arms a deadline and keeps serving,
|
||||
/// instead of blocking in sleep; what stop-sequence escalation, hello deadlines,
|
||||
/// and restart backoff are built from.
|
||||
pub fn timerOnce(endpoint: usize, ms: u64) bool {
|
||||
return sc.systemCall2(.timer_bind, endpoint, ms) == 0;
|
||||
}
|
||||
|
||||
/// Monotonic nanoseconds since boot — a time source for timeouts and short delays. It
|
||||
/// only ever moves forward. This is *not* wall-clock time (no date, no timezone — that
|
||||
/// is a user-space service layered on top). Deadline pattern for a bounded poll loop:
|
||||
///
|
||||
/// const deadline = clock() + timeout_ns;
|
||||
/// while (clock() < deadline) { ... }
|
||||
pub fn clock() u64 {
|
||||
return @intCast(sc.systemCall0(.clock));
|
||||
}
|
||||
|
||||
/// Wall-clock time in Unix epoch seconds (UTC) — the real date/time, from the RTC.
|
||||
/// Unlike `clock` (monotonic since boot), this tracks calendar time, so it is what a
|
||||
/// filesystem stamps as a file's modification time. Formatting it into a calendar
|
||||
/// date/timezone is user-space policy layered on top.
|
||||
pub fn wallClock() u64 {
|
||||
return @intCast(sc.systemCall0(.wall_clock));
|
||||
}
|
||||
|
||||
/// Copy bytes out of the kernel's in-memory diagnostic log — the accumulated
|
||||
/// stream of everything `write` (and the kernel itself) has emitted — starting at
|
||||
/// `offset`, into `out`. Returns the number of bytes copied (0 at end of buffer).
|
||||
/// A program reads the whole log by looping from offset 0, advancing by the return
|
||||
/// value, until it gets 0. This is how the boot log is persisted to disk on a
|
||||
/// headless/real machine where serial output is otherwise lost.
|
||||
pub fn klogRead(offset: usize, out: []u8) usize {
|
||||
return sc.systemCall3(.klog_read, offset, @intFromPtr(out.ptr), out.len);
|
||||
}
|
||||
|
||||
/// End the process. Never returns.
|
||||
pub fn exit(code: usize) noreturn {
|
||||
_ = sc.systemCall1(.exit, code);
|
||||
unreachable; // the kernel never returns from exit
|
||||
}
|
||||
|
||||
/// Start the binary bundled in the initial-ramdisk under `name` as a new ring-3
|
||||
/// process, returning the child's process id (or null on failure). The child's
|
||||
/// argv[0] is `name`, and the caller becomes its **supervisor** — the only process
|
||||
/// allowed to `kill` it. This is how a supervisor (the device manager) launches a
|
||||
/// driver it matched — danos-native, not POSIX (a spawn/exec family comes with the
|
||||
/// POSIX layer later).
|
||||
pub fn spawn(name: []const u8) ?u32 {
|
||||
return spawnSupervised(name, &.{}, null);
|
||||
}
|
||||
|
||||
/// Like `spawn`, but hands the child command-line arguments: they arrive as
|
||||
/// argv[1..] on its System V entry stack (argv[0] is still `name`).
|
||||
pub fn spawnWithArguments(name: []const u8, arguments: []const []const u8) ?u32 {
|
||||
return spawnSupervised(name, arguments, null);
|
||||
}
|
||||
|
||||
/// The full spawn: command-line arguments for the child, and an optional endpoint
|
||||
/// (a handle from `ipc.createIpcEndpoint`) the kernel notifies when the child ends
|
||||
/// — any way it ends: clean exit, fault, or `kill`. The notification arrives via
|
||||
/// `ipc.replyWait` as a badge with the child-exit bit set and the child's id in
|
||||
/// the low bits (`ipc.Received.isChildExit`/`childProcessId`), so one endpoint can
|
||||
/// supervise many children. Arguments are marshalled to the kernel as one
|
||||
/// NUL-separated blob; the combined arguments must fit `blob` (the kernel caps the
|
||||
/// blob at 256 bytes and argc at 8 anyway). Returns the child's process id, or
|
||||
/// null on failure.
|
||||
pub fn spawnSupervised(name: []const u8, arguments: []const []const u8, exit_endpoint: ?usize) ?u32 {
|
||||
var blob: [256]u8 = undefined;
|
||||
var len: usize = 0;
|
||||
for (arguments, 0..) |argument, i| {
|
||||
if (i != 0) {
|
||||
if (len >= blob.len) return null;
|
||||
blob[len] = 0;
|
||||
len += 1;
|
||||
}
|
||||
if (len + argument.len > blob.len) return null;
|
||||
@memcpy(blob[len..][0..argument.len], argument);
|
||||
len += argument.len;
|
||||
}
|
||||
const r = sc.systemCall5(.system_spawn, @intFromPtr(name.ptr), name.len, if (len == 0) 0 else @intFromPtr(&blob), len, exit_endpoint orelse abi.no_cap);
|
||||
if (r > ~@as(usize, 0) - 4095) return null; // a wrapped -errno
|
||||
return @intCast(r);
|
||||
}
|
||||
|
||||
/// Snapshot the process table into `out` (up to its length) and return the total
|
||||
/// number of live processes — which may exceed `out.len`; call again with a larger
|
||||
/// buffer for the full listing. Kernel tasks are included, with an empty name.
|
||||
/// The primitive `ps` is built on.
|
||||
pub fn processes(out: []abi.ProcessDescriptor) usize {
|
||||
return sc.systemCall2(.process_enumerate, @intFromPtr(out.ptr), out.len);
|
||||
}
|
||||
|
||||
/// Whether a process spawned under `name` (its argv[0]) is currently alive.
|
||||
pub fn isProcessRunning(name: []const u8) bool {
|
||||
var table: [32]ProcessDescriptor = undefined;
|
||||
const total = processes(&table);
|
||||
for (table[0..@min(total, table.len)]) |descriptor| {
|
||||
if (std.mem.eql(u8, descriptor.name[0..descriptor.name_length], name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// End process `id`. Only its supervisor — the process that spawned it — may;
|
||||
/// anyone else gets false, as does a stale or unknown id (ids are never reused).
|
||||
/// Delivery is prompt but asynchronous, like a signal: a target caught running on
|
||||
/// another core dies at its next system call or timer tick. True means the kill
|
||||
/// is accepted and irrevocable; the exit notification (if an endpoint was given
|
||||
/// at spawn) confirms completion.
|
||||
pub fn kill(id: u32) bool {
|
||||
return sc.systemCall1(.process_kill, id) == 0;
|
||||
}
|
||||
|
||||
/// Grant `len` bytes (rounded up to whole pages) of fresh, zeroed, writable
|
||||
/// memory and return the base virtual address. On failure returns a value in the
|
||||
/// top page (see `mmapFailed`). The user heap grows through this call.
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
//! The danos time interface — monotonic time, delays, and deadlines for user space.
|
||||
//!
|
||||
//! There is no time *service*: the kernel already owns the scheduling timer and
|
||||
//! surfaces it directly, so reading the clock is one system call (an `rdtsc` and a
|
||||
//! scale), never an IPC round trip (docs/timers.md explains why). This module is a
|
||||
//! thin, generic layer over the `clock`/`sleep`/`timer_bind` wrappers in `system.zig`
|
||||
//! — an ergonomic `Instant`/`Duration` front door, not new mechanism.
|
||||
//!
|
||||
//! It is **monotonic** time only: nanoseconds since boot, moving forward, no date or
|
||||
//! timezone. Wall-clock/calendar time is a separate user-space service (an RTC-backed
|
||||
//! CLOCK_REALTIME) layered on top later.
|
||||
|
||||
const std = @import("std");
|
||||
const system = @import("system.zig");
|
||||
|
||||
const nanos_per_micro: u64 = 1_000;
|
||||
const nanos_per_milli: u64 = 1_000_000;
|
||||
const nanos_per_second: u64 = 1_000_000_000;
|
||||
|
||||
/// A span of time, held as nanoseconds. Constructors name their unit; accessors
|
||||
/// truncate toward zero. `ceilMillis` rounds *up*, since `sleep`/`after` land on the
|
||||
/// kernel's millisecond granularity and rounding down could return early.
|
||||
pub const Duration = struct {
|
||||
ns: u64,
|
||||
|
||||
pub fn fromNanos(n: u64) Duration {
|
||||
return .{ .ns = n };
|
||||
}
|
||||
pub fn fromMicros(n: u64) Duration {
|
||||
return .{ .ns = n *| nanos_per_micro };
|
||||
}
|
||||
pub fn fromMillis(n: u64) Duration {
|
||||
return .{ .ns = n *| nanos_per_milli };
|
||||
}
|
||||
pub fn fromSeconds(n: u64) Duration {
|
||||
return .{ .ns = n *| nanos_per_second };
|
||||
}
|
||||
|
||||
pub fn asNanos(d: Duration) u64 {
|
||||
return d.ns;
|
||||
}
|
||||
pub fn asMicros(d: Duration) u64 {
|
||||
return d.ns / nanos_per_micro;
|
||||
}
|
||||
pub fn asMillis(d: Duration) u64 {
|
||||
return d.ns / nanos_per_milli;
|
||||
}
|
||||
pub fn asSeconds(d: Duration) u64 {
|
||||
return d.ns / nanos_per_second;
|
||||
}
|
||||
|
||||
/// Whole milliseconds, rounded up — the argument `sleep`/`after` pass the kernel.
|
||||
/// A non-zero sub-millisecond duration becomes 1 ms rather than 0.
|
||||
pub fn ceilMillis(d: Duration) u64 {
|
||||
return (d.ns +| (nanos_per_milli - 1)) / nanos_per_milli;
|
||||
}
|
||||
|
||||
pub fn plus(a: Duration, b: Duration) Duration {
|
||||
return .{ .ns = a.ns +| b.ns };
|
||||
}
|
||||
};
|
||||
|
||||
/// A point on the monotonic clock — nanoseconds since boot. Compare and subtract
|
||||
/// instants to measure elapsed time; it never runs backward, so `since` is safe to
|
||||
/// saturate at zero rather than wrap.
|
||||
pub const Instant = struct {
|
||||
ns: u64,
|
||||
|
||||
/// The span from `earlier` to `self`, saturating at zero if `earlier` is later
|
||||
/// (which the monotonic clock should never produce, but callers may pass any pair).
|
||||
pub fn since(self: Instant, earlier: Instant) Duration {
|
||||
return .{ .ns = self.ns -| earlier.ns };
|
||||
}
|
||||
|
||||
/// How long since this instant, sampled now.
|
||||
pub fn elapsed(self: Instant) Duration {
|
||||
return now().since(self);
|
||||
}
|
||||
|
||||
/// This instant advanced by `d` (a deadline, `d` from here).
|
||||
pub fn plus(self: Instant, d: Duration) Instant {
|
||||
return .{ .ns = self.ns +| d.ns };
|
||||
}
|
||||
|
||||
/// Whether the monotonic clock has reached this instant (used as a deadline).
|
||||
pub fn reached(deadline: Instant) bool {
|
||||
return now().ns >= deadline.ns;
|
||||
}
|
||||
};
|
||||
|
||||
/// The current monotonic time.
|
||||
pub fn now() Instant {
|
||||
return .{ .ns = system.clock() };
|
||||
}
|
||||
|
||||
/// Monotonic nanoseconds since boot — the raw `clock()` reading, for callers that
|
||||
/// want a plain integer instead of an `Instant`.
|
||||
pub fn monotonicNanos() u64 {
|
||||
return system.clock();
|
||||
}
|
||||
|
||||
/// Whether the monotonic clock is usable. The kernel returns 0 until the TSC is
|
||||
/// calibrated (`tsc_hz == 0`); a caller that needs real time can treat that as
|
||||
/// "unavailable" instead of assuming the clock advances.
|
||||
pub fn available() bool {
|
||||
return system.clock() != 0;
|
||||
}
|
||||
|
||||
/// Block the caller for at least `d`, rounded up to the kernel's millisecond
|
||||
/// granularity. For sub-millisecond precision the scheduler cannot express, use
|
||||
/// `spin`.
|
||||
pub fn sleep(d: Duration) void {
|
||||
system.sleep(d.ceilMillis());
|
||||
}
|
||||
|
||||
/// Block the caller for `ms` milliseconds — the coarse, allocation-free form.
|
||||
pub fn sleepMillis(ms: u64) void {
|
||||
system.sleep(ms);
|
||||
}
|
||||
|
||||
/// Busy-wait until `d` has elapsed, polling the monotonic clock. This burns the CPU
|
||||
/// on purpose, to hit sub-millisecond delays the scheduler's millisecond tick cannot.
|
||||
/// Prefer `sleep` for anything at or above a millisecond.
|
||||
pub fn spin(d: Duration) void {
|
||||
const deadline = now().plus(d);
|
||||
while (!deadline.reached()) {}
|
||||
}
|
||||
|
||||
/// Arm a one-shot timer against `endpoint` (a handle from `ipc.createIpcEndpoint`):
|
||||
/// after `d` the kernel posts a timer notification (`ipc.Received.isTimer`) there.
|
||||
/// Unlike `sleep`, this does not block — a service can keep serving IPC on the same
|
||||
/// endpoint while the deadline is pending. Rounds `d` up to milliseconds; returns
|
||||
/// false if the timer could not be armed. See `system.timerOnce`.
|
||||
pub fn after(endpoint: usize, d: Duration) bool {
|
||||
return system.timerOnce(endpoint, d.ceilMillis());
|
||||
}
|
||||
|
||||
test "Duration unit conversions round toward zero" {
|
||||
try std.testing.expectEqual(@as(u64, 1_000_000_000), Duration.fromSeconds(1).asNanos());
|
||||
try std.testing.expectEqual(@as(u64, 1_500), Duration.fromNanos(1_500).asNanos());
|
||||
try std.testing.expectEqual(@as(u64, 2), Duration.fromMillis(2).asMillis());
|
||||
try std.testing.expectEqual(@as(u64, 1), Duration.fromNanos(1_999_999).asMillis());
|
||||
try std.testing.expectEqual(@as(u64, 250), Duration.fromMicros(250).asMicros());
|
||||
}
|
||||
|
||||
test "ceilMillis rounds up, and never turns a nonzero span into zero" {
|
||||
try std.testing.expectEqual(@as(u64, 0), Duration.fromNanos(0).ceilMillis());
|
||||
try std.testing.expectEqual(@as(u64, 1), Duration.fromNanos(1).ceilMillis());
|
||||
try std.testing.expectEqual(@as(u64, 1), Duration.fromMillis(1).ceilMillis());
|
||||
try std.testing.expectEqual(@as(u64, 2), Duration.fromNanos(nanos_per_milli + 1).ceilMillis());
|
||||
try std.testing.expectEqual(@as(u64, 5), Duration.fromMillis(5).ceilMillis());
|
||||
}
|
||||
|
||||
test "Instant arithmetic: since saturates, plus/reached form deadlines" {
|
||||
const t0 = Instant{ .ns = 1_000 };
|
||||
const t1 = Instant{ .ns = 4_000 };
|
||||
try std.testing.expectEqual(@as(u64, 3_000), t1.since(t0).asNanos());
|
||||
// earlier-than-self can't happen on a monotonic clock; saturate rather than wrap.
|
||||
try std.testing.expectEqual(@as(u64, 0), t0.since(t1).asNanos());
|
||||
const deadline = t0.plus(Duration.fromNanos(2_500));
|
||||
try std.testing.expectEqual(@as(u64, 3_500), deadline.ns);
|
||||
}
|
||||
|
||||
test "saturating arithmetic does not overflow at the u64 ceiling" {
|
||||
const big = Duration.fromSeconds(std.math.maxInt(u64));
|
||||
try std.testing.expectEqual(@as(u64, std.math.maxInt(u64)), big.asNanos());
|
||||
const late = Instant{ .ns = std.math.maxInt(u64) };
|
||||
try std.testing.expectEqual(@as(u64, std.math.maxInt(u64)), late.plus(Duration.fromSeconds(10)).ns);
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
//! POSIX-style file API for user programs — the low level under C stdio. Files
|
||||
//! are named objects served by the user-space VFS server (system/services/vfs/vfs.zig); each
|
||||
//! call marshals a request, IPC_Calls the VFS, and unmarshals the reply. The
|
||||
//! kernel knows nothing of files or fds — the fd table lives here, per process.
|
||||
|
||||
const std = @import("std");
|
||||
const protocol = @import("vfs-protocol");
|
||||
const ipc = @import("ipc.zig");
|
||||
const danos = @import("danos");
|
||||
|
||||
pub const O_CREAT = protocol.O_CREAT;
|
||||
pub const SEEK_SET: u32 = 0;
|
||||
pub const SEEK_CURRENT: u32 = 1;
|
||||
pub const SEEK_END: u32 = 2;
|
||||
|
||||
// Resolve (and cache) the VFS server endpoint, looked up by well-known id.
|
||||
var vfs_handle: usize = 0;
|
||||
var vfs_resolved = false;
|
||||
fn vfs() ?usize {
|
||||
if (!vfs_resolved) {
|
||||
vfs_handle = ipc.lookup(.vfs) orelse return null;
|
||||
vfs_resolved = true;
|
||||
}
|
||||
return vfs_handle;
|
||||
}
|
||||
|
||||
const maximum_fds = 32;
|
||||
const Fd = struct { used: bool = false, node: u64 = 0, offset: u64 = 0 };
|
||||
var fds = [_]Fd{.{}} ** maximum_fds;
|
||||
|
||||
fn allocFd() ?usize {
|
||||
for (&fds, 0..) |*f, i| {
|
||||
if (!f.used) {
|
||||
f.* = .{ .used = true };
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const Result = struct { reply: protocol.Reply, payload: []u8 };
|
||||
|
||||
/// One request/reply round trip: [Request header][send payload] -> VFS ->
|
||||
/// [Reply header][receive payload]. The receive payload is written into `out`.
|
||||
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
||||
const h = vfs() orelse return null;
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const slen = @min(send.len, protocol.maximum_payload);
|
||||
@memcpy(message[protocol.request_size..][0..slen], send[0..slen]);
|
||||
|
||||
var rbuf: [protocol.message_maximum]u8 = undefined;
|
||||
const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null;
|
||||
if (n < protocol.reply_size) return null;
|
||||
const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]);
|
||||
const rpl = @min(n - protocol.reply_size, out.len);
|
||||
@memcpy(out[0..rpl], rbuf[protocol.reply_size..][0..rpl]);
|
||||
return .{ .reply = reply, .payload = out[0..rpl] };
|
||||
}
|
||||
|
||||
/// Open (or create, with O_CREAT) `path`; returns an fd or -1.
|
||||
pub fn open(path: []const u8, flags: u32) i32 {
|
||||
const fd = allocFd() orelse return -1;
|
||||
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = flags };
|
||||
const r = transact(request, path, &.{}) orelse {
|
||||
fds[fd].used = false;
|
||||
return -1;
|
||||
};
|
||||
if (r.reply.status != 0) {
|
||||
fds[fd].used = false;
|
||||
return -1;
|
||||
}
|
||||
fds[fd] = .{ .used = true, .node = r.reply.node, .offset = 0 };
|
||||
return @intCast(fd);
|
||||
}
|
||||
|
||||
fn fdPtr(fd: i32) ?*Fd {
|
||||
if (fd < 0 or fd >= maximum_fds) return null;
|
||||
const f = &fds[@intCast(fd)];
|
||||
return if (f.used) f else null;
|
||||
}
|
||||
|
||||
/// Read up to `buffer.len` bytes at the current offset; returns the count or -1.
|
||||
pub fn read(fd: i32, buffer: []u8) isize {
|
||||
const f = fdPtr(fd) orelse return -1;
|
||||
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
|
||||
const request = protocol.Request{ .operation = .read, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
|
||||
const r = transact(request, &.{}, buffer) orelse return -1;
|
||||
if (r.reply.status != 0) return -1;
|
||||
f.offset += r.reply.len;
|
||||
return @intCast(r.reply.len);
|
||||
}
|
||||
|
||||
/// Write `data` at the current offset; returns the count or -1.
|
||||
pub fn write(fd: i32, data: []const u8) isize {
|
||||
const f = fdPtr(fd) orelse return -1;
|
||||
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
|
||||
const request = protocol.Request{ .operation = .write, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
|
||||
const r = transact(request, data[0..want], &.{}) orelse return -1;
|
||||
if (r.reply.status != 0) return -1;
|
||||
f.offset += r.reply.len;
|
||||
return @intCast(r.reply.len);
|
||||
}
|
||||
|
||||
/// Reposition the fd's offset. Returns the new offset or -1. (SEEK_END needs the
|
||||
/// file size, which `stat` provides; handled by fetching it here.)
|
||||
pub fn lseek(fd: i32, off: i64, whence: u32) i64 {
|
||||
const f = fdPtr(fd) orelse return -1;
|
||||
const base: i64 = switch (whence) {
|
||||
SEEK_SET => 0,
|
||||
SEEK_CURRENT => @intCast(f.offset),
|
||||
SEEK_END => blk: {
|
||||
const request = protocol.Request{ .operation = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
||||
var sbuf: [@sizeOf(protocol.Stat)]u8 = undefined;
|
||||
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.Stat)) return -1;
|
||||
const st = std.mem.bytesToValue(protocol.Stat, sbuf[0..@sizeOf(protocol.Stat)]);
|
||||
break :blk @intCast(st.size);
|
||||
},
|
||||
else => return -1,
|
||||
};
|
||||
const pos = base + off;
|
||||
if (pos < 0) return -1;
|
||||
f.offset = @intCast(pos);
|
||||
return pos;
|
||||
}
|
||||
|
||||
/// Stat `path`. Returns 0 or -1.
|
||||
pub fn stat(path: []const u8, out: *protocol.Stat) i32 {
|
||||
// Open, stat by node, close — simple and enough for now.
|
||||
const fd = open(path, 0);
|
||||
if (fd < 0) return -1;
|
||||
defer close(fd);
|
||||
const f = fdPtr(fd).?;
|
||||
const request = protocol.Request{ .operation = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
||||
var sbuf: [@sizeOf(protocol.Stat)]u8 = undefined;
|
||||
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.Stat)) return -1;
|
||||
out.* = std.mem.bytesToValue(protocol.Stat, sbuf[0..@sizeOf(protocol.Stat)]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Close an fd (best effort — tells the VFS to release the open file).
|
||||
pub fn close(fd: i32) void {
|
||||
const f = fdPtr(fd) orelse return;
|
||||
const request = protocol.Request{ .operation = .close, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
||||
_ = transact(request, &.{}, &.{});
|
||||
f.used = false;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
//! USB class-driver client: the helper a keyboard, mouse, or mass-storage driver
|
||||
//! uses to reach its device through the xHCI bus driver, so it never hand-rolls
|
||||
//! the transfer-protocol IPC. Layered over `ipc` and the shared
|
||||
//! `usb-transfer-protocol` wire format, the way `input.zig` layers over the input
|
||||
//! service and `device.zig` over the raw device calls.
|
||||
//!
|
||||
//! A class driver, spawned with its interface's assigned device id as argv[1]:
|
||||
//! if (!usb.helloManager(id)) return; // meet the spawn deadline
|
||||
//! var device = usb.open(id) orelse return; // open + get its endpoints
|
||||
//! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors
|
||||
//! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously
|
||||
//! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop
|
||||
//!
|
||||
//! Reports are delivered to `device.endpoint` as asynchronous `InterruptReport`
|
||||
//! messages (the class driver runs a bare `replyWait` loop to read them, because
|
||||
//! the service harness drops buffered-message payloads — see service.zig).
|
||||
|
||||
const std = @import("std");
|
||||
const ipc = @import("ipc.zig");
|
||||
const system = @import("system.zig");
|
||||
const protocol = @import("usb-transfer-protocol");
|
||||
const device_manager = @import("device-manager-protocol");
|
||||
|
||||
pub const Endpoint = protocol.Endpoint;
|
||||
pub const InterruptReport = protocol.InterruptReport;
|
||||
pub const max_report_data = protocol.max_report_data;
|
||||
|
||||
// Endpoint transfer types (EndpointDescriptor attributes), for `findEndpoint`.
|
||||
pub const transfer_type_bulk: u8 = 2;
|
||||
pub const transfer_type_interrupt: u8 = 3;
|
||||
|
||||
/// An opened USB device: the bus endpoint to send requests to, this driver's own
|
||||
/// endpoint that reports arrive on, the device token, and the interface's
|
||||
/// endpoints (so a driver need not re-read the configuration descriptor).
|
||||
pub const Device = struct {
|
||||
bus: ipc.Handle,
|
||||
endpoint: ipc.Handle,
|
||||
token: u64,
|
||||
class: u8,
|
||||
subclass: u8,
|
||||
protocol_code: u8,
|
||||
interface_number: u8,
|
||||
endpoint_count: usize = 0,
|
||||
endpoints: [protocol.max_reported_endpoints]Endpoint = undefined,
|
||||
|
||||
/// The interface's first endpoint of the given transfer type and direction
|
||||
/// (`transfer_type_bulk` / `transfer_type_interrupt`), or null.
|
||||
pub fn findEndpoint(self: *const Device, transfer_type: u8, direction_in: bool) ?Endpoint {
|
||||
for (self.endpoints[0..self.endpoint_count]) |endpoint| {
|
||||
if (endpoint.transfer_type == transfer_type and (endpoint.address & 0x80 != 0) == direction_in) return endpoint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn controlTransfer(self: *Device, setup: [8]u8, direction_in: bool, data: []u8) ?usize {
|
||||
var request = protocol.ControlRequest{
|
||||
.device_token = self.token,
|
||||
.setup = setup,
|
||||
.direction_in = @intFromBool(direction_in),
|
||||
.data_length = @intCast(data.len),
|
||||
};
|
||||
if (!direction_in and data.len > 0) @memcpy(request.data[0..data.len], data);
|
||||
var reply: [@sizeOf(protocol.ControlReply)]u8 = undefined;
|
||||
const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
|
||||
if (length < @sizeOf(protocol.ControlReply)) return null;
|
||||
const control_reply = std.mem.bytesToValue(protocol.ControlReply, reply[0..@sizeOf(protocol.ControlReply)]);
|
||||
if (control_reply.status != 0) return null;
|
||||
const actual = @min(control_reply.actual_length, data.len);
|
||||
if (direction_in and actual > 0) @memcpy(data[0..actual], control_reply.data[0..actual]);
|
||||
return actual;
|
||||
}
|
||||
|
||||
/// A control transfer with no data stage (SET_PROTOCOL, SET_IDLE, ...). The
|
||||
/// `setup` is a bit-cast `usb_abi.Request`.
|
||||
pub fn controlOut(self: *Device, setup: [8]u8) bool {
|
||||
return self.controlTransfer(setup, false, &.{}) != null;
|
||||
}
|
||||
|
||||
/// A device-to-host control transfer, returning the bytes read into `out`.
|
||||
pub fn controlIn(self: *Device, setup: [8]u8, out: []u8) ?usize {
|
||||
return self.controlTransfer(setup, true, out);
|
||||
}
|
||||
|
||||
/// Begin periodic IN polling of an interrupt endpoint; reports flow back to
|
||||
/// `self.endpoint` as asynchronous `InterruptReport` messages.
|
||||
pub fn subscribeInterrupt(self: *Device, endpoint_address: u8, max_length: u16) bool {
|
||||
var request = protocol.InterruptSubscribeRequest{
|
||||
.device_token = self.token,
|
||||
.endpoint_address = endpoint_address,
|
||||
.max_length = max_length,
|
||||
};
|
||||
var reply: [@sizeOf(protocol.InterruptSubscribeReply)]u8 = undefined;
|
||||
const length = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return false;
|
||||
if (length < @sizeOf(protocol.InterruptSubscribeReply)) return false;
|
||||
return std.mem.bytesToValue(protocol.InterruptSubscribeReply, reply[0..@sizeOf(protocol.InterruptSubscribeReply)]).status == 0;
|
||||
}
|
||||
|
||||
/// One bulk transfer (IN or OUT per `endpoint_address`'s direction bit) to or
|
||||
/// from the caller's own DMA buffer at `physical`. Returns the bytes moved.
|
||||
pub fn bulk(self: *Device, endpoint_address: u8, physical: u64, length: u32) ?u32 {
|
||||
var request = protocol.BulkRequest{
|
||||
.device_token = self.token,
|
||||
.physical_address = physical,
|
||||
.length = length,
|
||||
.endpoint_address = endpoint_address,
|
||||
};
|
||||
var reply: [@sizeOf(protocol.BulkReply)]u8 = undefined;
|
||||
const replied = ipc.call(self.bus, std.mem.asBytes(&request), &reply) catch return null;
|
||||
if (replied < @sizeOf(protocol.BulkReply)) return null;
|
||||
const bulk_reply = std.mem.bytesToValue(protocol.BulkReply, reply[0..@sizeOf(protocol.BulkReply)]);
|
||||
if (bulk_reply.status != 0) return null;
|
||||
return bulk_reply.actual_length;
|
||||
}
|
||||
};
|
||||
|
||||
/// Look up the USB bus and open the device with the assigned id, handing over a
|
||||
/// freshly created endpoint for asynchronous interrupt reports. Retries while the
|
||||
/// bus is still coming up (a class driver races the bus driver at boot).
|
||||
pub fn open(device_id: u64) ?Device {
|
||||
var attempts: usize = 0;
|
||||
const bus = while (attempts < 100) : (attempts += 1) {
|
||||
if (ipc.lookup(.usb_bus)) |handle| break handle;
|
||||
system.sleep(20);
|
||||
} else return null;
|
||||
|
||||
const endpoint = ipc.createIpcEndpoint() orelse return null;
|
||||
var request = protocol.OpenRequest{ .device_id = device_id };
|
||||
var reply: [@sizeOf(protocol.OpenReply)]u8 = undefined;
|
||||
const result = ipc.callCap(bus, std.mem.asBytes(&request), &reply, endpoint) catch return null;
|
||||
if (result.len < @sizeOf(protocol.OpenReply)) return null;
|
||||
const open_reply = std.mem.bytesToValue(protocol.OpenReply, reply[0..@sizeOf(protocol.OpenReply)]);
|
||||
if (open_reply.status != 0) return null;
|
||||
|
||||
var device = Device{
|
||||
.bus = bus,
|
||||
.endpoint = endpoint,
|
||||
.token = open_reply.device_token,
|
||||
.class = open_reply.interface_class,
|
||||
.subclass = open_reply.interface_subclass,
|
||||
.protocol_code = open_reply.interface_protocol,
|
||||
.interface_number = open_reply.interface_number,
|
||||
.endpoint_count = @min(open_reply.endpoint_count, protocol.max_reported_endpoints),
|
||||
};
|
||||
for (0..device.endpoint_count) |index| device.endpoints[index] = open_reply.endpoints[index];
|
||||
return device;
|
||||
}
|
||||
|
||||
/// Hello the device manager as a class driver (Role.device) so a supervised
|
||||
/// spawn meets its hello deadline. Retries while the manager comes up.
|
||||
pub fn helloManager(device_id: u64) bool {
|
||||
var attempts: usize = 0;
|
||||
const manager = while (attempts < 100) : (attempts += 1) {
|
||||
if (ipc.lookup(.device_manager)) |handle| break handle;
|
||||
system.sleep(20);
|
||||
} else return false;
|
||||
|
||||
const hello = device_manager.Hello{ .role = @intFromEnum(device_manager.Role.device), .device_id = device_id };
|
||||
var reply: [device_manager.message_maximum]u8 = undefined;
|
||||
const length = ipc.call(manager, std.mem.asBytes(&hello), &reply) catch return false;
|
||||
if (length < device_manager.reply_size) return false;
|
||||
return std.mem.bytesToValue(device_manager.HelloReply, reply[0..device_manager.reply_size]).status == 0;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# xkeyboard-config — X11 keyboard layouts, compiled to Zig
|
||||
|
||||
This module turns a physical key (a **USB HID usage**, as the [input module](../../docs/input.md)
|
||||
delivers in `KeyEvent.keycode`) plus a modifier state into a **keysym** and, when the key
|
||||
produces one, a **character** (a Unicode scalar). It is what lets a `keycode` become a
|
||||
`character` — a keymap — without danos shipping an X11 runtime.
|
||||
|
||||
The layout data comes from the X11 [xkeyboard-config](https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config)
|
||||
database, but it is **compiled to native Zig at build time** rather than parsed at runtime.
|
||||
`tools/make-xkeyboard-config.py` reads the vendored xkb data and emits pure-data tables into
|
||||
`generated/layouts.zig`; `xkeyboard-config.zig` is the hand-written API over them. This is
|
||||
the same build-time-codegen pattern as `tools/make-initial-ramdisk.py`.
|
||||
|
||||
## Using it
|
||||
|
||||
```zig
|
||||
const xkb = @import("xkeyboard-config");
|
||||
|
||||
const m = xkb.map(xkb.us, key_event.keycode, .{ .shift = shift_held, .caps_lock = caps });
|
||||
if (m.character) |ch| { /* a printable Unicode scalar */ }
|
||||
// m.keysym is always set (e.g. an X11 keysym for Return / F1 / a dead key).
|
||||
|
||||
const layout = xkb.byName("gb") orelse xkb.us; // choose a layout by name
|
||||
for (xkb.all) |l| { /* enumerate available layouts */ }
|
||||
```
|
||||
|
||||
`Modifiers` carries `shift`, `caps_lock`, `level3` (AltGr), and `control`. `map` selects the
|
||||
level from the key's XKB *type* (the generated data) and those modifiers (the policy, in
|
||||
`xkeyboard-config.zig`), so data and semantics stay separable.
|
||||
|
||||
Layouts: **us, gb, de, fr, es, dvorak**.
|
||||
|
||||
## Regenerating
|
||||
|
||||
```sh
|
||||
python3 tools/make-xkeyboard-config.py fetch # network: download + vendor the data subset
|
||||
python3 tools/make-xkeyboard-config.py generate # offline: emit generated/layouts.zig
|
||||
# or, from the build:
|
||||
zig build gen-xkeyboard-config
|
||||
```
|
||||
|
||||
- **`fetch`** downloads the pinned xkeyboard-config release (version + sha256 in the script),
|
||||
resolves the `include` graph for the configured layouts, and vendors *only* the symbols
|
||||
files actually reached (plus `keysymdef.h`, `COPYING`, and `PROVENANCE.md`) into `vendor/`.
|
||||
Run it when bumping the version or adding a layout.
|
||||
- **`generate`** is deterministic and offline — same vendored input produces byte-identical
|
||||
output. To add a layout, extend `TARGETS` (and `HID_TO_NAME` if a new physical key is
|
||||
involved), then re-run `fetch` (to vendor any new includes) and `generate`.
|
||||
|
||||
## Scope
|
||||
|
||||
A pragmatic subset, enough for real Latin-script typing:
|
||||
|
||||
- **Group 1 only** — no multi-layout group switching.
|
||||
- **No dead-key / compose composition** — a dead key returns its keysym with no `character`
|
||||
(composing `´` + `e` → `é` is a higher layer's job).
|
||||
- **Curated key types** — the common XKB types (one/two-level, alphabetic, four-level, …);
|
||||
unmapped keys and unknown types fall back to level-by-shift.
|
||||
- **6 layouts** — extend via `TARGETS` as above.
|
||||
|
||||
## Licensing
|
||||
|
||||
xkeyboard-config and `keysymdef.h` (xorgproto) are MIT/X11 licensed. The vendored data
|
||||
subset carries the upstream `vendor/COPYING`, and `vendor/PROVENANCE.md` records the exact
|
||||
version, source URL, and sha256. The generated tables are a derived work under the same terms.
|
||||
File diff suppressed because it is too large
Load Diff
+190
@@ -0,0 +1,190 @@
|
||||
Copyright 1996 by Joseph Moss
|
||||
Copyright (C) 2002-2007 Free Software Foundation, Inc.
|
||||
Copyright (C) Dmitry Golubev <lastguru@mail.ru>, 2003-2004
|
||||
Copyright (C) 2004, Gregory Mokhin <mokhin@bog.msu.ru>
|
||||
Copyright (C) 2006 Erdal Ronahî
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and its
|
||||
documentation for any purpose is hereby granted without fee, provided that
|
||||
the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation, and that the name of the copyright holder(s) not be used in
|
||||
advertising or publicity pertaining to distribution of the software without
|
||||
specific, written prior permission. The copyright holder(s) makes no
|
||||
representations about the suitability of this software for any purpose. It
|
||||
is provided "as is" without express or implied warranty.
|
||||
|
||||
THE COPYRIGHT HOLDER(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
|
||||
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
|
||||
EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR
|
||||
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
Copyright (c) 1996 Digital Equipment Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
|
||||
THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of the Digital Equipment
|
||||
Corporation shall not be used in advertising or otherwise to promote
|
||||
the sale, use or other dealings in this Software without prior written
|
||||
authorization from Digital Equipment Corporation.
|
||||
|
||||
|
||||
Copyright 1996, 1998 The Open Group
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and its
|
||||
documentation for any purpose is hereby granted without fee, provided that
|
||||
the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation.
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of The Open Group shall
|
||||
not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization
|
||||
from The Open Group.
|
||||
|
||||
|
||||
Copyright 2004-2005 Sun Microsystems, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
Copyright (c) 1996 by Silicon Graphics Computer Systems, Inc.
|
||||
|
||||
Permission to use, copy, modify, and distribute this
|
||||
software and its documentation for any purpose and without
|
||||
fee is hereby granted, provided that the above copyright
|
||||
notice appear in all copies and that both that copyright
|
||||
notice and this permission notice appear in supporting
|
||||
documentation, and that the name of Silicon Graphics not be
|
||||
used in advertising or publicity pertaining to distribution
|
||||
of the software without specific prior written permission.
|
||||
Silicon Graphics makes no representation about the suitability
|
||||
of this software for any purpose. It is provided "as is"
|
||||
without any express or implied warranty.
|
||||
|
||||
SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON
|
||||
GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
|
||||
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
||||
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH
|
||||
THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
Copyright (c) 1996 X Consortium
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of the X Consortium shall
|
||||
not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization
|
||||
from the X Consortium.
|
||||
|
||||
|
||||
Copyright (C) 2004, 2006 Ævar Arnfjörð Bjarmason <avarab@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and its
|
||||
documentation for any purpose is hereby granted without fee, provided that
|
||||
the above copyright notice appear in all copies and that both that
|
||||
copyright notice and this permission notice appear in supporting
|
||||
documentation.
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of a copyright holder shall
|
||||
not be used in advertising or otherwise to promote the sale, use or
|
||||
other dealings in this Software without prior written authorization of
|
||||
the copyright holder.
|
||||
|
||||
|
||||
Copyright (C) 1999, 2000 by Anton Zinoviev <anton@lml.bas.bg>
|
||||
|
||||
This software may be used, modified, copied, distributed, and sold,
|
||||
in both source and binary form provided that the above copyright
|
||||
and these terms are retained. Under no circumstances is the author
|
||||
responsible for the proper functioning of this software, nor does
|
||||
the author assume any responsibility for damages incurred with its
|
||||
use.
|
||||
|
||||
Permission is granted to anyone to use, distribute and modify
|
||||
this file in any way, provided that the above copyright notice
|
||||
is left intact and the author of the modification summarizes
|
||||
the changes in this header.
|
||||
|
||||
This file is distributed without any expressed or implied warranty.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Vendored xkeyboard-config subset
|
||||
|
||||
- **Package**: xkeyboard-config 2.44
|
||||
- **Source**: https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config/-/archive/xkeyboard-config-2.44/xkeyboard-config-2.44.tar.gz
|
||||
- **sha256**: `35e34edeaf4e8da8d0696ff6b241ee11ddb1b8c6730bac7252d4d0a88ea5f05b`
|
||||
- **keysymdef.h**: xorgproto, copied from `/opt/homebrew/include/X11/keysymdef.h`
|
||||
- **License**: MIT/X11 (see COPYING)
|
||||
|
||||
Only the symbols files reachable from the generated layouts (tools/make-xkeyboard-config.py `TARGETS`) are vendored; regenerate with
|
||||
`python3 tools/make-xkeyboard-config.py fetch` then `... generate`.
|
||||
|
||||
Vendored symbols files:
|
||||
|
||||
- `symbols/de`
|
||||
- `symbols/es`
|
||||
- `symbols/fr`
|
||||
- `symbols/gb`
|
||||
- `symbols/kpdl`
|
||||
- `symbols/latin`
|
||||
- `symbols/level3`
|
||||
- `symbols/us`
|
||||
+2584
File diff suppressed because it is too large
Load Diff
+1232
File diff suppressed because it is too large
Load Diff
+250
@@ -0,0 +1,250 @@
|
||||
// Keyboard layouts for Spain.
|
||||
|
||||
// Modified for a real Spanish keyboard by Jon Tombs.
|
||||
default partial alphanumeric_keys
|
||||
xkb_symbols "basic" {
|
||||
|
||||
include "latin(type4)"
|
||||
|
||||
name[Group1]="Spanish";
|
||||
|
||||
key <TLDE> { [ masculine, ordfeminine, backslash, backslash ] };
|
||||
key <AE01> { [ 1, exclam, bar, exclamdown ] };
|
||||
key <AE03> { [ 3, periodcentered, numbersign, sterling ] };
|
||||
key <AE04> { [ 4, dollar, asciitilde, dollar ] };
|
||||
key <AE11> { [apostrophe, question, backslash, questiondown ] };
|
||||
key <AE12> { [exclamdown, questiondown, dead_cedilla, dead_ogonek] };
|
||||
|
||||
key <AD11> { [dead_grave, dead_circumflex, bracketleft, dead_abovering ] };
|
||||
key <AD12> { [ plus, asterisk, bracketright, dead_macron ] };
|
||||
|
||||
key <AC10> { [ ntilde, Ntilde, dead_tilde, dead_doubleacute ] };
|
||||
key <AC11> { [dead_acute, dead_diaeresis, braceleft, dead_caron ] };
|
||||
key <BKSL> { [ ccedilla, Ccedilla, braceright, dead_breve ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "winkeys" {
|
||||
|
||||
include "es(basic)"
|
||||
name[Group1]="Spanish (Windows)";
|
||||
include "eurosign(5)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "nodeadkeys" {
|
||||
|
||||
include "es(basic)"
|
||||
|
||||
name[Group1]="Spanish (no dead keys)";
|
||||
|
||||
key <AE12> { [exclamdown, questiondown, cedilla, ogonek ] };
|
||||
key <AD11> { [ grave, asciicircum, bracketleft, degree ] };
|
||||
key <AD12> { [ plus, asterisk, bracketright, macron ] };
|
||||
key <AC07> { [ j, J, ezh, EZH ] };
|
||||
key <AC10> { [ ntilde, Ntilde, asciitilde, doubleacute ] };
|
||||
key <AC11> { [ acute, diaeresis, braceleft, caron ] };
|
||||
key <BKSL> { [ ccedilla, Ccedilla, braceright, breve ] };
|
||||
key <AB10> { [ minus, underscore, ellipsis, abovedot ] };
|
||||
};
|
||||
|
||||
// Spanish Dvorak mapping (note R-H exchange)
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "dvorak" {
|
||||
|
||||
name[Group1]="Spanish (Dvorak)";
|
||||
|
||||
key <TLDE> {[ masculine, ordfeminine, backslash, degree ]};
|
||||
key <AE01> {[ 1, exclam, bar, onesuperior ]};
|
||||
key <AE02> {[ 2, quotedbl, at, twosuperior ]};
|
||||
key <AE03> {[ 3, periodcentered, numbersign, threesuperior ]};
|
||||
key <AE04> {[ 4, dollar, asciitilde, onequarter ]};
|
||||
key <AE05> {[ 5, percent, brokenbar, fiveeighths ]};
|
||||
key <AE06> {[ 6, ampersand, notsign, threequarters ]};
|
||||
key <AE07> {[ 7, slash, onehalf, seveneighths ]};
|
||||
key <AE08> {[ 8, parenleft, oneeighth, threeeighths ]};
|
||||
key <AE09> {[ 9, parenright, asciicircum ]};
|
||||
key <AE10> {[ 0, equal, grave, dead_doubleacute ]};
|
||||
key <AE11> {[ apostrophe, question, dead_macron, dead_ogonek ]};
|
||||
key <AE12> {[ exclamdown, questiondown, dead_breve, dead_abovedot ]};
|
||||
|
||||
key <AD01> {[ period, colon, less, guillemotleft ]};
|
||||
key <AD02> {[ comma, semicolon, greater, guillemotright ]};
|
||||
key <AD03> {[ ntilde, Ntilde, lstroke, Lstroke ]};
|
||||
key <AD04> {[ p, P, paragraph ]};
|
||||
key <AD05> {[ y, Y, yen ]};
|
||||
key <AD06> {[ f, F, tslash, Tslash ]};
|
||||
key <AD07> {[ g, G, dstroke, Dstroke ]};
|
||||
key <AD08> {[ c, C, cent, copyright ]};
|
||||
key <AD09> {[ h, H, hstroke, Hstroke ]};
|
||||
key <AD10> {[ l, L, sterling ]};
|
||||
key <AD11> {[ dead_grave, dead_circumflex, bracketleft, dead_caron ]};
|
||||
key <AD12> {[ plus, asterisk, bracketright, plusminus ]};
|
||||
|
||||
key <AC01> {[ a, A, ae, AE ]};
|
||||
key <AC02> {[ o, O, oslash, Oslash ]};
|
||||
key <AC03> {[ e, E, EuroSign ]};
|
||||
key <AC04> {[ u, U, aring, Aring ]};
|
||||
key <AC05> {[ i, I, oe, OE ]};
|
||||
key <AC06> {[ d, D, eth, ETH ]};
|
||||
key <AC07> {[ r, R, registered, trademark ]};
|
||||
key <AC08> {[ t, T, thorn, THORN ]};
|
||||
key <AC09> {[ n, N, eng, ENG ]};
|
||||
key <AC10> {[ s, S, ssharp, section ]};
|
||||
key <AC11> {[ dead_acute, dead_diaeresis, braceleft, dead_tilde ]};
|
||||
key <BKSL> {[ ccedilla, Ccedilla, braceright, dead_cedilla ]};
|
||||
|
||||
key <LSGT> {[ less, greater, guillemotleft, guillemotright ]};
|
||||
key <AB01> {[ minus, underscore, hyphen, macron ]};
|
||||
key <AB02> {[ q, Q, currency ]};
|
||||
key <AB03> {[ j, J ]};
|
||||
key <AB04> {[ k, K, kra ]};
|
||||
key <AB05> {[ x, X, multiply, division ]};
|
||||
key <AB06> {[ b, B ]};
|
||||
key <AB07> {[ m, M, mu ]};
|
||||
key <AB08> {[ w, W ]};
|
||||
key <AB09> {[ v, V ]};
|
||||
key <AB10> {[ z, Z ]};
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "cat" {
|
||||
|
||||
include "es(basic)"
|
||||
|
||||
name[Group1]="Catalan (Spain, with middle-dot L)";
|
||||
|
||||
key <AC09> { [ l, L, 0x1000140, 0x100013F ] };
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "ast" {
|
||||
|
||||
include "es(basic)"
|
||||
|
||||
name[Group1]="Asturian (Spain, with bottom-dot H and L)";
|
||||
|
||||
key <AC06> { [ h, H, 0x1001E25, 0x1001E24 ] };
|
||||
key <AC09> { [ l, L, 0x1001E37, 0x1001E36 ] };
|
||||
};
|
||||
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "olpc" {
|
||||
|
||||
// #HW-SPECIFIC
|
||||
|
||||
// http://wiki.laptop.org/go/OLPC_Spanish_Keyboard
|
||||
|
||||
include "us(basic)"
|
||||
name[Group1]="Spanish";
|
||||
|
||||
key <AE00> { [ masculine, ordfeminine ] };
|
||||
key <AE01> { [ 1, exclam, bar ] };
|
||||
key <AE02> { [ 2, quotedbl, at ] };
|
||||
key <AE03> { [ 3, dead_grave, numbersign, grave ] };
|
||||
key <AE05> { [ 5, percent, asciicircum, dead_circumflex ] };
|
||||
key <AE06> { [ 6, ampersand, notsign ] };
|
||||
key <AE07> { [ 7, slash, backslash ] };
|
||||
key <AE08> { [ 8, parenleft ] };
|
||||
key <AE09> { [ 9, parenright ] };
|
||||
key <AE10> { [ 0, equal ] };
|
||||
key <AE11> { [ apostrophe, question ] };
|
||||
key <AE12> { [ exclamdown, questiondown ] };
|
||||
|
||||
key <AD03> { [ e, E, EuroSign ] };
|
||||
key <AD11> { [ dead_acute, dead_diaeresis, acute, dead_abovering ] };
|
||||
key <AD12> { [ bracketleft, braceleft ] };
|
||||
|
||||
key <AC10> { [ ntilde, Ntilde ] };
|
||||
key <AC11> { [ plus, asterisk, dead_tilde ] };
|
||||
key <AC12> { [ bracketright, braceright, section ] };
|
||||
|
||||
key <AB08> { [ comma, semicolon ] };
|
||||
key <AB09> { [ period, colon ] };
|
||||
key <AB10> { [ minus, underscore ] };
|
||||
|
||||
key <I219> { [ less, greater, ISO_Next_Group ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "olpcm" {
|
||||
|
||||
// #HW-SPECIFIC
|
||||
|
||||
// Mechanical (non-membrane) OLPC Spanish keyboard layout.
|
||||
// See: http://wiki.laptop.org/go/OLPC_Spanish_Non-membrane_Keyboard
|
||||
|
||||
include "us(basic)"
|
||||
name[Group1]="Spanish";
|
||||
|
||||
key <AE00> { [ questiondown, exclamdown, backslash ] };
|
||||
key <AE01> { [ 1, exclam, bar ] };
|
||||
key <AE02> { [ 2, quotedbl, at ] };
|
||||
key <AE03> { [ 3, dead_grave, numbersign, grave ] };
|
||||
key <AE04> { [ 4, dollar, asciitilde, dead_tilde ] };
|
||||
key <AE05> { [ 5, percent, asciicircum, dead_circumflex ] };
|
||||
key <AE06> { [ 6, ampersand, notsign ] };
|
||||
key <AE07> { [ 7, slash, backslash ] }; // no '\' label on olpcm, leave for compatibility
|
||||
key <AE08> { [ 8, parenleft, masculine ] };
|
||||
key <AE09> { [ 9, parenright, ordfeminine ] };
|
||||
key <AE10> { [ 0, equal ] };
|
||||
key <AE11> { [ apostrophe, question ] };
|
||||
|
||||
key <AD03> { [ e, E, EuroSign ] };
|
||||
key <AD11> { [ dead_acute, dead_diaeresis, dead_abovering, acute ] };
|
||||
key <AD12> { [ plus, asterisk ] };
|
||||
|
||||
key <AC10> { [ ntilde, Ntilde ] };
|
||||
// no AC11 or AC12 on olpcm
|
||||
|
||||
key <AB08> { [ comma, semicolon ] };
|
||||
key <AB09> { [ period, colon ] };
|
||||
key <AB10> { [ minus, underscore ] };
|
||||
|
||||
key <AA02> { [ less, greater ] };
|
||||
key <AA06> { [ bracketleft, braceleft, ccedilla, Ccedilla ] };
|
||||
key <AA07> { [ bracketright, braceright ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "deadtilde" {
|
||||
|
||||
include "es(basic)"
|
||||
|
||||
name[Group1]="Spanish (dead tilde)";
|
||||
|
||||
key <AE04> { [ 4, dollar, dead_tilde, dollar ] };
|
||||
key <AC10> { [ ntilde, Ntilde, asciitilde, dead_doubleacute ] };
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "olpc2" {
|
||||
// #HW-SPECIFIC
|
||||
|
||||
// Modified variant of US International layout, specifically for Peru
|
||||
// Contact: Sayamindu Dasgupta <sayamindu@laptop.org>
|
||||
|
||||
include "us(olpc)"
|
||||
name[Group1]="Spanish";
|
||||
|
||||
key <AE03> { [ 3, numbersign, dead_grave, dead_grave] }; // combining grave
|
||||
key <I236> { [ XF86Start ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
// EXTRAS:
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "sun_type6" {
|
||||
include "sun_vndr/es(sun_type6)"
|
||||
};
|
||||
+1404
File diff suppressed because it is too large
Load Diff
+249
@@ -0,0 +1,249 @@
|
||||
// Keyboard layouts for Great Britain.
|
||||
|
||||
default partial alphanumeric_keys
|
||||
xkb_symbols "basic" {
|
||||
|
||||
// The basic UK layout, also known as the IBM 166 layout,
|
||||
// but with the useless brokenbar pushed two levels up.
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]="English (UK)";
|
||||
|
||||
key <TLDE> { [ grave, notsign, bar, bar ] };
|
||||
key <AE02> { [ 2, quotedbl, twosuperior, oneeighth ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, sterling ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
|
||||
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||
key <BKSL> { [numbersign, asciitilde, dead_grave, dead_breve ] };
|
||||
|
||||
key <LSGT> { [ backslash, bar, bar, brokenbar ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "intl" {
|
||||
|
||||
// A UK layout but with five accents made into dead keys:
|
||||
// grave, diaeresis, circumflex, acute, and tilde.
|
||||
// By Phil Jones <philjones1 at blueyonder.co.uk>.
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]="English (UK, intl., with dead keys)";
|
||||
|
||||
key <TLDE> { [ dead_grave, notsign, bar, bar ] };
|
||||
key <AE02> { [ 2, dead_diaeresis, twosuperior, onehalf ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, onethird ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
key <AE06> { [ 6, dead_circumflex, threequarters, onesixth ] };
|
||||
|
||||
key <AC11> { [ dead_acute, at, apostrophe, bar ] };
|
||||
key <BKSL> { [ numbersign, dead_tilde, bar, bar ] };
|
||||
|
||||
key <LSGT> { [ backslash, bar, bar, bar ] };
|
||||
key <AB08> { [ comma, less, ccedilla, Ccedilla ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "extd" {
|
||||
// Clone of the Microsoft "United Kingdom Extended" layout, which
|
||||
// includes dead keys for: grave; diaeresis; circumflex; tilde; and
|
||||
// accute. It also enables direct access to accute characters using
|
||||
// the Multi_key (Alt Gr).
|
||||
//
|
||||
// Taken from...
|
||||
// "Windows Keyboard Layouts"
|
||||
// https://docs.microsoft.com/en-gb/globalization/windows-keyboard-layouts#U
|
||||
//
|
||||
// -- Jonathan Miles <jon@cybah.co.uk>
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]="English (UK, extended, Windows)";
|
||||
|
||||
key <TLDE> { [ dead_grave, notsign, brokenbar, NoSymbol ] };
|
||||
key <AE02> { [ 2, quotedbl, dead_diaeresis, onehalf ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, onethird ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
key <AE06> { [ 6, asciicircum, dead_circumflex, NoSymbol ] };
|
||||
|
||||
key <AD02> { [ w, W, wacute, Wacute ] };
|
||||
key <AD03> { [ e, E, eacute, Eacute ] };
|
||||
key <AD06> { [ y, Y, yacute, Yacute ] };
|
||||
key <AD07> { [ u, U, uacute, Uacute ] };
|
||||
key <AD08> { [ i, I, iacute, Iacute ] };
|
||||
key <AD09> { [ o, O, oacute, Oacute ] };
|
||||
key <AD12> { [ bracketright, braceright, NoSymbol, bar ] };
|
||||
|
||||
key <AC01> { [ a, A, aacute, Aacute ] };
|
||||
key <AC11> { [ apostrophe, at, dead_acute, grave ] };
|
||||
key <BKSL> { [ numbersign, asciitilde, dead_tilde, backslash ] };
|
||||
|
||||
key <LSGT> { [ backslash, bar, NoSymbol, NoSymbol ] };
|
||||
key <AB03> { [ c, C, ccedilla, Ccedilla ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
// Describe the differences between the US Colemak layout
|
||||
// and a UK variant. By Andy Buckley (andy@insectnation.org)
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "colemak" {
|
||||
include "us(colemak)"
|
||||
|
||||
name[Group1]="English (UK, Colemak)";
|
||||
|
||||
key <TLDE> { [ grave, notsign, bar, asciitilde ] };
|
||||
key <AE02> { [ 2, quotedbl, twosuperior, oneeighth ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, sterling ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
|
||||
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||
key <BKSL> { [numbersign, asciitilde, dead_grave, dead_breve ] };
|
||||
|
||||
key <LSGT> { [ backslash, bar, asciitilde, brokenbar ] };
|
||||
};
|
||||
|
||||
// Colemak-DH (ISO) layout, UK Variant, https://colemakmods.github.io/mod-dh/
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "colemak_dh" {
|
||||
include "us(colemak_dh)"
|
||||
|
||||
name[Group1]="English (UK, Colemak-DH)";
|
||||
|
||||
key <TLDE> { [ grave, notsign, bar, asciitilde ] };
|
||||
key <AE02> { [ 2, quotedbl, twosuperior, oneeighth ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, sterling ] };
|
||||
key <AE04> { [ 4, dollar, EuroSign, onequarter ] };
|
||||
|
||||
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||
key <BKSL> { [numbersign, asciitilde, dead_grave, dead_breve ] };
|
||||
|
||||
key <AB05> { [ backslash, bar, asciitilde, brokenbar ] };
|
||||
};
|
||||
|
||||
|
||||
// Dvorak (UK) keymap (by odaen) allowing the usage of
|
||||
// the £ and ? key and swapping the @ and " keys.
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "dvorak" {
|
||||
include "us(dvorak-alt-intl)"
|
||||
|
||||
name[Group1]="English (UK, Dvorak)";
|
||||
|
||||
key <TLDE> { [ grave, notsign, bar, bar ] };
|
||||
key <AE02> { [ 2, quotedbl, twosuperior, NoSymbol ] };
|
||||
key <AE03> { [ 3, sterling, threesuperior, NoSymbol ] };
|
||||
key <AD01> { [ apostrophe, at ] };
|
||||
key <BKSL> { [ numbersign, asciitilde ] };
|
||||
key <LSGT> { [ backslash, bar ] };
|
||||
};
|
||||
|
||||
// Dvorak letter positions, but punctuation all in the normal UK positions.
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "dvorakukp" {
|
||||
include "gb(dvorak)"
|
||||
|
||||
name[Group1]="English (UK, Dvorak, with UK punctuation)";
|
||||
|
||||
key <AE11> { [ minus, underscore ] };
|
||||
key <AE12> { [ equal, plus ] };
|
||||
key <AD11> { [ bracketleft, braceleft ] };
|
||||
key <AD12> { [ bracketright, braceright ] };
|
||||
key <AD01> { [ slash, question ] };
|
||||
key <AC11> { [apostrophe, at, dead_circumflex, dead_caron] };
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "mac" {
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]= "English (UK, Macintosh)";
|
||||
|
||||
key <TLDE> { [ section, plusminus ] };
|
||||
key <AE02> { [ 2, at, EuroSign ] };
|
||||
key <AE03> { [ 3, sterling, numbersign ] };
|
||||
key <LSGT> { [ grave, asciitilde ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
include "level3(enter_switch)"
|
||||
};
|
||||
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "mac_intl" {
|
||||
|
||||
include "latin"
|
||||
|
||||
name[Group1]="English (UK, Macintosh, intl.)";
|
||||
|
||||
key <TLDE> { [ section, plusminus, notsign, notsign ] }; //dead_grave
|
||||
key <AE02> { [ 2, at, EuroSign, onehalf ] };
|
||||
key <AE03> { [ 3, sterling, twosuperior, onethird ] };
|
||||
key <AE04> { [ 4, dollar, threesuperior, onequarter ] };
|
||||
key <AE06> { [ 6, dead_circumflex, NoSymbol, onesixth ] };
|
||||
key <AD09> { [ o, O, oe, OE ] };
|
||||
|
||||
key <AC11> { [ dead_acute, dead_diaeresis, dead_diaeresis, bar ] }; //dead_doubleacute
|
||||
key <BKSL> { [ backslash, bar, numbersign, bar ] };
|
||||
|
||||
key <LSGT> { [ dead_grave, dead_tilde, brokenbar, bar ] };
|
||||
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "pl" {
|
||||
|
||||
// Polish accented letters on upper levels of corresponding base letters.
|
||||
// Idea from Wawrzyniec Niewodniczański, adapted by Aleksander Kowalski.
|
||||
|
||||
include "gb(basic)"
|
||||
|
||||
name[Group1]="Polish (British keyboard)";
|
||||
|
||||
key <AD03> { [ e, E, eogonek, Eogonek ] };
|
||||
key <AD09> { [ o, O, oacute, Oacute ] };
|
||||
|
||||
key <AC01> { [ a, A, aogonek, Aogonek ] };
|
||||
key <AC02> { [ s, S, sacute, Sacute ] };
|
||||
|
||||
key <AB01> { [ z, Z, zabovedot, Zabovedot ] };
|
||||
key <AB02> { [ x, X, zacute, Zacute ] };
|
||||
key <AB03> { [ c, C, cacute, Cacute ] };
|
||||
key <AB06> { [ n, N, nacute, Nacute ] };
|
||||
};
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "gla" {
|
||||
|
||||
// Grave-accented letters on the upper levels of the relevant vowels.
|
||||
|
||||
include "gb(basic)"
|
||||
|
||||
name[Group1]="Scottish Gaelic";
|
||||
|
||||
key <AD03> { [ e, E, egrave, Egrave ] };
|
||||
key <AD07> { [ u, U, ugrave, Ugrave ] };
|
||||
key <AD08> { [ i, I, igrave, Igrave ] };
|
||||
key <AD09> { [ o, O, ograve, Ograve ] };
|
||||
|
||||
key <AC01> { [ a, A, agrave, Agrave ] };
|
||||
};
|
||||
|
||||
// EXTRAS:
|
||||
|
||||
partial alphanumeric_keys
|
||||
xkb_symbols "sun_type6" {
|
||||
include "sun_vndr/gb(sun_type6)"
|
||||
};
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// The <KPDL> key is a mess.
|
||||
// It was probably originally meant to be a decimal separator.
|
||||
// Except since it was declared by USA people it didn't use the original
|
||||
// SI separator "," but a "." (since then the USA managed to f-up the SI
|
||||
// by making "." an accepted alternative, but standards still use "," as
|
||||
// default)
|
||||
// As a result users of SI-abiding countries expect either a "." or a ","
|
||||
// or a "decimal_separator" which may or may not be translated in one of the
|
||||
// above depending on applications.
|
||||
// It's not possible to define a default per-country since user expectations
|
||||
// depend on the conflicting choices of their most-used applications,
|
||||
// operating system, etc. Therefore it needs to be a configuration setting
|
||||
// Copyright © 2007 Nicolas Mailhot <nicolas.mailhot @ laposte.net>
|
||||
|
||||
|
||||
// Legacy <KPDL> #1
|
||||
// This assumes KP_Decimal will be translated in a dot
|
||||
partial keypad_keys
|
||||
xkb_symbols "dot" {
|
||||
|
||||
key.type[Group1]="KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, KP_Decimal ] }; // <delete> <separator>
|
||||
};
|
||||
|
||||
|
||||
// Legacy <KPDL> #2
|
||||
// This assumes KP_Separator will be translated in a comma
|
||||
partial keypad_keys
|
||||
xkb_symbols "comma" {
|
||||
|
||||
key.type[Group1]="KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, KP_Separator ] }; // <delete> <separator>
|
||||
};
|
||||
|
||||
|
||||
// Period <KPDL>, usual keyboard serigraphy in most countries
|
||||
partial keypad_keys
|
||||
xkb_symbols "dotoss" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, period, comma, 0x100202F ] }; // <delete> . , ⍽ (narrow no-break space)
|
||||
};
|
||||
|
||||
|
||||
// Period <KPDL>, usual keyboard serigraphy in most countries, latin-9 restriction
|
||||
partial keypad_keys
|
||||
xkb_symbols "dotoss_latin9" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, period, comma, nobreakspace ] }; // <delete> . , ⍽ (no-break space)
|
||||
};
|
||||
|
||||
|
||||
// Comma <KPDL>, what most non anglo-saxon people consider the real separator
|
||||
partial keypad_keys
|
||||
xkb_symbols "commaoss" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, comma, period, 0x100202F ] }; // <delete> , . ⍽ (narrow no-break space)
|
||||
};
|
||||
|
||||
|
||||
// Momayyez <KPDL>: Bahrain, Iran, Iraq, Kuwait, Oman, Qatar, Saudi Arabia, Syria, UAE
|
||||
partial keypad_keys
|
||||
xkb_symbols "momayyezoss" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, 0x100066B, comma, 0x100202F ] }; // <delete> ? , ⍽ (narrow no-break space)
|
||||
};
|
||||
|
||||
|
||||
// Abstracted <KPDL>, pray everything will work out (it usually does not)
|
||||
partial keypad_keys
|
||||
xkb_symbols "kposs" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ KP_Delete, KP_Decimal, KP_Separator, 0x100202F ] }; // <delete> ? ? ⍽ (narrow no-break space)
|
||||
};
|
||||
|
||||
// Spreadsheets may be configured to use the dot as decimal
|
||||
// punctuation, comma as a thousands separator and then semi-colon as
|
||||
// the list separator. Of these, dot and semi-colon is most important
|
||||
// when entering data by the keyboard; the comma can then be inferred
|
||||
// and added to the presentation afterwards. Using semi-colon as a
|
||||
// general separator may in fact be preferred to avoid ambiguities
|
||||
// in data files. Most times a decimal separator is hard-coded, it
|
||||
// seems to be period, probably since this is the syntax used in
|
||||
// (most) programming languages.
|
||||
partial keypad_keys
|
||||
xkb_symbols "semi" {
|
||||
|
||||
key.type[Group1]="FOUR_LEVEL_MIXED_KEYPAD" ;
|
||||
|
||||
key <KPDL> { [ NoSymbol, NoSymbol, semicolon ] };
|
||||
};
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
// Common Latin alphabet layout
|
||||
|
||||
default partial
|
||||
xkb_symbols "basic" {
|
||||
|
||||
key <AE01> { [ 1, exclam, onesuperior, exclamdown ] };
|
||||
key <AE02> { [ 2, at, twosuperior, oneeighth ] };
|
||||
key <AE03> { [ 3, numbersign, threesuperior, sterling ] };
|
||||
key <AE04> { [ 4, dollar, onequarter, dollar ] };
|
||||
key <AE05> { [ 5, percent, onehalf, threeeighths ] };
|
||||
key <AE06> { [ 6, asciicircum, threequarters, fiveeighths ] };
|
||||
key <AE07> { [ 7, ampersand, braceleft, seveneighths ] };
|
||||
key <AE08> { [ 8, asterisk, bracketleft, trademark ] };
|
||||
key <AE09> { [ 9, parenleft, bracketright, plusminus ] };
|
||||
key <AE10> { [ 0, parenright, braceright, degree ] };
|
||||
key <AE11> { [ minus, underscore, backslash, questiondown ] };
|
||||
key <AE12> { [ equal, plus, dead_cedilla, dead_ogonek ] };
|
||||
|
||||
key <AD01> { [ q, Q, at, Greek_OMEGA ] };
|
||||
key <AD02> { [ w, W, U017F, section ] };
|
||||
key <AD03> { [ e, E, e, E ] };
|
||||
key <AD04> { [ r, R, paragraph, registered ] };
|
||||
key <AD05> { [ t, T, tslash, Tslash ] };
|
||||
key <AD06> { [ y, Y, leftarrow, yen ] };
|
||||
key <AD07> { [ u, U, downarrow, uparrow ] };
|
||||
key <AD08> { [ i, I, rightarrow, idotless ] };
|
||||
key <AD09> { [ o, O, oslash, Oslash ] };
|
||||
key <AD10> { [ p, P, thorn, THORN ] };
|
||||
key <AD11> { [bracketleft, braceleft, dead_diaeresis, dead_abovering ] };
|
||||
key <AD12> { [bracketright, braceright, dead_tilde, dead_macron ] };
|
||||
|
||||
key <AC01> { [ a, A, ae, AE ] };
|
||||
key <AC02> { [ s, S, ssharp, U1E9E ] };
|
||||
key <AC03> { [ d, D, eth, ETH ] };
|
||||
key <AC04> { [ f, F, dstroke, ordfeminine ] };
|
||||
key <AC05> { [ g, G, eng, ENG ] };
|
||||
key <AC06> { [ h, H, hstroke, Hstroke ] };
|
||||
key <AC07> { [ j, J, dead_hook, dead_horn ] };
|
||||
key <AC08> { [ k, K, kra, ampersand ] };
|
||||
key <AC09> { [ l, L, lstroke, Lstroke ] };
|
||||
key <AC10> { [ semicolon, colon, dead_acute, dead_doubleacute ] };
|
||||
key <AC11> { [apostrophe, quotedbl, dead_circumflex, dead_caron ] };
|
||||
key <TLDE> { [ grave, asciitilde, notsign, notsign ] };
|
||||
|
||||
key <BKSL> { [ backslash, bar, dead_grave, dead_breve ] };
|
||||
key <AB01> { [ z, Z, guillemotleft, less ] };
|
||||
key <AB02> { [ x, X, guillemotright, greater ] };
|
||||
key <AB03> { [ c, C, cent, copyright ] };
|
||||
key <AB04> { [ v, V, doublelowquotemark, singlelowquotemark ] };
|
||||
key <AB05> { [ b, B, leftdoublequotemark, leftsinglequotemark ] };
|
||||
key <AB06> { [ n, N, rightdoublequotemark, rightsinglequotemark ] };
|
||||
key <AB07> { [ m, M, mu, masculine ] };
|
||||
key <AB08> { [ comma, less, U2022, multiply ] }; // bullet
|
||||
key <AB09> { [ period, greater, periodcentered, division ] };
|
||||
key <AB10> { [ slash, question, dead_belowdot, dead_abovedot ] };
|
||||
};
|
||||
|
||||
// Northern Europe ( Danish, Finnish, Norwegian, Swedish) common layout
|
||||
|
||||
partial
|
||||
xkb_symbols "type2" {
|
||||
|
||||
include "latin"
|
||||
|
||||
key <AE01> { [ 1, exclam, exclamdown, onesuperior ] };
|
||||
key <AE02> { [ 2, quotedbl, at, twosuperior ] };
|
||||
key <AE03> { [ 3, numbersign, sterling, threesuperior] };
|
||||
key <AE04> { [ 4, currency, dollar, onequarter ] };
|
||||
key <AE05> { [ 5, percent, onehalf, cent ] };
|
||||
key <AE06> { [ 6, ampersand, yen, fiveeighths ] };
|
||||
key <AE07> { [ 7, slash, braceleft, division ] };
|
||||
key <AE08> { [ 8, parenleft, bracketleft, guillemotleft] };
|
||||
key <AE09> { [ 9, parenright, bracketright, guillemotright] };
|
||||
key <AE10> { [ 0, equal, braceright, degree ] };
|
||||
|
||||
key <AD03> { [ e, E, EuroSign, cent ] };
|
||||
key <AD04> { [ r, R, registered, registered ] };
|
||||
key <AD05> { [ t, T, thorn, THORN ] };
|
||||
key <AD09> { [ o, O, oe, OE ] };
|
||||
key <AD11> { [ aring, Aring, dead_diaeresis, dead_abovering ] };
|
||||
key <AD12> { [dead_diaeresis, dead_circumflex, dead_tilde, dead_caron ] };
|
||||
|
||||
key <AC01> { [ a, A, ordfeminine, masculine ] };
|
||||
|
||||
key <AB03> { [ c, C, copyright, copyright ] };
|
||||
key <AB08> { [ comma, semicolon, dead_cedilla, dead_ogonek ] };
|
||||
key <AB09> { [ period, colon, periodcentered, dead_abovedot ] };
|
||||
key <AB10> { [ minus, underscore, dead_belowdot, dead_abovedot ] };
|
||||
};
|
||||
|
||||
// Slavic Latin ( Albanian, Croatian, Polish, Slovene, Yugoslav)
|
||||
// common layout
|
||||
|
||||
partial
|
||||
xkb_symbols "type3" {
|
||||
|
||||
include "latin"
|
||||
|
||||
key <AD01> { [ q, Q, backslash, Greek_OMEGA ] };
|
||||
key <AD02> { [ w, W, bar, section ] };
|
||||
key <AD06> { [ z, Z, leftarrow, yen ] };
|
||||
|
||||
key <AC04> { [ f, F, bracketleft, ordfeminine ] };
|
||||
key <AC05> { [ g, G, bracketright, ENG ] };
|
||||
key <AC08> { [ k, K, lstroke, ampersand ] };
|
||||
|
||||
key <AB01> { [ y, Y, guillemotleft, less ] };
|
||||
key <AB04> { [ v, V, at, grave ] };
|
||||
key <AB05> { [ b, B, braceleft, apostrophe ] };
|
||||
key <AB06> { [ n, N, braceright, acute ] };
|
||||
key <AB07> { [ m, M, section, masculine ] };
|
||||
key <AB08> { [ comma, semicolon, less, multiply ] };
|
||||
key <AB09> { [ period, colon, greater, division ] };
|
||||
};
|
||||
|
||||
// Another common Latin layout
|
||||
// (German, Estonian, Spanish, Icelandic, Italian, Latin American, Portuguese)
|
||||
|
||||
partial
|
||||
xkb_symbols "type4" {
|
||||
|
||||
include "latin"
|
||||
|
||||
key <AE02> { [ 2, quotedbl, at, oneeighth ] };
|
||||
key <AE06> { [ 6, ampersand, notsign, fiveeighths ] };
|
||||
key <AE07> { [ 7, slash, braceleft, seveneighths ] };
|
||||
key <AE08> { [ 8, parenleft, bracketleft, trademark ] };
|
||||
key <AE09> { [ 9, parenright, bracketright, plusminus ] };
|
||||
key <AE10> { [ 0, equal, braceright, degree ] };
|
||||
|
||||
key <AD03> { [ e, E, EuroSign, cent ] };
|
||||
|
||||
key <AB08> { [ comma, semicolon, U2022, multiply ] }; // bullet
|
||||
key <AB09> { [ period, colon, periodcentered, division ] };
|
||||
key <AB10> { [ minus, underscore, dead_belowdot, dead_abovedot ] };
|
||||
};
|
||||
|
||||
partial
|
||||
xkb_symbols "nodeadkeys" {
|
||||
|
||||
key <AE12> { [ equal, plus, cedilla, ogonek ] };
|
||||
key <AD11> { [bracketleft, braceleft, diaeresis, degree ] };
|
||||
key <AD12> { [bracketright, braceright, asciitilde, macron ] };
|
||||
key <AC07> { [ j, J, ezh, EZH ] };
|
||||
key <AC10> { [ semicolon, colon, acute, doubleacute ] };
|
||||
key <AC11> { [apostrophe, quotedbl, asciicircum, caron ] };
|
||||
key <BKSL> { [ backslash, bar, grave, breve ] };
|
||||
key <AB10> { [ slash, question, ellipsis, abovedot ] };
|
||||
};
|
||||
|
||||
partial
|
||||
xkb_symbols "type2_nodeadkeys" {
|
||||
|
||||
include "latin(nodeadkeys)"
|
||||
|
||||
key <AD11> { [ aring, Aring, diaeresis, degree ] };
|
||||
key <AD12> { [ diaeresis, asciicircum, asciitilde, caron ] };
|
||||
key <AB08> { [ comma, semicolon, cedilla, ogonek ] };
|
||||
key <AB09> { [ period, colon, periodcentered, abovedot ] };
|
||||
key <AB10> { [ minus, underscore, ellipsis, abovedot ] };
|
||||
};
|
||||
|
||||
partial
|
||||
xkb_symbols "type3_nodeadkeys" {
|
||||
|
||||
include "latin(nodeadkeys)"
|
||||
};
|
||||
|
||||
partial
|
||||
xkb_symbols "type4_nodeadkeys" {
|
||||
|
||||
include "latin(nodeadkeys)"
|
||||
|
||||
key <AB10> { [ minus, underscore, ellipsis, abovedot ] };
|
||||
};
|
||||
|
||||
// Added 2008.03.05 by Marcin Woliński
|
||||
// See http://marcinwolinski.pl/keyboard/ for a description.
|
||||
// Used by pl(intl)
|
||||
//
|
||||
// ┌─────┐
|
||||
// │ 2 4 │ 2 = Shift, 4 = Level3 + Shift
|
||||
// │ 1 3 │ 1 = Normal, 3 = Level3
|
||||
// └─────┘
|
||||
// ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┲━━━━━━━━━┓
|
||||
// │ ~ ~ │ ! ' │ @ " │ # ˝ │ $ ¸ │ % ˇ │ ^ ^ │ & ˘ │ * ̇ │ ( ̣ │ ) ° │ _ ¯ │ + ˛ ┃ ⌫ Back- ┃
|
||||
// │ ` ` │ 1 ¡ │ 2 © │ 3 • │ 4 § │ 5 € │ 6 ¢ │ 7 − │ 8 × │ 9 ÷ │ 0 ° │ - – │ = — ┃ space ┃
|
||||
// ┢━━━━━┷━┱───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┴─┬───┺━┳━━━━━━━┫
|
||||
// ┃ ┃ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │ { « │ } » ┃ Enter ┃
|
||||
// ┃Tab ↹ ┃ q │ w │ e │ r │ t │ y │ u │ i │ o │ p │ [ ‹ │ ] › ┃ ⏎ ┃
|
||||
// ┣━━━━━━━┻┱────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┴┬────┺┓ ┃
|
||||
// ┃ ┃ A │ S │ D │ F │ G │ H │ J │ K │ L │ : “ │ " ” │ | ¶ ┃ ┃
|
||||
// ┃Caps ⇬ ┃ a │ s │ d │ f │ g │ h │ j │ k │ l │ ; ‘ │ ' ’ │ \ ┃ ┃
|
||||
// ┣━━━━━━━━┹────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┬┴────┲┷━━━━━┻━━━━━━┫
|
||||
// ┃ │ Z │ X │ C │ V │ B │ N │ M │ < „ │ > · │ ? ¿ ┃ ┃
|
||||
// ┃Shift ⇧ │ z │ x │ c │ v │ b │ n │ m │ , ‚ │ . … │ / ⁄ ┃Shift ⇧ ┃
|
||||
// ┣━━━━━━━┳━━━━━┷━┳━━━┷━━━┱─┴─────┴─────┴─────┴─────┴─────┴───┲━┷━━━━━╈━━━━━┻━┳━━━━━━━┳━━━┛
|
||||
// ┃ ┃ ┃ ┃ ␣ ⍽ ┃ ┃ ┃ ┃
|
||||
// ┃Ctrl ┃Meta ┃Alt ┃ ␣ Space ⍽ ┃AltGr ⇮┃Menu ┃Ctrl ┃
|
||||
// ┗━━━━━━━┻━━━━━━━┻━━━━━━━┹───────────────────────────────────┺━━━━━━━┻━━━━━━━┻━━━━━━━┛
|
||||
|
||||
partial
|
||||
xkb_symbols "intl" {
|
||||
|
||||
key <TLDE> { [ grave, asciitilde, dead_grave, dead_tilde ] };
|
||||
key <AE01> { [ 1, exclam, exclamdown, dead_acute ] };
|
||||
key <AE02> { [ 2, at, copyright, dead_diaeresis ] };
|
||||
key <AE03> { [ 3, numbersign, U2022, dead_doubleacute ] }; // U+2022 is bullet (the name bullet does not work)
|
||||
key <AE04> { [ 4, dollar, section, dead_cedilla ] };
|
||||
key <AE05> { [ 5, percent, EuroSign, dead_caron ] };
|
||||
key <AE06> { [ 6, asciicircum, cent, dead_circumflex ] };
|
||||
key <AE07> { [ 7, ampersand, U2212, dead_breve ] }; // U+2212 is MINUS SIGN
|
||||
key <AE08> { [ 8, asterisk, multiply, dead_abovedot ] };
|
||||
key <AE09> { [ 9, parenleft, division, dead_belowdot ] };
|
||||
key <AE10> { [ 0, parenright, degree, dead_abovering ] };
|
||||
key <AE11> { [ minus, underscore, endash, dead_macron ] };
|
||||
key <AE12> { [ equal, plus, emdash, dead_ogonek ] };
|
||||
|
||||
key <AD01> { [ q, Q ] };
|
||||
key <AD02> { [ w, W ] };
|
||||
key <AD03> { [ e, E ] };
|
||||
key <AD04> { [ r, R ] };
|
||||
key <AD05> { [ t, T ] };
|
||||
key <AD06> { [ y, Y ] };
|
||||
key <AD07> { [ u, U ] };
|
||||
key <AD08> { [ i, I ] };
|
||||
key <AD09> { [ o, O ] };
|
||||
key <AD10> { [ p, P ] };
|
||||
key <AD11> { [bracketleft, braceleft, U2039, guillemotleft ] };
|
||||
key <AD12> { [bracketright, braceright, U203A, guillemotright ] };
|
||||
|
||||
key <AC01> { [ a, A ] };
|
||||
key <AC02> { [ s, S ] };
|
||||
key <AC03> { [ d, D ] };
|
||||
key <AC04> { [ f, F ] };
|
||||
key <AC05> { [ g, G ] };
|
||||
key <AC06> { [ h, H ] };
|
||||
key <AC07> { [ j, J ] };
|
||||
key <AC08> { [ k, K ] };
|
||||
key <AC09> { [ l, L ] };
|
||||
key <AC10> { [ semicolon, colon, leftsinglequotemark, leftdoublequotemark ] };
|
||||
key <AC11> { [apostrophe, quotedbl, rightsinglequotemark, rightdoublequotemark ] };
|
||||
|
||||
key <BKSL> { [ backslash, bar, NoSymbol, paragraph ] };
|
||||
key <AB01> { [ z, Z ] };
|
||||
key <AB02> { [ x, X ] };
|
||||
key <AB03> { [ c, C ] };
|
||||
key <AB04> { [ v, V ] };
|
||||
key <AB05> { [ b, B ] };
|
||||
key <AB06> { [ n, N ] };
|
||||
key <AB07> { [ m, M ] };
|
||||
key <AB08> { [ comma, less, singlelowquotemark, doublelowquotemark ] };
|
||||
key <AB09> { [ period, greater, ellipsis, periodcentered ] };
|
||||
key <AB10> { [ slash, question, U2044, questiondown ] }; // U+2044 is FRACTION SLASH
|
||||
};
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
// These variants assign ISO_Level3_Shift to various keys
|
||||
// so that levels 3 and 4 can be reached.
|
||||
|
||||
// The default behaviour:
|
||||
// the right Alt key (AltGr) chooses the third symbol engraved on a key.
|
||||
default partial modifier_keys
|
||||
xkb_symbols "ralt_switch" {
|
||||
key <RALT> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The right Alt key never chooses the third level.
|
||||
// This option attempts to undo the effect of a layout's inclusion of
|
||||
// 'ralt_switch'. You may want to also select another level3 option
|
||||
// to map the level3 shift to some other key.
|
||||
partial modifier_keys
|
||||
xkb_symbols "ralt_alt" {
|
||||
key <RALT> {[ Alt_R, Meta_R ], type[group1]="TWO_LEVEL" };
|
||||
modifier_map Mod1 { <RALT> };
|
||||
};
|
||||
|
||||
// The right Alt key (while pressed) chooses the third shift level,
|
||||
// and Compose is mapped to its second level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "ralt_switch_multikey" {
|
||||
key <RALT> {[ ISO_Level3_Shift, Multi_key ], type[group1]="TWO_LEVEL" };
|
||||
};
|
||||
|
||||
// Either Alt key (while pressed) chooses the third shift level.
|
||||
// (To be used mostly to imitate Mac OS functionality.)
|
||||
partial modifier_keys
|
||||
xkb_symbols "alt_switch" {
|
||||
include "level3(lalt_switch)"
|
||||
include "level3(ralt_switch)"
|
||||
};
|
||||
|
||||
// The left Alt key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "lalt_switch" {
|
||||
key <LALT> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The right Ctrl key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "switch" {
|
||||
key <RCTL> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Menu key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "menu_switch" {
|
||||
key <MENU> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// Either Win key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "win_switch" {
|
||||
include "level3(lwin_switch)"
|
||||
include "level3(rwin_switch)"
|
||||
};
|
||||
|
||||
// The left Win key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "lwin_switch" {
|
||||
key <LWIN> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The right Win key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "rwin_switch" {
|
||||
key <RWIN> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Enter key on the kepypad (while pressed) chooses the third shift level.
|
||||
// (This is especially useful for Mac laptops which miss the right Alt key.)
|
||||
partial modifier_keys
|
||||
xkb_symbols "enter_switch" {
|
||||
key <KPEN> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The CapsLock key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "caps_switch" {
|
||||
key <CAPS> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The CapsLock key (while pressed) chooses the third shift level and
|
||||
// Ctrl + CapsLock has the original CapsLock function.
|
||||
// The 2023 DIN standard for German keyboards recommends it as an option:
|
||||
// - https://de.wikipedia.org/wiki/E1_(Tastaturbelegung)#Feststelltaste/Umschaltsperre
|
||||
// - https://en.wikipedia.org/wiki/Caps_Lock#Abolition
|
||||
partial modifier_keys
|
||||
xkb_symbols "caps_switch_capslock_with_ctrl" {
|
||||
virtual_modifiers LevelThree;
|
||||
|
||||
key <CAPS> {
|
||||
type[Group1] = "PC_CONTROL_LEVEL2",
|
||||
symbols[Group1] = [ ISO_Level3_Shift, Caps_Lock ],
|
||||
// Explicit actions are preferred over modMap None/Mod5 { Caps_Lock }
|
||||
// because they have no side effect
|
||||
actions[Group1] = [ SetMods(modifiers = LevelThree), LockMods(modifiers = Lock) ]
|
||||
};
|
||||
};
|
||||
|
||||
// The Backslash key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "bksl_switch" {
|
||||
key <BKSL> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The AC11 key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "ac11_switch" {
|
||||
key <AC11> {[ ISO_Level3_Shift ], type[Group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Less/Greater key (while pressed) chooses the third shift level.
|
||||
partial modifier_keys
|
||||
xkb_symbols "lsgt_switch" {
|
||||
key <LSGT> {[ ISO_Level3_Shift ], type[group1]="ONE_LEVEL" };
|
||||
};
|
||||
|
||||
// The CapsLock key (while pressed) chooses the third shift level,
|
||||
// and latches when pressed together with another third-level chooser.
|
||||
partial modifier_keys
|
||||
xkb_symbols "caps_switch_latch" {
|
||||
key <CAPS> {[ ISO_Level3_Shift, ISO_Level3_Shift, ISO_Level3_Latch ],
|
||||
type[group1]="THREE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Backslash key (while pressed) chooses the third shift level,
|
||||
// and latches when pressed together with another third-level chooser.
|
||||
partial modifier_keys
|
||||
xkb_symbols "bksl_switch_latch" {
|
||||
key <BKSL> {[ ISO_Level3_Shift, ISO_Level3_Shift, ISO_Level3_Latch ],
|
||||
type[group1]="THREE_LEVEL" };
|
||||
};
|
||||
|
||||
// The Less/Greater key (while pressed) chooses the third shift level,
|
||||
// and latches when pressed together with another third-level chooser.
|
||||
partial modifier_keys
|
||||
xkb_symbols "lsgt_switch_latch" {
|
||||
key <LSGT> {[ ISO_Level3_Shift, ISO_Level3_Shift, ISO_Level3_Latch ],
|
||||
type[group1]="THREE_LEVEL" };
|
||||
};
|
||||
|
||||
// Top-row digit key 4 chooses third shift level when pressed alone.
|
||||
partial modifier_keys
|
||||
xkb_symbols "4_switch_isolated" {
|
||||
override key <AE04> {[ ISO_Level3_Shift ]};
|
||||
};
|
||||
|
||||
// Top-row digit key 9 chooses third shift level when pressed alone.
|
||||
partial modifier_keys
|
||||
xkb_symbols "9_switch_isolated" {
|
||||
override key <AE09> {[ ISO_Level3_Shift ]};
|
||||
};
|
||||
+2238
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,153 @@
|
||||
//! xkeyboard-config — keyboard layouts, compiled from the X11 xkeyboard-config database
|
||||
//! into native Zig. It turns a physical key (a USB HID usage, as the input module delivers)
|
||||
//! plus a modifier state into a **keysym** and, when the key produces one, a **character**
|
||||
//! (a Unicode scalar). This is the piece that lets a `KeyEvent.keycode` become a
|
||||
//! `KeyEvent.character`, without shipping an X11 runtime.
|
||||
//!
|
||||
//! The layout tables in `generated/layouts.zig` are produced by
|
||||
//! `tools/make-xkeyboard-config.py` (see ./README.md to regenerate). Those tables are
|
||||
//! deliberately pure data — each key carries its up-to-four levels and an XKB *type*. The
|
||||
//! type -> level selection semantics (which modifier picks which level) live here, so the
|
||||
//! data and the policy are separable.
|
||||
//!
|
||||
//! Scope (documented in README.md): group 1 only, no dead-key/compose composition (a dead
|
||||
//! key returns its keysym with no character), and a curated set of key types. Layouts:
|
||||
//! us, gb, de, fr, es, dvorak.
|
||||
//!
|
||||
//! Upstream xkeyboard-config and keysymdef.h are MIT/X11 licensed; see vendor/COPYING and
|
||||
//! vendor/PROVENANCE.md.
|
||||
|
||||
const std = @import("std");
|
||||
const generated = @import("layouts");
|
||||
|
||||
pub const Level = generated.Level;
|
||||
pub const KeyType = generated.KeyType;
|
||||
pub const Key = generated.Key;
|
||||
pub const Layout = generated.Layout;
|
||||
|
||||
/// The generated layouts, by name — as pointers, so they share identity with `all` and
|
||||
/// `byName` (and match the `*const Layout` that `map` takes).
|
||||
pub const us: *const Layout = &generated.us;
|
||||
pub const gb: *const Layout = &generated.gb;
|
||||
pub const de: *const Layout = &generated.de;
|
||||
pub const fr: *const Layout = &generated.fr;
|
||||
pub const es: *const Layout = &generated.es;
|
||||
pub const dvorak: *const Layout = &generated.dvorak;
|
||||
|
||||
/// Every generated layout, for enumeration (e.g. a settings UI).
|
||||
pub const all = generated.all;
|
||||
|
||||
/// The modifier state that selects a key's level. `level3` is AltGr (ISO Level3 Shift);
|
||||
/// `control` is accepted for completeness but does not affect level selection here.
|
||||
pub const Modifiers = struct {
|
||||
shift: bool = false,
|
||||
caps_lock: bool = false,
|
||||
level3: bool = false,
|
||||
control: bool = false,
|
||||
};
|
||||
|
||||
/// The result of a lookup: the X11 `keysym`, and the `character` it produces (a Unicode
|
||||
/// scalar) when it is a printable key — null for keys that produce none (Return, F1, a
|
||||
/// bare dead key, an unmapped key).
|
||||
pub const Mapping = struct {
|
||||
keysym: u32,
|
||||
character: ?u21,
|
||||
};
|
||||
|
||||
/// Which level (0..3) a key of `kind` selects under `mods`. XKB's canonical semantics:
|
||||
/// Shift picks the odd level, AltGr (level3) adds 2, and Caps acts like Shift for the
|
||||
/// alphabetic types. See the XKB "key types" — this covers the ones the vendored layouts
|
||||
/// use; anything else falls back to shift-or-not.
|
||||
fn selectLevel(kind: KeyType, mods: Modifiers) usize {
|
||||
const shift_or_caps = mods.shift != mods.caps_lock; // XOR: Caps behaves like Shift
|
||||
const low: usize = if (mods.shift) 1 else 0;
|
||||
const high: usize = if (mods.level3) 2 else 0;
|
||||
return switch (kind) {
|
||||
.one_level => 0,
|
||||
.two_level, .keypad, .other => low,
|
||||
.alphabetic => if (shift_or_caps) 1 else 0,
|
||||
.four_level => low + high,
|
||||
.four_level_alphabetic => (if (shift_or_caps) @as(usize, 1) else 0) + high,
|
||||
// Caps affects only the base pair, not the AltGr pair.
|
||||
.four_level_semialphabetic => if (mods.level3) 2 + low else (if (shift_or_caps) @as(usize, 1) else 0),
|
||||
};
|
||||
}
|
||||
|
||||
/// Map a physical key (`hid_usage`, a USB HID keyboard-page usage) under `mods` on
|
||||
/// `layout` to its keysym and character. Falls back gracefully when the selected level is
|
||||
/// undefined for the key: it drops the AltGr component, then the shift component, so a key
|
||||
/// with only a base/shift pair still yields something sensible under AltGr.
|
||||
pub fn map(layout: *const Layout, hid_usage: u8, mods: Modifiers) Mapping {
|
||||
const key = &layout.keys[hid_usage];
|
||||
var level = selectLevel(key.kind, mods);
|
||||
// Fall back to a defined level: full -> without AltGr -> base.
|
||||
if (key.levels[level].keysym == 0 and key.levels[level].unicode == 0) {
|
||||
const candidates = [_]usize{ level & 1, 0 };
|
||||
for (candidates) |candidate| {
|
||||
if (key.levels[candidate].keysym != 0 or key.levels[candidate].unicode != 0) {
|
||||
level = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const chosen = key.levels[level];
|
||||
return .{
|
||||
.keysym = chosen.keysym,
|
||||
.character = if (chosen.unicode != 0) @intCast(chosen.unicode) else null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Look up a layout by its name (`"us"`, `"gb"`, ...), or null if unknown.
|
||||
pub fn byName(name: []const u8) ?*const Layout {
|
||||
for (all) |layout| {
|
||||
if (std.mem.eql(u8, layout.name, name)) return layout;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- tests (host-run via `zig build test`) ---------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
// USB HID usages used in the tests (keyboard page 0x07).
|
||||
const hid_a: u8 = 0x04;
|
||||
const hid_1: u8 = 0x1e;
|
||||
const hid_3: u8 = 0x20;
|
||||
|
||||
test "us: letters obey shift and caps" {
|
||||
try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{}).character);
|
||||
try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .shift = true }).character);
|
||||
try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .caps_lock = true }).character);
|
||||
// Shift + Caps cancels for an alphabetic key.
|
||||
try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{ .shift = true, .caps_lock = true }).character);
|
||||
}
|
||||
|
||||
test "us: digits and their shifted symbols" {
|
||||
try testing.expectEqual(@as(?u21, '1'), map(us, hid_1, .{}).character);
|
||||
try testing.expectEqual(@as(?u21, '!'), map(us, hid_1, .{ .shift = true }).character);
|
||||
try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{}).character);
|
||||
try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character);
|
||||
// A digit is not alphabetic: Caps alone must not shift it.
|
||||
try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{ .caps_lock = true }).character);
|
||||
}
|
||||
|
||||
test "layouts differ: GB pound vs US hash on shift+3" {
|
||||
try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character);
|
||||
try testing.expectEqual(@as(?u21, '£'), map(gb, hid_3, .{ .shift = true }).character);
|
||||
}
|
||||
|
||||
test "french azerty places q where us has a" {
|
||||
try testing.expectEqual(@as(?u21, 'q'), map(fr, hid_a, .{}).character);
|
||||
try testing.expectEqual(@as(?u21, 'Q'), map(fr, hid_a, .{ .shift = true }).character);
|
||||
}
|
||||
|
||||
test "byName resolves and rejects" {
|
||||
try testing.expect(byName("us") == us);
|
||||
try testing.expect(byName("gb") == gb);
|
||||
try testing.expect(byName("nonsense") == null);
|
||||
}
|
||||
|
||||
test "unmapped key yields no character" {
|
||||
// HID 0x00 is not a key; every level is empty.
|
||||
try testing.expectEqual(@as(?u21, null), map(us, 0x00, .{}).character);
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
//! The **private kernel ↔ runtime** ABI: the raw system_call contract — the call
|
||||
//! numbers, `mmap` protection flags, the page size those calls work in, and the IPC
|
||||
//! name-registry ids and notification bit. Shared by the kernel dispatcher
|
||||
//! (system/kernel/process.zig) and the user-space runtime library (library/runtime/),
|
||||
//! so the two can never drift.
|
||||
//!
|
||||
//! **Application code does not speak this.** danos programs call the `runtime` library —
|
||||
//! the stable, danos-native ABI — and the runtime is the one thing that issues the
|
||||
//! actual system calls (POSIX code layers over the runtime, never on this directly). It
|
||||
//! is the same split as libSystem on macOS or win32 over the NT syscalls: the numbers
|
||||
//! here are an implementation detail the runtime hides and may renumber, not a public
|
||||
//! interface. See docs/coding-standards.md and library/runtime/.
|
||||
//!
|
||||
//! This is the *core* contract; the device half — `DeviceDescriptor` and friends, which
|
||||
//! also cross this boundary — lives with the device sub-project as [[device-abi]]
|
||||
//! (system/devices/device-abi.zig). The loader↔kernel handoff is [[boot-handoff]].
|
||||
|
||||
/// Page size every `mmap`/`munmap` grant and the boot memory map are measured in.
|
||||
/// 4 KiB on every architecture danos targets so far. Part of the ABI because the
|
||||
/// runtime aligns to it (grants are page-granular) and the kernel guarantees it.
|
||||
pub const page_size = 4096;
|
||||
|
||||
/// The kernel system_call numbers — the single source of truth shared by the kernel
|
||||
/// dispatcher (system/kernel/process.zig) and the user runtime library, so the two
|
||||
/// can never drift. The set is deliberately microkernel-minimal: file/device I/O
|
||||
/// is not here — it lives in user-space servers reached through the IPC calls.
|
||||
/// The table grows one milestone at a time; see docs/syscall.md.
|
||||
pub const SystemCall = enum(u64) {
|
||||
exit = 0, // exit(code): end the calling process
|
||||
yield = 1, // yield(): give up the rest of this quantum
|
||||
debug_write = 2, // debug_write(ptr, len): raw bytes to the kernel log (bring-up only)
|
||||
sleep = 3, // sleep(ms): block the caller for ms milliseconds
|
||||
mmap = 4, // mmap(len, prot) -> base: grant zeroed, page-aligned user pages
|
||||
munmap = 5, // munmap(base, len): release pages from a prior mmap
|
||||
create_ipc_endpoint = 6, // create_ipc_endpoint() -> handle: a new IPC endpoint
|
||||
ipc_register = 7, // ipc_register(service_id, handle): publish an endpoint by well-known id
|
||||
ipc_lookup = 8, // ipc_lookup(service_id) -> handle: find a published endpoint
|
||||
ipc_call = 9, // ipc_call(h, message, len, reply, cap) -> reply_len: send + block for reply
|
||||
ipc_reply_wait = 10, // ipc_reply_wait(h, reply, len, receive, cap) -> receive_len (+badge in rdx)
|
||||
device_enumerate = 11, // device_enumerate(buffer, maximum) -> count: snapshot the device table
|
||||
device_claim = 12, // device_claim(id) -> ok: take exclusive ownership of a device
|
||||
mmio_map = 13, // mmio_map(id, resource_index) -> vaddr: map a claimed device's MMIO into this AS
|
||||
irq_bind = 14, // irq_bind(id, resource_index, endpoint): deliver a device IRQ as an IPC notification
|
||||
irq_ack = 15, // irq_ack(id, resource_index): re-arm a bound IRQ after servicing it
|
||||
device_register = 16, // device_register(parent_id, descriptor) -> id: publish a child of a device you claimed
|
||||
system_spawn = 17, // system_spawn(name_ptr, name_len, arguments_ptr, arguments_len, exit_endpoint) -> child process id: start a named initial-ramdisk binary as a new ring-3 process
|
||||
dma_alloc = 18, // dma_alloc(len, flags) -> vaddr (rax), paddr (rdx): contiguous, pinned, uncacheable DMA memory
|
||||
dma_free = 19, // dma_free(vaddr, len) -> 0: release a prior dma_alloc
|
||||
msi_bind = 20, // msi_bind(device_id, endpoint) -> address (rax), data (rdx): a per-device MSI vector for a claimed device
|
||||
io_read = 21, // io_read(device_id, resource_index, offset, width) -> value: read a port in a claimed device's io_port resource
|
||||
io_write = 22, // io_write(device_id, resource_index, offset, width, value) -> 0: write a port in a claimed device's io_port resource
|
||||
clock = 23, // clock() -> nanoseconds since boot: a monotonic time source (for timeouts/delays)
|
||||
process_enumerate = 24, // process_enumerate(buffer, maximum) -> total: snapshot the task table
|
||||
process_kill = 25, // process_kill(id) -> 0/-errno: end a process this process spawned
|
||||
ipc_send = 26, // ipc_send(handle, message_ptr, message_len) -> 0/-errno: post a payload to an endpoint's async queue without blocking
|
||||
process_exit_reason = 27, // process_exit_reason(id) -> ExitReason/-errno: how a dead child ended (its supervisor only)
|
||||
process_subscribe = 28, // process_subscribe(endpoint) -> 0/-errno: subscribe to published exit events — every death posts a notification
|
||||
signal_bind = 29, // signal_bind(endpoint) -> 0/-errno: nominate the endpoint this process's signals arrive on
|
||||
process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself)
|
||||
timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse
|
||||
klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy the kernel RAM log buffer out to a user buffer (for persisting the boot log to disk)
|
||||
wall_clock = 33, // wall_clock() -> Unix epoch seconds (UTC): the RTC wall-clock time, for filesystem timestamps (mtime). Monotonic time is `clock`.
|
||||
_,
|
||||
};
|
||||
|
||||
/// How a process ended — recorded by the kernel at death, queried by the
|
||||
/// supervisor with `process_exit_reason`, and the input to its restart decision
|
||||
/// (docs/process-lifecycle.md): a clean exit meant to stop, a fault wants a
|
||||
/// restart with backoff, killed means the supervisor did it itself. The faults
|
||||
/// mirror the CPU exceptions a ring-3 process can die of; they are exit reasons,
|
||||
/// never delivered to the faulting process (recovery is restart, not a handler).
|
||||
pub const ExitReason = enum(u8) {
|
||||
exited = 0, // returned from main / called exit
|
||||
aborted = 1, // deliberate self-termination (reserved: no abort path yet)
|
||||
segmentation_fault = 2, // page fault
|
||||
illegal_instruction = 3, // invalid opcode
|
||||
arithmetic_fault = 4, // divide error, x87 or SIMD fault
|
||||
protection_fault = 5, // general protection fault
|
||||
fault = 6, // any other CPU exception
|
||||
killed = 7, // process_kill
|
||||
};
|
||||
|
||||
/// The x86 MSI message address base (`0xFEE0_0000`): a device raises an MSI by writing
|
||||
/// `data` to this address, which the Local APIC turns into an interrupt at the vector
|
||||
/// in `data`. The kernel returns the concrete (address, data) from `msi_bind`; this is
|
||||
/// the fixed prefix, exposed so a driver's config-space programming reads clearly.
|
||||
pub const msi_address_base: u64 = 0xFEE0_0000;
|
||||
|
||||
/// `dma_alloc` flags. `coherent` (uncacheable) is the portable default; the others are
|
||||
/// opt-in for specific hardware. `write_combining` needs PAT programming (not yet — it
|
||||
/// currently falls back to coherent); see docs/driver-model.md (M14).
|
||||
pub const dma_coherent: u64 = 1; // strong-uncacheable — the default, the only portable one
|
||||
pub const dma_write_combining: u64 = 2; // write-combining (framebuffers); needs PAT
|
||||
pub const dma_below_4g: u64 = 4; // physical address must fit 32 bits (legacy DMA engines)
|
||||
|
||||
/// Set in the badge returned by `ipc_reply_wait` when what arrived is an
|
||||
/// **asynchronous notification** (a device interrupt bound with `irq_bind`, or a
|
||||
/// child-exit notice — see `notify_exit_bit`) rather than a message from a client.
|
||||
/// There is no payload and no reply owed; the low bits carry the source. Shared so
|
||||
/// the kernel's ISR and the driver's event loop can't disagree about which bit
|
||||
/// means "the hardware spoke".
|
||||
pub const notify_badge_bit: u64 = 1 << 63;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **child-exit notification**:
|
||||
/// posted to the endpoint a supervisor passed to `system_spawn` when that child ends
|
||||
/// — by clean exit, by a fault, or by `process_kill`. The low bits carry the child's
|
||||
/// process id, so one endpoint can supervise many children (and even share with IRQ
|
||||
/// notifications, which never set this bit). The microkernel's SIGCHLD.
|
||||
pub const notify_exit_bit: u64 = 1 << 62;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **buffered message** — a payload
|
||||
/// posted to an endpoint's async queue by `ipc_send`, delivered through `ipc_reply_wait`
|
||||
/// like a notification (no reply owed) but carrying bytes in the receive buffer, not just
|
||||
/// a badge. This is what distinguishes a payload-bearing async message from a bare IRQ /
|
||||
/// child-exit notification (which sets neither this nor `notify_exit_bit`). The low bits
|
||||
/// carry the sender's task id. The async counterpart of the synchronous `ipc_call`, for
|
||||
/// broadcasts where a rendezvous is the wrong shape (the input service is the first user).
|
||||
pub const notify_message_bit: u64 = 1 << 61;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **signal notification** —
|
||||
/// the process-lifecycle vocabulary of docs/process-lifecycle.md, delivered to the
|
||||
/// endpoint the process nominated with `signal_bind`. The low bits carry the
|
||||
/// coalesced pending mask (bit positions = `Signal` values): signals are
|
||||
/// statements, not questions, and two pending terminates are one terminate.
|
||||
pub const notify_signal_bit: u64 = 1 << 60;
|
||||
|
||||
/// Set (alongside `notify_badge_bit`) in the badge of a **timer notification** —
|
||||
/// a one-shot `timer_bind` deadline landing. No payload bits: what to do when the
|
||||
/// deadline fires is whatever the receiver armed it for (a stop-sequence
|
||||
/// escalation, a restart backoff, an alarm).
|
||||
pub const notify_timer_bit: u64 = 1 << 59;
|
||||
|
||||
/// The signal vocabulary (docs/process-lifecycle.md): POSIX's concepts, danos's
|
||||
/// names, message delivery. The value is the bit position in the pending mask — a
|
||||
/// private kernel/runtime detail, free to change while they ship together. Kill
|
||||
/// is not here (it is `process_kill`, unhandleable by definition); faults are not
|
||||
/// here (they are `ExitReason`s — recovery is restart, not a handler); liveness is
|
||||
/// not here (a question, asked as the zero-length ping call, not a statement).
|
||||
pub const Signal = enum(u5) {
|
||||
terminate = 0, // finish up and exit (the polite half of the stop sequence)
|
||||
reload = 1, // re-read configuration / re-scan
|
||||
interrupt = 2, // interactive interrupt (no sender until a console exists)
|
||||
quit = 3, // as interrupt, by convention more final
|
||||
alarm = 4, // a timer the process armed for itself (unbuilt: no consumer yet)
|
||||
user_1 = 5, // service-defined
|
||||
user_2 = 6, // service-defined
|
||||
};
|
||||
|
||||
/// Capacity of `ProcessDescriptor.name` — matches the longest name `system_spawn`
|
||||
/// accepts, so a process's recorded name (its argv[0]) is never truncated.
|
||||
pub const maximum_process_name = 64;
|
||||
|
||||
/// What a process is doing right now, as reported by `process_enumerate`. Crosses
|
||||
/// the system_call boundary as `ProcessDescriptor.state`.
|
||||
pub const ProcessState = enum(u32) {
|
||||
ready = 0, // runnable, waiting for a core
|
||||
running = 1, // executing on a core right now
|
||||
blocked = 2, // waiting (sleeping, or blocked in IPC)
|
||||
};
|
||||
|
||||
/// One `process_enumerate` entry — the kernel's view of a live task, kernel tasks
|
||||
/// included (they carry an empty name and id 0 is the boot task). Fixed layout
|
||||
/// (extern) because it crosses the kernel↔user boundary by memory copy, like
|
||||
/// `DeviceDescriptor` in the device ABI.
|
||||
pub const ProcessDescriptor = extern struct {
|
||||
id: u32, // kernel-assigned process id; never reused (monotonic)
|
||||
supervisor: u32, // id of the process that spawned it (0 = the kernel)
|
||||
state: u32, // a ProcessState value
|
||||
priority: u32,
|
||||
name_length: u32,
|
||||
name: [maximum_process_name]u8, // argv[0] at spawn; empty for kernel tasks
|
||||
};
|
||||
|
||||
/// Well-known IPC service ids for the bootstrap name registry (create_ipc_endpoint +
|
||||
/// ipc_register/ipc_lookup). Small integers, so no string interning is needed
|
||||
/// during bring-up. The VFS server registers under `vfs`; clients look it up.
|
||||
pub const ServiceId = enum(u32) {
|
||||
vfs = 1,
|
||||
input = 2,
|
||||
ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes
|
||||
device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md)
|
||||
power = 5, // system power: events (button, lid, battery) + shutdown (docs/power.md; domain-named per docs/discovery.md — the acpi service registers it on x86, a PSCI service will on ARM)
|
||||
usb_bus = 6, // the xHCI host-controller driver's transfer endpoint; USB class drivers look it up and `callCap`-open their device to get a private per-device transfer channel (docs/driver-model.md)
|
||||
block = 7, // a block-device driver (USB mass storage today): read/write of fixed-size blocks, the storage a filesystem sits on
|
||||
fat = 8, // the FAT filesystem server; the VFS mounts it and forwards paths under its mount point (/mnt/usb) to it
|
||||
_,
|
||||
};
|
||||
|
||||
/// Protection flags for `mmap` (matching the usual C bit values).
|
||||
pub const prot_read: u64 = 1;
|
||||
pub const prot_write: u64 = 2;
|
||||
pub const prot_exec: u64 = 4;
|
||||
|
||||
/// `send_cap` / `received_cap` sentinel meaning "no capability" on the `ipc_call` /
|
||||
/// `ipc_reply_wait` cap-passing path (M13). `~0`, like `no_parent` — a real handle is
|
||||
/// a small index, so it can never collide.
|
||||
pub const no_cap: u64 = ~@as(u64, 0);
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Shared definitions that form the contract between a bootloader
|
||||
//! (boot/, e.g. efi.zig built as BOOTX64.efi) and the kernel (system/kernel/main.zig).
|
||||
//! The **loader ↔ kernel** contract: everything a bootloader (boot/, e.g. efi.zig
|
||||
//! built as BOOTX64.efi) and the kernel (system/kernel/kernel.zig) must agree on to
|
||||
//! hand control over — the handoff structures the loader fills in, plus the kernel's
|
||||
//! virtual-memory layout and the physical↔virtual addressing both sides use.
|
||||
//!
|
||||
//! Both binaries import this as the "danos" module, so the handoff layout is
|
||||
//! defined in exactly one place.
|
||||
//! Both binaries import this as the `boot-handoff` module, so the layout is defined
|
||||
//! in exactly one place. **User space never sees this** — the kernel↔user contract is
|
||||
//! [[abi]] (system/abi.zig); device types are [[device-abi]] (system/devices/device-abi.zig).
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
@@ -41,10 +44,6 @@ pub const Framebuffer = extern struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// Page size the memory map is measured in. 4 KiB on every architecture danos
|
||||
/// targets so far.
|
||||
pub const page_size = 4096;
|
||||
|
||||
/// The kernel's virtual-memory layout (higher-half). The kernel is linked at
|
||||
/// `kernel_virt_base` but loaded at a low physical address; all of RAM (and the
|
||||
/// device MMIO windows) is also mapped at `physmap_base + physical`, so the kernel
|
||||
@@ -58,105 +57,6 @@ pub const page_size = 4096;
|
||||
pub const physmap_base: u64 = 0xFFFF_8800_0000_0000;
|
||||
pub const kernel_virt_base: u64 = 0xFFFF_FFFF_8000_0000;
|
||||
|
||||
/// The kernel system_call numbers — the single source of truth shared by the kernel
|
||||
/// dispatcher (system/kernel/process.zig) and the user runtime library, so the two
|
||||
/// can never drift. The set is deliberately microkernel-minimal: file/device I/O
|
||||
/// is not here — it lives in user-space servers reached through the IPC calls.
|
||||
/// The table grows one milestone at a time; see docs/syscall.md.
|
||||
pub const SystemCall = enum(u64) {
|
||||
exit = 0, // exit(code): end the calling process
|
||||
yield = 1, // yield(): give up the rest of this quantum
|
||||
debug_write = 2, // debug_write(ptr, len): raw bytes to the kernel log (bring-up only)
|
||||
sleep = 3, // sleep(ms): block the caller for ms milliseconds
|
||||
mmap = 4, // mmap(len, prot) -> base: grant zeroed, page-aligned user pages
|
||||
munmap = 5, // munmap(base, len): release pages from a prior mmap
|
||||
create_endpoint = 6, // create_endpoint() -> handle: a new IPC endpoint
|
||||
ipc_register = 7, // ipc_register(service_id, handle): publish an endpoint by well-known id
|
||||
ipc_lookup = 8, // ipc_lookup(service_id) -> handle: find a published endpoint
|
||||
ipc_call = 9, // ipc_call(h, message, len, reply, cap) -> reply_len: send + block for reply
|
||||
ipc_reply_wait = 10, // ipc_reply_wait(h, reply, len, receive, cap) -> receive_len (+badge in rdx)
|
||||
device_enumerate = 11, // device_enumerate(buffer, maximum) -> count: snapshot the device table
|
||||
device_claim = 12, // device_claim(id) -> ok: take exclusive ownership of a device
|
||||
mmio_map = 13, // mmio_map(id, resource_index) -> vaddr: map a claimed device's MMIO into this AS
|
||||
irq_bind = 14, // irq_bind(id, resource_index, endpoint): deliver a device IRQ as an IPC notification
|
||||
irq_ack = 15, // irq_ack(id, resource_index): re-arm a bound IRQ after servicing it
|
||||
device_register = 16, // device_register(parent_id, descriptor) -> id: publish a child of a device you claimed
|
||||
_,
|
||||
};
|
||||
|
||||
/// Set in the badge returned by `ipc_reply_wait` when what arrived is an
|
||||
/// **asynchronous notification** (today: a device interrupt bound with `irq_bind`)
|
||||
/// rather than a message from a client. There is no payload and no reply owed; the
|
||||
/// low bits carry the source, a GSI. Shared so the kernel's ISR and the driver's
|
||||
/// event loop can't disagree about which bit means "the hardware spoke".
|
||||
pub const notify_badge_bit: u64 = 1 << 63;
|
||||
|
||||
/// A device class, mirroring system/devices/device-model.zig's `DeviceClass` **in order**
|
||||
/// (its `@intFromEnum` values cross the system_call boundary in `DeviceDescriptor.class`).
|
||||
/// Keep the two in sync.
|
||||
pub const DeviceClass = enum(u32) {
|
||||
root,
|
||||
processor,
|
||||
interrupt_controller,
|
||||
timer,
|
||||
pci_host_bridge,
|
||||
pci_device,
|
||||
acpi_device,
|
||||
unknown,
|
||||
};
|
||||
|
||||
/// A resource kind, mirroring system/devices/device-model.zig's `ResourceKind` in order.
|
||||
pub const ResourceKind = enum(u32) {
|
||||
memory,
|
||||
io_port,
|
||||
irq,
|
||||
bus_range,
|
||||
};
|
||||
|
||||
/// One device resource, as handed to a user-space driver (flat, extern).
|
||||
pub const ResourceDescriptor = extern struct {
|
||||
kind: u64, // a ResourceKind value
|
||||
start: u64,
|
||||
len: u64,
|
||||
};
|
||||
|
||||
pub const maximum_device_resources = 8;
|
||||
|
||||
/// `DeviceDescriptor.parent` for a device with no parent — a root of the device tree.
|
||||
pub const no_parent: u64 = ~@as(u64, 0);
|
||||
|
||||
/// A device, as snapshotted for user space by `device_enumerate`. A driver scans
|
||||
/// these to find the hardware it owns, claims it, and maps its MMIO.
|
||||
///
|
||||
/// `parent` makes the table a tree rather than a list, which is what a **bus driver**
|
||||
/// needs: it claims the bus, finds the devices below it, and publishes any it
|
||||
/// discovers itself with `device_register`. A registered child's resources must lie
|
||||
/// within its parent's (the kernel enforces this) — that containment is what makes
|
||||
/// delegation safe, since a device descriptor is otherwise a licence to map physical
|
||||
/// memory.
|
||||
pub const DeviceDescriptor = extern struct {
|
||||
id: u64,
|
||||
parent: u64, // a device id, or `no_parent`
|
||||
class: u64, // a DeviceClass value
|
||||
hid_len: u64,
|
||||
resource_count: u64,
|
||||
hid: [8]u8,
|
||||
resources: [maximum_device_resources]ResourceDescriptor,
|
||||
};
|
||||
|
||||
/// Well-known IPC service ids for the bootstrap name registry (create_endpoint +
|
||||
/// ipc_register/ipc_lookup). Small integers, so no string interning is needed
|
||||
/// during bring-up. The VFS server registers under `vfs`; clients look it up.
|
||||
pub const ServiceId = enum(u32) {
|
||||
vfs = 1,
|
||||
_,
|
||||
};
|
||||
|
||||
/// Protection flags for `mmap` (matching the usual C bit values).
|
||||
pub const prot_read: u64 = 1;
|
||||
pub const prot_write: u64 = 2;
|
||||
pub const prot_exec: u64 = 4;
|
||||
|
||||
/// Physical address -> its virtual address in the physmap. The single way the
|
||||
/// kernel dereferences a physical address once paging is up.
|
||||
///
|
||||
@@ -204,7 +104,7 @@ pub const MemoryKind = enum(u32) {
|
||||
/// firmware's variable descriptor-stride to worry about.
|
||||
pub const MemoryRegion = extern struct {
|
||||
base: u64, // physical start address
|
||||
pages: u64, // length in `page_size` units
|
||||
pages: u64, // length in 4 KiB pages (the [[abi]] `page_size` unit)
|
||||
kind: MemoryKind,
|
||||
_pad: u32 = 0,
|
||||
};
|
||||
@@ -242,15 +142,15 @@ pub const BootInformation = extern struct {
|
||||
/// A device-tree boot path leaves this 0 and (later) fills a `device_tree_blob`
|
||||
/// field instead, so the kernel discovers devices without knowing what booted it.
|
||||
acpi_rsdp: u64 = 0,
|
||||
/// The raw `/sbin/init` ELF image, read off the boot volume by the loader
|
||||
/// The raw `/system/services/init` ELF image, read off the boot volume by the loader
|
||||
/// into memory that survives the handoff (classified reserved, so the kernel
|
||||
/// identity-maps it and never allocates over it). 0/0 = no init found — the
|
||||
/// kernel boots without user space. Grows into a full initrd handoff later.
|
||||
/// kernel boots without user space. Grows into a full initial_ramdisk handoff later.
|
||||
init_base: u64 = 0,
|
||||
init_len: u64 = 0,
|
||||
/// The initrd image (a bundle of extra user binaries — the VFS server and
|
||||
/// The initial_ramdisk image (a bundle of extra user binaries — the VFS server and
|
||||
/// device drivers), read off the boot volume into memory that survives the
|
||||
/// handoff, same as `init` above. 0/0 = no initrd. See system/initrd.zig.
|
||||
initrd_base: u64 = 0,
|
||||
initrd_len: u64 = 0,
|
||||
/// handoff, same as `init` above. 0/0 = no initial_ramdisk. See system/initial-ramdisk.zig.
|
||||
initial_ramdisk_base: u64 = 0,
|
||||
initial_ramdisk_len: u64 = 0,
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
//! ACPI / PnP hardware-ID (`_HID`) names: the flat analog of pci-class.zig for
|
||||
//! `acpi_device` nodes. Unlike PCI, ACPI has no class/subclass/prog-IF taxonomy — a
|
||||
//! device's identity *is* its `_HID` string (`PNP0303` simply means "PS/2 keyboard"),
|
||||
//! so this is a plain id <-> name registry rather than a hierarchical decoder.
|
||||
//! The well-known PnP/ACPI IDs; vendor-specific ids (e.g. `QEMU0002`, `INTC1234`) have
|
||||
//! no standard name and decode to nothing. Pure reference data, so it is shared by
|
||||
//! kernel discovery (the device-tree dump) and any user-space driver or tool.
|
||||
//!
|
||||
//! Code that means a specific device names the `HardwareId` variant instead of its
|
||||
//! `_HID` string — `HardwareId.ps2_keyboard.hid()` reads without a registry lookup,
|
||||
//! where a bare `"PNP0303"` does not.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// The common standard PnP/ACPI hardware IDs, as named values. Prefix ranges hint at
|
||||
/// the grouping (PNP03xx keyboards, PNP0Fxx pointing devices, PNP0Cxx ACPI
|
||||
/// power/thermal, PNP0Axx buses), but there is no formal hierarchy — hence a flat
|
||||
/// enum over a flat registry.
|
||||
pub const HardwareId = enum {
|
||||
programmable_interrupt_controller,
|
||||
system_timer,
|
||||
high_precision_event_timer,
|
||||
dma_controller,
|
||||
ps2_keyboard,
|
||||
parallel_port,
|
||||
ecp_parallel_port,
|
||||
serial_port,
|
||||
floppy_disk_controller,
|
||||
system_speaker,
|
||||
pci_bus,
|
||||
generic_container,
|
||||
/// The second id the ACPI spec assigns the same "Generic Container Device" name.
|
||||
generic_container_extended,
|
||||
pci_express_root_bridge,
|
||||
real_time_clock,
|
||||
system_board,
|
||||
motherboard_reserved_resources,
|
||||
math_coprocessor,
|
||||
acpi_system_board,
|
||||
embedded_controller,
|
||||
control_method_battery,
|
||||
fan,
|
||||
power_button,
|
||||
lid,
|
||||
sleep_button,
|
||||
pci_interrupt_link,
|
||||
microsoft_ps2_mouse,
|
||||
ps2_mouse,
|
||||
ac_adapter,
|
||||
processor_device,
|
||||
processor_aggregator,
|
||||
processor_container,
|
||||
|
||||
const Entry = struct { hid: []const u8, name: []const u8 };
|
||||
|
||||
/// The registry row for this id: its `_HID` string and human-readable name.
|
||||
fn entry(self: HardwareId) Entry {
|
||||
return switch (self) {
|
||||
.programmable_interrupt_controller => .{ .hid = "PNP0000", .name = "Programmable Interrupt Controller (PIC)" },
|
||||
.system_timer => .{ .hid = "PNP0100", .name = "System Timer (PIT)" },
|
||||
.high_precision_event_timer => .{ .hid = "PNP0103", .name = "High Precision Event Timer (HPET)" },
|
||||
.dma_controller => .{ .hid = "PNP0200", .name = "DMA Controller" },
|
||||
.ps2_keyboard => .{ .hid = "PNP0303", .name = "PS/2 Keyboard" },
|
||||
.parallel_port => .{ .hid = "PNP0400", .name = "Standard LPT Parallel Port" },
|
||||
.ecp_parallel_port => .{ .hid = "PNP0401", .name = "ECP Parallel Port" },
|
||||
.serial_port => .{ .hid = "PNP0501", .name = "16550A-compatible Serial Port" },
|
||||
.floppy_disk_controller => .{ .hid = "PNP0700", .name = "PC Floppy Disk Controller" },
|
||||
.system_speaker => .{ .hid = "PNP0800", .name = "System Speaker" },
|
||||
.pci_bus => .{ .hid = "PNP0A03", .name = "PCI Bus" },
|
||||
.generic_container => .{ .hid = "PNP0A05", .name = "Generic Container Device" },
|
||||
.generic_container_extended => .{ .hid = "PNP0A06", .name = "Generic Container Device" },
|
||||
.pci_express_root_bridge => .{ .hid = "PNP0A08", .name = "PCI Express Root Bridge" },
|
||||
.real_time_clock => .{ .hid = "PNP0B00", .name = "Real-Time Clock (RTC)" },
|
||||
.system_board => .{ .hid = "PNP0C01", .name = "System Board" },
|
||||
.motherboard_reserved_resources => .{ .hid = "PNP0C02", .name = "Motherboard Reserved Resources" },
|
||||
.math_coprocessor => .{ .hid = "PNP0C04", .name = "Math Coprocessor" },
|
||||
.acpi_system_board => .{ .hid = "PNP0C08", .name = "ACPI System Board" },
|
||||
.embedded_controller => .{ .hid = "PNP0C09", .name = "ACPI Embedded Controller" },
|
||||
.control_method_battery => .{ .hid = "PNP0C0A", .name = "ACPI Control Method Battery" },
|
||||
.fan => .{ .hid = "PNP0C0B", .name = "ACPI Fan" },
|
||||
.power_button => .{ .hid = "PNP0C0C", .name = "ACPI Power Button" },
|
||||
.lid => .{ .hid = "PNP0C0D", .name = "ACPI Lid" },
|
||||
.sleep_button => .{ .hid = "PNP0C0E", .name = "ACPI Sleep Button" },
|
||||
.pci_interrupt_link => .{ .hid = "PNP0C0F", .name = "PCI Interrupt Link Device" },
|
||||
.microsoft_ps2_mouse => .{ .hid = "PNP0F03", .name = "Microsoft PS/2 Mouse" },
|
||||
.ps2_mouse => .{ .hid = "PNP0F13", .name = "PS/2 Mouse" },
|
||||
.ac_adapter => .{ .hid = "ACPI0003", .name = "AC Adapter" },
|
||||
.processor_device => .{ .hid = "ACPI0007", .name = "Processor Device" },
|
||||
.processor_aggregator => .{ .hid = "ACPI000C", .name = "Processor Aggregator" },
|
||||
.processor_container => .{ .hid = "ACPI0010", .name = "Processor Container" },
|
||||
};
|
||||
}
|
||||
|
||||
/// This id's `_HID` string (e.g. `.ps2_keyboard` -> "PNP0303").
|
||||
pub fn hid(self: HardwareId) []const u8 {
|
||||
return self.entry().hid;
|
||||
}
|
||||
|
||||
/// This id's human-readable name (e.g. `.ps2_keyboard` -> "PS/2 Keyboard").
|
||||
pub fn description(self: HardwareId) []const u8 {
|
||||
return self.entry().name;
|
||||
}
|
||||
|
||||
/// The named value for a `_HID` string, or null if it is not a known standard
|
||||
/// id (vendor-specific ids are not in the registry).
|
||||
pub fn fromHid(hid_string: []const u8) ?HardwareId {
|
||||
for (std.enums.values(HardwareId)) |id| {
|
||||
if (std.mem.eql(u8, id.hid(), hid_string)) return id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// The human-readable name for a `_HID` string, or "" if it is not a known standard
|
||||
/// id (vendor-specific ids have no registry name — callers just print the raw HID).
|
||||
pub fn description(hid: []const u8) []const u8 {
|
||||
return (HardwareId.fromHid(hid) orelse return "").description();
|
||||
}
|
||||
|
||||
test "decodes standard PnP/ACPI ids and leaves the rest alone" {
|
||||
const eq = std.testing.expectEqualStrings;
|
||||
try eq("PS/2 Keyboard", description("PNP0303"));
|
||||
try eq("PS/2 Mouse", description("PNP0F13"));
|
||||
try eq("PCI Express Root Bridge", description("PNP0A08"));
|
||||
try eq("Real-Time Clock (RTC)", description("PNP0B00"));
|
||||
try eq("", description("QEMU0002")); // vendor-specific: no standard name
|
||||
try eq("", description("")); // no HID at all
|
||||
}
|
||||
|
||||
test "named values round-trip through their _HID strings" {
|
||||
const testing = std.testing;
|
||||
try testing.expectEqualStrings("PNP0303", HardwareId.ps2_keyboard.hid());
|
||||
try testing.expectEqual(@as(?HardwareId, .ps2_mouse), HardwareId.fromHid("PNP0F13"));
|
||||
try testing.expectEqual(@as(?HardwareId, null), HardwareId.fromHid("QEMU0002"));
|
||||
for (std.enums.values(HardwareId)) |id| {
|
||||
try testing.expectEqual(@as(?HardwareId, id), HardwareId.fromHid(id.hid()));
|
||||
}
|
||||
}
|
||||
+202
-495
@@ -15,7 +15,8 @@
|
||||
//! the `Hal.mapMmio` callback the caller supplies (the architecture VMM's map primitive).
|
||||
|
||||
const std = @import("std");
|
||||
const danos = @import("danos");
|
||||
const boot_handoff = @import("boot-handoff");
|
||||
const abi = @import("abi");
|
||||
const parameters = @import("parameters");
|
||||
const device_model = @import("device-model.zig");
|
||||
const aml = @import("aml/aml.zig");
|
||||
@@ -39,6 +40,9 @@ pub const RegisterAccess = struct {
|
||||
/// Everything the power subsystem needs, extracted from the FADT and the AML
|
||||
/// sleep packages during discovery. Populated by `discover`, read by `power`.
|
||||
pub const PowerInformation = struct {
|
||||
/// The System Control Interrupt's GSI (FADT SCI_INT) — the line ACPI events
|
||||
/// (power button, GPEs) arrive on. Published to the acpi service for M21.
|
||||
sci_interrupt: u16 = 0,
|
||||
/// The SMM command port and the value that switches the platform into ACPI mode.
|
||||
smi_cmd: u16 = 0,
|
||||
acpi_enable: u8 = 0,
|
||||
@@ -50,7 +54,8 @@ pub const PowerInformation = struct {
|
||||
reset: RegisterAccess = .{},
|
||||
reset_value: u8 = 0,
|
||||
reset_supported: bool = false,
|
||||
/// SLP_TYP values for S5 (soft off) and S3 (suspend), from the AML sleep-state (`_Sx`) packages.
|
||||
/// SLP_TYP values for S5 (soft off) and S3 (suspend), from the AML sleep-state (`_Sx`)
|
||||
/// packages.
|
||||
s5: ?aml.SleepType = null,
|
||||
s3: ?aml.SleepType = null,
|
||||
};
|
||||
@@ -88,6 +93,20 @@ pub const PlatformInformation = struct {
|
||||
/// ISA-IRQ-to-GSI remappings from the MADT (for future IOAPIC routing).
|
||||
overrides: [16]IsoEntry = undefined,
|
||||
override_count: usize = 0,
|
||||
/// Whether an IOMMU (VT-d DMA-remapping unit) was found in the ACPI DMAR table.
|
||||
/// When false, `device_claim` on a DMA-capable device is equivalent to granting
|
||||
/// ring 0 — a device can DMA to any physical address (docs/driver-model.md M16).
|
||||
/// Detection is the first step; per-device domain enforcement lands with the first
|
||||
/// DMA driver.
|
||||
iommu_present: bool = false,
|
||||
/// MMIO base of the first DMA-remapping hardware unit (DMAR DRHD), when present.
|
||||
iommu_base: u64 = 0,
|
||||
/// The unit's Version register (offset 0x00) — its low byte is major.minor;
|
||||
/// reading it back nonzero confirms a real, mappable VT-d unit.
|
||||
iommu_version: u32 = 0,
|
||||
/// The unit's Capability register (offset 0x08): supported address widths, number
|
||||
/// of domains, etc. Recorded now; consumed when enforcement is built.
|
||||
iommu_capabilities: u64 = 0,
|
||||
};
|
||||
|
||||
/// Filled in by `discover`; the architecture layer reads it during bring-up.
|
||||
@@ -138,6 +157,13 @@ pub var namespace: ?aml.Namespace = null;
|
||||
/// Physical address of the DSDT the FADT points at, or 0.
|
||||
pub var dsdt_physical: u64 = 0;
|
||||
|
||||
/// The FADT itself (physical + length), published on the acpi-tables node so
|
||||
/// the ring-3 acpi service can read the PM1 event and GPE blocks it needs for
|
||||
/// the event side (docs/acpi.md — ACPI events). Distinguished from the AML
|
||||
/// blob resources by its intact "FACP" header — the blobs are header-stripped.
|
||||
var fadt_physical: u64 = 0;
|
||||
var fadt_length: u64 = 0;
|
||||
|
||||
// AML blocks (DSDT + any SSDTs) collected during the table walk, as physical
|
||||
// address + length of each table's post-header bytecode. Scanned after the walk
|
||||
// for the sleep-state (`_Sx`) packages.
|
||||
@@ -147,7 +173,7 @@ var aml_block_count: usize = 0;
|
||||
|
||||
fn addAmlBlock(sdt_physical: u64) void {
|
||||
if (aml_block_count >= aml_block_physical.len or sdt_physical == 0) return;
|
||||
const h: *const SystemDescriptorTableHeader = @ptrFromInt(danos.physicalToVirtual(sdt_physical));
|
||||
const h: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(sdt_physical));
|
||||
if (h.length <= @sizeOf(SystemDescriptorTableHeader)) return;
|
||||
aml_block_physical[aml_block_count] = sdt_physical + @sizeOf(SystemDescriptorTableHeader);
|
||||
aml_block_len[aml_block_count] = h.length - @sizeOf(SystemDescriptorTableHeader);
|
||||
@@ -182,7 +208,8 @@ const ExtendedSystemDescriptorPointer = extern struct {
|
||||
root_system_description_table_address: u32 align(1),
|
||||
/// The size of the RSDP.
|
||||
length: u32 align(1),
|
||||
/// A 64-bit physical address pointing to the XSDT. If the revision is at least 2, the XSDT should be used regardless of architecture, as the RSDT was deprecated.
|
||||
/// A 64-bit physical address pointing to the XSDT. If the revision is at least 2, the XSDT
|
||||
/// should be used regardless of architecture, as the RSDT was deprecated.
|
||||
extended_system_descriptor_table_address: u64 align(1),
|
||||
/// A checksum used for the entire table.
|
||||
extended_checksum: u8,
|
||||
@@ -232,6 +259,7 @@ const SLIT: [4]u8 = "SLIT".*;
|
||||
/// System Resource Affinity Table (SRAT)
|
||||
const SRAT: [4]u8 = "SRAT".*;
|
||||
/// Secondary System Description Table (SSDT)
|
||||
const DMAR: [4]u8 = "DMAR".*;
|
||||
const SSDT: [4]u8 = "SSDT".*;
|
||||
/// Serial Port Console Redirection table (SPCR) — the firmware's console UART.
|
||||
const SPCR: [4]u8 = "SPCR".*;
|
||||
@@ -341,49 +369,33 @@ const Hpet = extern struct {
|
||||
page_protection: u8,
|
||||
};
|
||||
|
||||
// --- PCI configuration-space header (first 64 bytes, common fields) ---------
|
||||
|
||||
const PciHeader = extern struct {
|
||||
vendor_id: u16 align(1),
|
||||
device_id: u16 align(1),
|
||||
command: u16 align(1),
|
||||
status: u16 align(1),
|
||||
revision_id: u8,
|
||||
prog_if: u8,
|
||||
subclass: u8,
|
||||
class_code: u8,
|
||||
cache_line_size: u8,
|
||||
latency_timer: u8,
|
||||
/// bit 7 set => multi-function device.
|
||||
header_type: u8,
|
||||
bist: u8,
|
||||
// 0x10 onward (BARs, etc.) depends on header_type; read separately.
|
||||
};
|
||||
|
||||
// --- Entry point ------------------------------------------------------------
|
||||
|
||||
/// Discover hardware from the ACPI tables rooted at `rsdp_physical` and populate
|
||||
/// `device_tree`. `hal` provides MMIO mapping (for PCIe ECAM) and port I/O. Also parses the
|
||||
/// FADT and the AML sleep-state (`_Sx`) packages into `power_information` for the power service.
|
||||
pub fn discover(rsdp_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
|
||||
pub fn discover(rsdp_physical: u64, memory_regions: []const boot_handoff.MemoryRegion, device_tree: *DeviceTree, hal: Hal) !void {
|
||||
if (rsdp_physical == 0) return error.NoRsdp;
|
||||
boot_memory_regions = memory_regions;
|
||||
|
||||
// Start clean so a re-run doesn't accumulate stale state.
|
||||
power_information = .{};
|
||||
fadt_physical = 0;
|
||||
fadt_length = 0;
|
||||
platform_information = .{};
|
||||
aml_stats = .{};
|
||||
namespace = null;
|
||||
dsdt_physical = 0;
|
||||
aml_block_count = 0;
|
||||
|
||||
const rsdp: *const RootSystemDescriptionPointer = @ptrFromInt(danos.physicalToVirtual(rsdp_physical));
|
||||
const rsdp: *const RootSystemDescriptionPointer = @ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical));
|
||||
if (!std.mem.eql(u8, &rsdp.signature, "RSD PTR ")) return error.BadRsdpSignature;
|
||||
// Revision 0 checksums only the first 20 bytes (the v1.0 RSDP).
|
||||
if (!checksumOk(@ptrFromInt(danos.physicalToVirtual(rsdp_physical)), 20)) return error.BadRsdpChecksum;
|
||||
if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical)), 20)) return error.BadRsdpChecksum;
|
||||
|
||||
if (rsdp.revision >= 2) {
|
||||
const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(danos.physicalToVirtual(rsdp_physical));
|
||||
if (!checksumOk(@ptrFromInt(danos.physicalToVirtual(rsdp_physical)), xsdp.length)) return error.BadXsdpChecksum;
|
||||
const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical));
|
||||
if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical)), xsdp.length)) return error.BadXsdpChecksum;
|
||||
try walkRoot(u64, xsdp.extended_system_descriptor_table_address, device_tree, hal);
|
||||
} else {
|
||||
try walkRoot(u32, rsdp.root_system_description_table_address, device_tree, hal);
|
||||
@@ -393,7 +405,7 @@ pub fn discover(rsdp_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
|
||||
// read the sleep types from it.
|
||||
var blocks: [aml_block_physical.len][]const u8 = undefined;
|
||||
for (0..aml_block_count) |i| {
|
||||
blocks[i] = @as([*]const u8, @ptrFromInt(danos.physicalToVirtual(aml_block_physical[i])))[0..aml_block_len[i]];
|
||||
blocks[i] = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(aml_block_physical[i])))[0..aml_block_len[i]];
|
||||
}
|
||||
const active = blocks[0..aml_block_count];
|
||||
if (aml.parse(device_tree.allocator, active)) |pr| {
|
||||
@@ -401,22 +413,68 @@ pub fn discover(rsdp_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
|
||||
aml_stats = .{ .nodes = namespace.?.nodeCount(), .consumed = pr.consumed, .total = pr.total };
|
||||
power_information.s5 = aml.sleepState(&namespace.?, 5);
|
||||
power_information.s3 = aml.sleepState(&namespace.?, 3);
|
||||
// Fold the namespace's Device objects into the generic tree.
|
||||
wireAcpiDevices(device_tree, &namespace.?, hal) catch {};
|
||||
// The namespace's Device objects are no longer folded into the kernel
|
||||
// tree (M20.3): the ring-3 acpi service claims the acpi-tables node
|
||||
// (published below), re-parses the same blobs, and registers + reports
|
||||
// the _HID devices itself. The kernel keeps the namespace only for the
|
||||
// \_S5 sleep type above.
|
||||
} else |_| {
|
||||
// AML parse failed (e.g. out of memory); power stays best-effort with
|
||||
// whatever the FADT alone provided.
|
||||
}
|
||||
|
||||
// Publish the acpi-tables node (docs/discovery.md): the AML blobs as
|
||||
// memory resources for the acpi service to map and parse in ring 3, a broad
|
||||
// io_port grant for the OperationRegion access its interpreter needs, and
|
||||
// the SCI for the events track (M21). Exactly one node, one trusted
|
||||
// claimant. Kept even when the kernel-side device building (above) retires
|
||||
// in M20.3 — the kernel still owns the *static* tables and \_S5.
|
||||
publishAcpiTablesNode(device_tree) catch {};
|
||||
}
|
||||
|
||||
/// Build the acpi-tables node (see the call site in discover). Best-effort: a
|
||||
/// failure here leaves the kernel-seeded tree working, only the ring-3 service
|
||||
/// finds nothing to claim.
|
||||
fn publishAcpiTablesNode(device_tree: *DeviceTree) !void {
|
||||
const node = try device_tree.addChild(device_tree.root, .acpi_tables, "acpi-tables");
|
||||
// One memory resource per AML block — page-aligned base down, length padded
|
||||
// up to cover the bytecode, so mmio_map hands the service a pointer into it.
|
||||
var i: usize = 0;
|
||||
while (i < aml_block_count and i < device_model.maximum_resources - 2) : (i += 1) {
|
||||
// mmio_map preserves the sub-page offset, so the service maps this and
|
||||
// gets a pointer straight to the bytecode.
|
||||
_ = node.addResource(.memory, aml_block_physical[i], aml_block_len[i]);
|
||||
}
|
||||
// The broad I/O grant: OperationRegions name whatever ports the firmware
|
||||
// chose (EC, PM1, GPE, SMBus); which ports cannot be known before the AML
|
||||
// that names them is parsed, so the grant is the whole space — the honest
|
||||
// trust boundary of docs/discovery.md (the acpi service's one trusted node).
|
||||
_ = node.addResource(.io_port, 0, 1 << 16);
|
||||
// A broad interrupt window: ACPI _CRS names legacy ISA IRQs (the PS/2 lines
|
||||
// 1 and 12, the RTC, …), and the service registers those devices under this
|
||||
// node, so it must own a superset. The range [0, 256) covers every GSI; the
|
||||
// SCI (recorded first, len 1) stays distinct so M21 can pick it out.
|
||||
if (power_information.sci_interrupt != 0) _ = node.addResource(.irq, power_information.sci_interrupt, 1);
|
||||
_ = node.addResource(.irq, 0, 256);
|
||||
// The FADT rides along (M21): the service reads the PM1 event / GPE blocks
|
||||
// from its own copy, telling it apart from the AML blobs by signature.
|
||||
if (fadt_physical != 0) _ = node.addResource(.memory, fadt_physical, fadt_length);
|
||||
}
|
||||
|
||||
/// The number of Device objects in the namespace built during discovery, or 0.
|
||||
pub fn amlDeviceCount() usize {
|
||||
if (namespace) |*ns| return aml.deviceCount(ns);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Walk the RSDT (Entry = u32) or XSDT (Entry = u64): validate it, then dispatch
|
||||
/// each SDT it points at. A bad individual table is skipped, not fatal.
|
||||
fn walkRoot(comptime Entry: type, root_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
|
||||
const header: *const SystemDescriptorTableHeader = @ptrFromInt(danos.physicalToVirtual(root_physical));
|
||||
if (!checksumOk(@ptrFromInt(danos.physicalToVirtual(root_physical)), header.length)) return error.BadRootChecksum;
|
||||
const header: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(root_physical));
|
||||
if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(root_physical)), header.length)) return error.BadRootChecksum;
|
||||
|
||||
const count = (header.length - @sizeOf(SystemDescriptorTableHeader)) / @sizeOf(Entry);
|
||||
const base: [*]const u8 = @ptrFromInt(danos.physicalToVirtual(root_physical));
|
||||
const base: [*]const u8 = @ptrFromInt(boot_handoff.physicalToVirtual(root_physical));
|
||||
const entries: [*]align(1) const Entry = @ptrCast(base + @sizeOf(SystemDescriptorTableHeader));
|
||||
|
||||
for (entries[0..count]) |ent| {
|
||||
@@ -427,18 +485,22 @@ fn walkRoot(comptime Entry: type, root_physical: u64, device_tree: *DeviceTree,
|
||||
|
||||
/// Dispatch a single SDT on its signature.
|
||||
fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void {
|
||||
const header: *const SystemDescriptorTableHeader = @ptrFromInt(danos.physicalToVirtual(sdt_physical));
|
||||
const header: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(sdt_physical));
|
||||
const sig = header.signature;
|
||||
if (std.mem.eql(u8, &sig, &APIC)) {
|
||||
try parseMadt(device_tree, header);
|
||||
} else if (std.mem.eql(u8, &sig, &MCFG)) {
|
||||
try parseMcfg(device_tree, hal, header);
|
||||
try parseMcfg(device_tree, header);
|
||||
} else if (std.mem.eql(u8, &sig, &HPET)) {
|
||||
try parseHpet(device_tree, hal, header);
|
||||
} else if (std.mem.eql(u8, &sig, &FACP)) {
|
||||
fadt_physical = sdt_physical;
|
||||
fadt_length = header.length;
|
||||
parseFadt(header);
|
||||
} else if (std.mem.eql(u8, &sig, &SPCR)) {
|
||||
parseSpcr(header);
|
||||
} else if (std.mem.eql(u8, &sig, &DMAR)) {
|
||||
parseDmar(hal, header);
|
||||
} else if (std.mem.eql(u8, &sig, &SSDT)) {
|
||||
// Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan.
|
||||
addAmlBlock(sdt_physical);
|
||||
@@ -515,7 +577,7 @@ fn parseMadt(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeade
|
||||
}
|
||||
|
||||
/// MCFG -> a pci_host_bridge per ECAM segment, then a PCI enumeration underneath.
|
||||
fn parseMcfg(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptorTableHeader) !void {
|
||||
fn parseMcfg(device_tree: *DeviceTree, header: *const SystemDescriptorTableHeader) !void {
|
||||
const total: usize = header.length;
|
||||
const base: [*]const u8 = @ptrCast(header);
|
||||
|
||||
@@ -530,100 +592,85 @@ fn parseMcfg(device_tree: *DeviceTree, hal: Hal, header: *const SystemDescriptor
|
||||
// ECAM window: 1 MiB of configuration space per bus.
|
||||
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
|
||||
_ = bridge.addResource(.bus_range, alloc.start_bus, bus_count);
|
||||
addBridgeApertures(bridge);
|
||||
// The bridge decodes the whole 16-bit I/O space toward its bus — the
|
||||
// window functions' I/O BARs must register-contain within (M19.2).
|
||||
_ = bridge.addResource(.io_port, 0, 1 << 16);
|
||||
|
||||
try enumeratePci(device_tree, bridge, hal, alloc.*);
|
||||
// The function walk itself retired to ring 3 (M19.3): the pci-bus
|
||||
// driver claims this bridge, repeats the scan through its ECAM grant,
|
||||
// and device_registers what it finds — the kernel seeds only the
|
||||
// bridge. The scan's equivalence was proven before the hand-off
|
||||
// (pci-scan), and the walk's history is in git if archaeology calls.
|
||||
}
|
||||
}
|
||||
|
||||
/// Brute-force scan the ECAM window's bus range for present PCI functions. No
|
||||
/// bridge recursion yet: on the ECAM path the host bridge decodes every bus in
|
||||
/// the window, so scanning the declared range finds everything QEMU exposes.
|
||||
fn enumeratePci(
|
||||
device_tree: *DeviceTree,
|
||||
bridge: *device_model.Device,
|
||||
hal: Hal,
|
||||
alloc: McfgAllocation,
|
||||
) !void {
|
||||
var bus: u16 = alloc.start_bus;
|
||||
while (bus <= alloc.end_bus) : (bus += 1) {
|
||||
var device: u8 = 0;
|
||||
while (device < 32) : (device += 1) {
|
||||
const h0: *align(1) const PciHeader = @ptrCast(pciConfigurationPtr(alloc, hal, @intCast(bus), device, 0));
|
||||
if (h0.vendor_id == 0xFFFF) continue; // no function 0 => slot empty
|
||||
/// The boot memory map, stored at discover() entry for the aperture derivation
|
||||
/// below (and, in M20, for the acpi-tables node's containment windows).
|
||||
var boot_memory_regions: []const boot_handoff.MemoryRegion = &.{};
|
||||
|
||||
const funcs: u8 = if (h0.header_type & 0x80 != 0) 8 else 1;
|
||||
var function: u8 = 0;
|
||||
while (function < funcs) : (function += 1) {
|
||||
const configuration = pciConfigurationPtr(alloc, hal, @intCast(bus), device, function);
|
||||
const h: *align(1) const PciHeader = @ptrCast(configuration);
|
||||
if (h.vendor_id == 0xFFFF) continue;
|
||||
|
||||
var nb: [24]u8 = undefined;
|
||||
const nm = std.fmt.bufPrint(&nb, "{s}:{x:0>2}:{x:0>2}.{d}", .{
|
||||
bridge.name(), bus, device, function,
|
||||
}) catch "pcidev";
|
||||
const node = try device_tree.addChild(bridge, .pci_device, nm);
|
||||
node.ids.pci_vendor = h.vendor_id;
|
||||
node.ids.pci_device = h.device_id;
|
||||
node.ids.pci_class = (@as(u24, h.class_code) << 16) |
|
||||
(@as(u24, h.subclass) << 8) | h.prog_if;
|
||||
node.ids.pci_bdf = (@as(u16, @intCast(bus)) << 8) | (@as(u16, device) << 3) | function;
|
||||
|
||||
// BARs only exist in header type 0 (normal devices), not bridges.
|
||||
if (h.header_type & 0x7F == 0) addBars(node, configuration);
|
||||
/// The bridge's MMIO apertures, derived from the boot memory map's holes
|
||||
/// (docs/discovery.md — apertures from the memory map): registered PCI functions carry BAR
|
||||
/// resources, and `device_register` containment demands the bridge own windows
|
||||
/// that cover them. Everything the firmware described is "not hole"; the low
|
||||
/// aperture runs from the end of the described space below 4 GiB up to the
|
||||
/// I/O-APIC region, the high one from 4 GiB (or the end of RAM above it) to
|
||||
/// the 46-bit line. Coarse, mechanical, and AML-free — available at boot no
|
||||
/// matter what later moved to user space.
|
||||
fn addBridgeApertures(bridge: *device_model.Device) void {
|
||||
// Below 4 GiB the described regions are sparse (RAM low, firmware flash
|
||||
// and tables high), so the holes are the *gaps between* them — a single
|
||||
// "after the last region" rule dies on OVMF's flash at the very top.
|
||||
// Sort-merge the described ranges, then keep the three largest gaps
|
||||
// (resource slots are bounded at 8 per device; ECAM + bus range + 3 + the
|
||||
// high aperture fits). Above 4 GiB one aperture runs from the end of the
|
||||
// described space to the 46-bit line.
|
||||
const Range = struct { base: u64, end: u64 };
|
||||
var below: [64]Range = undefined;
|
||||
var below_count: usize = 0;
|
||||
var high_end: u64 = 1 << 32;
|
||||
for (boot_memory_regions) |region| {
|
||||
const end = region.base + region.pages * 4096;
|
||||
// Above 4 GiB only *usable RAM* blocks the aperture: OVMF describes
|
||||
// its own 64-bit PCI window as a reserved region and then programs
|
||||
// BARs inside it — honoring reserved there would exclude the very
|
||||
// space BARs live in. Below 4 GiB every described region blocks (the
|
||||
// kernel image, the tables, the ramdisk all live there). Bring-up
|
||||
// trust: only the bridge's claimant can register into the aperture.
|
||||
if (region.kind == .usable and end > high_end) high_end = end;
|
||||
if (region.base >= (1 << 32) or below_count == below.len) continue;
|
||||
below[below_count] = .{ .base = region.base, .end = @min(end, 1 << 32) };
|
||||
below_count += 1;
|
||||
}
|
||||
// Insertion sort by base (the map is small and this runs once at boot).
|
||||
for (1..below_count) |i| {
|
||||
const key = below[i];
|
||||
var j = i;
|
||||
while (j > 0 and below[j - 1].base > key.base) : (j -= 1) below[j] = below[j - 1];
|
||||
below[j] = key;
|
||||
}
|
||||
// Walk the sorted ranges, collecting inter-region gaps of at least 1 MiB.
|
||||
var gaps: [3]Range = .{Range{ .base = 0, .end = 0 }} ** 3;
|
||||
var cursor: u64 = 0;
|
||||
var index: usize = 0;
|
||||
while (index <= below_count) : (index += 1) {
|
||||
const gap_end = if (index == below_count) (1 << 32) else below[index].base;
|
||||
if (gap_end > cursor and gap_end - cursor >= (1 << 20)) {
|
||||
// Keep the three largest, replacing the smallest kept so far.
|
||||
var smallest: usize = 0;
|
||||
for (gaps, 0..) |gap, gi| {
|
||||
if (gap.end - gap.base < gaps[smallest].end - gaps[smallest].base) smallest = gi;
|
||||
}
|
||||
if (gap_end - cursor > gaps[smallest].end - gaps[smallest].base) {
|
||||
gaps[smallest] = .{ .base = cursor, .end = gap_end };
|
||||
}
|
||||
}
|
||||
if (index < below_count and below[index].end > cursor) cursor = below[index].end;
|
||||
}
|
||||
}
|
||||
|
||||
/// Record and size the memory/IO windows named by a device's Base Address
|
||||
/// Registers. Sizing is the standard probe: disable decode, write all-ones, read
|
||||
/// back the writable (address) bits, restore. `size = ~mask + 1`.
|
||||
fn addBars(node: *device_model.Device, configuration: [*]align(1) u8) void {
|
||||
// Stop the device decoding its BARs while we transiently write all-ones.
|
||||
const command = rd(u16, configuration, 0x04);
|
||||
wr(u16, configuration, 0x04, command & ~@as(u16, 0b11));
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < 6) : (i += 1) {
|
||||
const off = 0x10 + i * 4;
|
||||
const orig = rd(u32, configuration, off);
|
||||
if (orig == 0) continue;
|
||||
|
||||
if (orig & 1 != 0) {
|
||||
// I/O-space BAR (16-bit address space on x86).
|
||||
wr(u32, configuration, off, 0xFFFF_FFFF);
|
||||
const readback = rd(u32, configuration, off);
|
||||
wr(u32, configuration, off, orig);
|
||||
const mask = readback & 0xFFFF_FFFC;
|
||||
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
|
||||
_ = node.addResource(.io_port, orig & 0xFFFF_FFFC, size);
|
||||
} else if ((orig >> 1) & 0x3 == 2) {
|
||||
// 64-bit memory BAR: this BAR pair spans two configuration slots.
|
||||
const orig_hi = rd(u32, configuration, off + 4);
|
||||
wr(u32, configuration, off, 0xFFFF_FFFF);
|
||||
wr(u32, configuration, off + 4, 0xFFFF_FFFF);
|
||||
const lo = rd(u32, configuration, off);
|
||||
const hi = rd(u32, configuration, off + 4);
|
||||
wr(u32, configuration, off, orig);
|
||||
wr(u32, configuration, off + 4, orig_hi);
|
||||
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
|
||||
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
|
||||
const address = (@as(u64, orig_hi) << 32) | (orig & 0xFFFF_FFF0);
|
||||
_ = node.addResource(.memory, address, size);
|
||||
i += 1; // consumed the high half
|
||||
} else {
|
||||
// 32-bit memory BAR.
|
||||
wr(u32, configuration, off, 0xFFFF_FFFF);
|
||||
const readback = rd(u32, configuration, off);
|
||||
wr(u32, configuration, off, orig);
|
||||
const mask = readback & 0xFFFF_FFF0;
|
||||
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
|
||||
_ = node.addResource(.memory, orig & 0xFFFF_FFF0, size);
|
||||
for (gaps) |gap| {
|
||||
if (gap.end > gap.base) _ = bridge.addResource(.memory, gap.base, gap.end - gap.base);
|
||||
}
|
||||
}
|
||||
|
||||
wr(u16, configuration, 0x04, command); // restore decode
|
||||
_ = bridge.addResource(.memory, high_end, (@as(u64, 1) << 46) - high_end);
|
||||
}
|
||||
|
||||
/// HPET -> a timer node with its register block as an MMIO resource, plus the GSI
|
||||
@@ -682,6 +729,7 @@ const fadt_pm1a_cnt_blk = 64; // u32 (I/O port)
|
||||
const fadt_pm1b_cnt_blk = 68; // u32 (I/O port)
|
||||
const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter
|
||||
const fadt_pm1_cnt_len = 89; // u8 (bytes)
|
||||
const fadt_sci_int = 46; // u16 (the SCI's GSI)
|
||||
const fadt_flags = 112; // u32
|
||||
const fadt_reset_register = 116; // GAS (12 bytes)
|
||||
const fadt_reset_value = 128; // u8
|
||||
@@ -699,6 +747,7 @@ fn parseFadt(header: *const SystemDescriptorTableHeader) void {
|
||||
const len: usize = header.length;
|
||||
const pi = &power_information;
|
||||
|
||||
pi.sci_interrupt = @truncate(fadt(u16, base, len, fadt_sci_int) orelse 0);
|
||||
pi.smi_cmd = @truncate(fadt(u32, base, len, fadt_smi_cmd) orelse 0);
|
||||
pi.acpi_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0;
|
||||
pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0;
|
||||
@@ -740,335 +789,42 @@ fn parseSpcr(header: *const SystemDescriptorTableHeader) void {
|
||||
platform_information.spcr_kind = fadt(u8, base, len, spcr_interface_type) orelse 0;
|
||||
}
|
||||
|
||||
// --- AML namespace -> generic device tree -----------------------------------
|
||||
// DMAR remapping-structure layout (Intel VT-d spec §8): the DMAR-specific header is 12
|
||||
// bytes (host-address-width, flags, 10 reserved), then a list of {type u16, length u16}
|
||||
// structures. Type 0 is a DRHD (DMA Remapping Hardware Unit Definition), whose 64-bit
|
||||
// register base sits at offset 8 within it.
|
||||
const dmar_structures_offset = 48; // 36-byte ACPI header + 12-byte DMAR header
|
||||
const dmar_type_drhd: u16 = 0;
|
||||
const drhd_register_base_offset = 8;
|
||||
|
||||
/// The PCI bus context while descending the ACPI namespace: the generic host
|
||||
/// bridge whose children ACPI address (`_ADR`) devices resolve against, and the bus number.
|
||||
const PciContext = struct { bridge: *device_model.Device, bus: u8 };
|
||||
/// DMAR -> detect the IOMMU. Find the first DMA-remapping hardware unit, map its
|
||||
/// register block, and record its version and capabilities. This is *detection only*:
|
||||
/// it tells the system an IOMMU exists (so `device_claim` on a DMA device could one day
|
||||
/// be gated by a per-device translation domain), but no domains are programmed yet —
|
||||
/// enforcement is built with the first DMA driver, which is what there is to protect and
|
||||
/// test against. See docs/driver-model.md (M16), the honest caveat.
|
||||
fn parseDmar(hal: Hal, header: *const SystemDescriptorTableHeader) void {
|
||||
const base: [*]align(1) const u8 = @ptrCast(header);
|
||||
const total: usize = header.length;
|
||||
|
||||
/// Mirror the ACPI namespace's Device objects into the generic tree, *merging*
|
||||
/// them with the PCI-enumerated nodes: a PCI root bridge (`PNP0A03`/`PNP0A08`)
|
||||
/// folds onto the existing `pci_host_bridge`, and each addressed (`_ADR`) device folds onto
|
||||
/// the matching PCI function (annotating it with the ACPI hardware ID (`_HID`) and nesting the
|
||||
/// ACPI-only children — keyboard, RTC, … — beneath it). Namespace devices with no
|
||||
/// PCI match land under a synthetic `acpi` node.
|
||||
fn wireAcpiDevices(device_tree: *DeviceTree, aml_namespace: *aml.Namespace, hal: Hal) !void {
|
||||
var arena = std.heap.ArenaAllocator.init(device_tree.allocator);
|
||||
defer arena.deinit();
|
||||
var interpreter = aml.Interpreter.init(aml_namespace, .{
|
||||
.mapMmio = hal.mapMmio,
|
||||
.pioRead = hal.pioRead,
|
||||
.pioWrite = hal.pioWrite,
|
||||
}, arena.allocator());
|
||||
|
||||
const acpi_root = try device_tree.addChild(device_tree.root, .unknown, "acpi");
|
||||
try mirrorDevices(device_tree, aml_namespace.root, acpi_root, null, &interpreter);
|
||||
}
|
||||
|
||||
fn mirrorDevices(device_tree: *DeviceTree, node: *aml.Node, parent_device: *device_model.Device, context: ?PciContext, interpreter: *aml.Interpreter) (error{OutOfMemory})!void {
|
||||
var child = node.first_child;
|
||||
while (child) |c| : (child = c.next_sibling) {
|
||||
if (c.kind != .device) {
|
||||
// A scope — the System Bus (\_SB), General Purpose Events (\_GPE), … —
|
||||
// descend without adding a node.
|
||||
try mirrorDevices(device_tree, c, parent_device, context, interpreter);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip devices the firmware reports as not present (via a device-status (`_STA`) method),
|
||||
// along with their whole subtree — per the ACPI rules.
|
||||
if (!devicePresent(interpreter, c)) continue;
|
||||
|
||||
var mirrored_device: *device_model.Device = undefined;
|
||||
var child_context = context;
|
||||
|
||||
if (isPciRootNode(c)) {
|
||||
// The PCI root bridge folds onto the generic host bridge.
|
||||
mirrored_device = matchHostBridge(device_tree) orelse
|
||||
try device_tree.addChild(parent_device, .acpi_device, &c.segment);
|
||||
child_context = .{ .bridge = mirrored_device, .bus = 0 };
|
||||
} else {
|
||||
// An addressed device folds onto its matching PCI function; anything
|
||||
// else becomes a fresh node under the current parent.
|
||||
mirrored_device = pick: {
|
||||
if (context) |pc| {
|
||||
if (readAdr(c)) |adr| {
|
||||
if (findPciNode(pc.bridge, pc.bus, adr)) |pnode| break :pick pnode;
|
||||
var off: usize = dmar_structures_offset;
|
||||
while (off + 4 <= total) {
|
||||
const kind = fadt(u16, base, total, off) orelse break;
|
||||
const length = fadt(u16, base, total, off + 2) orelse break;
|
||||
if (length < 4 or off + length > total) break; // malformed; stop rather than loop
|
||||
if (kind == dmar_type_drhd) {
|
||||
const register_base = fadt(u64, base, total, off + drhd_register_base_offset) orelse 0;
|
||||
if (register_base != 0) {
|
||||
const regs = hal.mapMmio(register_base, abi.page_size, true);
|
||||
platform_information.iommu_present = true;
|
||||
platform_information.iommu_base = register_base;
|
||||
platform_information.iommu_version = @as(*const volatile u32, @ptrFromInt(regs + 0x00)).*;
|
||||
platform_information.iommu_capabilities = @as(*const volatile u64, @ptrFromInt(regs + 0x08)).*;
|
||||
return; // first unit is enough for detection; multi-unit is future
|
||||
}
|
||||
}
|
||||
break :pick try device_tree.addChild(parent_device, .acpi_device, &c.segment);
|
||||
};
|
||||
off += length;
|
||||
}
|
||||
|
||||
applyHid(mirrored_device, c, interpreter);
|
||||
applyCrs(mirrored_device, c, interpreter);
|
||||
try mirrorDevices(device_tree, c, mirrored_device, child_context, interpreter);
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a device's status (`_STA`) to decide if it is present. An absent status
|
||||
/// (`_STA`) means present by default; an evaluation failure is treated as present too (we'd
|
||||
/// rather over-report than hide a device we couldn't introspect).
|
||||
fn devicePresent(interpreter: *aml.Interpreter, node: *aml.Node) bool {
|
||||
const sta = aml.Namespace.childOf(node, seg4("_STA")) orelse return true;
|
||||
const obj = interpreter.evaluate(sta, &.{}) catch return true;
|
||||
const status = obj.asInteger() catch return true;
|
||||
return (status & 0x01) != 0; // bit 0 = present
|
||||
}
|
||||
|
||||
/// The first PCI host bridge in the generic tree (segment 0).
|
||||
fn matchHostBridge(device_tree: *DeviceTree) ?*device_model.Device {
|
||||
var c = device_tree.root.first_child;
|
||||
while (c) |ch| : (c = ch.next_sibling) {
|
||||
if (ch.class == .pci_host_bridge) return ch;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The PCI function node under `bridge` at the address the device's address object
|
||||
/// (`_ADR`) names (device/function on
|
||||
/// `bus`), or null.
|
||||
fn findPciNode(bridge: *device_model.Device, bus: u8, adr: u32) ?*device_model.Device {
|
||||
const device: u16 = @truncate((adr >> 16) & 0x1F);
|
||||
const function: u16 = @truncate(adr & 0x7);
|
||||
const target: u16 = (@as(u16, bus) << 8) | (device << 3) | function;
|
||||
var c = bridge.first_child;
|
||||
while (c) |ch| : (c = ch.next_sibling) {
|
||||
if (ch.ids.pci_bdf) |bdf| {
|
||||
if (bdf == target) return ch;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// A device's address (`_ADR`) — a static integer Name — or null.
|
||||
fn readAdr(node: *aml.Node) ?u32 {
|
||||
const n = aml.Namespace.childOf(node, seg4("_ADR")) orelse return null;
|
||||
if (n.kind != .name) return null;
|
||||
var p: usize = 0;
|
||||
return @truncate(readIntObj(n.value, &p) orelse return null);
|
||||
}
|
||||
|
||||
/// Whether a namespace device is a PCI(e) host bridge (`PNP0A03` / `PNP0A08`).
|
||||
fn isPciRootNode(node: *aml.Node) bool {
|
||||
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return false;
|
||||
if (hid.kind != .name or hid.value.len == 0) return false;
|
||||
switch (hid.value[0]) {
|
||||
0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => {
|
||||
var p: usize = 0;
|
||||
const n = readIntObj(hid.value, &p) orelse return false;
|
||||
return n == 0x030AD041 or n == 0x080AD041; // PNP0A03 / PNP0A08
|
||||
},
|
||||
0x0D => {
|
||||
const s = cstr(hid.value[1..]);
|
||||
return std.mem.eql(u8, s, "PNP0A03") or std.mem.eql(u8, s, "PNP0A08");
|
||||
},
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a device's hardware ID (`_HID`) into the generic device: an integer decodes as an EISA
|
||||
/// id ("PNP0A03"), a string is taken verbatim. Handles both the common static
|
||||
/// Name form and a Method form (evaluated).
|
||||
fn applyHid(device: *device_model.Device, node: *aml.Node, interpreter: *aml.Interpreter) void {
|
||||
const hid = aml.Namespace.childOf(node, seg4("_HID")) orelse return;
|
||||
if (hid.kind == .method) {
|
||||
const obj = interpreter.evaluate(hid, &.{}) catch return;
|
||||
switch (obj) {
|
||||
.integer => |n| setEisaHid(device, @truncate(n)),
|
||||
.string => |s| device.setHid(s),
|
||||
else => {},
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (hid.kind != .name or hid.value.len == 0) return;
|
||||
const v = hid.value;
|
||||
switch (v[0]) {
|
||||
0x00, 0x01, 0xFF, 0x0A, 0x0B, 0x0C, 0x0E => {
|
||||
var p: usize = 0;
|
||||
const n = readIntObj(v, &p) orelse return;
|
||||
setEisaHid(device, @truncate(n));
|
||||
},
|
||||
0x0D => device.setHid(cstr(v[1..])), // StringPrefix
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn setEisaHid(device: *device_model.Device, id: u32) void {
|
||||
device.ids.acpi_hid = id;
|
||||
var buffer: [8]u8 = undefined;
|
||||
device.setHid(eisaIdToStr(id, &buffer));
|
||||
}
|
||||
|
||||
/// Parse a device's current resource settings (`_CRS`). The evaluator handles both the static
|
||||
/// `Buffer` form (a `Name`) and the method form uniformly, yielding the
|
||||
/// ResourceTemplate bytes we then decode.
|
||||
fn applyCrs(device: *device_model.Device, node: *aml.Node, interpreter: *aml.Interpreter) void {
|
||||
const crs = aml.Namespace.childOf(node, seg4("_CRS")) orelse return;
|
||||
const obj = interpreter.evaluate(crs, &.{}) catch return;
|
||||
const buffer = switch (obj) {
|
||||
.buffer => |b| b,
|
||||
else => return,
|
||||
};
|
||||
parseResourceTemplate(device, buffer);
|
||||
}
|
||||
|
||||
/// Walk a ResourceTemplate byte list, adding recognised descriptors as resources.
|
||||
fn parseResourceTemplate(device: *device_model.Device, bytes: []const u8) void {
|
||||
var i: usize = 0;
|
||||
while (i < bytes.len) {
|
||||
const tag = bytes[i];
|
||||
if (tag & 0x80 == 0) {
|
||||
// Small descriptor: length in low 3 bits, type in bits [6:3].
|
||||
const len: usize = tag & 0x07;
|
||||
const body = i + 1;
|
||||
if (body + len > bytes.len) break;
|
||||
switch ((tag >> 3) & 0x0F) {
|
||||
0x04 => if (len >= 2) { // IRQ: a 16-bit mask, one resource per set bit
|
||||
const mask = @as(u16, bytes[body]) | (@as(u16, bytes[body + 1]) << 8);
|
||||
var b: usize = 0;
|
||||
while (b < 16) : (b += 1) {
|
||||
if (mask & (@as(u16, 1) << @intCast(b)) != 0) _ = device.addResource(.irq, b, 1);
|
||||
}
|
||||
},
|
||||
0x08 => if (len >= 7) { // IO port: minimum at +1, length at +6
|
||||
_ = device.addResource(.io_port, rd16(bytes, body + 1), bytes[body + 6]);
|
||||
},
|
||||
0x09 => if (len >= 3) { // Fixed IO: base at +0, length at +2
|
||||
_ = device.addResource(.io_port, rd16(bytes, body), bytes[body + 2]);
|
||||
},
|
||||
0x0F => break, // EndTag
|
||||
else => {},
|
||||
}
|
||||
i = body + len;
|
||||
} else {
|
||||
// Large descriptor: 16-bit length follows the tag.
|
||||
if (i + 3 > bytes.len) break;
|
||||
const len: usize = @intCast(rd16(bytes, i + 1));
|
||||
const body = i + 3;
|
||||
if (body + len > bytes.len) break;
|
||||
switch (tag) {
|
||||
0x85 => if (len >= 17) { // Memory32: minimum at +1, length at +13
|
||||
_ = device.addResource(.memory, rd32(bytes, body + 1), rd32(bytes, body + 13));
|
||||
},
|
||||
0x86 => if (len >= 9) { // Memory32Fixed: base at +1, length at +5
|
||||
_ = device.addResource(.memory, rd32(bytes, body + 1), rd32(bytes, body + 5));
|
||||
},
|
||||
0x89 => if (len >= 2) { // Extended IRQ: count at +1, then count u32s
|
||||
const count = bytes[body + 1];
|
||||
var k: usize = 0;
|
||||
while (k < count and body + 2 + k * 4 + 4 <= body + len) : (k += 1) {
|
||||
_ = device.addResource(.irq, rd32(bytes, body + 2 + k * 4), 1);
|
||||
}
|
||||
},
|
||||
0x87, 0x88, 0x8A => parseAddressSpace(device, tag, bytes[body .. body + len]),
|
||||
else => {},
|
||||
}
|
||||
i = body + len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Word/DWord/QWord address-space descriptors: resource type at [0], then
|
||||
/// granularity/minimum/maximum/translation/length, each of width `w`.
|
||||
fn parseAddressSpace(device: *device_model.Device, tag: u8, body: []const u8) void {
|
||||
const w: usize = switch (tag) {
|
||||
0x88 => 2, // Word
|
||||
0x87 => 4, // DWord
|
||||
else => 8, // QWord (0x8A)
|
||||
};
|
||||
if (body.len < 3 + 5 * w) return;
|
||||
const minimum = readN(body, 3 + w, w);
|
||||
const length = readN(body, 3 + 4 * w, w);
|
||||
const kind: device_model.ResourceKind = switch (body[0]) {
|
||||
0 => .memory,
|
||||
1 => .io_port,
|
||||
else => .bus_range,
|
||||
};
|
||||
_ = device.addResource(kind, minimum, length);
|
||||
}
|
||||
|
||||
/// Decode a packed EISA id into its 7-char string (e.g. 0x030AD041 -> "PNP0A03").
|
||||
fn eisaIdToStr(id: u32, buffer: *[8]u8) []const u8 {
|
||||
const b0: u16 = @intCast(id & 0xFF);
|
||||
const b1: u16 = @intCast((id >> 8) & 0xFF);
|
||||
const b2: u8 = @truncate(id >> 16);
|
||||
const b3: u8 = @truncate(id >> 24);
|
||||
const mfg = (b0 << 8) | b1;
|
||||
buffer[0] = '@' + @as(u8, @intCast((mfg >> 10) & 0x1F));
|
||||
buffer[1] = '@' + @as(u8, @intCast((mfg >> 5) & 0x1F));
|
||||
buffer[2] = '@' + @as(u8, @intCast(mfg & 0x1F));
|
||||
buffer[3] = hexDigit((b2 >> 4) & 0xF);
|
||||
buffer[4] = hexDigit(b2 & 0xF);
|
||||
buffer[5] = hexDigit((b3 >> 4) & 0xF);
|
||||
buffer[6] = hexDigit(b3 & 0xF);
|
||||
return buffer[0..7];
|
||||
}
|
||||
|
||||
fn hexDigit(n: u8) u8 {
|
||||
return if (n < 10) '0' + n else 'A' + (n - 10);
|
||||
}
|
||||
|
||||
fn seg4(comptime s: *const [4:0]u8) [4]u8 {
|
||||
return s[0..4].*;
|
||||
}
|
||||
|
||||
fn cstr(bytes: []const u8) []const u8 {
|
||||
const index = std.mem.indexOfScalar(u8, bytes, 0) orelse bytes.len;
|
||||
return bytes[0..index];
|
||||
}
|
||||
|
||||
const PkgLen = struct { value: usize, size: usize };
|
||||
|
||||
fn packageLength(bytes: []const u8, p: usize) ?PkgLen {
|
||||
if (p >= bytes.len) return null;
|
||||
const lead = bytes[p];
|
||||
const follow: usize = lead >> 6;
|
||||
if (p + 1 + follow > bytes.len) return null;
|
||||
if (follow == 0) return .{ .value = lead & 0x3F, .size = 1 };
|
||||
var value: usize = lead & 0x0F;
|
||||
var i: usize = 0;
|
||||
while (i < follow) : (i += 1) value |= @as(usize, bytes[p + 1 + i]) << @intCast(4 + i * 8);
|
||||
return .{ .value = value, .size = 1 + follow };
|
||||
}
|
||||
|
||||
/// Read an AML integer object at `p`, advancing `p` past it.
|
||||
fn readIntObj(bytes: []const u8, p: *usize) ?u64 {
|
||||
if (p.* >= bytes.len) return null;
|
||||
const opcode = bytes[p.*];
|
||||
p.* += 1;
|
||||
return switch (opcode) {
|
||||
0x00 => 0,
|
||||
0x01 => 1,
|
||||
0xFF => 0xFF,
|
||||
0x0A => readLE(bytes, p, 1),
|
||||
0x0B => readLE(bytes, p, 2),
|
||||
0x0C => readLE(bytes, p, 4),
|
||||
0x0E => readLE(bytes, p, 8),
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn readLE(bytes: []const u8, p: *usize, n: usize) ?u64 {
|
||||
if (p.* + n > bytes.len) return null;
|
||||
const v = readN(bytes, p.*, n);
|
||||
p.* += n;
|
||||
return v;
|
||||
}
|
||||
|
||||
fn readN(bytes: []const u8, off: usize, n: usize) u64 {
|
||||
var v: u64 = 0;
|
||||
var k: usize = 0;
|
||||
while (k < n and off + k < bytes.len) : (k += 1) v |= @as(u64, bytes[off + k]) << @intCast(k * 8);
|
||||
return v;
|
||||
}
|
||||
|
||||
fn rd16(bytes: []const u8, off: usize) u64 {
|
||||
return readN(bytes, off, 2);
|
||||
}
|
||||
|
||||
fn rd32(bytes: []const u8, off: usize) u64 {
|
||||
return readN(bytes, off, 4);
|
||||
}
|
||||
|
||||
// --- helpers ----------------------------------------------------------------
|
||||
@@ -1111,58 +867,9 @@ fn readCntRegister(base: [*]align(1) const u8, len: usize, xoff: usize, legacy_o
|
||||
return .{ .mmio = false, .address = port, .width = width };
|
||||
}
|
||||
|
||||
/// The mapped configuration space of one PCI function (its 4 KiB ECAM page). Mapped
|
||||
/// writable so BAR sizing can probe it; reads and writes both go through here.
|
||||
fn pciConfigurationPtr(alloc: McfgAllocation, hal: Hal, bus: u8, device: u8, function: u8) [*]align(1) u8 {
|
||||
const physical = alloc.base_address +
|
||||
(@as(u64, bus - alloc.start_bus) << 20) +
|
||||
(@as(u64, device) << 15) +
|
||||
(@as(u64, function) << 12);
|
||||
// Map the configuration page (writable, for BAR sizing) and use the virtual
|
||||
// address the HAL hands back.
|
||||
return @ptrFromInt(hal.mapMmio(physical, danos.page_size, true));
|
||||
}
|
||||
|
||||
/// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer.
|
||||
/// x86 is little-endian and native, so an unaligned load suffices.
|
||||
fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
|
||||
const p: *align(1) const T = @ptrCast(bytes + off);
|
||||
return p.*;
|
||||
}
|
||||
|
||||
/// Write a little-endian integer at `off` through a (possibly unaligned) pointer.
|
||||
fn wr(comptime T: type, bytes: [*]align(1) u8, off: usize, value: T) void {
|
||||
const p: *align(1) T = @ptrCast(bytes + off);
|
||||
p.* = value;
|
||||
}
|
||||
|
||||
// --- tests ------------------------------------------------------------------
|
||||
|
||||
test "eisaIdToStr decodes a packed EISA id" {
|
||||
var buffer: [8]u8 = undefined;
|
||||
// 0x030AD041 is the well-known encoding of "PNP0A03" (PCI root bridge).
|
||||
try std.testing.expectEqualStrings("PNP0A03", eisaIdToStr(0x030AD041, &buffer));
|
||||
}
|
||||
|
||||
test "parseResourceTemplate extracts IO, IRQ, and fixed memory" {
|
||||
// ResourceTemplate { IO(minimum 0x60, len 8), IRQ(4), Memory32Fixed(0xFED00000, 0x1000) }
|
||||
const runtime = [_]u8{
|
||||
0x47, 0x01, 0x60, 0x00, 0x60, 0x00, 0x01, 0x08, // small IO descriptor
|
||||
0x22, 0x10, 0x00, // small IRQ descriptor (mask bit 4 -> IRQ 4)
|
||||
0x86, 0x09, 0x00, 0x01, 0x00, 0x00, 0xD0, 0xFE, 0x00, 0x10, 0x00, 0x00, // Memory32Fixed
|
||||
0x79, 0x00, // EndTag
|
||||
};
|
||||
var device = device_model.Device{};
|
||||
parseResourceTemplate(&device, &runtime);
|
||||
|
||||
try std.testing.expectEqual(@as(u8, 3), device.resource_count);
|
||||
const rs = device.resources[0..device.resource_count];
|
||||
try std.testing.expectEqual(device_model.ResourceKind.io_port, rs[0].kind);
|
||||
try std.testing.expectEqual(@as(u64, 0x60), rs[0].start);
|
||||
try std.testing.expectEqual(@as(u64, 8), rs[0].len);
|
||||
try std.testing.expectEqual(device_model.ResourceKind.irq, rs[1].kind);
|
||||
try std.testing.expectEqual(@as(u64, 4), rs[1].start);
|
||||
try std.testing.expectEqual(device_model.ResourceKind.memory, rs[2].kind);
|
||||
try std.testing.expectEqual(@as(u64, 0xFED00000), rs[2].start);
|
||||
try std.testing.expectEqual(@as(u64, 0x1000), rs[2].len);
|
||||
}
|
||||
|
||||
+62
-10
@@ -3,7 +3,7 @@
|
||||
//!
|
||||
//! This module has two stages. `parser.zig` walks the entire byte stream and
|
||||
//! records every named object into a namespace tree (`namespace.zig`), capturing
|
||||
//! method bodies and field/region layout. `interp.zig` then *evaluates* control
|
||||
//! method bodies and field/region layout. `interpreter.zig` then *evaluates* control
|
||||
//! methods on demand — running operators, control flow, and OperationRegion field
|
||||
//! access — so callers can resolve device status (`_STA`), current resource
|
||||
//! settings (`_CRS`), sleep states (`_Sx`), and the like against the live namespace.
|
||||
@@ -12,15 +12,20 @@ const std = @import("std");
|
||||
const opcode = @import("opcodes.zig");
|
||||
const parser = @import("parser.zig");
|
||||
|
||||
/// The named AML opcode/prefix bytes (`zero_opcode`, `byte_prefix`, …). Re-exported so
|
||||
/// callers that decode raw AML bytes — e.g. the acpi service reading a `_HID` integer —
|
||||
/// name the opcodes instead of writing bare 0x0A/0x0B/… literals (docs/coding-standards.md).
|
||||
pub const opcodes = @import("opcodes.zig");
|
||||
|
||||
pub const Namespace = @import("namespace.zig").Namespace;
|
||||
pub const Node = @import("namespace.zig").Node;
|
||||
pub const NodeKind = @import("namespace.zig").NodeKind;
|
||||
|
||||
/// The AML evaluator: interprets control methods (and reads Names/Fields) far
|
||||
/// enough for device discovery. See `interp.zig`.
|
||||
pub const Interpreter = @import("interp.zig").Interpreter;
|
||||
pub const Object = @import("interp.zig").Object;
|
||||
pub const EvaluateHal = @import("interp.zig").Hal;
|
||||
/// enough for device discovery. See `interpreter.zig`.
|
||||
pub const Interpreter = @import("interpreter.zig").Interpreter;
|
||||
pub const Object = @import("interpreter.zig").Object;
|
||||
pub const EvaluateHal = @import("interpreter.zig").Hal;
|
||||
|
||||
/// The SLP_TYP values written to PM1a/PM1b control to enter a sleep state.
|
||||
pub const SleepType = struct {
|
||||
@@ -50,6 +55,20 @@ pub fn parse(allocator: std.mem.Allocator, blocks: []const []const u8) !ParseRes
|
||||
return .{ .namespace = namespace, .consumed = consumed, .total = total };
|
||||
}
|
||||
|
||||
/// Count the Device objects in a parsed namespace — what the acpi service
|
||||
/// (docs/discovery.md) reports, and what the kernel's own parse counts
|
||||
/// so the two can be checked equal across the ring-3 move.
|
||||
pub fn deviceCount(namespace: *const Namespace) usize {
|
||||
return countKind(namespace.root, .device);
|
||||
}
|
||||
|
||||
fn countKind(node: *const Node, kind: NodeKind) usize {
|
||||
var n: usize = if (node.kind == kind) 1 else 0;
|
||||
var c = node.first_child;
|
||||
while (c) |child| : (c = child.next_sibling) n += countKind(child, kind);
|
||||
return n;
|
||||
}
|
||||
|
||||
/// Look up the `\_S{state}` sleep package in a parsed namespace and return its
|
||||
/// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent.
|
||||
pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType {
|
||||
@@ -126,17 +145,22 @@ test "parses a nested namespace and finds the sleep package" {
|
||||
// Scope(\_SB) packagelen=0x27
|
||||
0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F,
|
||||
// Device(PCI0) packagelen=0x1F
|
||||
0x5B, 0x82, 0x1F, 0x50, 0x43, 0x49, 0x30,
|
||||
0x5B, 0x82, 0x1F, 0x50, 0x43,
|
||||
0x49, 0x30,
|
||||
// Name(_HID, 0x11)
|
||||
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11,
|
||||
// Method(MTHD, flags=1) empty, packagelen=0x06
|
||||
0x14, 0x06, 0x4D, 0x54, 0x48, 0x44, 0x01,
|
||||
0x14, 0x06, 0x4D,
|
||||
0x54, 0x48, 0x44, 0x01,
|
||||
// Method(CALL, flags=0) { MTHD(Zero) }, packagelen=0x0B
|
||||
0x14, 0x0B, 0x43, 0x41, 0x4C, 0x4C, 0x00, 0x4D, 0x54, 0x48, 0x44, 0x00,
|
||||
0x14, 0x0B, 0x43, 0x41, 0x4C, 0x4C, 0x00, 0x4D,
|
||||
0x54, 0x48, 0x44, 0x00,
|
||||
// OperationRegion(DBG0, SystemIO, Word 0x0402, Byte 1)
|
||||
0x5B, 0x80, 0x44, 0x42, 0x47, 0x30, 0x01, 0x0B, 0x02, 0x04, 0x0A, 0x01,
|
||||
0x5B, 0x80, 0x44, 0x42, 0x47, 0x30, 0x01, 0x0B,
|
||||
0x02, 0x04, 0x0A, 0x01,
|
||||
// Field(DBG0, flags=1) { DBGB, 8 }, packagelen=0x0B
|
||||
0x5B, 0x81, 0x0B, 0x44, 0x42, 0x47, 0x30, 0x01, 0x44, 0x42, 0x47, 0x42, 0x08,
|
||||
0x5B, 0x81, 0x0B, 0x44, 0x42, 0x47, 0x30, 0x01,
|
||||
0x44, 0x42, 0x47, 0x42, 0x08,
|
||||
};
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
@@ -206,3 +230,31 @@ test "interpreter runs a method with args, arithmetic, and control flow" {
|
||||
const lo = try interpreter.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0
|
||||
try std.testing.expectEqual(@as(u64, 0), try lo.asInteger());
|
||||
}
|
||||
|
||||
test "interpreter records Notify(device, code)" {
|
||||
// Device(DEV_) { Name(_HID, 0x030AD041) } // PNP0A03-ish placeholder
|
||||
// Method(TST_, 0) { Notify(DEV_, 0x80); Return(Zero) }
|
||||
// Encoded: a Device holding a Name, then a Method issuing Notify on it.
|
||||
const blob = [_]u8{
|
||||
0x5B, 0x82, 0x0F, 0x44, 0x45, 0x56, 0x5F, // Device(DEV_) len=0x0F (pkglen + DEV_ + Name)
|
||||
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0C, 0x41, 0xD0, 0x0A, 0x03, // Name(_HID, DWord 0x030AD041)
|
||||
0x14, 0x0F, 0x54, 0x53, 0x54, 0x5F, 0x00, // Method(TST_, 0) len=0x0F (pkglen + TST_ + flags + body)
|
||||
0x86, 0x44, 0x45, 0x56, 0x5F, 0x0A, 0x80, // Notify(DEV_, 0x80)
|
||||
0xA4, 0x00, // Return(Zero)
|
||||
};
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var result = try parse(arena.allocator(), &.{&blob});
|
||||
const namespace = &result.namespace;
|
||||
const tst = namespace.resolve(namespace.root, false, 0, &.{.{ 'T', 'S', 'T', '_' }}) orelse return error.NoMethod;
|
||||
const dev = namespace.resolve(namespace.root, false, 0, &.{.{ 'D', 'E', 'V', '_' }}) orelse return error.NoDevice;
|
||||
|
||||
var interpreter = Interpreter.init(namespace, .{ .mapMmio = noMap, .pioRead = noRead, .pioWrite = noWrite }, arena.allocator());
|
||||
_ = try interpreter.evaluate(tst, &.{});
|
||||
|
||||
const events = interpreter.takeNotifications();
|
||||
try std.testing.expectEqual(@as(usize, 1), events.len);
|
||||
try std.testing.expectEqual(dev, events[0].node);
|
||||
try std.testing.expectEqual(@as(u64, 0x80), events[0].code);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,9 @@ const Frame = struct {
|
||||
/// A CreateField binding: a name that indexes into a buffer object.
|
||||
const BufferField = struct { buffer: *Node, byte_off: usize, bit_width: u32 };
|
||||
|
||||
/// One Notify(device, code) the interpreter executed.
|
||||
pub const NotifyEvent = struct { node: *Node, code: u64 };
|
||||
|
||||
pub const Interpreter = struct {
|
||||
namespace: *Namespace,
|
||||
hal: Hal,
|
||||
@@ -149,6 +152,11 @@ pub const Interpreter = struct {
|
||||
dynamic_overrides: std.AutoHashMapUnmanaged(*Node, Object) = .{},
|
||||
/// CreateField bindings active for the current evaluation.
|
||||
fields: std.AutoHashMapUnmanaged(*Node, BufferField) = .{},
|
||||
/// Notify(device, code) operations the last evaluation executed — a GPE or
|
||||
/// EC handler tells the OS "look at this device" this way. Bounded; the
|
||||
/// caller drains it with `takeNotifications` after `evaluate` (M21).
|
||||
notify_queue: [16]NotifyEvent = undefined,
|
||||
notify_count: usize = 0,
|
||||
|
||||
pub fn init(namespace: *Namespace, hal: Hal, arena: std.mem.Allocator) Interpreter {
|
||||
return .{ .namespace = namespace, .hal = hal, .arena = arena };
|
||||
@@ -157,6 +165,7 @@ pub const Interpreter = struct {
|
||||
/// Evaluate a namespace object: invoke a Method, read a Name's value, or read a
|
||||
/// Field. Resets per-evaluation runtime state first.
|
||||
pub fn evaluate(self: *Interpreter, node: *Node, args: []const Object) Error!Object {
|
||||
self.notify_count = 0;
|
||||
self.dynamic_overrides.clearRetainingCapacity();
|
||||
self.fields.clearRetainingCapacity();
|
||||
return self.invoke(node, args);
|
||||
@@ -267,6 +276,8 @@ pub const Interpreter = struct {
|
||||
},
|
||||
opcode.to_buffer_opcode => try self.passThroughUnary(current, frame),
|
||||
|
||||
opcode.notify_opcode => try self.notify(current, frame),
|
||||
|
||||
opcode.extended_opcode_prefix => try self.ext(current, frame),
|
||||
|
||||
// CreateXField: source, index, name (bit widths differ by op)
|
||||
@@ -542,6 +553,36 @@ pub const Interpreter = struct {
|
||||
try self.storeInto(current, frame, value);
|
||||
}
|
||||
|
||||
/// Notify(SuperName, NotifyValue): resolve the named device, evaluate the
|
||||
/// code, and record the pair for the caller to dispatch. AML control flow
|
||||
/// continues (Notify returns nothing).
|
||||
fn notify(self: *Interpreter, current: *Cursor, frame: *Frame) Error!Object {
|
||||
const lead = current.peek() orelse return error.Truncated;
|
||||
var target: ?*Node = null;
|
||||
if (isNameStart(lead)) {
|
||||
const name_path = try current.nameString();
|
||||
target = self.namespace.resolve(frame.scope, name_path.rooted, name_path.parents, name_path.slice());
|
||||
} else {
|
||||
// A non-name SuperName (Local/Arg holding a reference).
|
||||
const obj = try self.term(current, frame);
|
||||
if (obj == .reference) target = obj.reference;
|
||||
}
|
||||
const code = try self.evaluateInteger(current, frame);
|
||||
if (target) |node| {
|
||||
if (self.notify_count < self.notify_queue.len) {
|
||||
self.notify_queue[self.notify_count] = .{ .node = node, .code = code };
|
||||
self.notify_count += 1;
|
||||
}
|
||||
}
|
||||
return .uninitialized;
|
||||
}
|
||||
|
||||
/// The Notify events the last `evaluate` produced. Valid until the next
|
||||
/// `evaluate` clears the queue.
|
||||
pub fn takeNotifications(self: *Interpreter) []const NotifyEvent {
|
||||
return self.notify_queue[0..self.notify_count];
|
||||
}
|
||||
|
||||
fn storeInto(self: *Interpreter, current: *Cursor, frame: *Frame, value: Object) Error!void {
|
||||
const lead = current.peek() orelse return error.Truncated;
|
||||
if (isNameStart(lead)) {
|
||||
@@ -0,0 +1,95 @@
|
||||
//! The **device ABI**: the flat, `extern` device types that cross the system_call
|
||||
//! boundary — what `device_enumerate` hands a user-space driver, what
|
||||
//! `device_register` takes back. This is the devices sub-project's *public
|
||||
//! interface*, exposed as its own `device-abi` module the same way the VFS server
|
||||
//! exposes `vfs-protocol` — so both the kernel and user space depend on the contract
|
||||
//! by name, and neither reaches into the other's files.
|
||||
//!
|
||||
//! It is also the **single source of truth** for `DeviceClass` and `ResourceKind`:
|
||||
//! the kernel's rich, pointer-based device tree (system/devices/device-model.zig,
|
||||
//! which user space must never import) re-exports these, so the enum that a driver
|
||||
//! matches on and the enum the kernel classifies with are the *same* type — no
|
||||
//! hand-kept "mirror in order" to drift. The core kernel↔user ABI is [[abi]]; the
|
||||
//! loader↔kernel handoff is [[boot-handoff]].
|
||||
|
||||
/// A coarse classification of a device, independent of the describing firmware.
|
||||
/// Kept small on purpose; refine as real drivers arrive. `enum(u32)` because the
|
||||
/// `@intFromEnum` value crosses the system_call boundary in `DeviceDescriptor.class`.
|
||||
pub const DeviceClass = enum(u32) {
|
||||
/// The synthetic root every discovered device hangs beneath.
|
||||
root,
|
||||
processor,
|
||||
interrupt_controller,
|
||||
timer,
|
||||
/// A PCI(e) host bridge — the root of a PCI segment (owns an ECAM window).
|
||||
pci_host_bridge,
|
||||
/// A single PCI function.
|
||||
pci_device,
|
||||
/// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a
|
||||
/// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`).
|
||||
acpi_device,
|
||||
/// The ACPI tables themselves, published as one node for the user-space acpi
|
||||
/// service (docs/discovery.md): memory resources over the AML blobs,
|
||||
/// a broad io_port grant for OperationRegion access, and the SCI interrupt.
|
||||
/// The one node whose claimant is trusted to run firmware bytecode.
|
||||
acpi_tables,
|
||||
/// One interface of a USB device, registered by the xHCI bus driver. It owns
|
||||
/// no MMIO — it is reached through its controller — so it carries no
|
||||
/// resources; the (class, subclass, protocol) triple that says what it is
|
||||
/// travels in the bus report's identity, not here.
|
||||
usb_device,
|
||||
unknown,
|
||||
};
|
||||
|
||||
/// The kind of hardware resource a device occupies. `enum(u32)` for the same
|
||||
/// boundary-crossing reason as `DeviceClass` (see `ResourceDescriptor.kind`).
|
||||
pub const ResourceKind = enum(u32) {
|
||||
/// A memory-mapped I/O window: `start` is the physical base, `len` its size.
|
||||
memory,
|
||||
/// A legacy I/O-port range: `start` is the first port, `len` the count.
|
||||
io_port,
|
||||
/// An interrupt: `start` is the global system interrupt (GSI), `len` is 1.
|
||||
irq,
|
||||
/// A range of bus numbers owned by a bridge: `start`..`start+len`.
|
||||
bus_range,
|
||||
};
|
||||
|
||||
/// One device resource, as handed to a user-space driver (flat, extern).
|
||||
pub const ResourceDescriptor = extern struct {
|
||||
kind: u64, // a ResourceKind value
|
||||
start: u64,
|
||||
len: u64,
|
||||
};
|
||||
|
||||
pub const maximum_device_resources = 8;
|
||||
|
||||
/// `DeviceDescriptor.parent` for a device with no parent — a root of the device tree.
|
||||
pub const no_parent: u64 = ~@as(u64, 0);
|
||||
|
||||
/// `DeviceDescriptor.pci_class` for a device that is not a PCI function. (Zero would be
|
||||
/// ambiguous: 0x000000 is a real class code, "unclassified device".)
|
||||
pub const no_pci_class: u64 = ~@as(u64, 0);
|
||||
|
||||
/// A device, as snapshotted for user space by `device_enumerate`. A driver scans
|
||||
/// these to find the hardware it owns, claims it, and maps its MMIO.
|
||||
///
|
||||
/// `parent` makes the table a tree rather than a list, which is what a **bus driver**
|
||||
/// needs: it claims the bus, finds the devices below it, and publishes any it
|
||||
/// discovers itself with `device_register`. A registered child's resources must lie
|
||||
/// within its parent's (the kernel enforces this) — that containment is what makes
|
||||
/// delegation safe, since a device descriptor is otherwise a licence to map physical
|
||||
/// memory.
|
||||
pub const DeviceDescriptor = extern struct {
|
||||
id: u64,
|
||||
parent: u64, // a device id, or `no_parent`
|
||||
class: u64, // a DeviceClass value
|
||||
// The PCI class/subclass/prog-IF triple packed as 0xCCSSPP when this device is a PCI
|
||||
// function, or `no_pci_class` otherwise. This is how a manager tells *what* a
|
||||
// `pci_device` is (an xHCI controller, an AHCI controller) — decode the triple into
|
||||
// names with the pci-class module.
|
||||
pci_class: u64,
|
||||
hid_len: u64,
|
||||
resource_count: u64,
|
||||
hid: [8]u8,
|
||||
resources: [maximum_device_resources]ResourceDescriptor,
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Discovery backends (ACPI today, device-tree later) translate their native
|
||||
//! hardware description into this one shape, so the rest of the kernel walks a
|
||||
//! plain `Device` tree without knowing which firmware described the machine —
|
||||
//! the same discipline `root.zig`'s `MemoryKind` applies to memory and `architecture`
|
||||
//! the same discipline `ps2-library.zig`'s `MemoryKind` applies to memory and `architecture`
|
||||
//! applies to the CPU.
|
||||
//!
|
||||
//! This is deliberately minimal: enough to *describe* what was discovered (a
|
||||
@@ -12,6 +12,9 @@
|
||||
//! on top of this — nothing here presumes them.
|
||||
|
||||
const std = @import("std");
|
||||
const device_abi = @import("device-abi");
|
||||
const pci_class = @import("pci-class");
|
||||
const acpi_ids = @import("acpi-ids");
|
||||
|
||||
/// The hardware primitives a discovery backend needs but can't express portably.
|
||||
/// The kernel injects an implementation (the architecture VMM + port I/O), so the device
|
||||
@@ -26,17 +29,10 @@ pub const Hal = struct {
|
||||
pioWrite: *const fn (width: u8, port: u16, value: u32) void,
|
||||
};
|
||||
|
||||
/// The kind of hardware resource a device occupies.
|
||||
pub const ResourceKind = enum {
|
||||
/// A memory-mapped I/O window: `start` is the physical base, `len` its size.
|
||||
memory,
|
||||
/// A legacy I/O-port range: `start` is the first port, `len` the count.
|
||||
io_port,
|
||||
/// An interrupt: `start` is the global system interrupt (GSI), `len` is 1.
|
||||
irq,
|
||||
/// A range of bus numbers owned by a bridge: `start`..`start+len`.
|
||||
bus_range,
|
||||
};
|
||||
/// The kind of hardware resource a device occupies. Canonically defined by the
|
||||
/// device ABI (system/devices/device-abi.zig) and re-exported here, so the kernel's
|
||||
/// internal tree and the descriptors it hands user space share one enum.
|
||||
pub const ResourceKind = device_abi.ResourceKind;
|
||||
|
||||
/// One hardware resource claimed by a device.
|
||||
pub const Resource = struct {
|
||||
@@ -46,22 +42,10 @@ pub const Resource = struct {
|
||||
};
|
||||
|
||||
/// A coarse classification of a device, independent of the describing firmware.
|
||||
/// Kept small on purpose; refine as real drivers arrive.
|
||||
pub const DeviceClass = enum {
|
||||
/// The synthetic root every discovered device hangs beneath.
|
||||
root,
|
||||
processor,
|
||||
interrupt_controller,
|
||||
timer,
|
||||
/// A PCI(e) host bridge — the root of a PCI segment (owns an ECAM window).
|
||||
pci_host_bridge,
|
||||
/// A single PCI function.
|
||||
pci_device,
|
||||
/// A device named in the ACPI namespace (from the DSDT/SSDT), carrying a
|
||||
/// hardware ID (`_HID`) and, where static, current resource settings (`_CRS`).
|
||||
acpi_device,
|
||||
unknown,
|
||||
};
|
||||
/// Canonically defined by the device ABI (system/devices/device-abi.zig) and
|
||||
/// re-exported here — the enum a driver matches on and the one the kernel classifies
|
||||
/// with are the same type. Kept small on purpose; refine as real drivers arrive.
|
||||
pub const DeviceClass = device_abi.DeviceClass;
|
||||
|
||||
/// Firmware-independent identity. Each backend fills only the fields it knows;
|
||||
/// the rest stay null. The generic layer never branches on *how* an id was
|
||||
@@ -207,12 +191,31 @@ fn dumpNode(device: *const Device, depth: usize, emit: *const fn ([]const u8) vo
|
||||
|
||||
var buffer: [200]u8 = undefined;
|
||||
@memset(buffer[0..indent], ' ');
|
||||
const body = if (device.hid_len != 0)
|
||||
std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s}\n", .{ device.name(), @tagName(device.class), device.hid() }) catch return
|
||||
const body = if (device.hid_len != 0) blk: {
|
||||
// Decode the _HID to a human name when it's a known standard PnP/ACPI id.
|
||||
const desc = acpi_ids.description(device.hid());
|
||||
break :blk if (desc.len != 0)
|
||||
std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s} ({s})\n", .{ device.name(), @tagName(device.class), device.hid(), desc }) catch return
|
||||
else
|
||||
std.fmt.bufPrint(buffer[indent..], "{s} [{s}]\n", .{ device.name(), @tagName(device.class) }) catch return;
|
||||
std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s}\n", .{ device.name(), @tagName(device.class), device.hid() }) catch return;
|
||||
} else std.fmt.bufPrint(buffer[indent..], "{s} [{s}]\n", .{ device.name(), @tagName(device.class) }) catch return;
|
||||
emit(buffer[0 .. indent + body.len]);
|
||||
|
||||
// For a PCI function, decode its class code — the (class / subclass / prog-IF)
|
||||
// triple that says what it actually is, which the coarse `DeviceClass` can't.
|
||||
if (device.ids.pci_class) |packed_code| {
|
||||
const cc = pci_class.ClassCode.unpack(packed_code);
|
||||
var cbuf: [200]u8 = undefined;
|
||||
const pad = @min(indent + 2, 42);
|
||||
@memset(cbuf[0..pad], ' ');
|
||||
const pif = pci_class.progIfName(cc.base, cc.subclass, cc.prog_if);
|
||||
const cline = if (pif.len != 0)
|
||||
std.fmt.bufPrint(cbuf[pad..], "class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2} ({s})\n", .{ cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if, pif }) catch return
|
||||
else
|
||||
std.fmt.bufPrint(cbuf[pad..], "class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2}\n", .{ cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if }) catch return;
|
||||
emit(cbuf[0 .. pad + cline.len]);
|
||||
}
|
||||
|
||||
for (device.resources[0..device.resource_count]) |r| {
|
||||
var rbuf: [200]u8 = undefined;
|
||||
const pad = @min(indent + 2, 42);
|
||||
|
||||
@@ -0,0 +1,568 @@
|
||||
//! PCI class-code decoding: turn the (class, subclass, prog-IF) triple a PCI function
|
||||
//! reports in its configuration header into human-readable names. Every PCI function
|
||||
//! carries a 24-bit class code — base class (config byte 0x0B), subclass (0x0A), and
|
||||
//! programming interface (0x09) — that says *what it is* far more precisely than
|
||||
//! danos's coarse `DeviceClass`: an ISA bridge, a SATA/AHCI controller, and an xHCI USB
|
||||
//! controller are all just `pci_device` by class, and only this triple tells them
|
||||
//! apart. Pure reference data (from the PCI spec; see https://wiki.osdev.org/PCI) — no
|
||||
//! hardware access — so it is shared by kernel discovery (the device-tree dump) and any
|
||||
//! user-space tool (a future lspci, driver matching).
|
||||
//!
|
||||
//! The taxonomy is named, not numbered (docs/coding-standards.md, "Named values"): the
|
||||
//! base class is a `BaseClass` enum, and each class with defined subclasses gets a
|
||||
//! namespace holding its `SubClass` enum (and, where the spec defines them, per-subclass
|
||||
//! `ProgIf` enums) — the same shape as `usb-ids.zig`. Code that *means* a specific class
|
||||
//! names it (`BaseClass.serial_bus`, `serial_bus.usb.ProgIf.xhci`) rather than writing a
|
||||
//! bare 0x0C/0x03/0x30. The `className`/`subclassName`/`progIfName` functions still take
|
||||
//! the raw bytes a function reports in its header, because that is what hardware hands us.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// The three bytes of a PCI class code, unpacked from the `0xCCSSPP` value discovery
|
||||
/// records in `Device.ids.pci_class` (CC = base class, SS = subclass, PP = prog-IF).
|
||||
pub const ClassCode = struct {
|
||||
base: u8, // class code (config offset 0x0B)
|
||||
subclass: u8, // subclass (0x0A)
|
||||
prog_if: u8, // programming interface (0x09)
|
||||
|
||||
pub fn unpack(packed_code: u24) ClassCode {
|
||||
return .{
|
||||
.base = @intCast((packed_code >> 16) & 0xFF),
|
||||
.subclass = @intCast((packed_code >> 8) & 0xFF),
|
||||
.prog_if = @intCast(packed_code & 0xFF),
|
||||
};
|
||||
}
|
||||
|
||||
/// Re-pack the triple into the `0xCCSSPP` form. Lets code name a whole class code
|
||||
/// from its parts — `pack(.{ .base = @intFromEnum(BaseClass.serial_bus), … })` —
|
||||
/// instead of writing the literal 0x0C0330.
|
||||
pub fn pack(self: ClassCode) u24 {
|
||||
return (@as(u24, self.base) << 16) | (@as(u24, self.subclass) << 8) | self.prog_if;
|
||||
}
|
||||
};
|
||||
|
||||
/// Base class (config byte 0x0B). Non-exhaustive: an unlisted code is a real but
|
||||
/// unnamed class, decoded as "Unknown" rather than rejected.
|
||||
pub const BaseClass = enum(u8) {
|
||||
unclassified = 0x00,
|
||||
mass_storage = 0x01,
|
||||
network = 0x02,
|
||||
display = 0x03,
|
||||
multimedia = 0x04,
|
||||
memory = 0x05,
|
||||
bridge = 0x06,
|
||||
simple_communication = 0x07,
|
||||
base_system_peripheral = 0x08,
|
||||
input_device = 0x09,
|
||||
docking_station = 0x0A,
|
||||
processor = 0x0B,
|
||||
serial_bus = 0x0C,
|
||||
wireless = 0x0D,
|
||||
intelligent = 0x0E,
|
||||
satellite_communication = 0x0F,
|
||||
encryption = 0x10,
|
||||
signal_processing = 0x11,
|
||||
processing_accelerator = 0x12,
|
||||
non_essential_instrumentation = 0x13,
|
||||
co_processor = 0x40,
|
||||
unassigned = 0xFF,
|
||||
_,
|
||||
|
||||
pub fn name(self: BaseClass) []const u8 {
|
||||
return switch (self) {
|
||||
.unclassified => "Unclassified",
|
||||
.mass_storage => "Mass Storage Controller",
|
||||
.network => "Network Controller",
|
||||
.display => "Display Controller",
|
||||
.multimedia => "Multimedia Controller",
|
||||
.memory => "Memory Controller",
|
||||
.bridge => "Bridge",
|
||||
.simple_communication => "Simple Communication Controller",
|
||||
.base_system_peripheral => "Base System Peripheral",
|
||||
.input_device => "Input Device Controller",
|
||||
.docking_station => "Docking Station",
|
||||
.processor => "Processor",
|
||||
.serial_bus => "Serial Bus Controller",
|
||||
.wireless => "Wireless Controller",
|
||||
.intelligent => "Intelligent Controller",
|
||||
.satellite_communication => "Satellite Communication Controller",
|
||||
.encryption => "Encryption Controller",
|
||||
.signal_processing => "Signal Processing Controller",
|
||||
.processing_accelerator => "Processing Accelerator",
|
||||
.non_essential_instrumentation => "Non-Essential Instrumentation",
|
||||
.co_processor => "Co-Processor",
|
||||
.unassigned => "Unassigned Class (Vendor specific)",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// --- Per-class subclass (and prog-IF) taxonomies --------------------------------------
|
||||
// One namespace per base class that has defined subclasses, named after the class. Each
|
||||
// holds an exhaustive `SubClass` enum (so an unlisted code decodes to the class default,
|
||||
// not a wrong name), and, where the spec assigns them, per-subclass `ProgIf` enums.
|
||||
|
||||
pub const mass_storage = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
scsi_bus = 0x00,
|
||||
ide = 0x01,
|
||||
floppy = 0x02,
|
||||
ipi_bus = 0x03,
|
||||
raid = 0x04,
|
||||
ata = 0x05,
|
||||
serial_ata = 0x06,
|
||||
serial_attached_scsi = 0x07,
|
||||
non_volatile_memory = 0x08,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.scsi_bus => "SCSI Bus Controller",
|
||||
.ide => "IDE Controller",
|
||||
.floppy => "Floppy Disk Controller",
|
||||
.ipi_bus => "IPI Bus Controller",
|
||||
.raid => "RAID Controller",
|
||||
.ata => "ATA Controller",
|
||||
.serial_ata => "Serial ATA Controller",
|
||||
.serial_attached_scsi => "Serial Attached SCSI Controller",
|
||||
.non_volatile_memory => "Non-Volatile Memory Controller",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const serial_ata = struct {
|
||||
pub const ProgIf = enum(u8) {
|
||||
vendor_specific = 0x00,
|
||||
ahci = 0x01,
|
||||
serial_storage_bus = 0x02,
|
||||
|
||||
pub fn name(self: ProgIf) []const u8 {
|
||||
return switch (self) {
|
||||
.vendor_specific => "Vendor Specific Interface",
|
||||
.ahci => "AHCI 1.0",
|
||||
.serial_storage_bus => "Serial Storage Bus",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const non_volatile_memory = struct {
|
||||
pub const ProgIf = enum(u8) {
|
||||
nvmhci = 0x01,
|
||||
nvm_express = 0x02,
|
||||
|
||||
pub fn name(self: ProgIf) []const u8 {
|
||||
return switch (self) {
|
||||
.nvmhci => "NVMHCI",
|
||||
.nvm_express => "NVM Express",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
pub const network = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
ethernet = 0x00,
|
||||
token_ring = 0x01,
|
||||
fddi = 0x02,
|
||||
atm = 0x03,
|
||||
isdn = 0x04,
|
||||
picmg_multi_computing = 0x06,
|
||||
infiniband = 0x07,
|
||||
fabric = 0x08,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.ethernet => "Ethernet Controller",
|
||||
.token_ring => "Token Ring Controller",
|
||||
.fddi => "FDDI Controller",
|
||||
.atm => "ATM Controller",
|
||||
.isdn => "ISDN Controller",
|
||||
.picmg_multi_computing => "PICMG 2.14 Multi Computing Controller",
|
||||
.infiniband => "Infiniband Controller",
|
||||
.fabric => "Fabric Controller",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const display = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
vga_compatible = 0x00,
|
||||
xga = 0x01,
|
||||
three_dimensional = 0x02,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.vga_compatible => "VGA Compatible Controller",
|
||||
.xga => "XGA Controller",
|
||||
.three_dimensional => "3D Controller (Not VGA-Compatible)",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const vga_compatible = struct {
|
||||
pub const ProgIf = enum(u8) {
|
||||
vga = 0x00,
|
||||
compatible_8514 = 0x01,
|
||||
|
||||
pub fn name(self: ProgIf) []const u8 {
|
||||
return switch (self) {
|
||||
.vga => "VGA Controller",
|
||||
.compatible_8514 => "8514-Compatible Controller",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
pub const multimedia = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
video = 0x00,
|
||||
audio = 0x01,
|
||||
telephony = 0x02,
|
||||
audio_device = 0x03,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.video => "Multimedia Video Controller",
|
||||
.audio => "Multimedia Audio Controller",
|
||||
.telephony => "Computer Telephony Device",
|
||||
.audio_device => "Audio Device",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const memory = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
ram = 0x00,
|
||||
flash = 0x01,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.ram => "RAM Controller",
|
||||
.flash => "Flash Controller",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const bridge = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
host = 0x00,
|
||||
isa = 0x01,
|
||||
eisa = 0x02,
|
||||
mca = 0x03,
|
||||
pci_to_pci = 0x04,
|
||||
pcmcia = 0x05,
|
||||
nubus = 0x06,
|
||||
cardbus = 0x07,
|
||||
raceway = 0x08,
|
||||
pci_to_pci_semi_transparent = 0x09,
|
||||
infiniband_to_pci = 0x0A,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.host => "Host Bridge",
|
||||
.isa => "ISA Bridge",
|
||||
.eisa => "EISA Bridge",
|
||||
.mca => "MCA Bridge",
|
||||
.pci_to_pci => "PCI-to-PCI Bridge",
|
||||
.pcmcia => "PCMCIA Bridge",
|
||||
.nubus => "NuBus Bridge",
|
||||
.cardbus => "CardBus Bridge",
|
||||
.raceway => "RACEway Bridge",
|
||||
.pci_to_pci_semi_transparent => "PCI-to-PCI Bridge (Semi-Transparent)",
|
||||
.infiniband_to_pci => "InfiniBand-to-PCI Host Bridge",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const pci_to_pci = struct {
|
||||
pub const ProgIf = enum(u8) {
|
||||
normal_decode = 0x00,
|
||||
subtractive_decode = 0x01,
|
||||
|
||||
pub fn name(self: ProgIf) []const u8 {
|
||||
return switch (self) {
|
||||
.normal_decode => "Normal Decode",
|
||||
.subtractive_decode => "Subtractive Decode",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
pub const simple_communication = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
serial = 0x00,
|
||||
parallel = 0x01,
|
||||
multiport_serial = 0x02,
|
||||
modem = 0x03,
|
||||
gpib = 0x04,
|
||||
smart_card = 0x05,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.serial => "Serial Controller",
|
||||
.parallel => "Parallel Controller",
|
||||
.multiport_serial => "Multiport Serial Controller",
|
||||
.modem => "Modem",
|
||||
.gpib => "IEEE 488.1/2 (GPIB) Controller",
|
||||
.smart_card => "Smart Card Controller",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const serial = struct {
|
||||
pub const ProgIf = enum(u8) {
|
||||
compatible_8250 = 0x00,
|
||||
compatible_16450 = 0x01,
|
||||
compatible_16550 = 0x02,
|
||||
compatible_16650 = 0x03,
|
||||
compatible_16750 = 0x04,
|
||||
compatible_16850 = 0x05,
|
||||
compatible_16950 = 0x06,
|
||||
|
||||
pub fn name(self: ProgIf) []const u8 {
|
||||
return switch (self) {
|
||||
.compatible_8250 => "8250-Compatible (Generic XT)",
|
||||
.compatible_16450 => "16450-Compatible",
|
||||
.compatible_16550 => "16550-Compatible",
|
||||
.compatible_16650 => "16650-Compatible",
|
||||
.compatible_16750 => "16750-Compatible",
|
||||
.compatible_16850 => "16850-Compatible",
|
||||
.compatible_16950 => "16950-Compatible",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
pub const base_system_peripheral = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
pic = 0x00,
|
||||
dma = 0x01,
|
||||
timer = 0x02,
|
||||
rtc = 0x03,
|
||||
pci_hot_plug = 0x04,
|
||||
sd_host = 0x05,
|
||||
iommu = 0x06,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.pic => "PIC",
|
||||
.dma => "DMA Controller",
|
||||
.timer => "Timer",
|
||||
.rtc => "RTC Controller",
|
||||
.pci_hot_plug => "PCI Hot-Plug Controller",
|
||||
.sd_host => "SD Host Controller",
|
||||
.iommu => "IOMMU",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const input_device = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
keyboard = 0x00,
|
||||
digitizer_pen = 0x01,
|
||||
mouse = 0x02,
|
||||
scanner = 0x03,
|
||||
gameport = 0x04,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.keyboard => "Keyboard Controller",
|
||||
.digitizer_pen => "Digitizer Pen",
|
||||
.mouse => "Mouse Controller",
|
||||
.scanner => "Scanner Controller",
|
||||
.gameport => "Gameport Controller",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const serial_bus = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
firewire = 0x00,
|
||||
access_bus = 0x01,
|
||||
ssa = 0x02,
|
||||
usb = 0x03,
|
||||
fibre_channel = 0x04,
|
||||
smbus = 0x05,
|
||||
infiniband = 0x06,
|
||||
ipmi = 0x07,
|
||||
sercos = 0x08,
|
||||
canbus = 0x09,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.firewire => "FireWire (IEEE 1394) Controller",
|
||||
.access_bus => "ACCESS Bus Controller",
|
||||
.ssa => "SSA",
|
||||
.usb => "USB Controller",
|
||||
.fibre_channel => "Fibre Channel",
|
||||
.smbus => "SMBus Controller",
|
||||
.infiniband => "InfiniBand Controller",
|
||||
.ipmi => "IPMI Interface",
|
||||
.sercos => "SERCOS Interface (IEC 61491)",
|
||||
.canbus => "CANbus Controller",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const usb = struct {
|
||||
pub const ProgIf = enum(u8) {
|
||||
uhci = 0x00,
|
||||
ohci = 0x10,
|
||||
ehci = 0x20,
|
||||
xhci = 0x30,
|
||||
unspecified = 0x80,
|
||||
device = 0xFE,
|
||||
|
||||
pub fn name(self: ProgIf) []const u8 {
|
||||
return switch (self) {
|
||||
.uhci => "UHCI Controller",
|
||||
.ohci => "OHCI Controller",
|
||||
.ehci => "EHCI (USB2) Controller",
|
||||
.xhci => "XHCI (USB3) Controller",
|
||||
.unspecified => "Unspecified",
|
||||
.device => "USB Device (not a host controller)",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
pub const wireless = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
irda = 0x00,
|
||||
consumer_ir = 0x01,
|
||||
rf = 0x10,
|
||||
bluetooth = 0x11,
|
||||
broadband = 0x12,
|
||||
ethernet_802_1a = 0x20,
|
||||
ethernet_802_1b = 0x21,
|
||||
|
||||
pub fn name(self: SubClass) []const u8 {
|
||||
return switch (self) {
|
||||
.irda => "iRDA Compatible Controller",
|
||||
.consumer_ir => "Consumer IR Controller",
|
||||
.rf => "RF Controller",
|
||||
.bluetooth => "Bluetooth Controller",
|
||||
.broadband => "Broadband Controller",
|
||||
.ethernet_802_1a => "Ethernet Controller (802.1a)",
|
||||
.ethernet_802_1b => "Ethernet Controller (802.1b)",
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// --- Raw-byte decoding (what a function reports in its header) -------------------------
|
||||
|
||||
/// The name of an exhaustive class-code enum member, or null if `value` is not one — the
|
||||
/// bridge from a raw config byte to a named taxonomy above.
|
||||
fn enumName(comptime Enum: type, value: u8) ?[]const u8 {
|
||||
return (std.enums.fromInt(Enum, value) orelse return null).name();
|
||||
}
|
||||
|
||||
/// Name of the base class (byte 0x0B), e.g. `0x06` -> "Bridge".
|
||||
pub fn className(base: u8) []const u8 {
|
||||
return @as(BaseClass, @enumFromInt(base)).name();
|
||||
}
|
||||
|
||||
/// Name of the subclass within its base class, e.g. `(0x06, 0x01)` -> "ISA Bridge".
|
||||
/// Subclass `0x80` is "Other" by PCI convention; anything unlisted is "Unknown".
|
||||
pub fn subclassName(base: u8, subclass: u8) []const u8 {
|
||||
const named: ?[]const u8 = switch (@as(BaseClass, @enumFromInt(base))) {
|
||||
.mass_storage => enumName(mass_storage.SubClass, subclass),
|
||||
.network => enumName(network.SubClass, subclass),
|
||||
.display => enumName(display.SubClass, subclass),
|
||||
.multimedia => enumName(multimedia.SubClass, subclass),
|
||||
.memory => enumName(memory.SubClass, subclass),
|
||||
.bridge => enumName(bridge.SubClass, subclass),
|
||||
.simple_communication => enumName(simple_communication.SubClass, subclass),
|
||||
.base_system_peripheral => enumName(base_system_peripheral.SubClass, subclass),
|
||||
.input_device => enumName(input_device.SubClass, subclass),
|
||||
.serial_bus => enumName(serial_bus.SubClass, subclass),
|
||||
.wireless => enumName(wireless.SubClass, subclass),
|
||||
else => null,
|
||||
};
|
||||
return named orelse defaultSubclass(subclass);
|
||||
}
|
||||
|
||||
fn defaultSubclass(subclass: u8) []const u8 {
|
||||
return if (subclass == 0x80) "Other" else "Unknown";
|
||||
}
|
||||
|
||||
/// Name of the programming interface, for the subclasses that define standard ones
|
||||
/// (IDE modes, SATA/AHCI, NVMe, PCI-bridge decode, UART generation, USB host type).
|
||||
/// Returns "" when the prog-IF carries no standard meaning for this class/subclass —
|
||||
/// callers just print the hex byte in that case.
|
||||
pub fn progIfName(base: u8, subclass: u8, prog_if: u8) []const u8 {
|
||||
const named: ?[]const u8 = switch (@as(BaseClass, @enumFromInt(base))) {
|
||||
.mass_storage => switch (std.enums.fromInt(mass_storage.SubClass, subclass) orelse return "") {
|
||||
.serial_ata => enumName(mass_storage.serial_ata.ProgIf, prog_if),
|
||||
.non_volatile_memory => enumName(mass_storage.non_volatile_memory.ProgIf, prog_if),
|
||||
else => null,
|
||||
},
|
||||
.display => switch (std.enums.fromInt(display.SubClass, subclass) orelse return "") {
|
||||
.vga_compatible => enumName(display.vga_compatible.ProgIf, prog_if),
|
||||
else => null,
|
||||
},
|
||||
.bridge => switch (std.enums.fromInt(bridge.SubClass, subclass) orelse return "") {
|
||||
.pci_to_pci => enumName(bridge.pci_to_pci.ProgIf, prog_if),
|
||||
else => null,
|
||||
},
|
||||
.simple_communication => switch (std.enums.fromInt(simple_communication.SubClass, subclass) orelse return "") {
|
||||
.serial => enumName(simple_communication.serial.ProgIf, prog_if),
|
||||
else => null,
|
||||
},
|
||||
.serial_bus => switch (std.enums.fromInt(serial_bus.SubClass, subclass) orelse return "") {
|
||||
.usb => enumName(serial_bus.usb.ProgIf, prog_if),
|
||||
else => null,
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
return named orelse "";
|
||||
}
|
||||
|
||||
test "decodes the common class codes" {
|
||||
const eq = std.testing.expectEqualStrings;
|
||||
|
||||
const isa = ClassCode.unpack(0x06_01_00);
|
||||
try std.testing.expectEqual(@as(u8, 0x06), isa.base);
|
||||
try std.testing.expectEqual(@as(u8, 0x01), isa.subclass);
|
||||
try eq("Bridge", className(isa.base));
|
||||
try eq("ISA Bridge", subclassName(isa.base, isa.subclass));
|
||||
|
||||
const ahci = ClassCode.unpack(0x01_06_01);
|
||||
try eq("Mass Storage Controller", className(ahci.base));
|
||||
try eq("Serial ATA Controller", subclassName(ahci.base, ahci.subclass));
|
||||
try eq("AHCI 1.0", progIfName(ahci.base, ahci.subclass, ahci.prog_if));
|
||||
|
||||
const xhci = ClassCode.unpack(0x0C_03_30);
|
||||
try eq("Serial Bus Controller", className(xhci.base));
|
||||
try eq("USB Controller", subclassName(xhci.base, xhci.subclass));
|
||||
try eq("XHCI (USB3) Controller", progIfName(xhci.base, xhci.subclass, xhci.prog_if));
|
||||
}
|
||||
|
||||
test "unlisted codes fall back without a wrong name" {
|
||||
const eq = std.testing.expectEqualStrings;
|
||||
try eq("Unknown", className(0x77)); // no such base class
|
||||
try eq("Other", subclassName(0x01, 0x80)); // 0x80 is the PCI "Other" convention
|
||||
try eq("Unknown", subclassName(0x01, 0x7A)); // unlisted mass-storage subclass
|
||||
try eq("", progIfName(0x01, 0x06, 0x7F)); // no standard SATA prog-IF for 0x7F
|
||||
try eq("", progIfName(0x02, 0x00, 0x00)); // class with no prog-IF taxonomy at all
|
||||
}
|
||||
|
||||
test "named parts pack to the raw triple" {
|
||||
const xhci = ClassCode{
|
||||
.base = @intFromEnum(BaseClass.serial_bus),
|
||||
.subclass = @intFromEnum(serial_bus.SubClass.usb),
|
||||
.prog_if = @intFromEnum(serial_bus.usb.ProgIf.xhci),
|
||||
};
|
||||
try std.testing.expectEqual(@as(u24, 0x0C_03_30), xhci.pack());
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
//! compile-time choice.
|
||||
|
||||
const std = @import("std");
|
||||
const danos = @import("danos");
|
||||
const boot_handoff = @import("boot-handoff");
|
||||
const device_model = @import("device-model.zig");
|
||||
const acpi = @import("acpi.zig");
|
||||
const power = @import("power.zig");
|
||||
@@ -40,6 +40,13 @@ pub fn platformInformation() PlatformInformation {
|
||||
}
|
||||
|
||||
/// AML parse integrity/diagnostics (namespace node count, bytes consumed).
|
||||
/// The number of Device objects in the kernel's own AML namespace, or 0 if the
|
||||
/// parse produced none — the `acpi-parse` test compares the ring-3 service's
|
||||
/// count against this.
|
||||
pub fn amlDeviceCount() usize {
|
||||
return acpi.amlDeviceCount();
|
||||
}
|
||||
|
||||
pub fn amlStats() AmlStats {
|
||||
return acpi.aml_stats;
|
||||
}
|
||||
@@ -65,14 +72,15 @@ pub fn cpusDropped() usize {
|
||||
/// ACPI registers); pass the architecture implementation. Errors leave nothing to clean up
|
||||
/// beyond the tree's own allocations.
|
||||
pub fn discover(
|
||||
boot_information: *const danos.BootInformation,
|
||||
boot_information: *const boot_handoff.BootInformation,
|
||||
allocator: std.mem.Allocator,
|
||||
hal: Hal,
|
||||
) !DeviceTree {
|
||||
var device_tree = try DeviceTree.init(allocator);
|
||||
|
||||
if (boot_information.acpi_rsdp != 0) {
|
||||
try acpi.discover(boot_information.acpi_rsdp, &device_tree, hal);
|
||||
const memory_regions = @as([*]const boot_handoff.MemoryRegion, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.memory_map.regions)))[0..boot_information.memory_map.len];
|
||||
try acpi.discover(boot_information.acpi_rsdp, memory_regions, &device_tree, hal);
|
||||
} else {
|
||||
// No ACPI RSDP. A device-tree boot would parse its blob here; today that
|
||||
// path is a stub, so this reports the machine described itself no way we
|
||||
|
||||
@@ -0,0 +1,945 @@
|
||||
//! USB device-framework wire ABI: the set-up packets, standard requests, and standard
|
||||
//! descriptors every USB device speaks over its default control pipe, as defined by chapter 9
|
||||
//! of the USB 2.0 specification (see https://wiki.osdev.org/Universal_Serial_Bus). Pure data
|
||||
//! definitions — no hardware access — shared by the host-controller bus drivers (which build
|
||||
//! the requests) and anything that parses what devices return (device naming, driver
|
||||
//! matching, configuration). The structs mirror the wire byte-for-byte: multi-byte fields are
|
||||
//! little-endian and align(1), so a descriptor can be bit-cast straight out of a transfer
|
||||
//! buffer at any offset, and bitmap bytes are packed structs so no caller ever needs a magic
|
||||
//! mask. Class, subclass, and protocol code tables live in usb-ids.zig.
|
||||
|
||||
pub const DeviceState = enum(u8) {
|
||||
// Immediately after the USB device is attached to the USB system, it is in this state.
|
||||
// The USB specifications do not define the state of a USB device that is detached from
|
||||
// a USB system.
|
||||
attached,
|
||||
// A device is in this state after it has both been attached to the bus, and the VBUS line is
|
||||
// applied to the device (the host controller drives the VBUS at +5V, however this is only
|
||||
// particularly important for hardware developers). In this state, the device must not respond
|
||||
// to any bus transactions. The USB specification recognizes three potential scenarios with
|
||||
// respect to how a device draws power:
|
||||
// - Self-Powered Devices draw power from an external power source (e.g, a USB printer plugs
|
||||
// into the wall as well as a USB port). Although the device may be considered
|
||||
// technically "powered" even before attachment to the USB, it is still only considered
|
||||
// powered after the VBUS line is applied to the device.
|
||||
// - Bus-Powered Devices draw power solely from the USB up to 100mA.
|
||||
// - Self- or Bus-Powered Devices may draw power from either the bus or an external power
|
||||
// source, depending on the configuration. These devices may change power source at any
|
||||
// time. If a device is currently self-powered and requires more than 100mA of power, but
|
||||
// switches to being bus-powered, then the device must return to the Address state.
|
||||
powered,
|
||||
// A device in the powered state enters the default state after receiving a bus reset. In this
|
||||
// state, the device is addressable at the default, reserved address of 0. At this point, the
|
||||
// device is operating at the correct speed. The host is expected to allow 10 milliseconds
|
||||
// before expecting the device to respond to data transfers after reset.
|
||||
default,
|
||||
// A device enters this state after the host assigns it an address via the default control pipe,
|
||||
// which is always accessible whether the device's address has been set or not.
|
||||
address,
|
||||
// A device is in this state after the host examines its possible configurations and selects
|
||||
// one. All endpoint's data toggle bits are initialized to zero when a device enters this state.
|
||||
configured,
|
||||
// When no traffic is observed on the bus for a period of 1 millisecond, a USB device enters
|
||||
// this state, characterized by its low power consumption. The device's address and
|
||||
// configuration settings are maintained while suspended. A device exits the suspended state as
|
||||
// soon as it begins seeing bus activity again. The host is expected to allow 10 milliseconds
|
||||
// before expecting the device to respond to data transfers after resume.
|
||||
suspended,
|
||||
};
|
||||
|
||||
pub const RequestCode = enum(u8) {
|
||||
get_status = 0,
|
||||
clear_feature = 1,
|
||||
set_feature = 3,
|
||||
set_address = 5,
|
||||
get_descriptor = 6,
|
||||
set_descriptor = 7,
|
||||
get_configuration = 8,
|
||||
set_configuration = 9,
|
||||
get_interface = 10,
|
||||
set_interface = 11,
|
||||
sync_frame = 12,
|
||||
// Non-exhaustive: class-specific requests (HID, mass storage) reuse this byte
|
||||
// field with codes from their own class's namespace — see the class-request
|
||||
// constructors below. Some class codes numerically coincide with a standard
|
||||
// one; the wire byte is what matters, and the constructors set it explicitly.
|
||||
_,
|
||||
};
|
||||
|
||||
// Direction of an endpoint, from the host's point of view
|
||||
pub const EndpointDirection = enum(u1) {
|
||||
out = 0,
|
||||
in = 1,
|
||||
};
|
||||
|
||||
// Identifier newtypes: distinct wire-sized types for values that identify something on the
|
||||
// device rather than count something. Each is a non-exhaustive enum whose values originate
|
||||
// in the descriptors below and flow, still typed, into the standard request constructors —
|
||||
// so an interface number can never be passed where a configuration value is expected.
|
||||
|
||||
// The bus address of a device, assigned by the host with SET_ADDRESS. Addresses are 7 bits
|
||||
// wide.
|
||||
pub const DeviceAddress = enum(u7) {
|
||||
// The default address every device answers at after a reset, until SET_ADDRESS
|
||||
// completes
|
||||
default = 0,
|
||||
_,
|
||||
};
|
||||
|
||||
// Identifies a configuration; from ConfigurationDescriptor.configuration_value.
|
||||
pub const ConfigurationValue = enum(u8) {
|
||||
// Not configured: returned by GET_CONFIGURATION while the device is in the address
|
||||
// state, and passed to SET_CONFIGURATION to return a configured device to the address
|
||||
// state
|
||||
none = 0,
|
||||
_,
|
||||
};
|
||||
|
||||
// Identifies an interface within a configuration; from
|
||||
// InterfaceDescriptor.interface_number.
|
||||
pub const InterfaceNumber = enum(u8) { _ };
|
||||
|
||||
// Selects between the alternate settings of one interface; from
|
||||
// InterfaceDescriptor.alternate_setting.
|
||||
pub const AlternateSetting = enum(u8) {
|
||||
// The default setting of an interface
|
||||
default = 0,
|
||||
_,
|
||||
};
|
||||
|
||||
// The number of an endpoint within a device, 4 bits wide. The direction bit carried
|
||||
// alongside it tells the two endpoints sharing a number apart.
|
||||
pub const EndpointNumber = enum(u4) {
|
||||
// Endpoint zero: the default control pipe every device provides
|
||||
default_control = 0,
|
||||
_,
|
||||
};
|
||||
|
||||
// Index of a STRING descriptor, stored in descriptors that reference a string and passed to
|
||||
// GET_DESCRIPTOR to read it.
|
||||
pub const StringIndex = enum(u8) {
|
||||
// The device has no string descriptor for this field
|
||||
none = 0,
|
||||
_,
|
||||
};
|
||||
|
||||
// Characteristics of a device request (the bmRequestType field of a set-up packet). Fields are
|
||||
// declared least-significant first: recipient occupies bits 4...0, kind bits 6...5, and
|
||||
// direction bit 7.
|
||||
pub const RequestType = packed struct(u8) {
|
||||
// The recipient of the request (values 4...31 are reserved)
|
||||
recipient: Recipient,
|
||||
// The type of the request
|
||||
kind: Kind,
|
||||
// Data transfer direction. The value of this bit is ignored when length is zero.
|
||||
direction: Direction,
|
||||
|
||||
pub const Recipient = enum(u5) {
|
||||
device = 0,
|
||||
interface = 1,
|
||||
endpoint = 2,
|
||||
other = 3,
|
||||
};
|
||||
|
||||
pub const Kind = enum(u2) {
|
||||
standard = 0,
|
||||
class = 1,
|
||||
vendor = 2,
|
||||
reserved = 3,
|
||||
};
|
||||
|
||||
pub const Direction = enum(u1) {
|
||||
host_to_device = 0,
|
||||
device_to_host = 1,
|
||||
};
|
||||
};
|
||||
|
||||
pub const Request = extern struct {
|
||||
// Characteristics of the request
|
||||
request_type: RequestType,
|
||||
// Specific request
|
||||
request_code: RequestCode,
|
||||
// Word-sized field that may (or may not) serve as a parameter to the request, depending
|
||||
// on the specific request. For GET_DESCRIPTOR and SET_DESCRIPTOR, bit-cast a
|
||||
// DescriptorValue into this field.
|
||||
value: u16 align(1),
|
||||
// Word-sized field that may (or may not) serve as a parameter to the request, depending
|
||||
// on the specific request. Typically this field holds an index or an offset value. When
|
||||
// request_type specifies an endpoint or an interface as the recipient, bit-cast an
|
||||
// EndpointIndex or an InterfaceIndex into this field.
|
||||
index: u16 align(1),
|
||||
// Number of bytes to transfer if there is a DATA stage.
|
||||
// - If this field is non-zero, and request_type indicates a transfer from
|
||||
// device-to-host, then the device must never return more than length bytes of data.
|
||||
// However, a device may return less.
|
||||
// - If this field is non-zero, and request_type indicates a transfer from
|
||||
// host-to-device, then the host must send exactly length bytes of data. If the host
|
||||
// sends more than length bytes, the behavior of the device is undefined.
|
||||
length: u16 align(1),
|
||||
|
||||
// The format of the index field when request_type specifies an endpoint as the
|
||||
// recipient. The host should always set the direction bit to zero (but the device
|
||||
// should accept either value) when the endpoint is part of a control pipe.
|
||||
pub const EndpointIndex = packed struct(u16) {
|
||||
// Endpoint number
|
||||
number: EndpointNumber,
|
||||
// Reserved (reset to zero)
|
||||
reserved: u3 = 0,
|
||||
// Selects the OUT or the IN endpoint with the specified endpoint number
|
||||
direction: EndpointDirection,
|
||||
// Reserved (reset to zero)
|
||||
reserved_high: u8 = 0,
|
||||
};
|
||||
|
||||
// The format of the index field when request_type specifies an interface as the
|
||||
// recipient.
|
||||
pub const InterfaceIndex = packed struct(u16) {
|
||||
// Interface number
|
||||
number: u8,
|
||||
// Reserved (reset to zero)
|
||||
reserved: u8 = 0,
|
||||
};
|
||||
|
||||
// The format of the value field of GET_DESCRIPTOR and SET_DESCRIPTOR requests: the
|
||||
// descriptor type in the high byte, and the descriptor index in the low byte. The index
|
||||
// is used to select a specific descriptor (only for CONFIGURATION and STRING
|
||||
// descriptors) when several descriptors of that type are implemented by a device.
|
||||
pub const DescriptorValue = packed struct(u16) {
|
||||
// Descriptor index
|
||||
index: u8 = 0,
|
||||
// Descriptor type
|
||||
kind: DescriptorType,
|
||||
};
|
||||
};
|
||||
|
||||
// Feature selectors, used as the value field of CLEAR_FEATURE and SET_FEATURE requests. The
|
||||
// comment on each value notes the recipient the selector applies to.
|
||||
pub const FeatureSelector = enum(u16) {
|
||||
// Halts an endpoint (recipient: endpoint)
|
||||
endpoint_halt = 0,
|
||||
// Enables or disables the device's remote wakeup capability (recipient: device)
|
||||
device_remote_wakeup = 1,
|
||||
// Puts a hi-speed device into a test mode, selected by a TestMode value in the high
|
||||
// byte of the index field (recipient: device)
|
||||
test_mode = 2,
|
||||
};
|
||||
|
||||
// Test mode selectors, passed in the high byte of the index field of a SET_FEATURE request
|
||||
// with the test_mode feature selector. Values 06h...3Fh are reserved for standard test
|
||||
// selectors and C0h...FFh for vendor-specific test modes; all other unlisted values are
|
||||
// reserved.
|
||||
pub const TestMode = enum(u8) {
|
||||
test_j = 0x01,
|
||||
test_k = 0x02,
|
||||
test_se0_nak = 0x03,
|
||||
test_packet = 0x04,
|
||||
test_force_enable = 0x05,
|
||||
_,
|
||||
};
|
||||
|
||||
// The two bytes returned by a GET_STATUS request directed at a device. Fields are declared
|
||||
// least-significant first.
|
||||
pub const DeviceStatus = packed struct(u16) {
|
||||
// Whether the device is currently self-powered (as opposed to bus-powered). This bit
|
||||
// cannot be changed with the SET_FEATURE or CLEAR_FEATURE requests.
|
||||
self_powered: bool,
|
||||
// Whether the device is currently enabled to request remote wakeup. Changed with the
|
||||
// SET_FEATURE and CLEAR_FEATURE requests using the device_remote_wakeup feature
|
||||
// selector.
|
||||
remote_wakeup: bool,
|
||||
// Reserved (reset to zero)
|
||||
reserved: u14,
|
||||
};
|
||||
|
||||
// The two bytes returned by a GET_STATUS request directed at an endpoint. (A GET_STATUS
|
||||
// request directed at an interface returns two bytes that are entirely reserved.)
|
||||
pub const EndpointStatus = packed struct(u16) {
|
||||
// Whether the endpoint is currently halted. Set with the SET_FEATURE request using the
|
||||
// endpoint_halt feature selector, and cleared with CLEAR_FEATURE.
|
||||
halted: bool,
|
||||
// Reserved (reset to zero)
|
||||
reserved: u15,
|
||||
};
|
||||
|
||||
// A target for the standard requests that may be directed at the device, an interface, or
|
||||
// an endpoint.
|
||||
pub const Target = union(enum) {
|
||||
device,
|
||||
interface: InterfaceNumber,
|
||||
endpoint: Request.EndpointIndex,
|
||||
|
||||
fn recipient(target: Target) RequestType.Recipient {
|
||||
return switch (target) {
|
||||
.device => .device,
|
||||
.interface => .interface,
|
||||
.endpoint => .endpoint,
|
||||
};
|
||||
}
|
||||
|
||||
fn index(target: Target) u16 {
|
||||
return switch (target) {
|
||||
.device => 0,
|
||||
.interface => |number| @intFromEnum(number),
|
||||
.endpoint => |endpoint| @bitCast(endpoint),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Constructors for the standard device requests, one per RequestCode. Each returns a
|
||||
// ready-to-send set-up packet with the request_type, value, index, and length fields the
|
||||
// specification prescribes for that request.
|
||||
|
||||
// Reads the status of the given target: bit-cast the two bytes the device returns into a
|
||||
// DeviceStatus or an EndpointStatus. (The two bytes returned for an interface are entirely
|
||||
// reserved.)
|
||||
pub fn getStatus(target: Target) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = target.recipient(),
|
||||
.kind = .standard,
|
||||
.direction = .device_to_host,
|
||||
},
|
||||
.request_code = .get_status,
|
||||
.value = 0,
|
||||
.index = target.index(),
|
||||
.length = 2,
|
||||
};
|
||||
}
|
||||
|
||||
// Clears or disables the given feature. A device cannot be taken out of a test mode with
|
||||
// this request; test_mode is only cleared by cycling power.
|
||||
pub fn clearFeature(feature: FeatureSelector, target: Target) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = target.recipient(),
|
||||
.kind = .standard,
|
||||
.direction = .host_to_device,
|
||||
},
|
||||
.request_code = .clear_feature,
|
||||
.value = @intFromEnum(feature),
|
||||
.index = target.index(),
|
||||
.length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Sets or enables the given feature. For the test_mode feature selector, use setTestMode
|
||||
// instead: the test selector rides in the high byte of the index field.
|
||||
pub fn setFeature(feature: FeatureSelector, target: Target) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = target.recipient(),
|
||||
.kind = .standard,
|
||||
.direction = .host_to_device,
|
||||
},
|
||||
.request_code = .set_feature,
|
||||
.value = @intFromEnum(feature),
|
||||
.index = target.index(),
|
||||
.length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Puts a hi-speed device into the given test mode: a SET_FEATURE request with the test_mode
|
||||
// feature selector and the test selector in the high byte of the index field.
|
||||
pub fn setTestMode(mode: TestMode) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = .device,
|
||||
.kind = .standard,
|
||||
.direction = .host_to_device,
|
||||
},
|
||||
.request_code = .set_feature,
|
||||
.value = @intFromEnum(FeatureSelector.test_mode),
|
||||
.index = @as(u16, @intFromEnum(mode)) << 8,
|
||||
.length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Assigns the device its bus address, moving it from the default state to the address
|
||||
// state. The device does not answer at the new address until the status stage of this
|
||||
// request completes.
|
||||
pub fn setAddress(address: DeviceAddress) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = .device,
|
||||
.kind = .standard,
|
||||
.direction = .host_to_device,
|
||||
},
|
||||
.request_code = .set_address,
|
||||
.value = @intFromEnum(address),
|
||||
.index = 0,
|
||||
.length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Reads a descriptor from the device.
|
||||
// - descriptor_index selects among descriptors of the same type, and is only used for
|
||||
// configuration and string descriptors.
|
||||
// - language_id selects the language of a string descriptor, and is zero otherwise.
|
||||
// - length is the number of bytes to read; a device never returns more than length bytes,
|
||||
// but may return less if the descriptor is shorter.
|
||||
pub fn getDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = .device,
|
||||
.kind = .standard,
|
||||
.direction = .device_to_host,
|
||||
},
|
||||
.request_code = .get_descriptor,
|
||||
.value = @bitCast(Request.DescriptorValue{ .index = descriptor_index, .kind = kind }),
|
||||
.index = language_id,
|
||||
.length = length,
|
||||
};
|
||||
}
|
||||
|
||||
// Updates an existing descriptor or adds a new one (optional; many devices do not support
|
||||
// this request). The parameters mirror getDescriptor; the descriptor itself is sent in the
|
||||
// DATA stage.
|
||||
pub fn setDescriptor(kind: DescriptorType, descriptor_index: u8, language_id: u16, length: u16) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = .device,
|
||||
.kind = .standard,
|
||||
.direction = .host_to_device,
|
||||
},
|
||||
.request_code = .set_descriptor,
|
||||
.value = @bitCast(Request.DescriptorValue{ .index = descriptor_index, .kind = kind }),
|
||||
.index = language_id,
|
||||
.length = length,
|
||||
};
|
||||
}
|
||||
|
||||
// Reads the currently active configuration: @enumFromInt the byte the device returns into a
|
||||
// ConfigurationValue, which is none while the device is not configured.
|
||||
pub fn getConfiguration() Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = .device,
|
||||
.kind = .standard,
|
||||
.direction = .device_to_host,
|
||||
},
|
||||
.request_code = .get_configuration,
|
||||
.value = 0,
|
||||
.index = 0,
|
||||
.length = 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Selects the configuration with the given configuration_value (from
|
||||
// ConfigurationDescriptor.configuration_value), moving the device from the address state to
|
||||
// the configured state. Selecting none returns the device to the address state.
|
||||
pub fn setConfiguration(configuration_value: ConfigurationValue) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = .device,
|
||||
.kind = .standard,
|
||||
.direction = .host_to_device,
|
||||
},
|
||||
.request_code = .set_configuration,
|
||||
.value = @intFromEnum(configuration_value),
|
||||
.index = 0,
|
||||
.length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Reads the alternate setting currently selected for the given interface: @enumFromInt the
|
||||
// byte the device returns into an AlternateSetting.
|
||||
pub fn getInterface(interface: InterfaceNumber) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = .interface,
|
||||
.kind = .standard,
|
||||
.direction = .device_to_host,
|
||||
},
|
||||
.request_code = .get_interface,
|
||||
.value = 0,
|
||||
.index = @intFromEnum(interface),
|
||||
.length = 1,
|
||||
};
|
||||
}
|
||||
|
||||
// Selects an alternate setting (from InterfaceDescriptor.alternate_setting) for the given
|
||||
// interface.
|
||||
pub fn setInterface(interface: InterfaceNumber, alternate_setting: AlternateSetting) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = .interface,
|
||||
.kind = .standard,
|
||||
.direction = .host_to_device,
|
||||
},
|
||||
.request_code = .set_interface,
|
||||
.value = @intFromEnum(alternate_setting),
|
||||
.index = @intFromEnum(interface),
|
||||
.length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Reads the two-byte number of the frame in which the given isochronous endpoint's
|
||||
// repeating pattern of transfers begins.
|
||||
pub fn syncFrame(endpoint: Request.EndpointIndex) Request {
|
||||
return .{
|
||||
.request_type = .{
|
||||
.recipient = .endpoint,
|
||||
.kind = .standard,
|
||||
.direction = .device_to_host,
|
||||
},
|
||||
.request_code = .sync_frame,
|
||||
.value = 0,
|
||||
.index = @bitCast(endpoint),
|
||||
.length = 2,
|
||||
};
|
||||
}
|
||||
|
||||
// Class-specific requests. These carry a `kind = .class` request_type and a
|
||||
// request_code from the interface's class namespace (not the standard
|
||||
// RequestCode set above); the code is written into the same byte field, which
|
||||
// is why RequestCode is non-exhaustive. Each is directed at an interface, whose
|
||||
// number rides in the index field.
|
||||
|
||||
// The HID class request codes (USB HID 1.11 §7.2). Only the ones danos issues
|
||||
// are named; the field on the wire is the raw byte.
|
||||
pub const HidRequestCode = enum(u8) {
|
||||
get_report = 0x01,
|
||||
get_idle = 0x02,
|
||||
get_protocol = 0x03,
|
||||
set_report = 0x09,
|
||||
set_idle = 0x0A,
|
||||
set_protocol = 0x0B,
|
||||
};
|
||||
|
||||
// The two protocols a boot-capable HID device can run (USB HID 1.11 §7.2.5).
|
||||
// A driver selects `boot` for the simplified fixed-format boot report, usable
|
||||
// before a full report-descriptor parser exists.
|
||||
pub const HidProtocol = enum(u8) {
|
||||
boot = 0,
|
||||
report = 1,
|
||||
};
|
||||
|
||||
// SET_PROTOCOL: choose the boot or report protocol on a HID interface.
|
||||
pub fn setProtocol(interface: InterfaceNumber, protocol: HidProtocol) Request {
|
||||
return .{
|
||||
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device },
|
||||
.request_code = @enumFromInt(@intFromEnum(HidRequestCode.set_protocol)),
|
||||
.value = @intFromEnum(protocol),
|
||||
.index = @intFromEnum(interface),
|
||||
.length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// SET_IDLE: bound a HID interface's report rate. `duration` is in 4 ms units
|
||||
// (0 means report only on change); `report_id` selects a report (0 = all).
|
||||
pub fn setIdle(interface: InterfaceNumber, duration: u8, report_id: u8) Request {
|
||||
return .{
|
||||
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device },
|
||||
.request_code = @enumFromInt(@intFromEnum(HidRequestCode.set_idle)),
|
||||
.value = (@as(u16, duration) << 8) | report_id,
|
||||
.index = @intFromEnum(interface),
|
||||
.length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Bulk-Only Mass Storage Reset (USB MSC BOT §3.1): ready a mass-storage
|
||||
// interface for the next Command Block Wrapper after a protocol error.
|
||||
pub fn bulkOnlyMassStorageReset(interface: InterfaceNumber) Request {
|
||||
return .{
|
||||
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .host_to_device },
|
||||
.request_code = @enumFromInt(0xFF),
|
||||
.value = 0,
|
||||
.index = @intFromEnum(interface),
|
||||
.length = 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Get Max LUN (USB MSC BOT §3.2): read the highest logical unit number the
|
||||
// device supports (0 for a single-LUN flash drive). One byte is returned.
|
||||
pub fn getMaxLun(interface: InterfaceNumber) Request {
|
||||
return .{
|
||||
.request_type = .{ .recipient = .interface, .kind = .class, .direction = .device_to_host },
|
||||
.request_code = @enumFromInt(0xFE),
|
||||
.value = 0,
|
||||
.index = @intFromEnum(interface),
|
||||
.length = 1,
|
||||
};
|
||||
}
|
||||
|
||||
pub const DescriptorType = enum(u8) {
|
||||
device = 1,
|
||||
configuration = 2,
|
||||
string = 3,
|
||||
interface = 4,
|
||||
endpoint = 5,
|
||||
device_qualifier = 6,
|
||||
other_speed_configuration = 7,
|
||||
interface_power = 8,
|
||||
_,
|
||||
};
|
||||
|
||||
pub const DeviceDescriptor = extern struct {
|
||||
// Size of this descriptor in bytes
|
||||
length: u8,
|
||||
// DEVICE Descriptor Type
|
||||
descriptor_type: DescriptorType,
|
||||
// USB Specification Release Number in Binary-Coded Decimal (i.e, 2.10 is expressed as 210h).
|
||||
// Identifies the release of the USB Specification with with the device and its
|
||||
// descriptors are compliant.
|
||||
bcd_usb: u16 align(1),
|
||||
// Class code (assigned by the USB-IF)
|
||||
// - This field is reset to zero if each interface within a configuration specifies its own
|
||||
// class information and the various interfaces operate independently.
|
||||
// - A value of FFh in this field indicates the device class is vendor-specific.
|
||||
device_class: u8,
|
||||
// Subclass Code (assigned by the USB-IF)
|
||||
// - The subclass code of a device is qualified by the class code of that device.
|
||||
// - If device_class is reset to zero, then this field must also be reset to zero.
|
||||
// - When device_class is not set to FFh, then all values for this field are reserved for
|
||||
// assignment by the USB-IF.
|
||||
device_subclass: u8,
|
||||
// Protocol code (assigned by the USB-IF)
|
||||
// - The protocol code of a device is qualified by both the class and subclass codes of
|
||||
// that device.
|
||||
// - A value of 00h in this field means that the device may specify class-specific
|
||||
// protocols on an interface basis, though this is not a requirement.
|
||||
// - If this field is set to FFh, then the device uses a vendor-specific protocol.
|
||||
device_protocol: u8,
|
||||
// Maximum packet size for endpoint zero (8, 16, 32, or 64 are the only valid options)
|
||||
max_packet_size_0: u8,
|
||||
// Vendor ID (assigned by the USB-IF)
|
||||
vendor_id: u16 align(1),
|
||||
// Product ID (assigned by the USB-IF)
|
||||
product_id: u16 align(1),
|
||||
// Device release number in binary-coded decimal
|
||||
bcd_device: u16 align(1),
|
||||
// Index of STRING descriptor describing manufacturer
|
||||
manufacturer_index: StringIndex,
|
||||
// Index of STRING descriptor describing product
|
||||
product_index: StringIndex,
|
||||
// Index of STRING descriptor describing the device's serial number
|
||||
serial_number_index: StringIndex,
|
||||
// Number of possible configurations
|
||||
configuration_count: u8,
|
||||
};
|
||||
|
||||
pub const DeviceQualifierDescriptor = extern struct {
|
||||
// Size of this descriptor in bytes
|
||||
length: u8,
|
||||
// DEVICE_QUALIFIER Descriptor Type
|
||||
descriptor_type: DescriptorType,
|
||||
// USB Specification Release Number in Binary-Coded Decimal (i.e, 2.00 is expressed as 200h).
|
||||
// Identifies the release of the USB Specification with with the device and its
|
||||
// descriptors are compliant. This field must be at least 0200h.
|
||||
bcd_usb: u16 align(1),
|
||||
// Class code (assigned by the USB-IF)
|
||||
device_class: u8,
|
||||
// Subclass Code (assigned by the USB-IF)
|
||||
device_subclass: u8,
|
||||
// Protocol code (assigned by the USB-IF)
|
||||
device_protocol: u8,
|
||||
// Maximum packet size for endpoint zero (8, 16, 32, or 64 are the only valid options)
|
||||
max_packet_size_0: u8,
|
||||
// Number of possible configurations
|
||||
configuration_count: u8,
|
||||
// Reserved for future uses, must be zero.
|
||||
reserved: u8,
|
||||
};
|
||||
|
||||
pub const ConfigurationDescriptor = extern struct {
|
||||
// Size of this descriptor in bytes
|
||||
length: u8,
|
||||
// CONFIGURATION Descriptor Type
|
||||
descriptor_type: DescriptorType,
|
||||
// The total combined length in bytes of all the descriptors returned with the request for
|
||||
// this CONFIGURATION descriptor (including CONFIGURATION, INTERFACE, ENDPOINT, class- and
|
||||
// vendor-specific descriptors).
|
||||
total_length: u16 align(1),
|
||||
// Number of interfaces supported by this configuration
|
||||
interface_count: u8,
|
||||
// Value which when used as an argument in the SET_CONFIGURATION request, causes the device
|
||||
// to assume the configuration described by this descriptor.
|
||||
configuration_value: ConfigurationValue,
|
||||
// Index of STRING descriptor describing this configuration.
|
||||
configuration_index: StringIndex,
|
||||
// Configuration Characteristics
|
||||
attributes: Attributes,
|
||||
// Maximum power consumption of this device from the bus when fully operational and using
|
||||
// this configuration. Expressed in units of 2mA (i.e., a value of 50 in this field
|
||||
// indicates 100mA).
|
||||
// - A device reports with the attributes field whether the configuration is bus- or
|
||||
// self-powered, but the device status (retrieved with a GET_STATUS request) reports
|
||||
// whether the device is currently self-powered.
|
||||
// - If a device is disconnected from an external power source, it may not draw more
|
||||
// power from the bus than specified in this field.
|
||||
max_power: u8,
|
||||
|
||||
// Configuration characteristics. Fields are declared least-significant first.
|
||||
pub const Attributes = packed struct(u8) {
|
||||
// Reserved, reset to zero (D4...0)
|
||||
reserved: u5,
|
||||
// Whether Remote Wakeup is supported by this configuration (D5)
|
||||
remote_wakeup: bool,
|
||||
// Self-Powered (D6)
|
||||
// - false: Device runs on power supplied by the bus
|
||||
// - true: Device provides a local power source; if max_power is non-zero, the
|
||||
// device also may use bus power.
|
||||
self_powered: bool,
|
||||
// Reserved, must be set to one for historical reasons (D7)
|
||||
reserved_one: u1,
|
||||
};
|
||||
};
|
||||
|
||||
// This descriptor describes the configuration of a high-speed device if it were operating at
|
||||
// its alternative speed. The structure of the OTHER_SPEED_CONFIGURATION is identical to that
|
||||
// of the CONFIGURATION descriptor; the only difference is that the descriptor_type field
|
||||
// reflects that the descriptor is an OTHER_SPEED_CONFIGURATION descriptor.
|
||||
pub const OtherSpeedConfigurationDescriptor = ConfigurationDescriptor;
|
||||
|
||||
pub const InterfaceDescriptor = extern struct {
|
||||
// Size of this descriptor in bytes
|
||||
length: u8,
|
||||
// INTERFACE Descriptor Type
|
||||
descriptor_type: DescriptorType,
|
||||
// Number of this interface. Zero-based value which identifies the index of this interface
|
||||
// in the array of interfaces supported within a configuration.
|
||||
interface_number: InterfaceNumber,
|
||||
// Value used to select the alternate settings described by this INTERFACE descriptor for
|
||||
// the interface with the interface_number in the previous field. This value is zero if
|
||||
// this descriptor describes the default settings for a particular interface.
|
||||
alternate_setting: AlternateSetting,
|
||||
// Number of endpoints used by this interface, not including endpoint zero.
|
||||
endpoint_count: u8,
|
||||
// Class code (assigned by the USB-IF)
|
||||
// - A value of zero here is reserved for future standardization.
|
||||
// - If this value is FFh, the interface class is vendor-specific.
|
||||
// - All other values are reserved for assignment by the USB-IF.
|
||||
interface_class: u8,
|
||||
// Subclass code (assigned by the USB-IF)
|
||||
// - The subclass code in this field is qualified by the value of the interface_class
|
||||
// field.
|
||||
// - If interface_class is reset to zero, then this field must also be reset to zero.
|
||||
// - If interface_class is not set to the value of FFh, then all values of this field are
|
||||
// reserved for assignment by the USB-IF.
|
||||
interface_subclass: u8,
|
||||
// Protocol code (assigned by the USB-IF)
|
||||
// - The protocol code in this field is qualified by the values of the interface_class
|
||||
// and interface_subclass fields.
|
||||
// - If an interface supports class-specific requests, then this field identifies the
|
||||
// protocols that the device uses as defined by the specifications of the device class.
|
||||
// - If this field is reset to zero, then the device does not use a class-specific
|
||||
// protocol on this interface.
|
||||
// - If this field is set to FFh, then the device uses a vendor-specific protocol on
|
||||
// this interface.
|
||||
interface_protocol: u8,
|
||||
// Index of STRING descriptor describing this interface
|
||||
interface_index: StringIndex,
|
||||
};
|
||||
|
||||
pub const EndpointDescriptor = extern struct {
|
||||
// Size of this descriptor in bytes
|
||||
length: u8,
|
||||
// ENDPOINT Descriptor Type
|
||||
descriptor_type: DescriptorType,
|
||||
// The address of the endpoint on the USB device described by this descriptor
|
||||
endpoint_address: Address,
|
||||
// The endpoint's attributes
|
||||
attributes: Attributes,
|
||||
// Maximum packet size that this endpoint is capable of sending or receiving. For
|
||||
// isochronous endpoints, this value is used to reserve bus time; the pipe, however, may
|
||||
// not always use all of the reserved bus time.
|
||||
max_packet_size: MaxPacketSize align(1),
|
||||
// Interval for polling a device during a data transfer, expressed in units of microframes
|
||||
// for high-speed devices, and frames for low- and full-speed devices. The exact meaning of
|
||||
// the value in this field depends on the endpoint type and the operating speed of the
|
||||
// device:
|
||||
// - Full- and High-speed isochronous endpoints, and high-speed interrupt endpoints:
|
||||
// This field must be in the range from 1 to 16, and is used to calculate the period
|
||||
// as 2^(interval - 1). That is, a value of 4 calculates to 2^(4 - 1) = 2^3 = 8.
|
||||
// - Full- and Low-speed interrupt endpoints: This field must be in the range from
|
||||
// 1 to 255.
|
||||
// - High-speed bulk and control OUT endpoints: This field must be in the range from
|
||||
// 0 to 255, and specifies the maximum NAK rate of the endpoint. A value of zero
|
||||
// indicates that the endpoint never NAKs; other values indicate at most 1 NAK each
|
||||
// interval number of microframes.
|
||||
interval: u8,
|
||||
|
||||
// The address of an endpoint. Fields are declared least-significant first.
|
||||
pub const Address = packed struct(u8) {
|
||||
// Endpoint Number (D3...0)
|
||||
number: EndpointNumber,
|
||||
// Reserved, reset to zero (D6...4)
|
||||
reserved: u3,
|
||||
// Direction, ignored for control endpoints (D7)
|
||||
direction: EndpointDirection,
|
||||
};
|
||||
|
||||
// An endpoint's attributes. Fields are declared least-significant first.
|
||||
pub const Attributes = packed struct(u8) {
|
||||
// Transfer Type (D1...0)
|
||||
transfer_type: TransferType,
|
||||
// Synchronization Type; isochronous endpoints only, reserved and reset to zero for
|
||||
// other endpoint types (D3...2)
|
||||
synchronization: Synchronization,
|
||||
// Usage Type; isochronous endpoints only, reserved and reset to zero for other
|
||||
// endpoints (D5...4)
|
||||
usage: Usage,
|
||||
// Reserved, reset to zero (D7...6)
|
||||
reserved: u2,
|
||||
};
|
||||
|
||||
pub const TransferType = enum(u2) {
|
||||
control = 0,
|
||||
isochronous = 1,
|
||||
bulk = 2,
|
||||
interrupt = 3,
|
||||
};
|
||||
|
||||
pub const Synchronization = enum(u2) {
|
||||
none = 0,
|
||||
asynchronous = 1,
|
||||
adaptive = 2,
|
||||
synchronous = 3,
|
||||
};
|
||||
|
||||
pub const Usage = enum(u2) {
|
||||
data = 0,
|
||||
feedback = 1,
|
||||
implicit_feedback_data = 2,
|
||||
_,
|
||||
};
|
||||
|
||||
// The maximum packet size of an endpoint. Fields are declared least-significant first.
|
||||
pub const MaxPacketSize = packed struct(u16) {
|
||||
// Maximum packet size in bytes (bits 10...0)
|
||||
size: u11,
|
||||
// Number of additional transaction opportunities per microframe, for high-speed
|
||||
// isochronous and interrupt endpoints; reserved and reset to zero for other
|
||||
// endpoints (bits 12...11)
|
||||
additional_transactions: AdditionalTransactions,
|
||||
// Reserved, must be reset to zero (bits 15...13)
|
||||
reserved: u3,
|
||||
};
|
||||
|
||||
pub const AdditionalTransactions = enum(u2) {
|
||||
// None (1 transaction per microframe)
|
||||
none = 0,
|
||||
// 1 additional (2 transactions per microframe)
|
||||
one = 1,
|
||||
// 2 additional (3 transactions per microframe)
|
||||
two = 2,
|
||||
_,
|
||||
};
|
||||
};
|
||||
|
||||
// A STRING descriptor at index zero returns the list of LANGID codes supported by the
|
||||
// device; all other indices return a Unicode string. Both forms start with this two-byte
|
||||
// header, followed by the variable-length payload:
|
||||
// - index 0: an array of two-byte LANGID codes (wLangID[0] through wLangID[x])
|
||||
// - other indices: a Unicode string of N bytes
|
||||
pub const StringDescriptor = extern struct {
|
||||
// Size of this descriptor in bytes
|
||||
length: u8,
|
||||
// STRING Descriptor Type
|
||||
descriptor_type: DescriptorType,
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
test "wire sizes and offsets match the specification" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
|
||||
try expectEqual(8, @sizeOf(Request));
|
||||
try expectEqual(18, @sizeOf(DeviceDescriptor));
|
||||
try expectEqual(10, @sizeOf(DeviceQualifierDescriptor));
|
||||
try expectEqual(9, @sizeOf(ConfigurationDescriptor));
|
||||
try expectEqual(9, @sizeOf(InterfaceDescriptor));
|
||||
try expectEqual(7, @sizeOf(EndpointDescriptor));
|
||||
try expectEqual(2, @sizeOf(StringDescriptor));
|
||||
|
||||
try expectEqual(2, @offsetOf(DeviceDescriptor, "bcd_usb"));
|
||||
try expectEqual(8, @offsetOf(DeviceDescriptor, "vendor_id"));
|
||||
try expectEqual(17, @offsetOf(DeviceDescriptor, "configuration_count"));
|
||||
try expectEqual(2, @offsetOf(ConfigurationDescriptor, "total_length"));
|
||||
try expectEqual(4, @offsetOf(EndpointDescriptor, "max_packet_size"));
|
||||
}
|
||||
|
||||
test "bitmap packings match the specification" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const expect = std.testing.expect;
|
||||
|
||||
// bmRequestType for GET_DESCRIPTOR: device-to-host | standard | device = 80h
|
||||
const request_type = RequestType{
|
||||
.recipient = .device,
|
||||
.kind = .standard,
|
||||
.direction = .device_to_host,
|
||||
};
|
||||
try expectEqual(0x80, @as(u8, @bitCast(request_type)));
|
||||
|
||||
// wValue for GET_DESCRIPTOR(CONFIGURATION, index 0) = 0200h
|
||||
const descriptor_value = Request.DescriptorValue{ .kind = .configuration };
|
||||
try expectEqual(0x0200, @as(u16, @bitCast(descriptor_value)));
|
||||
|
||||
// wIndex for the IN endpoint 1 = 0081h
|
||||
const endpoint_index = Request.EndpointIndex{ .number = @enumFromInt(1), .direction = .in };
|
||||
try expectEqual(0x0081, @as(u16, @bitCast(endpoint_index)));
|
||||
|
||||
// Endpoint address 81h = IN endpoint 1
|
||||
const address: EndpointDescriptor.Address = @bitCast(@as(u8, 0x81));
|
||||
try expectEqual(1, @intFromEnum(address.number));
|
||||
try expectEqual(.in, address.direction);
|
||||
|
||||
// Endpoint attributes 03h = interrupt transfer
|
||||
const attributes: EndpointDescriptor.Attributes = @bitCast(@as(u8, 0x03));
|
||||
try expectEqual(.interrupt, attributes.transfer_type);
|
||||
|
||||
// wMaxPacketSize 0008h = 8 bytes, no additional transactions
|
||||
const max_packet_size: EndpointDescriptor.MaxPacketSize = @bitCast(@as(u16, 0x0008));
|
||||
try expectEqual(8, max_packet_size.size);
|
||||
try expectEqual(.none, max_packet_size.additional_transactions);
|
||||
|
||||
// Configuration attributes C0h = self-powered, with the historical D7 bit set
|
||||
const configuration_attributes: ConfigurationDescriptor.Attributes = @bitCast(@as(u8, 0xC0));
|
||||
try expect(configuration_attributes.self_powered);
|
||||
try expect(!configuration_attributes.remote_wakeup);
|
||||
try expectEqual(1, configuration_attributes.reserved_one);
|
||||
|
||||
// GET_STATUS words: device 0001h = self-powered; endpoint 0001h = halted
|
||||
const device_status: DeviceStatus = @bitCast(@as(u16, 0x0001));
|
||||
try expect(device_status.self_powered and !device_status.remote_wakeup);
|
||||
const endpoint_status: EndpointStatus = @bitCast(@as(u16, 0x0001));
|
||||
try expect(endpoint_status.halted);
|
||||
|
||||
// DescriptorType is non-exhaustive: class-specific values (HID = 21h) pass through
|
||||
const hid_type: DescriptorType = @enumFromInt(0x21);
|
||||
try expectEqual(0x21, @intFromEnum(hid_type));
|
||||
try expect(hid_type != .device);
|
||||
}
|
||||
|
||||
pub fn expectRequestBytes(request: Request, expected: [8]u8) !void {
|
||||
try std.testing.expectEqualSlices(u8, &expected, std.mem.asBytes(&request));
|
||||
}
|
||||
|
||||
test "standard request constructors encode the specification's set-up packets" {
|
||||
try expectRequestBytes(getStatus(.device), .{ 0x80, 0, 0, 0, 0, 0, 2, 0 });
|
||||
try expectRequestBytes(getStatus(.{ .interface = @enumFromInt(3) }), .{ 0x81, 0, 0, 0, 3, 0, 2, 0 });
|
||||
try expectRequestBytes(getStatus(.{ .endpoint = .{ .number = @enumFromInt(2), .direction = .in } }), .{ 0x82, 0, 0, 0, 0x82, 0, 2, 0 });
|
||||
try expectRequestBytes(clearFeature(.endpoint_halt, .{ .endpoint = .{ .number = @enumFromInt(1), .direction = .out } }), .{ 0x02, 1, 0, 0, 0x01, 0, 0, 0 });
|
||||
try expectRequestBytes(setFeature(.device_remote_wakeup, .device), .{ 0x00, 3, 1, 0, 0, 0, 0, 0 });
|
||||
try expectRequestBytes(setTestMode(.test_packet), .{ 0x00, 3, 2, 0, 0, 0x04, 0, 0 });
|
||||
try expectRequestBytes(setAddress(@enumFromInt(5)), .{ 0x00, 5, 5, 0, 0, 0, 0, 0 });
|
||||
try expectRequestBytes(getDescriptor(.device, 0, 0, 18), .{ 0x80, 6, 0, 1, 0, 0, 18, 0 });
|
||||
try expectRequestBytes(getDescriptor(.string, 2, 0x0409, 255), .{ 0x80, 6, 2, 3, 0x09, 0x04, 255, 0 });
|
||||
try expectRequestBytes(setDescriptor(.string, 2, 0x0409, 16), .{ 0x00, 7, 2, 3, 0x09, 0x04, 16, 0 });
|
||||
try expectRequestBytes(getConfiguration(), .{ 0x80, 8, 0, 0, 0, 0, 1, 0 });
|
||||
try expectRequestBytes(setConfiguration(@enumFromInt(1)), .{ 0x00, 9, 1, 0, 0, 0, 0, 0 });
|
||||
try expectRequestBytes(getInterface(@enumFromInt(2)), .{ 0x81, 10, 0, 0, 2, 0, 1, 0 });
|
||||
try expectRequestBytes(setInterface(@enumFromInt(2), @enumFromInt(1)), .{ 0x01, 11, 1, 0, 2, 0, 0, 0 });
|
||||
try expectRequestBytes(syncFrame(.{ .number = @enumFromInt(3), .direction = .in }), .{ 0x82, 12, 0, 0, 0x83, 0, 2, 0 });
|
||||
}
|
||||
|
||||
test "class request constructors encode the specification's set-up packets" {
|
||||
// bmRequestType for a host-to-device class request to an interface = 0x21;
|
||||
// device-to-host = 0xA1. The request_code byte is the class code, not a
|
||||
// standard one — SET_PROTOCOL 0x0B, SET_IDLE 0x0A, BOT reset 0xFF, Max LUN 0xFE.
|
||||
try expectRequestBytes(setProtocol(@enumFromInt(0), .boot), .{ 0x21, 0x0B, 0, 0, 0, 0, 0, 0 });
|
||||
try expectRequestBytes(setProtocol(@enumFromInt(1), .report), .{ 0x21, 0x0B, 1, 0, 1, 0, 0, 0 });
|
||||
try expectRequestBytes(setIdle(@enumFromInt(1), 0, 0), .{ 0x21, 0x0A, 0, 0, 1, 0, 0, 0 });
|
||||
try expectRequestBytes(bulkOnlyMassStorageReset(@enumFromInt(0)), .{ 0x21, 0xFF, 0, 0, 0, 0, 0, 0 });
|
||||
try expectRequestBytes(getMaxLun(@enumFromInt(0)), .{ 0xA1, 0xFE, 0, 0, 0, 0, 1, 0 });
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
//! USB class-code decoding: turn the (class, subclass, protocol) triple a USB device or
|
||||
//! interface reports in its descriptors into typed values. The device descriptor carries one
|
||||
//! triple for the whole device, and each interface descriptor carries its own; a class code
|
||||
//! of zero at the device level defers entirely to the interfaces. Subclass and protocol
|
||||
//! codes are qualified by the class code — the same value means different things under
|
||||
//! different classes — so there is no single SubClass or Protocol enum: each class with
|
||||
//! spec-defined codes gets its own namespace below. Pure reference data (from the USB-IF
|
||||
//! defined class codes; see https://www.usb.org/defined-class-codes) — no hardware access —
|
||||
//! so it is shared by kernel discovery and any user-space tool (device naming, driver
|
||||
//! matching).
|
||||
|
||||
// Base class codes (assigned by the USB-IF). The comment on each value notes where the code
|
||||
// may legally appear: in the device descriptor, in interface descriptors, or both.
|
||||
pub const Class = enum(u8) {
|
||||
// Use class information in the interface descriptors (device descriptor only). Each
|
||||
// interface within a configuration specifies its own class information and the various
|
||||
// interfaces operate independently.
|
||||
per_interface = 0x00,
|
||||
// Audio: speakers, microphones, sound cards (interface)
|
||||
audio = 0x01,
|
||||
// Communications and CDC control: modems, network adapters (both)
|
||||
communications = 0x02,
|
||||
// Human Interface Device: keyboards, mice, game controllers (interface)
|
||||
hid = 0x03,
|
||||
// Physical: force-feedback devices (interface)
|
||||
physical = 0x05,
|
||||
// Image: still-imaging cameras, scanners (interface)
|
||||
image = 0x06,
|
||||
// Printer (interface)
|
||||
printer = 0x07,
|
||||
// Mass storage: flash drives, external disks, card readers (interface)
|
||||
mass_storage = 0x08,
|
||||
// Hub (device descriptor only)
|
||||
hub = 0x09,
|
||||
// CDC-Data: the data interfaces paired with a communications control interface
|
||||
// (interface)
|
||||
cdc_data = 0x0A,
|
||||
// Smart card readers (interface)
|
||||
smart_card = 0x0B,
|
||||
// Content security (interface)
|
||||
content_security = 0x0D,
|
||||
// Video: webcams (interface)
|
||||
video = 0x0E,
|
||||
// Personal healthcare devices (interface)
|
||||
personal_healthcare = 0x0F,
|
||||
// Audio/Video devices (interface)
|
||||
audio_video = 0x10,
|
||||
// Billboard: describes alternate modes a USB Type-C device supports (device descriptor
|
||||
// only)
|
||||
billboard = 0x11,
|
||||
// USB Type-C bridge (interface)
|
||||
type_c_bridge = 0x12,
|
||||
// USB Bulk Display Protocol devices (interface)
|
||||
bulk_display = 0x13,
|
||||
// MCTP over USB protocol endpoint devices (interface)
|
||||
mctp = 0x14,
|
||||
// I3C devices (interface)
|
||||
i3c = 0x3C,
|
||||
// Diagnostic devices (both)
|
||||
diagnostic = 0xDC,
|
||||
// Wireless controllers: Bluetooth adapters (interface)
|
||||
wireless_controller = 0xE0,
|
||||
// Miscellaneous (both)
|
||||
miscellaneous = 0xEF,
|
||||
// Application-specific: firmware upgrade, IrDA bridges, test and measurement
|
||||
// (interface)
|
||||
application_specific = 0xFE,
|
||||
// Vendor-specific (both)
|
||||
vendor_specific = 0xFF,
|
||||
_,
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.hub. Hubs have no subclass codes; the
|
||||
// protocol distinguishes the hub's transaction-translator arrangement.
|
||||
pub const hub = struct {
|
||||
pub const Protocol = enum(u8) {
|
||||
// Full-speed hub
|
||||
full_speed = 0x00,
|
||||
// Hi-speed hub with a single transaction translator
|
||||
hi_speed_single_tt = 0x01,
|
||||
// Hi-speed hub with multiple transaction translators
|
||||
hi_speed_multi_tt = 0x02,
|
||||
// SuperSpeed hub (USB 3)
|
||||
super_speed = 0x03,
|
||||
_,
|
||||
};
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.hid.
|
||||
pub const hid = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
// No subclass
|
||||
none = 0x00,
|
||||
// Boot interface: the device also supports the simplified boot protocol, usable by
|
||||
// firmware before a full HID report-descriptor parser is available
|
||||
boot = 0x01,
|
||||
_,
|
||||
};
|
||||
|
||||
// Only meaningful when the subclass is boot
|
||||
pub const Protocol = enum(u8) {
|
||||
none = 0x00,
|
||||
keyboard = 0x01,
|
||||
mouse = 0x02,
|
||||
_,
|
||||
};
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.mass_storage. The subclass identifies the
|
||||
// command set the device understands; the protocol identifies the transport used to carry
|
||||
// commands, data, and status over the bus.
|
||||
pub const mass_storage = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
// SCSI command set not reported; de facto, treat as scsi
|
||||
not_reported = 0x00,
|
||||
// Reduced Block Commands: typically flash devices
|
||||
rbc = 0x01,
|
||||
// MMC-5 (ATAPI): CD and DVD drives
|
||||
atapi = 0x02,
|
||||
// QIC-157 tape drives (obsolete)
|
||||
qic_157 = 0x03,
|
||||
// UFI: floppy disk drives
|
||||
ufi = 0x04,
|
||||
// SFF-8070i (obsolete)
|
||||
sff_8070i = 0x05,
|
||||
// Transparent SCSI command set: the common case for flash drives and disks
|
||||
scsi = 0x06,
|
||||
// LSD FS: negotiated access to large storage devices
|
||||
lsd_fs = 0x07,
|
||||
// IEEE 1667
|
||||
ieee_1667 = 0x08,
|
||||
// Vendor-specific
|
||||
vendor_specific = 0xFF,
|
||||
_,
|
||||
};
|
||||
|
||||
pub const Protocol = enum(u8) {
|
||||
// Control/Bulk/Interrupt with command completion interrupt
|
||||
cbi_completion_interrupt = 0x00,
|
||||
// Control/Bulk/Interrupt without command completion interrupt
|
||||
cbi = 0x01,
|
||||
// Bulk-only transport: the common case for flash drives and disks
|
||||
bulk_only = 0x50,
|
||||
// USB attached SCSI
|
||||
uas = 0x62,
|
||||
// Vendor-specific
|
||||
vendor_specific = 0xFF,
|
||||
_,
|
||||
};
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.communications (CDC). The protocol codes
|
||||
// are model-specific; the useful invariant is the subclass, which selects the control model
|
||||
// the interface implements.
|
||||
pub const communications = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
// Direct line control model
|
||||
direct_line = 0x01,
|
||||
// Abstract control model: USB modems and serial adapters
|
||||
abstract_control = 0x02,
|
||||
// Telephone control model
|
||||
telephone = 0x03,
|
||||
// Multi-channel control model
|
||||
multi_channel = 0x04,
|
||||
// CAPI control model
|
||||
capi = 0x05,
|
||||
// Ethernet networking control model
|
||||
ethernet = 0x06,
|
||||
// ATM networking control model
|
||||
atm = 0x07,
|
||||
// Wireless handset control model
|
||||
wireless_handset = 0x08,
|
||||
// Device management
|
||||
device_management = 0x09,
|
||||
// Mobile direct line model
|
||||
mobile_direct_line = 0x0A,
|
||||
// OBEX
|
||||
obex = 0x0B,
|
||||
// Ethernet emulation model
|
||||
ethernet_emulation = 0x0C,
|
||||
// Network control model
|
||||
network_control = 0x0D,
|
||||
_,
|
||||
};
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.wireless_controller.
|
||||
pub const wireless_controller = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
// Radio frequency controllers
|
||||
radio_frequency = 0x01,
|
||||
_,
|
||||
};
|
||||
|
||||
// Only meaningful when the subclass is radio_frequency
|
||||
pub const Protocol = enum(u8) {
|
||||
// Bluetooth programming interface
|
||||
bluetooth = 0x01,
|
||||
// Ultra-wideband radio control
|
||||
ultra_wideband = 0x02,
|
||||
// Remote NDIS
|
||||
remote_ndis = 0x03,
|
||||
// Bluetooth AMP controller
|
||||
bluetooth_amp = 0x04,
|
||||
_,
|
||||
};
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.miscellaneous.
|
||||
pub const miscellaneous = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
// Common class
|
||||
common = 0x02,
|
||||
_,
|
||||
};
|
||||
|
||||
// Only meaningful when the subclass is common
|
||||
pub const Protocol = enum(u8) {
|
||||
// Interface association descriptor: at the device level, announces that the
|
||||
// configuration groups interfaces into functions with IADs
|
||||
interface_association = 0x01,
|
||||
_,
|
||||
};
|
||||
};
|
||||
|
||||
// Subclass and protocol codes qualified by Class.application_specific.
|
||||
pub const application_specific = struct {
|
||||
pub const SubClass = enum(u8) {
|
||||
// Device firmware upgrade
|
||||
firmware_upgrade = 0x01,
|
||||
// IrDA bridge
|
||||
irda_bridge = 0x02,
|
||||
// Test and measurement
|
||||
test_and_measurement = 0x03,
|
||||
_,
|
||||
};
|
||||
};
|
||||
|
||||
/// Pack a (class, subclass, protocol) triple into one 0xCCSSPP value — the
|
||||
/// bus-native identity a USB bus driver reports in `ChildAdded.identity` and the
|
||||
/// device manager matches on (the USB analog of a packed PCI class code). Mirrors
|
||||
/// `pci_class.ClassCode.pack`, so both sides build/decode the identical u64.
|
||||
pub fn packTriple(class: u8, subclass: u8, protocol: u8) u64 {
|
||||
return (@as(u64, class) << 16) | (@as(u64, subclass) << 8) | protocol;
|
||||
}
|
||||
|
||||
/// The inverse of `packTriple`.
|
||||
pub fn unpackTriple(triple: u64) struct { class: u8, subclass: u8, protocol: u8 } {
|
||||
return .{
|
||||
.class = @truncate(triple >> 16),
|
||||
.subclass = @truncate(triple >> 8),
|
||||
.protocol = @truncate(triple),
|
||||
};
|
||||
}
|
||||
|
||||
test "class codes match the USB-IF assignments" {
|
||||
const std = @import("std");
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
|
||||
try expectEqual(0x03, @intFromEnum(Class.hid));
|
||||
try expectEqual(0x09, @intFromEnum(Class.hub));
|
||||
try expectEqual(0xFF, @intFromEnum(Class.vendor_specific));
|
||||
|
||||
// A typical flash drive: mass storage, transparent SCSI, bulk-only transport.
|
||||
try expectEqual(0x06, @intFromEnum(mass_storage.SubClass.scsi));
|
||||
try expectEqual(0x50, @intFromEnum(mass_storage.Protocol.bulk_only));
|
||||
|
||||
// A boot keyboard: HID, boot subclass, keyboard protocol.
|
||||
try expectEqual(0x01, @intFromEnum(hid.SubClass.boot));
|
||||
try expectEqual(0x01, @intFromEnum(hid.Protocol.keyboard));
|
||||
|
||||
// Class codes are non-exhaustive: unlisted values pass through undamaged.
|
||||
const unknown: Class = @enumFromInt(0x42);
|
||||
try expectEqual(0x42, @intFromEnum(unknown));
|
||||
|
||||
_ = hub.Protocol.hi_speed_multi_tt;
|
||||
_ = communications.SubClass.abstract_control;
|
||||
_ = wireless_controller.Protocol.bluetooth;
|
||||
_ = miscellaneous.Protocol.interface_association;
|
||||
_ = application_specific.SubClass.firmware_upgrade;
|
||||
}
|
||||
|
||||
test "packTriple / unpackTriple round-trip the identity a bus driver reports" {
|
||||
const std = @import("std");
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
|
||||
// A boot keyboard interface: HID / boot / keyboard.
|
||||
const keyboard = packTriple(
|
||||
@intFromEnum(Class.hid),
|
||||
@intFromEnum(hid.SubClass.boot),
|
||||
@intFromEnum(hid.Protocol.keyboard),
|
||||
);
|
||||
try expectEqual(@as(u64, 0x03_01_01), keyboard);
|
||||
|
||||
// A flash drive interface: mass storage / SCSI / bulk-only.
|
||||
const storage = packTriple(
|
||||
@intFromEnum(Class.mass_storage),
|
||||
@intFromEnum(mass_storage.SubClass.scsi),
|
||||
@intFromEnum(mass_storage.Protocol.bulk_only),
|
||||
);
|
||||
try expectEqual(@as(u64, 0x08_06_50), storage);
|
||||
|
||||
const parts = unpackTriple(storage);
|
||||
try expectEqual(@as(u8, 0x08), parts.class);
|
||||
try expectEqual(@as(u8, 0x06), parts.subclass);
|
||||
try expectEqual(@as(u8, 0x50), parts.protocol);
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
//! /sbin/busd — a user-space **bus driver**, and the smallest honest example of one.
|
||||
//!
|
||||
//! A bus driver owns a device that *contains other devices*, enumerates them by some
|
||||
//! bus-specific protocol, and publishes each one into the kernel's device table so a
|
||||
//! class driver can claim it. PCI walks configuration space; USB walks hub descriptors. Here
|
||||
//! the "bus" is the HPET's register block and the "devices" are its comparators, each
|
||||
//! a 0x20-byte window at 0x100 + 0x20*n that can be driven independently.
|
||||
//!
|
||||
//! It's a toy bus, but nothing about the mechanism is: `busd` reads how many children
|
||||
//! exist from the hardware (GENERAL_CAP bits [12:8]), publishes one `DeviceDescriptor` per
|
||||
//! child with a sub-window of its own MMIO plus the shared IRQ, and the kernel checks
|
||||
//! every one of those resources is contained in what `busd` was granted. A comparator
|
||||
//! driver then claims a child and maps only *its* registers — not the whole block.
|
||||
//!
|
||||
//! It also proves the negative: registering a child whose window escapes the parent's
|
||||
//! is refused. Without that check, `device_register` would be a system_call for mapping
|
||||
//! arbitrary physical memory.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const device = runtime.device;
|
||||
|
||||
const register_general_cap = 0x000;
|
||||
|
||||
/// Comparator n's registers: configuration+comparator+FSB route, 0x20 bytes.
|
||||
fn timerWindow(hpet_base: u64, n: u64) device.ResourceDescriptor {
|
||||
return .{
|
||||
.kind = @intFromEnum(device.ResourceKind.memory),
|
||||
.start = hpet_base + 0x100 + 0x20 * n,
|
||||
.len = 0x20,
|
||||
};
|
||||
}
|
||||
|
||||
fn findHpet(buffer: []device.DeviceDescriptor) ?device.DeviceDescriptor {
|
||||
const total = device.enumerate(buffer);
|
||||
const n = @min(total, buffer.len);
|
||||
for (buffer[0..n]) |d| {
|
||||
if (d.class != @intFromEnum(device.DeviceClass.timer)) continue;
|
||||
if (d.parent != device.no_parent) continue; // the block, not a comparator child
|
||||
for (0..d.resource_count) |j| {
|
||||
if (d.resources[j].kind == @intFromEnum(device.ResourceKind.memory)) return d;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The parent's MMIO resource, and its IRQ if it has one.
|
||||
fn resourcesOf(d: device.DeviceDescriptor) struct { mmio: device.ResourceDescriptor, irq: ?device.ResourceDescriptor } {
|
||||
var mmio: device.ResourceDescriptor = undefined;
|
||||
var irq: ?device.ResourceDescriptor = null;
|
||||
for (0..d.resource_count) |j| {
|
||||
const r = d.resources[j];
|
||||
if (r.kind == @intFromEnum(device.ResourceKind.memory)) mmio = r;
|
||||
if (r.kind == @intFromEnum(device.ResourceKind.irq)) irq = r;
|
||||
}
|
||||
return .{ .mmio = mmio, .irq = irq };
|
||||
}
|
||||
|
||||
fn firstChildOf(buffer: []device.DeviceDescriptor, total: usize, parent_id: u64) ?u64 {
|
||||
for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||
if (d.parent == parent_id) return d.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("busd: out of memory\n");
|
||||
return;
|
||||
};
|
||||
|
||||
const parent = findHpet(buffer) orelse {
|
||||
_ = runtime.system.write("busd: no HPET\n");
|
||||
return;
|
||||
};
|
||||
const resource = resourcesOf(parent);
|
||||
|
||||
// Claim the bus. Everything below is subdivision of what this claim granted.
|
||||
//
|
||||
// Claims are exclusive, and at a normal boot the kernel spawns every initrd
|
||||
// binary — so hpetd may own the HPET already. That's not an error, it's the
|
||||
// capability model working: exit quietly and leave the device to its owner. The
|
||||
// `bus` test spawns busd alone, so there it wins the claim.
|
||||
if (!device.claim(parent.id)) {
|
||||
_ = runtime.system.write("busd: HPET already claimed by another driver, nothing to do\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Enumerate the bus: ask the hardware how many children it has.
|
||||
const base = device.mmioMap(parent.id, 0) orelse {
|
||||
_ = runtime.system.write("busd: mmio_map failed\n");
|
||||
return;
|
||||
};
|
||||
const cap: *volatile u64 = @ptrFromInt(base + register_general_cap);
|
||||
const n_children = ((cap.* >> 8) & 0x1F) + 1;
|
||||
|
||||
// Publish one child per comparator, each owning only its own window.
|
||||
var published: u64 = 0;
|
||||
var n: u64 = 0;
|
||||
while (n < n_children) : (n += 1) {
|
||||
var child = std.mem.zeroes(device.DeviceDescriptor);
|
||||
child.class = @intFromEnum(device.DeviceClass.timer);
|
||||
child.hid_len = 6;
|
||||
child.hid[0..6].* = "hpet-t".*;
|
||||
child.resource_count = 1;
|
||||
child.resources[0] = timerWindow(resource.mmio.start, n);
|
||||
// Comparators share the block's interrupt line; only one child can bind it,
|
||||
// but all of them may legitimately name it.
|
||||
if (resource.irq) |i| {
|
||||
child.resources[child.resource_count] = i;
|
||||
child.resource_count += 1;
|
||||
}
|
||||
|
||||
if (device.register(parent.id, &child) == null) {
|
||||
_ = runtime.system.write("busd: register failed\n");
|
||||
return;
|
||||
}
|
||||
published += 1;
|
||||
}
|
||||
|
||||
// The negative case. A window one byte past the end of the parent's must be
|
||||
// refused — otherwise device_register would be "map any physical page you like".
|
||||
// Confirm the table did not grow, not merely that the call returned null: null
|
||||
// also means NoSpace/BadParent, so a size check is what actually proves the
|
||||
// *containment* rule fired.
|
||||
const before = device.enumerate(buffer);
|
||||
var rogue = std.mem.zeroes(device.DeviceDescriptor);
|
||||
rogue.class = @intFromEnum(device.DeviceClass.unknown);
|
||||
rogue.resource_count = 1;
|
||||
rogue.resources[0] = .{
|
||||
.kind = @intFromEnum(device.ResourceKind.memory),
|
||||
.start = resource.mmio.start + resource.mmio.len,
|
||||
.len = 0x1000,
|
||||
};
|
||||
if (device.register(parent.id, &rogue) != null) {
|
||||
_ = runtime.system.write("busd: FAIL out-of-window child was accepted\n");
|
||||
return;
|
||||
}
|
||||
if (device.enumerate(buffer) != before) {
|
||||
_ = runtime.system.write("busd: FAIL rogue child leaked into the table\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// And confirm the children came back with the right parent and a *narrower*
|
||||
// window than the bus — read from the table, not from our own memory.
|
||||
const total = device.enumerate(buffer);
|
||||
var seen: u64 = 0;
|
||||
for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||
if (d.parent != parent.id) continue;
|
||||
const w = d.resources[0];
|
||||
if (w.start < resource.mmio.start or w.len >= resource.mmio.len) {
|
||||
_ = runtime.system.write("busd: FAIL child window is not inside the bus\n");
|
||||
return;
|
||||
}
|
||||
seen += 1;
|
||||
}
|
||||
if (seen != published) {
|
||||
_ = runtime.system.write("busd: FAIL child count mismatch\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegation, end to end: claim a child and map *it*. A real class driver would be
|
||||
// a different process; here busd plays both parts, which exercises the same path.
|
||||
// The child's window is 0x20 bytes at parent+0x100, so the register it sees at
|
||||
// offset 0 must be the same timer-0 configuration register the bus sees at 0x100.
|
||||
//
|
||||
// (mmio_map rounds to a page, so the child's mapping physically covers the whole
|
||||
// 4 KiB the HPET lives in — the granularity limit documented in docs/drivers.md.
|
||||
// The *resource* is narrow even though the page isn't.)
|
||||
const child_id = firstChildOf(buffer, device.enumerate(buffer), parent.id) orelse {
|
||||
_ = runtime.system.write("busd: FAIL no child to claim\n");
|
||||
return;
|
||||
};
|
||||
if (!device.claim(child_id)) {
|
||||
_ = runtime.system.write("busd: FAIL could not claim own child\n");
|
||||
return;
|
||||
}
|
||||
const child_base = device.mmioMap(child_id, 0) orelse {
|
||||
_ = runtime.system.write("busd: FAIL child mmio_map refused\n");
|
||||
return;
|
||||
};
|
||||
const via_child: *volatile u64 = @ptrFromInt(child_base);
|
||||
const via_bus: *volatile u64 = @ptrFromInt(base + 0x100);
|
||||
if (via_child.* != via_bus.*) {
|
||||
_ = runtime.system.write("busd: FAIL child window does not alias the bus register\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// A descriptor pointer into an unmapped page must fail the call, not fault the
|
||||
// kernel. Grab a page, free it, and register through the stale address: if the
|
||||
// kernel dereferenced it raw (rather than copying in through the page tables) this
|
||||
// would triple-fault QEMU and the test would time out instead of printing ok.
|
||||
const scratch = runtime.system.mmap(0x1000, runtime.system.PROT_READ | runtime.system.PROT_WRITE);
|
||||
if (!runtime.system.mmapFailed(scratch)) {
|
||||
_ = runtime.system.munmap(scratch, 0x1000);
|
||||
const descriptor: *const device.DeviceDescriptor = @ptrFromInt(scratch);
|
||||
if (device.register(parent.id, descriptor) != null) {
|
||||
_ = runtime.system.write("busd: FAIL register accepted an unmapped descriptor\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_ = runtime.system.write("busd: ok\n");
|
||||
while (true) runtime.system.sleep(1000);
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
//! /sbin/hpetd — a user-space HPET driver. It proves the whole driver model end to
|
||||
//! end: enumerate the device table, find the HPET, claim it, map its registers into
|
||||
//! this ring-3 address space (strong-uncacheable), **bind its interrupt to an IPC
|
||||
//! endpoint**, then sit blocked in `replyWait` until the hardware wakes it.
|
||||
//!
|
||||
//! Nothing here polls. Between interrupts the process is `.blocked` and off every
|
||||
//! scheduler queue; the core runs other work or idles. That is the point of the
|
||||
//! exercise — a driver is a process that sleeps until its device has something to
|
||||
//! say (see docs/drivers.md).
|
||||
//!
|
||||
//! The comparator is configured **level-triggered** on purpose. Edge would be
|
||||
//! simpler, but level is the discipline every real device line needs, and it forces
|
||||
//! the full cycle to be correct:
|
||||
//!
|
||||
//! kernel ISR mask the GSI -> EOI -> notify this endpoint
|
||||
//! hpetd wake, clear GENERAL_INT_STATUS (deasserts the line), re-arm
|
||||
//! hpetd irq_ack -> kernel unmasks the GSI
|
||||
//!
|
||||
//! Clear the status bit *before* acking, or the line is still asserted when the
|
||||
//! kernel unmasks and the I/O APIC redelivers forever.
|
||||
//!
|
||||
//! Register map (HPET spec 1.0a):
|
||||
//! 0x000 GENERAL_CAP [63:32] fs per tick, [12:8] number timers - 1
|
||||
//! 0x010 GENERAL_CONFIGURATION bit0 ENABLE_CNF, bit1 LEG_RT_CNF
|
||||
//! 0x020 GENERAL_INT_STATUS bit n = timer n asserted (write 1 to clear)
|
||||
//! 0x0F0 MAIN_COUNTER
|
||||
//! 0x100 TIMER0_CONFIGURATION bit1 INT_TYPE(1=level) bit2 INT_ENB bit3 TYPE(periodic)
|
||||
//! bits[13:9] INT_ROUTE, [63:32] INT_ROUTE_CAP
|
||||
//! 0x108 TIMER0_COMPARATOR
|
||||
|
||||
const runtime = @import("runtime");
|
||||
const device = runtime.device;
|
||||
const ipc = runtime.ipc;
|
||||
|
||||
const register_general_cap = 0x000;
|
||||
const register_general_configuration = 0x010;
|
||||
const register_int_status = 0x020;
|
||||
const register_main_counter = 0x0F0;
|
||||
const register_timer0_configuration = 0x100;
|
||||
const register_timer0_comparator = 0x108;
|
||||
|
||||
const configuration_enable: u64 = 1 << 0; // GENERAL_CONFIGURATION.ENABLE_CNF
|
||||
const configuration_leg_rt: u64 = 1 << 1; // GENERAL_CONFIGURATION.LEG_RT_CNF
|
||||
const tn_int_type_level: u64 = 1 << 1;
|
||||
const tn_int_enb: u64 = 1 << 2;
|
||||
const tn_type_periodic: u64 = 1 << 3;
|
||||
const tn_route_shift = 9;
|
||||
const tn_route_mask: u64 = 0x1F << tn_route_shift;
|
||||
|
||||
/// Interrupts to observe before declaring victory.
|
||||
const target_ticks = 5;
|
||||
|
||||
fn register(base: usize, off: usize) *volatile u64 {
|
||||
return @ptrFromInt(base + off);
|
||||
}
|
||||
|
||||
/// A timer-class device exposing both an MMIO window and an IRQ: its id, the two
|
||||
/// resource indices, and the GSI discovery chose out of `Tn_INT_ROUTE_CAP`.
|
||||
const Found = struct { device_id: u64, mmio: u64, irq: u64, gsi: u64 };
|
||||
|
||||
fn findHpet(buffer: []device.DeviceDescriptor) ?Found {
|
||||
const total = device.enumerate(buffer);
|
||||
const n = @min(total, buffer.len);
|
||||
for (buffer[0..n]) |d| {
|
||||
if (d.class != @intFromEnum(device.DeviceClass.timer)) continue;
|
||||
// Skip comparator children a bus driver may have published below the block
|
||||
// (see system/drivers/busd/busd.zig) — we want the register block itself.
|
||||
if (d.parent != device.no_parent) continue;
|
||||
var mmio: ?u64 = null;
|
||||
var irq: ?u64 = null;
|
||||
for (0..d.resource_count) |j| {
|
||||
switch (d.resources[j].kind) {
|
||||
@intFromEnum(device.ResourceKind.memory) => mmio = mmio orelse j,
|
||||
@intFromEnum(device.ResourceKind.irq) => irq = irq orelse j,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
if (mmio) |m| if (irq) |i| {
|
||||
return .{ .device_id = d.id, .mmio = m, .irq = i, .gsi = d.resources[i].start };
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
// Enumerate into a heap buffer (too big for the one-page user stack).
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 32) catch {
|
||||
_ = runtime.system.write("hpetd: out of memory\n");
|
||||
return;
|
||||
};
|
||||
|
||||
const hpet = findHpet(buffer) orelse {
|
||||
_ = runtime.system.write("hpetd: no HPET with an IRQ\n");
|
||||
return;
|
||||
};
|
||||
|
||||
if (!device.claim(hpet.device_id)) {
|
||||
_ = runtime.system.write("hpetd: claim failed\n");
|
||||
return;
|
||||
}
|
||||
const base = device.mmioMap(hpet.device_id, hpet.mmio) orelse {
|
||||
_ = runtime.system.write("hpetd: mmio_map failed\n");
|
||||
return;
|
||||
};
|
||||
|
||||
// The GSI discovery picked for us out of Tn_INT_ROUTE_CAP. Program the comparator
|
||||
// to raise exactly this line — the kernel will only bind the one it recorded.
|
||||
const gsi = hpet.gsi;
|
||||
|
||||
const endpoint = ipc.createEndpoint() orelse {
|
||||
_ = runtime.system.write("hpetd: create_endpoint failed\n");
|
||||
return;
|
||||
};
|
||||
|
||||
// --- program the hardware ------------------------------------------------
|
||||
// Counter period, so we can arm the comparator a fixed wall-clock distance out.
|
||||
const femtos_per_tick = register(base, register_general_cap).* >> 32;
|
||||
if (femtos_per_tick == 0) {
|
||||
_ = runtime.system.write("hpetd: bad HPET period\n");
|
||||
return;
|
||||
}
|
||||
const ticks_per_ms = 1_000_000_000_000 / femtos_per_tick;
|
||||
|
||||
// Stop the counter and take the legacy route off while we reconfigure.
|
||||
register(base, register_general_configuration).* &= ~(configuration_enable | configuration_leg_rt);
|
||||
|
||||
// Timer 0: one-shot, level-triggered, routed to our GSI, interrupt enabled.
|
||||
// One-shot (not periodic) sidesteps the HPET's Tn_value_SET accumulator quirk —
|
||||
// we simply re-arm from the driver on each interrupt, which is what a tickless
|
||||
// timer driver does anyway.
|
||||
var t0 = register(base, register_timer0_configuration).*;
|
||||
t0 &= ~(tn_route_mask | tn_type_periodic);
|
||||
t0 |= tn_int_type_level | tn_int_enb | (gsi << tn_route_shift);
|
||||
register(base, register_timer0_configuration).* = t0;
|
||||
|
||||
// Clear any stale assertion, then arm ~100 ms out and start the counter.
|
||||
register(base, register_int_status).* = 1;
|
||||
register(base, register_timer0_comparator).* = register(base, register_main_counter).* + ticks_per_ms * 100;
|
||||
register(base, register_general_configuration).* |= configuration_enable;
|
||||
|
||||
if (!device.irqBind(hpet.device_id, hpet.irq, endpoint)) {
|
||||
_ = runtime.system.write("hpetd: irq_bind failed\n");
|
||||
return;
|
||||
}
|
||||
_ = runtime.system.write("hpetd: bound, sleeping until the hardware speaks\n");
|
||||
|
||||
// --- the driver loop -----------------------------------------------------
|
||||
// Blocked in replyWait. No polling, no spinning: the next line of this function
|
||||
// runs only because an interrupt fired.
|
||||
var receive: [64]u8 = undefined;
|
||||
var count: usize = 0;
|
||||
while (count < target_ticks) {
|
||||
// Blocked here. The task is `.blocked` and off every scheduler queue; the
|
||||
// next line runs only because the HPET raised its line.
|
||||
const r = ipc.replyWait(endpoint, &.{}, &receive);
|
||||
if (!r.isNotification()) continue; // a client request, not our IRQ
|
||||
|
||||
// Quiet the device: write 1 to timer 0's status bit. Until this lands, the
|
||||
// line is still asserted and unmasking would refire immediately.
|
||||
register(base, register_int_status).* = 1;
|
||||
count += 1;
|
||||
|
||||
if (count < target_ticks) {
|
||||
register(base, register_timer0_comparator).* = register(base, register_main_counter).* + ticks_per_ms * 100;
|
||||
} else {
|
||||
// Last one: stop the source rather than re-arming, so the line is left
|
||||
// both quiet *and* unmasked by the ack below. Re-arming here would leave
|
||||
// a pending interrupt that nobody is waiting for, and the ISR would mask
|
||||
// the line again a moment later.
|
||||
register(base, register_timer0_configuration).* &= ~tn_int_enb;
|
||||
}
|
||||
|
||||
_ = runtime.system.write("hpetd: irq\n");
|
||||
if (!device.irqAck(hpet.device_id, hpet.irq)) {
|
||||
_ = runtime.system.write("hpetd: irq_ack failed\n");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_ = runtime.system.write("hpetd: ok\n");
|
||||
while (true) runtime.system.sleep(1000);
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
//! /system/drivers/pci-bus — the PCI bus driver: enumeration moved out of ring 0
|
||||
//! (docs/discovery.md). The device manager matches the `pci_host_bridge`
|
||||
//! node and spawns one instance per bridge, the bridge's device id as argv[1] —
|
||||
//! the same per-device contract as usb-xhci-bus.
|
||||
//!
|
||||
//! M19.1 (this increment): claim the bridge, map its ECAM window (resource 0;
|
||||
//! the bus range and the MMIO apertures follow it), walk every
|
||||
//! bus/device/function config header, and log what the walk finds — ending
|
||||
//! with "/system/drivers/pci-bus: N functions found", which the `pci-scan` scenario compares
|
||||
//! against the kernel's own enumeration. Registration and reports (M19.2), and
|
||||
//! the kernel walk's retirement (M19.3), build on this proven-equivalent scan.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const protocol = runtime.device_manager_protocol;
|
||||
const device = runtime.device;
|
||||
const pci_class = @import("pci-class");
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Log a discovered function with its (class / subclass / prog-IF) triple decoded
|
||||
/// to human names — the boot-log breadcrumb that says *what* the hardware is, so
|
||||
/// "class 0x01 (Mass Storage Controller) subclass 0x06 (Serial ATA Controller)
|
||||
/// progif 0x01 (AHCI 1.0)" reads straight off the log when writing a new driver.
|
||||
/// A dedicated wider buffer than `writeLine`'s, since the decoded names are long.
|
||||
fn logFunction(bus: u64, dev: u64, function: u64, class_triple: u32) void {
|
||||
const cc = pci_class.ClassCode.unpack(@truncate(class_triple));
|
||||
const pif = pci_class.progIfName(cc.base, cc.subclass, cc.prog_if);
|
||||
var line: [200]u8 = undefined;
|
||||
const text = if (pif.len != 0)
|
||||
std.fmt.bufPrint(&line, "/system/drivers/pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2} ({s})\n", .{ bus, dev, function, cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if, pif }) catch return
|
||||
else
|
||||
std.fmt.bufPrint(&line, "/system/drivers/pci-bus: {d}:{d}.{d} class 0x{x:0>2} ({s}) subclass 0x{x:0>2} ({s}) progif 0x{x:0>2}\n", .{ bus, dev, function, cc.base, pci_class.className(cc.base), cc.subclass, pci_class.subclassName(cc.base, cc.subclass), cc.prog_if }) catch return;
|
||||
_ = runtime.system.write(text);
|
||||
}
|
||||
|
||||
var bridge_id: u64 = protocol.no_device;
|
||||
var ecam_base: usize = 0;
|
||||
var ecam_physical: u64 = 0;
|
||||
var start_bus: u64 = 0;
|
||||
var bus_count: u64 = 0;
|
||||
var manager_handle: runtime.ipc.Handle = 0;
|
||||
|
||||
/// One aligned 32-bit read from a function's configuration space.
|
||||
fn configRead(bus: u64, dev: u64, function: u64, offset: u64) u32 {
|
||||
const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
|
||||
const register: *volatile u32 = @ptrFromInt(address);
|
||||
return register.*;
|
||||
}
|
||||
|
||||
fn configWrite(bus: u64, dev: u64, function: u64, offset: u64, value: u32) void {
|
||||
const address = ecam_base + (((bus - start_bus) << 20) | (dev << 15) | (function << 12) | offset);
|
||||
const register: *volatile u32 = @ptrFromInt(address);
|
||||
register.* = value;
|
||||
}
|
||||
|
||||
fn configRead16(bus: u64, dev: u64, function: u64, offset: u64) u16 {
|
||||
const word = configRead(bus, dev, function, offset & ~@as(u64, 3));
|
||||
return @truncate(word >> @intCast((offset & 3) * 8));
|
||||
}
|
||||
|
||||
fn configWrite16(bus: u64, dev: u64, function: u64, offset: u64, value: u16) void {
|
||||
const aligned = offset & ~@as(u64, 3);
|
||||
const shift: u5 = @intCast((offset & 3) * 8);
|
||||
const word = configRead(bus, dev, function, aligned);
|
||||
const mask = @as(u32, 0xFFFF) << shift;
|
||||
configWrite(bus, dev, function, aligned, (word & ~mask) | (@as(u32, value) << shift));
|
||||
}
|
||||
|
||||
/// Claim the bridge, map the ECAM, hello the manager, then scan.
|
||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
_ = endpoint;
|
||||
if (!device.claim(bridge_id)) {
|
||||
writeLine("/system/drivers/pci-bus: unable to claim bridge device {d}\n", .{bridge_id});
|
||||
return false;
|
||||
}
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("/system/drivers/pci-bus: out of memory\n");
|
||||
return false;
|
||||
};
|
||||
const total = device.enumerate(buffer);
|
||||
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||
if (d.id == bridge_id) break d;
|
||||
} else {
|
||||
writeLine("/system/drivers/pci-bus: device {d} not in the device tree\n", .{bridge_id});
|
||||
return false;
|
||||
};
|
||||
// Resource 0 is the ECAM window (1 MiB of config space per bus); the bus
|
||||
// range rides beside it. The MMIO apertures (M19.0) come after both.
|
||||
if (descriptor.resource_count < 2 or descriptor.resources[0].kind != @intFromEnum(device.ResourceKind.memory)) {
|
||||
_ = runtime.system.write("/system/drivers/pci-bus: bridge has no ECAM window\n");
|
||||
return false;
|
||||
}
|
||||
const bus_range = for (descriptor.resources[0..@intCast(descriptor.resource_count)]) |resource| {
|
||||
if (resource.kind == @intFromEnum(device.ResourceKind.bus_range)) break resource;
|
||||
} else {
|
||||
_ = runtime.system.write("/system/drivers/pci-bus: bridge has no bus range\n");
|
||||
return false;
|
||||
};
|
||||
start_bus = bus_range.start;
|
||||
bus_count = bus_range.len;
|
||||
ecam_physical = descriptor.resources[0].start;
|
||||
ecam_base = device.mmioMap(bridge_id, 0) orelse {
|
||||
_ = runtime.system.write("/system/drivers/pci-bus: ECAM mmio_map failed\n");
|
||||
return false;
|
||||
};
|
||||
|
||||
// The handshake, then the scan (reports join in M19.2).
|
||||
var manager: ?runtime.ipc.Handle = null;
|
||||
var tries: u32 = 0;
|
||||
while (manager == null and tries < 100) : (tries += 1) {
|
||||
manager = runtime.ipc.lookup(.device_manager);
|
||||
if (manager == null) runtime.system.sleep(20);
|
||||
}
|
||||
const h = manager orelse {
|
||||
_ = runtime.system.write("/system/drivers/pci-bus: no device manager to hello\n");
|
||||
return false;
|
||||
};
|
||||
const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = bridge_id };
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
|
||||
_ = runtime.system.write("/system/drivers/pci-bus: hello call failed\n");
|
||||
return false;
|
||||
};
|
||||
if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
|
||||
_ = runtime.system.write("/system/drivers/pci-bus: hello refused\n");
|
||||
return false;
|
||||
}
|
||||
manager_handle = h;
|
||||
|
||||
scan();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The brute-force walk the kernel does today, from ring 3: every bus in the
|
||||
/// range, 32 devices, 8 functions; vendor id FFFFh means nothing decodes there,
|
||||
/// and only multifunction devices get their functions 1..7 probed.
|
||||
fn scan() void {
|
||||
var found: u32 = 0;
|
||||
var bus: u64 = start_bus;
|
||||
while (bus < start_bus + bus_count) : (bus += 1) {
|
||||
var dev: u64 = 0;
|
||||
while (dev < 32) : (dev += 1) {
|
||||
const first = configRead(bus, dev, 0, 0);
|
||||
if (first & 0xFFFF == 0xFFFF) continue;
|
||||
const multifunction = (configRead(bus, dev, 0, 0x0C) >> 16) & 0x80 != 0;
|
||||
var function: u64 = 0;
|
||||
while (function < 8) : (function += 1) {
|
||||
if (function != 0 and !multifunction) break;
|
||||
const vendor_device = configRead(bus, dev, function, 0);
|
||||
if (vendor_device & 0xFFFF == 0xFFFF) continue;
|
||||
const class_revision = configRead(bus, dev, function, 0x08);
|
||||
found += 1;
|
||||
logFunction(bus, dev, function, class_revision >> 8);
|
||||
registerAndReport(bus, dev, function, class_revision >> 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
writeLine("/system/drivers/pci-bus: {d} functions found\n", .{found});
|
||||
}
|
||||
|
||||
/// Register one function under the bridge and report it to the manager. The
|
||||
/// descriptor mirrors the kernel's own recording byte for byte — config slice
|
||||
/// as resource 0, then the sized BARs — so during coexistence the idempotent
|
||||
/// device_register (M19.0) returns the kernel's existing node id rather than
|
||||
/// growing a duplicate, and the report carries the id drivers already use.
|
||||
fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void {
|
||||
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
|
||||
descriptor.class = @intFromEnum(device.DeviceClass.pci_device);
|
||||
descriptor.pci_class = class_triple;
|
||||
descriptor.resources[0] = .{
|
||||
.kind = @intFromEnum(device.ResourceKind.memory),
|
||||
.start = ecam_physical + (((bus - start_bus) << 20) | (dev << 15) | (function << 12)),
|
||||
.len = 4096,
|
||||
};
|
||||
descriptor.resource_count = 1;
|
||||
|
||||
// The standard BAR-sizing probe, exactly as the kernel does it: decode off,
|
||||
// write all-ones, read the writable mask back, restore. Header type 0 only.
|
||||
const header_type = (configRead(bus, dev, function, 0x0C) >> 16) & 0x7F;
|
||||
if (header_type == 0) {
|
||||
const command = configRead16(bus, dev, function, 0x04);
|
||||
configWrite16(bus, dev, function, 0x04, command & ~@as(u16, 0b11));
|
||||
var i: u64 = 0;
|
||||
while (i < 6) : (i += 1) {
|
||||
if (descriptor.resource_count >= 8) break;
|
||||
const off = 0x10 + i * 4;
|
||||
const original = configRead(bus, dev, function, off);
|
||||
if (original == 0) continue;
|
||||
const slot: usize = @intCast(descriptor.resource_count);
|
||||
if (original & 1 != 0) {
|
||||
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
||||
const readback = configRead(bus, dev, function, off);
|
||||
configWrite(bus, dev, function, off, original);
|
||||
const mask = readback & 0xFFFF_FFFC;
|
||||
const size: u32 = if (mask == 0) 0 else (~mask +% 1) & 0xFFFF;
|
||||
if (size == 0) continue; // unimplemented BAR — nothing to register
|
||||
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.io_port), .start = original & 0xFFFF_FFFC, .len = size };
|
||||
descriptor.resource_count += 1;
|
||||
} else if ((original >> 1) & 0x3 == 2) {
|
||||
const original_high = configRead(bus, dev, function, off + 4);
|
||||
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
||||
configWrite(bus, dev, function, off + 4, 0xFFFF_FFFF);
|
||||
const lo = configRead(bus, dev, function, off);
|
||||
const hi = configRead(bus, dev, function, off + 4);
|
||||
configWrite(bus, dev, function, off, original);
|
||||
configWrite(bus, dev, function, off + 4, original_high);
|
||||
const readback = (@as(u64, hi) << 32) | (lo & 0xFFFF_FFF0);
|
||||
const size: u64 = if (readback == 0) 0 else ~readback +% 1;
|
||||
i += 1; // consumed the high half regardless
|
||||
if (size == 0) continue;
|
||||
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = (@as(u64, original_high) << 32) | (original & 0xFFFF_FFF0), .len = size };
|
||||
descriptor.resource_count += 1;
|
||||
} else {
|
||||
configWrite(bus, dev, function, off, 0xFFFF_FFFF);
|
||||
const readback = configRead(bus, dev, function, off);
|
||||
configWrite(bus, dev, function, off, original);
|
||||
const mask = readback & 0xFFFF_FFF0;
|
||||
const size: u32 = if (mask == 0) 0 else ~mask +% 1;
|
||||
if (size == 0) continue;
|
||||
descriptor.resources[slot] = .{ .kind = @intFromEnum(device.ResourceKind.memory), .start = original & 0xFFFF_FFF0, .len = size };
|
||||
descriptor.resource_count += 1;
|
||||
}
|
||||
}
|
||||
configWrite16(bus, dev, function, 0x04, command);
|
||||
}
|
||||
|
||||
const registered = device.register(bridge_id, &descriptor) orelse {
|
||||
writeLine("/system/drivers/pci-bus: register refused for {d}:{d}.{d}\n", .{ bus, dev, function });
|
||||
return;
|
||||
};
|
||||
const report = protocol.ChildAdded{
|
||||
.parent = bridge_id,
|
||||
.bus_address = (bus << 8) | (dev << 3) | function,
|
||||
.identity = class_triple,
|
||||
.device_id = registered,
|
||||
};
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
_ = runtime.ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch {
|
||||
writeLine("/system/drivers/pci-bus: child report for {d}:{d}.{d} failed\n", .{ bus, dev, function });
|
||||
};
|
||||
}
|
||||
|
||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||
_ = message;
|
||||
_ = reply;
|
||||
_ = sender;
|
||||
_ = capability;
|
||||
return 0;
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const argument = init.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
|
||||
bridge_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/pci-bus: malformed bridge device id '{s}'\n", .{argument});
|
||||
return;
|
||||
};
|
||||
runtime.service.run(protocol.message_maximum, .{
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
});
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! PS/2 Keyboard Driver
|
||||
//!
|
||||
//! Spawned by the ps2-bus driver once the controller is initialized and the port
|
||||
//! has passed its interface test and device reset. The bus driver hands us our
|
||||
//! device HID as argv[1] and, optionally, a layout name (`"us"`, `"gb"`, ...) as
|
||||
//! argv[2].
|
||||
//!
|
||||
//! The 8042's ports (0x60/0x64) and IRQ1 live on the PNP0303 node, which the
|
||||
//! ps2-bus driver exclusively owns — so this driver never touches the hardware.
|
||||
//! Instead it **attaches** to the bus (handing over its endpoint as a capability)
|
||||
//! and receives every scancode byte as a forwarded asynchronous message. Each byte
|
||||
//! feeds the set-2 decoder; a decoded key becomes input-protocol events:
|
||||
//!
|
||||
//! scancode byte -> HID usage keycode -> key_down / key_up
|
||||
//! -> xkeyboard-config -> character -> key_press
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const xkb = @import("xkeyboard-config");
|
||||
const ps2 = @import("ps2-library.zig");
|
||||
const scancode = @import("scancode.zig");
|
||||
const device = runtime.device;
|
||||
const ipc = runtime.ipc;
|
||||
const protocol = runtime.input_protocol;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Look up the ps2-bus service, retrying while the bus (which spawned us before
|
||||
/// registering) is still coming up.
|
||||
fn lookupBus() ?ipc.Handle {
|
||||
var attempts: usize = 0;
|
||||
while (attempts < 100) : (attempts += 1) {
|
||||
if (ipc.lookup(.ps2_bus)) |handle| return handle;
|
||||
runtime.system.sleep(50);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The character a pressed key produces under `modifiers`, or 0 for none. The
|
||||
/// layout lookup answers for printable keys; the keys whose keysym has no Unicode
|
||||
/// mapping but that every consumer still expects as a character (Enter, Tab,
|
||||
/// Backspace, Escape) are given their ASCII control characters here.
|
||||
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: scancode.ModifierSnapshot) u32 {
|
||||
const mapping = xkb.map(layout, usage, .{
|
||||
.shift = modifiers.shift,
|
||||
.caps_lock = modifiers.caps_lock,
|
||||
.level3 = modifiers.right_alt,
|
||||
.control = modifiers.control,
|
||||
});
|
||||
if (mapping.character) |character| return character;
|
||||
return switch (@as(protocol.Keycode, @enumFromInt(usage))) {
|
||||
.enter, .keypad_enter => '\n',
|
||||
.tab => '\t',
|
||||
.backspace => 0x08,
|
||||
.escape => 0x1B,
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// The input protocol's modifier word for a snapshot.
|
||||
fn modifierWord(modifiers: scancode.ModifierSnapshot) u32 {
|
||||
var word: u32 = 0;
|
||||
if (modifiers.shift) word |= protocol.modifier_shift;
|
||||
if (modifiers.control) word |= protocol.modifier_control;
|
||||
if (modifiers.alt) word |= protocol.modifier_alt;
|
||||
return word;
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const hid = init.arguments.get(1).?;
|
||||
if (hid.len == 0) {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: no HID argument\n");
|
||||
return;
|
||||
}
|
||||
writeLine("/system/drivers/ps2-bus/keyboard: starting for hid {s}\n", .{hid});
|
||||
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: out of memory\n");
|
||||
return;
|
||||
};
|
||||
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
||||
writeLine("/system/drivers/ps2-bus/keyboard: no device for hid {s}\n", .{hid});
|
||||
return;
|
||||
}
|
||||
|
||||
// The layout is a spawn argument so a later settings source can choose it;
|
||||
// absent (as today) it defaults to us.
|
||||
const layout_name = init.arguments.get(2) orelse "us";
|
||||
const layout = xkb.byName(layout_name) orelse xkb.us;
|
||||
writeLine("/system/drivers/ps2-bus/keyboard: layout {s}\n", .{layout.name});
|
||||
|
||||
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
||||
// keyboard sends (it owns the controller; we own the decoding).
|
||||
const bus = lookupBus() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: ps2-bus service unavailable\n");
|
||||
return;
|
||||
};
|
||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.keyboard) };
|
||||
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: attach call failed\n");
|
||||
return;
|
||||
};
|
||||
if (attached.len < @sizeOf(ps2.AttachReply) or
|
||||
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
|
||||
{
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: attach refused\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Broadcast keyboard events through the input service so programs can listen
|
||||
// for them (docs/input.md).
|
||||
var source = runtime.input.connectSource() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: ok\n");
|
||||
|
||||
var decoder = scancode.Decoder{};
|
||||
var state = scancode.KeyboardState{};
|
||||
var receive: [@sizeOf(ps2.ForwardedByte)]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
|
||||
if (!got.isMessage() or got.len < @sizeOf(ps2.ForwardedByte)) continue;
|
||||
const forwarded = std.mem.bytesToValue(ps2.ForwardedByte, receive[0..@sizeOf(ps2.ForwardedByte)]);
|
||||
|
||||
const key = decoder.feed(@intCast(forwarded.byte & 0xFF)) orelse continue;
|
||||
const transition = state.apply(key);
|
||||
const modifiers = modifierWord(transition.modifiers);
|
||||
|
||||
switch (transition.action) {
|
||||
.pressed => {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(protocol.EventKind.key_down),
|
||||
.keycode = key.usage,
|
||||
.character = 0,
|
||||
.modifiers = modifiers,
|
||||
});
|
||||
const character = characterFor(layout, key.usage, transition.modifiers);
|
||||
if (character != 0) {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(protocol.EventKind.key_press),
|
||||
.keycode = key.usage,
|
||||
.character = character,
|
||||
.modifiers = modifiers,
|
||||
});
|
||||
}
|
||||
},
|
||||
// Typematic repeat: the key did not physically go down again, so no
|
||||
// key_down — but it keeps producing its character.
|
||||
.repeated => {
|
||||
const character = characterFor(layout, key.usage, transition.modifiers);
|
||||
if (character != 0) {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(protocol.EventKind.key_press),
|
||||
.keycode = key.usage,
|
||||
.character = character,
|
||||
.modifiers = modifiers,
|
||||
});
|
||||
}
|
||||
},
|
||||
.released => {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(protocol.EventKind.key_up),
|
||||
.keycode = key.usage,
|
||||
.character = 0,
|
||||
.modifiers = modifiers,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
//! PS/2 mouse packet assembly — the byte stream a streaming mouse sends, turned
|
||||
//! into decoded movement/button reports.
|
||||
//!
|
||||
//! A standard PS/2 mouse in stream mode sends three-byte packets:
|
||||
//!
|
||||
//! byte 0: | Y ovf | X ovf | Y sign | X sign | 1 | middle | right | left |
|
||||
//! byte 1: X movement (low eight bits; the sign bit lives in byte 0)
|
||||
//! byte 2: Y movement (likewise)
|
||||
//!
|
||||
//! Movement is nine-bit two's complement, PS/2 convention: positive X right,
|
||||
//! positive Y **up**. The decoded packet converts Y to the screen convention
|
||||
//! (positive down), matching what every consumer of relative motion expects.
|
||||
//! Bit 3 of byte 0 is always set — the resynchronization anchor: a byte at
|
||||
//! packet start with bit 3 clear cannot be a packet header and is dropped.
|
||||
//!
|
||||
//! Everything here is pure (no imports beyond `std`, no IO), so it is
|
||||
//! host-testable: the tests at the bottom run under `zig build test`.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// One decoded movement/button report, in screen convention (positive dy down).
|
||||
pub const Packet = struct {
|
||||
left: bool,
|
||||
right: bool,
|
||||
middle: bool,
|
||||
dx: i16,
|
||||
dy: i16,
|
||||
};
|
||||
|
||||
const header_always_set: u8 = 1 << 3;
|
||||
const header_left: u8 = 1 << 0;
|
||||
const header_right: u8 = 1 << 1;
|
||||
const header_middle: u8 = 1 << 2;
|
||||
const header_x_sign: u8 = 1 << 4;
|
||||
const header_y_sign: u8 = 1 << 5;
|
||||
const header_x_overflow: u8 = 1 << 6;
|
||||
const header_y_overflow: u8 = 1 << 7;
|
||||
|
||||
/// Device protocol bytes that can reach the packet stream around bring-up (the
|
||||
/// acknowledge to enable-reporting, a reset's self-test result). Both have bit 3
|
||||
/// set, so the header check alone cannot reject them; they are recognized only
|
||||
/// at packet start, where a real header cannot be one of them in practice.
|
||||
const response_acknowledge: u8 = 0xFA;
|
||||
const response_self_test_passed: u8 = 0xAA;
|
||||
|
||||
/// Accumulates the byte stream into `Packet`s. Feed it every byte the mouse
|
||||
/// sends; the third byte of each well-formed packet returns one.
|
||||
pub const Assembler = struct {
|
||||
bytes: [3]u8 = undefined,
|
||||
count: u8 = 0,
|
||||
|
||||
pub fn feed(self: *Assembler, byte: u8) ?Packet {
|
||||
if (self.count == 0) {
|
||||
// Resynchronize: a packet must start with a plausible header.
|
||||
if (byte & header_always_set == 0) return null;
|
||||
if (byte == response_acknowledge or byte == response_self_test_passed) return null;
|
||||
}
|
||||
self.bytes[self.count] = byte;
|
||||
self.count += 1;
|
||||
if (self.count < 3) return null;
|
||||
self.count = 0;
|
||||
|
||||
const header = self.bytes[0];
|
||||
// An overflowed count is garbage by definition; discard the packet.
|
||||
if (header & (header_x_overflow | header_y_overflow) != 0) return null;
|
||||
|
||||
return .{
|
||||
.left = header & header_left != 0,
|
||||
.right = header & header_right != 0,
|
||||
.middle = header & header_middle != 0,
|
||||
.dx = movement(self.bytes[1], header & header_x_sign != 0),
|
||||
// PS/2 positive Y is up; screen positive Y is down.
|
||||
.dy = -movement(self.bytes[2], header & header_y_sign != 0),
|
||||
};
|
||||
}
|
||||
|
||||
/// Nine-bit two's complement: the eight movement bits plus the header's sign.
|
||||
fn movement(low: u8, negative: bool) i16 {
|
||||
const value: i16 = low;
|
||||
return if (negative) value - 256 else value;
|
||||
}
|
||||
};
|
||||
|
||||
// --- tests (host-run via `zig build test`) ------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
fn feedAll(assembler: *Assembler, bytes: []const u8) ?Packet {
|
||||
var result: ?Packet = null;
|
||||
for (bytes) |byte| {
|
||||
if (assembler.feed(byte)) |packet| result = packet;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
test "plain motion decodes with screen-convention y" {
|
||||
var assembler = Assembler{};
|
||||
const packet = feedAll(&assembler, &.{ 0x08, 5, 3 }).?;
|
||||
try testing.expectEqual(@as(i16, 5), packet.dx);
|
||||
try testing.expectEqual(@as(i16, -3), packet.dy); // PS/2 up 3 -> screen -3
|
||||
try testing.expect(!packet.left and !packet.right and !packet.middle);
|
||||
}
|
||||
|
||||
test "negative movement sign-extends through the header bits" {
|
||||
var assembler = Assembler{};
|
||||
// X sign and Y sign set: dx = 0xFB - 256 = -5, dy raw = 0xFE - 256 = -2 -> screen +2.
|
||||
const packet = feedAll(&assembler, &.{ 0x08 | 0x10 | 0x20, 0xFB, 0xFE }).?;
|
||||
try testing.expectEqual(@as(i16, -5), packet.dx);
|
||||
try testing.expectEqual(@as(i16, 2), packet.dy);
|
||||
}
|
||||
|
||||
test "buttons decode from the header" {
|
||||
var assembler = Assembler{};
|
||||
const packet = feedAll(&assembler, &.{ 0x08 | 0x01 | 0x02, 0, 0 }).?;
|
||||
try testing.expect(packet.left);
|
||||
try testing.expect(packet.right);
|
||||
try testing.expect(!packet.middle);
|
||||
}
|
||||
|
||||
test "a byte with bit 3 clear at packet start is dropped" {
|
||||
var assembler = Assembler{};
|
||||
// The stray 0x02 cannot be a header; the following packet still decodes.
|
||||
try testing.expectEqual(@as(?Packet, null), assembler.feed(0x02));
|
||||
const packet = feedAll(&assembler, &.{ 0x09, 1, 0 }).?;
|
||||
try testing.expect(packet.left);
|
||||
try testing.expectEqual(@as(i16, 1), packet.dx);
|
||||
}
|
||||
|
||||
test "protocol bytes at packet start are dropped" {
|
||||
var assembler = Assembler{};
|
||||
try testing.expectEqual(@as(?Packet, null), assembler.feed(0xFA)); // enable-reporting ACK
|
||||
try testing.expectEqual(@as(?Packet, null), assembler.feed(0xAA)); // self-test passed
|
||||
const packet = feedAll(&assembler, &.{ 0x08, 7, 0 }).?;
|
||||
try testing.expectEqual(@as(i16, 7), packet.dx);
|
||||
}
|
||||
|
||||
test "an overflowed packet is discarded whole" {
|
||||
var assembler = Assembler{};
|
||||
try testing.expectEqual(@as(?Packet, null), feedAll(&assembler, &.{ 0x08 | 0x40, 0xFF, 0xFF }));
|
||||
// The assembler is back at packet start.
|
||||
const packet = feedAll(&assembler, &.{ 0x08, 1, 1 }).?;
|
||||
try testing.expectEqual(@as(i16, 1), packet.dx);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//! PS/2 Mouse Driver
|
||||
//!
|
||||
//! Spawned by the ps2-bus driver once the controller is initialized and the port
|
||||
//! has passed its interface test and device reset. The bus driver hands us our
|
||||
//! device HID as argv[1].
|
||||
//!
|
||||
//! Like the keyboard, this driver never touches the hardware: the 8042's ports
|
||||
//! and both port IRQs are owned by the ps2-bus driver (the auxiliary port's
|
||||
//! IRQ12 lives on the PNP0F13 node, which the bus claims alongside the
|
||||
//! controller). The driver **attaches** to the bus and receives every byte the
|
||||
//! mouse sends as a forwarded asynchronous message. The bytes assemble into
|
||||
//! three-byte packets, and each packet becomes input-protocol events:
|
||||
//!
|
||||
//! packet -> button transitions -> button_down / button_up
|
||||
//! -> movement -> motion (dx/dy, screen convention)
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const ps2 = @import("ps2-library.zig");
|
||||
const mouse_packet = @import("mouse-packet.zig");
|
||||
const device = runtime.device;
|
||||
const ipc = runtime.ipc;
|
||||
const protocol = runtime.input_protocol;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Look up the ps2-bus service, retrying while the bus (which spawned us before
|
||||
/// registering) is still coming up.
|
||||
fn lookupBus() ?ipc.Handle {
|
||||
var attempts: usize = 0;
|
||||
while (attempts < 100) : (attempts += 1) {
|
||||
if (ipc.lookup(.ps2_bus)) |handle| return handle;
|
||||
runtime.system.sleep(50);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The protocol's pressed-button bitmask for a packet.
|
||||
fn buttonMask(packet: mouse_packet.Packet) u32 {
|
||||
var mask: u32 = 0;
|
||||
if (packet.left) mask |= protocol.mouse_button_left;
|
||||
if (packet.right) mask |= protocol.mouse_button_right;
|
||||
if (packet.middle) mask |= protocol.mouse_button_middle;
|
||||
return mask;
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const hid = init.arguments.get(1).?;
|
||||
|
||||
if (hid.len == 0) {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: no HID argument\n");
|
||||
return;
|
||||
}
|
||||
writeLine("/system/drivers/ps2-bus/mouse: starting for hid {s}\n", .{hid});
|
||||
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: out of memory\n");
|
||||
return;
|
||||
};
|
||||
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
||||
writeLine("/system/drivers/ps2-bus/mouse: no device for hid {s}\n", .{hid});
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
||||
// mouse sends (it owns the controller; we own the decoding).
|
||||
const bus = lookupBus() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: ps2-bus service unavailable\n");
|
||||
return;
|
||||
};
|
||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
var attach = ps2.AttachRequest{ .device_type = @intFromEnum(ps2.DeviceType.mouse) };
|
||||
var attach_reply: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||
const attached = ipc.callCap(bus, std.mem.asBytes(&attach), &attach_reply, endpoint) catch {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: attach call failed\n");
|
||||
return;
|
||||
};
|
||||
if (attached.len < @sizeOf(ps2.AttachReply) or
|
||||
std.mem.bytesToValue(ps2.AttachReply, attach_reply[0..@sizeOf(ps2.AttachReply)]).status != @intFromEnum(ps2.AttachStatus.ok))
|
||||
{
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: attach refused\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Broadcast mouse events through the input service so programs can listen
|
||||
// for them (docs/input.md).
|
||||
var source = runtime.input.connectSource() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: ok\n");
|
||||
|
||||
var assembler = mouse_packet.Assembler{};
|
||||
var buttons: u32 = 0;
|
||||
var receive: [@sizeOf(ps2.ForwardedByte)]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
|
||||
if (!got.isMessage() or got.len < @sizeOf(ps2.ForwardedByte)) continue;
|
||||
const forwarded = std.mem.bytesToValue(ps2.ForwardedByte, receive[0..@sizeOf(ps2.ForwardedByte)]);
|
||||
|
||||
const packet = assembler.feed(@intCast(forwarded.byte & 0xFF)) orelse continue;
|
||||
const new_buttons = buttonMask(packet);
|
||||
|
||||
// A button transition per changed button, carrying the new whole mask.
|
||||
const changed = buttons ^ new_buttons;
|
||||
for ([_]u32{ protocol.mouse_button_left, protocol.mouse_button_right, protocol.mouse_button_middle }) |button| {
|
||||
if (changed & button == 0) continue;
|
||||
const kind: protocol.MouseEventKind = if (new_buttons & button != 0) .button_down else .button_up;
|
||||
_ = source.publishMouseEvent(.{
|
||||
.kind = @intFromEnum(kind),
|
||||
.button = button,
|
||||
.dx = 0,
|
||||
.dy = 0,
|
||||
.scroll_x = 0,
|
||||
.scroll_y = 0,
|
||||
.buttons = new_buttons,
|
||||
});
|
||||
}
|
||||
buttons = new_buttons;
|
||||
|
||||
if (packet.dx != 0 or packet.dy != 0) {
|
||||
_ = source.publishMouseEvent(.{
|
||||
.kind = @intFromEnum(protocol.MouseEventKind.motion),
|
||||
.button = 0,
|
||||
.dx = packet.dx,
|
||||
.dy = packet.dy,
|
||||
.scroll_x = 0,
|
||||
.scroll_y = 0,
|
||||
.buttons = new_buttons,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
//! The PS/2 Controller is located on the mainboard.
|
||||
//! In the early days the controller was a single chip (Intel 8042).
|
||||
//! As of today it is part of the Advanced Integrated Peripheral.
|
||||
//!
|
||||
//! It shows up in the device discovery as:
|
||||
//! KBD_ [acpi_device] hid=PNP0303 (PS/2 Keyboard)
|
||||
//! - io_port 0x60 len 0x1
|
||||
//! - io_port 0x64 len 0x1
|
||||
//! - irq 0x1 len 0x1
|
||||
//! MOU_ [acpi_device] hid=PNP0F13 (PS/2 Mouse)
|
||||
//! - irq 0xc len 0x1
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const acpi_ids = @import("acpi-ids");
|
||||
const ps2 = @import("ps2-library.zig");
|
||||
const device = runtime.device;
|
||||
const ipc = runtime.ipc;
|
||||
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so output
|
||||
/// from the child drivers (which run concurrently) can never interleave with it.
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Ask the device on `port` what it is, then spawn the matching driver from the
|
||||
/// initial-ramdisk, handing it the device's HID as argv[1]. The driver is chosen
|
||||
/// from what the device reports, not from the port number. Returns the identified
|
||||
/// type so the forwarding loop can route that port's bytes to the driver once it
|
||||
/// attaches, or null if nothing was spawned.
|
||||
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) ?ps2.DeviceType {
|
||||
const device_type = controller.identifyDevice(port) orelse {
|
||||
writeLine("/system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)});
|
||||
return null;
|
||||
};
|
||||
const driver_name = device_type.driverName() orelse {
|
||||
writeLine("/system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)});
|
||||
return null;
|
||||
};
|
||||
const hid = device_type.hid() orelse "";
|
||||
if (runtime.system.spawnWithArguments(driver_name, &.{hid}) != null) {
|
||||
writeLine("/system/drivers/ps2-bus: port {s} is a {s}, spawned {s}\n", .{ @tagName(port), hid, driver_name });
|
||||
return device_type;
|
||||
}
|
||||
writeLine("/system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Resource index of the controller's IRQ (IRQ1) on the PNP0303 descriptor, found
|
||||
/// the way the ports are found in `Controller.init`.
|
||||
fn findInterruptResourceIndex(descriptor: device.DeviceDescriptor) ?u64 {
|
||||
for (0..descriptor.resource_count) |index| {
|
||||
if (descriptor.resources[index].kind == @intFromEnum(device.ResourceKind.irq)) return index;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Forwarding endpoints of the attached child drivers, indexed by `ps2.Port`.
|
||||
/// Written when a child's `AttachRequest` arrives, read on every forwarded byte.
|
||||
var port_endpoints = [_]?ipc.Handle{ null, null };
|
||||
|
||||
/// Which device type each port identified as, so an attaching child (which knows
|
||||
/// its type, not its port) can be matched to the right port's byte stream.
|
||||
var port_device_types = [_]?ps2.DeviceType{ null, null };
|
||||
|
||||
/// Handle a child driver's `AttachRequest`: record the endpoint capability it
|
||||
/// passed as the forwarding target for the port whose device matches its type.
|
||||
/// Writes an `AttachReply` into `out` and returns its length.
|
||||
fn handleAttach(message: []const u8, got: ipc.Received, out: []u8) usize {
|
||||
const reply = struct {
|
||||
fn write(buffer: []u8, status: ps2.AttachStatus) usize {
|
||||
const header = ps2.AttachReply{ .status = @intFromEnum(status) };
|
||||
@memcpy(buffer[0..@sizeOf(ps2.AttachReply)], std.mem.asBytes(&header));
|
||||
return @sizeOf(ps2.AttachReply);
|
||||
}
|
||||
};
|
||||
|
||||
if (message.len < @sizeOf(ps2.AttachRequest)) return reply.write(out, .invalid_request);
|
||||
const request = std.mem.bytesToValue(ps2.AttachRequest, message[0..@sizeOf(ps2.AttachRequest)]);
|
||||
const endpoint = got.cap orelse return reply.write(out, .missing_endpoint);
|
||||
|
||||
for (&port_device_types, 0..) |maybe_type, port_index| {
|
||||
const device_type = maybe_type orelse continue;
|
||||
if (@intFromEnum(device_type) != request.device_type) continue;
|
||||
port_endpoints[port_index] = endpoint;
|
||||
writeLine("/system/drivers/ps2-bus: {s} driver attached\n", .{@tagName(device_type)});
|
||||
return reply.write(out, .ok);
|
||||
}
|
||||
return reply.write(out, .no_such_device);
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: out of memory\n");
|
||||
return;
|
||||
};
|
||||
|
||||
var has_two_channels = false;
|
||||
var maybe_controller: ?ps2.Controller = null;
|
||||
var maybe_interrupt_index: ?u64 = null;
|
||||
// The 8042's IO ports (0x60/0x64) are enumerated under the keyboard ACPI node
|
||||
// (PNP0303), so we init the controller from that descriptor — but which device
|
||||
// is on which port is decided later by identify, not by this HID.
|
||||
const maybe_controller_device_descriptor = device.findDeviceDescriptorByHid(buffer, acpi_ids.HardwareId.ps2_keyboard.hid());
|
||||
if (maybe_controller_device_descriptor) |controller_device_descriptor| {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: found PS/2 controller\n");
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: initializing controller\n");
|
||||
|
||||
if (!device.claim(controller_device_descriptor.id)) {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: unable to claim controller \n");
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = ps2.Controller.init(controller_device_descriptor) orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: controller is missing its IO ports\n");
|
||||
return;
|
||||
};
|
||||
maybe_controller = controller;
|
||||
maybe_interrupt_index = findInterruptResourceIndex(controller_device_descriptor);
|
||||
|
||||
controller.disablePort(.one);
|
||||
controller.disablePort(.two);
|
||||
controller.flushOutputBuffer();
|
||||
|
||||
const current = controller.readConfigurationByte() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: controller configuration timed out\n");
|
||||
return;
|
||||
};
|
||||
|
||||
const update = current & ~(ps2.configuration_first_port_interrupt |
|
||||
ps2.configuration_second_port_interrupt |
|
||||
ps2.configuration_first_port_translation);
|
||||
|
||||
if (controller.writeConfigurationByte(update) == null) {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: controller configuration timed out\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller.performSelfTest()) |reply| {
|
||||
if (reply != ps2.response_controller_test_passed) {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: perform controller self test failed\n");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: controller self test timed out\n");
|
||||
return;
|
||||
}
|
||||
|
||||
has_two_channels = controller.hasTwoChannels() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: controller channels timed out\n");
|
||||
return;
|
||||
};
|
||||
|
||||
if (has_two_channels) {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: has two channels\n");
|
||||
// keep the bus quiet until we have tested the ports and are ready to use them
|
||||
controller.disablePort(.two);
|
||||
} else {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: has one channel\n");
|
||||
}
|
||||
|
||||
// interface tests: always test port 1, test port 2 only if it exists
|
||||
const port_one_works = (controller.testPort(.one) orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: port 1 test timed out\n");
|
||||
return;
|
||||
}) == ps2.response_port_test_passed;
|
||||
|
||||
var port_two_works = false;
|
||||
if (has_two_channels) {
|
||||
port_two_works = (controller.testPort(.two) orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: port 2 test timed out\n");
|
||||
return;
|
||||
}) == ps2.response_port_test_passed;
|
||||
}
|
||||
|
||||
if (!port_one_works and !port_two_works) {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: no usable ports\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable the working ports. Their interrupts stay off until IRQ1 is bound
|
||||
// below — reset and identify use polled reads, which must never race the
|
||||
// interrupt-driven drain loop for bytes.
|
||||
controller.enablePort(.one);
|
||||
if (port_two_works) controller.enablePort(.two);
|
||||
|
||||
// reset each working device; a failing device is logged but does not
|
||||
// abort bring-up of the other one
|
||||
if (port_one_works) {
|
||||
if (controller.resetDevice(.one)) |passed| {
|
||||
if (!passed) _ = runtime.system.write("/system/drivers/ps2-bus: port 1 device reset failed\n");
|
||||
} else {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: port 1 device reset timed out\n");
|
||||
}
|
||||
}
|
||||
if (port_two_works) {
|
||||
if (controller.resetDevice(.two)) |passed| {
|
||||
if (!passed) _ = runtime.system.write("/system/drivers/ps2-bus: port 2 device reset failed\n");
|
||||
} else {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: port 2 device reset timed out\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Identify the device on each working port and hand it off to the driver
|
||||
// that matches what it reported — a port is not assumed to be a keyboard
|
||||
// or a mouse by its number.
|
||||
if (port_one_works) port_device_types[@intFromEnum(ps2.Port.one)] = spawnIdentifiedDriver(controller, .one);
|
||||
if (port_two_works) port_device_types[@intFromEnum(ps2.Port.two)] = spawnIdentifiedDriver(controller, .two);
|
||||
} else {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: no PS/2 controller found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = maybe_controller.?;
|
||||
const interrupt_index = maybe_interrupt_index orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: controller is missing its IRQ\n");
|
||||
return;
|
||||
};
|
||||
|
||||
// The endpoint the child drivers attach to and IRQ1 wakes. Registered under a
|
||||
// well-known id so the children can find it, the way input subscribers find
|
||||
// the input service.
|
||||
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: no endpoint\n");
|
||||
return;
|
||||
};
|
||||
if (!ipc.register(.ps2_bus, endpoint)) {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: register failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// From here on, only the interrupt path reads the data port. Drop anything a
|
||||
// device sent between enable-scanning and now, bind the IRQs, and only then
|
||||
// let the controller raise them — an interrupt with nobody bound is lost.
|
||||
controller.drainOutputBuffer();
|
||||
if (!device.irqBind(controller.device_id, interrupt_index, endpoint)) {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: irq_bind failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Port 2's interrupt (IRQ12) is enumerated on the auxiliary device's own ACPI
|
||||
// node (PNP0F13), not on the controller's — so if port 2 carries a device,
|
||||
// claim that node too and route its IRQ to the same endpoint. The IRQ belongs
|
||||
// to the *port*, whatever device identify found on it.
|
||||
var maybe_auxiliary_interrupt: ?struct { device_id: u64, interrupt_index: u64, gsi: u64 } = null;
|
||||
if (port_device_types[@intFromEnum(ps2.Port.two)] != null) {
|
||||
if (device.findDeviceDescriptorByHid(buffer, acpi_ids.HardwareId.ps2_mouse.hid())) |descriptor| {
|
||||
if (findInterruptResourceIndex(descriptor)) |auxiliary_index| {
|
||||
if (device.claim(descriptor.id) and device.irqBind(descriptor.id, auxiliary_index, endpoint)) {
|
||||
maybe_auxiliary_interrupt = .{
|
||||
.device_id = descriptor.id,
|
||||
.interrupt_index = auxiliary_index,
|
||||
.gsi = descriptor.resources[auxiliary_index].start,
|
||||
};
|
||||
} else {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: auxiliary irq_bind failed\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var configuration = controller.readConfigurationByte() orelse {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: controller configuration timed out\n");
|
||||
return;
|
||||
};
|
||||
if (port_device_types[@intFromEnum(ps2.Port.one)] != null) configuration |= ps2.Port.one.interruptBit();
|
||||
if (maybe_auxiliary_interrupt != null) configuration |= ps2.Port.two.interruptBit();
|
||||
_ = controller.writeConfigurationByte(configuration);
|
||||
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus: ok\n");
|
||||
|
||||
// The forwarding loop: an IRQ1 notification drains the output buffer, routing
|
||||
// each byte to the attached driver of the port it came from; a client message
|
||||
// is a child driver's AttachRequest.
|
||||
var reply_buffer: [@sizeOf(ps2.AttachReply)]u8 = undefined;
|
||||
var reply_len: usize = 0;
|
||||
var receive: [@sizeOf(ps2.AttachRequest)]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(endpoint, reply_buffer[0..reply_len], &receive, null);
|
||||
if (got.isNotification()) {
|
||||
reply_len = 0;
|
||||
if (got.isMessage() or got.isChildExit()) continue; // nothing sends us these
|
||||
while (true) {
|
||||
const current_status = ps2.status(controller.device_id, controller.status_index);
|
||||
if (current_status & ps2.status_output_buffer_full == 0) break;
|
||||
const byte = device.ioRead(controller.device_id, controller.data_index, 0, 1) orelse break;
|
||||
const port: ps2.Port = if (current_status & ps2.status_auxiliary_output != 0) .two else .one;
|
||||
if (port_endpoints[@intFromEnum(port)]) |child| {
|
||||
const forwarded = ps2.ForwardedByte{ .port = @intFromEnum(port), .byte = byte };
|
||||
_ = ipc.send(child, std.mem.asBytes(&forwarded));
|
||||
}
|
||||
// An unattached port's byte is dropped — e.g. a keystroke before
|
||||
// the keyboard driver has attached.
|
||||
}
|
||||
// Re-arm the line that woke us: the notification badge carries the
|
||||
// GSI, and IRQ1 and IRQ12 are acked through different device claims.
|
||||
if (maybe_auxiliary_interrupt) |auxiliary| {
|
||||
if (got.source() == auxiliary.gsi) {
|
||||
_ = device.irqAck(auxiliary.device_id, auxiliary.interrupt_index);
|
||||
} else {
|
||||
_ = device.irqAck(controller.device_id, interrupt_index);
|
||||
}
|
||||
} else {
|
||||
_ = device.irqAck(controller.device_id, interrupt_index);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
reply_len = handleAttach(receive[0..got.len], got, &reply_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
//! shared definitions between the different PS/2 drivers
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const acpi_ids = @import("acpi-ids");
|
||||
const device = runtime.device;
|
||||
const system = runtime.system;
|
||||
|
||||
/// PS-2 io ports:
|
||||
/// The PS/2 Controller itself uses 2 IO ports (usually, IO ports 0x60 and 0x64). Like many IO
|
||||
/// ports, reads and writes may access different internal registers.
|
||||
///
|
||||
/// Historical note: The PC-XT PPI had used port 0x61 to reset the keyboard interrupt request
|
||||
/// signal (among other unrelated functions). Port 0x61 has no keyboard related functions on AT and
|
||||
/// PS/2 compatibles.
|
||||
///
|
||||
/// The Data Port (typically IO Port 0x60) is used for reading data that was received from a PS/2
|
||||
/// device or from the PS/2 controller itself and writing data to a PS/2 device or to the PS/2
|
||||
/// controller itself.
|
||||
// Access type: Read/Write
|
||||
pub const dataPort = 0x60;
|
||||
// Access type: Read
|
||||
pub const statusRegisterPort = 0x64;
|
||||
// Access type: Write
|
||||
pub const CommandRegisterPort = 0x64;
|
||||
|
||||
/// How long to poll the status register before giving up. PS/2 controller
|
||||
/// responses normally arrive within a few milliseconds.
|
||||
pub const default_wait_timeout_nanoseconds: u64 = 10_000_000; // 10 ms
|
||||
|
||||
/// A PS/2 device reset (0xFF) runs the device's self-test (BAT), whose reply can
|
||||
/// take far longer than an ordinary controller response.
|
||||
pub const device_reset_timeout_nanoseconds: u64 = 750_000_000; // 750 ms
|
||||
|
||||
/// PS/2 controller commands, written to the command register (port 0x64).
|
||||
pub const cmd_read_configuration_byte: u8 = 0x20; // read controller configuration byte (internal RAM byte 0)
|
||||
pub const cmd_write_configuration_byte: u8 = 0x60; // write controller configuration byte (internal RAM byte 0)
|
||||
pub const cmd_disable_second_port: u8 = 0xA7; // disable second PS/2 port (dual-channel controllers only)
|
||||
pub const cmd_enable_second_port: u8 = 0xA8; // enable second PS/2 port (dual-channel controllers only)
|
||||
pub const cmd_test_second_port: u8 = 0xA9; // test second PS/2 port
|
||||
pub const cmd_test_controller: u8 = 0xAA; // controller self-test
|
||||
pub const cmd_test_first_port: u8 = 0xAB; // test first PS/2 port
|
||||
pub const cmd_diagnostic_dump: u8 = 0xAC; // read all bytes of internal RAM
|
||||
pub const cmd_disable_first_port: u8 = 0xAD; // disable first PS/2 port
|
||||
pub const cmd_enable_first_port: u8 = 0xAE; // enable first PS/2 port
|
||||
pub const cmd_read_controller_input_port: u8 = 0xC0; // read controller input port
|
||||
pub const cmd_read_controller_output_port: u8 = 0xD0; // read controller output port
|
||||
pub const cmd_write_controller_output_port: u8 = 0xD1; // write next data byte to the controller output port
|
||||
pub const cmd_write_first_port_output: u8 = 0xD2; // write next data byte to the first port output buffer
|
||||
pub const cmd_write_second_port_output: u8 = 0xD3; // write next data byte to the second port output buffer
|
||||
pub const cmd_write_second_port_input: u8 = 0xD4; // write next data byte to the second port input buffer (to the mouse)
|
||||
pub const cmd_pulse_system_reset: u8 = 0xFE; // pulse output line 0 low: resets the CPU
|
||||
|
||||
/// PS/2 status register bits (read from the status port, 0x64). Bit 4 is
|
||||
/// chipset-specific and intentionally omitted.
|
||||
pub const status_output_buffer_full: u8 = 1 << 0; // 1 = a byte is waiting to be read from the data port
|
||||
pub const status_input_buffer_full: u8 = 1 << 1; // 1 = the controller has not yet consumed the last write
|
||||
pub const status_system_flag: u8 = 1 << 2; // set once the controller passes POST
|
||||
pub const status_command_or_data: u8 = 1 << 3; // 1 = last write was a command, 0 = data
|
||||
/// Chipset-specific in the original spec, universal in practice on dual-channel
|
||||
/// controllers: set = the waiting byte came from the second port (the mouse).
|
||||
pub const status_auxiliary_output: u8 = 1 << 5;
|
||||
pub const status_timeout_error: u8 = 1 << 6; // 1 = time-out error
|
||||
pub const status_parity_error: u8 = 1 << 7; // 1 = parity error
|
||||
|
||||
/// Controller configuration byte bits (internal RAM byte 0; read/written via 0x20/0x60).
|
||||
pub const configuration_first_port_interrupt: u8 = 1 << 0; // 1 = first port IRQ (IRQ1) enabled
|
||||
pub const configuration_second_port_interrupt: u8 = 1 << 1; // 1 = second port IRQ (IRQ12) enabled
|
||||
pub const configuration_system_flag: u8 = 1 << 2; // 1 = system passed POST
|
||||
pub const configuration_first_port_clock_disabled: u8 = 1 << 4; // 1 = first port clock disabled
|
||||
pub const configuration_second_port_clock_disabled: u8 = 1 << 5; // 1 = second port clock disabled
|
||||
pub const configuration_first_port_translation: u8 = 1 << 6; // 1 = first port scancode translation enabled
|
||||
|
||||
/// Controller output port bits (read/written via 0xD0/0xD1).
|
||||
pub const output_port_system_reset: u8 = 1 << 0; // WARNING: keep this 1; writing 0 can lock the machine
|
||||
pub const output_port_a20_gate: u8 = 1 << 1; // A20 gate
|
||||
pub const output_port_second_port_clock: u8 = 1 << 2; // dual-channel controllers only
|
||||
pub const output_port_second_port_data: u8 = 1 << 3; // dual-channel controllers only
|
||||
pub const output_port_first_port_output_full: u8 = 1 << 4; // output buffer full from first port (IRQ1)
|
||||
pub const output_port_second_port_output_full: u8 = 1 << 5; // output buffer full from second port (IRQ12)
|
||||
pub const output_port_first_port_clock: u8 = 1 << 6; // first port clock
|
||||
pub const output_port_first_port_data: u8 = 1 << 7; // first port data
|
||||
|
||||
/// Controller self-test (0xAA) result codes.
|
||||
pub const response_controller_test_passed: u8 = 0x55;
|
||||
pub const response_controller_test_failed: u8 = 0xFC;
|
||||
|
||||
/// Port test (0xAB / 0xA9) result codes.
|
||||
pub const response_port_test_passed: u8 = 0x00;
|
||||
pub const response_port_test_clock_stuck_low: u8 = 0x01;
|
||||
pub const response_port_test_clock_stuck_high: u8 = 0x02;
|
||||
pub const response_port_test_data_stuck_low: u8 = 0x03;
|
||||
pub const response_port_test_data_stuck_high: u8 = 0x04;
|
||||
|
||||
/// PS/2 device commands, written to the data port (0x60) to reach the attached device.
|
||||
pub const device_cmd_identify: u8 = 0xF2; // identify device
|
||||
pub const device_cmd_enable_scanning: u8 = 0xF4;
|
||||
pub const device_cmd_disable_scanning: u8 = 0xF5;
|
||||
pub const device_cmd_reset: u8 = 0xFF; // reset and run the device self-test (BAT)
|
||||
|
||||
/// PS/2 device response bytes, read from the data port (0x60).
|
||||
pub const device_response_self_test_passed: u8 = 0xAA; // BAT succeeded after a reset
|
||||
pub const device_response_echo: u8 = 0xEE;
|
||||
pub const device_response_acknowledge: u8 = 0xFA; // ACK
|
||||
pub const device_response_self_test_failed_1: u8 = 0xFC; // BAT failure
|
||||
pub const device_response_self_test_failed_2: u8 = 0xFD; // BAT failure
|
||||
pub const device_response_resend: u8 = 0xFE; // ask the host to resend the last byte
|
||||
|
||||
/// PS/2 device identify (0xF2) reply bytes. A keyboard returns a two-byte id
|
||||
/// beginning with 0xAB; a mouse returns a single-byte id (0x00/0x03/0x04); an
|
||||
/// ancient AT keyboard returns nothing at all.
|
||||
pub const identify_keyboard_mf2: u8 = 0xAB; // first byte of a MF2 keyboard id (a subtype byte follows)
|
||||
pub const identify_mouse_standard: u8 = 0x00;
|
||||
pub const identify_mouse_scroll: u8 = 0x03; // mouse with scroll wheel
|
||||
pub const identify_mouse_five_button: u8 = 0x04; // 5-button mouse
|
||||
|
||||
fn waitReadable(id: u64, cmd_index: u64, wait_timeout_nanoseconds: u64) bool {
|
||||
const deadline = system.clock() + wait_timeout_nanoseconds;
|
||||
while (system.clock() < deadline) {
|
||||
if (status(id, cmd_index) & status_output_buffer_full != 0) return true; // OBF set -> data ready
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn waitWritable(id: u64, cmd_index: u64, wait_timeout_nanoseconds: u64) bool {
|
||||
const deadline = system.clock() + wait_timeout_nanoseconds;
|
||||
while (system.clock() < deadline) {
|
||||
if (status(id, cmd_index) & status_input_buffer_full == 0) return true; // IBF clear -> ok to write
|
||||
}
|
||||
return false; // timed out
|
||||
}
|
||||
|
||||
pub fn status(id: u64, cmd_index: u64) u8 {
|
||||
return @intCast(device.ioRead(id, cmd_index, 0, 1) orelse 0);
|
||||
}
|
||||
|
||||
pub fn sendCommand(id: u64, cmd_index: u64, byte: u8, timeout_nanoseconds: u64) bool {
|
||||
// wait IBF clear
|
||||
if (!waitWritable(id, cmd_index, timeout_nanoseconds)) return false;
|
||||
return device.ioWrite(id, cmd_index, 0, 1, byte);
|
||||
}
|
||||
|
||||
pub fn readData(id: u64, status_index: u64, data_index: u64, timeout_nanoseconds: u64) ?u8 {
|
||||
// OBF lives in the status register (0x64); wait for it there, then read the data port (0x60)
|
||||
if (!waitReadable(id, status_index, timeout_nanoseconds)) return null;
|
||||
return @intCast(device.ioRead(id, data_index, 0, 1) orelse 0);
|
||||
}
|
||||
|
||||
pub fn writeData(id: u64, status_index: u64, data_index: u64, byte: u8, timeout_nanoseconds: u64) bool {
|
||||
// IBF lives in the status register (0x64); wait for it to clear there, then write the data port
|
||||
// (0x60)
|
||||
if (!waitWritable(id, status_index, timeout_nanoseconds)) return false;
|
||||
return device.ioWrite(id, data_index, 0, 1, byte);
|
||||
}
|
||||
|
||||
pub const Port = enum(u2) {
|
||||
one,
|
||||
two,
|
||||
|
||||
/// Command register byte that disables this port.
|
||||
fn disableCommand(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.one => cmd_disable_first_port,
|
||||
.two => cmd_disable_second_port,
|
||||
};
|
||||
}
|
||||
|
||||
/// Command register byte that enables this port (and its clock).
|
||||
fn enableCommand(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.one => cmd_enable_first_port,
|
||||
.two => cmd_enable_second_port,
|
||||
};
|
||||
}
|
||||
|
||||
/// Command register byte that runs this port's interface test.
|
||||
fn testCommand(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.one => cmd_test_first_port,
|
||||
.two => cmd_test_second_port,
|
||||
};
|
||||
}
|
||||
|
||||
/// Configuration-byte bit that, when set, disables this port's clock.
|
||||
pub fn clockDisabledBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.one => configuration_first_port_clock_disabled,
|
||||
.two => configuration_second_port_clock_disabled,
|
||||
};
|
||||
}
|
||||
|
||||
/// Configuration-byte bit that, when set, enables this port's interrupt.
|
||||
pub fn interruptBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.one => configuration_first_port_interrupt,
|
||||
.two => configuration_second_port_interrupt,
|
||||
};
|
||||
}
|
||||
|
||||
/// Command register byte that writes the next data byte into this port's
|
||||
/// output buffer (makes a byte appear as if it came from the device).
|
||||
pub fn writeOutputBufferCommand(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.one => cmd_write_first_port_output,
|
||||
.two => cmd_write_second_port_output,
|
||||
};
|
||||
}
|
||||
|
||||
/// Controller command that must prefix a byte destined for this port's
|
||||
/// device. Port 1 is the default target of the data port, so it needs no
|
||||
/// prefix (null); port 2 requires the "write second port input" command.
|
||||
pub fn deviceInputCommand(self: Port) ?u8 {
|
||||
return switch (self) {
|
||||
.one => null,
|
||||
.two => cmd_write_second_port_input,
|
||||
};
|
||||
}
|
||||
|
||||
/// Controller output-port bit driving this port's clock line.
|
||||
pub fn outputPortClockBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.one => output_port_first_port_clock,
|
||||
.two => output_port_second_port_clock,
|
||||
};
|
||||
}
|
||||
|
||||
/// Controller output-port bit driving this port's data line.
|
||||
pub fn outputPortDataBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.one => output_port_first_port_data,
|
||||
.two => output_port_second_port_data,
|
||||
};
|
||||
}
|
||||
|
||||
/// Controller output-port bit set when this port's output buffer is full
|
||||
/// (wired to the port's IRQ line).
|
||||
pub fn outputPortBufferFullBit(self: Port) u8 {
|
||||
return switch (self) {
|
||||
.one => output_port_first_port_output_full,
|
||||
.two => output_port_second_port_output_full,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// The kind of device attached to a port, as reported by the device itself in
|
||||
/// response to the identify command — not assumed from the port number. Fixed
|
||||
/// `u32` values because the type also travels in an `AttachRequest`.
|
||||
pub const DeviceType = enum(u32) {
|
||||
keyboard = 0,
|
||||
mouse = 1,
|
||||
unknown = 2,
|
||||
|
||||
/// Initial-ramdisk name of the driver that serves this device type, or null
|
||||
/// if we could not classify it.
|
||||
pub fn driverName(self: DeviceType) ?[]const u8 {
|
||||
return switch (self) {
|
||||
.keyboard => "ps2-keyboard",
|
||||
.mouse => "ps2-mouse",
|
||||
.unknown => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Canonical ACPI HID for this device type, handed to the spawned driver as
|
||||
/// its command-line argument, or null if we could not classify it.
|
||||
pub fn hid(self: DeviceType) ?[]const u8 {
|
||||
return switch (self) {
|
||||
.keyboard => acpi_ids.HardwareId.ps2_keyboard.hid(),
|
||||
.mouse => acpi_ids.HardwareId.ps2_mouse.hid(),
|
||||
.unknown => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// --- the bus <-> child-driver forwarding protocol -----------------------------
|
||||
//
|
||||
// The 8042's ports and IRQ1 live on the PNP0303 node that only the ps2-bus driver
|
||||
// claims, so the child device drivers (ps2-keyboard, ps2-mouse) cannot read port
|
||||
// 0x60 themselves. Instead each child **attaches**: it calls the bus's well-known
|
||||
// `ps2_bus` endpoint with an `AttachRequest`, handing over its own endpoint as the
|
||||
// call's capability. From then on the bus forwards every byte the device sends as
|
||||
// a `ForwardedByte` via the asynchronous `ipc.send` — the IRQ path in the bus can
|
||||
// never block on a slow child, and the child never touches the controller.
|
||||
|
||||
/// A child driver registering for its device's bytes. `device_type` is a
|
||||
/// `DeviceType` value; the child's receive endpoint travels as the call's
|
||||
/// capability (`send_cap`).
|
||||
pub const AttachRequest = extern struct {
|
||||
device_type: u32,
|
||||
};
|
||||
|
||||
/// How the bus answered an `AttachRequest` (`AttachReply.status`).
|
||||
pub const AttachStatus = enum(i32) {
|
||||
ok = 0,
|
||||
/// The request was malformed (too short to be an `AttachRequest`).
|
||||
invalid_request = -1,
|
||||
/// The call carried no endpoint capability to forward to.
|
||||
missing_endpoint = -2,
|
||||
/// No port identified a device of the requested type.
|
||||
no_such_device = -3,
|
||||
};
|
||||
|
||||
/// Reply to an `AttachRequest`. `status` is an `AttachStatus` value.
|
||||
pub const AttachReply = extern struct {
|
||||
status: i32,
|
||||
_padding: u32 = 0,
|
||||
};
|
||||
|
||||
/// One raw byte read from the data port, forwarded to the attached child whose
|
||||
/// port it came from (routed by the status register's auxiliary-output bit).
|
||||
pub const ForwardedByte = extern struct {
|
||||
/// The `Port` the byte came from, as `@intFromEnum`.
|
||||
port: u32,
|
||||
byte: u32,
|
||||
};
|
||||
|
||||
/// A single PS/2 (8042) controller. Construct one with `Controller.init` and
|
||||
/// drive the controller through its methods; there is only ever one 8042 per
|
||||
/// machine, but holding the resolved resource indices in an instance keeps the
|
||||
/// call sites free of global state.
|
||||
pub const Controller = struct {
|
||||
device_id: u64,
|
||||
/// Resource index of the command/status port (0x64).
|
||||
status_index: u64,
|
||||
/// Resource index of the data port (0x60).
|
||||
data_index: u64,
|
||||
|
||||
/// Resolve the controller's IO-port resource indices from its device
|
||||
/// descriptor. Returns null if either the data or command/status port is
|
||||
/// missing from the descriptor.
|
||||
pub fn init(device_descriptor: device.DeviceDescriptor) ?Controller {
|
||||
var data_index: ?u64 = null;
|
||||
var status_index: ?u64 = null;
|
||||
|
||||
for (device_descriptor.resources, 0..device_descriptor.resource_count) |resource, resource_index| {
|
||||
if (resource.kind != @intFromEnum(device.ResourceKind.io_port)) continue;
|
||||
if (resource.start == dataPort) {
|
||||
data_index = @intCast(resource_index);
|
||||
} else if (resource.start == statusRegisterPort) {
|
||||
status_index = @intCast(resource_index);
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.device_id = device_descriptor.id,
|
||||
.data_index = data_index orelse return null,
|
||||
.status_index = status_index orelse return null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn disablePort(self: Controller, port: Port) void {
|
||||
// port enable/disable are controller commands and go to the command register (0x64)
|
||||
_ = sendCommand(self.device_id, self.status_index, port.disableCommand(), default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
pub fn enablePort(self: Controller, port: Port) void {
|
||||
// enabling a port also starts its clock
|
||||
_ = sendCommand(self.device_id, self.status_index, port.enableCommand(), default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
/// Run a port's interface test. Returns the controller's reply — compare it
|
||||
/// to `response_port_test_passed` (0x00) — or null on timeout.
|
||||
pub fn testPort(self: Controller, port: Port) ?u8 {
|
||||
if (!sendCommand(self.device_id, self.status_index, port.testCommand(), default_wait_timeout_nanoseconds)) return null;
|
||||
return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
pub fn flushOutputBuffer(self: Controller) void {
|
||||
// flush any stale byte the controller buffered
|
||||
_ = device.ioRead(self.device_id, self.data_index, 0, 1);
|
||||
}
|
||||
|
||||
pub fn readConfigurationByte(self: Controller) ?u8 {
|
||||
// ask the controller to place its configuration byte in the output buffer, then read it
|
||||
if (!sendCommand(self.device_id, self.status_index, cmd_read_configuration_byte, default_wait_timeout_nanoseconds)) return null;
|
||||
return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
pub fn writeConfigurationByte(self: Controller, update_byte: u8) ?u8 {
|
||||
// command 0x60 makes the controller store the next data-port byte as its configuration byte
|
||||
if (!sendCommand(self.device_id, self.status_index, cmd_write_configuration_byte, default_wait_timeout_nanoseconds)) return null;
|
||||
if (!writeData(self.device_id, self.status_index, self.data_index, update_byte, default_wait_timeout_nanoseconds)) return null;
|
||||
return update_byte;
|
||||
}
|
||||
|
||||
/// Run the controller self-test. Returns the reply — compare it to
|
||||
/// `response_controller_test_passed` (0x55) — or null on timeout.
|
||||
pub fn performSelfTest(self: Controller) ?u8 {
|
||||
if (!sendCommand(self.device_id, self.status_index, cmd_test_controller, default_wait_timeout_nanoseconds)) return null;
|
||||
return readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
}
|
||||
|
||||
/// Detect whether this is a dual-channel controller by temporarily enabling
|
||||
/// port 2 and checking whether its clock turned on. Note: this leaves port 2
|
||||
/// enabled; the caller should disable it again to keep the bus quiet until
|
||||
/// device bring-up.
|
||||
pub fn hasTwoChannels(self: Controller) ?bool {
|
||||
self.enablePort(.two);
|
||||
const configuration = self.readConfigurationByte() orelse return null;
|
||||
return (configuration & Port.two.clockDisabledBit()) == 0;
|
||||
}
|
||||
|
||||
/// Reset the device attached to `port` (device command 0xFF) and wait for
|
||||
/// its power-on self-test (BAT) result. Returns true if the device both
|
||||
/// acknowledged and passed, false if it reported a self-test failure, or
|
||||
/// null on timeout. The BAT reply can be slow, so the response reads use
|
||||
/// `device_reset_timeout_nanoseconds`.
|
||||
pub fn resetDevice(self: Controller, port: Port) ?bool {
|
||||
// A byte destined for port 2 must be prefixed with the "write to second
|
||||
// port input buffer" controller command (0xD4); port 1 is the default.
|
||||
if (port.deviceInputCommand()) |prefix| {
|
||||
if (!sendCommand(self.device_id, self.status_index, prefix, default_wait_timeout_nanoseconds)) return null;
|
||||
}
|
||||
if (!writeData(self.device_id, self.status_index, self.data_index, device_cmd_reset, default_wait_timeout_nanoseconds)) return null;
|
||||
|
||||
// A successful reset yields both an ACK (0xFA) and a self-test-passed
|
||||
// byte (0xAA). Their order is not guaranteed, so accept either ordering.
|
||||
var saw_acknowledge = false;
|
||||
var saw_self_test_passed = false;
|
||||
var reads: u8 = 0;
|
||||
while (reads < 2) : (reads += 1) {
|
||||
const reply = readData(self.device_id, self.status_index, self.data_index, device_reset_timeout_nanoseconds) orelse return null;
|
||||
switch (reply) {
|
||||
device_response_acknowledge => saw_acknowledge = true,
|
||||
device_response_self_test_passed => saw_self_test_passed = true,
|
||||
device_response_self_test_failed_1, device_response_self_test_failed_2 => return false,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return saw_acknowledge and saw_self_test_passed;
|
||||
}
|
||||
|
||||
/// Send one command byte to the device on `port` (applying the port-2 prefix
|
||||
/// as needed) and consume its acknowledgement. Returns true on ACK (0xFA),
|
||||
/// false on any other reply, or null on timeout.
|
||||
pub fn sendToDevice(self: Controller, port: Port, byte: u8) ?bool {
|
||||
if (port.deviceInputCommand()) |prefix| {
|
||||
if (!sendCommand(self.device_id, self.status_index, prefix, default_wait_timeout_nanoseconds)) return null;
|
||||
}
|
||||
if (!writeData(self.device_id, self.status_index, self.data_index, byte, default_wait_timeout_nanoseconds)) return null;
|
||||
const reply = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds) orelse return null;
|
||||
return reply == device_response_acknowledge;
|
||||
}
|
||||
|
||||
/// Discard any bytes sitting in the output buffer (for example the device-id
|
||||
/// byte a mouse emits after a reset) so they cannot be mistaken for the reply
|
||||
/// to a subsequent command.
|
||||
pub fn drainOutputBuffer(self: Controller) void {
|
||||
var guard: u8 = 0;
|
||||
while (guard < 16) : (guard += 1) {
|
||||
if (status(self.device_id, self.status_index) & status_output_buffer_full == 0) return;
|
||||
_ = device.ioRead(self.device_id, self.data_index, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask the device on `port` what it is (command 0xF2) and classify the reply.
|
||||
/// Scanning is disabled around the query so a streaming device cannot inject
|
||||
/// data bytes that look like the identifier. Returns the device type, or null
|
||||
/// if the identify command itself timed out.
|
||||
pub fn identifyDevice(self: Controller, port: Port) ?DeviceType {
|
||||
// Clear any leftover bytes (e.g. a post-reset mouse id) before we start.
|
||||
self.drainOutputBuffer();
|
||||
|
||||
// Stop the device reporting so its data can't be mistaken for the reply.
|
||||
if (self.sendToDevice(port, device_cmd_disable_scanning) == null) return null;
|
||||
|
||||
if (self.sendToDevice(port, device_cmd_identify) == null) return null;
|
||||
|
||||
// After the ACK, the device sends 0, 1, or 2 identifier bytes.
|
||||
const first = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
const device_type: DeviceType = if (first) |id| switch (id) {
|
||||
identify_keyboard_mf2 => blk: {
|
||||
// A MF2 keyboard sends a second subtype byte; consume and ignore it.
|
||||
_ = readData(self.device_id, self.status_index, self.data_index, default_wait_timeout_nanoseconds);
|
||||
break :blk .keyboard;
|
||||
},
|
||||
identify_mouse_standard, identify_mouse_scroll, identify_mouse_five_button => .mouse,
|
||||
else => .unknown,
|
||||
} else
|
||||
// No identifier bytes at all is a legacy AT keyboard.
|
||||
.keyboard;
|
||||
|
||||
// Resume scanning so the device works once its driver takes over.
|
||||
_ = self.sendToDevice(port, device_cmd_enable_scanning);
|
||||
return device_type;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,389 @@
|
||||
//! PS/2 scancode set 2 → USB HID usage decoding, plus the keyboard state a driver
|
||||
//! needs on top of it (pressed keys, modifier tracking, caps-lock toggle).
|
||||
//!
|
||||
//! Set 2 is what a keyboard sends when the 8042's legacy set-1 translation is off —
|
||||
//! which is how ps2-bus.zig deliberately configures the controller. A key's **make**
|
||||
//! code is one byte (two with an `E0` prefix for the "extended" keys added after the
|
||||
//! original AT layout); its **break** code is the same code behind an `F0` prefix.
|
||||
//! Pause alone is an eight-byte `E1` sequence with no break.
|
||||
//!
|
||||
//! The output vocabulary is USB HID keyboard-page usages (a=4, enter=40, ...), the
|
||||
//! same numbering the input protocol's `Keycode` and the xkeyboard-config layout
|
||||
//! tables use — so a decoded usage indexes a layout directly.
|
||||
//!
|
||||
//! Everything here is pure (no imports beyond `std`, no IO), so it is host-testable:
|
||||
//! the tests at the bottom run under `zig build test`.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// --- USB HID usages the state machine itself needs to recognize --------------
|
||||
|
||||
pub const usage_caps_lock: u8 = 0x39;
|
||||
pub const usage_left_control: u8 = 0xE0;
|
||||
pub const usage_left_shift: u8 = 0xE1;
|
||||
pub const usage_left_alt: u8 = 0xE2;
|
||||
pub const usage_right_control: u8 = 0xE4;
|
||||
pub const usage_right_shift: u8 = 0xE5;
|
||||
pub const usage_right_alt: u8 = 0xE6; // AltGr — selects XKB level 3
|
||||
|
||||
// --- scancode set 2 → HID usage tables ---------------------------------------
|
||||
|
||||
/// Single-byte (non-`E0`) make codes. Zero means "no key" — protocol bytes (ACK,
|
||||
/// BAT results) and reserved codes land there and decode to nothing.
|
||||
pub const set2_base: [256]u8 = blk: {
|
||||
var table = [_]u8{0} ** 256;
|
||||
// function row
|
||||
table[0x01] = 0x42; // F9
|
||||
table[0x03] = 0x3E; // F5
|
||||
table[0x04] = 0x3C; // F3
|
||||
table[0x05] = 0x3A; // F1
|
||||
table[0x06] = 0x3B; // F2
|
||||
table[0x07] = 0x45; // F12
|
||||
table[0x09] = 0x43; // F10
|
||||
table[0x0A] = 0x41; // F8
|
||||
table[0x0B] = 0x3F; // F6
|
||||
table[0x0C] = 0x3D; // F4
|
||||
table[0x78] = 0x44; // F11
|
||||
table[0x83] = 0x40; // F7
|
||||
// letters
|
||||
table[0x1C] = 0x04; // A
|
||||
table[0x32] = 0x05; // B
|
||||
table[0x21] = 0x06; // C
|
||||
table[0x23] = 0x07; // D
|
||||
table[0x24] = 0x08; // E
|
||||
table[0x2B] = 0x09; // F
|
||||
table[0x34] = 0x0A; // G
|
||||
table[0x33] = 0x0B; // H
|
||||
table[0x43] = 0x0C; // I
|
||||
table[0x3B] = 0x0D; // J
|
||||
table[0x42] = 0x0E; // K
|
||||
table[0x4B] = 0x0F; // L
|
||||
table[0x3A] = 0x10; // M
|
||||
table[0x31] = 0x11; // N
|
||||
table[0x44] = 0x12; // O
|
||||
table[0x4D] = 0x13; // P
|
||||
table[0x15] = 0x14; // Q
|
||||
table[0x2D] = 0x15; // R
|
||||
table[0x1B] = 0x16; // S
|
||||
table[0x2C] = 0x17; // T
|
||||
table[0x3C] = 0x18; // U
|
||||
table[0x2A] = 0x19; // V
|
||||
table[0x1D] = 0x1A; // W
|
||||
table[0x22] = 0x1B; // X
|
||||
table[0x35] = 0x1C; // Y
|
||||
table[0x1A] = 0x1D; // Z
|
||||
// digit row
|
||||
table[0x16] = 0x1E; // 1
|
||||
table[0x1E] = 0x1F; // 2
|
||||
table[0x26] = 0x20; // 3
|
||||
table[0x25] = 0x21; // 4
|
||||
table[0x2E] = 0x22; // 5
|
||||
table[0x36] = 0x23; // 6
|
||||
table[0x3D] = 0x24; // 7
|
||||
table[0x3E] = 0x25; // 8
|
||||
table[0x46] = 0x26; // 9
|
||||
table[0x45] = 0x27; // 0
|
||||
// control and whitespace
|
||||
table[0x5A] = 0x28; // Enter
|
||||
table[0x76] = 0x29; // Escape
|
||||
table[0x66] = 0x2A; // Backspace
|
||||
table[0x0D] = 0x2B; // Tab
|
||||
table[0x29] = 0x2C; // Space
|
||||
// punctuation
|
||||
table[0x4E] = 0x2D; // - _
|
||||
table[0x55] = 0x2E; // = +
|
||||
table[0x54] = 0x2F; // [ {
|
||||
table[0x5B] = 0x30; // ] }
|
||||
table[0x5D] = 0x31; // \ | (non-US hash on ISO boards, same position)
|
||||
table[0x4C] = 0x33; // ; :
|
||||
table[0x52] = 0x34; // ' "
|
||||
table[0x0E] = 0x35; // ` ~
|
||||
table[0x41] = 0x36; // , <
|
||||
table[0x49] = 0x37; // . >
|
||||
table[0x4A] = 0x38; // / ?
|
||||
table[0x61] = 0x64; // non-US backslash (the extra ISO key between shift and Z)
|
||||
// locks
|
||||
table[0x58] = usage_caps_lock;
|
||||
table[0x77] = 0x53; // Num Lock
|
||||
table[0x7E] = 0x47; // Scroll Lock
|
||||
// keypad
|
||||
table[0x7C] = 0x55; // keypad *
|
||||
table[0x7B] = 0x56; // keypad -
|
||||
table[0x79] = 0x57; // keypad +
|
||||
table[0x69] = 0x59; // keypad 1
|
||||
table[0x72] = 0x5A; // keypad 2
|
||||
table[0x7A] = 0x5B; // keypad 3
|
||||
table[0x6B] = 0x5C; // keypad 4
|
||||
table[0x73] = 0x5D; // keypad 5
|
||||
table[0x74] = 0x5E; // keypad 6
|
||||
table[0x6C] = 0x5F; // keypad 7
|
||||
table[0x75] = 0x60; // keypad 8
|
||||
table[0x7D] = 0x61; // keypad 9
|
||||
table[0x70] = 0x62; // keypad 0
|
||||
table[0x71] = 0x63; // keypad .
|
||||
// modifiers
|
||||
table[0x14] = usage_left_control;
|
||||
table[0x12] = usage_left_shift;
|
||||
table[0x11] = usage_left_alt;
|
||||
table[0x59] = usage_right_shift;
|
||||
break :blk table;
|
||||
};
|
||||
|
||||
/// `E0`-prefixed make codes. `E0 12` is the "fake shift" the keyboard wraps around
|
||||
/// Print Screen and navigation keys when a real shift is involved; it maps to zero
|
||||
/// here, so it decodes to nothing and only the real key comes through.
|
||||
pub const set2_extended: [256]u8 = blk: {
|
||||
var table = [_]u8{0} ** 256;
|
||||
table[0x11] = usage_right_alt;
|
||||
table[0x14] = usage_right_control;
|
||||
table[0x1F] = 0xE3; // left GUI
|
||||
table[0x27] = 0xE7; // right GUI
|
||||
table[0x2F] = 0x65; // application (menu)
|
||||
table[0x7C] = 0x46; // Print Screen (arrives as E0 12 E0 7C; the E0 12 decodes to nothing)
|
||||
table[0x4A] = 0x54; // keypad /
|
||||
table[0x5A] = 0x58; // keypad Enter
|
||||
table[0x70] = 0x49; // Insert
|
||||
table[0x6C] = 0x4A; // Home
|
||||
table[0x7D] = 0x4B; // Page Up
|
||||
table[0x71] = 0x4C; // Delete
|
||||
table[0x69] = 0x4D; // End
|
||||
table[0x7A] = 0x4E; // Page Down
|
||||
table[0x74] = 0x4F; // right arrow
|
||||
table[0x6B] = 0x50; // left arrow
|
||||
table[0x72] = 0x51; // down arrow
|
||||
table[0x75] = 0x52; // up arrow
|
||||
break :blk table;
|
||||
};
|
||||
|
||||
// --- the byte-stream decoder --------------------------------------------------
|
||||
|
||||
/// One decoded key transition: which key (as a USB HID usage) and whether this is
|
||||
/// a make (press or typematic repeat) or a break (release).
|
||||
pub const DecodedKey = struct {
|
||||
usage: u8,
|
||||
make: bool,
|
||||
};
|
||||
|
||||
/// Turns the raw set-2 byte stream into `DecodedKey`s. Feed it every byte the
|
||||
/// keyboard sends; most bytes complete a key and return one, prefix bytes return
|
||||
/// null and arm the state machine for the next byte.
|
||||
pub const Decoder = struct {
|
||||
const State = enum {
|
||||
idle,
|
||||
extended, // saw E0
|
||||
break_prefix, // saw F0
|
||||
extended_break, // saw E0 F0
|
||||
pause_skip, // inside the 8-byte E1 Pause sequence
|
||||
};
|
||||
|
||||
state: State = .idle,
|
||||
/// Bytes still to swallow in `pause_skip`.
|
||||
skip: u8 = 0,
|
||||
|
||||
/// The whole Pause make sequence is `E1 14 77 E1 F0 14 F0 77` — seven bytes
|
||||
/// after the leading `E1`, and no break sequence ever follows.
|
||||
const pause_bytes_after_e1: u8 = 7;
|
||||
|
||||
pub fn feed(self: *Decoder, byte: u8) ?DecodedKey {
|
||||
switch (self.state) {
|
||||
.idle => switch (byte) {
|
||||
0xE0 => self.state = .extended,
|
||||
0xF0 => self.state = .break_prefix,
|
||||
0xE1 => {
|
||||
self.state = .pause_skip;
|
||||
self.skip = pause_bytes_after_e1;
|
||||
},
|
||||
// Anything else is a make code — or a protocol byte (0xFA ACK,
|
||||
// 0xAA BAT-passed, 0xEE echo, ...), which the tables map to zero.
|
||||
else => return decoded(set2_base[byte], true),
|
||||
},
|
||||
.extended => switch (byte) {
|
||||
0xF0 => self.state = .extended_break,
|
||||
else => {
|
||||
self.state = .idle;
|
||||
return decoded(set2_extended[byte], true);
|
||||
},
|
||||
},
|
||||
.break_prefix => {
|
||||
self.state = .idle;
|
||||
return decoded(set2_base[byte], false);
|
||||
},
|
||||
.extended_break => {
|
||||
self.state = .idle;
|
||||
return decoded(set2_extended[byte], false);
|
||||
},
|
||||
.pause_skip => {
|
||||
self.skip -= 1;
|
||||
if (self.skip == 0) self.state = .idle;
|
||||
},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn decoded(usage: u8, make: bool) ?DecodedKey {
|
||||
if (usage == 0) return null; // unmapped or a protocol byte
|
||||
return .{ .usage = usage, .make = make };
|
||||
}
|
||||
};
|
||||
|
||||
// --- driver-side keyboard state -----------------------------------------------
|
||||
|
||||
/// What a key transition did, plus the modifier state to stamp on the resulting
|
||||
/// events (snapshotted after the transition was applied).
|
||||
pub const Transition = struct {
|
||||
pub const Action = enum {
|
||||
pressed, // physical make of a key that was up
|
||||
repeated, // typematic make of a key already down — no new key_down
|
||||
released, // physical break
|
||||
};
|
||||
action: Action,
|
||||
modifiers: ModifierSnapshot,
|
||||
};
|
||||
|
||||
/// The modifier state at one instant, in both vocabularies a driver needs: the
|
||||
/// input protocol's coarse bits (shift/control/alt) and the level-selection
|
||||
/// inputs xkeyboard-config takes (shift, caps_lock, AltGr as level3).
|
||||
pub const ModifierSnapshot = struct {
|
||||
shift: bool, // either shift held
|
||||
control: bool, // either control held
|
||||
alt: bool, // either alt held (including AltGr)
|
||||
right_alt: bool, // AltGr specifically — the XKB level-3 selector
|
||||
caps_lock: bool, // the toggle, not the key
|
||||
};
|
||||
|
||||
/// Tracks which keys are physically down and the caps-lock toggle, and classifies
|
||||
/// each decoded transition. Pure state — no IO — so repeat detection and modifier
|
||||
/// snapshots are host-testable.
|
||||
pub const KeyboardState = struct {
|
||||
/// One bit per HID usage: set while the key is physically down.
|
||||
pressed: [32]u8 = [_]u8{0} ** 32,
|
||||
caps_lock: bool = false,
|
||||
|
||||
pub fn apply(self: *KeyboardState, key: DecodedKey) Transition {
|
||||
const already_down = self.isPressed(key.usage);
|
||||
if (key.make) {
|
||||
if (!already_down) {
|
||||
self.setPressed(key.usage, true);
|
||||
if (key.usage == usage_caps_lock) self.caps_lock = !self.caps_lock;
|
||||
}
|
||||
return .{
|
||||
.action = if (already_down) .repeated else .pressed,
|
||||
.modifiers = self.snapshot(),
|
||||
};
|
||||
}
|
||||
self.setPressed(key.usage, false);
|
||||
return .{ .action = .released, .modifiers = self.snapshot() };
|
||||
}
|
||||
|
||||
pub fn isPressed(self: *const KeyboardState, usage: u8) bool {
|
||||
return self.pressed[usage / 8] & (@as(u8, 1) << @intCast(usage % 8)) != 0;
|
||||
}
|
||||
|
||||
fn setPressed(self: *KeyboardState, usage: u8, down: bool) void {
|
||||
const bit = @as(u8, 1) << @intCast(usage % 8);
|
||||
if (down) {
|
||||
self.pressed[usage / 8] |= bit;
|
||||
} else {
|
||||
self.pressed[usage / 8] &= ~bit;
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(self: *const KeyboardState) ModifierSnapshot {
|
||||
const right_alt = self.isPressed(usage_right_alt);
|
||||
return .{
|
||||
.shift = self.isPressed(usage_left_shift) or self.isPressed(usage_right_shift),
|
||||
.control = self.isPressed(usage_left_control) or self.isPressed(usage_right_control),
|
||||
.alt = self.isPressed(usage_left_alt) or right_alt,
|
||||
.right_alt = right_alt,
|
||||
.caps_lock = self.caps_lock,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// --- tests (host-run via `zig build test`) ------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
/// Feed `bytes` and return the single DecodedKey they should produce (fails the
|
||||
/// test if they produce none or more than one).
|
||||
fn feedOne(decoder: *Decoder, bytes: []const u8) !DecodedKey {
|
||||
var result: ?DecodedKey = null;
|
||||
for (bytes) |byte| {
|
||||
if (decoder.feed(byte)) |key| {
|
||||
try testing.expect(result == null);
|
||||
result = key;
|
||||
}
|
||||
}
|
||||
return result orelse error.TestExpectedResult;
|
||||
}
|
||||
|
||||
fn feedNone(decoder: *Decoder, bytes: []const u8) !void {
|
||||
for (bytes) |byte| try testing.expectEqual(@as(?DecodedKey, null), decoder.feed(byte));
|
||||
}
|
||||
|
||||
test "base make and break: A" {
|
||||
var decoder = Decoder{};
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = true }, try feedOne(&decoder, &.{0x1C}));
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = false }, try feedOne(&decoder, &.{ 0xF0, 0x1C }));
|
||||
}
|
||||
|
||||
test "extended make and break: right arrow" {
|
||||
var decoder = Decoder{};
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x4F, .make = true }, try feedOne(&decoder, &.{ 0xE0, 0x74 }));
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x4F, .make = false }, try feedOne(&decoder, &.{ 0xE0, 0xF0, 0x74 }));
|
||||
}
|
||||
|
||||
test "pause: the E1 sequence is consumed silently" {
|
||||
var decoder = Decoder{};
|
||||
try feedNone(&decoder, &.{ 0xE1, 0x14, 0x77, 0xE1, 0xF0, 0x14, 0xF0, 0x77 });
|
||||
// The decoder is back in idle: an ordinary key still decodes.
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x04, .make = true }, try feedOne(&decoder, &.{0x1C}));
|
||||
}
|
||||
|
||||
test "protocol bytes decode to nothing" {
|
||||
var decoder = Decoder{};
|
||||
try feedNone(&decoder, &.{ 0xFA, 0xAA, 0xEE }); // ACK, BAT-passed, echo
|
||||
}
|
||||
|
||||
test "print screen: the fake-shift E0 12 decodes to nothing" {
|
||||
var decoder = Decoder{};
|
||||
try feedNone(&decoder, &.{ 0xE0, 0x12 });
|
||||
try testing.expectEqual(DecodedKey{ .usage = 0x46, .make = true }, try feedOne(&decoder, &.{ 0xE0, 0x7C }));
|
||||
}
|
||||
|
||||
test "typematic repeat is classified, not re-pressed" {
|
||||
var state = KeyboardState{};
|
||||
const a = DecodedKey{ .usage = 0x04, .make = true };
|
||||
try testing.expectEqual(Transition.Action.pressed, state.apply(a).action);
|
||||
try testing.expectEqual(Transition.Action.repeated, state.apply(a).action);
|
||||
try testing.expectEqual(Transition.Action.repeated, state.apply(a).action);
|
||||
try testing.expectEqual(Transition.Action.released, state.apply(.{ .usage = 0x04, .make = false }).action);
|
||||
try testing.expectEqual(Transition.Action.pressed, state.apply(a).action);
|
||||
}
|
||||
|
||||
test "shift held shows in the snapshot of other keys" {
|
||||
var state = KeyboardState{};
|
||||
_ = state.apply(.{ .usage = usage_left_shift, .make = true });
|
||||
const transition = state.apply(.{ .usage = 0x04, .make = true });
|
||||
try testing.expect(transition.modifiers.shift);
|
||||
try testing.expect(!transition.modifiers.control);
|
||||
_ = state.apply(.{ .usage = usage_left_shift, .make = false });
|
||||
_ = state.apply(.{ .usage = 0x04, .make = false });
|
||||
try testing.expect(!state.apply(.{ .usage = 0x04, .make = true }).modifiers.shift);
|
||||
}
|
||||
|
||||
test "right alt reports both alt and the level-3 selector" {
|
||||
var state = KeyboardState{};
|
||||
_ = state.apply(.{ .usage = usage_right_alt, .make = true });
|
||||
const transition = state.apply(.{ .usage = 0x04, .make = true });
|
||||
try testing.expect(transition.modifiers.alt);
|
||||
try testing.expect(transition.modifiers.right_alt);
|
||||
}
|
||||
|
||||
test "caps lock toggles on make, not on repeat or break" {
|
||||
var state = KeyboardState{};
|
||||
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock);
|
||||
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock); // repeat
|
||||
try testing.expect(state.apply(.{ .usage = usage_caps_lock, .make = false }).modifiers.caps_lock);
|
||||
try testing.expect(!state.apply(.{ .usage = usage_caps_lock, .make = true }).modifiers.caps_lock); // second press: off
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
//! Pure decoders for USB HID **boot-protocol** reports — the simplified,
|
||||
//! fixed-format reports a boot keyboard and boot mouse send, the USB analog of
|
||||
//! the PS/2 scancode and mouse-packet decoders. No I/O: these turn report bytes
|
||||
//! into make/break transitions and motion, which the usb-hid drivers publish to
|
||||
//! the input service. Host-testable in isolation (like mouse-packet.zig).
|
||||
//!
|
||||
//! "Boot protocol" is a USB HID term (USB HID 1.11 §B) — the device reports in
|
||||
//! this fixed layout after SET_PROTOCOL(boot); it has nothing to do with system
|
||||
//! boot.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// --- keyboard ---------------------------------------------------------------
|
||||
|
||||
/// The 8-byte boot keyboard report: a modifier bitmap, a reserved byte, and up
|
||||
/// to six concurrently-pressed key usages.
|
||||
pub const KeyboardReport = extern struct {
|
||||
modifiers: u8 = 0,
|
||||
reserved: u8 = 0,
|
||||
keys: [6]u8 = .{ 0, 0, 0, 0, 0, 0 },
|
||||
};
|
||||
|
||||
// The modifier byte's bits (HID keyboard boot report).
|
||||
pub const modifier_left_control: u8 = 1 << 0;
|
||||
pub const modifier_left_shift: u8 = 1 << 1;
|
||||
pub const modifier_left_alt: u8 = 1 << 2;
|
||||
pub const modifier_left_gui: u8 = 1 << 3;
|
||||
pub const modifier_right_control: u8 = 1 << 4;
|
||||
pub const modifier_right_shift: u8 = 1 << 5;
|
||||
pub const modifier_right_alt: u8 = 1 << 6;
|
||||
pub const modifier_right_gui: u8 = 1 << 7;
|
||||
|
||||
pub const TransitionKind = enum { pressed, released };
|
||||
|
||||
/// One key going down or up. `usage` is a HID keyboard-page usage — modifier keys
|
||||
/// map to usages 224..231 — which is exactly the input protocol's `Keycode`.
|
||||
pub const Transition = struct { kind: TransitionKind, usage: u8 };
|
||||
|
||||
// A report can change at most all 8 modifiers and all 6 keys at once.
|
||||
pub const max_transitions = 8 + 6;
|
||||
|
||||
pub const Transitions = struct {
|
||||
items: [max_transitions]Transition = undefined,
|
||||
count: usize = 0,
|
||||
|
||||
fn add(self: *Transitions, transition: Transition) void {
|
||||
if (self.count < self.items.len) {
|
||||
self.items[self.count] = transition;
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slice(self: *const Transitions) []const Transition {
|
||||
return self.items[0..self.count];
|
||||
}
|
||||
};
|
||||
|
||||
/// Turns a stream of boot keyboard reports into make/break transitions by diffing
|
||||
/// each report against the last.
|
||||
pub const KeyboardDecoder = struct {
|
||||
previous: KeyboardReport = .{},
|
||||
|
||||
pub fn feed(self: *KeyboardDecoder, current: KeyboardReport) Transitions {
|
||||
var out = Transitions{};
|
||||
|
||||
// Rollover: 0x01 (ErrorRollOver) means more keys are held than the report
|
||||
// can carry, so the key array is invalid. Emit nothing and keep the prior
|
||||
// state (so the eventual releases still resolve against real keys).
|
||||
for (current.keys) |key| {
|
||||
if (key == 0x01) return out;
|
||||
}
|
||||
|
||||
// Modifiers: one make/break per changed bit; modifier usages are 224..231.
|
||||
const changed = current.modifiers ^ self.previous.modifiers;
|
||||
var bit: u3 = 0;
|
||||
while (true) : (bit += 1) {
|
||||
const mask = @as(u8, 1) << bit;
|
||||
if (changed & mask != 0) {
|
||||
out.add(.{
|
||||
.kind = if (current.modifiers & mask != 0) .pressed else .released,
|
||||
.usage = 224 + @as(u8, bit),
|
||||
});
|
||||
}
|
||||
if (bit == 7) break;
|
||||
}
|
||||
|
||||
// Keys made: present now, absent before.
|
||||
for (current.keys) |key| {
|
||||
if (key != 0 and !contains(&self.previous.keys, key)) out.add(.{ .kind = .pressed, .usage = key });
|
||||
}
|
||||
// Keys broken: present before, absent now.
|
||||
for (self.previous.keys) |key| {
|
||||
if (key != 0 and !contains(¤t.keys, key)) out.add(.{ .kind = .released, .usage = key });
|
||||
}
|
||||
|
||||
self.previous = current;
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
fn contains(keys: *const [6]u8, value: u8) bool {
|
||||
for (keys) |key| {
|
||||
if (key == value) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- mouse ------------------------------------------------------------------
|
||||
|
||||
/// A decoded boot mouse report: the button bitmap and relative motion. The wheel
|
||||
/// byte is present only on 4-byte reports (QEMU's usb-mouse sends one).
|
||||
pub const MouseReport = struct {
|
||||
buttons: u8 = 0,
|
||||
dx: i8 = 0,
|
||||
dy: i8 = 0,
|
||||
wheel: i8 = 0,
|
||||
has_wheel: bool = false,
|
||||
};
|
||||
|
||||
pub const mouse_button_left: u8 = 1 << 0;
|
||||
pub const mouse_button_right: u8 = 1 << 1;
|
||||
pub const mouse_button_middle: u8 = 1 << 2;
|
||||
|
||||
/// Parse a 3- or 4-byte boot mouse report. Note HID reports Y in screen
|
||||
/// convention (positive = down), so — unlike PS/2 — `dy` is NOT negated.
|
||||
pub fn parseMouse(bytes: []const u8) ?MouseReport {
|
||||
if (bytes.len < 3) return null;
|
||||
return .{
|
||||
.buttons = bytes[0],
|
||||
.dx = @bitCast(bytes[1]),
|
||||
.dy = @bitCast(bytes[2]),
|
||||
.wheel = if (bytes.len >= 4) @bitCast(bytes[3]) else 0,
|
||||
.has_wheel = bytes.len >= 4,
|
||||
};
|
||||
}
|
||||
|
||||
// --- tests ------------------------------------------------------------------
|
||||
|
||||
test "keyboard diff produces make and break transitions" {
|
||||
var decoder = KeyboardDecoder{};
|
||||
|
||||
// Press 'a' (usage 4).
|
||||
var t = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } });
|
||||
try std.testing.expectEqual(@as(usize, 1), t.count);
|
||||
try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind);
|
||||
try std.testing.expectEqual(@as(u8, 4), t.items[0].usage);
|
||||
|
||||
// Hold 'a', press 'b' (usage 5): only 'b' is new.
|
||||
t = decoder.feed(.{ .keys = .{ 4, 5, 0, 0, 0, 0 } });
|
||||
try std.testing.expectEqual(@as(usize, 1), t.count);
|
||||
try std.testing.expectEqual(@as(u8, 5), t.items[0].usage);
|
||||
|
||||
// Release everything: 'a' and 'b' both break.
|
||||
t = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } });
|
||||
try std.testing.expectEqual(@as(usize, 2), t.count);
|
||||
try std.testing.expectEqual(TransitionKind.released, t.items[0].kind);
|
||||
|
||||
// Press Left Shift (modifier bit 1 -> usage 225).
|
||||
t = decoder.feed(.{ .modifiers = modifier_left_shift });
|
||||
try std.testing.expectEqual(@as(usize, 1), t.count);
|
||||
try std.testing.expectEqual(@as(u8, 225), t.items[0].usage);
|
||||
try std.testing.expectEqual(TransitionKind.pressed, t.items[0].kind);
|
||||
}
|
||||
|
||||
test "rollover report is ignored but state is preserved" {
|
||||
var decoder = KeyboardDecoder{};
|
||||
_ = decoder.feed(.{ .keys = .{ 4, 0, 0, 0, 0, 0 } }); // press 'a'
|
||||
|
||||
const rollover = decoder.feed(.{ .keys = .{ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 } });
|
||||
try std.testing.expectEqual(@as(usize, 0), rollover.count);
|
||||
|
||||
// 'a' is still considered down, so releasing all keys now breaks it.
|
||||
const release = decoder.feed(.{ .keys = .{ 0, 0, 0, 0, 0, 0 } });
|
||||
try std.testing.expectEqual(@as(usize, 1), release.count);
|
||||
try std.testing.expectEqual(@as(u8, 4), release.items[0].usage);
|
||||
try std.testing.expectEqual(TransitionKind.released, release.items[0].kind);
|
||||
}
|
||||
|
||||
test "mouse report parses motion without inverting Y" {
|
||||
const three = parseMouse(&.{ mouse_button_left, 5, 0xFB }).?; // dy = -5
|
||||
try std.testing.expectEqual(mouse_button_left, three.buttons);
|
||||
try std.testing.expectEqual(@as(i8, 5), three.dx);
|
||||
try std.testing.expectEqual(@as(i8, -5), three.dy);
|
||||
try std.testing.expect(!three.has_wheel);
|
||||
|
||||
const four = parseMouse(&.{ 0, 0, 0, 0xFF }).?; // wheel = -1
|
||||
try std.testing.expect(four.has_wheel);
|
||||
try std.testing.expectEqual(@as(i8, -1), four.wheel);
|
||||
|
||||
try std.testing.expect(parseMouse(&.{ 0, 0 }) == null); // too short
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//! USB HID boot keyboard driver.
|
||||
//!
|
||||
//! Spawned by the device manager when the xHCI bus driver reports a HID / boot /
|
||||
//! keyboard interface (class 3, subclass 1, protocol 1); its assigned device id
|
||||
//! arrives as argv[1] and an optional layout name ("us", "gb", ...) as argv[2].
|
||||
//! It owns no hardware: it opens its device through the USB transfer protocol
|
||||
//! (`runtime.usb`), asks the device for the boot protocol, subscribes to its
|
||||
//! interrupt-IN endpoint, and turns each 8-byte boot report into input-protocol
|
||||
//! events, published to the input service — the USB analogue of ps2-bus/keyboard.
|
||||
//!
|
||||
//! interrupt report -> hid-report diff -> key_down / key_up
|
||||
//! -> xkeyboard-config -> character -> key_press
|
||||
//!
|
||||
//! Because a USB keyboard's usages ARE the input protocol's keycodes (both are
|
||||
//! HID keyboard page 0x07), the decode is nearly 1:1 — no scancode translation.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const usb_abi = @import("usb-abi");
|
||||
const xkb = @import("xkeyboard-config");
|
||||
const hid = @import("hid-report.zig");
|
||||
const ipc = runtime.ipc;
|
||||
const process = runtime.process;
|
||||
const input_protocol = runtime.input_protocol;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
// The modifier state a character lookup needs — derived from the report's
|
||||
// modifier byte, plus the driver-tracked caps-lock toggle.
|
||||
const ModifierSnapshot = struct {
|
||||
shift: bool,
|
||||
control: bool,
|
||||
right_alt: bool,
|
||||
caps_lock: bool,
|
||||
};
|
||||
|
||||
/// The character a key produces under `modifiers`, or 0 for none — the layout
|
||||
/// lookup for printable keys, with ASCII control characters for the keys every
|
||||
/// consumer expects (Enter, Tab, Backspace, Escape), exactly as ps2-bus/keyboard.
|
||||
fn characterFor(layout: *const xkb.Layout, usage: u8, modifiers: ModifierSnapshot) u32 {
|
||||
const mapping = xkb.map(layout, usage, .{
|
||||
.shift = modifiers.shift,
|
||||
.caps_lock = modifiers.caps_lock,
|
||||
.level3 = modifiers.right_alt,
|
||||
.control = modifiers.control,
|
||||
});
|
||||
if (mapping.character) |character| return character;
|
||||
return switch (@as(input_protocol.Keycode, @enumFromInt(usage))) {
|
||||
.enter, .keypad_enter => '\n',
|
||||
.tab => '\t',
|
||||
.backspace => 0x08,
|
||||
.escape => 0x1B,
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn modifierWord(modifiers: u8) u32 {
|
||||
var word: u32 = 0;
|
||||
if (modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0) word |= input_protocol.modifier_shift;
|
||||
if (modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0) word |= input_protocol.modifier_control;
|
||||
if (modifiers & (hid.modifier_left_alt | hid.modifier_right_alt) != 0) word |= input_protocol.modifier_alt;
|
||||
return word;
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const argument = init.arguments.get(1) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: missing device id (argv[1])\n");
|
||||
return;
|
||||
};
|
||||
const device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/usb-hid/keyboard: malformed device id '{s}'\n", .{argument});
|
||||
return;
|
||||
};
|
||||
const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us;
|
||||
|
||||
// Hello the manager first (meet the spawn deadline), then open the device.
|
||||
if (!runtime.usb.helloManager(device_id)) {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: hello to device manager failed\n");
|
||||
return;
|
||||
}
|
||||
var device = runtime.usb.open(device_id) orelse {
|
||||
writeLine("/system/drivers/usb-hid/keyboard: could not open device {d}\n", .{device_id});
|
||||
return;
|
||||
};
|
||||
const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: no interrupt-IN endpoint\n");
|
||||
return;
|
||||
};
|
||||
|
||||
// Ask for the boot protocol and an indefinite idle (report only on change).
|
||||
_ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot)));
|
||||
_ = device.controlOut(@bitCast(usb_abi.setIdle(@enumFromInt(device.interface_number), 0, 0)));
|
||||
|
||||
if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: interrupt subscribe failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
var source = runtime.input.connectSource() orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/keyboard: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = process.bindSignals(device.endpoint);
|
||||
writeLine("/system/drivers/usb-hid/keyboard: ok (device {d}, interface {d}, layout {s})\n", .{ device_id, device.interface_number, layout.name });
|
||||
|
||||
var decoder = hid.KeyboardDecoder{};
|
||||
var caps_lock = false;
|
||||
var receive: [64]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(device.endpoint, &.{}, &receive, null);
|
||||
if (!got.isNotification()) continue;
|
||||
if (process.signalsFrom(got.badge)) |signals| {
|
||||
if (signals.has(.terminate)) return;
|
||||
continue;
|
||||
}
|
||||
if (!got.isMessage() or got.len < @sizeOf(runtime.usb.InterruptReport)) continue;
|
||||
|
||||
const message = std.mem.bytesToValue(runtime.usb.InterruptReport, receive[0..@sizeOf(runtime.usb.InterruptReport)]);
|
||||
if (message.length < @sizeOf(hid.KeyboardReport)) continue;
|
||||
const report = std.mem.bytesToValue(hid.KeyboardReport, message.data[0..@sizeOf(hid.KeyboardReport)]);
|
||||
const transitions = decoder.feed(report);
|
||||
|
||||
// Caps Lock toggles on its own key-down (a stateful lock, not a modifier).
|
||||
for (transitions.slice()) |transition| {
|
||||
if (transition.kind == .pressed and @as(input_protocol.Keycode, @enumFromInt(transition.usage)) == .caps_lock) caps_lock = !caps_lock;
|
||||
}
|
||||
|
||||
const modifiers = ModifierSnapshot{
|
||||
.shift = report.modifiers & (hid.modifier_left_shift | hid.modifier_right_shift) != 0,
|
||||
.control = report.modifiers & (hid.modifier_left_control | hid.modifier_right_control) != 0,
|
||||
.right_alt = report.modifiers & hid.modifier_right_alt != 0,
|
||||
.caps_lock = caps_lock,
|
||||
};
|
||||
const modifier_word = modifierWord(report.modifiers);
|
||||
|
||||
for (transitions.slice()) |transition| {
|
||||
switch (transition.kind) {
|
||||
.pressed => {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(input_protocol.EventKind.key_down),
|
||||
.keycode = transition.usage,
|
||||
.character = 0,
|
||||
.modifiers = modifier_word,
|
||||
});
|
||||
const character = characterFor(layout, transition.usage, modifiers);
|
||||
if (character != 0) {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(input_protocol.EventKind.key_press),
|
||||
.keycode = transition.usage,
|
||||
.character = character,
|
||||
.modifiers = modifier_word,
|
||||
});
|
||||
}
|
||||
},
|
||||
.released => {
|
||||
_ = source.publishKeyboardEvent(.{
|
||||
.kind = @intFromEnum(input_protocol.EventKind.key_up),
|
||||
.keycode = transition.usage,
|
||||
.character = 0,
|
||||
.modifiers = modifier_word,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//! USB HID boot mouse driver.
|
||||
//!
|
||||
//! Spawned by the device manager when the xHCI bus driver reports a HID / boot /
|
||||
//! mouse interface (class 3, subclass 1, protocol 2); its assigned device id
|
||||
//! arrives as argv[1]. Like the keyboard driver it owns no hardware: it opens its
|
||||
//! device through the USB transfer protocol (`runtime.usb`), asks for the boot
|
||||
//! protocol, subscribes to its interrupt-IN endpoint, and turns each 3- or 4-byte
|
||||
//! boot report into input-protocol mouse events published to the input service.
|
||||
//!
|
||||
//! Unlike PS/2, HID reports Y in screen convention (positive = down), so motion
|
||||
//! is passed straight through (the decode in hid-report.zig does not negate it).
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const usb_abi = @import("usb-abi");
|
||||
const hid = @import("hid-report.zig");
|
||||
const ipc = runtime.ipc;
|
||||
const process = runtime.process;
|
||||
const input_protocol = runtime.input_protocol;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
// The current pressed-button bitmask in input-protocol terms.
|
||||
fn buttonMask(buttons: u8) u32 {
|
||||
var mask: u32 = 0;
|
||||
if (buttons & hid.mouse_button_left != 0) mask |= input_protocol.mouse_button_left;
|
||||
if (buttons & hid.mouse_button_right != 0) mask |= input_protocol.mouse_button_right;
|
||||
if (buttons & hid.mouse_button_middle != 0) mask |= input_protocol.mouse_button_middle;
|
||||
return mask;
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const argument = init.arguments.get(1) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/mouse: missing device id (argv[1])\n");
|
||||
return;
|
||||
};
|
||||
const device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/usb-hid/mouse: malformed device id '{s}'\n", .{argument});
|
||||
return;
|
||||
};
|
||||
|
||||
if (!runtime.usb.helloManager(device_id)) {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/mouse: hello to device manager failed\n");
|
||||
return;
|
||||
}
|
||||
var device = runtime.usb.open(device_id) orelse {
|
||||
writeLine("/system/drivers/usb-hid/mouse: could not open device {d}\n", .{device_id});
|
||||
return;
|
||||
};
|
||||
const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/mouse: no interrupt-IN endpoint\n");
|
||||
return;
|
||||
};
|
||||
|
||||
_ = device.controlOut(@bitCast(usb_abi.setProtocol(@enumFromInt(device.interface_number), .boot)));
|
||||
|
||||
if (!device.subscribeInterrupt(endpoint.address, endpoint.max_packet_size)) {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/mouse: interrupt subscribe failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
var source = runtime.input.connectSource() orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-hid/mouse: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = process.bindSignals(device.endpoint);
|
||||
writeLine("/system/drivers/usb-hid/mouse: ok (device {d}, interface {d})\n", .{ device_id, device.interface_number });
|
||||
|
||||
var previous_buttons: u8 = 0;
|
||||
var receive: [64]u8 = undefined;
|
||||
while (true) {
|
||||
const got = ipc.replyWait(device.endpoint, &.{}, &receive, null);
|
||||
if (!got.isNotification()) continue;
|
||||
if (process.signalsFrom(got.badge)) |signals| {
|
||||
if (signals.has(.terminate)) return;
|
||||
continue;
|
||||
}
|
||||
if (!got.isMessage() or got.len < @sizeOf(runtime.usb.InterruptReport)) continue;
|
||||
|
||||
const message = std.mem.bytesToValue(runtime.usb.InterruptReport, receive[0..@sizeOf(runtime.usb.InterruptReport)]);
|
||||
const length = @min(message.length, message.data.len);
|
||||
const report = hid.parseMouse(message.data[0..length]) orelse continue;
|
||||
const mask = buttonMask(report.buttons);
|
||||
|
||||
// Button transitions: one event per changed button bit.
|
||||
const changed = report.buttons ^ previous_buttons;
|
||||
inline for (.{
|
||||
.{ hid.mouse_button_left, input_protocol.mouse_button_left },
|
||||
.{ hid.mouse_button_right, input_protocol.mouse_button_right },
|
||||
.{ hid.mouse_button_middle, input_protocol.mouse_button_middle },
|
||||
}) |pair| {
|
||||
if (changed & pair[0] != 0) {
|
||||
_ = source.publishMouseEvent(.{
|
||||
.kind = @intFromEnum(if (report.buttons & pair[0] != 0) input_protocol.MouseEventKind.button_down else input_protocol.MouseEventKind.button_up),
|
||||
.button = pair[1],
|
||||
.dx = 0,
|
||||
.dy = 0,
|
||||
.scroll_x = 0,
|
||||
.scroll_y = 0,
|
||||
.buttons = mask,
|
||||
});
|
||||
}
|
||||
}
|
||||
previous_buttons = report.buttons;
|
||||
|
||||
// Relative motion (dy straight through — HID Y is already screen convention).
|
||||
if (report.dx != 0 or report.dy != 0) {
|
||||
_ = source.publishMouseEvent(.{
|
||||
.kind = @intFromEnum(input_protocol.MouseEventKind.motion),
|
||||
.button = 0,
|
||||
.dx = report.dx,
|
||||
.dy = report.dy,
|
||||
.scroll_x = 0,
|
||||
.scroll_y = 0,
|
||||
.buttons = mask,
|
||||
});
|
||||
}
|
||||
|
||||
// Wheel (4-byte reports only): positive = scroll up.
|
||||
if (report.has_wheel and report.wheel != 0) {
|
||||
_ = source.publishMouseEvent(.{
|
||||
.kind = @intFromEnum(input_protocol.MouseEventKind.scroll),
|
||||
.button = 0,
|
||||
.dx = 0,
|
||||
.dy = 0,
|
||||
.scroll_x = 0,
|
||||
.scroll_y = report.wheel,
|
||||
.buttons = mask,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//! USB Mass Storage Bulk-Only Transport (BOT) wire structures — the Command and
|
||||
//! Command Status Wrappers that bracket every command (USB MSC BOT §5). Pure data
|
||||
//! definitions, host-testable in isolation. The command inside the CBW is a SCSI
|
||||
//! CDB (see scsi.zig); the transport here just carries it and reports status.
|
||||
//!
|
||||
//! One command is three bulk transfers: CBW out, an optional data stage, CSW in.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// "USBC" — the signature at the head of every Command Block Wrapper.
|
||||
pub const cbw_signature: u32 = 0x43425355;
|
||||
/// "USBS" — the signature at the head of every Command Status Wrapper.
|
||||
pub const csw_signature: u32 = 0x53425355;
|
||||
|
||||
/// CBW `flags`: set for a device-to-host (IN) data stage, clear for OUT.
|
||||
pub const flag_data_in: u8 = 0x80;
|
||||
|
||||
/// The 31-byte Command Block Wrapper, sent on the bulk-OUT endpoint.
|
||||
pub const CommandBlockWrapper = extern struct {
|
||||
signature: u32 align(1) = cbw_signature,
|
||||
tag: u32 align(1),
|
||||
data_transfer_length: u32 align(1),
|
||||
flags: u8,
|
||||
lun: u8,
|
||||
cdb_length: u8,
|
||||
cdb: [16]u8 = [_]u8{0} ** 16,
|
||||
};
|
||||
|
||||
/// A device's answer to a command (the CSW `status` byte).
|
||||
pub const CommandStatus = enum(u8) {
|
||||
passed = 0,
|
||||
failed = 1,
|
||||
phase_error = 2,
|
||||
_,
|
||||
};
|
||||
|
||||
/// The 13-byte Command Status Wrapper, read from the bulk-IN endpoint.
|
||||
pub const CommandStatusWrapper = extern struct {
|
||||
signature: u32 align(1) = csw_signature,
|
||||
tag: u32 align(1),
|
||||
data_residue: u32 align(1),
|
||||
status: u8,
|
||||
};
|
||||
|
||||
comptime {
|
||||
std.debug.assert(@sizeOf(CommandBlockWrapper) == 31);
|
||||
std.debug.assert(@sizeOf(CommandStatusWrapper) == 13);
|
||||
}
|
||||
|
||||
test "wrapper sizes and signatures match the specification" {
|
||||
const cbw = CommandBlockWrapper{
|
||||
.tag = 0x11223344,
|
||||
.data_transfer_length = 512,
|
||||
.flags = flag_data_in,
|
||||
.lun = 0,
|
||||
.cdb_length = 10,
|
||||
};
|
||||
const bytes = std.mem.asBytes(&cbw);
|
||||
try std.testing.expectEqual(@as(usize, 31), bytes.len);
|
||||
// "USBC" little-endian.
|
||||
try std.testing.expectEqualSlices(u8, "USBC", bytes[0..4]);
|
||||
try std.testing.expectEqual(flag_data_in, bytes[12]);
|
||||
|
||||
const csw = std.mem.bytesToValue(CommandStatusWrapper, &[_]u8{
|
||||
0x55, 0x53, 0x42, 0x53, // "USBS"
|
||||
0x44, 0x33, 0x22, 0x11, // tag
|
||||
0x00, 0x00, 0x00, 0x00, // residue
|
||||
0x00, // passed
|
||||
});
|
||||
try std.testing.expectEqual(csw_signature, csw.signature);
|
||||
try std.testing.expectEqual(@as(u32, 0x11223344), csw.tag);
|
||||
try std.testing.expectEqual(@as(u8, @intFromEnum(CommandStatus.passed)), csw.status);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! The SCSI command descriptor blocks a transparent-SCSI (subclass 0x06) mass
|
||||
//! storage device understands, and the parsers for what they return. Pure data —
|
||||
//! host-testable. These CDBs go inside a Bulk-Only-Transport CBW (see
|
||||
//! bulk-only-transport.zig).
|
||||
//!
|
||||
//! Every multi-byte SCSI field is **big-endian** — the opposite of the USB wire
|
||||
//! ABI — so the LBA and transfer-length encodings are the load-bearing detail.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// SCSI operation codes.
|
||||
const op_test_unit_ready: u8 = 0x00;
|
||||
const op_request_sense: u8 = 0x03;
|
||||
const op_inquiry: u8 = 0x12;
|
||||
const op_read_capacity_10: u8 = 0x25;
|
||||
const op_read_10: u8 = 0x28;
|
||||
const op_write_10: u8 = 0x2A;
|
||||
|
||||
/// INQUIRY: standard device data (36 bytes: peripheral type, removable, vendor
|
||||
/// and product strings).
|
||||
pub fn inquiry(allocation_length: u8) [6]u8 {
|
||||
return .{ op_inquiry, 0, 0, 0, allocation_length, 0 };
|
||||
}
|
||||
|
||||
/// TEST UNIT READY: no data; success (CSW passed) means the unit is ready.
|
||||
pub fn testUnitReady() [6]u8 {
|
||||
return .{ op_test_unit_ready, 0, 0, 0, 0, 0 };
|
||||
}
|
||||
|
||||
/// REQUEST SENSE: 18 bytes of sense data (sense key + ASC/ASCQ) explaining the
|
||||
/// previous failure.
|
||||
pub fn requestSense(allocation_length: u8) [6]u8 {
|
||||
return .{ op_request_sense, 0, 0, 0, allocation_length, 0 };
|
||||
}
|
||||
|
||||
/// READ CAPACITY(10): 8 bytes back — the last LBA and the block size, both u32
|
||||
/// big-endian. Block count is last_lba + 1.
|
||||
pub fn readCapacity10() [10]u8 {
|
||||
return .{ op_read_capacity_10, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
}
|
||||
|
||||
/// READ(10): read `blocks` logical blocks starting at `lba` into the data stage.
|
||||
pub fn read10(lba: u32, blocks: u16) [10]u8 {
|
||||
var cdb = [_]u8{0} ** 10;
|
||||
cdb[0] = op_read_10;
|
||||
std.mem.writeInt(u32, cdb[2..6], lba, .big);
|
||||
std.mem.writeInt(u16, cdb[7..9], blocks, .big);
|
||||
return cdb;
|
||||
}
|
||||
|
||||
/// WRITE(10): write `blocks` logical blocks starting at `lba` from the data stage.
|
||||
pub fn write10(lba: u32, blocks: u16) [10]u8 {
|
||||
var cdb = [_]u8{0} ** 10;
|
||||
cdb[0] = op_write_10;
|
||||
std.mem.writeInt(u32, cdb[2..6], lba, .big);
|
||||
std.mem.writeInt(u16, cdb[7..9], blocks, .big);
|
||||
return cdb;
|
||||
}
|
||||
|
||||
/// Decode an 8-byte READ CAPACITY(10) reply.
|
||||
pub fn parseCapacity(bytes: [8]u8) struct { last_lba: u32, block_size: u32 } {
|
||||
return .{
|
||||
.last_lba = std.mem.readInt(u32, bytes[0..4], .big),
|
||||
.block_size = std.mem.readInt(u32, bytes[4..8], .big),
|
||||
};
|
||||
}
|
||||
|
||||
test "read/write CDBs encode the LBA and length big-endian" {
|
||||
const read = read10(0x01020304, 8);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 0x28, 0x00, 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x08, 0x00 }, &read);
|
||||
|
||||
const write = write10(0xAABBCCDD, 1);
|
||||
try std.testing.expectEqualSlices(u8, &.{ 0x2A, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0x00, 0x00, 0x01, 0x00 }, &write);
|
||||
|
||||
try std.testing.expectEqual(@as(u8, 0x25), readCapacity10()[0]);
|
||||
try std.testing.expectEqual(@as(u8, 0x12), inquiry(36)[0]);
|
||||
try std.testing.expectEqual(@as(u8, 36), inquiry(36)[4]);
|
||||
try std.testing.expectEqual(@as(u8, 0x00), testUnitReady()[0]);
|
||||
}
|
||||
|
||||
test "read capacity parses last LBA and block size" {
|
||||
// last_lba = 0x0003FFFF (262144 blocks), block_size = 512.
|
||||
const capacity = parseCapacity(.{ 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x02, 0x00 });
|
||||
try std.testing.expectEqual(@as(u32, 0x0003FFFF), capacity.last_lba);
|
||||
try std.testing.expectEqual(@as(u32, 512), capacity.block_size);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
//! USB mass-storage class driver (Bulk-Only Transport + transparent SCSI).
|
||||
//!
|
||||
//! Spawned by the device manager when the xHCI bus driver reports a mass-storage
|
||||
//! / SCSI / bulk-only interface (class 8, subclass 6, protocol 0x50); its device
|
||||
//! id arrives as argv[1]. It owns no hardware: it opens its device through the
|
||||
//! USB transfer protocol (`runtime.usb`), then drives it with the BOT command
|
||||
//! cycle — CBW out, an optional data stage, CSW in — carrying SCSI commands
|
||||
//! (READ CAPACITY, READ(10), WRITE(10)). Upward it is a block device: it serves
|
||||
//! the block protocol under `.block`, the storage a FAT filesystem sits on.
|
||||
//!
|
||||
//! Block data never crosses IPC: read/write name a caller-owned DMA buffer by
|
||||
//! physical address, which the data stage DMAs straight to/from.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const scsi = @import("scsi.zig");
|
||||
const bot = @import("bulk-only-transport.zig");
|
||||
const block_protocol = @import("block-protocol");
|
||||
const dma = runtime.dma;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
var device_id: u64 = 0;
|
||||
var device: runtime.usb.Device = undefined;
|
||||
var bulk_in: runtime.usb.Endpoint = undefined;
|
||||
var bulk_out: runtime.usb.Endpoint = undefined;
|
||||
|
||||
// DMA buffers for the transport: the 31-byte CBW, the 13-byte CSW, and a page
|
||||
// for the small command data (INQUIRY / READ CAPACITY / the self-check sector).
|
||||
var command_wrapper: dma.Region = undefined;
|
||||
var status_wrapper: dma.Region = undefined;
|
||||
var command_data: dma.Region = undefined;
|
||||
|
||||
var next_tag: u32 = 1;
|
||||
var block_size: u32 = 512;
|
||||
var block_count: u64 = 0;
|
||||
|
||||
/// One Bulk-Only-Transport command: send the CBW, run the data stage (to/from
|
||||
/// `data_physical`), read and validate the CSW. Returns true on a passed status.
|
||||
fn transact(cdb: []const u8, direction_in: bool, data_physical: u64, data_length: u32) bool {
|
||||
const tag = next_tag;
|
||||
next_tag +%= 1;
|
||||
|
||||
const wrapper: *bot.CommandBlockWrapper = @ptrFromInt(command_wrapper.virtual);
|
||||
wrapper.* = .{
|
||||
.tag = tag,
|
||||
.data_transfer_length = data_length,
|
||||
.flags = if (direction_in) bot.flag_data_in else 0,
|
||||
.lun = 0,
|
||||
.cdb_length = @intCast(cdb.len),
|
||||
};
|
||||
@memcpy(wrapper.cdb[0..cdb.len], cdb);
|
||||
|
||||
if (device.bulk(bulk_out.address, command_wrapper.physical, @sizeOf(bot.CommandBlockWrapper)) == null) return false;
|
||||
if (data_length > 0) {
|
||||
const endpoint = if (direction_in) bulk_in.address else bulk_out.address;
|
||||
if (device.bulk(endpoint, data_physical, data_length) == null) return false;
|
||||
}
|
||||
if (device.bulk(bulk_in.address, status_wrapper.physical, @sizeOf(bot.CommandStatusWrapper)) == null) return false;
|
||||
|
||||
const status: *const bot.CommandStatusWrapper = @ptrFromInt(status_wrapper.virtual);
|
||||
if (status.signature != bot.csw_signature or status.tag != tag) return false;
|
||||
return status.status == @intFromEnum(bot.CommandStatus.passed);
|
||||
}
|
||||
|
||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
_ = endpoint;
|
||||
if (!runtime.usb.helloManager(device_id)) {
|
||||
_ = runtime.system.write("/system/drivers/usb-storage: hello to device manager failed\n");
|
||||
return false;
|
||||
}
|
||||
device = runtime.usb.open(device_id) orelse {
|
||||
writeLine("/system/drivers/usb-storage: could not open device {d}\n", .{device_id});
|
||||
return false;
|
||||
};
|
||||
bulk_in = device.findEndpoint(runtime.usb.transfer_type_bulk, true) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-storage: no bulk-IN endpoint\n");
|
||||
return false;
|
||||
};
|
||||
bulk_out = device.findEndpoint(runtime.usb.transfer_type_bulk, false) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-storage: no bulk-OUT endpoint\n");
|
||||
return false;
|
||||
};
|
||||
command_wrapper = dma.alloc(4096, dma.coherent) orelse return false;
|
||||
status_wrapper = dma.alloc(4096, dma.coherent) orelse return false;
|
||||
command_data = dma.alloc(4096, dma.coherent) orelse return false;
|
||||
|
||||
// Bring the LUN up: wait for it to be ready (clearing the initial unit-attention
|
||||
// with REQUEST SENSE), identify it, and read its capacity.
|
||||
var tries: u32 = 0;
|
||||
while (tries < 10) : (tries += 1) {
|
||||
const ready = scsi.testUnitReady();
|
||||
if (transact(&ready, false, 0, 0)) break;
|
||||
const sense = scsi.requestSense(18);
|
||||
_ = transact(&sense, true, command_data.physical, 18);
|
||||
runtime.system.sleep(50);
|
||||
}
|
||||
const inquiry = scsi.inquiry(36);
|
||||
_ = transact(&inquiry, true, command_data.physical, 36);
|
||||
|
||||
const capacity_command = scsi.readCapacity10();
|
||||
if (!transact(&capacity_command, true, command_data.physical, 8)) {
|
||||
_ = runtime.system.write("/system/drivers/usb-storage: READ CAPACITY failed\n");
|
||||
return false;
|
||||
}
|
||||
var capacity_bytes: [8]u8 = undefined;
|
||||
const capacity_source: [*]const u8 = @ptrFromInt(command_data.virtual);
|
||||
@memcpy(&capacity_bytes, capacity_source[0..8]);
|
||||
const capacity = scsi.parseCapacity(capacity_bytes);
|
||||
block_size = capacity.block_size;
|
||||
block_count = @as(u64, capacity.last_lba) + 1;
|
||||
writeLine("/system/drivers/usb-storage: ready ({d} blocks x {d} bytes)\n", .{ block_count, block_size });
|
||||
|
||||
// Self-check: read block 0 and log its trailing signature (0x55AA for a boot
|
||||
// sector) — proof READ(10) works end to end over the bulk path.
|
||||
const read0 = scsi.read10(0, 1);
|
||||
if (block_size <= 4096 and transact(&read0, true, command_data.physical, block_size)) {
|
||||
const sector: [*]const u8 = @ptrFromInt(command_data.virtual);
|
||||
writeLine("/system/drivers/usb-storage: block 0 signature 0x{x:0>2}{x:0>2}\n", .{ sector[510], sector[511] });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Serve the block protocol: geometry, and whole-block read/write to/from the
|
||||
/// caller's DMA buffer (named by physical address).
|
||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||
_ = sender;
|
||||
_ = capability;
|
||||
if (message.len < block_protocol.request_size) return 0;
|
||||
const request = std.mem.bytesToValue(block_protocol.Request, message[0..block_protocol.request_size]);
|
||||
switch (request.operation) {
|
||||
@intFromEnum(block_protocol.Operation.geometry) => {
|
||||
return writeReply(reply, .{ .status = 0, .block_size = block_size, .block_count = block_count });
|
||||
},
|
||||
@intFromEnum(block_protocol.Operation.read) => {
|
||||
const count: u16 = @intCast(request.count);
|
||||
const cdb = scsi.read10(@intCast(request.lba), count);
|
||||
const ok = transact(&cdb, true, request.physical, request.count * block_size);
|
||||
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
|
||||
},
|
||||
@intFromEnum(block_protocol.Operation.write) => {
|
||||
const count: u16 = @intCast(request.count);
|
||||
const cdb = scsi.write10(@intCast(request.lba), count);
|
||||
const ok = transact(&cdb, false, request.physical, request.count * block_size);
|
||||
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
|
||||
},
|
||||
else => return 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn writeReply(reply: []u8, value: block_protocol.Reply) usize {
|
||||
const bytes = std.mem.asBytes(&value);
|
||||
@memcpy(reply[0..bytes.len], bytes);
|
||||
return bytes.len;
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const argument = init.arguments.get(1) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-storage: missing device id (argv[1])\n");
|
||||
return;
|
||||
};
|
||||
device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/usb-storage: malformed device id '{s}'\n", .{argument});
|
||||
return;
|
||||
};
|
||||
runtime.service.run(block_protocol.message_maximum, .{
|
||||
.service = .block,
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
});
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
|
||||
//! mass-storage driver) says to the xHCI bus driver over its well-known
|
||||
//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware —
|
||||
//! it reaches its device entirely through these messages, the way a PS/2 keyboard
|
||||
//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by
|
||||
//! `Operation`, the vfs-protocol / device-manager-protocol pattern.
|
||||
//!
|
||||
//! The shape:
|
||||
//! - **open** (a capability-passing `ipc.callCap`): the class driver hands over
|
||||
//! its own endpoint (for asynchronous interrupt reports) and its assigned
|
||||
//! device id, and receives a `device_token` plus its interface's endpoints.
|
||||
//! - **control / bulk** (synchronous `ipc.call`): one transfer, answered when
|
||||
//! it completes. Control data travels inline (descriptors, HID/MSC class
|
||||
//! requests are all small); bulk data travels by **physical address** — the
|
||||
//! class driver's own `dma_alloc`'d buffer — so a 512-byte sector never has
|
||||
//! to cross the 256-byte IPC boundary.
|
||||
//! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an
|
||||
//! interrupt endpoint; each report the device produces is then pushed to the
|
||||
//! class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`),
|
||||
//! exactly how the input service delivers events.
|
||||
//!
|
||||
//! Single controller assumption: one `.usb_bus` singleton serves QEMU's one xHCI.
|
||||
//! A multi-controller machine would need a per-controller endpoint (the device
|
||||
//! manager handing each class driver the right one); noted, not built.
|
||||
|
||||
/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM).
|
||||
pub const message_maximum: usize = 256;
|
||||
|
||||
/// The largest inline control-transfer payload. Sized so a whole message
|
||||
/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class
|
||||
/// requests are all far smaller.
|
||||
pub const max_inline_data: usize = 200;
|
||||
|
||||
/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport`
|
||||
/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are
|
||||
/// 8 bytes, boot mouse reports 3–4.
|
||||
pub const max_report_data: usize = 48;
|
||||
|
||||
/// Endpoints per interface reported back in an open reply (a boot HID interface
|
||||
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
|
||||
pub const max_reported_endpoints: usize = 4;
|
||||
|
||||
pub const Operation = enum(u32) {
|
||||
open = 0,
|
||||
control = 1,
|
||||
interrupt_subscribe = 2,
|
||||
bulk = 3,
|
||||
};
|
||||
|
||||
/// The endpoint facts a class driver needs, lifted from the endpoint descriptor
|
||||
/// the bus driver already parsed during enumeration.
|
||||
pub const Endpoint = extern struct {
|
||||
/// EndpointDescriptor address: direction in bit 7, number in bits 3:0.
|
||||
address: u8,
|
||||
/// 0 control, 1 isochronous, 2 bulk, 3 interrupt.
|
||||
transfer_type: u8,
|
||||
max_packet_size: u16,
|
||||
interval: u8,
|
||||
reserved: [3]u8 = .{ 0, 0, 0 },
|
||||
};
|
||||
|
||||
/// open: the class driver's receive endpoint rides as the call's capability, and
|
||||
/// `device_id` is the interface's assigned id (its argv[1]).
|
||||
pub const OpenRequest = extern struct {
|
||||
operation: u32 = @intFromEnum(Operation.open),
|
||||
reserved: u32 = 0,
|
||||
device_id: u64,
|
||||
};
|
||||
|
||||
/// The answer to open: a token scoping every later request to this device, the
|
||||
/// interface's class triple (a sanity check), and its endpoints.
|
||||
pub const OpenReply = extern struct {
|
||||
status: i32,
|
||||
endpoint_count: u32,
|
||||
device_token: u64,
|
||||
interface_class: u8,
|
||||
interface_subclass: u8,
|
||||
interface_protocol: u8,
|
||||
interface_number: u8,
|
||||
reserved2: u32 = 0,
|
||||
endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints,
|
||||
};
|
||||
|
||||
/// control: one EP0 control transfer. `setup` is a bit-cast `usb_abi.Request`.
|
||||
/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the
|
||||
/// reply carries up to `data_length` bytes back.
|
||||
pub const ControlRequest = extern struct {
|
||||
operation: u32 = @intFromEnum(Operation.control),
|
||||
reserved: u32 = 0,
|
||||
device_token: u64,
|
||||
setup: [8]u8,
|
||||
direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT)
|
||||
reserved2: u8 = 0,
|
||||
data_length: u16,
|
||||
reserved3: u32 = 0,
|
||||
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
|
||||
};
|
||||
|
||||
pub const ControlReply = extern struct {
|
||||
status: i32, // 0 success, negative on failure/stall
|
||||
actual_length: u32,
|
||||
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
|
||||
};
|
||||
|
||||
/// interrupt_subscribe: begin periodic IN polling of an interrupt endpoint. Each
|
||||
/// report the device returns is pushed to the caller's endpoint (handed over at
|
||||
/// open) as an asynchronous `InterruptReport`.
|
||||
pub const InterruptSubscribeRequest = extern struct {
|
||||
operation: u32 = @intFromEnum(Operation.interrupt_subscribe),
|
||||
reserved: u32 = 0,
|
||||
device_token: u64,
|
||||
endpoint_address: u8,
|
||||
reserved2: u8 = 0,
|
||||
max_length: u16, // bytes to request per poll (the endpoint's max packet size)
|
||||
};
|
||||
|
||||
pub const InterruptSubscribeReply = extern struct {
|
||||
status: i32,
|
||||
reserved: u32 = 0,
|
||||
};
|
||||
|
||||
/// bulk: one bulk IN or OUT transfer. `physical_address` is the class driver's own
|
||||
/// `dma_alloc`'d buffer — the controller DMAs straight to/from it, so the bulk
|
||||
/// data never crosses IPC. `endpoint_address`'s bit 7 selects IN vs OUT.
|
||||
pub const BulkRequest = extern struct {
|
||||
operation: u32 = @intFromEnum(Operation.bulk),
|
||||
reserved: u32 = 0,
|
||||
device_token: u64,
|
||||
physical_address: u64,
|
||||
length: u32,
|
||||
endpoint_address: u8,
|
||||
reserved2: u8 = 0,
|
||||
reserved3: u16 = 0,
|
||||
};
|
||||
|
||||
pub const BulkReply = extern struct {
|
||||
status: i32,
|
||||
actual_length: u32,
|
||||
};
|
||||
|
||||
/// An asynchronous interrupt report, pushed with `ipc.send` to a subscriber's
|
||||
/// endpoint. `Received.isMessage()` is set; there is no reply owed.
|
||||
pub const InterruptReport = extern struct {
|
||||
device_token: u64,
|
||||
endpoint_address: u8,
|
||||
length: u8,
|
||||
reserved: u16 = 0,
|
||||
data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
|
||||
};
|
||||
|
||||
comptime {
|
||||
const std = @import("std");
|
||||
// Every synchronous message must fit one IPC message; the async report must
|
||||
// fit an ipc_send payload slot.
|
||||
std.debug.assert(@sizeOf(ControlRequest) <= message_maximum);
|
||||
std.debug.assert(@sizeOf(ControlReply) <= message_maximum);
|
||||
std.debug.assert(@sizeOf(OpenReply) <= message_maximum);
|
||||
std.debug.assert(@sizeOf(InterruptReport) <= 64);
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
//! /system/drivers/usb-xhci-bus — the xHCI (USB 3) host-controller bus driver.
|
||||
//! The device manager spawns **one instance per controller** it discovers (a
|
||||
//! machine can carry several), passing the controller's device-tree id as
|
||||
//! argv[1]; this instance claims that device and no other, so multiple
|
||||
//! instances never fight over hardware.
|
||||
//!
|
||||
//! M18.2 (this increment): after the hello, real hardware — map the xHC's
|
||||
//! register window (the first memory BAR; resource 0 is the ECAM config
|
||||
//! space), read the capability registers, and walk the root-hub ports: one
|
||||
//! `child_added` report to the manager per connected port, carrying the port
|
||||
//! number and the PORTSC speed class as identity. No transfer rings yet —
|
||||
//! descriptors and USB class matching are the USB track; the connect bit and
|
||||
//! speed come straight from PORTSC, which reflects hardware state whether or
|
||||
//! not the controller is running.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const protocol = runtime.device_manager_protocol;
|
||||
const device = runtime.device;
|
||||
const usb_ids = @import("usb-ids");
|
||||
const usb_abi = @import("usb-abi");
|
||||
const transfer = @import("usb-transfer-protocol");
|
||||
const library = @import("usb-xhci-library.zig");
|
||||
|
||||
/// The controller engine (reset, rings, transfers), stood up in `initialise`.
|
||||
var controller: ?library.Controller = null;
|
||||
|
||||
/// This driver's service endpoint (registered as `.usb_bus`), where class-driver
|
||||
/// requests, signals, and the interrupt-poll timer all arrive.
|
||||
var service_endpoint: runtime.ipc.Handle = 0;
|
||||
|
||||
/// How often the driver drains the event ring for interrupt reports (~125 Hz),
|
||||
/// re-armed each tick. Frequent enough for responsive input.
|
||||
const poll_interval_ms: u64 = 8;
|
||||
|
||||
/// The class driver endpoints that opened each device, so interrupt reports can
|
||||
/// be pushed back to them. Keyed by the device token (the interface's device id).
|
||||
const Open = struct {
|
||||
used: bool = false,
|
||||
device_token: u64 = 0,
|
||||
report_endpoint: usize = 0,
|
||||
};
|
||||
var opens = [_]Open{.{}} ** 16;
|
||||
|
||||
fn recordOpen(device_token: u64, report_endpoint: usize) void {
|
||||
for (&opens) |*open| {
|
||||
if (open.used and open.device_token == device_token) {
|
||||
open.report_endpoint = report_endpoint;
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (&opens) |*open| {
|
||||
if (!open.used) {
|
||||
open.* = .{ .used = true, .device_token = device_token, .report_endpoint = report_endpoint };
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reportEndpointFor(device_token: u64) ?usize {
|
||||
for (&opens) |*open| {
|
||||
if (open.used and open.device_token == device_token) return open.report_endpoint;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so
|
||||
/// concurrent instances (one per controller) can never interleave mid-line.
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
var controller_id: u64 = protocol.no_device;
|
||||
|
||||
/// Claim the assigned controller, find its register window, and hello the
|
||||
/// manager. Any failure returns false: the process exits cleanly, which the
|
||||
/// manager reads as "meant to stop" — a missing assignment is not a crash loop.
|
||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
service_endpoint = endpoint;
|
||||
if (!device.claim(controller_id)) {
|
||||
writeLine("/system/drivers/usb-xhci-bus: unable to claim controller device {d}\n", .{controller_id});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch our own descriptor back for the controller's resources.
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: out of memory\n");
|
||||
return false;
|
||||
};
|
||||
const total = device.enumerate(buffer);
|
||||
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||
if (d.id == controller_id) break d;
|
||||
} else {
|
||||
writeLine("/system/drivers/usb-xhci-bus: device {d} not in the device tree\n", .{controller_id});
|
||||
return false;
|
||||
};
|
||||
|
||||
// The xHC's registers live behind the first memory BAR. Resource 0 is the
|
||||
// function's ECAM configuration space (M15), so the walk starts at 1.
|
||||
var register_index: u64 = 0;
|
||||
const register_window = for (descriptor.resources[1..@intCast(descriptor.resource_count)], 1..) |resource, index| {
|
||||
if (resource.kind == @intFromEnum(device.ResourceKind.memory)) {
|
||||
register_index = index;
|
||||
break resource;
|
||||
}
|
||||
} else {
|
||||
writeLine("/system/drivers/usb-xhci-bus: controller device {d} has no register BAR\n", .{controller_id});
|
||||
return false;
|
||||
};
|
||||
writeLine("/system/drivers/usb-xhci-bus: claimed controller device {d} (registers at 0x{x}, {d} bytes)\n", .{
|
||||
controller_id,
|
||||
register_window.start,
|
||||
register_window.len,
|
||||
});
|
||||
register_base = device.mmioMap(controller_id, register_index) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: mmio_map failed\n");
|
||||
return false;
|
||||
};
|
||||
|
||||
// Bring the controller up: reset it, stand up the command and event rings,
|
||||
// and start it running (the hardware half lives in usb-xhci-library.zig).
|
||||
controller = library.Controller.init(register_base) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller reset/bring-up failed\n");
|
||||
return false;
|
||||
};
|
||||
writeLine("/system/drivers/usb-xhci-bus: controller running ({d} slots, {d}-byte contexts)\n", .{
|
||||
controller.?.max_slots,
|
||||
controller.?.context_size,
|
||||
});
|
||||
// The proof of life: a No-Op command round-trips the command ring, the event
|
||||
// ring, the doorbell, and the cycle-bit bookkeeping. If this completes, the
|
||||
// engine is sound; transfers build on exactly this machinery.
|
||||
if (controller.?.noOpCommand()) {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: command ring running (no-op ok)\n");
|
||||
} else {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: no-op command did not complete\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The handshake: role, protocol version, assignment — inside the manager's
|
||||
// deadline (the lookup retries cover the manager still registering).
|
||||
var manager: ?runtime.ipc.Handle = null;
|
||||
var tries: u32 = 0;
|
||||
while (manager == null and tries < 100) : (tries += 1) {
|
||||
manager = runtime.ipc.lookup(.device_manager);
|
||||
if (manager == null) runtime.system.sleep(20);
|
||||
}
|
||||
const h = manager orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: no device manager to hello\n");
|
||||
return false;
|
||||
};
|
||||
const hello = protocol.Hello{ .role = @intFromEnum(protocol.Role.bus), .device_id = controller_id };
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
const n = runtime.ipc.call(h, std.mem.asBytes(&hello), &reply) catch {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello call failed\n");
|
||||
return false;
|
||||
};
|
||||
if (n < protocol.reply_size or std.mem.bytesToValue(protocol.HelloReply, reply[0..protocol.reply_size]).status != 0) {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello refused\n");
|
||||
return false;
|
||||
}
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: hello acknowledged\n");
|
||||
|
||||
scanPorts(h);
|
||||
|
||||
// Arm the poll timer that drains interrupt reports from the event ring. It is
|
||||
// re-armed on each tick in onNotification; class drivers subscribe later.
|
||||
_ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
|
||||
return true;
|
||||
}
|
||||
|
||||
var register_base: usize = 0;
|
||||
|
||||
/// The xHCI default Protocol Speed IDs (the PORTSC port-speed field, bits 13:10)
|
||||
/// decoded to human names — the boot-log breadcrumb for what actually enumerated on
|
||||
/// a port, the USB analog of the pci-bus class-code line. A controller may redefine
|
||||
/// these through its Supported Protocol capability, but the defaults cover every
|
||||
/// speed QEMU and real hardware report at this (pre-descriptor) stage.
|
||||
fn speedName(speed: u32) []const u8 {
|
||||
return switch (speed) {
|
||||
1 => "Full-speed (USB 2.0, 12 Mb/s)",
|
||||
2 => "Low-speed (USB 2.0, 1.5 Mb/s)",
|
||||
3 => "High-speed (USB 2.0, 480 Mb/s)",
|
||||
4 => "SuperSpeed (USB 3.0, 5 Gb/s)",
|
||||
5 => "SuperSpeedPlus (USB 3.1, 10 Gb/s)",
|
||||
else => "unknown speed",
|
||||
};
|
||||
}
|
||||
|
||||
/// The root-hub scan and enumeration: for each connected port, bring the device
|
||||
/// up (reset → enable slot → address), read its descriptors, and register +
|
||||
/// report one child per interface — carrying the interface's (class, subclass,
|
||||
/// protocol) triple as identity, which is what the device manager matches a
|
||||
/// class driver against.
|
||||
fn scanPorts(manager: runtime.ipc.Handle) void {
|
||||
const engine = if (controller) |*c| c else {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n");
|
||||
return;
|
||||
};
|
||||
writeLine("/system/drivers/usb-xhci-bus: {d} root-hub ports\n", .{engine.max_ports});
|
||||
|
||||
var port: u32 = 1;
|
||||
var connected: u32 = 0;
|
||||
while (port <= engine.max_ports) : (port += 1) {
|
||||
const port_status = engine.portStatus(port);
|
||||
if (port_status & 1 == 0) continue; // CCS: nothing connected
|
||||
connected += 1;
|
||||
const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} connected — {s} (speed class {d})\n", .{ port, speedName(speed), speed });
|
||||
|
||||
const usb_device = engine.setupDevice(port, speed) orelse {
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} device setup failed\n", .{port});
|
||||
continue;
|
||||
};
|
||||
if (!engine.enumerate(usb_device)) {
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} enumeration failed\n", .{port});
|
||||
continue;
|
||||
}
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)\n", .{
|
||||
port,
|
||||
usb_device.device_descriptor.vendor_id,
|
||||
usb_device.device_descriptor.product_id,
|
||||
usb_device.interface_count,
|
||||
});
|
||||
|
||||
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
|
||||
// Record the id each interface was registered as, so a class driver
|
||||
// opening the interface (by that id) resolves to it.
|
||||
if (reportInterface(manager, port, interface.*)) |registered| {
|
||||
interface.registered_device_id = registered;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n");
|
||||
}
|
||||
|
||||
/// Register one interface as a resource-less child of the controller and report
|
||||
/// it to the device manager. The identity is the packed USB class triple, so the
|
||||
/// manager can match a class driver (HID keyboard, mouse, mass storage); the
|
||||
/// registered device id becomes that driver's argv[1] assignment. Returns the
|
||||
/// registered device id, or null if registration or the report failed.
|
||||
fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.InterfaceInfo) ?u64 {
|
||||
const identity = usb_ids.packTriple(interface.class, interface.subclass, interface.protocol);
|
||||
|
||||
// A USB device is reached through its controller, not by MMIO, so the child
|
||||
// carries no resources; register() allows that. Its bus-local identity — the
|
||||
// (port, interface) address, written as a short "P<port>I<interface>" tag in
|
||||
// the hid field — makes each interface a distinct kernel node (the register
|
||||
// dedup keys on class/pci_class/hid/resources, all otherwise identical here)
|
||||
// and keeps re-registration idempotent across a bus restart: the same port
|
||||
// and interface always map back to the same device id.
|
||||
var descriptor = std.mem.zeroes(device.DeviceDescriptor);
|
||||
descriptor.class = @intFromEnum(device.DeviceClass.usb_device);
|
||||
descriptor.pci_class = device.no_pci_class;
|
||||
descriptor.resource_count = 0;
|
||||
var hid_buffer: [8]u8 = undefined;
|
||||
const hid_text = std.fmt.bufPrint(&hid_buffer, "P{d}I{d}", .{ port, interface.number }) catch "";
|
||||
descriptor.hid_len = hid_text.len;
|
||||
@memcpy(descriptor.hid[0..hid_text.len], hid_text);
|
||||
const registered = device.register(controller_id, &descriptor) orelse {
|
||||
writeLine("/system/drivers/usb-xhci-bus: register refused for port {d} interface {d}\n", .{ port, interface.number });
|
||||
return null;
|
||||
};
|
||||
|
||||
const report = protocol.ChildAdded{
|
||||
.parent = controller_id,
|
||||
.bus_address = (@as(u64, port) << 8) | interface.number,
|
||||
.identity = identity,
|
||||
.device_id = registered,
|
||||
};
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
_ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch {
|
||||
writeLine("/system/drivers/usb-xhci-bus: child report for port {d} interface {d} failed\n", .{ port, interface.number });
|
||||
return null;
|
||||
};
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} interface {d} class {d}/{d}/{d} registered as device {d}\n", .{
|
||||
port,
|
||||
interface.number,
|
||||
interface.class,
|
||||
interface.subclass,
|
||||
interface.protocol,
|
||||
registered,
|
||||
});
|
||||
return registered;
|
||||
}
|
||||
|
||||
/// Serve the USB transfer protocol: a class driver opens its device, then issues
|
||||
/// control / interrupt-subscribe / bulk requests against it.
|
||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||
_ = sender;
|
||||
if (message.len < 4) return 0;
|
||||
const operation = std.mem.readInt(u32, message[0..4], .little);
|
||||
return switch (operation) {
|
||||
@intFromEnum(transfer.Operation.open) => handleOpen(message, reply, capability),
|
||||
@intFromEnum(transfer.Operation.control) => handleControl(message, reply),
|
||||
@intFromEnum(transfer.Operation.interrupt_subscribe) => handleSubscribe(message, reply),
|
||||
@intFromEnum(transfer.Operation.bulk) => handleBulk(message, reply),
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn writeReply(reply: []u8, value: anytype) usize {
|
||||
const bytes = std.mem.asBytes(&value);
|
||||
@memcpy(reply[0..bytes.len], bytes);
|
||||
return bytes.len;
|
||||
}
|
||||
|
||||
/// open: resolve the assigned device id to an interface, remember the caller's
|
||||
/// endpoint (for interrupt reports), and answer with a device token + the
|
||||
/// interface's endpoints so the class driver need not re-read the config.
|
||||
fn handleOpen(message: []const u8, reply: []u8, capability: ?runtime.ipc.Handle) usize {
|
||||
if (message.len < @sizeOf(transfer.OpenRequest)) return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
|
||||
const request = std.mem.bytesToValue(transfer.OpenRequest, message[0..@sizeOf(transfer.OpenRequest)]);
|
||||
const engine = if (controller) |*c| c else return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
|
||||
const found = engine.findInterface(request.device_id) orelse return writeReply(reply, transfer.OpenReply{ .status = -1, .endpoint_count = 0, .device_token = 0, .interface_class = 0, .interface_subclass = 0, .interface_protocol = 0, .interface_number = 0 });
|
||||
|
||||
if (capability) |endpoint| recordOpen(request.device_id, endpoint);
|
||||
|
||||
var open_reply = transfer.OpenReply{
|
||||
.status = 0,
|
||||
.endpoint_count = found.interface.endpoint_count,
|
||||
.device_token = request.device_id,
|
||||
.interface_class = found.interface.class,
|
||||
.interface_subclass = found.interface.subclass,
|
||||
.interface_protocol = found.interface.protocol,
|
||||
.interface_number = found.interface.number,
|
||||
};
|
||||
const count = @min(found.interface.endpoint_count, transfer.max_reported_endpoints);
|
||||
for (found.interface.endpoints[0..count], 0..) |endpoint, index| {
|
||||
open_reply.endpoints[index] = .{
|
||||
.address = endpoint.address,
|
||||
.transfer_type = endpoint.transfer_type,
|
||||
.max_packet_size = endpoint.max_packet_size,
|
||||
.interval = endpoint.interval,
|
||||
};
|
||||
}
|
||||
return writeReply(reply, open_reply);
|
||||
}
|
||||
|
||||
/// control: one EP0 control transfer, small data inline both ways.
|
||||
fn handleControl(message: []const u8, reply: []u8) usize {
|
||||
if (message.len < @sizeOf(transfer.ControlRequest)) return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
|
||||
const request = std.mem.bytesToValue(transfer.ControlRequest, message[0..@sizeOf(transfer.ControlRequest)]);
|
||||
const engine = if (controller) |*c| c else return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
|
||||
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.ControlReply{ .status = -1, .actual_length = 0 });
|
||||
|
||||
const setup = std.mem.bytesToValue(usb_abi.Request, &request.setup);
|
||||
const direction_in = request.direction_in != 0;
|
||||
const data_length = @min(request.data_length, transfer.max_inline_data);
|
||||
var data: [transfer.max_inline_data]u8 = undefined;
|
||||
if (!direction_in) @memcpy(data[0..data_length], request.data[0..data_length]);
|
||||
|
||||
const ok = engine.controlTransfer(found.device, setup, data[0..data_length], direction_in);
|
||||
var control_reply = transfer.ControlReply{ .status = if (ok) 0 else -1, .actual_length = if (ok) data_length else 0 };
|
||||
if (ok and direction_in) @memcpy(control_reply.data[0..data_length], data[0..data_length]);
|
||||
return writeReply(reply, control_reply);
|
||||
}
|
||||
|
||||
/// interrupt_subscribe: arm periodic IN polling; reports flow back asynchronously.
|
||||
fn handleSubscribe(message: []const u8, reply: []u8) usize {
|
||||
if (message.len < @sizeOf(transfer.InterruptSubscribeRequest)) return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||
const request = std.mem.bytesToValue(transfer.InterruptSubscribeRequest, message[0..@sizeOf(transfer.InterruptSubscribeRequest)]);
|
||||
const engine = if (controller) |*c| c else return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||
const report_endpoint = reportEndpointFor(request.device_token) orelse return writeReply(reply, transfer.InterruptSubscribeReply{ .status = -1 });
|
||||
const ok = engine.subscribeInterrupt(found.device, endpoint, request.device_token, report_endpoint);
|
||||
return writeReply(reply, transfer.InterruptSubscribeReply{ .status = if (ok) 0 else -1 });
|
||||
}
|
||||
|
||||
/// bulk: one bulk transfer to/from the class driver's own DMA buffer (by physical
|
||||
/// address), so sector-sized data never crosses IPC.
|
||||
fn handleBulk(message: []const u8, reply: []u8) usize {
|
||||
if (message.len < @sizeOf(transfer.BulkRequest)) return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
|
||||
const request = std.mem.bytesToValue(transfer.BulkRequest, message[0..@sizeOf(transfer.BulkRequest)]);
|
||||
const engine = if (controller) |*c| c else return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
|
||||
const found = engine.findInterface(request.device_token) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
|
||||
const endpoint = library.Controller.endpointForAddress(found.interface, request.endpoint_address) orelse return writeReply(reply, transfer.BulkReply{ .status = -1, .actual_length = 0 });
|
||||
const transferred = engine.bulkTransfer(found.device, endpoint, request.physical_address, request.length);
|
||||
return writeReply(reply, transfer.BulkReply{ .status = if (transferred != null) 0 else -1, .actual_length = transferred orelse 0 });
|
||||
}
|
||||
|
||||
/// The poll timer landed: drain any interrupt reports off the event ring and push
|
||||
/// each to the class driver that subscribed, then re-arm the timer.
|
||||
fn onNotification(badge: u64) void {
|
||||
if (badge & runtime.ipc.notify_timer_bit == 0) return;
|
||||
if (controller) |*engine| {
|
||||
engine.pump();
|
||||
while (engine.takeReport()) |report| {
|
||||
var message = transfer.InterruptReport{
|
||||
.device_token = report.device_token,
|
||||
.endpoint_address = report.endpoint_address,
|
||||
.length = @intCast(@min(report.length, transfer.max_report_data)),
|
||||
};
|
||||
const n = @min(report.length, transfer.max_report_data);
|
||||
@memcpy(message.data[0..n], report.data[0..n]);
|
||||
_ = runtime.ipc.send(report.report_endpoint, std.mem.asBytes(&message));
|
||||
}
|
||||
}
|
||||
_ = runtime.system.timerOnce(service_endpoint, poll_interval_ms);
|
||||
}
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const argument = init.arguments.get(1) orelse {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: missing controller device id (argv[1])\n");
|
||||
return;
|
||||
};
|
||||
controller_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/usb-xhci-bus: malformed controller device id '{s}'\n", .{argument});
|
||||
return;
|
||||
};
|
||||
runtime.service.run(transfer.message_maximum, .{
|
||||
.service = .usb_bus,
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
.on_notification = onNotification,
|
||||
});
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user