rigless 0.2.1

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The ARMv7-M machine, what a target of that core family is.
//!
//! This is what an ARMv7-M target says about itself: a typed field for each of its properties, and
//! one each for its variant, its instruction rate, and the region an SoC boots from. A [`Config`]
//! is that [`Core`] with the windows of the processor's own bus the runtime answers, the units
//! the part has, and what the architecture leaves to the implementation, which the family's
//! [`processor`](crate::cpu::armv7m::processor) takes as it is.
//! [`Config::spec`] makes the spec whose constructor hands it over. A machine module fills the
//! fields with its SoC's numbers and calls [`Core::spec`] or [`Core::spec_with_parts`].

use std::num::{NonZeroU32, NonZeroU64};
use std::sync::Arc;

use crate::cpu::armv7m::Window;
use crate::elf::Machine;
use crate::emulator::{Alias, Boot, CoreSpec};

/// The core this module describes, as the family's refusals name it.
#[cfg(not(emulator_absent))]
pub(crate) const CORE: &str = "the ARMv7-M core";

/// The machine of the ELF files the core runs: `EM_ARM`, which is 40, in 32-bit files, as the
/// toolchain writes every Thumb firmware's header.
const ELF: Machine = Machine::new(40, false);

/// Where the core reads the address it starts from.
const RESET_VECTOR: u64 = 0;
/// The exceptions every core has before its first interrupt line, reset among them.
const SYSTEM_EXCEPTIONS: u64 = 16;
/// The bytes of one entry in the vector table.
const VECTOR_BYTES: u64 = 4;
/// The least a vector table is aligned to, however few exceptions it has.
const TABLE_ALIGNMENT: u64 = 128;

// ceiling: ARMv7-M cores only; upgrade when the family models v8-M's system control space.
/// A core the machine runs, by the name it takes for it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Variant {
    /// Cortex-M3.
    M3,
    /// Cortex-M4.
    M4,
    /// Cortex-M7.
    M7,
}

impl Variant {
    /// Every core the machine accepts, in the order it lists them.
    pub const ALL: [Self; 3] = [Self::M3, Self::M4, Self::M7];

    /// The name the machine takes for this core.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::M3 => "cortex-m3",
            Self::M4 => "cortex-m4",
            Self::M7 => "cortex-m7",
        }
    }

    // ceiling: the Cortex-M3's and M7's options below come from no manual cited here; upgrade when
    // their technical reference manuals are cited.
    /// Whether the core has the DSP extension, which makes it ARMv7E-M, DDI 0403E.b A1.3. The M4
    /// has it, 100166 section 3.3.2.
    #[must_use]
    pub(crate) const fn dsp(self) -> bool {
        matches!(self, Self::M4 | Self::M7)
    }

    /// Every floating-point unit a part on this core may have. The M4's is FPv4-SP, and it's
    /// optional, 100166 sections 1.4 and 7.1.
    #[must_use]
    pub(crate) const fn fpus(self) -> &'static [Fpu] {
        match self {
            Self::M3 => &[Fpu::None],
            Self::M4 => &[Fpu::None, Fpu::FpV4Sp],
            Self::M7 => &[Fpu::None, Fpu::FpV5Sp, Fpu::FpV5Dp],
        }
    }

    /// Every MPU a part on this core may have. The M4's has eight regions, and it's optional,
    /// 100166 sections 1.4 and 2.2.
    #[must_use]
    pub(crate) const fn mpus(self) -> &'static [MpuRegions] {
        match self {
            Self::M3 | Self::M4 => &[MpuRegions::None, MpuRegions::Eight],
            Self::M7 => &[MpuRegions::None, MpuRegions::Eight, MpuRegions::Sixteen],
        }
    }

    /// The unit a board that doesn't name one gets: the fullest the core may have, so a board from
    /// before units were named keeps one. That's the Cortex-M4F's on an M4 and double-precision
    /// `FPv5` on an M7. An M3 can't have one.
    #[must_use]
    pub(crate) const fn default_fpu(self) -> Fpu {
        match self {
            Self::M3 => Fpu::None,
            Self::M4 => Fpu::FpV4Sp,
            Self::M7 => Fpu::FpV5Dp,
        }
    }
}

/// The floating-point unit a part has, DDI 0403E.b A1.3 and Table A1-1.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Fpu {
    /// No floating-point extension, so CP10 and CP11 don't exist.
    None,
    /// FPv4-SP, single precision only: the Cortex-M4F's.
    FpV4Sp,
    /// `FPv5`, single precision only.
    FpV5Sp,
    /// `FPv5` with double precision.
    FpV5Dp,
}

impl Fpu {
    /// The name the machine takes for this unit.
    #[must_use]
    pub(crate) const fn name(self) -> &'static str {
        match self {
            Self::None => "none",
            Self::FpV4Sp => "fpv4-sp",
            Self::FpV5Sp => "fpv5-sp",
            Self::FpV5Dp => "fpv5-dp",
        }
    }
}

impl std::fmt::Display for Fpu {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(self.name())
    }
}

/// How many regions a part's MPU has, which `MPU_TYPE.DREGION` reads, DDI 0403E.b B3.5.5.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum MpuRegions {
    /// No MPU at all.
    None = 0,
    /// Eight regions: the Cortex-M4's, and what a board that doesn't say takes.
    #[default]
    Eight = 8,
    /// Sixteen regions.
    Sixteen = 16,
}

impl MpuRegions {
    /// The regions, as `MPU_TYPE.DREGION` reads them.
    #[must_use]
    pub(crate) const fn count(self) -> u8 {
        self as u8
    }
}

/// `CPUID` of a Cortex-M4 r0p1, 100166 section 4.3.
// ceiling: an M3, and an M7 whose machine gives none, answer the M4's CPUID; upgrade when the
// Cortex-M3's and Cortex-M7's technical reference manuals are cited.
const CORTEX_M4_CPUID: u32 = 0x410F_C241;

/// The caches a part's core has, as its cache identification registers say, DDI 0403E.b B4.8.
///
/// The words are the implementation's. The core answers them and nothing more: it caches nothing,
/// so a maintenance operation does nothing and nothing it runs is ever stale.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Caches {
    /// `CLIDR`, the caches at each level, B4.8.1.
    pub clidr: u32,
    /// `CTR`, the smallest line of each kind of cache, B4.8.4.
    pub ctr: u32,
    /// Each cache's `CCSIDR`, B4.8.2, at the index that's the value of `CSSELR` selecting it,
    /// B4.8.3: the level 1 data or unified cache's, then the level 1 instruction cache's, then
    /// level 2's in the same order. One not given reads as zero.
    pub ccsidr: &'static [u32],
}

/// Whether the core predicts branches, which `CCR.BP` says, DDI 0403E.b B3.2.8.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum BranchPrediction {
    /// The core doesn't predict branches, so `CCR.BP` is RAZ/WI.
    #[default]
    Unsupported,
    /// Always, since it can't be turned off, so `CCR.BP` is RAO/WI.
    Always,
}

/// A register a part's core has in the system control space's IMPLEMENTATION DEFINED range,
/// `0xE000EF90` to `0xE000EFCC`, DDI 0403E.b Table B3-6.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ImplementationRegister {
    /// Its offset into the system control space, which starts at `0xE000E000`: a word from
    /// `0xF90` to `0xFCC`.
    pub offset: u64,
    /// What it holds after a reset.
    pub reset: u32,
    /// The bits a write sets. The rest keep what they hold.
    pub writable: u32,
}

/// The ARMv7-M core as a machine module configures it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Core {
    /// Which core.
    pub variant: Variant,
    /// The core clock, which drives the timers inside the core.
    pub clock_hz: NonZeroU64,
    /// The interrupt lines the controller has, each one a line the runtime can raise.
    pub interrupt_lines: NonZeroU32,
    /// The priority bits the interrupt controller implements, which the part's manual gives in its
    /// section on the controller. A firmware that reads back a priority it wrote sees this many bits
    /// of it. Zero leaves the count to the core's own default, as the machine spells it.
    pub priority_bits: u8,
    /// The reference clock of the core's own timer is the core clock divided by this. The part's
    /// manual gives the divider in its section on that timer's clock source, and eight is common. A
    /// wrong one makes a kernel's tick built on that timer run fast or slow by the same ratio.
    pub refclk_divider: NonZeroU32,
    /// How many instructions the core retires in a second of virtual time. It's a choice: every
    /// target this crate carries retires 250 million, four nanoseconds each, which is about what a
    /// core at a few hundred megahertz does. The part's own clock, one instruction a cycle, is the
    /// other natural pick. A lower rate makes the emulated core slower against its own peripherals
    /// and the plant, a higher one faster, and nothing else depends on it.
    pub instructions_per_second: NonZeroU64,
    /// The memory region the SoC's boot configuration puts where the core fetches its reset vector.
    ///
    /// The core answers at address zero with that region's bytes, so a vector table linked at the
    /// start of the flash region is what the core finds at its reset vector. A name the MCU's memory
    /// map does not have is refused when the session opens, before the core fetches from it. A
    /// part whose vector table is elsewhere at reset, [`Config::vector_table`], fetches it there
    /// instead, so this region answers only at its own addresses, and a name the memory map lacks
    /// is refused all the same.
    pub boot: &'static str,
}

impl Core {
    /// The description of a target's core on this machine, answering for `ppb` of the processor's
    /// own bus, with the variant's fullest floating-point unit and eight MPU regions.
    ///
    /// The windows are the MCU's, from [`crate::cpu::armv7m::windows`], rather than anything this
    /// module decides: the core answers the processor's peripheral bus itself unless told which
    /// stretches of it the runtime has a model behind, and the runtime's own bus is what knows.
    #[must_use]
    pub fn spec(self, ppb: &[Window]) -> CoreSpec {
        Config::new(self, ppb).spec()
    }

    /// [`Core::spec`] for a part whose manual names its floating-point unit and its MPU. One the
    /// variant can't have is refused when the run starts. A board that calls [`Core::spec`] gets an
    /// MPU of eight regions, and the Cortex-M4F's unit on an M4.
    #[must_use]
    pub fn spec_with_parts(self, ppb: &[Window], fpu: Fpu, mpu_regions: MpuRegions) -> CoreSpec {
        let mut config = Config::new(self, ppb);
        config.fpu = fpu;
        config.mpu_regions = mpu_regions;
        config.spec()
    }

    /// What a vector table's address is a multiple of: the table's size rounded up to a power of two,
    /// and never less than 128 bytes (ARMv7-M ARM B1.5.3).
    pub(crate) fn table_alignment(self) -> NonZeroU64 {
        let size = (SYSTEM_EXCEPTIONS + u64::from(self.interrupt_lines.get())) * VECTOR_BYTES;
        NonZeroU64::new(size.next_power_of_two().max(TABLE_ALIGNMENT))
            .expect("a power of two is above zero")
    }
}

/// What the family makes a core from: the machine's [`Core`], the windows of the processor's own
/// bus the runtime answers, the units the part has, and what DDI 0403E.b leaves to the
/// implementation.
///
/// [`Config::new`] makes one with the variant's fullest floating-point unit, eight MPU regions,
/// the Cortex-M4's `CPUID`, no caches, no branch prediction, no registers of the implementation's
/// own, and the vector table at zero, and each field is yours to set after.
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Config {
    /// The core.
    pub core: Core,
    /// The stretches of the processor's own bus the runtime has a model behind.
    pub ppb: Vec<Window>,
    /// The floating-point unit.
    pub fpu: Fpu,
    /// The MPU's regions.
    pub mpu_regions: MpuRegions,
    /// What `CPUID` reads, which says what core this is and its revision, DDI 0403E.b B3.2.3.
    pub cpuid: u32,
    /// The caches, or `None` for a core without, whose cache identification registers read as zero
    /// and whose `CCR.IC` and `CCR.DC` are RAZ/WI, B3.2.8.
    pub caches: Option<Caches>,
    /// Whether the core predicts branches.
    pub branch_prediction: BranchPrediction,
    /// The registers the core has in the system control space's IMPLEMENTATION DEFINED range. One
    /// outside that range is refused when the run starts.
    pub implementation_registers: Vec<ImplementationRegister>,
    /// Where the vector table is at reset, for a part whose configuration inputs set `VTOR`'s
    /// reset value, DDI 0403E.b B3.2.5. `VTOR` reads it from reset on, and the core takes its stack
    /// pointer and reset vector from there, B1.5.5. Address zero is then whatever the memory map
    /// has there. `None` is zero, where [`Core::boot`] also answers. An address that isn't a
    /// multiple of the table's alignment, B1.5.3, is refused when the run starts.
    pub vector_table: Option<u32>,
}

impl Config {
    /// `core` answering for `ppb` of the processor's own bus, with the fullest floating-point unit
    /// its variant may have, an MPU of eight regions, and nothing else the implementation chooses.
    #[must_use]
    pub fn new(core: Core, ppb: &[Window]) -> Self {
        Self {
            core,
            ppb: ppb.to_vec(),
            fpu: core.variant.default_fpu(),
            mpu_regions: MpuRegions::default(),
            cpuid: CORTEX_M4_CPUID,
            caches: None,
            branch_prediction: BranchPrediction::Unsupported,
            implementation_registers: Vec::new(),
            vector_table: None,
        }
    }

    /// The description of a target's core made as this configuration says, which hands it to
    /// the family's [`processor`](crate::cpu::armv7m::processor).
    #[must_use]
    pub fn spec(self) -> CoreSpec {
        let core = self.core;
        let vector_table = self.vector_table;
        let mut spec = CoreSpec::new(
            Arc::new(move |endian| crate::cpu::armv7m::processor(&self, endian)),
            core.instructions_per_second,
            Boot::new(
                vector_table.map_or(RESET_VECTOR, u64::from),
                core.table_alignment(),
            ),
        );
        spec.boot.region = Some(core.boot);
        // A core whose vector table resets to zero fetches the reset vector there, so the boot
        // region answers there too.
        if vector_table.is_none() {
            spec.aliases = vec![Alias {
                region: core.boot,
                at: RESET_VECTOR,
            }];
        }
        spec.elf = Some(ELF);
        spec.code_address = crate::cpu::armv7m::code_address;
        spec
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn core() -> Core {
        Core {
            variant: Variant::M4,
            clock_hz: NonZeroU64::new(168_000_000).unwrap(),
            interrupt_lines: NonZeroU32::new(91).unwrap(),
            priority_bits: 4,
            refclk_divider: NonZeroU32::new(8).unwrap(),
            instructions_per_second: NonZeroU64::new(250_000_000).unwrap(),
            boot: "flash",
        }
    }

    const WINDOWS: [Window; 2] = [
        Window {
            base: 0xE000_0000,
            size: 0x1000,
        },
        Window {
            base: 0xE000_1000,
            size: 0x8,
        },
    ];

    #[test]
    fn a_config_takes_the_variants_fullest_unit_and_eight_regions_unless_a_part_names_them() {
        let config = Config::new(core(), &WINDOWS);
        assert_eq!(
            (config.fpu, config.mpu_regions, &config.ppb[..]),
            (Fpu::FpV4Sp, MpuRegions::Eight, &WINDOWS[..])
        );
        let m3 = Config::new(
            Core {
                variant: Variant::M3,
                ..core()
            },
            &[],
        );
        assert_eq!(m3.fpu, Fpu::None);
    }

    #[test]
    fn a_vector_table_is_aligned_to_its_size_and_never_less_than_128_bytes() {
        let alignment = |lines| {
            Core {
                interrupt_lines: NonZeroU32::new(lines).unwrap(),
                ..core()
            }
            .spec(&WINDOWS)
            .boot
            .alignment
            .get()
        };
        assert_eq!(alignment(91), 512, "107 entries are 428 bytes");
        assert_eq!(alignment(1), 128, "17 entries are 68 bytes");
        assert_eq!(alignment(16), 128, "32 entries are 128 bytes");
        assert_eq!(alignment(17), 256);
    }

    #[test]
    fn the_boot_region_is_the_one_the_core_also_finds_at_its_reset_vector() {
        let spec = core().spec(&WINDOWS);
        assert_eq!(
            spec.aliases,
            [Alias {
                region: "flash",
                at: 0
            }]
        );
        assert_eq!(spec.boot.reset, 0);
        let from_system = Core {
            boot: "system",
            ..core()
        };
        assert_eq!(
            from_system.spec(&WINDOWS).aliases,
            [Alias {
                region: "system",
                at: 0
            }],
            "the SoC's boot configuration says which region, not the family"
        );
    }

    /// A vector table the machine puts elsewhere at reset, DDI 0403E.b B3.2.5, is where the core
    /// boots, aligned as any table is, B1.5.3, and the boot region answers nowhere but its own
    /// addresses, so zero is whatever the memory map has there.
    #[test]
    fn a_vector_table_put_elsewhere_at_reset_is_where_the_core_boots_with_nothing_at_zero() {
        let mut config = Config::new(core(), &WINDOWS);
        config.vector_table = Some(0x0010_0000);
        let spec = config.spec();
        assert_eq!(
            (spec.boot.reset, spec.boot.alignment.get(), spec.aliases),
            (0x0010_0000, 512, Vec::new())
        );
        assert_eq!(
            spec.boot.region,
            Some("flash"),
            "the region, which the session still checks"
        );
    }
}