diff --git a/artiq/gateware/jesd204_tools.py b/artiq/gateware/jesd204_tools.py
new file mode 100644
index 000000000..bc7a9c853
--- /dev/null
+++ b/artiq/gateware/jesd204_tools.py
@@ -0,0 +1,107 @@
+from collections import namedtuple
+
+from migen import *
+from migen.genlib.cdc import MultiReg
+from migen.genlib.resetsync import AsyncResetSynchronizer
+from misoc.interconnect.csr import *
+
+from jesd204b.common import (JESD204BTransportSettings,
+                             JESD204BPhysicalSettings,
+                             JESD204BSettings)
+from jesd204b.phy.gth import GTHChannelPLL as JESD204BGTHChannelPLL
+from jesd204b.phy import JESD204BPhyTX
+from jesd204b.core import JESD204BCoreTX
+from jesd204b.core import JESD204BCoreTXControl
+
+
+class UltrascaleCRG(Module, AutoCSR):
+    linerate = int(6e9)
+    refclk_freq = int(150e6)
+    fabric_freq = int(125e6)
+
+    def __init__(self, platform, use_rtio_clock=False):
+        self.jreset = CSRStorage(reset=1)
+        self.jref = Signal()
+        self.refclk = Signal()
+        self.clock_domains.cd_jesd = ClockDomain()
+
+        refclk2 = Signal()
+        refclk_pads = platform.request("dac_refclk", 0)
+        platform.add_period_constraint(refclk_pads.p, 1e9/self.refclk_freq)
+        self.specials += [
+            Instance("IBUFDS_GTE3", i_CEB=self.jreset.storage, p_REFCLK_HROW_CK_SEL=0b00,
+                     i_I=refclk_pads.p, i_IB=refclk_pads.n,
+                     o_O=self.refclk, o_ODIV2=refclk2),
+            AsyncResetSynchronizer(self.cd_jesd, self.jreset.storage),
+        ]
+
+        if use_rtio_clock:
+            self.comb += self.cd_jesd.clk.eq(ClockSignal("rtio"))
+        else:
+            self.specials += Instance("BUFG_GT", i_I=refclk2, o_O=self.cd_jesd.clk)
+
+        jref = platform.request("dac_sysref")
+        jref_se = Signal()
+        self.specials += [
+            Instance("IBUFDS_IBUFDISABLE",
+                p_USE_IBUFDISABLE="TRUE", p_SIM_DEVICE="ULTRASCALE",
+                i_IBUFDISABLE=self.jreset.storage,
+                i_I=jref.p, i_IB=jref.n,
+                o_O=jref_se),
+            # SYSREF normally meets s/h at the FPGA, except during margin
+            # scan. Be paranoid and use a double-register anyway.
+            MultiReg(jref_se, self.jref, "jesd")
+        ]
+
+
+PhyPads = namedtuple("PhyPads", "txp txn")
+
+
+class UltrascaleTX(Module, AutoCSR):
+    def __init__(self, platform, sys_crg, jesd_crg, dac):
+        ps = JESD204BPhysicalSettings(l=8, m=4, n=16, np=16)
+        ts = JESD204BTransportSettings(f=2, s=2, k=16, cs=0)
+        settings = JESD204BSettings(ps, ts, did=0x5a, bid=0x5)
+
+        jesd_pads = platform.request("dac_jesd", dac)
+        phys = []
+        for i in range(len(jesd_pads.txp)):
+            cpll = JESD204BGTHChannelPLL(
+                    jesd_crg.refclk, jesd_crg.refclk_freq, jesd_crg.linerate)
+            self.submodules += cpll
+            phy = JESD204BPhyTX(
+                    cpll, PhyPads(jesd_pads.txp[i], jesd_pads.txn[i]),
+                    jesd_crg.fabric_freq, transceiver="gth")
+            platform.add_period_constraint(phy.transmitter.cd_tx.clk,
+                    40*1e9/jesd_crg.linerate)
+            platform.add_false_path_constraints(
+                sys_crg.cd_sys.clk,
+                jesd_crg.cd_jesd.clk,
+                phy.transmitter.cd_tx.clk)
+            phys.append(phy)
+
+        to_jesd = ClockDomainsRenamer("jesd")
+        self.submodules.core = core = to_jesd(JESD204BCoreTX(
+            phys, settings, converter_data_width=64))
+        self.submodules.control = control = to_jesd(JESD204BCoreTXControl(core))
+        core.register_jsync(platform.request("dac_sync", dac))
+        core.register_jref(jesd_crg.jref)
+
+
+# This assumes:
+#  * coarse RTIO frequency = 16*SYSREF frequency
+#  * JESD and coarse RTIO clocks are the same
+#    (only reset may differ).
+#  * SYSREF meets setup/hold at the FPGA when sampled
+#    in the JESD/RTIO domain.
+#
+# Look at the 4 LSBs of the coarse RTIO timestamp counter
+# to determine SYSREF phase.
+
+class SysrefSampler(Module, AutoCSR):
+    def __init__(self, coarse_ts, jref):
+        self.sample_result = CSRStatus()
+
+        sample = Signal()
+        self.sync.rtio += If(coarse_ts[:4] == 0, sample.eq(jref))
+        self.specials += MultiReg(sample, self.sample_result.status)
diff --git a/artiq/gateware/targets/sayma_amc.py b/artiq/gateware/targets/sayma_amc.py
index 08d7b0127..3079de4a3 100755
--- a/artiq/gateware/targets/sayma_amc.py
+++ b/artiq/gateware/targets/sayma_amc.py
@@ -2,11 +2,9 @@
 
 import argparse
 import os
-from collections import namedtuple
 import warnings
 
 from migen import *
-from migen.genlib.resetsync import AsyncResetSynchronizer
 
 from misoc.cores import gpio
 from misoc.integration.soc_sdram import soc_sdram_args, soc_sdram_argdict
@@ -14,18 +12,11 @@ from misoc.integration.builder import builder_args, builder_argdict
 from misoc.interconnect.csr import *
 from misoc.targets.sayma_amc import BaseSoC, MiniSoC
 
-from jesd204b.common import (JESD204BTransportSettings,
-                             JESD204BPhysicalSettings,
-                             JESD204BSettings)
-from jesd204b.phy.gth import GTHChannelPLL as JESD204BGTHChannelPLL
-from jesd204b.phy import JESD204BPhyTX
-from jesd204b.core import JESD204BCoreTX
-from jesd204b.core import JESD204BCoreTXControl
-
 from artiq.gateware.amp import AMPSoC
 from artiq.gateware import serwb
 from artiq.gateware import remote_csr
 from artiq.gateware import rtio
+from artiq.gateware import jesd204_tools
 from artiq.gateware.rtio.phy import ttl_simple, sawg
 from artiq.gateware.drtio.transceiver import gth_ultrascale
 from artiq.gateware.drtio.siphaser import SiPhaser7Series
@@ -35,77 +26,10 @@ from artiq.build_soc import build_artiq_soc
 from artiq import __version__ as artiq_version
 
 
-PhyPads = namedtuple("PhyPads", "txp txn")
-to_jesd = ClockDomainsRenamer("jesd")
-
-
-class AD9154CRG(Module, AutoCSR):
-    linerate = int(6e9)
-    refclk_freq = int(150e6)
-    fabric_freq = int(125e6)
-
-    def __init__(self, platform, use_rtio_clock=False):
-        self.jreset = CSRStorage(reset=1)
-        self.jref = Signal()
-        self.refclk = Signal()
-        self.clock_domains.cd_jesd = ClockDomain()
-
-        refclk2 = Signal()
-        refclk_pads = platform.request("dac_refclk", 0)
-        platform.add_period_constraint(refclk_pads.p, 1e9/self.refclk_freq)
-        self.specials += [
-            Instance("IBUFDS_GTE3", i_CEB=self.jreset.storage, p_REFCLK_HROW_CK_SEL=0b00,
-                     i_I=refclk_pads.p, i_IB=refclk_pads.n,
-                     o_O=self.refclk, o_ODIV2=refclk2),
-            AsyncResetSynchronizer(self.cd_jesd, self.jreset.storage),
-        ]
-
-        if use_rtio_clock:
-            self.comb += self.cd_jesd.clk.eq(ClockSignal("rtio"))
-        else:
-            self.specials += Instance("BUFG_GT", i_I=refclk2, o_O=self.cd_jesd.clk)
-
-        jref = platform.request("dac_sysref")
-        self.specials += Instance("IBUFDS_IBUFDISABLE",
-            p_USE_IBUFDISABLE="TRUE", p_SIM_DEVICE="ULTRASCALE",
-            i_IBUFDISABLE=self.jreset.storage,
-            i_I=jref.p, i_IB=jref.n,
-            o_O=self.jref)
-
-
-class AD9154JESD(Module, AutoCSR):
-    def __init__(self, platform, sys_crg, jesd_crg, dac):
-        ps = JESD204BPhysicalSettings(l=8, m=4, n=16, np=16)
-        ts = JESD204BTransportSettings(f=2, s=2, k=16, cs=0)
-        settings = JESD204BSettings(ps, ts, did=0x5a, bid=0x5)
-
-        jesd_pads = platform.request("dac_jesd", dac)
-        phys = []
-        for i in range(len(jesd_pads.txp)):
-            cpll = JESD204BGTHChannelPLL(
-                    jesd_crg.refclk, jesd_crg.refclk_freq, jesd_crg.linerate)
-            self.submodules += cpll
-            phy = JESD204BPhyTX(
-                    cpll, PhyPads(jesd_pads.txp[i], jesd_pads.txn[i]),
-                    jesd_crg.fabric_freq, transceiver="gth")
-            platform.add_period_constraint(phy.transmitter.cd_tx.clk,
-                    40*1e9/jesd_crg.linerate)
-            platform.add_false_path_constraints(
-                sys_crg.cd_sys.clk,
-                jesd_crg.cd_jesd.clk,
-                phy.transmitter.cd_tx.clk)
-            phys.append(phy)
-
-        self.submodules.core = core = to_jesd(JESD204BCoreTX(
-            phys, settings, converter_data_width=64))
-        self.submodules.control = control = to_jesd(JESD204BCoreTXControl(core))
-        core.register_jsync(platform.request("dac_sync", dac))
-        core.register_jref(jesd_crg.jref)
-
-
 class AD9154(Module, AutoCSR):
     def __init__(self, platform, sys_crg, jesd_crg, dac):
-        self.submodules.jesd = AD9154JESD(platform, sys_crg, jesd_crg, dac)
+        self.submodules.jesd = jesd204_tools.UltrascaleTX(
+            platform, sys_crg, jesd_crg, dac)
 
         self.sawgs = [sawg.Channel(width=16, parallelism=4) for i in range(4)]
         self.submodules += self.sawgs
@@ -117,7 +41,8 @@ class AD9154(Module, AutoCSR):
 
 class AD9154NoSAWG(Module, AutoCSR):
     def __init__(self, platform, sys_crg, jesd_crg, dac):
-        self.submodules.jesd = AD9154JESD(platform, sys_crg, jesd_crg, dac)
+        self.submodules.jesd = jesd204_tools.UltrascaleTX(
+            platform, sys_crg, jesd_crg, dac)
 
         self.sawgs = []
 
@@ -231,7 +156,7 @@ class Standalone(MiniSoC, AMPSoC, RTMCommon):
         self.submodules += phy
         rtio_channels.append(rtio.Channel.from_phy(phy))
 
-        self.submodules.ad9154_crg = AD9154CRG(platform)
+        self.submodules.ad9154_crg = jesd204_tools.UltrascaleCRG(platform)
         if with_sawg:
             cls = AD9154
         else:
@@ -276,6 +201,10 @@ class Standalone(MiniSoC, AMPSoC, RTMCommon):
                                                       self.get_native_sdram_if())
         self.csr_devices.append("rtio_analyzer")
 
+        self.submodules.sysref_sampler = jesd204_tools.SysrefSampler(
+            self.rtio_core.coarse_ts, self.ad9154_crg.jref)
+        self.csr_devices.append("sysref_sampler")
+
 
 class Master(MiniSoC, AMPSoC, RTMCommon):
     mem_map = {
@@ -433,7 +362,8 @@ class Satellite(BaseSoC, RTMCommon):
         self.submodules += phy
         rtio_channels.append(rtio.Channel.from_phy(phy))
 
-        self.submodules.ad9154_crg = AD9154CRG(platform, use_rtio_clock=True)
+        self.submodules.ad9154_crg = jesd204_tools.UltrascaleCRG(
+            platform, use_rtio_clock=True)
         if with_sawg:
             cls = AD9154
         else:
@@ -491,6 +421,10 @@ class Satellite(BaseSoC, RTMCommon):
         self.config["I2C_BUS_COUNT"] = 1
         self.config["HAS_SI5324"] = None
 
+        self.submodules.sysref_sampler = jesd204_tools.SysrefSampler(
+            self.drtio0.coarse_ts, self.ad9154_crg.jref)
+        self.csr_devices.append("sysref_sampler")
+
         rtio_clk_period = 1e9/rtio_clk_freq
         gth = self.drtio_transceiver.gths[0]
         platform.add_period_constraint(gth.txoutclk, rtio_clk_period)