From adf225ea5a31b8f0fdb3a1318722c165f0eeabdc Mon Sep 17 00:00:00 2001 From: xarantolus Date: Sat, 16 May 2026 14:23:14 +0000 Subject: [PATCH 1/3] CAN: add frame receive timestamp This allows users to implement time synchronization based on CAN timings. --- machine/cortex-m/src/native/can.rs | 9 ++- machine/cortex-m/src/stub/can.rs | 2 + machine/cortex-m/st/stm32l4/interface/can.c | 28 +++++++- .../cortex-m/st/stm32l4/interface/export.h | 4 +- src/drivers/can.rs | 64 +++++++++++++++++++ 5 files changed, 103 insertions(+), 4 deletions(-) diff --git a/machine/cortex-m/src/native/can.rs b/machine/cortex-m/src/native/can.rs index 8b59bd6..6cbea2f 100644 --- a/machine/cortex-m/src/native/can.rs +++ b/machine/cortex-m/src/native/can.rs @@ -11,6 +11,9 @@ pub struct Frame { pub data: [u8; 8], pub len: u8, pub is_extended: bool, + /// 64-bit-extended bxCAN SOF hardware timestamp (1 tick = 1 CAN + /// bit-time, captured at the SOF sample point). RX only; 0 on Tx. + pub hw_timestamp_rx: u64, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -92,7 +95,8 @@ fn frame_to_c(frame: &Frame) -> bindings::can_frame_t { data: frame.data, len: frame.len, is_extended: frame.is_extended as u8, - reserved: 0, + // Tx: unused (no Tx mailbox sets TGT). + hw_timestamp_rx: 0, } } @@ -102,6 +106,7 @@ fn frame_from_c(c: &bindings::can_frame_t) -> Frame { data: c.data, len: c.len, is_extended: c.is_extended != 0, + hw_timestamp_rx: c.hw_timestamp_rx, } } @@ -143,7 +148,7 @@ pub fn receive(dev: &Device, out: &mut Frame) -> Result { data: [0u8; 8], len: 0, is_extended: 0, - reserved: 0, + hw_timestamp_rx: 0, }; let rc = unsafe { bindings::can_receive(dev.0.index, &mut cframe) }; match rc { diff --git a/machine/cortex-m/src/stub/can.rs b/machine/cortex-m/src/stub/can.rs index 692c401..0edf9a0 100644 --- a/machine/cortex-m/src/stub/can.rs +++ b/machine/cortex-m/src/stub/can.rs @@ -10,6 +10,8 @@ pub struct Frame { pub data: [u8; 8], pub len: u8, pub is_extended: bool, + /// Mirrors `native::can::Frame`; always 0 on the stub (no timer). + pub hw_timestamp_rx: u64, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] diff --git a/machine/cortex-m/st/stm32l4/interface/can.c b/machine/cortex-m/st/stm32l4/interface/can.c index 9741b81..46da0c2 100644 --- a/machine/cortex-m/st/stm32l4/interface/can.c +++ b/machine/cortex-m/st/stm32l4/interface/can.c @@ -43,6 +43,11 @@ static uint32_t s_rx_hw_ovr_fifo1[CAN_SLOT_COUNT]; static uint32_t s_rx_peak_fmp[CAN_SLOT_COUNT]; static uint32_t s_rx_get_fails[CAN_SLOT_COUNT]; +/* Wrap-extension state for the 16-bit bxCAN SOF timestamp: previous + * raw read + accumulated high bits. ISR-only; reset in can_init. */ +static uint16_t s_ts_last[CAN_SLOT_COUNT]; +static uint64_t s_ts_hi[CAN_SLOT_COUNT]; + static struct { can_irq_handler_fn fn; void *ctx; @@ -192,6 +197,10 @@ int can_init(const can_bus_cfg_t *cfg) { rx->tail = 0; rx->count = 0; + /* Reset the per-slot HW-timestamp wrap tracker (see drain_fifo). */ + s_ts_last[cfg->index] = 0; + s_ts_hi[cfg->index] = 0; + s_irqn[cfg->index].rx0_irqn = cfg->rx0_irqn; s_irqn[cfg->index].rx1_irqn = cfg->rx1_irqn; @@ -216,7 +225,9 @@ int can_init(const can_bus_cfg_t *cfg) { h->Init.TimeSeg2 = ts2; h->Init.AutoBusOff = ENABLE; h->Init.AutoRetransmission = ENABLE; - h->Init.TimeTriggeredMode = DISABLE; + /* TTCM=1: latch the bxCAN bit-time counter into RDTxR.TIME at each + * RX SOF. No Tx mailbox sets TGT, so Tx framing is unaffected. */ + h->Init.TimeTriggeredMode = ENABLE; h->Init.AutoWakeUp = DISABLE; /* RFLM=1: keep the oldest 3 frames on overflow; FOVR counts drops. */ h->Init.ReceiveFifoLocked = ENABLE; @@ -345,6 +356,7 @@ int can_receive(uint8_t slot, can_frame_t *out) { out->id = src->id; out->len = src->len; out->is_extended = src->is_extended; + out->hw_timestamp_rx = src->hw_timestamp_rx; memcpy(out->data, (const void *)src->data, sizeof(out->data)); rx->head = (rx->head + 1u) % CAN_RX_BUF_SIZE; @@ -411,6 +423,19 @@ static void drain_fifo(CAN_HandleTypeDef *hcan, uint8_t slot_idx, s_rx_frames_fifo1[slot_idx]++; } + /* Extend the HW SOF timestamp (hdr.Timestamp, 1 tick = 1 CAN + * bit-time) to 64 bits. Frames arrive here in order, so raw < + * previous means one 16-bit wrap. Done before the overflow drop + * so dropped frames still advance the tracker. Caveat: a >65.5 ms + * gap with no RX frame hides a wrap — fine, sync traffic is + * periodic and far faster than that. */ + uint16_t ts_raw = (uint16_t)hdr.Timestamp; + if (ts_raw < s_ts_last[slot_idx]) { + s_ts_hi[slot_idx] += 0x10000ull; + } + s_ts_last[slot_idx] = ts_raw; + uint64_t ts_ext = s_ts_hi[slot_idx] | (uint64_t)ts_raw; + if (rx->count >= CAN_RX_BUF_SIZE) { s_rx_drops[slot_idx]++; continue; @@ -420,6 +445,7 @@ static void drain_fifo(CAN_HandleTypeDef *hcan, uint8_t slot_idx, slot->id = (hdr.IDE == CAN_ID_EXT) ? hdr.ExtId : hdr.StdId; slot->len = (hdr.DLC > 8) ? 8 : (uint8_t)hdr.DLC; slot->is_extended = (hdr.IDE == CAN_ID_EXT); + slot->hw_timestamp_rx = ts_ext; memcpy((void *)slot->data, data, sizeof(data)); rx->tail = (rx->tail + 1u) % CAN_RX_BUF_SIZE; diff --git a/machine/cortex-m/st/stm32l4/interface/export.h b/machine/cortex-m/st/stm32l4/interface/export.h index 0d97dd2..4d1e03e 100644 --- a/machine/cortex-m/st/stm32l4/interface/export.h +++ b/machine/cortex-m/st/stm32l4/interface/export.h @@ -154,7 +154,9 @@ typedef struct uint8_t data[8]; uint8_t len; uint8_t is_extended; - uint16_t reserved; + /* RX only: 64-bit-extended bxCAN SOF timestamp (1 tick = 1 CAN + * bit-time), filled by drain_fifo. 0 on the Tx path. */ + uint64_t hw_timestamp_rx; } can_frame_t; typedef struct diff --git a/src/drivers/can.rs b/src/drivers/can.rs index ed09772..fd06da2 100644 --- a/src/drivers/can.rs +++ b/src/drivers/can.rs @@ -195,3 +195,67 @@ impl Device { pub fn init() { let _ = LazyLock::force(&BUSES); } + +/// Host mirror of the SOF-timestamp wrap-extension in C `drain_fifo()` +/// (`machine/cortex-m/st/stm32l4/interface/can.c`) — keep in sync. +/// Value units: CAN bit-times since boot; wall-time conversion is the +/// consumer's job (divide by bitrate). +#[cfg(test)] +mod hw_ts_extend_spec { + /// One extension step. `last`/`hi`: persisted per-slot state; + /// `raw`: new `TIME[15:0]`. Returns `(new_last, new_hi, extended)`. + fn extend(last: u16, hi: u64, raw: u16) -> (u16, u64, u64) { + let hi = if raw < last { hi + 0x1_0000 } else { hi }; + (raw, hi, hi | raw as u64) + } + + /// Walk a sequence of raw readings from zeroed state, as the ISR + /// does, and collect the extended values. + fn run(raws: &[u16]) -> Vec { + let mut last = 0u16; + let mut hi = 0u64; + let mut out = Vec::new(); + for &r in raws { + let (l, h, ext) = extend(last, hi, r); + last = l; + hi = h; + out.push(ext); + } + out + } + + #[test] + fn monotonic_within_one_epoch_passes_through() { + assert_eq!(run(&[0, 1, 100, 5_000, 65_535]), [0, 1, 100, 5_000, 65_535]); + } + + #[test] + fn single_wrap_carries_into_high_word() { + assert_eq!(run(&[65_500, 30]), [65_500, 0x1_0000 + 30]); + } + + #[test] + fn many_consecutive_wraps_accumulate() { + // Each step is below the previous => one wrap per step. + let v = run(&[60_000, 10, 5, 4, 3]); + assert_eq!( + v, + [60_000, 0x1_0000 + 10, 0x2_0000 + 5, 0x3_0000 + 4, 0x4_0000 + 3] + ); + // strictly monotonic across wraps + assert!(v.windows(2).all(|w| w[1] > w[0])); + } + + #[test] + fn equal_reading_is_not_treated_as_wrap() { + // rule is `<`, not `<=`: equality must not bump hi + assert_eq!(run(&[1234, 1234]), [1234, 1234]); + } + + #[test] + fn idle_gap_longer_than_one_epoch_undercounts_is_known_limitation() { + // Known caveat: a >65.5 ms RX gap hides full wraps (the `<` + // rule sees only one). Fine — sync traffic is far faster. + assert_eq!(run(&[100, 90]), [100, 0x1_0000 + 90]); + } +} From d9b869f0ba1a7f89b743dc54f1a59f112654172e Mon Sep 17 00:00:00 2001 From: Philipp Erhardt Date: Sun, 17 May 2026 07:25:55 +0000 Subject: [PATCH 2/3] Formatting --- src/drivers/can.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/drivers/can.rs b/src/drivers/can.rs index fd06da2..4d68ef2 100644 --- a/src/drivers/can.rs +++ b/src/drivers/can.rs @@ -240,7 +240,13 @@ mod hw_ts_extend_spec { let v = run(&[60_000, 10, 5, 4, 3]); assert_eq!( v, - [60_000, 0x1_0000 + 10, 0x2_0000 + 5, 0x3_0000 + 4, 0x4_0000 + 3] + [ + 60_000, + 0x1_0000 + 10, + 0x2_0000 + 5, + 0x3_0000 + 4, + 0x4_0000 + 3 + ] ); // strictly monotonic across wraps assert!(v.windows(2).all(|w| w[1] > w[0])); From f4806fab15282cac7137a851f88801fecd19aed2 Mon Sep 17 00:00:00 2001 From: Philipp Erhardt Date: Sun, 17 May 2026 07:31:42 +0000 Subject: [PATCH 3/3] Documentation --- machine/cortex-m/st/stm32l4/interface/can.c | 7 ++++--- src/drivers/can.rs | 11 +++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/machine/cortex-m/st/stm32l4/interface/can.c b/machine/cortex-m/st/stm32l4/interface/can.c index 46da0c2..9c35980 100644 --- a/machine/cortex-m/st/stm32l4/interface/can.c +++ b/machine/cortex-m/st/stm32l4/interface/can.c @@ -426,9 +426,10 @@ static void drain_fifo(CAN_HandleTypeDef *hcan, uint8_t slot_idx, /* Extend the HW SOF timestamp (hdr.Timestamp, 1 tick = 1 CAN * bit-time) to 64 bits. Frames arrive here in order, so raw < * previous means one 16-bit wrap. Done before the overflow drop - * so dropped frames still advance the tracker. Caveat: a >65.5 ms - * gap with no RX frame hides a wrap — fine, sync traffic is - * periodic and far faster than that. */ + * so dropped frames still advance the tracker. Caveat: an RX gap + * longer than 65536 bit-times (one counter period; ~65.5 ms at + * 1 Mbit/s, scaling with bitrate) hides a wrap — fine, sync + * traffic is periodic and far faster than that. */ uint16_t ts_raw = (uint16_t)hdr.Timestamp; if (ts_raw < s_ts_last[slot_idx]) { s_ts_hi[slot_idx] += 0x10000ull; diff --git a/src/drivers/can.rs b/src/drivers/can.rs index 4d68ef2..97aae83 100644 --- a/src/drivers/can.rs +++ b/src/drivers/can.rs @@ -198,8 +198,10 @@ pub fn init() { /// Host mirror of the SOF-timestamp wrap-extension in C `drain_fifo()` /// (`machine/cortex-m/st/stm32l4/interface/can.c`) — keep in sync. -/// Value units: CAN bit-times since boot; wall-time conversion is the -/// consumer's job (divide by bitrate). +/// Value units: CAN bit-times since CAN init / peripheral reset (the +/// bxCAN counter is zeroed on every `can_init`, not tied to system +/// uptime); wall-time conversion is the consumer's job (divide by +/// bitrate). #[cfg(test)] mod hw_ts_extend_spec { /// One extension step. `last`/`hi`: persisted per-slot state; @@ -260,8 +262,9 @@ mod hw_ts_extend_spec { #[test] fn idle_gap_longer_than_one_epoch_undercounts_is_known_limitation() { - // Known caveat: a >65.5 ms RX gap hides full wraps (the `<` - // rule sees only one). Fine — sync traffic is far faster. + // Known caveat: an RX gap longer than 65536 bit-times (one + // counter period) hides full wraps (the `<` rule sees only + // one). Fine — sync traffic is far faster. assert_eq!(run(&[100, 90]), [100, 0x1_0000 + 90]); } }