ESP32 - dynamická zmena vzorkovacej frekvencie I2S

05.01.2020 | 19:15 | Mirecove dristy | Miroslav Bendík

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.

Detail dosky s MAX98357A
Obrázok 1: Detail dosky s 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%.

Integrovaný obvod MAX98357A
Obrázok 2: Integrovaný obvod MAX98357A

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ý
Obsah balenia z aliexpresu
Obrázok 3: Obsah balenia z aliexpresu

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.

Zadná strana
Obrázok 4: Zadná stranaa

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
Moje tragické zapojenie
Obrázok 5: Moje tragické zapojenie

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:

Výpočet hodín I2S
Obrázok 6: Výpočet hodín I2s

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.

    • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 05.01.2020 | 23:56
      Avatar vxmery Mint 21.3 Cinnamon  Používateľ

      Výborne urobené, akoby som po rokoch čítal skriptá :)

    • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 06.01.2020 | 22:57
      Avatar bedňa LegacyIce-antiX  Administrátor

      Jako pointu ako to funguje chápem, ale na čo to vo finále bude?

      Táto správa neobsahuje vírus, pretože nepoužívam MS Windows. http://kernelultras.org
      • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 07.01.2020 | 11:24
        Avatar Miroslav Bendík Gentoo  Administrátor

        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.

        • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 09.01.2020 | 06:54
          Avatar Richard Antix  Používateľ

          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?

           

          • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 09.01.2020 | 08:48
            Avatar Miroslav Bendík Gentoo  Administrátor

            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.

        • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 09.01.2020 | 07:10
          Avatar Livan Manjaro s XFCE  Používateľ

          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.

          • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 09.01.2020 | 08:54
            Avatar Miroslav Bendík Gentoo  Administrátor

            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:

            • digitálny sériový audio výstup (s voliteľnou kompresiou)
            • digitálny sériový audio vstup (s voliteľnou kompresiou)
            • analógový audio výstup
            • analógový audio vstup
            • max 24-bitový digitálny paralelný výstup (tzv LCD mode)
            • max 24-bitový digitálny paralelný vstup + vsync + hsync signály (tzv camera mode)

            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é.

    • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 09.01.2020 | 11:50
      Avatar redhawk75   Používateľ

      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

      • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 09.01.2020 | 12:10
        Avatar Miroslav Bendík Gentoo  Administrátor

        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ď.

        • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 09.01.2020 | 12:15
          Avatar redhawk75   Používateľ

          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

      • RE: ESP32 - dynamická zmena vzorkovacej frekvencie I2S 09.01.2020 | 12:33
        Avatar Miroslav Bendík Gentoo  Administrátor

        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.