16.93MHz All Mode SDR(Transceiver) 2

試作基板で評価を行ったところ,何点か修正が必要なところが出てきたので新たに基板を製作した.見た目はほとんど変わらないが,ノイズ対策としてAFパワーアンプの電源とGNDを完全に分離した.機能的には受信アンプのゲインを半固定抵抗で可変できるようにしたのと,DACのMUTE端子を ESP32-S3 の IO0 に接続しMUTE制御ができるようにした.ただ今のところ使用する予定はなく,MUTE はソフトウェア的に行っている.

実装状態

問題なく動作することが確認できたので,ファームウェアと回路図など製作に必要な情報をすべてGitHubにUPした(詳細はそちらを参照願います).

tjlab-jf3hzb/All_mode_SDR

現状のファームウェアの機能は以下のとおり.
・オールモード(SSB,CW,AM,FM)の送受信(16.93MHz固定)
・AGC(受信時),マイクコンプレッサ,リミッタ(送信時)
・バンドスコープ
・Sメータ表示(LCD)
・Sメータ用電圧出力
・外部AGC用電圧出力(過大入力時の制御用)
・パスバンドコントロール(中心周波数,帯域幅,ノッチ)
・スケルチ(FM受信時)
など

P.S.
製作に必要な情報はGitHubに公開していますが,完成品のご希望があれば頒布を検討しますのでご連絡ください.価格は概算ですが2万円を少し超えると思います.

16.93MHz All Mode SDR(Transceiver)

Xの投稿もご参照ください.

固定周波数(16.93MHz)のオールモードSDRトランシーバを試作した.構成は下図のとおり.

かつてSSBジェネレータと呼ばれていたものに相当する.SSBジェネレータは当然SSB(一部CWも)のみ送受可能だが,SDRであれば多モードにしてもハードウェアの規模は変わらないのでオールモードトランシーバにする.これにコンバータを接続すれば任意のバンドのトランシーバや受信機が構成できる.

ただ,SDRにありがちな「メニューを呼び出して操作する」ような形態ではなく,古い無線機のようにVRやロータリSWによって操作するものにした.そのためLCDを接続しなくてもつまみの方向で設定状態がわかるので,かつてのSSBジェネレータを使用するのとほとんど同じ感覚で使用できる.LCDを使用しない場合,Sメータを振らせる電圧を出力するようにした.

作成した基板と回路図.




以下の動画は,ファームウェアを制作し動作をさせたもの.


PBT動作
送信
パッシブDBMを接続して7MHzを受信

ほぼ満足できるファームウェアができたので,ハードの修正内容を反映した基板をあらためて製作することにする.最終的にはGithubでファームウェアを含め情報を公開予定.

P.S.
回路修正が必要となったため,今回の試作の残りの基板(未実装8枚)はボツとなりますが,もし必要な方がありましたら無償で差し上げます.ファームウェアも必要でしたら提供可能です(現時点ではファームウェアだけの提供はしません).ただし無保証,原則サポート無しという条件を承諾していただける方限定です.詳細はメールでご連絡ください.

終了しました.(12.17, 2023)

30kHz All Mode SDR(Receiver)

ギターエフェクタ用に作成したボードを使って,30kHzオールモード(AM, FM, SSB)レシーバの動作確認をした(最終的にはこれの前にダウンコンバータをつけて広帯域レシーバに仕上げる予定).

エフェクタ用基板

このボードはESP32-S3にI2SインターフェースのAudioCodecを接続した単純なものなので,実験用としても使いやすい.

プログラムは過去のブログ記事のとおりだが,中心周波数30kHz,帯域+-25kHzとするためサンプリング周波数のみ120kHzに変更している.

余談になるが,過去に動作していたプログラムが動作しなかった.原因は esp32 のボードマネージャのバージョン.2.0.10以降だとDSP関係の処理(FIRフィルタ)がまともに動作しない.DSPライブラリに不備があるようだ.そういえばFFTも,条件によって虚数部の符号が逆になったりインデックスが1つずれたり…  とりあえず2.0.9 を使っていこうと思う.

ADCにはPCM1808を使っている.これの動作保証は96kHzまでだが問題なく動作した.倍の192kHzでも動作したので120kHzサンプリングで動作させてもあまり問題はなさそう.
DACはPCM5102Aでこちらの最大サンプリング周波数は384kHzで余裕がある.

最終的にレシーバに仕上げるにはこれに前にダウンコンバータが必要で,一般的には直交ミキサを用い,その出力をIQ 2ch入力で受けて60kHz離れたイメージを除去する.また各モードで必要な帯域制限は信号処理で行う.

しかし,現実問題として直交ミキサをどんなに調整しても広帯域でかつ満足できる性能で動作させることは難しい.具体的にはあらゆる周波数で60kHz離れたイメージを十分減衰させる(つまりIQバランスを保つ)のは難しく,調整も面倒なので(物理的な)直交ミキサは使いたくない.

このような理由でダウンコンバート時のイメージはフィルタで切ることを前提にして,上図のように入力は解析信号(IQ 2ch)ではなく実信号(1ch)とした.

 


AM受信

 


FM受信


SSB(USB)受信


SSB受信(逆サイド)

フィルタ設計

過去の記事で,ESP32-S3でSDRを構築するためのコードを示してきたが,その中でフィルタの係数については具体的なものは示していなかったので,フィルタ係数を求める方法についてまとめておく.

ツールとしては GNU Octave を用いる.

以下に示すスクリプトを GNU Octave で実行すればフィルタ係数が記述されたヘッダファイルが生成される.

 

1.IIR LPF(8次エリプティック LPF)

%  This file name: "IIR_LPF_design.m"
%-----------------------------------------------
% To generate "IIR_LPF.h",
%    execute this script for MATLAB/GNU Octave.
%
%      Jan. 5, 2023  by T. Uebo
%-----------------------------------------------
pkg load signal;
clear;
close all;
clc;

%-- Modify this section if you need ----
fs=96e3; %[Hz]
fc=5e3; %{Hz]
Rp=1;  %[dB]
Rs=100; %[dB]
%----------------------------------------

% 8th LPF
ord=8;

% Elliptic LPF
[b,a]=ellip(ord, Rp, Rs, 2*fc/fs);

% Chebyshev LPF
%[b,a]=cheby1(ord, Rp, 2*fc/fs);

figure(1);
freqz(b,a,2048,fs);

[sos, g]=tf2sos(b,a);
g=g.^(1/(ord/2));
B0=g.*sos(1,1:3);
A0=sos(1,5:6);
B1=g.*sos(2,1:3);
A1=sos(2,5:6);
B2=g.*sos(3,1:3);
A2=sos(3,5:6);
B3=g.*sos(4,1:3);
A3=sos(4,5:6);


FID=fopen("IIR_LPF.h", "w");
fprintf(FID,"// %dth IIR Elliptic LPF\n" ,ord);
fprintf(FID,"// fs=%d[Hz]\n" ,fs);
fprintf(FID,"// fc=%d[Hz]\n" ,fc);
fprintf(FID,"// Ripple=%d[dB]\n", Rp);
fprintf(FID,"// Att=%d[dB]\n", Rs);
fprintf(FID,"// \n");
fprintf(FID,"//      b0 + b1*Z^(-1) + b2*z^(-2)  \n");
fprintf(FID,"// H(z)=--------------------------- \n");
fprintf(FID,"//       1 + a1*z^(-1) + a2*z^(-2)  \n");
fprintf(FID,"// \n");
fprintf(FID,"// {b0, b1, b2, a1, a2}\n");

fprintf(FID,"float biquad0[] =\n");
fprintf(FID,"{%e,%e,%e,%e,%e};\n",[B0 A0]);

fprintf(FID,"float biquad1[] =\n");
fprintf(FID,"{%e,%e,%e,%e,%e};\n",[B1 A1]);

fprintf(FID,"float biquad2[] =\n");
fprintf(FID,"{%e,%e,%e,%e,%e};\n",[B2 A2]);

fprintf(FID,"float biquad3[] =\n");
fprintf(FID,"{%e,%e,%e,%e,%e};\n",[B3 A3]);

fclose(FID);

 

2. FIR LPF

%  This file name: "FIR_LPF_design.m"
%-----------------------------------------------
% To generate "FIR_LPF.h",
%    execute this script for MATLAB/GNU Octave.
%
%      Jan. 5, 2023  by T. Uebo
%-----------------------------------------------
pkg load signal;
clear;
close all;
clc;

%-- Modify this section if you need ----
Nfir=256;
fs=16e3;  %[Hz]
fc=3e3;    %[Hz]
%----------------------------------------

b = fir1(Nfir-1,2*fc/fs, 'low',...
         blackmanharris(Nfir) );

figure(1)
freqz(b,1,2048, fs)
figure(2)
plot(b);

FID=fopen("FIR_LPF.h", "w");

fprintf(FID, "float FIR_LPF[]={\n");
for k=[1:Nfir-1]
fprintf(FID,"%e,\n", b(k));
end;
fprintf(FID,"%e\n", b(Nfir));
fprintf(FID,"};\n");

fclose(FID);

 

3. 複素係数FIR BPF

%  This file name: "CPLX_BPF_design.m"
%-------------------------------------------------
% To generate "CPLX_filter.h",
%    execute this script for MATLAB/GNU Octave.
%
%      Jan. 5, 2023  by T. Uebo
%-------------------------------------------------
pkg load signal
clear;
close all;
clc;

%--- Modify this section if you need --------
% Define Pass Band: f1(Hz) to f2(Hz)
f1=250;
f2=2850;

%Sampling frequency
fs=16e3;
%Number of Taps
Ndat=256;

%--Set a Window function---
%wf=ones(Ndat,1);
%wf=hanning(Ndat);
%wf=hamming(Ndat);
wf=blackman(Ndat);
%wf=blackmanharris(Ndat);
%wf=chebwin(Ndat);
%--------------------------------------------


%--- Calc. complex FIR coeff. ---------------
if (f1<f2)
  fa=f1+fs/2;
  fb=f2+fs/2;
else
  fa=f2+fs/2;
  fb=f1+fs/2;
end

ka=round( fa/(fs/Ndat) );
kb=round( fb/(fs/Ndat) );

fres=[ zeros(1,ka-1)...
       ones(1,kb+1-ka)...
       zeros(1,Ndat-kb)];

fres=[fres(Ndat/2+1:end) fres(1:Ndat/2)];

imp_res=ifft(fres);

Ich=[real(imp_res(Ndat/2+1:end))...
     real(imp_res(1:Ndat/2))];
Qch=[imag(imp_res(Ndat/2+1:end))...
     imag(imp_res(1:Ndat/2))];
Ich=Ich.*wf.';
Qch=Qch.*wf.';


%----Generate coeff. file ------------------
FID=fopen('CPLX_filter.h', 'w');

fprintf(FID, 'float CF_Re[%d]={\n' , Ndat);
for p=[1:Ndat-1]
fprintf(FID,'%e,\n', Ich(p));
end;
fprintf(FID,'%e\n', Ich(Ndat) );
fprintf(FID,'};\n');

fprintf(FID, 'float CF_Im[%d]={\n' , Ndat);
for p=[1:Ndat-1]
fprintf(FID,'%e,\n', Qch(p) );
end;
fprintf(FID,'%e\n',  Qch(Ndat) );
fprintf(FID,'};\n');

fclose(FID);



%---- Display results ------------------------
figure(1); plot(Ich);
ylabel('FIR coeff. (Real Part)');
figure(2); plot(Qch);
ylabel('FIR coeff. (Imaginary Part)');
figure(3);
[hr,fr]=freqz(Ich+j*Qch,[1], Ndat*16,'whole',fs);
fr=fr-fs/2;
hr=[hr(Ndat*8+1:end); hr(1:Ndat*8)];
plot(fr, 20.*log10( abs(hr)) );
xlabel('Frequency(Hz)');
ylabel('Response(dB)');
grid on;

figure(4)
subplot(1,2,1)
plot(fr, 20.*log10( abs(hr)) );
xlabel('Frequency(Hz)');
ylabel('Response(dB)');
grid on;
axis([f1-500, f1+500, -80 10]);

subplot(1,2,2)
plot(fr, 20.*log10( abs(hr)) );
xlabel('Frequency(Hz)');
ylabel('Response(dB)');
grid on;
axis([f2-500, f2+500, -80 10]);

ESP32-S3 を使った周波数カウンタ

ESP32-S3 のみで構成する周波数カウンタ.正常にカウントできる上限はおよそ31MHz.

ESP32用の周波数カウンタのライブラリとしては FreqCountESP というのがあったので試してみたが,測定値が数10カウントくらいばらつく(ゲート制御をソフトウェアでやっている?)ので自分で作ることにした.コードは以下のとおり.(ただし,LCD表示コードは含みません.コードの動作確認はできていますがその保証やサポートはしませんのでご了承ください)

//
//  f_counter.ino
//
//  Frequncy counter
//    ESP32-S3
//
//  December 8, 2022  T.Uebo JF3HZB
//

#include <driver/pcnt.h>
#include "soc/pcnt_struct.h"

//
// GPIO 4 :カウンター入力
// GPIO 5 :TB出力,ゲート制御入力,ラッチ割り込み
// (ピンは使用しているが何もつながない)
//
#define PULSE_INPUT_PIN 4 // カウンターパルス入力
#define GATE_CTRL_PIN 5  // ゲート信号入力
#define TIME_BASE 5       // Time Base 出力
#define LATCH_EN 5     // カウント値ラッチ割込み入力

#define F_RESO 10  //Hz
#define F_ADJ 1.0f //0.9999999f

#define LEDC_CHANNEL_0 0  //channel max 15
#define LEDC_TIMER_BIT 14 //max 14bit
#define LEDC_BASE_FREQ (int)((float)F_RESO * 0.9f)
#define LEDC_DUTY      (int)((float)(1<<LEDC_TIMER_BIT) * 0.9f) 

int count_ovf = 0;
int fcount;
int frequency;


pcnt_isr_handle_t user_isr_handle = NULL;
static void IRAM_ATTR pcnt_intr_handler(void *arg);
void IRAM_ATTR pcnt_intr_handler(void *arg) 
{
  PCNT.int_clr.val = BIT(PCNT_UNIT_0); //割り込みクリア
  if(PCNT.status_unit[PCNT_UNIT_0].cnt_thr_h_lim_lat_un)
    count_ovf++;
}


void latch_fcount()
{
  int16_t cc;
  pcnt_get_counter_value(PCNT_UNIT_0, &cc);
  fcount = (int)cc + count_ovf * 32768;
  pcnt_counter_clear(PCNT_UNIT_0);
  count_ovf = 0;
}



void setup(void) {
  Serial.begin(115200);

  // Frequncy Counter, Unit0/ch0
  pcnt_config_t pcnt_config; 
        pcnt_config.pulse_gpio_num = PULSE_INPUT_PIN;
        pcnt_config.ctrl_gpio_num = GATE_CTRL_PIN;
        pcnt_config.lctrl_mode = PCNT_MODE_DISABLE;
        pcnt_config.hctrl_mode = PCNT_MODE_KEEP;
        pcnt_config.channel = PCNT_CHANNEL_0;
        pcnt_config.unit = PCNT_UNIT_0;
        pcnt_config.pos_mode = PCNT_COUNT_INC;
        pcnt_config.neg_mode = PCNT_COUNT_DIS;
        pcnt_config.counter_h_lim = 32767;
        pcnt_config.counter_l_lim = -32768;

  pcnt_unit_config(&pcnt_config);
  pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_H_LIM); 
  //pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_L_LIM);

  pcnt_counter_pause(PCNT_UNIT_0);
  pcnt_counter_clear(PCNT_UNIT_0);
  pcnt_counter_resume(PCNT_UNIT_0); //Start

  // オーバーフロー割込み許可
  pcnt_isr_register(pcnt_intr_handler, NULL, 0, &user_isr_handle);
  pcnt_intr_enable(PCNT_UNIT_0);
 
  // タイムベース信号用PWM出力の設定
  ledcSetup(LEDC_CHANNEL_0, LEDC_BASE_FREQ, LEDC_TIMER_BIT);
  ledcAttachPin(TIME_BASE, LEDC_CHANNEL_0);
  ledcWrite(LEDC_CHANNEL_0, LEDC_DUTY);

  attachInterrupt(LATCH_EN, latch_fcount, FALLING);

}


void loop(void)
{
  frequency=(int)( (double)fcount * F_ADJ ) * F_RESO;
  Serial.println(frequency);
}

GPIO4に信号を入力する.GPIO5はTime base 信号出力,ゲート信号入力,ラッチトリガに使用している.GPIO5はNCにしておくこと.


カウンタ,タイマ,PWMモジュールの初期化が大部分で,あとは割込みでカウンタのオーバーフローの計数とゲートが閉じたあとのカウント値の算出をしている.

Time Base信号として ledc を使ってPWM信号を生成しているが,周波数カウンタとして必要な精度では設定できない(このような用途は想定されていないのだろう).どれくらいの誤差が発生するかは調べていない.ledc に拠らずPWMモジュールにバインドされているTimerのレジスタに直接分周比を書けば正確な設定ができると思う.が基準発振がCPUクロックなのでそもそも誤差があるだろうし,面倒なのでF_ADJ という定数をかけて合わせ込むことにした.
F_RESOを変更すれば一応分解能を変えられるが,変えると誤差も変わるだろうし対応が面倒なのでラジオ組み込み用なら10Hz固定でいいだろう.
計測用にするなら,PWMをなくして(ledc 関連のコードを削除),GPIO5 に外部から 高精度のTime Base 信号を入れる方がいい.
また,カウントの上限が31MHz程度だが,パルスカウンタを4個持っていてそれぞれに入力が2chあるので,それらにパルスを振り分けてカウントさせてやれば分解能を落とさずに上限周波数を上げられる.

ESP32-S3 AF信号処理ボード(SSB生成)

SSBの生成のプログラムを作成し信号を観測した.プログラムは下図に従って作成.

入力信号は 1.5kHz の正弦波.キャリア周波数 24kHz でUSBを生成した.
よって出力は 25.5kHz の正弦波となる.

スペクトラムの実測結果は以下のとおり.

完全ディジタル処理によるSSB生成なので当然ではあるが,キャリア(24kHz)も逆サイドバンド(22.5kHz)もまったく観測できない.

測定側のダイナミックレンジが十分ではないが,少なくとも -70dB は抑圧されている.無調整で(調整のやりようがないが)十分な性能が得られている.

帯域を広げていくと少し問題点が見えてくる.

まず,100kHz まで観測すると信号(25.5kHz)の高調波が見える.

これはDAC(PCM5102A)の出力が 500mVpp のときだが,2次高調波は -60 数dB .
これくらいであれば問題ないが,DAC出力を大きくすると悪化していく.

ちなみに,2Vpp のときには -50dB となった.送信時はDAC出力レベルは控えめがよさそう.

さらに観測帯域を 1MHz まで広げると下図のようになった.

384kHzとその整数倍の前後にスプリアスが生じている.これはPCM5102Aが最終的に384kHzまでオーバーサンプリングして信号を出力しているためで,波形を見ればよくわかる(下図)

0次ホールド特性どおりのスペクトラムといえる.

結論から言えば,問題点はすべてDACに起因するもので,実際に使用する場合は DACの出力に適切なフィルタが必要だろう.

ESP32-S3 AF信号処理ボード(全モード復調)

記事中のプログラムは,Arduino IDE 2.1.1, Board Manager esp32 ver. 2.0.9 で動作確認しています.

AM,FMの復調処理を加えて,クラシックなすべてのモードの復調ができるようにした.前の記事と重複する内容があるがあらためて記載する.

まず,デシメーションまでの構成は下図のとおり.

DeMod. のブロックに各モードの復調処理が入る.内容は以下のとおり.

SSBの復調は以下のようなコードとなる.(前の記事と同じ)

     // USB
      dsps_fir_f32(&sfirRe, Lch_in, Lch_out, BLOCK_SAMPLES/DOWN_SAMPLE);
      dsps_fir_f32(&sfirIm, Rch_in, Rch_out, BLOCK_SAMPLES/DOWN_SAMPLE);
      for(int i=0; i<BLOCK_SAMPLES/DOWN_SAMPLE; i++){
        Lch_in[i] =Lch_out[i] - Rch_out[i];
      }
      
      // LSB
      dsps_fir_f32(&sfirRe, Lch_in, Lch_out, BLOCK_SAMPLES/DOWN_SAMPLE);
      dsps_fir_f32(&sfirIm, Rch_in, Rch_out, BLOCK_SAMPLES/DOWN_SAMPLE);
      for(int i=0; i<BLOCK_SAMPLES/DOWN_SAMPLE; i++){
        Lch_in[i] = Lch_out[i] + Rch_out[i]; 
      }
      

複素係数フィルタをとおして,実部と虚部を加算するだけなので非常にシンプル.

AMの復調は以下のとおり.

      // AM
      dsps_fir_f32(&sfirAMRe, Lch_in, Lch_out, BLOCK_SAMPLES/DOWN_SAMPLE);
      dsps_fir_f32(&sfirAMIm, Rch_in, Rch_out, BLOCK_SAMPLES/DOWN_SAMPLE);
      for(int i=0; i<BLOCK_SAMPLES/DOWN_SAMPLE; i++){
        Re=Lch_out[i];
        Im=Rch_out[i];
        float Demod_AM = sqrt(Re*Re+Im*Im); 
        Rch_in[i] = Demod_AM*0.003 + zDC*0.997; // DC component
        zDC = Rch_in[i];                        // DC component
        Lch_in[i] = 3.0f*(Demod_AM - zDC);
      }

複素係数フィルタの代わりに実部,虚部を同じ特性のLPFにとおし(デシメーションフィルタがあるのでこれはなくてもよい),その振幅 Demod_AM を求めている. Demod_AM にはキャリアに相当するDC成分が含まれているのでそれをカットし出力としている.
zDC は float のグローバル変数.

FMの復調は,一般的には信号の位相を求めてそれを微分する.微分は1サンプル前のデータの位相との差をとればよい.その通りやればいいのだが「微分=ノイズ増長」というイメージがあってやりたくなかったので別の方法でFM復調をすることにした.

具体的には上の図にあるように,1サンプル前のデータの複素共役を掛けてその偏角を求める.複素数同士の乗算ではその偏角は加算されるが,複素共役をとった場合は偏角は差分( ≃ 微分)となる.これによって差分や除算なしにFMの復調処理ができた.一般的な方法と比べてどれほどの効果があるかは確認していない.コードは以下のとおり.

      // FM
      for(int i=0; i<BLOCK_SAMPLES/DOWN_SAMPLE; i++){
        Re = Lch_in[i]*zRe + Rch_in[i]*zIm;
        Im = Rch_in[i]*zRe - Lch_in[i]*zIm;
        zRe = Lch_in[i];
        zIm = Rch_in[i];
        Lch_in[i] = 1e5 * atan2(Im, Re) * (float)(fsample/DOWN_SAMPLE);
      }

zRE,zIm は,float のグローバル変数.

ESP32-S3 AF信号処理ボード(SSB復調)

記事中のプログラムは,Arduino IDE 2.1.1, Board Manager esp32 ver. 2.0.9 で動作確認しています.

24kHzのSSB信号を復調する処理を組み込んでみた.
構成は下図のとおり.

96kHzサンプリングで入力した信号をダウンコンバートしたあとダウンサンプリングしているので,計算量はESP32でもまだ余裕があるくらいに抑えることができている.

コードは以下のとおり.
ただし各フィルタの係数に関してはまだ検討の余地があるため値を設定する処理は含めていない.
(雛形,参考用として開示しますので詳細な動作の説明やプログラムの追加,修正を求めることはご遠慮願います.)

// 24kHz SSB RX on ESP32-S3
// T.Uebo  October 17 , 2022

// I2S Master MODE 96kHz/32bit
//
//   Mclk  GPIO 39
//   Bclk  GPIO 42
//   LRclk GPIO 2
//   Dout  GPIO 41
//   Din   GPIO 1

#include <esp_dsp.h>
#include <driver/i2s.h>
#define fsample 96000
#define DOWN_SAMPLE 6
#define BLOCK_SAMPLES (DOWN_SAMPLE*16)

//buffers
int rxbuf[BLOCK_SAMPLES*2], txbuf[BLOCK_SAMPLES*2];
float Lch_in[BLOCK_SAMPLES], Rch_in[BLOCK_SAMPLES];
float Lch_out[BLOCK_SAMPLES], Rch_out[BLOCK_SAMPLES];

// Decimation Filter
#define Ndec 512
float dec_fir_coeff[Ndec];   // フィルタ係数
float z_decL[Ndec], z_decR[Ndec];
fir_f32_t sfir_decL, sfir_decR;

// Complex coeff. Filter
#define Ncmplx 256
float CF_Re[Ncmplx], CF_Im[Ncmplx];  // フィルタ係数
float z_firRe[Ncmplx], z_firIm[Ncmplx];
fir_f32_t sfirRe, sfirIm;

// Anti-aliasing Filter
float IIR_coeff0[5], IIR_coeff1[5], IIR_coeff2[5], IIR_coeff3[5];  // フィルタ係数
float z_IIR0[2], z_IIR1[2], z_IIR2[2], z_IIR3[2];

// 24kHz Local OSC.
#define N_LO 4 
float LO_I[N_LO]={1.0f, 0, -1.0f, 0};
float LO_Q[N_LO]={0, -1.0f, 0, 1.0f};
int pt_LO = 0;

// for FFT
#define Nfft 4096
float dat[Nfft*2];
float wf[Nfft];
float pow_sp[Nfft];

//Ring Buffer
#define Ring_Buf_Size (Nfft+256)
volatile float d_Lch[Ring_Buf_Size];
volatile float d_Rch[Ring_Buf_Size];
volatile int pt = 0;


/*-----------------------------------------------------------------------------------------------
  Setup
-------------------------------------------------------------------------------------------------*/
void setup(void) {
  Serial.begin(115200);
  delay(50);

  digitalWrite(48, LOW);
  pinMode(48, OUTPUT);
  digitalWrite(48, LOW);
 

  // I2S setup (Lables are defined in i2s_types.h, esp_intr_alloc.h)----------------------------------------
  i2s_config_t i2s_config = {
    .mode =  (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX  | I2S_MODE_RX),
    .sample_rate = fsample,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LOWMED,
    .dma_buf_count = 2,
    .dma_buf_len = BLOCK_SAMPLES*4, //*2
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0,
    .mclk_multiple = I2S_MCLK_MULTIPLE_256,
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL);

  i2s_pin_config_t pin_config = {
        .mck_io_num = 39,
        .bck_io_num = 42,
        .ws_io_num = 2,
        .data_out_num = 41,
        .data_in_num = 1                                                       
    };
  i2s_set_pin( I2S_NUM_0, &pin_config);

  // set up FIR --------------------------------------------------------
  // Decimation filter
  dsps_fird_init_f32(&sfir_decL, dec_fir_coeff, z_decL, Ndec, DOWN_SAMPLE, 0);
  dsps_fird_init_f32(&sfir_decR, dec_fir_coeff, z_decR, Ndec, DOWN_SAMPLE, 0);
  // Complex FIR Filter
  dsps_fir_init_f32(&sfirRe, CF_Re, z_firRe, Ncmplx);
  dsps_fir_init_f32(&sfirIm, CF_Im, z_firIm, Ncmplx);
}


/*-----------------------------------------------------------------------------------------------
  Signal Process Loop
-------------------------------------------------------------------------------------------------*/
void loop(void) {
  size_t readsize = 0;

  //Input from I2S codec
  esp_err_t rxfb = i2s_read(I2S_NUM_0, &rxbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);
  if (rxfb == ESP_OK && readsize==BLOCK_SAMPLES*2*4) {

    digitalWrite(48, HIGH);

    int j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      Lch_in[i] = (float) rxbuf[j];
      Rch_in[i] = (float) rxbuf[j+1];
      j+=2;

      d_Lch[pt]=Lch_in[i];
      d_Rch[pt]=Rch_in[i];
      pt++; if( pt == Ring_Buf_Size ) pt=0;
    }
    
    //-------Signal process -------------------------------
    
    // for (int i=0; i<BLOCK_SAMPLES; i++) {
    //   Lch_out[i] = Lch_in[i];
    //   Rch_out[i] = Rch_in[i];    
    // }

    // Down conversion,  24kHz to 0
    for(int i=0; i<BLOCK_SAMPLES; i++){
      Lch_out[i] = Lch_in[i]*LO_I[pt_LO] - Rch_in[i]*LO_Q[pt_LO];
      Rch_out[i] = Rch_in[i]*LO_I[pt_LO] + Lch_in[i]*LO_Q[pt_LO];
      pt_LO++; if(pt_LO==N_LO) pt_LO=0;
    }
    
    // Decimation (Down sampling) 
    dsps_fird_f32(&sfir_decL, Lch_out, Lch_in, BLOCK_SAMPLES);
    dsps_fird_f32(&sfir_decR, Rch_out, Rch_in, BLOCK_SAMPLES);
  
    // Demod.  -----------------------------------------------------------------------
    // SSB/CW
    // Complex coeff. Filter
    dsps_fir_f32(&sfirRe, Lch_in, Lch_out, BLOCK_SAMPLES/DOWN_SAMPLE);
    dsps_fir_f32(&sfirIm, Rch_in, Rch_out, BLOCK_SAMPLES/DOWN_SAMPLE);

    for(int i=0; i<BLOCK_SAMPLES/DOWN_SAMPLE; i++){
      Lch_in[i] = Lch_out[i] - Rch_out[i];
      //Lch_in[i] = Lch_out[i] + Rch_out[i]; 
    }
    // Demod. output : Lch_in[]


    // UP sampling (Interpolation)---------------------------------------
    for(int i=BLOCK_SAMPLES/DOWN_SAMPLE-1; i>=0;  i--){
      Lch_out[i*DOWN_SAMPLE]=Lch_in[i];
      for(int j=1; j<DOWN_SAMPLE; j++){
        Lch_out[i*DOWN_SAMPLE+j]=0;
      }
    }
    // Anti-aliasing Filter
    dsps_biquad_f32(Lch_out, Lch_in, BLOCK_SAMPLES, IIR_coeff0, z_IIR0);
    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, IIR_coeff1, z_IIR1);
    dsps_biquad_f32(Lch_out, Lch_in, BLOCK_SAMPLES, IIR_coeff2, z_IIR2);
    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, IIR_coeff3, z_IIR3);

    //Output to I2S codec
    j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      txbuf[j]   = (int) Lch_out[i];
      txbuf[j+1] = (int) Lch_out[i];
      j+=2;

      // d_Lch[pt]=Lch_out[i];
      // d_Rch[pt]=Rch_out[i];
      // pt++; if( pt == Ring_Buf_Size ) pt=0;
    }

    i2s_write( I2S_NUM_0, &txbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);

  }
  digitalWrite(48, LOW);
}

とりあえず問題なく動作しているので,今後詳細に動作を確認していくことにする.
特にADCに内蔵の入力フィルタの切れがあまりよくなさそうで,48kHz以上が折り返してきて56kHzくらいでようやく見えなくなるのが気になる.

ESP32-S3 AF信号処理ボード(デシメーションフィルタ)

記事中のプログラムは,Arduino IDE 2.1.1, Board Manager esp32 ver. 2.0.9 で動作確認しています.

Teensy4はデシメーションの必要がないくらい処理性能が高いが,ESP32-S3はTeensy4に比べて非力なので,デシメーションが必要となるケースが多いかもしれない.

ライブラリには Decimation FIR filter が用意されているので試してみた.デシメーションフィルタは普通のFIRフィルタに比べ,同じタップ数でもダウンサンプル比をNとすれば,ポリフェーズ分解によってフィルタ処理自体も理想的には1/Nになる.またサンプリング周波数も以降は1/Nとなるのであとの処理も軽くなる.

以下のコードを実行させて,ダウンサンプル比4のデシメーションフィルタと通常のFIRフィルタの処理時間と入出力信号を確認した.タップ数はいずれも1024.

#include <esp_dsp.h>

#define BLOCK_SAMPLES 64 
#define Ndec 1024

fir_f32_t fir_dec;
float coeffs_fir_dec[Ndec];
float z_fir_dec[Ndec];

void setup(void) {
  Serial.begin(115200);
  delay(50);

  digitalWrite(48, LOW);
  pinMode(48, OUTPUT);
  digitalWrite(48, LOW);  

  float si[BLOCK_SAMPLES];   // Input signal
  float so[BLOCK_SAMPLES]; // Output of FIR
  float sod[BLOCK_SAMPLES]; // Output of Decimation FIR

  // 入力信号(SIN波)
  for(int i; i<BLOCK_SAMPLES; i++){
    si[i] = sin(2.0f*2.0f*M_PI*(float)i/BLOCK_SAMPLES);
  }

  // FIR 係数(入力を2倍にするフィルタ)
  dsps_d_gen_f32(coeffs_fir_dec, Ndec, 0);
  coeffs_fir_dec[0] =2.0f;

  // fir_dec をデシメーションフィルタとして初期化
  //(ダウンサンプル比4) 
  dsps_fird_init_f32(&fir_dec, coeffs_fir_dec, z_fir_dec, Ndec, 4, 0);
    digitalWrite(48, HIGH);
  dsps_fird_f32(&fir_dec, si, sod, BLOCK_SAMPLES);
    digitalWrite(48, LOW);

  // fir_dec を通常のFIRフィルタとして初期化 
  dsps_fir_init_f32(&fir_dec, coeffs_fir_dec, z_fir_dec, Ndec);
    digitalWrite(48, HIGH);
  dsps_fir_f32(&fir_dec, si, so, BLOCK_SAMPLES);
    digitalWrite(48, LOW);
    
   for(int i; i<BLOCK_SAMPLES; i++){
     Serial.printf("%f %f %f;\n",si[i], so[i], sod[i] );
   }
}

void loop(void) {
}

実行時間は下図のとおり,デシメーションフィルタは通常のフィルタに比べおよそ1/3の処理時間になった.1/4でないのはオーバーヘッドなどを考えれば当然かと思うが,デシメーションFIRフィルタによってダウンサンプルを同時に行えば,処理時間をかなり減らすことができそう.もちろん信号帯域は狭くなるが.

入出力信号は下図のようになった.サンプル数が64 から1/4 の16に減少しているのがわかる(17個目以降はゴミ).フィルタ処理としては通常のFIRと同じ処理が行われている(2倍).

ESP32-S3 AF信号処理ボード(FFT)

記事中のプログラムは,Arduino IDE 2.1.1, Board Manager esp32 ver. 2.0.9 で動作確認しています.

Espressif DSP Library のFFT関数を使ってみた.ポイント数は4096までサポートされている.信号処理のパフォーマンスを落としたくないのでFFTは信号処理とは別のコアで実行させたい.信号はリングバッファ経由で渡すようにした.

// I2S on ESP32-S3
// T.Uebo  October 12 , 2022

// I2S Master MODE 48kHz/32bit
//
//   Mclk  GPIO 39
//   Bclk  GPIO 42
//   LRclk GPIO 2
//   Dout  GPIO 41
//   Din   GPIO 1

#include <esp_dsp.h>
#include <driver/i2s.h>
#define fsample 48000
#define BLOCK_SAMPLES 64
#define MUTE 40 // MUTE control (LOW: Mute)

//buffers
int rxbuf[BLOCK_SAMPLES*2], txbuf[BLOCK_SAMPLES*2];
float Lch_in[BLOCK_SAMPLES], Rch_in[BLOCK_SAMPLES];
float Lch_out[BLOCK_SAMPLES], Rch_out[BLOCK_SAMPLES];

// for Filter work
float zL0[2], zR0[2], coeffs0[5];
float zL1[2], zR1[2], coeffs1[5];

#define Nfir 512
fir_f32_t firL_st, firR_st;
float coeffs_firL[Nfir], coeffs_firR[Nfir];
float z_firL[Nfir], z_firR[Nfir];

#define Nfft 4096
float dat[Nfft*2];
float wf[Nfft];          // 窓関数
float pow_sp[Nfft];  // パワースペクトル

//リングバッファ
#define Ring_Buf_Size (Nfft+256)
volatile float d_Lch[Ring_Buf_Size];
volatile float d_Rch[Ring_Buf_Size];
volatile int pt = 0; // リングバッファのポインタ


TaskHandle_t H_Task;//タスクハンドル


/*--------------------------------------------------
   core 0 で処理するTask   Alt_Thread()
---------------------------------------------------*/
void Alt_Thread(void *args) {

  dsps_fft2r_init_fc32(NULL, Nfft);  // 回転因子生成
  dsps_wind_hann_f32(wf, Nfft);   // 窓関数生成

  /*----------------------------------------------------
    Loop
  ------------------------------------------------------*/
  while (1) {
    // リングバッファから信号をコピー
    int ptc = pt;
    ptc -= Nfft; if(ptc < 0) ptc += Ring_Buf_Size;
    int j=0;
    for(int i = 0; i<Nfft; i++){
      dat[j]   = d_Lch[ptc];       // Re 
      dat[j+1] = d_Rch[ptc];    //Im
      ptc++; if(ptc == Ring_Buf_Size) ptc=0;
      j+=2;
    }

    // 窓関数を掛ける
    j=0;
    for(int i = 0; i<Nfft; i++){
      dat[j]   *= wf[i];
      dat[j+1] *= wf[i];
      j+=2;
    }

    // FFT
    dsps_fft2r_fc32(dat, Nfft);
    dsps_bit_rev2r_fc32(dat, Nfft);
    
    // パワースペクトルの計算
    float Re, Im;
    j=0;
    for(int i=0; i<Nfft; i++){
      Re=dat[j]; Im=dat[j+1];
      pow_sp[i] = Re*Re + Im*Im;
      pow_sp[i]=10.0f*log10(pow_sp[i]) -120;
      if(pow_sp[i]<0) pow_sp[i]=0;
      j+=2;
    }
    // パワースペクトル[dB] pow_sp[i]

    delay(1);
  }

}



/*-----------------------------------------------------------------------------------------------
  Setup
-------------------------------------------------------------------------------------------------*/
void setup(void) {
  Serial.begin(115200);
  delay(50);

  xTaskCreatePinnedToCore(Alt_Thread, "Alt_Thread", 8192, NULL, 4, &H_Task, 0); 

  //Mute Control
  pinMode(MUTE, OUTPUT);
  digitalWrite(MUTE, HIGH); //unmute

  pinMode(48, OUTPUT);

  //-- Filter settings -------------------------------------
  // set IIR Filter Coeffs. 
  dsps_biquad_gen_lpf_f32(coeffs0, 0.45f, 0.71f);
  dsps_biquad_gen_lpf_f32(coeffs1, 0.45f, 0.71f);

  // set up FIR
  dsps_fir_init_f32(&firL_st, coeffs_firL, z_firL, Nfir);
  dsps_fir_init_f32(&firR_st, coeffs_firR, z_firR, Nfir);
  // set FIR Filter Coeffs. 
  dsps_d_gen_f32(coeffs_firL, Nfir, 0);
  dsps_d_gen_f32(coeffs_firR, Nfir, 0);


  // I2S setup (Lables are defined in i2s_types.h, esp_intr_alloc.h)----------------------------------------
  i2s_config_t i2s_config = {
    .mode =  (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX  | I2S_MODE_RX),
    .sample_rate = fsample,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LOWMED,
    .dma_buf_count = 2,
    .dma_buf_len = BLOCK_SAMPLES*4,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0,
    .mclk_multiple = I2S_MCLK_MULTIPLE_256,
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL);

  i2s_pin_config_t pin_config = {
        .mck_io_num = 39,
        .bck_io_num = 42,
        .ws_io_num = 2,
        .data_out_num = 41,
        .data_in_num = 1                                                       
    };
  i2s_set_pin( I2S_NUM_0, &pin_config);

}


/*-----------------------------------------------------------------------------------------------
  Signal Process Loop
-------------------------------------------------------------------------------------------------*/
void loop(void) {
  size_t readsize = 0;

  //Input from I2S codec
  esp_err_t rxfb = i2s_read(I2S_NUM_0, &rxbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);
  if (rxfb == ESP_OK && readsize==BLOCK_SAMPLES*2*4) {

    digitalWrite(48, HIGH);

    int j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      Lch_in[i] = (float) rxbuf[j];
      Rch_in[i] = (float) rxbuf[j+1];
      j+=2;
    }   
    
    //-------Signal process -------------------------------
    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, coeffs0, zL0);
    dsps_biquad_f32(Rch_in, Rch_out, BLOCK_SAMPLES, coeffs0, zR0);

    dsps_biquad_f32(Lch_out, Lch_in, BLOCK_SAMPLES, coeffs1, zL1);
    dsps_biquad_f32(Rch_out, Rch_in, BLOCK_SAMPLES, coeffs1, zR1);

    dsps_fir_f32(&firL_st, Lch_in, Lch_out, BLOCK_SAMPLES);
    dsps_fir_f32(&firR_st, Rch_in, Rch_out, BLOCK_SAMPLES);

    //Output to I2S codec
    j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      txbuf[j]   = (int) Lch_out[i];
      txbuf[j+1] = (int) Rch_out[i];
      j+=2;
    }
    i2s_write( I2S_NUM_0, &txbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);


    //core0 のTaskでFFTを実行するために信号をリングバッファにコピー
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      d_Lch[pt]=Lch_out[i];
      d_Rch[pt]=Rch_out[i];
      pt++; if( pt == Ring_Buf_Size ) pt=0;
    }


  }

  digitalWrite(48, LOW);
}

FFTの処理をまとめると以下とおり.
1.#define Nfft でポイント数定義
2.配列を準備 float dat[Nfft*2]; float wf[Nfft];
3.dsps_fft2r_init_fc32(NULL, Nfft); で回転因子を生成
4.dsps_wind_hann_f32(wf, Nfft); で窓関数生成.(hann 窓しか用意されてない)
5.処理する信号に窓関数を掛けてdat[ ]に入れる.実部:偶数番目,虚部:奇数番目
6.dsps_fft2r_fc32(dat, Nfft); FFT処理
7.dsps_bit_rev2r_fc32(dat, Nfft); ビットリバース処理
8.結果は,dat[ ] に上書きされる.実部:偶数番目,虚部:奇数番目

あとは必要に応じてパワースペクトルにするなりすればOK.上のコードでは 最終 pow_sp[ ] にパワースペクトルを入れている.

ビットリバース関数が用意されているが,これを使う必要があるかどうかドキュメントからは読み取れなかった.動作させて確認したところ dsps_fft2r_fc32にはビットリバース処理は含まれておらず,この関数は必須だった.また1/Nfftの係数もかかっていないようで,結局 dsps_fft2r_fc32 は純粋にバタフライ演算のみをしていると思われる.いちいち係数をかけるのもめんどうなので上のコードではパワーをdBに変換した後で適当な定数を引いている.

ESP32-S3 AF信号処理ボード(I2S レイテンシー)

I2Sのレイテンシーをチェックした.楽器用エフェクタの場合レイテンシーが大きいと演奏に支障をきたすなど問題が大きい.いろんな意見をいただいた結果 2ms 以下を目標としている.

一方,SDR受信機単独なら数10msのレイテンシーは問題ないだろう.トランシーバーの場合は送受切り替えがあるのでレイテンシーが大きいのはまずいがどの辺が限界かよくわからない.CWの高速短点よりは十分小さい方がよいのかもしれない.

ちなみに Teensy 4.0で同様のシステムを組んだ場合は, fs=48kHz,BLOCK_SAMPLES=16 のとき 1.88ms だった.

ESP32-S3 では fs=48kHz,BLOCK_SAMPLES=64 としているが,このとき下図のとおりレイテンシーは 32.6ms で 2ms よりはるかに大きくなってしまった(信号周波数は1kHz).

そこで BLOCK_SAMPLES=4 とし DMAバッファ数も限界まで減らた ( .dma_buf_count = 2 )ところ,下図のとおり 1.86ms となり 2ms を切ることができた.

この値は Teesy4.0ではBLOCK_SAMPLES=16で実現できたのだが… ESP32-S3 では 4ブロックごとに処理しているようなので実質 BLOCK_SAMPLES=4 の 4倍で 16 ということか?

BLOCK_SAMPLES が少ないと I2S 処理のオーバーヘッドの割合が増し,信号処理量の限界が下がる.ちなみにこの状態だと IIR Biquad x 20個,512Tap FIR x 2個 は処理しきれない.400Tap FIR x 2 くらいまで減らせばOKだった.

ESP32-S3 AF信号処理ボード(マルチタスク)

ESP32は Dual Core なので1つのコアを信号処理に専念させて,他の処理を別のコアで処理すればパフォーマンスがよくなる.
setup(), loop() はCore1 で実行される.ここには信号処理のみ記述することにして,Core0で実行するTaskにその他の処理を入れていくことにする.

参考にさせていただいたサイト
mgo-tec電子工作
ESP32でマルチコアを試す

Core0で実行するTaskを追加したコードを下に示す.

// I2S on ESP32-S3
// T.Uebo  October 7, 2022

// I2S Master MODE 48kHz/32bit
//
//   Mclk  GPIO 39
//   Bclk  GPIO 42
//   LRclk GPIO 2
//   Dout  GPIO 41
//   Din   GPIO 1

#include <esp_dsp.h>
#include <driver/i2s.h>
#define fsample 48000
#define BLOCK_SAMPLES 64
#define MUTE 40 // MUTE control (LOW: Mute)


//buffers
int rxbuf[BLOCK_SAMPLES*2], txbuf[BLOCK_SAMPLES*2];
float Lch_in[BLOCK_SAMPLES], Rch_in[BLOCK_SAMPLES];
float Lch_out[BLOCK_SAMPLES], Rch_out[BLOCK_SAMPLES];

// for IIR Filter
float zL0[2], zR0[2], coeffs0[5];


TaskHandle_t H_Task;  //タスクハンドル

/*--------------------------------------------------
   core 0 で処理するTask   Alt_Thread()
---------------------------------------------------*/
void Alt_Thread(void *args) {
  //
  //  このTask の setup() に相当する処理
  //
  float fc;
  static int tc;

  tc=0;

  while (1) { 
    //
    //  この Task の loop() に相当する処理
    //
    fc = (float)tc/30000.0f;
    dsps_biquad_gen_lpf_f32(coeffs0, fc, 1.0f); 
    tc++; if(tc>=3000) tc=0;


    delay(1);  // 必須
  }

}


/*-----------------------------------------------------------------------------------------------
  Setup
-------------------------------------------------------------------------------------------------*/
void setup(void) {
  Serial.begin(115200);
  delay(50);
   
  xTaskCreatePinnedToCore(Alt_Thread, "Alt_Thread", 8192, NULL, 4, &H_Task, 0);
  // 
  // xTaskCreatePinnedToCore(
  //     [タスク名],  "[タスク名]",  [スタックメモリサイズ],
  //     NULL,  [タスク優先順位](0-24)],
  //     [タスクハンドルのポインタ(&H_Task)],
  //     [Core ID(0 or 1)]
  //  ); 

  
  //Mute Control
  pinMode(MUTE, OUTPUT);
  digitalWrite(MUTE, HIGH); //unmute

  pinMode(48, OUTPUT);

  // set IIR Filter Coeffs.(as LPF)
  dsps_biquad_gen_lpf_f32(coeffs0, 0.1f, 1.0f); // fc = 0.1*fsample[Hz], Q=1.0


  // I2S setup (Lables are defined in i2s_types.h, esp_intr_alloc.h)----------------------------------------
  i2s_config_t i2s_config = {
    .mode =  (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX  | I2S_MODE_RX),
    .sample_rate = fsample,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LOWMED,
    .dma_buf_count = 6,
    .dma_buf_len = BLOCK_SAMPLES*4,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0,
    .mclk_multiple = I2S_MCLK_MULTIPLE_256,
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL);

  i2s_pin_config_t pin_config = {
        .mck_io_num = 39,
        .bck_io_num = 42,
        .ws_io_num = 2,
        .data_out_num = 41,
        .data_in_num = 1                                                       
    };
  i2s_set_pin( I2S_NUM_0, &pin_config);

}


/*-----------------------------------------------------------------------------------------------
  Signal Process Loop
-------------------------------------------------------------------------------------------------*/
void loop(void) {
  size_t readsize = 0; 
 
  //Input from I2S codec
  esp_err_t rxfb = i2s_read(I2S_NUM_0, &rxbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);
  if (rxfb == ESP_OK && readsize==BLOCK_SAMPLES*2*4) {

    digitalWrite(48, HIGH);

    int j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      Lch_in[i] = (float) rxbuf[j];
      Rch_in[i] = (float) rxbuf[j+1];
      j+=2;
    }   
    
    //-------Signal process -------------------------------
    //IIR Filter
    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, coeffs0, zL0);
    dsps_biquad_f32(Rch_in, Rch_out, BLOCK_SAMPLES, coeffs0, zR0);
    //------------------------------------------------------

    //Output to I2S codec
    j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      txbuf[j]   = (int) Lch_out[i];
      txbuf[j+1] = (int) Rch_out[i];
      j+=2;
    }
    i2s_write( I2S_NUM_0, &txbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY); 

  }
  digitalWrite(48, LOW);
}

追加分はハイライト表示している.まとめると,
1.setup() の前にタスクハンドルを定義 TaskHandle_t H_Task;
2.setup() の中にTask 宣言をする一文を入れる.xTaskCreatePinnedToCore
3.宣言したTaskの関数を記述 Alt_Thread()

Alt_Thread() では,一例として IIR LPF のカットオフ周波数をスイープする処理を記述した(動作確認済).

ESP32-S3 AF信号処理ボード(I2Sの設定)

I2S接続のCODECはとりあえず動いているが,設定に関してもう少し把握しておきたいので調べてみた.具体的にはヘッダファイル i2s.h じっくりながめただけ.

気になっていたのは,MCLKに関しては何も設定を書いていないのにGPIO0から出ていること.GPIO0はストラップピンで boot スイッチにつながっているので気持ちが悪い. ESP32-S3のスペックとしてはどのピンにもアサインできるので MCLK は別のポートに変更したい.

参考にしたサイトのコードにはそれらしい部分はなかったが,ヘッダファイルをみると i2s_pin_config_t; のメンバーの中に int mck_io_num; がいた.
そこで下のように自分のソースコードに 1行(ハイライト部分)追加した.

  i2s_pin_config_t pin_config = {
        .mck_io_num = 39,
        .bck_io_num = 42,
        .ws_io_num = 2,
        .data_out_num = 41,
        .data_in_num = 1
    };
  i2s_set_pin( I2S_NUM_0, &pin_config);

これで,MCLK がGPIO39から出てくるようになった.
回路も修正して問題なく動作している.

もう一つ,MCLKの周波数も特に設定していないが256fs が出てきている.これは変更することはないと思うが確認しておかないと気持ちが悪い.

こちらも i2s_config_t のメンバーに mclk_multiple がいるようで,これをセットすればよさそう.

  i2s_config_t i2s_config = {
    .mode =  (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX  | I2S_MODE_RX),
    .sample_rate = fsample,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .intr_alloc_flags = ESP_INTR_FLAG_LOWMED,
    .dma_buf_count = 6,
    .dma_buf_len = BLOCK_SAMPLES*4,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0,
    .mclk_multiple = I2S_MCLK_MULTIPLE_256,
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL);

その他の変更はないが,i2s_types.h, esp_intr_alloc.h を参照してそこで定義されているものはラベルに変更した.

ESP32-S3 AF信号処理ボード(DSPライブラリ)

記事中のプログラムは,Arduino IDE 2.1.1, Board Manager esp32 ver. 2.0.9 で動作確認しています.

信号処理をするにはDSPライブラリがあると大変ありがたい,というか必須かもしれない.ESP32ではどうなっているのか調べると,Espressif DSP Library があり一通りメジャーな処理は用意されている.ただ,Arduino IDE で使えるかどうかが問題.

ESP32のボードマネージャがインストールされたフォルダを調べてみると,かなり深い階層の非常に見つけ難いところに,ヘッダファイル(esp_dsp.h)が置かれていた.どうやらソースコードはないがライブラリも入っている様子.

試しに,IIRフィルタ(2次LPF)を実装してみた.
コードを下に示す.前回からに変更部分はハイライト表示してある.

// I2S on ESP32-S3
// T.Uebo  October 1 , 2022

// I2S Master MODE 48kHz/32bit
//
//   Mclk  GPIO 0
//   Bclk  GPIO 42
//   LRclk GPIO 2
//   Dout  GPIO 41
//   Din   GPIO 1

#include <esp_dsp.h>
#include <driver/i2s.h>
#define fsample 48000
#define BLOCK_SAMPLES 64
#define MUTE 40 // MUTE control (LOW: Mute)


//buffers
int rxbuf[BLOCK_SAMPLES*2], txbuf[BLOCK_SAMPLES*2];
float Lch_in[BLOCK_SAMPLES], Rch_in[BLOCK_SAMPLES];
float Lch_out[BLOCK_SAMPLES], Rch_out[BLOCK_SAMPLES];

// for IIR Filter
float zL0[2], zR0[2];
float coeffs[5];


/*-----------------------------------------------------------------------------------------------
  Setup
-------------------------------------------------------------------------------------------------*/
void setup(void) {
  Serial.begin(115200);
  delay(50);

  //Mute Control
  pinMode(MUTE, OUTPUT);
  digitalWrite(MUTE, HIGH); //unmute

  pinMode(48, OUTPUT);

  // set IIR Filter Coeffs.(as LPF)
  dsps_biquad_gen_lpf_f32(coeffs, 0.01f, 1.0f); // fc = 0.01*fsample[Hz], Q=1.0


  // I2S setup ------------------------------------------------------------
  i2s_config_t i2s_config = {
    .mode =  (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX  | I2S_MODE_RX),
    .sample_rate = fsample,
    .bits_per_sample = (i2s_bits_per_sample_t)32,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0,
    .dma_buf_count = 6,
    .dma_buf_len = BLOCK_SAMPLES*4,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0,
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL);

  i2s_pin_config_t pin_config = {
        .bck_io_num = 42,
        .ws_io_num = 2,
        .data_out_num = 41,
        .data_in_num = 1                                                       
    };
  i2s_set_pin( I2S_NUM_0, &pin_config);

}


/*-----------------------------------------------------------------------------------------------
  Signal Process Loop
-------------------------------------------------------------------------------------------------*/
void loop(void) {
  size_t readsize = 0; 
 
  //Input from I2S codec
  esp_err_t rxfb = i2s_read(I2S_NUM_0, &rxbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);
  if (rxfb == ESP_OK && readsize==BLOCK_SAMPLES*2*4) {

    digitalWrite(48, HIGH);

    int j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      Lch_in[i] = (float) rxbuf[j];
      Rch_in[i] = (float) rxbuf[j+1];
      j+=2;
    }   
    
    //-------Signal process -------------------------------
    //IIR Filter
    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, coeffs, zL0);
    dsps_biquad_f32(Rch_in, Rch_out, BLOCK_SAMPLES, coeffs, zR0);

    //------------------------------------------------------


    //Output to I2S codec
    j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      txbuf[j]   = (int) Lch_out[i];
      txbuf[j+1] = (int) Rch_out[i];
      j+=2;
    }
    i2s_write( I2S_NUM_0, &txbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY); 

  }
  digitalWrite(48, LOW);
}

I2Sで24bit整数データを取得し,float 32bit で処理しそれを24bit整数にしてI2Sで出力.
これであっさり動作した.float で処理できるのはオーバーフローを気にしなくていいので助かる.
係数生成用の関数も用意されていて動的にパラメータを変えていくような時には便利.自分で作っても大した手間ではないが(Teensyのときはそうした).
高次のチェビシェフとかの係数は作れない.そういう場合も含めてフィルタ設計にはOctaveが便利なので普段はそちらを常用している.

信号処理量の限界を知るために,GPIO48 に信号処理中はHIGHそれ以外はLOWを出力するようにしてその信号を観測してみた.
(GPIO48 はオンボードのRGB LEDに接続されているので信号によってLEDの状態も変化するが,LED制御用の信号ではないのでLEDがどうなるかは不定)

IIR Biquad フィルタがLch,Rch それぞれ1段なので,処理にかかる時間の割合は非常に小さい.ただHIGHのところが一定でないようなので,拡大すると下のようになっていた.

どうも4ブロック分連続して処理するようになっているらしい.理由はI2Sドライバのソースコードがないのでよくわからない(見てもわからないか…).そういうものとして理解しておこう.

次に,信号処理を限界まで追加してみる.
Lch,Rch それぞれに 512Tap の FIR + 10段のIIR Biquad を設定した. IIRフィルタの係数はL,R同じ.
コードは以下のとおり.

// I2S on ESP32-S3
// T.Uebo  October 4, 2022

// I2S Master MODE 48kHz/32bit
//
//   Mclk  GPIO 0
//   Bclk  GPIO 42
//   LRclk GPIO 2
//   Dout  GPIO 41
//   Din   GPIO 1

#include <esp_dsp.h>
#include <driver/i2s.h>
#define fsample 48000
#define BLOCK_SAMPLES 64
#define MUTE 40 // MUTE control (LOW: Mute)

//buffers
int rxbuf[BLOCK_SAMPLES*2], txbuf[BLOCK_SAMPLES*2];
float Lch_in[BLOCK_SAMPLES], Rch_in[BLOCK_SAMPLES];
float Lch_out[BLOCK_SAMPLES], Rch_out[BLOCK_SAMPLES];

// for IIR Filter
float zL0[2], zR0[2], coeffs0[5];  // Z^-1配列(Lch),Z^-1配列(Rch),係数
float zL1[2], zR1[2], coeffs1[5];
float zL2[2], zR2[2], coeffs2[5];
float zL3[2], zR3[2], coeffs3[5];
float zL4[2], zR4[2], coeffs4[5];
float zL5[2], zR5[2], coeffs5[5];
float zL6[2], zR6[2], coeffs6[5];
float zL7[2], zR7[2], coeffs7[5];
float zL8[2], zR8[2], coeffs8[5];
float zL9[2], zR9[2], coeffs9[5];

// for FIR filter
#define Nfir 512
fir_f32_t firL_st, firR_st;      //  FIRではワーク用の構造体を用意する必要がある
float coeffs_firL[Nfir], coeffs_firR[Nfir];  // 係数用配列
float z_firL[Nfir], z_firR[Nfir];  // Z^-1配列

/*-----------------------------------------------------------------------------------------------
  Setup
-------------------------------------------------------------------------------------------------*/
void setup(void) {
  Serial.begin(115200);
  delay(50);

  //Mute Control
  pinMode(MUTE, OUTPUT);
  digitalWrite(MUTE, HIGH); //unmute

  pinMode(48, OUTPUT);

  // I2S setup ------------------------------------------------------------
  i2s_config_t i2s_config = {
    .mode =  (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX  | I2S_MODE_RX),
    .sample_rate = fsample,
    .bits_per_sample = (i2s_bits_per_sample_t)32,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0,
    .dma_buf_count = 6,
    .dma_buf_len = BLOCK_SAMPLES*4,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0,
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL);

  i2s_pin_config_t pin_config = {
        .bck_io_num = 42,
        .ws_io_num = 2,
        .data_out_num = 41,
        .data_in_num = 1                                                       
    };
  i2s_set_pin( I2S_NUM_0, &pin_config);

  //-- Filter settings -------------------------------------
  // IIR Filter の係数を生成 
  dsps_biquad_gen_lpf_f32(coeffs0, 0.45f, 1.0f);
  dsps_biquad_gen_lpf_f32(coeffs1, 0.45f, 1.0f);
  dsps_biquad_gen_lpf_f32(coeffs2, 0.45f, 1.0f);
  dsps_biquad_gen_lpf_f32(coeffs3, 0.45f, 1.0f);
  dsps_biquad_gen_lpf_f32(coeffs4, 0.45f, 1.0f);
  dsps_biquad_gen_lpf_f32(coeffs5, 0.45f, 1.0f);
  dsps_biquad_gen_lpf_f32(coeffs6, 0.45f, 1.0f);
  dsps_biquad_gen_lpf_f32(coeffs7, 0.45f, 1.0f);
  dsps_biquad_gen_lpf_f32(coeffs8, 0.45f, 1.0f);
  dsps_biquad_gen_lpf_f32(coeffs9, 0.45f, 1.0f);

  //  FIR構造体の初期化
  dsps_fir_init_f32(&firL_st, coeffs_firL, z_firL, Nfir);
  dsps_fir_init_f32(&firR_st, coeffs_firR, z_firR, Nfir);

  //  FIR Filter の係数を生成. 
  dsps_d_gen_f32(coeffs_firL, Nfir, 0);  // 係数列の先頭を1,その他0
  dsps_d_gen_f32(coeffs_firR, Nfir, Nfir-1); // 係数列の最後1,その他0
}

/*-----------------------------------------------------------------------------------------------
  Signal Process Loop
-------------------------------------------------------------------------------------------------*/
void loop(void) {
  size_t readsize = 0;

  //Input from I2S codec
  esp_err_t rxfb = i2s_read(I2S_NUM_0, &rxbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);
  if (rxfb == ESP_OK && readsize==BLOCK_SAMPLES*2*4) {

    digitalWrite(48, HIGH);

    int j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      Lch_in[i] = (float) rxbuf[j];
      Rch_in[i] = (float) rxbuf[j+1];
      j+=2;
    }   
    
    //-------Signal process -------------------------------
    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, coeffs0, zL0);
    dsps_biquad_f32(Rch_in, Rch_out, BLOCK_SAMPLES, coeffs0, zR0);

    dsps_biquad_f32(Lch_out, Lch_in, BLOCK_SAMPLES, coeffs1, zL1);
    dsps_biquad_f32(Rch_out, Rch_in, BLOCK_SAMPLES, coeffs1, zR1);

    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, coeffs2, zL2);
    dsps_biquad_f32(Rch_in, Rch_out, BLOCK_SAMPLES, coeffs2, zR2);

    dsps_biquad_f32(Lch_out, Lch_in, BLOCK_SAMPLES, coeffs3, zL3);
    dsps_biquad_f32(Rch_out, Rch_in, BLOCK_SAMPLES, coeffs3, zR3);

    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, coeffs4, zL4);
    dsps_biquad_f32(Rch_in, Rch_out, BLOCK_SAMPLES, coeffs4, zR4);

    dsps_biquad_f32(Lch_out, Lch_in, BLOCK_SAMPLES, coeffs5, zL5);
    dsps_biquad_f32(Rch_out, Rch_in, BLOCK_SAMPLES, coeffs5, zR5);

    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, coeffs6, zL6);
    dsps_biquad_f32(Rch_in, Rch_out, BLOCK_SAMPLES, coeffs6, zR6);

    dsps_biquad_f32(Lch_out, Lch_in, BLOCK_SAMPLES, coeffs7, zL7);
    dsps_biquad_f32(Rch_out, Rch_in, BLOCK_SAMPLES, coeffs7, zR7);

    dsps_biquad_f32(Lch_in, Lch_out, BLOCK_SAMPLES, coeffs8, zL8);
    dsps_biquad_f32(Rch_in, Rch_out, BLOCK_SAMPLES, coeffs8, zR8);

    dsps_biquad_f32(Lch_out, Lch_in, BLOCK_SAMPLES, coeffs9, zL9);
    dsps_biquad_f32(Rch_out, Rch_in, BLOCK_SAMPLES, coeffs9, zR9);

    dsps_fir_f32(&firL_st, Lch_in, Lch_out, BLOCK_SAMPLES);
    dsps_fir_f32(&firR_st, Rch_in, Rch_out, BLOCK_SAMPLES);

    //------------------------------------------------------

    //Output to I2S codec
    j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      txbuf[j]   = (int) Lch_out[i];
      txbuf[j+1] = (int) Rch_out[i];
      j+=2;
    }
    i2s_write( I2S_NUM_0, &txbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);

  }
  digitalWrite(48, LOW);
}

これでGPIO48の信号を見てみると下のようにほぼ限界になっている.

260μsの隙間があるのでもう少し詰められる.試しのFIRのタップ数を増やしていくと580くらいで出力の音がおかしくなった.

ほぼ予想どおり.
これくらいの処理能力ならデシメーション無しでSSBのベースバンド処理はいけそう.エフェクタならFIRは使わないのでかなり余裕をもって使える.

ESP32-S3 AF信号処理ボード

前回の記事に書いたものは回路図も書かずI2Sの動作の確認のためだけに適当につくったものだったが,きちんと情報をまとめておくためにもあらためて詳しく記事にしていく.

これをもとに何をしたいかというと,SDR(趣味)と楽器用のエフェクタ(仕事).どちらもやることはよく似ている.さすがにエフェクタの方に複素係数フィルタの出番はないが.

まずは回路図を作成した.
(見づらい場合は,「名前を付けて画像を保存」してからViewerで拡大して閲覧ください)

CODECは前回の記事から変更して,DACにはPCM5102A,ADCにはPCM1808を使用した.限られた手持ちのデバイス中ではこの組み合わせが一番ノイズが少なかった.
PCM5102Aは単体のデバイスではなくモジュールを使った(紫色の基板).アマゾンで検索するとたくさんでてくる.

PCM1808のI2S制御信号にはダンピング抵抗を入れている.これがないと出力にプチプチとノイズが入る.抵抗値は適当なので最適値があるかもしれない.

LCD用のポートも確保しておいた.一応8bitパラレルを想定しているが今後ポートが足りなくなるようならSPIにする.ESP32よりポート数は増えているが十分ではないかもしれない.

以下にこの回路で動作するコードを示す.
環境は Arduino IDE + ESP32ボードマネージャ(2.0.5)
(折り返し等で見づらい場合は全コピーしてテキストエディタに貼り付けてください)

// I2S on ESP32-S3
// T.Uebo  October 4 , 2022

// I2S Master MODE 48kHz/32bit
//
//   Mclk  GPIO 0
//   Bclk  GPIO 42
//   LRclk GPIO 2
//   Dout  GPIO 41
//   Din   GPIO 1

#include <driver/i2s.h>
#define fsample 48000
#define BLOCK_SAMPLES 64

//buffers
int rxbuf[BLOCK_SAMPLES*2], txbuf[BLOCK_SAMPLES*2];
float Lch_in[BLOCK_SAMPLES], Rch_in[BLOCK_SAMPLES];
float Lch_out[BLOCK_SAMPLES], Rch_out[BLOCK_SAMPLES];

#define MUTE 40 // MUTE control (LOW: Mute)

/*-----------------------------------------------------------------------------------------------
  Setup
-------------------------------------------------------------------------------------------------*/
void setup(void) {

  Serial.begin(115200);
  delay(50);

  pinMode(MUTE, OUTPUT);
  digitalWrite(MUTE, HIGH); // unmute

  // I2S setup ------------------------------------------------------------
  i2s_config_t i2s_config = {
    .mode =  (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX  | I2S_MODE_RX),
    .sample_rate = fsample,
    .bits_per_sample = (i2s_bits_per_sample_t)32,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0,
    .dma_buf_count = 6,
    .dma_buf_len = BLOCK_SAMPLES*4,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0,
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL);

  i2s_pin_config_t pin_config = {
        .bck_io_num = 42,
        .ws_io_num = 2,
        .data_out_num = 41,
        .data_in_num = 1                                                       
    };
  i2s_set_pin( I2S_NUM_0, &pin_config);

}


/*-----------------------------------------------------------------------------------------------
  Signal Process Loop
-------------------------------------------------------------------------------------------------*/
void loop(void) {
  size_t readsize = 0;  
  //Input
  esp_err_t res = i2s_read(I2S_NUM_0, &rxbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);
  if (res == ESP_OK && readsize==BLOCK_SAMPLES*2*4) {
    int j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      Lch_in[i] = (float) rxbuf[j];
      Rch_in[i] = (float) rxbuf[j+1];
      j+=2;
    }   
    
    //-------Signal process -------------------------------
    for (int i=0; i<BLOCK_SAMPLES; i++) {   
      Lch_out[i] = Lch_in[i];
      Rch_out[i] = Rch_in[i];           
    }

    //------------------------------------------------------

    //Output
    j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      txbuf[j]   = (int) Lch_out[i];
      txbuf[j+1] = (int) Rch_out[i];
      j+=2;
    }
    i2s_write( I2S_NUM_0, &txbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);

  }

}

動作は,入力された信号をそのまま出力する.
GPIO 40をミュート制御用の信号としているが,回路は対応していないので今のところ意味はない.

今後,//——-Signal process —————- の部分に処理を追加していく.

ESP32ではバックグラウンドでfreeRTOSが動作しているそうでなかなか理解が難しい(Teensy もそうだったか?).i2s.h ( I2S.h ではない)に書かれてある内容を頼りに用意されている関数を使ってとりあえず動作はしたが,自分自身はよく理解しているとは言い難い.とはいえやりたいことは信号処理なので,それ以外は先人の成果をありがたく使わせていただくことにしたいと思う.

参考にさせていただいたサイト

ESP32-S3 で I2S Audio CODEC を動かす

備忘録代わりの投稿.
ESP32-S3-DevKitC-1 を入手したので,I2S接続の Audio CODEC つなげて動かしてみた.
開発環境はArduino IDE.ESP32のボードマネージャはバージョン2.0.5をインストールした.
(2.0.3以降でS3に対応しているらしい)

CODECには PCM3060PW を使った.
まずESP32用のI2Sのスケッチ例があったので試してみたがまともに動作せず.これらは libraries フォルダにある I2S.h, I2S.cpp  を使っていたのでその中身を見てみると,サンプリング周波数が16kHz以下かつ16bitで使うようにコメントされていて無駄な時間を費やしてしまった.
結局 driver フォルダの i2s.h を使い,下記のコードでうまく動作した.

// I2S on ESP32-S3
// T.Uebo  October 1 , 2022

// I2S Master MODE 48kHz/32bit
//
//   Mclk  GPIO 0
//   Bclk  GPIO 42
//   LRclk GPIO 2
//   Dout  GPIO 41
//   Din   GPIO 1

#include <driver/i2s.h>

#define fs 48000
#define BLOCK_SAMPLES 128

#define Reset_CODEC 40 // HW Reset signal for CODEC 

//buffers
int rxbuf[BLOCK_SAMPLES*2], txbuf[BLOCK_SAMPLES*2];
float Lch_in[BLOCK_SAMPLES], Rch_in[BLOCK_SAMPLES];
float Lch_out[BLOCK_SAMPLES], Rch_out[BLOCK_SAMPLES];


void setup(void) {
  //HW Reset for CODEC
  pinMode(Reset_CODEC, OUTPUT);
  digitalWrite(Reset_CODEC, HIGH);
  delay(1);
  digitalWrite(Reset_CODEC, LOW);
  delay(1);
  digitalWrite(Reset_CODEC, HIGH);

  // setup I2S 
  i2s_config_t i2s_config = {
    .mode =  (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX  | I2S_MODE_RX),
    .sample_rate = fs,
    .bits_per_sample = (i2s_bits_per_sample_t)32,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0,
    .dma_buf_count = 6,
    .dma_buf_len = BLOCK_SAMPLES*4,
    .use_apll = false,
    .tx_desc_auto_clear = true,
    .fixed_mclk = 0,
  };
  i2s_driver_install( I2S_NUM_0, &i2s_config, 0, NULL);

  i2s_pin_config_t pin_config = {
        .bck_io_num = 42,
        .ws_io_num = 2,
        .data_out_num = 41,
        .data_in_num = 1                                                       
    };
  i2s_set_pin( I2S_NUM_0, &pin_config);
  i2s_zero_dma_buffer(I2S_NUM_0);
}


void loop(void) {
  size_t readsize = 0; 

  //Input from I2S codec
  esp_err_t rxfb = i2s_read(I2S_NUM_0, &rxbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);
  if (rxfb == ESP_OK && readsize==BLOCK_SAMPLES*2*4) {
    
    int j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      Lch_in[i] = (float) rxbuf[j];
      Rch_in[i] = (float) rxbuf[j+1];
      j+=2;
    }    
    
    //-------Signal process -------------------------------
    for (int i=0; i<BLOCK_SAMPLES; i++) {
    
      Lch_out[i] = Lch_in[i];
      Rch_out[i] = Rch_in[i];
            
    }
    //------------------------------------------------------


    //Output to I2S codec
    j=0;
    for (int i=0; i<BLOCK_SAMPLES; i++) {
      txbuf[j] = (int) Lch_out[i];
      txbuf[j+1] = (int) Rch_out[i];
      j+=2;
    }
    i2s_write( I2S_NUM_0, &txbuf[0], BLOCK_SAMPLES*2*4, &readsize, portMAX_DELAY);    	  	    
  }

}

Freeverb (Teensy Audio ライブラリ)

Teensy 4 でリバーブの処理をさせようと思い,ライブラリにあるFreeverb を試してみたところ,FIRフィルタと同様信号レベルが低下してくるとノイズが生じてくる(残響音の減衰の最後の方は特に顕著).やはりこれも16bit整数処理の影響かと思われるので,32bitフロートで書き直すことにする.

書き直すにあたってまず,ライブラリのソースを調べて具体的な処理内容を確認した.

Freeverbの中身はSchroederのアルゴリズムがベースになっていた.ただし,よく見かける 4個のComb filterと2個のAllpass filter によるものではなく,下図のようにそれぞれ2倍の 8個と4個になっていて,”High quality Reverb effect”との表記があった.

ただし,この処理ブロックは,残響のテイルの部分を生成するものなので,空間の残響をそれなりにシミュレートするには,Early reflection の生成や Pre delay の処理を追加する必要がある.

Comb filter は以下のようになっていた.

図のようにダンピング処理のための1次LPFを持っているので自然なリバーブが得られそう.
(このフィルタをBPFとかにすれば,Shimmer Reverb になるのかも?)

Allpass filter は以下のとおり.

ライブラリソースでは a=0.5 に固定されているようだが,その場合 1-a^2 = 0.75 でなければならない.コードをみるとこれ(1-a^2)も 0.5 になっているように解釈できた(自分の読み間違いかもしれないが).単なるバグなのか何か意図があってそうしているのかわからないがもしそうなら特性はAllpass にはなっていない.とはいえ残響音の密度を上げるという観点からは聴感上大きな違いが出るようにも思えないのであまり気にしないでもいいか… 書き直しの際はセオリー通り 1-a^2=0.75 にする.

Teensy Audio ライブラリ

Teensy 4 でオーディオ信号処理を行うにはAudioライブラリを用いるのが簡便でよいが,以下のような制限がある.

(1) 処理が16bit整数のみ
(2) FIRフィルタのタップ数が200まで
(3) SDRで必要となる処理が用意されていない(直交・極変換,微分,平方根など)
など.

まず,2048タップのFIRフィルタを作ろうとして (2) の制限に引っかかった.
そこで,タップ数の少ないFIRフィルタを複数用いそれらを適切な Delay を通して駆動し,
出力をMIXする方法を試してみた(下図).

128タップのFIRフィルタを16個並べて2048タップのFIRフィルタを構成している.これはうまく動作していて問題は無いように思えたが,動作確認を続けていくうちに,信号レベルが低下してくるとバックグラウンドノイズではない別のノイズが生成されてくることに気づいた(信号レベルが大きいときは気にならない).これは以前に16bit整数処理のIIRフィルタ(8次)の動作確認したときと同様の現象で,このFIRフィルタもおそらくアンダーフローを起こしていると思われた(IIRのときよりは顕著でないが).

となるとAudioライブラリは16bit整数処理のみなので,このライブラリ使う限り改善は望めそうにない.そこで Audioライブラリでは I2S と queue のみを使用し,信号処理はコードで書くことにする(下図).

queue_send から信号をコードに取り込み,処理したあと queue_ret に信号を返す.
信号処理には CMSIS DSP Software Library で用意されている関数を用いる.信号処理関数が豊富に,かつ float で処理する関数も用意されているのでそれらを使えば上述のノイズの問題は解消できるはず.

ということで,以下のようなコード作成して動作確認をしてみた.
(なお,コードを見やすくするために実際に動作確認できた状態から抜粋して示しています.このままでおそらく動作するとは思いますが保証はしません.間違いのご指摘は歓迎いたしますが「動作しない」というクレームはご遠慮願います.)

//#include <arm_math.h>
#include <Audio.h>

AudioInputI2S            i2s_IN;
AudioMixer4              mixer_IN;
AudioRecordQueue         queue_send;
AudioPlayQueue           queue_ret;
AudioOutputI2S           i2s_OUT;
AudioConnection          patchCord1(i2s_IN, 0, mixer_IN, 0);
AudioConnection          patchCord2(i2s_IN, 1, mixer_IN, 1);
AudioConnection          patchCord3(mixer_IN, queue_send);
AudioConnection          patchCord4(queue_ret, 0, i2s_OUT, 0);
AudioConnection          patchCord5(queue_ret, 0, i2s_OUT, 1);

#define NtapFIR 2048
float32_t fircoeffs[ NtapFIR ];
float32_t firState[ NtapFIR + 2*AUDIO_BLOCK_SAMPLES - 1 ];

arm_fir_instance_f32 firEQ;

float32_t sigf_IN [AUDIO_BLOCK_SAMPLES];
float32_t sigf_OUT[AUDIO_BLOCK_SAMPLES];
int16_t sig16[AUDIO_BLOCK_SAMPLES];

#include <FlexiTimer2.h>


//Signal process --------------------
void sig_proc()
{
  int i;
  int16_t *p_sig;  
  if(queue_send.available() != 0){

    //INPUT
    p_sig = queue_send.readBuffer();
    for(i=0; i<AUDIO_BLOCK_SAMPLES; i++) 
              sigf_IN[i]=(float32_t)p_sig[i];
    
    //FIR
    arm_fir_f32(
          &firEQ,
          sigf_IN, 
          sigf_OUT, 
          AUDIO_BLOCK_SAMPLES
    );

    //OUTPUT
    for(i=0; i<AUDIO_BLOCK_SAMPLES; i++) 
              sig16[i]=(int16_t)(sigf_OUT[i]);
    queue_ret.play(sig16, AUDIO_BLOCK_SAMPLES);
    queue_send.freeBuffer();
  }
 
}

//--------------------------------------
void setup() {
  
  arm_fir_init_f32(
    &firEQ, 
    NtapFIR, 
    fircoeffs, 
    firState, 
    AUDIO_BLOCK_SAMPLES
  );

  //Set freq. response to flat   (Impulse)
  for(int i=0; i<NtapFIR; i++) fircoeffs[i] = 0;
  fircoeffs[1024] = 1.0f;
  
  //Reset CODEC
  pinMode(22, OUTPUT);
  digitalWrite(22, LOW);
  delay(100);
  digitalWrite(22, HIGH);
  
  AudioMemory(100);
  mixer_IN.gain(0, 1.0f);
  mixer_IN.gain(1, 1.0f);
  mixer_IN.gain(2, 0.0f);
  mixer_IN.gain(3, 0.0f);


  // call "sig_proc" every 2ms
  FlexiTimer2::set(2, 1.0/1000, sig_proc);
  FlexiTimer2::start();
 
  queue_send.begin();  
}

//------------------------------------------
void loop() {
      //Set fircoeffs[] 
}

動作確認の結果 32bit float 処理にすることでノイズが消えた.やはり16bit整数処理が問題だったようだ.
AudioライブラリはGUIでシステムを構築できるので取っつきやすいが,結局コードで書いた方がfloat 処理ができるしFIRフィルタの処理も1行で済むので楽(設定はいろいろ必要だがそれはAudioライブラリも同じ).
あとは I2S のブロックが16bitでなく32bitでインターフェースできれば,CODECのダイナミックレンジを十分に活かせるのだが…
とはいえ内部処理は 32bit float にできたので微分処理なども少し安心してできると思う.

Fully digital SSB generator(2)

1.試作した完全ディジタルSSBジェネレータ

ソフトウェアは前の記事で示したように,AF PSNの処理の後に極座標変換処理を行い,その絶対値を振幅,また位相の微分値とキャリア周波数の和を周波数として,DDSを制御するコードを追加した.

2.スペクトルの確認
キャリア周波数:7.1MHz
モード:LSB
帯域:300Hz – 2800Hz
入力:1kHzの正弦波

逆サイドバンド,キャリア漏れは全く確認できない.
帯域内の盛り上がりはおそらくノイズと思われる.上の写真のような作りのため当然かと.
基板をおこせば改善できそう.

動作の様子

Fully digital SSB generator(1)

1.はじめに
PSNでSSBを発生させる場合,AF PSN をディジタル化することで無調整で実用的なレベルのAFのIQ信号が簡単に得られる.
とはいえ,キャリア漏れとかサイドバンドサプレッションに関しては,結局その後のRF直交ミキサの性能や調整状態に依存してしまう(AF信号が理想的であっても).特にキャリア漏れについては周波数が大きく変わると再調整の必要があり広帯域にわたって調整なしで使うのは難しい.ということで,AF PSNだけでなくSSBジェネレータ全体をディジタル化したい.

2.AF PSN について
AF PSNの実現方法としては,入力(実信号 Ai )をヒルベルト変換しそれを虚数部 Aq とするのが一般的( Ai + jAq ).
時間領域で考えれば,ヒルベルト変換は広帯域にわたって同一振幅で正確に90度位相シフトすることが重要で,その点に着目して設計や調整を行う.

一方,周波数領域で考えれば,AF PSNは負(または正)の周波数成分をカットするフィルタである(AF PSNの記事).
これはヒルベルト変換を周波数領域で説明しただけのことなので,ヒルベルト変換でもフィルタでも変わりはない.実現手法が違うため具現化したときにそれに起因した差が生じているだけである.

周波数領域では次のように考えることができる.
実信号の正の周波数成分と負の成分は複素共役の関係にあるので,もとの実信号は単に Ai ではなく,正の成分 (Ai + jAq)/2 と負の成分 (Ai – jAq )/2 の和で,Ai=(Ai + jAq)/2 + (Ai – jAq)/2 となっているので,実信号の負の周波数成分をカットすることで自ずと Ai + jAq (の1/2)となる.

そもそも,実信号 Ai を正の周波数成分と負の成分に分離したときに,その虚数部 Aq を表すものがヒルベルト変換なわけで,「ヒルベルト変換操作で生成した Aq を虚数部として付加する」というのはエミュレーションであって,考え方としては本質的でないような気はする.

AF PSNがフィルタとみなせるならば,それにバンドパス特性を併せ持たせて,例えば,正の 300Hz から 3kHz を通過させる特性にすれば,BPFを別に設ける必要がない.

3.PSN式SSBジェネレータ(従来方式)
AF PSN で得られた信号(Ai + jAq)を周波数シフトすればSSBが得られる.具体的には目的周波数を ωc とすれば複素正弦波 exp(jωc t)=cos(ωc t) + jsin(ωc t)を掛ければよい.
複素SSB信号=(Ai + jAq) * ( cos(ωc t) + jsin(ωc t) )
最終的に必要なのは実信号(実部)なので,
SSB信号=Ai*cos(ωc t) – Aq*sin(ωc t)
これが直交ミキサを使う方法の原理で下図のような構成となる.

4.完全ディジタル化
Ai + jAq を 極座標形式に変換する.

これを目的周波数 ωc にシフトするために複素正弦波 exp(jωc t) を掛けると次式のように複素SSB信号が得られる.

最終的に必要なのは実信号(実部)なので,

この式からわかるように周波数 ωc のSSB信号を得るには,周波数 ωc のキャリア信号に対して,振幅をm(t)で,周波数をω(t)で同時に変調すればよい.
これは振幅変調機能のあるDDSを使えば簡単に実現できるので,ディジタルAF PSNとDDSを組み合わせれば,完全ディジタルSSBジェネレータが実現できる(下図).

当然,振幅あるいは周波数それぞれ単独で変調を掛けることもできるので,SSBだけでなくAMもFMも簡単に発生させることができるし,DDSの本来の機能によって任意の周波数のキャリアを生成できるので,オールバンドオールモードのジェネレータが完全ディジタルで作れることになる.

とりあえず,AD9951を使って実験してみたところ,動作は問題ない様子.

Si5351単体で3MHz以下の直交信号を出力する

Si5351Aは Multisynth の delay パラメータを使って直交信号を出力することができるが,パラメータの設定範囲が最大127のため,直交信号を出力できる範囲はおよそ3MHz以上に限られる.(Si5351で直交信号) そこで 別の方法によって 3MHz以下の直交信号を得ることにした.

Si5351の構成は図通りで,Mutisynthの分周比M0, M1をある値で固定しておき,PLLのフィードバック分周比Nを変化させて周波数をコントロールするのが一般的.

si5351_01.png

今回のポイントは,M0, M1も分数分周比をとることができるので,M0, M1によって周波数を細かく制御することが可能という点.ただ,Mの値の制御ではスムーズな周波数制御はできないと思うのでMの制御は位相差π/2を得るためだけの手段とし,周波数の制御はPLLの分周比Nの制御で行う.

Mの制御で希望の位相差を得るには,M0,M1を時間差をつけて変化させればよい.
例えば,M0, M1 = m’ としておいてPLLリセットする(これで位相が0にそろう).
その後, M0 = m  (m<m’,  M1 = m’  はそのまま) とすれば  f I>f Q となり
周波数差 fd = f I – f Q  が生じる.
これによって,f I の位相が f Q に対して時間経過とともに進んでいく.
経過時間を Td  とすれば,位相の進み θd は次式で表される.

eq1

よって,θd = π/2 となる Td は,

eq2.png

となるので,この時間経過後に M1 = m にセットすれば,π/2の位相差を持った信号が得られる.この操作は一度行えばよい.それ以降,m の変更がなく,かつ,周波数の変更をPLLで行えば位相差は維持される.

例として,2.0MHzの直交信号を得る場合について手順を示す.
条件は,m = 300,  fd = 4Hz,  Multisynthのモードは fractional.

  1.   fvco = 300*2.0MHz = 600MHz となるよう  N =  600MHz/25MHz = 24  にセット
  2.  出力周波数 f I, fQ = 2.0MHz –  4Hz  となるよう,
    M0, M1=600MHz / ( 2MHz-4Hz ) = 300.0006 にセット
  3.  PLLリセット
  4.  M0 = 300 にセット
  5.  Td = 1/(4fd) = 62.5ms 経過後に M1 = 300 にセット

これで2MHzの直交信号が得られる.以降,周波数を変化させる場合,例えば1.99MHzに変更するには,Nのみ変更し,N = 1.99MHz*300 / 25MHz = 23.88 にセットする.
ここで重要なのはPLLリセットしないこと.
fd は任意だが小さい方が位相差の精度がよいかもしれない.ただしTdが大きくなり待ち時間が長くなる.

以下動作例

出力周波数=1.18MHz
20200827_193438

出力周波数=588kHz
20200827_193537.jpg

 

保証された動作ではないかもしれないが,いまのところ変な動作はしていない様子.

SDR using dsPIC for SSB MODE

FM,AMモードはまだ納得できる状態ではないが,SSBに関しては問題なさそうなので,ファームウェア(ソースコード)などを公開した.

Source code,  Circuit diagram

IIR, FIR フィルタの特性を変えるためのツールも置いてある.

dsPICの内部処理は以下のとおり.

SSB_demod.jpg

原理的にはこれまで通り,複素係数フィルタで正(または負)の周波数成分をカットするフィルタ式.
コードを書いていて思ったのだが実際の処理内容はPSNと酷似している.なので,複素係数フィルタをAF PSN とみなしてもよいと思う.
hilbert 変換自体が負の周波数をカットするハイパスフィルタだし,一方,複素係数フィルタの虚数部は hilbert 変換器と同じものになっていることを考えれば.本質的に PSN方式 もフィルタ方式も同じということだろう(もちろん実現手法の違いはある).

FM demodulation (SDR using dsPIC)

FMの復調機能の追加を行った.
AMの復調ができているので,それを少しモディファイするだけでOK.
つまりAMは振幅を求めるがFMは位相を求めて微分すればよいので,下図のような構成になる.

FM_demod.jpg

tan の逆関数は安直にテーブル参照にした.math ライブラリのatanやatan2関数は処理が重くて使えなかった.
今のところSGからのFM信号はうまく復調できている.リミッタを深く効かせた通常のFM復調と違ってノイズは多め.この方式はI,Q信号の瞬時値から位相を計算するため,リミッタが使えないから仕方ない.

もう少しテストが必要かもしれないが,とりあえず SSB(USB/LSB),CW,AM,FM モードの受信が可能になった.

AM demodulation (SDR using dsPIC) (2)

Direct Conversion 方式でAM復調がうまくいった… 確かにほぼうまくいったのだが,問題があることがわかった.
ローカル信号のリークが周波数によっては予想以上に大きくなるところがあり,アンプをDC結合したためにリークの直流成分でアンプが飽和してしまい,その周波数では受信不能となる.飽和しないレベルであれば動作はするが,ダイナミックレンジが狭くなる.
直前の記事で,「直交ミキサでベースバンドに変換するDC方式はなかなか難しい」と書いた通りになった.

まず,アンプをAC結合に戻し直流による飽和が起こらないようにした.

次に,ローカル発振(VFO)周波数 ωc を受信周波数 ωm より ωi  低くして,受信したAM信号の周波数を ωi に変換し,これを増幅した後AD変換してdsPIC に取り込むようにした.
今回 ωi = 2 π 12.5kHz としている.

AM_demod2.jpg

その後信号処理で,上図に示すようにベースバンドに変換している.
(乗算器が4個必要なのは,複素数同士の掛け算のため)
LPF以降は前回と同じ.

dsPIC33FJの能力の限界に近い処理量のようだが,AM受信自体は前回と同様問題なくできている様子.受信周波数を500kHzから54MHzまで変えてみたが,受信できなくなる周波数は無いようので,しばらくこれで動作確認をしてみる.

AM demodulation (SDR using dsPIC)

dsPICによるSDRでSSBやCWはうまく受信できている.これに加えてAMモードも実装したい.AM受信はSSBモードでもできるが,キャリア周波数を完全に合わせないと受信信号のピッチが変わってしまう.会話なら問題も少ないが音楽は不協和音になってしまって聞きづらいので,やはりAMモードが必要だと思う.

AMの復調をするには,下図のように,IchとQchをそれぞれ2乗して加算したあと平方根をとればよい.

AM_demod

理屈は簡単だが,直交ミキサでベースバンドに変換するDC方式だと,キャリア周波数が0Hz(直流)に変換されるので実際にはなかなか難しい.なので多くは,キャリア周波数を0Hzではなく10kHzなどに変換し,それを信号処理してAM復調をする.

ただ今回使用しているdsPICの性能にあまり余裕がないので,上図のとおりDC方式にする.

LPFの出力は次式となる.eq1_2

AMの場合,A + m(t) > 0 なので,

eq3

これの直流分 A/2 をカットすれば復調完了で,式(3)にはキャリア周波数とローカル周波数の差の要素は含まれないので,チューニングのずれで復調される信号ピッチが変わるということはない.

それはいいのだが,実際はミキサの出力にはローカル信号のリークに起因する直流成分が加算される.DBMにしてもリークを0にすることは現実的には困難で,また厄介なことにリークの量は一定ではなく,周波数によって変化する.

リークは直流として生じるので問題なさそうに思えるが,そうではない.
リーク成分を vi, vq として,それらを考慮した場合のLPFの出力を I’ , Q’ とすれば,

eq4_5

このとき,出力は,

eq6

となる.面倒なのでこれ以上の式の展開はしないが,明らかに出力に歪が生じてしまう.チューニングがあっている(ωc=ωm)のときは歪だけだが,チューニングがずれると式(6)の第3,4 項によってビート成分とその高調波も出てくる.

結局,リーク成分vi, vq を除去することが必須であることがわかる.ただ厳密にはそれは不可能なので,条件をつけて譲歩するしかない.

その条件は,「厳密にチューニングがあう(ωc=ωm)ことはない」ということ.

もし ωc=ωm だったら,

eq7_8

となるが,残念なことに第1項と第3項はどちらも直流で分離不可能.

仮に式(7),(8)から直流分を除去すると次式となり,

eq9_10

出力は次式となる.

eq11

元の信号を全波整流したものとなってしまって復調できない
(  m(t) が正負の値をとることに注意).
AMの場合,A+m(t)が常に>0.そうなるようにAが足されていることに大きな意味がある.

 

…ということで,ωc=ωmのときはあきらめて,厳密にそうなることは稀だということに期待する.

ωc≠ωmならば,式(1), (2)をみればわかるように,I, Q は,周波数差|ωc – ωm|に相当する周期をもつ交流となる.リーク込みのLPF出力は式(4), (5) だが,これの時間平均をとれば,I,Qはともに0に収束し最終的に vi, vq のみとなって,リークによる直流分が得られる.得られた値を信号処理時にI, Q信号から引けばよい.

今回,平均処理を2~3秒間の移動平均としてみる.例えば,平均時間2秒なら,1/2=0.5Hz以上周波数がずれていればよい.AMで0.5Hz以内にゼロインするのは難しいと思うので実用的にはOKだろう.たまたま合ってしまう可能性はゼロではないけど,受信信号とローカル信号は全く無相関なのでその状態が長く続くとも思えない…(と希望的観測しておこう).最終的に約2.6秒とした.

ここまで書いて気付いたが,これってカットオフ周波数の非常に低いHPFで直流をカットしているのと同じことだった.ただカットオフ周波数が0.5Hzとかになるので,容量がものすごく大きなCが要る(10000uFとか)し,カットオフ周波数の調整も面倒そうなので,信号処理でやる意味はありそう.

実際の動作の様子はこちら( https://youtu.be/txrccQwB3Pc )

チューニングのずれによるピッチの変化がなくなりAMらしくなった.少しビートが残っている感じがするが,まあいいんじゃないかと思う.むしろチューニングの目安となっていいかもしれない.

ついでにAGC処理も追加しておいた.
ちなみにスピーカのすぐ上に見える回路はアナログでAGCをやろうとした残骸.
信号処理でAGCがうまくいったので使っていない.
また,直流分も通すためにミキサからAD変換までの回路をDC結合に変更した.

そろそろ基板におこせる段階かな.
その前にFMもやってみようか…

SDR using dsPIC33FJ64GP802 (2)

レシーバをテストしていて気づいたのだが,RF入力のレベルが小さくなっていくと,あるレベル(しきい値)でノイズゲートが掛かったようにいきなり無音となる.さらによく調べてみたら,しきい値付近のレベルだと信号やノイズがとぎれとぎれになってブツブツという音がする.どうもAD変換時の量子化が原因のようだ.しきい値はAD変換の量子化レベルであって,dsPICの場合,12bitなので,3.3V/4096=805 uV.
これ以下の変化は検知できず,一定値の信号として処理され結果として無音になる.また,これを超えるか超えないか微妙なレベルだと変なノイズが出る.

量子化レベル以下の変化を取り込む方法として,よく知られているものとしてはディザー信号を重畳させる方法がある.

つまり,量子化レベルより十分大きな振幅を持つ信号(ディザー信号)を目的信号に加え,量子化レベルを超えさせる.ディザー信号は,目的信号と周波数帯域が重ならないスペクトルを持つ信号とする(一般的には,目的信号より高い周波数領域に成分を持つノイズなど).

目的信号+ディザー信号をAD変換で取り込んだら,信号処理(フィルタ)でディザー信号の成分をカットすればよい.

数値計算で確認してみた結果を以下に.

入力信号は振幅0.1,周波数800Hzとする.

nodither
+/-0.5を量子化の区切りとする.
上段のグラフのように入力信号の振幅が0.1の場合,AD変換器で量子化されると中段のグラフのように一定値0となってしまい入力信号の情報は失われる.当然スペクトラムも下段のとおり全域で0となる.

次に,振幅50,周波数6.25kHzの正弦波をディザー信号として加えてみる.

dither

ディザー信号を入力信号に重畳すれば,量子化されても入力信号の情報は失われないことが
スペクトルをみればわかる.
不要な6.25kHzは,デシメーションフィルタがカットしてくれるので問題ない.

 

もう一つの例として,入力信号が量子化レベルをぎりぎり超える程度の振幅(0.55とした)を持つケースを以下に示す.

nodither2
入力信号は取り込めているが高調波成分が生じている.量子化された波形をみれば当然.

 

ディザー信号ありの場合.

dither2

こちらの方が,高調波成分が少なく,低ひずみで信号が取り込めているのがわかる.

 

実回路で確認.
下図のとおり,DACのLポートから6.25kHzを出力し,オペアンプの入力に加えた.

dither_circuit1

とりあえず,空中配線で…

20200620_225906

信号レベルが低くても無音となることはなく,変なノイズも発生しなくなった.

 

SDR using dsPIC33FJ64GP802

dsPICで受信用のAF PSN試作して動作確認まで行っていたが,それを使って実際に受信機を組んでみた.

Source code ,  Circuit diagram

動作の様子 Youtube

今のところ,AGCはもちろん,RFアンプも入力のフィルタもないが,それなりにうまく動作しているようなので,きちんとした受信機に仕上げてみようと思う.

AF PSN for DC Reciever

以前,SSB送信用のAF PSN をdsPICで製作した.
その後,受信用もできないかと気になりつつも,ずいぶん時間が経ってしまった.
もちろん,dsPIC33FJ64GP802にこだわらなければ何とでもなるが,
このdsPIC  1個でできれば面白いと思う.

よく知られているとおり,PSN受信機の構成は以下のようになる.

r01.jpg

AF PSNの部分をdsPIC33FJ64GP802 1個で構成したいのだが,
ADCが12bitであることと、2ch同時サンプルができないことが課題.
特に,12bitでは受信用としてダイナミックレンジが満足できない気がして,
いま一つやる気が起きなかったが,ようやく,「とりあえずやってみるか」
という気分になってきた.

受信用も,送信用のAF PSN同様,複素係数フィルタを使う.
r02.jpg

解析信号でブロック図を書くと以下のとおりで,送信用AF PSNもそうだが,
本質的にはフィルタ式と同じ.r03.jpg

 

結論から言えば,以下のような構成で,受信用AF PSNを実現できた.
r04.jpg

r06.jpg

r07b.jpg

r08b.jpg

一応設計通り動作している様子.
実際に受信機として使い物になるかどうかは,後日評価したい.

 

以下は詳細.

1.AD変換
送信用は,入力信号がほぼ話者の声だけと考えられるので,
アンチエイリアスフィルタが無くてもほぼ問題なかったが
受信用はそういうわけにはいかず,AD変換前にナイキスト周波数以上の成分を
十分に除去しなければならない.
カットオフ周波数がナイキスト周波数(1/2サンプリング周波数)に近い
シャープな切れ味のLPFを使って,
サンプリング周波数をあまり上げることなく十分な帯域をとりたいところだが,
そのようなフィルタは,カットオフ付近の移相量が大きく素子感度が高いため
温度変化など環境変化やその他様々な要因で特性が変化しやすい.
一番の問題は,そのようなフィルタが2個(I, Q)必要なことと,
それらが常に同じ特性でなければならないこと.
したがって,素子感度を下げ2個フィルタの特性のばらつきをできるだけ抑えたい.

そのためには,カットオフ付近で位相がなだらかに変化するフィルタが望ましいので,
ベッセルフィルタがよいと思う.
当然,カットオフ(振幅)特性もなだらかになり,シャープな切れ味にはならないので,
アンチエイリアスの能力は低下する.
そこでサンプリング周波数をできるだけ高くする.今回は100kHzとした.
また,dsPIC33FJ64GP802は48MHzのオーバークロックで動作させる.

I,Q 2ch同時サンプルはできず,2μs程度の遅延がありサンプリングタイミングがずれる.
2μsは,例えば3kHzにおいては,位相で 2.16deg に相当する.
これは無視できるレベルではないので,後段の複素係数FIRフィルタで
この遅延を補正することにした.

 

2.デシメーション用 IIRフィルタ
100kHzサンプリングですべて処理できればいいのだが,
dsPIC33FJ64GP802の性能では難しそうなので,
サンプリング周波数を1/8(12.5kHz)に下げる.
そのためには,後段の複素係数フィルタのナイキスト周波数(6.25kHz)以上の成分を
十分に除去しなければならない.
(このIIRフィルタのサンプリング周波数は100kHz)
I,Q 2chのフィルタが必要だが,ディジタルフィルタなので,
特性に違いが生じたり変化する心配はない.
できるだけシャープなカットオフ特性をもったLPFを使いたいので,
今回は,4次Elliptic LPF にしてみた.
IIRフィルタは設計が面倒だが,動作時はFIRより少ない計算量で済む.

2次IIRフィルタは以下のような構成になる.4次はこれを2段直列に接続する.

r05.jpg
kは本来不要なパラメータだが(k=1でよい),dsPICのように固定小数点演算の場合は,
計算途中で結果がオーバーフローしないようにk>1とする必要がある.
今回は k=4 とする.

IIRフィルタの設計には,GNU Octave (フリーウェア)を使った.
設計用のスクリプト ( IIR_ellip_LPF_design.m ) を実行すれば,
b0/k, b1/k, b2/k, a1/k, a2/k, k を定義するヘッダファイルが生成される.
signal パッケージを使うので,Octaveを起動したらコマンドラインで,

pkg load signal

とタイプしておく必要がある.

次に,dsPICでIIRフィルタを実行するソースコード(dsPIC33,DSPライブラリ使用)を
以下に記載しておく.

//— IIR ———————————–
//4th IIR Elliptic LPF coeff.
//fs=100000[Hz],  fc=3000[Hz]
//Ripple=1[dB],  Att=70[dB]

fractional _XDATA(2) IIR_coef0[] =
//{b0/k, b1/k, b2/k, a1/k, a2/k, log2(k) }
{ 53, -26, 53, 15311, -7199,  2};

fractional _XDATA(2) IIR_coef1[] =
//{b0/k, b1/k, b2/k, a1/k, a2/k, log2(k) }
{720, -1161, 720, 15706, -7794,  2};

fractional _YBSS(2) Z_Re_0[5];
fractional _YBSS(2) Z_Re_1[5];
fractional x, y;

void IIR_Ellip_LPF(void)
{
//– 1st stage——————————————
Z_Re_0[0]=x;
y=VectorDotProduct(5, &Z_Re_0[0], &IIR_coef0[0]);
y<<=IIR_coef0[5];
Z_Re_0[2]=Z_Re_0[1];  Z_Re_0[1]=Z_Re_0[0];
Z_Re_0[4]=Z_Re_0[3];  Z_Re_0[3]=y;

//– 2nd stage——————————————
Z_Re_1[0]=y;
y=VectorDotProduct(5, &Z_Re_1[0], &IIR_coef1[0]);
y<<=IIR_coef1[5];
Z_Re_1[2]=Z_Re_1[1];  Z_Re_1[1]=Z_Re_1[0];
Z_Re_1[4]=Z_Re_1[3];  Z_Re_1[3]=y;
//output:  y;
}

DSPライブラリ関数にはIIRフィルタもあるが,処理速度が遅かったので
VectorDotProduct 関数を使って自作してみた.
これはI chのみなので,もう一組IIRフィルタが必要となる.

伝達特性(設計値)は,以下のとおり
(横軸はナイキスト周波数50kHzで正規化)r09.jpg

 

3.複素係数FIRフィルタ(PSN)
サンプリング周波数とタップ数が違うだけで送信用と同じ.
今回128タップとしている(これが限界).

FIRの係数を求める際,AD変換の遅延を補正するよう考慮している.
具体的には,遅延をTとして Im側にのみ周波数特性にexp(- j2πf T) を掛けている.
 Source code,   Circuit diagram, and  Octave Script for filter design 

Signal Meter

VFOのダイアル表示で行った画像処理を使って,シグナルメータを作ってみた.
バックグラウンドにスケールイメージのビットマップを貼り付けておき,
その上を指針が1つ動くだけなので,VFOのダイアル表示よりは処理は軽い.
とは言え,オブジェクト(今回は指針だけだが)のローテーション,
アンチエリアシングやバックグラウンドとのアルファブレンドといった処理を行い,
16bitカラー表示をするので,それなりの性能のMPUが必要かと思い,
PIC32MX250F128B を使ってみた.
(USBは使わないので,PIC32MX150…でいいのだが手持ちがあったので)

20190925_220237.jpg

20190925_220317.jpg

20190925_221951.jpg

動画はこちら

96x64ピクセルのOLEDにしては,きれいな表示ができた気がする.

VFO(6)

ディジタルVFOの場合,周波数ステップの切替えが必須かと思う.
ただ,実際操作してみるとステップ切替え操作はかなり煩わしいもので,なんとかしたいと思っていた.
切替え頻度を減らすにはパルス数の多いエンコーダを使えばよさそうだが,今一つスッキリ解決するような気がしない.それにエンコーダが高価.

そこで,特に目新しいものではないが回転速度を検出してそれに応じて連続的に周波数ステップを変化させるようにしてみた.ここに動画をUPしておく

今回試した方法は次のとおり.
ダイアル速度を vd,加速を始める速度しきい値を vth として,次式のように変数 L に積算していく.
(1) vd>vth のときのみ, L=L + (vd – vth);

また,速度 vdが 0 のときは,減速用の定数をRdec として,
(2) L=L – Rdec;   ただし,L<0 ならL=0;

次に,Lの値からRaccを加速率の定数として次式でステップ数を決める.

(3) ステップ=最小ステップ + Racc*L^n;

現状,n=2 としているが,他の定数も含め操作感に関わる値なので,いろいろと調整するといいと思う.また際限なくステップが大きくなるのを回避するためには,Lの上限を設定すればいい.

ダイアルの速度の検出は簡単で,エンコーダカウント値の絶対値が速度そのものなので,特に何か処理が必要なことはない.
公開しているソースで言えば,count という変数の絶対値が速度( vd=|count| ).
理由は,count はメインループの1回の処理時間当たりのカウント値なので,その絶対値はダイアルの速度に比例したものとなるため.

結局,メインループの処理1回で変化させる周波数Δfは,

(4)    Δf=count*( 最小ステップ + Racc*L^n );

動画は,
vth=2, Racc=0.002, Rdec=1.0, n=2, 最小ステップ=10[Hz]
で動作させたもの.

個人的には,ステップ切替えスイッチは無くてもいい,と思えるくらいの操作感になっていると思う.

VFO(5)

VFOの回路図とソースコードを下記に公開しておいた.
Circuit diagram  and  Arduino Sketch
youtube Movie

画像表示の際アンチエイリアス処理を施しているので,まずその違いを以下に.

アンチエイリアス処理なし

アンチなし.jpg

アンチエイリアス処理あり

20190212_234133.jpg

(数字のフォントが違うのはアンチエイリアス処理とは関係ありません…)

この違いが重要かどうかは人それぞれかと思うが,個人的には上のアンチエイリアス処理なしの表示だと気分がよくない.

次に,LCDとOLEDの比較を.

まず,LCD

20190212_220612.jpg

20190212_220500.jpg

入手できるほとんどのLCDは視野角が広くなく,見るべき方向が規定されている.
このLCDは向かって右から見ると非常にきれいだが,左からみると線は欠けるし色合いもおかしい.
アンチエイリアス処理による微妙な輝度を再現できていない感じ.

たぶん携帯電話用のLCDだと思うので,縦において少し下方から見るように作られているのだと思う.実際ほとんどのLCDのデータシートには,View angle : 6 o’clock と表記がある.

OLEDの場合.

20190212_224237.jpg

20190212_224229.jpg

当然ながら,どの角度から見ても見え方に変化はなくきれい.

OLEDが100%よさそうだが欠点もある.

20190209_021913.jpg

明るい色のベタはむらが出るし(写真ではわかりにくいが.縞が見えるのはスキャンによるもの),ドライブ電流が足りなくなるのか暗くなる.
「一部だけ白」などはきれいだか,「全面白」とかはあまりよくない.

結論としては,暗いバックグラウンド色で使うならOLED.
明るいバックグラウンド色にするなら見る角度限定でLCDか…

VFO(4)

LCDへの表示がうまくいったので,次はOLED(NHD-1.69-160128UGC3) に表示させてみる.
これのコントローラチップは,SEPS525
16bitごとにCSの区切りが必要なのかどうか読み切れなったが,なんとなく「当然必要でしょ」という雰囲気が感じられたので,LCDのときのようにはいかないかも… と思いながら,とりあえず同じ転送プログラムを試してみた.
やはりNG…
spi.write16() を使って1ピクセルずつ送るとOKだったので,16bitごとにCSの区切りが必要のようだ.
そこで転送プログラム以下のようにしてみた.

s2.png

ブロックサイズを16にしてやることで,16bitごとにCSがネゲートされうまくいった.

20190212_234133.jpg
しかし,CSが頻繁にネゲートされるのでその時間(1回あたり600ns程度)が加わり,転送時間が25ms程度になってしまった.
これにダイアルイメージ描画時間35msとあわせて,更新レートはおよそ60msとなった.
これくらいになると動きのスムーズさがいま一つと感じるのは否めない.
RX621のシステムでは8bitパラレル転送+描画時間20msで更新レートが約30ms程度だったので,それに比べるとかなり動きが悪く見える.

何とかしたいところだが,これ以上プログラムの最適化で何とかできるようにも思えないので,デュアルコアを活用し描画処理と転送を別のコアで行うことで速度UPを試みる.

こちらのサイトを参考にさせていただき,メインのLOOPとは別のコアで動作するもう一つのタスクを作った.

setup()内で,

xTaskCreatePinnedToCore(task0, “Task0”, 4096, NULL, 1, NULL, 0);

として,次の関数を用意すればいいようだ.

void task0(void* arg)
{
while (1)
{
//SPI転送処理
delay(1);
}
}

delay(1) がないとシステムリセットが繰り返し発生した.
よくわかっていないが,おそらくOSに処理を返す必要があるからだろうと推測.
また,同じディレイ時間にも関わらず delayMicroseconds(1000) ではNGだった.

とにかく,これでloop() とtask0() が別コアに割り当てられ同時に動く(と思う).
あとは,この2つの処理間でのデータの同期に気を付ければいい.

というわけで,
一時はあきらめて,IOピンがまだ余っているのでOLEDはパラレルでいこうか…,などと考えたが,OLEDでもSPIで更新レートを50ms以下になんとかすることができた.

もっと簡単にRX621からESP32に移行できると思っていたが,思いの外苦労してしまった.
とは言え,初めてESP32を使ったのにもかかわらず,短時間でそれなりのものが組めたのは,いろんな方がネットにUPしてくれている情報のおかげなのは間違いない.

感謝の意味を込めて今回作成したソース(VFOsys)をこちらに公開しておく(無保証,サポートなしで).
エンコーダを回せば,ダイアルが動き(動画)その周波数が出力されるだけのものなので,そこからはこのソースをベースに各々機能を追加していただければと思う.

とにかく,ようやくシステムがうまく動き出したので,次はLCDとOLEDの比較をしてみよう.

VFO(3)

RX621で開発を進めていたところESP32が気になってきて調べてみると,
さらに安価で動作クロックも速く,しかもデュアルコア…
これは乗り換えるしかない,ということで  ESP32-DevKitC  を入手.20190206_125309~2.jpg
Arduino環境で開発できるようで,こちらを参考にさせていただきセットアップ.

ESP32のIO数があまり多くないので,ディスプレイはSPIで制御することにする.
幸い,OLED(NHD-1.69-160128UGC3) は,シリアル/パラレルどちらの接続もできる.
同じ解像度(160 x 128)のシリアル接続のLCDも用意して比較することにした.

20190213_024447.jpg

回路は下記のとおり.
LCDとOLEDは排他利用となる.Si5351は秋月電子のモジュール.

VFO.PNG

20190212_233108.jpg 20190212_233146.jpg

ディスプレイのインターフェース信号の配置が気持ち悪いが,ESP32内蔵のSPIを使うには
こうなってしまうようだ.
任意のピンでSPIができるようだが,その場合ライブラリによるソフトウェアSPIになり,転送速度がかなり遅くなるらしい.
以前,ダイアル表示のカウンタを作ったとき経験だが,ダイアルイメージの更新レートがおおよそ50ms以上になってくるとダイアルの動きがスムーズに見えなくなってくる感じだったので,これ以下にはしたい.

SPIのレジスタを直接操作しないと無理だろうなと思い,いろいろ調べてみると,
こちらのサイトに手掛かりがあり参考になった.
ESP32のリファレンスマニュアルも読んでみると,SPIには32bit x 16 (512bit)のバッファがあって,そこにデータを貯めておき(例では,半分の256bit),これを1ブロックとして一機に送出する方法らしい.これを必要回数繰り返す.
今回は,16bit/pixelx128x160=327680 bit 転送しないといけないので,
バッファをフルに使えば,327680/512=640回 となる.

以下のようなプログラム(抜粋)を組んで動作を確認した.
配列GRAM65k[][](16bitカラーのイメージデータ)をすべてディスプレイに転送する.

s1.png

オシロで波形を見てみると,1ブロック転送中はCSはアサートされたままになるようだ.
16bitごとにCSを区切らないといけないのでは?と思ったが,どうやらST7735やILI93xxといったディスプレイコントローラはその必要はないようだ.

肝心の転送時間だが,SPIクロック周波数が27MHzでおよそ12ms だった.
クロック周波数がディスプレイコントローラの動作範囲を超えているが,
「実力的にOK」との情報が多数あったのでそれに期待.

結果としては,上手く動作した.
20190212_220612.jpg

ただ,転送時間は12msだがダイアルイメージの描画処理に35msほどかかっている.
RX621のときは20ms以下だったのでかなり遅くなってしまったのは予想外だった.
CPUクロックが2倍以上なのにどういうことだろう?
当然ながら,速くなることを期待してたのでかなりテンションが下がる…
バックグラウンドで動いているfreeRTOSのせいかもしれないし,
デュアルコアも活かせてないし仕方ないか…

でもまあ,CPU自体RXよりかなり安価でRAMが520KBもあるのは捨てがたい.
現状,更新レートは目標通り50ms以下(12ms+35ms)にはなっていて,視覚的にもあまり違和感はないのでこのまま進めることにしよう.

次は,OLEDで動作確認しないと.

VFO(2)

アナログダイアル表示のVFOシステムがようやく具体的になってきた.
以前つくったカウンタのハードはほぼそのままで,ファームだけを新しく作ろうと思っていたが,コスト下げるためにハードも新しくすることにする.

CPUにPIC32MZを使っていたが,高いのでRX621にする.
基板を起こすまでは秋月のボードでデバッグできるので,その辺りも選択理由.
ただし,RAMが96kBなのでカラーディスプレイの解像度を,160x128に落とさざるを得ない.ただ,それもコストダウンにつながるのでいいかと…
160x128だと画面が1.7~1.8インチ.小さすぎないか少々気になるが,機械のパネルに配置するには,むしろこれくらいの大きさがちょうといいかもしれない.

ところが,LCDを探していると同じ解像度と大きさのカラーOLEDが見つかった.

NHD-1.69-160128UGC3

3000円を超えるのでどうしようか迷ったが,一度使ってみることにした.
(コストダウンしようとしているのに本末転倒…)
LCDは,見る角度で色合いが変わったり,見にくくなるのが非常に気になっていたため
それが解消されるなら,ありかなと思う.

とりあえず表示だけさせて雰囲気を確認.

oled1.jpg
アンチエイリアス処理をしているので輪郭が少々ぼやけるが,画面が1.7インチと小さいので,目視ではあまり気にならない.
この解像度(160x128)では,アンチエイリアス処理をしないと微妙な角度の短い線などは,見るに堪えない形状になる.

こんな角度でも色合いや見やすさは変わらない.20190122_125705_Burst01.jpg20190122_125736.jpg

よさそうなので,もう少しファームを作り込んでみた.

oled5.jpg

あとは,Si5351AからLo 信号と,必要に応じてCar信号 を出すようにするのと,
カウンタと同様,周波数オフセット値の設定など自由にできるようにする機能を
追加していく.

スペアナ制御ツール

先日,アンリツのスペアナMS2661Aを中古で入手した.
9kHz~3GHzの帯域で,RBWの最小は30Hz(オプション付)と
性能的には納得して購入したが,測定データの保存方法で悩んでしまった.

20181010_061554

メモリインターフェースオプションが付いていて,
PCカードメモリにデータを保存できるようだが,
今時PCカードメモリなど入手困難なので,SD用PCカードアダプタを使ってみた.

結果は残念ながら,”メモリタイプが異なる”とのメッセージが出てNGだった.
このスペアナが作られた時代にはSDは無かったのかもしれない.

仕方がないので,GPIBかRS232Cポート経由で,
データをPCに転送して表示・保存することにした.
ただ,プログラムを作る必要があるので少々面倒だ.
(なので,できればメモリインターフェースを使いたかった)

測定器といえばGPIBというところだが,
PC側にもGPIBインターフェースが必要になり,面倒なのでRS232Cを使うことにした.
ただ,通信速度が9600bps と,今時としてはかなり遅い感じがするのは否めない.

簡単なプログラムを作って試したところ,うまい具合にデータの取得・表示ができた.

42641369_1386471288151860_1717727983411658752_n

42727699_1386471231485199_1732282193058725888_n

必要な人は他にはおそらくいないと思うが,Windows10で動作するMS2661A用の
データキャプチャソフトをこちらに置いておく (  MS2661A.exe ) 

センター,スパン,スタート,ストップ周波数に関しては,
このソフトからリモートコントロールできる.
通信速度が遅いので,データの取得には  3, 4秒かかる.
これが速ければ,リアルタイムでPCに波形表示ができるのだが・・・

FT817ND   433MHz とその第2高調波
FT817_430M

433MHz拡大
FT817_433.000MHz

 

FIRフィルタ

前回AF PSNを複素係数のFIRフィルタで作成したが,その係数を変えれば任意の周波数特性を持ったフィルタが作れることはいうまでもなく,AF PSN はその一例にすぎない.

FIRフィルタの係数は,周波数特性を逆フーリエ変換すれば得られるので手間はかからない.とはいえ手計算できるものでもないので,AF PSN 作成の際に,係数を求めるために作ったツールをここにUPしておいた(Calc_Coeff_of_CPLXBPF.m).

これは,拡張子mが示すようにMATLAB,GNU Octave 上で動くスクリプトである.
GNU Octave はフリーウェアでこちらから入手でき,Windows の場合,octave-4.4.0-w64_1-installer.exe ならインストールは容易かと思う.

スクリプト内でパスバンドの周波数を書き換えて実行すれば,その特性を実現する係数を定義するヘッダファイルが自動生成される.
これをincludeしてコンパイルすれば,設定どおりのパスバンドを持ったBPFが実現できる.
複素係数フィルタであるが,実数部のみ使用すれば普通のフィルタと同じである.

さて,前回製作したAF PSNであるが,パスバンド内のリップルとパスバンド外の減衰特性が気になった.周波数特性を求めたところ下図のようになった.

Rect

理由は,窓関数を使用していなかった(=矩形窓)ためである.
そこで,フィルタ係数に窓関数を掛けてみた.
(窓関数はスクリプト内で指定できるようにしてある)

Hann 窓
han

Hamming 窓
hamming

Blackman 窓
blackman

Bartlett 窓
bartlett

使用する窓関数としては Hamming か Blackman になるだろう.
Blackman のほうが減衰特性がなだらかであるが,それでも十分急峻な減衰特性なのでこれがよいと思う.
(ヘッダファイルとCソースファイルを更新しているので,日付を確認の上ご利用願います)

AF PSN

PSN方式でSSBを発生させるためのAF PSNを試作した.01
PSN方式のSSBジェネレータは上図のような構成であるが,大概AF PSNの実現がネックとなる.

アナログ回路でAF PSNを実現する方法としては,ナガード型PSN,多段のオールパスフィルタ,PPSNなどが知られている.
これらは,性能が十分でなかったり,回路規模が大きい,部品定数の精度が要求される,など少々難しいところがあり,さらにアナログであるが故,環境(周囲温度など)の変化による特性の変化にも気を付ける必要がある.

そこで今回はAF PSN をディジタル信号処理で実現することにした.これは近年よく見かけるSDRと同様なものである.

まず,SSBの発生方法から確認していく.SSBの生成方法はシンプルに考えれば下図のとおりである.
02
周波数シフトは,AF PSNの後の直交変調器で行われるので,
結局AF PSNとは,AF信号の負(あるいは正)の周波数成分を除去するものである.

ならばバンドパスフィルタ(BPF)で実現できそうだが,フィルタの周波数特性は,下図の上段に示すように,正の周波数領域と負の領域で対称なものしか実現できない(実係数フィルタの場合).

03

しかしながら,フィルタの係数を複素数にすれば(複素係数フィルタ),上図下段に示すような非対称な周波数特性をもったフィルタが実現できる.

複素係数フィルタを用い,信号を複素数表現すれば,SSB生成の流れは下図のようになる.
04

今回はこの複素係数フィルタを,dsPIC33FJ64GP802 を用いディジタル信号処理で実現してみた.下図のように,複素係数フィルタの出力の実部がAF PSNの0°出力,虚部が90° 出力に相当する.
05

 

複素係数フィルタの構成は,512Tap FIR フィルタとした.
06

 

フィルタの周波数特性(設計目標)は下図のとおり.
+300Hz~+3kHzのバンドパスで,負の領域はすべて阻止する.
Fres_cmplx

 

これを逆フーリエ変換し,FIRフィルタの係数を求めた.
サンプリング周波数は,使用するdsPICの性能により 約10kHz に設定した.

フィルタ係数(実部:R0 – R511)
FirCoeff_re

フィルタ係数(虚部:Q0 – Q511)FirCoeff_im

複素係数フィルタのプログラムのソースコード  Source  code

dsPIC周辺の接続は下図のとおり.07

 

動作確認の結果を以下にまとめておく.

20180706_071500 dsPIC

入力200Hz
f0200

入力300Hz
f0300

入力1000Hz
f1000

入力2000Hz
f2000

入力2900Hz
f2900.jpg

入力3000Hz
f3000

これらの結果を見る限りは使い物になりそうな感じがする.
部品選別や調整などなしに,プログラムを書き込みさえすればこれと全く同じ結果が得られる.
あとは実際にSSBジェネレータにしたときにどのような品質の信号になるか,確認が必要だろう.

ヤマハPortaSound PS3修理

3か所,音の出ない鍵盤があるとのことで修理を依頼された.
PS3は最も初期のポータサウンドの一つらしい.

20180602_112810

 

とりあえず開腹して,一枚目の基板を外した.
YAMAHAのオリジナルICを使用している.
年代を感じさせる作りで,ピッチの基準はLC発振のようだ.

20180602_094518

 

鍵盤下の基板を外したところ.

20180602_100253
音の出ない鍵盤に対応する接点をショートさせてみても,やはり音が出ない.
ということで接点不良ではなさそう.

 

該当する接点につながっているパターンをたどっていくと,電池の液漏れによるパターンの腐食があった.
調べてみると導通がない.音の出ない鍵盤に対応する接点はすべてこのラインにつながっているので,故障の原因はこれだろう.

20180602_102749

 

 

ということで,バイパス.
隣りのパターンも怪しかったので処置しておいた.

20180602_105935

 

これで音も出るようになり,修理完了.

ヘッドマイク

最近は中国製の安いヘッドマイクが購入できる.これらはほぼエレクトレットコンデンサマイクでバイアス電源が要る.
一方ミキサのXLRマイク入力には,大抵ファンタム電源が供給できるようになっているが,これはコンデンサマイク用でエレクトレットコンデンサマイクのバイアスには使えない.
また,エレクトレットコンデンサマイクの出力は不平衡なので,そのまま長く引き回すとノイズがのる.
そのようなわけで,ヘッドマイクをミキサのマイク入力(XLRコネクタ)につなぐためのアダプタケーブルを製作した.

20180508_230606~2
ケースの中身は,ファンタム電源からエレクトレットコンデンサマイク用のバイアスを作る分圧回路と不平衡を平衡に変換する差動アンプである.
試作ということで,ケースを3Dプリンタで作り空中配線で仕上げたが,結構面倒でユニバーサル基板を使えばよかったかもしれない.

20180508_232430~2

ケースにはベルトなどに引っ掛けるようクリップをつけた.
こういうのは3Dプリンタのおかげで簡単にできるのでありがたい.

VFO

カウンタ基板にはSi5351が実装できるようにすでにパターンを作っているので,
次にダイアルイメージ表示のVFOをやってみようと思う.
カウンタの場合はすべて受け身の動作でよかったが,
VFOとなると,システムから考え直さないといけない.

20180507_211713~2

周波数はエンコーダで設定するが,
周波数に応じて外部のバンドパスフィルタを切り替える,
などということも必要となるから,そういう制御信号をださないといけないし,
どの周波数のとき,どのフィルタを使うのか,
そこはユーザが設定できるようにしないといけない.
Si5351を使うなら,もちろんキャリア信号も出したいから,
その周波数の設定と,モードによっては受信時はオフに…
と,考えたらきりがない感じだが,少し時間をかけて検討しよう.

周波数カウンタ

周波数カウンタは,今では格安のキットなどもあるので,
今更…な感があるが,今回少々変わったものを作った.
とはいえ,実用性がよいとか,性能が向上したとかではない.

カウントした周波数を数値ではなく,
あえて読み取り精度が低下するダイアルイメージで表示する.
20180415_132715

昔,ラジオなど作るときに一番工作に苦労したのが周波数表示ダイアルだった.
今は,周波数カウンタやPLL,DDSといったデバイスがあるので
そのようなダイアルは不要であるが…
とはいえ,ディジタル表示よりもダイアルがいい,と思うことがときどきあり,
そのあたりが製作の動機である.

表示には2.4インチのTFTカラーLCDを使った.
CPUはPIC32MZEF.
SRAMが512kBあるので外付けメモリなしでOKだった.

 

表示のレイアウトなど,気分でいろいろと変られるように設定用のPCアプリもつくってみた.
”dcon.exe”

dconfg

以下は設定例.
sample00

完成品を¥10,000 で頒布しています.
お問い合わせのフォームからご連絡ください.

音響ホーン

以前は40KHzのトランスデューサ(写真左)を使ってバットディテクターを製作していたが,
40KHz付近しか観測できない,という制約があった.
今回のバットディテクターでは,
広帯域で超音波を観測するためにMEMSマイク(写真右)を使っている.
ただ,トランスデューサは40KHz付近の感度は高く,
40KHzに限った感度の比較では,MEMSマイクは今一つな感じがする.

20180311_133045~2

写真を見ればわかるように,
MEMSマイクの音響ホールは直径0.5mmしかない.
トランスデューサの方は,内部の振動板らしきものは,直径8mmほどある.
開口面積がMEMSマイクの方が格段に小さいため,
感度が悪いのも無理はない.

そこで,音響ホーンを用いて開口面積を大きくし,感度の向上を試みた.

20180311_131230~2

3Dプリンタで作成した超音波用の音響ホーン.

定量的なデータはとっていないが,聞き比べた感じでは,
トランスデューサと同等以上の感度が得られているようだ.

バットディテクタ

知人からの相談がきっかけで,
新しい方式のバットディテクターを開発,試作した.

詳細はこちら

バットディテクターは,コウモリの出す超音波を可聴音に変換するもので,
主な方式として次の3種類のものが従来からある.

・ヘテロダイン式 (Heterodyne)
・フリークエンシーディビジョン式(Frequency Division:周波数分周式)
・タイムエキスパンション式(Time Expansion:時間軸延長式)
Wikiより.

これらの方式では,

・ヘテロダイン式
チューニング操作による探索が必須.周波数が大きく異なるものは同時観測できない.
・周波数分周式
レベルが最も高い信号しか観測できない.周波数以外の情報が失われる.
・時間軸延長式
リアルタイムで観測できない.

といった短所がある.

今回開発したのものは,
超音波領域の信号のスペクトルを周波数方向に圧縮し
全てを可聴音の領域に詰め込む方法.
「スペクトラム圧縮式」としておく.
この方式では,従来方式の上記欠点が改善される.
つまり,チューニング操作不要で,
周波数に関わらず同時に複数対象を
リアルタイムに観測できる.

sc01

信号処理方法としては,下記のように2種類考えられる.
今回は,下の方法で試作をしてみた.
20171212-2

Si5351で直交信号

直交信号が得られると非常に有用なので,Si5351で試してみた.
MultiSynth のDelay機能を使えば,ch間の位相差(時間差)を設定できる.
結論としては,3MHz以上で直交信号を得ることができた.

サンプルコード(Si5351Aの設定に関する部分のみ)を修正しておいた.
ch0, ch1 に出力されるようになっている.

以下詳細.

Delayは,td= N/(4*fvco) —-(1)
で,N は,0~127の範囲で設定できる.

出力周波数は, fout=fvco/(M*R) —- (2)
(M:MultiSynthの分周比,R:出力dividerの分周比)

90度位相差に相当するDelayは, td=1/(4*fout) —- (3)

(1), (2), (3) より,
N=M*R

つまり,fvco と fout の比(fvco/fout )を設定すれば
90度の位相差となる.

ここでNの値として設定できるのが最大127まで,というのが制限になる.
fvcoの下限は375MHzであるから,
(fvco=fxtal*(a+b/c) で,a>=15, fxtal=25MHz.よって25*15=375 )

375/127≒2.95MHz.これが,直交信号が得られる下限となる.

ちなみに,VCO自体の周波数範囲は,
使用したチップの実測で 200MHz~1160MHz くらいあるようで,
意外と広帯域である.

3MHz
20170314_3M

7MHz
20170314_7M

14MHz
20170314_14M

21MHz
20170314_21M

28MHz
20170314_28M

50MHz
20170314_50M

Si5351

Clock Generator Si5351A搭載のモジュールを入手したので動かしてみた.
今回作成したサンプルコード
(Si5351Aの設定に関する部分のみ)を置いておく.

これだけで2.5kHz ~ 200MHzの任意の周波数を発生できる.
周波数分解能は出力周波数にもよるが,約6Hz(150MHz~200MHz時)
出力周波数が低いほど高分解能になる(Divider で分周するため).
20170313_1

7.000000MHz 出力
20170313_7M

受信機でモニタした限りでは,通信に使えそうなC/Nではないかと思う.
矩形波なので高調波の問題はあるが,
受信用のローカルOSCとして使用するならこのままでいいかもしれない.

200.000000MHz 出力
20170313_200M

こちらは,モニタできないので波形のみ観測.
100MHzのオシロなので,フィルタがかかってきれいな波形になっているが,
信号がおよそ200MHzで出ていることは確認できる.

位相も設定できるようなので,後日試してみたい.