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
This commit is contained in:
Robert McMahon 2026-03-31 20:10:30 -07:00
parent f831f47adc
commit abaf7e66fb
6 changed files with 409 additions and 2 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/tsf_sync_rt_starter /tsf_sync_rt_starter
/tsf_affine_example
*.o *.o

View File

@ -4,12 +4,21 @@ LDLIBS = -lpthread -lm
TARGET = tsf_sync_rt_starter TARGET = tsf_sync_rt_starter
SRC = tsf_sync_rt_starter.c SRC = tsf_sync_rt_starter.c
all: $(TARGET) AFFINE_OBJ = tsf_affine.o
EXAMPLE = tsf_affine_example
all: $(TARGET) $(EXAMPLE)
$(TARGET): $(SRC) $(TARGET): $(SRC)
$(CC) $(CFLAGS) -o $@ $(SRC) $(LDLIBS) $(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: clean:
rm -f $(TARGET) rm -f $(TARGET) $(EXAMPLE) $(AFFINE_OBJ)
.PHONY: all clean .PHONY: all clean

View File

@ -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 radios 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 heads hardware TSF. Leave each
chips TSF running freely.
3. For every other radio, maintain a **linear (affine) map** between master
TSF and that radios 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 radios 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 radios 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]

206
tsf_affine.c Normal file
View File

@ -0,0 +1,206 @@
/* SPDX-License-Identifier: GPL-2.0 OR MIT */
#include "tsf_affine.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
/* 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;
}
}

60
tsf_affine.h Normal file
View File

@ -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 (N1) fitted maps + identity for the master.
*/
#ifndef TSF_AFFINE_H
#define TSF_AFFINE_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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

66
tsf_affine_example.c Normal file
View File

@ -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 <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#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;
}