Compare commits

...

4 Commits

Author SHA1 Message Date
1ed8f720d6 Correct the PTP page: no PTP on a 4B, but the CM4 has it
Reader sent Geerling's CM4 PTP write-up, which shows two CM4s syncing to 10-15ns.
Our headline overclaimed ('impossible on a Pi 4') and our mechanism was wrong (we
blamed the bcmgenet MAC for not exposing a PHC).

The real story is better: the CM4 (BCM54210PE) and the 4B (BCM54213PE) report the
IDENTICAL PHY ID 0x600d84a2, so the kernel cannot tell them apart by asking the
chip. bcm-phy-ptp.c disambiguates by MDIO bus address instead — addr 0 = CM4,
proceed; addr 1 = 4B, return NULL. Ours is addr 1.

It is not a kernel config gap and not our RT patch: CONFIG_BCM_NET_PHYPTP=y and
CONFIG_NETWORK_PHY_TIMESTAMPING=y are both set. The driver loads, looks at the
board, and declines.

Correction is visible on the page rather than quietly patched. Credit to Geerling.
2026-07-14 14:50:27 -06:00
61d9c1d0b2 Light-theme logo variant — the wordmark was invisible
Starlight renders the logo as an <img>, so CSS can't reach inside it. The single
cream-on-near-black SVG left a cream wordmark on a cream page. Ship a light
variant with warm ink and the darker brass the light theme already uses.
2026-07-14 10:58:36 -06:00
48c99432fc Dashboard: mobile layout fix (grid min-content blowout) 2026-07-14 10:56:23 -06:00
674ad25662 Move to escapement.warehack.ing; fix GFM tables in MDX
The landing page's headline results table was rendering as a paragraph of raw
pipe characters. Astro applies remark-gfm to .md but it wasn't reaching the MDX
pipeline, so every .md page rendered tables fine and only index.mdx was broken —
which is exactly why it went unnoticed. mdx({remarkPlugins:[remarkGfm]}) fixes it.

cuckoo.warehack.ing stays as a permanent redirect; it was live briefly.
2026-07-14 10:51:35 -06:00
12 changed files with 292 additions and 50 deletions

View File

@ -2,7 +2,7 @@
**What the Raspberry Pi time-server guides get wrong, and the numbers to prove it.**
Docs: **[cuckoo.warehack.ing](https://cuckoo.warehack.ing)**
Docs: **[escapement.warehack.ing](https://escapement.warehack.ing)**
We built a GPS-disciplined Stratum 1 NTP server on a Raspberry Pi 4, followed the
published advice, and measured everything. Most of that advice is wrong on this

View File

@ -100,8 +100,22 @@ svg { width: 1.15em; height: 1.15em; fill: none; stroke: currentColor; stroke-wi
@keyframes pulse { 70% { box-shadow: 0 0 0 7px rgba(52, 211, 153, 0); } 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); } }
/* ---------- layout ---------- */
main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18px; }
/* `grid-template-columns: minmax(0, 1fr)` is load-bearing, not decoration.
*
* A grid column defaults to `auto`, whose MINIMUM is min-content and grid items
* refuse to shrink below it. The sources table has seven columns, giving it a
* ~700px min-content, and that number propagates up and pins `main` to 771px
* inside a 375px phone. Every panel then overflows and the document gets a
* horizontal scrollbar. `.table-hold { overflow-x: auto }` cannot rescue it,
* because the wrapper's own ancestors still won't shrink.
*
* The 0 in minmax(0, 1fr) overrides that automatic min-content floor. Same reason
* .panel needs min-width: 0. Remove either and mobile breaks again.
*/
main { max-width: 1180px; margin: 0 auto; padding: 20px;
display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; }
.panel {
min-width: 0;
background: linear-gradient(var(--panel), var(--bg-2));
border: 1px solid var(--border-soft); border-radius: 16px; padding: 18px;
box-shadow: var(--shadow);
@ -254,10 +268,77 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p
.ss-plate__cta svg { width: 14px; height: 14px; transition: transform .2s; }
.ss-plate__link:hover .ss-plate__cta svg { transform: translateX(2px); }
/* ---------- mobile ----------
*
* This is a status page. On a phone you are almost always asking ONE question:
* "is the clock still good?" So the hero answer time, stratum, lock must be
* readable without scrolling, and everything else can wait below the fold.
*
* The tables and the signal bars scroll sideways WITHIN their panels rather than
* being crushed into unreadability. That's deliberate: a squeezed IPv6 address
* wrapped over four lines is worse than one you swipe.
*/
@media (max-width: 700px) {
main { padding: 14px; gap: 14px; }
.panel { padding: 14px; border-radius: 14px; }
/* A panel head is a title plus its readout. Side by side they fight for a
narrow line and the title wraps mid-phrase; stacked, both stay whole. */
.panel-head { flex-direction: column; align-items: flex-start; gap: 6px; }
.spark-meta { font-size: 11px; }
/* Stack the clock above the stratum badge, and left-align the badge so the
eye travels straight down instead of ping-ponging left-right. */
.hero { flex-direction: column; align-items: flex-start; gap: 14px; }
.verdict { text-align: left; width: 100%; }
.ref-line { margin-top: 6px; }
/* clamp() already scales this, but 11vw on a narrow phone leaves the seconds
hard against the edge. Give it a slightly bigger floor and tighter tracking. */
.clock { font-size: clamp(38px, 13vw, 60px); letter-spacing: 0; }
/* Two columns, not one: six stat cards single-file is a lot of thumb. */
.cards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.card { padding: 12px; }
.card-value { font-size: 21px; }
.spark-big { height: 90px; }
.snr { min-height: 150px; }
/* Swipe the source table rather than wrap it. The shadow hint tells the reader
there IS more to the right otherwise a cut-off column just looks broken. */
.table-hold {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
background:
linear-gradient(to right, var(--panel) 30%, transparent) left / 24px 100% no-repeat,
linear-gradient(to left, var(--panel) 30%, transparent) right / 24px 100% no-repeat,
radial-gradient(farthest-side at 0 50%, rgba(0,0,0,.35), transparent) left / 12px 100% no-repeat,
radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.35), transparent) right / 12px 100% no-repeat;
background-attachment: local, local, scroll, scroll;
}
.tbl { min-width: 560px; }
.topbar { padding: 12px 14px; }
.brand h1 { font-size: 17px; }
.brand-sub { font-size: 11.5px; }
.brand-icon { width: 26px; height: 26px; }
.foot { padding: 10px 14px 24px; }
/* Three items on one line is unreadable at 375px — let them stack. */
.foot-meta { flex-direction: column; align-items: flex-start; gap: 6px; }
}
@media (max-width: 560px) {
.ss-plate__link { flex-direction: column; align-items: flex-start; }
}
/* Comfortable thumb targets. The WCAG 2.2 minimum is 24px; 44px is the iOS HIG
figure and the one that actually feels right when you're standing up. */
@media (pointer: coarse) {
.foot-docs, .ss-plate__link { min-height: 44px; }
}
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
/* "approx" marker when GPSNTP_POSITION=coarse */

View File

@ -4,6 +4,6 @@
COMPOSE_PROJECT_NAME=cuckoo-escapement-docs
# Production: cuckoo.warehack.ing
# Local dev: cuckoo.l.warehack.ing (internal only — never reference publicly)
DOMAIN=cuckoo.warehack.ing
# Production: escapement.warehack.ing
# Local dev: escapement.l.warehack.ing (internal only — never reference publicly)
DOMAIN=escapement.warehack.ing

View File

@ -56,4 +56,4 @@ deploy: ## Pull main + rebuild the docs container on the prod host
@echo "==> deploying $(DEPLOY_HOST):$(DEPLOY_PATH)"
ssh -A $(DEPLOY_HOST) "cd $(DEPLOY_PATH) && git fetch origin main && git reset --hard origin/main && cd docs-site && make prod"
@echo "==> sanity check"
@curl -s -o /dev/null -w " HTTP %{http_code} %{url_effective}\n" "https://cuckoo.warehack.ing/explanation/preempt-rt-made-it-worse/"
@curl -s -o /dev/null -w " HTTP %{http_code} %{url_effective}\n" "https://escapement.warehack.ing/explanation/preempt-rt-made-it-worse/"

View File

@ -10,16 +10,17 @@
// when the dev server runs behind Caddy (TLS-terminating proxy) — without an
// explicit host/protocol/clientPort, Vite's WebSocket drops every ~10s.
//
// Site URL comes from DOMAIN so one image serves both cuckoo.warehack.ing (prod)
// and cuckoo.l.warehack.ing (local dev).
// Site URL comes from DOMAIN so one image serves both escapement.warehack.ing (prod)
// and escapement.l.warehack.ing (local dev).
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import starlight from "@astrojs/starlight";
import { defineConfig } from "astro/config";
import remarkGfm from "remark-gfm";
import starlightLinksValidator from "starlight-links-validator";
const domain = process.env.DOMAIN ?? "cuckoo.warehack.ing";
const domain = process.env.DOMAIN ?? "escapement.warehack.ing";
export default defineConfig({
site: `https://${domain}`,
@ -39,12 +40,20 @@ export default defineConfig({
description:
"What the Raspberry Pi time-server guides get wrong, and the numbers to prove it. " +
"GPS Stratum 1 on a Pi 4: PREEMPT_RT makes PPS jitter worse, the PPS interrupt " +
"cannot be pinned, PTP is impossible, and your dashboard is taxing your clock.",
"cannot be pinned, PTP needs a CM4 not a 4B, and your dashboard is taxing your clock.",
// The mark IS the word "cuckoo" — a rebus. replacesTitle stops Starlight
// rendering the title text beside it (which would read "…escapement The
// Cuckoo Escapement"). The SVG's aria-label carries the full name.
logo: { src: "./src/assets/logo.svg", replacesTitle: true },
//
// Two variants, because Starlight renders the logo as an <img> and CSS
// cannot reach inside it. The single cream-on-black logo was invisible in
// the light theme — cream wordmark on a cream page.
logo: {
light: "./src/assets/logo-light.svg",
dark: "./src/assets/logo.svg",
replacesTitle: true,
},
favicon: "/favicon.svg",
customCss: ["./src/styles/brass.css"],
@ -99,7 +108,11 @@ export default defineConfig({
pagination: true,
lastUpdated: true,
}),
mdx(),
// GFM tables do NOT render in .mdx without this, even though they work fine
// in plain .md — Astro's remark-gfm doesn't reach the MDX pipeline here. The
// failure is silent and ugly: the table falls through as a paragraph of raw
// pipe characters. It shipped that way on the landing page. Don't remove.
mdx({ remarkPlugins: [remarkGfm] }),
sitemap(),
],
});

View File

@ -2,7 +2,7 @@
#
# Default (no --profile flag):
# prod-style — Caddy serves the built dist/. Use for production-like
# deploys (the public site at cuckoo.warehack.ing runs this).
# deploys (the public site at escapement.warehack.ing runs this).
#
# --profile dev:
# Astro dev server with HMR. Volume mounts on src/ so edits hot-reload.
@ -13,7 +13,7 @@
#
# Both services attach to the external `caddy` network and expose
# themselves to caddy-docker-proxy via labels. Edit DOMAIN in .env to
# switch between cuckoo.warehack.ing (prod) and cuckoo.l.warehack.ing
# switch between escapement.warehack.ing (prod) and escapement.l.warehack.ing
# (local-dev tier).
services:
@ -28,10 +28,16 @@ services:
networks:
- caddy
labels:
caddy: ${DOMAIN:-cuckoo.warehack.ing}
caddy: ${DOMAIN:-escapement.warehack.ing}
caddy.reverse_proxy: "{{upstreams 80}}"
# encode + gzip already in the container; let caddy pass through.
# The site was briefly live at cuckoo.warehack.ing before the rename. Keep
# the old host as a permanent redirect rather than letting any link that
# escaped rot. {uri} preserves the path, so deep links survive too.
caddy_1: cuckoo.warehack.ing
caddy_1.redir: https://escapement.warehack.ing{uri} permanent
docs-dev:
profiles: ["dev"]
build:
@ -41,7 +47,7 @@ services:
container_name: cuckoo-escapement-docs-dev
restart: unless-stopped
environment:
- DOMAIN=${DOMAIN:-cuckoo.l.warehack.ing}
- DOMAIN=${DOMAIN:-escapement.l.warehack.ing}
- ASTRO_TELEMETRY_DISABLED=1
volumes:
# Hot-reload bind mounts. node_modules stays inside the container
@ -53,7 +59,7 @@ services:
networks:
- caddy
labels:
caddy: ${DOMAIN:-cuckoo.l.warehack.ing}
caddy: ${DOMAIN:-escapement.l.warehack.ing}
caddy.reverse_proxy: "{{upstreams 4321}}"
# Vite HMR over WebSocket. Caddy's defaults close "idle" WS
# connections after ~10-15s; HMR doesn't send app-level pings, so

View File

@ -12,6 +12,7 @@
"@astrojs/sitemap": "^3.7.2",
"@astrojs/starlight": "^0.39.2",
"astro": "^6.3.1",
"remark-gfm": "^4.0.1",
"sharp": "^0.34.0",
"starlight-links-validator": "^0.24.0"
}

View File

@ -14,6 +14,7 @@
"@astrojs/sitemap": "^3.7.2",
"@astrojs/starlight": "^0.39.2",
"astro": "^6.3.1",
"remark-gfm": "^4.0.1",
"sharp": "^0.34.0",
"starlight-links-validator": "^0.24.0"
}

View File

@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 78" role="img" aria-label="The Cuckoo Escapement">
<style>
.mono { font-family: "JetBrains Mono","SF Mono",Menlo,monospace; font-weight: 700; }
.cream { fill: #2b2721; }
.gear { fill: none; stroke: #a97c1f; stroke-width: 1.9; stroke-linejoin: round; }
.rim { fill: none; stroke: #a97c1f; stroke-width: 1.3; opacity: .8; }
.tick { stroke: #a97c1f; stroke-width: 1.4; opacity: .5; stroke-linecap: round; }
.head { fill: #2b2721; }
.eye { fill: #faf7f1; }
</style>
<!-- one ring, two readings: gear teeth outside (escapement), hour ticks inside (dial) -->
<path d="M 30.61 13.60 L 34.99 6.14 L 41.01 6.14 L 45.39 13.60 L 46.36 13.91 L 54.29 10.46 L 59.16 14.00 L 58.33 22.60 L 58.93 23.43 L 67.37 25.29 L 69.23 31.02 L 63.49 37.49 L 63.49 38.51 L 69.23 44.98 L 67.37 50.71 L 58.93 52.57 L 58.33 53.40 L 59.16 62.00 L 54.29 65.54 L 46.36 62.09 L 45.39 62.40 L 41.01 69.86 L 34.99 69.86 L 30.61 62.40 L 29.64 62.09 L 21.71 65.54 L 16.84 62.00 L 17.67 53.40 L 17.07 52.57 L 8.63 50.71 L 6.77 44.98 L 12.51 38.51 L 12.51 37.49 L 6.77 31.02 L 8.63 25.29 L 17.07 23.43 L 17.67 22.60 L 16.84 14.00 L 21.71 10.46 L 29.64 13.91 Z" class="gear"/>
<circle cx="38.0" cy="38.0" r="23.5" class="rim"/>
<g class="tick">
<line x1="38.00" y1="15.50" x2="38.00" y2="18.50"/>
<line x1="49.25" y1="18.51" x2="47.75" y2="21.11"/>
<line x1="57.49" y1="26.75" x2="54.89" y2="28.25"/>
<line x1="60.50" y1="38.00" x2="57.50" y2="38.00"/>
<line x1="57.49" y1="49.25" x2="54.89" y2="47.75"/>
<line x1="49.25" y1="57.49" x2="47.75" y2="54.89"/>
<line x1="38.00" y1="60.50" x2="38.00" y2="57.50"/>
<line x1="26.75" y1="57.49" x2="28.25" y2="54.89"/>
<line x1="18.51" y1="49.25" x2="21.11" y2="47.75"/>
<line x1="15.50" y1="38.00" x2="18.50" y2="38.00"/>
<line x1="18.51" y1="26.75" x2="21.11" y2="28.25"/>
<line x1="26.75" y1="18.51" x2="28.25" y2="21.11"/>
</g>
<!-- the cuckoo, head only, beak open mid-call -->
<g class="head">
<!-- crest: three feathers, swept back. this is what says "bird" -->
<path d="M 30 26 L 27 16 L 35 23 Z"/>
<path d="M 36 23 L 36 13 L 42 22 Z"/>
<path d="M 42 23 L 46 15 L 47 25 Z"/>
<circle cx="37" cy="39" r="13.5"/> <!-- big round head -->
<path d="M 47 33.5 L 62 34.5 L 48.5 40 Z"/> <!-- upper beak: SHORT + chunky -->
<path d="M 47.5 42 L 60 46.5 L 46 44 Z"/> <!-- lower beak, gape open -->
</g>
<circle cx="40.5" cy="35.5" r="2.2" class="eye"/>
<text x="92" y="52" class="mono cream" font-size="33">escapement</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,14 +1,33 @@
---
title: Why PTP is off the table on a Pi 4
description: PTP's entire value is hardware timestamping. The Pi 4's NIC has no PTP hardware clock. Software PTP is a worse NTP.
title: No PTP on a Pi 4 Model B (but yes on a CM4)
description: The CM4 does hardware PTP. The Pi 4 Model B does not — and both report the identical PHY ID, so the kernel tells them apart by MDIO address and declines ours by name.
sidebar:
order: 3
---
The reference builds all reach for **PTP** (IEEE 1588), and they're right to: on
the right hardware it's dramatically better than NTP.
:::caution[Corrected — 2026-07-14]
This page used to be called *"Why PTP is off the table on a Pi 4"* and blamed the
`bcmgenet` MAC driver for not exposing a PHC. **The headline overclaimed and the
mechanism was wrong.** A reader sent us
[Jeff Geerling's CM4 PTP write-up](https://www.jeffgeerling.com/blog/2022/ptp-and-ieee-1588-hardware-timestamping-on-raspberry-pi-cm4/),
which shows the **Compute Module 4 doing hardware PTP** — two of them synced to
within 1015 nanoseconds.
The Pi 4 is not the right hardware. One command settles it:
Our measurement was real, but it is specific to the **Pi 4 Model B**, and the
actual reason is far better than "unsupported." Corrected version below. This is
the site working as intended.
:::
PTP's entire value is **hardware timestamping**: the network silicon stamps the
packet as it crosses the wire, outside the operating system. That's what takes
scheduling, driver latency and queueing out of the measurement, and it's why PTP
reaches nanoseconds where NTP reaches microseconds.
With no PHC (PTP Hardware Clock), PTP is just a protocol — packets stamped by the
*kernel*, on the *CPU*, subject to precisely the jitter you were trying to escape.
A more complicated NTP with worse tooling.
On our board:
```console
$ ethtool -T eth0
@ -17,36 +36,106 @@ Capabilities:
software-receive
software-system-clock
PTP Hardware Clock: none
Hardware Transmit Timestamp Modes: none
Hardware Receive Filter Modes: none
$ ls /dev/ptp*
ls: cannot access '/dev/ptp*': No such file or directory
```
**`PTP Hardware Clock: none`.** There isn't one. There's no `/dev/ptp0` to open.
On a CM4 that same command reports `PTP Hardware Clock: 0`, and
`/sys/class/ptp/ptp0/clock_name` says `bcm_phy_ptp`. Same family, same era,
different answer.
## Why that's fatal rather than inconvenient
## Two different chips — that report the same ID
PTP's whole advantage is **hardware timestamping**: the network card itself
stamps the packet as it crosses the wire, in silicon, outside the operating
system. That's what removes kernel scheduling, driver latency, and queueing from
the measurement, and it's why PTP reaches nanoseconds where NTP reaches
microseconds.
| board | PHY | hardware PTP |
|---|---|---|
| Compute Module 4 | BCM54210PE | **yes** |
| Compute Module 5 | — | **yes**, out of the box |
| Pi 4 Model B | BCM54213PE | no |
Take the hardware clock away and PTP is just... a protocol. Software-timestamped
PTP has the packets stamped by the *kernel*, on the *CPU*, subject to exactly the
scheduling jitter you were trying to escape. It is a more complicated NTP with
worse tooling.
Here's the part that makes this worth a page. **Both PHYs report an identical ID:**
## But the guides say the Pi 4's PHY supports PTP
```console
$ cat /sys/class/mdio_bus/unimac-mdio--19/unimac-mdio--19:01/phy_id
0x600d84a2
```
They do, and the *chip* does — the BCM54213PE PHY has PTP capability on paper.
It doesn't matter. The Pi 4's `bcmgenet` MAC driver doesn't expose a PHC, so
Linux has nothing to give you. And the reference builds that make PTP work feed
the PPS into the NIC through a **SYNC pin that only the CM4/CM5 break out** — a
regular Pi 4 board doesn't route it anywhere you can reach.
That value is `PHY_ID_BCM54213PE` in `include/linux/brcmphy.h` — and a CM4
reports it too. The silicon genuinely differs; the identifier does not. So the
kernel *cannot* tell these boards apart by asking the chip who it is.
## So don't chase it
## So it tells them apart by where they're plugged in
We spent real time on this before running `ethtool -T`, which we should have run
first. If your board reports `PTP Hardware Clock: none`, close the tab. Put the
effort into the PPS path instead — that's where the nanoseconds actually are, and
From the Raspberry Pi kernel, `drivers/net/phy/bcm-phy-ptp.c`, comments and all:
```c
struct bcm_ptp_private *bcm_ptp_probe(struct phy_device *phydev)
{
switch (BRCM_PHY_MODEL(phydev)) {
case PHY_ID_BCM54210E:
break;
#ifdef PHY_ID_BCM54213PE
case PHY_ID_BCM54213PE:
switch (phydev->mdio.addr) {
case 0: // CM4 - this is a BCM54210PE which supports PTP
break;
case 1: // 4B - this is a BCM54213PE which doesn't
return NULL;
default: // Unknown - assume it's BCM54210PE
break;
}
break;
```
The disambiguator is the **MDIO bus address**. A CM4's PHY sits at address 0; a
Pi 4 Model B's sits at address 1. Ours:
```console
$ ls -d /sys/class/mdio_bus/unimac-mdio--19/unimac-mdio--19:*
/sys/class/mdio_bus/unimac-mdio--19/unimac-mdio--19:01
^^ address 1 — the 4B branch
```
Address 1 → `return NULL` → no PHC is ever registered. That is the whole story.
## It is not your kernel, and there is no option to flip
This is the part worth internalising, because it's a trap that looks exactly like
a misconfiguration. All the support is *present*:
```console
$ grep -E "CONFIG_NETWORK_PHY_TIMESTAMPING|CONFIG_BCM_NET_PHYPTP" .config
CONFIG_NETWORK_PHY_TIMESTAMPING=y
CONFIG_BCM_NET_PHYPTP=y
```
The driver is compiled in. The PHY ID is a recognised constant. The code path
exists, and it runs. **It runs, looks at your board, and declines.** No kernel
option changes that, no patch, no `rpi-update` — the refusal is a deliberate and
correct statement about the silicon in front of it.
It also means our [`IRQF_NO_THREAD` kernel](/reference/the-patch/) is not the
culprit. That was the obvious suspicion when we re-opened this, so we checked it
first. It isn't.
## What to actually do
**On a Pi 4 Model B:** stop. Spend the effort on the PPS path, which is where the
nanoseconds actually live — and
[it needs the help](/explanation/preempt-rt-made-it-worse/).
**If you want PTP on a Pi:** buy a **CM4 or CM5**, not a 4B. You get `/dev/ptp0`,
`ethtool -T` reports hardware transmit and receive timestamping, and on the CM4 IO
board you can drive a PPS out of pin 9. Geerling's post is the guide; we're not
going to rewrite it.
**Either way, run `ethtool -T` first.** We didn't, and burned real time on a
question that one command answers.
:::note[Credit]
The correction, and every CM4 figure here, comes from
[Jeff Geerling's write-up](https://www.jeffgeerling.com/blog/2022/ptp-and-ieee-1588-hardware-timestamping-on-raspberry-pi-cm4/)
and the kernel patches it links to. We had the right conclusion for the wrong
reason, and stated it too broadly. Checking beats believing — including when the
thing being checked is us.
:::

View File

@ -12,11 +12,18 @@ A GPS-disciplined Stratum 1 NTP server: **Raspberry Pi 4** + **BerryGPS-IMU v4**
199 ns**, root delay ~1 ns, survives a cold power cut unattended. About $130 of
parts, replacing an appliance that costs $1,500$10,000.
## What the guides get wrong on a Pi 4
## What the guides get wrong on a Pi 4 Model B
:::note[Model B, specifically]
These are properties of the **Pi 4 Model B**, not the whole Pi 4 family. The
**CM4 and CM5 do hardware PTP** — [we got that one wrong at first, and corrected
it](/explanation/no-ptp-on-a-pi-4/). If a claim here matters to you, check it on
*your* board. That's the entire point of this site.
:::
| Claim | Reality |
|---|---|
| "Use PTP for real precision" | **Impossible.** `ethtool -T eth0``PTP Hardware Clock: none`. No hardware timestamping exists on this NIC. |
| "Use PTP for real precision" | **Not on a 4B — but yes on a CM4.** Both report the *same* PHY ID, so the kernel disambiguates by MDIO address and hands the 4B a `return NULL`. `ethtool -T``PTP Hardware Clock: none`. |
| "Isolate the PPS IRQ on a dedicated core" | **Not permitted.** GPIO IRQs demux through `pinctrl-bcm2835` and reject `smp_affinity`. |
| "Install PREEMPT_RT" | **Made jitter 3× worse** until patched — it threads the handler that takes the timestamp. |
| "Raise the GPS baud rate" | **Irrelevant.** PPS offset measured 1 ns at 9600 vs 115200. Identical. NMEA only *labels* the second. |

View File

@ -76,9 +76,10 @@ board rather than take our word for it. That's the whole point.
costs you the accuracy.
[→ Why](/explanation/the-interrupt-you-cannot-move/)
</Card>
<Card title="PTP is impossible on a Pi 4" icon="error">
`ethtool -T eth0` → `PTP Hardware Clock: none`. There is no hardware
timestamping. Software PTP is just a worse NTP. Don't chase it.
<Card title="No PTP on a 4B — but yes on a CM4" icon="error">
The CM4 and the 4B report the **same PHY ID**, so the kernel tells them apart
by MDIO address and hands the 4B a `return NULL`. The driver is compiled in;
it looks at your board and declines. Buy a CM4 if you want PTP.
[→ Why](/explanation/no-ptp-on-a-pi-4/)
</Card>
<Card title="Your dashboard is taxing your clock" icon="rocket">