rigless 0.3.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 general-purpose timer of RM0090 rev 22 chapter 18 and the advanced-control timer of
//! chapter 17, by the model RM0433 rev 9 gives the STM32H743's timers too, in
//! [`crate::soc::st::tim`]. [`General`] is the `tim2` layout, bound to all four timers chapter 18
//! covers: TIM2 and TIM5, which count in 32 bits, and TIM3 and TIM4, which count in 16. What
//! separates the four is the counter's width and the option register, and both belong to the
//! instance. Chapter 17's TIM1 and TIM8 answer from their own layout through [`Advanced`]. It puts
//! every register the chapters share at the same offset with the same bits, and adds the repetition
//! counter and the break and dead-time register. Everything chapter 17 adds has its own rows below.
//!
//! What this part's manual and errata sheet give it, and the STM32H743's don't, is the part's own and
//! the model takes it from [`PART`]: the fields only RM0433 names are absent here, so `OCxM` is three
//! bits, 17.4.7 and 18.4.7, `SMS` and `TS` three, 18.4.3, `REP` eight, 17.4.13, and there's no
//! `UIFREMAP`, `MMS2`, `TISEL`, `ETRSEL`, channel 5 or 6 or `BRK2`. `0x50` is the option register,
//! 18.4.19 and 18.4.20. The break is the `BKIN` pin alone, which rests low, 17.3.12. The model wakes
//! at every counter event, and nothing on the part acknowledges a timer's request.
//!
//! Every functional section 18.3.1 to 18.3.15 and every register 18.4.1 to 18.4.19 is implemented,
//! and so is every functional section 17.3.1 to 17.3.20 and every register 17.4.1 to 17.4.20.
//! Section 18.3.16 and 17.3.21, both called "Debug mode", are the second table's. Nothing here is
//! register-only.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | Time-base unit | `PSC + 1` input cycles per count, the prescaler buffered until the update, `ARR` preloaded under `ARPE`, the update at overflow or underflow. "The counter is blocked while the auto-reload value is null" | 18.3.1, 18.4.10 to 18.4.12, 17.4.12 | `the_update_comes_every_period_without_drift`, `arr_preload_and_the_prescaler_take_effect_at_the_update`, `a_null_auto_reload_blocks_the_counter` |
//! | Counter modes | Up, down, and the three center-aligned modes with `DIR` set by hardware and the flags gated by `CMS`. A match at the crest counts as up and one at the valley as down, and a value above `ARR` flags at the crest only, figure 165 | 18.3.2, 18.4.1, 18.4.5 | `down_counting_underflows_from_arr_to_zero`, `center_aligned_counting_turns_at_arr_and_zero`, `center_aligned_flags_follow_figure_165` |
//! | `UDIS`, `URS`, `UG`, `OPM` | The update disabled keeps the shadows, the request source gates the flag, `UG` reinitializes, one-pulse stops at the update | 18.3.2, 18.4.1, 18.4.6 | `ug_reinitializes_and_sets_uif_unless_urs`, `udis_keeps_the_shadows_but_reinitializes`, `one_pulse_mode_stops_at_the_update` |
//! | Clock selection | Internal clock, external clock mode 1 on the trigger input, external clock mode 2 on `ETR`, the internal triggers | 18.3.3, 18.4.3 | `external_clock_mode_1_counts_the_trigger_edges`, `external_clock_mode_2_counts_etr_edges_through_its_prescaler` |
//! | `CK_INT` | The timer clock as the part's tree feeds it, section 6.2's `TIMxCLK`, followed as it changes: what was counted stands, the next cycle comes at the new rate, and a wake-up placed at the old rate is placed again. A clock at nothing stops the counter where it is. What came due before the change is settled at it, and an interrupt it flags rises there, 18.4.4 | 6.2, 18.3.3, 18.4.4 | `the_counter_carries_across_a_change_of_clock`, `a_clock_at_nothing_stops_the_counter`, `a_change_of_clock_raises_what_came_due_before_it` |
//! | Writes and events | An event comes at its own instant and sees the registers as they stood, so a write that lands after it reaches the update that follows, as the preload of 18.3.1 does | 18.3.1, 18.4.12 | `a_write_comes_after_the_events_due_before_it` |
//! | Capture/compare channels | A preload and a shadow per channel, `CCxS` choosing the direction and the input, `TRC` as the input under an internal trigger, `CCxS` frozen while enabled. Channels 5 and 6 are RM0433's and nothing here | 18.3.4, 18.4.7 to 18.4.9, 18.4.13 to 18.4.16 | `ccr_preload_takes_effect_at_the_update`, `an_input_channel_is_read_only`, `a_channel_on_trc_captures_the_internal_trigger`, `channels_5_and_6_are_nothing_here` |
//! | Input capture | The filter, the edge polarity, the prescaler, the capture into `CCRx`, `CCxIF` and `CCxOF`, cleared by reading. The filter is "an event counter in which N consecutive events are needed to validate a transition", so a transition it's counting keeps its count when the level is driven again or `SMCR` written | 18.3.4, 18.3.5, 18.4.5, 18.4.7 | `a_capture_latches_the_counter_and_flags_it`, `the_input_filter_drops_a_short_pulse`, `the_capture_prescaler_captures_every_nth_edge`, `a_filter_keeps_counting_a_transition_through_a_write` |
//! | PWM input | Two channels on one input with opposite polarity and the reset mode | 18.3.6 | `pwm_input_measures_period_and_duty` |
//! | Forced output | `OCxM` 100 and 101 force the reference, and the flag still follows the match | 18.3.7 | `forced_output_holds_its_level_and_the_flag_still_matches` |
//! | Output compare | Frozen, active, inactive, and toggle on match, with `CCxIF`, polarity and enable. A value above `ARR` flags at the boundary | 18.3.8, 18.4.5, 18.4.9 | `a_compare_match_toggles_the_output_at_its_instant`, `active_and_inactive_on_match_set_the_reference`, `a_compare_value_above_arr_flags_at_the_overflow` |
//! | PWM | Modes 1 and 2, edge-aligned both ways and center-aligned, 0% and 100%. The reference is the comparison except while `ETRF` holds it | 18.3.9, figure 165 | `pwm_mode_1_is_high_below_ccr`, `pwm_holds_at_zero_and_above_arr`, `pwm_mode_2_down_counting_inverts_the_comparison`, `center_aligned_pwm_follows_figure_165` |
//! | The pulse on the pin | [`Tim::pulse`] reads an edge-aligned PWM waveform off the shadows as a high time and a period on the virtual clock, through the polarity, less chapter 17's dead-time where the complementary output is enabled. A level, a capture or a center-aligned waveform reads as no pulse | 18.3.9, 17.3.11 | `the_pulse_is_the_compare_over_the_period_through_the_polarity`, `a_pulse_is_read_only_off_a_driven_edge_aligned_pwm_pin`, `the_advanced_pulse_needs_moe_and_loses_the_dead_time` |
//! | One-pulse mode | The trigger starts the counter, the pulse spans `CCR` to `ARR`, `OCxFE` acts on the trigger | 18.3.10 | `one_pulse_mode_stops_at_the_update`, `fast_enable_acts_on_the_trigger` |
//! | `OCxREF` clear | A rising `ETRF` clears the reference until the next update under `OCxCE`. A level still high at the update doesn't clear it again, figure 167 | 18.3.11 | `etrf_clears_the_reference_until_the_update`, `an_etrf_clear_holds_through_a_comparison_that_flips` |
//! | Encoder interface | Modes 1 to 3 counting on one or both inputs with the direction from the other, table 97. `DIR` follows every transition, counted or not. Mode 1 counts `TI1FP1` and mode 2 `TI2FP2` as 18.4.3 says, where 18.3.12's first paragraph has the two swapped | 18.3.12, 18.4.3 | `encoder_mode_counts_with_the_phase`, `encoder_modes_1_and_2_count_one_input_and_steer_on_both` |
//! | XOR input | `TI1S` feeds channel 1 with `CH1 ^ CH2 ^ CH3` | 18.3.13, 18.4.2 | `ti1s_xors_the_three_inputs` |
//! | Slave modes | Reset, gated, and trigger on the selected trigger input, with `TIF`. Both edges under `CCxNP`. The external clock once when modes 1 and 2 both name `ETRF` | 18.3.14, 18.4.3, 18.4.9 | `reset_mode_reinitializes_on_the_trigger`, `gated_mode_counts_while_the_trigger_is_high`, `trigger_mode_starts_on_the_edge`, `trigger_mode_takes_both_edges_under_ccnp`, `etrf_as_both_external_clocks_counts_once` |
//! | Master mode | `TRGO` from reset, `UG` or the slave reset, enable, update, the compare pulse, or a reference | 18.3.15, 18.4.2 | `trgo_follows_the_master_mode`, `trgo_pulses_on_the_compare_and_the_slave_reset` |
//! | `DIER`, `SR` | One interrupt level: any flag set with its enable set | 18.4.4, 18.4.5 | `the_interrupt_is_the_level_of_flag_and_enable` |
//! | DMA requests | Update, capture/compare, and trigger requests, `CCDS`, each held on its request output until the transfer serves it, 10.3.2: a `CCRx` access a channel's, the burst's last access the update's and the trigger's. The burst through `DCR` and `DMAR` reaches the register at `CR1` plus `(DBA + DMA index) x 4` under its write rules, the option register at index 20, and reads it as the bus would, so `CNT` is the count. A word with no register, and `DMAR` itself, reads zero and takes nothing | 10.3.2, 18.4.4, 18.4.17, 18.4.18 | `dma_requests_are_counted_and_the_burst_walks_the_registers`, `the_trigger_request_and_the_burst_into_sr`, `a_dma_request_stands_until_the_transfer_serves_it`, `the_burst_reaches_the_register_its_address_names` |
//! | `TIM2_OR` | `ITR1_RMP` held for the board, whose mux puts `TIM8_TRGO`, the PTP, OTG FS, or OTG HS source on `ITR1`. The timer counts whichever arrives | 18.4.19 | `itr1_remap_moves_the_internal_trigger` |
//! | `CNT`, reset state | A write starts the count there. `ARR` is all ones at reset, in the instance's own width | 18.4.10, 18.4.12 | `a_write_to_cnt_sets_the_count`, `a_timer_started_without_an_auto_reload_counts_to_its_own_width` |
//! | The counter's width | 16 bits on TIM3 and TIM4, 32 on TIM2 and TIM5: `CNT`, `ARR` and every `CCRx` count, reload and compare in it, and table 100 leaves their high half reserved on the narrow pair, so a write there doesn't stand | 18.2, 18.4.10 to 18.4.16, table 100 | `a_narrow_timer_counts_and_reloads_in_sixteen_bits`, `a_wide_timer_counts_and_reloads_in_thirty_two` |
//! | Chapter 17's layout | TIM1 and TIM8 count in sixteen bits and answer from the `tim1` table: the registers the chapters share at the same offsets and bits, which `the_shared_fields_sit_at_the_same_bits_in_both_layouts` holds, `RCR` at `0x30`, `BDTR` at `0x44`, and `ARR` at `0xFFFF` out of reset, where ST's file says zero | 17.2, 17.4.12, 17.4.21 | `an_advanced_timer_answers_from_chapter_17s_layout` |
//! | Repetition counter | The update comes "only when the repetition counter has reached zero", every `N + 1` overflows, or underflows, or both center-aligned. `UG` and the slave reset bring it at once and reload the counter. A written `RCR` waits "until the next repetition update event". `REP` is bits 7:0 and the bits above are reserved | 17.3.3, 17.4.13 | `the_update_comes_every_repetition_plus_one`, `center_aligned_the_repetition_counts_both_boundaries`, `the_repetition_counter_is_eight_bits` |
//! | `MOE`, `OSSI`, `OSSR`, `OISx`, `OISxN` | Table 95. With `MOE` clear the outputs aren't driven, or under `OSSI` they're held at their idle levels as soon as either enable is set. With it set, an output whose enable is clear isn't driven, or under `OSSR` it's driven at its inactive level | 17.3.11, 17.3.12, 17.4.2, 17.4.18 | `moe_gates_the_outputs_and_ossi_picks_the_idle_level`, `off_state_selection_drives_the_inactive_level_of_a_disabled_output` |
//! | Complementary outputs and dead-time | With both enabled, `OCx` is the reference and `OCxN` its opposite, each rising edge delayed by `DTG`'s four ranges of `tDTS`, and a pulse narrower than the delay isn't generated. `OCxN` alone "is not complemented". Each goes through its own polarity | 17.3.11, 17.4.9, 17.4.18, figures 120 to 122 | `the_complementary_output_is_the_reference_inverted_with_a_dead_time_on_each_rise`, `an_output_alone_on_the_complementary_pin_is_not_inverted` |
//! | The break | The `BKIN` pin under `BKE` and `BKP` clears `MOE`, sets `BIF`, and holds both while active. `BG` and the clock security system's pulse do the same and hold nothing. `AOE` sets `MOE` again at the next update while the pin isn't active | 17.3.12, 17.4.5, 17.4.6, 17.4.18 | `a_break_on_the_pin_holds_moe_low_and_bif_high_until_the_pin_releases`, `a_software_break_is_a_pulse`, `the_clock_securitys_failure_reaches_the_advanced_timers_break` |
//! | Commutation | Under `CCPC` the enables, polarities and modes of the three channels with a complementary output take effect at `COMG`, or at a rising edge of `TRGI` under `CCUS`. `COMIF` rises, and a request goes out under `COMDE` | 17.3.14, 17.4.2, 17.4.4, 17.4.6 | `the_commutation_event_applies_the_preloaded_control_bits` |
//! | `LOCK` | Written once after reset. Level 1 freezes `DTG`, `OISx`, `BKE`, `BKP` and `AOE`, level 2 those and the polarities of an output channel with `OSSR` and `OSSI`, and level 3 those and `OCxM` and `OCxPE` | 17.4.18 | `lock_freezes_what_each_level_says` |
//! | Four lines | The break, the update, the trigger with the commutation, and the capture and compare flags each raise their own line, in table 63's order | 17.4.4, 17.4.5, table 63 | `chapter_17s_flags_raise_four_lines` |
//! | The burst on chapter 17's map | `DBA` twelve is `RCR` and seventeen `BDTR`, where chapter 18's map is reserved | 17.4.19 | `the_dma_burst_reaches_the_repetition_and_break_registers` |
//! | ES0206 rev 24, 2.7.1 | The system break "is not latched": under `AOE` the outputs are re-armed at the following period after a clock failure, where a latched break would hold them | modeled | `erratum_2_7_1_a_clock_failure_re_arms_the_outputs_under_aoe` |
//! | ES0206 rev 24, 2.7.3 | A second match on the next counter cycle after a `CCR` change, in the three patterns: a toggle output doesn't toggle again, and center-aligned the flag doesn't rise, at the crest and the valley "when `TIMx_RCR` = 0". Other modes are unaffected | modeled | `erratum_2_7_3_misses_the_consecutive_match`, `erratum_2_7_3_at_the_valley_and_in_other_modes`, `erratum_2_7_3_center_aligned_needs_the_repetition_counter_at_zero` |
//! | ES0206 rev 24, 2.7.4 | In reset mode a reference cleared by `ETRF` stays inactive one extra cycle when the reset precedes the compare | modeled | `erratum_2_7_4_holds_the_cleared_reference_one_extra_cycle` |
//! | The option register, on TIM5 | This is written against TIM2's layout and TIM5 is bound to it, so TIM5's option register answers as TIM2's. The two aren't the same register: 18.4.19 gives TIM2 `ITR1_RMP` at bits 11:10, remapping internal trigger 1, and 18.4.20 gives TIM5 `TI4_RMP` at bits 7:6, putting the LSI, the LSE or the wake-up on channel 4 to calibrate against. So on TIM5 a write to bits 7:6 is dropped where the part would take it, and bits 11:10 take a write the part would ignore. That's the only difference between the two tables ST's file emits for the wide pair. Every other register, field and bit of the two is identical. And it's out of the reach of anything that counts: no clock source, no capture and no compare passes through it. It's one register further than the ports' rule went, which collapsed layouts differing by reset values alone, and it's the price of one model answering for both | 18.4.19, 18.4.20 | `each_pair_of_timers_answers_from_the_one_layout_it_shares` |
//! | Reset | A reset of the part, or a write that leaves the timer's bit of `RCC_APB1RSTR` or `RCC_APB2RSTR` set, stops the counter and leaves the timer as it was built, its clock at the rate the tree drives it, its inputs where they're driven and the clock security system still reaching its break. The bit's write lowers each request a reader heard rise as well, at once. ChibiOS pulses the bit as a PWM driver starts | 6.1.1, 6.3.8, 6.3.9, 17.4, 18.4 | `a_reset_stops_the_counter_and_keeps_the_clock`, `a_reset_bit_puts_its_timer_back_and_leaves_the_others_alone`, `a_reset_bit_puts_its_advanced_timer_back_and_keeps_its_break` |
//! | The option register, on TIM3 and TIM4 | Table 100 gives the register at `0x50` to TIM2 and TIM5 and to neither of the others, so the narrow pair's table is the wide pair's up to that register and then stops. The two answer from their own table rather than from TIM2's, and `Device::layout` is how the model says which instance it's answering for. No row the model reaches is one the narrow pair lacks | table 100 | `each_pair_of_timers_answers_from_the_one_layout_it_shares`, `every_timer_the_part_binds_is_the_shape_it_is_bound_to` |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | Debug mode: the counter holding while the core is halted and the debug module says so | 18.3.16, 17.3.21 | `dbg.rs` gives the reason: a debugger halt already stops every counter's virtual clock at once, so a per-timer halt input has nothing left to add. `Tim::halt` used to be that input and is gone |
//! | An advanced timer's outputs disabled "as if the MOE bit was reset" when `DBG_TIMx_STOP` halts its counter | 17.3.21 | The same gap as the row above, and `dbg.rs` records it from the freeze bit's side |
//! | `MSM`, the trigger's resynchronization delay for slaves | 18.4.3 | Not modeled. See the delay note below |
//! | Access widths: a 32-bit register written by words, the rest by half-words or words | 18.4 | The manual names the widths a write must use and not the effect of a narrower one, so a narrower write lands in its lane |
//! | A trigger pulse lost when the slave's clock isn't enabled yet | ES0206 rev 24, 2.7.2 | Not modeled. It's one of Table 4's documentation errata, and its own text calls it "a documentation issue rather than a product limitation" |
//! | `BKE` and `BKP` taking "1 APB clock cycle" to be effective, and the resynchronization of `MOE` | 17.3.12, 17.4.18 | A write here is effective as it lands and reads back at once. See the delay note below |
//! | The dead-time on the way to the idle state, "OCx=OISx and OCxN=OISxN after a dead-time" | 17.3.12, table 95 | The idle levels are driven as `MOE` falls. The two cycles the page gives the way there are the delay note's |
//! | `BDE`, the break's DMA request | 17.3.12 | Named there and given no bit in 17.4.4, so there's nothing to set |
//! | TIM8's own table | 17.4.21 | ST's file gives TIM8 TIM1's layout, and the part answers for it from that. The two chapters give the pair one map |
//! | A register write within the clock-enable delay is lost | ES0206 rev 24, 2.2.7 | Not modeled, as `rcc.rs` records for the same erratum: a gate opens at the instant its bit is written |
//! | A DMA request reaching a controller | 18.4.4, tables 43 and 44 | The timer drives its request outputs, and `peripherals.rs` leaves the timers' rows of both tables out, so no stream serves a timer yet |
//!
//! Where the manual gives a resynchronization delay of a cycle or two for an external edge, `MSM`
//! included, the model applies the edge on the cycle it arrives in. The input filter validates a
//! transition after its `N` samples and drops a transition the input reverts before then. The
//! sampling phase within a cycle isn't modeled. Both are said here and nowhere else.

use crate::device::{DeviceContext, Level, RegisterFile};
use crate::soc::RegisterSpec;
use crate::soc::st::tim::{BreakInput, BreakSource, Errata, Kind, Part, Spelling, Wakeups};
use crate::soc::stm32f427::generated::{tim1, tim2, tim3};

pub use crate::soc::st::tim::{
    CHANNEL_REQUESTS, COMMUTATION_REQUEST, Channel, Counter, DmaRequests, Family, Flags,
    TRIGGER_REQUEST, Tim, UPDATE_REQUEST,
};

/// What RM0090 rev 22 and ES0206 rev 24 give this part's timers.
pub const PART: Part = Part {
    tables: &[&tim1::REGISTERS, &tim2::REGISTERS],
    // `IC1PSC` and `IC2PSC` of 17.4.7 and 18.4.7.
    spellings: &[
        Spelling {
            register: "CCMR1_Input",
            manual: "IC1PSC",
            file: "ICPCS",
        },
        Spelling {
            register: "CCMR1_Input",
            manual: "IC2PSC",
            file: "IC2PCS",
        },
    ],
    wakeups: Wakeups::Every,
    break_pin: Some(Level::Low),
    acknowledged: None,
    errata: Errata {
        consecutive_compare_missed: true,
        clear_outlasts_reset: true,
        trigger_lost_at_reload: false,
    },
};

/// The general-purpose timers of chapter 18.
#[derive(Debug, Clone, Copy)]
pub struct General;

/// The advanced-control timers of chapter 17.
#[derive(Debug, Clone, Copy)]
pub struct Advanced;

impl Family for General {
    type Register = tim2::Register;
    type Instance = Counter;
    const PART: &'static Part = ∂
    const KINDS: &'static [Kind] = &[Kind::General];

    fn layout(counter: Counter) -> &'static [RegisterSpec] {
        counter.layout()
    }
}

// Chapter 17's four lines, in the order the instance's table gives them, table 63.
const BREAK: usize = 0;
const UPDATE: usize = 1;
const TRIGGER_AND_COMMUTATION: usize = 2;
const CAPTURE_COMPARE: usize = 3;

impl Family for Advanced {
    type Register = tim1::Register;
    type Instance = Counter;
    const PART: &'static Part = ∂
    const KINDS: &'static [Kind] = &[Kind::FourChannelBreak];
    const LINES: usize = 4;

    fn line(flags: Flags) -> usize {
        match flags {
            Flags::Break => BREAK,
            Flags::Update => UPDATE,
            Flags::TriggerAndCommutation => TRIGGER_AND_COMMUTATION,
            Flags::CaptureCompare => CAPTURE_COMPARE,
        }
    }
}

impl Counter {
    /// The vendor's table for one of this part's general-purpose timers counting in this, which is
    /// what the part points it at.
    ///
    /// Table 100 gives the option register at `0x50` to TIM2 and TIM5 and to neither of the others,
    /// so the narrow pair's table is the wide pair's up to that register and then stops. The model is
    /// written against the wide one and reaches every row of either.
    #[must_use]
    pub const fn layout(self) -> &'static [RegisterSpec] {
        match self {
            Self::Narrow => &tim3::REGISTERS,
            Self::Wide => &tim2::REGISTERS,
        }
    }
}

impl Tim<Advanced> {
    /// The level on the `BKIN` pin, 17.3.12.
    pub fn drive_break(
        &mut self,
        level: Level,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        self.drive_break_source(
            BreakInput::First,
            BreakSource::Pin,
            level,
            registers,
            context,
        );
    }
}

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

    /// Every register and field the two layouts share sits at the same offset and the same bits,
    /// which is what lets one model written against chapter 18's tables read chapter 17's registers
    /// through them.
    #[test]
    fn the_shared_fields_sit_at_the_same_bits_in_both_layouts() {
        let mut shared = 0;
        for general in tim2::REGISTERS {
            let Some(advanced) = tim1::REGISTERS
                .iter()
                .find(|candidate| candidate.name == general.name)
            else {
                continue;
            };
            assert_eq!(general.offset, advanced.offset, "{}", general.name);
            for field in general.fields {
                let Some(twin) = advanced
                    .fields
                    .iter()
                    .find(|candidate| candidate.name == field.name)
                else {
                    continue;
                };
                assert_eq!(field.bit, twin.bit, "{}.{}", general.name, field.name);
                assert_eq!(field.width, twin.width, "{}.{}", general.name, field.name);
                shared += 1;
            }
        }
        assert!(shared > 80, "{shared} fields shared");
    }
}