このチュートリアルでは(それと呼べる場合)、ESP32マイクロプロセッサを使用してオーディオファイルの再生をすばやく簡単に整理する方法を紹介します。
少し理論
ウィキペディアが教えてくれるように、ESP32は一連の低コスト、低電力のマイクロコンピューターです。これらは、Wi-FiおよびBluetoothコントローラーとアンテナが統合されたシステムオンチップ(SoC)です。シングルコアおよびデュアルコアバリアントのTensilicaXtensaLX6コアに基づいています。無線周波数パスがシステムに統合されています。 MKは、中国の企業Espressif Systemsによって作成および開発され、40nmプロセス技術に従ってTSMCによって製造されています。チップの機能の詳細については、Wikipediaページと公式ドキュメントをご覧ください。
かつて、このコントローラーをマスターする一環として、サウンドを再生したいと思いました。最初はPWMを使わないといけないと思いました。しかし、ドキュメントを詳しく読んだ後、8ビットDACの2つのチャネルの存在を発見しました。もちろん、これは根本的に問題を変えました。
テクニカルリファレンスによると、ESP32のDACは、特定のバッファを使用する抵抗のチェーン(明らかに、R2Rチェーンを意味します)上に構築されています。出力電圧は、0ボルトから供給電圧(3.3ボルト)まで8ビット(つまり256値)の分解能で変化させることができます。 2つのチャネルの変換は独立しています。組み込みのCWジェネレーターとDMAサポートもあります。
私は今のところDMAに入らないことに決め、タイマーに基づいてプレーヤーを作成することに限定しました。ご存知のように、PCM形式の最も単純なWAVファイルを再現するには、ファイルで指定されたサンプリングレートでファイルから生データを読み取り、DACチャネルを介してプッシュし、データのビット数をDACのビット数に事前に減らします(必要な場合)。私は幸運でした。古いゲームのリソースからリッピングされた、WAV PCM8ビット11025Hzモノラルフォーマットのサウンドのセットを見つけました。これは、DACチャネルを1つだけ使用することを意味します。
11025Hzの割り込みを生成できるタイマーも必要です。同じテクニカルリファレンスによると、ESP32には、それぞれ2つのタイマーを備えた2つのタイマーモジュールが搭載されており、合計4つのタイマーがあります。これらは64ビットで、それぞれに16ビットのプリスケーラーがあり、レベルまたはエッジで割り込みを生成する機能があります。
理論から実践へ
esp-idfのwave_genの例を用意して、コードの作成に取り掛かりました。私はファイルシステムを作成することを気にしませんでした。目標は音を出すことであり、ESP32から本格的なプレーヤーを作ることではありませんでした。
まず、WAVファイルの1つをsish配列に追い越しました。Debianに組み込まれているxxdユーティリティは、これに大いに役立ちました。簡単なコマンド
$ xxd -i file.wav > file.c
内部に16進形式のデータの配列があり、ファイルサイズをバイト単位で含む別の変数があるsishファイルを取得します。
次に、配列の最初の44バイト(WAVファイルのヘッダー)をコメントアウトしました。途中で、フィールドごとに解析し、必要なすべての情報を見つけました。
const uint8_t sound_wav[] = {
// 0x52, 0x49, 0x46, 0x46, // chunk "RIFF"
// 0xaa, 0xb4, 0x01, 0x00, // chunk length
// 0x57, 0x41, 0x56, 0x45, // "WAVE"
// 0x66, 0x6d, 0x74, 0x20, // subchunk1 "fmt"
// 0x10, 0x00, 0x00, 0x00, // subchunk1 length
// 0x01, 0x00, // audio format PCM
// 0x01, 0x00, // 1 channel, mono
// 0x11, 0x2b, 0x00, 0x00, // sample rate
// 0x11, 0x2b, 0x00, 0x00, // byte rate
// 0x01, 0x00, // bytes per sample
// 0x08, 0x00, // bits per sample per channel
// 0x64, 0x61, 0x74, 0x61, // subchunk2 "data"
// 0x33, 0xb4, 0x01, 0x00, // subchunk2 length, bytes
ここから、ファイルに1つのチャネル、11025ヘルツのサンプリングレート、サンプルあたり8ビットの解像度があることがわかります。プログラムでヘッダーを解析する場合は、バイト順序を考慮する必要があることに注意してください。WAVでは、リトルエンディアン、つまり最下位バイトが最初になります。
サウンド情報を格納するための構造タイプを作成することになりました。
typedef struct _audio_info
{
uint32_t sampleRate;
uint32_t dataLength;
const uint8_t *data;
} audio_info_t;
そして、構造自体のインスタンスを作成し、次のように入力します。
const audio_info_t sound_wav_info =
{
11025, // sampleRate
111667, // dataLength
sound_wav // data
};
この構造では、sampleRateフィールドは同じ名前のヘッダーフィールドの値であり、dataLengthフィールドはsubchunk2 lengthフィールドの値であり、dataフィールドはデータを含む配列へのポインターです。
次に、ヘッダーファイルを接続しました。
#include "driver/timer.h"
#include "driver/dac.h"
wave_genの例のように、タイマーとそのアラーム割り込みハンドラーを初期化するための関数プロトタイプを作成しました。
static void IRAM_ATTR timer0_ISR(void *ptr)
{
}
static void timerInit()
{
}
それから彼は初期化関数を記入し始めました。
ESP32のタイマーは、最終的に80MHzに等しいAPB_CLK_FREQからクロックされます。driver
/ timer.h:
#define TIMER_BASE_CLK (APB_CLK_FREQ) /*!< Frequency of the clock on the input of the timer groups */
soc / soc.h:
#define APB_CLK_FREQ ( 80*1000000 ) //unit: Hz
アラーム割り込みを生成する必要があるカウンター値を取得するには、タイマーのクロック周波数をプリスケーラーの値で除算し、次に割り込みをトリガーするために必要な周波数(私たちの場合は11025 Hz)で除算する必要があります。割り込みハンドラーで、再現したいデータを含む構造へのポインターを渡します。
したがって、タイマー初期化関数は次のようになります。
static void timerInit()
{
timer_config_t config = {
.divider = 8, //
.counter_dir = TIMER_COUNT_UP, //
.counter_en = TIMER_PAUSE, // -
.alarm_en = TIMER_ALARM_EN, // Alarm
.intr_type = TIMER_INTR_LEVEL, //
.auto_reload = 1, //
};
//
ESP_ERROR_CHECK(timer_init(TIMER_GROUP_0, TIMER_0, &config));
//
ESP_ERROR_CHECK(timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0x00000000ULL));
// Alarm
ESP_ERROR_CHECK(timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, TIMER_BASE_CLK / config.divider / sound_wav_info.sampleRate));
//
ESP_ERROR_CHECK(timer_enable_intr(TIMER_GROUP_0, TIMER_0));
//
timer_isr_register(TIMER_GROUP_0, TIMER_0, timer0_ISR, (void *)&sound_wav_info, ESP_INTR_FLAG_IRAM, NULL);
//
timer_start(TIMER_GROUP_0, TIMER_0);
}
タイマーのクロック周波数は、どのプリスケーラーを設定しても、11025で割り切れません。そのため、必要な周波数にできるだけ近い周波数の分周器を選択しました。
それでは、割り込みハンドラーの作成に移りましょう。ここではすべてが単純です。アレイから次のバイトを取得し、それをDACにフィードして、アレイに沿ってさらに移動します。ただし、まず、タイマー割り込みフラグをクリアして、アラーム割り込みを再開する必要があります。
static uint32_t wav_pos = 0;
static void IRAM_ATTR timer0_ISR(void *ptr)
{
//
timer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0);
// Alarm
timer_group_enable_alarm_in_isr(TIMER_GROUP_0, TIMER_0);
audio_info_t *audio = (audio_info_t *)ptr;
if (wav_pos >= audio->dataLength) wav_pos = 0;
dac_output_voltage(DAC_CHANNEL_1, *(audio->data + wav_pos));
wav_pos ++;
}
はい、ESP32の組み込みDACを使用すると、1つの組み込み関数dac_output_voltageを呼び出すことになります(実際にはそうではありません)。
実際、それだけです。次に、app_main()関数内で必要なDACチャネルの操作を有効にし、タイマーを初期化する必要があります。
void app_main(void)
{
…
ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1));
timerInit();
組み立て、点滅、リスニング:)基本的に、スピーカーをコントローラーの脚に直接接続できます。再生されます。ただし、アンプを使用することをお勧めします。ビンに置いてあったTDA7050を使用しました。
それで全部です。はい、ようやく歌い始めたとき、思ったよりずっと楽になったと思いました。ただし、この記事は、ESP32をマスターし始めたばかりの人に何らかの形で役立つかもしれません。
多分いつか(そして誰かがこの記事の下で好きなら)私はDMAを使ってESP32DACを運転するでしょう。この場合、組み込みのI2Sモジュールを使用する必要があるため、ここではさらに興味深いものです。
UPD。
私はそれがどのように機能するかを示す例を示すことにしました。これは、OLEDとLoRaトランシーバーを備えたHeltecのボードですが、もちろんこの場合は使用されません。