ESP32 - dynamická zmena vzorkovacej frekvencie I2S
Nedávno som si kúpil mikrokontrolér ESP32 kvôli prestavbe starého nefunkčného tranzistorového rádia na internetové rádio. U môjho malého projektu chcem dosiahnuť čo najplynulejšie prehrávanie internetových streamov bez výpadkov, alebo počuteľného praskania.
MAX98357A
Ako obyčajne predstavím najskôr nový hardvér, o ktorom som ešte nepísal. Vývojovú dosku s ESP32 som predstavil už minule, takže tú vynechám, ale ukážem čosi zaujímavejšie. Ide o digitálny koncový zosilňovač triedy D - MAX98357A.
Trieda D znamená, že zosilňovač pracuje v spínanom režime. Vďaka tomu dosahuje celkom slušný výkon (3W) pri účinnosti 92%. Pre porovnanie maximálna teoretická účinnosť zosilňovača triedy A je 25%.
Tento celkom zaujímavý kúsok sa dá napájať napätím 2.5V - 5.5V. Na vstupe prijíma digitálne dáta v rôznych formátoch s rôznymi frekvenciami (konfigurácia je automatická). Zosilňovač dokáže pracovať ako výstup ľavého kanála, pravého kanála, alebo dokáže mixovať oba kanály dokopy v závislosti na napätí SD_MODE stupu. Dosky z aliexpresu zvyknú mať prednastavené mixovanie oboch kanálov. Upraviť sa to dá vhodnou voľbou pull up rezistoru na SD_MODE. Kombináciou 2 identických zosilňovačov je vďaka tomu možné dosiahnuť stereo výstup aj keď oba dostávajú signál pomocou jediného dátového vodiča. Vstup SD_MODE má nasledujúce režimy:
Napätie | Kanál |
---|---|
pod 0.16 V | Vypnutý |
nad 0.16 V | (Ľavý + Pravý) / 2 |
nad 0.77 V | Pravý |
nad 1.4 V | Ľavý |
Na zosilňovači chýba vstup pre nastavenie hlasitosti (gain, umožňuje len skokové zmeny). Našťastie zosilňovač zvláda 32-bitový vstup, takže ovládanie hlasitosti sa dá v pohode vyriešiť softvérovo.
Zapojenie zosilňovača
Pri testovaní som používal nasledovné zapojenie:
Zosilňovač | Kanál |
---|---|
LRC | GPIO 25 |
BCLK | GPIO 26 |
DIN | GPIO 22 |
GAIN | - |
SD | - |
GND | GND |
VIN | 5 V |
Offtopic: čím spájkujete tieto 2.54mm piny? Všade v tutoriáloch vidím mikropájku, ale ja osobne používam obyčajnú starú trafopájku.
Prehrávanie zvuku
Pre bezproblémové prehrávanie internetovéh streamu je potrebné udrživať približne rovnakú rýchlosť prehrávania, ako je rýchlosť vysielania. Hodiny esp-32 môžu ísť o chlp rýchlejšie, zariadenie má málo RAM, takže buffer to nezachráni a pri podtečení buffera bude jednoducho sekať. V takom prípade to môžme vyriešiť drobným znížením vzorkovacej frekvencie, aby kompenzovala rôznu rýchlosť hodín.
Nasleduje malý testovací kód, ktorý prehráva sínusovku. Kód budem písať v štandardnom esp-idf. Na začiatok pár deklarácií / definícií:
#include <math.h> #include "driver/gpio.h" #include "driver/i2s.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "sdkconfig.h" #define AUDIO_OUTPUT_BUFFER_COUNT 4 #define AUDIO_OUTPUT_BUFFER_SIZE 375 #define AUDIO_BITS_PER_SAMPLE I2S_BITS_PER_SAMPLE_32BIT #define AUDIO_I2S_PORT I2S_NUM_0 typedef int32_t audio_sample_t; typedef struct audio_output_t { uint32_t sample_rate; i2s_port_t port; int bps; } audio_output_t; static const char *TAG = "audio"; static i2s_dev_t* const I2S[I2S_NUM_MAX] = {&I2S0, &I2S1}; static audio_sample_t audio_buffer[AUDIO_OUTPUT_BUFFER_SIZE << 1]; static audio_output_t audio_output = { .port = AUDIO_I2S_PORT, .bps = AUDIO_BITS_PER_SAMPLE, .sample_rate = 44100, };
V podstate len includujem pár knižníc a definujem štruktúru audio_output
, ktorou sa budem odkazovať na konkrétny výstup. Zatiaľ nič zaujímavé.
Pokračujem malým kódom na vygenerovanie sínusovky.
static void generate_sine() { audio_sample_t max_val = 0x7fffffff; for (size_t i = 0; i < (AUDIO_OUTPUT_BUFFER_SIZE << 1); ++i) { double rad = M_PI * i / AUDIO_OUTPUT_BUFFER_SIZE * 3; audio_buffer[i] = max_val * sin(rad); } }
Inicializujem I2S rozhranie.
void audio_output_init(audio_output_t *output) { i2s_config_t i2s_config = { .mode = I2S_MODE_MASTER | I2S_MODE_TX, .sample_rate = output->sample_rate, .bits_per_sample = output->bps, .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format = I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2, .dma_buf_count = AUDIO_OUTPUT_BUFFER_COUNT, .dma_buf_len = AUDIO_OUTPUT_BUFFER_SIZE, .tx_desc_auto_clear = true, .use_apll = false, }; i2s_pin_config_t pin_config = { .bck_io_num = GPIO_NUM_26, .ws_io_num = GPIO_NUM_25, .data_out_num = GPIO_NUM_22, .data_in_num = I2S_PIN_NO_CHANGE }; i2s_driver_install(output->port, &i2s_config, 0, NULL); i2s_zero_dma_buffer(output->port); i2s_set_pin(output->port, &pin_config); } void audio_output_destroy(audio_output_t *output) { i2s_driver_uninstall(output->port); }
Pridáme funkciu pre zmenu vzorkovacej frekvencie:
void audio_output_set_sample_rate(audio_output_t *output, int rate) { i2s_set_sample_rates(output->port, rate); }
Zostáva už len napísať nekonečnú slučku, ktorá bude prehrávať buffer a funkciu main.
void audio_output_write(audio_output_t *output, audio_sample_t *buf, size_t buf_size) { size_t bytes_written; i2s_write(output->port, buf, buf_size, &bytes_written, portMAX_DELAY); } void send_audio(void *arg) { while (1) { audio_output_write((audio_output_t *)arg, audio_buffer, sizeof(audio_buffer)); } } void app_main(void) { // Generate audio generate_sine(); // Initialize audio output audio_output_init(&audio_output); // Create audio task TaskHandle_t send_audio_task; xTaskCreate(&send_audio, "send_audio", 2048, &audio_output, 5, &send_audio_task); // Change sample rates for (size_t i = 0; i < 50; ++i) { vTaskDelay(30 / portTICK_PERIOD_MS); audio_output_set_sample_rate(&audio_output, 44100 - 200 * i); } // Stop audio output vTaskDelete(send_audio_task); // Clean audio_output_destroy(&audio_output); vTaskDelay(portMAX_DELAY); vTaskDelete(NULL); }
Všetko funguje správne, akurát zvuk počas zmeny vzorkovacej frekvencie praská. Výsledný zvuk si môžte vypočuť na youtube. Môže za to bezpečné odstavenie i2s pri volaní i2s_set_sample_rates
.
Hardvér zvláda zmenu vzorkovacej frekvencie bez odstavenia I2S. Môžme tak urobiť zápisom parametrov priamo do registrov.
Podľa dokumentácie by mali byť hodiny vypočítané podľa nasledujúcej schémy:
Ak chceme dosiahnuť frekvenciu 44100 vzoriek za sekundu a máme frekvenciu hodín 160MHz tj. 40MHz na vstupe I2S musíme dosiahnuť aby v menovateli bola taká hodnota, aby bolo odoslaných 2822400 bytov za sekundu (44100 * 32 [bitová hĺbka] * 2 [stereo]). Frekvenciu 40 000 000Hz musíme deliť číslom 14,172336. Potrebujeme teda vyriešiť:
14,172336 = N + (b / a)
Podiel b/a nesmie presiahnuť 1, takže za N môžme rovno dosiahnuť 14. Zostáva zistiť koeficienty a a b. Tie musia byť v rozsahu 0-63. ESP-IDF dosádza do vzorca vždy a=63, ale ja som sa rozhodol zistiť hodnotu s najmenšou chybou pomocou hrubej sily.
void audio_output_set_sample_rate(audio_output_t *output, int rate) { int sample_size = AUDIO_BITS_PER_SAMPLE * 2; // Stereo - 2-násobná dĺžka bitovej hĺbky int i2s_frequency = APB_CLK_FREQ / 2; // Polovičná frekvencia APB hodín // Frekvencia je Fpll / (1 / (N + (b / a))) // requested_divisor = (N + (b / a)) - máme výsledok a hľadáme parametre N, b, a // N podľa dokumentácie musí byť väčšie než 1 a menšie než 255 // a, b musia byť v rozsahu 0-63 // a != 0 // a musí byť väčšie než b (toto nie je nikde zdokumentované) double requested_divisor = (double)i2s_frequency / (rate * sample_size); if (requested_divisor >= 255 || requested_divisor < 2) { ESP_LOGE(TAG, "Unsupported sample rate: %d", rate); return; } // Inicializujem najlepšie doteraz nájdené riešenie double min_error = 1.0; int best_n = (int)requested_divisor; int best_b = 0; int best_a = 1; // Vyhľadanie najlepšieho riešenia hrubou silou for (int a = 1; a < 64; ++a) { int b = (requested_divisor - (double)best_n) * (double)a; if (b > 63) { continue; } double divisor = (double)best_n + (double)b / (double)a; double error = divisor > requested_divisor ? divisor - requested_divisor : requested_divisor - divisor; if (error < min_error) { min_error = error; best_a = a; best_b = b; } b++; if (b > 63) { continue; } divisor = (double)best_n + (double)b / (double)a; error = divisor > requested_divisor ? divisor - requested_divisor : requested_divisor - divisor; if (error < min_error) { min_error = error; best_a = a; best_b = b; } } // Zobrazenie najlepšieho riešenia double final_rate = (double)i2s_frequency / (double)sample_size / ((double)best_n + (double)best_b / (double)best_a); ESP_LOGI(TAG, "Requested samplerate change to: %d, final samplerate %f, N: %d, b: %d, a: %d", rate, final_rate, best_n, best_b, best_a); // Zmena hodín I2S[output->port]->clkm_conf.clkm_div_a = 63; I2S[output->port]->clkm_conf.clkm_div_b = best_b; I2S[output->port]->clkm_conf.clkm_div_a = best_a; I2S[output->port]->clkm_conf.clkm_div_num = best_n; }
Výsledkom je možnosť nastaviť vzorkovaciu frekvenciu bez prerušenia prehrávania.
Pre pridávanie komentárov sa musíte prihlásiť.
Výborne urobené, akoby som po rokoch čítal skriptá :)
Jako pointu ako to funguje chápem, ale na čo to vo finále bude?
Rádio, ktoré sa pripojí na internet a bude prehrávať internetový stream. Nič viac za tým netreba hľadať.
Aby bolo jasné táto časť funguje a funguje už stabilne, ale postupne zverejňujem blogy o starších problémoch, s ktorými som sa trápil. Predomnou zostáva ešte asi najväčší problém - vyriešiť interakciu s užívateľom. Teraz ma čaká pripojenie LCD k zvukovému výstupu (!!!), čo nie je ktovie ako zdokumentované. V podstate v dokumentácii vidím, že digitálne zvukové rozhranie (I2S) tu dokáže pracovať ako vstup aj výstup, okrem toho dokáže pracovať aj v analógovom režime ... no ok beriem, že to ešte súvisí so zvukom. Zároveň I2S v esp32 má režim LCD (wtf?), ktorý umožňuje generovať tuším 24 paralelných signálov + 2 hodinové signály. Okrem toho môže v tomto režime fungovať aj ako vstup (kamera). No proste I2S je jedna veľká hŕba zle zdokumentovaných registrov a ja neviem vôbec ako pomocou toho kŕmiť svoj paralelný display s rozhraním 8080. Dúfam, že na to prídem bez použitia logického analyzátora, ktorý by sa mi teraz asi veľmi hodil.
Je to veľmi zaujímavý i ambiciózny projekt.
Nie je jednoduchšie (nevravím, že lepšie, najmä ak chce človek skúšať a učiť sa) pripojiť LCD k ESP32, napr. cez I2C, čo je rokmi overená vec?
Mám starý LCD s paralelným rozhraním 8080 (16 pin + hodiny + riadiace signály + ďalších pár pinov pre SD a TS, dokopy 40-pinový interface). Nemám čo s nim urobiť takže ho chcem využiť do tohto projektu. Viem, že na ESP nemám dosť pinov, takže plán je použiť 74373 na rozšírenie dátových vodičov z 8 na 16, posielať 1 hodinovom cykle 8 do záchytného registra, v druhom cykle 8 bitov mimo záchytného registra a zároveň ten istý pin, ktorý bude prepínať medzi záchytným registrom (dolných 8 bitov) a priamym prístupom (horných 8 bitov) bude slúžiť aj ako riadiaci signál LCD (write + hodiny).
Okrem toho mám ešte jeden malý sériový LCD, ale ten nie je moc vhodný pretože je moc malý. I2C nie, to je príliš pomalé. Chcem v najhoršom možnom prípade dosiahnuť 30fps, ideálne však 60, takže budem sa snažiť tlačiť to cez DMA a I2S.
Ambiciózne by to ani moc nebolo keby bolo ESP32 por ale už nie jiadne zdokumentované. Dokumentácia je docela naprd a ja neviem, či to zvládnem bez logického analyzátora. Ako tak čítam I2S robí v paralelnom režime všeličo, má to milion konfiguračných registrov, ku ktorým je jediná dokumentácia - že existujú, ale už nie je popísané čo registre robia. Ja budem musieť uhádnuť správnu kombináciu registrov ... ah asi som fakt mal kúpiť logický analyzátor.
To som nepochopil. Načo je dobré pripojiť LCD k zvukovému výstupu? Je to myslené tak, že tam bude zobrazovať trebárs aktuálnu úroveň zvuku alebo čo? Priame pripojenie mi akosi nedáva zmysel.
Ja to chcem pripojiť k I2S1. I2S0 bude slúžiť ako digitálny stereo audio výstup a I2S1 ako display výstup. Tu je problém ako je navrhnutý alebo zdokumentovaný ESP32.
I2S je totiž digitálne sériové audio rozhranie. Oni tak nazývajú svoj hardvér ktorý vie:
V dokumentácii to volajú audio rozhranie, ale kľudne by sa to mohlo volať všestranne použiteľný multimediálny kontrolér, alebo ešte lepšie nedefinovaný blob.
Takéhoto bordelu v dokumentácii majú viacej, napr. PWM volajú LED kontrolér, ako keby sa PWM-kom nedalo ovládať nič iné.
https://www.instructables.com/id/Internet-Radio-Using-an-ESP32/
https://github.com/Edzelf/ESP32-Radio
ja som robil netradio na arduino nano r3. nakoniec som to vzdal a pouzivam pasivne chladeny 10-15W pos terminal
Ten projekt poznám. Je to arduino. Pritom ESP32 má vynikajúce sdk, nevidím dôvod babrať sa s arduinom.
V zásade kód je celý vtesnaný do jedného veľkého megasúboru, čo komplikuje úpravy. Všetko je to tak hrozne ad-hoc riešené, až to vyzerá amatérsky. Omnoho profesionálnejšie na mňa pôsobí toto rádio. Ok výsledok nevyzerá tak pôsobivo, ale kód je elegantný a členený do logických celkov.
A prečo som vlastne neforkol tento projekt a nepokračujem? Pretože som tak trochu masochista. Pretože ma na ESP32 serie milion drobností a chcem aspoň na niektoré napísať riešenie.
Jednou z tých drobností je absencia rozumného emulátora / simulátora. Jednoducho som človek, ktorému lezie na nervy čakanie pár sekúnd až minút až naflashuje zariadenie a zistí, že havaruje hneď ako sa pripojí na internet. Preto som spolu s projektom začal písať simulátor. Prosím nemýliť emulátor so simulátorom. To, čo robím je, že popri binárke pre ESP32 sa kompiluje normálna linuxová binárka. Namiesto volaní esp-idf sa volajú rovnako vyzerajúce funkcie, ale namiesto hw volajú nejaké ďalšie linuxové api napr. zvukový výstup sa prekladá na alsu. Freertos volania sa prekladajú na posix volania atď.
mno inspiracia je, dik. idem nieco posnorit :-) esp32 mam aj nejake dalsie komponenty tak pokukam.
esp32 nepoznam, pouzival som len arduino a rpi
ale este do konca dovolenky mam aspon co robit.
i ked by som mal najprv dokoncit ten arduino monitor na PC
A keby som nebol blbec tak kúpim toto, dám tam linux, pripojím svoj škaredý 40-pinový display (ktorý je mimochodom kompatibilný, lebo táto doštička má podporu 8080 interfacu) a nemusím sa obmedzovať kvôli RAM-ke. Možno by som mohol dorobiť aj akcelerované prehrávanie videa z youtubu.