godspeed_sdk/dma.rs
1// SPDX-License-Identifier: Apache-2.0
2//! DMA arena access for userspace driver services (§12, §18).
3//!
4//! Part of the SDK's audited hardware/ABI layer (§18.1), alongside `mmio.rs` and
5//! the syscall ABI. A [`Dma`] wraps a kernel-granted, physically-contiguous
6//! arena: the driver builds device structures in it (via the read/write
7//! helpers) and hands the controller physical addresses (via [`Dma::phys_at`]).
8//! Driver services use this safe wrapper and never write `unsafe` themselves;
9//! `Dma` is only constructable inside this crate, from a kernel-granted region.
10
11/// A physically-contiguous DMA arena granted to a driver (e.g. via
12/// [`crate::ServiceContext::dma_region`]). The CPU accesses it through `base`, the device through
13/// `phys`. Both views cover the same `len` bytes one-to-one.
14///
15/// SEC-28 (SMP-port contract, `kernel/src/arch/CLAUDE.md`): this wrapper does NO cache maintenance.
16/// That is correct only because of how each port maps the arena (`DMA_ARENA_UNCACHED`): x86 maps it
17/// cacheable, its DMA being coherent; arm32 and aarch64, whose DMA is not, map it UNCACHED, which is
18/// their answer to SEC-28; riscv64 maps it cacheable. A port whose DMA is not coherent must map it
19/// uncached too, or add maintenance here - else the CPU and the device can see stale copies.
20#[derive(Clone, Copy)]
21pub struct Dma {
22 base: *mut u8,
23 phys: u64,
24 len: usize,
25}
26
27impl Dma {
28 /// Crate-internal: only the SDK constructs a `Dma`, from a kernel-granted
29 /// region, which is what makes the volatile accesses below sound.
30 pub(crate) fn new(base: *mut u8, phys: u64, len: usize) -> Self {
31 Self { base, phys, len }
32 }
33
34 /// Physical base address - program this (plus offsets) into the controller.
35 #[inline]
36 pub fn phys_base(&self) -> u64 {
37 self.phys
38 }
39
40 /// Length of the arena in bytes.
41 #[inline]
42 pub fn len(&self) -> usize {
43 self.len
44 }
45
46 /// Bounds-check an `off..off+size` access against the arena length (SEC-4). A driver bug that
47 /// lets a device-supplied value drive `off` past the arena is caught here as a loud panic
48 /// (killing only the one driver) instead of a silent out-of-arena CPU access - which, for an
49 /// IOMMU-passthrough driver, is a write ANYWHERE in RAM (§26.7). `checked_add` so a wrapping
50 /// `off` cannot slip past. The `unsafe` accessors below are only actually memory-safe with this.
51 #[inline]
52 fn check(&self, off: usize, size: usize) {
53 assert!(
54 off.checked_add(size).map_or(false, |end| end <= self.len),
55 "Dma access out of arena bounds",
56 );
57 }
58
59 /// Physical address of byte offset `off` within the arena.
60 #[inline]
61 pub fn phys_at(&self, off: usize) -> u64 {
62 self.check(off, 0);
63 self.phys + off as u64
64 }
65
66 /// Zero the whole arena.
67 ///
68 /// **Deliberately not `write_bytes`.** That lowers to `memset`, and a `memset` is free to reach
69 /// for whatever the ISA makes fastest - unaligned stores, cache-line zeroing hints (AArch64's
70 /// `DC ZVA`), non-temporal stores. Every one of those is legal on the Normal cacheable memory
71 /// x86 maps this arena as, and every one of them is a **fault** on the Device memory a
72 /// non-coherent port maps it as instead (`DMA_ARENA_UNCACHED`, the SEC-28 answer): Device-nGnRnE
73 /// forbids unaligned access outright and cache maintenance by address on it is not meaningful.
74 ///
75 /// So the clear is an explicit aligned 64-bit volatile loop, which is the one shape that is
76 /// correct on both. The arena is page-based (a whole number of 4 KiB frames at a page-aligned
77 /// VA), so the fast path always applies; the byte tail exists only so this function is total
78 /// rather than silently leaving a remainder, and it cannot run on a real grant.
79 ///
80 /// `volatile` matters as much as the alignment: an ordinary store loop over Device memory is
81 /// something the optimiser may merge back into a `memset` call, which would reintroduce exactly
82 /// what this avoids.
83 pub fn zero(&self) {
84 let words = self.len / 8;
85 for i in 0..words {
86 // SAFETY: base..base+len is the kernel-granted mapped arena (Dma is only constructed
87 // from one) and is page-aligned, so `base + i*8` is 8-byte aligned and in bounds for
88 // every i < len/8.
89 unsafe { core::ptr::write_volatile(self.base.add(i * 8) as *mut u64, 0) }
90 }
91 for off in words * 8..self.len {
92 // SAFETY: off < len, so this is in bounds; a byte store needs no alignment.
93 unsafe { core::ptr::write_volatile(self.base.add(off), 0) }
94 }
95 }
96
97 // ---------------------------------------------------------------------------------------------
98 // ALIGNMENT
99 //
100 // The multi-byte accessors below compose from BYTE accesses whenever the offset is not naturally
101 // aligned. That is not defensiveness; on a non-coherent arch this arena is mapped **Device**
102 // memory (AArch64 Device-nGnRnE, §SEC-28 - the mapping is what removes the need for cache
103 // maintenance), and Device memory does not permit unaligned access AT ALL. An unaligned 16- or
104 // 32-bit load there is not slow, it is an ALIGNMENT FAULT.
105 //
106 // The doc comments used to say "2-byte aligned" / "4-byte aligned" and leave it at that, which
107 // put the obligation on every caller. Drivers cannot honour it: parsing a USB configuration
108 // descriptor means walking to offsets the DEVICE chose, so `wMaxPacketSize` lands at an odd
109 // address whenever the descriptors before it summed to an odd length. The Pi 4 found this the
110 // first time the xhci service reached a real hub:
111 //
112 // ESR_EL1 = 0x92000021 (data abort, DFSC 0b100001 = alignment fault)
113 // FAR_EL1 = 0x200005003 <- arena base + 0x5003, an ODD offset
114 // task = xhci
115 //
116 // x86 hides this completely: unaligned loads just work there, so every such call site is correct
117 // on the arch it was written for and faults on the arch it was ported to. Putting the fix HERE
118 // rather than at the call sites means it cannot be forgotten by the next driver.
119 //
120 // Aligned offsets keep the single wide volatile access, so nothing on the fast path pays for it.
121
122 /// Read an 8-bit value at byte offset `off` (`off < len`).
123 #[inline]
124 pub fn read8(&self, off: usize) -> u8 {
125 self.check(off, 1);
126 // SAFETY: base is a valid kernel-granted mapping; check() bounded off in range.
127 unsafe { core::ptr::read_volatile(self.base.add(off)) }
128 }
129
130 /// Read a 16-bit value at byte offset `off` (`off + 2 <= len`; any alignment - see ALIGNMENT above).
131 #[inline]
132 pub fn read16(&self, off: usize) -> u16 {
133 self.check(off, 2);
134 if off & 1 != 0 {
135 return u16::from_le_bytes([self.read8(off), self.read8(off + 1)]);
136 }
137 // SAFETY: as read8; aligned 16-bit access in range.
138 unsafe { core::ptr::read_volatile(self.base.add(off) as *const u16) }
139 }
140
141 /// Read a 32-bit value at byte offset `off` (`off + 4 <= len`; any alignment - see ALIGNMENT above).
142 #[inline]
143 pub fn read32(&self, off: usize) -> u32 {
144 self.check(off, 4);
145 if off & 3 != 0 {
146 return u32::from_le_bytes([
147 self.read8(off), self.read8(off + 1), self.read8(off + 2), self.read8(off + 3),
148 ]);
149 }
150 // SAFETY: base is a valid kernel-granted mapping; check() bounded off in range.
151 unsafe { core::ptr::read_volatile(self.base.add(off) as *const u32) }
152 }
153
154 /// Write a 32-bit value at byte offset `off` (`off + 4 <= len`; any alignment - see ALIGNMENT above).
155 #[inline]
156 pub fn write32(&self, off: usize, val: u32) {
157 self.check(off, 4);
158 if off & 3 != 0 {
159 let b = val.to_le_bytes();
160 for (k, v) in b.iter().enumerate() { self.write8(off + k, *v); }
161 return;
162 }
163 // SAFETY: as read32; volatile so the device observes ordered writes.
164 unsafe { core::ptr::write_volatile(self.base.add(off) as *mut u32, val) }
165 }
166
167 /// Write an 8-bit value at byte offset `off` (`off < len`). For byte-granular
168 /// device structures (e.g. an e1000 TX descriptor's command byte, or frame bytes).
169 #[inline]
170 pub fn write8(&self, off: usize, val: u8) {
171 self.check(off, 1);
172 // SAFETY: as read8; volatile so the device observes ordered writes.
173 unsafe { core::ptr::write_volatile(self.base.add(off), val) }
174 }
175
176 /// Write a 16-bit value at byte offset `off` (`off + 2 <= len`; any alignment - see ALIGNMENT above).
177 #[inline]
178 pub fn write16(&self, off: usize, val: u16) {
179 self.check(off, 2);
180 if off & 1 != 0 {
181 let b = val.to_le_bytes();
182 self.write8(off, b[0]);
183 self.write8(off + 1, b[1]);
184 return;
185 }
186 // SAFETY: as read16; volatile 16-bit write in range.
187 unsafe { core::ptr::write_volatile(self.base.add(off) as *mut u16, val) }
188 }
189
190 /// Read a 64-bit value at byte offset `off` (`off + 8 <= len`; any alignment - see ALIGNMENT above).
191 #[inline]
192 pub fn read64(&self, off: usize) -> u64 {
193 self.check(off, 8);
194 if off & 7 != 0 {
195 return (self.read32(off) as u64) | ((self.read32(off + 4) as u64) << 32);
196 }
197 // SAFETY: as read32; 64-bit aligned access in range.
198 unsafe { core::ptr::read_volatile(self.base.add(off) as *const u64) }
199 }
200
201 /// Write a 64-bit value at byte offset `off` (`off + 8 <= len`; any alignment - see ALIGNMENT above).
202 #[inline]
203 pub fn write64(&self, off: usize, val: u64) {
204 self.check(off, 8);
205 if off & 7 != 0 {
206 self.write32(off, val as u32);
207 self.write32(off + 4, (val >> 32) as u32);
208 return;
209 }
210 // SAFETY: as read32; 64-bit aligned access in range.
211 unsafe { core::ptr::write_volatile(self.base.add(off) as *mut u64, val) }
212 }
213}