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