2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DSP-radio LSI SI4732-A10をRaspberryPi Pico+Micropythonで動かしてみた

Posted at

はじめに

これまで、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のコンソールがなくても操作できるように操作状態を表示する(使いながら調整)
    • 表示情報は制御系側が生成しグローバル変数で共有することで受け渡す
  • スレッド動作の導入
    • 操作系・表示系の反応が鈍い印象だったため、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でもこの位の処理は問題なくこなせることを実感

参考情報

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?