{"format":"ptob-page","version":1,"canonical":"https://ptob.dev/blog/esp32-wireless-softpll/","title":"ESP32 BLE Sensor Sync Without GPS (SoftPLL) — PToB Blog","description":"Synchronize ESP32-S3 sensors over Bluetooth Low Energy without GPS or UTC. See how a SoftPLL corrects clock drift, then run the example on your computer.","language":"en","robots":"index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1","dates":{"published":"2026-11-08T00:00:00.000Z","modified":"2026-11-08T00:00:00.000Z"},"authors":["Jonathan Yantis"],"mainEntity":"https://ptob.dev/blog/esp32-wireless-softpll/#article","representations":{"application/json":"https://ptob.dev/blog/esp32-wireless-softpll/?format=json","text/markdown":"https://ptob.dev/blog/esp32-wireless-softpll/?format=markdown"},"documentation":"https://ptob.dev/agent-formats.json","social":{"openGraph":{"title":"ESP32 BLE Sensor Sync Without GPS (SoftPLL) — PToB Blog","description":"Synchronize ESP32-S3 sensors over Bluetooth Low Energy without GPS or UTC. See how a SoftPLL corrects clock drift, then run the example on your computer.","url":"https://ptob.dev/blog/esp32-wireless-softpll/","type":"article","siteName":"PToB","locale":"en_US","image":{"url":"https://ptob.dev/blog/esp32-wireless-softpll-v1.jpg","secureUrl":"https://ptob.dev/blog/esp32-wireless-softpll-v1.jpg","type":"image/jpeg","width":"1600","height":"1000","alt":"One ESP32-S3 clock sends Bluetooth timing to two sensor boards whose readings map onto a shared elapsed timeline."}},"twitter":{"card":"summary_large_image","title":"ESP32 BLE Sensor Sync Without GPS (SoftPLL) — PToB Blog","description":"Synchronize ESP32-S3 sensors over Bluetooth Low Energy without GPS or UTC. See how a SoftPLL corrects clock drift, then run the example on your computer.","image":{"url":"https://ptob.dev/blog/esp32-wireless-softpll-v1.jpg","alt":"One ESP32-S3 clock sends Bluetooth timing to two sensor boards whose readings map onto a shared elapsed timeline."}}},"citations":[],"provenance":{"sourceUrl":"https://ptob.dev/blog/esp32-wireless-softpll/","sourceMediaType":"text/html","sourceSha256":"189de9864107cd82b16b879a58c18c8f27a3f2116b85b7557ce11d8d967438ba","rightsUrl":"https://github.com/yantisj2/PToB/blob/main/LICENSING.md"},"contentUsage":{"aiTrain":true,"search":true,"aiInput":true,"reason":"summary-or-introduction"},"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://ptob.dev/#website","url":"https://ptob.dev/","name":"PToB","alternateName":["Precision Time over BLE","Precision Time over Bluetooth"],"description":"Bluetooth and USB software clocks: W12 60.3 ns relative-PPS RMS over 522/600 observations, separate 64 s sampled holdover, and CoreSE diagnostics.","inLanguage":["en","de"],"publisher":{"@id":"https://ptob.dev/#jonathan-yantis"}},{"@type":"Person","@id":"https://ptob.dev/#jonathan-yantis","name":"Jonathan Yantis","url":"https://github.com/yantisj2/PToB/blob/main/CREDITS.md"},{"@type":"Project","@id":"https://ptob.dev/#project","name":"PToB","alternateName":"Precision Time over BLE","url":"https://ptob.dev/","description":"An independent protocol project researching precision time transfer across Bluetooth-enabled microcontrollers and USB clients.","sameAs":"https://github.com/yantisj2/PToB","member":{"@id":"https://ptob.dev/#jonathan-yantis"},"knowsAbout":{"@id":"https://ptob.dev/#protocol"}},{"@type":"Thing","@id":"https://ptob.dev/#protocol","name":"Precision Time over BLE","alternateName":"PToB","description":"An experimental PTP-inspired protocol proposal for wireless clock synchronization over Bluetooth Low Energy."},{"@type":"WebPage","@id":"https://ptob.dev/blog/esp32-wireless-softpll/#webpage","url":"https://ptob.dev/blog/esp32-wireless-softpll/","name":"ESP32 BLE Sensor Sync Without GPS (SoftPLL) — PToB Blog","description":"Synchronize ESP32-S3 sensors over Bluetooth Low Energy without GPS or UTC. See how a SoftPLL corrects clock drift, then run the example on your computer.","inLanguage":"en","keywords":["ESP32-S3","Wireless sensor synchronization","Bluetooth Low Energy","SoftPLL"],"isPartOf":{"@id":"https://ptob.dev/#website"},"author":{"@id":"https://ptob.dev/#jonathan-yantis"},"about":{"@id":"https://ptob.dev/#protocol"},"dateModified":"2026-11-08T00:00:00.000Z","image":"https://ptob.dev/blog/esp32-wireless-softpll-v1.jpg","breadcrumb":{"@id":"https://ptob.dev/blog/esp32-wireless-softpll/#breadcrumbs"},"mainEntity":{"@id":"https://ptob.dev/blog/esp32-wireless-softpll/#article"}},{"@type":"BreadcrumbList","@id":"https://ptob.dev/blog/esp32-wireless-softpll/#breadcrumbs","itemListElement":[{"@type":"ListItem","position":1,"name":"PToB","item":"https://ptob.dev/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://ptob.dev/blog/"},{"@type":"ListItem","position":3,"name":"Sync ESP32 sensors over BLE without GPS","item":"https://ptob.dev/blog/esp32-wireless-softpll/"}]},{"@type":"Blog","@id":"https://ptob.dev/blog/#blog","url":"https://ptob.dev/blog/","name":"PToB Blog","description":"Engineering notes on shared time, inexpensive hardware and the experiments behind PToB.","inLanguage":"en","publisher":{"@id":"https://ptob.dev/#jonathan-yantis"},"isPartOf":{"@id":"https://ptob.dev/#website"},"blogPost":[{"@id":"https://ptob.dev/blog/esp32-wireless-softpll/#article"},{"@id":"https://ptob.dev/blog/esp32-clock-fundamentals/#article"},{"@id":"https://ptob.dev/blog/firmware-stability-time-mesh/#article"},{"@id":"https://ptob.dev/blog/lab-sensor-timing-economics/#article"},{"@id":"https://ptob.dev/blog/starlink-local-stratum-1-ntp/#article"},{"@id":"https://ptob.dev/blog/raspberry-pi-ptp-ntp-ble/#article"},{"@id":"https://ptob.dev/blog/gnss-resilient-time-mesh/#article"},{"@id":"https://ptob.dev/blog/passive-lora-channel-hopping/#article"},{"@id":"https://ptob.dev/blog/esp32-sub-microsecond-holdover/#article"},{"@id":"https://ptob.dev/blog/local-time-utc-mesh/#article"},{"@id":"https://ptob.dev/blog/grandmaster-failover-multi-master-ensemble/#article"},{"@id":"https://ptob.dev/blog/ble-clocks-indoor-navigation/#article"},{"@id":"https://ptob.dev/blog/passive-ble-esp32/#article"},{"@id":"https://ptob.dev/blog/why-utc/#article"},{"@id":"https://ptob.dev/blog/why-ptob/#article"}]},{"@type":"BlogPosting","@id":"https://ptob.dev/blog/esp32-wireless-softpll/#article","url":"https://ptob.dev/blog/esp32-wireless-softpll/","headline":"Sync ESP32 sensors over BLE without GPS","description":"Synchronize ESP32-S3 sensors over Bluetooth Low Energy without GPS or UTC. See how a SoftPLL corrects clock drift, then run the example on your computer.","mainEntityOfPage":{"@id":"https://ptob.dev/blog/esp32-wireless-softpll/#webpage"},"isPartOf":{"@id":"https://ptob.dev/blog/#blog"},"author":{"@id":"https://ptob.dev/#jonathan-yantis"},"publisher":{"@id":"https://ptob.dev/#jonathan-yantis"},"inLanguage":"en","dateCreated":"2026-11-08T00:00:00.000Z","dateModified":"2026-11-08T00:00:00.000Z","datePublished":"2026-11-08T00:00:00.000Z","image":"https://ptob.dev/blog/esp32-wireless-softpll-v1.jpg","articleSection":"ESP32 firmware examples · Part 2","keywords":["ESP32-S3","Wireless sensor synchronization","Bluetooth Low Energy","SoftPLL"],"wordCount":1293,"citation":[{"@type":"CreativeWork","name":"PToB · ESP32-REF 01 — Wireless BLE SoftPLL","url":"https://github.com/yantisj2/PToB/tree/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/01_wireless_softpll"},{"@type":"CreativeWork","name":"PToB · Wireless protocol and session rules","url":"https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/docs/wireless-protocol.md"},{"@type":"CreativeWork","name":"PToB · ESP32-S3 timestamp adapter","url":"https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/docs/esp32s3-port.md"},{"@type":"CreativeWork","name":"PToB · SoftPLL implementation","url":"https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/components/ref_softpll/ref_softpll.c"},{"@type":"CreativeWork","name":"PToB · Two-follower simulation","url":"https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/01_wireless_softpll/simulation.c"}],"about":{"@id":"https://ptob.dev/#protocol"}}]}],"content":{"mediaType":"text/markdown","headings":[{"level":1,"text":"Sync ESP32 sensors over BLE without GPS","url":"https://ptob.dev/blog/esp32-wireless-softpll/#main"},{"level":2,"text":"Match two timestamps to one radio event","url":"https://ptob.dev/blog/esp32-wireless-softpll/#match-two-timestamps-to-one-radio-event"},{"level":2,"text":"Learn the pace, then close the gap","url":"https://ptob.dev/blog/esp32-wireless-softpll/#learn-the-pace-then-close-the-gap"},{"level":2,"text":"Put sensor readings on the shared timeline","url":"https://ptob.dev/blog/esp32-wireless-softpll/#put-sensor-readings-on-the-shared-timeline"},{"level":2,"text":"Know when the clock is ready","url":"https://ptob.dev/blog/esp32-wireless-softpll/#know-when-the-clock-is-ready"},{"level":2,"text":"Try it with no boards attached","url":"https://ptob.dev/blog/esp32-wireless-softpll/#try-it-with-no-boards-attached"},{"level":2,"text":"Explore the details","url":"https://ptob.dev/blog/esp32-wireless-softpll/#related-title"},{"level":3,"text":"Building a basic ESP32 clock","url":"https://ptob.dev/blog/esp32-wireless-softpll/#main"},{"level":3,"text":"How passive BLE time works on ESP32","url":"https://ptob.dev/blog/esp32-wireless-softpll/#main"},{"level":2,"text":"Sources and further reading","url":"https://ptob.dev/blog/esp32-wireless-softpll/#sources-title"},{"level":2,"text":"Cite this article","url":"https://ptob.dev/blog/esp32-wireless-softpll/#cite-title"}],"links":[{"text":"Jonathan Yantis","url":"https://github.com/yantisj2/PToB/blob/main/CREDITS.md"},{"text":"Example 01, Wireless BLE SoftPLL","url":"https://ptob.dev/blog/esp32-wireless-softpll/#source-example"},{"text":"ESP32-S3 timestamp adapter","url":"https://ptob.dev/blog/esp32-wireless-softpll/#source-adapter"},{"text":"Event matching and session rules","url":"https://ptob.dev/blog/esp32-wireless-softpll/#source-protocol"},{"text":"Building a basic ESP32 clock","url":"https://ptob.dev/blog/esp32-clock-fundamentals/"},{"text":"SoftPLL implementation","url":"https://ptob.dev/blog/esp32-wireless-softpll/#source-loop"},{"text":"Lock checks and expiry in the code","url":"https://ptob.dev/blog/esp32-wireless-softpll/#source-loop"},{"text":"Run Example 01","url":"https://ptob.dev/blog/esp32-wireless-softpll/#source-example"},{"text":"Change the simulation","url":"https://ptob.dev/blog/esp32-wireless-softpll/#source-simulation"},{"text":"Building a basic ESP32 clock Part 1 explains how software converts ticks to time and corrects a clock without making its reading jump.","url":"https://ptob.dev/blog/esp32-clock-fundamentals/"},{"text":"How passive BLE time works on ESP32 Follow a radio event into the receiver’s clock estimate.","url":"https://ptob.dev/blog/passive-ble-esp32/"},{"text":"PToB · ESP32-REF 01 — Wireless BLE SoftPLL","url":"https://github.com/yantisj2/PToB/tree/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/01_wireless_softpll"},{"text":"PToB · Wireless protocol and session rules","url":"https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/docs/wireless-protocol.md"},{"text":"PToB · ESP32-S3 timestamp adapter","url":"https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/docs/esp32s3-port.md"},{"text":"PToB · SoftPLL implementation","url":"https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/components/ref_softpll/ref_softpll.c"},{"text":"PToB · Two-follower simulation","url":"https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/01_wireless_softpll/simulation.c"},{"text":"https://ptob.dev/blog/esp32-wireless-softpll/","url":"https://ptob.dev/blog/esp32-wireless-softpll/"},{"text":"Back to the blog","url":"https://ptob.dev/blog/"},{"text":"Inspect the project evidence","url":"https://github.com/yantisj2/PToB/blob/main/evidence/README.md"}],"text":"# Sync ESP32 sensors over BLE without GPS\n\nLet one ESP32-S3 set the pace. A software phase-locked loop helps nearby sensor boards put their readings on the same timeline without a UTC reference.\n\n[Jonathan Yantis](https://github.com/yantisj2/PToB/blob/main/CREDITS.md) 8 November 2026  5 min read\n\n![One ESP32-S3 clock sends Bluetooth timing to two sensor boards whose readings map onto a shared elapsed timeline.](https://ptob.dev/blog/esp32-wireless-softpll-v1.svg)\n\nIn this example, a free-running ESP32-S3 gives nearby sensor boards a common timeline over Bluetooth Low Energy.\n\nIn this post\n\nPut two accelerometers on opposite ends of a workbench and tap it. If one records the vibration before the other, did the vibration really arrive earlier, or are the clocks on the two boards out of step?\n\nTo compare the readings, both boards need to use the same time axis. They can use one ESP32-S3 as a reference and measure elapsed time from an arbitrary starting point. The reference runs on its own oscillator, so the boards can do this indoors without GPS or a network time server. The group follows that oscillator’s pace even if it drifts against UTC, the world’s shared time standard. This is the setup in [Example 01, Wireless BLE SoftPLL](https://ptob.dev/blog/esp32-wireless-softpll/#source-example).\n\n## Match two timestamps to one radio event\n\nThe reference board sends a Bluetooth Low Energy (BLE) timing packet 16 times a second. Both followers listen to the same broadcasts. Each saves a local timestamp from the controller when it hears an original packet. A later broadcast carries the reference board’s estimate of when that original went out. The follower matches the two by publication number, radio event counter and channel within the same source session. That gives it a pair of readings for the earlier event.\n\nThe follower records its local tick at the original packet. The follow-up supplies the estimated reference time to pair with that tick.\n\nThe timestamp has to describe the radio event. By the time an application callback runs, the packet has already arrived and other work may have delayed the callback. The example reads a controller receive timestamp in 500 ns increments. On the transmit side, it estimates the event time from the controller’s schedule. Radio and path delays can still shift the pair, even when the timestamps repeat closely. The timestamp adapter is specific to the ESP32-S3 and the ESP-IDF version pinned in the repo. [ESP32-S3 timestamp adapter](https://ptob.dev/blog/esp32-wireless-softpll/#source-adapter) · [Event matching and session rules](https://ptob.dev/blog/esp32-wireless-softpll/#source-protocol)\n\n## Learn the pace, then close the gap\n\nImagine both follower clocks agree at the start. If one runs 20 parts per million fast and the other runs 15 parts per million slow, ten seconds later their uncorrected readings differ by 350 microseconds. The longer the interval, the more that rate difference matters.\n\nA **software phase-locked loop**, or **SoftPLL**, uses a rolling window of timestamp pairs to estimate how much time each local tick should represent. A steady rise in clock error tells you the clock is gaining. The gap between the current clock reading and the fitted reference time is the **phase error** the loop needs to close.\n\nIllustrative timing pairs. The slope tells you how fast the error grows. The height above zero tells you how far ahead the clock is at that instant.\n\nThe fit estimates the pace. The loop then updates the software clock and gradually closes the remaining phase gap by briefly speeding up or slowing down the reported time. This is the same *slew* from Part 1, now driven by observations. The clock keeps advancing and the crystal runs at its own pace. [Building a basic ESP32 clock](https://ptob.dev/blog/esp32-clock-fundamentals/) · [SoftPLL implementation](https://ptob.dev/blog/esp32-wireless-softpll/#source-loop)\n\n## Put sensor readings on the shared timeline\n\nWhen a sensor records an event, capture the local tick as close to the sampling instant as the hardware allows. Use that tick and the currently valid software clock to label the reading in shared time. Each board applies its own conversion. Now you can compare the two vibration traces on one time axis, even if the data reaches your laptop in the opposite order.\n\nIllustrative values on a zoomed-in timeline. After conversion, the recorded events are 300 µs apart.\n\nThat comparison still depends on when the sensors actually sample and how well you capture that instant. Scheduling both sensors to sample together is a separate job. If you need to verify simultaneous sampling, measure the sampling edges against a common instrument while BLE continues to train the clocks.\n\n## Know when the clock is ready\n\nBefore applying a follow-up, the example compares its reference value with the prediction the follower saved at the original event. Its **LOOP LOCK** indicator requires enough of those prediction errors to stay small, a trained rate and little phase correction left. It tells you how well the software clock tracks its BLE observations. A fixed delay shared by those observations can still bias the clock.\n\nIf usable pairs stop arriving, the clock becomes unavailable 500 ms after the last accepted pair’s original receive event. A new source session starts a new acquisition. Keep the raw tick and source session with your data so a gap or restart stays visible when you compare the traces later. [Lock checks and expiry in the code](https://ptob.dev/blog/esp32-wireless-softpll/#source-loop)\n\n## Try it with no boards attached\n\nRun the host exercise on your computer. It uses the firmware’s loop with one simulated leader and two followers that run at +20 and −15 parts per million. Predict the sign of each follower’s correction, then check the output. After that, make one follower miss more packets and watch how many events you can still compare. The output tracks missing opportunities as well as timing spread, so you can see both effects. [Run Example 01](https://ptob.dev/blog/esp32-wireless-softpll/#source-example) · [Change the simulation](https://ptob.dev/blog/esp32-wireless-softpll/#source-simulation)\n\nEvery timestamp needs a reference.\n**Which event did you capture?**\n\n## Explore the details\n\n[Building a basic ESP32 clock Part 1 explains how software converts ticks to time and corrects a clock without making its reading jump.](https://ptob.dev/blog/esp32-clock-fundamentals/)[How passive BLE time works on ESP32 Follow a radio event into the receiver’s clock estimate.](https://ptob.dev/blog/passive-ble-esp32/)\n\n## Sources and further reading\n\nPrimary references for the hardware and methods discussed.\n\n1. [PToB · ESP32-REF 01 — Wireless BLE SoftPLL](https://github.com/yantisj2/PToB/tree/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/01_wireless_softpll)\n\n   Run the host exercise or inspect the ESP32-S3 leader and follower setup.\n2. [PToB · Wireless protocol and session rules](https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/docs/wireless-protocol.md)\n\n   See how the follower matches radio events, checks the source session and expires stale time.\n3. [PToB · ESP32-S3 timestamp adapter](https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/docs/esp32s3-port.md)\n\n   Trace the receive timestamp, transmit estimate and dependencies on the pinned ESP-IDF controller.\n4. [PToB · SoftPLL implementation](https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/components/ref_softpll/ref_softpll.c)\n\n   Read the rate fit, phase correction, prediction scoring and lock checks.\n5. [PToB · Two-follower simulation](https://github.com/yantisj2/PToB/blob/c8686bb38de0802f21e9b93b803c538dc7b2306f/firmware/examples/esp32-ref/01_wireless_softpll/simulation.c)\n\n   Change the simulated oscillator rates or packet loss and see what happens to the clock estimates.\n\n## Cite this article\n\nJonathan Yantis. Sync ESP32 sensors over BLE without GPS PToB Blog. Published 8 November 2026.\n\nArticle permalink: [https://ptob.dev/blog/esp32-wireless-softpll/](https://ptob.dev/blog/esp32-wireless-softpll/)\n\n[Back to the blog](https://ptob.dev/blog/)[Inspect the project evidence](https://github.com/yantisj2/PToB/blob/main/evidence/README.md)"}}
