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);
}