kernel: route exclusive interrupts through masked PLIC claims
566537b1fc0d5dfa9947666ede25ead59fc9d6b996964bb2b6008fd5341f6ff7
Verified: make -C kernel check with the machine-capable emulator; all pass.
1 parent
e7e57c61
kernel/Makefile
+9 -5
| 5 | 5 | COMPILE := $(HOST_EMU) -memory-size=385024 -data-size=348160 -stack-size=512 -run $(COMPILER) |
|
| 6 | 6 | MODULES := core/fdt.rad core/platform.rad core/frames.rad core/abi.rad core/handles.rad \ |
|
| 7 | 7 | core/events.rad core/domains.rad core/resources.rad core/capabilities.rad \ |
|
| 8 | 8 | core/memory.rad core/state.rad core/pages.rad core/atomic.rad core/cpu.rad \ |
|
| 9 | 9 | core/budgets.rad core/contexts.rad core/clock.rad core/budget_caps.rad \ |
|
| 10 | - | core/timers.rad core/notifications.rad |
|
| 11 | - | CORE_ASM := arch/atomic.ras arch/context.ras arch/clock.ras |
|
| 10 | + | core/timers.rad core/notifications.rad core/devices.rad core/interrupts.rad core/mmio.rad |
|
| 11 | + | CORE_ASM := arch/atomic.ras arch/context.ras arch/clock.ras arch/mmio.ras |
|
| 12 | 12 | CORE := -pkg core -mod core.rad $(addprefix -mod ,$(MODULES) $(CORE_ASM)) |
|
| 13 | 13 | CHECK_MODULES := check/boot.rad check/fixture.rad check/frames.rad check/handles.rad \ |
|
| 14 | 14 | check/domains.rad check/capabilities.rad check/pages.rad check/events.rad \ |
|
| 15 | - | check/budgets.rad check/budget_caps.rad check/timers.rad check/notifications.rad |
|
| 15 | + | check/budgets.rad check/budget_caps.rad check/timers.rad check/notifications.rad check/devices.rad |
|
| 16 | 16 | ||
| 17 | 17 | .PHONY: all check clean compiler-check |
|
| 18 | 18 | all: kernel.rv64 |
|
| 19 | 19 | ||
| 20 | 20 | compiler-check: |
| 29 | 29 | $(COMPILE) $(CORE) -pkg check -mod check.rad $(addprefix -mod ,$(CHECK_MODULES)) -entry check -o $@ |
|
| 30 | 30 | ||
| 31 | 31 | context.rv64: context.rad context/wait.rad check/context.ras arch/entry.ras core.rad $(MODULES) $(CORE_ASM) $(COMPILER) |
|
| 32 | 32 | $(COMPILE) $(CORE) -pkg context -start arch/entry.ras -mod context.rad -mod context/wait.rad -mod check/context.ras -entry context -o $@ |
|
| 33 | 33 | ||
| 34 | - | check: all check.rv64 context.rv64 |
|
| 34 | + | interrupt.rv64: interrupt.rad check/interrupt.ras arch/entry.ras core.rad $(MODULES) $(CORE_ASM) $(COMPILER) |
|
| 35 | + | $(COMPILE) $(CORE) -pkg interrupt -start arch/entry.ras -mod interrupt.rad -mod check/interrupt.ras -entry interrupt -o $@ |
|
| 36 | + | ||
| 37 | + | check: all check.rv64 context.rv64 interrupt.rv64 |
|
| 35 | 38 | $(HOST_EMU) -run check.rv64 |
|
| 36 | 39 | $(EMU) -machine -no-guard-stack -max-steps=100000000 -count-instructions -run kernel.rv64 |
|
| 37 | 40 | $(EMU) -machine -no-guard-stack -max-steps=1000000 -count-instructions -run context.rv64 |
|
| 41 | + | $(EMU) -machine -harts=2 -no-guard-stack -irq=3 -irq-at=4000000 -uart-rx=52 -uart-rx-at=8000000 -max-steps=100000000 -count-instructions -run interrupt.rv64 |
|
| 38 | 42 | ||
| 39 | 43 | clean: |
|
| 40 | - | rm -f kernel.rv64 check.rv64 context.rv64 |
|
| 44 | + | rm -f kernel.rv64 check.rv64 context.rv64 interrupt.rv64 |
kernel/NOTES.md
+18 -2
| 1 | 1 | # Kernel implementation decisions |
|
| 2 | 2 | ||
| 3 | 3 | The specification at https://radiant.computer/system/kernel takes precedence |
|
| 4 | 4 | for fixed call numbers, handle layout, rights, and object behavior. These notes |
|
| 5 | - | record the contracts established through step 13 of the 22-step plan. |
|
| 5 | + | record the contracts established through step 14 of the 22-step plan. |
|
| 6 | 6 | ||
| 7 | 7 | ## Source and trust boundary |
|
| 8 | 8 | ||
| 9 | 9 | - Kernel mechanisms use freestanding Radiance; RAS owns machine entry, register |
|
| 10 | 10 | state, atomics, and MMIO. Hosted checks exercise the same mechanism modules. |
| 173 | 173 | - Wait validates consumer progress before blocking. Resumption requires an |
|
| 174 | 174 | unread event and positive retained budget. Wakeup requires an explicit Wake |
|
| 175 | 175 | handle and records the sender identity. Idle waiting does not consume ticks |
|
| 176 | 176 | from the suspended context's budget. |
|
| 177 | 177 | ||
| 178 | + | ## Devices and external interrupts |
|
| 179 | + | ||
| 180 | + | - Device access validates live authority, rights, complete byte range, overflow, |
|
| 181 | + | and physical alignment. Its admitted address is consumed by one MMIO operation; |
|
| 182 | + | it cannot become a user pointer. |
|
| 183 | + | - One FDT-selected PLIC machine context services external interrupts; other |
|
| 184 | + | contexts remain masked. Access only implemented source words and fence memory |
|
| 185 | + | and device I/O. |
|
| 186 | + | - A claimed source stays masked and active until notification consumption. The |
|
| 187 | + | driver drains the device first. Completion briefly enables the route because |
|
| 188 | + | the PLIC may require its enable bit. Claims survive queue backpressure and |
|
| 189 | + | exclusive receiver transfer. |
|
| 190 | + | - Missing, dead, or stale receiver authority leaves the source masked. Invalid |
|
| 191 | + | consumer progress cannot release a claim or rearm a source. The two-hart |
|
| 192 | + | IRQ/UART check exercises the primary PLIC route, not multicore dispatch. |
|
| 193 | + | ||
| 178 | 194 | ## Validation |
|
| 179 | 195 | ||
| 180 | 196 | Use the current machine-capable sibling emulator. Set `RAD_EMULATOR`, pass |
|
| 181 | 197 | `EMU` to the kernel Make invocation, or put `emulator` on PATH. The kernel build |
|
| 182 | 198 | checks compiler dependencies. From the repository root, run: |
|
| 183 | 199 | ||
| 184 | 200 | ```sh |
|
| 185 | 201 | make -C kernel check |
|
| 186 | 202 | ``` |
|
| 187 | 203 | ||
| 188 | - | Exercise Timeout, Wait, machine idle, timer expiry, event consumption, retained notifications, and unchanged budget during idle. |
|
| 204 | + | Run the two-hart IRQ/UART scenario with IRQ 3 at tick 4,000,000 and UART byte R at tick 8,000,000. Check retained-claim transfer through a full queue, one delivery, draining, dropped authority, and the next timer wake. |
|
| 189 | 205 | ||
| 190 | 206 | Run the context reservation probe with an emulator that retains LR/SC |
|
| 191 | 207 | reservations across traps. This checks the kernel's reservation invalidation. |
kernel/arch/mmio.ras
added
+14 -0
| 1 | + | // Full I/O and memory fences surround each native 32-bit controller access. |
|
| 2 | + | .text; |
|
| 3 | + | .export @"core::mmio::read32"; |
|
| 4 | + | @"core::mmio::read32" |
|
| 5 | + | fence; |
|
| 6 | + | lwu %a0 0(%a0); |
|
| 7 | + | fence; |
|
| 8 | + | ret; |
|
| 9 | + | .export @"core::mmio::write32"; |
|
| 10 | + | @"core::mmio::write32" |
|
| 11 | + | fence; |
|
| 12 | + | sw %a1 0(%a0); |
|
| 13 | + | fence; |
|
| 14 | + | ret; |
kernel/check.rad
+2 -0
| 10 | 10 | mod events; |
|
| 11 | 11 | mod budgets; |
|
| 12 | 12 | mod budget_caps; |
|
| 13 | 13 | mod timers; |
|
| 14 | 14 | mod notifications; |
|
| 15 | + | mod devices; |
|
| 15 | 16 | ||
| 16 | 17 | /// Run the available kernel mechanism checks. |
|
| 17 | 18 | @default fn main() -> u32 { |
|
| 18 | 19 | frames::run(); |
|
| 19 | 20 | boot::run(); |
| 24 | 25 | events::run(); |
|
| 25 | 26 | budgets::run(); |
|
| 26 | 27 | budget_caps::run(); |
|
| 27 | 28 | timers::run(); |
|
| 28 | 29 | notifications::run(); |
|
| 30 | + | devices::run(); |
|
| 29 | 31 | return 0; |
|
| 30 | 32 | } |
kernel/check/devices.rad
added
+88 -0
| 1 | + | //! Device materialization authority and byte-range boundary checks; no hardware I/O. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::capabilities; |
|
| 5 | + | use core::contexts; |
|
| 6 | + | use core::devices; |
|
| 7 | + | use core::domains; |
|
| 8 | + | use core::fdt; |
|
| 9 | + | use core::frames; |
|
| 10 | + | use core::handles; |
|
| 11 | + | use core::memory; |
|
| 12 | + | use core::platform; |
|
| 13 | + | use core::resources; |
|
| 14 | + | use core::state; |
|
| 15 | + | ||
| 16 | + | /// One caller and its device authority. |
|
| 17 | + | static DOMAINS: [domains::Domain; 1] = undefined; |
|
| 18 | + | /// Device windows, including deliberately invalid physical bounds. |
|
| 19 | + | static OBJECTS: [resources::Slot; 4] = undefined; |
|
| 20 | + | /// Context storage required by shared kernel state. |
|
| 21 | + | static CONTEXTS: [contexts::Context; 1] = undefined; |
|
| 22 | + | /// Empty RAM pool; materialization must not access physical device bytes. |
|
| 23 | + | static POOL: frames::Pool = undefined; |
|
| 24 | + | /// Unused frame metadata backing. |
|
| 25 | + | static PINS: [u16; 1] = undefined; |
|
| 26 | + | /// Unused Page claim backing. |
|
| 27 | + | static ASSIGNED: [bool; 1] = undefined; |
|
| 28 | + | /// Domain grant backing. |
|
| 29 | + | static GRANTS: [u64; 1] = undefined; |
|
| 30 | + | /// Shared state for the checked materialization entry point. |
|
| 31 | + | static KERNEL: state::State = undefined; |
|
| 32 | + | ||
| 33 | + | /// Install a real device resource and read-only capability without touching MMIO. |
|
| 34 | + | fn device(base: u64, size: u64) -> abi::Handle { |
|
| 35 | + | let object = try! resources::create(&mut OBJECTS[..], resources::Value::Device(fdt::Range { base, size })); |
|
| 36 | + | resources::retain(&mut OBJECTS[..], object); |
|
| 37 | + | let slot = try! handles::vacant(&DOMAINS[0].handles); |
|
| 38 | + | return handles::install(&mut DOMAINS[0].handles, slot, object, abi::READ); |
|
| 39 | + | } |
|
| 40 | + | ||
| 41 | + | /// Observe the public error result rather than private validation steps. |
|
| 42 | + | fn error(handle: abi::Handle, offset: u64, width: u64, rights: u16) -> abi::Error { |
|
| 43 | + | let _address = try devices::access(&KERNEL, 0, handle, offset, width, rights) catch result { |
|
| 44 | + | return result; |
|
| 45 | + | }; |
|
| 46 | + | return abi::Error::Ok; |
|
| 47 | + | } |
|
| 48 | + | ||
| 49 | + | /// Check exact-end bounds, physical alignment, arithmetic overflow, and authority. |
|
| 50 | + | export fn run() { |
|
| 51 | + | domains::init(&mut DOMAINS[..]); |
|
| 52 | + | resources::init(&mut OBJECTS[..]); |
|
| 53 | + | contexts::init(&mut CONTEXTS[0], 0); |
|
| 54 | + | let mut machine: platform::Platform = undefined; |
|
| 55 | + | set machine.memoryCount = 0; |
|
| 56 | + | set machine.reservedCount = 0; |
|
| 57 | + | try! frames::init(&mut POOL, &machine); |
|
| 58 | + | let mut ram: memory::Memory = undefined; |
|
| 59 | + | memory::init(&mut ram, &mut POOL, &mut PINS[..], &mut ASSIGNED[..], &mut GRANTS[..], 1); |
|
| 60 | + | set KERNEL = state::State { domains: &mut DOMAINS[..], resources: &mut OBJECTS[..], contexts: &mut CONTEXTS[..], memory: ram }; |
|
| 61 | + | let root = domains::root(&mut DOMAINS[..], 0); |
|
| 62 | + | let registers = device(0x1000, 16); |
|
| 63 | + | assert try! devices::access(&KERNEL, 0, registers, 8, 8, abi::READ) == 0x1008; |
|
| 64 | + | assert error(registers, 16, 1, abi::READ) == abi::Error::InvalidArg; |
|
| 65 | + | assert error(registers, 12, 8, abi::READ) == abi::Error::InvalidArg; |
|
| 66 | + | assert error(registers, 0xffffffffffffffff, 2, abi::READ) == abi::Error::InvalidArg; |
|
| 67 | + | assert error(registers, 0, 0, abi::READ) == abi::Error::InvalidArg; |
|
| 68 | + | assert error(registers, 0, 3, abi::READ) == abi::Error::InvalidArg; |
|
| 69 | + | assert error(registers, 1, 2, abi::READ) == abi::Error::InvalidArg; |
|
| 70 | + | assert error(registers, 0, 1, 0) == abi::Error::InvalidArg; |
|
| 71 | + | assert error(registers, 0, 1, abi::READ | abi::EXECUTE) == abi::Error::InvalidArg; |
|
| 72 | + | assert error(registers, 0, 1, abi::READ | abi::WRITE) == abi::Error::Denied; |
|
| 73 | + | assert error(root, 0, 1, abi::READ) == abi::Error::BadHandle; |
|
| 74 | + | ||
| 75 | + | // Alignment belongs to the physical address, not just the region offset. |
|
| 76 | + | let unaligned = device(0x2001, 8); |
|
| 77 | + | assert try! devices::access(&KERNEL, 0, unaligned, 3, 4, abi::READ) == 0x2004; |
|
| 78 | + | assert error(unaligned, 0, 4, abi::READ) == abi::Error::InvalidArg; |
|
| 79 | + | let overflowing = device(0xfffffffffffffffc, 16); |
|
| 80 | + | assert error(overflowing, 4, 1, abi::READ) == abi::Error::InvalidArg; |
|
| 81 | + | ||
| 82 | + | // A still-packed handle cannot name another incarnation of its resource slot. |
|
| 83 | + | let entry = try! capabilities::lookup(&KERNEL, 0, registers); |
|
| 84 | + | set OBJECTS[entry.object.index].epoch += 1; |
|
| 85 | + | assert error(registers, 0, 1, abi::READ) == abi::Error::BadHandle; |
|
| 86 | + | try! capabilities::drop(&mut KERNEL, 0, unaligned); |
|
| 87 | + | assert error(unaligned, 0, 1, abi::READ) == abi::Error::BadHandle; |
|
| 88 | + | } |
kernel/check/interrupt.ras
added
+14 -0
| 1 | + | // Byte MMIO for the trusted physical-device regression only. |
|
| 2 | + | .text; |
|
| 3 | + | .export @"interrupt::read8"; |
|
| 4 | + | @"interrupt::read8" |
|
| 5 | + | fence; |
|
| 6 | + | lbu %a0 0(%a0); |
|
| 7 | + | fence; |
|
| 8 | + | ret; |
|
| 9 | + | .export @"interrupt::write8"; |
|
| 10 | + | @"interrupt::write8" |
|
| 11 | + | fence; |
|
| 12 | + | sb %a1 0(%a0); |
|
| 13 | + | fence; |
|
| 14 | + | ret; |
kernel/core.rad
+3 -0
| 18 | 18 | export mod contexts; |
|
| 19 | 19 | export mod clock; |
|
| 20 | 20 | export mod budget_caps; |
|
| 21 | 21 | export mod timers; |
|
| 22 | 22 | export mod notifications; |
|
| 23 | + | export mod devices; |
|
| 24 | + | export mod interrupts; |
|
| 25 | + | export mod mmio; |
kernel/core/devices.rad
added
+32 -0
| 1 | + | //! Capability-checked MMIO address materialization; never device I/O. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::capabilities; |
|
| 5 | + | use core::resources; |
|
| 6 | + | use core::state; |
|
| 7 | + | ||
| 8 | + | /// Materialize one loader-verified MMIO instruction's complete byte range. |
|
| 9 | + | /// The returned address is not Page authority and is consumed by that instruction. |
|
| 10 | + | export fn access(kernel: *state::State, caller: u32, handle: abi::Handle, offset: u64, width: u64, rights: u16) -> u64 throws (abi::Error) { |
|
| 11 | + | let entry = try capabilities::lookup(kernel, caller, handle); |
|
| 12 | + | if entry.object.kind <> abi::Kind::Device { throw abi::Error::BadHandle; } |
|
| 13 | + | if rights == 0 or rights & ~(abi::READ | abi::WRITE) <> 0 { |
|
| 14 | + | throw abi::Error::InvalidArg; |
|
| 15 | + | } |
|
| 16 | + | if entry.rights & rights <> rights { throw abi::Error::Denied; } |
|
| 17 | + | if width <> 1 and width <> 2 and width <> 4 and width <> 8 { |
|
| 18 | + | throw abi::Error::InvalidArg; |
|
| 19 | + | } |
|
| 20 | + | let case resources::Value::Device(region) = kernel.resources[entry.object.index].value else { |
|
| 21 | + | panic "access: device kind mismatch"; |
|
| 22 | + | }; |
|
| 23 | + | if offset > region.size or width > region.size - offset |
|
| 24 | + | or region.base > 0xffffffffffffffff - offset { |
|
| 25 | + | throw abi::Error::InvalidArg; |
|
| 26 | + | } |
|
| 27 | + | let address = region.base + offset; |
|
| 28 | + | if address > 0xffffffffffffffff - (width - 1) or address % width <> 0 { |
|
| 29 | + | throw abi::Error::InvalidArg; |
|
| 30 | + | } |
|
| 31 | + | return address; |
|
| 32 | + | } |
kernel/core/interrupts.rad
added
+180 -0
| 1 | + | //! Exclusive PLIC routing with retained claims and bounded queue backpressure. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::domains; |
|
| 5 | + | use core::events; |
|
| 6 | + | use core::mmio; |
|
| 7 | + | use core::platform; |
|
| 8 | + | use core::resources; |
|
| 9 | + | use core::state; |
|
| 10 | + | ||
| 11 | + | /// Private state for one source; source zero is never enabled or published. |
|
| 12 | + | export record Source: Copy { |
|
| 13 | + | /// Installed Interrupt resource identity, not its mutable receiver. |
|
| 14 | + | object: abi::Object, |
|
| 15 | + | /// A real hardware claim still awaiting publication or consumer progress. |
|
| 16 | + | claimed: bool, |
|
| 17 | + | /// Domain incarnation whose queue contains this claim, or the empty identity. |
|
| 18 | + | published: abi::Object, |
|
| 19 | + | } |
|
| 20 | + | ||
| 21 | + | /// One machine-context PLIC service route, serialized by the kernel lock. |
|
| 22 | + | /// All online harts may call poll; only the selected hart handles external traps. |
|
| 23 | + | /// Domain execution routing and remote wakeups are separate from this controller. |
|
| 24 | + | export record Controller: Copy { |
|
| 25 | + | /// Validated physical PLIC register base. |
|
| 26 | + | base: u64, |
|
| 27 | + | /// Actual FDT machine-context index, never derived from the hardware hart ID. |
|
| 28 | + | context: u32, |
|
| 29 | + | /// Largest usable source identifier; strictly less than MAX_IRQS. |
|
| 30 | + | limit: u32, |
|
| 31 | + | /// Shadow of the selected context's enable words; sole writer is this module. |
|
| 32 | + | enabled: [u32; platform::MAX_IRQS / 32], |
|
| 33 | + | /// Constant-space resource mapping and notification state, indexed by source. |
|
| 34 | + | sources: [Source; platform::MAX_IRQS], |
|
| 35 | + | } |
|
| 36 | + | ||
| 37 | + | /// Initialize once before external interrupts or secondary kernel entry are enabled. |
|
| 38 | + | /// machine must be a validated platform and hartIndex an online-hart array index. |
|
| 39 | + | /// Sources stay disabled until register followed by poll observes a live receiver. |
|
| 40 | + | export unsafe fn init(controller: *mut Controller, machine: *platform::Platform, hartIndex: u32) { |
|
| 41 | + | assert machine.hartCount > 0 and machine.hartCount <= platform::MAX_HARTS; |
|
| 42 | + | assert hartIndex < machine.hartCount; |
|
| 43 | + | assert machine.irqCount > 0 and machine.irqCount < platform::MAX_IRQS; |
|
| 44 | + | set controller.base = machine.plic.base; |
|
| 45 | + | set controller.context = machine.harts[hartIndex].plicContext; |
|
| 46 | + | set controller.limit = machine.irqCount; |
|
| 47 | + | set controller.enabled = [0; platform::MAX_IRQS / 32]; |
|
| 48 | + | for source in 0..platform::MAX_IRQS { |
|
| 49 | + | set controller.sources[source] = Source { |
|
| 50 | + | object: domains::none(), claimed: false, published: domains::none(), |
|
| 51 | + | }; |
|
| 52 | + | } |
|
| 53 | + | for hart in 0..machine.hartCount { |
|
| 54 | + | let context = machine.harts[hart].plicContext as u64; |
|
| 55 | + | // Touch only implemented source words; reserved PLIC words may fault. |
|
| 56 | + | for word in 0..controller.limit / 32 + 1 { |
|
| 57 | + | mmio::write32(controller.base + 0x2000 + context * 0x80 + word as u64 * 4, 0); |
|
| 58 | + | } |
|
| 59 | + | // Priority one is eligible only on the selected service context. |
|
| 60 | + | let mut threshold: u32 = 1; |
|
| 61 | + | if hart == hartIndex { set threshold = 0; } |
|
| 62 | + | mmio::write32(controller.base + 0x200000 + context * 0x1000, threshold); |
|
| 63 | + | } |
|
| 64 | + | for source in 1..controller.limit + 1 { |
|
| 65 | + | mmio::write32(controller.base + source as u64 * 4, 1); |
|
| 66 | + | } |
|
| 67 | + | } |
|
| 68 | + | ||
| 69 | + | /// Bind an installed exclusive IRQ resource before its first poll; no MMIO occurs. |
|
| 70 | + | /// The mapping cannot be replaced, so resource-slot reuse never inherits a claim. |
|
| 71 | + | export fn register(controller: *mut Controller, source: u32, object: abi::Object) { |
|
| 72 | + | assert source > 0 and source <= controller.limit; |
|
| 73 | + | assert object.kind == abi::Kind::Interrupt and object.epoch <> 0; |
|
| 74 | + | assert controller.sources[source].object.kind == abi::Kind::Empty; |
|
| 75 | + | set controller.sources[source].object = object; |
|
| 76 | + | } |
|
| 77 | + | ||
| 78 | + | /// Change one selected-context enable bit without disturbing other source masks. |
|
| 79 | + | unsafe fn enable(controller: *mut Controller, source: u32, active: bool) { |
|
| 80 | + | let word = source / 32; |
|
| 81 | + | let bit: u32 = 1 << (source % 32); |
|
| 82 | + | let previous = controller.enabled[word]; |
|
| 83 | + | let mut next = previous & ~bit; |
|
| 84 | + | if active { set next = previous | bit; } |
|
| 85 | + | if next == previous { return; } |
|
| 86 | + | set controller.enabled[word] = next; |
|
| 87 | + | mmio::write32(controller.base + 0x2000 + controller.context as u64 * 0x80 + word as u64 * 4, next); |
|
| 88 | + | } |
|
| 89 | + | ||
| 90 | + | /// Complete a retained hardware claim after user service, while its route is enabled. |
|
| 91 | + | /// A PLIC can ignore completion through a disabled route. |
|
| 92 | + | unsafe fn complete(controller: *mut Controller, source: u32) { |
|
| 93 | + | enable(controller, source, true); |
|
| 94 | + | mmio::write32(controller.base + 0x200004 + controller.context as u64 * 0x1000, source); |
|
| 95 | + | } |
|
| 96 | + | ||
| 97 | + | /// Resolve the current exclusive holder, checking both resource and domain epochs. |
|
| 98 | + | fn target(controller: *Controller, kernel: *state::State, source: u32) -> abi::Object { |
|
| 99 | + | let object = controller.sources[source].object; |
|
| 100 | + | if object.kind <> abi::Kind::Interrupt |
|
| 101 | + | or not resources::live(kernel.resources, kernel.domains, object) { |
|
| 102 | + | return domains::none(); |
|
| 103 | + | } |
|
| 104 | + | let slot = &kernel.resources[object.index]; |
|
| 105 | + | let case resources::Value::Interrupt(irq) = slot.value else { return domains::none(); }; |
|
| 106 | + | if slot.references <> 1 or irq.number <> source or not domains::live(kernel.domains, irq.target) { |
|
| 107 | + | return domains::none(); |
|
| 108 | + | } |
|
| 109 | + | return irq.target; |
|
| 110 | + | } |
|
| 111 | + | ||
| 112 | + | /// Test the authoritative coalescing bit only after refresh accepted consumer head. |
|
| 113 | + | fn outstanding(queue: *events::Queue, source: u32) -> bool { |
|
| 114 | + | return queue.pending[source / 64] & (1 << (source as u64 % 64)) <> 0; |
|
| 115 | + | } |
|
| 116 | + | ||
| 117 | + | /// Reconcile retained claims with current ownership and validated consumer progress. |
|
| 118 | + | /// Never claims hardware or fabricates a notification merely because a source exists. |
|
| 119 | + | /// A transfer republishes retained work to the new incarnation, coalescing if needed. |
|
| 120 | + | /// Drop or domain death discards work and leaves the source masked. Invalid consumer |
|
| 121 | + | /// progress also leaves it masked, but preserves the claim for a later valid head. |
|
| 122 | + | export unsafe fn poll(controller: *mut Controller, kernel: *mut state::State) { |
|
| 123 | + | for source in 1..controller.limit + 1 { |
|
| 124 | + | let receiver = target(controller, kernel, source); |
|
| 125 | + | let pending = &mut controller.sources[source]; |
|
| 126 | + | if receiver.kind == abi::Kind::Empty { |
|
| 127 | + | if pending.claimed { complete(controller, source); } |
|
| 128 | + | set pending.claimed = false; |
|
| 129 | + | set pending.published = domains::none(); |
|
| 130 | + | enable(controller, source, false); |
|
| 131 | + | continue; |
|
| 132 | + | } |
|
| 133 | + | let queue = &mut kernel.domains[receiver.index].events; |
|
| 134 | + | try events::refresh(queue) catch { |
|
| 135 | + | enable(controller, source, false); |
|
| 136 | + | continue; |
|
| 137 | + | }; |
|
| 138 | + | if pending.claimed { |
|
| 139 | + | let previous = pending.published; |
|
| 140 | + | if previous.kind == abi::Kind::Domain and previous.index == receiver.index |
|
| 141 | + | and previous.epoch == receiver.epoch { |
|
| 142 | + | if not outstanding(queue, source) { |
|
| 143 | + | complete(controller, source); |
|
| 144 | + | set pending.claimed = false; |
|
| 145 | + | set pending.published = domains::none(); |
|
| 146 | + | } |
|
| 147 | + | } else { |
|
| 148 | + | // Keep the real claim across Busy and transfers until push succeeds. |
|
| 149 | + | try events::push(queue, events::Event { kind: 1, reserved: 0, code: source, value: 0 }, events::Class::Interrupt(source)) catch { |
|
| 150 | + | enable(controller, source, false); |
|
| 151 | + | continue; |
|
| 152 | + | }; |
|
| 153 | + | set pending.published = receiver; |
|
| 154 | + | } |
|
| 155 | + | } |
|
| 156 | + | enable(controller, source, not pending.claimed and not outstanding(queue, source) |
|
| 157 | + | and queue.ordinary < events::CAPACITY - events::CRITICAL); |
|
| 158 | + | } |
|
| 159 | + | } |
|
| 160 | + | ||
| 161 | + | /// Claim at most limit sources, masking each until its notification is consumed. |
|
| 162 | + | /// The gateway stays active while the driver services the device. Consumer |
|
| 163 | + | /// progress permits completion and rearming without a held-level interrupt loop. |
|
| 164 | + | /// Call under the kernel lock; poll also runs before every subsequent user dispatch. |
|
| 165 | + | export unsafe fn handle(controller: *mut Controller, kernel: *mut state::State) { |
|
| 166 | + | let claim = controller.base + 0x200004 + controller.context as u64 * 0x1000; |
|
| 167 | + | for count in 0..controller.limit { |
|
| 168 | + | let source = mmio::read32(claim); |
|
| 169 | + | if source == 0 { break; } |
|
| 170 | + | // Initialization disables every out-of-range source; never index by bad MMIO. |
|
| 171 | + | if source > controller.limit { |
|
| 172 | + | mmio::write32(claim, source); |
|
| 173 | + | break; |
|
| 174 | + | } |
|
| 175 | + | enable(controller, source, false); |
|
| 176 | + | set controller.sources[source].claimed = true; |
|
| 177 | + | set controller.sources[source].published = domains::none(); |
|
| 178 | + | } |
|
| 179 | + | poll(controller, kernel); |
|
| 180 | + | } |
kernel/core/mmio.rad
added
+7 -0
| 1 | + | //! Machine-only access to validated, naturally aligned controller registers. |
|
| 2 | + | ||
| 3 | + | /// Read one mapped 32-bit register, ordered before and after all memory and I/O. |
|
| 4 | + | export unsafe fn read32(address: u64) -> u32; |
|
| 5 | + | ||
| 6 | + | /// Write one mapped 32-bit register, ordered before and after all memory and I/O. |
|
| 7 | + | export unsafe fn write32(address: u64, value: u32); |
kernel/interrupt.rad
added
+148 -0
| 1 | + | //! PLIC claims, level-triggered UART service, and exclusive receiver transfer. |
|
| 2 | + | ||
| 3 | + | use core::abi; |
|
| 4 | + | use core::atomic; |
|
| 5 | + | use core::capabilities; |
|
| 6 | + | use core::clock; |
|
| 7 | + | use core::contexts; |
|
| 8 | + | use core::cpu; |
|
| 9 | + | use core::devices; |
|
| 10 | + | use core::domains; |
|
| 11 | + | use core::events; |
|
| 12 | + | use core::fdt; |
|
| 13 | + | use core::frames; |
|
| 14 | + | use core::handles; |
|
| 15 | + | use core::interrupts; |
|
| 16 | + | use core::memory; |
|
| 17 | + | use core::mmio; |
|
| 18 | + | use core::platform; |
|
| 19 | + | use core::resources; |
|
| 20 | + | use core::state; |
|
| 21 | + | ||
| 22 | + | /// One ordered byte access to an already checked device address. |
|
| 23 | + | unsafe fn read8(address: u64) -> u8; |
|
| 24 | + | /// One ordered byte write to an already checked device address. |
|
| 25 | + | unsafe fn write8(address: u64, value: u8); |
|
| 26 | + | /// Independent interrupt receivers. |
|
| 27 | + | static DOMAINS: [domains::Domain; 2] = undefined; |
|
| 28 | + | /// One UART device and two exclusive interrupt sources. |
|
| 29 | + | static OBJECTS: [resources::Slot; 3] = undefined; |
|
| 30 | + | /// No user context is needed for this physical notification check. |
|
| 31 | + | static CONTEXTS: [contexts::Context; 1] = undefined; |
|
| 32 | + | /// Empty RAM accounting; this check does not allocate user Pages. |
|
| 33 | + | static POOL: frames::Pool = undefined; |
|
| 34 | + | /// Empty-pool pins. |
|
| 35 | + | static PINS: [u16; 1] = undefined; |
|
| 36 | + | /// Empty-pool claims. |
|
| 37 | + | static ASSIGNED: [bool; 1] = undefined; |
|
| 38 | + | /// Empty-pool grants. |
|
| 39 | + | static GRANTS: [u64; 2] = undefined; |
|
| 40 | + | /// Private PLIC ownership and coalescing state. |
|
| 41 | + | static PLIC: interrupts::Controller = undefined; |
|
| 42 | + | /// Serialized control-plane state. |
|
| 43 | + | static KERNEL: state::State = undefined; |
|
| 44 | + | ||
| 45 | + | /// Install exclusive authority for one real PLIC source. |
|
| 46 | + | fn interrupt(source: u32, root: abi::Object) -> abi::Handle { |
|
| 47 | + | let object = try! resources::create(&mut OBJECTS[..], resources::Value::Interrupt(resources::Interrupt { number: source, target: root })); |
|
| 48 | + | resources::retain(&mut OBJECTS[..], object); |
|
| 49 | + | interrupts::register(&mut PLIC, source, object); |
|
| 50 | + | return handles::install(&mut DOMAINS[0].handles, try! handles::vacant(&DOMAINS[0].handles), object, abi::RIGHTS); |
|
| 51 | + | } |
|
| 52 | + | ||
| 53 | + | /// Observe exactly one IRQ event, then release it to the producer. |
|
| 54 | + | fn consume(receiver: u32, source: u32) { |
|
| 55 | + | let ring = &mut DOMAINS[receiver].events.ring; |
|
| 56 | + | let head = atomic::load(&ring.head); |
|
| 57 | + | assert atomic::load(&ring.tail) - head == 1; |
|
| 58 | + | let event = ring.data[head & (events::CAPACITY - 1)]; |
|
| 59 | + | assert event.kind == 1 and event.code == source and event.value == 0; |
|
| 60 | + | atomic::store(&mut ring.head, head + 1); |
|
| 61 | + | } |
|
| 62 | + | ||
| 63 | + | /// Read the physical enable register, not the controller's private shadow. |
|
| 64 | + | unsafe fn enabled(source: u32) -> bool { |
|
| 65 | + | let address = PLIC.base + 0x2000 + PLIC.context as u64 * 0x80 + (source / 32) as u64 * 4; |
|
| 66 | + | return mmio::read32(address) & (1 << (source % 32)) <> 0; |
|
| 67 | + | } |
|
| 68 | + | ||
| 69 | + | /// Wake from the next external interrupt and run the actual PLIC service path. |
|
| 70 | + | unsafe fn receive(frame: *mut cpu::Frame) { |
|
| 71 | + | clock::interrupts(2048); |
|
| 72 | + | cpu::idle(frame); |
|
| 73 | + | clock::interrupts(0); |
|
| 74 | + | assert frame.cause == 0x800000000000000b; |
|
| 75 | + | interrupts::handle(&mut PLIC, &mut KERNEL); |
|
| 76 | + | } |
|
| 77 | + | ||
| 78 | + | /// Exercise an injected edge at tick 4,000,000 and UART byte R at tick 8,000,000. |
|
| 79 | + | @default unsafe fn main(hart: u64, description: *u8) -> u32 { |
|
| 80 | + | assert hart == 0; |
|
| 81 | + | let size = try! fdt::word(@sliceOf(description, 40), 4); |
|
| 82 | + | assert size >= 40 and size <= fdt::MAX_BYTES; |
|
| 83 | + | let mut tree: fdt::Tree = undefined; |
|
| 84 | + | try! fdt::decode(@sliceOf(description, size), &mut tree); |
|
| 85 | + | let mut machine: platform::Platform = undefined; |
|
| 86 | + | try! platform::discover(&tree, &mut machine); |
|
| 87 | + | assert machine.harts[0].id == 0 and machine.devices[0].irq == 10; |
|
| 88 | + | interrupts::init(&mut PLIC, &machine, 0); |
|
| 89 | + | domains::init(&mut DOMAINS[..]); |
|
| 90 | + | resources::init(&mut OBJECTS[..]); |
|
| 91 | + | contexts::init(&mut CONTEXTS[0], 0); |
|
| 92 | + | let mut empty: platform::Platform = undefined; |
|
| 93 | + | set empty.memoryCount = 0; |
|
| 94 | + | set empty.reservedCount = 0; |
|
| 95 | + | try! frames::init(&mut POOL, &empty); |
|
| 96 | + | let mut ram: memory::Memory = undefined; |
|
| 97 | + | memory::init(&mut ram, &mut POOL, &mut PINS[..], &mut ASSIGNED[..], &mut GRANTS[..], 2); |
|
| 98 | + | set KERNEL = state::State { domains: &mut DOMAINS[..], resources: &mut OBJECTS[..], contexts: &mut CONTEXTS[..], memory: ram }; |
|
| 99 | + | let own = domains::root(&mut DOMAINS[..], 0); |
|
| 100 | + | let root = try! domains::resolve(&DOMAINS[..], 0, own, abi::CREATE); |
|
| 101 | + | let child = try! domains::create(&mut DOMAINS[..], root, 0); |
|
| 102 | + | let target = handles::install(&mut DOMAINS[0].handles, 2, child, abi::DOMAIN_RIGHTS); |
|
| 103 | + | let device = try! resources::create(&mut OBJECTS[..], resources::Value::Device(machine.devices[0].region)); |
|
| 104 | + | resources::retain(&mut OBJECTS[..], device); |
|
| 105 | + | let uart = handles::install(&mut DOMAINS[0].handles, 3, device, abi::READ | abi::WRITE); |
|
| 106 | + | let edge = interrupt(3, root); |
|
| 107 | + | let _uartIrq = interrupt(10, root); |
|
| 108 | + | write8(try! devices::access(&KERNEL, 0, uart, 1, 1, abi::WRITE), 1); |
|
| 109 | + | interrupts::poll(&mut PLIC, &mut KERNEL); |
|
| 110 | + | assert enabled(3) and enabled(10); |
|
| 111 | + | let mut frame: cpu::Frame = undefined; |
|
| 112 | + | cpu::init(&mut frame, 0, 0, 0, 0); |
|
| 113 | + | receive(&mut frame); |
|
| 114 | + | assert not enabled(3); |
|
| 115 | + | for i in 0..events::CAPACITY - events::CRITICAL { |
|
| 116 | + | try! events::push(&mut DOMAINS[1].events, events::Event { kind: 5, reserved: 0, code: i, value: 0 }, events::Class::Ordinary); |
|
| 117 | + | } |
|
| 118 | + | let moved = try! capabilities::transfer(&mut KERNEL, 0, edge, target, abi::RIGHTS as u64); |
|
| 119 | + | interrupts::poll(&mut PLIC, &mut KERNEL); |
|
| 120 | + | assert not enabled(3); |
|
| 121 | + | consume(0, 3); |
|
| 122 | + | atomic::store(&mut DOMAINS[1].events.ring.head, events::CAPACITY - events::CRITICAL); |
|
| 123 | + | interrupts::poll(&mut PLIC, &mut KERNEL); |
|
| 124 | + | interrupts::poll(&mut PLIC, &mut KERNEL); |
|
| 125 | + | consume(1, 3); |
|
| 126 | + | interrupts::poll(&mut PLIC, &mut KERNEL); |
|
| 127 | + | assert enabled(3); |
|
| 128 | + | try! capabilities::drop(&mut KERNEL, 1, moved); |
|
| 129 | + | interrupts::poll(&mut PLIC, &mut KERNEL); |
|
| 130 | + | assert not enabled(3); |
|
| 131 | + | ||
| 132 | + | receive(&mut frame); |
|
| 133 | + | assert not enabled(10); |
|
| 134 | + | for i in 0..4 { interrupts::poll(&mut PLIC, &mut KERNEL); } |
|
| 135 | + | let byte = read8(try! devices::access(&KERNEL, 0, uart, 0, 1, abi::READ)); |
|
| 136 | + | assert byte == 82; |
|
| 137 | + | write8(try! devices::access(&KERNEL, 0, uart, 0, 1, abi::WRITE), byte); |
|
| 138 | + | consume(0, 10); |
|
| 139 | + | interrupts::poll(&mut PLIC, &mut KERNEL); |
|
| 140 | + | assert enabled(10); |
|
| 141 | + | clock::arm(machine.clint.base, machine.harts[0].clintIndex, clock::read(machine.clint.base) + 1000); |
|
| 142 | + | clock::interrupts(2176); |
|
| 143 | + | cpu::idle(&mut frame); |
|
| 144 | + | clock::interrupts(0); |
|
| 145 | + | clock::arm(machine.clint.base, machine.harts[0].clintIndex, 0xffffffffffffffff); |
|
| 146 | + | assert frame.cause == 0x8000000000000007; |
|
| 147 | + | return 0; |
|
| 148 | + | } |