From abaf7e66fb4ba4683d4d0e303184dabbb1cbea94 Mon Sep 17 00:00:00 2001 From: Robert McMahon Date: Tue, 31 Mar 2026 20:10:30 -0700 Subject: [PATCH] Add affine TSF pool for master-to-radio mapping Introduce tsf_affine (sliding-window LS fit) for N radios with one master identity and fitted maps for the rest. Add example binary target and team email describing the software timebase approach. Ignore built tsf_affine_example alongside the existing starter. Made-with: Cursor --- .gitignore | 1 + Makefile | 13 +- TEAM_EMAIL_affine_tsf_mapping.txt | 65 ++++++++++ tsf_affine.c | 206 ++++++++++++++++++++++++++++++ tsf_affine.h | 60 +++++++++ tsf_affine_example.c | 66 ++++++++++ 6 files changed, 409 insertions(+), 2 deletions(-) create mode 100644 TEAM_EMAIL_affine_tsf_mapping.txt create mode 100644 tsf_affine.c create mode 100644 tsf_affine.h create mode 100644 tsf_affine_example.c diff --git a/.gitignore b/.gitignore index c2b800b..9714dcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /tsf_sync_rt_starter +/tsf_affine_example *.o diff --git a/Makefile b/Makefile index 06c0099..b862b34 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,21 @@ LDLIBS = -lpthread -lm TARGET = tsf_sync_rt_starter SRC = tsf_sync_rt_starter.c -all: $(TARGET) +AFFINE_OBJ = tsf_affine.o +EXAMPLE = tsf_affine_example + +all: $(TARGET) $(EXAMPLE) $(TARGET): $(SRC) $(CC) $(CFLAGS) -o $@ $(SRC) $(LDLIBS) +$(AFFINE_OBJ): tsf_affine.c tsf_affine.h + $(CC) $(CFLAGS) -c -o $@ tsf_affine.c + +$(EXAMPLE): tsf_affine_example.c $(AFFINE_OBJ) + $(CC) $(CFLAGS) -o $@ tsf_affine_example.c $(AFFINE_OBJ) -lm + clean: - rm -f $(TARGET) + rm -f $(TARGET) $(EXAMPLE) $(AFFINE_OBJ) .PHONY: all clean diff --git a/TEAM_EMAIL_affine_tsf_mapping.txt b/TEAM_EMAIL_affine_tsf_mapping.txt new file mode 100644 index 0000000..ce10f89 --- /dev/null +++ b/TEAM_EMAIL_affine_tsf_mapping.txt @@ -0,0 +1,65 @@ +Subject: FiWiTSF — software TSF mapping (no per-radio HW adjustments) + +Team, + +We are changing how we relate timing across radios. + +Background +---------- +Today we can step each radio’s hardware TSF toward a master using mac80211 +debugfs (or a future non-debugfs hook). That works, but it means frequent +writes, driver interaction, and coupling to whatever the MAC thinks “TSF” is +for medium access. + +Proposed approach +----------------- +1. Pick one radio as the **master** timeline. All scheduling and coordination + are expressed in **master TSF time**. + +2. **Do not** continuously adjust each radio head’s hardware TSF. Leave each + chip’s TSF running freely. + +3. For every other radio, maintain a **linear (affine) map** between master + TSF and that radio’s TSF: + + t_radio ≈ α_i · t_master + β_i + + Offset, drift, and measurement noise are absorbed by updating (α_i, β_i) + from paired samples (t_master, t_radio) taken in a scheduling thread. + Filters (e.g. bounded window least-squares, Kalman on offset + skew) can + sit on top of the same idea. + +4. **Per-thread timing:** each radio’s sampling / bookkeeping can use its own + `clock_nanosleep` slot; we are not trying to wake every thread at the same + instant. Barriers or ordering are only needed where we need a consistent + snapshot (e.g. one master read shared across followers for a given cycle). + +5. **Lookup:** given a master TSF value, we **predict** each radio’s TSF for + scheduling decisions in software. Inverse maps (radio → master) are just + the affine inverse when α_i ≠ 0. + +Scale (24 radios) +----------------- +With **24 radios** and **one** designated master, we need **23** affine maps +(one per non-master radio). The master is the identity map: +t_radio_master = t_master (α = 1, β = 0). + +Caveats +------- +• This gives a unified **software** timebase. Features that truly require + hardware TSF alignment to the master (specific offload, certain MAC + behaviors) still need separate consideration. + +• Maps must be **refreshed**; clocks drift. A sliding window or recursive + estimator keeps (α, β) stable under noise. + +• PCIe isolation and spare cores reduce **contention**; they do not remove + scheduler jitter. The affine layer is what makes “free-running” radio TSFs + usable for coordinated scheduling. + +We will land a small C module in FiWiTSF for the pool of maps, sample updates, +and master→per-radio lookup; the existing RT sync binary remains available for +labs that still want HW stepping. + +Thanks, +[Your name] diff --git a/tsf_affine.c b/tsf_affine.c new file mode 100644 index 0000000..6337a33 --- /dev/null +++ b/tsf_affine.c @@ -0,0 +1,206 @@ +/* SPDX-License-Identifier: GPL-2.0 OR MIT */ + +#include "tsf_affine.h" + +#include +#include +#include + +/* TSF is uint64 µs; add a signed correction with defined wrap at 2^64. */ +static uint64_t tsf_add_s64(uint64_t base, int64_t delta) +{ + if (delta >= 0) + return base + (uint64_t)delta; + return base - (uint64_t)(-(unsigned long long)delta); +} + +static void map_refit(struct tsf_affine_map *m) +{ + size_t n = m->win_count; + double sum_dm = 0.0, sum_dr = 0.0; + double var_dm = 0.0, cov_dm_dr = 0.0; + size_t i; + + if (n == 0) { + m->alpha = 1.0; + m->beta = 0.0; + return; + } + if (n == 1) { + int64_t dm0 = m->dm[0]; + int64_t dr0 = m->dr[0]; + + m->alpha = 1.0; + m->beta = (double)dr0 - (double)dm0; + return; + } + + for (i = 0; i < n; i++) { + sum_dm += (double)m->dm[i]; + sum_dr += (double)m->dr[i]; + } + { + double mean_dm = sum_dm / (double)n; + double mean_dr = sum_dr / (double)n; + + for (i = 0; i < n; i++) { + double x = (double)m->dm[i] - mean_dm; + double y = (double)m->dr[i] - mean_dr; + + var_dm += x * x; + cov_dm_dr += x * y; + } + if (var_dm < 1e-6) { + m->alpha = 1.0; + m->beta = mean_dr - mean_dm; + } else { + m->alpha = cov_dm_dr / var_dm; + m->beta = mean_dr - m->alpha * mean_dm; + } + } +} + +static int map_push_sample(struct tsf_affine_map *m, uint64_t master_tsf, + uint64_t radio_tsf) +{ + int64_t dm, dr; + + if (!m->have_anchor) { + m->anchor_master = master_tsf; + m->anchor_radio = radio_tsf; + m->have_anchor = 1; + m->dm[0] = 0; + m->dr[0] = 0; + m->win_count = 1; + m->win_pos = 1 % m->win_cap; + map_refit(m); + return 0; + } + + dm = (int64_t)(master_tsf - m->anchor_master); + dr = (int64_t)(radio_tsf - m->anchor_radio); + + if (m->win_count < m->win_cap) { + m->dm[m->win_count] = dm; + m->dr[m->win_count] = dr; + m->win_count++; + m->win_pos = m->win_count % m->win_cap; + } else { + m->dm[m->win_pos] = dm; + m->dr[m->win_pos] = dr; + m->win_pos = (m->win_pos + 1) % m->win_cap; + } + map_refit(m); + return 0; +} + +static uint64_t predict_radio(const struct tsf_affine_map *m, uint64_t master_tsf) +{ + int64_t dm; + double dr_est; + + if (!m->have_anchor) + return master_tsf; + dm = (int64_t)(master_tsf - m->anchor_master); + dr_est = m->alpha * (double)dm + m->beta; + return tsf_add_s64(m->anchor_radio, (int64_t)llround(dr_est)); +} + +int tsf_affine_pool_init(struct tsf_affine_pool *p, unsigned int n_radios, + unsigned int master_idx, size_t window_cap) +{ + unsigned int i; + + if (!p || n_radios == 0 || n_radios > TSF_AFFINE_MAX_RADIOS || + master_idx >= n_radios || window_cap == 0) + return -1; + + memset(p, 0, sizeof(*p)); + p->n_radios = n_radios; + p->master_idx = master_idx; + + for (i = 0; i < n_radios; i++) { + struct tsf_affine_map *m = &p->maps[i]; + + m->alpha = 1.0; + m->beta = 0.0; + m->have_anchor = 0; + if (i == master_idx) { + m->win_cap = 0; + m->dm = NULL; + m->dr = NULL; + continue; + } + m->win_cap = window_cap; + m->dm = calloc(window_cap, sizeof(int64_t)); + m->dr = calloc(window_cap, sizeof(int64_t)); + if (!m->dm || !m->dr) { + tsf_affine_pool_fini(p); + return -1; + } + } + return 0; +} + +void tsf_affine_pool_fini(struct tsf_affine_pool *p) +{ + unsigned int i, n; + + if (!p) + return; + n = p->n_radios; + for (i = 0; i < n; i++) { + free(p->maps[i].dm); + free(p->maps[i].dr); + p->maps[i].dm = NULL; + p->maps[i].dr = NULL; + } + memset(p, 0, sizeof(*p)); +} + +void tsf_affine_pool_sample(struct tsf_affine_pool *p, unsigned int radio_idx, + uint64_t master_tsf, uint64_t radio_tsf) +{ + if (!p || radio_idx >= p->n_radios || radio_idx == p->master_idx) + return; + map_push_sample(&p->maps[radio_idx], master_tsf, radio_tsf); +} + +bool tsf_affine_map_ready(const struct tsf_affine_map *m) +{ + return m && m->have_anchor; +} + +bool tsf_affine_pool_master_to_radio(const struct tsf_affine_pool *p, + unsigned int radio_idx, uint64_t master_tsf, + uint64_t *out_radio_tsf) +{ + if (!p || !out_radio_tsf || radio_idx >= p->n_radios) + return false; + if (radio_idx == p->master_idx) { + *out_radio_tsf = master_tsf; + return true; + } + if (!tsf_affine_map_ready(&p->maps[radio_idx])) + return false; + *out_radio_tsf = predict_radio(&p->maps[radio_idx], master_tsf); + return true; +} + +void tsf_affine_pool_master_to_all(const struct tsf_affine_pool *p, + uint64_t master_tsf, + uint64_t out_radio[TSF_AFFINE_MAX_RADIOS]) +{ + unsigned int i; + + if (!p || !out_radio) + return; + for (i = 0; i < p->n_radios; i++) { + if (i == p->master_idx) + out_radio[i] = master_tsf; + else if (tsf_affine_map_ready(&p->maps[i])) + out_radio[i] = predict_radio(&p->maps[i], master_tsf); + else + out_radio[i] = 0; + } +} diff --git a/tsf_affine.h b/tsf_affine.h new file mode 100644 index 0000000..b34a4bb --- /dev/null +++ b/tsf_affine.h @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: GPL-2.0 OR MIT */ +/* + * Affine maps from master TSF to each radio TSF: t_radio ≈ α·t_master + β. + * One master among N radios → (N−1) fitted maps + identity for the master. + */ + +#ifndef TSF_AFFINE_H +#define TSF_AFFINE_H + +#include +#include +#include + +#define TSF_AFFINE_MAX_RADIOS 32 + +struct tsf_affine_map { + double alpha; + double beta; + uint64_t anchor_master; + uint64_t anchor_radio; + int have_anchor; + /* Window of (dm, dr) with dm = master - anchor_master, dr = radio - anchor_radio */ + int64_t *dm; + int64_t *dr; + size_t win_cap; + size_t win_count; + size_t win_pos; /* next write index */ +}; + +struct tsf_affine_pool { + unsigned int n_radios; + unsigned int master_idx; + struct tsf_affine_map maps[TSF_AFFINE_MAX_RADIOS]; +}; + +int tsf_affine_pool_init(struct tsf_affine_pool *p, unsigned int n_radios, + unsigned int master_idx, size_t window_cap); + +void tsf_affine_pool_fini(struct tsf_affine_pool *p); + +/* Push one simultaneous pair (same physical instant). Ignored for master_idx. */ +void tsf_affine_pool_sample(struct tsf_affine_pool *p, unsigned int radio_idx, + uint64_t master_tsf, uint64_t radio_tsf); + +/* + * Given master TSF, fill out_radio[i] for all i. + * Master index: out_radio[master_idx] == master_tsf exactly. + */ +void tsf_affine_pool_master_to_all(const struct tsf_affine_pool *p, + uint64_t master_tsf, + uint64_t out_radio[TSF_AFFINE_MAX_RADIOS]); + +/* Single radio; returns false if radio_idx invalid or map not yet usable. */ +bool tsf_affine_pool_master_to_radio(const struct tsf_affine_pool *p, + unsigned int radio_idx, uint64_t master_tsf, + uint64_t *out_radio_tsf); + +bool tsf_affine_map_ready(const struct tsf_affine_map *m); + +#endif diff --git a/tsf_affine_example.c b/tsf_affine_example.c new file mode 100644 index 0000000..c08231d --- /dev/null +++ b/tsf_affine_example.c @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: GPL-2.0 OR MIT */ +/* Example: 24 radios, radio 0 = master → 23 affine maps. */ + +#include "tsf_affine.h" + +#include +#include +#include + +#define N_RADIO 24u +#define MASTER 0u +#define WINDOW 32u + +/* Toy “true” radio TSF: same rate as master, fixed offset per index (for sanity check). */ +static uint64_t true_radio_tsf(unsigned int idx, uint64_t master_tsf) +{ + if (idx == MASTER) + return master_tsf; + return master_tsf + (uint64_t)idx * 1000000u; +} + +int main(void) +{ + struct tsf_affine_pool pool; + uint64_t out[TSF_AFFINE_MAX_RADIOS]; + uint64_t m; + unsigned k, i; + + if (tsf_affine_pool_init(&pool, N_RADIO, MASTER, WINDOW) != 0) { + perror("tsf_affine_pool_init"); + return 1; + } + + /* Train: several snapshots */ + for (k = 0; k < 8; k++) { + m = 9000000000000ull + (uint64_t)k * 5000000ull; + for (i = 0; i < N_RADIO; i++) + tsf_affine_pool_sample(&pool, i, m, true_radio_tsf(i, m)); + } + + m = 9000000000000ull + 100000000ull; + tsf_affine_pool_master_to_all(&pool, m, out); + + printf("master TSF=%" PRIu64 " (radio %u identity)\n", m, + (unsigned)MASTER); + for (i = 0; i < N_RADIO; i++) { + uint64_t truth = true_radio_tsf(i, m); + int64_t err; + + if (i == MASTER) { + printf(" [%2u] pred=%" PRIu64 " (master)\n", i, out[i]); + continue; + } + if (out[i] == 0 && !tsf_affine_map_ready(&pool.maps[i])) { + printf(" [%2u] map not ready\n", i); + continue; + } + err = (int64_t)(out[i] - truth); + printf(" [%2u] pred=%" PRIu64 " truth=%" PRIu64 " err=%" PRId64 + " µs\n", + i, out[i], truth, err); + } + + tsf_affine_pool_fini(&pool); + return 0; +}