ESP8266 RDS generator.

Everything technical about radio can be discussed here, whether it's transmitting or receiving. Guides, charts, diagrams, etc. are all welcome.
Post Reply
User avatar
EFR
no manz can test innit
no manz can test innit
Posts: 168
Joined: Mon May 20, 2024 5:39 pm

ESP8266 RDS generator.

Post by EFR » Fri Mar 07, 2025 8:35 am

I have never had time to test this, this piece of code has been collecting dust on my hdd last couple years.
What I know, it should work on any cheap ESP8266, just filter I²S output with something simple to cut harmonics and trow 10uF DC blocking to the output before MPX output.

If it wont work, there is still ChatGPT, even idiot like me can code with it, just paste this to it, and is vompiler shows some errors, paste all of it to the chatgpt and ask why it does that, and how to fix it..

Code: Select all

#include <Arduino.h>
#include "driver/i2s.h"

#define RDS_FREQ 57000      // 57 kHz RDS Subcarrier
#define RDS_BITRATE 1187.5  // RDS bit rate
#define SAMPLE_RATE 228000  // Sampling rate for I²S
#define I2S_WS 25           // Word select (not used)
#define I2S_BCK 26          // Bit clock
#define I2S_DATA 27         // Data output

const char *station_name = "COMMUNITY FM"; // Change this to your station name

void setupI2S() {
    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
        .sample_rate = SAMPLE_RATE,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
        .communication_format = I2S_COMM_FORMAT_I2S_MSB,
        .intr_alloc_flags = 0,
        .dma_buf_count = 8,
        .dma_buf_len = 1024,
        .use_apll = false
    };
    i2s_pin_config_t pin_config = {
        .bck_io_num = I2S_BCK,
        .ws_io_num = I2S_WS,
        .data_out_num = I2S_DATA,
        .data_in_num = -1
    };

    i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_NUM_0, &pin_config);
}

// Function to generate BPSK-modulated RDS signal
void sendRDS() {
    size_t bytes_written;
    int16_t buffer[1024];

    for (int i = 0; i < 1024; i++) {
        buffer[i] = 32767 * sin(2 * PI * RDS_FREQ * i / SAMPLE_RATE); // Generate 57kHz sine wave
    }

    while (true) {
        i2s_write(I2S_NUM_0, buffer, sizeof(buffer), &bytes_written, portMAX_DELAY);
    }
}

void setup() {
    Serial.begin(115200);
    setupI2S();
    sendRDS();
}

void loop() {
    // Nothing here, continuous I2S output
}
If anyone does get it running, please report here.

Now, go and make RDS great again.
Fight For Free Radio!

Frequent Lee
big in da game.. trust
big in da game.. trust
Posts: 93
Joined: Sat Jul 16, 2016 10:57 am

Re: ESP8266 RDS generator.

Post by Frequent Lee » Fri Mar 07, 2025 9:57 am

I've got a couple of these in my components drawers, I'll give this a test over the weekend and report back my experiences in a basic bench test into a common London type driver board.

User avatar
EFR
no manz can test innit
no manz can test innit
Posts: 168
Joined: Mon May 20, 2024 5:39 pm

Re: ESP8266 RDS generator.

Post by EFR » Fri Mar 07, 2025 10:22 am

Nice, I have some also, but never have had time to test this. I know station who used these, and it worked.

I have their second version also somewhere, it had an WiFi connection and page where text etc can be edited.
Fight For Free Radio!

jvok
tower block dreamin
tower block dreamin
Posts: 301
Joined: Sun Aug 16, 2020 2:44 pm

Re: ESP8266 RDS generator.

Post by jvok » Fri Mar 07, 2025 10:38 am

That code just puts out a continuous 57khz subcarrier with no modulation. Either its only half finished or theres a big chunk of code missing

shuffy
tower block dreamin
tower block dreamin
Posts: 395
Joined: Sun Oct 05, 2014 3:55 pm

Re: ESP8266 RDS generator.

Post by shuffy » Fri Mar 07, 2025 11:49 am

Maybe also ask chat GPT about the i2s clock jitter inherent in these SBCs due to the way they derive it from the master clock, and its effect on certain types of receiver resolving the RDS (and stereo pilot in some cases). That should sort it. :)
He said shuffy! I said WOT? Woo!

User avatar
EFR
no manz can test innit
no manz can test innit
Posts: 168
Joined: Mon May 20, 2024 5:39 pm

Re: ESP8266 RDS generator.

Post by EFR » Fri Mar 07, 2025 1:31 pm

jvok wrote: Fri Mar 07, 2025 10:38 am That code just puts out a continuous 57khz subcarrier with no modulation. Either its only half finished or theres a big chunk of code missing
I dont know, I just get that from on email.
Fight For Free Radio!

jvok
tower block dreamin
tower block dreamin
Posts: 301
Joined: Sun Aug 16, 2020 2:44 pm

Re: ESP8266 RDS generator.

Post by jvok » Fri Mar 07, 2025 4:07 pm

shuffy wrote: Fri Mar 07, 2025 11:49 am Maybe also ask chat GPT about the i2s clock jitter inherent in these SBCs due to the way they derive it from the master clock, and its effect on certain types of receiver resolving the RDS (and stereo pilot in some cases). That should sort it. :)
ESP32 has a dedicated audio PLL for the I2S clock so should be better than other chips. I've never heard bad things about it anyway

Some DACs have their own clock generator too so you could use that instead of the ESP32 internal clock

User avatar
EFR
no manz can test innit
no manz can test innit
Posts: 168
Joined: Mon May 20, 2024 5:39 pm

Re: ESP8266 RDS generator.

Post by EFR » Fri Mar 07, 2025 5:18 pm

Thats one sketch too from my email, partially finished. Emailed that guy but just got mail delivery error.

Code: Select all

#include <Arduino.h>
#include "driver/i2s.h"

#define RDS_FREQ 57000      // 57 kHz RDS subcarrier
#define RDS_BITRATE 1187.5  // RDS bit rate
#define SAMPLE_RATE 228000  // Sampling rate for I²S
#define I2S_WS 25           // Word select (not used)
#define I2S_BCK 26          // Bit clock
#define I2S_DATA 27         // Data output

const char *station_name = "COMMUNITY FM";  // Change this to your station name

// RDS Group structure
uint16_t rds_group[4];

// RDS Generator Function
void generateRDS() {
    // Example: Simple Program Service Name (PS)
    rds_group[0] = 0x0C00; // Group type 0A, PI code
    rds_group[1] = 0xE0E0; // 'C' 'O'
    rds_group[2] = 0xE5E5; // 'M' 'M'
    rds_group[3] = 0xE9E9; // 'U' 'N' (next pair: 'I' 'T')

    // TODO: Extend this for full 8-character PS name
}

// I²S Setup for RDS Output
void setupI2S() {
    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
        .sample_rate = SAMPLE_RATE,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
        .communication_format = I2S_COMM_FORMAT_I2S_MSB,
        .intr_alloc_flags = 0,
        .dma_buf_count = 8,
        .dma_buf_len = 1024,
        .use_apll = false
    };
    i2s_pin_config_t pin_config = {
        .bck_io_num = I2S_BCK,
        .ws_io_num = I2S_WS,
        .data_out_num = I2S_DATA,
        .data_in_num = -1  // Not used
    };

    i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_NUM_0, &pin_config);
}

// Generate and send BPSK-modulated RDS signal
void sendRDS() {
    size_t bytes_written;
    int16_t buffer[1024];

    for (int i = 0; i < 1024; i++) {
        buffer[i] = 32767 * sin(2 * PI * RDS_FREQ * i / SAMPLE_RATE); // Generate 57kHz sine wave
    }

    while (true) {
        i2s_write(I2S_NUM_0, buffer, sizeof(buffer), &bytes_written, portMAX_DELAY);
    }
}

void setup() {
    Serial.begin(115200);
    generateRDS();  // Generate initial RDS data
    setupI2S();     // Initialize I²S
    sendRDS();      // Start transmitting RDS
}

void loop() {
    // Nothing here, continuous I²S output
}
Fight For Free Radio!

BlackBeard
big in da game.. trust
big in da game.. trust
Posts: 90
Joined: Tue Sep 20, 2016 9:44 am

Re: ESP8266 RDS generator.

Post by BlackBeard » Thu Mar 27, 2025 6:24 am

EFR, I received my ESP32 yesterday. I'll give it a try this weekend. I also fixed the code. We will see, if it works now.
EFR wrote: Fri Mar 07, 2025 5:18 pm Thats one sketch too from my email, partially finished. Emailed that guy but just got mail delivery error.

User avatar
EFR
no manz can test innit
no manz can test innit
Posts: 168
Joined: Mon May 20, 2024 5:39 pm

Re: ESP8266 RDS generator.

Post by EFR » Thu Mar 27, 2025 1:49 pm

BlackBeard wrote: Thu Mar 27, 2025 6:24 am EFR, I received my ESP32 yesterday. I'll give it a try this weekend. I also fixed the code. We will see, if it works now.
EFR wrote: Fri Mar 07, 2025 5:18 pm Thats one sketch too from my email, partially finished. Emailed that guy but just got mail delivery error.
I have also some ESP8266 modules waiting, one old tube rig has been taking my time.

EL84 VFO driving modulated EL84 stage driving GU43B....
Fight For Free Radio!

BlackBeard
big in da game.. trust
big in da game.. trust
Posts: 90
Joined: Tue Sep 20, 2016 9:44 am

Re: ESP8266 RDS generator.

Post by BlackBeard » Sat Mar 29, 2025 6:50 pm

EFR wrote: Thu Mar 27, 2025 1:49 pm
BlackBeard wrote: Thu Mar 27, 2025 6:24 am EFR, I received my ESP32 yesterday. I'll give it a try this weekend. I also fixed the code. We will see, if it works now.
EFR wrote: Fri Mar 07, 2025 5:18 pm Thats one sketch too from my email, partially finished. Emailed that guy but just got mail delivery error.
I have also some ESP8266 modules waiting, one old tube rig has been taking my time.

EL84 VFO driving modulated EL84 stage driving GU43B....
Tried the following (completed) code today, didn't work sadly (also added blocking cap & resistor to prevent clipping):


#include <Arduino.h>
#include "driver/i2s.h"
#include <math.h>

#define RDS_FREQ 57000 // 57 kHz RDS-Subcarrier
#define RDS_BITRATE 1187.5 // RDS-Bitrate
#define SAMPLE_RATE 228000 // I²S-Samplingrate
#define I2S_BCK 26
#define I2S_DATA 27

const char *station_name = "COMMUNITY"; // Max. 8 Zeichen für PS
const uint16_t PI_CODE = 0x1234; // Eigenen PI-Code setzen!

// RDS-Gruppenstruktur mit CRC
struct RDSGroup {
uint16_t block1;
uint16_t block2;
uint16_t block3;
uint16_t block4;
};

RDSGroup ps_groups[4]; // 4 Gruppen für 8-Zeichen PS-Name
uint8_t group_idx = 0;

// CRC-Generator für RDS
uint16_t calcCRC(uint16_t data, uint16_t crc = 0) {
for(uint8_t i=0; i<16; i++) {
crc = ((data >> (15-i)) & 1) ^ ((crc >> 9) & 1) ? (crc << 1) ^ 0x1B5 : crc << 1;
}
return crc & 0x3FF;
}

// PS-Name-Gruppen generieren
void generatePSGroups() {
for(uint8_t i=0; i<4; i++) {
ps_groups.block1 = (0x0A << 12) | (group_idx << 4) | (PI_CODE >> 12);
ps_groups.block2 = PI_CODE;
ps_groups.block3 = ((uint16_t)station_name[2*i] << 8) | station_name[2*i+1];
ps_groups.block4 = ((uint16_t)station_name[2*i] << 8) | station_name[2*i+1];

// CRC für Block 1 und 2 berechnen
ps_groups.block1 |= calcCRC(ps_groups.block1) << 16;
ps_groups.block2 |= calcCRC(ps_groups.block2) << 16;
}
}

// BPSK-Modulation
void modulateBPSK(uint8_t bit, int16_t* buffer, size_t len) {
static float phase = 0;
for(size_t i=0; i<len; i++) {
buffer = 32767 * sin(phase + (bit ? M_PI : 0));
phase = fmod(phase + 2 * M_PI * RDS_FREQ / SAMPLE_RATE, 2*M_PI);
}
}

// RDS-Bitstrom generieren
void sendRDS() {
uint32_t bitstream = 0;
uint8_t bit_pos = 0;
int16_t buffer[192]; // 1187.5 bps → 228000/1187.5 ≈ 192 samples/bit

while(1) {
// Nächste RDS-Gruppe senden
RDSGroup group = ps_groups[group_idx];
group_idx = (group_idx + 1) % 4;

// Datenrahmen zusammensetzen (104 Bits pro Gruppe)
uint32_t data = ((uint32_t)group.block1 << 48) | ((uint32_t)group.block2 << 32)
| ((uint32_t)group.block3 << 16) | group.block4;

for(uint8_t i=0; i<104; i++) {
uint8_t bit = (data >> (103-i)) & 1;
modulateBPSK(bit, buffer, 192);
size_t bytes_written;
i2s_write(I2S_NUM_0, buffer, sizeof(buffer), &bytes_written, portMAX_DELAY);
}
}
}

void setup() {
generatePSGroups();
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
.use_apll = true // Für präzisere Frequenz
};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_BCK,
.data_out_num = I2S_DATA,
.data_in_num = -1
};
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pin_config);
sendRDS();
}

void loop() {}

BlackBeard
big in da game.. trust
big in da game.. trust
Posts: 90
Joined: Tue Sep 20, 2016 9:44 am

Re: ESP8266 RDS generator.

Post by BlackBeard » Wed Apr 02, 2025 8:41 pm

Continued working on it using the ESP32 and a little PCM5102 board. Tried the following code. I finally got a 57kHz-signal on my spectrum analyzer! The TEF6686 didn't decode it though when I fed it to my little mini tx. Any hints on how to continue? Didn't apply a RC-filter to the analogue output yet, might this be a problem?

Code: Select all

#include <Arduino.h>
#include "driver/i2s.h"
#include <math.h> // Für M_PI und sinf

// ---------------------------------------------------------------------
// Grundlegende RDS-Parameter
// ---------------------------------------------------------------------
static const uint32_t I2S_SAMPLE_RATE = 228000;  // Ausreichend hoch (4 * 57kHz)
static const uint32_t RDS_SUBCARRIER  = 57000;   // 57 kHz
static const float    RDS_BITRATE     = 1187.5;  // 1187,5 Bit/s

// Anzahl Samples pro Bit = SampleRate / Bitrate
// Runden zum nächsten Integer, da die Anzahl ganzzahlig sein muss.
static const int SAMPLES_PER_BIT = (int)((float)I2S_SAMPLE_RATE / RDS_BITRATE + 0.5f);

// Korrigierte Sample Rate basierend auf ganzzahliger Samples/Bit Anzahl,
// um Phasenfehler zu minimieren (optional aber sauberer)
// static const uint32_t ADJUSTED_SAMPLE_RATE = (uint32_t)(RDS_BITRATE * SAMPLES_PER_BIT); // Ergibt hier ca. 228000

// Offset-Wörter (je 10 Bit) laut RDS für die vier Blöcke A,B,C,D
// Hexadezimal: 0x0FC, 0x198, 0x168, 0x1B4
static const uint16_t OFFSET_WORD[4] = { 0x0FC, 0x198, 0x168, 0x1B4 };

// Generator-Polynom für die RDS-CRC (10 Bit) => x^10 + x^8 + x^7 + x^5 + x^4 + x^3 + 1
// Entspricht 0x5B9 (MSB des Polynoms zuerst, 11 Bit Darstellung inkl. x^10)
static const uint16_t RDS_CRC_POLY = 0x5B9; // Wirkt als 10-Bit Check (Bits 9..0)

// ---------------------------------------------------------------------
// I2S Konfiguration
// ---------------------------------------------------------------------
#define I2S_PORT I2S_NUM_0
#define I2S_BCK_PIN  26
#define I2S_WS_PIN   25 // LRCK
#define I2S_DATA_PIN 27

// ---------------------------------------------------------------------
// Beispielhafte Sender-Infos
// ---------------------------------------------------------------------
static const uint16_t PI_CODE = 0xABCD;   // Deine 16-Bit Senderkennung (ändere dies!)
static const bool     TP_FLAG = false;    // Traffic Programme Flag (true/false)
static const uint8_t  PTY_CODE = 2;      // Programmtyp z.B. 2 = News (EU-PTY)
// PS Name MUSS genau 8 Zeichen lang sein! Mit Leerzeichen auffüllen falls kürzer.
static const char* PS_NAME_8CHARS = "ESP RDS "; // Beispiel

// ---------------------------------------------------------------------
// Puffer für RDS-Gruppen: 4 Segmente (je 2 Zeichen) => 0A-Gruppe
// Jeder Eintrag rdsGroups[i][j] ist ein 26-Bit-Wort: data(16)+crc(10)
// ---------------------------------------------------------------------
static uint32_t rdsGroups[4][4]; // 4 Gruppen à 4 Blöcke (A,B,C,D)

// ---------------------------------------------------------------------
// CRC-Berechnung für 16 Datenbits nach RDS-Standard
// ---------------------------------------------------------------------
uint16_t rdsComputeCRC(uint16_t data16) {
    uint32_t reg = ((uint32_t)data16) << 10; // 16 Daten + 10 Nullen = 26 Bit Register
    uint32_t poly_shifted = (uint32_t)RDS_CRC_POLY << 15; // Polynom auf die oberen Bits schieben

    for (int i = 0; i < 16; i++) {
        // Prüfe oberstes Bit des (logischen) 26-Bit Registers (Bit 25)
        if (reg & 0x02000000) { // 1 << 25
            reg ^= poly_shifted;
        }
        reg <<= 1; // Nächstes Bit rein schieben
    }
    // Die oberen 10 Bits (Bits 25..16) nach den 16 Schiebe-Operationen sind der CRC
    uint16_t crc = (uint16_t)((reg >> 16) & 0x3FF); // 10 Bit extrahieren
    return crc;
}

// ---------------------------------------------------------------------
// Hilfsfunktion: RDS-Block erstellen (16 Bit Daten + 10 Bit Checksumme)
// ---------------------------------------------------------------------
uint32_t rdsMakeBlock(uint16_t blockData, int blockIndex) {
    uint16_t crc = rdsComputeCRC(blockData);
    crc ^= (OFFSET_WORD[blockIndex] & 0x3FF); // XOR mit dem passenden Offset-Word
    // Block = (Daten << 10) | CRC
    uint32_t blockWord = (((uint32_t)blockData) << 10) | crc;
    return blockWord; // Enthält die 26 relevanten Bits in den unteren Positionen
}

// ---------------------------------------------------------------------
// Erzeugt eine RDS-Gruppe vom Typ 0A (für PS-Name)
// ---------------------------------------------------------------------
void buildGroup0A(uint16_t pi, bool tp, uint8_t pty, char c1, char c2, uint32_t group[4]) {
    // --- Block A: PI-Code ---
    group[0] = rdsMakeBlock(pi, 0); // blockIndex=0 => Offset A

    // --- Block B: Group Type 0, Version A, TP, PTY etc. ---
    // Bits: 15..12=0000 (Group 0), 11=0 (Version A), 10=TP, 9..5=PTY, 4..0=Diverse Flags
    uint16_t blockB = 0;
    blockB |= (0b0000 << 12);      // Group Type 0
    blockB |= (0 << 11);           // Version A ('A' Version)
    blockB |= ((tp ? 1 : 0) << 10); // TP Flag
    blockB |= ((pty & 0x1F) << 5);  // PTY Code (5 Bits)
    // PS Segment Index: Bits 1,0 in Block B bestimmen, welcher Teil des 8-Zeichen PS gesendet wird
    // Finde den Index basierend auf c1 (quick & dirty Annahme, dass c1 das erste Zeichen des Segments ist)
    int ps_seg_idx = 0;
    for(int k=0; k<8; k++) { if(PS_NAME_8CHARS[k] == c1) { ps_seg_idx = k / 2; break; } }
    blockB |= (ps_seg_idx & 0x03); // Segment Adresse 0..3 in Bits 1,0 (korrigiert)

    // Weitere Flags (TA=0, M/S=0, DI=0) bleiben 0 hier. Bit 4 ist TA, Bit 3 M/S, Bit 2 DI.
    group[1] = rdsMakeBlock(blockB, 1); // Offset B

    // --- Block C: Für Gruppe 0A üblicherweise PI-Code Wiederholung ---
    // Alternative: AF Codes, aber PI Wiederholung ist einfacher und üblich für reinen PS
    group[2] = rdsMakeBlock(pi, 2); // Offset C

    // --- Block D: Enthält 2 ASCII-Zeichen des PS-Namens ---
    uint16_t blockD = ((uint16_t)(uint8_t)c1 << 8) | (uint8_t)c2;
    group[3] = rdsMakeBlock(blockD, 3); // Offset D
}

// ---------------------------------------------------------------------
// Erstellt vier 0A-Gruppen für den gesamten 8-stelligen PS-Name
// ---------------------------------------------------------------------
void createPSGroups() {
    Serial.println("Erstelle RDS Gruppen für PS: ");
    for (int i = 0; i < 4; i++) {
        char c1 = PS_NAME_8CHARS[i * 2];
        char c2 = PS_NAME_8CHARS[i * 2 + 1];
        Serial.printf("  Gruppe %d: Zeichen '%c%c'\n", i, c1, c2);
        buildGroup0A(PI_CODE, TP_FLAG, PTY_CODE, c1, c2, rdsGroups[i]);

        // Debug: Zeige erstellte Blöcke (optional)
        // Serial.printf("    Block A: 0x%08X\n", rdsGroups[i][0]);
        // Serial.printf("    Block B: 0x%08X\n", rdsGroups[i][1]);
        // Serial.printf("    Block C: 0x%08X\n", rdsGroups[i][2]);
        // Serial.printf("    Block D: 0x%08X\n", rdsGroups[i][3]);
    }
    Serial.println("RDS Gruppen erstellt.");
}

// ---------------------------------------------------------------------
// I2S-Setup
// ---------------------------------------------------------------------
void setupI2S() {
    i2s_config_t i2s_config = {
        .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
        .sample_rate = I2S_SAMPLE_RATE,
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
        .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // Standard Stereo - sende Signal auf beiden Kanälen
        .communication_format = I2S_COMM_FORMAT_I2S_MSB,
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // oder 0
        .dma_buf_count = 8,
        .dma_buf_len = 1024, // Puffergröße evtl. anpassen
        .use_apll = false, // APLL wird nicht benötigt für diese Rate
        .tx_desc_auto_clear = true
    };

    i2s_pin_config_t pin_config = {
        .bck_io_num = I2S_BCK_PIN,
        .ws_io_num = I2S_WS_PIN,
        .data_out_num = I2S_DATA_PIN,
        .data_in_num = I2S_PIN_NO_CHANGE // Nicht benutzt
    };

    esp_err_t err;
    err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
    if (err != ESP_OK) {
      Serial.printf("Fehler bei i2s_driver_install: %d\n", err);
      return;
    }
     err = i2s_set_pin(I2S_PORT, &pin_config);
     if (err != ESP_OK) {
      Serial.printf("Fehler bei i2s_set_pin: %d\n", err);
       return;
    }
    Serial.println("I2S Treiber installiert und Pins konfiguriert.");
}

// ---------------------------------------------------------------------
// Globale Variablen für BPSK Modulation
// ---------------------------------------------------------------------
static int current_differential_phase = 0; // 0 oder 1, für differentielles Encoding
static double phase_accumulator = 0.0;     // Kontinuierliche Phase für Sinus-Trägerwelle
const double phase_step = 2.0 * M_PI * (double)RDS_SUBCARRIER / (double)I2S_SAMPLE_RATE;
const int16_t AMPLITUDE = 20000; // Amplitude (max 32767), etwas lauter stellen

// ---------------------------------------------------------------------
// Schreibt die Samples für EIN Bit in den I2S Puffer
//   - Implementiert differenzielle BPSK
//   - Sorgt für Phasenkontinuität der Trägerwelle
// ---------------------------------------------------------------------
void rdsOutputBit(uint8_t bit, int16_t* buffer, size_t& buffer_idx) {
    // Differentielles Encoding: Phase nur bei '1' ändern
    if (bit == 1) {
        current_differential_phase ^= 1; // Toggle 0 <-> 1
    }

    // Phase für den Sinus: +phi wenn current_differential_phase=0, -phi wenn =1
    // Entspricht Multiplikation mit +1 oder -1
    float phase_sign = (current_differential_phase == 0) ? 1.0f : -1.0f;

    for (int i = 0; i < SAMPLES_PER_BIT; i++) {
        // Berechne Sinuswert basierend auf globalem Phasenakkumulator
        float sin_val = sinf((float)phase_accumulator);

        // Multipliziere mit BPSK Phase (+1 oder -1) und Amplitude
        int16_t sample_value = (int16_t)(phase_sign * sin_val * AMPLITUDE);

        // Schreibe Stereo-Sample (beide Kanäle gleich)
        buffer[buffer_idx++] = sample_value; // Linker Kanal
        buffer[buffer_idx++] = sample_value; // Rechter Kanal

        // Update globalen Phasenakkumulator für nächsten Sample
        phase_accumulator += phase_step;
        // Wrap phase accumulator bei 2*PI um Fließkommafehler zu vermeiden
        if (phase_accumulator >= 2.0 * M_PI) {
            phase_accumulator -= 2.0 * M_PI;
        }
    }
}


// ---------------------------------------------------------------------
// Schreibt einen 26-Bit RDS-Block bitweise in den I2S Puffer
// ---------------------------------------------------------------------
void rdsOutputBlock(uint32_t blockWord, int16_t* buffer, size_t& buffer_idx) {
    // Sende 26 Bits, MSB zuerst (Bit 25 -> Bit 0)
    for (int i = 25; i >= 0; i--) {
        uint8_t bit = (blockWord & (1UL << i)) ? 1 : 0;
        rdsOutputBit(bit, buffer, buffer_idx);
    }
}

// ---------------------------------------------------------------------
// Sendet eine komplette RDS-Gruppe (4 Blöcke) in den I2S Puffer
// ---------------------------------------------------------------------
void rdsOutputGroup(const uint32_t group[4], int16_t* buffer, size_t& buffer_idx) {
    for (int b = 0; b < 4; b++) { // Blöcke A, B, C, D
        rdsOutputBlock(group[b], buffer, buffer_idx);
    }
}

// ---------------------------------------------------------------------
// Endlosschleife: Rotiert über die 4 PS-Gruppen und sendet sie via I2S
// ---------------------------------------------------------------------
void sendRDSLoop() {
    // Buffer für Samples vorbereiten
    // Jede Gruppe = 4 Blöcke * 26 Bits/Block = 104 Bits
    // Samples pro Gruppe = 104 Bits * SAMPLES_PER_BIT Samples/Bit
    // Da wir Stereo senden (I2S_CHANNEL_FMT_RIGHT_LEFT), benötigen wir 2 * Samples
    const size_t samples_per_group_mono = (size_t)104 * SAMPLES_PER_BIT;
    const size_t samples_per_group_stereo = samples_per_group_mono * 2;
    const size_t buffer_size_samples = samples_per_group_stereo; // Puffer genau für eine Gruppe

    // Speicher dynamisch allokieren (besser als großer Stack-Buffer)
    int16_t* i2s_buffer = (int16_t*)malloc(buffer_size_samples * sizeof(int16_t));
    if (!i2s_buffer) {
        Serial.println("FEHLER: Konnte I2S Puffer nicht allokieren!");
        return; // Oder ESP neu starten
    }
    Serial.printf("I2S Puffer Größe: %d Samples (%d Bytes)\n", buffer_size_samples, buffer_size_samples * sizeof(int16_t));

    size_t bytes_written = 0;
    int current_group_index = 0;

    Serial.println("Starte RDS Sendeschleife...");

    while (true) {
        size_t buffer_write_index = 0; // Index für den i2s_buffer (in Samples)

        // Fülle den Buffer mit den Samples für die aktuelle Gruppe
        rdsOutputGroup(rdsGroups[current_group_index], i2s_buffer, buffer_write_index);

        // Schreibe den gefüllten Buffer via I2S
        // buffer_write_index sollte jetzt == buffer_size_samples sein
        esp_err_t err = i2s_write(I2S_PORT, i2s_buffer, buffer_write_index * sizeof(int16_t), &bytes_written, portMAX_DELAY);

         if (err != ESP_OK) {
            Serial.printf("Fehler beim I2S Schreiben: %d\n", err);
         }
        //else {
            // Optional: Nur bei Erfolg loggen, um Serial nicht zu fluten
            // Serial.printf("Gruppe %d gesendet (%d Bytes)\n", current_group_index, bytes_written);
        //}

        // Nächste Gruppe für den nächsten Durchlauf auswählen (0 -> 1 -> 2 -> 3 -> 0 ...)
        current_group_index = (current_group_index + 1) % 4;

        // Kleine Pause ist optional, I2S blockiert normalerweise, bis Puffer frei ist
        // vTaskDelay(pdMS_TO_TICKS(5));
    }

    // Dieser Teil wird in der Endlosschleife nie erreicht, aber der Vollständigkeit halber:
    free(i2s_buffer);
    i2s_driver_uninstall(I2S_PORT);
}

// ---------------------------------------------------------------------
// SETUP
// ---------------------------------------------------------------------
void setup() {
    Serial.begin(115200);
    delay(2000); // Warte kurz auf Serial Monitor
    Serial.println("\n--- ESP32 RDS Encoder Start ---");

    // 1) RDS-Gruppen für den PS-Namen vorberechnen
    createPSGroups();

    // 2) I2S initialisieren
    setupI2S();

    // 3) Endlosschleife für die RDS-Übertragung starten
    //    Hinweis: Diese Funktion blockiert und kehrt nicht zurück!
    sendRDSLoop();
}

// ---------------------------------------------------------------------
void loop() {
    // Wird nie erreicht, da sendRDSLoop() eine Endlosschleife ist.
}

Albert H
proppa neck!
proppa neck!
Posts: 2957
Joined: Tue Apr 05, 2016 1:23 am

Re: ESP8266 RDS generator.

Post by Albert H » Wed Apr 02, 2025 9:01 pm

The lack of filter will just mean that the baseband will contain extra crud, If there's correctly modulated data on the carrier, it should be detected and decoded by your receiver, even without the filtering.
"Why is my rig humming?"
"Because it doesn't know the words!"
;)

Post Reply