This is an old revision of the document!
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
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) { SPI.beginTransaction(SPISettings(62500, MSBFIRST, SPI_MODE1)); SPI.transfer(c0); delayMicroseconds(874); SPI.transfer(c1); delayMicroseconds(874); SPI.transfer(c2); delayMicroseconds(874); SPI.transfer(c3); delayMicroseconds(874); SPI.transfer(c4); delayMicroseconds(874); SPI.transfer(c5); delayMicroseconds(874); SPI.transfer(c6); delayMicroseconds(874); SPI.transfer(c7); SPI.endTransaction(); }
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); // wait for radio to boot // Init SPI for Arduino Uno (uses default pins) SPI.begin(); // uses hardware SPI: SCK(13), MISO(12), MOSI(11) 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) { // disc trk min sec send_package(0x34, 0xBF ^ cd, 0xFF ^ tr, 0xFF, 0xFF, mode, 0xCF, 0x3C); prevMillis = millis(); // reset timer #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); }
Fişierele se încarcă pe wiki folosind facilitatea Add Images or other files. Namespace-ul în care se încarcă fişierele este de tipul :pm:prj20??:c? sau :pm:prj20??:c?:nume_student (dacă este cazul). Exemplu: Dumitru Alin, 331CC → :pm:prj2009:cc:dumitru_alin.