DCF77 — My Own Atomic Time Transmitter
~ / dan / projects / dcf77.html ← back
DCF77 — My Own Atomic Time Transmitter
Reference: harlock974/time-signal  ·  Dec 2025  ·  Cyprus
PRE Idea · Setup · Build
goal
Build my own atomic time signal transmitter. Bedroom-scale.
50 mW where Mainflingen has 50 kW. Sync my watch from across
the room with a thing I soldered myself.

The premise

Forty years in IT. OSCP. OSCE. Kubernetes clusters in production. A soldering iron in my hand for the first time since the original Xbox modchip era. It started on a Saturday afternoon in November when I cracked open a dead radio-controlled wall clock to harvest the ferrite antenna. By 17:41 that same evening, the time-signal software was running and the watch was attempting to receive. It just wasn't synced yet.

The proper hardware build came four weeks later. Saturday December 6, evening tinkering at the bench. Oscilloscope on the LC tank. By 03:02 AM Sunday December 7, the Casio Wave Ceptor displayed GET SIG and the correct date. Watch synced. From my bedroom in Paphos.

This is the story of those weeks.

What is DCF77?

DCF77 is the longwave time signal transmitter operated by the Physikalisch-Technische Bundesanstalt in Mainflingen, Germany. It broadcasts the current time on 77.5 kHz, encoded as second-by-second amplitude modulation of the carrier. Every radio-controlled clock you have ever owned in Europe gets its time from this station. From its 50 kW transmitter and 150-meter antenna, the signal nominally covers about 2,000 kilometers — Mainflingen to Cyprus is 2,300 km, well outside the receivable range. No commercial DCF77 watch ever syncs here, day or night.

The plan was not to receive DCF77. The plan was to be DCF77. A miniature one. Bedroom-scale. 50 milliwatts instead of 50 kilowatts. A 4.5-centimeter ferrite rod instead of a 150-meter tower. Efficiency: 0.00012% of the real thing. Functional reach: enough to sync the watch on my nightstand.

The hardware stack

The reference project came from harlock974/time-signal on GitHub — a Raspberry Pi-based time signal generator that toggles a GPIO pin at 77.5 kHz, encoding the DCF77 protocol second-by-second. The Pi handles the signal generation in software. Everything between the GPIO pin and the air is your problem.

signal chain
RPi GPIO  -->  Push-pull MOSFET driver  -->  Series resistor
                                                       |
              Gate-coupled BS170/BS250            10R current limit
                                                       |
                                                LC tank circuit
                                                (ferrite + 2.2nF cap)
                                                       |
                                                Radiated 77.5 kHz

The ferrite antenna was salvaged from a dead wall-mounted radio clock. This turned out to be a critical and counterintuitive choice. The ferrite from a receiver clock is factory-tuned for a specific time signal — and the one I pulled was originally tuned for JJY (Japan's equivalent station at 40 kHz). I discovered this by measuring on the oscilloscope: the LC resonance was nowhere near 77.5 kHz. After recalculation:

resonance math
f = 1 / (2 * pi * sqrt(L*C))

For 77.5 kHz with 700 uH ferrite:
C = 1 / (4 * pi^2 * 700uH * 77500^2) ~ 6 nF

I retuned it by swapping the parallel capacitor until the scope showed clean resonance at 77.5 kHz. Standard value 6.8 nF, later refined to 2.2 nF on the final perfboard after measuring the actual ferrite inductance more precisely.

The driver stage went through evolution. Driving the ferrite directly from a Pi GPIO pin produced a weak, distorted signal — 3.3V at very limited current is not enough to push useful magnetic flux through a coil. The first upgrade was a single MOSFET; this worked but ran hot and produced asymmetric waveforms. The final design is a complementary push-pull pair: BS250 (P-channel) on top, BS170 (N-channel) on the bottom, gates tied together to the GPIO. When GPIO is high, BS170 sinks; when low, BS250 sources. The drain junction drives the LC tank through the 10R series resistor.

This is a textbook Class-D output stage. It is also the moment when I first really understood what "complementary push-pull" means, because I had to wire it on a breadboard, see what happened, and trace the consequences with a probe.

What killed me, what saved me

Dead MOSFET, day one. The first BS170 I plugged in was already dead from the factory or from static somewhere in its life. The circuit didn't work. I assumed I had wired it wrong. I rewired everything three times. Eventually I put the scope probe on the drain and saw nothing changing when the GPIO toggled. Replaced the MOSFET with a fresh one from the same tray. Signal appeared instantly. Lesson burned in: trust the measurement, not the assumption.

40 kHz vs 77.5 kHz. The salvaged ferrite was originally tuned for the Japanese JJY40 station. I spent an embarrassing amount of time wondering why the LC tank looked like it was resonating "near" the right frequency but the watch wouldn't sync. The scope eventually showed the actual resonance peak — about 40 kHz, exactly the JJY band. Recalculation and cap swap fixed it.

Cooperative switching artifacts. The push-pull output had high-frequency spikes on every transition — both MOSFETs briefly conducting at the crossover, creating shoot-through current that contaminated the waveform with harmonics. Cleaner sine = more energy at 77.5 kHz = better range. Increasing the series resistor to 22R softened the edges. Adding a 100nF decoupling cap across the 5V rail stabilized the supply against the switching transients. The waveform on the scope went from "kicks" to something approximating a real sine wave.

Timezone confusion. The Pi was running in Europe/Paphos timezone (EET, UTC+2). The watch is hardcoded to interpret DCF77 as CET (UTC+1) and then convert to its configured local time. Result: watch displayed +1 hour from reality. Fix: set the Pi's timezone to Europe/Berlin. The transmitter now broadcasts CET as DCF77 actually does, and the watch correctly converts to EET locally.

The software side

The transmitter runs as a systemd service. No screen sessions, no nohup hacks, no ssh-dependent processes:

/etc/systemd/system/dcf77.service
[Unit]
Description=DCF77 Time Signal Transmitter
After=network.target

[Service]
ExecStart=/home/pi/time-signal/time-signal -s DCF77
Restart=always

[Install]
WantedBy=multi-user.target

systemctl enable dcf77 and the transmitter comes up automatically on every boot. If it crashes, it restarts. If the Pi reboots, it restarts. It has now been running continuously for months.

Validation

03:02 AM, Sunday December 7. Casio Wave Ceptor about 50 cm from the transmitter. Display: GET SIG. Date: 7-12-2025. The watch was actively receiving and decoding my signal.

This is the moment that matters. Not "the scope shows the right frequency." Not "the LC tank resonates correctly." A Casio Wave Ceptor that had spent its entire life in Cyprus showing "NO SIG" — because DCF77 from Mainflingen never reaches this island — locked onto my 50-milliwatt signal from across the bedroom and started behaving like it was sitting on a kitchen table in Frankfurt. Binary outcome. Synced or didn't sync. Mine.

Range testing followed: roughly 2.5 meters of reliable reception. Beyond that, the watch fails to lock. Adequate for the bedroom, the only environment that matters.

PHOTOS The actual build, chronologically
POST Learnings · Afterthoughts · Timeline

The physical build

After breadboard validation, I moved to perfboard. First soldering work in over twenty years — the last time was the original Xbox modchip era. No third-hand clamp. No loupe lamp. No proper desoldering gear. Eyes that struggle with 0603 components. The work was slow, the joints were uneven, but every connection passed continuity check. The board sits in a plastic Tupperware case (plastic is RF-transparent at 77.5 kHz; metal would create a Faraday cage and kill the signal) in a cupboard near the bed. It has not been touched since.

Known limitations and the upgrade path I deferred

The current build has one structural weakness: the ferrite antenna is a receiver-grade part being used as a transmitter. Receiver ferrites are optimized for sensitivity to incoming flux — they have many turns of fine wire on a small core for high inductance per volume. Transmitter antennas need the opposite: low resistance, high current handling, large radiating surface area.

The 50 cm reliable-sync distance is what you get with this compromise. For substantially better range — say, anywhere in the apartment — the antenna should be a purpose-built transmit loop: 10–20 turns of thicker wire wound on a 10–20 cm diameter form, retuned to 77.5 kHz with a parallel capacitor. More current, more radiating area, more flux density at distance.

This is a v2.0 problem. The current build covers the bedroom, which is the only place I need it to cover. Optimization is infinite. Working is binary.

Why this mattered

I have spent four decades in IT. I have built things in Kubernetes, in Python, in Rust, in PowerShell, in Bash. I have certifications in offensive security that required me to chain together exploits across multiple systems under time pressure. And yet, until this weekend, I had never built a thing that produced a measurable physical effect in the real world from first principles.

Software has a tendency to lie to you. kubectl get pods says Running, but is the application actually serving traffic? You write a test, the test passes — does the test test what you think it tests? Every layer of abstraction is a place where the system can be subtly broken in a way that looks fine.

A 77.5 kHz transmitter is not subtle. The scope shows a sine wave or it doesn't. The watch syncs or it doesn't. The frequency is right or wrong. Physics does not negotiate, does not gaslight, does not pretend to be working. It either is, or it isn't.

This is the right kind of project for the right kind of brain. The hyperfocus took over completely — I was sketching board layouts in my head while trying to sleep, got up at 5:30 Sunday morning to keep going, and finished the perfboard soldering by mid-afternoon. Forty years of waiting to find out hardware could feel like this.

The DCF77 transmitter still runs. Every morning the watch is on the correct time. Bedroom is calibrated to UTC, atomically, by a Tupperware container in a cupboard. That's the entire summary.

This was the gateway drug. Six months later: BELLA DAC 1.

learnings · timeline
Learnings:
  - Trust the measurement, not the assumption. The first MOSFET
    was dead. I rewired the circuit three times before reaching
    for the scope. The scope answered in 10 seconds.
  - Receiver ferrites are factory-tuned for whatever station
    the original device received. Salvaged parts come with
    hidden assumptions. Measure first, assume nothing.
  - Push-pull MOSFET drivers have shoot-through. The series
    resistor isn't optional -- it's the difference between a
    clean sine and a contaminated mess.
  - systemd is the only acceptable way to run a persistent
    service. Everything else is technical debt by default.
  - Plastic enclosure good. Metal enclosure = Faraday cage =
    no signal. Counterintuitive until you remember the physics.
  - Software lies. Hardware doesn't. Physics either works or
    it doesn't. There is no middle ground, no "it should work
    but doesn't" -- there is only "the scope shows it" or "the
    scope doesn't." Forty years of software made hardware
    feel like coming home.

Timeline:
  - 2025-11-08 Sat 14:41: Cracked open a dead wall clock.
    Extracted the ferrite antenna. Discovered it was tuned
    for JJY40 (Japan), not DCF77.
  - 2025-11-08 Sat 17:36: First reception attempt.
    Bare ferrite + alligator clips. Watch in RC! mode.
    Software broadcasting. No sync yet.
  - 2025-11 / early 12: Ordering MOSFETs, breadboarding
    the push-pull driver, capacitor tuning math.
  - 2025-12-06 Sat 19:23: Siglent oscilloscope on the
    LC tank. Tuning the resonance, sorting out the
    shoot-through artifacts.
  - 2025-12-07 Sun 03:02 AM: GET SIG. Watch displays
    7-12-2025. Date: correct. Time: correct. Mine.
  - 2025-12-07 Sun morning: Perfboard layout. First
    soldering work in 20+ years (last time: Xbox
    modchips). Slow, ugly, functional.
  - 2025-12-07 Sun ~09:44: Tupperware enclosure sealed.
    Bedroom installation. Cat indifferent.
  - Later: systemd service, permanent installation in
    cupboard. Green LED glowing through the lid.
  - Months since: Still running. Watch always on time.
    Has not been touched.

Status: Production. Bedroom UTC calibration active.
dcf77.html · 50 mW · 77.5 kHz · bedroom-scale
2026-02-24