ImperiumBT este un adaptor Bluetooth audio creat pentru casetofoanele din gama VAG (ex: Volkswagen Gamma 5, Audi Concert, Skoda Symphony, etc) care nu dispun de intrare auxiliara.
Proiectul are ca scop modernizarea acestor sisteme audio fara a le modifica structura, prin emularea unui CD changer prin interfata SPI a unui Arduino Uno si prin transmiterea semnalului audio de la un ESP32 cu Bluetooth integrat.
Pentru a putea trimite orice fel de semnal audio catre casetofonul auto, vom folosi un Arduino Uno, iar cu ajutorul interfetei SPI vom trimite o secvente de date catre casetofon. In lipsa acestei secvente, casetofonul in momentul in care este aleasa optiunea de redare de pe CD, va genera o eroare de tipul NO CD CHANGER chiar daca pe inputul audio este trimis semnal.
Folosim urmatoarele define-uri pentru a indica fiecare comanda suportata de casetofon:
#define CDC_PREFIX1 0x53 #define CDC_PREFIX2 0x2C #define CDC_END_CMD 0x14 #define CDC_PLAY 0xE4 #define CDC_STOP 0x10 #define CDC_NEXT 0xF8 #define CDC_PREV 0x78 #define CDC_SEEK_FWD 0xD8 #define CDC_SEEK_RWD 0x58 #define CDC_CD1 0x0C #define CDC_CD2 0x8C #define CDC_CD3 0x4C #define CDC_CD4 0xCC #define CDC_CD5 0x2C #define CDC_CD6 0xAC #define CDC_SCAN 0xA0 #define CDC_SFL 0x60 #define CDC_PLAY_NORMAL 0x08 #define MODE_PLAY 0xFF #define MODE_SHFFL 0x55 #define MODE_SCAN 0x00 uint8_t cd, tr, mode; unsigned long prevMillis = 0;
Functia urmatoare send_package() trimite un pachet de cate 8 octeti pe SPI, cu intarziere pe fiecare transfer:
void send_package(uint8_t c0, uint8_t c1, uint8_t c2, uint8_t c3, uint8_t c4, uint8_t c5, uint8_t c6, uint8_t c7) { uint8_t data[8] = {c0, c1, c2, c3, c4, c5, c6, c7}; for (int i = 0; i < 8; i++) { SPDR = data[i]; while (!(SPSR & (1 << SPIF))); delayMicroseconds(874); } }
Initializam SPI, dupa cum urmeaza, astfel incat sa ruleze cu o viteza de transfer de 62.5kHz
void spi_init() { DDRB |= (1 << PB3) | (1 << PB5); DDRB &= ~(1 << PB4); SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR1) | (1 << SPR0); SPSR &= ~(1 << SPI2X); }
In functia setup() initializam valorile initiale pentru disc, mod de redare, piesa, precum si interfata SPI. Dupa care trimitem o secventa de comenzi pentru load si idle
void setup() { cd = 1; tr = 1; mode = MODE_PLAY; #ifdef DEBUG Serial.begin(9600); #endif delay(1000); spi_init(); send_package(0x74, 0xBE, 0xFE, 0xFF, 0xFF, 0xFF, 0x8F, 0x7C); // idle delayMicroseconds(10000); send_package(0x34, 0xFF, 0xFE, 0xFE, 0xFE, 0xFF, 0xFA, 0x3C); // load disc delayMicroseconds(100000); send_package(0x74, 0xBE, 0xFE, 0xFF, 0xFF, 0xFF, 0x8F, 0x7C); // idle delayMicroseconds(10000); #ifdef DEBUG Serial.println("Sent idle/load/idle commands"); #endif }
In continuare tot pe Arduino, in functia loop() vom trimite la fiecare 50ms cate un pachet care contine discul, piesa si modul de redare pentru a mentine conexiunea activa cu casetofonul auto ales:
void loop() { if ((millis() - prevMillis) > 50) { send_package(0x34, 0xBF ^ cd, 0xFF ^ tr, 0xFF, 0xFF, mode, 0xCF, 0x3C); prevMillis = millis();
#ifdef DEBUG Serial.println("Sent packet"); #endif } }
In urmatoarele bucati de cod voi descrie cum am folosit un ESP32 pentru a deveni un receiver audio prin interfata Bluetooth a acestuia si sa redirectionez semnalul audio prin interfata I2S catre DAC extern (e.g. CS4344)
Definim in continuare pinii folositi pentru interfata I2S, butoanele de play/pause pe care le vom folosi, pinii SPI folositi pentru ecranul TFT, care va folosi biblioteca Adafruit_ST7735, precum si variabilele folosite pentru debounce:
#define I2S_BCLK 26 #define I2S_LRCK 25 #define I2S_DATA 22 #define I2S_MCLK 3 // sau I2S_PIN_NO_CHANGE
// === Butoane === #define BTN_PLAY 12 #define BTN_PAUSE 14 // redenumit din NEXT
// === Ecran ST7735 === #define TFT_CS 5 #define TFT_RST 4 #define TFT_DC 16
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); BluetoothA2DPSink a2dp_sink;
String current_song = "Fara melodie"; String last_displayed_song = "";
volatile bool playPressed = false; volatile bool pausePressed = false;
unsigned long lastDebouncePlay = 0; unsigned long lastDebouncePause = 0; const unsigned long debounceDelay = 150;
unsigned long lastPlayPressTime = 0; bool waitingForSecondClick = false;
Functia urmatoare metadata_callback() va fi folosita pentru trimiterea de text pe ecranul TFT:
void metadata_callback(uint8_t id, const uint8_t *text) { if (id == 0x01) { current_song = String((char*)text); } }
In functia update_display() vom scrie momentan numele melodiei curente:
void update_display() { if (current_song != last_displayed_song) { tft.fillScreen(ST77XX_BLACK); tft.setTextColor(ST77XX_WHITE); tft.setTextSize(1); tft.setCursor(2, 10); tft.println("Redare Bluetooth:"); tft.setCursor(2, 30); tft.setTextWrap(true); tft.setTextSize(1); tft.println(current_song); tft.setCursor(2, 60); tft.setTextSize(1); tft.println("Play=D12 | Pause=D14"); last_displayed_song = current_song; } }
Definim urmatoarele 2 functii care vor declansa intreruperile corespunzatoare cand unul din cele 2 butoane este selectat:
void IRAM_ATTR isr_play() { if ((millis() - lastDebouncePlay) > debounceDelay) { playPressed = true; lastDebouncePlay = millis(); } }
void IRAM_ATTR isr_pause() { if ((millis() - lastDebouncePause) > debounceDelay) { pausePressed = true; lastDebouncePause = millis(); } }
In functia setup() initializam interfata I2S cu un sample rate de 44.1 Mhz si un MCLK de 11.2896 Mhz, precum si pinii pe care ii folosim pentr DAC-ul CS4344. Mai apoi pornim receiver-ul Bluetooth A2DP si initializam o conexiune 'ESP32_Speaker'. In final setam butoanele D12 si D14 ca intrari cu rezistenta de pull-up interna, pe care atasam 2 intreruperi.
void setup() { Serial.begin(115200); i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = 44100, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = 0, .dma_buf_count = 8, .dma_buf_len = 64, .use_apll = true, .tx_desc_auto_clear = true, .fixed_mclk = 11289600 }; i2s_pin_config_t pin_config = { .mck_io_num = I2S_MCLK, .bck_io_num = 26, .ws_io_num = 25, .data_out_num = 22, .data_in_num = I2S_PIN_NO_CHANGE }; a2dp_sink.set_pin_config(pin_config); a2dp_sink.set_avrc_metadata_callback(metadata_callback); a2dp_sink.start("ESP32_Speaker"); tft.initR(INITR_BLACKTAB); tft.setRotation(1); update_display(); pinMode(BTN_PLAY, INPUT_PULLUP); pinMode(BTN_PAUSE, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(BTN_PLAY), isr_play, FALLING); attachInterrupt(digitalPinToInterrupt(BTN_PAUSE), isr_pause, FALLING); }
In functia loop() se executa continuu si verifica daca butoanele D12 si D14 sunt apasate, precum actualizeaza si ecranul TFT:
void loop() { unsigned long now = millis(); if (playPressed) { playPressed = false; if (waitingForSecondClick && (now - lastPlayPressTime < doubleClickDelay)) { a2dp_sink.next(); waitingForSecondClick = false; } else { lastPlayPressTime = now; waitingForSecondClick = true; } } if (waitingForSecondClick && (now - lastPlayPressTime > doubleClickDelay)) { a2dp_sink.play(); waitingForSecondClick = false; } if (pausePressed) { pausePressed = false; a2dp_sink.pause(); } update_display(); delay(50); }