Use the kernel PPS path. Do not write a Python or C GPIO polling loop for timing. User-space timing will add scheduling latency; the kernel PPS driver timestamps the edge much earlier and exposes it as /dev/pps0.
Wiring:
ESP32 GPIO27(PPS) -----------------> Pi 5 GPIO18 / phy pin 12
ESP32 GND -----------------> Pi 5 GND / phy pin 14
Time Source Design
PPS alone is not a complete clock. It tells the Pi exactly where the second boundary is, but not which UTC second it is.
Chrony therefore needs:
PPS on/dev/pps0for the precise second edge.
A normal time source, such as the ESP32 GNSS NTP server, for UTC date and second numbering.
Flow:
M10S GNSS
|
| GNSS time + PPS
v
ESP32
|
| GPIO27 PPS
v
Raspberry Pi 5 GPIO18
|
| kernel PPS timestamp
v
/dev/pps0
|
v
chronyd
|
+-- PPS: precise second boundary
+-- ESP32 NTP: UTC second/date
|
v
Pi 5 serves LAN NTP as stratum 1
pull-down=keeps the input from floating when PPS is disconnected.
schmitt-trigger can help clean up marginal/noisy edges on a wire.
Do not add assert-falling-edge for the normal GNSS PPS case. The default is rising-edge assert, which is what you usually want from a GNSS 1PPS output.
Use this minimal configuration as the core of the file. Adjust the server IP and allow network for your LAN.
# Coarse UTC source.# This gives chronyd the actual UTC second/date.
server 192.168.10.90 iburst minpoll 3 maxpoll 3 prefer
# Precise PPS source.# ESP32 GPIO27 -> Pi 5 GPIO18 -> kernel PPS -> /dev/pps0.
refclock PPS /dev/pps0 refid PPS poll 0 prefer
# Clock discipline.
makestep 0.1 3
rtcsync
# Allow LAN clients to query this Pi as an NTP server.# Change this to match your network.
allow 192.168.0.0/16
# Optional logging.
log tracking measurements statistics
logdir /var/log/chrony
ESP32 PPS
|
v
Pi 5 RP1 GPIO interrupt
|
v
Linux PPS kernel timestamp
|
v
/dev/pps0
|
v
chronyd
A custom C or Python program would run after the kernel schedules it, which adds avoidable latency and jitter. For a stratum-1 NTP server, use the kernel PPS driver plus Chrony.
Some screenshot of comparing PPS and nano sec in chrony
root@OpenWrt:~# ntpd -w -d -p 192.168.10.196
ntpd: sending query to 192.168.10.196
ntpd: reply from 192.168.10.196: offset:-0.002124 delay:0.010506 status:0x24 strat:1 refid:0x00535047 rootdelay:0.000000 reach:0x01
ntpd: sending query to 192.168.10.196
ntpd: reply from 192.168.10.196: offset:-0.001935 delay:0.008853 status:0x24 strat:1 refid:0x00535047 rootdelay:0.000000 reach:0x03
ntpd: sending query to 192.168.10.196
ntpd: reply from 192.168.10.196: offset:+0.003002 delay:0.008686 status:0x24 strat:1 refid:0x00535047 rootdelay:0.000000 reach:0x07
uci show system | grep ntp
cat /etc/config/system
/etc/init.d/sysntpd status
Usage: ntpd [-dnqNwl] [-I IFACE] [-S PROG] [-p PEER]...
NTP client/server
-d[d] Verbose
-n Run in foreground
-q Quit after clock is set
-N Run at high priority
-w Do not set time (only query peers), implies -n
-S PROG Run PROG after stepping time, stratum change, and every 11 min
-p PEER Obtain time from PEER (may be repeated)
-l Also run as server on port 123
-I IFACE Bind server to IFACE, implies -l
root@lp-arm-5:~# ethtool -T eth0
Time stamping parameters for eth0:
Capabilities:
hardware-transmit
software-transmit
hardware-receive
software-receive
software-system-clock
hardware-raw-clock
Hardware timestamp provider index: 0
Hardware timestamp provider qualifier: Precise (IEEE 1588 quality)
Hardware Transmit Timestamp Modes:
off
on
onestep-sync
Hardware Receive Filter Modes:
none
all
root@lp-arm-5:~# phc_ctl /dev/ptp0 get
phc_ctl[644486.511]: clock time is 1787465854.590438111 or Sun Aug 23 11:47:34 2026
root@lp-arm-5:~# phc_ctl /dev/ptp0 caps
phc_ctl[644504.310]:
capabilities:
64000000 maximum frequency adjustment (ppb)
0 programable alarms
0 external time stamp channels
0 programmable periodic signals
0 configurable input/output pins
has pulse per second support
doesn't have cross timestamping support
doesn't have adjust phase support
root@lp-arm-5:~# date
sudo phc_ctl /dev/ptp0 get
sudo hwclock -r
Sun Aug 23 13:21:04 IST 2026
phc_ctl[645456.567]: clock time is 1787466824.601451911 or Sun Aug 23 12:03:44 2026
2026-08-23 13:21:04.580550+05:30
root@lp-arm-5:~# timedatectl && ntpdate -q 192.168.10.90
Local time: Sat 2026-08-22 14:55:11 IST
Universal time: Sat 2026-08-22 09:25:11 UTC
RTC time: Sat 2026-08-22 09:25:11
Time zone: Asia/Kolkata (IST, +0530)
System clock synchronized: yes
NTP service: inactive
RTC in local TZ: no
2026-08-22 14:55:11.120999 (+0530) +0.017578 +/- 0.004635 192.168.10.90 s1 no-leap
root@lp-arm-5:~# while true; do ntpdate -q 192.168.10.90; sleep 1; done
2026-08-22 14:56:07.223000 (+0530) +0.014346 +/- 0.009613 192.168.10.90 s1 no-leap
2026-08-22 14:56:08.310000 (+0530) +0.016642 +/- 0.008483 192.168.10.90 s1 no-leap
2026-08-22 14:56:09.393000 (+0530) +0.019201 +/- 0.004101 192.168.10.90 s1 no-leap
2026-08-22 14:56:10.473000 (+0530) +0.016223 +/- 0.012315 192.168.10.90 s1 no-leap
After ~12 hours
root@lp-arm-5:~# head /var/log/gnss.log
===== GNSS NTP measurement Sat Aug 22 23:12:48 IST 2026 =====
2026-08-22 23:11:43.945 offset=158783.000 us drift=N/A us/s drift=N/A ppm
2026-08-22 23:11:45.030 offset=156998.000 us drift=1625.399 us/s drift=1625.399 ppm
2026-08-22 23:11:46.114 offset=160638.000 us drift=-3348.105 us/s drift=-3348.105 ppm
2026-08-22 23:11:47.198 offset=162109.000 us drift=-1357.056 us/s drift=-1357.056 ppm
root@lp-arm-5:~# tail /var/log/gnss.log
2026-08-23 12:07:41.968 offset=395067.000 us drift=-5542.332 us/s drift=-5542.332 ppm
2026-08-23 12:07:43.079 offset=397609.000 us drift=-2343.630 us/s drift=-2343.630 ppm
2026-08-23 12:07:44.187 offset=394536.000 us drift=2763.032 us/s drift=2763.032 ppm
First sample:
2026-08-22 23:11:43.945
offset = 158783 us
= 158.783 ms
Last sample:
2026-08-23 12:07:50.758
offset = 389441 us
= 389.441 ms
Therefore:
389.441 ms - 158.783 ms
= 230.658 ms
So if this trend continued, you'd expect roughly:
1 day ≈ +428 ms
7 days ≈ +3.0 seconds
30 days ≈ +12.8 seconds
1 year ≈ +156 seconds (~2.6 minutes)
So the Pi4 clock offset increased by approximately: +230.7 ms
Chrony Service useful parameter
systemctl status chrony
nano /etc/chrony/chrony.conf
server 192.168.10.196 iburst minpoll 5 maxpoll 5
#5 is (2 raise to 5) = 32sec
makestep 1 3
Startup:
update #1 → offset 2.5 s → STEP immediately
update #2 → offset 0.2 s → normal slew
update #3 → offset 1.2 s → STEP immediately
Later:
update #4 → offset 2.0 s → NO step; chrony slews it
maxupdateskew 100.0
means if chrony estimates the clock's frequency correction with uncertainty greater than 100 ppm, it will not use that update to adjust the clock. Lower = stricter confidence requirement.
I have raspberry pi 4 with raspbian os lite installed and the OS is directly installed on SSD which is connected with USB3.0 with connector USB-to-SATA. Whenever the power outage happens the pi some time boots and sometime it doesn’t and it also takes lots of time to boot 10min+
First I checked with below command what services are taking longer time and disabled docker and kubelet rpi-eeprom-update
root@lp-arm-3:~# systemd-analyze blame
1min 36.522s docker.service
51.427s containerd.service
50.609s rpi-eeprom-update.service
50.590s user@1000.service
50.221s ModemManager.service
49.702s NetworkManager-wait-online.service
38.108s apt-daily-upgrade.service
32.277s dpkg-db-backup.service
2.558s dev-sda2.device
1.281s raspi-config.service
670ms man-db.service
613ms NetworkManager.service
583ms keyboard-setup.service
536ms systemd-logind.service
512ms systemd-udev-trigger.service
429ms systemd-journald.service
root@lp-arm-3:~# systemd-analyze
Startup finished in 36.419s (kernel) + 2min 33.375s (userspace) = 3min 9.794s
multi-user.target reached after 2min 33.318s in userspace.
root@lp-arm-3:~# systemd-analyze critical-chain
The time when unit became active or started is printed after the "@" character.
The time the unit took to start is printed after the "+" character.
multi-user.target @2min 33.318s
└─kubelet.service @1min 6.646s
└─network-online.target @55.038s
└─NetworkManager-wait-online.service @5.315s +49.702s
└─NetworkManager.service @4.687s +613ms
└─dbus.service @4.552s +97ms
└─basic.target @4.509s
└─sockets.target @4.509s
└─docker.socket @4.485s +23ms
└─sysinit.target @4.468s
└─systemd-timesyncd.service @4.091s +375ms
└─systemd-tmpfiles-setup.service @3.805s +208ms
└─local-fs.target @3.777s
└─run-user-1000.mount @5.576s
└─local-fs-pre.target @1.650s
└─keyboard-setup.service @1.065s +583ms
└─systemd-journald.socket @1.019s
└─-.mount @946ms
└─-.slice @946ms
This did not fix issue.
Main issue was USB SATA driver speed.
I enabled permanent logs for boot
journalctl --list-boots
journalctl -b -1
show only error
journalctl -b -1 -p err
Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#9 uas_eh_abort_handler 0 uas-tag 15 inflight: CMD OUT Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#9 CDB: opcode=0x2a 2a 00 01 50 21 78 00 00 08 00 Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#4 uas_eh_abort_handler 0 uas-tag 6 inflight: CMD Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#4 CDB: opcode=0x35 35 00 00 00 00 00 00 00 00 00 Feb 07 20:40:53 lp-arm-3.home kernel: scsi host0: uas_eh_device_reset_handler start Feb 07 20:40:53 lp-arm-3.home kernel: usb 2-2: reset SuperSpeed USB device number 2 using xhci_hcd Feb 07 20:40:53 lp-arm-3.home kernel: scsi host0: uas_eh_device_reset_handler success Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#8 UNKNOWN(0x2003) Result: hostbyte=0x07 driverbyte=DRIVER_OK cmd_age=61s Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#8 CDB: opcode=0x28 28 00 00 5e dc f8 00 00 10 00 Feb 07 20:40:53 lp-arm-3.home kernel: I/O error, dev sda, sector 6216952 op 0x0:(READ) flags 0x80700 phys_seg 2 prio class 2 Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#2 UNKNOWN(0x2003) Result: hostbyte=0x07 driverbyte=DRIVER_OK cmd_age=61s Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#2 CDB: opcode=0x28 28 00 02 88 f5 e0 00 00 30 00 Feb 07 20:40:53 lp-arm-3.home kernel: I/O error, dev sda, sector 42530272 op 0x0:(READ) flags 0x80700 phys_seg 6 prio class 2 Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#1 UNKNOWN(0x2003) Result: hostbyte=0x07 driverbyte=DRIVER_OK cmd_age=61s Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#1 CDB: opcode=0x28 28 00 00 5e cb c8 00 01 00 00 Feb 07 20:40:53 lp-arm-3.home kernel: I/O error, dev sda, sector 6212552 op 0x0:(READ) flags 0x80700 phys_seg 32 prio class 2 Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#3 UNKNOWN(0x2003) Result: hostbyte=0x07 driverbyte=DRIVER_OK cmd_age=61s Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#3 CDB: opcode=0x28 28 00 02 84 ee 68 00 00 50 00 Feb 07 20:40:53 lp-arm-3.home kernel: I/O error, dev sda, sector 42266216 op 0x0:(READ) flags 0x80700 phys_seg 10 prio class 2 Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#6 UNKNOWN(0x2003) Result: hostbyte=0x07 driverbyte=DRIVER_OK cmd_age=55s Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#6 CDB: opcode=0x28 28 00 02 80 6e 38 00 00 90 00 Feb 07 20:40:53 lp-arm-3.home kernel: I/O error, dev sda, sector 41971256 op 0x0:(READ) flags 0x80700 phys_seg 18 prio class 2 Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#7 UNKNOWN(0x2003) Result: hostbyte=0x07 driverbyte=DRIVER_OK cmd_age=49s Feb 07 20:40:53 lp-arm-3.home kernel: sd 0:0:0:0: [sda] tag#7 CDB: opcode=0x28 28 00 02 80 3c d0 00 00 58 00 Feb 07 20:40:53 lp-arm-3.home kernel: I/O error, dev sda, sector 41958608 op 0x0:(READ) flags 0x80700 phys_seg 11 prio class 2
The kernel is resetting the USB
Disabled UAS for SSD – fixed the issue
lsusb
#get the USB ID eg 7825:a2a4
vi /boot/firmware/cmdline.txt
append this line at the end
usb-storage.quirks=7825:a2a4:u
reboot
root@lp-arm-3:~# lsusb -t
/: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 5000M
|__ Port 2: Dev 2, If 0, Class=Mass Storage, Driver=usb-storage, 5000M
/: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 480M
|__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
usb-storage (BOT):
sends one command at a time
waits for completion
has simpler recovery
tolerates retries and delays