Управление громкостью многозонного усилителя при помощи приложения для Android и Arduino


    Прежде всего хочу поблагодарить 470 читателей проголосовавших за продолжение в статье про многозонный усилитель.

    Эксплуатация усилителя показала, что регулировать громкость не очень удобно находясь в помещении, которое может быть достаточно далеко находиться от кладовки где расположен усилитель с регуляторами громкости. Приходится несколько раз сбегать в кладовку и обратно добиваясь желаемого уровня громкости.

    Так родилась идея регулятора, который всегда с тобой в кармане, т.е. приложения для телефона которое может управлять усилителем через Wi-Fi сеть.

    Для реализации цифрового управления уровнем громкости усилителя механический потенциометр будет заменен электронным (DPOT – Digital Potentiometer). Среди не особо большого разнообразия доступных DPOT был выбран MCP41050 номиналом в 50 кОм что соответствует номиналу замещаемого механического аналога.

    Это одноканальный потенциометр, следовательно, на 1 стерео усилитель потребуется 2 штуки. Существуют также сдвоенные версии из этой-же серии (MCP42XXX), но мне технологически было удобнее использовать 2 раздельных. Рассмотрим вкратце как он работает.



    Аналоговая часть представлена выводами 5-7, вывод 6 (PW0) является движком (Wiper) потенциометра. Управление производится посредством SPI (Serial Peripheral Interface) (выводы 1-3). К выводам Vss и Vdd подводится питание 5V. Программирование чипа заключается в последовательной посылке Command Byte и Data Byte устанавливающего позицию движка потенциометра в позицию 0-255.

    Доработка усилителя.
    Как я рассказывал в предыдущей статье, я выбрал самый дешевый из готовых усилителей за $2.7 и мне его было не жалко курочить ради эксперимента. Для начала удаляем (аккуратно выпаиваем) механический потенциометр как показано на картинке:



    В освободившееся место будет установлен наш сдвоенный электронный регулятор.
    Сборка регулятора.
    Разрежем макетную плату вдоль а потом еще поперек на 3 части как показано на картинке:



    Если пару раз провести острым ножом вдоль отверстий, то плата легко ломается руками как печенье. После этого нужно напильником слегка подравнять края. Из получившихся кусков нам понадобятся 2 маленьких, они имеют размер примерно 1.5 x 2 см.
    Выводы 2-4, 8 чипов соединяются параллельно, поэтому удобно собрать обе платы в виде сэндвича:



    Для соединения управляющей цепи и питания с платой Arduino используем кусок кабеля-шлейфа. Цифровые линии управления при этом располагаем подальше от аналоговых цепей во избежание наводок.

    Собранный регулятор после предварительного тестирования впаиваем в усилитель:



    Как показало тестирование, добавление DPOT с цифровыми цепями управления во входные цепи усилителя не привело к появлению заметных на слух шумов или наводок.

    Принципиальная электрическая схема



    Программа для Arduino.
    За основу взят метод управления “SPI вручную” (“SPI by Hand”) описанный здесь little-scale.blogspot.it/2007/07/spi-by-hand.html. В нем существенны 2 функции.
    Функция spi_transfer побитно пересылает байт в чип.

    void spi_transfer(byte working)
    {
    for(int i = 1; i <= 8; i++)                          // Set up a loop of 8 iterations (8 bits in a byte)
    { if (working > 127) { digitalWrite (POT_MOSI,HIGH) ; }  // If the MSB is a 1 then set MOSI high
    else { digitalWrite (POT_MOSI, LOW) ; }                  // If the MSB is a 0 then set MOSI low
    digitalWrite (CLKdpot,HIGH) ;                        // Pulse the CLKdpot high
    working = working << 1 ;                             // Bit-shift the working byte
    digitalWrite(CLKdpot,LOW) ;                          // Pulse the CLKdpot low
    }
    }
    


    Функция spi_out посылает байты команды и данных в чип который выбран установкой в логический 0 линии CS.

    void spi_out(int CS, byte cmd_byte, byte data_byte)
    {
    digitalWrite (CS, LOW);       // Set the passed ChipSelect pin to low to start programming
      spi_transfer(cmd_byte);     // Send the passed COMMAND BYTE
      delay(2);
      spi_transfer(data_byte);    // Send the passed DATA BYTE
      delay(2);
    digitalWrite(CS, HIGH);       // Set the passed ChipSelect pin to high to end programming
    }
    


    Поскольку управление решено было реализовать по локальной сети, а не через Bluetooth, в схему замешаны Еthernet shield, Web server в стандартном включении. Забегая немного вперед нужно отметить что программа для телефона создавалась в MIT App Inventor для которого не существует реализации TCP клиента. Поэтому управление пришлось делать пересылкой команд в параметрах запроса GET.

    После выделения команд (param) и значений (value) из строки запроса они посылаются для управления нашими DPOT-ми:

    param = readString.substring(6,9);
    value = readString.substring(10,13).toInt();
    if (param=="V1L") {V1L=value; spi_out(CS1, cmd_byte, V1L);}
    if (param=="V1R") {V1R=value; spi_out(CS2, cmd_byte, V1R);}
    if (param=="MU1") {spi_out(CS1, cmd_byte, V1L/5); spi_out(CS2, cmd_byte, V1R/5);}
    if (param=="UM1") {spi_out(CS1, cmd_byte, V1L); spi_out(CS2, cmd_byte, V1R);}
    


    Команды V1L, V1R – установить уровень громкости первого левого/правого канала соответствующим значению value которое может быть равным 0 – 255.
    Команды MU1, UM1 – Mute, Unmute. Временное приглушение (исходный уровень /5) и возврат громкости к исходному значению.

    Скетч целиком

    #include <UIPEthernet.h>
    #include <String.h>
    int CS1 = 19; // Chip Select
    int CS2 = 18;
    int CS3 = 17;
    int CS4 = 16;
    int CS5 = 15;
    int CS6 = 14;
    int CS7 = 8;
    int CS8 = 7;
    int CLKdpot = 4; // Clock pin 4 arduino
    int POT_MOSI = 5; // MOSI pin 5 arduino
    byte cmd_byte = B00010011 ; // Command byte 'write' data to POT
    uint8_t POTposition1 = 10; //initialize DPOT set initial position
    uint8_t POTposition2 = 10;
    uint8_t POTposition3 = 10; 
    uint8_t POTposition4 = 10;
    uint8_t POTposition5 = 10; 
    uint8_t POTposition6 = 10;
    uint8_t POTposition7 = 10; 
    uint8_t POTposition8 = 10;
    uint8_t mac[6] = {0x00,0x01,0x02,0x03,0x04,0x05};
    uint8_t ip[4] = {192, 168, 6, 25}; // IP address for the webserver
    uint16_t port = 80; // Use port 80 - the standard for HTTP
    EthernetServer server(80);
    String readString = String(100);
    String param = String(3); 
    int value = 0;
    int V1L = 0;
    int V1R = 0;
    int V2L = 0;
    int V2R = 0;
    int V3L = 0;
    int V3R = 0;
    int V4L = 0;
    int V4R = 0;
    
    void spi_transfer(byte working)
    {
     for(int i = 1; i <= 8; i++)                          // Set up a loop of 8 iterations (8 bits in a byte)
      { if (working > 127) { digitalWrite (POT_MOSI,HIGH) ; }  // If the MSB is a 1 then set MOSI high
        else { digitalWrite (POT_MOSI, LOW) ; }                  // If the MSB is a 0 then set MOSI low
     digitalWrite (CLKdpot,HIGH) ;                        // Pulse the CLKdpot high
     working = working << 1 ;                             // Bit-shift the working byte
     digitalWrite(CLKdpot,LOW) ;                          // Pulse the CLKdpot low
    }
    }
    
    void spi_out(int CS, byte cmd_byte, byte data_byte)
    {
     digitalWrite (CS, LOW);       // Set the passed ChipSelect pin to low to start programming
     spi_transfer(cmd_byte);     // Send the passed COMMAND BYTE
     delay(2);
     spi_transfer(data_byte);    // Send the passed DATA BYTE
     delay(2);
     digitalWrite(CS, HIGH);       // Set the passed ChipSelect pin to high to end programming
    }
    
    void setup() {
      Serial.begin(9600);
      pinMode (CS1, OUTPUT);
      pinMode (CS2, OUTPUT);
      pinMode (CS3, OUTPUT);
      pinMode (CS4, OUTPUT);
      pinMode (CS5, OUTPUT);
      pinMode (CS6, OUTPUT);
      pinMode (CS7, OUTPUT);
      pinMode (CS8, OUTPUT);
      pinMode (CLKdpot, OUTPUT);
      pinMode (POT_MOSI, OUTPUT);
      spi_out(CS1, cmd_byte, POTposition1);
      spi_out(CS2, cmd_byte, POTposition2);
      spi_out(CS3, cmd_byte, POTposition3);
      spi_out(CS4, cmd_byte, POTposition4);
      spi_out(CS5, cmd_byte, POTposition5);
      spi_out(CS6, cmd_byte, POTposition6);
      spi_out(CS7, cmd_byte, POTposition7);
      spi_out(CS8, cmd_byte, POTposition8);
    
      // start the Ethernet connection and the server:
      Ethernet.begin(mac, ip);
      server.begin();
      Serial.print("server is at ");
      Serial.println(Ethernet.localIP());
    }
    
    void loop() {  // listen for incoming clients
      readString="";
      EthernetClient client = server.available();
      if (client) {
        Serial.println("new client");
        // an http request ends with a blank line
        boolean currentLineIsBlank = true;
        while (client.connected()) {
          if (client.available()) {
            char c = client.read();
            size_t pos = 0;
            if (readString.length() < 16) {
            //store characters to string
            readString +=c;
            } 
            // if you've gotten to the end of the line (received a newline
            // character) and the line is blank, the http request has ended,
            // so you can send a reply
            if (c == '\n' && currentLineIsBlank) {
              // send a standard http response header
              client.println("HTTP/1.1 200 OK");
              client.println("Content-Type: text/html");
              client.println("Connection: close");
              client.println();
              client.println("<!DOCTYPE HTML>");
              client.println("<html>");
              client.println("</html>");
              break;
            }
            if (c == '\n') {
              // you're starting a new line
              currentLineIsBlank = true;
            }
            else if (c != '\r') {
              // you've gotten a character on the current line
              currentLineIsBlank = false;
            }
          }
        }
        // give the web browser time to receive the data
        delay(1);
        // close the connection:
        client.stop();
        Serial.println("client disconnected");
        Serial.println(readString);
        param = readString.substring(6,9);
        value = readString.substring(10,13).toInt();
        Serial.println(param);
        Serial.println(value);
        
        if (param=="V1L") {V1L=value; spi_out(CS1, cmd_byte, V1L);}
        if (param=="V1R") {V1R=value; spi_out(CS2, cmd_byte, V1R);}
        if (param=="V2L") {V2L=value; spi_out(CS3, cmd_byte, V2L);}
        if (param=="V2R") {V2R=value; spi_out(CS4, cmd_byte, V2R);}
        if (param=="V3L") {V3L=value; spi_out(CS5, cmd_byte, V3L);}
        if (param=="V3R") {V3R=value; spi_out(CS6, cmd_byte, V3R);}
        if (param=="V4L") {V4L=value; spi_out(CS7, cmd_byte, V4L);}
        if (param=="V4R") {V4R=value; spi_out(CS8, cmd_byte, V4R);}
    
        if (param=="MU1") {
           spi_out(CS1, cmd_byte, V1L/5); spi_out(CS2, cmd_byte, V1R/5);
           spi_out(CS3, cmd_byte, V2L/5); spi_out(CS4, cmd_byte, V2R/5);
           spi_out(CS5, cmd_byte, V3L/5); spi_out(CS6, cmd_byte, V3R/5);
           spi_out(CS7, cmd_byte, V4L/5); spi_out(CS8, cmd_byte, V4R/5);
          }
        if (param=="UM1") {
           spi_out(CS1, cmd_byte, V1L); spi_out(CS2, cmd_byte, V1R);
           spi_out(CS3, cmd_byte, V2L); spi_out(CS4, cmd_byte, V2R);
           spi_out(CS5, cmd_byte, V3L); spi_out(CS6, cmd_byte, V3R);
           spi_out(CS7, cmd_byte, V4L); spi_out(CS8, cmd_byte, V4R);
          }
      }
    }
    



    Приложение “Volume Control” для Android.
    Приложение создано при помощи инструмента MIT App Inventor. Приложение имеет 2 экрана: основной экран и экран установок. Основной экран включает 4 идентичные секции, по одной на зону. Экран установок содержит контролы для установки URL соответствующему IP адресу Arduino, а также названия зон.



    Немного деталей, поясняющих работу программы.
    Значения установок и позиции регуляторов громкости сохраняются в TinyDB и используются при инициализации приложения при его открытии. Пример сохранения значения уровня левого канала первой зоны при закрытии приложения:



    Как уже сказано было выше, используется компонент WebViewer для посылки команд методом Get в составе запроса к веб серверу, запущенному на Arduino.
    Посылка команд как часто повторяющаяся операция выделена в процедуру SendCommand.



    К примеру, при изменении позиции регулятора левого канала первой зоны она будет вызвана так:



    При этом будет послан запрос вида http://192.168.6.25/?V1L=156
    Если приложение запущено на смартфоне, то звук можно автоматически приглушить при ответе на звонок и восстановить при его окончании:



    При нажатии на кнопку “Mute” вызывается процедура Mute которая в свою очередь вызывает SendCommand и меняет цвет и название кнопки:



    Файл проекта для App Inventor 2 вышлю желающим по запросу.

    В заключение, привожу видео демонстрирующее работу приложения. Задержка с переключением экрана связана с тем что приложение запущено в MIT AI2 Companion.

    • +20
    • 25,1k
    • 4
    Поделиться публикацией

    Похожие публикации

    Комментарии 4

      +1
      Хорошо, в идеале вообще обойтись только esp8266 ====SPI===> MCP42XXX + питание. В esp8266 минимальная реализация http для GET запросов (on/off). Profit.
        0
        Вероятно еще понадобится port expander (например MCP23S17). Попробую сделать в следующей версии.
        0
        Update: добавил принципиальную эл. схему.
          0
          Кто может подсказать как отключить автоматические снижение громкости до 20%, андройд лалипап такой заботливый, но дико раздражает примерно раз в неделю он делает это против моей воли

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое