في هذا البرنامج التعليمي (إذا كان بإمكانك تسميته) سأوضح لك كيف يمكنك بسرعة وببساطة تنظيم تشغيل ملف صوتي باستخدام متحكم ESP32.
قليلا من النظرية
كما تخبرنا ويكيبيديا ، فإن ESP32 عبارة عن سلسلة من وحدات التحكم الدقيقة منخفضة التكلفة ومنخفضة الطاقة. إنها نظام على شريحة (SoC) مع وحدات تحكم وهوائيات Wi-Fi و Bluetooth مدمجة. استنادًا إلى نواة Tensilica Xtensa LX6 في متغيرات أحادية وثنائية النواة. تم دمج مسار تردد الراديو في النظام. تم إنشاء وتطوير MK بواسطة شركة Espressif Systems الصينية ، ويتم تصنيعها بواسطة TSMC وفقًا لتقنية المعالجة 40 نانومتر. يمكنك قراءة المزيد عن إمكانيات الشريحة على صفحة ويكيبيديا وفي الوثائق الرسمية.
مرة واحدة ، كجزء من إتقان وحدة التحكم هذه ، أردت تشغيل الصوت عليها. في البداية اعتقدت أنني سأضطر إلى استخدام PWM. ومع ذلك ، بعد قراءة الوثائق عن كثب ، اكتشفت وجود قناتين من DAC 8 بت. بالطبع ، هذا غير الأمر جذريًا.
يقول المرجع الفني أن DAC في ESP32 مبني على سلسلة من المقاومات (على ما يبدو ، فهذا يعني سلسلة R2R) باستخدام نوع من المخزن المؤقت. يمكن أن يتنوع جهد الخرج من 0 فولت إلى جهد إمداد (3.3 فولت) بدقة 8 بت (أي 256 قيمة). تحويل القناتين مستقل. يوجد أيضًا مولد CW مدمج ودعم DMA.
قررت عدم الخوض في DMA في الوقت الحالي ، وقصرت نفسي على بناء لاعب على أساس مؤقت. كما تعلم ، من أجل إعادة إنتاج أبسط ملف WAV لتنسيق PCM ، يكفي قراءة البيانات الأولية منه بمعدل أخذ العينات المحدد في الملف ودفعه عبر قنوات DAC ، مما يقلل بشكل مبدئي (إذا لزم الأمر) من شهاد البيانات إلى شاهد DAC. لقد كنت محظوظًا: لقد وجدت مجموعة من الأصوات بتنسيق أحادي WAV PCM 8 بت 11025 هرتز ، مأخوذة من موارد لعبة قديمة. هذا يعني أننا سنستخدم قناة DAC واحدة فقط.
سنحتاج أيضًا إلى مؤقت قادر على توليد مقاطعات 11025 هرتز. وفقًا للمرجع الفني نفسه ، يحتوي ESP32 على وحدتي مؤقت مع مؤقتين لكل منهما ، بإجمالي أربعة عدادات. وهي بعرض 64 بت ، ولكل منها مقياس مسبق 16 بت والقدرة على إنشاء مقاطعة على مستوى أو حافة.
من النظرية إلى التطبيق
مسلحًا بمثال wave_gen من esp-idf ، انطلقت لكتابة الكود. لم أزعج نفسي بإنشاء نظام ملفات: كان الهدف هو الحصول على صوت ، وليس إخراج لاعب كامل من ESP32.
بادئ ذي بدء ، لقد تجاوزت أحد ملفات WAV إلى مجموعة sish. ساعدتني أداة xxd المضمنة في دبيان كثيرًا في هذا الأمر. أمر بسيط
$ xxd -i file.wav > file.c
نحصل على ملف 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
من هنا يمكنك أن ترى أن ملفنا يحتوي على قناة واحدة ، ومعدل أخذ العينات 11025 هرتز ودقة 8 بت لكل عينة. لاحظ أنه إذا كنت أرغب في تحليل الرأس برمجيًا ، فحينئذٍ سأحتاج إلى مراعاة ترتيب البايت: في WAV يكون الأمر Little-endian ، أي البايت الأقل أهمية أولاً.
انتهى بي الأمر بإنشاء نوع هيكل لتخزين المعلومات الصوتية:
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 ، وحقل البيانات هو مؤشر إلى مصفوفة بها بيانات.
ثم قمت بتوصيل ملفات الرأس:
#include "driver/timer.h"
#include "driver/dac.h"
وأنشأ نماذج أولية من الوظائف لتهيئة المؤقت ومعالج مقاطعة الإنذار الخاص به ، كما هو الحال في مثال wave_gen:
static void IRAM_ATTR timer0_ISR(void *ptr)
{
}
static void timerInit()
{
}
ثم بدأ في ملء وظيفة التهيئة.
تنتهي أجهزة ضبط الوقت في ESP32 بتسجيلها من APB_CLK_FREQ تساوي 80 ميجا هرتز:
السائق / المؤقت. 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 هرتز). في معالج المقاطعة ، سنقوم بتمرير مؤشر إلى البنية بالبيانات التي نريد إعادة إنتاجها.
وبالتالي ، تبدو وظيفة تهيئة المؤقت كما يلي:
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 ++;
}
نعم ، العمل مع DAC المدمج في ESP32 يتلخص في استدعاء وظيفة مضمنة واحدة dac_output_voltage (في الواقع لا).
في الواقع ، هذا كل شيء. نحتاج الآن إلى تمكين تشغيل قناة DAC التي نحتاجها داخل وظيفة app_main () وتهيئة المؤقت:
void app_main(void)
{
…
ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1));
timerInit();
التجميع والوميض والاستماع :) بشكل أساسي ، يمكنك توصيل السماعة مباشرة بساق وحدة التحكم - ستلعب. لكن من الأفضل استخدام مكبر للصوت. لقد استخدمت TDA7050 الذي كان موجودًا في حاوياتي.
هذا كل شئ. نعم ، عندما بدأت أخيرًا في الغناء ، اعتقدت أيضًا أن كل شيء أصبح أسهل بكثير مما كنت أعتقد. ومع ذلك ، ربما تساعد هذه المقالة بطريقة ما لأولئك الذين بدأوا للتو في إتقان ESP32.
ربما يومًا ما (وإذا كان أي شخص يحب هذا المقال السفلي) ، سأقود ESP32 DAC باستخدام DMA. لا يزال الأمر أكثر إثارة للاهتمام هناك ، لأنه في هذه الحالة سيكون عليك العمل مع وحدة I2S المدمجة.
محدث.
قررت أن أعطي مثالًا على كيفية التظاهر. هذه لوحة من Heltec مزودة بجهاز الإرسال والاستقبال OLED و LoRa ، والتي ، بالطبع ، لا يتم استخدامها في هذه الحالة.