Post

Custom DBTOs for Embedded board PPS signals

A while ago, I wrote about using a cheap GPS module as a NTP PPS source, and hot on the heals of Telstra’s NTP issues, I figured I should resurrect my GPS time source. Just because.

This used a serial port and the DCD pin to make this work. However, what if you wanted to hook up an embedded board and use the onboard GPIOs instead?

Reviewing the GPS module, you’ll see it’s pretty straight forward. TX / RX for serial data, PPS for the pulse, and power. GPS Module

As such, we want a 5v power, ground, a UART, and a PPS source.

Looking at the Rock 5B GPIO pinout, we can see multple UARTS are available, but I’m going to use UART2. By default, this is being used as a debug serial console - which I really don’t need - but its low hanging fruit since the majority is already configured. I’m also going to use Pin 7 (GPIO3_C3) as the PPS - as it remains compatible with many Raspberry Pi NTP boards that are available.

The exact pinout I used is:

1
2
3
4
5
Rock 5B pin 4  (+5.0V)    -> GPS VCC (Red)
Rock 5B pin 6  (GND)      -> GPS GND (Black Thick)
Rock 5B pin 7  (GPIO3_C3) -> GPS PPS (Black)
Rock 5B pin 8  (UART2_TX) -> GPS RX (White)
Rock 5B pin 10 (UART2_RX) -> GPS TX (Green)

This is all done on Fedora 44 aarch64, installed as a normal arm64 UEFI OS using the EDK2-RK3588 bootloader.

Firstly, we want to make a Device Tree Overlay (dtbo) to remap some functions. This will remap GPIO3_C3 to a pps-gpio pin and set it up at boot. The next part (fragment@2) is to disable the built in serial console on ttyS2 or UART2 to allow us to use it for GPS communications.

Save the following file to /usr/local/lib/firmware/pps-rock5b.dts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target-path = "/";

        __overlay__ {
            pps: pps {
                compatible = "pps-gpio";
                pinctrl-names = "default";
                pinctrl-0 = <&pps_pins>;
                gpios = <&gpio3 19 0>;
                status = "okay";
            };
        };
    };

    fragment@1 {
        target = <&pinctrl>;
        __overlay__ {

            pps {
                pps_pins: pps_pins {
                    rockchip,pins = <3 19 0 &pcfg_pull_none>;
                };
            };
        };
    };

    fragment@2 {
        target-path = "/chosen";

        __overlay__ {
            stdout-path = "tty0";
        };
    };
};

We then need to compile this Device Tree Source (dts) into the binary overlay (dtbo) with:

dtc -@ -I dts -O dtb -o pps-rock5b.dtbo pps-rock5b.dts

This now gives us our compiled overlay which is ready to merge in with the Fedora kernel’s stock DTB for this hardware definition.

We also need to do this for every new kernel that is installed - as we’ll need to apply this overlay to every stock distributed DTB file. The easy way to do this is install a hook in /etc/kernel/install.d/. Save this as /etc/kernel/install.d/90-rock5b-pps.install and chmod +x it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/bin/bash

COMMAND="$1"
KERNEL_VERSION="$2"
ENTRY_DIR="$3"

case "$COMMAND" in
    add)
        ;;
    remove)
        exit 0
        ;;
    *)
        exit 0
        ;;
esac

DTB_DIR="/usr/lib/modules/${KERNEL_VERSION}/dtb/rockchip"
BASE_DTB="${DTB_DIR}/rk3588-rock-5b.dtb"
OUTPUT_DTB="${DTB_DIR}/rk3588-rock-5b-pps.dtb"
OVERLAY="/usr/local/lib/firmware/pps-rock5b.dtbo"

if [ ! -f "$BASE_DTB" ]; then
    echo "rock5b-pps: base DTB not found: $BASE_DTB"
    exit 0
fi

if [ ! -f "$OVERLAY" ]; then
    echo "rock5b-pps: overlay not found: $OVERLAY"
    exit 0
fi

echo "rock5b-pps: applying PPS overlay to ${KERNEL_VERSION}"

fdtoverlay \
    -i "$BASE_DTB" \
    -o "$OUTPUT_DTB" \
    "$OVERLAY"

if [ $? -ne 0 ]; then
    echo "rock5b-pps: fdtoverlay failed"
    exit 1
fi

echo "rock5b-pps: installing DTB into ${ENTRY_DIR}"

cp -f "$OUTPUT_DTB" \
      "${ENTRY_DIR}/rk3588-rock-5b-pps.dtb"

if [ $? -ne 0 ]; then
    echo "rock5b-pps: failed copying DTB to ${ENTRY_DIR}"
    exit 1
fi

echo "rock5b-pps: installed ${ENTRY_DIR}/rk3588-rock-5b-pps.dtb"

exit 0

We also need to tell the kernel install scripts to load the custom modified DTB instead of the stock one. This is done with the file /etc/kernel/devicetree.

1
rockchip/rk3588-rock-5b-pps.dtb

You can then trigger off the kernel-install script to rebuild everything automatically.

kernel-install add $(uname -r) /lib/modules/$(uname -r)/vmlinuz

After running this, you should see some output text from our hook that shows the DTBO being applied:

1
2
3
rock5b-pps: applying PPS overlay to 7.1.4-200.fc44.aarch64
rock5b-pps: installing DTB into /boot/efi/280626e6191c4e0089774fa5a1708ef7/7.1.4-200.fc44.aarch64
rock5b-pps: installed /boot/efi/280626e6191c4e0089774fa5a1708ef7/7.1.4-200.fc44.aarch64/rk3588-rock-5b-pps.dtb

At this point, it should be safe to reboot.

After the reboot, you should see some new messages in the dmesg about loading the pps modules, see the new entry for /dev/pps0, and be able to run ppstest against it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ dmesg | grep pps
[    0.053948] pps_core: LinuxPPS API ver. 1 registered
[    0.053950] pps_core: Software ver. 5.3.6 - Copyright 2005-2007 Rodolfo Giometti <giometti@linux.it>
[  109.131898] pps_ldisc: PPS line discipline registered

$ ls -l /dev/pps*
crw-rw---- 1 root root 244, 0 Jul 21 00:42 /dev/pps0

$ ppstest /dev/pps0
trying PPS source "/dev/pps0"
found PPS source "/dev/pps0"
ok, found 1 source(s), now start fetching data...
source 0 - assert 1784691579.000027046, sequence: 802488 - clear  0.000000000, sequence: 0
source 0 - assert 1784691579.999996774, sequence: 802489 - clear  0.000000000, sequence: 0
source 0 - assert 1784691581.000000625, sequence: 802490 - clear  0.000000000, sequence: 0
source 0 - assert 1784691582.000000394, sequence: 802491 - clear  0.000000000, sequence: 0
source 0 - assert 1784691582.999996661, sequence: 802492 - clear  0.000000000, sequence: 0
source 0 - assert 1784691583.999992633, sequence: 802493 - clear  0.000000000, sequence: 0

Setting up the GPS module for me was the same as the previous post, just substituting ttyS0 for ttyS2. I also set things to use 115200 this time instead of 38400 in the last post. Functionally, this probably makes no difference at all.

There is a slight difference to /etc/chrony.conf in the PPS definition. This time, I used:

1
2
refclock SHM 0 refid GPS offset 0.0 delay 0.0 noselect
refclock PPS /dev/pps0 refid PPS lock GPS offset 0.0 delay 0.0

After letting it run for a few days, accuracy is pretty good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ chronyc tracking
Reference ID    : 50505300 (PPS)
Stratum         : 1
Ref time (UTC)  : Wed Jul 22 03:45:26 2026
System time     : 0.000000098 seconds fast of NTP time
Last offset     : +0.000000037 seconds
RMS offset      : 0.000001243 seconds
Frequency       : 26.075 ppm fast
Residual freq   : +0.000 ppm
Skew            : 0.015 ppm
Root delay      : 0.000000000 seconds
Root dispersion : 0.000011834 seconds
Update interval : 16.0 seconds
Leap status     : Normal

$ chronyc sourcestats
Name/IP Address            NP  NR  Span  Frequency  Freq Skew  Offset  Std Dev
==============================================================================
GPS                        16   9   241     -5.659      3.935    +64ms   318us
PPS                        13   6   195     +0.001      0.029     +6ns  1472ns
This post is licensed under CC BY 4.0 by the author.