If you want to pull live station metadata (song titles, traffic flags, and station IDs) out of the FM broadcast band, the Raspberry Pi FM radio RDS project using the Silicon Labs Si4703 chip is the benchmark build. Unlike basic analog tuners, the Si4703 natively decodes the 57 kHz RDS subcarrier and exposes the parsed data blocks over I2C. This guide targets the Raspberry Pi 4 Model B (4GB) running Raspberry Pi OS Bookworm (64-bit), utilizing the Pi's hardware I2C0 bus and a Python smbus2 control script.
Hardware BOM and I2C Pin Mapping
The Si4703 operates at 3.3V logic, which perfectly matches the Raspberry Pi's GPIO levels. Do not use 5V-tolerant breakouts without a level shifter, or you will back-feed 5V into the Pi's SDA/SCL pins and risk damaging the BCM2711 SoC.
- Microcontroller: Raspberry Pi 4 Model B (4GB) — ~$55 USD
- FM Tuner: SparkFun Si4703 FM Tuner Evaluation Board (SEN-10302) or generic equivalent — ~$15-$25 USD
- Audio Out: 3.5mm TRS cable to passive amplified speakers (The Si4703 has a built-in headphone amplifier; do not route audio through the Pi's PWM DAC).
- Antenna: 75cm stranded copper wire soldered to the breakout's FM antenna pad (or use the headphone cable shield if using 3.5mm headphones).
Si4703 to Raspberry Pi 4 Pinout
The Si4703 requires two extra GPIO pins beyond standard I2C to handle the hardware reset and I2C mode selection (SEN).
| Si4703 Breakout Pin | Raspberry Pi 4 Pin (40-Pin Header) | BCM GPIO Number | Function / Notes |
|---|---|---|---|
| VCC | Pin 1 | N/A (3.3V Power) | Must be 3.3V. Do not connect to 5V. |
| GND | Pin 6 | N/A (Ground) | Common ground reference. |
| SDIO | Pin 3 | GPIO 2 (SDA1) | I2C Data line. |
| SCLK | Pin 5 | GPIO 3 (SCL1) | I2C Clock line. |
| RST | Pin 11 | GPIO 17 | Active low hardware reset. |
| SEN | Pin 13 | GPIO 27 | Serial Enable. HIGH = I2C mode (Addr 0x10). |
Si4703 Register Map and RDS Data Structure
Before writing code, you must understand how the Si4703 handles memory. It does not use standard single-byte I2C register reads. Instead, it uses a shadow register architecture. Reading pulls 32 bytes (16 registers) starting from 0x0A and wrapping around to 0x09. Writing pushes 12 bytes (6 registers) starting from 0x02 to 0x07.
Here are the critical registers for tuning and RDS extraction:
| Register Address | Name | Key Bits for RDS / Tuning | Target Value / Action |
|---|---|---|---|
| 0x02 | POWERCFG | Bit 14: DMUTE, Bit 11: RDSM, Bit 0: ENABLE | Set DMUTE=1, RDSM=1 (Standard RDS mode), ENABLE=1 |
| 0x03 | CHANNEL | Bits 9:0: CHAN, Bit 15: TUNE | Write CHAN value, set TUNE=1, wait 60ms, clear TUNE. |
| 0x04 | SYSCONFIG1 | Bit 12: RDSIEN, Bit 11: RDS, Bit 1: DE | Set RDS=1 to enable RDS processing. DE=1 for 50µs de-emphasis (EU) or 0 for 75µs (US). |
| 0x0A | STATUSRSSI | Bit 15: RDSR, Bit 14: STC, Bit 8:0: RSSI | Poll RDSR (RDS Ready). When 1, read Blocks A-D. |
| 0x0B - 0x0E | RDSA - RDSD | 16-bit RDS Data Blocks A, B, C, D | Block A = PI Code. Block B = Group Type / PTY / TP. |
Python I2C Control and RDS Parsing Code
The following script uses the smbus2 library to handle the shadow register reads and writes. It initializes the chip, tunes to a local station (97.1 MHz in this example), and parses the RDS Program Identification (PI) code and basic Program Service (PS) name from Group 0A blocks.
Note: Install dependencies via sudo apt install python3-smbus2 or pip install smbus2. Ensure I2C is enabled in raspi-config.
import time
import smbus2
from smbus2 import SMBus
# --- Pin & I2C Configuration ---
I2C_BUS = 1
SI4703_ADDR = 0x10 # 0x10 when SEN is HIGH
RST_PIN = 17 # BCM 17
SEN_PIN = 27 # BCM 27
class Si4703:
def __init__(self, bus_num=I2C_BUS, addr=SI4703_ADDR):
self.bus = SMBus(bus_num)
self.addr = addr
self.regs = [0] * 16 # Shadow register array (0x00 to 0x0F)
def hardware_reset(self):
"""Executes the strict Si4703 I2C mode reset sequence."""
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(RST_PIN, GPIO.OUT)
GPIO.setup(SEN_PIN, GPIO.OUT)
GPIO.output(SEN_PIN, GPIO.HIGH) # SEN High selects I2C mode
GPIO.output(RST_PIN, GPIO.LOW)
time.sleep(0.001) # Min 1ms low pulse
GPIO.output(RST_PIN, GPIO.HIGH)
time.sleep(0.110) # Wait 110ms for internal boot
def read_registers(self):
"""Reads 32 bytes starting from 0x0A, wrapping to 0x09."""
try:
raw = self.bus.read_i2c_block_data(self.addr, 0x0A, 32)
# Reassemble 16-bit registers from 8-bit I2C bytes
for i in range(16):
self.regs[(0x0A + i) % 16] = (raw[i*2] << 8) | raw[i*2 + 1]
except OSError as e:
print(f"I2C Read Failed: {e}")
raise
def write_registers(self):
"""Writes 12 bytes starting from 0x02 to 0x07."""
raw = []
for i in range(2, 8):
raw.append((self.regs[i] >> 8) & 0xFF)
raw.append(self.regs[i] & 0xFF)
try:
self.bus.write_i2c_block_data(self.addr, 0x02, raw)
except OSError as e:
print(f"I2C Write Failed: {e}")
raise
def tune(self, freq_mhz):
"""Tunes to a specific FM frequency (e.g., 97.1)."""
self.read_registers()
# Formula: Channel = (Freq - 87.5) / 0.2 (assuming 200kHz spacing)
channel = int((freq_mhz - 87.5) / 0.2)
self.regs[0x03] = channel | 0x8000 # Set TUNE bit (Bit 15)
self.regs[0x02] |= 0x4000 # Ensure DMUTE is clear during tune
self.write_registers()
time.sleep(0.060)
self.read_registers()
self.regs[0x03] &= ~0x8000 # Clear TUNE bit
self.write_registers()
time.sleep(0.060)
def enable_rds(self):
"""Enables RDS processing and standard mode."""
self.read_registers()
self.regs[0x02] |= 0x4000 # DMUTE = 1 (Unmute audio)
self.regs[0x02] |= 0x0800 # RDSM = 1 (Standard RDS mode)
self.regs[0x02] |= 0x0001 # ENABLE = 1
self.regs[0x04] |= 0x1000 # RDS = 1 (Enable RDS)
self.write_registers()
def parse_rds(self):
"""Checks RDS Ready bit and extracts PI code."""
self.read_registers()
status = self.regs[0x0A]
if status & 0x8000: # RDSR (Bit 15) is 1
pi_code = self.regs[0x0B] # Block A is always the PI code
block_b = self.regs[0x0C]
group_type = (block_b >> 12) & 0x0F
print(f"RDS Synced | PI Code: {pi_code:04X} | Group: {group_type}")
return pi_code
return None
if __name__ == "__main__":
radio = Si4703()
try:
radio.hardware_reset()
radio.enable_rds()
radio.tune(97.1) # Replace with a strong local station
print("Tuned. Polling for RDS blocks...")
while True:
radio.parse_rds()
time.sleep(0.05) # RDS groups arrive roughly every 40-80ms
except KeyboardInterrupt:
print("\nStopping radio.")
except Exception as e:
print(f"Fatal error: {e}")
Debugging I2C and RDS Sync Failures
When bringing up I2C peripherals on the Pi, the physical layer is almost always where builds stall. If your script crashes on the first read_registers() call, here is the exact diagnostic path.
The First Three Things to Check
- SEN Pin State: The Si4703 defaults to a 3-wire SPI-like mode on boot. The SEN pin must be pulled HIGH before the reset pulse. If SEN is floating or LOW, the chip will ignore I2C traffic entirely.
- I2C Address Mismatch: Run
i2cdetect -y 1. If SEN is HIGH, the address is0x10. If SEN is LOW, it shifts to0x11. If you see neither, check your 3.3V and GND connections. - Reset Timing Violations: The Si4703 datasheet mandates a minimum 110ms delay after releasing the RST pin before the first I2C transaction. Python's
time.sleep(0.110)is sufficient, but if you are running this on a heavily loaded Pi 5, OS scheduling jitter might cut it short. Bump it to0.150if you get intermittent boot failures.
Exact Error Strings and Ranked Causes
Error 1: OSError: [Errno 121] Remote I/O error
This is an I2C NACK (Not Acknowledged). The Pi sent the address, but no device pulled SDA low to acknowledge.
- Cause A (Most Likely): SEN pin is LOW or floating, putting the chip in SPI mode. Fix: Wire SEN to 3.3V or set GPIO 27 HIGH.
- Cause B: You are using a 5V breakout board without a level shifter, and the Pi's 3.3V SDA HIGH signal isn't crossing the 5V logic threshold. Fix: Add a bi-directional logic level shifter.
Error 2: OSError: [Errno 110] Connection timed out
This indicates I2C clock stretching failure. The Si4703 is holding SCL low, but the Pi's I2C controller times out waiting for it to release.
- Cause A: The Pi's I2C baudrate is too fast for the Si4703's internal state machine during the shadow register read. Fix: Edit
/boot/firmware/config.txtand adddtparam=i2c_arm_baudrate=50000to drop the bus speed to 50kHz, then reboot. - Cause B: Missing pull-up resistors on SDA/SCL. The SparkFun breakout includes 4.7kΩ pull-ups, but cheap generic clones often omit them. Fix: Add 4.7kΩ resistors from SDA and SCL to 3.3V.
Extending and Simplifying the Build
Once you have basic PI code extraction working, you have a decision to make regarding project scope.
How to Simplify
If the shadow-register I2C logic is causing too many headaches, or you just want audio without metadata, swap the Si4703 for the RDA5807M. The RDA5807M uses standard, linear I2C register reads (no shadow wrapping) and is widely available on sub-$2 AliExpress modules. You will lose native hardware RDS parsing, but for a basic digital-tuner internet-radio bridge, it cuts the software complexity in half.
How to Extend
To turn this into a standalone kitchen radio, you need to solve two problems: user input and audio noise.
- Add a Rotary Encoder: Wire a KY-040 rotary encoder to GPIO 5 and GPIO 6. Use the
gpiozeroRotaryEncoderclass to catch rotation events and increment/decrement thechannelvariable in the tuning formula. Add a push-button on GPIO 12 to trigger an RDS seek sequence (setting the SEEK bit in 0x02). - Upgrade the Audio Path: The Si4703's analog out is clean, but if you are mixing it with Pi-generated audio (like local MP3 playback), the Pi's PWM audio out will introduce severe switching noise. Bypass the Pi's analog jack entirely by adding an Adafruit MAX98357A I2S DAC (~$6). Route the Pi's I2S PCM pins to the DAC, and use
alsato mix the streams digitally before they hit the amplifier.
For deeper hardware integration details, refer to the SparkFun Si4703 Hookup Guide and the official Raspberry Pi I2C Configuration Documentation. Always verify your local FM band spacing (100kHz in Europe/Japan vs 200kHz in the US) before finalizing your channel calculation math.






