LoginSignup
9
7

More than 1 year has passed since last update.

M5StickCで ESPNowトランシーバーを作ってみる話

Last updated at Posted at 2021-09-22

1. はじめに

M5StickC には マイクロフォンがついており
HAT として スピーカーに簡単接続できる

最近ESPNowっぽいものでセンサを繋ぎまくった経緯があり
どれくらいの大きいデータを送信できるのかの検証を兼ねて
作ってみる

  • 近くのホームセンターで ESPNow で 33m の通信が出来感動したことは言うまでもありません
  • 最初は なんちゃってBle通信だったんですけど 10m くらいしか出来なかった

2. M5StickC I2Sの扱い方

色々とWEBをさまよったあげく
lang-shipさんの素敵なサイトに辿りつく

なるほど..
録音は I2S Mic -> soundBuffer -> soundStorage
再生は soundStorage -> soundBuffer -> I2S Speaker
って感じで食わせるという事なんですね

3. HATを使ってI2Sで再生もできちゃうのね

2個の M5StickC は持っているのだが
HAT は 1個しかない... 100円ガチャで入手したスピーカーとか
繋いでも大丈夫なんだろうか...
という事で半田コテで GND/G26 に繋がるように製作する

IMG_3765.jpg

IMG_3766.jpg

4. 仕様

二つの M5StickC に同じプログラムを転送して
録音が終わったら soundStorage 全体を ESPNow で転送して
ブロードキャストしてあげれば MacAddress に関係なく
受信した側は soundStorage の中身を再生すれば良い
これでトランシーバーっぽいのが出来上がるという事になる
転送の開始と転送の最後に1Byteのフラグ(STX/ETX)を送り
旨い具合に同期を取る感じで....

  • ESPNOW_SEND_DELAY が3になってますが delay を入れないと大きいデータは上手い具合に同期が取れないようで resurt を見てちょっと愕然としました:frowning2:
myESPNow.h
//============================================> myESPNow.h
//
// M5Stick-mic-3.ino
// +-----+
// |     |
// |     | Rec:
// |     | I2S Mic -> soundBuffer -> soundStorage
// |     |
// |     | Play:
// |M5   | soundStorage -> soundBuffer -> I2S Speaker
// |Stick|
// +-----+
//
// M5WalkyTalky.ino
// +-----+                                            +-----+
// |     |                                            |     |
// |     | Rec:                                       |     |
// |     | I2S Mic -> soundBuffer -> soundStorage     |     |
// |     |                                            |     |
// |     |                                      :Play |     |
// |Talk | soundStorage -> soundBuffer -> I2S Speaker |Listn|
// |   er|                                            |   er|
// +-----+                                            +-----+
//

#include <esp_now.h>
#include <WiFi.h>
esp_now_peer_info_t slave;
#define ESPNOW_MAXSEND (250)        // ESPNow送信最大値
#define ESPNOW_SEND_DELAY (3)       // 最低の待ち時間
#define STX (0x02) // 転送開始
#define ETX (0x03) // 転送終了

// MacAdrsを表示する
void dispAdrs(const uint8_t *mac_addr)
{
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  M5.Lcd.println(macStr);
}

void prePlay()
{
  // 初期雑音を消す
  memset(soundStorage, 0, sizeof(soundStorage));
  recPos = 128;
  i2sPlay();
}

// タイトル表示
void titleDisp()
{
  M5.Lcd.print(" ");
  M5.Lcd.setTextColor(WHITE, BLACK);
  M5.Lcd.println(" M5WalkyTalky ");
  M5.Lcd.setTextColor(BLACK, WHITE);
  M5.Lcd.println(" BtnA Rec/Speak" );
  M5.Lcd.println(" BtnB Play" );
}

// データ送信コールバック
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  //  dispAdrs(mac_addr);
  //  M5.Lcd.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail");
}

// データ受信コールバック
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *recv, int recvcnt) {
  static int row_ = 0;
  //  dispAdrs(mac_addr);
  Serial.printf("[recv] recvcnt:%d\n", recvcnt);

  if (recvcnt == 1) {
    // 転送された長さが1Byte?
    if (recv[0] == STX) {
      // STX?
      row_ = 0;
      recPos = 0;
      memset(soundStorage, 0x0, sizeof(soundStorage));
    } else {
      // ETX?
      Serial.printf("[recv] row:%d recPos:%d\n", row_, recPos);
      digitalWrite(10, !HIGH);
      M5.Lcd.setCursor(0, 24);
      M5.Lcd.println(" Play...   ");
      M5.Lcd.setCursor(0, 32);
      M5.Lcd.printf(" Recv <= %5d Byte", recPos);
      i2sPlay();
      M5.Lcd.setCursor(0, 24);
      M5.Lcd.println("           ");
      digitalWrite(10, !LOW);
    }
  } else {
    // recvcntが1以外
    memcpy(&soundStorage[recPos], recv, recvcnt);
    recPos += recvcnt;
    row_ ++;
  }
}

void resultCheck(int cnt, esp_err_t result)
{
  switch (result) {
    case ESP_OK:
      break;
    case ESP_ERR_ESPNOW_NOT_INIT:
      Serial.printf("%d:ESPNOW not Init.\n", cnt);
      break;
    case ESP_ERR_ESPNOW_ARG:
      Serial.printf("%d:Invalid Argument.\n", cnt);
      break;
    case ESP_ERR_ESPNOW_INTERNAL:
      Serial.printf("%d:Internal Error.\n", cnt);
      break;
    case ESP_ERR_ESPNOW_NO_MEM:
      Serial.printf("%d:ESP_ERR_ESPNOW_NO_MEM.\n", cnt);
      break;
    case ESP_ERR_ESPNOW_NOT_FOUND:
      Serial.printf("%d:Peer not found.\n", cnt);
      break;
    default:
      Serial.printf("%d:Not sure what happened.\n", cnt);
      break;
  }
}

void sendESPNow()
{
  uint8_t dt[1] = {0};
  size_t sendPos = 0;
  int mod = recPos % ESPNOW_MAXSEND;
  int row = recPos / ESPNOW_MAXSEND;

  esp_err_t result;
  Serial.printf("[send] row:%d mod:%d\n", row, mod);
  M5.Lcd.setCursor(0, 32);
  M5.Lcd.printf(" Send => %5d Byte", recPos);

  dt[0] = STX;
  result = esp_now_send(slave.peer_addr, dt, 1);
  delay(ESPNOW_SEND_DELAY);
  resultCheck(-1, result);

  int i;
  for (i = 0; i < row; i++) {
    result = esp_now_send(slave.peer_addr, &soundStorage[sendPos], ESPNOW_MAXSEND);
    delay(ESPNOW_SEND_DELAY);
    resultCheck(i, result);
    sendPos += ESPNOW_MAXSEND;
  }
  if (mod) {
    result = esp_now_send(slave.peer_addr, &soundStorage[sendPos], mod);
    delay(ESPNOW_SEND_DELAY);
    resultCheck(i, result);
    sendPos += mod;
  }
  dt[0] = ETX;
  result = esp_now_send(slave.peer_addr, dt, 1);
  delay(ESPNOW_SEND_DELAY);
  resultCheck(-1, result);
  Serial.printf("[send] sendPos %d\n", sendPos);
}

void setupESPNow()
{
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  if (esp_now_init() == ESP_OK) {
    Serial.print("ESPNow Init Success\n");
  } else {
    Serial.print("ESPNow Init Failed\n");
    ESP.restart();
  }
  pinMode(10, OUTPUT);
  digitalWrite(10, !LOW);
  memset(&slave, 0x0, sizeof(slave));
  for (int i = 0; i < sizeof(slave.peer_addr); ++i) {
    slave.peer_addr[i] = (uint8_t)0xff;
  }
  esp_err_t addStatus = esp_now_add_peer(&slave);
  esp_now_register_send_cb(OnDataSent);
  esp_now_register_recv_cb(OnDataRecv);
}

//<============================================ myESPNow.h

5. lang-shipさんの ino に組み込む

  • M5WalkyTalky.ino myESPNow.h は同じフォルダに入れて下さい
  • 立ち上がりに スピーカーから雑音が出ちゃうので prePlay(recPos = 128 で短い再生)を実行
  • 100KByte を確保しようとすると謎のエラー しかたなく80KByteに調整
  • サンプリングレートは 44100 で 3秒くらいしか音が出ない、それだと話にならないので 16384 で調整、これで5秒は話せる
  • 録音は BtnA、 離すと自動的にブロードキャストします
  • 話したあとに BtnB で再生出来ます
  • 受信した側も BtnB で再度再生が出来ます
  • 修正部分には //edit がついてます
M5WalkyTalky.ino
//
// M5StickCのマイクを使ってみる その3 録音再生
// https://lang-ship.com/blog/work/m5stickc-mic-3/
//
//

#include <M5StickC.h>
#include <driver/i2s.h>

#define htonl(x) ( ((x)<<24 & 0xFF000000UL) | \
                   ((x)<< 8 & 0x00FF0000UL) | \
                   ((x)>> 8 & 0x0000FF00UL) | \
                   ((x)>>24 & 0x000000FFUL) )

#define PIN_CLK       (0)           // I2S Clock PIN
#define PIN_DATA      (34)          // I2S Data PIN
#define SAMPLING_RATE (16384)       // サンプリングレート(44100, 22050, 16384, 8192, more...) // edit
#define BUFFER_LEN    (1024 *  1)   // バッファサイズ // edit
#define STORAGE_LEN   (1024 * 80)   // 本体保存容量(MAX 100K前後) // edit

#define WAVE_EXPORT   (0)           // WAVEファイルに出力するか


uint8_t soundBuffer[BUFFER_LEN];    // DMA転送バッファ
uint8_t soundStorage[STORAGE_LEN];  // サウンドデータ保存領域

bool recFlag = false;               // 録音状態
int recPos = 0;                     // 録音の長さ

// 録音をする
void i2sRecord() {
  // 録音用設定
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
    .sample_rate          = SAMPLING_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ALL_RIGHT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 2,
    .dma_buf_len          = BUFFER_LEN,
    .use_apll             = false,
    .tx_desc_auto_clear   = true,
    .fixed_mclk           = 0,
  };

  // PIN設定
  i2s_pin_config_t pin_config;
  pin_config.bck_io_num   = I2S_PIN_NO_CHANGE;
  pin_config.ws_io_num    = PIN_CLK;
  pin_config.data_out_num = I2S_PIN_NO_CHANGE;
  pin_config.data_in_num  = PIN_DATA;

  // 録音設定実施
  i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM_0, &pin_config);
  i2s_set_clk(I2S_NUM_0, SAMPLING_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

  // 録音開始
  recFlag = true;
  xTaskCreatePinnedToCore(i2sRecordTask, "i2sRecordTask", 2048, NULL, 1, NULL, 1);
}

// 再生をする
void i2sPlay() {
  // 再生設定
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
    .sample_rate          = SAMPLING_RATE,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 2,
    .dma_buf_len          = 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);
  i2s_zero_dma_buffer(I2S_NUM_0);

  // 再生
  size_t transBytes;
  size_t playPos = 0;
  while ( playPos < recPos ) {
    for ( int i = 0 ; i < BUFFER_LEN ; i += 2 ) {
      soundBuffer[i] = 0;                         // 下位8ビットは無視される
      soundBuffer[i + 1] = soundStorage[playPos]; // 上位8ビットにuint8_tのデータを入れる
      playPos++;
    }

    // データ転送
    i2s_write(I2S_NUM_0, (char*)soundBuffer, BUFFER_LEN, &transBytes, (100 / portTICK_RATE_MS));
//    Serial.println(playPos); // edit
  }
  // 後始末
  i2s_zero_dma_buffer(I2S_NUM_0);
  i2s_driver_uninstall(I2S_NUM_0);
}

// 録音用タスク
void i2sRecordTask(void* arg)
{
  // 初期化
  recPos = 0;
  memset(soundStorage, 0, sizeof(soundStorage));

  // 録音処理
  while (recFlag) {
    size_t transBytes;

    // I2Sからデータ取得
    i2s_read(I2S_NUM_0, (char*)soundBuffer, BUFFER_LEN, &transBytes, (100 / portTICK_RATE_MS));

    // int16_t(12bit精度)をuint8_tに変換
    for (int i = 0 ; i < transBytes ; i += 2 ) {
      if ( recPos < STORAGE_LEN ) {
        int16_t* val = (int16_t*)&soundBuffer[i];
        soundStorage[recPos] = ( *val + 32768 ) / 256;
        recPos++;
      } else {
        digitalWrite(10, !LOW); // edit
      }
    }
    Serial.printf("transBytes = %d, STORAGE_LEN=%d, recPos=%d\n", transBytes, STORAGE_LEN, recPos);
    vTaskDelay(1 / portTICK_RATE_MS);
  }

  i2s_driver_uninstall(I2S_NUM_0);

  // タスク削除
  vTaskDelete(NULL);
}

#include "myESPNow.h" // edit

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(WHITE);
  M5.Lcd.setTextColor(BLACK, WHITE);
  /* // edit
    M5.Lcd.println("Sound Recorder");
    M5.Lcd.println("BtnA Record");
    M5.Lcd.println("BtnB Play");
  */
  setupESPNow();  // edit
  prePlay(); // edit
  titleDisp();  // edit
}

void loop() {
  M5.update();

  if ( M5.BtnA.wasPressed() ) {
    digitalWrite(10, !HIGH); // edit
    // 録音スタート
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println(" Rec...    ");
    Serial.println("Record Start");
    i2sRecord();
  } else if ( M5.BtnA.wasReleased() ) {
    digitalWrite(10, !LOW); // edit
    // 録音ストップ
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println("           ");
    recFlag = false;
    delay(100); // 録音終了まで待つ
    sendESPNow(); // edit
    Serial.println("Record Stop");

    // WAVEファイルをシリアルに出力
    if ( WAVE_EXPORT ) {
      Serial.printf("52494646");                        // RIFFヘッダ
      Serial.printf("%08lx", htonl(recPos + 44 - 8));   // 総データサイズ+44(チャンクサイズ)-8(ヘッダサイズ)
      Serial.printf("57415645");                        // WAVEヘッダ
      Serial.printf("666D7420");                        // フォーマットチャンク
      Serial.printf("10000000");                        // フォーマットサイズ
      Serial.printf("0100");                            // フォーマットコード
      Serial.printf("0100");                            // チャンネル数
      Serial.printf("%08lx", htonl(SAMPLING_RATE));     // サンプリングレート
      Serial.printf("%08lx", htonl(SAMPLING_RATE));     // バイト/秒
      Serial.printf("0200");                            // ブロック境界
      Serial.printf("0800");                            // ビット/サンプル
      Serial.printf("64617461");                        // dataチャンク
      Serial.printf("%08lx", htonl(recPos));            // 総データサイズ

      for (int n = 0; n <= recPos; n++) {
        Serial.printf("%02x", soundStorage[n]);
      }
      Serial.printf("\n");
    }
  } else if ( M5.BtnB.wasReleased() ) {
    // 再生スタート
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println(" Play...");
    Serial.println("Play Start");
    i2sPlay();
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println("        ");
    Serial.println("Play Stop");
  }

  delay(10);
}

6. 最後に

ESPNow の転送はセンサーでは随分とやっていたのですが
さすがに 80KByte はきつかったです
でも これで写真でも映像でも送れる事がわかって一安心です

いつも素晴らしい src を公開してくださっている lang-shipさんに感謝です
今後ともよろしくお願い申し上げます
https://lang-ship.com/blog/

9
7
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
9
7