rigless 0.1.0

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
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The Instrumentation Trace Macrocell, ARM DDI 0403E.b C1.7, as the Cortex-M4 has it, 100166
//! chapter 10: the stimulus ports software writes trace to, their enables and their privilege, the
//! control register, and the identification registers. A write that goes out is D4.2.8's
//! Instrumentation packet, which the processor hands its bus.
//!
//! **What is modelled.**
//!
//! | Behaviour | Source | Test |
//! |---|---|---|
//! | 32 stimulus ports, `ITM_TER0` and four `ITM_TPR` bits, as the Cortex-M4 implements them, and every other stimulus port and enable RAZ/WI | 100166 table 10-1 and 10.3, C1.7.4, C1.7.5 | `the_cortex_m4s_ports_and_no_more_are_there` |
//! | A byte, halfword or word write to an enabled port, with `ITM_TCR.ITMENA` and `DEMCR.TRCENA` set, goes out as an Instrumentation packet of that size, its payload least significant byte first | C1.7.1, C1.7.3, C1.6.5, D4.2.7, D4.2.8 | `a_stimulus_write_goes_out_as_an_instrumentation_packet_only_when_enabled`, `family`'s `the_firmware_reaches_the_itm_as_c1_7_says` |
//! | A port reads `FIFOREADY` set when it and the ITM are on, since the stream takes every packet at once, and clear, the buffer full, when either is off | C1.7.1, C1.7.3 | `a_stimulus_port_reads_fifo_ready` |
//! | Unprivileged software writes a port and its enable bit only where `ITM_TPR` leaves the port's group unprivileged, a clear bit, and never `ITM_TPR` or `ITM_TCR`. 100166 section 10.2's note says the opposite, that unprivileged code writes where the bit is set. C1.7.5 is taken, since it gives what each value of the field means where the note only sums up access, and 100166 section 10.3 gives the field no values of its own | C1.7.1, C1.7.5 | `an_unprivileged_write_tpr_refuses_is_dropped`, `family`'s `the_firmware_reaches_the_itm_as_c1_7_says` |
//! | With `DEMCR.TRCENA` clear the registers read zero, one of the UNKNOWN values C1.6.5 allows, and ignore writes, which it leaves IMPLEMENTATION DEFINED | C1.6.5, C1.7.3 | `a_stimulus_write_goes_out_as_an_instrumentation_packet_only_when_enabled` |
//! | `ITM_TCR`'s fields read back as written but `BUSY`, which reads 0 since nothing waits to drain | C1.7.6 | `the_cortex_m4s_ports_and_no_more_are_there` |
//! | The peripheral and component identification registers | 100166 table 10-1 | `the_cortex_m4s_ports_and_no_more_are_there` |
//! | Reset leaves the ITM as it was, since only a power-on reset clears it | C1.7.2 table C1-11 | `family`'s `a_reset_leaves_the_itm_as_it_was` |
//!
//! **What it leaves out, and why.**
//!
//! | Behaviour | Source | Reason |
//! |---|---|---|
//! | Local and global timestamp packets | C1.7.1, D4.2.4, D4.2.5 | A timestamp says when on the trace clock a packet left, which the stream has no wire to time. `TSENA` and `GTSFREQ` are kept and do nothing |
//! | Synchronization packets | C1.7.1, D4.2.1 | They recover byte alignment on a serial trace port, and the stream is bytes already. `SYNCENA` is kept and does nothing |
//! | Overflow packets | C1.7.1, D4.2.3 | The stream takes every packet at once, so the output buffer is never full |
//! | The DWT's hardware source packets and `TXENA` | C1.7.1, D4.2.9 | The DWT this crate models has no event sources that send them |
//! | Identification registers on a core other than the Cortex-M4 | 100166 table 10-1 | The Cortex-M4's are the ones on the shelf. Another core's read as zero |
//! | A privileged write while `ITMENA` is clear | C1.7.1 | C1.7.1 says the ITM ignores a privileged write to any of its registers then, which would leave no way to set `ITMENA`. It's read here as a write to a stimulus port, which is dropped |

use serde::{Deserialize, Serialize};

use super::registers::{
    get, instrumentation, is_set, itm_stim, itm_tcr, itm_tpr, mask, privmask, set, stimena,
};
use super::translate::Size;

/// Where each register is in the ITM's 4 KiB, C1.7.2 table C1-11.
const STIM: std::ops::Range<u32> = 0x000..0x400;
const TER: std::ops::Range<u32> = 0xE00..0xE20;
const TPR: u32 = 0xE40;
const TCR: u32 = 0xE80;

/// The stimulus ports the Cortex-M4 has, 100166 table 10-1.
const PORTS: u32 = 32;

/// The ports one `ITM_TPR` bit covers, C1.7.5.
const PORTS_A_PRIVILEGE_BIT: u32 = 8;

/// The peripheral and component identification registers from `0xFD0`, 100166 table 10-1.
const IDENTIFICATION: [u32; 12] = [
    0x04, 0x00, 0x00, 0x00, 0x01, 0xB0, 0x3B, 0x00, 0x0D, 0xE0, 0x05, 0xB1,
];
const IDENTIFICATION_AT: u32 = 0xFD0;

/// D4.2.7's payload size code, `SS`, for a write of `size`.
const fn payload_size(size: Size) -> u32 {
    match size {
        Size::Byte => 0b01,
        Size::Halfword => 0b10,
        Size::Word => 0b11,
    }
}

/// The ITM's registers.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Itm {
    /// `ITM_TER0`, a bit a port.
    enabled: u32,
    /// `ITM_TPR`'s `PRIVMASK`, a bit for each eight ports that only privileged software writes.
    privileged_only: u32,
    /// `ITM_TCR`.
    control: u32,
}

/// Who writes the ITM.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Writer {
    /// Privileged software, or a debugger, C1.7.1.
    pub(crate) privileged: bool,
    /// `DEMCR.TRCENA`, C1.6.5.
    pub(crate) enabled: bool,
}

impl Itm {
    /// The word at `offset`, with `DEMCR.TRCENA` `enabled`, but for the identification
    /// registers, which [`Itm::identification`] answers.
    pub(crate) fn read(&self, offset: u32, enabled: bool) -> u32 {
        if !enabled {
            return 0;
        }
        match offset {
            _ if STIM.contains(&offset) => {
                set(itm_stim::FIFOREADY, 0, u32::from(self.port_on(offset / 4)))
            }
            _ if TER.start == offset => self.enabled,
            TPR => self.privileged_only,
            TCR => self.control & !mask(itm_tcr::BUSY),
            _ => 0,
        }
    }

    /// The Cortex-M4's identification register at `offset`, if one's there, 100166 table 10-1.
    pub(crate) fn identification(offset: u32) -> Option<u32> {
        let index = offset.checked_sub(IDENTIFICATION_AT)? / 4;
        IDENTIFICATION.get(usize::try_from(index).ok()?).copied()
    }

    /// A write of `value`, of `size`, at `offset`, by `writer`, and the Instrumentation packet it
    /// sends, if it sends one.
    pub(crate) fn write(
        &mut self,
        offset: u32,
        size: Size,
        value: u32,
        writer: Writer,
    ) -> Option<Vec<u8>> {
        if !writer.enabled {
            return None;
        }
        match offset {
            _ if STIM.contains(&offset) => {
                let port = offset / 4;
                if !self.port_on(port) || !self.may_write(port, writer.privileged) {
                    return None;
                }
                let header = set(instrumentation::A, 0, port);
                let header = set(instrumentation::SS, header, payload_size(size));
                let payload = value.to_le_bytes();
                let mut packet = vec![u8::try_from(header).expect("a byte")];
                packet.extend_from_slice(&payload[..size.bytes() as usize]);
                return Some(packet);
            }
            _ if TER.start == offset => {
                // An unprivileged write reaches only the ports its group lets it.
                let reachable = (0..PORTS)
                    .filter(|&port| self.may_write(port, writer.privileged))
                    .fold(0, |bits, port| bits | mask(&stimena(Self::port(port))));
                self.enabled = self.enabled & !reachable | value & reachable;
            }
            TPR if writer.privileged => self.privileged_only = value & mask(itm_tpr::PRIVMASK),
            TCR if writer.privileged => self.control = value & TCR_WRITABLE,
            _ => {}
        }
        None
    }

    /// An implemented port's number as a field's bit.
    fn port(port: u32) -> u8 {
        u8::try_from(port).expect("one of 32")
    }

    /// Whether `port` is on: implemented, enabled in `ITM_TER0`, and the ITM on.
    fn port_on(&self, port: u32) -> bool {
        port < PORTS
            && is_set(&stimena(Self::port(port)), self.enabled)
            && is_set(itm_tcr::ITMENA, self.control)
    }

    /// Whether software, `privileged` or not, may write `port` and its enable bit, C1.7.5.
    fn may_write(&self, port: u32, privileged: bool) -> bool {
        let group = u8::try_from(port / PORTS_A_PRIVILEGE_BIT).expect("one of four");
        privileged || get(&privmask(group), self.privileged_only) == 0
    }
}

/// `ITM_TCR`'s fields software writes, C1.7.6: all but `BUSY` and the reserved bits.
const TCR_WRITABLE: u32 = mask(itm_tcr::ITMENA)
    | mask(itm_tcr::TSENA)
    | mask(itm_tcr::SYNCENA)
    | mask(itm_tcr::TXENA)
    | mask(itm_tcr::SWOENA)
    | mask(itm_tcr::TSPRESCALE)
    | mask(itm_tcr::GTSFREQ)
    | mask(itm_tcr::TRACEBUSID);

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

    const PRIVILEGED: Writer = Writer {
        privileged: true,
        enabled: true,
    };
    const UNPRIVILEGED: Writer = Writer {
        privileged: false,
        enabled: true,
    };

    /// An ITM with `ITMENA` and the ports in `ports` on.
    fn on(ports: u32) -> Itm {
        let mut itm = Itm::default();
        itm.write(TCR, Size::Word, mask(itm_tcr::ITMENA), PRIVILEGED);
        itm.write(TER.start, Size::Word, ports, PRIVILEGED);
        itm
    }

    #[test]
    fn a_stimulus_write_goes_out_as_an_instrumentation_packet_only_when_enabled() {
        let mut itm = on(1 << 0 | 1 << 31);
        assert_eq!(
            itm.write(0, Size::Byte, 0x168, PRIVILEGED),
            Some(vec![0b0000_0001, 0x68])
        );
        assert_eq!(
            itm.write(0, Size::Halfword, 0x03A1, PRIVILEGED),
            Some(vec![0b0000_0010, 0xA1, 0x03]),
            "D4.2.8 figure D4-12's halfword"
        );
        assert_eq!(
            itm.write(31 * 4, Size::Word, 0x1234_5678, PRIVILEGED),
            Some(vec![0b1111_1011, 0x78, 0x56, 0x34, 0x12])
        );
        assert_eq!(
            itm.write(4, Size::Byte, 1, PRIVILEGED),
            None,
            "port 1 is off"
        );
        let off = Writer {
            privileged: true,
            enabled: false,
        };
        assert_eq!(itm.write(0, Size::Byte, 1, off), None, "TRCENA clear");
        assert_eq!(itm.read(TER.start, false), 0, "and it reads zero");
        itm.write(TCR, Size::Word, 0, PRIVILEGED);
        assert_eq!(
            itm.write(0, Size::Byte, 1, PRIVILEGED),
            None,
            "ITMENA clear"
        );
    }

    #[test]
    fn a_stimulus_port_reads_fifo_ready() {
        let mut itm = on(1);
        assert_eq!(itm.read(0, true), mask(itm_stim::FIFOREADY));
        assert_eq!(itm.read(4, true), 0, "a port that's off reads full");
        itm.write(TCR, Size::Word, 0, PRIVILEGED);
        assert_eq!(
            itm.read(0, true),
            0,
            "and so does every port with the ITM off"
        );
    }

    #[test]
    fn an_unprivileged_write_tpr_refuses_is_dropped() {
        let mut itm = on(0x0000_0101);
        itm.write(TPR, Size::Word, 0b0001, PRIVILEGED);
        assert_eq!(
            itm.write(0, Size::Byte, 7, UNPRIVILEGED),
            None,
            "ports 0 to 7"
        );
        assert_eq!(
            itm.write(8 * 4, Size::Byte, 7, UNPRIVILEGED),
            Some(vec![8 << 3 | 0b01, 7]),
            "ports 8 to 15"
        );
        itm.write(TER.start, Size::Word, 0, UNPRIVILEGED);
        assert_eq!(itm.read(TER.start, true), 0x1, "port 0's bit is privileged");
        itm.write(TPR, Size::Word, 0, UNPRIVILEGED);
        itm.write(TCR, Size::Word, 0, UNPRIVILEGED);
        assert_eq!(itm.read(TPR, true), 0b0001);
        assert!(is_set(itm_tcr::ITMENA, itm.read(TCR, true)));
    }

    #[test]
    fn the_cortex_m4s_ports_and_no_more_are_there() {
        let mut itm = on(u32::MAX);
        itm.write(TPR, Size::Word, u32::MAX, PRIVILEGED);
        assert_eq!(itm.read(TPR, true), 0xF, "four groups of eight");
        assert_eq!(itm.read(TER.start, true), u32::MAX);
        itm.write(TER.start + 4, Size::Word, u32::MAX, PRIVILEGED);
        assert_eq!(itm.read(TER.start + 4, true), 0, "ITM_TER1 is RAZ/WI");
        assert_eq!(
            itm.write(32 * 4, Size::Byte, 1, PRIVILEGED),
            None,
            "port 32"
        );
        assert_eq!(itm.read(32 * 4, true), 0);
        itm.write(TCR, Size::Word, u32::MAX, PRIVILEGED);
        assert_eq!(
            itm.read(TCR, true),
            TCR_WRITABLE,
            "BUSY and the reserved bits"
        );
        let ids: Vec<u32> = (0..12)
            .filter_map(|index| Itm::identification(IDENTIFICATION_AT + 4 * index))
            .collect();
        assert_eq!(ids, IDENTIFICATION);
        assert_eq!(Itm::identification(0xF00), None);
        assert_eq!(Itm::identification(0x1000), None);
    }
}