Arduino-ESP32 ver2でもI2S経由で内蔵DACを利用


# 1. はじめに
ESP32は簡単にI2Sを利用できますが、Arduino-ESP32 ver2以降で従来のコードがうまく動かないことがあります。今回はDACを内蔵している従来型ESP32(末尾にSやCが付かないもの)を対象に、Arduino-ESP32 ver2 でI2S経由で内蔵DAC(内部DAC)を使う例を挙げたいと思います。

# 2. バージョン差異について

## 2.1. 背景

2022年現在、Arduino-ESP32は、ver1系とver2系があり、それぞれESP-IDFのver3以前とver4以降が元になっています。
ESP-IDF ver4では従来型DACを内蔵していないESP32-C3やESP32-Sシリーズへの対応のため、ペリフェラル関連の関数を細かく分けられるなどの大幅な変更が発生しました。
とはいえ、従来のソースコードが扱えるようにと、従来の関数も利用できるように工夫されています。
従来の関数は、新規の関数で構成されており、Arduino-ESP32 ver2でのI2S周りはわずかに変更が存在します。


## 2.2. I2Sにおけるコンパイラバージョンの差異

次のドキュメントが判り易いです。

https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32/api-reference/peripherals/i2s.html#i2s-file-structure

従来のものはlegacy、新しいものはnewと記載されています。
特に次の図が従来と新しいものについてイメージしやすいです。
![](https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32/_images/i2s_file_structure.png)

なお、従来バージョンのドキュメントは次にあります。

https://docs.espressif.com/projects/esp-idf/en/v3.3.3/api-reference/peripherals/i2s.html

I2Sで内蔵を使うにあたって、従来と新しいもので異なるのは次の2つです。
- 通信モード
- 出力ワード位置

## 2.3. 通信モードの変更

従来通信モードで使っていたのは、```I2S_COMM_FORMAT_I2S_MSB ```でしたが、新規では```I2S_COMM_FORMAT_STAND_MSB```です。
どのような通信をしているかは、次の部分を見ると判りやすいです。

https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32/api-reference/peripherals/i2s.html#standard-mode

まとめると次のようになります
| | パラメタ| 値|参考|
|---|---|---|---|
|従来|i2s_config_t.communication_format|I2S_COMM_FORMAT_I2S_MSB|1|
|新規| i2s_driver_config_t.communication_format| I2S_COMM_FORMAT_STAND_MSB|2 |

なお、Arduino-ESP32 V2でi2s_config_tを定義しても、i2s_driver_config_tに置き換えられます。


## 2.4. 出力ワード位置

内蔵DACは8bit出力です。I2Sは16bit幅ですので、その中の8bitを使うことになります。Arduino-ESP32 新旧とも変わらず上位8bitが内蔵DACに渡されます。
ただ、右チャネル(DAC_0 / IO25)への出力は、communication_formatの違いのためか、従来は後半のワードだったのが、新しいものは前半のワードになりました。
まとめると、次の表のようになります。

| ver \ Byte | 0 |  1 |  2 | 3 |
|---|---|---|---|---|
|従来|(無視)|左|(無視)| **右**| 
|新規|(無視)|**右**|(無視)| 左| 

1サンプルだけDMA転送するコードを考えた場合、
従来は次の通りです。
```c++
uint8_t  buf[4];
buf[3] = sound();
size_t t;
i2s_write(I2S_NUM_0,buf,sizeof(buf), &t, portMAX_DELAY);
```

新規は次の通りです。
```c++
uint8_t  buf[4];
buf[1] = sound();
size_t t;
i2s_write(I2S_NUM_0,buf,sizeof(buf), &t, portMAX_DELAY);
```
もしかしたら、I2Sは配列を16bitで1ワードとして考えるべき、という意見があるかもしれません。その場合は、次のように記載することになるでしょう。

従来は次の通りです。
```c++
uint16_t  buf[2];
buf[1] = sound()<<8;
size_t t;
i2s_write(I2S_NUM_0,buf,sizeof(buf), &t, portMAX_DELAY);
```

新規は次の通りです。
```c++
uint16_t  buf[2];
buf[0] = sound()<<8;
size_t t;
i2s_write(I2S_NUM_0,buf,sizeof(buf), &t, portMAX_DELAY);
```

なお、sound()は符号無し1バイトを返す関数です。例えば、次のようなノコギリ波を出力する関数を想定しています。

```c++
uint8_t sound(){
 static uint8_t k;
 return( k+=1);
}
```

## 2.3. コンパイラバージョンの確認

ESP-IDF はマクロで```IDF_VER```にバージョン値がコンパイル時に渡されるようになっているようです。

https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32/versions.html#checking-the-current-version

これは、Arduino-ESP32 ver2系でも有効です。そして、ver1系では定義されていません。
よって、2022年現在は、```IDF_VER```が定義されているか否かを確認することで、Arduino-ESP32 ver2系かver1系かをコンパイル時に知る事ができます。


# 3. サンプルコード

次に示すコードは、https://qiita.com/dzonesasaki/items/a01ab2af42f4ce722120 の修正版コードです。(見る時は ▶ をクリックして下さい。)

<details><summary>esp32_DacI2sSample01.ino</summary><div>

```ino
#include <driver/i2s.h>


#define PARAM_SAMPLING_FREQUENCY  (44100)
#define NUM_BUFFER_LEN    (1024)

const uint8_t cui_DC_OFFSET = 127;//fixed value
const float cf_FREQ_ORIGN = 55.0;
uint8_t gmuiBuffer[NUM_BUFFER_LEN];

const uint8_t cuiBPM = 80; //beats per minute

const uint8_t cui_AMPLITUDE = 100;//max 127
const uint8_t cuiNumNote = 12; //number of note
const uint8_t cuiMusOctave = 4; 
uint8_t guiMusLen = 4; //note length

float gmfItn[cuiNumNote]; // inversed cycle of current pitch
uint32_t gmuiTn[cuiNumNote];// cycle of current pitch
float gfMusNotesLen = (float)PARAM_SAMPLING_FREQUENCY*60.0*4 /(float)cuiBPM /(float)guiMusLen+0.5; 

uint32_t guiPosNoteCurrent = 0;//index for Current Note
uint32_t guiXt = 0; //counter for ocsillator
uint32_t guiXn = 0; //counter for note

#ifdef IDF_VER
#define I2SOFFSET 1
#define MY_I2S_COMM_FORMAT I2S_COMM_FORMAT_STAND_MSB
#else
#define I2SOFFSET 3
#define MY_I2S_COMM_FORMAT I2S_COMM_FORMAT_I2S_MSB
#endif

void setup_i2s()
{
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
    .sample_rate          = PARAM_SAMPLING_FREQUENCY,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = MY_I2S_COMM_FORMAT,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 2,
    .dma_buf_len          = NUM_BUFFER_LEN,
    .use_apll             = false,
    .tx_desc_auto_clear   = true,
    .fixed_mclk           = 0,
  };

  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, NULL);               // 25
  i2s_zero_dma_buffer(I2S_NUM_0);
}


uint8_t sg_main(){
  int16_t iRes = 0;
  float ftmp = sin(2*PI*gmfItn[guiPosNoteCurrent]*guiXt);
  iRes = (int16_t)(ftmp*cui_AMPLITUDE);
  iRes += cui_DC_OFFSET;

  guiXt++;
  guiXn++;

  // reset counter for current pitch
  if(guiXt >= gmuiTn[guiPosNoteCurrent]){
    guiXt = 0;
  }

  // reset counter for current note
  if(guiXn >= gfMusNotesLen){
    guiXn = 0;
    guiPosNoteCurrent +=1;    
    if(guiPosNoteCurrent >= cuiNumNote){
      guiPosNoteCurrent = 0;
    }
  }

  return (uint8_t)iRes;
}//end sg_main()

void  setup_calc_music_note(){
  for(uint32_t k =0; k < cuiNumNote; k++){
    gmfItn[k] = pow(2.0,(k*2)/12.0+cuiMusOctave)*cf_FREQ_ORIGN/(float)PARAM_SAMPLING_FREQUENCY;
    gmuiTn[k] = (1.0/gmfItn[k]);
  }
}//end setup_calc_music_note()

void setup_zero_clear_buffer(){
  for (uint32_t k = 0; k < NUM_BUFFER_LEN; k++) gmuiBuffer[k] = 0;
}//end setup_zeroClear_buffer()

void setup() {
  Serial.begin(115200);
  setup_i2s();
  setup_calc_music_note();
  setup_zero_clear_buffer();
  
}//end setup()

void loop() {
  size_t transBytes;
  i2s_write(I2S_NUM_0, (char*)gmuiBuffer, NUM_BUFFER_LEN, &transBytes, portMAX_DELAY);

  for (uint32_t k = 0; k < NUM_BUFFER_LEN; k += 4) {
    gmuiBuffer[k +  I2SOFFSET] = sg_main();
  } 
}//end loop()

```


</div></details>


このコードの肝は、マクロで値を切り替える点です。

```
#ifdef IDF_VER
#define I2SOFFSET 1
#define MY_I2S_COMM_FORMAT I2S_COMM_FORMAT_STAND_MSB
#else
#define I2SOFFSET 3
#define MY_I2S_COMM_FORMAT I2S_COMM_FORMAT_I2S_MSB
#endif
```

表にすると次のようになります。

| | communication_format| DAC_0用データ位置|参考|
|---|---|---|---|
|従来|I2S_COMM_FORMAT_I2S_MSB | 4Byte目(0はじまりの3)|
|新規| I2S_COMM_FORMAT_STAND_MSB |2Byte目(0はじまりの1)|


# 4. おわりに

2016年ごろまではWi-Fi搭載したマイコンというと1万円近くしていたのが、数百円で購入できるESP8266とESP32の登場によって、IoT分野は大きく変化したのではないでしょうか。
そんなESP32は従来型からcシリーズやsシリーズのようにコアが刷新され種類が豊富になってきました。
しかし、ペリフェラル周りも変化が生じ、ESP-IDFを見るとそのまま利用できるようにという工夫は見られるものの、今回のように微妙に異なる点が現れたりしています。
最近raspberry pi pico wという新しいライバルも現れましたが、入手性では今のところESP32しかない状況です。ESP32を使う限り、この変更を受け入れるしかないのだろうと思います。
今後、ネット接続可能なマイコンがどのようになっていくか、楽しみにしたいと思います。


# A. 参考

https://github.com/espressif/arduino-esp32/issues/6856#issuecomment-1153294550

https://qiita.com/ain1084/items/1c00669461d079d3b242