العمل مع عينات الصوت ESP32

نشرت: 2022-03-11

ESP32 هو متحكم دقيق من الجيل التالي يدعم WiFi و Bluetooth. إنها خليفة Espressif ومقرها شنغهاي للمتحكم الدقيق ESP8266 المشهور جدًا - وبالنسبة لجمهور الهواة ، ثوري.

تعتبر ESP32 من بين وحدات التحكم الدقيقة ، وتشمل مواصفات ESP32 كل شيء ما عدا حوض المطبخ. إنه منتج نظام على شريحة (SoC) ويتطلب عمليا نظام تشغيل للاستفادة من جميع ميزاته.

سيشرح هذا البرنامج التعليمي ESP32 ويحل مشكلة معينة تتعلق بأخذ عينات من المحول التناظري إلى الرقمي (ADC) من مقاطعة المؤقت. سوف نستخدم Arduino IDE. حتى لو كان أحد أسوأ IDEs من حيث مجموعات الميزات ، فإن Arduino IDE سهل الإعداد والاستخدام على الأقل لتطوير ESP32 ، ولديه أكبر مجموعة من المكتبات لمجموعة متنوعة من وحدات الأجهزة الشائعة. ومع ذلك ، سنستخدم أيضًا العديد من واجهات برمجة تطبيقات ESP-IDF الأصلية بدلاً من واجهات Arduino ، لأسباب تتعلق بالأداء.

صوت ESP32: المؤقتات والمقاطعات

يحتوي ESP32 على أربعة أجهزة توقيت ، مقسمة إلى مجموعتين. جميع أجهزة ضبط الوقت هي نفسها ، حيث تحتوي على أجهزة قياس مسبق 16 بت وعدادات 64 بت. يتم استخدام قيمة المقياس المسبق للحد من إشارة ساعة الجهاز - التي تأتي من ساعة داخلية 80 ميجاهرتز تدخل في المؤقت - لكل علامة Nth. الحد الأدنى لقيمة المقياس المسبق هو 2 ، مما يعني أنه يمكن للمقاطعات إطلاق النار رسميًا عند 40 ميجاهرتز على الأكثر. هذا ليس سيئًا ، لأنه يعني أنه عند أعلى دقة للمؤقت ، يجب أن يتم تنفيذ كود المعالج في 6 دورات ساعة على الأكثر (240 ميجاهرتز الأساسية / 40 ميجاهرتز). تحتوي المؤقتات على العديد من الخصائص المرتبطة:

  • divider - قيمة المقياس المسبق للتردد
  • counter_en - ما إذا كان قد تم تمكين عداد 64 بت المرتبط بالمؤقت (صحيح عادةً)
  • counter_dir - ما إذا كان العداد يزيد أو ينقص
  • alarm_en تم تمكين "المنبه" ، أي إجراء العداد
  • auto_reload - ما إذا تمت إعادة ضبط العداد عند بدء الإنذار

بعض أوضاع المؤقت المميزة والمهمة هي:

  • تم تعطيل المؤقت. الجهاز لا يدق على الإطلاق.
  • تم تمكين المؤقت ، ولكن تم تعطيل المنبه. جهاز المؤقت يعمل بشكل مؤقت ، وهو يعمل على زيادة العداد الداخلي أو إنقاصه اختياريًا ، ولكن لا شيء آخر يحدث.
  • تم تمكين عداد الوقت وتم تمكين المنبه الخاص به أيضًا. كما كان من قبل ، ولكن هذه المرة يتم تنفيذ بعض الإجراءات عندما يصل عداد المؤقت إلى قيمة معينة تم تكوينها: يتم إعادة تعيين العداد و / أو إنشاء مقاطعة.

يمكن قراءة عدادات المؤقتات عن طريق كود تعسفي ، ولكن في معظم الحالات ، نحن مهتمون بعمل شيء ما بشكل دوري ، وهذا يعني أننا سنقوم بتكوين جهاز المؤقت لإنشاء مقاطعة ، وسنكتب رمزًا للتعامل معها.

يجب أن تنتهي وظيفة معالج المقاطعة قبل إنشاء المقاطعة التالية ، مما يعطينا حدًا أقصى صارمًا لمدى تعقيد الوظيفة. بشكل عام ، يجب أن يقوم معالج المقاطعة بأقل قدر ممكن من العمل.

لتحقيق أي شيء معقد عن بُعد ، يجب بدلاً من ذلك تعيين علامة يتم التحقق منها بواسطة رمز عدم المقاطعة. غالبًا ما يتم تفريغ أي نوع من الإدخال / الإخراج أكثر تعقيدًا من قراءة أو تعيين طرف واحد على قيمة واحدة بشكل أفضل إلى معالج منفصل.

في بيئة ESP-IDF ، يمكن استخدام وظيفة vTaskNotifyGiveFromISR() لإخطار مهمة أن معالج المقاطعة (يسمى أيضًا روتين خدمة المقاطعة ، أو ISR) لديه شيء للقيام به. يبدو الرمز كما يلي:

 portMUX_TYPE DRAM_ATTR timerMux = portMUX_INITIALIZER_UNLOCKED; TaskHandle_t complexHandlerTask; hw_timer_t * adcTimer = NULL; // our timer void complexHandler(void *param) { while (true) { // Sleep until the ISR gives us something to do, or for 1 second uint32_t tcount = ulTaskNotifyTake(pdFALSE, pdMS_TO_TICKS(1000)); if (check_for_work) { // Do something complex and CPU-intensive } } } void IRAM_ATTR onTimer() { // A mutex protects the handler from reentry (which shouldn't happen, but just in case) portENTER_CRITICAL_ISR(&timerMux); // Do something, eg read a pin. if (some_condition) { // Notify complexHandlerTask that the buffer is full. BaseType_t xHigherPriorityTaskWoken = pdFALSE; vTaskNotifyGiveFromISR(complexHandlerTask, &xHigherPriorityTaskWoken); if (xHigherPriorityTaskWoken) { portYIELD_FROM_ISR(); } } portEXIT_CRITICAL_ISR(&timerMux); } void setup() { xTaskCreate(complexHandler, "Handler Task", 8192, NULL, 1, &complexHandlerTask); adcTimer = timerBegin(3, 80, true); // 80 MHz / 80 = 1 MHz hardware clock for easy figuring timerAttachInterrupt(adcTimer, &onTimer, true); // Attaches the handler function to the timer timerAlarmWrite(adcTimer, 45, true); // Interrupts when counter == 45, ie 22.222 times a second timerAlarmEnable(adcTimer); }

ملاحظة: تم توثيق الوظائف المستخدمة في الكود في هذه المقالة باستخدام ESP-IDF API وفي مشروع ESP32 Arduino core GitHub.

ذاكرات وحدة المعالجة المركزية وهندسة هارفارد

من المهم جدًا ملاحظة عبارة IRAM_ATTR في تعريف معالج المقاطعة onTimer() . والسبب في ذلك هو أن مراكز وحدة المعالجة المركزية يمكنها فقط تنفيذ التعليمات (والوصول إلى البيانات) من ذاكرة الوصول العشوائي المضمنة ، وليس من وحدة تخزين الفلاش حيث يتم تخزين رمز البرنامج والبيانات بشكل طبيعي. للتغلب على ذلك ، تم تخصيص جزء من إجمالي 520 كيلوبايت من ذاكرة الوصول العشوائي كـ IRAM ، وهي ذاكرة تخزين مؤقت سعة 128 كيلوبايت تُستخدم لتحميل التعليمات البرمجية بشفافية من وحدة تخزين فلاش. يستخدم ESP32 حافلات منفصلة للكود والبيانات ("هندسة هارفارد") بحيث يتم التعامل معها بشكل منفصل ، وهذا يمتد إلى خصائص الذاكرة: IRAM خاص ، ولا يمكن الوصول إليه إلا عند حدود عنوان 32 بت.

في الواقع ، ذاكرة ESP32 غير موحدة للغاية. مناطق مختلفة منها مخصصة لأغراض مختلفة: يبلغ الحد الأقصى للمنطقة المستمرة حوالي 160 كيلوبايت في الحجم ، وكل الذاكرة "العادية" التي يمكن لبرامج المستخدم الوصول إليها يبلغ إجماليها حوالي 316 كيلوبايت فقط.

تحميل البيانات من وحدة تخزين الفلاش بطيء ويمكن أن يتطلب الوصول إلى ناقل SPI ، لذلك يجب أن يتناسب أي رمز يعتمد على السرعة مع ذاكرة التخزين المؤقت لـ IRAM ، وغالبًا ما يكون أصغر بكثير (أقل من 100 كيلوبايت) نظرًا لاستخدام جزء منه بواسطة نظام التشغيل. والجدير بالذكر أن النظام سيقوم بإنشاء استثناء إذا لم يتم تحميل رمز معالج المقاطعة في ذاكرة التخزين المؤقت عند حدوث مقاطعة. تحميل شيء ما من وحدة تخزين فلاش بمجرد حدوث المقاطعة سيكون بطيئًا للغاية وكابوسًا لوجستيًا. يقوم محدد IRAM_ATTR الموجود في معالج onTimer() بإخبار المحول البرمجي والرابط بتمييز هذا الرمز على أنه رمز خاص — سيتم وضعه بشكل ثابت في IRAM ولن يتم استبداله مطلقًا.

ومع ذلك ، فإن IRAM_ATTR ينطبق فقط على الوظيفة المحددة عليها - لا تتأثر أي وظائف يتم استدعاؤها من تلك الوظيفة.

أخذ عينات بيانات صوت ESP32 من مقاطعة المؤقت

تتضمن الطريقة المعتادة لأخذ عينات الإشارات الصوتية من المقاطعة الاحتفاظ بمخزن ذاكرة مؤقت للعينات ، وتعبئته ببيانات عينات ، ثم إخطار مهمة المعالج بأن البيانات متاحة.

يوثق ESP-IDF adc1_get_raw() التي تقيس البيانات على قناة ADC معينة على الطرف الأول ADC (تستخدم الثانية بواسطة WiFi). ومع ذلك ، فإن استخدامه في كود معالج المؤقت ينتج عنه برنامج غير مستقر ، لأنه وظيفة معقدة تستدعي عددًا غير بسيط من وظائف IDF الأخرى - لا سيما تلك التي تتعامل مع الأقفال - ولا adc1_get_raw() ولا الوظائف مكالماتها مميزة بـ IRAM_ATTR . سيتعطل معالج المقاطعة بمجرد تنفيذ جزء كبير بما يكفي من التعليمات البرمجية مما قد يتسبب في تبديل وظائف ADC خارج IRAM - وقد يكون هذا هو مكدس WiFi-TCP / IP-HTTP أو مكتبة نظام ملفات SPIFFS ، او اي شيء اخر.

ملاحظة: تم تصميم بعض وظائف IDF خصيصًا (وتم تمييزها بـ IRAM_ATTR ) بحيث يمكن استدعاؤها من معالجات المقاطعة. وظيفة vTaskNotifyGiveFromISR() من المثال أعلاه هي إحدى هذه الوظائف.

الطريقة الأكثر ملاءمة لجيش الدفاع الإسرائيلي للالتفاف حول هذا الأمر هي أن يقوم معالج المقاطعة بإخطار المهمة عندما يلزم أخذ عينة ADC ، وجعل هذه المهمة تقوم بأخذ العينات وإدارة المخزن المؤقت ، مع احتمال استخدام مهمة أخرى لتحليل البيانات (أو ضغط أو نقل أو أيا كانت الحالة). لسوء الحظ ، هذا غير فعال للغاية. يتضمن كل من جانب المعالج (الذي يخطر المهمة بوجود عمل يتعين القيام به) وجانب المهمة (الذي يلتقط مهمة للقيام بها) تفاعلات مع نظام التشغيل وآلاف التعليمات التي يتم تنفيذها. هذا النهج ، رغم أنه صحيح من الناحية النظرية ، يمكن أن يعطل وحدة المعالجة المركزية كثيرًا بحيث يترك القليل من طاقة وحدة المعالجة المركزية الاحتياطية للمهام الأخرى.

الحفر من خلال كود مصدر IDF

عادةً ما يكون أخذ عينات البيانات من ADC مهمة بسيطة ، لذا تتمثل الإستراتيجية التالية في معرفة كيفية قيام جيش الدفاع الإسرائيلي بذلك ، وتكرارها في الكود الخاص بنا مباشرةً ، دون الاتصال بواجهة برمجة التطبيقات المتوفرة. يتم تنفيذ الوظيفة adc1_get_raw() في ملف rtc_module.c الخاص بجيش الدفاع الإسرائيلي ، ومن بين الأشياء الثمانية أو نحو ذلك التي تقوم بها ، يقوم واحد فقط بأخذ عينات من ADC ، والذي يتم بواسطة استدعاء adc_convert() . لحسن الحظ ، adc_convert() هي وظيفة بسيطة تقوم بتجربة ADC من خلال معالجة سجلات الأجهزة الطرفية عبر بنية عالمية تسمى SENS .

يعد تكييف هذا الكود بحيث يعمل في برنامجنا (وتقليد سلوك adc1_get_raw() ) أمرًا سهلاً. تبدو هكذا:

 int IRAM_ATTR local_adc1_read(int channel) { uint16_t adc_value; SENS.sar_meas_start1.sar1_en_pad = (1 << channel); // only one channel is selected while (SENS.sar_slave_addr1.meas_status != 0); SENS.sar_meas_start1.meas1_start_sar = 0; SENS.sar_meas_start1.meas1_start_sar = 1; while (SENS.sar_meas_start1.meas1_done_sar == 0); adc_value = SENS.sar_meas_start1.meas1_data_sar; return adc_value; }

الخطوة التالية هي تضمين الرؤوس ذات الصلة حتى يصبح متغير SENS متاحًا:

 #include <soc/sens_reg.h> #include <soc/sens_struct.h>

أخيرًا ، نظرًا لأن adc1_get_raw() ينفذ بعض خطوات التكوين قبل أخذ عينات ADC ، فيجب استدعاؤه مباشرةً بعد إعداد ADC. بهذه الطريقة يمكن إجراء التكوين ذي الصلة قبل بدء تشغيل المؤقت.

الجانب السلبي لهذا النهج هو أنه لا يعمل بشكل جيد مع وظائف جيش الدفاع الإسرائيلي الأخرى. بمجرد استدعاء بعض الأجهزة الطرفية أو برنامج التشغيل أو جزء عشوائي من التعليمات البرمجية التي تعيد تعيين تكوين ADC ، لن تعمل وظيفتنا المخصصة بشكل صحيح. لا تؤثر WiFi و PWM و I2C و SPI على الأقل على تكوين ADC. في حالة تأثير شيء ما عليه ، سيؤدي استدعاء adc1_get_raw() إلى تكوين ADC بشكل مناسب مرة أخرى.

أخذ عينات الصوت ESP32: الكود النهائي

مع وجود وظيفة local_adc_read() في مكانها الصحيح ، يبدو رمز معالج المؤقت كما يلي:

 #define ADC_SAMPLES_COUNT 1000 int16_t abuf[ADC_SAMPLES_COUNT]; int16_t abufPos = 0; void IRAM_ATTR onTimer() { portENTER_CRITICAL_ISR(&timerMux); abuf[abufPos++] = local_adc1_read(ADC1_CHANNEL_0); if (abufPos >= ADC_SAMPLES_COUNT) { abufPos = 0; // Notify adcTask that the buffer is full. BaseType_t xHigherPriorityTaskWoken = pdFALSE; vTaskNotifyGiveFromISR(adcTaskHandle, &xHigherPriorityTaskWoken); if (xHigherPriorityTaskWoken) { portYIELD_FROM_ISR(); } } portEXIT_CRITICAL_ISR(&timerMux); }

هنا ، adcTaskHandle هي مهمة FreeRTOS التي سيتم تنفيذها لمعالجة المخزن المؤقت ، باتباع بنية وظيفة complexHandler في مقتطف التعليمات البرمجية الأول. ستعمل نسخة محلية من المخزن المؤقت للصوت ، ويمكنها بعد ذلك معالجتها في وقت فراغها. على سبيل المثال ، قد تقوم بتشغيل خوارزمية FFT على المخزن المؤقت ، أو يمكنها ضغطها ونقلها عبر شبكة WiFi.

ومن المفارقات أن استخدام Arduino API بدلاً من ESP-IDF API (أي analogRead() بدلاً من adc1_get_raw() ) سيعمل لأن وظائف Arduino مميزة بـ IRAM_ATTR . ومع ذلك ، فهي أبطأ بكثير من مثيلاتها في ESP-IDF لأنها توفر مستوى أعلى من التجريد. عند الحديث عن الأداء ، فإن وظيفة قراءة ADC المخصصة لدينا تبلغ ضعف سرعة وظيفة ESP-IDF.

مشاريع ESP32: لنظام التشغيل أو لا لنظام التشغيل

ما فعلناه هنا - إعادة تنفيذ واجهة برمجة تطبيقات لنظام التشغيل للتغلب على بعض المشكلات التي لن تكون موجودة حتى إذا لم نستخدم نظام تشغيل - هو توضيح جيد لإيجابيات وسلبيات استخدام نظام التشغيل في المركز الأول.

تتم برمجة وحدات التحكم الدقيقة الأصغر مباشرة ، أحيانًا في كود المجمع ، ويتمتع المطورون بالتحكم الكامل في كل جانب من جوانب تنفيذ البرنامج ، وفي كل تعليمات وحدة المعالجة المركزية ، وجميع حالات جميع الأجهزة الطرفية على الشريحة. يمكن أن يصبح هذا مملاً بشكل طبيعي حيث يصبح البرنامج أكبر ويستخدم المزيد والمزيد من الأجهزة. المتحكم الدقيق المعقد مثل ESP32 ، مع مجموعة كبيرة من الأجهزة الطرفية ، وقلبي وحدة المعالجة المركزية ، وتخطيط ذاكرة معقد وغير منتظم ، سيكون صعبًا وشاقًا في البرمجة من البداية.

بينما يضع كل نظام تشغيل بعض القيود والمتطلبات على الكود الذي يستخدم خدماته ، فإن الفوائد تستحق ذلك عادةً: تطوير أسرع وأبسط. ومع ذلك ، في بعض الأحيان يمكننا ، وفي المساحة المضمنة في كثير من الأحيان ، الالتفاف حوله.

الموضوعات ذات الصلة: كيف صنعت محطة طقس اردوينو كاملة الوظائف