@@ -87,6 +87,16 @@ pub struct MipsCore {
8787 /// Key = `(delta >> 16) / 100 * 100`, value = number of occurrences.
8888 #[ cfg( feature = "developer_ip7" ) ]
8989 pub compare_delta_stats : std:: collections:: HashMap < u32 , u32 > ,
90+ /// Learned slow-tick CP0 delta in hardware counts (16.16 fixed-point, >> 16 = integer counts).
91+ /// Initialised to 0 (unknown). First delta seen is assumed to be the 100 Hz (slow) tick.
92+ compare_delta_slow : u64 ,
93+ /// Learned fast-tick CP0 delta in hardware counts.
94+ /// Initialised to 0 (unknown). Set once we see a delta ~10x smaller than delta_slow.
95+ compare_delta_fast : u64 ,
96+ /// The raw 16.16 fixed-point delta programmed in the *previous* Compare write.
97+ /// Used for calibration: dt_ns/dc measure the old interval, so count_step must be
98+ /// computed against the old delta, not the new one. Zero = no previous write yet.
99+ compare_delta_prev : u64 ,
90100 pub cp0_status : u32 , // 12: Status Register
91101 pub cp0_cause : u32 , // 13: Cause Register
92102 pub cp0_epc : u64 , // 14: Exception Program Counter
@@ -232,6 +242,9 @@ impl MipsCore {
232242 compare_last_instant : std:: time:: Instant :: now ( ) ,
233243 #[ cfg( feature = "developer_ip7" ) ]
234244 compare_delta_stats : std:: collections:: HashMap :: new ( ) ,
245+ compare_delta_slow : 0 ,
246+ compare_delta_fast : 0 ,
247+ compare_delta_prev : 0 ,
235248 cp0_status : 0 ,
236249 cp0_cause : 0 ,
237250 cp0_epc : 0 ,
@@ -442,6 +455,54 @@ impl MipsCore {
442455 }
443456 }
444457
458+ /// Bin a CP0 Compare delta into a target tick period in nanoseconds
459+ /// (1_000_000 for 1 kHz, 10_000_000 for 100 Hz).
460+ ///
461+ /// `delta` may be in any consistent unit (e.g. raw 16.16 fixed-point) — only the
462+ /// ratios between slow and fast buckets matter. Maintains two learned buckets:
463+ /// `compare_delta_slow` (100 Hz, seeded on first call) and `compare_delta_fast`
464+ /// (~10x smaller, 1 kHz). All comparisons use ±5% fuzzy equality.
465+ /// Returns `Some(target_ns)` or `None` for a zero/degenerate delta.
466+ fn bin_compare_delta ( & mut self , d : u64 ) -> Option < u64 > {
467+ if d == 0 {
468+ return None ;
469+ }
470+ // ±5% fuzzy equality.
471+ let fuzzy_eq = |a : u64 , b : u64 | -> bool {
472+ let threshold = a. max ( b) * 5 / 100 ;
473+ a. abs_diff ( b) <= threshold
474+ } ;
475+
476+ if self . compare_delta_slow == 0 {
477+ // First delta ever — seed slow bucket, assume 100 Hz.
478+ self . compare_delta_slow = d;
479+ return Some ( 10_000_000 ) ;
480+ }
481+
482+ if fuzzy_eq ( d, self . compare_delta_slow ) {
483+ return Some ( 10_000_000 ) ;
484+ }
485+
486+ if self . compare_delta_fast != 0 && fuzzy_eq ( d, self . compare_delta_fast ) {
487+ return Some ( 1_000_000 ) ;
488+ }
489+
490+ // Check if d ≈ slow/10 (i.e. ~10x smaller → fast tick).
491+ if fuzzy_eq ( d * 10 , self . compare_delta_slow ) {
492+ self . compare_delta_fast = d;
493+ return Some ( 1_000_000 ) ;
494+ }
495+
496+ if d > self . compare_delta_slow {
497+ // Larger than slow — one-shot or low-freq timer; update slow bucket.
498+ self . compare_delta_slow = d;
499+ Some ( 10_000_000 )
500+ } else {
501+ // Unrecognised intermediate — fall back to slow.
502+ Some ( 10_000_000 )
503+ }
504+ }
505+
445506 /// Write CP0 register by index.
446507 /// When reg 12 (Status) is written, invokes `status_changed_cb` with (old, new).
447508 pub fn write_cp0 ( & mut self , reg : u32 , value : u64 ) {
@@ -485,44 +546,45 @@ impl MipsCore {
485546 if self . compare_last_cycles != 0 {
486547 let dc = cycles_now. wrapping_sub ( self . compare_last_cycles ) ;
487548 let dt_ns = now. duration_since ( self . compare_last_instant ) . as_nanos ( ) as u64 ;
488- let delta = self . cp0_compare . wrapping_sub ( self . cp0_count ) ;
489- // Record delta in frequency map (rounded to nearest 100 hw-counts).
549+ // New delta being programmed (what the *next* interval will fire at),
550+ // stored as raw 16.16 fixed-point for use in the next calibration.
551+ let new_delta = self . cp0_compare . wrapping_sub ( self . cp0_count ) ;
490552 #[ cfg( feature = "developer_ip7" ) ]
491553 {
492- let bucket = ( ( delta >> 16 ) as u32 / 100 ) * 100 ;
554+ let bucket = ( ( new_delta >> 16 ) as u32 / 100 ) * 100 ;
493555 * self . compare_delta_stats . entry ( bucket) . or_insert ( 0 ) += 1 ;
494556 }
495- // Snap the intended tick period to 1ms or 10ms based on estimated target:
496- // target_ns = delta * 2 * dt_ns / dc
497- // then use that as the denominator multiplier instead of a fixed 1_000_000.
498- const TARGET_1MS : u64 = 1_000_000 ;
499- const TARGET_10MS : u64 = 10_000_000 ;
500- if dc > 0 {
501- let target_ns = delta. saturating_mul ( dt_ns) / dc * 2 ;
502- // If estimated target is under 3ms, treat as 1kHz tick, else 100Hz.
503- let snapped_ns = if ( target_ns >> 16 ) < 3_000_000 {
504- TARGET_1MS
505- } else {
506- TARGET_10MS
507- } ;
508- let denom = dc. saturating_mul ( snapped_ns) ;
509- self . count_step = ( delta. saturating_mul ( dt_ns) / denom)
510- . clamp ( 1 << 12 , 10 << 15 ) ;
511- #[ cfg( feature = "developer_ip7" ) ]
512- {
513- let total_samples: u32 = self . compare_delta_stats . values ( ) . sum ( ) ;
514- if total_samples <= 5 {
515- eprintln ! ( "compare calib: delta={} dt_ns={} dc={} target_ns={} ({}ms) snapped={}ms count_step={}" ,
516- delta >> 16 , dt_ns, dc,
517- target_ns >> 16 , ( target_ns >> 16 ) / 1_000_000 ,
518- snapped_ns / 1_000_000 ,
519- self . count_step) ;
557+ // dt_ns/dc measure the interval since the last Compare write, which
558+ // ran under compare_delta_prev. Calibrate against that, not new_delta,
559+ // so a tick-rate switch doesn't mix old timing with the new delta.
560+ // If there was no previous delta (first write after reset), skip.
561+ let prev_delta = self . compare_delta_prev ;
562+ if prev_delta != 0 {
563+ if let Some ( snapped_ns) = self . bin_compare_delta ( prev_delta) {
564+ if dc > 0 {
565+ let denom = dc. saturating_mul ( snapped_ns) ;
566+ self . count_step = ( prev_delta. saturating_mul ( dt_ns) / denom)
567+ . clamp ( 1 << 12 , 10 << 15 ) ;
568+ #[ cfg( feature = "developer_ip7" ) ]
569+ {
570+ let total_samples: u32 = self . compare_delta_stats . values ( ) . sum ( ) ;
571+ if total_samples <= 10 {
572+ eprintln ! ( "compare calib: prev_d={} new_d={} dt_ns={} dc={} \
573+ snapped={}ms slow={} fast={} count_step={}",
574+ prev_delta >> 16 , new_delta >> 16 , dt_ns, dc,
575+ snapped_ns / 1_000_000 ,
576+ self . compare_delta_slow >> 16 ,
577+ self . compare_delta_fast >> 16 ,
578+ self . count_step) ;
579+ }
580+ }
581+ } else {
582+ self . count_step = 1 << 15 ;
520583 }
584+ self . count_step_atomic . store ( self . count_step , Ordering :: Relaxed ) ;
521585 }
522- } else {
523- self . count_step = 1 << 15 ;
524586 }
525- self . count_step_atomic . store ( self . count_step , Ordering :: Relaxed ) ;
587+ self . compare_delta_prev = new_delta ;
526588 }
527589 // First write: keep default count_step (1<<15), just record state.
528590 self . compare_last_cycles = cycles_now;
0 commit comments