Project 5: The Hidden Text (RDS Decoder)
Decode Radio Data System (RDS) from FM broadcasts to recover station IDs and radio text.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Advanced |
| Time Estimate | 3-4 weeks |
| Main Programming Language | Python (Alternatives: C, Rust) |
| Alternative Programming Languages | C++ |
| Coolness Level | High |
| Business Potential | Medium (broadcast monitoring) |
| Prerequisites | FM demod, filters, PLL basics |
| Key Topics | 57 kHz subcarrier, BPSK, Costas loop, differential decoding |
1. Learning Objectives
By completing this project, you will:
- Isolate the 57 kHz RDS subcarrier from FM baseband.
- Recover the suppressed carrier using a Costas loop or squaring method.
- Demodulate BPSK at 1187.5 bps and recover bit timing.
- Decode RDS groups, validate checkwords, and extract text.
- Build a robust decoder that runs on live FM data.
2. All Theory Needed (Per-Concept Breakdown)
2.1 RDS Subcarrier Extraction and Carrier Recovery
Fundamentals
RDS is embedded in FM broadcasts as a 57 kHz subcarrier. It is a suppressed-carrier BPSK signal at 1187.5 bits per second. The 57 kHz subcarrier is the third harmonic of the 19 kHz stereo pilot, which provides a convenient reference. To decode RDS, you must isolate the subcarrier with a band-pass filter, then recover a coherent carrier phase. Because the carrier is suppressed, you need a phase-locked loop (PLL) or a squaring method to regenerate it.
A Costas loop is a common choice. It is a PLL variant that locks to the phase of a BPSK signal by minimizing the quadrature component. Once you have a stable carrier, you can mix the signal to baseband and recover the BPSK data.
Deep Dive into the Concept
FM demod yields a baseband signal that contains multiple components: mono audio (0-15 kHz), a 19 kHz pilot, a 38 kHz stereo subcarrier (L-R), and the 57 kHz RDS subcarrier. The RDS subcarrier is relatively weak (about -20 dB below the main audio). This means your band-pass filter must be precise to extract it without too much noise. A narrow band-pass around 57 kHz (e.g., 2-4 kHz bandwidth) is typical.
Once isolated, the RDS signal is BPSK with a suppressed carrier. BPSK flips the phase of a carrier by 180 degrees to encode bits. If you just look at the signal, the carrier is missing. The squaring method works by squaring the signal, which doubles the frequency and removes the phase flips, producing a strong tone at 114 kHz. You can then divide by 2 (or use a PLL to lock to the 57 kHz component) and recover the carrier phase. This method is simple but can be noisy and may have ambiguity in phase.
The Costas loop provides a more robust solution. It mixes the input with a local oscillator (cos and sin), low-pass filters the I and Q outputs, and uses their product to generate a phase error term. This error drives the loop filter and adjusts the oscillator. When locked, the I component contains the data, and the Q component is minimized. Implementing a Costas loop requires careful selection of loop bandwidth and damping to balance tracking and noise.
The pilot tone at 19 kHz can be used as a reference: 57 kHz is exactly 3x the pilot. If you track the pilot with a PLL and multiply its phase by 3, you can derive a stable RDS carrier reference. This technique improves reliability on weak signals and is used in some professional decoders.
How this fits on projects
- You will implement subcarrier extraction in §5.10.
- The PLL concepts appear again in P07 (NOAA APT sync) and P09 (GSM timing).
Definitions & key terms
- RDS: Radio Data System, digital data in FM broadcasts.
- Subcarrier: A lower-frequency carrier embedded inside a modulated signal.
- Costas loop: PLL variant for BPSK/QPSK carrier recovery.
- Pilot tone: 19 kHz FM stereo reference.
Mental model diagram (ASCII)
FM baseband -> BPF(57k) -> Costas loop -> BPSK baseband
How it works (step-by-step, with invariants and failure modes)
- Band-pass filter around 57 kHz.
- Recover carrier phase (Costas loop or squaring).
- Mix to baseband (I component).
- Low-pass to remove noise.
Failure modes:
- Too wide BPF = noisy data.
- PLL loop bandwidth too wide = jitter.
Minimal concrete example
# BPF around 57 kHz
bpf = firwin(numtaps, [54e3, 60e3], fs=fs, pass_zero=False)
sub = lfilter(bpf, 1.0, fm_baseband)
Common misconceptions
- “RDS is audible.” It is inaudible but embedded in the FM baseband.
- “Any PLL works.” Costas loop is needed for suppressed-carrier BPSK.
Check-your-understanding questions
- Why is RDS at 57 kHz?
- Why do we need carrier recovery for RDS?
- What happens if the BPF is too wide?
Check-your-understanding answers
- It is the third harmonic of the 19 kHz pilot, making synchronization easier.
- The carrier is suppressed; without recovery the phase is ambiguous.
- Noise and adjacent components corrupt the BPSK data.
Real-world applications
- FM broadcast metadata and program information.
- Station identification in automotive receivers.
Where you’ll apply it
- This project: §3.4, §5.10.
- Also used in: P03 FM Receiver.
References
- Collins, “SDR for Engineers” Ch. 5
- Lyons, “Understanding DSP” Ch. 13
Key insights
RDS decoding begins with isolating and phase-locking the weak 57 kHz subcarrier.
Summary
You learned how to extract the RDS subcarrier and recover a coherent carrier for BPSK demodulation.
Homework/Exercises to practice the concept
- Plot FM baseband spectrum and identify the 57 kHz peak.
- Compare squaring vs Costas loop recovery.
- Vary PLL bandwidth and observe lock stability.
Solutions to the homework/exercises
- You should see a small peak at 57 kHz and a pilot at 19 kHz.
- Squaring is simpler but noisier; Costas loop is cleaner.
- Too narrow loses lock; too wide yields jitter.
2.2 BPSK Demodulation, Differential Decoding, and Group Sync
Fundamentals
RDS uses binary phase-shift keying (BPSK) at 1187.5 bps. After carrier recovery, the signal is a baseband waveform whose polarity flips to encode bits. Because there is a 180-degree phase ambiguity in carrier recovery, RDS uses differential encoding. This means you decode bits based on transitions rather than absolute polarity. RDS data is organized into groups of 4 blocks (A, B, C, D). Each block is 26 bits: 16 data bits and 10 check bits. The check bits are a CRC-like syndrome that identifies block boundaries and provides error detection.
Deep Dive into the Concept
Once you have a coherent baseband, you must recover symbol timing. At 1187.5 bps, each symbol is about 842 microseconds. If your sampling rate is 240 kHz (after decimation), each symbol spans about 202 samples. You can recover timing by using a zero-crossing method or a simple early-late gate. A practical approach in a first decoder is to oversample and find the best sampling phase by scanning for maximum eye opening.
Differential decoding is crucial. Suppose the carrier recovery yields a signal inverted by 180 degrees. If you look at raw bits, all bits would be inverted. Differential decoding solves this by encoding bits based on transitions: if the signal flips, that indicates a 1; if it stays the same, a 0 (or vice versa). This removes the ambiguity and makes decoding robust.
Group and block synchronization relies on checkwords. The 10-bit checkword is computed using a generator polynomial and then XORed with a block-specific offset word. By testing the syndrome, you can identify which block you are in and align the group. This is how you find the start of a group without external framing. Implementing this requires a small syndrome calculator and a sliding window over the bitstream. Once aligned, you can decode Program Service (PS) text and Radio Text (RT) fields. PS is an 8-character station name transmitted repeatedly. RT is a 64-character text message transmitted in segments.
A good decoder must handle bit errors. You can use the checkword to detect errors and drop bad blocks. Advanced decoders use soft decisions and error correction, but for this project, detection and dropping is enough.
How this fits on projects
- You will implement bit timing and differential decoding in §5.10.
- Similar differential decoding appears in P06 (NRZI) and P08 (POCSAG framing).
Definitions & key terms
- BPSK: Binary phase shift keying.
- Differential decoding: Decoding based on transitions, not absolute phase.
- Syndrome: Checkword used for error detection and block identification.
- Group: A set of four RDS blocks (A-D).
Mental model diagram (ASCII)
Baseband -> Timing -> Slice bits -> Differential -> Block sync -> Text
How it works (step-by-step, with invariants and failure modes)
- Recover symbol timing and sample at symbol centers.
- Convert samples to bits (threshold at 0).
- Differentially decode bits.
- Slide a 26-bit window and compute syndrome.
- Align blocks and decode groups.
Failure modes:
- Wrong timing yields random bits.
- Missing differential decoding yields inverted data.
Minimal concrete example
# differential decode
out = []
prev = bits[0]
for b in bits[1:]:
out.append(b ^ prev)
prev = b
Common misconceptions
- “Phase recovery removes ambiguity.” It still leaves 180-degree ambiguity.
- “Block boundaries are fixed.” They must be found from syndromes.
Check-your-understanding questions
- Why is differential decoding required?
- What do the RDS checkwords provide?
- How is symbol timing recovered?
Check-your-understanding answers
- To resolve 180-degree phase ambiguity from carrier recovery.
- Error detection and block identification.
- By sampling at optimal phase, often found by scanning or early-late gating.
Real-world applications
- Broadcast metadata and station identification.
- Emergency alert text transmissions.
Where you’ll apply it
- This project: §3.7, §5.10.
- Also used in: P06 AIS, P08 POCSAG.
References
- ETSI EN 50067 (RDS standard)
- Lyons, “Understanding DSP” Ch. 13
Key insights
Differential decoding and syndrome-based sync are what turn raw BPSK into meaningful text.
Summary
You learned how to recover timing, resolve phase ambiguity, and decode RDS groups.
Homework/Exercises to practice the concept
- Implement differential decoding on a synthetic BPSK stream.
- Compute syndromes and detect block boundaries.
- Simulate bit errors and observe block dropouts.
Solutions to the homework/exercises
- Differential decoding restores correct bit sequence regardless of inversion.
- Syndrome patterns identify block A/B/C/D.
- Errors cause checkword failure; blocks are dropped.
3. Project Specification
3.1 What You Will Build
An RDS decoder that extracts the 57 kHz subcarrier, demodulates BPSK, and outputs station ID and radio text.
3.2 Functional Requirements
- Extract 57 kHz subcarrier with band-pass filter.
- Recover carrier phase (Costas or squaring).
- Recover symbol timing and slice bits.
- Differentially decode and synchronize blocks.
- Parse and output PS and RT fields.
3.3 Non-Functional Requirements
- Performance: Real-time decode on laptop.
- Reliability: Stable decoding for 5+ minutes on strong stations.
- Usability: CLI flags for sample rate, pilot lock, and debug plots.
3.4 Example Usage / Output
$ python rds_decoder.py --freq 99.5e6 --fs 2.4e6
[RDS] PS: JAZZ101
[RDS] RT: NOW PLAYING - JOHN COLTRANE
3.5 Data Formats / Schemas / Protocols
- Input: FM baseband IQ or demodulated audio.
- Output: text lines and optional JSON.
3.6 Edge Cases
- Station without RDS.
- Weak RDS causing intermittent text.
- Pilot tone missing or drifting.
3.7 Real World Outcome
Clear station ID and radio text displayed continuously for a valid RDS station.
3.7.1 How to Run (Copy/Paste)
python rds_decoder.py --input fm_99_5.iq --fs 2400000 --freq 99.5e6 --pilot-lock
3.7.2 Golden Path Demo (Deterministic)
Use a known RDS test IQ file. Expected: stable PS text and RT segments.
3.7.3 CLI Transcript (Exact)
$ python rds_decoder.py --input test_rds.iq --fs 2400000
[RDS] PS: ROCK979
[RDS] RT: TRAFFIC UPDATE AT 5PM
3.7.4 Failure Demo
$ python rds_decoder.py --input fm_no_rds.iq --fs 2400000
[WARN] No RDS blocks decoded
[EXIT] code=3
4. Solution Architecture
4.1 High-Level Design
FM IQ -> FM demod -> BPF(57k) -> Carrier recovery -> BPSK demod -> Block parser
4.2 Key Components
| Component | Responsibility | Key Decisions | |———–|—————-|—————| | Subcarrier Filter | Extract 57 kHz | BPF width | | Carrier Recovery | Lock to 57 kHz | Costas vs squaring | | Symbol Timing | Find sampling phase | Early-late vs scan | | Parser | Decode groups | Syndrome handling |
4.3 Data Structures (No Full Code)
class RdsState:
pll_phase: float
block_sync: int
4.4 Algorithm Overview
Key Algorithm: RDS Demod
- FM demod to baseband.
- Band-pass at 57 kHz.
- Carrier recovery and mix to baseband.
- Timing recovery and bit slicing.
- Differential decode and block parse.
Complexity Analysis:
- Time: O(N)
- Space: O(N) buffers
5. Implementation Guide
5.1 Development Environment Setup
pip install numpy scipy
5.2 Project Structure
rds/
├── src/
│ ├── main.py
│ ├── subcarrier.py
│ ├── costas.py
│ ├── timing.py
│ └── parser.py
└── tests/
5.3 The Core Question You’re Answering
“How do you extract a 57 kHz data channel hidden inside an FM audio signal?”
5.4 Concepts You Must Understand First
- Subcarrier extraction and carrier recovery (§2.1)
- BPSK and differential decoding (§2.2)
5.5 Questions to Guide Your Design
- What bandwidth isolates the RDS subcarrier best?
- How will you detect and correct phase inversion?
- How will you confirm block alignment?
5.6 Thinking Exercise
Sketch the FM baseband spectrum and label the RDS subcarrier at 57 kHz.
5.7 The Interview Questions They’ll Ask
- Why is RDS at 57 kHz?
- What does the Costas loop do?
- Why differential decoding?
5.8 Hints in Layers
- Start with FM demod output from Project 3.
- Apply a narrow band-pass around 57 kHz.
- Use squaring or Costas loop to recover carrier.
- Implement differential decoding before block parsing.
5.9 Books That Will Help
| Topic | Book | Chapter | |——-|——|———| | BPSK | Lyons | Ch. 13 | | PLL | Collins | Ch. 5 |
5.10 Implementation Phases
Phase 1: Foundation (5-7 days)
Goals: Extract subcarrier and recover carrier. Checkpoint: Stable 57 kHz tone in spectrum.
Phase 2: Core Functionality (7-10 days)
Goals: Demodulate bits and sync blocks. Checkpoint: Correct PS text for known station.
Phase 3: Polish & Edge Cases (5-7 days)
Goals: Robust decoding and logging. Checkpoint: Stable decoding for 5 minutes.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Carrier recovery | Squaring vs Costas | Costas | Better under noise | | Timing recovery | Scan vs early-late | Scan (first) | Simplicity |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples | |———|———|———-| | Unit | PLL and differential decode | Synthetic bits | | Integration | End-to-end RDS | Known IQ capture | | Edge | No RDS station | Expect no output |
6.2 Critical Test Cases
- Known RDS file decodes PS text.
- Inverted signal still decodes after differential.
- Pilot missing should trigger warning.
6.3 Test Data
Use public RDS sample IQ files with known PS/RT.
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution | |———|———|———-| | Wide BPF | Noisy bits | Narrow to 2-4 kHz | | No differential decode | Garbled text | Apply XOR decoding | | PLL too wide | Jitter | Reduce loop bandwidth |
7.2 Debugging Strategies
- Plot constellation after carrier recovery.
- Print syndrome errors for block alignment.
7.3 Performance Traps
- Overly long filters at high sample rates.
8. Extensions & Challenges
8.1 Beginner Extensions
- Display group type statistics.
8.2 Intermediate Extensions
- Decode clock time (CT) groups.
8.3 Advanced Extensions
- Build a live RDS dashboard with history.
9. Real-World Connections
9.1 Industry Applications
- Broadcast metadata monitoring.
- Automotive radio systems.
9.2 Related Open Source Projects
- redsea: RDS decoder library.
- Gqrx: FM receiver with RDS support.
9.3 Interview Relevance
- Shows PLL, BPSK, and framing knowledge.
10. Resources
10.1 Essential Reading
- ETSI EN 50067 (RDS specification)
- Lyons, “Understanding DSP” Ch. 13
10.2 Video Resources
- RDS decoding tutorials (GNU Radio videos)
10.3 Tools & Documentation
- GNURadio RDS blocks for reference.
10.4 Related Projects in This Series
11. Self-Assessment Checklist
11.1 Understanding
- I can explain why RDS uses 57 kHz.
- I can implement differential decoding.
11.2 Implementation
- PS text is stable for at least 5 minutes.
- RT text decodes correctly.
11.3 Growth
- I can decode additional RDS group types.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Decode PS text from at least one station.
Full Completion:
- Decode PS and RT with stability.
Excellence (Going Above & Beyond):
- Full group decoding and live dashboard.