Skip to content

Instantly share code, notes, and snippets.

@JonathanThorpe
Last active September 13, 2023 03:44
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save JonathanThorpe/f23480c781ca62a28647 to your computer and use it in GitHub Desktop.
Save JonathanThorpe/f23480c781ca62a28647 to your computer and use it in GitHub Desktop.
Si473x i2c/i2s radio for Rasperrby Pi
#!/usr/bin/python3
import quick2wire.i2c as i2c
import time
import RPi.GPIO as GPIO
import alsaaudio
import threading
import logging
import sys
#Work in progress library for Si473x for Raspberry Pi by Jonathan Thorpe <jt@jonthorpe.net>
#SiPiRadio
#
#Wiring:
# RPi Si473x Module
# i2s SDA i2c SDIO
# i2s SCL i2c SCLK
#
# GPIO 15 RESET
# GPIO 4 RCLK
#
# PCM_CLK DCLK
# PCM_FS DFS
# PCM_IN DOUT
#
# GPIO 4 must be set to clock mode at the appropriate rate.
# Can't find a way to do this in Python yet - use the command line:
# gpio -g mode 4 clock
# gpio -g clock 4 34406
#
# Before running this program, you need to load the my_loader.c kernel module from here:
# https://www.raspberrypi.org/forums/viewtopic.php?f=44&t=91237
#
# Before compiling this module, ensure daifmt is set in clock and frame slave mode (Si47xx operates in slave only mode):
# .daifmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS
#
#
# TODO (essentially implement more of the programmer's guide):
# 1. Support AM/SW/LW
# 2. Get Status information
# 3. FM RDS information
# 4. Make sure I'm using ALSA properly - seems a bit hacky
#
# Acknowledgments:
# https://github.com/rickeywang/Si4737_i2c was useful for seeing how to program this device over i2c
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
# ===========================================================================
# Si473x I2C / I2S Class
# ===========================================================================
class SiPiRadio():
#GPIO pin for issuing the device reset
GPIO_RESET=15
#Si473x i2c address
I2C_ADDRESS=0x63
#The i2s Device as found from ALSA (my_loader.ko)
AUDIO_IN_DEV="hw:CARD=sndrpisimplecar,DEV=0"
#Default device to send audio out of
AUDIO_OUT_DEV="default"
#Audio sample rate
AUDIO_SAMPLE_RATE=48000
AUDIO_CHANNELS=2
AUDIO_BITS_SAMPLE=16
pcm_audio_in=None
pcm_audio_out=None
REFCLK_FREQ=34406
REFCLK_PRESCALE=1
#Constants
SI4735_CMD_POWER_UP=0x01
SI4735_CMD_GET_REV=0x10
SI4735_CMD_POWER_DOWN=0x11
SI4735_CMD_SET_PROPERTY=0x12
SI4735_CMD_GET_PROPERTY=0x13
SI4735_CMD_FM_TUNE_FREQ=0x20, 0x00
SI4735_CMD_FM_TUNE_STATUS=0x22
SI4735_CMD_GET_INT_STATUS=0x14
#Define Si4735 Output modes
SI4735_OUT_RDS=0x00 # RDS only
SI4735_OUT_ANALOG=0x05
SI4735_OUT_DIGITAL1=0x0B # DCLK, LOUT/DFS, ROUT/DIO
SI4735_OUT_DIGITAL2=0xB0 # DCLK, DFS, DIO
SI4735_OUT_BOTH=(SI4735_OUT_ANALOG | SI4735_OUT_DIGITAL2)
#Statuses
SI4735_STATUS_CTS=0x80
SI4735_STATUS_ERR=0x40
SI4735_STATUS_STCINT=0x01
#Properties
SI4735_PROP_REFCLK_FREQ=0x00, 0x02, 0x01
SI4735_PROP_REFCLK_PRESCALE=0x00, 0x02, 0x02
SI4735_PROP_DIGITAL_OUTPUT_SAMPLE_RATE=0x00, 0x01, 0x04
SI4735_PROP_DIGITAL_OUTPUT_FORMAT=0x00, 0x01, 0x02
SI4735_PROP_RX_VOLUME=0x40, 0x00
#Flags
SI4735_DIGITAL_I2S=0x01, 0x00
SI4735_FLG_INTACK=0x01
#Modes
SI4735_MODE_LW=0
SI4735_MODE_AM=1
SI4735_MODE_SW=2
SI4735_MODE_FM=3
mode=SI4735_MODE_FM
record_stop = threading.Event()
record_thread = None
def byteHigh(self, val):
return val >> 8
def byteLow(self, val):
return val & 0xFF
def sendCommand(self, cmd, *args):
with i2c.I2CMaster() as bus:
if (isinstance(cmd, int)):
bytesToSend=(cmd,) + args
else:
bytesToSend=cmd + args
logging.debug("Command: " + " ".join('0x%02x' % i for i in bytesToSend))
bus.transaction(i2c.writing_bytes(self.I2C_ADDRESS, *bytesToSend))
def getStatus(self):
with i2c.I2CMaster() as bus:
return bus.transaction(i2c.reading(self.I2C_ADDRESS, 1))[0][0]
def ctsWait(self):
status = 0
while not status & self.SI4735_STATUS_CTS:
status = self.getStatus()
logging.debug("Returned status is: {0:#04x}".format(status))
def intWait(self, interruptType):
status = 0
while not status & interruptType:
self.sendCommand(self.SI4735_CMD_GET_INT_STATUS, 0x00)
time.sleep(0.125)
status = self.getStatus()
if (not status & interruptType):
logging.debug('Still waiting. Got status {0:#04x}'.format(status))
def sendWait(self, cmd, *args):
self.sendCommand(cmd, *args)
self.ctsWait()
def setProperty(self, property, *args):
self.sendWait((self.SI4735_CMD_SET_PROPERTY,)+property+args)
def setupCaptureDevice(self):
logging.debug('Setting up capture device')
self.pcm_audio_in = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NORMAL, self.AUDIO_IN_DEV)
self.pcm_audio_in.setchannels(self.AUDIO_CHANNELS)
self.pcm_audio_in.setrate(self.AUDIO_SAMPLE_RATE)
self.pcm_audio_in.setformat(alsaaudio.PCM_FORMAT_S16_LE)
def setupPlaybackDevice(self):
logging.debug('Setting up playback device')
self.pcm_audio_out = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NONBLOCK)
self.pcm_audio_out.setchannels(self.AUDIO_CHANNELS)
self.pcm_audio_out.setrate(self.AUDIO_SAMPLE_RATE)
self.pcm_audio_out.setformat(alsaaudio.PCM_FORMAT_S16_LE)
logging.debug('Done setting up playback device')
def captureAudio(self, arg, stop_event):
self.pcm_audio_in.setperiodsize(160)
while(not self.record_stop.is_set()):
l, data = self.pcm_audio_in.read()
if l:
#logging.debug("Sending output audio...")
self.pcm_audio_out.write(data)
def powerUp(self):
logging.debug('Powering up si473x')
GPIO.output(self.GPIO_RESET, False)
time.sleep(0.01)
GPIO.output(self.GPIO_RESET, True)
time.sleep(0.01)
self.sendWait(self.SI4735_CMD_POWER_UP, 0x00, self.SI4735_OUT_BOTH)
#Configure REFCLK
self.setProperty(self.SI4735_PROP_REFCLK_FREQ,
self.byteHigh(self.REFCLK_FREQ),
self.byteLow(self.REFCLK_FREQ))
self.setProperty(self.SI4735_PROP_REFCLK_PRESCALE,
self.byteHigh(self.REFCLK_PRESCALE),
self.byteLow(self.REFCLK_PRESCALE))
self.record_thread.start()
self.setProperty(self.SI4735_PROP_DIGITAL_OUTPUT_SAMPLE_RATE,
self.byteHigh(self.AUDIO_SAMPLE_RATE),
self.byteLow(self.AUDIO_SAMPLE_RATE))
self.setProperty(self.SI4735_PROP_DIGITAL_OUTPUT_FORMAT, *self.SI4735_DIGITAL_I2S)
def setFrequency(self, freq):
if(self.mode == self.SI4735_MODE_FM):
logging.debug('Setting frequency to {0} ({1:#04x} {2:#04x})'.format(freq, self.byteHigh(freq), self.byteLow(freq)))
self.sendWait(self.SI4735_CMD_FM_TUNE_FREQ, self.byteHigh(freq), self.byteLow(freq), 0x00, 0x00)
logging.debug('Frequency set, just waiting for tuning to complete')
self.intWait(self.SI4735_STATUS_STCINT)
if(self.mode == self.SI4735_MODE_FM):
self.sendCommand(self.SI4735_CMD_FM_TUNE_STATUS, self.SI4735_FLG_INTACK)
def setVolume(self, vol):
logging.debug('Setting volume to {0:#04x}'.format(vol))
self.setProperty(self.SI4735_PROP_RX_VOLUME, vol)
def init(self):
self.setupCaptureDevice()
self.setupPlaybackDevice()
def __init__(self):
self.record_thread = threading.Thread(target=self.captureAudio, args=(1, self.record_stop))
radio = SiPiRadio()
GPIO.setmode(GPIO.BOARD)
GPIO.setup(radio.GPIO_RESET, GPIO.OUT)
radio.init()
radio.powerUp()
radio.setVolume(0x63)
radio.setFrequency(10570)
logging.debug("Execution Complete")
print("q=quit")
cmd = ""
while True:
cmd = input("Command: ")
if(cmd=="q"):
break
radio.record_stop.set()
GPIO.cleanup()
@punx77
Copy link

punx77 commented Jun 26, 2018

Hello,I found that your library is very usefull! Do you plan to insert rds decoding?I'trying to do that but seems a, little bit tricky

@iz2k
Copy link

iz2k commented Mar 4, 2020

Really nice code. I am using it to interface a Si4735 and works fine with minor changes.

In my case, I was using an external crystal clock to drive REFCLK, and the analog audio output works fine this way. However, in order to use the I2S output it lookslike REFCLK has to be driven digitally, as you are doing with GPIO4.

Regarding the GPIO4 CLK mode configuration, you could use the pigpio daemon to control GPIOs, and then configure them via python with the hardware_clock or hardware_PWM routines: http://abyz.me.uk/rpi/pigpio/python.html#hardware_clock

If you do not want to depend on the pigpio library, you could also just call shell commands from python using the subprocess class:

import subprocess
import shlex

cmd = shlex.split("gpio -g mode 4 clock")	
subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
cmd = shlex.split("gpio -g clock 4 34406")	
subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)

In this case of course, you will depend on having the gpio tool installed on the machine. You could also try to include this binary within your python files.

@JonathanThorpe
Copy link
Author

@iz2k, glad you find the code useful - unfortunately, I stopped maintaining it as the Si4735 have become hard to source after Silicon Labs discontinued this particular IC. May I ask what sort of project you're using this for?

Thanks for the hints regarding REFCLK.

@iz2k
Copy link

iz2k commented Mar 4, 2020

I am working on my flip-clock project: https://github.com/iz2k/flip-clock

Amongst others, I have used a USB RTL-SDR dongle to tune FM radio, and I was trying to build a new hat for the Pi including two MAX98357A I2S audio DAC (Left + Right) for the audio output, and connect the output of the SI4735 to the I2S input of the Pi. I am testing this with evaluation boards, and for now I can pipe the tuned radio incoming from I2S input to analog output of the Pi. I am confident I will get it piped to the I2S output, and thus to the 3W audio DACs.

I have noticed that the SI4735 is not easy to find, but I already have the unit in my Eval Kit which should be enough for my device. But just in case, do you happen to know a similar alternative that is not discontinued?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment