Compare commits
4 Commits
2026.07.14
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ed8f720d6 | |||
| 61d9c1d0b2 | |||
| 48c99432fc | |||
| 674ad25662 |
@ -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
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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/"
|
||||
|
||||
@ -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(),
|
||||
],
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
1
docs-site/package-lock.json
generated
1
docs-site/package-lock.json
generated
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
43
docs-site/src/assets/logo-light.svg
Normal file
43
docs-site/src/assets/logo-light.svg
Normal 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 |
@ -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 10–15 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.
|
||||
:::
|
||||
|
||||
@ -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. |
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user