Skip to main content

can_hal/
timing.rs

1//! CAN bit-timing types.
2//!
3//! These are validated value types - invalid configurations are unrepresentable
4//! at the type level rather than caught at `connect()` time.
5
6/// Bit-timing sample point as a fraction of the bit time.
7///
8/// Stored internally as per-mille (e.g., `875` for `87.5%`). The CAN
9/// specification recommends sample points in the 70%–87.5% range; this type
10/// enforces `[500, 950]` per-mille (50%–95%) so out-of-range values are
11/// rejected before they reach a backend's timing solver.
12///
13/// Construct via [`SamplePoint::from_per_mille`] (`const fn`, so out-of-range
14/// literals fail at compile time when wrapped in a `const` context), or via
15/// the preset constants:
16///
17/// ```
18/// use can_hal::SamplePoint;
19///
20/// // Compile-time literal (asserted at compile time when used in a const context):
21/// const SP_87_5: SamplePoint = SamplePoint::from_per_mille(875);
22/// assert_eq!(SP_87_5.per_mille(), 875);
23///
24/// // Or use a preset:
25/// assert_eq!(SamplePoint::NOMINAL_DEFAULT.per_mille(), 700);
26/// assert_eq!(SamplePoint::DATA_DEFAULT.per_mille(), 800);
27/// ```
28///
29/// Out-of-range per-mille values panic - at compile time in a `const`
30/// context, or at runtime otherwise:
31///
32/// ```compile_fail
33/// # use can_hal::SamplePoint;
34/// const TOO_HIGH: SamplePoint = SamplePoint::from_per_mille(2000);
35/// ```
36///
37/// ```should_panic
38/// # use can_hal::SamplePoint;
39/// // Runtime construction with an out-of-range value panics.
40/// SamplePoint::from_per_mille(2000);
41/// ```
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct SamplePoint(u16);
44
45impl SamplePoint {
46    /// Default nominal-phase sample point: 70%. Matches the cross-adapter
47    /// interop convention used by the PCAN and Kvaser backends in this
48    /// workspace.
49    pub const NOMINAL_DEFAULT: Self = Self::from_per_mille(700);
50
51    /// Default data-phase sample point: 80%. Matches the cross-adapter
52    /// interop convention used by the PCAN and Kvaser backends in this
53    /// workspace.
54    pub const DATA_DEFAULT: Self = Self::from_per_mille(800);
55
56    /// 75% sample point.
57    pub const PCT_75: Self = Self::from_per_mille(750);
58
59    /// 87.5% sample point - a common industrial recommendation for
60    /// classic CAN at modest bus lengths.
61    pub const PCT_87_5: Self = Self::from_per_mille(875);
62
63    /// Construct from per-mille (e.g., `875` for `87.5%`, `700` for `70%`).
64    ///
65    /// Panics if `per_mille` is outside `[500, 950]`. In a `const` context
66    /// (i.e., when initializing a `const` binding or inside `const { ... }`),
67    /// this panic occurs at compile time.
68    #[must_use]
69    pub const fn from_per_mille(per_mille: u16) -> Self {
70        assert!(
71            per_mille >= 500 && per_mille <= 950,
72            "sample point per-mille must be in [500, 950]"
73        );
74        Self(per_mille)
75    }
76
77    /// Get the per-mille value (e.g., `875` for `87.5%`).
78    #[must_use]
79    pub const fn per_mille(self) -> u16 {
80        self.0
81    }
82
83    /// Convert to a fraction (e.g., `0.875` for `87.5%`). Not `const`
84    /// because f32 division isn't `const`-stable on this crate's MSRV.
85    #[must_use]
86    pub fn as_fraction(self) -> f32 {
87        f32::from(self.0) / 1000.0
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn nominal_default_is_70_percent() {
97        assert_eq!(SamplePoint::NOMINAL_DEFAULT.per_mille(), 700);
98        assert!((SamplePoint::NOMINAL_DEFAULT.as_fraction() - 0.70).abs() < 1e-6);
99    }
100
101    #[test]
102    fn data_default_is_80_percent() {
103        assert_eq!(SamplePoint::DATA_DEFAULT.per_mille(), 800);
104        assert!((SamplePoint::DATA_DEFAULT.as_fraction() - 0.80).abs() < 1e-6);
105    }
106
107    #[test]
108    fn pct_87_5_is_875_per_mille() {
109        assert_eq!(SamplePoint::PCT_87_5.per_mille(), 875);
110        assert!((SamplePoint::PCT_87_5.as_fraction() - 0.875).abs() < 1e-6);
111    }
112
113    #[test]
114    fn from_per_mille_accepts_range_bounds() {
115        assert_eq!(SamplePoint::from_per_mille(500).per_mille(), 500);
116        assert_eq!(SamplePoint::from_per_mille(950).per_mille(), 950);
117    }
118
119    #[test]
120    #[should_panic = "sample point per-mille must be in [500, 950]"]
121    #[allow(clippy::let_underscore_must_use)]
122    fn from_per_mille_rejects_too_low() {
123        let _ = SamplePoint::from_per_mille(499);
124    }
125
126    #[test]
127    #[should_panic = "sample point per-mille must be in [500, 950]"]
128    #[allow(clippy::let_underscore_must_use)]
129    fn from_per_mille_rejects_too_high() {
130        let _ = SamplePoint::from_per_mille(951);
131    }
132}