2023年8月26日土曜日

ESP32でMAX98357Aを使ってみた

 I2S接続のモノラルなアンプ付きDACがAliexpressで155円だったのでポチってみた。送料は一応このDAC2個の他に3点入れても170円だった(トータル1000円かからないお買い物)

MAX98357A搭載のモジュールなんだけど、1チップでI2SのDACとD級アンプが内蔵されていてスピーカーと直結できる模様。モノラルなのでステレオで出したい場合は2個必要らしい。一応2個買ったけど。

MAX98357A
見た目はAdafruitのものとほぼ同じである。基板の色が違うぐらいしか…
デフォルトでは5Vで動作させるとモードがLRミックスでゲインが9dbに設定されているっぽい。

さっそくESP32に接続して使ってみた。

ライブラリは有名所っぽいのが2種類あったのでまずはESP32-audioI2Sを試してみた。
こちらのライブラリは簡単にMP3なネットラジオを再生できるようなサンプルがあったのでそのまま試してみた。
しかしビットレートが高くなったとき?にプツプツと切れるっぽい。ESP32でストリーミングデータを受信しながらMP3デコードはちょっと厳しいのかな?

ライブラリもデュアルコアのESP32にしか対応していないっぽいし…

念のため別な方のライブラリも試してみることに。
ESP8266AudioはESP-WROOM-02でも音声が再生できるようなライブラリ。内蔵DACにも対応している。
ESP32でこのライブラリを使う場合のデフォルトピン配置は
LRC→25
BCLK→26
DIN→22
になっていた。このモジュールはアンプ内臓なのでVinは5Vに接続した。

こちらにはWebRadioというサンプルがあったのでそのまま使ってみた。しかしブラウザからシリアルコンソールに出たIPにアクセスしてもブラウザに"ERR_EMPTY_RESPONSE"というエラーが出て設定画面が開けない…

色々探していると、サンプルのweb.cpp内の処理でflush()関数がESP32 Arduino Coreのバージョン1と2で動作が異なるのに起因しているっぽい。このサンプルは1.0.x向けに作られているのでここらへんを2.0.xで動くように作り変えないといけない。GitHubのフォーラムに修正方法が載っていた。
ということでweb.cpp内の
    client->flush();
ってなってるところ2箇所を
    while (client->available()) {
      client->read();
    }
のように変更した。
Coreのv1ではflush()関数がバッファクリアしていたのに対して、v2ではflush()関数が処理が終わるのを待つような仕様になったためらしい。なのでバッファが0になるまでreadでバッファをクリアしている。(Serialも同じような仕様変更あったな…)
この変更をしたらブラウザで設定画面が開けるようになった。

同じラジオ局で試してみたがこっちのほうが安定性がいいかも。
最初の方はプツプツ切れたりするんだけど安定するとちゃんと再生できてるっぽい?

ちなみにESP32のI2S機能はGPIO Matrixに対応しているっぽいのでピンは入力専用ピン以外だったらどこでも使えそう。ライブラリによって微妙にピン設定が違うのでちょっとハマったけど、好きなピンに設定してしまえば良さそう。wclkPinはモジュール上はLRCだったりとか呼び方も微妙に違ったり…

さすが外付けDAC。ジャンクノートPCのスピーカを繋いで使ってみたけど音質は結構クリア。簡易的なWebラジオとして使う分には十分なんじゃないかな。


2023年8月5日土曜日

ESP32でWiFi SDカードリーダを作ってみた。

 前にESP32でFTPサーバを立てられるようにしていたんだけど、ESP32側で生成したログファイルとかをダウンロードするだけならブラウザ経由でダウンロードしたほうが手っ取り早いんじゃないかということでWeb版のファイルマネージャーを作ってみた。

SDカードはSD_MMC.hで高速にアクセスできる配線にしてある。起動時にプルアップしておけないIOは起動後にプルアップするようにGPIO27でプルアップしてみた。

#include <WiFi.h>
#include <WebServer.h>
#include "SD_MMC.h"
#include "esp_mac.h"  //MACアドレス取得用

#define WiFi_password "12345678"
#define WiFi_ch 3
#define SD_PULLUP 27

WebServer server(80);
File uploadFile;

String htmlHeader() {
  return R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP32 SD_MMC File Manager</title>
</head>
<body>
<h2>ESP32 SD_MMC File Manager</h2>

<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="upload">
<input type="submit" value="Upload">
</form>
<hr>
)rawliteral";
}

void listFiles() {
  String html = htmlHeader();
  File root = SD_MMC.open("/");
  File file = root.openNextFile();
  while (file) {
    String name = file.name();
    html += "<p>";
    html += name;
    html += " [<a href=\"/download?name=" + name + "\">Download</a>]";
    html += " [<a href=\"/delete?name=" + name + "\">Delete</a>]";
    html += "</p>";
    file = root.openNextFile();
  }
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleDownload() {
  if (!server.hasArg("name")) {
    server.send(400, "text/plain", "Bad Request");
    return;
  }
  String filename = server.arg("name");
  String path = "/" + filename;
  File file = SD_MMC.open(path);
  if (!file) {
    server.send(404, "text/plain", "File Not Found");
    return;
  }
  server.sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
  server.sendHeader("Content-Type", "application/octet-stream");
  server.sendHeader("Connection", "close");
  server.streamFile(file, "application/octet-stream");
  file.close();
}

void handleDelete() {
  if (!server.hasArg("name")) {
    server.send(400, "text/plain", "Bad Request");
    return;
  }
  String filename = "/" + server.arg("name");
  if (SD_MMC.exists(filename)) {
    SD_MMC.remove(filename);
  }
  server.sendHeader("Location", "/");
  server.send(303);
}

void handleUpload() {
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    String filename = "/" + upload.filename;
    uploadFile = SD_MMC.open(filename, FILE_WRITE);
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    if (uploadFile) {
      uploadFile.write(upload.buf, upload.currentSize);
    }
  } else if (upload.status == UPLOAD_FILE_END) {
    if (uploadFile) {
      uploadFile.close();
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(SD_PULLUP, OUTPUT);
  digitalWrite(SD_PULLUP, HIGH);
  if (!SD_MMC.begin()) {
    Serial.println("SD_MMC Mount Failed");
    return;
  }
  if (SD_MMC.cardType() == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }
  Serial.println("SD_MMC Ready");
  uint8_t macaddr[6];
  esp_read_mac(macaddr, ESP_MAC_WIFI_SOFTAP);  //WiFi Macアドレス読み込み
  char SSID[14];
  sprintf(SSID, "ESP32-%02X%02X", macaddr[4], macaddr[5]);
  WiFi.softAP(SSID, WiFi_password, WiFi_ch);  //SoftAPを起動
  server.on("/", listFiles);
  server.on("/download", handleDownload);
  server.on("/delete", handleDelete);
  server.on(
    "/upload", HTTP_POST, []() {
      server.sendHeader("Location", "/");
      server.send(303);
    },
    handleUpload);
  server.begin();
}

void loop() {
  server.handleClient();
}

一応アップロード機能もついてるのファイルアップロードもできる。ディレクトリは面倒なのでとりあえず非対応で。ログファイルを生成してダウンロードする用途だったら十分使えそうなレベル。スマホとかでもそのまま使えるのでFTPより便利かも…