ESP32 Ses Örnekleme ile Çalışmak

Yayınlanan: 2022-03-11

ESP32, yeni nesil, WiFi ve Bluetooth özellikli bir mikro denetleyicidir. Bu, Şanghay merkezli Espressif'in çok popüler ve meraklı izleyiciler için devrim niteliğindeki ESP8266 mikro denetleyicisinin halefidir.

Mikrodenetleyiciler arasında bir dev olan ESP32'nin özellikleri, mutfak lavabosu dışında her şeyi içerir. Bir çip üzerinde sistem (SoC) ürünüdür ve tüm özelliklerinden yararlanmak için pratik olarak bir işletim sistemi gerektirir.

Bu ESP32 öğreticisi, bir zamanlayıcı kesintisinden analogdan dijitale dönüştürücünün (ADC) örneklenmesine ilişkin belirli bir sorunu açıklayacak ve çözecektir. Arduino IDE'yi kullanacağız. Özellik kümeleri açısından en kötü IDE'lerden biri olsa bile, Arduino IDE'nin ESP32 geliştirmesi için kurulumu ve kullanımı en azından kolaydır ve çeşitli ortak donanım modülleri için en geniş kitaplık koleksiyonuna sahiptir. Bununla birlikte, performans nedenleriyle Arduino API'leri yerine birçok yerel ESP-IDF API'sini de kullanacağız.

ESP32 Ses: Zamanlayıcılar ve Kesintiler

ESP32, iki gruba ayrılmış dört donanım zamanlayıcı içerir. Tüm zamanlayıcılar aynıdır, 16 bit ön ölçekleyicilere ve 64 bit sayaçlara sahiptir. Ön ölçek değeri, zamanlayıcıya giren dahili bir 80 MHz saatinden gelen donanım saat sinyalini her N. tik ile sınırlamak için kullanılır. Minimum ön ölçek değeri 2'dir; bu, kesintilerin resmi olarak en fazla 40 MHz'de tetiklenebileceği anlamına gelir. Bu kötü değildir, çünkü en yüksek zamanlayıcı çözünürlüğünde, işleyici kodunun en fazla 6 saat döngüsünde (240 MHz çekirdek/40 MHz) yürütülmesi gerekir. Zamanlayıcıların birkaç ilişkili özelliği vardır:

  • divider — frekans ön ölçek değeri
  • counter_en — zamanlayıcının ilişkili 64 bitlik sayacının etkin olup olmadığı (genellikle doğrudur)
  • counter_dir artırılıp artırılmadığını veya azaltıldığını
  • alarm_en “alarm”ın, yani sayacın eyleminin etkin olup olmadığı
  • auto_reload alarm tetiklendiğinde sayacın sıfırlanıp sıfırlanmadığı

Önemli farklı zamanlayıcı modlarından bazıları şunlardır:

  • Zamanlayıcı devre dışı. Donanım hiç çalışmıyor.
  • Zamanlayıcı etkin, ancak alarm devre dışı. Zamanlayıcı donanımı tıklıyor, isteğe bağlı olarak dahili sayacı artırıyor veya azaltıyor, ancak başka hiçbir şey olmuyor.
  • Zamanlayıcı etkinleştirilir ve alarmı da etkinleştirilir. Daha önce olduğu gibi, ancak bu sefer zamanlayıcı sayacı belirli, yapılandırılmış bir değere ulaştığında bazı eylemler gerçekleştirilir: Sayaç sıfırlanır ve/veya bir kesme oluşturulur.

Zamanlayıcıların sayaçları keyfi kodla okunabilir, ancak çoğu durumda, periyodik olarak bir şeyler yapmakla ilgileniriz ve bu, zamanlayıcı donanımını bir kesme oluşturacak şekilde yapılandıracağımız ve bunu işlemek için kod yazacağımız anlamına gelir.

Bir kesme işleyici işlevi, bir sonraki kesme oluşturulmadan önce bitmelidir, bu da bize işlevin ne kadar karmaşık olabileceği konusunda kesin bir üst sınır verir. Genel olarak, bir kesme işleyicisi yapabileceği en az miktarda işi yapmalıdır.

Uzaktan karmaşık bir şey elde etmek için bunun yerine kesintisiz kod tarafından kontrol edilen bir bayrak ayarlaması gerekir. Tek bir pini okumaktan veya tek bir değere ayarlamaktan daha karmaşık olan her türlü G/Ç'nin genellikle ayrı bir işleyiciye yüklenmesi daha iyidir.

ESP-IDF ortamında, FreeRTOS işlevi vTaskNotifyGiveFromISR() , kesme işleyicisinin (Kesme Hizmeti Rutini veya ISR olarak da adlandırılır) yapması gereken bir görevi olduğunu bildirmek için kullanılabilir. Kod şöyle görünür:

 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); }

Not: Bu makale boyunca kodda kullanılan işlevler, ESP-IDF API'si ve ESP32 Arduino çekirdek GitHub projesinde belgelenmiştir.

CPU Önbellekleri ve Harvard Mimarisi

Dikkat edilmesi gereken çok önemli bir şey, onTimer() kesme işleyicisinin tanımındaki IRAM_ATTR yan tümcesidir. Bunun nedeni, CPU çekirdeklerinin program kodunun ve verilerin normal olarak depolandığı flash depolamadan değil, yalnızca gömülü RAM'den talimatları yürütebilmesi (ve verilere erişebilmesidir). Bunu aşmak için, toplam 520 KiB RAM'in bir kısmı, flash depolamadan şeffaf bir şekilde kod yüklemek için kullanılan 128 KiB'lik bir önbellek olan IRAM olarak ayrılmıştır. ESP32, kod ve veri ("Harvard mimarisi") için ayrı veriyolları kullanır, bu nedenle bunlar ayrı ayrı işlenir ve bu, bellek özelliklerine kadar uzanır: IRAM özeldir ve yalnızca 32 bit adres sınırlarından erişilebilir.

Aslında, ESP32 belleği çok düzensizdir. Bunun farklı bölgeleri farklı amaçlar için ayrılmıştır: Maksimum sürekli bölge boyutu yaklaşık 160 KiB'dir ve kullanıcı programları tarafından erişilebilen tüm “normal” belleğin toplamı yalnızca 316 KiB civarındadır.

Flaş depolamadan veri yüklemek yavaştır ve SPI veri yolu erişimi gerektirebilir, bu nedenle hıza dayanan herhangi bir kodun IRAM önbelleğine uymaya özen göstermesi ve bir kısmı tarafından kullanıldığı için genellikle çok daha küçük (100 KiB'den az) olması gerekir. işletim sistemi. Özellikle, bir kesme meydana geldiğinde kesme işleyici kodu önbelleğe yüklenmezse sistem bir istisna oluşturacaktır. Bir kesinti olduğu gibi flash depolamadan bir şey yüklemek hem çok yavaş hem de lojistik bir kabus olurdu. onTimer() işleyicisindeki IRAM_ATTR belirteci, derleyiciye ve bağlayıcıya bu kodu özel olarak işaretlemesini söyler; bu kod statik olarak IRAM'a yerleştirilir ve asla değiştirilmez.

Ancak, IRAM_ATTR yalnızca üzerinde belirtildiği işlev için geçerlidir; bu işlevden çağrılan işlevler etkilenmez.

Bir Zamanlayıcı Kesintisinden ESP32 Ses Verilerini Örnekleme

Bir kesintiden ses sinyallerinin örneklenmesinin olağan yolu, örneklerin bir bellek arabelleğini korumayı, bunu örneklenmiş verilerle doldurmayı ve ardından bir işleyici görevine verilerin mevcut olduğunu bildirmeyi içerir.

ESP-IDF, birinci ADC çevre birimindeki (ikincisi WiFi tarafından kullanılır) belirli bir ADC kanalındaki verileri ölçen adc1_get_raw() işlevini belgeler. Bununla birlikte, bunun zamanlayıcı işleyici kodunda kullanılması kararsız bir programla sonuçlanır, çünkü önemsiz sayıda diğer IDF işlevlerini - özellikle de kilitlerle ilgilenenleri - çağıran ve ne adc1_get_raw() ne de işlevleri çağıran karmaşık bir işlevdir. çağrıları IRAM_ATTR ile işaretlenir. ADC işlevlerinin IRAM'den değiştirilmesine neden olacak yeterince büyük bir kod parçası yürütülür yürütülmez, kesme işleyicisi çökecektir ve bu, WiFi-TCP/IP-HTTP yığını veya SPIFFS dosya sistemi kitaplığı olabilir. ya da başka bir şey.

Not: Bazı IDF işlevleri, kesme işleyicilerinden çağrılabilmeleri için özel olarak hazırlanmıştır (ve IRAM_ATTR ile işaretlenmiştir). Yukarıdaki vTaskNotifyGiveFromISR() işlevi böyle bir işlevdir.

Bunu aşmanın en IDF dostu yolu, kesme işleyicisinin bir ADC örneğinin alınması gerektiğinde bir görevi bildirmesi ve bu görevin, muhtemelen veri analizi için başka bir görev (veya sıkıştırma veya iletim veya durum ne olursa olsun). Ne yazık ki, bu son derece verimsizdir. Hem işleyici tarafı (bir göreve yapılması gereken bir iş olduğunu bildirir) hem de görev tarafı (yapılacak bir görevi seçer) işletim sistemi ile etkileşimleri ve yürütülmekte olan binlerce talimatı içerir. Bu yaklaşım, teorik olarak doğru olsa da, CPU'yu o kadar fazla tıkayabilir ki, diğer görevler için çok az yedek CPU gücü bırakır.

IDF Kaynak Kodu ile Kazma

Bir ADC'den veri örneklemek genellikle basit bir iştir, bu nedenle sonraki strateji, IDF'nin bunu nasıl yaptığını görmek ve sağlanan API'yi çağırmadan doğrudan kodumuzda çoğaltmaktır. adc1_get_raw() işlevi, IDF'nin rtc_module.c dosyasında uygulanır ve yaptığı sekiz ya da daha fazla şeyden yalnızca biri, adc_convert() çağrısıyla yapılan ADC'yi örnekliyor. Neyse ki, adc_convert() , SENS adlı global bir yapı aracılığıyla çevresel donanım kayıtlarını manipüle ederek ADC'yi örnekleyen basit bir işlevdir.

Bu kodu programımızda çalışacak şekilde uyarlamak (ve adc1_get_raw() davranışını taklit etmek) kolaydır. Şuna benziyor:

 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; }

Sonraki adım, SENS değişkeninin kullanılabilir hale gelmesi için ilgili başlıkları eklemektir:

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

Son olarak, adc1_get_raw() , ADC'yi örneklemeden önce bazı yapılandırma adımlarını gerçekleştirdiğinden, ADC kurulduktan hemen sonra doğrudan çağrılmalıdır. Bu şekilde zamanlayıcı başlatılmadan önce ilgili konfigürasyon yapılabilir.

Bu yaklaşımın dezavantajı, diğer IDF işlevleriyle iyi oynamamasıdır. ADC yapılandırmasını sıfırlayan başka bir çevre birimi, sürücü veya rastgele bir kod parçası çağrıldığında, özel işlevimiz artık düzgün çalışmayacaktır. En azından WiFi, PWM, I2C ve SPI, ADC yapılandırmasını etkilemez. Bir şeyin onu etkilemesi durumunda, adc1_get_raw() çağrısı ADC'yi yeniden uygun şekilde yapılandıracaktır.

ESP32 Ses Örnekleme: Son Kod

local_adc_read() işlevi yerindeyken, zamanlayıcı işleyici kodumuz şöyle görünür:

 #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); }

Burada adcTaskHandle , ilk kod parçacığındaki complexHandler işlevinin yapısını izleyerek arabelleği işlemek için uygulanacak FreeRTOS görevidir. Ses arabelleğinin yerel bir kopyasını oluşturacak ve daha sonra boş zamanlarında işleyebilir. Örneğin, arabellek üzerinde bir FFT algoritması çalıştırabilir veya onu sıkıştırıp WiFi üzerinden iletebilir.

Paradoksal olarak, Arduino işlevleri IRAM_ATTR ile işaretlendiğinden, ESP-IDF API'si yerine Arduino API'sini kullanmak (yani analogRead() yerine adc1_get_raw() ) işe yarayacaktır. Ancak, daha yüksek düzeyde bir soyutlama sağladıkları için ESP-IDF olanlardan çok daha yavaştırlar. Performanstan bahsetmişken, özel ADC okuma işlevimiz ESP-IDF'den yaklaşık iki kat daha hızlıdır.

ESP32 Projeleri: İşletim Sistemine veya İşletim Sistemine Değil

Burada yaptığımız şey (bir işletim sistemi kullanmasaydık orada olmayacak olan bazı sorunların üstesinden gelmek için işletim sisteminin bir API'sini yeniden uygulamak) ilk yer.

Daha küçük mikro denetleyiciler doğrudan, bazen montajcı kodunda programlanır ve geliştiriciler, programın yürütülmesinin her yönü, her bir CPU talimatı ve çip üzerindeki tüm çevre birimlerinin tüm durumları üzerinde tam kontrole sahiptir. Program büyüdükçe ve daha fazla donanım kullandıkça bu doğal olarak sıkıcı hale gelebilir. ESP32 gibi geniş bir çevre birimi seti, iki CPU çekirdeği ve karmaşık, tek tip olmayan bir bellek düzenine sahip karmaşık bir mikro denetleyiciyi sıfırdan programlamak zor ve zahmetli olacaktır.

Her işletim sistemi, hizmetlerini kullanan koda bazı sınırlamalar ve gereksinimler getirirken, faydaları genellikle buna değer: daha hızlı ve daha basit geliştirme. Bununla birlikte, bazen yapabiliriz ve gömülü uzayda genellikle etrafından dolaşmalıyız.

İlgili: Tam İşlevli Bir Arduino Hava İstasyonunu Nasıl Yaptım