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 timers of RM0433 rev 9 chapters 38 to 42, by the model RM0090 rev 22 gives the STM32F427's timers too,
//! in [`crate::soc::st::tim`]: one model by its [`Family`], the layout it is addressed through, and its
//! [`Kind`], the timer it is. Fourteen timers share it: the advanced-control TIM1 and TIM8 (chapter 38), the
//! general-purpose TIM2 to TIM5 (39), TIM12 to TIM14 (40), TIM15 to TIM17 (41), and the basic TIM6 and TIM7
//! (42). The registers sit at the same offsets in every layout, so the model names one by its slot and finds
//! its row once, and a timer answers for the registers its kind has. A register a kind lacks but its layout
//! carries, as TIM12 does of TIM2's table, stores what is written and does nothing.
//!
//! What this part's manual and errata sheet give it, and the STM32F427's don't, is the part's own and the
//! model takes it from [`PART`]: the four-bit `OCxM` of 38.4.7 and `SMS[3]` and `TS[4:3]` of 38.4.3,
//! `UIFREMAP` of 38.4.1, `MMS2` of 38.4.2, `TISEL` of 38.4.30, `ETRSEL` and the break sources of 38.4.26,
//! channels 5 and 6, the sixteen-bit `REP` of 38.4.15, and `BRK2` and the system breaks of 38.3.16.
//! `TIMx_CCMR3` is at `0x54`, 38.4.23, where the STM32F427 has an option register at `0x50`. An undriven break
//! source asserts nothing, the model wakes only while something outside could see its events, and DMAMUX1's
//! acknowledge serves a request.
//!
//! **What the model handles and what falls to the register file.** Every register the chapters give has an
//! arm: `CNT`, `ARR`, `PSC` and the `CCRx` keep their width, `CR1`, `CR2`, `SMCR`, `EGR`, the capture/compare
//! mode and enable registers, `BDTR`, `AF1`, `AF2`, `TISEL`, `DCR` and `DMAR` act, and `SR` and `DIER` are
//! read for the flags and the lines. `RCR` is read at the update. The register file holds the stored bits and
//! applies `SR`'s `rc_w0` that the part corrects.
//!
//! | Register or section | Behavior | Source | Test |
//! |---|---|---|---|
//! | Time base | `PSC + 1` cycles a count, the prescaler buffered to the update, `ARR` preloaded under `ARPE`, a counter blocked while `ARR` is zero | 38.3.1, 39.3.1, 40.3.1, 41.4.1, 42.3.1, 39.4.15 | `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, center-aligned with `DIR` the hardware's and the flags gated by `CMS` | 38.3.2, 39.3.2 | `down_counting_underflows_from_arr_to_zero`, `center_aligned_counting_turns_at_arr_and_zero`, `center_aligned_flags_follow_figure_435` |
//! | `UDIS`, `URS`, `UG`, `OPM` | Update gates and one pulse | 38.4.1, 38.4.6 | `ug_reinitializes_and_sets_uif_unless_urs`, `udis_keeps_the_shadows_but_reinitializes`, `one_pulse_mode_stops_at_the_update` |
//! | Counter width | 32 bits on TIM2 and TIM5, 16 on the rest | 39.2 | `a_narrow_timer_counts_and_reloads_in_sixteen_bits`, `a_wide_timer_counts_and_reloads_in_thirty_two` |
//! | Repetition counter | An update every `REP + 1` overflows, sixteen bits on TIM1 and TIM8 and eight on TIM15 to TIM17 | 38.3.3, 38.4.15, 41.5.13 | `the_update_comes_every_repetition_plus_one`, `tim15_has_a_dead_time_on_channel_1_and_an_eight_bit_repetition_counter` |
//! | Clock | `CK_INT` followed as the tree changes it, 64 MHz out of reset ([`RESET_CLOCK_HZ`]), external clock modes 1 and 2, the encoder | 38.3.5, 8.5.6 | `the_counter_carries_across_a_change_of_clock`, `a_timer_built_with_no_clock_runs_at_the_rate_it_wakes_up_with`, `external_clock_mode_1_counts_the_trigger_edges`, `encoder_mode_counts_with_the_phase` |
//! | `ETR` | `ETP`, `ETPS`, `ETF`, and the source `ETRSEL` chooses | 38.3.4, 39.4.22 | `etp_inverts_and_etf_filters_etr`, `etrsel_and_tisel_choose_the_source_the_timer_sees` |
//! | Input capture | Filter, edge, prescaler, `CCxIF` and `CCxOF`, the input `TISEL` chooses, PWM input | 38.3.6 to 38.3.8, 38.4.30 | `a_capture_latches_the_counter_and_flags_it`, `the_input_filter_drops_a_short_pulse`, `pwm_input_measures_period_and_duty` |
//! | Output compare | Frozen, active, inactive, toggle, forced, flags at the match | 38.3.9, 38.3.10 | `a_compare_match_toggles_the_output_at_its_instant`, `forced_output_holds_its_level_and_the_flag_still_matches` |
//! | PWM | Modes 1 and 2, preloads, and [`Tim::pulse`] and [`Tim::complementary_pulse`] reading the waveform | 38.3.11 | `pwm_mode_1_is_high_below_ccr`, `the_pulse_is_the_compare_over_the_period_through_the_polarity`, `the_motor_outputs_on_tim5_and_tim4_make_the_pulse_the_driver_asks_for` |
//! | Asymmetric, combined and 3-phase PWM | The four-bit `OCxM`, `OCxREFC`, channels 5 and 6, `GC5Cx` | 38.3.12 to 38.3.14 | `asymmetric_pwm_takes_the_first_reference_up_and_the_second_down`, `combined_pwm_gives_the_or_or_the_and_of_a_pairs_two_references`, `channel_5_groups_into_channel_1_and_flags_its_own_match` |
//! | Complementary outputs, dead-time | Table 337, `DTG`, the off-states | 38.3.15 | `the_complementary_output_is_the_reference_inverted_with_a_dead_time_on_each_rise`, `the_motor_outputs_on_tim8s_complementary_pins_make_their_pulse` |
//! | Break | `BRK` and `BRK2` with their sources, polarities, filters, `MOE`, `AOE`, `BIF`, `B2IF`, `SBIF`, system breaks | 38.3.16, 38.3.17, 38.4.20 | `a_break_on_the_pin_holds_moe_low_and_bif_high_until_the_pin_releases`, `break_2_leaves_the_outputs_at_their_inactive_level_and_flags_b2if`, `a_break_gathers_its_enabled_sources_each_through_its_own_polarity`, `the_break_filter_needs_its_samples_before_the_break_acts`, `a_system_break_flags_sbif_and_clears_moe_when_the_break_is_enabled` |
//! | `OCxREF` clear, 6-step, `LOCK` | `OCxCE` on `ETRF`, commutation, three lock levels. Level 3 holds the modes of a channel "configured in output" as the write finds it, whatever `CCxS` the write carries | 38.3.18, 38.3.19, 38.4.7, 38.4.20 | `etrf_clears_the_reference_until_the_update`, `the_commutation_event_applies_the_preloaded_control_bits`, `lock_freezes_what_each_level_says`, `lock_level_3_holds_the_modes_of_a_channel_the_write_finds_an_output` |
//! | One pulse, retriggerable | `OCxFE`, `SMS` 1000 with `OCxM` 1000 and 1001 | 38.3.20, 38.3.21 | `fast_enable_acts_on_the_trigger`, `a_retriggerable_pulse_starts_on_the_trigger_and_is_extended_by_another` |
//! | `UIFREMAP` | `UIF` in `CNT` bit 31, hiding the wide counter's | 38.3.23, 39.3.16 | `uifremap_copies_the_update_flag_into_bit_31_of_the_counter`, `uifremap_hides_the_wide_counters_top_bit_from_reads` |
//! | `TI1S` | `XOR` of two or three pins | 38.3.24 | `ti1s_xors_the_three_inputs` |
//! | Slave modes, `TRGO`, `TRGO2` | Reset, gated, trigger, combined, `MMS`, `MMS2` | 38.3.26, 38.3.27 | `reset_mode_reinitializes_on_the_trigger`, `gated_mode_counts_while_the_trigger_is_high`, `trgo_follows_the_master_mode`, `trgo2_carries_the_event_mms2_selects` |
//! | DMA | A request stands from its event until the transfer serves it, "pending until served by the DMA controller": by the last access of the `DBL + 1` to `DMAR` for update, trigger and commutation, by the access to `CCRx` for a channel, or by [`Tim::acknowledge`] where the data goes elsewhere. The burst walks the registers through `DCR` and `DMAR`, and a read of `DMAR` answers as the register it reaches | 17.1, 38.3.28, Table 103 | `an_update_request_stands_until_the_burst_has_been_served`, `a_request_raised_before_its_controller_is_enabled_is_standing_when_it_looks`, `a_dshot_burst_loads_the_four_compare_values_an_update`, `a_dmar_read_answers_as_the_register_it_reaches_does`, `the_request_outputs_are_the_numbers_table_103_inputs_are_wired_to` |
//! | Settling | The counter is settled at each access and woken for only when an interrupt, request, pin, trigger output or slave could see its events, and a write is applied after the events due before it | | `reading_the_counter_over_and_over_is_a_pure_function_of_the_instant`, `center_aligned_flags_follow_figure_435` |
//! | `LOCK` and the reset of `AF1` | The lock compares a write with the register as reset left it | 38.4.26, 38.4.20 | `lock_level_1_keeps_the_alternate_function_registers_at_their_reset_value` |
//! | Lines | One for most, four on TIM1 and TIM8 | Table 147 | `chapter_38s_flags_raise_four_lines`, `tim8_raises_its_own_four_lines` |
//! | The ChibiOS tick | TIM12 and TIM5 free-running with an alarm in `CCR1` | 40.4, 39.4 | `an_alarm_through_ccr1_interrupts_at_its_microsecond_exactly`, `an_alarm_past_the_wrap_waits_for_the_counter_to_come_round`, `the_bootloaders_tick_on_tim5_counts_in_thirty_two_bits` |
//! | TIM12 to TIM17, TIM6, TIM7 | Their channels and modes by [`Kind`] | 40, 41, 42 | `tim12_has_two_channels_and_counts_up_only`, `tim13_is_one_channel_and_its_oc1_is_the_pin_a_slave_can_trigger_on`, `tim15_has_one_break_and_flags_a_system_break_in_bif`, `tim16_and_tim17_are_one_channel_with_a_break_and_only_the_plain_modes`, `a_basic_timer_counts_to_its_auto_reload_and_raises_its_line` |
//! | Resets and corrections | `ARR` and `AF1`/`AF2` reset values, `SR` `rc_w0` | 38.4.14, 38.4.26, 38.4.5 | `every_timer_comes_out_of_reset_as_the_manual_gives_it`, `every_timers_status_register_clears_on_a_zero_and_ignores_a_one` |
//! | Snapshot, reset | The state serializes, a reset restarts the timer | | `the_model_state_round_trips_through_a_snapshot`, `a_reset_stops_the_counter_and_keeps_the_clock`, `two_runs_of_one_script_are_the_same_run` |
//! | ES0392 rev 15 2.16.1 | In combined reset and trigger with `MSM` and one pulse, a trigger at `ARR` starts no pulse. Present on revision V | modeled | `erratum_2_16_1_loses_a_trigger_that_arrives_at_the_auto_reload` |
//! | ES0392 rev 15 2.16.2 | A consecutive compare one cycle later is missed | modeled | `erratum_2_16_2_misses_the_consecutive_match`, `erratum_2_16_2_at_the_valley_and_in_other_modes` |
//! | ES0392 rev 15 2.16.3 | The output clear with an external reset holds one extra cycle | modeled | `erratum_2_16_3_holds_the_cleared_reference_one_extra_cycle` |
//!
//! **What the model leaves out, and why.**
//!
//! | Behavior | Source | Reason |
//! |---|---|---|
//! | Debug mode: the counter holding while the core is halted | 38.3.29, 42.3.5 | A debugger halt already stops the virtual clock |
//! | `MSM`, the trigger's resynchronization delay, and the 1 APB cycle on `BKE`/`BKP` writes | 38.4.3, 38.4.20 | The manual gives a cycle or two and the model applies the edge as it arrives. Only erratum 2.16.1 reads `MSM` |
//! | The bidirectional break's output drive | 38.3.17 | The comparator's open-drain output belongs to the comparator and the pin |
//! | Which timer feeds which `ITRx`, and the converters' triggers | Tables 336, 341, 344, 349 | The part wires [`Tim::trigger_output`] to [`Tim::drive_internal_trigger`] |
//! | The Hall sensor interface | 38.3.25 | A configuration of rows above across two timers |
//! | The dead-time on the way to the idle state | 38.3.16 | The idle levels are driven as `MOE` falls |
//! | Combined and retriggerable waveforms in [`Tim::pulse`] | 38.3.12 to 38.3.14 | The pin is read with [`Tim::output`] and [`Tim::reference`] |
//! | ES0392 rev 15 2.2.2, timer system breaks do not work | | Absent on revision V, this part's: the system breaks act |
//! | ES0392 rev 15 2.16.4, unexpected output when `ocref_clr` is used in combined or asymmetric modes | | Present on revision V with no workaround, and the sheet gives no waveform to reproduce |
//! | Wiring `TIMx_CCMR3` and other holes | 39.4.30 | ChibiOS writes the word at `0x54` of every timer. Where the layout has no register there, the part answers it as undefined space in a peripheral, which reads zero and drops the write, Table 8 note 1, and the session is told |
//!
//! **What the part must wire.** Clocks: TIM2 to TIM7 and TIM12 to TIM14 run on `rcc_timx_ker_ck`, TIM1, TIM8,
//! TIM15, TIM16 and TIM17 on `rcc_timy_ker_ck`, Table 57. Requests: [`UPDATE_REQUEST`], [`CHANNEL_REQUESTS`],
//! [`TRIGGER_REQUEST`] and [`COMMUTATION_REQUEST`] for the multiplexer of Table 103, levels it reads, and
//! [`Tim::acknowledge`] from the controller for a transfer that doesn't touch the timer. Interrupts by position in
//! the instance's table. System breaks by [`Tim::breaks_on`].
//!
//! **Where this model departs from the vendor's file.** `SR` is read-write there where the manual types every
//! flag `rc_w0`, `ARR` resets to zero, `AF1` and `AF2` to zero, and `RCR`'s `REP` is eight bits on TIM1 where
//! 38.4.15 gives sixteen. The part corrects the first three in `memory_map.rs`, and the model reads sixteen.
//! ST's file misspells `OC4CE` and `OC4M[3]`, and the model reads their bits.
//!
//! **Determinism.** The model reads no clock and draws no number. `two_runs_of_one_script_are_the_same_run`
//! drives two instances through one script and compares their interrupts, registers and state.

use crate::device::{DeviceContext, Level, RegisterFile};
use crate::soc::st::tim::{Errata, Part, Spelling, Wakeups};
use crate::soc::stm32h743::generated::tim1;

use super::dmamux::acknowledged_request;

mod family;

pub use crate::soc::st::tim::{
    BreakInput, BreakSource, CHANNEL_REQUESTS, COMMUTATION_REQUEST, Channel, Counter, DmaRequests,
    Family, Flags, Kind, TRIGGER_REQUEST, Tim, Timer, UPDATE_REQUEST,
};
pub use family::{
    ADVANCED_TIMERS, Advanced, BASIC_TIMERS, Basic, General, Stm32h743Family, TIMER_15, TIMER_16,
    TIMER_17, TIMERS, Tim15, Tim16, Tim17,
};

/// The rate every timer runs at out of reset: 64 MHz. `RCC_CR` resets with the 64 MHz internal oscillator
/// as the system clock, `HSIDIV` at "Division by 1, hsi(_ker)_ck = 64 MHz (default after reset)", RM0433 rev 9
/// section 8.7.2. `RCC_D1CFGR`, `RCC_D2CFGR` and `RCC_CFGR` reset to zero, sections 8.7.7 to 8.7.9, which
/// divides the system clock by nothing on the way to the bus, and with `D2PPRE1` and `D2PPRE2` below
/// `0b100` and `TIMPRE` clear, Table 57 in section 8.5.6 has the timer clock "equal to the bus clock".
pub const RESET_CLOCK_HZ: u64 = 64_000_000;

/// The reset value of `AF1` and `AF2` on the timers with a break: `BKINE` set, the `BKIN` pin enabled as a
/// break source, RM0433 rev 9 sections 38.4.26 to 38.4.29, 41.5.19, 41.6.17 and 41.6.19.
pub const BREAK_INPUT_ENABLED: u64 = 0x0000_0001;

/// What RM0433 rev 9 and ES0392 rev 15 give this part's timers.
pub const PART: Part = Part {
    tables: &[&tim1::REGISTERS],
    spellings: &[Spelling {
        register: "CCMR2_Output",
        manual: "OC4M_3",
        file: "OC4M_4",
    }],
    wakeups: Wakeups::WhenSeen,
    break_pin: None,
    acknowledged: Some(acknowledged_request),
    errata: Errata {
        consecutive_compare_missed: true,
        clear_outlasts_reset: true,
        trigger_lost_at_reload: true,
    },
};

impl<F: Stm32h743Family> Tim<F> {
    /// The timer `timer` at the rate it wakes up with, [`RESET_CLOCK_HZ`], until the part's clock tree
    /// drives it otherwise.
    #[must_use]
    pub fn new(timer: Timer) -> Self {
        Self::with_clock(RESET_CLOCK_HZ, timer)
    }

    /// The level on one source of one break, section 38.3.16. A source nothing has driven asserts
    /// nothing. The break takes the request after its polarity and its filter.
    #[expect(
        clippy::too_many_arguments,
        reason = "the break, its source and the level, and what every call is handed"
    )]
    pub fn drive_break(
        &mut self,
        input: BreakInput,
        source: BreakSource,
        level: Level,
        registers: &mut RegisterFile,
        context: &mut DeviceContext<'_>,
    ) {
        self.drive_break_source(input, source, level, registers, context);
    }
}