I2S jelgenerátor ESP8266-tal

Sziasztok!

Próbálok összerakni egy I2S jelgenerátort Wemos D1 minivel. A cél az lenne, hogy 1024 bit amplitudójú szinuszjelet generáljon 440 Hz-en, amit egy DAC-ba vezetnék. A loop-ba tettem egy for ciklust, ami 1 másodperc alatt, azaz 44100 WS periódus alatt kellene, hogy végigmenjen, kiszámolja a szinusz amplitudót, átalakítja kettes komplemensre, és kiírja a DATA kimenetre, ami megy a DAC-ra. Nem világos azonban, hogy a for ciklus lefutása után mi történik. Azt szeretném, hogy kezdje újból a loop-ot, de mintha nem azonnal kezdené újra a for ciklust. A GPIO3 a DATA (SD), a D1,D2,D5,D6 kapcsolókkal beállítható "track number" 0-tól 15-ig. WS a GPIO2 (D4), SCK a GPIO15 (D8). Egy másik általam írt I2S kódban a TrackNumber és az i2s_write_lr jól működik. Azt nem értem, hogy az I2S sampling rate (44100 Hz) hogyan viszonyul a loop lefutási idejéhez. A loop-on belül szeretnék adatot kiírni 44100-szor, és ezt ismételni késedelem nélkül. Elnézést, kezdő vagyok, valamit elnézhettem, és a kódon is lehetne optimalizálni.

#include <I2S.h>

void setup() {
  pinMode(3,OUTPUT);
  pinMode(D1,INPUT);
  pinMode(D2,INPUT);
  pinMode(D5,INPUT);
  pinMode(D6,INPUT);
  digitalWrite(3,0);
  i2s_begin();
  i2s_set_rate(44100);
}

void loop() {
    uint8_t track=TrackNumber();
    uint16_t Freq = 440; // frequency in Hz    
    float Omega = 2*PI*Freq; // angular frequency in radians

    for (uint16_t Pace=0; Pace < 44100; Pace++) {
    float Cycle = (Pace*Omega)/44100;
    float Ampnorm = ((sin(Cycle)+1)/2)*1023; // normalized amplitude 0...1023 float
    uint16_t AmpOB = Ampnorm + 0.5; // normalized amplitude rounded integer 0...1023 (0...03FF) offset binary
    uint16_t AmpOffset = pow(2,track)-512 + AmpOB; // amplitude DC shifted according to track number
    uint16_t AmpSB = 0x8000 - AmpOffset; // amplitude signed binary
    uint16_t Amp2C = (~AmpSB)+1; // amplitude 2s complement
    i2s_write_lr(Amp2C, Amp2C);
    }

uint8_t TrackNumber(void) {
  uint8_t(var1);
  uint8_t(var2);
  uint8_t(var4);
  uint8_t(var8);
  uint8_t(track_number);
  if (digitalRead(D1)) var1=1;
   else var1=0;
  if (digitalRead(D2)) var2=2;
   else var2=0;
  if (digitalRead(D5)) var4=4;
   else var4=0;
  if (digitalRead(D6)) var8=8;
   else var8=0;
  track_number = var8 + var4 + var2 + var1;
  return (track_number);
}  

Hozzászólások

Gyanítom, hogy az i2s_write_lr blokkol addig, amíg el kell küldeni a samplet. A headerben ez van:

/*

 i2s_write_sample will block when you're sending data too quickly, so you can just

 generate and push data as fast as you can and i2s_write_sample will regulate the

 speed.*/

Úgyhogy ha elég gyors a ciklusod, akkor működik. Más esetben bufferbe kellene írni, majd DMA (i2s_write_buffer).

Köszönöm. Beteszek egy DMA buffer ellenőrzést, i2s_is_empty(). Tudnék valahogy gyorsítani a cikluson? 

Ha egyesével írsz bele, minek az ellenőrzés? Ráadásul megvárni, míg tök üres lesz a FIFO...az írás megvárja, míg lesz szabad hely.

Gyorsítani? Mondjuk átírod integer aritmetikára float helyett.

Vagy kiszámolod a függvény első negyedét előre, beteszed egy tömbbe (ez 44100/4*2 byte, kb. 20 kB, elfér az ESP RAM-jában), és a loop()-ban csak kiolvasod onnan.

Miért nem rakod táblázatba? Elfér 201 bájtban.

A Wemost akarod gyakorolni, vagy a jelgenerator a lenyeg? Utobbi esetben egy AD9850 vagy a csaladbol megfelelo eszkoz jo lehet. Arduinohoz meg lib is van, gondolom ESP-hez is. Ill. letezik breakout boardos valtozata, csak ossze kell dugdosni.

A strange game. The only winning move is not to play. How about a nice game of chess?

Egy androidos telefon kimenete nem jó erre a célra? Megannyi signal generátor app van, ha jó a dac, jó lesz a jelalak is..

https://play.google.com/store/apps/details?id=com.keuwl.functiongenerat…

"Nem akkor van baj amikor nincs baj, hanem amikor van!"
Népi bölcsesség

Jó, és nekem is pont ez a kedvenc generátorom, rendkívül alacsony a torzítása. Én éppen DAC teszteléséhez építettem ezt az ESP8266-os eszközt, amivel mindenféle extrém kondíciókat tudok beállítani, pl. hogy viselkedik a DAC amikor sok bit egyszerre vált (pl. 0000 1111 1111 1111 > 0001 0000 0000 0000), és ennek a hatása -60 dB amplitudójú szinuszjel torzítására, amit a nullátmenetnél fellépő linearitás-hiba okoz. Méricskélek, szórakozok...

Szerkesztve: 2024. 05. 27., h – 18:58

Nincs veletlenul fenn az ESP a halozaton? Ha korabban mentettel bele autoconnectes beallitast, akkor azt szepen "tolja belul". A loop() hivasok kozott ilyenkor tuti, hogy van valamennyi housekeeping, hogy a halozaton aktiv maradhasson.

Ez miért ennyire bonyolult? A TrackNumber() szerintem egyetlen return némi argumentummal.

pow(2,track)

Már úgy érted 1 << track, ugye? ;)

(~AmpSB)+1

Tehát (uint16_t) -AmpSB. Ha nem kényes a fordító, a castolás sem kell.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Szerkesztve: 2024. 05. 27., h – 23:06

Ez a kód így nem az, ami jó. Valahogy úgy csinálmám, hogy felizgatnék egy timer-t és egy DMA kontrollert. A timer 44100 Hz-cel triggerelné a DMA kontrollert, a DMA forrása egy buffer, célja az I2S periféria. A buffert két félbufferre bontanám, s amikor elértük a felső felét, megszakítás keletkezne, s feltöltenénk FIFO-ból az alját, amikor a felső felének végéhez értünk, s az alsó felének lejátszása következik, akkor pedig FIFO-ból feltöltjük a felső felét. A FIFO-t a 440 Hz-es mintákkal lehet tölteni alapszintről addig, amíg megtelik a buffer. Ha legközelebb lesz benne üres hely, tesszük bele az újabb mintákat, persze folytatólagosan, nem elfelejtve, hogy az Omega * t fázishelyzet hol tartott.

Mert ennek most nem is tudom, mi végzi az időzítését. Ha az i2s_write_lr() blokkol, akkor utána marha gyorsan kell adni neki a következő mintát, amiről, ha lemaradunk, meg fog nyúlni az egész, recsegni, kopogni fog a hang. Semmi rugalmasság nem lesz a rendszerben, még egy elég gyors MCU is kevés lesz hozzá, ráadásul semmi mást nem tud majd csinálni, kommunikálni, LED-et villogtatni, más taskokat futtatni. Ellenben, ha úgy csinálod, ahogy írtam, csak átlagos sebességben kell tudni kellő sebességgel etetni a FIFO-t.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Szerkesztve: 2024. 05. 28., k – 10:04

Én azzal kezdeném, hogy ennél az I2S-nél a végső órajel (ami alapján a DAC működik) miből áll elő, és hogy az I2S lib mit csinál? Az I2S jelforma elég egyszerű, a Wiki találat alapján is implementálható lenne talán. Eleve kérdés, hogy szoftveresen, vagy hardveresen állítja-e elő a csipped? És a másik kérdés, hogy milyen időzítési pontosságot igényet, tehát hogy van-e bufferelve a másik végén, és hogy miből áll végül elő a kimeneti órajel? Úgy érzékelem a leírása alapján, hogy a WS vonal egyben a kimenet órajele is lehetne.

(Régi megfigyelésem, hogy az Arduino libek nem jól hordozhatóak platformok között - tehát csak néhány csipen működik jól, vagy csak bizonyos körülmények között működnek, vagy nem stabilak, stb. Nem lehet megúszni, hogy belenézz, hogy mit csinál és adaptáld a saját igényeidhez.)

Ha nincs hardveres I2S támogatás a csipben, akkor én úgy valósítanám meg, hogy egy PWM-re felprogramozható timer adná a WS jelet pontosan az elvárt frekvenciára hangolva, és ezzel párhuzamosan egy interruptot is adna, amivel triggerelnék egy SPI átvitelt. Mert úgy tűnik, hogy egy okosan beállított SPI (ami minden csipen van), plusz a vele szinkronban járó WS jel adja ki az I2S protokoll.

Az SPI részét meg lehet csinálni teljesen szoftveresen is persze.

ESP32-ben van hardveres I2S, neked annyi a feladatod, hogy felprogramozd, majd a FIFO-t olyan gyorsan etesd, hogy ne ürüljön ki, ami az egyszerű blokkolós függvényekkel annyira nem egyszerű, mert ezek megvárják, amíg a DMA buffer kiürül:

https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-referen…

Győzelem! Biztosan nem a legszebb kód, de működik. Oszcilloszkópon nézve azt kapom, amit elvártam. Köszönöm mindenkinek a segítséget. Ilyen lett: (track_number csak 9 és felette használom)

#include <I2S.h>
float Cycle[2205];
uint16_t Freq; // frequency in Hz divided by 20    
float Omega; // angular frequency in radians
float Ampnorm[2205];
    
void setup() {
  pinMode(3,OUTPUT);
  pinMode(D1,INPUT);
  pinMode(D2,INPUT);
  pinMode(D5,INPUT);
  pinMode(D6,INPUT);
  digitalWrite(3,0);
  i2s_begin();
  i2s_set_rate(44100);
  Freq = 22;
  Omega = 2*PI*Freq;
  for (uint16_t i=0; i < 2205; i++) {
  Cycle[i]=(i*Omega)/2205;
  Ampnorm[i] = ((sin(Cycle[i])+1)/2)*1023; // normalized amplitude 0...1023 float
  }
}

void loop() {
    uint16_t TrackNumber();
    for (uint16_t j=0; j < 2205; j++) {
    uint16_t AmpOB = Ampnorm[j] + 0.5; // normalized amplitude rounded integer 0...1023 (0...03FF) offset binary
    uint16_t AmpOffset = TrackNumber()-512 + AmpOB; // amplitude DC shifted according to track number
    uint16_t Amp2C = 0x8000 xor AmpOffset; // first bit swapped (XOR)
    i2s_write_lr(Amp2C, Amp2C);
    }

uint16_t TrackNumber(void) {
  uint8_t(var1);
  uint8_t(var2);
  uint8_t(var4);
  uint8_t(var8);
  uint8_t(track_number);
  if (digitalRead(D1)) var1=1;
   else var1=0;
  if (digitalRead(D2)) var2=2;
   else var2=0;
  if (digitalRead(D5)) var4=4;
   else var4=0;
  if (digitalRead(D6)) var8=8;
   else var8=0;
  track_number = var8 + var4 + var2 + var1; // track 0 ... 15
  return (1<<track_number); // 1, 2, 4, 8, ... 32768
}