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:
parent
f831f47adc
commit
abaf7e66fb
|
|
@ -1,2 +1,3 @@
|
||||||
/tsf_sync_rt_starter
|
/tsf_sync_rt_starter
|
||||||
|
/tsf_affine_example
|
||||||
*.o
|
*.o
|
||||||
|
|
|
||||||
13
Makefile
13
Makefile
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 <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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue