Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
# JetBrains IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
.github/
|
||||||
@@ -2,12 +2,31 @@
|
|||||||
Codename: Shodan
|
Codename: Shodan
|
||||||
Version: 1
|
Version: 1
|
||||||
|
|
||||||
A small operating system, written from scratch in Zig — a bootloader (`boot/`)
|
A small resilient operating system, written from scratch in Zig.
|
||||||
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
|
## Zen of DanOS:
|
||||||
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
|
- Resilient Micro-Kernel Architecture.
|
||||||
channels. See [`docs/`](docs/README.md) for how each piece works.
|
- 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
|
## Prerequisites
|
||||||
|
|
||||||
@@ -30,8 +49,10 @@ channels. See [`docs/`](docs/README.md) for how each piece works.
|
|||||||
zig build
|
zig build
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces the UEFI bootloader (`zig-out/bin/BOOTX64.efi`) and the kernel ELF
|
Produces a FHS-shaped `zig-out/` that *is* the danos filesystem and the boot volume:
|
||||||
(`zig-out/bin/kernel`).
|
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
|
## Run
|
||||||
|
|
||||||
@@ -58,9 +79,13 @@ straight into CI.
|
|||||||
|
|
||||||
## Documentation
|
## 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).
|
[`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
|
## Logo
|
||||||
|
|
||||||
San Serif Text "Dan OS" with a black karate belt around it.
|
San Serif Text "Dan OS" with a black karate belt around it.
|
||||||
|
|||||||
+41
-39
@@ -1,22 +1,24 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const uefi = std.os.uefi;
|
const uefi = std.os.uefi;
|
||||||
const elf = std.elf;
|
const elf = std.elf;
|
||||||
const danos = @import("danos");
|
const boot_handoff = @import("boot-handoff");
|
||||||
const BootInformation = danos.BootInformation;
|
const BootInformation = boot_handoff.BootInformation;
|
||||||
const GraphicsOutput = uefi.protocol.GraphicsOutput;
|
const GraphicsOutput = uefi.protocol.GraphicsOutput;
|
||||||
const EdidActive = uefi.protocol.edid.Active;
|
const EdidActive = uefi.protocol.edid.Active;
|
||||||
const MemoryMapSlice = uefi.tables.MemoryMapSlice;
|
const MemoryMapSlice = uefi.tables.MemoryMapSlice;
|
||||||
|
|
||||||
/// Name of the kernel ELF on the boot volume (installed to the ESP root by
|
// The boot volume is the FHS-shaped zig-out (see build.zig / docs/README.md), so the
|
||||||
/// build.zig). UEFI wants a UTF-16, null-terminated path.
|
// loader reads each artifact from its addressed FHS path. UEFI paths use backslashes;
|
||||||
const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("kernel");
|
// 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 kernel image: /system/kernel.
|
||||||
/// the FAT driver walks the components itself, so no directory dance needed).
|
const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\kernel");
|
||||||
const init_file_name = std.unicode.utf8ToUtf16LeStringLiteral("sbin\\init");
|
|
||||||
|
|
||||||
/// Path of the initrd image on the boot volume (the VFS server + drivers).
|
/// The init program: /system/services/init.
|
||||||
const initrd_file_name = std.unicode.utf8ToUtf16LeStringLiteral("initrd.img");
|
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.
|
/// Physical page size, and the sentinel UEFI uses to seek to end-of-file.
|
||||||
const page_size = 4096;
|
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
|
// report the reason (boot services are still up) and park the machine so the
|
||||||
// message stays on screen.
|
// message stays on screen.
|
||||||
boot() catch |err| {
|
boot() catch |err| {
|
||||||
log("\r\ndanos: boot failed: ");
|
log("\r\nEFI: boot failed: ");
|
||||||
logBytes(@errorName(err));
|
logBytes(@errorName(err));
|
||||||
log("\r\n");
|
log("\r\n");
|
||||||
while (true) asm volatile ("hlt");
|
while (true) asm volatile ("hlt");
|
||||||
@@ -43,7 +45,7 @@ fn boot() !noreturn {
|
|||||||
var boot_information: BootInformation = .{
|
var boot_information: BootInformation = .{
|
||||||
// A missing GOP (a headless machine) is not fatal — hand the kernel a
|
// 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.
|
// "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,
|
.base = 0,
|
||||||
.width = 0,
|
.width = 0,
|
||||||
.height = 0,
|
.height = 0,
|
||||||
@@ -61,16 +63,16 @@ fn boot() !noreturn {
|
|||||||
|
|
||||||
const entry = try loadKernel(bs, &boot_information);
|
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| {
|
loadInit(bs, &boot_information) catch |err| {
|
||||||
log("danos: no sbin/init (");
|
log("EFI: no /system/services/init (");
|
||||||
logBytes(@errorName(err));
|
logBytes(@errorName(err));
|
||||||
log(") - booting without user space\r\n");
|
log(") - booting without user space\r\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Best effort: the initrd (VFS server + drivers) is optional too.
|
// Best effort: the initial_ramdisk (VFS server + drivers) is optional too.
|
||||||
loadInitrd(bs, &boot_information) catch |err| {
|
loadInitialRamdisk(bs, &boot_information) catch |err| {
|
||||||
log("danos: no initrd (");
|
log("EFI: no initial_ramdisk (");
|
||||||
logBytes(@errorName(err));
|
logBytes(@errorName(err));
|
||||||
log(")\r\n");
|
log(")\r\n");
|
||||||
};
|
};
|
||||||
@@ -82,7 +84,7 @@ fn boot() !noreturn {
|
|||||||
// the map and exiting would invalidate the map key.
|
// the map and exiting would invalidate the map key.
|
||||||
const cr3 = try buildBootstrapTables(bs, &boot_information);
|
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);
|
boot_information.memory_map = try exitBootServices(bs);
|
||||||
|
|
||||||
// Switch onto our tables and jump to the kernel in one uninterruptible step.
|
// 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)
|
/// 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.
|
/// 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
|
// Enumerate the handles carrying the Graphics Output Protocol. We go through
|
||||||
// handles (rather than locateProtocol) so we can also ask them for their EDID,
|
// handles (rather than locateProtocol) so we can also ask them for their EDID,
|
||||||
// which is what tells us the panel's native resolution.
|
// 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
|
/// Map a GOP pixel format to ours. bit_mask / blt_only have no linear 32bpp
|
||||||
/// layout we can paint into, so they're rejected.
|
/// 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) {
|
return switch (fmt) {
|
||||||
.red_green_blue_reserved_8_bit_per_color => .rgbx,
|
.red_green_blue_reserved_8_bit_per_color => .rgbx,
|
||||||
.blue_green_red_reserved_8_bit_per_color => .bgrx,
|
.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
|
// 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
|
// 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
|
// 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.
|
// 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
|
// The kernel later builds its own precise tables (paging.init) and abandons
|
||||||
// these; they leak as reserved LoaderData (~a handful of frames).
|
// 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;
|
var address: u64 = 0;
|
||||||
while (address < 4 * gib) : (address += 2 << 20) {
|
while (address < 4 * gib) : (address += 2 << 20) {
|
||||||
try pool.map2M(pml4, address, address); // identity
|
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
|
// 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;
|
const fb_end = fb.base + @as(u64, fb.pitch) * fb.height;
|
||||||
while (p < fb_end) : (p += 2 << 20) {
|
while (p < fb_end) : (p += 2 << 20) {
|
||||||
try pool.map2M(pml4, p, p);
|
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
|
// (and 4 KiB-mapping them would collide with the 2 MiB identity leaves), so
|
||||||
// only map segments that actually live in the higher half.
|
// only map segments that actually live in the higher half.
|
||||||
for (boot_information.kernel_segments[0..boot_information.kernel_segment_count]) |seg| {
|
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;
|
var off: u64 = 0;
|
||||||
while (off < seg.pages * page_size) : (off += page_size) {
|
while (off < seg.pages * page_size) : (off += page_size) {
|
||||||
try pool.map4K(pml4, seg.virtual + off, seg.physical + off);
|
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];
|
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.
|
/// loading itself (into ring-3 mappings) — the loader just carries the bytes.
|
||||||
fn loadInit(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
fn loadInit(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||||
const image = try loadFile(bs, init_file_name);
|
const image = try loadFile(bs, init_file_name);
|
||||||
boot_information.init_base = @intFromPtr(image.ptr);
|
boot_information.init_base = @intFromPtr(image.ptr);
|
||||||
boot_information.init_len = image.len;
|
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.
|
/// Ferry the initial_ramdisk (the VFS server + drivers) to the kernel, same as init.
|
||||||
fn loadInitrd(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
fn loadInitialRamdisk(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||||
const image = try loadFile(bs, initrd_file_name);
|
const image = try loadFile(bs, initial_ramdisk_file_name);
|
||||||
boot_information.initrd_base = @intFromPtr(image.ptr);
|
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
||||||
boot_information.initrd_len = image.len;
|
boot_information.initial_ramdisk_len = image.len;
|
||||||
log("danos: initrd loaded\r\n");
|
log("EFI: initial_ramdisk loaded\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate the ELF, copy every PT_LOAD segment to its physical address, and
|
/// 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
|
/// 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 key), so retry until it takes. Both buffers are LoaderData, which survives
|
||||||
/// the exit, so the returned map stays valid for the kernel.
|
/// 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;
|
var attempts: usize = 0;
|
||||||
while (attempts < 8) : (attempts += 1) {
|
while (attempts < 8) : (attempts += 1) {
|
||||||
const info = try bs.getMemoryMapInfo();
|
const info = try bs.getMemoryMapInfo();
|
||||||
// Spare descriptors to absorb the growth from the allocations below.
|
// Spare descriptors to absorb the growth from the allocations below.
|
||||||
const cap = info.len + 8;
|
const cap = info.len + 8;
|
||||||
const map_buffer = try bs.allocatePool(.loader_data, cap * info.descriptor_size);
|
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 {
|
const map = bs.getMemoryMap(map_buffer) catch {
|
||||||
_ = bs.freePool(map_buffer.ptr) catch {};
|
_ = bs.freePool(map_buffer.ptr) catch {};
|
||||||
_ = bs.freePool(regions_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
|
/// 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
|
/// 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.
|
/// never sees UEFI's vocabulary — the same seam the framebuffer already uses.
|
||||||
fn convertMemoryMap(map: MemoryMapSlice, out: []u8) danos.MemoryMap {
|
fn convertMemoryMap(map: MemoryMapSlice, out: []u8) boot_handoff.MemoryMap {
|
||||||
const regions: [*]danos.MemoryRegion = @ptrCast(@alignCast(out.ptr));
|
const regions: [*]boot_handoff.MemoryRegion = @ptrCast(@alignCast(out.ptr));
|
||||||
// We're about to call boot-services memory `usable`, but our own stack lives
|
// 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
|
// 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.
|
// 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;
|
if (d.number_of_pages == 0) continue;
|
||||||
var kind = classify(d);
|
var kind = classify(d);
|
||||||
// The descriptor we're executing on stays reserved (see rsp above).
|
// 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;
|
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.
|
// Coalesce with the previous region if it's the same kind and contiguous.
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
const previous = ®ions[count - 1];
|
const previous = ®ions[count - 1];
|
||||||
if (previous.kind == kind and
|
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;
|
previous.pages += d.number_of_pages;
|
||||||
continue;
|
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).
|
/// 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
|
/// Anything unrecognised is `reserved` — the safe default; our own LoaderData (the
|
||||||
/// kernel image and these buffers) lands there and stays reserved.
|
/// 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;
|
if (!d.attribute.wb) return .mmio;
|
||||||
return switch (d.type) {
|
return switch (d.type) {
|
||||||
.conventional_memory, .boot_services_code, .boot_services_data => .usable,
|
.conventional_memory, .boot_services_code, .boot_services_data => .usable,
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ fn addUserBinary(
|
|||||||
b: *std.Build,
|
b: *std.Build,
|
||||||
target: std.Build.ResolvedTarget,
|
target: std.Build.ResolvedTarget,
|
||||||
runtime_module: *std.Build.Module,
|
runtime_module: *std.Build.Module,
|
||||||
|
posix_module: *std.Build.Module,
|
||||||
|
mmio_module: *std.Build.Module,
|
||||||
|
xkeyboard_config_module: *std.Build.Module,
|
||||||
|
acpi_ids_module: *std.Build.Module,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
root: []const u8,
|
root: []const u8,
|
||||||
) *std.Build.Step.Compile {
|
) *std.Build.Step.Compile {
|
||||||
@@ -74,6 +78,17 @@ fn addUserBinary(
|
|||||||
.stack_protector = false,
|
.stack_protector = false,
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "runtime", .module = runtime_module },
|
.{ .name = "runtime", .module = runtime_module },
|
||||||
|
// POSIX/C compatibility layer, available to any program that wants it
|
||||||
|
// (danos-native code uses `runtime` directly). See library/posix/.
|
||||||
|
.{ .name = "posix", .module = posix_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 +106,40 @@ pub fn build(b: *std.Build) void {
|
|||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
// Shared handoff definitions (BootInformation, Framebuffer, ...). No target is set,
|
// The three shared contracts, each with its own audience so every import
|
||||||
// so the module inherits the target of whichever binary imports it — the
|
// declares which one it speaks (no target is set, so each inherits the target of
|
||||||
// freestanding kernel or the UEFI bootloader.
|
// whichever binary imports it). See docs/coding-standards.md.
|
||||||
const danos_module = b.addModule("danos", .{
|
// boot-handoff : loader <-> kernel (BootInformation, framebuffer, VM layout)
|
||||||
.root_source_file = b.path("system/danos.zig"),
|
// 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"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
|
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
|
||||||
@@ -111,7 +155,8 @@ pub fn build(b: *std.Build) void {
|
|||||||
const architecture_module = b.addModule("architecture", .{
|
const architecture_module = b.addModule("architecture", .{
|
||||||
.root_source_file = b.path("system/kernel/architecture/x86_64/cpu.zig"),
|
.root_source_file = b.path("system/kernel/architecture/x86_64/cpu.zig"),
|
||||||
.imports = &.{
|
.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
|
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus, ist_stack_size, timer_hz
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -130,7 +175,11 @@ pub fn build(b: *std.Build) void {
|
|||||||
const platform_module = b.addModule("platform", .{
|
const platform_module = b.addModule("platform", .{
|
||||||
.root_source_file = b.path("system/devices/platform.zig"),
|
.root_source_file = b.path("system/devices/platform.zig"),
|
||||||
.imports = &.{
|
.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)
|
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus (the discovery pool)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -144,23 +193,82 @@ pub fn build(b: *std.Build) void {
|
|||||||
.root_source_file = b.path("system/services/vfs/protocol.zig"),
|
.root_source_file = b.path("system/services/vfs/protocol.zig"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// The user-space runtime library (a nascent libc): system_call wrappers, the
|
// The input wire protocol: the input service's public interface, exposed as its own
|
||||||
// C-convention heap, IPC helpers, the process start shim. Compiled into every
|
// module the same way vfs-protocol is. Shared by the input service, the runtime's
|
||||||
// user binary (see addUserBinary), so it inherits each exe's `.large` code
|
// `input` helper (subscribe/publish), and every source and subscriber.
|
||||||
// model — do NOT set a target/code_model here. It imports `danos` for the
|
const input_protocol_module = b.addModule("input-protocol", .{
|
||||||
// shared SystemCall numbers and `vfs-protocol` for the file API.
|
.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", .{
|
const runtime_module = b.addModule("runtime", .{
|
||||||
.root_source_file = b.path("library/runtime/runtime.zig"),
|
.root_source_file = b.path("library/runtime/runtime.zig"),
|
||||||
.imports = &.{
|
.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 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 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 POSIX / C compatibility layer, a separate library layered strictly over the
|
||||||
|
// runtime (it calls the runtime's IPC/heap, never system calls directly). This is
|
||||||
|
// the one place POSIX/C spellings are allowed verbatim — see docs/coding-standards.md
|
||||||
|
// and library/posix/posix.zig.
|
||||||
|
const posix_module = b.addModule("posix", .{
|
||||||
|
.root_source_file = b.path("library/posix/posix.zig"),
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "runtime", .module = runtime_module },
|
||||||
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
.{ .name = "vfs-protocol", .module = vfs_protocol_module },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The initrd container format, shared by the kernel (unpacks it) and the
|
// The initial_ramdisk container format, shared by the kernel (unpacks it) and the
|
||||||
// build-time packer tools/mkinitrd.zig (produces it). No dependencies.
|
// build-time packer tools/make-initial-ramdisk.py (produces it). No dependencies.
|
||||||
const initrd_module = b.addModule("initrd", .{
|
const initial_ramdisk_module = b.addModule("initial-ramdisk", .{
|
||||||
.root_source_file = b.path("system/initrd.zig"),
|
.root_source_file = b.path("system/initial-ramdisk.zig"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Compile-time configuration the kernel reads as `@import("build_options")`. The
|
// Compile-time configuration the kernel reads as `@import("build_options")`. The
|
||||||
@@ -183,7 +291,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
const exe = b.addExecutable(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = "kernel",
|
.name = "kernel",
|
||||||
.root_module = b.createModule(.{
|
.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,
|
.target = kernel_target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
|
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
|
||||||
@@ -193,12 +301,14 @@ pub fn build(b: *std.Build) void {
|
|||||||
.stack_check = false, // stack-probe calls have no runtime to land in
|
.stack_check = false, // stack-probe calls have no runtime to land in
|
||||||
.stack_protector = false,
|
.stack_protector = false,
|
||||||
.imports = &.{
|
.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 = "architecture", .module = architecture_module },
|
||||||
.{ .name = "platform", .module = platform_module },
|
.{ .name = "platform", .module = platform_module },
|
||||||
.{ .name = "parameters", .module = parameters_module },
|
.{ .name = "parameters", .module = parameters_module },
|
||||||
.{ .name = "build_options", .module = build_options_module },
|
.{ .name = "build_options", .module = build_options_module },
|
||||||
.{ .name = "initrd", .module = initrd_module },
|
.{ .name = "initial-ramdisk", .module = initial_ramdisk_module },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -214,43 +324,123 @@ pub fn build(b: *std.Build) void {
|
|||||||
// (.text at 1 MiB), which the loader allocates and copies into.
|
// (.text at 1 MiB), which the loader allocates and copies into.
|
||||||
exe.image_base = 0xFFFFFFFF80100000;
|
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,
|
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
|
||||||
// linked into the kernel's user region against the `runtime` runtime library, and
|
// linked into the kernel's user region against the `runtime` runtime library, and
|
||||||
// started in ring 3 by the kernel's user-ELF loader.
|
// 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");
|
const init_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig");
|
||||||
b.installArtifact(init_exe);
|
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
|
// 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,
|
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
|
||||||
// which unpacks it and spawns each program (system/initrd.zig).
|
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
|
||||||
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, "vfs", "system/services/vfs/vfs.zig");
|
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_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 vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_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 ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
||||||
const busd_exe = addUserBinary(b, kernel_target, runtime_module, "busd", "system/drivers/busd/busd.zig");
|
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_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, posix_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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig");
|
||||||
|
const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_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, posix_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, posix_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, posix_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, posix_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 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, posix_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, posix_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, posix_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, posix_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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.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:
|
// (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"});
|
const mk_run = b.addSystemCommand(&.{"python3"});
|
||||||
mk_run.addFileArg(b.path("tools/mkinitrd.py"));
|
mk_run.addFileArg(b.path("tools/make-initial-ramdisk.py"));
|
||||||
const initrd_img = mk_run.addOutputFileArg("initrd.img");
|
const initial_ramdisk_img = mk_run.addOutputFileArg("initial-ramdisk.img");
|
||||||
mk_run.addArg("vfs");
|
mk_run.addArg("vfs");
|
||||||
mk_run.addFileArg(vfs_exe.getEmittedBin());
|
mk_run.addFileArg(vfs_exe.getEmittedBin());
|
||||||
mk_run.addArg("vfs-test");
|
mk_run.addArg("vfs-test");
|
||||||
mk_run.addFileArg(vfstest_exe.getEmittedBin());
|
mk_run.addFileArg(vfstest_exe.getEmittedBin());
|
||||||
mk_run.addArg("hpetd");
|
mk_run.addArg("ps2-bus");
|
||||||
mk_run.addFileArg(hpetd_exe.getEmittedBin());
|
mk_run.addFileArg(ps2_bus_exe.getEmittedBin());
|
||||||
mk_run.addArg("busd");
|
mk_run.addArg("ps2-keyboard");
|
||||||
mk_run.addFileArg(busd_exe.getEmittedBin());
|
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("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());
|
||||||
|
|
||||||
// Install the image to zig-out/bin (so the QEMU test harness picks it up like
|
// Also install the packed binaries to their FHS homes, so zig-out is a true image
|
||||||
// the other binaries). The run-x86-64 ESP install is added below.
|
// of the filesystem — even though at boot they arrive inside the initial-ramdisk.
|
||||||
const initrd_install = b.addInstallFile(initrd_img, "bin/initrd.img");
|
for ([_]struct { *std.Build.Step.Compile, []const u8 }{
|
||||||
b.getInstallStep().dependOn(&initrd_install.step);
|
.{ 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" },
|
||||||
|
}) |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.
|
// 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
|
// Each is its own binary/entry (a loader is built for its own target); today
|
||||||
@@ -265,12 +455,16 @@ pub fn build(b: *std.Build) void {
|
|||||||
}),
|
}),
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
.imports = &.{
|
.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);
|
||||||
|
|
||||||
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
|
// --- 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
|
// Firmware lives in different places per OS/distro, so probe the known
|
||||||
@@ -301,21 +495,8 @@ pub fn build(b: *std.Build) void {
|
|||||||
"/usr/local/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Intel)
|
"/usr/local/share/qemu/edk2-i386-vars.fd", // macOS Homebrew (Intel)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assemble an EFI System Partition layout: esp/EFI/BOOT/BOOTX64.efi
|
// The FHS zig-out (installed above) *is* the boot volume — no separate ESP to
|
||||||
const efi_install = b.addInstallArtifact(efiexe, .{
|
// assemble. QEMU presents it to the guest as a FAT drive below.
|
||||||
.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 firmware needs to write NVRAM, so give it a writable copy of the vars.
|
// The firmware needs to write NVRAM, so give it a writable copy of the vars.
|
||||||
const vars_copy = b.addSystemCommand(&.{ "cp", "-f", ovmf_vars });
|
const vars_copy = b.addSystemCommand(&.{ "cp", "-f", ovmf_vars });
|
||||||
@@ -323,6 +504,19 @@ pub fn build(b: *std.Build) void {
|
|||||||
|
|
||||||
const run_efi = b.addSystemCommand(&.{
|
const run_efi = b.addSystemCommand(&.{
|
||||||
"qemu-system-x86_64",
|
"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",
|
"-machine",
|
||||||
"q35",
|
"q35",
|
||||||
"-m",
|
"-m",
|
||||||
@@ -332,10 +526,10 @@ pub fn build(b: *std.Build) void {
|
|||||||
});
|
});
|
||||||
run_efi.addArg("-drive");
|
run_efi.addArg("-drive");
|
||||||
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
run_efi.addPrefixedFileArg("if=pflash,format=raw,file=", vars_out);
|
||||||
// Present the ESP directory to the guest as a FAT drive.
|
// Present the FHS zig-out to the guest as a FAT drive — it is the boot volume.
|
||||||
run_efi.addArgs(&.{
|
run_efi.addArgs(&.{
|
||||||
"-drive",
|
"-drive",
|
||||||
b.fmt("format=raw,file=fat:rw:{s}/esp", .{b.install_path}),
|
b.fmt("format=raw,file=fat:rw:{s}", .{b.install_path}),
|
||||||
"-net",
|
"-net",
|
||||||
"none",
|
"none",
|
||||||
// Emulated display advertising 1280x720 as its native (EDID preferred)
|
// Emulated display advertising 1280x720 as its native (EDID preferred)
|
||||||
@@ -346,16 +540,19 @@ pub fn build(b: *std.Build) void {
|
|||||||
"-device",
|
"-device",
|
||||||
"VGA,edid=on,xres=1280,yres=720",
|
"VGA,edid=on,xres=1280,yres=720",
|
||||||
});
|
});
|
||||||
// Always capture the guest's serial0 (the kernel's machine-readable log) to a
|
// Capture the guest's serial0 (danos's machine-readable log) to the qemu-test
|
||||||
// timestamped file under zig-out, so each run leaves its own log behind.
|
// scratch area — a dev/host artifact, kept out of the FHS boot volume we mount.
|
||||||
const serial_log = b.fmt("{s}/run-x86-64-serial0-{s}.log", .{ b.install_path, timestamp(b) });
|
// (/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.addArgs(&.{ "-serial", b.fmt("file:{s}", .{serial_log}) });
|
||||||
run_efi.step.dependOn(&efi_install.step);
|
// The whole FHS zig-out must be installed (and the scratch dir created) before we mount it.
|
||||||
run_efi.step.dependOn(&kernel_install.step);
|
run_efi.step.dependOn(b.getInstallStep());
|
||||||
run_efi.step.dependOn(&init_install.step);
|
run_efi.step.dependOn(&make_log_dir.step);
|
||||||
run_efi.step.dependOn(&initrd_esp_install.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);
|
run_efi_step.dependOn(&run_efi.step);
|
||||||
|
|
||||||
// const run_cmd = b.addRunArtifact(exe);
|
// const run_cmd = b.addRunArtifact(exe);
|
||||||
@@ -368,18 +565,66 @@ pub fn build(b: *std.Build) void {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Tests run on the host. The kernel and bootloader target freestanding/UEFI
|
// 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
|
// 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).
|
// here (compiled for the host rather than inheriting a freestanding target) —
|
||||||
const mod_tests = b.addTest(.{
|
// 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
|
||||||
|
}) |root| {
|
||||||
|
const mod_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path(root),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_module = b.createModule(.{
|
||||||
.root_source_file = b.path("system/danos.zig"),
|
.root_source_file = b.path("library/xkeyboard-config/xkeyboard-config.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "layouts", .module = xkb_layouts_module },
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
test_step.dependOn(&b.addRunArtifact(xkb_tests).step);
|
||||||
|
|
||||||
const run_mod_tests = b.addRunArtifact(mod_tests);
|
// 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);
|
||||||
|
|
||||||
const test_step = b.step("test", "Run tests");
|
// Convenience: `zig build gen-xkeyboard-config` regenerates the layout tables from the
|
||||||
test_step.dependOn(&run_mod_tests.step);
|
// 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);
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-19
@@ -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
|
until its hardware interrupts it**. The claim is the capability; `irq_ack` is the
|
||||||
unmask.
|
unmask.
|
||||||
14. **[driver-model.md](driver-model.md) — buses, classes and host controllers.** How
|
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
|
real driver stacks factor into three shapes and how families share code. The
|
||||||
proposed ABI for the three primitives still missing (capability passing, DMA +
|
three primitives it proposed are long since built (M13 capability passing,
|
||||||
memory barriers, MSI).
|
M14 DMA + barriers, M15 MSI), and the driver *contract* on top of them —
|
||||||
15. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
|
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.
|
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||||
|
|
||||||
Start with the north star:
|
Start with the north star:
|
||||||
@@ -64,6 +85,10 @@ Start with the north star:
|
|||||||
|
|
||||||
Cutting across all of these:
|
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
|
- **[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,
|
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.
|
leaving room for other systems (e.g. an AArch64 Raspberry Pi) later.
|
||||||
@@ -75,7 +100,16 @@ Cutting across all of these:
|
|||||||
when to build it, and how to keep it architecture-agnostic.
|
when to build it, and how to keep it architecture-agnostic.
|
||||||
- **[acpi.md](acpi.md) — finding the ACPI tables.** The concrete x86 locator chain:
|
- **[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`,
|
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
|
- **[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
|
(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.
|
right choice depends on whether danos is chasing real-time or resilience.
|
||||||
@@ -125,33 +159,63 @@ 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
|
what you see under `system/` in the source is what a running danos represents under
|
||||||
`/system`.
|
`/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)
|
system/ → /system danos's own internals (the self-representation)
|
||||||
danos.zig the kernel↔user ABI contract (the `danos` module)
|
boot-handoff.zig the loader↔kernel contract (the `boot-handoff` module)
|
||||||
parameters.zig initrd.zig shared contracts
|
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
|
kernel/ IPC, memory, scheduling, the private syscall dispatch
|
||||||
architecture/x86_64/ the `architecture` module (never named by generic code)
|
architecture/x86_64/ the `architecture` module (never named by generic code)
|
||||||
devices/ the device model /system/devices reflects (+ aml/)
|
devices/ the device model /system/devices reflects (+ aml/)
|
||||||
drivers/ hpetd/ busd/ one sub-project per driver → /system/drivers
|
device-abi.zig the device wire types (the `device-abi` module)
|
||||||
services/ init/ vfs/ system servers → /system/services (vfs/ holds
|
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)
|
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 — the stable application ABI
|
||||||
|
posix/ POSIX/C compatibility, layered over runtime
|
||||||
boot/ → /boot the loaders
|
boot/ → /boot the loaders
|
||||||
tools/ test/ host-side build + QEMU test harness
|
tools/ test/ host-side build + QEMU test harness
|
||||||
```
|
```
|
||||||
|
|
||||||
A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns
|
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
|
the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the POSIX
|
||||||
file layer imports by name. `usb`/`block` drivers will expose their protocols the same
|
layer imports by name. `usb`/`block` drivers will expose their protocols the same way.
|
||||||
way.
|
|
||||||
|
`library/posix/` is special: it is the **one place** POSIX/C spellings are allowed
|
||||||
|
verbatim (`stat`, `O_CREAT`, `fopen`, `errno`). Everywhere else follows the danos
|
||||||
|
naming rule with no exception — see [coding-standards.md](coding-standards.md). The
|
||||||
|
POSIX layer calls the runtime, never the kernel's system calls directly, so it never
|
||||||
|
appears in the private-ABI path.
|
||||||
|
|
||||||
## Source map
|
## Source map
|
||||||
|
|
||||||
| Area | Code |
|
| Area | Code |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Boot methods (one per way of booting the kernel) | `boot/` — `efi.zig` (UEFI) → `BOOTX64.efi` |
|
| Boot methods (one per way of booting the kernel) | `boot/` — `efi.zig` (UEFI) → `BOOTX64.efi` |
|
||||||
| Kernel entry, panic, bring-up | `system/kernel/main.zig` |
|
| Kernel entry, panic, bring-up | `system/kernel/kernel.zig` |
|
||||||
| Shared loader↔kernel contract (`BootInfo`, `Framebuffer`, `MemoryMap`, `Syscall`, ABI) | `system/danos.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` |
|
| Physical frame allocator | `system/kernel/pmm.zig` |
|
||||||
| Kernel heap (`std.mem.Allocator`) | `system/kernel/heap.zig` |
|
| Kernel heap (`std.mem.Allocator`) | `system/kernel/heap.zig` |
|
||||||
| Scheduler (fixed-priority preemptive; blocking, wait queues) | `system/kernel/scheduler.zig` |
|
| Scheduler (fixed-priority preemptive; blocking, wait queues) | `system/kernel/scheduler.zig` |
|
||||||
@@ -159,14 +223,15 @@ way.
|
|||||||
| IPC channels between kernel threads (message passing) | `system/kernel/ipc.zig` |
|
| 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` |
|
| 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` |
|
| 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` |
|
| 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/` |
|
| Hardware discovery (ACPI/device tree) behind one neutral device model | `system/devices/` |
|
||||||
| Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` |
|
| Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` |
|
||||||
| In-kernel test cases | `system/kernel/tests.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/` |
|
| 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/` |
|
| danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access — the stable application ABI | `library/runtime/` |
|
||||||
| System services (init, the VFS server + its `protocol` module) | `system/services/` |
|
| POSIX/C compatibility (`posix`): unistd, stdio — the one place POSIX names are allowed | `library/posix/` |
|
||||||
| Device drivers, one sub-project each (`hpetd` leaf driver, `busd` bus driver) | `system/drivers/` |
|
| 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` |
|
| Build + `run-x86-64` (QEMU/OVMF) | `build.zig` |
|
||||||
| QEMU integration test harness | `test/qemu_test.py` |
|
| 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
|
UEFI configuration table
|
||||||
│ the loader reads the RSDP's physical address
|
│ 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
|
│ the kernel forwards the whole BootInfo
|
||||||
▼
|
▼
|
||||||
platform.discover(boot_info, …) system/devices/platform.zig
|
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
|
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`
|
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:
|
deposits a value in the handoff struct:
|
||||||
|
|
||||||
```zig
|
```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
|
part that answers "where are the tables?" — everything past the RSDP is just following
|
||||||
more pointers the tables themselves provide.
|
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
|
## Related
|
||||||
|
|
||||||
- [efi.md](efi.md) — the loader that captures the RSDP before `ExitBootServices`.
|
- [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
|
- [memory-map.md](memory-map.md) — the same loader-captures / kernel-consumes seam, and
|
||||||
the ACPI-reclaim memory the RSDP lives in.
|
the ACPI-reclaim memory the RSDP lives in.
|
||||||
- [discovery.md](discovery.md) — the broader (still-evolving) plan for turning these
|
- [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`
|
- [arch.md](arch.md) — why the kernel reaches the device code through a `platform`
|
||||||
module and never names ACPI directly.
|
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
|
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
|
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
|
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
|
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
|
be substantial and per-arch, and *that* is when we extract an entry interface into
|
||||||
|
|||||||
+72
-15
@@ -6,7 +6,7 @@ Conventions for danos source. The overriding one, from which most of the rest fo
|
|||||||
> abbreviation is an acronym.**
|
> abbreviation is an acronym.**
|
||||||
|
|
||||||
`interruptDispatch`, not `intDisp`. `message_len`, not `message_len` (`msg` expands, `len`
|
`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
|
`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
|
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,
|
microkernel whose whole argument is that a human can hold each piece in their head,
|
||||||
@@ -58,13 +58,22 @@ abbreviation, expand it.
|
|||||||
|
|
||||||
Three, and only three.
|
Three, and only three.
|
||||||
|
|
||||||
1. **Foreign ABI names are spelled exactly as the ABI spells them.** A function that
|
1. **Foreign ABI names are spelled exactly as the ABI spells them — but only inside
|
||||||
*is* the C or POSIX interface keeps its name: `fopen`, `fwrite`, `fread`, `malloc`,
|
the layer that *is* that ABI.** A function that *is* the C or POSIX interface keeps
|
||||||
`calloc`, `realloc`, `free`, `memcpy`, `mmap`, `munmap`, `open`, `read`, `write`,
|
its name: `fopen`, `fwrite`, `fread`, `malloc`, `calloc`, `realloc`, `free`,
|
||||||
`close`, `lseek`, `stat`, `errno`. We don't get to rename `fwrite` to
|
`memcpy`, `mmap`, `munmap`, `open`, `read`, `write`, `close`, `lseek`, `stat`,
|
||||||
`fileWrite` — it wouldn't be `fwrite` any more. This also covers the syscall
|
`errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be
|
||||||
*wrappers* that exist to match those names. It does **not** license inventing new
|
`fwrite` any more.
|
||||||
abbreviated names in that style.
|
|
||||||
|
**This exception is scoped to one place: `library/posix/`.** A file under
|
||||||
|
`library/posix/` *is* the foreign ABI, so it keeps the ABI's spellings — that is the
|
||||||
|
whole rule for that directory. **Everywhere else, Zig/danos naming applies with no
|
||||||
|
POSIX exception**, so there is nothing to get wrong: if you're not in
|
||||||
|
`library/posix/`, expand it. A concept POSIX also has gets a danos name outside that
|
||||||
|
layer — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create`
|
||||||
|
flag, not `O_CREAT`; `library/posix/` is what maps `stat`→`status` and
|
||||||
|
`O_CREAT`→`create` at the boundary. (The `syscall` *wrappers* elsewhere are not an
|
||||||
|
exception to this — 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,
|
2. **Zig idioms are spelled the way Zig spells them.** Three names are the language's,
|
||||||
not ours, and are left alone:
|
not ours, and are left alone:
|
||||||
@@ -83,11 +92,12 @@ Three, and only three.
|
|||||||
keep `i`; a coordinate may be `x`, `y`. The moment the scope is big enough that the
|
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.
|
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
|
That's all — no Unix-abbreviation exception. The source directories are full words
|
||||||
their conventional names — `src`, `lib`, `sbin`, `bin`, `docs` — as do daemon
|
(`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver
|
||||||
programs by their `d` suffix (`hpetd`, `busd`, following `sshd`/`httpd`). These are
|
lives in `system/drivers/` and a service in `system/services/`, so the *location*
|
||||||
names a Unix reader already knows; expanding them fights the convention rather than
|
already says what it is. Encoding the role in the name too (`busd`, `vfsd`) is
|
||||||
serving it.
|
redundant — the program is just `ps2-bus`, `vfs`. Don't put in a name what its directory
|
||||||
|
already tells you.
|
||||||
|
|
||||||
## A note on collisions
|
## A note on collisions
|
||||||
|
|
||||||
@@ -118,15 +128,46 @@ Within those spelling rules, follow Zig's own conventions:
|
|||||||
|
|
||||||
- **Types** — `PascalCase`: `DeviceDescriptor`, `Endpoint`, `WaitQueue`.
|
- **Types** — `PascalCase`: `DeviceDescriptor`, `Endpoint`, `WaitQueue`.
|
||||||
- **Functions** — `camelCase`: `mapUserDeviceInto`, `notifyFromIsr`.
|
- **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`.
|
`notify_badge_bit`.
|
||||||
|
|
||||||
**File names are `kebab-case`.** A file named for a multi-word thing hyphenates it:
|
**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`,
|
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
|
`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.)
|
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
|
## Why acronyms are the line
|
||||||
|
|
||||||
Because an acronym has no letters to restore. `MMIO` doesn't become "memory mapped
|
Because an acronym has no letters to restore. `MMIO` doesn't become "memory mapped
|
||||||
@@ -135,3 +176,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
|
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
|
is a clipped form of?* If yes, write the word. If it's an initialism standing in for a
|
||||||
phrase, leave it.
|
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.
|
||||||
|
|||||||
@@ -4,39 +4,39 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o
|
|||||||
|
|
||||||
## Directory structure
|
## Directory structure
|
||||||
|
|
||||||
| Path | Description |
|
| Path | Description |
|
||||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| / | Primary hierarchy root and root directory of the entire file system hierarchy. |
|
| / | 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). |
|
| /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). |
|
| /dev | POSIX Device files (e.g., /dev/null, /dev/disk0, /dev/tty, /dev/random). |
|
||||||
| /etc | Host-specific system-wide configuration files. |
|
| /etc | Host-specific system-wide configuration files. |
|
||||||
| /home | Users' home directories, containing saved files, personal settings, etc. |
|
| /home | Users' home directories, containing saved files, personal settings, etc. |
|
||||||
| /lib | Libraries essential for the binaries in /bin and /sbin. eg realtime, system, ipc etc. |
|
| /lib | Libraries essential for the binaries in /bin and /sbin. eg realtime, system, ipc etc. |
|
||||||
| /sbin | Essential system binaries (e.g init) |
|
| /sbin | Essential system binaries (e.g init) |
|
||||||
| /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 |
|
| /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 | 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/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/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 |
|
| /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. |
|
| /tmp | Directory for temporary files (see also /var/tmp). Often not preserved between system reboots and may be severely size-restricted. |
|
||||||
| /usr | Secondary hierarchy for read-only user data; contains the majority of (multi-)user utilities and applications. Should be shareable and read-only. |
|
| /usr | Secondary hierarchy for read-only user data; contains the majority of (multi-)user utilities and applications. Should be shareable and read-only. |
|
||||||
| /var | Variable files: files whose content is expected to continually change during normal operation of the system, such as logs, spool files, and temporary e-mail files. |
|
| /var | Variable files: files whose content is expected to continually change during normal operation of the system, such as logs, spool files, and temporary e-mail files. |
|
||||||
|
|
||||||
## File types
|
## File types
|
||||||
|
|
||||||
POSIX specifies the long format of the ls command to represent the Unix file type as the first letter for an entry.
|
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 |
|
| 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). |
|
| 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. |
|
| 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. |
|
| 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. |
|
||||||
| FIFO special | p | A named pipe: an in-order byte stream between processes, where writers block until a reader opens the other end. |
|
| FIFO special | p | A named pipe: an in-order byte stream between processes, where writers block until a reader opens the other end. |
|
||||||
| block special | b | A device node addressed in fixed-size blocks with the kernel free to buffer and reorder access (e.g., /dev/disk0). |
|
| block special | b | A device node addressed in fixed-size blocks with the kernel free to buffer and reorder access (e.g., /dev/disk0). |
|
||||||
| character special | c | A device node addressed as an unbuffered byte stream, delivered to the driver in order (e.g., /dev/tty, /dev/null). |
|
| character special | c | A device node addressed as an unbuffered byte stream, delivered to the driver in order (e.g., /dev/tty, /dev/null). |
|
||||||
| socket | s | A named endpoint for bidirectional message-passing between processes, bound to a path rather than an address. |
|
| socket | s | A named endpoint for bidirectional message-passing between processes, bound to a path rather than an address. |
|
||||||
|
|
||||||
## /dev
|
## /dev
|
||||||
|
|
||||||
@@ -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
|
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
|
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
|
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
|
on `replyWait` for either an interrupt or a client request. `system/drivers/ps2-bus/ps2-bus.zig`
|
||||||
that program, minus the client half.
|
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
|
The obstacle was never the file type; it is which hardware a ring-3 driver can reach.
|
||||||
drive. Port I/O is unavailable to user space — the TSS I/O permission bitmap is absent
|
Direct `in`/`out` from user space is still a #GP (no TSS I/O bitmap, IOPL never raised),
|
||||||
and IOPL is never raised — so `in`/`out` from a driver is a #GP. That excludes the
|
but a driver no longer needs it: **`io_read`/`io_write`** grant port access the same way
|
||||||
16550 UART at `0x3F8` and PS/2 at `0x60`/`0x64`, which is to say it excludes the
|
`mmio_map` grants memory — gated by `device_claim` and the device's discovered `io_port`
|
||||||
obvious implementations of `/dev/tty`, `/dev/ttyS0` and a keyboard node. Until either
|
resource. So the 16550 UART at `0x3F8` and the PS/2 controller at `0x60`/`0x64` (and thus
|
||||||
port I/O grants or a memory-mapped UART exist, serial output stays a kernel service
|
`/dev/ttyS0` and a keyboard node) are now writable as ordinary ring-3 drivers; the
|
||||||
reached through the `write` system call rather than a file. A memory-mapped device such
|
low-rate legacy hardware that needs port I/O is fine with a syscall per access. A
|
||||||
as the framebuffer has no such problem and is the more likely first real entry here.
|
memory-mapped device such as the framebuffer, needing no port I/O at all, remains the
|
||||||
|
easiest first entry.
|
||||||
|
|
||||||
### Block devices
|
### 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
|
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.
|
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
|
A block driver is now **writable, but not yet memory-safe.** Every storage controller
|
||||||
plainly because it is not a matter of unwritten code. Every storage controller worth
|
worth naming is a bus master: it is programmed by handing it the physical address of a
|
||||||
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
|
||||||
descriptor ring and left to read and write memory on its own. A ring-3 driver cannot
|
**`dma_alloc`** now provides — physically contiguous, pinned, uncacheable, with its
|
||||||
build such a ring, because `mmap` returns writeback-cached, physically discontiguous
|
physical address disclosed — and **`/lib/mmio`**'s barriers order the descriptor writes
|
||||||
pages and never discloses their physical address. Nor should it be allowed to: a device
|
against the doorbell, and **`msi_bind`** delivers completions. So an AHCI or NVMe driver
|
||||||
programmed with an arbitrary physical address writes to arbitrary physical memory, and
|
can be written today (the M14/M15 work in [driver-model.md](driver-model.md); the earlier
|
||||||
page tables do not sit between a device and RAM — an IOMMU does. Granting a DMA-capable
|
"cannot host a block driver at all" is no longer true).
|
||||||
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.
|
|
||||||
|
|
||||||
Block devices therefore wait on DMA-capable memory, memory barriers, and VT-d/DMAR —
|
What is *not* yet true is that it is safe. A device programmed with an arbitrary physical
|
||||||
the M14–M16 work in [driver-model.md](driver-model.md). A ramdisk over the initrd is
|
address writes to arbitrary physical memory, and page tables do not sit between a device
|
||||||
the one block-shaped thing implementable now, and it needs no driver process.
|
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
|
### 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
|
time at. Making `sleep` itself sub-millisecond would take a tickless one-shot
|
||||||
timer — a later step.
|
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
|
## Two kinds of vector, one dispatch
|
||||||
|
|
||||||
The IDT now installs gates `0-47`: the 32 exceptions plus the device range. Every
|
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)
|
## What's next (not done here)
|
||||||
|
|
||||||
- **The keyboard**: the PS/2 controller is port-mapped (`0x60`/`0x64`), and ring 3
|
- **The keyboard**: the PS/2 controller is port-mapped (`0x60`/`0x64`), and port I/O is
|
||||||
has no port I/O yet, so the first *input* device is blocked on either an I/O
|
now available to ring 3 via the claim-gated `io_read`/`io_write` syscalls
|
||||||
permission bitmap or `io_in`/`io_out` syscalls ([drivers.md](drivers.md)).
|
([drivers.md](drivers.md)) — so the first *input* device is unblocked; it just needs
|
||||||
- **MSI/MSI-X**: per-device vectors, edge-triggered and unshared, which retire the
|
writing (claim the controller, `irq_bind` GSI 1, read scancodes from `0x60`).
|
||||||
I/O APIC's mask/ack cycle and its 24-GSI ceiling.
|
- **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
|
- **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.
|
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
|
- [ipc.md](ipc.md) — the channels that interrupts-as-messages and the device manager
|
||||||
will ride on.
|
will ride on.
|
||||||
- [vision.md](vision.md) — why drivers belong in isolated user space at all.
|
- [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
|
## 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
|
`DeviceDesc`, each with a parent, a class, and a set of resources. Firmware discovery
|
||||||
seeds it ([discovery.md](discovery.md)); `device_register` grows it.
|
seeds it ([discovery.md](discovery.md)); `device_register` grows it.
|
||||||
|
|
||||||
Three invariants make it a capability system rather than a directory:
|
Three invariants make it a capability system rather than a directory:
|
||||||
|
|
||||||
1. **A claim is exclusive.** `device_claim(id)` succeeds once. Everything downstream —
|
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
|
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
|
map its `.memory` resources and bind its `.irq` resources. This is why
|
||||||
`device_register` cannot be a free-for-all.
|
`device_register` cannot be a free-for-all.
|
||||||
3. **Therefore: containment.** Every resource of a registered child must lie inside a
|
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
|
ever *subdivide* what it already holds. Without this, `device_register` would be a
|
||||||
syscall named "map any physical page you like."
|
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
|
### 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
|
danos ships no demo bus driver — the real ones are `pci-bus`, `ps2-bus`, and
|
||||||
its "devices" are the block's comparators:
|
`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
|
```zig
|
||||||
_ = dev.claim(bus.id); // 1. own the bus
|
_ = 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
|
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
|
`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
|
whose window escapes the bus is refused; the in-kernel `containment` test asserts the
|
||||||
asserts the kernel's table upholds it.
|
kernel's table upholds that ([drivers.md](drivers.md)).
|
||||||
|
|
||||||
A USB device has *no* resources at all: `resource_count = 0`, because it's addressed
|
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.
|
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:
|
and its clients. The pattern generalises directly:
|
||||||
|
|
||||||
```
|
```
|
||||||
lib/
|
library/
|
||||||
rt.zig module "rt" — syscalls, heap, ipc, dev, stdio
|
runtime/ module "runtime" — syscalls, heap, ipc, device, stdio
|
||||||
mmio.zig module "mmio" — volatile register access + barriers [M14]
|
mmio/ module "mmio" — volatile register access + barriers [M14]
|
||||||
bus/
|
bus/
|
||||||
pci.zig module "pci" — ECAM, BAR decode, capability walk
|
pci/ module "pci" — ECAM, BAR decode, capability walk
|
||||||
usb.zig module "usb" — descriptors, control transfers, hubs
|
usb/ module "usb" — descriptors, control transfers, hubs
|
||||||
proto/
|
proto/
|
||||||
vfs.zig module "proto.vfs" (today: system/services/vfs/protocol.zig)
|
vfs/ module "vfs-protocol" (today: system/services/vfs/protocol.zig)
|
||||||
block.zig module "proto.block"
|
block/ module "block-protocol"
|
||||||
hid.zig module "proto.hid"
|
hid/ module "hid-protocol"
|
||||||
|
|
||||||
sbin/
|
system/drivers/ one sub-project each → /system/drivers (no `d` suffix)
|
||||||
xhcid.zig HCD + bus driver imports rt, pci, usb, mmio
|
xhci/ HCD + bus driver imports runtime, pci, usb, mmio
|
||||||
usbhid.zig class driver imports rt, usb, proto.hid
|
usb-hid/ class driver imports runtime, usb, hid-protocol
|
||||||
blockd.zig class driver imports rt, proto.block
|
block/ class driver imports runtime, block-protocol
|
||||||
```
|
```
|
||||||
|
|
||||||
The only build change needed: [`addUserBinary`](build.zig) currently takes exactly one
|
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
|
- **M11** — `irq_bind` / `irq_ack`. IRQ delivered as an IPC notification; mask before
|
||||||
EOI; `irq_ack` is the unmask.
|
EOI; `irq_ack` is the unmask.
|
||||||
- **M12** — `parent` in `DeviceDesc`, `device_register` with resource containment.
|
- **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
|
So: **bus drivers work now, and they're started by the device manager, not the kernel.**
|
||||||
exactly what would fix it.
|
HCDs and class drivers do not work yet. Here is exactly why, and exactly what would fix it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Proposed ABI
|
# 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
|
**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)`,
|
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
|
// 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
|
**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
|
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
|
(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.)
|
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.
|
**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
|
[`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
|
`.irq`; there is no `_PRT` parsing anywhere in the tree. The HPET is the one exception —
|
||||||
HPET advertises its own routing options in its own registers — a privilege no ordinary
|
it advertises its own interrupt routing in its own registers, a privilege no ordinary
|
||||||
device has.
|
device has.
|
||||||
|
|
||||||
**The fix, in two halves.**
|
**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
|
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.
|
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.
|
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*.
|
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
|
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
|
`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
|
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
|
`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
|
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
|
A driver is, in one sentence, *a process that sleeps until its device has something to
|
||||||
say.*
|
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 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 |
|
| # | 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
|
silently intercept it. Instead the kernel checks two things (`process.ownedGsi`, and
|
||||||
the same check at the top of `sysMmioMap`):
|
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`
|
- the resource at `res_idx` is of the right *kind* — `memory` for `mmio_map`, `irq`
|
||||||
for `irq_bind`
|
for `irq_bind`
|
||||||
|
|
||||||
@@ -138,14 +184,18 @@ Two properties worth knowing:
|
|||||||
|
|
||||||
## A whole driver
|
## 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
|
```zig
|
||||||
const hpet = findHpet(buf) orelse return; // device_enumerate, look for
|
const hpet = findHpet(buf) orelse return; // device_enumerate, look for
|
||||||
// class=timer with memory + irq
|
// class=timer with memory + irq
|
||||||
_ = dev.claim(hpet.dev_id); // the capability
|
_ = dev.claim(hpet.dev_id); // the capability
|
||||||
const base = dev.mmioMap(hpet.dev_id, hpet.mmio).?;
|
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
|
// program the hardware over the mapping we were just handed
|
||||||
reg(base, 0x100).* = level | int_enb | (hpet.gsi << 9); // timer 0 config
|
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
|
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 proved `mmio_map` without
|
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
|
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`
|
**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
|
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
|
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`.
|
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
|
See [`system/drivers/pci-bus/pci-bus.zig`](../system/drivers/pci-bus/pci-bus.zig) for a
|
||||||
[driver-model.md](driver-model.md) for how bus drivers, class drivers and host
|
real one — it claims a PCI host bridge, maps its ECAM window, and publishes each function
|
||||||
controller drivers fit together.
|
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
|
## What the kernel does not do for you
|
||||||
|
|
||||||
@@ -222,26 +273,24 @@ controller drivers fit together.
|
|||||||
|
|
||||||
Worth knowing before you write the second driver:
|
Worth knowing before you write the second driver:
|
||||||
|
|
||||||
- **Ring 3 has no port I/O.** The TSS I/O permission bitmap is absent
|
Several things this list used to warn about are now available (see
|
||||||
(`tss.zig`: `iomap_base = @sizeOf(Tss)`), and IOPL is never raised, so `in`/`out`
|
[driver-model.md](driver-model.md)): **port I/O** (`io_read`/`io_write`, claim-gated by
|
||||||
from a driver is a #GP. That rules out a user-space 16550 UART (`0x3F8`), PS/2
|
the device's `io_port` resource — direct ring-3 `in`/`out` is still a #GP, so a PS/2 or
|
||||||
(`0x60`/`0x64`), and legacy PCI config (`0xCF8`/`0xCFC`). Everything must be MMIO.
|
16550 driver goes through these), **DMA memory** (`dma_alloc`: contiguous, pinned,
|
||||||
`io_port` resources are recorded by discovery and then ignored.
|
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
|
- **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
|
granting one grants the other. A `device_register`ed child's *resource* can be narrower
|
||||||
than a page, but its *mapping* can't.
|
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
|
- **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
|
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
|
and RAM; an IOMMU does. The IOMMU is now *detected* (M16), but no translation domains
|
||||||
device is effectively equivalent to granting ring 0. This is the largest gap between
|
are programmed, so `device_claim` on a DMA-capable device is still effectively
|
||||||
the design's promise and what it delivers.
|
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
|
- **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
|
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`).
|
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
|
## Verifying it
|
||||||
|
|
||||||
The `hpet` test spawns `hpetd` from the initrd and watches the serial log. The driver
|
No demo driver ships to prove this end to end; the *real* drivers do, so the tests
|
||||||
prints `hpetd: ok` only after being woken five times, and its loop's only exit is
|
target them and the kernel primitives directly:
|
||||||
through `replyWait` returning a notification — it cannot reach that line by polling.
|
|
||||||
|
|
||||||
The last check doesn't trust the driver's self-report at all: the kernel reads the I/O
|
- **`device-manager`** — boots only the device manager, which discovers the PCI host
|
||||||
APIC redirection entry back and asserts the line really is routed to a device vector,
|
bridge, matches `pci-bus`, and `system_spawn`s it. The test reads kernel state — the
|
||||||
really is level-triggered, and really was left unmasked by the driver's final
|
process table and the device tree — to confirm pci-bus came up and registered the
|
||||||
`irq_ack`.
|
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
|
$ python3 test/qemu_test.py device-manager acpi-ps2 pci-scan containment irqfree iopass
|
||||||
hpet ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
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')
|
irqfree ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||||
iopass ... 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)
|
## What's next (not done here)
|
||||||
|
|
||||||
The big ones — capability passing (class drivers), DMA + barriers and MSI (host
|
The big driver-model pieces — capability passing (class drivers), DMA + barriers, MSI,
|
||||||
controller drivers), and the IOMMU — have proposed signatures in
|
and IOMMU detection — are **now done** ([driver-model.md](driver-model.md), M13–M16), as
|
||||||
[driver-model.md](driver-model.md). Smaller items:
|
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
|
- **Releasing a claim.** There is no `dev_release`, and `devices_broker` never drops a claim on
|
||||||
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
|
|
||||||
exit — only IRQ bindings are released. A dead driver's device stays owned forever,
|
exit — only IRQ bindings are released. A dead driver's device stays owned forever,
|
||||||
which blocks restart.
|
which blocks restart.
|
||||||
- **Unregistering children.** `device_register` only appends. A USB device that is
|
- **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
|
unplugged cannot be removed, and a bus driver in a loop can exhaust the 64-entry
|
||||||
table.
|
table.
|
||||||
- **Restart.** A driver that dies should release its claim, have its device quiesced,
|
- **Restart.** A supervisor that *spawns* drivers now exists — the device-manager starts
|
||||||
and be respawned by a supervisor. Some pieces (`releaseIrqs`, `device_grant`
|
them with `system_spawn` — but a supervisor that *restarts* them does not. A driver that
|
||||||
teardown, the claim table) exist; the policy doesn't.
|
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
|
- **Interrupt priority / threaded IRQ latency.** `notifyFromIsr` enqueues the woken
|
||||||
driver but doesn't preempt (`wakeLocked` deliberately leaves that to the caller), so
|
driver but doesn't preempt (`wakeLocked` deliberately leaves that to the caller), so
|
||||||
a woken driver waits for the next scheduling point.
|
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:
|
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
|
The boot volume is the **FHS-shaped `zig-out`** itself (see the repository-layout note
|
||||||
`uefi` target, installs it to `esp/EFI/BOOT/BOOTX64.efi`, and drops the kernel ELF
|
in [README.md](README.md)): `build.zig` installs `boot/efi.zig` (built for the `uefi`
|
||||||
at `esp/kernel`. The `run-x86-64` step then points QEMU at OVMF (UEFI firmware for
|
target) to `zig-out/EFI/BOOT/BOOTX64.efi` — the one path UEFI firmware fixes — and lays
|
||||||
virtual machines) and presents that `esp/` directory to the guest as a FAT drive.
|
the rest out by FHS path: the kernel at `zig-out/system/kernel`, init at
|
||||||
The firmware finds `BOOTX64.efi` and runs it — that's our `main()`.
|
`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
|
## 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
|
- Use the **LoadedImage** protocol to discover which device we booted from, then
|
||||||
**SimpleFileSystem** to open that volume.
|
**SimpleFileSystem** to open that volume.
|
||||||
- Open the file named `danos`, seek to the end to learn its size, rewind, and read
|
- Open the kernel ELF at its FHS path (`system\kernel`), seek to the end to learn its
|
||||||
the whole ELF into a firmware-allocated pool buffer. (`read` may return short, so
|
size, rewind, and read the whole ELF into a firmware-allocated pool buffer. (`read`
|
||||||
we loop.)
|
may return short, so we loop.)
|
||||||
- Parse the ELF: validate the `\x7fELF` magic and the `x86_64` machine type, then
|
- Parse the ELF: validate the `\x7fELF` magic and the `x86_64` machine type, then
|
||||||
walk the program headers. For every `PT_LOAD` segment we:
|
walk the program headers. For every `PT_LOAD` segment we:
|
||||||
- reserve the exact physical pages it's linked at (`p_paddr`) via
|
- reserve the exact physical pages it's linked at (`p_paddr`) via
|
||||||
@@ -121,7 +124,7 @@ entirely ours.
|
|||||||
### 4. Jump to the kernel
|
### 4. Jump to the kernel
|
||||||
|
|
||||||
```zig
|
```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);
|
@ptrFromInt(entry);
|
||||||
kernel(&boot_info);
|
kernel(&boot_info);
|
||||||
```
|
```
|
||||||
@@ -139,17 +142,19 @@ kernel is freestanding and uses the **SysV AMD64** convention (first argument in
|
|||||||
read garbage.
|
read garbage.
|
||||||
|
|
||||||
So both sides pin the convention explicitly to SysV via the shared
|
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
|
`boot_handoff.kernel_abi` (defined in `system/boot-handoff.zig`). The loader's
|
||||||
and the kernel's `_start` both reference it, so the pointer lands in the register
|
function-pointer type and the kernel's `_start` both reference it, so the pointer lands
|
||||||
the kernel expects. This is the whole reason `kernel_abi` lives in the shared
|
in the register the kernel expects. This is the whole reason `kernel_abi` lives in the
|
||||||
`danos` module: it's a contract both binaries must agree on. See
|
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.
|
[sysv.md](sysv.md) for what "SysV" means and where else it shows up.
|
||||||
|
|
||||||
## The handoff contract
|
## The handoff contract
|
||||||
|
|
||||||
The loader and kernel are two *separate* binaries built for two different targets,
|
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
|
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
|
- `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).
|
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
|
power on
|
||||||
-> UEFI firmware initialises hardware
|
-> 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
|
-> grab boot services
|
||||||
-> queryFramebuffer (via GOP: EDID native res, setMode, describe fb)
|
-> 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)
|
-> exitBootServices (retry until the memory-map key holds)
|
||||||
-> jump to e_entry, boot_info pointer in RDI
|
-> 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
|
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,
|
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.
|
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
|
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.)
|
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
|
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:
|
address it:
|
||||||
|
|
||||||
| Field | Meaning |
|
| Field | Meaning |
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@ treats the call:
|
|||||||
signature for a kernel entry point — the bootloader jumps in and nothing ever
|
signature for a kernel entry point — the bootloader jumps in and nothing ever
|
||||||
jumps back out.
|
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
|
`kmain` which is `noreturn`, which ends by calling `arch.halt()` which is
|
||||||
`noreturn`. The "never returns" property is threaded all the way down.
|
`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
|
## Reporting a fault
|
||||||
|
|
||||||
`isr_common` calls `exceptionHandler`, which forwards to a swappable `on_fault`
|
`isr_common` calls `exceptionHandler`, which forwards to a swappable `on_fault`
|
||||||
hook. The generic kernel installs a reporter (`onException` in `main.zig`) that
|
hook. The generic kernel installs a reporter (`onException` in `kernel.zig`) that
|
||||||
prints, in red, the exception name and vector, the error code, the faulting RIP
|
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
|
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
|
**CR2**. What happens next depends on where the fault came from:
|
||||||
terminal; the point is that it's now **visible** instead of a silent reset.
|
|
||||||
|
- **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
|
The hook is set before `arch.init()` in `kmain`, so a fault during setup is still
|
||||||
caught.
|
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
|
every capability is either well-known (the registry) or inherited — there's no way
|
||||||
to delegate one.
|
to delegate one.
|
||||||
- **Asynchronous / buffered send** for the cases where a rendezvous is the wrong
|
- **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
|
- **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.
|
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
|
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
|
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
|
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.
|
serial-less machine boots and runs correctly — it just goes quiet.
|
||||||
|
|
||||||
## Last-resort channels (no text output at all)
|
## 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
|
## 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
|
```zig
|
||||||
pub const MemoryKind = enum(u32) {
|
pub const MemoryKind = enum(u32) {
|
||||||
@@ -121,7 +121,7 @@ The kernel receives a plain array and reads it with zero UEFI knowledge:
|
|||||||
|
|
||||||
```zig
|
```zig
|
||||||
const mm = boot_info.memory_map;
|
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| {
|
for (regions) |r| {
|
||||||
if (r.kind == .usable) usable_pages += r.pages;
|
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
|
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
|
`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:
|
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 |
|
| 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: a
|
||||||
|
**musl-based C layer** (growing out of library/posix) that wires C programs to the
|
||||||
|
danos runtime — 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
|
# 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.**
|
**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
|
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
|
the reason the [microkernel](vision.md) shape was chosen, and it's a *separate* goal
|
||||||
@@ -111,9 +122,11 @@ Honest boundaries:
|
|||||||
## Suggested ordering
|
## Suggested ordering
|
||||||
|
|
||||||
1. **User mode + address-space isolation** — the shared prerequisite (also on the
|
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
|
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.
|
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
|
4. **Resource cleanup on death** — reclaim memory/MMIO/IPC/IRQ, via caps or a grant
|
||||||
table.
|
table.
|
||||||
|
|||||||
+1
-1
@@ -203,7 +203,7 @@ next lands.
|
|||||||
[scheduling.md](scheduling.md#affinity-pinning-a-task-to-a-core)). The `affinity`
|
[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 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.
|
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
|
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
|
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
|
**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)]
|
- **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()`**
|
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.
|
- **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
|
# 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
|
```zig
|
||||||
pub const kernel_abi: std.builtin.CallingConvention = .{ .x86_64_sysv = .{} };
|
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
|
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
|
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
|
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
|
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
|
whole reason `kernel_abi` lives in the shared contract — see [efi.md](efi.md) for
|
||||||
the handoff it governs.
|
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
|
## Where else it surfaces
|
||||||
|
|
||||||
- **The red zone → `red_zone = false`.** `build.zig` disables the red zone for the
|
- **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:
|
There are two layers:
|
||||||
|
|
||||||
- **Host unit tests** (`zig build test`) — for pure, platform-independent logic in
|
- **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
|
the shared contracts (`system/boot-handoff.zig`, `system/abi.zig`,
|
||||||
for the host and run natively.
|
`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
|
- **QEMU integration tests** (`python3 test/qemu_test.py`) — boot the real kernel
|
||||||
and check its behaviour. This is the interesting part.
|
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
|
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
|
**higher-half kernel** with a physmap, and **user space**: per-process address
|
||||||
spaces, `syscall`/`sysret` with the `swapgs` discipline, a user-ELF loader, and
|
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).
|
own page tables — plus a [test harness](testing.md).
|
||||||
|
|
||||||
- **Isolation track** — **user mode + address-space isolation**. *Done: a
|
- **Isolation track** — **user mode + address-space isolation**. *Done: a
|
||||||
higher-half kernel with a physmap (the low half is user space), per-process
|
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,
|
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
|
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
|
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),
|
syscalls (IPC_Call/IPC_ReplyWait — they arrive with the second user server),
|
||||||
|
|||||||
@@ -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,13 @@
|
|||||||
|
//! DanOS's POSIX / C compatibility layer — `unistd`, `stdio`, and (later) the C
|
||||||
|
//! `errno` / `struct stat` / `extern "C"` surface. This is the *one* place POSIX and
|
||||||
|
//! C spellings are allowed to appear verbatim (see docs/coding-standards.md): a file
|
||||||
|
//! under library/posix/ *is* the foreign ABI, so it keeps the ABI's names. Everything
|
||||||
|
//! it touches on the danos side (the VFS protocol, the runtime) uses danos names,
|
||||||
|
//! which this layer translates to at the boundary.
|
||||||
|
//!
|
||||||
|
//! It is layered strictly *over* the runtime: it calls the runtime's IPC and heap,
|
||||||
|
//! never the kernel's system calls directly. danos-native applications use the
|
||||||
|
//! runtime; this exists so *POSIX* software can too.
|
||||||
|
|
||||||
|
pub const unistd = @import("unistd.zig");
|
||||||
|
pub const stdio = @import("stdio.zig");
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const unistd = @import("unistd.zig");
|
const unistd = @import("unistd.zig");
|
||||||
const heap = @import("heap.zig");
|
const heap = @import("runtime").heap;
|
||||||
|
|
||||||
pub const SEEK_SET = unistd.SEEK_SET;
|
pub const SEEK_SET = unistd.SEEK_SET;
|
||||||
pub const SEEK_CURRENT = unistd.SEEK_CURRENT;
|
pub const SEEK_CURRENT = unistd.SEEK_CURRENT;
|
||||||
@@ -5,10 +5,9 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const protocol = @import("vfs-protocol");
|
const protocol = @import("vfs-protocol");
|
||||||
const ipc = @import("ipc.zig");
|
const ipc = @import("runtime").ipc;
|
||||||
const danos = @import("danos");
|
|
||||||
|
|
||||||
pub const O_CREAT = protocol.O_CREAT;
|
pub const O_CREAT = protocol.create;
|
||||||
pub const SEEK_SET: u32 = 0;
|
pub const SEEK_SET: u32 = 0;
|
||||||
pub const SEEK_CURRENT: u32 = 1;
|
pub const SEEK_CURRENT: u32 = 1;
|
||||||
pub const SEEK_END: u32 = 2;
|
pub const SEEK_END: u32 = 2;
|
||||||
@@ -110,11 +109,11 @@ pub fn lseek(fd: i32, off: i64, whence: u32) i64 {
|
|||||||
SEEK_SET => 0,
|
SEEK_SET => 0,
|
||||||
SEEK_CURRENT => @intCast(f.offset),
|
SEEK_CURRENT => @intCast(f.offset),
|
||||||
SEEK_END => blk: {
|
SEEK_END => blk: {
|
||||||
const request = protocol.Request{ .operation = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
||||||
var sbuf: [@sizeOf(protocol.Stat)]u8 = undefined;
|
var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
||||||
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
||||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.Stat)) return -1;
|
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1;
|
||||||
const st = std.mem.bytesToValue(protocol.Stat, sbuf[0..@sizeOf(protocol.Stat)]);
|
const st = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]);
|
||||||
break :blk @intCast(st.size);
|
break :blk @intCast(st.size);
|
||||||
},
|
},
|
||||||
else => return -1,
|
else => return -1,
|
||||||
@@ -126,17 +125,17 @@ pub fn lseek(fd: i32, off: i64, whence: u32) i64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stat `path`. Returns 0 or -1.
|
/// Stat `path`. Returns 0 or -1.
|
||||||
pub fn stat(path: []const u8, out: *protocol.Stat) i32 {
|
pub fn stat(path: []const u8, out: *protocol.FileStatus) i32 {
|
||||||
// Open, stat by node, close — simple and enough for now.
|
// Open, stat by node, close — simple and enough for now.
|
||||||
const fd = open(path, 0);
|
const fd = open(path, 0);
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
defer close(fd);
|
defer close(fd);
|
||||||
const f = fdPtr(fd).?;
|
const f = fdPtr(fd).?;
|
||||||
const request = protocol.Request{ .operation = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
|
||||||
var sbuf: [@sizeOf(protocol.Stat)]u8 = undefined;
|
var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
||||||
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
const r = transact(request, &.{}, &sbuf) orelse return -1;
|
||||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.Stat)) return -1;
|
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1;
|
||||||
out.* = std.mem.bytesToValue(protocol.Stat, sbuf[0..@sizeOf(protocol.Stat)]);
|
out.* = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3,13 +3,15 @@
|
|||||||
//! ownership of its hardware; the claim is the capability the kernel checks before
|
//! ownership of its hardware; the claim is the capability the kernel checks before
|
||||||
//! mapping registers or routing an IRQ.
|
//! 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");
|
const sc = @import("system-call.zig");
|
||||||
|
|
||||||
pub const DeviceDescriptor = danos.DeviceDescriptor;
|
pub const DeviceDescriptor = device_abi.DeviceDescriptor;
|
||||||
pub const ResourceDescriptor = danos.ResourceDescriptor;
|
pub const ResourceDescriptor = device_abi.ResourceDescriptor;
|
||||||
pub const DeviceClass = danos.DeviceClass;
|
pub const DeviceClass = device_abi.DeviceClass;
|
||||||
pub const ResourceKind = danos.ResourceKind;
|
pub const ResourceKind = device_abi.ResourceKind;
|
||||||
|
|
||||||
inline fn failed(r: usize) bool {
|
inline fn failed(r: usize) bool {
|
||||||
return r > ~@as(usize, 0) - 4095;
|
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.
|
/// `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.
|
/// 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
|
/// 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 {
|
pub fn irqAck(device_id: u64, resource_index: u64) bool {
|
||||||
return !failed(sc.systemCall2(.irq_ack, device_id, resource_index));
|
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);
|
||||||
|
}
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
//! lock and larger alignments come when user programs gain threads.
|
//! lock and larger alignments come when user programs gain threads.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const danos = @import("danos");
|
const abi = @import("abi");
|
||||||
const system = @import("system.zig");
|
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
|
/// A block header, at the start of every block; while free it also links the
|
||||||
/// free list via `next`.
|
/// 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.
|
/// grants usually are adjacent). Returns false if the kernel is out of memory.
|
||||||
fn grow(minimum_bytes: usize) bool {
|
fn grow(minimum_bytes: usize) bool {
|
||||||
const bytes = alignUp(@max(minimum_bytes, chunk), page_size);
|
const bytes = alignUp(@max(minimum_bytes, chunk), page_size);
|
||||||
const ret = system.mmap(bytes, system.PROT_READ | system.PROT_WRITE);
|
const ret = system_calls.mmap(bytes, system_calls.PROT_READ | system_calls.PROT_WRITE);
|
||||||
if (system.mmapFailed(ret)) return false;
|
if (system_calls.mmapFailed(ret)) return false;
|
||||||
|
|
||||||
const block: *Block = @ptrFromInt(ret);
|
const block: *Block = @ptrFromInt(ret);
|
||||||
block.size = bytes;
|
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
|
//! reached this way. The server side (`replyWait`, which returns two values) is
|
||||||
//! added with the first server binary.
|
//! added with the first server binary.
|
||||||
|
|
||||||
const danos = @import("danos");
|
const abi = @import("abi");
|
||||||
const sc = @import("system-call.zig");
|
const sc = @import("system-call.zig");
|
||||||
|
|
||||||
/// A small-int handle into the calling process's handle table.
|
/// 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.
|
/// Create a new endpoint owned by this process; returns its handle.
|
||||||
pub fn createEndpoint() ?Handle {
|
pub fn createIpcEndpoint() ?Handle {
|
||||||
const r = sc.systemCall0(.create_endpoint);
|
const r = sc.systemCall0(.create_ipc_endpoint);
|
||||||
return if (failed(r)) null else r;
|
return if (failed(r)) null else r;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish endpoint `h` under a well-known service id so other processes find it.
|
/// 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));
|
return !failed(sc.systemCall2(.ipc_register, @intFromEnum(id), h));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the endpoint published under `id`, installing a handle to it in this
|
/// Find the endpoint published under `id`, installing a handle to it in this
|
||||||
/// process.
|
/// process.
|
||||||
pub fn lookup(id: danos.ServiceId) ?Handle {
|
pub fn lookup(id: abi.ServiceId) ?Handle {
|
||||||
const r = sc.systemCall1(.ipc_lookup, @intFromEnum(id));
|
const r = sc.systemCall1(.ipc_lookup, @intFromEnum(id));
|
||||||
return if (failed(r)) null else r;
|
return if (failed(r)) null else r;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const CallError = error{Failed};
|
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`.
|
/// 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 {
|
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 (try callCap(h, message, reply, null)).len;
|
||||||
return if (failed(r)) error.Failed else r;
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// 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
|
/// bound device interrupt — rather than a client's message. The low bits carry the
|
||||||
/// GSI. See `isNotification`.
|
/// 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
|
/// Set alongside `notify_badge_bit` when the notification is a **signal** — the
|
||||||
/// task id, or an IRQ notification if the high bit is set).
|
/// 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 {
|
pub const Received = struct {
|
||||||
len: usize,
|
len: usize,
|
||||||
badge: u64,
|
badge: u64,
|
||||||
|
cap: ?Handle,
|
||||||
|
|
||||||
/// True if this wake-up was a device interrupt, not a client request. A driver's
|
/// True if this wake-up was an asynchronous notification (a device interrupt
|
||||||
/// event loop branches on this; there is no reply owed on the notification path.
|
/// 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 {
|
pub fn isNotification(self: Received) bool {
|
||||||
return self.badge & notify_badge_bit != 0;
|
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 {
|
pub fn source(self: Received) u64 {
|
||||||
return self.badge & ~notify_badge_bit;
|
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
|
/// Server side of IPC_ReplyWait: deliver `reply` to the client last received (if any,
|
||||||
/// any), then block until the next request arrives in `receive`. Returns its length
|
/// optionally handing it `send_cap`), then block until the next request arrives in
|
||||||
/// and the sender badge. This system_call returns two values — the length in rax and
|
/// `receive`. Returns its length, the sender badge, and any capability the request
|
||||||
/// the badge in rdx — so it needs a hand-written stub: rdx is a read-write
|
/// carried (in `.cap`). Three return values — length in rax, badge in rdx, received
|
||||||
/// operand (input = reply length, arg #3; output = badge).
|
/// handle in r8 — so it needs a hand-written stub: rdx is read-write (in: reply length,
|
||||||
pub fn replyWait(h: Handle, reply: []const u8, receive: []u8) Received {
|
/// 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 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"
|
asm volatile ("syscall"
|
||||||
: [rax] "={rax}" (rax),
|
: [rax] "={rax}" (rax),
|
||||||
[rdx] "+{rdx}" (rdx),
|
[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),
|
[a0] "{rdi}" (h),
|
||||||
[a1] "{rsi}" (@intFromPtr(reply.ptr)),
|
[a1] "{rsi}" (@intFromPtr(reply.ptr)),
|
||||||
[a3] "{r10}" (@intFromPtr(receive.ptr)),
|
[a3] "{r10}" (@intFromPtr(receive.ptr)),
|
||||||
[a4] "{r8}" (receive.len),
|
[a5] "{r9}" (send_cap orelse abi.no_cap),
|
||||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
: .{ .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,45 @@
|
|||||||
//! const runtime = @import("runtime");
|
//! const runtime = @import("runtime");
|
||||||
//! pub const panic = runtime.panic;
|
//! pub const panic = runtime.panic;
|
||||||
//! comptime { _ = &runtime.start._start; } // pull the entry shim in
|
//! 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");
|
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 heap = @import("heap.zig");
|
||||||
pub const ipc = @import("ipc.zig");
|
pub const ipc = @import("ipc.zig");
|
||||||
pub const start = @import("start.zig");
|
pub const start = @import("start.zig");
|
||||||
/// The VFS wire protocol (shared with the VFS server).
|
/// The VFS wire protocol (shared with the VFS server).
|
||||||
pub const vfs_protocol = @import("vfs-protocol");
|
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.
|
/// 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.
|
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
|
||||||
pub const stdio = @import("stdio.zig");
|
|
||||||
/// Device access for drivers: enumerate/claim/mmioMap.
|
/// Device access for drivers: enumerate/claim/mmioMap.
|
||||||
pub const device = @import("device.zig");
|
pub const device = @import("device.zig");
|
||||||
|
/// DMA-capable memory for drivers: contiguous, pinned, uncacheable buffers.
|
||||||
|
pub const dma = @import("dma.zig");
|
||||||
|
|
||||||
/// Re-exported so a user binary can `pub const panic = runtime.panic;`.
|
/// Re-exported so a user binary can `pub const panic = runtime.panic;`.
|
||||||
pub const panic = start.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.
|
/// The heap as a `std.mem.Allocator`, for Zig `std` containers in user code.
|
||||||
pub const allocator = heap.allocator;
|
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 std = @import("std");
|
||||||
const system = @import("system.zig");
|
const system = @import("system.zig");
|
||||||
|
const process = @import("process.zig");
|
||||||
|
|
||||||
/// The kernel enters at `_start` with rsp 16-aligned, but a SystemV function expects
|
/// The kernel enters at `_start` with rsp 16-aligned, pointing at the System V
|
||||||
/// rsp ≡ 8 (mod 16) on entry (as if reached by `call`). The `call` below pushes
|
/// process-entry block it built: argc, argv pointers, NULL, envp terminator, the
|
||||||
/// the 8-byte return address, satisfying the ABI before any Zig frame runs; the
|
/// auxiliary vector, then the strings (see system/kernel/process.zig,
|
||||||
/// `ud2` is a safety net if `rt_start` ever returns.
|
/// `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 {
|
pub export fn _start() callconv(.naked) noreturn {
|
||||||
asm volatile (
|
asm volatile (
|
||||||
|
\\mov %%rsp, %%rdi
|
||||||
\\call rt_start
|
\\call rt_start
|
||||||
\\ud2
|
\\ud2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The first Zig frame. The heap is lazy (first alloc grows it), so there is no
|
/// The first Zig frame, entered with `stack` pointing at the kernel-built entry
|
||||||
/// runtime init to order here — just hand control to the program's `main`.
|
/// block. Build the `process.Init` from it and dispatch to the program's `main`,
|
||||||
export fn rt_start() callconv(.c) noreturn {
|
/// 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
|
const root = @import("root"); // the user binary's root source file
|
||||||
root.main();
|
const main_information = @typeInfo(@TypeOf(root.main)).@"fn";
|
||||||
system.exit(0);
|
|
||||||
|
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.
|
/// No runtime to unwind into — report a panic as a nonzero exit code.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
//! Note argument #3 goes in **r10, not rcx** — rcx is unavailable across the
|
//! Note argument #3 goes in **r10, not rcx** — rcx is unavailable across the
|
||||||
//! instruction, so the kernel reads the 4th argument from r10.
|
//! instruction, so the kernel reads the 4th argument from r10.
|
||||||
|
|
||||||
const danos = @import("danos");
|
const abi = @import("abi");
|
||||||
const SystemCall = danos.SystemCall;
|
const SystemCall = abi.SystemCall;
|
||||||
|
|
||||||
pub inline fn systemCall0(n: SystemCall) usize {
|
pub inline fn systemCall0(n: SystemCall) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
@@ -19,34 +19,49 @@ pub inline fn systemCall0(n: SystemCall) usize {
|
|||||||
pub inline fn systemCall1(n: SystemCall, a0: usize) usize {
|
pub inline fn systemCall1(n: SystemCall, a0: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [ret] "={rax}" (-> usize),
|
||||||
: [n] "{rax}" (@intFromEnum(n)), [a0] "{rdi}" (a0),
|
: [n] "{rax}" (@intFromEnum(n)),
|
||||||
|
[a0] "{rdi}" (a0),
|
||||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn systemCall2(n: SystemCall, a0: usize, a1: usize) usize {
|
pub inline fn systemCall2(n: SystemCall, a0: usize, a1: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [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 });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn systemCall3(n: SystemCall, a0: usize, a1: usize, a2: usize) usize {
|
pub inline fn systemCall3(n: SystemCall, a0: usize, a1: usize, a2: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [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 });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn systemCall4(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize) usize {
|
pub inline fn systemCall4(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [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 });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn systemCall5(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize) usize {
|
pub inline fn systemCall5(n: SystemCall, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize) usize {
|
||||||
return asm volatile ("syscall"
|
return asm volatile ("syscall"
|
||||||
: [ret] "={rax}" (-> usize),
|
: [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 });
|
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-5
@@ -1,15 +1,20 @@
|
|||||||
//! Typed system_call surface for user space — thin wrappers over the raw `system_call`
|
//! 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.
|
//! 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");
|
const sc = @import("system-call.zig");
|
||||||
|
|
||||||
/// `mmap` protection flags (matching the usual C bit values). Grants are always
|
/// `mmap` protection flags (matching the usual C bit values). Grants are always
|
||||||
/// readable+writable today; the kernel does not yet honour finer prot.
|
/// readable+writable today; the kernel does not yet honour finer prot.
|
||||||
pub const PROT_READ: usize = danos.prot_read;
|
pub const PROT_READ: usize = abi.prot_read;
|
||||||
pub const PROT_WRITE: usize = danos.prot_write;
|
pub const PROT_WRITE: usize = abi.prot_write;
|
||||||
pub const PROT_EXEC: usize = danos.prot_exec;
|
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.
|
/// Give up the rest of this quantum.
|
||||||
pub fn yield() void {
|
pub fn yield() void {
|
||||||
@@ -27,12 +32,102 @@ pub fn sleep(ms: usize) void {
|
|||||||
_ = sc.systemCall1(.sleep, ms);
|
_ = 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));
|
||||||
|
}
|
||||||
|
|
||||||
/// End the process. Never returns.
|
/// End the process. Never returns.
|
||||||
pub fn exit(code: usize) noreturn {
|
pub fn exit(code: usize) noreturn {
|
||||||
_ = sc.systemCall1(.exit, code);
|
_ = sc.systemCall1(.exit, code);
|
||||||
unreachable; // the kernel never returns from exit
|
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
|
/// 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
|
/// 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.
|
/// 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
//! 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
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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
|
//! The **loader ↔ kernel** contract: everything a bootloader (boot/, e.g. efi.zig
|
||||||
//! (boot/, e.g. efi.zig built as BOOTX64.efi) and the kernel (system/kernel/main.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
|
//! Both binaries import this as the `boot-handoff` module, so the layout is defined
|
||||||
//! defined in exactly one place.
|
//! 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");
|
const std = @import("std");
|
||||||
|
|
||||||
@@ -29,10 +32,10 @@ pub const PixelFormat = enum(u32) {
|
|||||||
/// Output Protocol (a headless server, say). The kernel must treat on-screen
|
/// Output Protocol (a headless server, say). The kernel must treat on-screen
|
||||||
/// output as optional and never assume a framebuffer exists.
|
/// output as optional and never assume a framebuffer exists.
|
||||||
pub const Framebuffer = extern struct {
|
pub const Framebuffer = extern struct {
|
||||||
base: usize, // the memory address where pixel data starts (0 = none)
|
base: usize, // the memory address where pixel data starts (0 = none)
|
||||||
width: u32, // visible pixels per row (e.g. 1920)
|
width: u32, // visible pixels per row (e.g. 1920)
|
||||||
height: u32, // visible rows (e.g. 1080)
|
height: u32, // visible rows (e.g. 1080)
|
||||||
pitch: u32, // bytes from the start of one row to the start of the next
|
pitch: u32, // bytes from the start of one row to the start of the next
|
||||||
format: PixelFormat,
|
format: PixelFormat,
|
||||||
|
|
||||||
/// Whether a usable framebuffer was handed over.
|
/// Whether a usable framebuffer was handed over.
|
||||||
@@ -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
|
/// 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
|
/// `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
|
/// 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 physmap_base: u64 = 0xFFFF_8800_0000_0000;
|
||||||
pub const kernel_virt_base: u64 = 0xFFFF_FFFF_8000_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
|
/// Physical address -> its virtual address in the physmap. The single way the
|
||||||
/// kernel dereferences a physical address once paging is up.
|
/// 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.
|
/// firmware's variable descriptor-stride to worry about.
|
||||||
pub const MemoryRegion = extern struct {
|
pub const MemoryRegion = extern struct {
|
||||||
base: u64, // physical start address
|
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,
|
kind: MemoryKind,
|
||||||
_pad: u32 = 0,
|
_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`
|
/// 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.
|
/// field instead, so the kernel discovers devices without knowing what booted it.
|
||||||
acpi_rsdp: u64 = 0,
|
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
|
/// 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
|
/// 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_base: u64 = 0,
|
||||||
init_len: 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
|
/// 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.
|
/// handoff, same as `init` above. 0/0 = no initial_ramdisk. See system/initial-ramdisk.zig.
|
||||||
initrd_base: u64 = 0,
|
initial_ramdisk_base: u64 = 0,
|
||||||
initrd_len: 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).
|
//! the `Hal.mapMmio` callback the caller supplies (the architecture VMM's map primitive).
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const danos = @import("danos");
|
const boot_handoff = @import("boot-handoff");
|
||||||
|
const abi = @import("abi");
|
||||||
const parameters = @import("parameters");
|
const parameters = @import("parameters");
|
||||||
const device_model = @import("device-model.zig");
|
const device_model = @import("device-model.zig");
|
||||||
const aml = @import("aml/aml.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
|
/// Everything the power subsystem needs, extracted from the FADT and the AML
|
||||||
/// sleep packages during discovery. Populated by `discover`, read by `power`.
|
/// sleep packages during discovery. Populated by `discover`, read by `power`.
|
||||||
pub const PowerInformation = struct {
|
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.
|
/// The SMM command port and the value that switches the platform into ACPI mode.
|
||||||
smi_cmd: u16 = 0,
|
smi_cmd: u16 = 0,
|
||||||
acpi_enable: u8 = 0,
|
acpi_enable: u8 = 0,
|
||||||
@@ -50,7 +54,8 @@ pub const PowerInformation = struct {
|
|||||||
reset: RegisterAccess = .{},
|
reset: RegisterAccess = .{},
|
||||||
reset_value: u8 = 0,
|
reset_value: u8 = 0,
|
||||||
reset_supported: bool = false,
|
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,
|
s5: ?aml.SleepType = null,
|
||||||
s3: ?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).
|
/// ISA-IRQ-to-GSI remappings from the MADT (for future IOAPIC routing).
|
||||||
overrides: [16]IsoEntry = undefined,
|
overrides: [16]IsoEntry = undefined,
|
||||||
override_count: usize = 0,
|
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.
|
/// 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.
|
/// Physical address of the DSDT the FADT points at, or 0.
|
||||||
pub var dsdt_physical: u64 = 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
|
// 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
|
// address + length of each table's post-header bytecode. Scanned after the walk
|
||||||
// for the sleep-state (`_Sx`) packages.
|
// for the sleep-state (`_Sx`) packages.
|
||||||
@@ -147,7 +173,7 @@ var aml_block_count: usize = 0;
|
|||||||
|
|
||||||
fn addAmlBlock(sdt_physical: u64) void {
|
fn addAmlBlock(sdt_physical: u64) void {
|
||||||
if (aml_block_count >= aml_block_physical.len or sdt_physical == 0) return;
|
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;
|
if (h.length <= @sizeOf(SystemDescriptorTableHeader)) return;
|
||||||
aml_block_physical[aml_block_count] = sdt_physical + @sizeOf(SystemDescriptorTableHeader);
|
aml_block_physical[aml_block_count] = sdt_physical + @sizeOf(SystemDescriptorTableHeader);
|
||||||
aml_block_len[aml_block_count] = h.length - @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),
|
root_system_description_table_address: u32 align(1),
|
||||||
/// The size of the RSDP.
|
/// The size of the RSDP.
|
||||||
length: u32 align(1),
|
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),
|
extended_system_descriptor_table_address: u64 align(1),
|
||||||
/// A checksum used for the entire table.
|
/// A checksum used for the entire table.
|
||||||
extended_checksum: u8,
|
extended_checksum: u8,
|
||||||
@@ -232,6 +259,7 @@ const SLIT: [4]u8 = "SLIT".*;
|
|||||||
/// System Resource Affinity Table (SRAT)
|
/// System Resource Affinity Table (SRAT)
|
||||||
const SRAT: [4]u8 = "SRAT".*;
|
const SRAT: [4]u8 = "SRAT".*;
|
||||||
/// Secondary System Description Table (SSDT)
|
/// Secondary System Description Table (SSDT)
|
||||||
|
const DMAR: [4]u8 = "DMAR".*;
|
||||||
const SSDT: [4]u8 = "SSDT".*;
|
const SSDT: [4]u8 = "SSDT".*;
|
||||||
/// Serial Port Console Redirection table (SPCR) — the firmware's console UART.
|
/// Serial Port Console Redirection table (SPCR) — the firmware's console UART.
|
||||||
const SPCR: [4]u8 = "SPCR".*;
|
const SPCR: [4]u8 = "SPCR".*;
|
||||||
@@ -341,49 +369,33 @@ const Hpet = extern struct {
|
|||||||
page_protection: u8,
|
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 ------------------------------------------------------------
|
// --- Entry point ------------------------------------------------------------
|
||||||
|
|
||||||
/// Discover hardware from the ACPI tables rooted at `rsdp_physical` and populate
|
/// 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
|
/// `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.
|
/// 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;
|
if (rsdp_physical == 0) return error.NoRsdp;
|
||||||
|
boot_memory_regions = memory_regions;
|
||||||
|
|
||||||
// Start clean so a re-run doesn't accumulate stale state.
|
// Start clean so a re-run doesn't accumulate stale state.
|
||||||
power_information = .{};
|
power_information = .{};
|
||||||
|
fadt_physical = 0;
|
||||||
|
fadt_length = 0;
|
||||||
platform_information = .{};
|
platform_information = .{};
|
||||||
aml_stats = .{};
|
aml_stats = .{};
|
||||||
namespace = null;
|
namespace = null;
|
||||||
dsdt_physical = 0;
|
dsdt_physical = 0;
|
||||||
aml_block_count = 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;
|
if (!std.mem.eql(u8, &rsdp.signature, "RSD PTR ")) return error.BadRsdpSignature;
|
||||||
// Revision 0 checksums only the first 20 bytes (the v1.0 RSDP).
|
// 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) {
|
if (rsdp.revision >= 2) {
|
||||||
const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(danos.physicalToVirtual(rsdp_physical));
|
const xsdp: *const ExtendedSystemDescriptorPointer = @ptrFromInt(boot_handoff.physicalToVirtual(rsdp_physical));
|
||||||
if (!checksumOk(@ptrFromInt(danos.physicalToVirtual(rsdp_physical)), xsdp.length)) return error.BadXsdpChecksum;
|
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);
|
try walkRoot(u64, xsdp.extended_system_descriptor_table_address, device_tree, hal);
|
||||||
} else {
|
} else {
|
||||||
try walkRoot(u32, rsdp.root_system_description_table_address, device_tree, hal);
|
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.
|
// read the sleep types from it.
|
||||||
var blocks: [aml_block_physical.len][]const u8 = undefined;
|
var blocks: [aml_block_physical.len][]const u8 = undefined;
|
||||||
for (0..aml_block_count) |i| {
|
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];
|
const active = blocks[0..aml_block_count];
|
||||||
if (aml.parse(device_tree.allocator, active)) |pr| {
|
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 };
|
aml_stats = .{ .nodes = namespace.?.nodeCount(), .consumed = pr.consumed, .total = pr.total };
|
||||||
power_information.s5 = aml.sleepState(&namespace.?, 5);
|
power_information.s5 = aml.sleepState(&namespace.?, 5);
|
||||||
power_information.s3 = aml.sleepState(&namespace.?, 3);
|
power_information.s3 = aml.sleepState(&namespace.?, 3);
|
||||||
// Fold the namespace's Device objects into the generic tree.
|
// The namespace's Device objects are no longer folded into the kernel
|
||||||
wireAcpiDevices(device_tree, &namespace.?, hal) catch {};
|
// 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 |_| {
|
} else |_| {
|
||||||
// AML parse failed (e.g. out of memory); power stays best-effort with
|
// AML parse failed (e.g. out of memory); power stays best-effort with
|
||||||
// whatever the FADT alone provided.
|
// 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
|
/// 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.
|
/// 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 {
|
fn walkRoot(comptime Entry: type, root_physical: u64, device_tree: *DeviceTree, hal: Hal) !void {
|
||||||
const header: *const SystemDescriptorTableHeader = @ptrFromInt(danos.physicalToVirtual(root_physical));
|
const header: *const SystemDescriptorTableHeader = @ptrFromInt(boot_handoff.physicalToVirtual(root_physical));
|
||||||
if (!checksumOk(@ptrFromInt(danos.physicalToVirtual(root_physical)), header.length)) return error.BadRootChecksum;
|
if (!checksumOk(@ptrFromInt(boot_handoff.physicalToVirtual(root_physical)), header.length)) return error.BadRootChecksum;
|
||||||
|
|
||||||
const count = (header.length - @sizeOf(SystemDescriptorTableHeader)) / @sizeOf(Entry);
|
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));
|
const entries: [*]align(1) const Entry = @ptrCast(base + @sizeOf(SystemDescriptorTableHeader));
|
||||||
|
|
||||||
for (entries[0..count]) |ent| {
|
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.
|
/// Dispatch a single SDT on its signature.
|
||||||
fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void {
|
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;
|
const sig = header.signature;
|
||||||
if (std.mem.eql(u8, &sig, &APIC)) {
|
if (std.mem.eql(u8, &sig, &APIC)) {
|
||||||
try parseMadt(device_tree, header);
|
try parseMadt(device_tree, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &MCFG)) {
|
} 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)) {
|
} else if (std.mem.eql(u8, &sig, &HPET)) {
|
||||||
try parseHpet(device_tree, hal, header);
|
try parseHpet(device_tree, hal, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &FACP)) {
|
} else if (std.mem.eql(u8, &sig, &FACP)) {
|
||||||
|
fadt_physical = sdt_physical;
|
||||||
|
fadt_length = header.length;
|
||||||
parseFadt(header);
|
parseFadt(header);
|
||||||
} else if (std.mem.eql(u8, &sig, &SPCR)) {
|
} else if (std.mem.eql(u8, &sig, &SPCR)) {
|
||||||
parseSpcr(header);
|
parseSpcr(header);
|
||||||
|
} else if (std.mem.eql(u8, &sig, &DMAR)) {
|
||||||
|
parseDmar(hal, header);
|
||||||
} else if (std.mem.eql(u8, &sig, &SSDT)) {
|
} else if (std.mem.eql(u8, &sig, &SSDT)) {
|
||||||
// Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan.
|
// Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan.
|
||||||
addAmlBlock(sdt_physical);
|
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.
|
/// 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 total: usize = header.length;
|
||||||
const base: [*]const u8 = @ptrCast(header);
|
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.
|
// ECAM window: 1 MiB of configuration space per bus.
|
||||||
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
|
_ = bridge.addResource(.memory, alloc.base_address, bus_count << 20);
|
||||||
_ = bridge.addResource(.bus_range, alloc.start_bus, bus_count);
|
_ = 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
|
/// The boot memory map, stored at discover() entry for the aperture derivation
|
||||||
/// bridge recursion yet: on the ECAM path the host bridge decodes every bus in
|
/// below (and, in M20, for the acpi-tables node's containment windows).
|
||||||
/// the window, so scanning the declared range finds everything QEMU exposes.
|
var boot_memory_regions: []const boot_handoff.MemoryRegion = &.{};
|
||||||
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
|
|
||||||
|
|
||||||
const funcs: u8 = if (h0.header_type & 0x80 != 0) 8 else 1;
|
/// The bridge's MMIO apertures, derived from the boot memory map's holes
|
||||||
var function: u8 = 0;
|
/// (docs/discovery.md — apertures from the memory map): registered PCI functions carry BAR
|
||||||
while (function < funcs) : (function += 1) {
|
/// resources, and `device_register` containment demands the bridge own windows
|
||||||
const configuration = pciConfigurationPtr(alloc, hal, @intCast(bus), device, function);
|
/// that cover them. Everything the firmware described is "not hole"; the low
|
||||||
const h: *align(1) const PciHeader = @ptrCast(configuration);
|
/// aperture runs from the end of the described space below 4 GiB up to the
|
||||||
if (h.vendor_id == 0xFFFF) continue;
|
/// 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
|
||||||
var nb: [24]u8 = undefined;
|
/// matter what later moved to user space.
|
||||||
const nm = std.fmt.bufPrint(&nb, "{s}:{x:0>2}:{x:0>2}.{d}", .{
|
fn addBridgeApertures(bridge: *device_model.Device) void {
|
||||||
bridge.name(), bus, device, function,
|
// Below 4 GiB the described regions are sparse (RAM low, firmware flash
|
||||||
}) catch "pcidev";
|
// and tables high), so the holes are the *gaps between* them — a single
|
||||||
const node = try device_tree.addChild(bridge, .pci_device, nm);
|
// "after the last region" rule dies on OVMF's flash at the very top.
|
||||||
node.ids.pci_vendor = h.vendor_id;
|
// Sort-merge the described ranges, then keep the three largest gaps
|
||||||
node.ids.pci_device = h.device_id;
|
// (resource slots are bounded at 8 per device; ECAM + bus range + 3 + the
|
||||||
node.ids.pci_class = (@as(u24, h.class_code) << 16) |
|
// high aperture fits). Above 4 GiB one aperture runs from the end of the
|
||||||
(@as(u24, h.subclass) << 8) | h.prog_if;
|
// described space to the 46-bit line.
|
||||||
node.ids.pci_bdf = (@as(u16, @intCast(bus)) << 8) | (@as(u16, device) << 3) | function;
|
const Range = struct { base: u64, end: u64 };
|
||||||
|
var below: [64]Range = undefined;
|
||||||
// BARs only exist in header type 0 (normal devices), not bridges.
|
var below_count: usize = 0;
|
||||||
if (h.header_type & 0x7F == 0) addBars(node, configuration);
|
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;
|
||||||
}
|
}
|
||||||
}
|
for (gaps) |gap| {
|
||||||
|
if (gap.end > gap.base) _ = bridge.addResource(.memory, gap.base, gap.end - gap.base);
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
_ = bridge.addResource(.memory, high_end, (@as(u64, 1) << 46) - high_end);
|
||||||
wr(u16, configuration, 0x04, command); // restore decode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HPET -> a timer node with its register block as an MMIO resource, plus the GSI
|
/// 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_pm1b_cnt_blk = 68; // u32 (I/O port)
|
||||||
const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter
|
const fadt_pm_tmr_blk = 76; // u32 (I/O port) — the PM timer counter
|
||||||
const fadt_pm1_cnt_len = 89; // u8 (bytes)
|
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_flags = 112; // u32
|
||||||
const fadt_reset_register = 116; // GAS (12 bytes)
|
const fadt_reset_register = 116; // GAS (12 bytes)
|
||||||
const fadt_reset_value = 128; // u8
|
const fadt_reset_value = 128; // u8
|
||||||
@@ -699,6 +747,7 @@ fn parseFadt(header: *const SystemDescriptorTableHeader) void {
|
|||||||
const len: usize = header.length;
|
const len: usize = header.length;
|
||||||
const pi = &power_information;
|
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.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_enable = fadt(u8, base, len, fadt_acpi_enable) orelse 0;
|
||||||
pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0;
|
pi.acpi_disable = fadt(u8, base, len, fadt_acpi_disable) orelse 0;
|
||||||
@@ -740,337 +789,44 @@ fn parseSpcr(header: *const SystemDescriptorTableHeader) void {
|
|||||||
platform_information.spcr_kind = fadt(u8, base, len, spcr_interface_type) orelse 0;
|
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
|
/// DMAR -> detect the IOMMU. Find the first DMA-remapping hardware unit, map its
|
||||||
/// bridge whose children ACPI address (`_ADR`) devices resolve against, and the bus number.
|
/// register block, and record its version and capabilities. This is *detection only*:
|
||||||
const PciContext = struct { bridge: *device_model.Device, bus: u8 };
|
/// 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*
|
var off: usize = dmar_structures_offset;
|
||||||
/// them with the PCI-enumerated nodes: a PCI root bridge (`PNP0A03`/`PNP0A08`)
|
while (off + 4 <= total) {
|
||||||
/// folds onto the existing `pci_host_bridge`, and each addressed (`_ADR`) device folds onto
|
const kind = fadt(u16, base, total, off) orelse break;
|
||||||
/// the matching PCI function (annotating it with the ACPI hardware ID (`_HID`) and nesting the
|
const length = fadt(u16, base, total, off + 2) orelse break;
|
||||||
/// ACPI-only children — keyboard, RTC, … — beneath it). Namespace devices with no
|
if (length < 4 or off + length > total) break; // malformed; stop rather than loop
|
||||||
/// PCI match land under a synthetic `acpi` node.
|
if (kind == dmar_type_drhd) {
|
||||||
fn wireAcpiDevices(device_tree: *DeviceTree, aml_namespace: *aml.Namespace, hal: Hal) !void {
|
const register_base = fadt(u64, base, total, off + drhd_register_base_offset) orelse 0;
|
||||||
var arena = std.heap.ArenaAllocator.init(device_tree.allocator);
|
if (register_base != 0) {
|
||||||
defer arena.deinit();
|
const regs = hal.mapMmio(register_base, abi.page_size, true);
|
||||||
var interpreter = aml.Interpreter.init(aml_namespace, .{
|
platform_information.iommu_present = true;
|
||||||
.mapMmio = hal.mapMmio,
|
platform_information.iommu_base = register_base;
|
||||||
.pioRead = hal.pioRead,
|
platform_information.iommu_version = @as(*const volatile u32, @ptrFromInt(regs + 0x00)).*;
|
||||||
.pioWrite = hal.pioWrite,
|
platform_information.iommu_capabilities = @as(*const volatile u64, @ptrFromInt(regs + 0x08)).*;
|
||||||
}, arena.allocator());
|
return; // first unit is enough for detection; multi-unit is future
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break :pick try device_tree.addChild(parent_device, .acpi_device, &c.segment);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
off += length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 ----------------------------------------------------------------
|
// --- helpers ----------------------------------------------------------------
|
||||||
|
|
||||||
/// Sum `len` bytes; an ACPI table/pointer is valid when the low 8 bits are zero.
|
/// Sum `len` bytes; an ACPI table/pointer is valid when the low 8 bits are zero.
|
||||||
@@ -1111,58 +867,9 @@ fn readCntRegister(base: [*]align(1) const u8, len: usize, xoff: usize, legacy_o
|
|||||||
return .{ .mmio = false, .address = port, .width = width };
|
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.
|
/// Read a little-endian integer at `off` from a (possibly unaligned) byte pointer.
|
||||||
/// x86 is little-endian and native, so an unaligned load suffices.
|
/// x86 is little-endian and native, so an unaligned load suffices.
|
||||||
fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
|
fn rd(comptime T: type, bytes: [*]align(1) const u8, off: usize) T {
|
||||||
const p: *align(1) const T = @ptrCast(bytes + off);
|
const p: *align(1) const T = @ptrCast(bytes + off);
|
||||||
return p.*;
|
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
|
//! This module has two stages. `parser.zig` walks the entire byte stream and
|
||||||
//! records every named object into a namespace tree (`namespace.zig`), capturing
|
//! 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
|
//! methods on demand — running operators, control flow, and OperationRegion field
|
||||||
//! access — so callers can resolve device status (`_STA`), current resource
|
//! access — so callers can resolve device status (`_STA`), current resource
|
||||||
//! settings (`_CRS`), sleep states (`_Sx`), and the like against the live namespace.
|
//! 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 opcode = @import("opcodes.zig");
|
||||||
const parser = @import("parser.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 Namespace = @import("namespace.zig").Namespace;
|
||||||
pub const Node = @import("namespace.zig").Node;
|
pub const Node = @import("namespace.zig").Node;
|
||||||
pub const NodeKind = @import("namespace.zig").NodeKind;
|
pub const NodeKind = @import("namespace.zig").NodeKind;
|
||||||
|
|
||||||
/// The AML evaluator: interprets control methods (and reads Names/Fields) far
|
/// The AML evaluator: interprets control methods (and reads Names/Fields) far
|
||||||
/// enough for device discovery. See `interp.zig`.
|
/// enough for device discovery. See `interpreter.zig`.
|
||||||
pub const Interpreter = @import("interp.zig").Interpreter;
|
pub const Interpreter = @import("interpreter.zig").Interpreter;
|
||||||
pub const Object = @import("interp.zig").Object;
|
pub const Object = @import("interpreter.zig").Object;
|
||||||
pub const EvaluateHal = @import("interp.zig").Hal;
|
pub const EvaluateHal = @import("interpreter.zig").Hal;
|
||||||
|
|
||||||
/// The SLP_TYP values written to PM1a/PM1b control to enter a sleep state.
|
/// The SLP_TYP values written to PM1a/PM1b control to enter a sleep state.
|
||||||
pub const SleepType = struct {
|
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 };
|
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
|
/// 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.
|
/// first two integer elements (SLP_TYP for PM1a / PM1b), or null if absent.
|
||||||
pub fn sleepState(namespace: *Namespace, state: u8) ?SleepType {
|
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
|
// Scope(\_SB) packagelen=0x27
|
||||||
0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F,
|
0x10, 0x27, 0x5C, 0x5F, 0x53, 0x42, 0x5F,
|
||||||
// Device(PCI0) packagelen=0x1F
|
// Device(PCI0) packagelen=0x1F
|
||||||
0x5B, 0x82, 0x1F, 0x50, 0x43, 0x49, 0x30,
|
0x5B, 0x82, 0x1F, 0x50, 0x43,
|
||||||
|
0x49, 0x30,
|
||||||
// Name(_HID, 0x11)
|
// Name(_HID, 0x11)
|
||||||
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11,
|
0x08, 0x5F, 0x48, 0x49, 0x44, 0x0A, 0x11,
|
||||||
// Method(MTHD, flags=1) empty, packagelen=0x06
|
// 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
|
// 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)
|
// 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
|
// 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);
|
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
|
const lo = try interpreter.evaluate(tst, &.{.{ .integer = 2 }}); // 2+5=7 !> 10 -> 0
|
||||||
try std.testing.expectEqual(@as(u64, 0), try lo.asInteger());
|
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.
|
/// A CreateField binding: a name that indexes into a buffer object.
|
||||||
const BufferField = struct { buffer: *Node, byte_off: usize, bit_width: u32 };
|
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 {
|
pub const Interpreter = struct {
|
||||||
namespace: *Namespace,
|
namespace: *Namespace,
|
||||||
hal: Hal,
|
hal: Hal,
|
||||||
@@ -149,6 +152,11 @@ pub const Interpreter = struct {
|
|||||||
dynamic_overrides: std.AutoHashMapUnmanaged(*Node, Object) = .{},
|
dynamic_overrides: std.AutoHashMapUnmanaged(*Node, Object) = .{},
|
||||||
/// CreateField bindings active for the current evaluation.
|
/// CreateField bindings active for the current evaluation.
|
||||||
fields: std.AutoHashMapUnmanaged(*Node, BufferField) = .{},
|
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 {
|
pub fn init(namespace: *Namespace, hal: Hal, arena: std.mem.Allocator) Interpreter {
|
||||||
return .{ .namespace = namespace, .hal = hal, .arena = arena };
|
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
|
/// Evaluate a namespace object: invoke a Method, read a Name's value, or read a
|
||||||
/// Field. Resets per-evaluation runtime state first.
|
/// Field. Resets per-evaluation runtime state first.
|
||||||
pub fn evaluate(self: *Interpreter, node: *Node, args: []const Object) Error!Object {
|
pub fn evaluate(self: *Interpreter, node: *Node, args: []const Object) Error!Object {
|
||||||
|
self.notify_count = 0;
|
||||||
self.dynamic_overrides.clearRetainingCapacity();
|
self.dynamic_overrides.clearRetainingCapacity();
|
||||||
self.fields.clearRetainingCapacity();
|
self.fields.clearRetainingCapacity();
|
||||||
return self.invoke(node, args);
|
return self.invoke(node, args);
|
||||||
@@ -267,6 +276,8 @@ pub const Interpreter = struct {
|
|||||||
},
|
},
|
||||||
opcode.to_buffer_opcode => try self.passThroughUnary(current, frame),
|
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),
|
opcode.extended_opcode_prefix => try self.ext(current, frame),
|
||||||
|
|
||||||
// CreateXField: source, index, name (bit widths differ by op)
|
// CreateXField: source, index, name (bit widths differ by op)
|
||||||
@@ -542,6 +553,36 @@ pub const Interpreter = struct {
|
|||||||
try self.storeInto(current, frame, value);
|
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 {
|
fn storeInto(self: *Interpreter, current: *Cursor, frame: *Frame, value: Object) Error!void {
|
||||||
const lead = current.peek() orelse return error.Truncated;
|
const lead = current.peek() orelse return error.Truncated;
|
||||||
if (isNameStart(lead)) {
|
if (isNameStart(lead)) {
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
//! 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,
|
||||||
|
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
|
//! Discovery backends (ACPI today, device-tree later) translate their native
|
||||||
//! hardware description into this one shape, so the rest of the kernel walks a
|
//! hardware description into this one shape, so the rest of the kernel walks a
|
||||||
//! plain `Device` tree without knowing which firmware described the machine —
|
//! 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.
|
//! applies to the CPU.
|
||||||
//!
|
//!
|
||||||
//! This is deliberately minimal: enough to *describe* what was discovered (a
|
//! This is deliberately minimal: enough to *describe* what was discovered (a
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
//! on top of this — nothing here presumes them.
|
//! on top of this — nothing here presumes them.
|
||||||
|
|
||||||
const std = @import("std");
|
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 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
|
/// 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,
|
pioWrite: *const fn (width: u8, port: u16, value: u32) void,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The kind of hardware resource a device occupies.
|
/// The kind of hardware resource a device occupies. Canonically defined by the
|
||||||
pub const ResourceKind = enum {
|
/// device ABI (system/devices/device-abi.zig) and re-exported here, so the kernel's
|
||||||
/// A memory-mapped I/O window: `start` is the physical base, `len` its size.
|
/// internal tree and the descriptors it hands user space share one enum.
|
||||||
memory,
|
pub const ResourceKind = device_abi.ResourceKind;
|
||||||
/// 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 hardware resource claimed by a device.
|
/// One hardware resource claimed by a device.
|
||||||
pub const Resource = struct {
|
pub const Resource = struct {
|
||||||
@@ -46,22 +42,10 @@ pub const Resource = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// A coarse classification of a device, independent of the describing firmware.
|
/// A coarse classification of a device, independent of the describing firmware.
|
||||||
/// Kept small on purpose; refine as real drivers arrive.
|
/// Canonically defined by the device ABI (system/devices/device-abi.zig) and
|
||||||
pub const DeviceClass = enum {
|
/// re-exported here — the enum a driver matches on and the one the kernel classifies
|
||||||
/// The synthetic root every discovered device hangs beneath.
|
/// with are the same type. Kept small on purpose; refine as real drivers arrive.
|
||||||
root,
|
pub const DeviceClass = device_abi.DeviceClass;
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Firmware-independent identity. Each backend fills only the fields it knows;
|
/// 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
|
/// 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;
|
var buffer: [200]u8 = undefined;
|
||||||
@memset(buffer[0..indent], ' ');
|
@memset(buffer[0..indent], ' ');
|
||||||
const body = if (device.hid_len != 0)
|
const body = if (device.hid_len != 0) blk: {
|
||||||
std.fmt.bufPrint(buffer[indent..], "{s} [{s}] hid={s}\n", .{ device.name(), @tagName(device.class), device.hid() }) catch return
|
// Decode the _HID to a human name when it's a known standard PnP/ACPI id.
|
||||||
else
|
const desc = acpi_ids.description(device.hid());
|
||||||
std.fmt.bufPrint(buffer[indent..], "{s} [{s}]\n", .{ device.name(), @tagName(device.class) }) catch return;
|
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}] 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]);
|
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| {
|
for (device.resources[0..device.resource_count]) |r| {
|
||||||
var rbuf: [200]u8 = undefined;
|
var rbuf: [200]u8 = undefined;
|
||||||
const pad = @min(indent + 2, 42);
|
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.
|
//! compile-time choice.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const danos = @import("danos");
|
const boot_handoff = @import("boot-handoff");
|
||||||
const device_model = @import("device-model.zig");
|
const device_model = @import("device-model.zig");
|
||||||
const acpi = @import("acpi.zig");
|
const acpi = @import("acpi.zig");
|
||||||
const power = @import("power.zig");
|
const power = @import("power.zig");
|
||||||
@@ -40,6 +40,13 @@ pub fn platformInformation() PlatformInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// AML parse integrity/diagnostics (namespace node count, bytes consumed).
|
/// 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 {
|
pub fn amlStats() AmlStats {
|
||||||
return acpi.aml_stats;
|
return acpi.aml_stats;
|
||||||
}
|
}
|
||||||
@@ -65,14 +72,15 @@ pub fn cpusDropped() usize {
|
|||||||
/// ACPI registers); pass the architecture implementation. Errors leave nothing to clean up
|
/// ACPI registers); pass the architecture implementation. Errors leave nothing to clean up
|
||||||
/// beyond the tree's own allocations.
|
/// beyond the tree's own allocations.
|
||||||
pub fn discover(
|
pub fn discover(
|
||||||
boot_information: *const danos.BootInformation,
|
boot_information: *const boot_handoff.BootInformation,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
hal: Hal,
|
hal: Hal,
|
||||||
) !DeviceTree {
|
) !DeviceTree {
|
||||||
var device_tree = try DeviceTree.init(allocator);
|
var device_tree = try DeviceTree.init(allocator);
|
||||||
|
|
||||||
if (boot_information.acpi_rsdp != 0) {
|
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 {
|
} else {
|
||||||
// No ACPI RSDP. A device-tree boot would parse its blob here; today that
|
// 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
|
// path is a stub, so this reports the machine described itself no way we
|
||||||
|
|||||||
@@ -0,0 +1,857 @@
|
|||||||
|
//! 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.
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Direction of an endpoint, from the host's point of view
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
const InterfaceNumber = enum(u8) { _ };
|
||||||
|
|
||||||
|
// Selects between the alternate settings of one interface; from
|
||||||
|
// InterfaceDescriptor.alternate_setting.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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,
|
||||||
|
|
||||||
|
const Recipient = enum(u5) {
|
||||||
|
device = 0,
|
||||||
|
interface = 1,
|
||||||
|
endpoint = 2,
|
||||||
|
other = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Kind = enum(u2) {
|
||||||
|
standard = 0,
|
||||||
|
class = 1,
|
||||||
|
vendor = 2,
|
||||||
|
reserved = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Direction = enum(u1) {
|
||||||
|
host_to_device = 0,
|
||||||
|
device_to_host = 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.)
|
||||||
|
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.
|
||||||
|
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.)
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DescriptorType = enum(u8) {
|
||||||
|
device = 1,
|
||||||
|
configuration = 2,
|
||||||
|
string = 3,
|
||||||
|
interface = 4,
|
||||||
|
endpoint = 5,
|
||||||
|
device_qualifier = 6,
|
||||||
|
other_speed_configuration = 7,
|
||||||
|
interface_power = 8,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
const OtherSpeedConfigurationDescriptor = ConfigurationDescriptor;
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TransferType = enum(u2) {
|
||||||
|
control = 0,
|
||||||
|
isochronous = 1,
|
||||||
|
bulk = 2,
|
||||||
|
interrupt = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Synchronization = enum(u2) {
|
||||||
|
none = 0,
|
||||||
|
asynchronous = 1,
|
||||||
|
adaptive = 2,
|
||||||
|
synchronous = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
//! 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.
|
||||||
|
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.
|
||||||
|
const hub = struct {
|
||||||
|
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.
|
||||||
|
const hid = struct {
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
const mass_storage = struct {
|
||||||
|
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,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
const communications = struct {
|
||||||
|
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.
|
||||||
|
const wireless_controller = struct {
|
||||||
|
const SubClass = enum(u8) {
|
||||||
|
// Radio frequency controllers
|
||||||
|
radio_frequency = 0x01,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only meaningful when the subclass is radio_frequency
|
||||||
|
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.
|
||||||
|
const miscellaneous = struct {
|
||||||
|
const SubClass = enum(u8) {
|
||||||
|
// Common class
|
||||||
|
common = 0x02,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only meaningful when the subclass is common
|
||||||
|
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.
|
||||||
|
const application_specific = struct {
|
||||||
|
const SubClass = enum(u8) {
|
||||||
|
// Device firmware upgrade
|
||||||
|
firmware_upgrade = 0x01,
|
||||||
|
// IrDA bridge
|
||||||
|
irda_bridge = 0x02,
|
||||||
|
// Test and measurement
|
||||||
|
test_and_measurement = 0x03,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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 @@
|
|||||||
|
//! /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;
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
_ = 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var register_base: usize = 0;
|
||||||
|
|
||||||
|
/// One 32-bit volatile register read at `offset` from the mapped window.
|
||||||
|
fn readRegister(offset: usize) u32 {
|
||||||
|
const register: *volatile u32 = @ptrFromInt(register_base + offset);
|
||||||
|
return register.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 port scan: read the capability registers for the port count
|
||||||
|
/// and the operational-register offset, then one PORTSC per port. The connect
|
||||||
|
/// bit (CCS) and the speed field reflect hardware state directly — no
|
||||||
|
/// controller reset or run needed to *see* the devices; driving them needs the
|
||||||
|
/// rings (the USB track).
|
||||||
|
fn scanPorts(manager: runtime.ipc.Handle) void {
|
||||||
|
// Capability registers: CAPLENGTH is byte 0 of the first dword; HCSPARAMS1
|
||||||
|
// carries MaxPorts in bits 31:24.
|
||||||
|
const capability_length = readRegister(0) & 0xFF;
|
||||||
|
const structural = readRegister(0x04);
|
||||||
|
const maximum_ports: u32 = structural >> 24;
|
||||||
|
writeLine("/system/drivers/usb-xhci-bus: {d} root-hub ports\n", .{maximum_ports});
|
||||||
|
|
||||||
|
// PORTSC registers: operational base + 0x400 + 0x10 per port (1-based).
|
||||||
|
var port: u32 = 1;
|
||||||
|
var connected: u32 = 0;
|
||||||
|
while (port <= maximum_ports) : (port += 1) {
|
||||||
|
const port_status = readRegister(capability_length + 0x400 + 0x10 * (port - 1));
|
||||||
|
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 report = protocol.ChildAdded{
|
||||||
|
.parent = controller_id,
|
||||||
|
.bus_address = port,
|
||||||
|
.identity = speed,
|
||||||
|
};
|
||||||
|
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} failed\n", .{port});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (connected == 0) _ = runtime.system.write("/system/drivers/usb-xhci-bus: no devices connected\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No bus protocol to serve yet — transfer requests arrive with the USB track.
|
||||||
|
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 {
|
||||||
|
_ = 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(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
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//! The initrd (initial ramdisk) container format — shared by the build-time
|
//! The initial_ramdisk (initial ramdisk) container format — shared by the build-time
|
||||||
//! packer (tools/mkinitrd.zig) and the kernel that unpacks it. Deliberately
|
//! packer (tools/make-initial-ramdisk.py) and the kernel that unpacks it. Deliberately
|
||||||
//! trivial: a header, a table of fixed-size entries, then the concatenated file
|
//! trivial: a header, a table of fixed-size entries, then the concatenated file
|
||||||
//! blobs. We own both producer and consumer, so it need be no fancier.
|
//! blobs. We own both producer and consumer, so it need be no fancier.
|
||||||
//!
|
//!
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
/// "DNRD" — identifies a danos initrd image.
|
/// "DNRD" — identifies a danos initial_ramdisk image.
|
||||||
pub const magic: u32 = 0x444E5244;
|
pub const magic: u32 = 0x444E5244;
|
||||||
|
|
||||||
pub const Header = extern struct {
|
pub const Header = extern struct {
|
||||||
@@ -24,7 +24,7 @@ pub const Entry = extern struct {
|
|||||||
len: u64, // blob length in bytes
|
len: u64, // blob length in bytes
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A validated view over an initrd image. `init` checks the magic and that the
|
/// A validated view over an initial_ramdisk image. `init` checks the magic and that the
|
||||||
/// entry table fits; `entry` bounds-checks each blob against the image.
|
/// entry table fits; `entry` bounds-checks each blob against the image.
|
||||||
pub const Reader = struct {
|
pub const Reader = struct {
|
||||||
image: []const u8,
|
image: []const u8,
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
//! map). Every interrupt must be acknowledged with an end-of-interrupt write, or
|
//! map). Every interrupt must be acknowledged with an end-of-interrupt write, or
|
||||||
//! the LAPIC won't deliver the next one.
|
//! the LAPIC won't deliver the next one.
|
||||||
|
|
||||||
const danos = @import("danos");
|
const boot_handoff = @import("boot-handoff");
|
||||||
const io = @import("io.zig");
|
const io = @import("io.zig");
|
||||||
const paging = @import("paging.zig");
|
const paging = @import("paging.zig");
|
||||||
|
|
||||||
@@ -80,6 +80,35 @@ var timer_hz: u32 = 0;
|
|||||||
var tsc_hz: u64 = 0;
|
var tsc_hz: u64 = 0;
|
||||||
var tsc_base: u64 = 0;
|
var tsc_base: u64 = 0;
|
||||||
|
|
||||||
|
/// Whether the TSC is architecturally **invariant** — a constant rate regardless of
|
||||||
|
/// P/C-state transitions, and thus valid as a clocksource (CPUID leaf 0x80000007,
|
||||||
|
/// EDX bit 8). AMD and modern Intel set it; the bare qemu64 model does not. Measured
|
||||||
|
/// frequency alone is not enough: a non-invariant TSC speeds up and slows down with
|
||||||
|
/// the core clock, so reading it as wall time would drift.
|
||||||
|
var tsc_invariant: bool = false;
|
||||||
|
/// Cleared if the cross-core warp check (checkWarpSource) ever sees the TSC read
|
||||||
|
/// lower on one core than the max another core has already published — i.e. the
|
||||||
|
/// per-core TSCs are not synchronized, and a task migrating cores could see time go
|
||||||
|
/// backward. Starts true (assume synchronized until proven otherwise).
|
||||||
|
var tsc_synced: bool = true;
|
||||||
|
/// The worst backward skew the warp check observed, in TSC cycles (0 = none).
|
||||||
|
var tsc_warp_cycles: u64 = 0;
|
||||||
|
|
||||||
|
/// The monotonic clock's source. The TSC when it is invariant *and* synchronized —
|
||||||
|
/// the fast `rdtsc` path taken on real Intel/AMD and modern VMs. Otherwise the HPET
|
||||||
|
/// main counter: a single fixed-rate counter, immune to both per-core skew and
|
||||||
|
/// frequency scaling, so it stays accurate on a bare VM or a warped machine.
|
||||||
|
const ClockSource = enum { tsc, hpet };
|
||||||
|
var clock_source: ClockSource = .tsc;
|
||||||
|
|
||||||
|
/// HPET standby clocksource, set up in calibrate() whenever an HPET exists (whether
|
||||||
|
/// or not calibration itself measured against it): its frequency, the counter value
|
||||||
|
/// chosen as the zero point, and its width mask. Only a 64-bit HPET is used as a
|
||||||
|
/// clocksource — a 32-bit one wraps too fast to be monotonic without accumulation.
|
||||||
|
var hpet_clock_hz: u64 = 0;
|
||||||
|
var hpet_clock_base: u64 = 0;
|
||||||
|
var hpet_clock_mask: u64 = ~@as(u64, 0);
|
||||||
|
|
||||||
/// Read the 64-bit Time Stamp Counter.
|
/// Read the 64-bit Time Stamp Counter.
|
||||||
fn rdtsc() u64 {
|
fn rdtsc() u64 {
|
||||||
var low: u32 = undefined;
|
var low: u32 = undefined;
|
||||||
@@ -121,7 +150,7 @@ pub fn init() void {
|
|||||||
|
|
||||||
const msr = io.rdmsr(ia32_apic_base_msr);
|
const msr = io.rdmsr(ia32_apic_base_msr);
|
||||||
// Reach the LAPIC through the physmap (paging.init maps its page there).
|
// Reach the LAPIC through the physmap (paging.init maps its page there).
|
||||||
base = @intCast(danos.physicalToVirtual(msr & 0xFFFFF000)); // physical base is bits 12+
|
base = @intCast(boot_handoff.physicalToVirtual(msr & 0xFFFFF000)); // physical base is bits 12+
|
||||||
io.wrmsr(ia32_apic_base_msr, msr | (1 << 11)); // global enable
|
io.wrmsr(ia32_apic_base_msr, msr | (1 << 11)); // global enable
|
||||||
|
|
||||||
write(register_spurious, 0x100 | spurious_vector); // bit 8 = software enable
|
write(register_spurious, 0x100 | spurious_vector); // bit 8 = software enable
|
||||||
@@ -160,6 +189,15 @@ fn waitIcrIdle() void {
|
|||||||
while (read(register_icr_low) & icr_delivery_pending != 0) {}
|
while (read(register_icr_low) & icr_delivery_pending != 0) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send this core a fixed interrupt at `vector` (the "self" destination shorthand). A
|
||||||
|
/// device raises an MSI by writing its (address, data) to the LAPIC; with no such
|
||||||
|
/// device on QEMU's HPET, a self-IPI is the stand-in that lets the MSI vector-routing
|
||||||
|
/// path be tested end to end. Shorthand self (bits 19:18 = 01) | assert (bit 14).
|
||||||
|
pub fn selfIpi(vector: u8) void {
|
||||||
|
write(register_icr_low, 0x4_4000 | @as(u32, vector));
|
||||||
|
waitIcrIdle();
|
||||||
|
}
|
||||||
|
|
||||||
/// The calibration window: we time everything against a 10 ms reference interval.
|
/// The calibration window: we time everything against a 10 ms reference interval.
|
||||||
const calib_ms = 10;
|
const calib_ms = 10;
|
||||||
|
|
||||||
@@ -211,6 +249,31 @@ pub fn calibrate() void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tsc_base = rdtsc(); // the clock's zero point (boot)
|
tsc_base = rdtsc(); // the clock's zero point (boot)
|
||||||
|
|
||||||
|
// Decide whether the TSC is trustworthy as a clocksource. Frequency (measured
|
||||||
|
// above, possibly against the HPET/PIT) is necessary but not sufficient: the TSC
|
||||||
|
// must also be *invariant* (CPUID 0x80000007 EDX[8]). AMD and modern Intel set
|
||||||
|
// this; the bare qemu64 model does not.
|
||||||
|
tsc_invariant = tscIsInvariant();
|
||||||
|
|
||||||
|
// Bring up the HPET as a standby clocksource whenever one exists — even on the
|
||||||
|
// CPUID-0x15 path where calibration never touched it — so a non-invariant TSC
|
||||||
|
// (here) or an unsynchronized one (checkWarpSource, during SMP bring-up) can fall
|
||||||
|
// back to a source that is immune to both. hpetHz() maps + enables the counter
|
||||||
|
// and is idempotent if calibration already used it.
|
||||||
|
if (configuration_hpet_base != 0) {
|
||||||
|
if (hpetHz()) |hz| {
|
||||||
|
hpet_clock_mask = hpetMask();
|
||||||
|
if (hpet_clock_mask == ~@as(u64, 0)) { // only a 64-bit HPET is monotonic enough
|
||||||
|
hpet_clock_hz = hz;
|
||||||
|
hpet_clock_base = readHpet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the source: the fast TSC when invariant, else the HPET if we have one.
|
||||||
|
// (checkWarpSource may still demote TSC -> HPET later if the cores' TSCs skew.)
|
||||||
|
if (!tsc_invariant and hpet_clock_hz != 0) clock_source = .hpet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the LAPIC timer one-shot from its maximum count while a monotonic reference
|
/// Run the LAPIC timer one-shot from its maximum count while a monotonic reference
|
||||||
@@ -266,7 +329,7 @@ fn calibratePit() void {
|
|||||||
// --- reference clocks ------------------------------------------------------
|
// --- reference clocks ------------------------------------------------------
|
||||||
|
|
||||||
/// TSC frequency from CPUID leaf 0x15 (crystal_hz * numerator / denominator), or
|
/// TSC frequency from CPUID leaf 0x15 (crystal_hz * numerator / denominator), or
|
||||||
/// null if the CPU doesn't enumerate it (common under QEMU).
|
/// null if the CPU doesn't enumerate it (common under QEMU, and on AMD).
|
||||||
fn cpuidTscHz() ?u64 {
|
fn cpuidTscHz() ?u64 {
|
||||||
if (cpuid(0).eax < 0x15) return null;
|
if (cpuid(0).eax < 0x15) return null;
|
||||||
const r = cpuid(0x15);
|
const r = cpuid(0x15);
|
||||||
@@ -274,6 +337,15 @@ fn cpuidTscHz() ?u64 {
|
|||||||
return @as(u64, r.ecx) * r.ebx / r.eax;
|
return @as(u64, r.ecx) * r.ebx / r.eax;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the CPU advertises an **invariant** TSC (CPUID leaf 0x80000007, EDX
|
||||||
|
/// bit 8) — the architectural guarantee, on both Intel and AMD, that the TSC ticks
|
||||||
|
/// at a constant rate across P/C-states and never stops. Requires the extended-leaf
|
||||||
|
/// range to reach 0x80000007 first.
|
||||||
|
fn tscIsInvariant() bool {
|
||||||
|
if (cpuid(0x80000000).eax < 0x80000007) return false;
|
||||||
|
return (cpuid(0x80000007).edx & (1 << 8)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
const CpuidRegs = struct { eax: u32, ebx: u32, ecx: u32, edx: u32 };
|
const CpuidRegs = struct { eax: u32, ebx: u32, ecx: u32, edx: u32 };
|
||||||
|
|
||||||
fn cpuid(leaf: u32) CpuidRegs {
|
fn cpuid(leaf: u32) CpuidRegs {
|
||||||
@@ -301,11 +373,20 @@ fn hpetWrite64(off: usize, value: u64) void {
|
|||||||
@as(*volatile u64, @ptrFromInt(configuration_hpet_base + off)).* = value;
|
@as(*volatile u64, @ptrFromInt(configuration_hpet_base + off)).* = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the HPET has been mapped into the physmap yet, so `configuration_hpet_base`
|
||||||
|
/// already holds the virtual address. `hpetHz` is called more than once (calibration
|
||||||
|
/// may use the HPET, and the standby-clocksource setup asks for it again), and mapping
|
||||||
|
/// an already-mapped base a second time would double-offset it into an overflow.
|
||||||
|
var hpet_mapped: bool = false;
|
||||||
|
|
||||||
/// Map + enable the HPET and return its tick frequency, or null if unusable.
|
/// Map + enable the HPET and return its tick frequency, or null if unusable.
|
||||||
/// Maps the HPET into the physmap and switches configuration_hpet_base to that virtual
|
/// Maps the HPET into the physmap and switches configuration_hpet_base to that virtual
|
||||||
/// address, so the register accessors reach it without the identity map.
|
/// address, so the register accessors reach it without the identity map. Idempotent.
|
||||||
fn hpetHz() ?u64 {
|
fn hpetHz() ?u64 {
|
||||||
configuration_hpet_base = paging.mapMmio(configuration_hpet_base, 0x400, true);
|
if (!hpet_mapped) {
|
||||||
|
configuration_hpet_base = paging.mapMmio(configuration_hpet_base, 0x400, true);
|
||||||
|
hpet_mapped = true;
|
||||||
|
}
|
||||||
const caps = hpetRead64(0x00);
|
const caps = hpetRead64(0x00);
|
||||||
const period_fs = caps >> 32; // femtoseconds per tick
|
const period_fs = caps >> 32; // femtoseconds per tick
|
||||||
if (period_fs == 0) return null;
|
if (period_fs == 0) return null;
|
||||||
@@ -355,24 +436,169 @@ pub fn tscHz() u64 {
|
|||||||
return tsc_hz;
|
return tsc_hz;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monotonic high-resolution clock, from the TSC. A function per resolution, each
|
// Monotonic high-resolution clock. A function per resolution, each scaling the
|
||||||
// scaling the cycle delta directly at its unit (the 128-bit intermediate avoids
|
// counter delta directly at its unit (the 128-bit intermediate avoids overflow
|
||||||
// overflow across a long uptime). nanos() resolves to a few ns; millis() is what
|
// across a long uptime). nanos() resolves to a few ns on the TSC; millis() is what
|
||||||
// the scheduler uses for sleep deadlines.
|
// the scheduler uses for sleep deadlines. The source is the TSC when it is invariant
|
||||||
|
// and synchronized, else the HPET counter (see clock_source) — the branch is one
|
||||||
|
// global load and the TSC path is unchanged from before.
|
||||||
|
|
||||||
|
/// The selected source's counter delta since its zero point.
|
||||||
|
fn clockCount() u64 {
|
||||||
|
return switch (clock_source) {
|
||||||
|
.tsc => rdtsc() -% tsc_base,
|
||||||
|
// A 64-bit HPET (the only kind we select) never wraps in any realistic
|
||||||
|
// uptime, so the wrapping subtraction is exact.
|
||||||
|
.hpet => readHpet() -% hpet_clock_base,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The selected source's frequency (0 if the clock is unavailable/uncalibrated).
|
||||||
|
fn clockHertz() u64 {
|
||||||
|
return switch (clock_source) {
|
||||||
|
.tsc => tsc_hz,
|
||||||
|
.hpet => hpet_clock_hz,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn nanos() u64 {
|
pub fn nanos() u64 {
|
||||||
if (tsc_hz == 0) return 0;
|
const hz = clockHertz();
|
||||||
return @intCast(@as(u128, rdtsc() -% tsc_base) * 1_000_000_000 / tsc_hz);
|
if (hz == 0) return 0;
|
||||||
|
return @intCast(@as(u128, clockCount()) * 1_000_000_000 / hz);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn micros() u64 {
|
pub fn micros() u64 {
|
||||||
if (tsc_hz == 0) return 0;
|
const hz = clockHertz();
|
||||||
return @intCast(@as(u128, rdtsc() -% tsc_base) * 1_000_000 / tsc_hz);
|
if (hz == 0) return 0;
|
||||||
|
return @intCast(@as(u128, clockCount()) * 1_000_000 / hz);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn millis() u64 {
|
pub fn millis() u64 {
|
||||||
if (tsc_hz == 0) return 0;
|
const hz = clockHertz();
|
||||||
return @intCast(@as(u128, rdtsc() -% tsc_base) * 1_000 / tsc_hz);
|
if (hz == 0) return 0;
|
||||||
|
return @intCast(@as(u128, clockCount()) * 1_000 / hz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the CPU advertises an invariant TSC (CPUID 0x80000007 EDX[8]).
|
||||||
|
pub fn tscInvariant() bool {
|
||||||
|
return tsc_invariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test hook: force the TSC clocksource on, as if the CPU had advertised an invariant
|
||||||
|
/// TSC. QEMU's TCG accelerator (the only one for an x86 guest on an Apple-Silicon
|
||||||
|
/// host) does not expose the invariant-TSC bit — its emulated TSC isn't invariant — so
|
||||||
|
/// the tsc-sync test can't reach the real-Intel/AMD/KVM path through CPUID. This lets
|
||||||
|
/// that test exercise the TSC clocksource and the cross-core warp check anyway. tsc_base
|
||||||
|
/// is left as-is so the switch from the HPET is continuous.
|
||||||
|
pub fn forceTscClocksourceForTest() void {
|
||||||
|
tsc_invariant = true;
|
||||||
|
clock_source = .tsc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many per-AP warp checks actually ran (a rendezvous completed) — lets a test
|
||||||
|
/// confirm the cross-core check executed rather than being skipped.
|
||||||
|
pub fn warpChecksRun() u32 {
|
||||||
|
return warp_checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the per-core TSCs are synchronized (no backward warp seen at bring-up).
|
||||||
|
pub fn tscSynced() bool {
|
||||||
|
return tsc_synced;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active monotonic clocksource, for the boot log and tests.
|
||||||
|
pub fn clockSourceName() []const u8 {
|
||||||
|
return switch (clock_source) {
|
||||||
|
.tsc => "tsc",
|
||||||
|
.hpet => "hpet",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- cross-core TSC synchronization ("warp") check -------------------------
|
||||||
|
// Two cores hammer a shared "max seen" TSC value under a lock; if either reads a
|
||||||
|
// value below that max, its TSC lags the other's, and time would run backward for a
|
||||||
|
// task migrating between them (Linux calls this a warp). danos brings APs up one at a
|
||||||
|
// time, so this runs pairwise: the BSP (source) against each AP (target) as it comes
|
||||||
|
// online. It only matters — and only runs — while the TSC is the clocksource; on a
|
||||||
|
// machine already on the HPET (a bare VM) the whole rendezvous is skipped.
|
||||||
|
|
||||||
|
var warp_lock: u32 = 0;
|
||||||
|
var warp_last: u64 = 0;
|
||||||
|
var warp_bsp_ready: u32 = 0;
|
||||||
|
var warp_ap_ready: u32 = 0;
|
||||||
|
var warp_stop: u32 = 0;
|
||||||
|
var warp_checks: u32 = 0; // completed per-AP rendezvous count (for the tsc-sync test)
|
||||||
|
|
||||||
|
const warp_rounds: u32 = 1 << 20; // locked reads on the BSP: ~1 ms at GHz rates
|
||||||
|
const warp_spin_limit: u64 = 1 << 32; // bound every rendezvous wait so a lost core can't hang boot
|
||||||
|
|
||||||
|
fn warpTick() void {
|
||||||
|
while (@cmpxchgWeak(u32, &warp_lock, 0, 1, .acquire, .monotonic) != null) asm volatile ("pause");
|
||||||
|
const t = rdtsc();
|
||||||
|
if (t < warp_last) {
|
||||||
|
const delta = warp_last - t;
|
||||||
|
if (delta > tsc_warp_cycles) tsc_warp_cycles = delta;
|
||||||
|
tsc_synced = false;
|
||||||
|
} else {
|
||||||
|
warp_last = t;
|
||||||
|
}
|
||||||
|
@atomicStore(u32, &warp_lock, 0, .release);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spin (bounded) until `flag` is nonzero; false on timeout.
|
||||||
|
fn warpAwait(flag: *u32) bool {
|
||||||
|
var spins: u64 = 0;
|
||||||
|
while (@atomicLoad(u32, flag, .acquire) == 0) : (spins += 1) {
|
||||||
|
if (spins >= warp_spin_limit) return false;
|
||||||
|
asm volatile ("pause");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// BSP side of the pairwise TSC warp check, run once per AP as it reports in. No-op
|
||||||
|
/// unless the TSC is the active clocksource. If the AP's TSC proves to lag, demote
|
||||||
|
/// the monotonic clock to the HPET without a discontinuity.
|
||||||
|
pub fn checkWarpSource() void {
|
||||||
|
if (clock_source != .tsc) return;
|
||||||
|
warp_last = 0;
|
||||||
|
@atomicStore(u32, &warp_stop, 0, .release);
|
||||||
|
@atomicStore(u32, &warp_ap_ready, 0, .release);
|
||||||
|
@atomicStore(u32, &warp_bsp_ready, 1, .release);
|
||||||
|
if (!warpAwait(&warp_ap_ready)) { // AP never joined the rendezvous; skip, don't hang
|
||||||
|
@atomicStore(u32, &warp_bsp_ready, 0, .release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < warp_rounds) : (i += 1) warpTick();
|
||||||
|
@atomicStore(u32, &warp_stop, 1, .release);
|
||||||
|
@atomicStore(u32, &warp_bsp_ready, 0, .release);
|
||||||
|
warp_checks += 1;
|
||||||
|
|
||||||
|
if (!tsc_synced and hpet_clock_hz != 0) demoteToHpet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AP side: join the BSP's warp check, then return so the core can enter the
|
||||||
|
/// scheduler. Bounded so a missing BSP can't strand the core.
|
||||||
|
pub fn checkWarpTarget() void {
|
||||||
|
if (clock_source != .tsc) return;
|
||||||
|
if (!warpAwait(&warp_bsp_ready)) return;
|
||||||
|
@atomicStore(u32, &warp_ap_ready, 1, .release);
|
||||||
|
var spins: u64 = 0;
|
||||||
|
while (@atomicLoad(u32, &warp_stop, .acquire) == 0) : (spins += 1) {
|
||||||
|
if (spins >= warp_spin_limit) return;
|
||||||
|
warpTick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch the clocksource from the TSC to the HPET without a discontinuity: choose
|
||||||
|
/// the HPET zero point so it reads the same nanosecond value the TSC does right now,
|
||||||
|
/// so time neither jumps nor runs backward across the switch. Called when the warp
|
||||||
|
/// check proves the per-core TSCs unsynchronized.
|
||||||
|
fn demoteToHpet() void {
|
||||||
|
const now_ns = @as(u128, rdtsc() -% tsc_base) * 1_000_000_000 / tsc_hz;
|
||||||
|
const equivalent_ticks: u64 = @intCast(now_ns * hpet_clock_hz / 1_000_000_000);
|
||||||
|
hpet_clock_base = readHpet() -% equivalent_ticks;
|
||||||
|
clock_source = .hpet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acknowledge the current interrupt so the LAPIC will deliver the next one.
|
/// Acknowledge the current interrupt so the LAPIC will deliver the next one.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//! build.zig — no change to the generic code. Keep everything CPU-specific here
|
//! build.zig — no change to the generic code. Keep everything CPU-specific here
|
||||||
//! (halt, the descriptor tables, later paging), and nothing generic.
|
//! (halt, the descriptor tables, later paging), and nothing generic.
|
||||||
|
|
||||||
const danos = @import("danos");
|
const boot_handoff = @import("boot-handoff");
|
||||||
const parameters = @import("parameters");
|
const parameters = @import("parameters");
|
||||||
const gdt = @import("gdt.zig");
|
const gdt = @import("gdt.zig");
|
||||||
const tss = @import("tss.zig");
|
const tss = @import("tss.zig");
|
||||||
@@ -87,6 +87,14 @@ pub fn setSystemCallResult2(state: *CpuState, value: u64) void {
|
|||||||
state.rdx = value;
|
state.rdx = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write a *third* system_call return value (r8 here). r8 is an input argument
|
||||||
|
/// register (arg #4), but the syscall/int-0x80 stubs push and pop it around the
|
||||||
|
/// dispatch, so a value written into the frame is restored to the user on return.
|
||||||
|
/// Used by the IPC cap-passing calls to hand back the received capability handle.
|
||||||
|
pub fn setSystemCallResult3(state: *CpuState, value: u64) void {
|
||||||
|
state.r8 = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// Bring up the serial port (the kernel's machine-readable log). No dependencies,
|
/// Bring up the serial port (the kernel's machine-readable log). No dependencies,
|
||||||
/// so it can be the very first thing called.
|
/// so it can be the very first thing called.
|
||||||
pub fn serialInit() void {
|
pub fn serialInit() void {
|
||||||
@@ -132,7 +140,7 @@ pub fn init() void {
|
|||||||
/// Build the kernel's own page tables (with real permissions) and switch onto
|
/// Build the kernel's own page tables (with real permissions) and switch onto
|
||||||
/// them. Needs the frame allocator and the boot info (for the memory map and the
|
/// them. Needs the frame allocator and the boot info (for the memory map and the
|
||||||
/// kernel's segment layout). Call once the frame allocator is up.
|
/// kernel's segment layout). Call once the frame allocator is up.
|
||||||
pub fn enablePaging(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const danos.BootInformation) void {
|
pub fn enablePaging(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const boot_handoff.BootInformation) void {
|
||||||
paging.init(allocFrame, freeFrame, boot_information);
|
paging.init(allocFrame, freeFrame, boot_information);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +168,12 @@ pub fn mapUserDeviceInto(root: u64, virtual: u64, physical: u64, len: u64) void
|
|||||||
paging.mapUserDeviceInto(root, virtual, physical, len);
|
paging.mapUserDeviceInto(root, virtual, physical, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map coherent DMA RAM into address space `root`: strong-uncacheable, RW+NX, but
|
||||||
|
/// reclaimed on teardown (real RAM, not MMIO). For dma_alloc.
|
||||||
|
pub fn mapUserDmaInto(root: u64, virtual: u64, physical: u64, len: u64) void {
|
||||||
|
paging.mapUserDmaInto(root, virtual, physical, len);
|
||||||
|
}
|
||||||
|
|
||||||
/// Map a page into the kernel address space (non-executable). For the heap, etc.
|
/// Map a page into the kernel address space (non-executable). For the heap, etc.
|
||||||
pub fn mapPage(virtual: u64, physical: u64, writable: bool) void {
|
pub fn mapPage(virtual: u64, physical: u64, writable: bool) void {
|
||||||
paging.map(virtual, physical, writable);
|
paging.map(virtual, physical, writable);
|
||||||
@@ -410,6 +424,12 @@ pub fn irqEoi() void {
|
|||||||
apic.eoi();
|
apic.eoi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send this core a fixed interrupt at `vector`. Stands in for a device's MSI write
|
||||||
|
/// so the MSI vector-routing path can be exercised without MSI-capable hardware.
|
||||||
|
pub fn selfIpi(vector: u8) void {
|
||||||
|
apic.selfIpi(vector);
|
||||||
|
}
|
||||||
|
|
||||||
/// Enable the Local APIC, calibrate its timer against the best available reference
|
/// Enable the Local APIC, calibrate its timer against the best available reference
|
||||||
/// (see apic.calibrate — no longer the PIT by default), and start it firing at
|
/// (see apic.calibrate — no longer the PIT by default), and start it firing at
|
||||||
/// `timer_hz` — the kernel's real-time heartbeat. Interrupts still have to be
|
/// `timer_hz` — the kernel's real-time heartbeat. Interrupts still have to be
|
||||||
@@ -449,6 +469,35 @@ pub fn clockHz() u64 {
|
|||||||
return apic.tscHz();
|
return apic.tscHz();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the CPU guarantees an **invariant** TSC (CPUID 0x80000007 EDX[8] on
|
||||||
|
/// x86; the analogous architectural guarantee elsewhere). When false the TSC is not
|
||||||
|
/// used as the clocksource.
|
||||||
|
pub fn clockInvariant() bool {
|
||||||
|
return apic.tscInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the per-core clock counters are synchronized (no backward warp observed
|
||||||
|
/// at SMP bring-up). When false the clock falls back off the TSC.
|
||||||
|
pub fn clockSynchronized() bool {
|
||||||
|
return apic.tscSynced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active monotonic clocksource, for the boot log ("tsc" or "hpet" on x86).
|
||||||
|
pub fn clockSourceName() []const u8 {
|
||||||
|
return apic.clockSourceName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test hook: force the TSC clocksource on, to exercise the TSC + warp-check path on
|
||||||
|
/// a hypervisor that won't advertise an invariant TSC (see apic.forceTscClocksourceForTest).
|
||||||
|
pub fn forceTscClocksourceForTest() void {
|
||||||
|
apic.forceTscClocksourceForTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many per-AP TSC warp checks completed (for the tsc-sync test).
|
||||||
|
pub fn warpChecksRun() u32 {
|
||||||
|
return apic.warpChecksRun();
|
||||||
|
}
|
||||||
|
|
||||||
/// Unmask maskable interrupts (`sti`) so device interrupts get delivered.
|
/// Unmask maskable interrupts (`sti`) so device interrupts get delivered.
|
||||||
pub fn enableInterrupts() void {
|
pub fn enableInterrupts() void {
|
||||||
asm volatile ("sti");
|
asm volatile ("sti");
|
||||||
@@ -470,8 +519,7 @@ pub fn saveInterrupts() u64 {
|
|||||||
\\cli
|
\\cli
|
||||||
: [f] "=r" (flags),
|
: [f] "=r" (flags),
|
||||||
:
|
:
|
||||||
: .{ .memory = true }
|
: .{ .memory = true });
|
||||||
);
|
|
||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
//! triple-faults and silently resets the machine. With it, the CPU vectors into
|
//! triple-faults and silently resets the machine. With it, the CPU vectors into
|
||||||
//! our stubs, which capture the register state and hand it to a dispatcher.
|
//! our stubs, which capture the register state and hand it to a dispatcher.
|
||||||
//!
|
//!
|
||||||
//! Vectors split in two: 0-31 are CPU exceptions (terminal — reported and
|
//! Vectors split in two: 0-31 are CPU exceptions, handed to the `on_fault` hook
|
||||||
//! halted); 32+ are device interrupts (a registered handler runs, the APIC is
|
//! and never returned from (the kernel's handler kills a faulting user process
|
||||||
//! acknowledged, and we return to the interrupted code).
|
//! and reschedules, or halts the core for a kernel-mode fault); 32+ are device
|
||||||
|
//! interrupts (a registered handler runs, the APIC is acknowledged, and we
|
||||||
|
//! return to the interrupted code).
|
||||||
|
|
||||||
const gdt = @import("gdt.zig");
|
const gdt = @import("gdt.zig");
|
||||||
const tss = @import("tss.zig");
|
const tss = @import("tss.zig");
|
||||||
@@ -76,22 +78,22 @@ fn defaultFault(_: *const CpuState) noreturn {
|
|||||||
|
|
||||||
/// Names for the 32 defined exception vectors, for readable output.
|
/// Names for the 32 defined exception vectors, for readable output.
|
||||||
const names = [_][]const u8{
|
const names = [_][]const u8{
|
||||||
"divide error", "debug",
|
"divide error", "debug",
|
||||||
"NMI", "breakpoint",
|
"NMI", "breakpoint",
|
||||||
"overflow", "bound range exceeded",
|
"overflow", "bound range exceeded",
|
||||||
"invalid opcode", "device not available",
|
"invalid opcode", "device not available",
|
||||||
"double fault", "coprocessor segment overrun",
|
"double fault", "coprocessor segment overrun",
|
||||||
"invalid TSS", "segment not present",
|
"invalid TSS", "segment not present",
|
||||||
"stack-segment fault", "general protection fault",
|
"stack-segment fault", "general protection fault",
|
||||||
"page fault", "reserved (15)",
|
"page fault", "reserved (15)",
|
||||||
"x87 floating-point", "alignment check",
|
"x87 floating-point", "alignment check",
|
||||||
"machine check", "SIMD floating-point",
|
"machine check", "SIMD floating-point",
|
||||||
"virtualization", "control protection",
|
"virtualization", "control protection",
|
||||||
"reserved (22)", "reserved (23)",
|
"reserved (22)", "reserved (23)",
|
||||||
"reserved (24)", "reserved (25)",
|
"reserved (24)", "reserved (25)",
|
||||||
"reserved (26)", "reserved (27)",
|
"reserved (26)", "reserved (27)",
|
||||||
"hypervisor injection", "VMM communication",
|
"hypervisor injection", "VMM communication",
|
||||||
"security exception", "reserved (31)",
|
"security exception", "reserved (31)",
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn vectorName(vector: u64) []const u8 {
|
pub fn vectorName(vector: u64) []const u8 {
|
||||||
@@ -162,8 +164,9 @@ pub fn loadOnThisCpu() void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Called by isr_common (isr.s) with a pointer to the trap frame. Exported so the
|
/// Called by isr_common (isr.s) with a pointer to the trap frame. Exported so the
|
||||||
/// assembly stubs can `call` it by name. Exceptions are terminal; device
|
/// assembly stubs can `call` it by name. Exceptions never return here (on_fault
|
||||||
/// interrupts run their handler, get acknowledged, and return.
|
/// kills the faulting process or halts the core); device interrupts run their
|
||||||
|
/// handler, get acknowledged, and return.
|
||||||
export fn interruptDispatch(state: *CpuState) callconv(.c) void {
|
export fn interruptDispatch(state: *CpuState) callconv(.c) void {
|
||||||
if (state.vector < 32) {
|
if (state.vector < 32) {
|
||||||
on_fault(state); // CPU exception — never returns
|
on_fault(state); // CPU exception — never returns
|
||||||
|
|||||||
@@ -10,10 +10,11 @@
|
|||||||
//! Everything is 4 KiB pages — precise and simple; the extra table memory is
|
//! Everything is 4 KiB pages — precise and simple; the extra table memory is
|
||||||
//! negligible against available RAM.
|
//! negligible against available RAM.
|
||||||
|
|
||||||
const danos = @import("danos");
|
const boot_handoff = @import("boot-handoff");
|
||||||
|
const abi = @import("abi");
|
||||||
const io = @import("io.zig");
|
const io = @import("io.zig");
|
||||||
|
|
||||||
const page_size = danos.page_size;
|
const page_size = abi.page_size;
|
||||||
|
|
||||||
// Page-table entry bits.
|
// Page-table entry bits.
|
||||||
const present: u64 = 1 << 0;
|
const present: u64 = 1 << 0;
|
||||||
@@ -58,7 +59,7 @@ const bootstrap_physmap_limit: u64 = 4 << 30;
|
|||||||
/// both the loader's bootstrap tables and the kernel's own, which share the
|
/// both the loader's bootstrap tables and the kernel's own, which share the
|
||||||
/// physmap base.
|
/// physmap base.
|
||||||
fn tableAt(physical: u64) *[512]u64 {
|
fn tableAt(physical: u64) *[512]u64 {
|
||||||
return @ptrFromInt(danos.physicalToVirtual(physical));
|
return @ptrFromInt(boot_handoff.physicalToVirtual(physical));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn allocTable() u64 {
|
fn allocTable() u64 {
|
||||||
@@ -102,12 +103,12 @@ fn mapRangePhysmap(pml4: u64, physical_base: u64, len: u64, flags: u64) void {
|
|||||||
var address = physical_base & ~@as(u64, page_size - 1);
|
var address = physical_base & ~@as(u64, page_size - 1);
|
||||||
const end = physical_base + len;
|
const end = physical_base + len;
|
||||||
while (address < end) : (address += page_size) {
|
while (address < end) : (address += page_size) {
|
||||||
mapPage(pml4, danos.physicalToVirtual(address), address, flags);
|
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, flags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn regions(mm: danos.MemoryMap) []const danos.MemoryRegion {
|
fn regions(mm: boot_handoff.MemoryMap) []const boot_handoff.MemoryRegion {
|
||||||
return @as([*]const danos.MemoryRegion, @ptrFromInt(danos.physicalToVirtual(mm.regions)))[0..mm.len];
|
return @as([*]const boot_handoff.MemoryRegion, @ptrFromInt(boot_handoff.physicalToVirtual(mm.regions)))[0..mm.len];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable the NX bit in the page-table format (EFER.NXE). Must happen before we
|
/// Enable the NX bit in the page-table format (EFER.NXE). Must happen before we
|
||||||
@@ -118,7 +119,7 @@ fn enableNx() void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the address space and switch onto it.
|
/// Build the address space and switch onto it.
|
||||||
pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const danos.BootInformation) void {
|
pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const boot_handoff.BootInformation) void {
|
||||||
alloc_frame = allocFrame;
|
alloc_frame = allocFrame;
|
||||||
free_frame = freeFrame;
|
free_frame = freeFrame;
|
||||||
enableNx();
|
enableNx();
|
||||||
@@ -136,7 +137,7 @@ pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot
|
|||||||
// the kernel touches directly), RW + NX.
|
// the kernel touches directly), RW + NX.
|
||||||
const fb = boot_information.framebuffer;
|
const fb = boot_information.framebuffer;
|
||||||
mapRangePhysmap(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute);
|
mapRangePhysmap(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute);
|
||||||
mapPage(pml4, danos.physicalToVirtual(0xFEE00000), 0xFEE00000, present | writable | no_execute);
|
mapPage(pml4, boot_handoff.physicalToVirtual(0xFEE00000), 0xFEE00000, present | writable | no_execute);
|
||||||
|
|
||||||
// 3. The kernel's own segments at their higher-half link addresses, mapped
|
// 3. The kernel's own segments at their higher-half link addresses, mapped
|
||||||
// to their low physical load addresses with real ELF permissions: code
|
// to their low physical load addresses with real ELF permissions: code
|
||||||
@@ -165,8 +166,7 @@ pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot
|
|||||||
asm volatile ("mov %[pml4], %%cr3"
|
asm volatile ("mov %[pml4], %%cr3"
|
||||||
:
|
:
|
||||||
: [pml4] "r" (pml4),
|
: [pml4] "r" (pml4),
|
||||||
: .{ .memory = true }
|
: .{ .memory = true });
|
||||||
);
|
|
||||||
on_own_tables = true; // now on the kernel's physmap (covers all RAM)
|
on_own_tables = true; // now on the kernel's physmap (covers all RAM)
|
||||||
init_done = true; // the kernel half is fixed from here
|
init_done = true; // the kernel half is fixed from here
|
||||||
}
|
}
|
||||||
@@ -206,11 +206,11 @@ pub fn mapMmio(physical: u64, len: u64, writable_page: bool) u64 {
|
|||||||
const last = physical + (if (len == 0) 1 else len) - 1;
|
const last = physical + (if (len == 0) 1 else len) - 1;
|
||||||
var address = first;
|
var address = first;
|
||||||
while (address <= (last & ~@as(u64, page_size - 1))) : (address += page_size) {
|
while (address <= (last & ~@as(u64, page_size - 1))) : (address += page_size) {
|
||||||
const virtual = danos.physicalToVirtual(address);
|
const virtual = boot_handoff.physicalToVirtual(address);
|
||||||
mapPage(kernel_pml4, virtual, address, flags);
|
mapPage(kernel_pml4, virtual, address, flags);
|
||||||
invalidate(virtual);
|
invalidate(virtual);
|
||||||
}
|
}
|
||||||
return danos.physicalToVirtual(physical);
|
return boot_handoff.physicalToVirtual(physical);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like `descend`, but also sets the U/S bit on the intermediate entry (new or
|
/// Like `descend`, but also sets the U/S bit on the intermediate entry (new or
|
||||||
@@ -272,6 +272,30 @@ pub fn mapUserDeviceInto(pml4: u64, virtual: u64, physical: u64, len: u64) void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map `[physical, physical+len)` into the user half rooted at `pml4` as **coherent
|
||||||
|
/// DMA memory**: strong-uncacheable (PCD|PWT — a device reads/writes this RAM without
|
||||||
|
/// snooping the CPU caches) but, unlike `mapUserDeviceInto`, **without** `device_grant`
|
||||||
|
/// — because these frames are real RAM from `pmm.allocContiguous`, so teardown
|
||||||
|
/// (`freeSubtree`) must return them to the allocator like any other user page. RW + NX.
|
||||||
|
/// The caller aligns `virtual`/`physical` and places `virtual` in the DMA arena.
|
||||||
|
pub fn mapUserDmaInto(pml4: u64, virtual: u64, physical: u64, len: u64) void {
|
||||||
|
const flags: u64 = present | user | writable | no_execute | pcd | pwt;
|
||||||
|
const first = physical & ~@as(u64, page_size - 1);
|
||||||
|
const last = (physical + (if (len == 0) 1 else len) - 1) & ~@as(u64, page_size - 1);
|
||||||
|
var off: u64 = 0;
|
||||||
|
while (first + off <= last) : (off += page_size) {
|
||||||
|
const v = virtual + off;
|
||||||
|
const pml4e = &tableAt(pml4)[(v >> 39) & 0x1FF];
|
||||||
|
const pdpt = descendUser(pml4e);
|
||||||
|
const pdpte = &tableAt(pdpt)[(v >> 30) & 0x1FF];
|
||||||
|
const pd = descendUser(pdpte);
|
||||||
|
const pde = &tableAt(pd)[(v >> 21) & 0x1FF];
|
||||||
|
const pt = descendUser(pde);
|
||||||
|
tableAt(pt)[(v >> 12) & 0x1FF] = ((first + off) & address_mask) | flags;
|
||||||
|
invalidate(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new address space: a fresh PML4 with an empty user half and the
|
/// Create a new address space: a fresh PML4 with an empty user half and the
|
||||||
/// kernel's higher half shared in (copying PML4[256..512), whose entries point
|
/// kernel's higher half shared in (copying PML4[256..512), whose entries point
|
||||||
/// at the kernel's PDPTs — pre-created at init and never restaled, so growth in
|
/// at the kernel's PDPTs — pre-created at init and never restaled, so growth in
|
||||||
@@ -384,6 +408,5 @@ fn invalidate(virtual: u64) void {
|
|||||||
\\invlpg (%%rax)
|
\\invlpg (%%rax)
|
||||||
:
|
:
|
||||||
: [v] "r" (virtual),
|
: [v] "r" (virtual),
|
||||||
: .{ .rax = true, .memory = true }
|
: .{ .rax = true, .memory = true });
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
//! Once a core has its own descriptor tables, LAPIC, and timer, it calls the generic
|
//! Once a core has its own descriptor tables, LAPIC, and timer, it calls the generic
|
||||||
//! scheduler entry and joins the run loop — mechanism here, policy there.
|
//! scheduler entry and joins the run loop — mechanism here, policy there.
|
||||||
|
|
||||||
const danos = @import("danos");
|
const boot_handoff = @import("boot-handoff");
|
||||||
const io = @import("io.zig");
|
const io = @import("io.zig");
|
||||||
const gdt = @import("gdt.zig");
|
const gdt = @import("gdt.zig");
|
||||||
const tss = @import("tss.zig");
|
const tss = @import("tss.zig");
|
||||||
@@ -85,7 +85,7 @@ fn arm() void {
|
|||||||
const start = @extern([*]const u8, .{ .name = "ap_trampoline_start" });
|
const start = @extern([*]const u8, .{ .name = "ap_trampoline_start" });
|
||||||
const end = @extern([*]const u8, .{ .name = "ap_trampoline_end" });
|
const end = @extern([*]const u8, .{ .name = "ap_trampoline_end" });
|
||||||
const len = @intFromPtr(end) - @intFromPtr(start);
|
const len = @intFromPtr(end) - @intFromPtr(start);
|
||||||
const destination: [*]u8 = @ptrFromInt(danos.physicalToVirtual(tramp_physical));
|
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(tramp_physical));
|
||||||
@memcpy(destination[0..len], start[0..len]);
|
@memcpy(destination[0..len], start[0..len]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ fn arm() void {
|
|||||||
/// reported in — it's long past the trampoline by then, in the kernel image; a
|
/// reported in — it's long past the trampoline by then, in the kernel image; a
|
||||||
/// core that never answered is dead and can't be mid-climb.
|
/// core that never answered is dead and can't be mid-climb.
|
||||||
fn disarm() void {
|
fn disarm() void {
|
||||||
const destination: [*]u8 = @ptrFromInt(danos.physicalToVirtual(tramp_physical));
|
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(tramp_physical));
|
||||||
@memset(destination[0..page_size], 0);
|
@memset(destination[0..page_size], 0);
|
||||||
paging.unmap(tramp_physical); // drop the transient low identity mapping
|
paging.unmap(tramp_physical); // drop the transient low identity mapping
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ fn disarm() void {
|
|||||||
fn param(comptime name: []const u8) *align(1) volatile u64 {
|
fn param(comptime name: []const u8) *align(1) volatile u64 {
|
||||||
const start = @intFromPtr(@extern([*]const u8, .{ .name = "ap_trampoline_start" }));
|
const start = @intFromPtr(@extern([*]const u8, .{ .name = "ap_trampoline_start" }));
|
||||||
const sym = @intFromPtr(@extern([*]const u8, .{ .name = name }));
|
const sym = @intFromPtr(@extern([*]const u8, .{ .name = name }));
|
||||||
return @ptrFromInt(danos.physicalToVirtual(tramp_physical + (sym - start)));
|
return @ptrFromInt(boot_handoff.physicalToVirtual(tramp_physical + (sym - start)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wake the core with Local APIC id `apic_id` as dense CPU `index`, hand it
|
/// Wake the core with Local APIC id `apic_id` as dense CPU `index`, hand it
|
||||||
@@ -148,7 +148,14 @@ pub fn startAp(apic_id: u32, stack_top: usize, percpu: usize, index: usize, cr3:
|
|||||||
// Wait up to 100 ms for the AP to reach apEntry and set the flag.
|
// Wait up to 100 ms for the AP to reach apEntry and set the flag.
|
||||||
const deadline = apic.millis() + 100;
|
const deadline = apic.millis() + 100;
|
||||||
while (apic.millis() < deadline) {
|
while (apic.millis() < deadline) {
|
||||||
if (@atomicLoad(u32, &ap_alive, .acquire) != 0) return true;
|
if (@atomicLoad(u32, &ap_alive, .acquire) != 0) {
|
||||||
|
// Cross-check this core's TSC against the BSP's before it joins the run
|
||||||
|
// loop: an unsynchronized TSC must be caught before any task can migrate
|
||||||
|
// onto this core and observe time going backward. No-op unless the TSC is
|
||||||
|
// the clocksource (apic.checkWarpSource).
|
||||||
|
apic.checkWarpSource();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
asm volatile ("pause");
|
asm volatile ("pause");
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -177,6 +184,11 @@ fn apEntry(percpu: usize) callconv(.c) noreturn {
|
|||||||
|
|
||||||
@atomicStore(u32, &ap_alive, 1, .release); // "architecture state up" — BSP is polling this
|
@atomicStore(u32, &ap_alive, 1, .release); // "architecture state up" — BSP is polling this
|
||||||
|
|
||||||
|
// Rendezvous with the BSP for the TSC warp check (no-op unless the TSC is the
|
||||||
|
// clocksource) before joining the run loop, so this core's clock is vetted before
|
||||||
|
// it can run any task.
|
||||||
|
apic.checkWarpTarget();
|
||||||
|
|
||||||
if (secondary_entry) |enterScheduler| enterScheduler(); // joins the run loop
|
if (secondary_entry) |enterScheduler| enterScheduler(); // joins the run loop
|
||||||
while (true) asm volatile ("hlt"); // (only if no entry was registered)
|
while (true) asm volatile ("hlt"); // (only if no entry was registered)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
//! never assumes a display exists.
|
//! never assumes a display exists.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const danos = @import("danos");
|
const boot_handoff = @import("boot-handoff");
|
||||||
|
|
||||||
/// The one framebuffer console, valid only when `con_present`.
|
/// The one framebuffer console, valid only when `con_present`.
|
||||||
var con: Console = undefined;
|
var con: Console = undefined;
|
||||||
@@ -22,7 +22,7 @@ var con_present: bool = false;
|
|||||||
|
|
||||||
/// Set up the console over `fb`, or mark it absent if there's no usable
|
/// Set up the console over `fb`, or mark it absent if there's no usable
|
||||||
/// framebuffer. Clears the screen when present.
|
/// framebuffer. Clears the screen when present.
|
||||||
pub fn init(fb: danos.Framebuffer) void {
|
pub fn init(fb: boot_handoff.Framebuffer) void {
|
||||||
if (!fb.present()) {
|
if (!fb.present()) {
|
||||||
con_present = false;
|
con_present = false;
|
||||||
return;
|
return;
|
||||||
@@ -58,7 +58,7 @@ const glyph_bytes = glyph_h; // 8 pixels wide => 1 byte per row
|
|||||||
const glyph_data = 32; // PSF2 header size
|
const glyph_data = 32; // PSF2 header size
|
||||||
|
|
||||||
pub const Console = struct {
|
pub const Console = struct {
|
||||||
fb: danos.Framebuffer,
|
fb: boot_handoff.Framebuffer,
|
||||||
cols: u32,
|
cols: u32,
|
||||||
rows: u32,
|
rows: u32,
|
||||||
col: u32 = 0,
|
col: u32 = 0,
|
||||||
@@ -66,12 +66,12 @@ pub const Console = struct {
|
|||||||
fg: u32 = 0x00c8_c8c8, // light grey
|
fg: u32 = 0x00c8_c8c8, // light grey
|
||||||
bg: u32 = 0x0000_0000, // black
|
bg: u32 = 0x0000_0000, // black
|
||||||
|
|
||||||
pub fn init(fb: danos.Framebuffer) Console {
|
pub fn init(fb: boot_handoff.Framebuffer) Console {
|
||||||
// Reach the framebuffer through the physmap, so the pointer stays valid
|
// Reach the framebuffer through the physmap, so the pointer stays valid
|
||||||
// once the low identity map is gone. The base is mapped by both the
|
// once the low identity map is gone. The base is mapped by both the
|
||||||
// loader's bootstrap tables and paging.init.
|
// loader's bootstrap tables and paging.init.
|
||||||
var mapped = fb;
|
var mapped = fb;
|
||||||
if (fb.base != 0) mapped.base = danos.physicalToVirtual(fb.base);
|
if (fb.base != 0) mapped.base = boot_handoff.physicalToVirtual(fb.base);
|
||||||
return .{
|
return .{
|
||||||
.fb = mapped,
|
.fb = mapped,
|
||||||
.cols = fb.width / glyph_w,
|
.cols = fb.width / glyph_w,
|
||||||
@@ -154,5 +154,3 @@ pub const Console = struct {
|
|||||||
while (x < self.fb.width) : (x += 1) destination[x] = source[x];
|
while (x < self.fb.width) : (x += 1) destination[x] = source[x];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const platform = @import("platform");
|
const platform = @import("platform");
|
||||||
const danos = @import("danos");
|
const device_abi = @import("device-abi");
|
||||||
|
|
||||||
const maximum_devices = 64;
|
const maximum_devices = 64;
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ const maximum_devices = 64;
|
|||||||
/// `device_release` to reclaim on exit) is future work — see docs/driver-model.md.
|
/// `device_release` to reclaim on exit) is future work — see docs/driver-model.md.
|
||||||
const maximum_children_per_parent = 16;
|
const maximum_children_per_parent = 16;
|
||||||
|
|
||||||
var devices: [maximum_devices]danos.DeviceDescriptor = undefined;
|
var devices: [maximum_devices]device_abi.DeviceDescriptor = undefined;
|
||||||
var claimed: [maximum_devices]?u32 = .{null} ** maximum_devices; // owner task id, or null
|
var claimed: [maximum_devices]?u32 = .{null} ** maximum_devices; // owner task id, or null
|
||||||
var count: usize = 0;
|
var count: usize = 0;
|
||||||
|
|
||||||
@@ -46,13 +46,13 @@ pub fn init(device_tree: *const platform.DeviceTree) void {
|
|||||||
count = 0;
|
count = 0;
|
||||||
dropped = 0;
|
dropped = 0;
|
||||||
for (&claimed) |*c| c.* = null;
|
for (&claimed) |*c| c.* = null;
|
||||||
walk(device_tree.root, danos.no_parent);
|
walk(device_tree.root, device_abi.no_parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record `node` (unless it's the synthetic root) and recurse, threading the id we
|
/// Record `node` (unless it's the synthetic root) and recurse, threading the id we
|
||||||
/// assigned it down to its children as their parent.
|
/// assigned it down to its children as their parent.
|
||||||
fn walk(node: *platform.Device, parent_id: u64) void {
|
fn walk(node: *platform.Device, parent_id: u64) void {
|
||||||
const id = if (node.class == .root) danos.no_parent else record(node, parent_id);
|
const id = if (node.class == .root) device_abi.no_parent else record(node, parent_id);
|
||||||
var child = node.first_child;
|
var child = node.first_child;
|
||||||
while (child) |c| : (child = c.next_sibling) walk(c, id);
|
while (child) |c| : (child = c.next_sibling) walk(c, id);
|
||||||
}
|
}
|
||||||
@@ -60,16 +60,17 @@ fn walk(node: *platform.Device, parent_id: u64) void {
|
|||||||
fn record(node: *platform.Device, parent_id: u64) u64 {
|
fn record(node: *platform.Device, parent_id: u64) u64 {
|
||||||
if (count >= maximum_devices) {
|
if (count >= maximum_devices) {
|
||||||
dropped += 1;
|
dropped += 1;
|
||||||
return danos.no_parent; // children of a dropped node become roots, not orphans
|
return device_abi.no_parent; // children of a dropped node become roots, not orphans
|
||||||
}
|
}
|
||||||
var d = std.mem.zeroes(danos.DeviceDescriptor);
|
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
||||||
d.id = count;
|
d.id = count;
|
||||||
d.parent = parent_id;
|
d.parent = parent_id;
|
||||||
d.class = @intFromEnum(node.class);
|
d.class = @intFromEnum(node.class);
|
||||||
|
d.pci_class = if (node.ids.pci_class) |code| code else device_abi.no_pci_class;
|
||||||
const h = node.hid();
|
const h = node.hid();
|
||||||
d.hid_len = @min(h.len, d.hid.len);
|
d.hid_len = @min(h.len, d.hid.len);
|
||||||
@memcpy(d.hid[0..d.hid_len], h[0..d.hid_len]);
|
@memcpy(d.hid[0..d.hid_len], h[0..d.hid_len]);
|
||||||
const rc = @min(node.resource_count, danos.maximum_device_resources);
|
const rc = @min(node.resource_count, device_abi.maximum_device_resources);
|
||||||
d.resource_count = rc;
|
d.resource_count = rc;
|
||||||
for (0..rc) |i| {
|
for (0..rc) |i| {
|
||||||
const r = node.resources[i];
|
const r = node.resources[i];
|
||||||
@@ -82,7 +83,7 @@ fn record(node: *platform.Device, parent_id: u64) u64 {
|
|||||||
|
|
||||||
/// Copy up to `out.len` device descriptors into `out`; returns the total count
|
/// Copy up to `out.len` device descriptors into `out`; returns the total count
|
||||||
/// available (which may exceed `out.len`).
|
/// available (which may exceed `out.len`).
|
||||||
pub fn enumerate(out: []danos.DeviceDescriptor) usize {
|
pub fn enumerate(out: []device_abi.DeviceDescriptor) usize {
|
||||||
const n = @min(count, out.len);
|
const n = @min(count, out.len);
|
||||||
@memcpy(out[0..n], devices[0..n]);
|
@memcpy(out[0..n], devices[0..n]);
|
||||||
return count;
|
return count;
|
||||||
@@ -103,8 +104,21 @@ pub fn ownerOf(id: u64) ?u32 {
|
|||||||
return claimed[@intCast(id)];
|
return claimed[@intCast(id)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Release every claim held by `owner` — called by the process layer on every
|
||||||
|
/// path out of a process (exit, fault, kill), so a restarted driver can claim its
|
||||||
|
/// hardware again (docs/process-lifecycle.md iron rule 1: cleanup is the kernel's
|
||||||
|
/// job). The devices stay in the table — they describe hardware, which did not go
|
||||||
|
/// away — only their ownership clears.
|
||||||
|
pub fn releaseAllOwnedBy(owner: u32) void {
|
||||||
|
for (claimed[0..count]) |*slot| {
|
||||||
|
if (slot.*) |o| {
|
||||||
|
if (o == owner) slot.* = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Resource `index` of device `id`, or null if out of range.
|
/// Resource `index` of device `id`, or null if out of range.
|
||||||
pub fn resourceOf(id: u64, index: u64) ?danos.ResourceDescriptor {
|
pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
|
||||||
if (id >= count) return null;
|
if (id >= count) return null;
|
||||||
const d = &devices[@intCast(id)];
|
const d = &devices[@intCast(id)];
|
||||||
if (index >= d.resource_count) return null;
|
if (index >= d.resource_count) return null;
|
||||||
@@ -115,9 +129,18 @@ pub fn resourceOf(id: u64, index: u64) ?danos.ResourceDescriptor {
|
|||||||
/// interval containment; for an irq it's equality, since an interrupt line is not
|
/// interval containment; for an irq it's equality, since an interrupt line is not
|
||||||
/// divisible. Zero-length child ranges are refused — an empty window is meaningless
|
/// divisible. Zero-length child ranges are refused — an empty window is meaningless
|
||||||
/// and would otherwise vacuously "fit" anywhere.
|
/// and would otherwise vacuously "fit" anywhere.
|
||||||
fn contains(parent: danos.ResourceDescriptor, child: danos.ResourceDescriptor) bool {
|
fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool {
|
||||||
if (parent.kind != child.kind) return false;
|
if (parent.kind != child.kind) return false;
|
||||||
if (child.kind == @intFromEnum(danos.ResourceKind.irq)) return parent.start == child.start;
|
if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) {
|
||||||
|
// Range containment: an interrupt line is still indivisible (a child owns
|
||||||
|
// exactly one GSI), but a parent may own a *range* of lines so a broad
|
||||||
|
// owner — the acpi-tables node, whose firmware names any legacy IRQ —
|
||||||
|
// can contain its children's specific lines. A length-1 parent range is
|
||||||
|
// exactly the old equality rule, so existing single-IRQ parents are
|
||||||
|
// unaffected.
|
||||||
|
const span = if (parent.len == 0) 1 else parent.len;
|
||||||
|
return child.start >= parent.start and child.start < parent.start + span;
|
||||||
|
}
|
||||||
if (child.len == 0 or parent.len == 0) return false;
|
if (child.len == 0 or parent.len == 0) return false;
|
||||||
// No overflow: a resource that wraps the address space is not containable.
|
// No overflow: a resource that wraps the address space is not containable.
|
||||||
const child_end = std.math.add(u64, child.start, child.len) catch return false;
|
const child_end = std.math.add(u64, child.start, child.len) catch return false;
|
||||||
@@ -149,10 +172,10 @@ fn childCount(parent_id: u64) usize {
|
|||||||
/// `owner` must have claimed `parent_id`, and every resource in `descriptor` must be
|
/// `owner` must have claimed `parent_id`, and every resource in `descriptor` must be
|
||||||
/// contained in a parent resource of the same kind. A device with no resources is
|
/// contained in a parent resource of the same kind. A device with no resources is
|
||||||
/// fine and common: a USB device is addressed through its controller, not by MMIO.
|
/// fine and common: a USB device is addressed through its controller, not by MMIO.
|
||||||
pub fn register(parent_id: u64, owner: u32, descriptor: *const danos.DeviceDescriptor) RegisterError!u64 {
|
pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.DeviceDescriptor) RegisterError!u64 {
|
||||||
const parent_owner = ownerOf(parent_id) orelse return error.BadParent;
|
const parent_owner = ownerOf(parent_id) orelse return error.BadParent;
|
||||||
if (parent_owner != owner) return error.BadParent;
|
if (parent_owner != owner) return error.BadParent;
|
||||||
if (descriptor.resource_count > danos.maximum_device_resources) return error.TooManyResources;
|
if (descriptor.resource_count > device_abi.maximum_device_resources) return error.TooManyResources;
|
||||||
if (childCount(parent_id) >= maximum_children_per_parent) return error.TooManyChildren;
|
if (childCount(parent_id) >= maximum_children_per_parent) return error.TooManyChildren;
|
||||||
if (count >= maximum_devices) return error.NoSpace;
|
if (count >= maximum_devices) return error.NoSpace;
|
||||||
|
|
||||||
@@ -166,10 +189,31 @@ pub fn register(parent_id: u64, owner: u32, descriptor: *const danos.DeviceDescr
|
|||||||
if (!ok) return error.NotContained;
|
if (!ok) return error.NotContained;
|
||||||
}
|
}
|
||||||
|
|
||||||
var d = std.mem.zeroes(danos.DeviceDescriptor);
|
// Idempotent on exact match (docs/device-manager.md): a restarted
|
||||||
|
// registering bus re-registers what it rediscovers, and the table has no
|
||||||
|
// unregister — an identical (class, identity, resources) child under the
|
||||||
|
// same parent returns the existing id instead of appending a duplicate.
|
||||||
|
for (devices[0..count]) |*existing| {
|
||||||
|
if (existing.parent != parent_id) continue;
|
||||||
|
if (existing.class != descriptor.class) continue;
|
||||||
|
if (existing.pci_class != descriptor.pci_class) continue;
|
||||||
|
if (existing.hid_len != descriptor.hid_len) continue;
|
||||||
|
if (!std.mem.eql(u8, existing.hid[0..@intCast(existing.hid_len)], descriptor.hid[0..@intCast(descriptor.hid_len)])) continue;
|
||||||
|
if (existing.resource_count != descriptor.resource_count) continue;
|
||||||
|
var same = true;
|
||||||
|
for (0..@intCast(descriptor.resource_count)) |i| {
|
||||||
|
const a = existing.resources[i];
|
||||||
|
const b = descriptor.resources[i];
|
||||||
|
if (a.kind != b.kind or a.start != b.start or a.len != b.len) same = false;
|
||||||
|
}
|
||||||
|
if (same) return existing.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
||||||
d.id = count;
|
d.id = count;
|
||||||
d.parent = parent_id;
|
d.parent = parent_id;
|
||||||
d.class = descriptor.class;
|
d.class = descriptor.class;
|
||||||
|
d.pci_class = descriptor.pci_class;
|
||||||
d.hid_len = @min(descriptor.hid_len, d.hid.len);
|
d.hid_len = @min(descriptor.hid_len, d.hid.len);
|
||||||
@memcpy(d.hid[0..@intCast(d.hid_len)], descriptor.hid[0..@intCast(d.hid_len)]);
|
@memcpy(d.hid[0..@intCast(d.hid_len)], descriptor.hid[0..@intCast(d.hid_len)]);
|
||||||
d.resource_count = descriptor.resource_count;
|
d.resource_count = descriptor.resource_count;
|
||||||
@@ -13,11 +13,11 @@
|
|||||||
//! interrupt handlers (ours don't). A lock comes with threads/SMP.
|
//! interrupt handlers (ours don't). A lock comes with threads/SMP.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const danos = @import("danos");
|
const abi = @import("abi");
|
||||||
const architecture = @import("architecture");
|
const architecture = @import("architecture");
|
||||||
const pmm = @import("pmm.zig");
|
const pmm = @import("pmm.zig");
|
||||||
|
|
||||||
const page_size = danos.page_size;
|
const page_size = abi.page_size;
|
||||||
|
|
||||||
/// Virtual base of the heap: the start of the higher half, which is unmapped and
|
/// Virtual base of the heap: the start of the higher half, which is unmapped and
|
||||||
/// well clear of the identity-mapped low half. (Canonical on x86_64; an architecture that
|
/// well clear of the identity-mapped low half. (Canonical on x86_64; an architecture that
|
||||||
|
|||||||
@@ -22,13 +22,14 @@
|
|||||||
//! copy is a later security-track item, matching the existing debug_write gap.
|
//! copy is a later security-track item, matching the existing debug_write gap.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const danos = @import("danos");
|
const boot_handoff = @import("boot-handoff");
|
||||||
|
const abi = @import("abi");
|
||||||
const architecture = @import("architecture");
|
const architecture = @import("architecture");
|
||||||
const scheduler = @import("scheduler.zig");
|
const scheduler = @import("scheduler.zig");
|
||||||
const sync = @import("sync.zig");
|
const sync = @import("sync.zig");
|
||||||
const heap = @import("heap.zig");
|
const heap = @import("heap.zig");
|
||||||
|
|
||||||
const page_size = danos.page_size;
|
const page_size = abi.page_size;
|
||||||
const Task = scheduler.Task;
|
const Task = scheduler.Task;
|
||||||
|
|
||||||
/// Largest message a single call/reply may carry. Bumping it is trivial; kept
|
/// Largest message a single call/reply may carry. Bumping it is trivial; kept
|
||||||
@@ -45,13 +46,39 @@ pub const EFAULT: i64 = 3; // buffer unmapped / out of the user half
|
|||||||
pub const ENOENT: i64 = 4; // no such registered service
|
pub const ENOENT: i64 = 4; // no such registered service
|
||||||
pub const ENOSPC: i64 = 5; // handle table or registry full
|
pub const ENOSPC: i64 = 5; // handle table or registry full
|
||||||
pub const ENOMEM: i64 = 6; // out of memory
|
pub const ENOMEM: i64 = 6; // out of memory
|
||||||
|
pub const EPEER: i64 = 7; // peer died before replying (its process exited or was killed)
|
||||||
|
pub const ESRCH: i64 = 8; // no such process (process_kill of an unknown/dead id)
|
||||||
|
pub const EPERM: i64 = 9; // not permitted (process_kill by anyone but the supervisor)
|
||||||
|
|
||||||
/// A badge with this bit set is an asynchronous notification (e.g. an IRQ), not a
|
/// A badge with this bit set is an asynchronous notification (e.g. an IRQ), not a
|
||||||
/// message from a client — there is no reply owed. The low bits carry the source
|
/// message from a client — there is no reply owed. The low bits carry the source
|
||||||
/// (a GSI for IRQs). Posted by `notifyFromIsr`, from the ISR in system/kernel/irq.zig;
|
/// (a GSI for IRQs). Posted by `notifyFromIsr`, from the ISR in system/kernel/irq.zig;
|
||||||
/// the message path uses a plain task-id badge with this bit clear. Defined in the
|
/// the message path uses a plain task-id badge with this bit clear. Defined in the
|
||||||
/// shared contract (system/danos.zig), because ring 3 has to test the same bit.
|
/// shared kernel↔user ABI (system/abi.zig), because ring 3 has to test the same bit.
|
||||||
pub const notify_badge_bit: u64 = danos.notify_badge_bit;
|
pub const notify_badge_bit: u64 = abi.notify_badge_bit;
|
||||||
|
|
||||||
|
/// Set (with `notify_badge_bit`) when a `replyWait` wake carries a buffered payload
|
||||||
|
/// posted by `send` (`ipc_send`), rather than a bare IRQ/exit notification. Shared with
|
||||||
|
/// ring 3 through the ABI so the receiver can tell "a message arrived" from "the hardware
|
||||||
|
/// spoke".
|
||||||
|
pub const notify_message_bit: u64 = abi.notify_message_bit;
|
||||||
|
|
||||||
|
/// Largest payload a single `send` (`ipc_send`) may post. Kept small — the payload rides
|
||||||
|
/// inline in every `Endpoint`, and the async path is for events (a `KeyEvent` is 16
|
||||||
|
/// bytes), not bulk transfer, which is what `call` and future shared pages are for.
|
||||||
|
pub const POST_MAXIMUM: usize = 64;
|
||||||
|
|
||||||
|
/// Depth of an endpoint's async payload ring. Absorbs a burst while a receiver is briefly
|
||||||
|
/// busy; a full ring drops the *oldest* message (see `send`).
|
||||||
|
const post_capacity: usize = 16;
|
||||||
|
|
||||||
|
/// One buffered message: a length-prefixed payload plus the sender's task id (delivered
|
||||||
|
/// in the low bits of the receiver's badge).
|
||||||
|
const PostSlot = struct {
|
||||||
|
length: u16 = 0,
|
||||||
|
sender_id: u64 = 0,
|
||||||
|
bytes: [POST_MAXIMUM]u8 = undefined,
|
||||||
|
};
|
||||||
|
|
||||||
/// End of the user (low) canonical half — user buffers must lie below it.
|
/// End of the user (low) canonical half — user buffers must lie below it.
|
||||||
const user_half_end: u64 = 0x0000_8000_0000_0000;
|
const user_half_end: u64 = 0x0000_8000_0000_0000;
|
||||||
@@ -70,9 +97,15 @@ pub const Endpoint = struct {
|
|||||||
notify_buffer: [8]u64 = undefined,
|
notify_buffer: [8]u64 = undefined,
|
||||||
notify_head: u8 = 0,
|
notify_head: u8 = 0,
|
||||||
notify_tail: u8 = 0,
|
notify_tail: u8 = 0,
|
||||||
|
// Pending buffered messages (payloads posted by `send`), a small FIFO ring. Unlike
|
||||||
|
// notifications — which are a level and coalesce — these are discrete messages, so a
|
||||||
|
// full ring drops the oldest rather than merging.
|
||||||
|
post_buffer: [post_capacity]PostSlot = undefined,
|
||||||
|
post_head: u16 = 0,
|
||||||
|
post_tail: u16 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn createEndpoint() ?*Endpoint {
|
pub fn createIpcEndpoint() ?*Endpoint {
|
||||||
const endpoint = heap.allocator().create(Endpoint) catch return null;
|
const endpoint = heap.allocator().create(Endpoint) catch return null;
|
||||||
endpoint.* = .{};
|
endpoint.* = .{};
|
||||||
return endpoint;
|
return endpoint;
|
||||||
@@ -91,6 +124,7 @@ pub fn dropRef(endpoint: *Endpoint) void {
|
|||||||
// --- sender FIFO (endpoint-local, via Task.next) ----------------------------
|
// --- sender FIFO (endpoint-local, via Task.next) ----------------------------
|
||||||
|
|
||||||
fn enqueueSender(endpoint: *Endpoint, t: *Task) void {
|
fn enqueueSender(endpoint: *Endpoint, t: *Task) void {
|
||||||
|
t.ipc_wait_endpoint = @ptrCast(endpoint); // so a kill can unlink a parked caller
|
||||||
t.next = null;
|
t.next = null;
|
||||||
if (endpoint.sender_tail) |tail| tail.next = t else endpoint.sender_head = t;
|
if (endpoint.sender_tail) |tail| tail.next = t else endpoint.sender_head = t;
|
||||||
endpoint.sender_tail = t;
|
endpoint.sender_tail = t;
|
||||||
@@ -100,10 +134,34 @@ fn dequeueSender(endpoint: *Endpoint) ?*Task {
|
|||||||
const t = endpoint.sender_head orelse return null;
|
const t = endpoint.sender_head orelse return null;
|
||||||
endpoint.sender_head = t.next;
|
endpoint.sender_head = t.next;
|
||||||
if (endpoint.sender_head == null) endpoint.sender_tail = null;
|
if (endpoint.sender_head == null) endpoint.sender_tail = null;
|
||||||
|
t.ipc_wait_endpoint = null;
|
||||||
t.next = null;
|
t.next = null;
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unlink `t` from the sender FIFO it queues in, if any — the kill path for a
|
||||||
|
/// client parked in `call` that no server has received yet. Without this, a dead
|
||||||
|
/// caller would later be dequeued as a dangling pointer. The endpoint is still
|
||||||
|
/// alive here: `t`'s own handle table holds a reference until closeHandles runs
|
||||||
|
/// (which the kill path does *after* this). Precondition: the big kernel lock is
|
||||||
|
/// held.
|
||||||
|
pub fn abandonSenderLocked(t: *Task) void {
|
||||||
|
const endpoint: *Endpoint = @ptrCast(@alignCast(t.ipc_wait_endpoint orelse return));
|
||||||
|
t.ipc_wait_endpoint = null;
|
||||||
|
var previous: ?*Task = null;
|
||||||
|
var node = endpoint.sender_head;
|
||||||
|
while (node) |n| : ({
|
||||||
|
previous = n;
|
||||||
|
node = n.next;
|
||||||
|
}) {
|
||||||
|
if (n != t) continue;
|
||||||
|
if (previous) |p| p.next = t.next else endpoint.sender_head = t.next;
|
||||||
|
if (endpoint.sender_tail == t) endpoint.sender_tail = previous;
|
||||||
|
t.next = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- cross-address-space copy ----------------------------------------------
|
// --- cross-address-space copy ----------------------------------------------
|
||||||
|
|
||||||
/// Copy `len` bytes from `source_va` in address space `source_as` to `destination_va` in
|
/// Copy `len` bytes from `source_va` in address space `source_as` to `destination_va` in
|
||||||
@@ -124,8 +182,8 @@ fn copyAcross(source_as: u64, source_va: u64, destination_as: u64, destination_v
|
|||||||
const s_left = page_size - ((source_va + off) & (page_size - 1));
|
const s_left = page_size - ((source_va + off) & (page_size - 1));
|
||||||
const d_left = page_size - ((destination_va + off) & (page_size - 1));
|
const d_left = page_size - ((destination_va + off) & (page_size - 1));
|
||||||
const n = @min(@min(s_left, d_left), len - off);
|
const n = @min(@min(s_left, d_left), len - off);
|
||||||
const source: [*]const u8 = @ptrFromInt(danos.physicalToVirtual(s));
|
const source: [*]const u8 = @ptrFromInt(boot_handoff.physicalToVirtual(s));
|
||||||
const destination: [*]u8 = @ptrFromInt(danos.physicalToVirtual(d));
|
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(d));
|
||||||
@memcpy(destination[0..n], source[0..n]);
|
@memcpy(destination[0..n], source[0..n]);
|
||||||
off += n;
|
off += n;
|
||||||
}
|
}
|
||||||
@@ -147,7 +205,7 @@ pub fn copyFromUser(user_as: u64, user_va: u64, destination: []u8) bool {
|
|||||||
const s = architecture.translate(user_as, user_va + off) orelse return false;
|
const s = architecture.translate(user_as, user_va + off) orelse return false;
|
||||||
const s_left = page_size - ((user_va + off) & (page_size - 1));
|
const s_left = page_size - ((user_va + off) & (page_size - 1));
|
||||||
const n = @min(s_left, destination.len - off);
|
const n = @min(s_left, destination.len - off);
|
||||||
const source: [*]const u8 = @ptrFromInt(danos.physicalToVirtual(s));
|
const source: [*]const u8 = @ptrFromInt(boot_handoff.physicalToVirtual(s));
|
||||||
@memcpy(destination[off..][0..n], source[0..n]);
|
@memcpy(destination[off..][0..n], source[0..n]);
|
||||||
off += n;
|
off += n;
|
||||||
}
|
}
|
||||||
@@ -156,10 +214,28 @@ pub fn copyFromUser(user_as: u64, user_va: u64, destination: []u8) bool {
|
|||||||
|
|
||||||
// --- the two IPC operations -------------------------------------------------
|
// --- the two IPC operations -------------------------------------------------
|
||||||
|
|
||||||
|
/// Share the capability named by handle `cap` in `from`'s table into `to`'s table,
|
||||||
|
/// bumping the endpoint's refcount (the sender keeps its handle — this is a copy, not
|
||||||
|
/// a move). Returns the handle it landed at in `to` (>= 0), or `-EBADF` if `cap` names
|
||||||
|
/// no live handle, or `-ENOSPC` if `to`'s table is full. Callers only invoke this when
|
||||||
|
/// `cap != no_cap`. Used by both IPC directions to carry an endpoint with a message.
|
||||||
|
fn shareCapability(from: *Task, to: *Task, cap: u64) i64 {
|
||||||
|
const endpoint = resolveHandle(from, cap) orelse return -EBADF;
|
||||||
|
endpoint.refcount += 1;
|
||||||
|
const handle = installHandle(to, endpoint);
|
||||||
|
if (handle < 0) {
|
||||||
|
dropRef(endpoint); // undo the bump; the receiver had no room
|
||||||
|
return -ENOSPC;
|
||||||
|
}
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
/// Client side of IPC_Call: send `[message_ptr, message_len)` to `endpoint` and block until a
|
/// Client side of IPC_Call: send `[message_ptr, message_len)` to `endpoint` and block until a
|
||||||
/// server replies into `[reply_ptr, reply_cap)`. Returns the reply length, or a
|
/// server replies into `[reply_ptr, reply_cap)`. Returns the reply length, or a
|
||||||
/// negative errno. Runs as the current task.
|
/// negative errno. `send_cap` (a handle, or `no_cap`) is an endpoint transferred to the
|
||||||
pub fn call(endpoint: *Endpoint, message_ptr: u64, message_len: u64, reply_ptr: u64, reply_cap: u64) i64 {
|
/// server with the request; `out_received_cap` receives the handle of an endpoint the
|
||||||
|
/// server sent back in its reply, or `no_cap`. Runs as the current task.
|
||||||
|
pub fn call(endpoint: *Endpoint, message_ptr: u64, message_len: u64, reply_ptr: u64, reply_cap: u64, send_cap: u64, out_received_cap: *u64) i64 {
|
||||||
if (message_len > MESSAGE_MAXIMUM or reply_cap > MESSAGE_MAXIMUM) return -E2BIG;
|
if (message_len > MESSAGE_MAXIMUM or reply_cap > MESSAGE_MAXIMUM) return -E2BIG;
|
||||||
const flags = sync.enter();
|
const flags = sync.enter();
|
||||||
defer sync.leave(flags);
|
defer sync.leave(flags);
|
||||||
@@ -169,12 +245,15 @@ pub fn call(endpoint: *Endpoint, message_ptr: u64, message_len: u64, reply_ptr:
|
|||||||
me.ipc_send_len = message_len;
|
me.ipc_send_len = message_len;
|
||||||
me.ipc_reply_ptr = reply_ptr;
|
me.ipc_reply_ptr = reply_ptr;
|
||||||
me.ipc_reply_cap = reply_cap;
|
me.ipc_reply_cap = reply_cap;
|
||||||
|
me.ipc_send_cap = send_cap;
|
||||||
|
me.ipc_received_cap = abi.no_cap;
|
||||||
me.ipc_status = 0;
|
me.ipc_status = 0;
|
||||||
|
|
||||||
enqueueSender(endpoint, me); // join the FIFO, then...
|
enqueueSender(endpoint, me); // join the FIFO, then...
|
||||||
scheduler.wakeLocked(&endpoint.receive_wait_queue); // ...wake a waiting server (no-op if none)
|
scheduler.wakeLocked(&endpoint.receive_wait_queue); // ...wake a waiting server (no-op if none)
|
||||||
scheduler.blockCurrentLocked(); // block until the reply readies us again
|
scheduler.blockCurrentLocked(); // block until the reply readies us again
|
||||||
|
|
||||||
|
out_received_cap.* = me.ipc_received_cap; // a capability the replier sent back, or no_cap
|
||||||
return me.ipc_status; // reply length or -errno, written by the replier
|
return me.ipc_status; // reply length or -errno, written by the replier
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,30 +263,53 @@ pub fn call(endpoint: *Endpoint, message_ptr: u64, message_len: u64, reply_ptr:
|
|||||||
/// to `out_badge` and returns the request length, or a negative errno. A pending
|
/// to `out_badge` and returns the request length, or a negative errno. A pending
|
||||||
/// notification is delivered ahead of client requests (length 0, badge with
|
/// notification is delivered ahead of client requests (length 0, badge with
|
||||||
/// `notify_badge_bit` set, no reply owed).
|
/// `notify_badge_bit` set, no reply owed).
|
||||||
pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_ptr: u64, receive_cap: u64, out_badge: *u64) i64 {
|
pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_ptr: u64, receive_cap: u64, send_cap: u64, out_badge: *u64, out_received_cap: *u64) i64 {
|
||||||
if (reply_len > MESSAGE_MAXIMUM or receive_cap > MESSAGE_MAXIMUM) return -E2BIG;
|
if (reply_len > MESSAGE_MAXIMUM or receive_cap > MESSAGE_MAXIMUM) return -E2BIG;
|
||||||
const flags = sync.enter();
|
const flags = sync.enter();
|
||||||
defer sync.leave(flags);
|
defer sync.leave(flags);
|
||||||
|
|
||||||
const me = scheduler.current();
|
const me = scheduler.current();
|
||||||
|
out_received_cap.* = abi.no_cap; // no capability received unless a request delivers one
|
||||||
|
|
||||||
// (1) Reply to the client we're still holding, if any.
|
// (1) Reply to the client we're still holding, if any — carrying `send_cap` to it.
|
||||||
if (me.ipc_client) |client| {
|
if (me.ipc_client) |client| {
|
||||||
me.ipc_client = null;
|
me.ipc_client = null;
|
||||||
const n = @min(reply_len, client.ipc_reply_cap);
|
const n = @min(reply_len, client.ipc_reply_cap);
|
||||||
if (copyAcross(me.aspace, reply_ptr, client.aspace, client.ipc_reply_ptr, n)) {
|
client.ipc_received_cap = abi.no_cap;
|
||||||
client.ipc_status = @intCast(n);
|
if (!copyAcross(me.aspace, reply_ptr, client.aspace, client.ipc_reply_ptr, n)) {
|
||||||
} else {
|
|
||||||
client.ipc_status = -EFAULT;
|
client.ipc_status = -EFAULT;
|
||||||
|
} else if (send_cap != abi.no_cap) {
|
||||||
|
// Transfer the reply's capability into the client. A failure fails the
|
||||||
|
// client's `call` rather than delivering a reply without its promised cap.
|
||||||
|
const shared = shareCapability(me, client, send_cap);
|
||||||
|
if (shared < 0) {
|
||||||
|
client.ipc_status = shared; // -EBADF (bad handle) or -ENOSPC (client table full)
|
||||||
|
} else {
|
||||||
|
client.ipc_received_cap = @intCast(shared);
|
||||||
|
client.ipc_status = @intCast(n);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client.ipc_status = @intCast(n);
|
||||||
}
|
}
|
||||||
scheduler.readyLocked(client); // its `call` now returns
|
scheduler.readyLocked(client); // its `call` now returns
|
||||||
}
|
}
|
||||||
|
|
||||||
// (2) Receive the next request (or notification), blocking until one is ready.
|
// (2) Receive the next request (or notification / buffered message), blocking until
|
||||||
|
// one is ready. Bare notifications (IRQ/exit) come first — they're latency-sensitive
|
||||||
|
// and carry no payload — then buffered messages, then synchronous client requests.
|
||||||
while (true) {
|
while (true) {
|
||||||
if (popNotify(endpoint)) |badge| {
|
if (popNotify(endpoint)) |badge| {
|
||||||
out_badge.* = badge | notify_badge_bit;
|
out_badge.* = badge | notify_badge_bit;
|
||||||
return 0; // notification: no payload, no reply owed
|
return 0; // notification: no payload, no reply owed, no cap
|
||||||
|
}
|
||||||
|
if (popPost(endpoint)) |slot| {
|
||||||
|
const n = @min(@as(usize, slot.length), receive_cap);
|
||||||
|
// Copy from the kernel-resident ring slot (source aspace 0) into the receiver.
|
||||||
|
if (!copyAcross(0, @intFromPtr(&slot.bytes), me.aspace, receive_ptr, n)) {
|
||||||
|
continue; // bad receive buffer: drop this message, keep serving
|
||||||
|
}
|
||||||
|
out_badge.* = slot.sender_id | notify_badge_bit | notify_message_bit;
|
||||||
|
return @intCast(n); // async message: payload delivered, no reply owed, no cap
|
||||||
}
|
}
|
||||||
if (dequeueSender(endpoint)) |caller| {
|
if (dequeueSender(endpoint)) |caller| {
|
||||||
const n = @min(caller.ipc_send_len, receive_cap);
|
const n = @min(caller.ipc_send_len, receive_cap);
|
||||||
@@ -216,6 +318,17 @@ pub fn replyWait(endpoint: *Endpoint, reply_ptr: u64, reply_len: u64, receive_pt
|
|||||||
scheduler.readyLocked(caller);
|
scheduler.readyLocked(caller);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Install the capability the caller sent, if any, into my table. A failure
|
||||||
|
// fails the caller's `call` and does not deliver — no half-delivered cap.
|
||||||
|
if (caller.ipc_send_cap != abi.no_cap) {
|
||||||
|
const shared = shareCapability(caller, me, caller.ipc_send_cap);
|
||||||
|
if (shared < 0) {
|
||||||
|
caller.ipc_status = shared; // -EBADF or -ENOSPC
|
||||||
|
scheduler.readyLocked(caller);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out_received_cap.* = @intCast(shared);
|
||||||
|
}
|
||||||
me.ipc_client = caller; // remember who to reply to
|
me.ipc_client = caller; // remember who to reply to
|
||||||
out_badge.* = caller.id;
|
out_badge.* = caller.id;
|
||||||
return @intCast(n);
|
return @intCast(n);
|
||||||
@@ -233,6 +346,44 @@ fn popNotify(endpoint: *Endpoint) ?u64 {
|
|||||||
return badge;
|
return badge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take the oldest buffered message from the post ring, or null if empty. Returns a
|
||||||
|
/// pointer into the endpoint's own storage — valid until the next `send`/`popPost` under
|
||||||
|
/// the same lock region, which is all the copy-out in `replyWait` needs.
|
||||||
|
fn popPost(endpoint: *Endpoint) ?*const PostSlot {
|
||||||
|
if (endpoint.post_head == endpoint.post_tail) return null;
|
||||||
|
const slot = &endpoint.post_buffer[endpoint.post_head % post_capacity];
|
||||||
|
endpoint.post_head +%= 1;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client-free side of async IPC (`ipc_send`): copy `[source_va, len)` from address space
|
||||||
|
/// `source_as` into `endpoint`'s post ring and wake a waiting receiver — **without
|
||||||
|
/// blocking the sender** and with no reply owed. `sender_id` rides along, delivered in the
|
||||||
|
/// low bits of the receiver's badge. Returns 0, or a negative errno (`-E2BIG` if the
|
||||||
|
/// payload exceeds `POST_MAXIMUM`, `-EFAULT` if the source buffer is unmapped / out of the
|
||||||
|
/// user half). A full ring drops the *oldest* message (advancing `post_head`), because a
|
||||||
|
/// buffered message is discrete, not a level: keeping the newest keeps input responsive.
|
||||||
|
/// Precondition: the big kernel lock is held.
|
||||||
|
pub fn sendLocked(endpoint: *Endpoint, source_as: u64, source_va: u64, len: u64, sender_id: u64) i64 {
|
||||||
|
if (len > POST_MAXIMUM) return -E2BIG;
|
||||||
|
// Drop the oldest if the ring is full, so this newest message always lands.
|
||||||
|
if (endpoint.post_tail -% endpoint.post_head >= post_capacity) endpoint.post_head +%= 1;
|
||||||
|
const slot = &endpoint.post_buffer[endpoint.post_tail % post_capacity];
|
||||||
|
if (!copyFromUser(source_as, source_va, slot.bytes[0..@intCast(len)])) return -EFAULT;
|
||||||
|
slot.length = @intCast(len);
|
||||||
|
slot.sender_id = sender_id;
|
||||||
|
endpoint.post_tail +%= 1;
|
||||||
|
scheduler.wakeLocked(&endpoint.receive_wait_queue);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `sendLocked` wrapped in its own critical section, for the `ipc_send` syscall path.
|
||||||
|
pub fn send(endpoint: *Endpoint, source_as: u64, source_va: u64, len: u64, sender_id: u64) i64 {
|
||||||
|
const flags = sync.enter();
|
||||||
|
defer sync.leave(flags);
|
||||||
|
return sendLocked(endpoint, source_as, source_va, len, sender_id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Post an asynchronous notification carrying `badge` to `endpoint` and wake a waiting
|
/// Post an asynchronous notification carrying `badge` to `endpoint` and wake a waiting
|
||||||
/// receiver. Precondition: the big kernel lock is held.
|
/// receiver. Precondition: the big kernel lock is held.
|
||||||
///
|
///
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user