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 とし DAMバッファ数も限界まで減らた ( .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 のカットオフ周波数をスイープする処理を記述した(動作確認済).