-
-
Save JonathanThorpe/f23480c781ca62a28647 to your computer and use it in GitHub Desktop.
#!/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 <[email protected]> | |
#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() |
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.
@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.
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?
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