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_affine_example
|
||||
*.o
|
||||
|
|
|
|||
13
Makefile
13
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
|
||||
|
|
|
|||
|
|
@ -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