diff --git a/README.md b/README.md index befffba..82f28fe 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **What the Raspberry Pi time-server guides get wrong, and the numbers to prove it.** -Docs: **[escapement.warehack.ing](https://escapement.warehack.ing)** +Docs: **[cuckoo.warehack.ing](https://cuckoo.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 diff --git a/dashboard/src/gpsntp_dashboard/static/style.css b/dashboard/src/gpsntp_dashboard/static/style.css index 0e3071b..b7b918c 100644 --- a/dashboard/src/gpsntp_dashboard/static/style.css +++ b/dashboard/src/gpsntp_dashboard/static/style.css @@ -100,22 +100,8 @@ 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 ---------- */ -/* `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; } +main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; 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); @@ -268,77 +254,10 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; .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 */ diff --git a/docs-site/.env.example b/docs-site/.env.example index 38f021f..e77a21f 100644 --- a/docs-site/.env.example +++ b/docs-site/.env.example @@ -4,6 +4,6 @@ COMPOSE_PROJECT_NAME=cuckoo-escapement-docs -# Production: escapement.warehack.ing -# Local dev: escapement.l.warehack.ing (internal only — never reference publicly) -DOMAIN=escapement.warehack.ing +# Production: cuckoo.warehack.ing +# Local dev: cuckoo.l.warehack.ing (internal only — never reference publicly) +DOMAIN=cuckoo.warehack.ing diff --git a/docs-site/Makefile b/docs-site/Makefile index af563f6..fbc9515 100644 --- a/docs-site/Makefile +++ b/docs-site/Makefile @@ -39,21 +39,21 @@ shell: ## Open a shell in the running dev container (debugging) # ---- Production deploy -------------------------------------------------- # -# `make deploy` SSHes to dell01 (where the docs run, behind the shared -# caddy-docker-proxy), has it pull origin/main from Gitea via the forwarded -# agent key, and rebuilds the container. Nothing persistent is provisioned. +# `make deploy` pulls origin/main on the warehack.ing prod host and rebuilds +# the docs container. Agent-forwarding (`-A`) lets the remote `git pull` use +# the operator's local SSH key for Gitea — nothing persistent is provisioned +# on the deploy host. # -# `git fetch origin` (bare) — NOT `git fetch origin main`, which only writes -# FETCH_HEAD, leaving origin/main unresolved so the reset fails on a fresh -# checkout. Override DEPLOY_HOST / DEPLOY_PATH for a different target. +# Override DEPLOY_HOST / DEPLOY_PATH for a different deployment without +# editing this file. The defaults are the warehack.ing cookie-cutter shape # (see ~/.claude/references/warehacking.md). -DEPLOY_HOST ?= dell01 -DEPLOY_PATH ?= warehack-ing/cuckoo-escapement +DEPLOY_HOST ?= warehack-ing@warehack.ing +DEPLOY_PATH ?= ~/cuckoo-escapement .PHONY: deploy -deploy: ## Pull origin/main on dell01 + rebuild the docs container - @echo "==> deploying on $(DEPLOY_HOST):$(DEPLOY_PATH)" - ssh -A $(DEPLOY_HOST) "cd $(DEPLOY_PATH) && git fetch --quiet origin && git reset --hard origin/main && cd docs-site && make prod" - @echo "==> sanity check (title of a real page; bogus path should 404)" - @curl -sS "https://escapement.warehack.ing/explanation/preempt-rt-made-it-worse/" | grep -oE "[^<]*" || echo " title not found" +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/" diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index 5732dc2..435948b 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -10,17 +10,16 @@ // 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 escapement.warehack.ing (prod) -// and escapement.l.warehack.ing (local dev). +// Site URL comes from DOMAIN so one image serves both cuckoo.warehack.ing (prod) +// and cuckoo.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 ?? "escapement.warehack.ing"; +const domain = process.env.DOMAIN ?? "cuckoo.warehack.ing"; export default defineConfig({ site: `https://${domain}`, @@ -40,20 +39,12 @@ 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 needs a CM4 not a 4B, and your dashboard is taxing your clock.", + "cannot be pinned, PTP is impossible, 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. - // - // Two variants, because Starlight renders the logo as an 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, - }, + logo: { src: "./src/assets/logo.svg", replacesTitle: true }, favicon: "/favicon.svg", customCss: ["./src/styles/brass.css"], @@ -108,11 +99,7 @@ export default defineConfig({ pagination: true, lastUpdated: true, }), - // 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] }), + mdx(), sitemap(), ], }); diff --git a/docs-site/docker-compose.yml b/docs-site/docker-compose.yml index ad03415..cb28879 100644 --- a/docs-site/docker-compose.yml +++ b/docs-site/docker-compose.yml @@ -2,7 +2,7 @@ # # Default (no --profile flag): # prod-style — Caddy serves the built dist/. Use for production-like -# deploys (the public site at escapement.warehack.ing runs this). +# deploys (the public site at cuckoo.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 escapement.warehack.ing (prod) and escapement.l.warehack.ing +# switch between cuckoo.warehack.ing (prod) and cuckoo.l.warehack.ing # (local-dev tier). services: @@ -28,16 +28,10 @@ services: networks: - caddy labels: - caddy: ${DOMAIN:-escapement.warehack.ing} + caddy: ${DOMAIN:-cuckoo.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: @@ -47,7 +41,7 @@ services: container_name: cuckoo-escapement-docs-dev restart: unless-stopped environment: - - DOMAIN=${DOMAIN:-escapement.l.warehack.ing} + - DOMAIN=${DOMAIN:-cuckoo.l.warehack.ing} - ASTRO_TELEMETRY_DISABLED=1 volumes: # Hot-reload bind mounts. node_modules stays inside the container @@ -59,7 +53,7 @@ services: networks: - caddy labels: - caddy: ${DOMAIN:-escapement.l.warehack.ing} + caddy: ${DOMAIN:-cuckoo.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 diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json index 7ba2fd5..3966f5a 100644 --- a/docs-site/package-lock.json +++ b/docs-site/package-lock.json @@ -12,7 +12,6 @@ "@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" } diff --git a/docs-site/package.json b/docs-site/package.json index 96dc08d..c74adc2 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -14,7 +14,6 @@ "@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" } diff --git a/docs-site/src/assets/logo-light.svg b/docs-site/src/assets/logo-light.svg deleted file mode 100644 index 1586853..0000000 --- a/docs-site/src/assets/logo-light.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - escapement - diff --git a/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md b/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md index 04a3390..14fe93c 100644 --- a/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md +++ b/docs-site/src/content/docs/explanation/no-ptp-on-a-pi-4.md @@ -1,33 +1,14 @@ --- -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. +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. sidebar: order: 3 --- -:::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 reference builds all reach for **PTP** (IEEE 1588), and they're right to: on +the right hardware it's dramatically better than NTP. -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: +The Pi 4 is not the right hardware. One command settles it: ```console $ ethtool -T eth0 @@ -36,106 +17,36 @@ Capabilities: software-receive software-system-clock PTP Hardware Clock: none - -$ ls /dev/ptp* -ls: cannot access '/dev/ptp*': No such file or directory +Hardware Transmit Timestamp Modes: none +Hardware Receive Filter Modes: none ``` -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. +**`PTP Hardware Clock: none`.** There isn't one. There's no `/dev/ptp0` to open. -## Two different chips — that report the same ID +## Why that's fatal rather than inconvenient -| board | PHY | hardware PTP | -|---|---|---| -| Compute Module 4 | BCM54210PE | **yes** | -| Compute Module 5 | — | **yes**, out of the box | -| Pi 4 Model B | BCM54213PE | no | +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. -Here's the part that makes this worth a page. **Both PHYs report an identical ID:** +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. -```console -$ cat /sys/class/mdio_bus/unimac-mdio--19/unimac-mdio--19:01/phy_id -0x600d84a2 -``` +## But the guides say the Pi 4's PHY supports PTP -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. +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. -## So it tells them apart by where they're plugged in +## So don't chase it -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 +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 [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. -::: diff --git a/docs-site/src/content/docs/findings.md b/docs-site/src/content/docs/findings.md index a46b007..ee7cb67 100644 --- a/docs-site/src/content/docs/findings.md +++ b/docs-site/src/content/docs/findings.md @@ -12,18 +12,11 @@ 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 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. -::: +## What the guides get wrong on a Pi 4 | Claim | Reality | |---|---| -| "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`. | +| "Use PTP for real precision" | **Impossible.** `ethtool -T eth0` → `PTP Hardware Clock: none`. No hardware timestamping exists on this NIC. | | "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. | diff --git a/docs-site/src/content/docs/index.mdx b/docs-site/src/content/docs/index.mdx index 8ab6809..07cd3bb 100644 --- a/docs-site/src/content/docs/index.mdx +++ b/docs-site/src/content/docs/index.mdx @@ -76,10 +76,9 @@ board rather than take our word for it. That's the whole point. costs you the accuracy. [→ Why](/explanation/the-interrupt-you-cannot-move/) - - 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. + + `ethtool -T eth0` → `PTP Hardware Clock: none`. There is no hardware + timestamping. Software PTP is just a worse NTP. Don't chase it. [→ Why](/explanation/no-ptp-on-a-pi-4/)