Skip to content

Commit 7cf38e5

Browse files
josephnefclaude
andcommitted
Wi-Fi hardware-timestamping prototypes: USB RX SO_TIMESTAMPING + PCIe PHC/rtw88
Out-of-band reference tooling behind the timing findings in docs/timing-accuracy.md. None of it is part of the devourer library build. USB (surfaces the vendor rtl88x2cu RX hardware timestamp): - Bump the reference/rtl88x2cu submodule to the merged CONFIG_RTW_HWTSTAMP driver (attaches the RX-descriptor TSF to skb_hwtstamps -> SO_TIMESTAMPING). - tests/rx_hwtstamp_probe.c: AF_PACKET + SO_TIMESTAMPING reader. - tests/rx_hwtstamp_validate.sh: capability check (driver swap + ethtool -T). - tests/rx_hwtstamp_onair.sh: on-air value check against a devourer open AP. PCIe (RTL8821CE, rtw88): - tests/pcie_phc/: a passive PTP hardware clock off the MAC TSF over BAR2 MMIO (rtw88 stays bound), a gettime-latency probe, an Intel-I226 cross-check, and a one-hunk rtw88 RX skb_hwtstamps patch. See tests/pcie_phc/README.md. - tests/pcie_tsf_read.c: userspace BAR2 TSF reader. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ab3ecfa commit 7cf38e5

12 files changed

Lines changed: 912 additions & 1 deletion

tests/pcie_phc/Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Out-of-tree build of the RTL8821CE TSF PHC prototype.
2+
# make # build against the running kernel
3+
# make KDIR=/path # build against a specific kernel tree
4+
obj-m += rtl8821ce_phc.o
5+
6+
KDIR ?= /lib/modules/$(shell uname -r)/build
7+
PWD := $(shell pwd)
8+
9+
all:
10+
$(MAKE) -C $(KDIR) M=$(PWD) modules
11+
12+
clean:
13+
$(MAKE) -C $(KDIR) M=$(PWD) clean

tests/pcie_phc/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# PCIe hardware-timestamping / PTP prototypes (RTL8821CE)
2+
3+
Reference tooling behind the PCIe timing numbers in
4+
[`docs/timing-accuracy.md`](../../docs/timing-accuracy.md). These are kernel-side
5+
experiments on the Radxa X4's onboard RTL8821CE (`rtw88`), run out-of-band — they
6+
are **not** part of the devourer library build.
7+
8+
## Contents
9+
10+
- `rtl8821ce_phc.c` + `Makefile` — a passive PTP hardware clock (`/dev/ptpN`)
11+
backed by the 8821CE's 802.11 MAC TSF, read over BAR2 MMIO. It does **not** bind
12+
the PCI device: `rtw88` stays bound and keeps the chip alive, and the module
13+
only `ioremap`s the BAR and reads the free-running TSF, exposed as a
14+
disciplinable clock via `timecounter`/`cyclecounter`.
15+
- `validate.sh` — builds, loads, and exercises the PHC with `phc_ctl` / `phc2sys`.
16+
- `pcie_phc_lat.c` — measures the PHC gettime read window
17+
(`PTP_SYS_OFFSET_EXTENDED`): the PCIe MMIO floor (~3.7 µs), versus tens of µs
18+
for the equivalent read over USB.
19+
- `ptp_crosscheck.sh` — runs `ptp4l` on the board's Intel I226 Ethernet (a real
20+
IEEE-1588 NIC) and cross-compares the Wi-Fi TSF PHC against it on one machine.
21+
- `rtw88_rx_hwtstamp.patch` — a one-hunk `rtw88` patch attaching the RX-descriptor
22+
TSF to `skb_hwtstamps` before `ieee80211_rx_napi`, so a `SO_TIMESTAMPING` socket
23+
reads the 802.11 hardware RX timestamp on the PCIe / `mac80211` path.
24+
25+
## Requirements
26+
27+
Kernel headers, `linuxptp`, and `ethtool` on the target. The MAC TSF is
28+
power-gated while the card is idle — bring the interface up in monitor mode (or
29+
associate) so the TSF is clocked before loading the PHC.
30+
31+
## Scope
32+
33+
These explore hardware-RX timestamping and a real PHC on the PCIe part. Two-way
34+
`ptp4l` over the Wi-Fi radio remains blocked on a host-reported TX-egress
35+
timestamp (a firmware limitation); the board's I226 Ethernet is the real-PTP
36+
reference. See `docs/timing-accuracy.md` for the measured results and that limit.

tests/pcie_phc/pcie_phc_lat.c

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// pcie_phc_lat.c — measure the PHC gettime floor of /dev/ptpN via the
2+
// PTP_SYS_OFFSET_EXTENDED ioctl. Each sample brackets the driver's device read
3+
// with a system-clock pre/post timestamp; (post - pre) is the time the PHC
4+
// spends reading its hardware clock — for the RTL8821CE TSF PHC that is a BAR2
5+
// MMIO load. This is the number that makes a PCIe PHC "tractable": ~us and low
6+
// jitter, versus a ~68 us jittery USB control transfer.
7+
//
8+
// Build: cc -O2 -o pcie_phc_lat pcie_phc_lat.c
9+
// Run: sudo ./pcie_phc_lat /dev/ptpN [n_samples]
10+
#include <fcntl.h>
11+
#include <linux/ptp_clock.h>
12+
#include <stdint.h>
13+
#include <stdio.h>
14+
#include <stdlib.h>
15+
#include <sys/ioctl.h>
16+
#include <unistd.h>
17+
18+
static uint64_t ns(struct ptp_clock_time t) { return t.sec * 1000000000ull + t.nsec; }
19+
static int cmp(const void *a, const void *b) {
20+
uint64_t x = *(const uint64_t *)a, y = *(const uint64_t *)b;
21+
return x < y ? -1 : x > y ? 1 : 0;
22+
}
23+
24+
int main(int argc, char **argv) {
25+
const char *dev = argc > 1 ? argv[1] : "/dev/ptp0";
26+
int n = argc > 2 ? atoi(argv[2]) : 25;
27+
if (n < 1) n = 1;
28+
if (n > PTP_MAX_SAMPLES) n = PTP_MAX_SAMPLES;
29+
30+
int fd = open(dev, O_RDONLY);
31+
if (fd < 0) { perror("open ptp"); return 1; }
32+
33+
struct ptp_sys_offset_extended off = {0};
34+
off.n_samples = n;
35+
if (ioctl(fd, PTP_SYS_OFFSET_EXTENDED, &off) < 0) { perror("PTP_SYS_OFFSET_EXTENDED"); return 1; }
36+
37+
uint64_t *win = malloc(sizeof(uint64_t) * n);
38+
double mean = 0;
39+
for (int i = 0; i < n; i++) {
40+
uint64_t pre = ns(off.ts[i][0]); // system before device read
41+
uint64_t post = ns(off.ts[i][2]); // system after device read
42+
win[i] = post - pre;
43+
mean += win[i];
44+
}
45+
mean /= n;
46+
qsort(win, n, sizeof(uint64_t), cmp);
47+
printf("PHC gettime read window over %d samples (%s):\n", n, dev);
48+
printf(" mean=%.0f ns p50=%llu ns p99=%llu ns min=%llu ns max=%llu ns\n",
49+
mean, (unsigned long long)win[n / 2], (unsigned long long)win[n * 99 / 100],
50+
(unsigned long long)win[0], (unsigned long long)win[n - 1]);
51+
// Also show the device (PHC) time from the middle sample, for sanity.
52+
printf(" phc time (mid sample) = %llu ns\n", (unsigned long long)ns(off.ts[n / 2][1]));
53+
return 0;
54+
}

tests/pcie_phc/ptp_crosscheck.sh

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
# ptp_crosscheck.sh — on the radxa-x4, (1) show the Intel I226 Ethernet doing
3+
# real IEEE-1588 hardware PTP, and (2) cross-compare the 8821CE Wi-Fi TSF PHC
4+
# against that real PTP clock on the SAME machine — quantifying the Wi-Fi TSF's
5+
# frequency error and short-term stability versus genuine PTP hardware, with no
6+
# second machine and no network dependency.
7+
#
8+
# sudo tests/pcie_phc/ptp_crosscheck.sh
9+
set -uo pipefail
10+
cd "$(dirname "$0")"
11+
ETH=enp2s0
12+
WIF=wlp1s0
13+
MOD=rtl8821ce_phc
14+
15+
cleanup() {
16+
echo "[xchk] cleanup"
17+
sudo pkill -x ptp4l phc2sys 2>/dev/null
18+
sudo rmmod "$MOD" 2>/dev/null
19+
sudo ip link set "$WIF" down 2>/dev/null
20+
sudo iw dev "$WIF" set type managed 2>/dev/null
21+
sudo ip link set "$WIF" up 2>/dev/null
22+
command -v nmcli >/dev/null && sudo nmcli dev set "$WIF" managed yes 2>/dev/null
23+
}
24+
trap cleanup EXIT
25+
26+
# --- find the igc (Ethernet) PHC ---
27+
IGC_PTP=""
28+
for d in /sys/class/ptp/ptp*; do
29+
n=$(cat "$d/clock_name" 2>/dev/null)
30+
case "$n" in
31+
rtl8821ce_tsf) : ;; # skip ours if already loaded
32+
*) [ -e "/dev/$(basename "$d")" ] && IGC_PTP="/dev/$(basename "$d")" ;;
33+
esac
34+
done
35+
echo "[xchk] Ethernet (I226) PHC = ${IGC_PTP:-<none>}"
36+
37+
# --- 1. prove the I226 does hardware-timestamped PTP ---
38+
echo "[xchk] === ptp4l on $ETH (I226): does it use HARDWARE timestamping? ==="
39+
sudo timeout 8 ptp4l -i "$ETH" -m 2>&1 | grep -iE "clock|hardware|selected|MASTER|assuming|state" | head -6 || true
40+
41+
# --- put the Wi-Fi MAC in monitor mode so the TSF ticks, then load our PHC ---
42+
echo "[xchk] arming Wi-Fi TSF PHC"
43+
sudo pkill -x wpa_supplicant 2>/dev/null
44+
command -v nmcli >/dev/null && sudo nmcli dev set "$WIF" managed no 2>/dev/null
45+
sleep 1
46+
sudo ip link set "$WIF" down; sudo iw dev "$WIF" set type monitor 2>/dev/null; sudo ip link set "$WIF" up
47+
sudo iw dev "$WIF" set channel 6 2>/dev/null
48+
sleep 1
49+
make -s >/dev/null 2>&1
50+
sudo rmmod "$MOD" 2>/dev/null
51+
sudo insmod "./$MOD.ko" || { echo "[xchk] PHC insmod failed"; exit 1; }
52+
sleep 1
53+
WIF_PTP=""
54+
for d in /sys/class/ptp/ptp*; do
55+
[ "$(cat "$d/clock_name" 2>/dev/null)" = "rtl8821ce_tsf" ] && WIF_PTP="/dev/$(basename "$d")"
56+
done
57+
echo "[xchk] Wi-Fi TSF PHC = ${WIF_PTP:-<none>}"
58+
[ -z "$WIF_PTP" ] && { echo "[xchk] no Wi-Fi PHC"; exit 1; }
59+
[ -z "$IGC_PTP" ] && { echo "[xchk] no Ethernet PHC to compare against"; exit 1; }
60+
61+
# --- 2. cross-compare: discipline the Wi-Fi PHC to the I226 hardware clock.
62+
# The Wi-Fi PHC is the slave (system + I226 untouched). Steady-state:
63+
# 'freq' ~= the Wi-Fi crystal's frequency error vs the I226 (ppb)
64+
# 'offset' jitter ~= the Wi-Fi TSF short-term stability vs real PTP hardware ---
65+
echo "[xchk] === phc2sys: Wi-Fi TSF ($WIF_PTP) vs real PTP hardware ($IGC_PTP), ~25 s ==="
66+
sudo timeout 27 phc2sys -s "$IGC_PTP" -c "$WIF_PTP" -O 0 -m 2>&1 | grep -iE "offset" | tail -20 || true
67+
echo "[xchk] done"

0 commit comments

Comments
 (0)