FreeBSD 14 USB - BYO-NTP/recipes GitHub Wiki

This page is a build recipe for Stratum 1 NTP servers on any USB-equipped server with FreeBSD 14, using one of the 4 GNSS listed in the table and any of the NTP daemons listed. For modern computers without a UART (serial port) or GPIO, USB + PPS is capable of precision within single digit microseconds (~/- .000009 seconds). For a Stratum 0 NTP clock, that is excessively adequate.

date server os gnss daemon 🗣️
2025-07 Chuwi Larkbox X
Raspberry Pi 4
Raspberry Pi 5
Xeon E5
FreeBSD
14
M8T
M8Q
7M
U7
6M
chrony
NTPsec
ntp
link

1. Install OS

FreeBSD install instructions for Pi 4, Pi 5, or Chuwi Larkbox. For everything else, just follow the FreeBSD handbook.

2. Configure GNSS

Follow the assembly instructions for your GNSS on the Assemble USB TTL page.

3. Configure USB

If you don't use the mouse on your server, avoid potential interference from the USB driver detecting the GNSS serial adapter as a mouse:

sysrc devmatch_blocklist+="ums"

Plug the USB adapter into the FreeBSD server and verify it is present:

usbconfig | egrep -i 'uart|serial|blox|gps'
# FTDI USB to TTL adapter
ugen1.2: <FT232 Serial (UART) IC Future Technology Devices International, Ltd> at usbus1, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (90mA)
# VK-162
ugen1.6: <u-blox AG - www.u-blox.com u-blox 7 - GPS/GNSS Receiver> at usbus1
umodem0: <u-blox AG - www.u-blox.com u-blox 7 - GPS/GNSS Receiver, class 2/0, rev 1.10/1.00, addr 5> on usbus1

FreeBSD has recognized the USB device, the uftdi or umodem driver has configured it, and ucom will have presented it as a tty.

Find the new TTY:

ls /dev/cuaU?
/dev/cuaU0

There it is at cuaU0. Export it so all the subsequent commands can find it:

export GNSS_TTY_DEVICE=cuaU0

Configure the TTY and read some data from it (Control-C to disconnect):

stty -f /dev/$GNSS_TTY_DEVICE.init cs8 clocal -cstopb -parenb -echo raw 115200
cat /dev/$GNSS_TTY_DEVICE
$GPRMC,145934.00,A,4746.15234,N,12219.54351,W,0.056,,050625,,*09
$GPZDA,145934.00,05,06,2025,00,00*6E
$GPRMC,145935.00,A,4746.15237,N,12219.54346,W,0.056,,050625,,*0D
$GPZDA,145935.00,05,06,2025,00,00*6F
$GPRMC,145936.00,A,4746.15241,N,12219.54341,W,0.058,,050625,,*06
$GPZDA,145936.00,05,06,2025,00,00*6C
^C

It works! Make the serial port change permanent:

test -d /usr/local/etc/devd || mkdir -p /usr/local/etc/devd
cat > /usr/local/etc/devd/gps_tty_init.conf <<EO_GPS_TTY
notify 0 {
        match "cdev" "cuaU[0-1]";
        action "stty -f /dev/$cdev.init cs8 clocal -cstopb -parenb -echo raw 115200";
};
EO_GPS_TTY
service devd restart

The NTP daemons look to /dev/gps0 by default, so link gps0 to the USB serial port using devfs:

test -L /dev/gps0 && rm /dev/gps0
grep -q gps0 /etc/devfs.conf && sed -i '' -e '/^link.*gps0$/d' /etc/devfs.conf
echo "link	$GNSS_TTY_DEVICE	gps0" >> /etc/devfs.conf
service devfs restart

4. Configure PPS

Note: VK-162 devices get to skip this step.

Enable PPS on the UART CTS pin to match the wiring. See ucom for details.

sysctl hw.usb.ucom.pps_mode=1
grep -q pps_mode /boot/loader.conf || echo 'hw.usb.ucom.pps_mode=1' >> /boot/loader.conf

Create /dev/pps0 for the NTP daemons using devfs links:

test -L /dev/pps0 && rm /dev/pps0
grep -q pps0 /etc/devfs.conf && sed -i '' -e '/^link.*pps0$/d' /etc/devfs.conf
cat >> /etc/devfs.conf <<EOL
link	$GNSS_TTY_DEVICE	pps0
link	$GNSS_TTY_DEVICE	gpspps0
EOL
service devfs restart

5. Install a NTP daemon

Offsets

The latency of each GNSS requires a custom offset (in milliseconds), so export the GNSS specific values from the table (replace the ?s):

gnss NMEA PPS
M8T 0.14 0.1
M8Q 0.09 0.1
7M 0.11 0.1
U7 0.05
6M 0.7 0.1
export GNSS_OFFSET_NMEA="0.??"
export GNSS_OFFSET_PPS="0.??"

If needed, each section heading is a link with many more details about installing, configuring, and verifying that particular NTP daemon. Otherwise, choose the commands for the NTP daemon you want and the installer will do the rest.

export NTP_REFCLOCKS=$(cat <<EO_CHRONY
refclock SHM 0         refid NMEA precision 3e-2 poll 4 offset $GNSS_OFFSET_NMEA prefer
refclock PPS /dev/pps0 refid PPS  precision 5e-6 poll 2 offset $GNSS_OFFSET_PPS minsamples 32 lock NMEA trust
EO_CHRONY
)
curl -sS https://byo-ntp.github.io/tools/chrony/install.sh | sh

Note: minsamples 32 and a quick poll time (1 or 2) smoothes out much of the USB-induced jitter.

Note: NTPsec's NMEA+PPS has a pathological case when the GPS fix is lost and PPS drops. The accuracy then is worse than using only the NMEA source. Increasing the poll frequency to minpoll 3 maxpoll 5 improves it but the gpsd driver handles that case much better. In every other case, reducing the polling time improves accuracy.

NTPsec nmea

export NTP_REFCLOCKS=$(cat <<EO_NTPSEC_NMEA
refclock nmea refid NMEA minpoll 4 maxpoll 5 time2 $GNSS_OFFSET_NMEA prefer mode 8 baud 115200
refclock pps  refid PPS  minpoll 1 maxpoll 2 time1 $GNSS_OFFSET_PPS  prefer
EO_NTPSEC_NMEA
)
curl -sS https://byo-ntp.github.io/tools/ntpsec/install.sh | sh

Alternatively, via NTPsec's GPSD JSON driver:

export NTP_REFCLOCKS=$(cat <<EO_NTPSEC_GPSD
refclock gpsd unit 0   refid NMEA minpoll 3 maxpoll 5 time2 $GNSS_OFFSET_NMEA prefer
refclock gpsd unit 128 refid PPS  minpoll 1 maxpoll 2 time1 0.0 flag1 1 prefer
EO_NTPSEC_GPSD
)
curl -sS https://byo-ntp.github.io/tools/ntpsec/install.sh | sh
export NTP_REFCLOCKS=$(cat <<EO_NTP
server 127.127.20.0 minpoll 4 maxpoll 5 mode 88 prefer
fudge  127.127.20.0 refid NMEA time2 $GNSS_OFFSET_NMEA minjitter 0.03

server 127.127.22.0 minpoll 3 maxpoll 4
fudge  127.127.22.0 refid PPS time1 $GNSS_OFFSET_PPS minjitter 0.005
EO_NTP
)
curl -sS https://byo-ntp.github.io/tools/ntp/install.sh | sh

Alternatively, via gpsd using ntp's GPSD-NG driver:

export NTP_REFCLOCKS=$(cat <<EO_NTP_NG
server 127.127.46.0 minpoll 3 maxpoll 4 mode 0 prefer
fudge 127.127.46.0 refid NMEA time2 $GNSS_OFFSET_NMEA minjitter 0.05

server 127.127.46.128 minpoll 3 maxpoll 4 prefer
fudge 127.127.46.128 refid PPS flag1 1 time1 0.0
EO_NTP_NG
)
curl -sS https://byo-ntp.github.io/tools/ntp/install.sh | sh

Observe

  • watch the status in near real time (requires installing gnu-watch):
    • printf '\e[8;9;80t'; ssh -t pi5 gnu-watch -n2 chronyc sources
    • printf '\e[8;9;80t'; ssh -t pi5 gnu-watch -n2 ntpq -c peer
  • Measure the offset
  • Gather statistics with telegraf + influxdb + grafana or similar.

References

Performance

gnss chrony NTPsec ntp
M8T 3.3 µs
88 µs
70 µs
M8Q 1.9 µs
62 µs
61 µs
7M ? µs
? µs
143 µs
VK-172 2600 µs
5800 µs
6800 µs
6M 4.5 µs
25 µs
22 µs
⚠️ **GitHub.com Fallback** ⚠️