Skip to content

Commit 47204d9

Browse files
committed
Start of Si4713 support work
1 parent a5bf041 commit 47204d9

File tree

3 files changed

+298
-1
lines changed

3 files changed

+298
-1
lines changed

Si4713.py

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import logging
2+
import sys
3+
import os
4+
from time import sleep
5+
from datetime import datetime
6+
7+
from config import config
8+
from basicI2C import basicI2C
9+
from Transmitter import Transmitter
10+
11+
class Si4713(Transmitter):
12+
def __init__(self):
13+
logging.info('Initializing Si4713 transmitter')
14+
super().__init__()
15+
self.I2C = basicI2C(0x63) # Si4713 default I2C address
16+
self.PS = self.PSBuffer(self, ' ', int(config['DynRDSPSUpdateRate']))
17+
self.RT = self.RTBuffer(self, ' ', int(config['DynRDSRTUpdateRate']))
18+
19+
# Si4713 Commands
20+
CMD_POWER_UP = 0x01
21+
CMD_GET_REV = 0x10
22+
CMD_POWER_DOWN = 0x11
23+
CMD_SET_PROPERTY = 0x12
24+
CMD_GET_PROPERTY = 0x13
25+
CMD_TX_TUNE_FREQ = 0x30
26+
CMD_TX_TUNE_POWER = 0x31
27+
CMD_TX_TUNE_MEASURE = 0x32
28+
CMD_TX_TUNE_STATUS = 0x33
29+
CMD_TX_ASQ_STATUS = 0x34
30+
CMD_TX_RDS_BUFF = 0x35
31+
CMD_TX_RDS_PS = 0x36
32+
CMD_GET_INT_STATUS = 0x14
33+
34+
# Si4713 Properties
35+
PROP_TX_COMPONENT_ENABLE = 0x2100
36+
PROP_TX_AUDIO_DEVIATION = 0x2101
37+
PROP_TX_PILOT_DEVIATION = 0x2102
38+
PROP_TX_RDS_DEVIATION = 0x2103
39+
PROP_TX_PREEMPHASIS = 0x2106
40+
PROP_TX_RDS_PI = 0x2C01
41+
PROP_TX_RDS_PS_MIX = 0x2C02
42+
PROP_TX_RDS_PS_MISC = 0x2C03
43+
PROP_TX_RDS_PS_REPEAT_COUNT = 0x2C04
44+
PROP_REFCLK_FREQ = 0x0201
45+
46+
# Status bits
47+
STATUS_CTS = 0x80
48+
49+
def _wait_for_cts(self, timeout=100):
50+
"""Wait for Clear To Send status"""
51+
start_time = datetime.now()
52+
while (datetime.now() - start_time).total_seconds() * 1000 < timeout:
53+
status = self.I2C.read(0x00, 1)[0]
54+
if status & self.STATUS_CTS:
55+
return True
56+
sleep(0.001)
57+
return False
58+
59+
def _send_command(self, cmd, args=None):
60+
"""Send a command to the Si4713"""
61+
if args is None:
62+
args = []
63+
64+
# Write command and arguments
65+
self.I2C.write(cmd, args, False)
66+
67+
# Wait for CTS
68+
return self._wait_for_cts()
69+
70+
def _set_property(self, prop, value):
71+
"""Set a property on the Si4713"""
72+
args = [
73+
0x00, # Reserved
74+
(prop >> 8) & 0xFF, # Property high byte
75+
prop & 0xFF, # Property low byte
76+
(value >> 8) & 0xFF, # Value high byte
77+
value & 0xFF # Value low byte
78+
]
79+
return self._send_command(self.CMD_SET_PROPERTY, args)
80+
81+
def startup(self):
82+
logging.info('Starting Si4713 transmitter')
83+
84+
# Power up in transmit mode
85+
args = [
86+
0x12, # CTS interrupt disabled, GPO2 output enabled, transmit mode
87+
0x50 # Analog input mode
88+
]
89+
if not self._send_command(self.CMD_POWER_UP, args):
90+
logging.error('Failed to power up Si4713')
91+
sys.exit(-1)
92+
93+
sleep(0.11) # Wait for power up
94+
95+
# Verify chip by getting revision
96+
self._send_command(self.CMD_GET_REV, [])
97+
rev_data = self.I2C.read(0x00, 9)
98+
if not (rev_data[0] & self.STATUS_CTS):
99+
logging.error('Failed to read Si4713 revision. Is this a Si4713 chip?')
100+
sys.exit(-1)
101+
102+
logging.info('Si4713 Part Number: %02x, Firmware: %d.%d, Component: %d.%d',
103+
rev_data[1], rev_data[2], rev_data[3], rev_data[6], rev_data[7])
104+
105+
# Set reference clock (32.768 kHz crystal)
106+
self._set_property(self.PROP_REFCLK_FREQ, 32768)
107+
108+
# Enable stereo, pilot, and RDS
109+
self._set_property(self.PROP_TX_COMPONENT_ENABLE, 0x0007)
110+
111+
# Set audio deviation (68.25 kHz)
112+
self._set_property(self.PROP_TX_AUDIO_DEVIATION, 6825)
113+
114+
# Set pilot deviation (6.75 kHz)
115+
self._set_property(self.PROP_TX_PILOT_DEVIATION, 675)
116+
117+
# Set RDS deviation (2 kHz)
118+
self._set_property(self.PROP_TX_RDS_DEVIATION, 200)
119+
120+
# Set pre-emphasis
121+
if config['DynRDSPreemphasis'] == "50us":
122+
self._set_property(self.PROP_TX_PREEMPHASIS, 1) # 50 us
123+
else:
124+
self._set_property(self.PROP_TX_PREEMPHASIS, 0) # 75 us
125+
126+
# Configure RDS
127+
self._set_property(self.PROP_TX_RDS_PS_MIX, 0x03) # Mix mode
128+
self._set_property(self.PROP_TX_RDS_PS_MISC, 0x1808) # Standard settings
129+
self._set_property(self.PROP_TX_RDS_PS_REPEAT_COUNT, 3) # Repeat 3 times
130+
131+
# Set frequency from config
132+
tempFreq = int(float(config['DynRDSFrequency']) * 100) # Convert to 10 kHz units
133+
args = [
134+
0x00, # Reserved
135+
(tempFreq >> 8) & 0xFF, # Frequency high byte
136+
tempFreq & 0xFF # Frequency low byte
137+
]
138+
self._send_command(self.CMD_TX_TUNE_FREQ, args)
139+
sleep(0.25) # Wait for tune
140+
141+
# Set transmission power
142+
power = 115 # Max power ~1W
143+
if 'DynRDSSi4713ChipPower' in config:
144+
power = int(config['DynRDSSi4713ChipPower'])
145+
146+
args = [
147+
0x00, # Reserved
148+
power & 0xFF,
149+
0x00 # Antenna cap (0 = auto)
150+
]
151+
self._send_command(self.CMD_TX_TUNE_POWER, args)
152+
sleep(0.25)
153+
154+
self.update()
155+
super().startup()
156+
157+
def update(self):
158+
# Si4713 doesn't have AGC or soft clipping settings like QN8066
159+
# Most audio settings are configured via properties during startup
160+
pass
161+
162+
def shutdown(self):
163+
logging.info('Stopping Si4713 transmitter')
164+
# Power down the transmitter
165+
self._send_command(self.CMD_POWER_DOWN, [])
166+
super().shutdown()
167+
168+
def reset(self, resetdelay=1):
169+
# Used to restart the transmitter
170+
self.shutdown()
171+
del self.I2C
172+
self.I2C = basicI2C(0x63)
173+
sleep(resetdelay)
174+
self.startup()
175+
176+
def status(self):
177+
# Get transmitter status
178+
self._send_command(self.CMD_TX_TUNE_STATUS, [0x01]) # Clear interrupt
179+
status_data = self.I2C.read(0x00, 8)
180+
181+
if status_data[0] & self.STATUS_CTS:
182+
freq = (status_data[2] << 8) | status_data[3]
183+
power = status_data[5]
184+
antenna_cap = status_data[6]
185+
noise = status_data[7]
186+
187+
logging.info('Status - Freq: %.1f MHz - Power: %d - Antenna Cap: %d - Noise: %d',
188+
freq / 100.0, power, antenna_cap, noise)
189+
190+
super().status()
191+
192+
def updateRDSData(self, PSdata='', RTdata=''):
193+
logging.debug('Si4713 updateRDSData')
194+
super().updateRDSData(PSdata, RTdata)
195+
self.PS.updateData(PSdata)
196+
self.RT.updateData(RTdata)
197+
198+
def sendNextRDSGroup(self):
199+
# If more advanced mixing of RDS groups is needed, this is where it would occur
200+
logging.excessive('Si4713 sendNextRDSGroup')
201+
self.PS.sendNextGroup()
202+
self.RT.sendNextGroup()
203+
204+
def transmitRDS(self, rdsBytes):
205+
"""
206+
Transmit RDS group using Si4713's TX_RDS_BUFF command
207+
rdsBytes: 8-byte array containing the RDS group
208+
"""
209+
logging.excessive('Transmit %s', ' '.join('0x{:02x}'.format(a) for a in rdsBytes))
210+
211+
# Si4713 uses CMD_TX_RDS_BUFF to load RDS data
212+
# Command format: CMD, status, FIFO count, RDS data (8 bytes)
213+
args = [0x00] # Clear interrupt
214+
args.extend(rdsBytes)
215+
216+
success = self._send_command(self.CMD_TX_RDS_BUFF, args)
217+
218+
if not success:
219+
logging.error('Failed to transmit RDS group')
220+
# RDS has failed to update, reset the Si4713
221+
self.reset()
222+
return
223+
224+
# RDS specifications indicate 87.6ms to send a group
225+
sleep(0.087)
226+
227+
class PSBuffer(Transmitter.RDSBuffer):
228+
# Sends RDS type 0B groups - Program Service
229+
# Fragment size of 8, Groups send 2 characters at a time
230+
def __init__(self, outer, data, delay=4):
231+
super().__init__(data, 8, 2, delay)
232+
# Include outer for the common transmitRDS function that both PSBuffer and RTBuffer use
233+
self.outer = outer
234+
235+
def updateData(self, data):
236+
super().updateData(data)
237+
# Adjust last fragment to make all 8 characters long
238+
self.fragments[-1] = self.fragments[-1].ljust(self.frag_size)
239+
logging.info('PS %s', self.fragments)
240+
241+
def sendNextGroup(self):
242+
if self.currentGroup == 0 and (datetime.now() - self.lastFragmentTime).total_seconds() >= self.delay:
243+
self.currentFragment = (self.currentFragment + 1) % len(self.fragments)
244+
self.lastFragmentTime = datetime.now()
245+
logging.debug('Send PS Fragment \'%s\'', self.fragments[self.currentFragment])
246+
247+
rdsBytes = [self.pi_byte1, self.pi_byte2, 0b10<<2 | self.pty>>3, (0b00111 & self.pty)<<5 | self.currentGroup, self.pi_byte1, self.pi_byte2]
248+
rdsBytes.append(ord(self.fragments[self.currentFragment][self.currentGroup * self.group_size]))
249+
rdsBytes.append(ord(self.fragments[self.currentFragment][self.currentGroup * self.group_size + 1]))
250+
251+
self.outer.transmitRDS(rdsBytes)
252+
self.currentGroup = (self.currentGroup + 1) % (self.frag_size // self.group_size)
253+
254+
class RTBuffer(Transmitter.RDSBuffer):
255+
# Sends RDS type 2A groups - RadioText
256+
# Max fragment size of 64, Groups send 4 characters at a time
257+
def __init__(self, outer, data, delay=7):
258+
self.ab = 0
259+
super().__init__(data, int(config['DynRDSRTSize']), 4, delay)
260+
self.outer = outer
261+
262+
def updateData(self, data):
263+
super().updateData(data)
264+
# Add 0x0d to end of last fragment to indicate RT is done
265+
# TODO: This isn't quite correct - Should put 0x0d where a break is indicated in the rdsStyleText
266+
if len(self.fragments[-1]) < self.frag_size:
267+
self.fragments[-1] += chr(0x0d)
268+
self.ab = not self.ab
269+
logging.info('RT %s', self.fragments)
270+
271+
def sendNextGroup(self):
272+
# Will block for ~80-90ms for RDS Group to be sent
273+
# Check time, if it has been long enough AND a full RT fragment has been sent, move to next fragment
274+
# Flip A/B bit, send next group, if last group set full RT sent flag
275+
# Need to make sure full RT group has been sent at least once before moving on
276+
if self.currentGroup == 0 and (datetime.now() - self.lastFragmentTime).total_seconds() >= self.delay:
277+
self.currentFragment = (self.currentFragment + 1) % len(self.fragments)
278+
self.lastFragmentTime = datetime.now()
279+
self.ab = not self.ab
280+
# Change \r (0x0d) to be [0d] for logging so it is visible in case of debugging
281+
logging.debug('Send RT Fragment \'%s\'', self.fragments[self.currentFragment].replace('\r','<0d>'))
282+
283+
# TODO: Seems like this could be improved
284+
rdsBytes = [self.pi_byte1, self.pi_byte2, 0b1000<<2 | self.pty>>3, (0b00111 & self.pty)<<5 | self.ab<<4 | self.currentGroup]
285+
rdsBytes.append(ord(self.fragments[self.currentFragment][self.currentGroup * self.group_size]))
286+
rdsBytes.append(ord(self.fragments[self.currentFragment][self.currentGroup * self.group_size + 1]) if len(self.fragments[self.currentFragment]) - self.currentGroup * self.group_size >= 2 else 0x20)
287+
rdsBytes.append(ord(self.fragments[self.currentFragment][self.currentGroup * self.group_size + 2]) if len(self.fragments[self.currentFragment]) - self.currentGroup * self.group_size >= 3 else 0x20)
288+
rdsBytes.append(ord(self.fragments[self.currentFragment][self.currentGroup * self.group_size + 3]) if len(self.fragments[self.currentFragment]) - self.currentGroup * self.group_size >= 4 else 0x20)
289+
290+
self.outer.transmitRDS(rdsBytes)
291+
self.currentGroup += 1
292+
if self.currentGroup * self.group_size >= len(self.fragments[self.currentFragment]):
293+
self.currentGroup = 0

Transmitter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
# PSBuffer (RDSBuffer)
1919
# RTBuffer (RDSBuffer)
2020

21+
# Si4713 (Transmitter)
22+
# PSBuffer (RDSBuffer)
23+
# RTBuffer (RDSBuffer)
24+
2125
class Transmitter:
2226
def __init__(self):
2327
# Common class init

settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
"options": {
8585
"SELECT TRANSMITTER": "None",
8686
"QN8066": "QN8066",
87-
"Si4713 (planned for future release)": "zzSi4713"
87+
"Si4713": "Si4713"
8888
},
8989
"default": "None",
9090
"children": {

0 commit comments

Comments
 (0)