<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://satpulse.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://satpulse.net/" rel="alternate" type="text/html" /><updated>2026-08-16T07:10:02+00:00</updated><id>https://satpulse.net/feed.xml</id><title type="html">SatPulse</title><subtitle>SatPulse makes it easy to use a GPS receiver as a source of time for a PTP and NTP server</subtitle><author><name>James Clark</name></author><entry><title type="html">Radxa ZERO 3E with a Waveshare GNSS HAT</title><link href="https://satpulse.net/2026/06/09/radxa-zero-3e-with-a-waveshare-gnss-hat.html" rel="alternate" type="text/html" title="Radxa ZERO 3E with a Waveshare GNSS HAT" /><published>2026-06-09T00:00:00+00:00</published><updated>2026-06-09T00:00:00+00:00</updated><id>https://satpulse.net/2026/06/09/radxa-zero-3e-with-a-waveshare-gnss-hat</id><content type="html" xml:base="https://satpulse.net/2026/06/09/radxa-zero-3e-with-a-waveshare-gnss-hat.html"><![CDATA[<p>I have been wondering whether there are any SBCs out there that might have advantages over Raspberry Pis for connection to GNSS receivers.
I am happy to report that the Radxa ZERO 3E does pretty well:
Gigabit Ethernet in a Pi Zero form factor is a nice combo.</p>

<h2 id="radxa-zero-3e">Radxa ZERO 3E</h2>

<p>I have been experimenting with a <a href="https://radxa.com/products/zeros/zero3e/">Radxa ZERO 3E</a>. Key hardware features</p>

<ul>
  <li>Pi Zero form factor</li>
  <li>1.6GHz quad core Cortex-A55 (roughly Pi 3B+ performance)</li>
  <li>1 to 8GB RAM</li>
  <li>Gigabit Ethernet port</li>
  <li>40-pin GPIO</li>
  <li>optional PoE HAT</li>
  <li>USB 2.0 type C power port</li>
  <li>USB 3.0 type C port</li>
  <li>micro HDMI video port</li>
</ul>

<p>I was able to buy a 1GB model a month ago on AliExpress for $24 excluding shipping.</p>

<p>This is quite a compelling hardware proposition for something to connect a GNSS module to compared to what Raspberry Pi has on offer.
The strength of Raspberry Pi is in the software ecosystem.
But this has Armbian standard support, which as far as I can tell is what you want if you are not using Raspberry Pi.</p>

<h2 id="waveshare-gnss-hats">Waveshare GNSS HATs</h2>

<p>I was particularly interested in getting something in the Pi Zero form factor in order to combine it with a HAT because <a href="https://www.waveshare.com/">Waveshare</a> make a number of GNSS modules in a Pi Zero form factor designed to plug into the 40-pin GPIO connector on a Raspberry Pi. They have versions using:</p>

<ul>
  <li><a href="https://www.waveshare.com/product/iot-communication/long-range-wireless/gnss-gps/lc29h-gps-hat.htm">Quectel LC29H</a></li>
  <li><a href="https://www.waveshare.com/product/iot-communication/long-range-wireless/gnss-gps/l76k-gps-hat.htm">Quectel L76K</a></li>
  <li><a href="https://www.waveshare.com/product/iot-communication/long-range-wireless/gnss-gps/neo-m8t-gnss-timing-hat.htm">u-blox NEO-M8T</a></li>
  <li><a href="https://www.waveshare.com/product/iot-communication/long-range-wireless/gnss-gps/zed-f9p-gps-rtk-hat.htm">u-blox ZED-F9P</a></li>
  <li><a href="https://www.waveshare.com/product/iot-communication/long-range-wireless/gnss-gps/max-m8q-gnss-hat.htm">u-blox MAX-M8Q</a></li>
</ul>

<p>I tried it with the LC29H(DA) which is RTK capable. Physically they connect together with no problem.
The combination is tiny and quite neat.</p>

<h2 id="os-setup">OS setup</h2>

<p>Setting up the OS isn’t hard but is not completely obvious.</p>

<p>I first downloaded an <a href="https://armbian.com/boards/radxa-zero3">Armbian image</a>.
I chose the Minimal, Debian 13 image with the non-vendor kernel 6.18.9.
I used <a href="https://etcher.balena.io/">Balena Etcher</a> on macOS to write the image.</p>

<p>Armbian setup needs a monitor and keyboard attached.</p>

<p>There are two things that need setting up: the serial connection and the PPS connection.</p>

<h3 id="serial">Serial</h3>

<p>The UART on the HAT is wired to pins 8 and 10 on the GPIO header.
These are UART2 on the ZERO 3 which shows up as /dev/ttyS2.
By default, this is enabled but configured as a serial console.
So all that is necessary is to stop it being used as a serial console.</p>

<p>To do this, first edit <code class="language-plaintext highlighter-rouge">/boot/armbianEnv.txt</code> to change the line with <code class="language-plaintext highlighter-rouge">console=both</code> to <code class="language-plaintext highlighter-rouge">console=display</code>.</p>

<p>Then disable the getty service:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl disable --now serial-getty@ttyS2.service
sudo systemctl mask serial-getty@ttyS2.service
</code></pre></div></div>

<h3 id="pps">PPS</h3>

<p>Most Waveshare HATs wire the GNSS PPS output to pin 12.
(The L76K does not wire PPS up at all, so it won’t work well for timing applications.)
To make PPS work, we therefore have to create a device overlay that wires up pin 12 on the Radxa as a PPS input.</p>

<p>Create a file <code class="language-plaintext highlighter-rouge">/tmp/rk3566-zero3e-pps-gpio12.dts</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/dts-v1/;
/plugin/;

/ {
	compatible = "radxa,zero-3e", "rockchip,rk3566";

	fragment@0 {
		target-path = "/";
		__overlay__ {
			pps_gpio12: pps-gpio12 {
				compatible = "pps-gpio";
				pinctrl-names = "default";
				pinctrl-0 = &lt;&amp;pps_gpio12_pin&gt;;

				/*
				 * Physical pin 12 = GPIO3_A3.
				 * Rockchip GPIO specifier is &lt;controller line flags&gt;,
				 * so this is gpio3 line 3, active high.
				 */
				gpios = &lt;&amp;gpio3 3 0&gt;;

				/*
				 * For normal GNSS 1PPS active-high pulses, leave this out.
				 * For falling-edge PPS timestamping, uncomment:
				 *
				 * assert-falling-edge;
				 */

				status = "okay";
			};
		};
	};

	fragment@1 {
		target = &lt;&amp;pinctrl&gt;;
		__overlay__ {
			pps-gpio12 {
				pps_gpio12_pin: pps-gpio12-pin {
					/*
					 * &lt;bank pin mux config&gt;
					 * bank 3, PA3, mux 0 = GPIO, no pull.
					 */
					rockchip,pins = &lt;3 3 0 &amp;pcfg_pull_none&gt;;
				};
			};
		};
	};
};
</code></pre></div></div>
<p>(ChatGPT was able to create something that worked first time, slightly to my surprise.)</p>

<p>To install this overlay:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt install -y device-tree-compiler
sudo armbian-add-overlay /tmp/rk3566-zero3e-pps-gpio12.dts
</code></pre></div></div>

<p>Then reboot</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo reboot
</code></pre></div></div>

<p>This creates a <code class="language-plaintext highlighter-rouge">/dev/pps0</code> device.</p>

<h2 id="satpulse">SatPulse</h2>

<p>To use the LC29H with SatPulse, this is enough to get started:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[serial]
device = "/dev/ttyS2"
speed = 115200

[gps]
vendor = "quectel"

[[http]]
listen = ":2000"
</code></pre></div></div>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[I have been wondering whether there are any SBCs out there that might have advantages over Raspberry Pis for connection to GNSS receivers. I am happy to report that the Radxa ZERO 3E does pretty well: Gigabit Ethernet in a Pi Zero form factor is a nice combo.]]></summary></entry><entry><title type="html">SatPulse 0.2 released</title><link href="https://satpulse.net/2026/05/08/satpulse-0.2-released.html" rel="alternate" type="text/html" title="SatPulse 0.2 released" /><published>2026-05-08T02:30:00+00:00</published><updated>2026-05-08T02:30:00+00:00</updated><id>https://satpulse.net/2026/05/08/satpulse-0.2-released</id><content type="html" xml:base="https://satpulse.net/2026/05/08/satpulse-0.2-released.html"><![CDATA[<p>I have released <a href="https://github.com/jclark/satpulse/releases/tag/v0.2">SatPulse 0.2</a>. This is a major new release with a lot of new functionality (more than 60% of the code is new since 0.1).</p>

<p>Highlights of this release:</p>

<ul>
  <li>there is a <a href="/2026/05/05/evolving-a-new-phc-synchronization-architecture-for-satpulse-0.2.html">new PHC synchronization architecture</a>, including a GNSS/PHC simulator (which has a <a href="/man/satpulsetool-syncsim.1.html">CLI</a>)</li>
  <li>it supports <a href="/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse.html">seven vendor protocols in addition to UBX</a></li>
  <li>it is evolving beyond the original PTP use case to become an <a href="/2026/04/11/design-of-satpulse-compared-with-gpsd.html">alternative to gpsd</a> for some server-side use cases, in particular it can now be used as a <a href="/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs.html">source of time for an NTP server</a> (either <a href="https://chrony-project.org/">chrony</a> or <a href="https://github.com/pendulum-project/ntpd-rs">ntpd-rs</a>) <a href="/2026/04/01/using-satpulse-for-timing-without-a-phc.html">without needing a PHC</a></li>
  <li>it has had <a href="/2026/05/03/satpulse-0.2-hardware-test-matrix.html">extensive testing on a wide range of hardware</a></li>
</ul>

<p>I created a <a href="https://github.com/jclark/satpulse/discussions/268">discussion thread</a> where you can ask questions, share experiences and suggest new features.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[I have released SatPulse 0.2. This is a major new release with a lot of new functionality (more than 60% of the code is new since 0.1).]]></summary></entry><entry><title type="html">Evolving a new PHC synchronization architecture for SatPulse 0.2</title><link href="https://satpulse.net/2026/05/05/evolving-a-new-phc-synchronization-architecture-for-satpulse-0.2.html" rel="alternate" type="text/html" title="Evolving a new PHC synchronization architecture for SatPulse 0.2" /><published>2026-05-05T04:30:00+00:00</published><updated>2026-05-05T04:30:00+00:00</updated><id>https://satpulse.net/2026/05/05/evolving-a-new-phc-synchronization-architecture-for-satpulse-0.2</id><content type="html" xml:base="https://satpulse.net/2026/05/05/evolving-a-new-phc-synchronization-architecture-for-satpulse-0.2.html"><![CDATA[<p>One of the major changes in SatPulse 0.2 is a new architecture for the PHC synchronization subsystem.
The PHC synchronization subsystem has two inputs: a stream of timestamps from the PHC and a stream of messages from the GPS receiver.
Its primary function is to synchronize the time of the PHC with the GPS receiver’s time.
This involves generating a stream of <em>samples</em> from the two streams of timestamps and messages.
A sample says what the offset is between PHC time and GNSS time.
These samples are then used to synchronize the PHC and to update the PTP grandmaster with the synchronization status.</p>

<p>Generating samples includes the following tasks:</p>
<ul>
  <li>pulse edge filtering: some Intel NICs generate timestamps for both edges of a pulse; in this case, we have to identify which edges are leading edges</li>
  <li>sample completion: the timestamp for a leading edge marks the top of a second; completing the sample means determining which second that is</li>
  <li>sawtooth correction: a GPS receiver can only generate a pulse on an edge of its internal clock, but there can be an offset between the edge of its internal clock and the top of the second; a timing-grade GPS receiver will output a message for each pulse giving the size of this offset; the sample then needs to be adjusted for this offset</li>
</ul>

<h2 id="01-phc-synchronization-architecture">0.1 PHC synchronization architecture</h2>

<p>My initial implementation of PHC synchronization followed the approach of the <code class="language-plaintext highlighter-rouge">ts2phc</code> program,
included in LinuxPTP.
The approach consists of a 2-stage pipeline.
The initial stage generates samples by combining timestamps from the PHC with time-of-day information from GPS messages.
The samples are fed into a second stage, which uses a PI servo to adjust the phase and frequency of the PHC.</p>

<p>This approach evolved to add a monitoring stage to the pipeline between the sample-generation stage and the servo.
This monitoring stage had a variety of responsibilities.
It determined whether the PHC was in sync with GNSS time, and used this to dynamically update
the PTP grandmaster’s clock quality.
It also performed outlier detection using a MAD algorithm.</p>

<p>I found two major problems with this pipeline approach.
The first problem was that each stage in the pipeline ended up
maintaining its own state, but these states were not coordinated.</p>

<ul>
  <li>the sample-generation stage had an initialization state for analyzing the intervals between edges;
this was used with Intel NICs that timestamp both edges of a pulse to ensure that trailing edges were ignored</li>
  <li>the monitoring stage maintained state of whether the PHC was synchronized to GNSS time</li>
  <li>the servo stage maintained state related to deciding whether to step the PHC</li>
</ul>

<p>This became particularly problematic for the sample-generation stage.
It’s important for PHC synchronization to be as reliable as possible.
I found that GPS messages were not completely reliable for determining time-of-day information.
Perhaps the most common problem is that the GPS emits too many messages for the available serial bandwidth,
which causes messages to be delayed or dropped.
When in a synchronized state, a more reliable way is to use the PHC, since the PHC will be accurate to within a microsecond or so,
but this doesn’t work at all when the PHC is not synchronized.
However, the sample-generation stage doesn’t have access to the synchronization state of downstream stages.
The sample-generation stage became increasingly complex over time, using ad hoc heuristics to decide whether to prefer
information from the PHC or from messages.</p>

<p>This ties into the second main problem. I had very limited ability to test the pipeline as a whole.
My main approach was to save the inputs and outputs of the sample-generation stage;
I could then replay the inputs to make sure they produced the same outputs.
But if the sample-generation stage was affected by the monitoring stage, this would no longer be possible.</p>

<p>The first problem meant that a rewrite was needed: I hadn’t decomposed the problem in the best way.
And if I was going to do a rewrite, then I should solve the second problem once and for all,
and that meant I needed a simulator.</p>

<p>The most significant open source project in the timing simulation space that I know of is Miroslav Lichvar’s <a href="https://gitlab.com/chrony/clknetsim">clknetsim</a>.
In fact, this project is what made me realise that serious testing needed a simulator.
However, clknetsim would not work for SatPulse, because it relies on being able to redirect system calls using LD_PRELOAD,
but SatPulse is written in Go and on Linux Go usually produces statically linked executables; system calls are raw kernel syscalls, which do not
go through a dynamic library. So that meant I needed to develop my own simulator.
Also clknetsim does not handle the GPS side of things.</p>

<h2 id="simulator">Simulator</h2>

<p>The goal of the simulator is not to be perfect, but to be realistic enough to enable closed-loop testing of synchronization algorithms.</p>

<p>The simulator is initialized with a configuration and performs a simulation for some period of time.
The simulator is driven by the progress of simulated time, which represents true time.
As simulated time progresses, the simulator emits timestamps and GPS messages.
It also implements a PHC interface that can be used to adjust the phase and frequency of the simulated PHC.
There is a crucial feedback loop:
each timestamp is measured with respect to the PHC and has to take account of any phase and frequency adjustments made through the PHC interface.
Another complicating factor is that GPS messages can include sawtooth corrections for the PPS signal
and these corrections have to match the timestamps being generated.</p>

<p>When run under a simulator, the code under test produces its normal output,
but the simulator can observe the offsets between the simulated true time and the simulated PHC.
It can produce a log of these offsets and also generate statistics such as the maximum offset and the Allan deviation.
These statistics could only be produced in real-world testing by using a reference clock that tracks UTC with much greater accuracy than a GPS PPS signal. This would require expensive hardware such as a caesium clock or better still, a hydrogen maser; a rubidium clock would not be sufficient.</p>

<p>The configuration includes error models for the PHC oscillator and the GPS PPS signal.
Each error model consists of a number of components that describe different sources of error, which are combined additively.
For example, the PHC error model has components for white, flicker and random walk FM noise.
The GPS PPS error model includes a component for sawtooth error,
which is used in generating both the timestamp for the PPS edge and the sawtooth correction in the corresponding GPS message.</p>

<p>In Go, the error models are represented by <code class="language-plaintext highlighter-rouge">func(t float64) float64</code>:
the return value gives the instantaneous error at simulated time t.
The return value for the PHC error model is a frequency error,
whereas the return value for the GPS PPS error model is a phase error.
This reflects the underlying physical reality that an oscillator is a continuous process whose state at any instant is a rate, whereas a PPS signal is a discrete process whose state for each pulse is a position in time.</p>

<p>The error models can be derived from physical measurements made of the PHC oscillator and the GPS PPS signal.
(A PHC oscillator can be measured by making the PHC output a PPS signal while free-running.)
In a future post, I will go into more detail about how I made measurements and used them to derive error models.</p>

<h2 id="02-phc-synchronization-architecture">0.2 PHC synchronization architecture</h2>

<p>The approach in 0.2 is modal. There are three modes: reset, converging and tracking.
At a high level, these modes work as follows.
Reset is the initial mode: its job is to generate a single, reliable sample; it does this by collecting a batch of timestamps and GPS messages.
After generating the sample, reset mode will perform a step of the PHC so as to guarantee that the PHC is close to the GNSS time.
At that point, it transitions to converging mode. Its job is to aggressively adjust the frequency so as to bring the PHC into as precise as possible alignment with GNSS time.
When the offsets between the PHC and timestamps are no longer decreasing, it transitions to tracking mode.
Its job is to continually tweak the PHC frequency so as to keep the offsets as small as possible.
It remains in tracking mode so long as the offsets indicate that the PHC is still synchronized to GNSS time.
If synchronization is lost, it transitions to reset mode.</p>

<p>Each mode is associated with a clock quality notified to the PTP grandmaster:
tracking mode is associated with a clock quality representing a synchronized state;
reset and converging mode are associated with a clock quality representing an unsynchronized state.</p>

<p>The following table summarizes the operation of the modes.</p>

<table>
  <thead>
    <tr>
      <th>Task</th>
      <th>Reset</th>
      <th>Converging</th>
      <th>Tracking</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pulse edge filtering</td>
      <td>analysis of batch of pulse edges</td>
      <td>parity from leading edge learned in reset</td>
      <td>alignment of edge to top of second</td>
    </tr>
    <tr>
      <td>Sample completion</td>
      <td>alignment of batch of timestamps and messages</td>
      <td>round timestamp to nearest second</td>
      <td>round timestamp to nearest second</td>
    </tr>
    <tr>
      <td>Sawtooth correction</td>
      <td>not applied</td>
      <td>not applied</td>
      <td>applied</td>
    </tr>
    <tr>
      <td>Outlier detection</td>
      <td>validation of batch of timestamps and messages</td>
      <td>none</td>
      <td>MAD-based</td>
    </tr>
    <tr>
      <td>PHC control</td>
      <td>step when leaving mode</td>
      <td>aggressive PI servo</td>
      <td>gentle PI servo</td>
    </tr>
    <tr>
      <td>Successful exit</td>
      <td>valid sample</td>
      <td>offsets stabilize</td>
      <td>none</td>
    </tr>
    <tr>
      <td>Failure exit</td>
      <td>none</td>
      <td>too many missing samples</td>
      <td>too many bad samples</td>
    </tr>
  </tbody>
</table>

<p>The key point to notice in the table is that each mode performs its tasks very differently.
I want to focus particularly on sample completion, which is the most fundamental part of sample generation.
In production, SatPulse should be spending 99.9999% of its time in tracking mode,
and in tracking mode sample completion is utterly trivial:
the PHC clock will be accurate to a microsecond, so you can just round the timestamp to know which second the timestamp is for.</p>

<p>In contrast, sample completion in reset mode is quite elaborate.
If reset mode makes the wrong choice of second, then that will persist throughout the operation of the daemon,
so the implementation takes a lot of care to ensure that it is right.
It collects time messages and timestamps for several seconds,
and then performs multiple consistency and quality checks.</p>

<p>Reset mode has to correlate time messages with timestamps.
For time messages, we record the monotonic time at which we read the first character of the message.
But these monotonic times cannot be compared directly with the timestamps, which are in the PHC time domain.
The natural way to handle this is to record the monotonic time immediately after the timestamp is read.
But the Raspberry Pi CM4/CM5 ethernet PHY driver has a quirk which makes this insufficient by itself:
the driver can deliver the timestamp to user space up to 0.25s after the pulse occurred.
To handle this, we also record the PHC time immediately after reading the timestamp,
and then adjust the post-read monotonic time by the difference between the post-read PHC time and the timestamp.
We also have to account for the possibility that the PHC is fast or slow.
The average interval in PHC time between successive pulses tells us how much PHC time corresponds to one second,
and we use this to scale the PHC difference before using it to adjust the monotonic time.</p>

<p>The decomposition of responsibilities is as follows.
The main implementation package is <code class="language-plaintext highlighter-rouge">phcsync</code>.
It has a controller, which is responsible for orchestrating the modes.
For each mode, there is a sample-generator and a sample-processor.
The sample-generator is responsible for pulse edge filtering, sample completion and sawtooth correction;
in tracking mode it uses the pulse width discovered in reset mode.
The sample-processor is responsible for outlier detection,
and for determining when and how to adjust the PHC and change mode;
these PHC adjustments and mode changes are then performed by the controller.
The sample-processors for converging and tracking mode share a PI servo implementation;
in tracking mode, the servo is initialized using the PHC frequency error discovered in reset mode.
The controller feeds samples from the sample-generator to the sample-processor.
The controller also synthesizes missing samples and feeds them to the sample-processor.
The controller notifies the PTP grandmaster for mode changes that imply a change in clock quality.
Thus, as in 0.1, there is a 3-stage pipeline: sample-generator then sample-processor then controller.
But the pipeline is driven by the controller, and the sample-generator and sample-processor
are mode-specific.</p>

<p>The other implementation package is <code class="language-plaintext highlighter-rouge">timemsg</code>,
which serves as a bridge between <code class="language-plaintext highlighter-rouge">phcsync</code> and the GPS subsystem.
<code class="language-plaintext highlighter-rouge">timemsg</code> maintains a buffer of recent time-related messages from the GPS receiver.
<code class="language-plaintext highlighter-rouge">phcsync</code> defines an interface which captures what it needs to know about time messages,
and <code class="language-plaintext highlighter-rouge">timemsg</code> implements this.
Reset mode obviously depends on this interface,
but tracking mode also uses it for sawtooth corrections.
This separation between <code class="language-plaintext highlighter-rouge">phcsync</code> and <code class="language-plaintext highlighter-rouge">timemsg</code> was also designed to enable <code class="language-plaintext highlighter-rouge">timemsg</code>
to be reused for a new feature in 0.2: samples can be provided to an NTP server
based on serial timing, without needing a PHC.</p>

<p>The benefits in terms of user-visible features of the 0.2 implementation are relatively modest.
Reset mode can disambiguate leading and trailing edges even with a 50% duty cycle.
PHC synchronization parameters are now fully configurable using a new <code class="language-plaintext highlighter-rouge">[sync]</code> section of the config file.
The simulator has a CLI that can be used to tune these parameters, in particular the Kp/Ki constants for the tracking servo.</p>

<p>But the major wins from the new architecture are in terms of improving reliability and providing a foundation for future development.
The most important missing feature at the moment is holdover:
the modal architecture can accommodate this in a natural way.
Sample generation has a clean and principled architecture that solves the problems this had in 0.1;
in tracking mode, it does not depend on time messages and so should be more reliable.
The most important aspect of the architecture is, I believe, the simulator.
This solves the testability problem we had in 0.1 and improves the reliability of 0.2.
But it is also crucial for future development: without a simulator,
it would be very difficult to develop a reliable implementation of complex features like holdover.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[One of the major changes in SatPulse 0.2 is a new architecture for the PHC synchronization subsystem. The PHC synchronization subsystem has two inputs: a stream of timestamps from the PHC and a stream of messages from the GPS receiver. Its primary function is to synchronize the time of the PHC with the GPS receiver’s time. This involves generating a stream of samples from the two streams of timestamps and messages. A sample says what the offset is between PHC time and GNSS time. These samples are then used to synchronize the PHC and to update the PTP grandmaster with the synchronization status.]]></summary></entry><entry><title type="html">SatPulse 0.2 hardware test matrix</title><link href="https://satpulse.net/2026/05/03/satpulse-0.2-hardware-test-matrix.html" rel="alternate" type="text/html" title="SatPulse 0.2 hardware test matrix" /><published>2026-05-03T07:10:00+00:00</published><updated>2026-05-03T07:10:00+00:00</updated><id>https://satpulse.net/2026/05/03/satpulse-0.2-hardware-test-matrix</id><content type="html" xml:base="https://satpulse.net/2026/05/03/satpulse-0.2-hardware-test-matrix.html"><![CDATA[<p>I have set up 12 different machines for automated testing of SatPulse.
I have chosen the hardware to provide coverage along multiple dimensions:
CPU architecture, OS, PHC, GPS protocol and serial connection type.</p>

<table>
  <thead>
    <tr>
      <th>Hostname</th>
      <th>Machine</th>
      <th>OS</th>
      <th>Ethernet</th>
      <th>GPS</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td rowspan="2">abondance</td>
      <td rowspan="2">ASUS S500SD</td>
      <td rowspan="2">Debian 13</td>
      <td>I225-T1</td>
      <td>ZED-F9P</td>
    </tr>
    <tr>
      <td>I225-T1</td>
      <td>BG7TBL CM55</td>
    </tr>
    <tr>
      <td>brie</td>
      <td>Intel 12th Gen PC</td>
      <td>Ubuntu 24.04</td>
      <td>E810-XXVDA4T</td>
      <td>NEO-F10T</td>
    </tr>
    <tr>
      <td>gubbeen</td>
      <td>Raspberry Pi 5</td>
      <td>Debian 13</td>
      <td>I210-T1</td>
      <td>M10050-KB</td>
    </tr>
    <tr>
      <td>maasdam</td>
      <td>Minisforum MS-01</td>
      <td>Fedora 43</td>
      <td>I226-T1</td>
      <td>ZED-F9T-20B</td>
    </tr>
    <tr>
      <td>morbier</td>
      <td>Raspberry Pi CM4</td>
      <td>Debian 11</td>
      <td>onboard</td>
      <td>LEA-M8F</td>
    </tr>
    <tr>
      <td>pecorino</td>
      <td>Raspberry Pi CM4</td>
      <td>Fedora 42</td>
      <td>onboard</td>
      <td>LEA-6T</td>
    </tr>
    <tr>
      <td>roquefort</td>
      <td>Raspberry Pi CM4</td>
      <td>Debian 12</td>
      <td>onboard</td>
      <td>ZED-X20P</td>
    </tr>
    <tr>
      <td>serpa</td>
      <td>Raspberry Pi CM5</td>
      <td>Debian 13</td>
      <td>onboard</td>
      <td>UM980</td>
    </tr>
    <tr>
      <td>taleggio</td>
      <td>Raspberry Pi 5</td>
      <td>Debian 12</td>
      <td>TimeHAT</td>
      <td>NEO-M9N</td>
    </tr>
    <tr>
      <td>valencay</td>
      <td>Raspberry Pi CM5</td>
      <td>Debian 12</td>
      <td>onboard</td>
      <td>LEA-M8T</td>
    </tr>
    <tr>
      <td>valtellina</td>
      <td>Raspberry Pi CM5</td>
      <td>Debian 13</td>
      <td>onboard</td>
      <td>NEO-F10N</td>
    </tr>
    <tr>
      <td>wensleydale</td>
      <td>Raspberry Pi CM4</td>
      <td>Debian 12</td>
      <td>onboard</td>
      <td>UM960</td>
    </tr>
  </tbody>
</table>

<p>More details:</p>

<dl>
  <dt>abondance</dt>
  <dd>ASUS S500SD desktop (Intel B660 chipset) with an Intel i5-12400 and 64 GB RAM. It has two Intel I225-T1 PCIe cards: one is connected to an internal ArduSimple simpleRTK2B M.2, which has a u-blox ZED-F9P, and the other is connected over RS232 to a BG7TBL CM55 GPSDO, which has a LEA-M8T. This supports cross-timestamping with <a href="/hardware/ptm.html">PTM</a>.</dd>
  <dt>brie</dt>
  <dd>MSI Z690I motherboard with an Intel i9-12900K and 32 GB RAM. It has an Intel E810-XXVDA4T PCIe card. GPS is a u-blox EVK-F10T connected by USB.</dd>
  <dt>gubbeen</dt>
  <dd>Raspberry Pi 5 with 16 GB RAM in a Geekworm X1010 expansion board with PCIe slot, which contains an Intel I210-T1. GPS is a u-blox M10050-KB (u-blox M10 standard-precision) connected to UART0.</dd>
  <dt>maasdam</dt>
  <dd>Minisforum MS-01 with an Intel i5-12600H and 32 GB RAM. It has an I226-T1 PCIe card. GPS is a u-blox ZED-F9T-20B in a gnss.store USB dongle. This supports cross-timestamping with <a href="/hardware/ptm.html">PTM</a>.</dd>
  <dt>morbier</dt>
  <dd>Raspberry Pi CM4 with 8 GB RAM and 32 GB eMMC, on the official IO board. GPS is a u-blox LEA-M8F on a Timebeat sandwich board.</dd>
  <dt>pecorino</dt>
  <dd>Raspberry Pi CM4 with 8 GB RAM and 32 GB eMMC, on the official IO board. GPS is a u-blox LEA-6T connected to UART3.</dd>
  <dt>roquefort</dt>
  <dd>Raspberry Pi CM4 with 4 GB RAM and 8 GB eMMC, on a Waveshare PoE board. GPS is a u-blox EVK-X20P connected by USB.</dd>
  <dt>serpa</dt>
  <dd>Raspberry Pi CM5 with 2 GB RAM and NVMe SSD, on a Geekworm X1500 carrier board. GPS is a Unicore UM980 connected to UART0.</dd>
  <dt>taleggio</dt>
  <dd>Raspberry Pi 5 with 4 GB RAM with a Timebeat TimeHAT, which uses an Intel I226-LM. GPS is a u-blox NEO-M9N in the TimeHAT’s M.2 slot.</dd>
  <dt>valencay</dt>
  <dd>Raspberry Pi CM5 with 2 GB RAM and 16 GB eMMC, on the official IO board. GPS is a u-blox LEA-M8T connected to UART0.</dd>
  <dt>valtellina</dt>
  <dd>Raspberry Pi CM5 Lite with 8 GB RAM and NVMe SSD, on the official IO board. GPS is a u-blox NEO-F10N connected to UART0.</dd>
  <dt>wensleydale</dt>
  <dd>Raspberry Pi CM4 with 8 GB RAM and 8 GB eMMC, on the official IO board. GPS is a Unicore UM960 connected to UART0.</dd>
</dl>

<p>The testing uses the Ansible playbooks in the <a href="https://github.com/jclark/satpulse/tree/master/systest">systest</a> directory.
It runs both satpulsed and chrony on the target machines, with satpulsed supplying samples for chrony.
The main checks are that</p>
<ul>
  <li>satpulsed logs show that synchronization has been established and not lost; and</li>
  <li>chrony has selected the satpulsed refclock; chrony is also configured to use highly-accurate NTP servers on the LAN, and won’t select the satpulsed refclock if it diverges too much from those NTP servers.</li>
</ul>

<p>It also uses the <code class="language-plaintext highlighter-rouge">clocklog.py</code> script to produce some statistics from the clock.log about the quality of synchronization.</p>

<p>I also have a <a href="/setup/monitor.html">Grafana dashboard</a> which shows metrics from each test machine.</p>

<p><img src="/assets/images/test-matrix-grafana-dashboard.png" alt="Grafana dashboard showing metrics from each test machine" /></p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[I have set up 12 different machines for automated testing of SatPulse. I have chosen the hardware to provide coverage along multiple dimensions: CPU architecture, OS, PHC, GPS protocol and serial connection type.]]></summary></entry><entry><title type="html">Desktop GUI preview</title><link href="https://satpulse.net/2026/04/26/desktop-gui-preview.html" rel="alternate" type="text/html" title="Desktop GUI preview" /><published>2026-04-26T00:00:00+00:00</published><updated>2026-04-26T00:00:00+00:00</updated><id>https://satpulse.net/2026/04/26/desktop-gui-preview</id><content type="html" xml:base="https://satpulse.net/2026/04/26/desktop-gui-preview.html"><![CDATA[<p>I have been working on a desktop GUI for SatPulse. Here are a couple of demo videos.</p>

<!-- Courtesy of embedresponsively.com -->

<div class="responsive-video-container">
    <iframe src="https://www.youtube-nocookie.com/embed/lBE5Ls4ly0M" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
  </div>

<!-- Courtesy of embedresponsively.com -->

<div class="responsive-video-container">
    <iframe src="https://www.youtube-nocookie.com/embed/aDrLsjJSugU" frameborder="0" webkitallowfullscreen="" mozallowfullscreen="" allowfullscreen=""></iframe>
  </div>

<p>This is a native app running on macOS. It uses the same
<a href="/2026/04/11/design-of-satpulse-compared-with-gpsd.html">GPS library</a>
that <code class="language-plaintext highlighter-rouge">satpulsed</code> and <code class="language-plaintext highlighter-rouge">satpulsetool</code> use.
The code is on the <code class="language-plaintext highlighter-rouge">desktop-gui</code> branch of the <a href="https://github.com/jclark/satpulse">repo</a>.</p>

<p>It uses a tabbed layout, with tabs for:</p>

<ul>
  <li>monitoring - this is conceptually quite similar to what the Web monitoring app does, but has a richer set of components</li>
  <li>log-level monitoring - this is similar to what you get with the packet log in <code class="language-plaintext highlighter-rouge">satpulsetool</code> or with the <code class="language-plaintext highlighter-rouge">--packet-log</code> option in <code class="language-plaintext highlighter-rouge">satpulsed</code>; but instead of giving a stream of packets, it groups packets by protocol and message ID; it also allows decoding of packets similar to what is provided by <code class="language-plaintext highlighter-rouge">satpulsetool decode</code></li>
  <li>high-level configuration - this allows you to configure the GPS receiver in a high-level, device-independent way, without needing any knowledge of the receiver protocol, similar to <code class="language-plaintext highlighter-rouge">satpulsetool gps</code></li>
  <li>low-level configuration - this uses message files to provide a lower-level device-dependent way, similar to <code class="language-plaintext highlighter-rouge">-m</code> and <code class="language-plaintext highlighter-rouge">-t</code> options of <code class="language-plaintext highlighter-rouge">satpulsetool gps</code></li>
  <li>corrections - this allows you to pull RTCM corrections from an NTRIP caster or TCP server and feed them to the receiver, with a live view of the corrections stream; I plan to provide this functionality in a future release of <code class="language-plaintext highlighter-rouge">satpulsed</code> using a <code class="language-plaintext highlighter-rouge">[stream.pull]</code> section in the configuration file</li>
</ul>

<p>This is built using <a href="https://wails.io/">Wails</a>, which supports Linux and Windows, as well as macOS.
But my initial testing has focused on macOS, partly because that’s what I use day-to-day for desktop work,
and partly because there’s a gap on macOS, since GNSS vendors provide their proprietary apps only for Windows.</p>

<p>This is still very much in the preview phase: some aspects of the UI are rough, particularly the monitoring tab.
With Wails, the UI is written using standard Web technologies (HTML, CSS and JavaScript),
which run in the platform’s native webview component.
The desktop GUI and the <code class="language-plaintext highlighter-rouge">satpulsed</code> Web monitoring app are both written using Preact and Tailwind CSS;
this should make it possible in the future for the monitoring tab to share Preact components with satpulsed.</p>

<p>Why bother with writing a GUI at all? I developed the device-independent configuration approach to give the best possible
user experience for <code class="language-plaintext highlighter-rouge">satpulsed</code>. But it turned out to be a lot more engineering effort than I anticipated.
And to be honest, from the point of view of <code class="language-plaintext highlighter-rouge">satpulsed</code>, it doesn’t really earn its keep:
the added benefit to users is too small to justify the effort that went into it.
I added <code class="language-plaintext highlighter-rouge">satpulsetool</code> to help unlock the value of device-independent configuration to a wider audience.
But <code class="language-plaintext highlighter-rouge">satpulsetool</code> can’t fully do this.
Most users only have one GNSS receiver, so the value to users of this configuration model
isn’t so much that it’s device-independent,
but that it avoids requiring the user to know anything about the receiver’s native protocol;
and the kind of user that benefits most from this is a non-technical user, who will want a GUI not a CLI.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[I have been working on a desktop GUI for SatPulse. Here are a couple of demo videos.]]></summary></entry><entry><title type="html">Design of SatPulse compared with GPSd</title><link href="https://satpulse.net/2026/04/11/design-of-satpulse-compared-with-gpsd.html" rel="alternate" type="text/html" title="Design of SatPulse compared with GPSd" /><published>2026-04-11T00:00:00+00:00</published><updated>2026-04-11T00:00:00+00:00</updated><id>https://satpulse.net/2026/04/11/design-of-satpulse-compared-with-gpsd</id><content type="html" xml:base="https://satpulse.net/2026/04/11/design-of-satpulse-compared-with-gpsd.html"><![CDATA[<p>In version 0.1 SatPulse focused on a specialized use case:
transferring time from a GPS to a PTP hardware clock (PHC).
In <a href="/2026/04/01/first-pre-release-of-satpulse-0.2.html">version 0.2</a>,
SatPulse’s scope is much broader.
It now includes a rich, general-purpose GPS subsystem,
which supports a <a href="/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse.html">wide range of vendor protocols</a>.
I believe 0.2 already has everything needed to provide full support for
<a href="/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs.html">timing-oriented use</a>
of a GPS receiver on Linux.
It also includes most of the pieces that are needed to support
precision positioning using RTK and NTRIP; this will be rounded out in upcoming releases.</p>

<p>At the moment, support for GPS receivers in Linux is almost universally based on GPSd.
In this post I want to explain some key design choices I made for SatPulse
that are different from those made by GPSd.
My goal is to help people understand when it might be worth considering SatPulse
as an alternative to GPSd.
I want to emphasize that SatPulse is not attempting to be a replacement for GPSd.
GPSd does what it sets out to do very well, as evidenced by its popularity.</p>

<p>TL;DR: Consider SatPulse for server-side use of a GPS receiver,
or when GPS receiver configuration is needed.</p>

<p>Let me start by giving a brief overview of how GPSd works.
For a fuller description, see the <a href="https://aosabook.org/en/v2/gpsd.html">GPSd chapter</a> from the Architecture of Open Source Applications book.
GPSd is both a daemon, written in C, and a suite of related tools.
It is centered around a device-independent data model of the information
provided by periodic messages emitted by GPS receivers.
A single instance of the daemon acts as a multiplexer.
Streams of messages are read from multiple sources such as serial devices,
converted to the device-independent data model and provided to multiple clients.
GPSd’s primary API is a service API: a client runs as a separate process and
interacts with GPSd over a TCP socket using a JSON protocol;
it also provides a client library API that wraps the service API.
In the GPSd architecture, the daemon does not perform application-level work on the data;
its role is restricted to multiplexing and conversion into the device-independent data model.
GPSd has a zero-configuration philosophy:
when a user plugs in a GPS receiver, a GPSd client should work without requiring
the user to perform any configuration.
GPSd provides only a very limited device-independent abstraction for GPS
receiver configuration; most importantly it has the concept of enabling a <em>binary</em> mode,
which configures the receiver to output messages using its vendor-specific binary protocol instead of NMEA.
Instead it provides a separate Python program, ubxtool, for configuring u-blox receivers using the UBX protocol.</p>

<p>Like GPSd, SatPulse is also both a daemon and a suite of related tools,
and it is also centered around a similar device-independent data model.
SatPulse is written in Go.
Its codebase is comparable in size to GPSd’s.
SatPulse compiles into two executables:
<code class="language-plaintext highlighter-rouge">satpulsed</code>, which is a daemon, and <code class="language-plaintext highlighter-rouge">satpulsetool</code>, which is a command line tool.
All the application-level functionality that SatPulse provides
is included in <code class="language-plaintext highlighter-rouge">satpulsed</code> or <code class="language-plaintext highlighter-rouge">satpulsetool</code> depending on whether that functionality
needs to work as a daemon or not.
The daemon is configured using a file in TOML syntax.
One instance of the daemon runs for each device to which a receiver is attached.
This allows each instance to have its own configuration file,
and for systemd to manage the daemon lifecycle.
SatPulse does not attempt to discover GPS receivers:
it opens a serial device only when explicitly configured to do so.
The configuration file has a modular structure, with separate TOML tables to configure
each aspect of application-level functionality.
The second executable, <code class="language-plaintext highlighter-rouge">satpulsetool</code>, is a suite of command-line tools.
It uses a subcommand syntax, so for example <code class="language-plaintext highlighter-rouge">satpulsetool gps</code> runs the <code class="language-plaintext highlighter-rouge">gps</code> tool.
These separate tools are bundled into a single executable because the Go language has a runtime that makes executables quite large.
The GPS subsystem of SatPulse is structured as a reusable library of Go packages.
Both <code class="language-plaintext highlighter-rouge">satpulsed</code> and <code class="language-plaintext highlighter-rouge">satpulsetool</code> use this library for their GPS-related functionality:
<code class="language-plaintext highlighter-rouge">satpulsetool</code> does not need <code class="language-plaintext highlighter-rouge">satpulsed</code> to be running.</p>

<p>The most significant GPS functionality in SatPulse that goes beyond what GPSd provides
is support for GPS receiver configuration.
For basic GPS usage, the factory default configuration is often sufficient.
But modern GPS receivers even at modest price points have started to offer
more advanced features, such as RTK, PPP-HAS or OSNMA,
which typically require some configuration to be used.
SatPulse provides a device-independent abstraction for GPS receiver configuration.
This allows you to choose which specific aspects of the configuration should be changed.
So, for example, you can specify that a time pulse should be enabled with a specific pulse width,
that the time pulse should be enabled only when the receiver has a lock,
and that it should be aligned to the system time of a particular GNSS;
or you can specify that the receiver should operate in time mode with specific fixed ECEF coordinates.
The changes to be made are given to the configuration engine which figures out how to apply
them to a specific receiver. This often involves reading the existing configuration of the receiver,
so that only the specified aspects of the configuration are changed.
An important part of configuring a GPS receiver is enabling the right set of periodic messages.
Each receiver divides up information in different ways.
So instead of specifying particular named device-dependent messages (e.g. UBX-NAV-PVT), you specify the needed information in data-model terms (e.g. the time in UTC and position in geodetic coordinates).
The engine then enables the best set of messages that provide this information.
The configuration engine is part of the library and is used both by <code class="language-plaintext highlighter-rouge">satpulsed</code> and <code class="language-plaintext highlighter-rouge">satpulsetool</code>.
Having <code class="language-plaintext highlighter-rouge">satpulsed</code> perform receiver configuration works well because it can
infer some aspects of receiver configuration from the application-level configuration.
For example, if the configuration file specifies a PHC to be disciplined,
then the daemon will ensure a 1PPS time pulse is enabled.
But configuration changes that affect the receiver’s non-volatile
memory or interrupt receiver operation (such as changing the enabled GNSS constellations) are only
done when specifically requested by the user with <code class="language-plaintext highlighter-rouge">satpulsetool</code>.
Implementing device-independent configuration is significantly more complex than implementing
the device-independent data model for periodic messages.
In the Go package that implements the device-independent abstractions for the u-blox UBX protocol,
about 70% of the code is devoted to configuration.
SatPulse also provides an alternative lower-level approach to configuration,
which is less challenging to implement:
see <a href="/2026/01/29/improving-gps-configuration.html">this blog post</a> for more detail.</p>

<p>The device-independent data model provided by the GPS subsystem can be serialized as JSON.
The daemon uses this for its Web dashboard feature.
It includes an HTTP server with an endpoint that exposes this data model as JSON-encoded server-sent events (SSE).
Another endpoint serves an HTML page which uses JavaScript to connect to the SSE endpoint and display a dashboard.
The daemon does not yet expose this data model over a network socket for consumption by third-party applications.
This will be straightforward to do, but I want to stabilize the data model first.</p>

<p>However, SatPulse emphasizes a different approach to allowing multiple independent applications to share access to
a single receiver, based on providing network access to packet streams.
The daemon can be configured to make TCP ports or Unix domain sockets
proxy the packets emitted by the receiver,
optionally filtering packets by protocol,
and optionally also allowing writing to the receiver, with a configurable locking strategy to prevent conflicts between writers.
This provides functionality similar to ser2net, but is protocol-aware.
Streams of native-protocol packets are already a well-defined wire-format.
Exposing this directly on a network endpoint allows receiver sharing without the need to define
a new daemon-dependent wire-format.
Many applications already exist that can work with such packet streams.
Here are three examples.</p>
<ol>
  <li>Every vendor that I know of provides an application for configuring their receivers, typically Windows-only.
Many of these applications, notably u-center, allow the receiver to be accessed over a TCP socket.
These can be used with SatPulse by taking advantage of the read-write feature.</li>
  <li>IANA has <a href="https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=nmea">registered</a> the service name nmea-0183 and port 10110 for NMEA over TCP or UDP. <a href="https://gitlab.freedesktop.org/geoclue/geoclue/-/wikis/home">GeoClue</a> is a D-Bus service that provides location information. It includes an NMEA backend that uses DNS-based service discovery (RFC 6763 as implemented by Avahi) to discover nmea-0183 services on the network. SatPulse can be configured to expose the NMEA service on port 10110, Avahi can be configured to advertise it and then the GeoClue NMEA backend can discover it and expose it to desktop apps over D-Bus.</li>
  <li>RTK works by providing the rover’s receiver with a stream of RTCM packets emitted by a base station’s receiver. In a production environment, NTRIP is typically used to move packets from a base to a rover. An RTK base station uses a NTRIP server to provide RTCM packets to an NTRIP caster; an RTK rover uses an NTRIP client to get RTCM packets from the caster. NTRIP support is planned for a future release of SatPulse. But one popular open-source caster, the <a href="https://igs.bkg.bund.de/ntrip/bkgcaster">BKG NtripCaster</a> can <a href="https://igs.bkg.bund.de/root_ftp/NTRIP/software/caster/ntripcaster_manual.html#c5">pull</a> RTCM data from a TCP port without using NTRIP. This allows SatPulse to be used today as the GPS component of a combined RTK and time server.</li>
</ol>

<p>We can summarize the key design choices for SatPulse that are different from GPSd as follows:</p>

<ol>
  <li>it is written in Go</li>
  <li>the primary GPS API is a library API</li>
  <li>the daemon does application-level work</li>
  <li>the daemon has a configuration file</li>
  <li>the daemon has a separately configured instance per receiver</li>
  <li>it provides a device-independent model for GPS configuration</li>
  <li>it emphasizes packet streams as a foundational layer</li>
</ol>

<p>So why did I make these choices? They were not independent.
The initial PTP use case requires a daemon to do substantial application-level work:
it uses timestamp events from the Linux PHC subsystem in combination
with messages from the GPS receiver to discipline the PHC and send metadata updates to the PTP daemon.
I didn’t want this use case to require two independent daemons.
Once the daemon is doing application-level work, then there needs to be a way to configure it.
Having one daemon instance per serial device makes things straightforward:
each instance can have its own configuration file
and systemd can ensure that the daemon is not started until the serial device is ready.</p>

<p>Even with the initial PTP use case, the daemon has significant internal concurrency:
reading from the serial device, reading timestamp events, and updating the PTP daemon
are naturally concurrent with the main PHC-disciplining loop.
Other features like packet proxying or HTTP monitoring involve additional concurrency.
I would not attempt implementing a daemon with this level of concurrency
except in a modern language like Go, which is memory safe and has language support for concurrency.
The implementation of <code class="language-plaintext highlighter-rouge">satpulsetool</code> also uses concurrency, although to a lesser extent than <code class="language-plaintext highlighter-rouge">satpulsed</code>.
Go makes it relatively straightforward to have a library that supports concurrency in a flexible way.</p>

<p>I hope this post has made it clear that GPSd and SatPulse are very different programs,
which have made different design choices for good reasons.
SatPulse would not be a suitable replacement for many uses of GPSd.
But I think there are several use-cases, particularly on the server side, where
SatPulse’s more integrated approach and support for GPS configuration offers significant advantages.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[In version 0.1 SatPulse focused on a specialized use case: transferring time from a GPS to a PTP hardware clock (PHC). In version 0.2, SatPulse’s scope is much broader. It now includes a rich, general-purpose GPS subsystem, which supports a wide range of vendor protocols. I believe 0.2 already has everything needed to provide full support for timing-oriented use of a GPS receiver on Linux. It also includes most of the pieces that are needed to support precision positioning using RTK and NTRIP; this will be rounded out in upcoming releases.]]></summary></entry><entry><title type="html">Building an NTP server on a Raspberry Pi with chrony or ntpd-rs</title><link href="https://satpulse.net/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs.html" rel="alternate" type="text/html" title="Building an NTP server on a Raspberry Pi with chrony or ntpd-rs" /><published>2026-04-06T00:00:00+00:00</published><updated>2026-04-06T00:00:00+00:00</updated><id>https://satpulse.net/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs</id><content type="html" xml:base="https://satpulse.net/2026/04/06/building-an-ntp-server-on-a-raspberry-pi-with-chrony-or-ntpd-rs.html"><![CDATA[<p>In an <a href="https://satpulse.net/2026/04/01/using-satpulse-for-timing-without-a-phc.html">earlier post</a>, I described how SatPulse can now work without the specialised PHC hardware it previously required. This means, in particular, that it is possible to use SatPulse to build an NTP server on a regular Raspberry Pi, rather than requiring a Raspberry Pi CM4 or CM5.</p>

<p>In this post, I will explain how to do this, using two different NTP servers: chrony and ntpd-rs.</p>

<h2 id="hardware">Hardware</h2>

<p>Before we get into the details of configuration, let me say something about GPS hardware.
When using a Raspberry Pi, the PPS signal is connected to a GPIO pin on the Pi.
This means that the time of the PPS edge is measured in software by the kernel,
rather than in hardware.
The accuracy you will get is in the microsecond range.
Basic GPS receivers can achieve an accuracy of 50ns in normal conditions.
So my main hardware advice is that in this context a fancy, expensive GPS receiver will not deliver any measurable benefit.</p>

<p>GPS receivers are available in a Pi HAT form factor, which plugs into the 40-pin GPIO header.
These HATs typically wire the GPS PPS signal to pin 12 (GPIO 18).
I do not recommend these when using PHC hardware, because the GPS PPS signal needs to be wired to
the SYNC_OUT pin on a different header.
But when using a normal Raspberry Pi, they are very convenient,
although you typically pay a premium for this convenience.</p>

<h2 id="setup">Setup</h2>

<p>The following steps can be done exactly as described in <a href="https://satpulse.net/setup/index.html">SatPulse Setup Guide</a>.</p>

<ol>
  <li><a href="https://satpulse.net/setup/rpi-os.html">Install Raspberry Pi OS</a></li>
  <li><a href="https://satpulse.net/setup/rpi-uart.html">Configure the UART</a></li>
  <li><a href="https://satpulse.net/setup/satpulse-install.html">Install SatPulse</a> but choose the 0.2 pre-release</li>
  <li><a href="https://satpulse.net/setup/gps-serial.html">Verify the GPS serial connection</a>; the device will be <code class="language-plaintext highlighter-rouge">/dev/ttyAMA0</code></li>
</ol>

<p>Configuration of kernel PPS is different.
First, configure pin 12 (GPIO 18) as a PPS pin, by adding the following at the bottom of <code class="language-plaintext highlighter-rouge">/boot/firmware/config.txt</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dtoverlay=pps-gpio,gpiopin=18
</code></pre></div></div>

<p>Reboot after this. To verify the PPS signal is working, install pps-tools using</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt install pps-tools
</code></pre></div></div>

<p>Then do:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo ppstest /dev/pps0
</code></pre></div></div>

<p>It should show PPS events once per second:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>trying PPS source "/dev/pps0"
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1775392263.000000336, sequence: 170178 - clear  0.000000000, sequence: 0
source 0 - assert 1775392264.000000215, sequence: 170179 - clear  0.000000000, sequence: 0
source 0 - assert 1775392264.999998780, sequence: 170180 - clear  0.000000000, sequence: 0
source 0 - assert 1775392265.999999085, sequence: 170181 - clear  0.000000000, sequence: 0
</code></pre></div></div>

<p>Exit with Ctrl-C.</p>

<p>Now you need to edit <code class="language-plaintext highlighter-rouge">/etc/satpulse.toml</code>.  Most important point is to remove the <code class="language-plaintext highlighter-rouge">[phc]</code> section.
All you need is this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[serial]
device = "/dev/ttyAMA0"
# fix to match your serial device speed
speed = 9600

[gps]
config = true
vendor = "u-blox"

# Enable HTTP monitoring on port 2000
[[http]]
listen = ":2000"

[ntp]
# Use this for chrony
sock.path = "/var/run/chrony.satpulse.sock"
# Use this for ntpd-rs
# sock.path = "/run/ntpd-rs/satpulse.ttyAMA0.sock"
</code></pre></div></div>

<p>Uncomment the sock.path line for whichever of chrony or ntpd-rs you use.</p>

<p>You can now start SatPulse using</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl start satpulse@ttyAMA0
</code></pre></div></div>

<p>You can point a browser at port 2000 and you should get a page showing information
from the GPS receiver.</p>

<h3 id="chrony">Chrony</h3>

<p>To configure chrony, first install with</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt install chrony
</code></pre></div></div>

<p>Then create a file <code class="language-plaintext highlighter-rouge">/etc/chrony/conf.d/satpulse.conf</code> with the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>refclock PPS /dev/pps0 poll 2 lock GPS refid PPS
refclock SOCK /var/run/chrony.satpulse.sock offset 0.1 delay 0.2 refid GPS noselect
</code></pre></div></div>

<p>The refclock PPS line makes chrony use the kernel PPS API to read PPS samples from /dev/pps0.
These are accurate but lack time-of-day information.
The refclock SOCK line makes chrony read samples generated by satpulsed.
These are inaccurate but include time-of-day.
The <code class="language-plaintext highlighter-rouge">offset 0.1</code> corrects for serial messages coming 0.1 second after the top of the second.</p>

<p>Chrony can use the samples from satpulsed to complete the PPS samples.
Chrony can also use samples from other NTP servers to complete the samples,
so if you want to check that this is really working,
you should at least temporarily comment out the <code class="language-plaintext highlighter-rouge">pool</code> line from <code class="language-plaintext highlighter-rouge">/etc/chrony/chrony.conf</code>.</p>

<h3 id="ntpd-rs">ntpd-rs</h3>

<p>Ubuntu has <a href="https://discourse.ubuntu.com/t/ntpd-rs-its-about-time/79154">announced</a> plans to adopt <a href="https://github.com/pendulum-project/ntpd-rs">ntpd-rs</a> as its default NTP server, replacing chrony, primarily because of memory safety.
Since SatPulse is written in Go, the combination of ntpd-rs and SatPulse provides a fully memory-safe timing stack.
Like SatPulse, ntpd-rs uses TOML for its configuration files and supports Prometheus,
so the combination makes for a pleasantly harmonious configuration and observability experience.</p>

<p>To install, download a deb file from the <a href="https://github.com/pendulum-project/ntpd-rs/releases">Releases page</a>.
You need at least version 1.7.2.
(The version in Raspberry Pi OS will not work.)</p>

<p>Next, you will need to remove your existing NTP daemon (e.g. systemd-timesyncd or chrony):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt remove systemd-timesyncd chrony
</code></pre></div></div>

<p>Then install ntpd-rs</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo dpkg -i ./ntpd-rs_1.7.2-1_arm64.deb
</code></pre></div></div>

<p>Now you need to set things up so that ntpd-rs can access <code class="language-plaintext highlighter-rouge">/dev/pps0</code>.
Create a file <code class="language-plaintext highlighter-rouge">/etc/udev/rules.d/99-ntpd-rs-pps.rules</code> containing the line</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>KERNEL=="pps0", GROUP="ntpd-rs", MODE="0640"
</code></pre></div></div>

<p>This ensures that when /dev/pps0 is created at boot time it will have a group and permissions
that enables ntpd-rs to access it. To make this take effect without booting, do</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo udevadm control --reload
sudo udevadm trigger /dev/pps0
</code></pre></div></div>

<p>Now we need to configure ntpd-rs.
Put the following in <code class="language-plaintext highlighter-rouge">/etc/ntpd-rs/ntp.toml</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[observability]
log-level = "debug"
observation-path = "/var/run/ntpd-rs/observe"

[[source]]
mode = "pps"
path = "/dev/pps0"
precision = 1e-7
accuracy = 1e-6

[[source]]
mode = "sock"
precision = 1e-2
path = "/run/ntpd-rs/satpulse.ttyAMA0.sock"
accuracy = 0.2

[synchronization]
minimum-agreeing-sources = 1

[[server]]
listen = "0.0.0.0:123"
</code></pre></div></div>

<p>When I tried initially to get this working with version 1.7.1 of ntpd-rs,
it didn’t work. I submitted an <a href="https://github.com/pendulum-project/ntpd-rs/issues/2169">issue</a>,
and within a day one of the developers, David Venhoek, had added a <a href="https://github.com/pendulum-project/ntpd-rs/pull/2171">feature</a> to enable this configuration.
This was included in version 1.7.2, released a few days ago.
It adds the ability to specify the accuracy of sources as distinct from their precision.
Accuracy means how close measurements are to true time.
Precision means how much measurements vary from each other.
The serial timing measurements from satpulsed are quite precise
but are extremely inaccurate.
Specifying a low accuracy for samples from satpulsed
ensures that ntpd-rs uses the PPS samples as the primary source,
and uses the satpulsed samples just to complete the PPS samples.
But note that if you specify an accuracy of 0.25 or worse,
you need to increase <code class="language-plaintext highlighter-rouge">maximum-source-uncertainty</code> from its default of 0.25.</p>

<p>Now you can start ntpd-rs using</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl start ntpd-rs
</code></pre></div></div>

<p>You can verify it’s working by using</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntp-ctl status
</code></pre></div></div>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[In an earlier post, I described how SatPulse can now work without the specialised PHC hardware it previously required. This means, in particular, that it is possible to use SatPulse to build an NTP server on a regular Raspberry Pi, rather than requiring a Raspberry Pi CM4 or CM5.]]></summary></entry><entry><title type="html">First pre-release of SatPulse 0.2</title><link href="https://satpulse.net/2026/04/01/first-pre-release-of-satpulse-0.2.html" rel="alternate" type="text/html" title="First pre-release of SatPulse 0.2" /><published>2026-04-01T11:15:00+00:00</published><updated>2026-04-01T11:15:00+00:00</updated><id>https://satpulse.net/2026/04/01/first-pre-release-of-satpulse-0.2</id><content type="html" xml:base="https://satpulse.net/2026/04/01/first-pre-release-of-satpulse-0.2.html"><![CDATA[<p>I just made the first pre-release of SatPulse 0.2.</p>

<p>The <a href="https://github.com/jclark/satpulse/releases/tag/v0.2-pre-20260401">release notes</a> have more information.</p>

<p>I would be grateful if you could try it out and let me know how you get on.</p>

<p>I created a <a href="https://github.com/jclark/satpulse/discussions/248">discussion thread</a> where you can ask questions and share experiences.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[I just made the first pre-release of SatPulse 0.2.]]></summary></entry><entry><title type="html">A tour of the GPS modules supported by SatPulse 0.2</title><link href="https://satpulse.net/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse.html" rel="alternate" type="text/html" title="A tour of the GPS modules supported by SatPulse 0.2" /><published>2026-04-01T10:15:00+00:00</published><updated>2026-04-01T10:15:00+00:00</updated><id>https://satpulse.net/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse</id><content type="html" xml:base="https://satpulse.net/2026/04/01/a-tour-of-the-gps-modules-supported-by-satpulse.html"><![CDATA[<p>For the last 8 months, I have been working on broadening the range of GNSS hardware supported by SatPulse. In 0.1, there was support only for u-blox modules. In 0.2, I have added support for a broad range of modules from other vendors, all of which are Chinese.</p>

<p>Before getting into the details of the supported modules, I want to talk about what supporting a GPS module means.
All GPS modules support NMEA which provides information about navigation solutions as they are computed by the module. GPS modules also support RTCM, which is used both to consume corrections provided to the module and to provide corrections for use by other modules. Apart from that, every GPS module supports one or more vendor-specific protocols. The messages used by these protocols can be divided into two groups. The first group performs a similar function to NMEA: they are messages periodically emitted by the module to provide information about the navigation solutions computed and other aspects of the ongoing operation of the module. The second group performs configuration: these include both requests that are input to the module to query and alter configuration, and messages that are output by the module as responses to these requests. Supporting a module involves enabling SatPulse to make use of both kinds of message.</p>

<p>SatPulse supports vendor-specific periodic messages in the obvious way, by converting vendor-specific messages into a uniform abstraction. In the source code, this abstraction is defined by the <code class="language-plaintext highlighter-rouge">gpsprot</code> (GPS protocol) package. In 0.1, there were abstract messages that provide information about</p>

<ul>
  <li>the current time in either UTC or TAI; also includes sawtooth error/quantization error and time accuracy</li>
  <li>the progress of a survey-in operation</li>
  <li>the next scheduled leap second</li>
  <li>satellite positions and signal strengths</li>
</ul>

<p>In 0.2, there are additional messages that provide information about</p>

<ul>
  <li>the characteristics of a navigation solution; this includes
    <ul>
      <li>the technique used to compute the solution (e.g. ranging code or carrier phase)</li>
      <li>dimensionality of the fix i.e. 3D, 2D or time only</li>
      <li>what sensors beyond GNSS contributed to the solution (e.g. INS)</li>
      <li>kinds of corrections used (e.g. SBAS, RTK, PPP)</li>
      <li>accuracy (position, velocity)</li>
      <li>DOP in all its flavours</li>
      <li>age of differential corrections</li>
      <li>GNSS constellations and bands used in computing the solution</li>
    </ul>
  </li>
  <li>position, in ECEF or geodetic coordinates</li>
  <li>velocity, in ECEF or geodetic coordinates (i.e speed and course-over-ground, or NED)</li>
</ul>

<p>These abstract messages can be serialized as JSON, and can be seen in the event log, which also includes information about pulse edges.
The messages also feed the observability subsystem, which exposes them as Prometheus metrics.</p>

<p>For configuration, there are now two kinds of configuration support as I described in more detail in my <a href="https://satpulse.net/2026/01/29/improving-gps-configuration.html">earlier blog</a>.</p>

<ul>
  <li>high-level configuration, intent-based configuration, where you describe what you want, and SatPulse turns that into the appropriate messages for the specific module</li>
  <li>low-level configuration, which is based on message files</li>
</ul>

<p>High-level configuration in 0.2 provides a similar set of features as in 0.1. There are a couple of new properties that can be set:</p>

<ul>
  <li>minimum elevation angle for satellites to be used in the navigation solution</li>
  <li>RTCM base ID - the reference station ID for the RTCM messages that the module generates</li>
</ul>

<p>I have added a few features to the message file implementation since the last blog:</p>

<ul>
  <li>the message file implementation now understands the request/response patterns of various protocols, which allows it to identify which messages from the module are responses to messages sent</li>
  <li>there is a file inclusion capability, which is useful for allowing model-specific message files to share messages that work across multiple vendor models</li>
</ul>

<p>The high/low-level configuration split leads naturally to two tiers of support. In both tiers, there is</p>

<ul>
  <li>support for generating the full range of abstract messages from the vendor-specific messages</li>
  <li>support for the protocol in the message file implementation</li>
</ul>

<p>The difference is that Tier 1 provides configuration support primarily via the high-level mechanism; message files are used to supplement this to support vendor-specific features that are not exposed via high-level configuration, whereas Tier 2 uses message files to provide configuration support.
For Tier 2, the provided message files use a common set of conventions for tags, which provides a degree of vendor-independence in the user interface.</p>

<p>SatPulse 0.1 had support only for u-blox. In 0.2, there is support for the following vendors</p>

<ol>
  <li>u-blox - tier 1</li>
  <li>Unicore - tier 1</li>
  <li>Quectel - tier 2</li>
  <li>Zhongke/CASIC - tier 2</li>
  <li>Allystar - tier 2</li>
  <li>Techtotop/Taidou - tier 2</li>
  <li>ByNav - tier 2</li>
  <li>ComNav/SinoGNSS - tier 2</li>
</ol>

<p>Apart from u-blox, these vendors are all based in China. There are other Western vendors, but they are all either much more expensive than u-blox (Septentrio, Trimble, NovAtel) or the chips are designed to be integrated into mobile phones and are not available as modules that you can buy separately (e.g. Broadcom). Septentrio has recently come down in price a bit and they also have an excellent reputation for timing, so I hope to add support for Septentrio in the future. I already have some support for NovAtel, because some Chinese vendors have adopted the NovAtel OEM 6/7 series protocol.</p>

<p>I have chosen which Chinese modules to support based on a number of factors:</p>

<ul>
  <li>Are they at least dual-band? Given the low cost of dual-band modules, there are relatively few cases where single-band modules remain interesting.</li>
  <li>Do they have any features that are particularly interesting for precision timing and/or positioning?</li>
  <li>How common and popular are they on Taobao?</li>
  <li>Are there any Western vendors that resell them?</li>
</ul>

<p>Taobao recently started supporting direct shipping to my country of residence,
which makes using Taobao much more convenient.
I have found Taobao vendors to be considerably more knowledgeable about what they are selling than AliExpress vendors.
If you can buy from Taobao, I recommend it.</p>

<h2 id="u-blox">u-blox</h2>

<p>SatPulse supports all u-blox modules starting from LEA-6T all the way through to ZED-X20P. I have tested it on all timing modules:</p>

<ul>
  <li>LEA-6T</li>
  <li>LEA-M8T</li>
  <li>LEA-M8F</li>
  <li>ZED-F9T (both L1/L2 and L1/L5 variants)</li>
  <li>NEO-F10T</li>
</ul>

<p>and on both current high-precision modules:</p>

<ul>
  <li>ZED-F9P</li>
  <li>ZED-X20P</li>
</ul>

<p>and on many standard precision modules, including:</p>

<ul>
  <li>NEO-M9N</li>
  <li>M10050-KB</li>
  <li>NEO-F10N (which is L1/L5)</li>
</ul>

<p>In 0.2, the improvements in u-blox support are:</p>

<ul>
  <li>support for richer information (navigation solution characteristics, position, velocity)</li>
  <li>support for additional configuration properties (minimum elevation and RTCM base id)</li>
  <li>support for ZED-X20P</li>
</ul>

<p>I have done much more testing on u-blox modules than on any other brand.</p>

<h2 id="unicore">Unicore</h2>

<p>There is tier 1 support for the UM980 family, which consists of</p>

<ul>
  <li>UM980, base model, which includes support for Galileo HAS (which is promised but not yet available for u-blox ZED-X20P)</li>
  <li>UM981, adds INS</li>
  <li>UM982, adds dual antennas</li>
  <li>UM960, lower end version, without PPP support</li>
</ul>

<p>These use a protocol which is similar to that used by NovAtel OEM6/7 messages. Periodic data messages have dual binary/ASCII formats;
the packet formats are similar, but the binary packet format has different sync bytes.
Configuration messages are ASCII lines, similar but different from NovAtel.
These modules also have some undocumented support for some periodic data messages that use the same packet format as NovAtel.
This includes the RANGECMPB raw message which can be used by RTKLIB to generate RINEX observation files.</p>

<p>In the West, these modules are distributed by <a href="https://www.ardusimple.com/product/simplertk3b-budget/">ArduSimple</a>, <a href="https://gnss.store/collections/unicore-gnss-modules">gnss.store</a> and <a href="https://www.sparkfun.com/sparkfun-triband-gnss-rtk-breakout-um980.html">SparkFun</a>.</p>

<p>SparkFun have a <a href="https://github.com/sparkfun/SparkFun_RTK_Torch/tree/main/UM980_Firmware">GitHub repo with the latest UM980 firmware</a>.</p>

<p>Unicore have another range of modules UM6XX, which uses a completely different protocol, which is not yet supported.</p>

<h2 id="quectel">Quectel</h2>

<p>Quectel have a bewildering array of GNSS modules. There are two families that I find interesting and that SatPulse supports:</p>

<ul>
  <li>LG290P family - this is Quectel’s high-end module which includes support for Galileo HAS in the most recent firmware
    <ul>
      <li>LG290P is the base model</li>
      <li>LG580P is the dual antenna variant</li>
      <li>LG680P is the LG290P in the 22x17mm u-blox ZED form factor</li>
    </ul>
  </li>
  <li>LC29H family - this is a relatively inexpensive L1/L5 model based on the Airoha AG3335 chipset; there are several variants, of which the most interesting are:
    <ul>
      <li>LC29H(AA) is the best fit for base-station case; it can produce RTCM corrections but does not have the RTK engine that can consume them; it is supposed to get OSNMA support at some point</li>
      <li>LC29H(DA) is the one that is suitable for use as an RTK rover</li>
    </ul>
  </li>
</ul>

<p>Quectel’s vendor specific protocol uses the NMEA proprietary extension mechanism. The NMEA sentences start with <code class="language-plaintext highlighter-rouge">$PQTM</code> (The <code class="language-plaintext highlighter-rouge">P</code> is the NMEA proprietary extension mechanism, and <code class="language-plaintext highlighter-rouge">QTM</code> is Quectel’s 3-letter identifier). The LG290P supports a richer set of PQTM messages than the LC29H.
The LC29H also supports some NMEA proprietary sentences defined by Airoha; these start with <code class="language-plaintext highlighter-rouge">$PAIR</code>.</p>

<p>In the West, SparkFun sell an <a href="https://www.sparkfun.com/sparkfun-quadband-gnss-rtk-breakout-lg290p-qwiic.html">LG290P board</a>.
They also have a <a href="https://github.com/sparkfun/SparkFun_RTK_Postcard/tree/main/Firmware">GitHub repo with the latest LG290P firmware</a>.</p>

<p>On Taobao, a good shop to buy from is <a href="https://mzhtek.taobao.com/">Mozi Technology/Mozihao</a>.</p>

<h2 id="zhongkecasic">Zhongke/CASIC</h2>

<p><a href="https://icofchina.com/">Zhongke Microelectronics</a> is best known for the ATGM332D and ATGM336H series of modules.
The difference between these is the form-factor: ATGM332D uses the u-blox NEO form factor,
whereas the ATGM336H uses the MAX form factor.
The notable feature of these modules is that they are extraordinarily cheap.
The bare module (without a board) is about $1.25 on Taobao.</p>

<p>The vendor-specific protocol is CASIC. CAS stands for Chinese Academy of Sciences,
and Zhongke in Chinese also refers to the Chinese Academy of Sciences. The CASIC protocol is UBX-like.</p>

<p>There are literally dozens of different versions of these modules, and there are some very significant differences between them.</p>

<p>The most common version is ATGM332D-5N31. These module names follow a common pattern.
The ‘5’ indicates the generation of the chip used (in this case, the AT6558).
‘N’ likely stands for navigation. The ‘3’ designates which constellations are supported:
1 means GPS, 2 means BeiDou, 4 means GLONASS, and these are added together to indicate the set of constellations supported. So 3 means GPS+BeiDou and 7 means GPS+BeiDou+GLONASS.
I haven’t figured out how the final digit works.</p>

<p>There is a more recent and less common 6 series (e.g. the ATGM332D-6N74) which adds support for Galileo. This uses the AT6668 chip. More interestingly, there is also an F8 series (e.g. <a href="https://www.icofchina.com/daohang/duopin/2531.html">ATGM332D-F8N76</a>), which is dual-band.
This uses the AT9880 chip.
It is not common yet but I expect it will become common:
like the rest of the ATGM332D series this is extraordinarily cheap, about $2.15 on Taobao;
it seems to be the cheapest dual-band module in the world.</p>

<p>The 5 series use a default baud rate of 9600, whereas the 6 and F8 series use 115200.
More significantly, although they all use the same packet format,
the 6 and F8 series support a different set of messages.
The 5 series uses NAV class messages, whereas the 6 and F8 series uses NAV2 class messages.</p>

<p>There is some English language documentation available for the version of CASIC that uses NAV messages;
there is nothing available on the NAV2 messages.
Fortunately, one of my Taobao sellers was able to supply me with a Chinese language protocol manual which describes this version of CASIC.
SatPulse has support for both the NAV messages and the NAV2 messages.</p>

<p>As well as the low-end ATGM332D/ATGM336H modules, Zhongke also make higher-end modules designed explicitly for timing.
I have the AT632-6T-30. This is similar to the 6 series of the ATGM332D, and is L1 only, but has a full set of timing features, which makes it interesting.
These use a TIM2 class of message. In particular it has a TIM2-TPX message including quantization error.
It is interesting to have a modern L1 timing module; u-blox have not done an L1 timing module since the LEA-M8T.
One noticeable difference is that the peak-to-peak amplitude of the sawtooth error is about 6ns on the AT632 compared to 21ns on the M8T,
presumably reflecting the higher clock speed of the more modern module.</p>

<p>Zhongke provide a GnssToolkit3 application for Windows for configuring their modules.</p>

<h2 id="allystar">Allystar</h2>

<p>I first came across <a href="https://www.allystar.com/en">Allystar</a> because of their TAu1201 module, which has been one of the cheapest L1/L5 modules available.
The <a href="https://xinghewei.tmall.com/">StarRiver store</a> on Taobao sell a variant of their SR1723 board with the TAU1201.
This costs about $7.50. With various fees, shipping and tax to Thailand, the all-in cost is about $10.50.
In the West, it will be a bit more.
The TAU1201 uses their Cynosure III chip. Their latest chip is Cynosure IV, which is available in the TAU951M-P200.
This is a more expensive module that also does RTK.</p>

<p>Allystar provide a Windows app called <a href="https://docs.datagnss.com/rtk-board/files/Satrack_client_V1.31.007.zip">Satrack</a> for working with Allystar modules.</p>

<p>Allystar’s binary protocol is UBX-like. They haven’t done a particularly thorough job of it.
For example, the message that provides information about satellite positions and signals provides
less information than is available via NMEA, and Satrack doesn’t make use of it.</p>

<h2 id="bynav">Bynav</h2>

<p><a href="https://www.bynav.com/en/">Bynav</a> makes M20/M10 series of modules based on their own Alice 22nm SoC.
They are strong in the automotive industry.
I have been mainly testing with the M20, but have also tried the M10.
The M21 adds an IMU on top of the M20; the M22 adds a slightly better grade of IMU.
The M20D adds dual antennas.
The M10 is a smaller-footprint, cheaper, lower-end version of the M20 (update rate of 10Hz vs 50Hz on the M20).
They seem pretty decent for RTK.</p>

<p>Bynav’s vendor-specific protocol is NovAtel-style. Their binary and ASCII packet formats for periodic messages (called logs in NovAtel parlance)
are exactly the same as documented by NovAtel for the OEM6/7 range.
Bynav support some messages defined by NovAtel and add some messages of their own.
The configuration commands are NovAtel-style, but not compatible.</p>

<p>In the West, they are sold by <a href="https://gnss.store/collections/bynav-gnss-modules">gnss.store</a>.</p>

<h2 id="comnavsinognss">ComNav/SinoGNSS</h2>

<p>This company brands itself as <a href="https://www.comnavtech.com/">ComNav Technology</a> for Western audiences,
but uses <a href="https://www.sinognss.com/">SinoGNSS</a> for Chinese audiences.
Their latest modules are the K9xx series.
Base module is <a href="https://www.comnavtech.com/product/oem/k901.html">K901</a>. <a href="https://www.comnavtech.com/product/oem/k902.html">K902</a> adds an IMU. <a href="https://www.comnavtech.com/product/oem/k922.html">K922</a> adds dual antennas.
They all have support for Galileo HAS.
These are widely available on Taobao.</p>

<p>The protocol documentation on the ComNav site is rather out of date.
My Taobao seller gave me the current Chinese-language protocol documentation,
which was from another company called <a href="https://www.qinnav.com/product/module/">Qeetek</a>, which also advertises these modules.
So I suspect these modules are actually made by Qeetek.</p>

<p>The situation with the protocol is very similar to Bynav.
Their binary and ASCII packet formats for periodic messages are exactly the same as NovAtel.
They support some messages defined by NovAtel and some of their own.
The configuration commands are NovAtel-style, but not compatible.
But the configuration system is not in my view well-designed.
One major deficiency is that it does not allow the current configuration to be queried.
Another problem is that responses to configuration commands are not designed to be machine readable:
they are not wrapped in a protocol packet that allows them to be distinguished from other traffic from the module.</p>

<p>Apart from the weakness in the configuration protocol, I found convergence of both Galileo HAS 
and its BeiDou equivalent (B2b-PPP) to be flaky.</p>

<h2 id="techtotoptaidou">Techtotop/Taidou</h2>

<p>I had not heard of <a href="https://www.techtotop.com/enindex.aspx">Techtotop</a>, known in Chinese as Taidou, until a month or so ago.
They make their own GNSS silicon, but they seem to be almost completely unknown outside China.</p>

<p>I find them interesting because they have relatively inexpensive modules specifically designed for timing.
The one I have is the T303-5D, which is an L1/L5 module. This isn’t on their web site, but I believe it is just a smaller footprint
version of the <a href="https://www.techtotop.com/detail.aspx?cid=1088">T302-5D</a>.
I got this module from <a href="https://shop471758324.taobao.com/">this shop on Taobao</a>.</p>

<p>It uses a UBX-style protocol called SDBP, about which there appears to be exactly zero information available in English.
My Taobao seller was able to provide a Chinese language protocol manual with full details.
The quality of documentation and protocol design seem good to me:
it has everything I expect of a timing module, including quantization error reporting.</p>

<p>Techtotop also provide the <a href="https://www.techtotop.com/category.aspx?NodeID=49">TDMonitor</a> application for Windows,
which is similar to u-center (although the UI is all Chinese).</p>

<h2 id="where-satpulse-is-heading">Where SatPulse is heading</h2>

<p>My vision for SatPulse 0.1 was quite narrow: to transfer time from a GPS module to a PTP hardware clock. But it turns out doing a really good job of that requires a
complex and sophisticated GPS subsystem.
The subsystem should work for GPS modules from many vendors not just from u-blox.
When monitoring a timing GPS, you want rich information about the current state of GPS,
including the characteristics of the navigation solution.
And it turns out that getting position information from the GPS is also useful:
for example, you may want to use HAS to establish the fixed position to be used in timing.
If you have done the work to create a GPS subsystem that works well for timing,
you have done at least 80% of the work to create a GPS subsystem that works well for a wide variety of other applications.
It makes sense to do that extra work, because the market for timing is relatively small:
many more people care about precision positioning than care about precision timing.
So this is where I am heading with SatPulse:
precision timing is still a core part of the mission;
but I want to add the few extra features that are needed to make SatPulse useful for
a broader range of applications, in particular precision positioning.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[For the last 8 months, I have been working on broadening the range of GNSS hardware supported by SatPulse. In 0.1, there was support only for u-blox modules. In 0.2, I have added support for a broad range of modules from other vendors, all of which are Chinese.]]></summary></entry><entry><title type="html">Using SatPulse for timing without a PHC</title><link href="https://satpulse.net/2026/04/01/using-satpulse-for-timing-without-a-phc.html" rel="alternate" type="text/html" title="Using SatPulse for timing without a PHC" /><published>2026-04-01T09:45:00+00:00</published><updated>2026-04-01T09:45:00+00:00</updated><id>https://satpulse.net/2026/04/01/using-satpulse-for-timing-without-a-phc</id><content type="html" xml:base="https://satpulse.net/2026/04/01/using-satpulse-for-timing-without-a-phc.html"><![CDATA[<p>Up to now, using SatPulse for timing has required some <a href="/hardware/">very specialized hardware</a>.
Over the last couple of days, I have implemented a feature that removes this requirement.
It works very simply. SatPulse already has the ability to talk to an NTP server using chrony’s refclock SOCK protocol;
this is configured by the <code class="language-plaintext highlighter-rouge">[ntp]</code> section of <code class="language-plaintext highlighter-rouge">satpulse.toml</code>.
SatPulse also has the ability to work without a PTP hardware clock;
this is configured simply by leaving out the <code class="language-plaintext highlighter-rouge">[phc]</code> section of <code class="language-plaintext highlighter-rouge">satpulse.toml</code>.
In this mode, you can use SatPulse for monitoring and for GNSS packet distribution.
What I’ve implemented is to make the <code class="language-plaintext highlighter-rouge">[ntp]</code> section work without a <code class="language-plaintext highlighter-rouge">[phc]</code> section.
In this situation, SatPulse will use the timing of serial messages from the receiver
to generate samples to send to the NTP server.
On its own, the accuracy from serial timing will be very poor: worse than you would typically get from NTP over a network.
But this becomes useful when combined with an NTP server that has the ability to read a PPS signal.
For example chrony has a PPS refclock that uses the kernel PPS subsystem.
Chrony can also read a PPS signal from a PHC using the PHC refclock with the extpps option.
In this case the NTP server uses the much more accurate PPS signal to determine when a second starts,
and will use the information from SatPulse to determine which second it is.</p>

<p>I have hooked this up to SatPulse’s GPS auto-configuration system (enabled with <code class="language-plaintext highlighter-rouge">config=true</code> in the <code class="language-plaintext highlighter-rouge">[gps]</code> section).
So when you have an <code class="language-plaintext highlighter-rouge">[ntp]</code> and no <code class="language-plaintext highlighter-rouge">[phc]</code> section, it will configure the GPS appropriately (e.g enable a PPS,
enable time mode, enable messages reporting UTC time).
I think this will provide quite a nice way of running an NTP server: as well as auto-configuration, you get the
other SatPulse conveniences like a Web dashboard and Prometheus metrics.</p>

<p>I have only tested this very lightly. I used the chrony PHC extpps option to test,
since all my machines are currently set up with the PPS connected to a PHC.
I used the following as the satpulsed configuration:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[serial]
device = "/dev/ttyACM0"
speed = 38400
[gps]
config = true
vendor = "u-blox"
[ntp]
sock.path = "/var/run/chrony.satpulse.clk.sock"
</code></pre></div></div>

<p>and then used this as my chrony configuration:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>refclock PHC /dev/ptp0:extpps:pin=1 width 0.1 poll 2 lock NMEA refid PPS
refclock SOCK /var/run/chrony.satpulse.clk.sock offset 0.1 delay 0.2 refid NMEA noselect
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">lock NMEA</code> option tells chrony to use the samples from SatPulse to complete the PHC samples;
the <code class="language-plaintext highlighter-rouge">noselect</code> option tells chrony not to use the SatPulse samples as an independent time source.</p>

<p>Using chrony to read the PHC like this has one advantage over using SatPulse with a <code class="language-plaintext highlighter-rouge">[phc]</code> section:
SatPulse will adjust the phase and frequency of the PHC, which is what you want for PTP,
whereas chrony will leave it free-running.
This makes it easy to use the hardware-timestamping feature of chrony, which requires
a free-running PHC.
With SatPulse, you would need to use a virtual PHC, which I have not yet implemented support for.
So using chrony like this is right now the best approach if you want NTP hardware timestamping
and are not interested in PTP.</p>]]></content><author><name>James Clark</name></author><summary type="html"><![CDATA[Up to now, using SatPulse for timing has required some very specialized hardware. Over the last couple of days, I have implemented a feature that removes this requirement. It works very simply. SatPulse already has the ability to talk to an NTP server using chrony’s refclock SOCK protocol; this is configured by the [ntp] section of satpulse.toml. SatPulse also has the ability to work without a PTP hardware clock; this is configured simply by leaving out the [phc] section of satpulse.toml. In this mode, you can use SatPulse for monitoring and for GNSS packet distribution. What I’ve implemented is to make the [ntp] section work without a [phc] section. In this situation, SatPulse will use the timing of serial messages from the receiver to generate samples to send to the NTP server. On its own, the accuracy from serial timing will be very poor: worse than you would typically get from NTP over a network. But this becomes useful when combined with an NTP server that has the ability to read a PPS signal. For example chrony has a PPS refclock that uses the kernel PPS subsystem. Chrony can also read a PPS signal from a PHC using the PHC refclock with the extpps option. In this case the NTP server uses the much more accurate PPS signal to determine when a second starts, and will use the information from SatPulse to determine which second it is.]]></summary></entry></feed>