تعلم كيفية التحدث ، أو ESP32 DAC وجهاز ضبط الوقت

أثناء تطوير جهاز واحد مثير جدًا للاهتمام (إذا كان لدي ما يكفي من القوة فقط) ، قررت أنه سيكون من الجيد أن يتحدث الجهاز. كان وجود DAC ثنائي القناة 8 بت في وحدة التحكم الدقيقة المستهدفة ، ESP32 من Espressif Systems ، مفيدًا.



في هذا البرنامج التعليمي (إذا كان بإمكانك تسميته) سأوضح لك كيف يمكنك بسرعة وببساطة تنظيم تشغيل ملف صوتي باستخدام متحكم 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 ، والتي ، بالطبع ، لا يتم استخدامها في هذه الحالة.






All Articles