530 lines
18 KiB
C++
530 lines
18 KiB
C++
// по материалам:
|
||
//https://www.instructables.com/Simple-LCD-MENU-Using-Arduino/
|
||
// https://alexgyver.ru/lessons/
|
||
// https://www.hackster.io/mircemk/diy-arduino-vfd-display-20x2-vu-volume-unit-meter-37898f
|
||
// https://github.com/AlexGyver/FHTSpectrumAnalyzer/blob/master/Firmware/spertrum1602/spertrum1602.ino
|
||
|
||
#include <EEPROM.h>
|
||
#include <Wire.h>
|
||
|
||
|
||
#define GAIN 5 // усиление 0...50
|
||
#define STEP 20 // плавность полос 0...20
|
||
#define RL 1 // RL - горизонт, вертикаль 0...1
|
||
// Выходы для управления реле (вкл.вкл ЦАП/Блютуз)
|
||
#define RELAY_POWER_USB 7
|
||
#define RELAY_POWER_BT 6
|
||
//#define RELAY_POWER_MP3 10
|
||
#define RELAY_OUT_USB 8
|
||
#define RELAY_OUT_BT 9
|
||
|
||
// Индикаторы включения (подключения) входов
|
||
#define OPTOCOUPLE_USB 10
|
||
#define OPTOCOUPLE_COAX 11
|
||
#define OPTOCOUPLE_BT 12
|
||
// Входы звукового сигнала для анализатора спектра
|
||
#define AUDIO_IN_L 0
|
||
#define AUDIO_IN_R 1
|
||
|
||
|
||
// Кнопки
|
||
#define BTN_MENU 2 // Кнопка меню
|
||
#define BTN_EXECUTE 3 // Кнопка подтверждения
|
||
#define INIT_ADDR 1023 // номер резервной ячейки
|
||
#define INIT_KEY 1 // ключ первого запуска
|
||
|
||
// ---------------- НАСТРОЙКИ ----------------
|
||
#define DRIVER_VERSION 0 // 0 - маркировка драйвера кончается на 4АТ, 1 - на 4Т
|
||
#define GAIN_CONTROL 0 // ручная настройка потенциометром на громкость (1 - вкл, 0 - выкл)
|
||
|
||
#define AUTO_GAIN 0 // автонастройка по громкости (экспериментальная функция)
|
||
#define VOL_THR 30 // порог тишины (ниже него отображения на матрице не будет)
|
||
|
||
#define LOW_PASS 30 // нижний порог чувствительности шумов (нет скачков при отсутствии звука)
|
||
#define DEF_GAIN 80 // максимальный порог по умолчанию (при GAIN_CONTROL игнорируется)
|
||
|
||
#define FHT_N 256 // ширина спектра х2
|
||
// вручную забитый массив тонов, сначала плавно, потом круче
|
||
byte posOffset[16] = {2, 3, 4, 6, 8, 10, 12, 14, 16, 20, 25, 30, 35, 60, 80, 100};
|
||
// ---------------- НАСТРОЙКИ ----------------
|
||
|
||
// --------------- БИБЛИОТЕКИ ---------------
|
||
#define LOG_OUT 1
|
||
#include <FHT.h> // преобразование Хартли
|
||
#include <Wire.h>
|
||
#include <LiquidCrystal_I2C.h>
|
||
#define printByte(args) write(args);
|
||
double prevVolts = 100.0;
|
||
// --------------- БИБЛИОТЕКИ ---------------
|
||
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
|
||
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
|
||
|
||
|
||
// Адрес PT2257 (0x44 в 7-битном формате, или 0x88 в 8-битном)
|
||
#define PT2257_ADDRESS 0x44
|
||
|
||
// LiquidCrystal lcdVuMeter(2, 3, 4, 5, 6, 7);// RS,E,D4,D5,D6,D7
|
||
LiquidCrystal_I2C lcdVuMeter(0x27, 16, 2);
|
||
LiquidCrystal_I2C lcdService(0x26, 16, 2);
|
||
|
||
bool usb, bluetooth, mp3;
|
||
int menu = 1;
|
||
bool menuShow = false;
|
||
int att_coax = 0;
|
||
int att_spdif = 0;
|
||
int att_bt = 0;
|
||
|
||
// ------------------------------------- ПОЛОСОЧКИ -------------------------------------
|
||
byte v1[8] = {0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111};
|
||
byte v2[8] = {0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111, 0b11111};
|
||
byte v3[8] = {0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111, 0b11111, 0b11111};
|
||
byte v4[8] = {0b00000, 0b00000, 0b00000, 0b00000, 0b11111, 0b11111, 0b11111, 0b11111};
|
||
byte v5[8] = {0b00000, 0b00000, 0b00000, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111};
|
||
byte v6[8] = {0b00000, 0b00000, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111};
|
||
byte v7[8] = {0b00000, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111};
|
||
byte v8[8] = {0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111, 0b11111};
|
||
// ------------------------------------- ПОЛОСОЧКИ -------------------------------------
|
||
byte gain = DEF_GAIN; // усиление по умолчанию
|
||
unsigned long gainTimer;
|
||
byte maxValue, maxValue_f;
|
||
float k = 0.1;
|
||
|
||
void setup() {
|
||
Wire.begin();
|
||
// Инициализация PT2257 - сброс и включение
|
||
writePT2257(0xFF); // Сброс
|
||
delay(100);
|
||
writePT2257(0xCE); // Включение (0xCF для выключения)
|
||
setVolume(79);
|
||
|
||
lcdVuMeter.init();
|
||
lcdVuMeter.backlight();
|
||
lcdVuMeter.clear();
|
||
lcdService.init();
|
||
lcdService.backlight();
|
||
lcdService.clear();
|
||
// lcdService.setCursor(0, 0);
|
||
// lcdService.print("Menu");
|
||
|
||
// Кнопка управления (переключения)
|
||
pinMode(BTN_MENU, INPUT_PULLUP);
|
||
pinMode(BTN_EXECUTE, INPUT_PULLUP);
|
||
// Выходы для управления реле (вкл.вкл ЦАП/Блютуз)
|
||
pinMode(RELAY_POWER_USB, OUTPUT);
|
||
pinMode(RELAY_POWER_BT, OUTPUT);
|
||
// pinMode(RELAY_POWER_MP3, OUTPUT);
|
||
pinMode(RELAY_OUT_USB, OUTPUT);
|
||
pinMode(RELAY_OUT_BT, OUTPUT);
|
||
// pinMode(RELAY_OUT_MP3_LEFT, OUTPUT);
|
||
// pinMode(RELAY_OUT_MP3_RIGHT, OUTPUT);
|
||
|
||
// Входы индикаторов с USB ЦАП
|
||
pinMode(OPTOCOUPLE_USB, INPUT_PULLUP);
|
||
pinMode(OPTOCOUPLE_COAX, INPUT_PULLUP);
|
||
// Вход индикатора с BlueTooth адаптера
|
||
pinMode(OPTOCOUPLE_BT, INPUT_PULLUP);
|
||
|
||
// lcdVuMeter.createChar(5,znak_l);
|
||
// lcdVuMeter.begin(16, 2);// lcdVuMeter 16X2
|
||
// analogReference(INTERNAL); // если очень маленький уровень сигнала
|
||
pinMode(A0,INPUT);// A0 - аналоговый вход R
|
||
pinMode(A1,INPUT);// A1 - аналоговый вход L
|
||
|
||
// первый запуск и установка значений переменных в память
|
||
if (EEPROM.read(INIT_ADDR) != INIT_KEY) {
|
||
EEPROM.write(INIT_ADDR, INIT_KEY);
|
||
usb = true;
|
||
bluetooth = false;
|
||
mp3 = false;
|
||
att_coax = 0; // Минимальная громкость
|
||
att_spdif = 0; // Минимальная громкость
|
||
att_bt = 0; // Минимальная громкость
|
||
|
||
EEPROM.put(10, usb);
|
||
EEPROM.put(12, bluetooth);
|
||
EEPROM.put(16, att_coax);
|
||
EEPROM.put(18, att_spdif);
|
||
EEPROM.put(20, att_bt);
|
||
} else {
|
||
usb = EEPROM.read(10);
|
||
bluetooth = EEPROM.read(12);
|
||
// mp3 = EEPROM.read(14);
|
||
att_coax = EEPROM.read(16);
|
||
att_spdif = EEPROM.read(18);
|
||
att_bt = EEPROM.read(20);
|
||
}
|
||
|
||
// поднимаем частоту опроса аналогового порта до 38.4 кГц, по теореме
|
||
// Котельникова (Найквиста) частота дискретизации будет 19 кГц
|
||
// http://yaab-arduino.blogspot.ru/2015/02/fast-sampling-from-analog-input.html
|
||
sbi(ADCSRA, ADPS2);
|
||
cbi(ADCSRA, ADPS1);
|
||
sbi(ADCSRA, ADPS0);
|
||
|
||
// для увеличения точности уменьшаем опорное напряжение,
|
||
// выставив EXTERNAL и подключив Aref к выходу 3.3V на плате через делитель
|
||
// GND ---[2х10 кОм] --- REF --- [10 кОм] --- 3V3
|
||
analogReference(EXTERNAL);
|
||
|
||
|
||
if (usb) {
|
||
digitalWrite(RELAY_POWER_BT,HIGH);
|
||
digitalWrite(RELAY_POWER_USB,HIGH);
|
||
delay(500);
|
||
digitalWrite(RELAY_OUT_USB,HIGH);
|
||
digitalWrite(RELAY_OUT_BT,LOW);
|
||
applyCurrentVolume(); // Применить громкость после инициализации
|
||
}
|
||
|
||
if (bluetooth) {
|
||
digitalWrite(RELAY_POWER_USB,HIGH);
|
||
delay(500);
|
||
digitalWrite(RELAY_OUT_USB,LOW);
|
||
digitalWrite(RELAY_OUT_BT,HIGH);
|
||
digitalWrite(RELAY_POWER_BT,LOW);
|
||
applyCurrentVolume(); // Применить громкость после инициализации
|
||
}
|
||
if (mp3) {
|
||
}
|
||
lcdChars(); // подхватить коды полосочек
|
||
updateMenu();
|
||
}
|
||
|
||
void lcdChars() {
|
||
lcdVuMeter.createChar(0, v1);
|
||
lcdVuMeter.createChar(1, v2);
|
||
lcdVuMeter.createChar(2, v3);
|
||
lcdVuMeter.createChar(3, v4);
|
||
lcdVuMeter.createChar(4, v5);
|
||
lcdVuMeter.createChar(5, v6);
|
||
lcdVuMeter.createChar(6, v7);
|
||
lcdVuMeter.createChar(7, v8);
|
||
}
|
||
|
||
// Получаем тип подключенного входа к USB ЦАПу
|
||
String setUSBinput() {
|
||
if (!digitalRead(OPTOCOUPLE_USB) && usb) {
|
||
return "input: USB ";
|
||
} else if (!digitalRead(OPTOCOUPLE_COAX) && usb) {
|
||
return "input: Coaxial";
|
||
} else {
|
||
return "input: Off ";
|
||
}
|
||
}
|
||
// Получаем статус подключения устройства к bluetooth
|
||
String setBTstatus() {
|
||
if (!digitalRead(OPTOCOUPLE_BT) && bluetooth) {
|
||
return " connected";
|
||
} else {
|
||
return " ";
|
||
}
|
||
}
|
||
|
||
|
||
void updateMenu() {
|
||
switch (menu) {
|
||
case 0:
|
||
menu = 1;
|
||
break;
|
||
case 1:
|
||
lcdService.clear();
|
||
lcdService.print("> USB DAC");
|
||
lcdService.setCursor(0, 1);
|
||
if(usb){
|
||
lcdService.print("On Vol: ");
|
||
lcdService.print(getVolumeLabel(usb ? (att_spdif) : 0));
|
||
}else{
|
||
lcdService.print("Off");
|
||
}
|
||
break;
|
||
case 2:
|
||
lcdService.clear();
|
||
lcdService.print("> Bluetooth");
|
||
lcdService.setCursor(0, 1);
|
||
if(bluetooth){
|
||
lcdService.print("On Vol: ");
|
||
lcdService.print(getVolumeLabel(bluetooth ? att_bt : 0));
|
||
}else{
|
||
lcdService.print("Off");
|
||
}
|
||
break;
|
||
case 4:
|
||
lcdService.clear();
|
||
lcdService.print("> Att S/PDIF");
|
||
lcdService.setCursor(0, 1);
|
||
lcdService.print("Level: ");
|
||
lcdService.print(getVolumeLabel(att_spdif));
|
||
break;
|
||
case 5:
|
||
lcdService.clear();
|
||
lcdService.print("> Att Coax");
|
||
lcdService.setCursor(0, 1);
|
||
lcdService.print("Level: ");
|
||
lcdService.print(getVolumeLabel(att_coax));
|
||
break;
|
||
case 6:
|
||
lcdService.clear();
|
||
lcdService.print("> Att BlueTooth");
|
||
lcdService.setCursor(0, 1);
|
||
lcdService.print("Level: ");
|
||
lcdService.print(getVolumeLabel(att_bt));
|
||
break;
|
||
case 7:
|
||
menu = 0;
|
||
menuShow = false;
|
||
lcdService.clear();
|
||
break;
|
||
}
|
||
}
|
||
|
||
String getVolumeLabel(int level) {
|
||
switch(level) {
|
||
case 0: return "Max";
|
||
case 1: return "Med";
|
||
case 2: return "Min";
|
||
default: return "Err";
|
||
}
|
||
}
|
||
int attenuationVolume(int i) {
|
||
// Циклическое переключение между 0,1,2
|
||
return (i + 1) % 3;
|
||
}
|
||
int convertAttToVolume(int att) {
|
||
// PT2257: 0 = максимум громкости, 79 = минимум громкости
|
||
// Нам нужно: 0 = максимум, 1 = среднее, 2 = минимум
|
||
switch(att) {
|
||
case 0: return 0; // Максимальная громкость (0 dB)
|
||
case 1: return 1; // Средняя громкость (40 dB)
|
||
case 2: return 2; // Минимальная громкость (79 dB)
|
||
default: return 0;
|
||
}
|
||
}
|
||
void executeMenuAction() {
|
||
switch (menu) {
|
||
case 1:
|
||
actionUSB();
|
||
// При переключении на USB установите соответствующую громкость
|
||
applyCurrentVolume();
|
||
break;
|
||
case 2:
|
||
actionBT();
|
||
// При переключении на Bluetooth установите соответствующую громкость
|
||
applyCurrentVolume();
|
||
break;
|
||
case 4:
|
||
att_spdif = attenuationVolume(att_spdif);
|
||
EEPROM.put(18, att_spdif);
|
||
if (usb && !digitalRead(OPTOCOUPLE_USB)) {
|
||
setVolume(convertAttToVolume(att_spdif));
|
||
}
|
||
break;
|
||
case 5:
|
||
att_coax = attenuationVolume(att_coax);
|
||
EEPROM.put(16, att_coax);
|
||
if (usb && !digitalRead(OPTOCOUPLE_COAX)) {
|
||
setVolume(convertAttToVolume(att_coax));
|
||
}
|
||
break;
|
||
case 6:
|
||
att_bt = attenuationVolume(att_bt);
|
||
EEPROM.put(20, att_bt);
|
||
if (bluetooth) {
|
||
setVolume(convertAttToVolume(att_bt));
|
||
}
|
||
break;
|
||
}
|
||
EEPROM.put(10, usb);
|
||
EEPROM.put(12, bluetooth);
|
||
updateMenu(); // Обновляем отображение меню
|
||
}
|
||
|
||
void applyCurrentVolume() {
|
||
if (usb) {
|
||
if (!digitalRead(OPTOCOUPLE_USB)) {
|
||
setVolume(convertAttToVolume(att_spdif));
|
||
} else if (!digitalRead(OPTOCOUPLE_COAX)) {
|
||
setVolume(convertAttToVolume(att_coax));
|
||
}
|
||
} else if (bluetooth) {
|
||
setVolume(convertAttToVolume(att_bt));
|
||
}
|
||
}
|
||
void actionUSB() {
|
||
// lcdService.clear();
|
||
lcdService.setCursor(2,1);
|
||
lcdService.print("On ");
|
||
digitalWrite(RELAY_POWER_BT,HIGH);
|
||
// digitalWrite(RELAY_POWER_MP3,HIGH);
|
||
digitalWrite(RELAY_POWER_USB,HIGH);
|
||
delay(500);
|
||
digitalWrite(RELAY_OUT_USB,HIGH);
|
||
digitalWrite(RELAY_OUT_BT,LOW);
|
||
// digitalWrite(RELAY_OUT_MP3_LEFT,HIGH);
|
||
// digitalWrite(RELAY_OUT_MP3_RIGHT,HIGH);
|
||
usb = true;
|
||
bluetooth = false;
|
||
mp3 = false;
|
||
|
||
// delay(1500);
|
||
}
|
||
|
||
void actionBT() {
|
||
// lcdService.clear();
|
||
lcdService.setCursor(2,1);
|
||
lcdService.print("On ");
|
||
digitalWrite(RELAY_POWER_USB,HIGH);
|
||
// digitalWrite(RELAY_POWER_MP3,HIGH);
|
||
delay(500);
|
||
// digitalWrite(RELAY_OUT_MP3_LEFT,HIGH);
|
||
// digitalWrite(RELAY_OUT_MP3_RIGHT,HIGH);
|
||
delay(1000);
|
||
digitalWrite(RELAY_OUT_USB,LOW);
|
||
digitalWrite(RELAY_OUT_BT,HIGH);
|
||
digitalWrite(RELAY_POWER_BT,LOW);
|
||
usb = false;
|
||
bluetooth = true;
|
||
mp3 = false;
|
||
|
||
// delay(1500);
|
||
}
|
||
|
||
|
||
///================== Аттенюатор ===========================
|
||
|
||
void writePT2257(byte command) {
|
||
Wire.beginTransmission(PT2257_ADDRESS);
|
||
Wire.write(command);
|
||
Wire.endTransmission();
|
||
}
|
||
|
||
void setVolume(int volume) {
|
||
// PT2257 принимает ослабление от 0 до 79 dB
|
||
volume = constrain(volume, 0, 79);
|
||
|
||
// Разделяем на десятки и единицы
|
||
int tens = volume / 10;
|
||
int units = volume % 10;
|
||
|
||
// Устанавливаем громкость для левого канала
|
||
writePT2257(0xD0 + tens); // Десятки для левого канала
|
||
writePT2257(0xE0 + units); // Единицы для левого канала
|
||
|
||
// Устанавливаем громкость для правого канала
|
||
writePT2257(0x50 + tens); // Десятки для правого канала
|
||
writePT2257(0x60 + units); // Единицы для правого канала
|
||
}
|
||
|
||
void setVolumeLeft(int volume) {
|
||
volume = constrain(volume, 0, 79);
|
||
int tens = volume / 10;
|
||
int units = volume % 10;
|
||
|
||
writePT2257(0xD0 + tens);
|
||
writePT2257(0xE0 + units);
|
||
}
|
||
|
||
void setVolumeRight(int volume) {
|
||
volume = constrain(volume, 0, 79);
|
||
int tens = volume / 10;
|
||
int units = volume % 10;
|
||
|
||
writePT2257(0x50 + tens);
|
||
writePT2257(0x60 + units);
|
||
}
|
||
|
||
void mute(bool muteOn) {
|
||
writePT2257(muteOn ? 0x7D : 0x7E); // Mute/unmute
|
||
}
|
||
|
||
///=============================================================
|
||
|
||
uint32_t btnTimer = 0;
|
||
|
||
void loop() {
|
||
lcdService.setCursor(0,0);
|
||
// Выводим на экран информацию какой модуль включен
|
||
|
||
if(!menuShow){
|
||
if(usb){
|
||
lcdService.print("USB | S/PDIF");
|
||
lcdService.setCursor(0,1);
|
||
lcdService.print(setUSBinput());
|
||
// actionUSB();
|
||
}
|
||
if(bluetooth){
|
||
lcdService.print("Bluetooth");
|
||
lcdService.setCursor(0,1);
|
||
lcdService.print(setBTstatus());
|
||
// actionBT();
|
||
}
|
||
// if(mp3){
|
||
// lcdService.print("MP3 module");
|
||
//// actionMP3();
|
||
// }
|
||
}
|
||
// bool btnMenuState = digitalRead(BTN_MENU);
|
||
// bool btnExecuteState = digitalRead(BTN_EXECUTE);
|
||
|
||
if (!digitalRead(BTN_MENU)){
|
||
menuShow = true;
|
||
menu ++;
|
||
updateMenu();
|
||
delay(100);
|
||
while (!digitalRead(BTN_MENU));
|
||
}
|
||
if (!digitalRead(BTN_EXECUTE)){
|
||
executeMenuAction();
|
||
updateMenu();
|
||
delay(100);
|
||
while (!digitalRead(BTN_EXECUTE));
|
||
}
|
||
|
||
// if (GAIN_CONTROL) gain = map(analogRead(POT_PIN), 0, 1023, 0, 150);
|
||
|
||
analyzeAudio(); // функция FHT, забивает массив fht_log_out[] величинами по спектру
|
||
|
||
for (int pos = 0; pos < 16; pos++) { // для окошек дисплея с 0 по 15
|
||
// найти максимум из пачки тонов
|
||
if (fht_log_out[posOffset[pos]] > maxValue) maxValue = fht_log_out[posOffset[pos]];
|
||
|
||
lcdVuMeter.setCursor(pos, 0);
|
||
|
||
// преобразовать значение величины спектра в диапазон 0..15 с учётом настроек
|
||
int posLevel = map(fht_log_out[posOffset[pos]], LOW_PASS, gain, 0, 15);
|
||
posLevel = constrain(posLevel, 0, 15);
|
||
|
||
if (posLevel > 7) { // если значение больше 7 (значит нижний квадратик будет полный)
|
||
lcdVuMeter.printByte(posLevel - 8); // верхний квадратик залить тем что осталось
|
||
lcdVuMeter.setCursor(pos, 1); // перейти на нижний квадратик
|
||
lcdVuMeter.printByte(7); // залить его полностью
|
||
} else { // если значение меньше 8
|
||
lcdVuMeter.print(" "); // верхний квадратик пустой
|
||
lcdVuMeter.setCursor(pos, 1); // нижний квадратик
|
||
lcdVuMeter.printByte(posLevel); // залить полосками
|
||
}
|
||
}
|
||
|
||
if (AUTO_GAIN) {
|
||
maxValue_f = maxValue * k + maxValue_f * (1 - k);
|
||
if (millis() - gainTimer > 1500) { // каждые 1500 мс
|
||
// если максимальное значение больше порога, взять его как максимум для отображения
|
||
if (maxValue_f > VOL_THR) gain = maxValue_f;
|
||
|
||
// если нет, то взять порог побольше, чтобы шумы вообще не проходили
|
||
else gain = 100;
|
||
gainTimer = millis();
|
||
}
|
||
}
|
||
// setVolume(2);
|
||
}
|
||
void analyzeAudio() {
|
||
for (int i = 0 ; i < FHT_N ; i++) {
|
||
int sample = (analogRead(AUDIO_IN_L) + analogRead(AUDIO_IN_R)) / 2;
|
||
fht_input[i] = sample; // put real data into bins
|
||
}
|
||
fht_window(); // window the data for better frequency response
|
||
fht_reorder(); // reorder the data before doing the fht
|
||
fht_run(); // process the data in the fht
|
||
fht_mag_log(); // take the output of the fht
|
||
}
|