はじめに
これまで、MacBookAirにSDR(Software Definition Radio)用のUSBデバイスを接続してラジオ放送やアマチュア無線の受信を楽しんでいたのだが(年齢がバレる?)、昨年OSをバージョンアップしたら、MACがSDRデバイスを認識しなくなってしまった。認識できる方法を物色するという道もあったのかもしれないが、以前から気になっていたDSPラジオ用LSIを使ってDSPラジオを自作し、同じような環境を構築することにした。
DSPラジオ用LSIについて
ラジオを自作するのに便利なデバイスは色々なタイプのものが売られている。ラジオ放送はメディアとしては先行きが微妙と言われている割にはまだデバイスが健在なのが個人的には嬉しい。今回注目しているDSPラジオ向けのLSIは数100円程度で売られているが、その内部はSDRモジュールと同じ構成(周波数変換+IQ復調部)であり、加えて音声復調用プロセッサ(おそらくDSP)が組み込まれており、PCなしで放送受信からアナログ音声出力まで実現している。Amazon等で中華製の短波ラジオが数千円〜数万円位で色々と発売されているが、これらはこのタイプのLSIが使われていることが多い。DSPラジオLSIの大きな特徴の一つは、アナログICと異なりアナログ的な調整が必要な高周波用のコイルやバリコンのような部品を一切使わずに長波から短波・超短波までの周波数の電波の受信を実現している点にある。その一方でこのLSIを動作させるには、I2C等のI/Fでマイコンと接続してレジスタ制御を行う必要があり、ハンダ付け工作だけではラジオとして完成させることができない。
すでにGitHubにはArduinoとDSPラジオLSIをI2Cで接続して動かすプロジェクトがあり、そこからsketchファイルを落としてくればすぐ使うことができるのだが、自分の手元には使えるArduinoがない。一方で、手元に遊んでいるraspberrypi-picoがあること、LSIのアプリケーションノートが入手可能であること、LSI制御はI2C+GPIOのみで対応可能であること、という条件を鑑み、今回は、アプリケーションノートを見ながらmicropythonを使ってフルスクラッチで制御コードを作成し、DSPラジオを動かしてみることにした。
Arduinoを使って動かしている事例は多く見かけたので、あえてこの流れには乗らず(もちろん参考にはさせていただいた)、かつmicropythonの実力確認も兼ねてやってみようと思った、というモチベーションもあった。それからフルスクラッチで組むことで、後で何か変更しようと思った場合にはやりやすい、ということもある。
ハードウェア構成について
AITENDOでSI4732-A10というDSP-LSIが基板実装済み状態で販売されているのでこれを利用した。このLSI基板には、LSI動作に必要な水晶発振器、I2Cバスのプルアップ抵抗、電源のパスコン、までは実装されているため、オーディオ出力用のカップリングキャパシタ、リセット制御用のプルダウン抵抗、アンテナ接続ピンに必要なセラミックキャパシタ、等をLSI基板と一緒にブレッドボード上に実装すれば、最低限のデバイス動作環境は組み上がる。このLSI基板、必要な周辺部品、raspberrypi-pico、操作用のプッシュボタン・ロータリーエンコーダ、情報表示用のOLEDモジュールをブレッドボード上に実装して今回のターゲットとした。参考回路図とターゲットの外観を以下に示す。
デバイスの初期設定・動作設定はI2Cで行うので配線するのは当然であるが、リセットピンも起動時にpicoから制御する必要があるため、この制御のための配線が必須である。電源投入時に所望のタイミングでハードリセットを行わないとI2C通信が機能しない。それから、操作用のロータリーエンコーダとプッシュスイッチにはチャタリング対策のCRフィルタを設けるべきである。お試しで実験する場合は設けなくても構わないが、通常使用時に素早く操作した場合に、思いもよらない動作をして戸惑うことがある(例えば、右に回しているのにカウンタが逆に戻る、等の異常動作が発生する、等)。
ソフトウェアについて
以下のような実装手順で作業を進めた。開発作業は、VSCodeにRaspberrypy-Pico開発用プラグインを入れて行っている。
- リセット制御とI2Cでスレーブアドレスを確認できるところまで実装
- LSIのスレーブアドレスが確認できればOK
- アプリケーションノートのフローチャートに従い、最低限の設定を行うためのコードを実装
- AM放送受信の最小設定を決め打ちで送り、音が出ればOK
- 操作系と機能の対応付けを行う
- 受信周波数と音量はロータリーエンコーダで可変する
- FM/AMはプッシュスイッチで切り替える
- …等の操作仕様と制御レジスタの対応を決める
- ドライバコードの追加
- ロータリーエンコーダ
- プッシュスイッチ
- SI4732制御用クラスの定義
- 機能を極力関数で呼び出せるようにする
- OLEDモジュールへの情報表示を追加
- ssd1306ドライバを追加し、デバイス制御とは異なるI2Cポートに接続して動作させる
- バスの競合回避だけでなく、OLED制御時に発生するノイズがSI4732に飛び込むことを避けるために実施
- VSCodeのコンソールがなくても操作できるように操作状態を表示する(使いながら調整)
- 表示情報は制御系側が生成しグローバル変数で共有することで受け渡す
- ssd1306ドライバを追加し、デバイス制御とは異なるI2Cポートに接続して動作させる
- スレッド動作の導入
- 操作系・表示系の反応が鈍い印象だったため、OLED表示処理とそれ以外の処理を分離してスレッド動作するように変更->反応の鈍さが改善
参考コードを以下に示す。暇な時間を見計らって実装していたため、足掛け2ヶ月位かかっている。
import machine # type: ignore
import ssd1306
import utime # type: ignore
import math
import _thread
from machine import Pin, UART, PWM, I2C, ADC, Timer # type: ignore
# SI4732クラス定義
class si4732( ) :
def __init__( self, i2c ) :
self.waitSec = 0.04 # [sec]
self.slavAdr = 0x11 # SI4732 I2C slave address
self.setprpt = 0x12 # set property command
self.AmSwMin = 2300 # MHz
self.AmSwMax = 23000 # MHz
self.FRQ_JOQR = 1134 # kHz
self.FRQ_JOGU = 7800 # x10kHz
self.RSSI = 0
self.SNR = 0
self.frq = 1134
self.frq_fm = 7800
self.i2c = i2c
return
def gen2bytes( self, val ) :
val &= 0xFFFF
up = (val >> 8) & 0xFF
lo = (val ) & 0xFF
return up, lo
def registerRead( self, size ) :
utime.sleep( self.waitSec )
rtn = self.i2c.readfrom( self.slavAdr, size )
return rtn
def registerWrite( self, reg, key = 0x80, wait_scale = 1.0 ) :
com = bytes( reg )
utime.sleep( self.waitSec )
self.i2c.writeto( self.slavAdr, com )
return
def ResetDevice( self, resetPin ) :
print('[SI4732] reset')
resetPin.value(1)
utime.sleep(2.0)
resetPin.value(0)
utime.sleep(0.1)
resetPin.value(1)
return
def PowerUpFmAnalog( self ) :
print('[SI4732] PowerUP FM')
com = [ 0x01, 0xD0, 0x05 ]
self.registerWrite( com )
utime.sleep(1)
return
def PowerUpAmAnalog( self ) :
print('[SI4732] PowerUP AM')
com = [ 0x01, 0xD1, 0x05 ]
self.registerWrite( com )
return
def PowerDown( self ) :
print('[SI4732] Power Down')
com = [ 0x11 ]
self.registerWrite( com )
utime.sleep( self.waitSec )
return
def GetRevision( self ) :
print('[SI4732] Get Revision')
com = [ 0x10 ]
self.registerWrite( com, key = 0x80, wait_scale = 1.0 )
rtn = self.registerRead( 9 )
while True :
utime.sleep( self.waitSec * 2 )
rtn = self.i2c.readfrom( self.slavAdr, 9 )
if rtn[0] == 0x80 :
print( 'OK' )
break
return rtn
def SetGpoIen( self ) :
com = [ self.setprpt, 0x00, 0x00, 0x01, 0x00, 0xC9 ]
self.registerWrite( com )
return
def SetRxVolume( self, vol = 0x3F ) :
com = [ self.setprpt, 0x00, 0x40, 0x00, 0x00, (vol & 0x3F) ]
self.registerWrite( com )
return
def SetRxHardMute( self, muteL=0, muteR=0 ) :
mute_mode = 0x3 & ( (muteL & 0x1)<<1 | (muteR & 0x1) )
com = [ self.setprpt, 0x40, 0x01, 0x00, mute_mode ]
self.registerWrite( com )
return
def SetRxHardMuteIn( self ) :
self.SetRxHardMute(muteL=1, muteR=1 )
return
def SetRxHardMuteOut( self ) :
self.SetRxHardMute(muteL=0, muteR=0 )
return
def SetFmDeEmphasis( self, flag = True ) :
nFlag = 0x00
if flag == True : nFlag = 0x01
com = [ self.setprpt, 0x00, 0x11, 0x00, 0x00, nFlag ]
self.registerWrite( com )
return
def SetFmBlendRssiStereoThreshold( self, val = 0x31 ) :
com = [ self.setprpt, 0x00, 0x18, 0x00, 0x00, val ]
self.registerWrite( com )
return
def SetFmBlendRssiMonoThreshold( self, val = 0x1E ) :
com = [ self.setprpt, 0x00, 0x18, 0x01, 0x00, val ]
self.registerWrite( com )
return
def SetFmMaxTuneError( self, val = 0x28 ) :
com = [ self.setprpt, 0x00, 0x11, 0x08, 0x00, val ]
self.registerWrite( com )
return
def SetFmRsqIntSource( self, val = 0x8F ) :
com = [ self.setprpt, 0x00, 0x12, 0x00, 0x00, val ]
self.registerWrite( com )
return
def SetFmRsqSnrHiThreshold( self, val = 0x1E ) :
com = [ self.setprpt, 0x00, 0x12, 0x01, 0x00, val ]
self.registerWrite( com )
return
def SetFmRsqSnrLoThreshold( self, val = 0x06 ) :
com = [ self.setprpt, 0x00, 0x12, 0x02, 0x00, val ]
self.registerWrite( com )
return
def SetFmRsqRssiHiThreshold( self, val = 0x32 ) :
com = [ self.setprpt, 0x00, 0x12, 0x03, 0x00, val ]
self.registerWrite( com )
return
def SetFmRsqRssiLoThreshold( self, val = 0x18 ) :
com = [ self.setprpt, 0x00, 0x12, 0x04, 0x00, val ]
self.registerWrite( com )
return
def SetFmRsqBlendThreshold( self, val = 0xB2 ) :
# 0xB2 = 0x80 | 0x32
com = [ self.setprpt, 0x00, 0x12, 0x07, 0x00, val ]
self.registerWrite( com )
return
def SetFmSoftMuteMaxAttenuation( self, val = 0x0A ) :
com = [ self.setprpt, 0x00, 0x13, 0x02, 0x00, val ]
self.registerWrite( com )
return
def SetFmSoftMuteSnrThreshold( self, val = 0x06 ) :
com = [ self.setprpt, 0x00, 0x13, 0x03, 0x00, val ]
self.registerWrite( com )
return
def FmTuneFreq( self, frq_fm ) :
self.frq_fm = frq_fm
frqU, frqL = self.gen2bytes( frq_fm )
com = [ 0x20, 0x00, frqU, frqL, 0x00 ]
com = bytes( com )
self.i2c.writeto( self.slavAdr, com )
while True :
utime.sleep( self.waitSec )
rtn = self.i2c.readfrom( self.slavAdr, 1 )
if rtn[0] == 0x80 :
break
return
def GetIntStatus( self ) :
com = [ 0x14 ]
self.registerWrite( com, key = 0x81, wait_scale = 1 )
return
def FmTuneStatus( self ) :
com = [ 0x22, 0x01 ]
self.registerWrite( com )
return self.registerRead( 8 )
def FmRsqStatus( self ) :
com = [ 0x23, 0x01 ]
self.registerWrite( com )
return self.registerRead( 8 )
def FmAgcOverRide( self, sel = 0, gain = 13 ) :
agc_mode = sel & 0x1
lna_gain = gain
if gain < 0 : lna_gain = 0
elif gain > 26 : lna_gain = 26
com = [ 0x28, agc_mode, lna_gain ]
self.registerWrite( com )
return
def SetAmChannelFilter( self, val = 0 ) :
com = [ self.setprpt, 0x00, 0x31, 0x02, 0x01, val ]
self.registerWrite( com )
return
def SetAmDeEmphasis( self, flag ) :
nFlag = 0x00
if flag == True : nFlag = 0x01
com = [ self.setprpt, 0x00, 0x31, 0x00, 0x00, nFlag ]
self.registerWrite( com )
return
def SetAmAvcMaxGain( self, rfgain = 0x1543 ) :
gainval = int( 340.2 * (rfgain + 1) * 9 )
# 下限値
if gainval < 0x1000 : gainval = 0x1000
# 上限値
elif gainval > 0x7800 : gainval = 0x7800
ub, lb = self.gen2bytes( gainval )
com = [ self.setprpt, 0x00, 0x31, 0x03, ub, lb ]
self.registerWrite( com )
return
def SetAmRsqInterrupts( self, val = 0x08 ) :
com = [ self.setprpt, 0x00, 0x32, 0x00, 0x00, val ]
self.registerWrite( com )
return
def SetAmRsqSnrHiThreshold( self, val = 0x0A ) :
com = [ self.setprpt, 0x00, 0x32, 0x01, 0x00, val ]
self.registerWrite( com )
return
def SetAMRsqSnrLoThreshold( self, val = 0x0A ) :
com = [ self.setprpt, 0x00, 0x32, 0x02, 0x00, val ]
self.registerWrite( com )
return
def SetAmRsqRssiHiThreshold( self, val = 0x1E ) :
com = [ self.setprpt, 0x00, 0x32, 0x03, 0x00, val ]
self.registerWrite( com )
return
def SetAmRsqRssiLoThreshold( self, val = 0x0A ) :
com = [ self.setprpt, 0x00, 0x32, 0x04, 0x00, val ]
self.registerWrite( com, 0x80 )
return
def SetAmSoftMuteMaxAttenuation( self, val = 0x0A ) :
com = [ self.setprpt, 0x00, 0x33, 0x02, 0x00, val ]
self.registerWrite( com )
return
def SetAmSoftMuteSnrThreshold( self, val = 0x09 ) :
com = [ self.setprpt, 0x00, 0x33, 0x03, 0x00, val ]
self.registerWrite( com )
return
def AmTuneFreq( self, frq_am ) :
self.frq = frq_am
# SW(2.3MHz-23MHz)はアンテナキャパシタの設定を変更する
antc = 0x00
if ( self.AmSwMin <= self.frq ) and ( self.frq <= self.AmSwMax ) :
antc = 0x01
frqU, frqL = self.gen2bytes( frq_am )
com = [ 0x40, 0x00, frqU, frqL, 0x00, antc ]
self.registerWrite( com )
return
def AmTuneStatus( self ) :
com = [ 0x42, 0x01 ]
self.registerWrite( com )
return self.registerRead( 8 )
def AmRsqStatus( self ) :
com = [ 0x43, 0x01 ]
self.registerWrite( com )
return self.registerRead( 6 )
def AmAgcStatus( self ) :
com = [ 0x47 ]
self.registerWrite( com )
return self.registerRead( 3 )
# AM受信状態確認
def getRxStatus( self ) :
rtn = self.AmRsqStatus( )
buf = []
for val in rtn: buf.append(val)
self.RSSI = buf[4] # RSSI (受信信号強度)
self.SNR = buf[5] # SNR (信号対雑音比)
return self.RSSI, self.SNR
# FM受信状態確認
def getRxStatusFM( self ) :
rtn = self.FmRsqStatus( )
buf = []
for val in rtn: buf.append(val)
self.RSSI = buf[4] # RSSI (受信信号強度)
self.SNR = buf[5] # SNR (信号対雑音比)
return self.RSSI, self.SNR
# 使用ピンの初期化
# SI4732 xRST制御ピン
# 電源投入と同時にxRSTが上がってはならない
rst = Pin( 7, Pin.OUT )
# UI用Push Switch
pb0 = Pin( 8, Pin.IN, Pin.PULL_UP )
# RotaryEncoderのPush(reP),A(reU),B(reD)端子のPin割り当てとpull-up
reP = Pin( 13, Pin.IN, Pin.PULL_UP )
reD = Pin( 14, Pin.IN, Pin.PULL_UP )
reU = Pin( 15, Pin.IN, Pin.PULL_UP )
# RotaryEncoderのPush(reP1),A(reU1),B(reD1)端子のPin割り当てとpull-up
reP1 = Pin( 6, Pin.IN, Pin.PULL_UP )
reD1 = Pin( 4, Pin.IN, Pin.PULL_UP )
reU1 = Pin( 5, Pin.IN, Pin.PULL_UP )
# デバイスドライバの設定
# I2C(SI4732・OLEDディスプレイ制御・等)
SCL_HZ = 1000000 # SCL = 800kHz ノイズとレスポンスの兼ね合いでこの値
i2c = I2C( 1, sda = Pin( 10 ), scl = Pin( 11 ), freq = SCL_HZ )
i2c0 = I2C( 0, sda = Pin( 0 ), scl = Pin( 1 ), freq = SCL_HZ )
# デバイスクラスインスタンス生成
SI4732 = si4732( i2c )
# SSD1306_I2Cインスタンス生成
WIDTH = 128 #pixel
HEIGHT = 64 #pixel
SADDR = 0x3C
display = ssd1306.SSD1306_I2C( width=WIDTH, height=HEIGHT, i2c=i2c0, addr=SADDR )
# ロータリーエンコーダの回転方向判定テーブル
CODE_TABLE = {
0 : +1,
1 : +1,
2 : -1,
3 : -1
}
# 周波数ステップ参照テーブル
FRQ_STEP_TABLE = {
'MW' : {
0 : 1000,
1 : 100,
2 : 10,
3 : 9,
4 : 1,
5 : 9,
6 : 10,
7 : 100,
},
'SW' : {
0 : 1000,
1 : 100,
2 : 10,
3 : 5,
4 : 1,
5 : 5,
6 : 10,
7 : 100,
},
'FM' : {
0 : 1000, # 10M
1 : 500, # 5M
2 : 100, # 1M
3 : 50, # 500k
4 : 10, # 100k
5 : 50,
6 : 100,
7 : 500,
}
}
# 動作モード参照テーブル
MODE_TABLE = {
'AM' : {
0 : 'VOLUME',
1 : 'RFGAIN',
2 : 'BWMODE',
3 : 'AGCSEL',
4 : 'AGCIDX',
5 : 'AGCSEL',
6 : 'BWMODE',
7 : 'RFGAIN',
},
'FM' : {
0 : 'VOLUME',
1 : 'AGCSEL',
2 : 'AGCIDX',
3 : 'AGCSEL',
4 : 'VOLUME',
5 : 'AGCSEL',
6 : 'AGCIDX',
7 : 'AGCSEL',
}
}
# 帯域幅選択を幅の大きい順に並べ替え
BW_MODE_TABLE = { 0:0x00, 1:0x01, 2:0x02, 3:0x06, 4:0x03, 5:0x05, 6:0x04 }
BWNAME = ['6.0k', '4.0k', '3.0k', '2.5k', '2.0k', '1.8k', '1.0k']
CONT_TH = 2 # カウンタ不感帯の幅のしきい値
# ロータリーエンコーダのカウンタ更新処理(正転・逆転)
def update_counter( r_count, c_count, step, c_max, c_min ) :
# 同じ方向の値がCONT_TH回連続したら方向を確定
# 増加方向判定
if r_count >= CONT_TH :
r_count = 0
c_count += step
# 上限判定
if c_count > c_max : c_count = c_max
# 減少方向判定
elif r_count <= -CONT_TH:
r_count = 0
c_count -= step
# 下限判定
if c_count < c_min : c_count = c_min
return r_count, c_count
# 起動時表制御用フラグ
INTRO = 'START' # 'DONE'
# ADC0/1/2 : 使用可能
# ADC3 : 電圧検出用にボード上でアサイン済み(変更不可)
# ADC4 : チップ内部で温度センサが接続済み(変更不可)
temp_sens = ADC(4)
volt_sens = ADC(3)
# ADC4の分解能(16bit)を電圧値に変換
q_step = 3.3 / 65535
# 割込ハンドラ関数
'''
実装の要点
ISR内の処理は極力軽くすること
割込バリアは通常処理側のISRと共有している変数を参照する箇所に入れる
'''
# ADC検知出力
def get_env_info():
# 実際の温度を算出
global temp_sens
global volt_sens
temp = round((27.0 - (q_step*temp_sens.read_u16() - 0.706)/0.001721),1)
volt = q_step*volt_sens.read_u16()
print(f'system_tmp:{temp:2.2f}[C] / system_volt:{volt:2.2f}')
return temp
# ロータリーエンコーダ検出
# RotaryEncoderの回転方向判定のための定数定義
#CNT_MIN = 500 # 0
#CNT_MAX = 1600 # 2047
CNT_MIN = 230 # 0
CNT_MAX = 23000 # 2047
CNT_CTR = int(((CNT_MAX - CNT_MIN)>>1) + CNT_MIN) # 1024
COUNTER = 1134 # JOQR
VOL_MAX = 63 # カウンタ初期値
VOL_MIN = 0 # カウンタ初期値
VOL_CNT = VOL_MAX>>1 # カウンタ初期値
DIR_THR = 2 # エンコーダ位相判定のしきい値
# RotaryEncoderの回転方向判定のための初期値
PreVal = 0x3 # [reD,reU]
CurVal = 0 # [reD,reU]
r_count = 0 # -CONT_TH - +CONT_TH
dTimePr = 0 # 前回COUNTERを更新した時刻
dTimeCr = 0 # 今回COUNTERを更新した時刻
frq = COUNTER
frq_step = FRQ_STEP_TABLE['MW'][0]
FLIP_COUNT = 0
FLIP_FLAG = 0
COUNTER_AM = COUNTER # x 1[kHz]
CNT_MIN_FM = 7600 # 76MHz
CNT_MAX_FM = 10600 # 106MHz
COUNTER_FM = 7800 # x10[kHz]
frq_fm = COUNTER_FM
frq_step_fm = FRQ_STEP_TABLE['FM'][0] # x10[kHz]
COUNTER1 = 0
r_count1 = 0 # -CONT_TH - +CONT_TH
PreVal1 = 0x3 # [reD1,reU1]
CurVal1 = 0 # [reD1,reU1]
vol = VOL_CNT
FLIP_FLAG1 = 0
FLIP_COUNT1 = 0
RT_MODE = 'AMFMSW'
rfgain = 0
RFGAIN_CNT = 0
RFGAIN_MAX = 9
RFGAIN_MIN = 0
agcmode = 0
AGCMODE_CNT = 0
AGCMODE_MAX = 1
AGCMODE_MIN = 0
agcmode_FM = 0
AGCMODE_CNT_FM = 0
AGCMODE_MAX_FM = 1
AGCMODE_MIN_FM = 0
agcindex = 0
AGCINDEX_CNT = 0
AGCINDEX_MAX = 127
AGCINDEX_MIN = 0
agcindex_FM = 0
AGCINDEX_CNT_FM = 0
AGCINDEX_MAX_FM = 26
AGCINDEX_MIN_FM = 0
bwmode = 0
BWMODE_CNT = 0
BWMODE_MAX = 6
BWMODE_MIN = 0
amfmsw = 0
AMFMSW_CNT = 0
AMFMSW_MAX = 1
AMFMSW_MIN = 0
mode_1 = 0
MODE_1_CNT = 0
MODE_1_MAX = 3
MODE_1_MIN = 0
mode_2 = 0
MODE_2_CNT = 0
MODE_2_MAX = 3
MODE_2_MIN = 0
RX_MODE = 'AM'
RSSI = 0
SNR = 0
PUSH_SW_COUNTER = 0
# 表示ドット:128dot x 64dot
# 文字サイズ:8dot x 8dot -> 8x3=24, 32-24=8
# 表示文字数:16文字 x 8行
'''
# 0123456789ABCDEF
# 0)RX:12345kHz/1000
# 8)AM AF:63 RF:256
#16)BW:2.5k AGC:OFF
#24)IDX:127
#32)RSSI:123 SNR:123
#40)
#48)
#56)________|_______
'''
def display_info( text, posy=0, addline=False ):
if addline == False : display.fill(0)
display.text(text, 0, posy, True)
display.show()
#### 1行目
# 0123456789ABCDEF
# RX:frqcykHz/step : frqcy=500-23000[kHz] step=1/5/10/100/1000[kHz]
FRQ_STEP_DISP_AM = {
1 : '1k',
5 : '5k',
9 : '9k',
10 : '10k',
50 : '50k',
100 : '100k',
1000 : '1M',
}
def display_freq():
# 周波数を表示
display.rect(0,0,128,8,0,True)
text = f'Fr:{frq:5d}kHz/{FRQ_STEP_DISP_AM[frq_step]}'
display.text(text, 0, 0, True)
def display_freq_FM():
# 周波数を表示
display.rect(0,0,128,8,0,True)
text = f'Fr:{frq_fm/100:3.1f}MHz/{frq_step_fm/100:2.1f}M'
display.text(text, 0, 0, True)
#### 2行目
# 8)AM AF:63 RF:256
def display_rxmode():
display.rect(0,8,24,8,0,True)
text = RX_MODE
display.text(text,0,8,True)
# 8)SSB AF:63 RF:256
def display_volume():
display.rect(8*7,8,8*8,8,0,True) # エリアをクリア
text = f' AF:{vol:2d}'
display.text(text, 8*3, 8, True)
# 5-7(8はスペース)
def display_rfgain():
display.rect(8*13,8,8*15,8,0,True) # エリアをクリア
text = f'RF:{rfgain:2d}'
display.text(text, 8*10, 8, True)
# 9-11(12はスペース)
# 0123456789ABCDEF
#16)AGC:OFF BW:2.5k
def display_bwmode():
display.rect(8*9,16,8*7,8,0,True) # エリアをクリア
text = f'BW:{BWNAME[bwmode]}'
display.text(text, 8*9, 16, True)
# 13-15
# 0123456789ABCDEF
#16)AGC:OFF
AGCNAME = { 0:'ON ', 1:'OFF' }
def display_agcmode():
display.rect(0,16,8*9,8,0,True) # エリアをクリア
text = f'AGC:{AGCNAME[agcmode]}'
display.text(text, 0, 16, True)
# 0123456789ABCDEF
#16)AGCSEL: OFF
def display_agcmode_FM():
display.rect(0,16,8*16,8,0,True) # エリアをクリア
text = f'AGCSEL:{AGCNAME[agcmode_FM]}'
display.text(text, 0, 16, True)
# 0123456789ABCDEF
#24)AGCIDX:127
def display_agcindex():
display.rect(0,24,8*10,8,0,True) # エリアをクリア
text = f'AGCIDX:{agcindex:3d}'
display.text(text, 0, 24, True)
def display_agcindex_FM():
display.rect(0,24,8*10,8,0,True) # エリアをクリア
text = f'AGCIDX:{agcindex_FM:3d}'
display.text(text, 0, 24, True)
#### 3行目
# 0123456789ABCDEF
# 8)AM AF:63 RF:256
def display_mode():
display.rect(3,36,128,8,0,True) # エリアをクリア
text = RT_MODE
display.text(text, 3, 36, True)
display.hline( 0,34,56,1)
display.hline( 0,44,56,1)
display.vline( 0,34,10,1)
display.vline(56,34,10,1)
#### 4行目
# 0123456789ABCDEF
#32)RSSI:123 SNR:123
def display_status() :
global RSSI
global SNR
display.rect(0,32,128,8,0,True) # エリアをクリア
# 受信状態を取得
text = f'RSSI:{RSSI:3d} SNR:{SNR:3d}'
display_mode()
display_signal_level()
def display_status_FM() :
global RSSI
global SNR
display.rect(0,32,128,8,0,True) # エリアをクリア
text = f'RSSI:{RSSI:3d} SNR:{SNR:3d}'
display_mode()
display_signal_level()
#### 5行目
def display_signal_level() :
snr = SNR
if SNR == 0: snr = 1
sig = RSSI
if RSSI == 0: sig = 1
display.fill_rect(0, 48, 128, 6, 0) # clear
display.fill_rect(0, 48, 2+int(16*math.log2(snr)+0.5), 1, 1)
display.fill_rect(0, 51, 2+int(16*math.log2(sig)+0.5), 2, 1)
text='0 1 2 3 4 5 6 7S'
display.text(text, 0, 56, True)
DIF = 0
RT_ENC = False
# ロータリーエンコーダ0回転検知
def isr_det_rotation( pin ) :
# グローバル変数仕様宣言
global CurVal
global PreVal
global RT_MODE
global DIF
global RT_ENC
RT_MODE = 'TUNING'
# 現在の値を取得
CurVal = ((0x1 & reD.value()) << 1) | (0x1 & reU.value())
# デコード
if CurVal != PreVal :
DIF = ( ((0x3 & PreVal) << 1) ^ (0x3 & CurVal) ) & 0x3
PreVal = CurVal
RT_ENC = True
else :
DIF = 0
RT_ENC = False
return
# ロータリーエンコーダ1回転検知
DIF1 = 0
RT_ENC1 = False
def isr_det_rotation1( pin ) :
# グローバル変数仕様宣言
global CurVal1
global PreVal1
global RT_MODE
global DIF1
global RT_ENC1
global RT_MODE
if RT_MODE == 'TUNING': RT_MODE = 'VOLUME'
# 現在の値を取得
CurVal1 = ((0x1 & reD1.value()) << 1) | (0x1 & reU1.value())
# デコード
if CurVal1 != PreVal1 :
DIF1 = ( ((0x3 & PreVal1) << 1) ^ (0x3 & CurVal1) ) & 0x3
PreVal1 = CurVal1
RT_ENC1 = True
else :
DIF1 = 0
RT_ENC1 = False
return
# ロータリーエンコーダ0ボタン検知
RT_PUSH = False
def isr_rotary_push( pin ) :
global RT_PUSH
RT_PUSH = pin
global RT_MODE
RT_MODE = 'TUNING'
return
# ロータリーエンコーダ1ボタン検知
RT_PUSH1 = False
def isr_rotary_push1( pin ) :
global RT_PUSH1
RT_PUSH1 = pin
return
PUSHSW = False
# push switch押下検出
# 割込発生で0-3までカウントする
def isr_push_sw( pin ) :
global PUSH_SW_COUNTER
global RT_MODE
global PUSHSW
PUSHSW = pin
RT_MODE = 'AMFMSW'
PUSH_SW_COUNTER = (PUSH_SW_COUNTER + 1) & 0x3
return
def rotary_push_control( ) :
#int_state = machine.disable_irq()
global COUNTER
global COUNTER_AM
global frq_step
global FLIP_FLAG
global COUNTER_FM
global frq_step_fm
global FLIP_COUNT
global RT_PUSH
global RT_MODE
FLIP_COUNT = 1
int_state = machine.disable_irq()
RT_MODE_ = RT_MODE
machine.enable_irq( int_state )
# カウンタをリセットする
if FLIP_COUNT == 1 :
FLIP_COUNT = 0
RT_MODE_ = 'TUNING'
# ステップ数を読み出す
if RX_MODE == 'AM':
if 520 <= COUNTER_AM and COUNTER_AM <= 1700 :
frq_step = FRQ_STEP_TABLE['MW'][FLIP_FLAG]
else :
frq_step = FRQ_STEP_TABLE['SW'][FLIP_FLAG]
elif RX_MODE == 'FM':
frq_step_fm = FRQ_STEP_TABLE[RX_MODE][FLIP_FLAG]
FLIP_FLAG = ( FLIP_FLAG + 1 ) & 0x7
led.toggle()
int_state = machine.disable_irq()
RT_MODE = RT_MODE_
RT_PUSH = False
machine.enable_irq( int_state )
return
def rotary_push1_control( ):
global COUNTER1
global FLIP_FLAG1
global FLIP_COUNT1
global RT_MODE
global RT_PUSH1
int_state = machine.disable_irq()
RT_MODE_ = RT_MODE
machine.enable_irq( int_state )
FLIP_COUNT1 = 1
# カウンタをリセットする
if FLIP_COUNT1 == 1 :
FLIP_COUNT1 = 0
# モードを読み出す
if RX_MODE == 'AM' or RX_MODE == 'FM' :
RT_MODE_ = MODE_TABLE[RX_MODE][FLIP_FLAG1]
FLIP_FLAG1 = ( FLIP_FLAG1 + 1 ) & 0x7
led.toggle()
int_state = machine.disable_irq()
RT_MODE = RT_MODE_
RT_PUSH1 = False
machine.enable_irq( int_state )
return
def pushsw_control():
global PUSH_SW_COUNTER
global RT_MODE
global RX_MODE
global PUSHSW
RT_MODE = 'TUNING'
if PUSH_SW_COUNTER == 0 :
RX_MODE = 'FM'
SI4732.SetRxVolume( 0 )
SI4732.PowerDown()
SI4732.PowerUpFmAnalog()
SI4732.GetRevision()
SI4732.SetRxHardMuteIn()
SI4732.FmTuneFreq( COUNTER_FM )
SI4732.GetIntStatus()
SI4732.FmTuneStatus()
SI4732.SetFmDeEmphasis()
SI4732.FmAgcOverRide( 1, 6 )
SI4732.SetRxVolume( 32 )
SI4732.SetRxHardMuteOut()
elif PUSH_SW_COUNTER == 1 :
RX_MODE = 'AM'
SI4732.SetRxVolume( 0 )
SI4732.PowerDown()
SI4732.PowerUpAmAnalog()
SI4732.GetRevision()
SI4732.SetRxHardMuteIn()
SI4732.AmTuneFreq( COUNTER_AM )
SI4732.GetIntStatus()
SI4732.AmTuneStatus()
SI4732.SetAmChannelFilter( 0x00 )
SI4732.SetRxVolume( 32 )
SI4732.SetRxHardMuteOut()
elif PUSH_SW_COUNTER == 2 :
RX_MODE = 'SSB'
elif PUSH_SW_COUNTER == 3 :
RX_MODE = 'NUL'
int_state = machine.disable_irq()
PUSHSW = False
machine.enable_irq( int_state )
return
# ロータリーエンコーダによるカウンタ値更新
def dec_rotation( ) :
# グローバル変数使用宣言
global CurVal
global PreVal
global r_count
global COUNTER
global COUNTER_AM
global COUNTER_FM
global frq
global frq_step
global frq_fm
global frq_step_fm
global DIR_THR
global DIF
global RT_ENC
RT_ENC = False
# 前回と変化があった場合
# 判定処理と連続性計測用カウンタの更新
r_count += CODE_TABLE[int(DIF)]
# 連続性計測用カウンタの値で検出結果を判定
# (ロータリーエンコーダのチャタリング対策)
if RT_MODE == 'TUNING':
if RX_MODE == 'AM' :
r_count, COUNTER = update_counter( r_count, COUNTER, frq_step, CNT_MAX, CNT_MIN )
COUNTER_AM = COUNTER
frq = COUNTER
SI4732.AmTuneFreq( frq )
SI4732.GetIntStatus()
elif RX_MODE == 'FM' :
r_count, COUNTER_FM = update_counter( r_count, COUNTER_FM, frq_step_fm, CNT_MAX_FM, CNT_MIN_FM )
frq_fm = COUNTER_FM
SI4732.FmTuneFreq( frq_fm )
SI4732.GetIntStatus()
else :
pass
else :
pass
return
# ロータリーエンコーダによるカウンタ値更新
def dec_rotation1( ) :
# グローバル変数使用宣言
global CurVal1
global PreVal1
global r_count1
global COUNTER1
global VOL_CNT
global vol
global RFGAIN_CNT
global rfgain
global BWMODE_CNT
global bwmode
global AMFMSW_CNT
global amfmsw
global AGCMODE_CNT
global agcmode
global AGCINDEX_CNT
global agcindex
global AGCMODE_CNT_FM
global agcmode_FM
global AGCINDEX_CNT_FM
global agcindex_FM
global MODE_2_CNT
global mode_2
global RX_MODE
global DIF1
global RT_ENC1
RT_ENC1 = False
# 判定処理と連続性計測用カウンタの更新
r_count1 += CODE_TABLE[int(DIF1)]
# 連続性計測用カウンタの値で検出結果を判定
# (ロータリーエンコーダのチャタリング対策)
# 連続性カウンタの値3以上(同じ判定が3回連続)の場合に限り方向確定
# 正方向の場合
if RX_MODE == 'AM':
if RT_MODE == 'VOLUME':
r_count1, VOL_CNT = update_counter( r_count1, VOL_CNT, 1, VOL_MAX, VOL_MIN )
vol = VOL_CNT
SI4732.SetRxVolume( vol )
elif RT_MODE == 'RFGAIN':
r_count1, RFGAIN_CNT = update_counter( r_count1, RFGAIN_CNT, 1, RFGAIN_MAX, RFGAIN_MIN )
rfgain = RFGAIN_CNT
SI4732.SetAmAvcMaxGain( rfgain )
elif RT_MODE == 'BWMODE':
r_count1, BWMODE_CNT = update_counter( r_count1, BWMODE_CNT, 1, BWMODE_MAX, BWMODE_MIN )
bwmode = BWMODE_CNT
SI4732.SetAmChannelFilter( BW_MODE_TABLE[bwmode] )
elif RT_MODE == 'AGCSEL':
r_count1, AGCMODE_CNT = update_counter( r_count1, AGCMODE_CNT, 1, AGCMODE_MAX, AGCMODE_MIN )
agcmode = AGCMODE_CNT # 0 or 1
val = int(agcmode) & 0x01
com = [ 0x48, val, 0x7f ]
SI4732.registerWrite( com )
elif RT_MODE == 'AGCIDX':
# AGC_OFFの場合のみ有効化
if agcmode == 0x1:
r_count1, AGCINDEX_CNT = update_counter( r_count1, AGCINDEX_CNT, 1, AGCINDEX_MAX, AGCINDEX_MIN )
agcindex = AGCINDEX_CNT * 2
val = int(agcindex) & 0xFF
com = [ 0x48, 0x01, val ]
SI4732.registerWrite( com )
else :
pass
elif RX_MODE == 'FM':
if RT_MODE == 'VOLUME':
r_count1, VOL_CNT = update_counter( r_count1, VOL_CNT, 1, VOL_MAX, VOL_MIN )
vol = VOL_CNT
SI4732.SetRxVolume( vol )
elif RT_MODE == 'AGCSEL':
r_count1, AGCMODE_CNT_FM = update_counter( r_count1, AGCMODE_CNT_FM, 1, AGCMODE_MAX_FM, AGCMODE_MIN_FM )
agcmode_FM = AGCMODE_CNT_FM # 0 or 1
val = int(agcmode_FM) & 0x01
SI4732.FmAgcOverRide( val, gain = 6 )
elif RT_MODE == 'AGCIDX':
# AGC_OFFの場合のみ有効化
if agcmode_FM == 0x1 :
r_count1, AGCINDEX_CNT_FM = update_counter( r_count1, AGCINDEX_CNT_FM, 1, AGCINDEX_MAX_FM, AGCINDEX_MIN_FM )
agcindex_FM = AGCINDEX_CNT_FM
val = int(agcindex_FM) & 0x1F
SI4732.FmAgcOverRide( 0x1, val )
else :
pass
else :
pass
return
# 割込ハンドラ設定
# ロータリーエンコーダの割込を設定
# Rotary Push-SW : 押下でLowになりハンドラをcall
reP.irq( trigger=Pin.IRQ_FALLING, handler=isr_rotary_push )
# Rotary-Encoders : ピン変化時にハンドラをcall
reU.irq( trigger=Pin.IRQ_FALLING|Pin.IRQ_RISING, handler=isr_det_rotation )
reD.irq( trigger=Pin.IRQ_FALLING|Pin.IRQ_RISING, handler=isr_det_rotation )
# Rotary Push-SW : 押下でLowになりハンドラをcall
reP1.irq( trigger=Pin.IRQ_FALLING, handler=isr_rotary_push1 )
# Rotary-Encoders : ピン変化時にハンドラをcall
reU1.irq( trigger=Pin.IRQ_FALLING|Pin.IRQ_RISING, handler=isr_det_rotation1 )
reD1.irq( trigger=Pin.IRQ_FALLING|Pin.IRQ_RISING, handler=isr_det_rotation1 )
# プッシュスイッチの割込を設定
# Push-SW : 押下でLowになりハンドラをcall
pb0.irq( trigger=Pin.IRQ_FALLING, handler=isr_push_sw )
# 基本情報表示
print(f'CPU Frequency is {machine.freq()}') # 初期値が最速クロックらしい(125MHz)
get_env_info()
# デバイスのアドレスをスキャンします
print('scan I2C1 bus')
addr = i2c.scan()
for val in addr:
print(f'I2C1 slave address: 0x{val:02x}')
print('scan I2C0 bus')
addr = i2c0.scan()
for val in addr:
print(f'I2C0 slave address: 0x{val:02x}')
# 状態変数の初期化
mes_cycle = 0
start_time0 = 0
start_time1 = 0
# 内蔵LEDはCYW43439無線チップに接続されています(文字列で "LED" を指定する
led = Pin("LED", Pin.OUT)
led.value(1) # LEDの状態を切り替えます
utime.sleep(0.1)
led.value(0) # LEDの状態を切り替えます
# core1 : OLED表示処理
def core1( ):
display_interval_count = 0x0003
display_info( 'Start init.', posy=8 )
# 設定が終わるまで待ち合わせ
k = 0
while INTRO == 'START' :
utime.sleep(0.4)
display.text( '>', k, 17 )
display.show()
k += 8
display.text( 'init. done', 0, 26 )
display.show()
utime.sleep(1)
display.rect(0,0,128,64,0,True) # エリアをクリア
display.text( '----------------', 0, 8 )
display.text( ' Welcome to ', 0, 24 )
display.text( ' DSP Radio ', 0, 36 )
display.text( '----------------', 0, 52 )
display.show()
utime.sleep(1)
display.rect(0,0,128,64,0,True) # エリアをクリア
utime.sleep(1)
n = 0
while True:
# 受信状態表示
if n == display_interval_count :
n = 0
display_volume()
display_mode()
display_rxmode()
if RX_MODE == 'AM' :
display_freq()
display_bwmode()
display_agcindex()
display_agcmode()
display_status()
display_rfgain()
elif RX_MODE == 'FM' :
display_freq_FM()
display_agcmode_FM()
display_agcindex_FM()
display_status_FM()
display.show()
n += 1
# デバイス制御・I/F制御
def core0( ) :
global INTRO
global RX_MODE
INTRO = 'START'
RX_MODE = 'FM'
SI4732.ResetDevice( rst )
SI4732.PowerUpFmAnalog( )
SI4732.GetRevision( )
SI4732.SetRxHardMuteIn( )
SI4732.SetRxVolume( 0 )
SI4732.FmTuneFreq( COUNTER_FM )
SI4732.GetIntStatus( )
SI4732.FmTuneStatus( )
SI4732.FmAgcOverRide( 1, 6 )
SI4732.SetRxVolume( VOL_CNT )
SI4732.SetRxHardMuteOut( )
INTRO = 'DONE'
while True:
if RT_ENC :
dec_rotation()
if RT_ENC1 :
dec_rotation1()
if PUSHSW :
pushsw_control()
if RT_PUSH :
rotary_push_control()
if RT_PUSH1 :
rotary_push1_control()
# 受信状態を取得
global RSSI
global SNR
if RX_MODE == 'FM':
RSSI, SNR = SI4732.getRxStatusFM()
elif RX_MODE == 'AM':
RSSI, SNR = SI4732.getRxStatus()
# core1を別スレッドでスタート
_thread.start_new_thread( core1, () )
# メインスレッドでcore0関数を実行
core0( )
#EOF#
はまったところ
- リセット制御に失敗してI2Cの開通に時間がかかった
- LSIのパワーオン設定をアプリケーションノート通りに実装して試していたが全く音が出ず、結局パワーオンのレジスタ設定を水晶発振器を使う設定に変更することでようやく動き出した
- アンテナピン周りの回路に誤りがあるのに気づかず、感度悪いなーと思いながらしばらく検討をしていた(修正後はむしろ感度高すぎのような状態に)
- ロータリーエンコーダの操作感の調整に手間取る
- 実装当初は表示部の変化が操作部の操作についてこない残念な操作感
- 割込ハンドラの処理内容を吟味・極力スリム化する等の調整を行うことでかなり改善
- 最終的には、スレッドの導入で表示部処理を別CPUに任せるようにすることでほぼ理想の操作感に変わった。
- micropythonのthreadはGILの縛りがない本当のスレッドなので2-CPUのpicoでの利用価値は相当ある。
実装結果(現状)
- 中波(AM)放送、海外短波放送(〜23MHz)、FM放送が受信可能(アンテナには数mのワイヤーアンテナを使用)
- 首都圏のAM放送はしょぼいアンテナでも非常に強力に受信できる(アンテナを長くすると過入力となりRFゲインを下げたくなるほど)
- 遠隔地(関西圏・西日本)のAM放送は夜間に同調型ループアンテナを使うとノイズを抑えて聴取が可能
- 海外短波放送は大半の日本語放送は受信できている(オーストラリアからの放送が最遠方)
- 首都圏のFM放送は強力に受信できる
- 周波数・音量・RF感度・受信帯域幅(AM)・AGC機能の調整が可能
- 音量調整は可能だが、LSI出力そのままではヘッドホンをギリギリ駆動できるレベルで余裕がなかったので、オペアンプで低周波増幅回路を組んで追加している
- 周波数可変はロータリーエンコーダで行う(ロータリーエンコーダのプッシュボタンでステップを変更可能)
- AMの受信帯域幅調整は効果があるが6kHzが最大帯域であり中波放送受信時はもう少し広くてもよいと思うこともある
- 微弱信号受信時は、AGCのパラメータを変更すると信号が浮かび上がってくることがあるが、近くの周波数に強力な信号があるとそれが漏れてきて痛し痒し
- アンテナ端子に中波放送受信に合わせたインダクタを接続すると、短波受信時にAM放送が漏れ込んできて妨害されるという問題があり、短波放送受信時は中波放送受信用インダクタをアンテナ端子から切り離す回路を追加している。
- ラストメモリ機能を追加(ファイルで内部設定データをバックアップ・リストア)
今後とまとめ
- SSB機能の追加
- アマチュア無線受信に必須の機能。パッチデータのロードとレジスタ制御追加が必要
- 不安定なブレッドボードからユニバーサル基板に組み直し(はんだ付け作業)
- ロータリーエンコーダに追加(使用頻度の高い機能の分離を検討)
- 出力オペアンプの周波数特性をAM/FMで切り替える回路を追加する
- 短波放送に合わせると、高域と低域を制限したほうが聴きやすいが、FM受信時はこれだと音質がよくないという問題
- 赤外線リモコン追加?
感想
- micropythonでもこの位の処理は問題なくこなせることを実感
参考情報
-
PU2CLR SI4735 Library for Arduino
- ハード関係の情報も含めほとんどの情報はここにある
-
SI4732-A10-GSR基板入手先
- LSI実装済み基板を入手する必要がある
- Si4732によるオールウェーブラジオ受信機の製作
- ロータリーエンコーダの使い方
- ロータリーエンコーダ(プッシュボタン付き)・OLEDモジュール(SSD1306)はAmazonで入手可能