Воспроизведение звука: D + FPGA = ?

В этой статье мы расскажем о том, как можно самым нестандартным образом воспроизвести звуковой файл с компьютера без помощи звуковой карты и колонок. То, что будет описано ниже возможно благодаря исключительно удачному союзу современной цифровой схемотехники и компьютерных технологий, и вы вполне можете все это повторить своими собственными руками при наличии некоторого количества свободного времени.

Если вас заинтересовало что мы придумали на этот раз, то добро пожаловать в эту статью.

Для того, чтобы провести эксперимент с воспроизведением звука с помощью ультрасовременных технологий нам потребуются:

  • Персональный компьютер;
  • Незанятый USB-порт, которого не жалко на эксперименты;
  • Программируемая логическая интегральная схема (ПЛИС) производства фирмы Lattice Semiconductor серии iCE40;
  • Компилятор D с установленным менеджером пакетов dub;
  • Библиотека из реестра dub под названием serialport;
  • Библиотека QtE5
  • Звуковой редактор Audacity
  • Интегрированная среда разработки под ПЛИС серии iCE40 — Icestudio;
  • Подключение к интернету;
  • Набор проводков и макетная плата;
  • Конденсатор на 4700 пФ и резистор на 3.3 кОм
  • Наушники или динамик

Первое, что необходимо для экспериментов — это плата программируемой логики (плата ПЛИС или по-английски FPGA) от компании Lattice Semiconductor. Некоторым людям такой выбор FPGA покажется странным, поскольку им известно, что существуют два крупных производителя микросхем программируемой логики — Xilinx и Altera, а предпочтение отданное малоизвестной фирме выглядит неправильным.

Объясню этот момент: дело в том, что две вышеупомянутые фирмы поделили между собой рынок ПЛИС и производят платы высокого качества, но вместе с тем и с очень высокой ценой. Также, производители плат выпускают еще и свои средства разработки, которые включают в себя не только отладочные наборы, но и целые интегрированные системы разработки. Из-за того, что фирмы выпускают практически все что нужно разработчику, в конкуренции практически нет, то как вы понимаете, с конечного пользователя снимут денег по максимуму, а удобства клиент так и не дождется — платы стоят очень дорого даже при очень скромных параметрах, а среды разработки занимают много дискового пространства, не бесплатны и не отличаются удобством.

Таким образом, нетрудно придти к выводу, что разработчик попадает в полную зависимость от производителя, а открытых средств разработки для плат от Xilinx или Altera нет и не предвидится, и да, платы начального уровня тоже дорогие. Кроме того, есть еще некоторая проблема — недоступность ПЛИС в радиотехнических магазинах, так любезно снабжающих нас сырьем для опытов, вследствии чего пришлось искать иные варианты относительно ПЛИС…

Тем не менее, абсолютно случайно я наткнулся на занятную статью от блога «Записки программиста», которая была посвящена знакомству с практически полностью открытым программным обеспечением разработки под FPGA, который называется iceStorm и рассчитан на ПЛИС серии iCE40 от фирмы Lattice Semiconductor. В рамках этой статьи, автор блога описывал свой опыт знакомства с интересной отладочной платой iCEStick и инструментарием iCEStorm, и тем самым надежно привлек мое внимание к разработке на ПЛИС…

Благодаря Ali Express я приобрел отладочный комплект iCEStick, который выглядит как обычная флешка без пластиковой оболочки, разве что чуть побольше размером:

Как видите, плата очень маленькая, но с очень большими возможностями:

  • Высокопроизводительный и малопотребляющий чип iCE40HX1K с 1280 логическими ячейками;
  • USB-устройство на базе FTDI 2232H, которое может быть использовано и для программирования чипа FPGA и для реализации интерфейса UART;
  • ИК-приемник/передатчик Vishay TFDU4101;
  • 5 пользовательских светодиодов;
  • Разъем PMOD для подключения периферийных модулей;
  • 12 МГц MEMS-осциллятор от Discera;
  • 32 МБит SPI Flash-память N25Q32 от Micron;
  • 16 цифровых пинов ввода/вывода (все поддерживают TTL уровень 3.3 В)
  • USB-разъем, облегчающий программирование ПЛИС и ее связь с компьютером.

Далее, я установил джентльменский набор программного обеспечения для разработки: yosys (программа для синтеза из исходных текстов на языках описания аппаратуры), arachne-pnr (программа для выполнения «разведения» полученного дизайна) и icestorm (комплект утилит для прошивки и настройки ПЛИС).

Поскольку работаю я с Linux, то пришлось несколько повозиться с установкой и выполнить ее из командной строки:

yaourt -S yosys-git arachne-pnr-git icestorm-git iverilog-git gtkwave

Установка такого набора, согласно некоторым источникам, не является необходимой и сейчас вы поймете почему. Дело в том, что на базе iceStorm и экспериментального комплекта утилит для FPGA apio, развивается целая графическая IDE для разработки под чипы iCE40 и называется она Icestudio:

Icestudio — это визуальный редактор для разработки на FPGA, который сочетает в себе возможности легкого создания прошивочного файла с помощью интуитивно понятного интерфейса блочного конструктора с возможностями традиционного описания дизайна аппаратуры с помощью простого редактора кода.

Также Icestdio позволяет без всяких проблем сочетать оба подхода при необходимости дополняя уже созданный дизайн с помощью языка описания аппаратуры Verilog.

Именно Icestudio мы будем использовать для создания нашего проекта, скачав его под свою операционную систему со страницы готовых релизов.

Далее, доустанавливаем компоненты, которые нужны для корректной работы Icestudio, а именно тулчейн (toolchain, т.е инструменты разработчика) и драйвера для работы с чипом FTDI, который установлен на плату iCEStick. Установка тулчейна легко выполняется прямо из Icestudio простым переходом в меню Tools > Toolchain > Install, а установка драйверов осуществляется переходом в Tools > Drivers > Enable.

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

Среда Icestudio предполагает, что весь проект, который вы создаете представляет собой некий вид диаграммы или, лучше сказать, схемы. Эта схема будет состоять из некоторых уже предопределенных блоков (доступных в верхнем правом углу области рисования окна — вы увидите надписи с небольшими стрелочками вниз, это и есть палитра доступных элементов), соединенных между собой через входы и выходы. Эти входы и выходы могут иметь как физический смысл (это конкретные пины платы), так и быть просто входными/выходными пинами для последующих блоков. Также, в отличии от обычных электрических схем, здесь не существует некоторого заданного направления движения (такого, как например, движение тока по схеме) и нет строгого порядка исполнения — вы можете рассматривать все соединения в диаграмме как некие воздействия или взаимосвязи между функциональными блоками.

Помимо наличия встроенных блоков, Icestudio разрешает создавать свои блоки и даже подключать целые сторонние коллекции блоков (вы можете изучить это сами), а нас прежде всего будет интересовать пункт меню File и подпункт Add as block, который позволяет добавить уже нарисованную схему Icestudio так, как будто это встроенный примитив !

Я рекомендую вам самостоятельно поэкспериментировать с созданием схем в Icestudio, и также посмотреть немного уроков по Verilog, т.к совсем без него сделать что-либо действительно стоящее в Icestudio не получится.

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

Проведя пару опытов со средой, мы продвигаемся дальше и немного освоившись с отрисовкой функциональных диаграмм, плавно перейдем к нашему исходному плану с воспроизведением звука, про который я так ничего и не сказал.

Для этого сначала выберем модель платы с помощью Select > Board и подпункта меню iCEStick Evaluation Kit.

После выполнения этих ответственных шагов я наконец могу поведать детали коварного плана по воспроизведению звука средствами FPGA. Все дело в том, что для воспроизведения звука необходимо вспомнить то, что звук является аналоговым процессом, а то, что называется звуковым файлом представляет собой попытку цифрового описания аналогового процесса. Именно это приходит на ум, когда вообще упоминается звук в контексте современной технологии, и это наводит на мысль о том, что для работы со звуком его нужно переводить из одного представления в другое.

Идея нашего плана довольно проста: мы возьмем простейшую цифровую форму представления звука, а если быть более конкретным, то волновую форму, быстро перенесем ее в плату программируемой логики (а это цифровое устройство), а затем нужно будет как-то трансформировать полученные платой данные в обычный аналоговый звук, подав его на самые непритязательные наушники.

Первый шаг нашего плана — это передача звуковой информации из файла в плату.

Однако, форматов звуковых файлов достаточно много, и практически все они основаны на сложных математических преобразованиях, что усложняет нашу работу, а очень небольшой набор ячеек в iCEStick скорее всего не позволит реализовать сложную математику, и у меня просто не хватит квалификации, чтобы привести это к форме Icestudio или к Verilog-коду.

Но, существует очень простой формат звуковых файлов, который описывет амплитуду некоторой звуковой волны, осуществляя выборку некоторых ее значений через равные интервалы времени с достаточно большой частотой (частотой дискретизации). Этот формат называется WAV и его мы будем использовать в практически сыром байтовом виде, поскольку изнутри он представляет собой набор некоторых значений сопоставимых с параметрами сложной звуковой волны.

Передать же файл побайтово во внешнее устройство очень просто — для этого можно использовать интерфейс последовательного порта, который хорошо реализован в чипе FTDI на плате с FPGA.

Для того, чтобы реализовать такую передачу — необходимо иметь функциональный блок UART-приемника просто для того, чтобы иметь возможность принять данные в байтовом виде для возможной обработки внутри ПЛИС. Ко всеобщему разочарованию, блока UART-приемника в Icestudio нет, но к счастью, есть возможность использования любого кода на Verilog прямо внутри основного проекта, либо создания своих блоков из такого кода.

И вот, вооружившись минимальными (почти нулевыми) знаниями в Verilog и уроками по FPGA с сайта nandland, на основе одного из уроков я создал модуль приемника для последовательного порта:

Вы можете его скачать прямо в виде проекта для Icestudio и использовать в своих проектах (Бонусом идет описание передатчика для UART, также в формате Icestudio).

Второй шаг — преобразование цифровой формы звука (т.е переданных байтов) в аналоговую форму и воспроизведение звука.

Для того, чтобы преобразовать некоторое N-битное значение в аналоговый вид существуют специальные устройства под названием Цифро-аналоговые преобразователи или сокращенно ЦАПы, представляющие собой отдельные микросхемы или даже лестницы резисторов по схеме R-2R, объединенные с операционным усилителем в режиме повторителя (честно говоря, я не настолько сильно разбираюсь а электронике). Исходя из этого, нам на выход от блока приемника данных с UART нужно подключить такой преобразователь, после чего результат работы ЦАП подать на аудио-устройство, которым в нашем случае будут наушники или динамик.

Казалось бы проблема решена, но не тут было — у меня не оказалось микросхемы ЦАП (в iCEStick ее попросту нет) и не оказалось комплекта резисторов для сборки матрицы R-2R, не говоря уже и про простейший операционный усилитель…

Решение проблемы пришло неожиданно: дело в том, что я давно слежу за интересным сайтом проекта Марсоход, который занимается популяризацией электроники на платах FPGA от Altera, а на этом сайте предлагается свой и весьма оригинальный подход к нашей проблеме. Идея, которую предлагает автор блога о Марсоходе заключается в использовании очень простого дельта-сигма ЦАП, предложенного компанией Xilinx в этом документе.

В итоге я позаимствовал блок дельта-сигма цифро-аналогового преобразователя, реализовав его в виде блока для Icestudio:

(сам блок вы можете скачать отсюда)

Теперь, когда блоки уже подготовлены нам нужно просто соединить их вместе, подключив выходы одного блока к другому, а также подключив источник тактового сигнала CLK ко входам clk блока UART и блока дельта-сигама ЦАП; и источник входящих байтов от COM-порта RX ко входу блока UART:

(итоговый дизайн можно скачать отсюда)

И вот сейчас мы можем собрать результирующий дизайн, нажав клавиши Ctrl + B (Build) и загрузить его в плату, нажав клавиши Ctrl + U (Upload).

После этого, аккуратно собираем схему из резистора и конденсатора, объединив их в одну RC-цепь: для этого соединяем конденсатор с землей (т.е с любым из выходов GND платы iCEStick и оставляя выход от земли конденсатора для наушников) и соединяем выход 44 (это выход BR10 в дизайне) с резистором (отводя точку соединения резистора и конденсатора на наушники):

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

Для выполнения этой задачи нам потребуется любой музыкальный файл и свободный звуковой редактор Audacity, который поможет нам сконвертировать исходный файл в нужный нам формат WAV.

Открываем любой файл в редакторе Audacity, затем нажимаем на кнопку Соло, которая находится перед изображением звуковой дорожки. Далее переходим в меню Дорожки и выбираем пункт Свести, после чего переходим в подпункт этого меню под названием Свести стерео в моно и выбираем именно этот пункт. В том же меню дорожки выбираем пункт Сменить частоту дискретизации дорожки и вводим число 22050 (данная частота выбрана из-за того, что скорость последовательного порта в нашем проекте будет составлять 230400 бит/с, что едва хватает на приемлимое воспроизведение звука через порт).

А теперь самое важное: правильное сохранение файла.

Для этого переходим в меню Файл и пункт Export, выбираем подпункт Экспорт аудио. Перед нами появляется диалоговое окно внизу которого нам нужно выбрать Прочие несжатые файлы, затем поставить заголовок WAV (NIST Sphere) и кодирование Signed 8-bit PCM, и просто сохранить файл под понравившемся именем:

После этого переходим к самой банальной части — передача файла через COM-порт средствами D.

Наше приложение представляет собой совсем обыкновенную форму с полем ввода текста, в котором будет отображаться имя передаваемого файла, кнопкой, по которой открывается диалоговое окно выбора WAV-файла, селектором порта (приложение писалось под линукс) и двумя интуитивно понятными кнопками Start и Stop.

На объяснении кода останавливаться не буду, поэтому просто код приложения (файл gui.d):

module fpga_music.gui;

private
{
    import std.algorithm;
    import std.file;
    import std.path;
    import std.range;
    import std.stdio;
    import std.string;

    import qte5;
    import serialport;
}

alias WindowType = QtE.WindowType;
alias normalWindow = WindowType.Window;

extern(C)
{
    void onSelectButton(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runSelect;
    }

    void onStartButton(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runStart;
    }

    void onStopButton(MainForm* mainFormPointer) 
    {
        (*mainFormPointer).runStop;
    }
}

class MainForm : QWidget
{
    private
    {
        QVBoxLayout vbox0;
        QHBoxLayout hbox0, hbox1, hbox2;
        QGroupBox group0, group1;
        QLineEdit line0;
        QComboBox combo0;
        QPushButton button0, button1, button2;
        QAction action0, action1, action2;
    }

    private
    {
        enum PORT_SPEED = 230_400;
        SerialPortBlk device;
        bool isPlayed;
    }

    this(QWidget parent, WindowType windowType)
    {
        super(parent, windowType); 
        setWindowTitle("FPGA Music tranceiver");
        setFixedWidth(700);
        setFixedHeight(200);

        vbox0 = new QVBoxLayout(null);

        hbox0 = new QHBoxLayout(null);

        line0 = new QLineEdit(null);
        line0.setReadOnly(true);

        button0 = new QPushButton("Select...", null);

        hbox0
            .addWidget(line0)
            .addWidget(button0);

        group0 = new QGroupBox(null);
		group0.setText("WAV file:");
		group0.setLayout(hbox0);

        hbox1 = new QHBoxLayout(null);

        combo0 = new QComboBox(null);
		
		SerialPort
				 .listAvailable
				 .filter!(a => a.startsWith("/dev/ttyUSB"))
				 .enumerate(0)
				 .each!(a => combo0.addItem(a[1], a[0]));

        hbox1.addWidget(combo0);

        group1 = new QGroupBox(null);
		group1.setText("COM port:");
		group1.setLayout(hbox1);

        hbox2 = new QHBoxLayout(null);

        button1 = new QPushButton("Play", null);
        button2 = new QPushButton("Stop", null);

        hbox2
            .addWidget(button1)
            .addWidget(button2);

        action0 = new QAction(null, &onSelectButton, aThis);
        action1 = new QAction(null, &onStartButton, aThis);
        action2 = new QAction(null, &onStopButton, aThis);

        connects(button0, "clicked()", action0, "Slot()");
        connects(button1, "clicked()", action1, "Slot()");
        connects(button2, "clicked()", action2, "Slot()");

        vbox0
            .addWidget(group0)
            .addWidget(group1)
            .addLayout(hbox2);

		setLayout(vbox0);        
    }

    void runSelect()
    {
         
       QFileDialog fileDialog = new QFileDialog('+', null);
       auto filename = fileDialog.getOpenFileNameSt("Open WAV file", "", "*.wav"); 
       line0.setText(filename);
    }

    void runStart()
    {
        // button1.setEnabled(false);
        // button2.setEnabled(true);

        if (line0.text!string != "")
        {
            if (line0.text!string.exists)
            {
                isPlayed = true;
                SerialPortBlk device = 	new SerialPortBlk(combo0.text!string, PORT_SPEED);
	            device.config = SPConfig(PORT_SPEED, DataBits.data8, Parity.none, StopBits.one);

                File file;
	            file.open(line0.text!string, "rb");
                
                foreach (ubyte[] buffer; file.byChunk(8912))
                {
                    if (isPlayed)
                    {
                        device.write(buffer);
                    }
                    else
                    {
                        break;
                    }
                }
                isPlayed = false;
            }
        }
    }

    void runStop()
    {
        //button1.setEnabled(true);
        //button2.setEnabled(false);
        isPlayed = false;
    }
}

Внешний вид приложения:

И внешний вид системы для испытаний:

Таким образом, мы получили простую связку FPGA с компьютером при помощи нетривиальных сред разработки, небольшого количества электроники и языка программирования D !

P.S: Бонус от нас — модуль передатчика UART и внешний вид сигнала от ЦАП в нашем собственном простом спектроанализаторе (об этом мы еще расскажем):

Добавить комментарий