
Вот уже почти закончился сентябрь. Студенты уже давно вернулись за парты и учатся.
Многие начали изучать цифровую обработку сигналов. А как известно, лучше предмет пощупать один раз своими руками, чем десять раз прочитать о нём в учебнике.
В этой статье я расскажу о захвате звукового сигнала платой FPGA MCY316. Захват сигнала это только первый этап перед обработкой. Получим сигнал и передадим эти данные в ПК. Если всё получится, то в следующих работах добавим в ПЛИС цифровой фильтр
Прежде всего, давайте немного о самой плате. Плата MCY316 предназначена для начального ознакомления с технологией ПЛИС студентами и инженерами. Эта плата MCY316 является почти полным близнецом платы MCY112, о которой я уже рассказывал на страницах хабра. Принципиальное отличие этой платы — здесь стоит более новая микросхема ПЛИС Altera Cyclone III с почти 16ю тысячами логических элементов. Да, я согласен, что чип конечно не самый современный, но он очень даже не плох.
Все FPGA проекты от первой платы MCY112 портированы и на эту плату MCY316, практически один в один. То есть здесь и светодиодами поморгать можно, и запустить процессор RISC-V и видео фреймбуффер можно сделать. Все эти проекты уже есть, их можно брать изучать. Теперь мы добрались до обработки звука.
Рассмотрим плату подробнее:

- Кварцевый генератор 100Мгц;
- Две пользовательские кнопки;
- Восемь пользовательских светодиодов и 7-ми сегментный индикатор;
- SDRAM IS42S32200B, 8 (или 16) Мбайт, 32 разряда шина данных (на обратной стороне платы);
- Двухканальная аудио АЦП PCM1801U,16 бит, 48 Кгц;
- Двухканальный аудио выход для Дельта Сигма ЦАП (8бит);
- Три разъема для установки плат расширения, квазисовместимые с Raspberry Pi;
- SPI Flash W25Q16, 2 Мбайта для автозагрузки ПЛИС:
- SPI Flash W25Q16, 2 Мбайта для пользовательских данных;
- Разъем для установки внешнего USB JTAG программатора, например MBFTDI или UsbBlaster
На плате, как вы видите стоит разъем Аудио Jack и микросхема АЦП PCM1801. Мне предстоит работать с ней из FPGA.
Микросхема PCM1801 имеет последовательный интерфейс и может работать в двух режимах FMT: Left-Justified и I2S, но режимы вообще-то мало чем отличаюся. В обоих случаях на микросхему нужно подать частоту SCKI выше частоты оцифровки скажем в 512 раз выше, чем частота оцифровки. Кроме того, на микросхему нужно подать еще сигналы LRCK, который определяет канал передачи и собственно частоту последовательного сдвига BCK. Сигнал DOUT из PCM1801 передает последовательный код старшими битами вперед, по 16 бит на каждый канал.
Вот так временные диаграммы описаны в Datasheet:

Плата MCY316 использует режим работы Left-Justified.
На микросхему PCM1801 я подаю частоту 24МГц, частота BCK у меня будет в 16 раз ниже, чем SCKI, а LRCK будет еще ниже в 32 раза (16 бит * 2 канала). Таким образом, частота оцифровки получится 24000000 / 512 = 46875 Герц.
Код модуля работы с PCM1801 на языке Verilog HDL у меня выглядит вот так:
module pcm1801( input wire scki, input wire dout, output wire lrck, output wire bck, output reg[15:0]Left, output reg[15:0]Right ); reg [8:0]cnt; always @(posedge scki) cnt <= cnt+1; assign lrck = cnt[8]; assign bck = cnt[3]; reg [15:0]LeftSR; reg [15:0]RightSR; always @(posedge bck) begin if(lrck==1'b1) begin LeftSR <= { LeftSR[14:0], dout }; if( cnt[7:4]==4'hF ) Left<= { LeftSR[14:0], dout }; end if(lrck==1'b0) begin RightSR <= { RightSR[14:0], dout }; if( cnt[7:4]==4'hF ) Right<= { RightSR[14:0], dout }; end end endmodule
В микросхемах FPGA код нельзя отлаживать традиционными программистскими дебагерами, ведь код не исполняется последовательно по шагам. Наоборот, запись во многие регистры проекта происходит одновременно и параллельно. Тем не менее, интересующие нас сигналы можно посмотреть используя специальные инструменты, например у Altera/Intel это инструмент SignalTap. В проект можно добавить специальную логику, которая будет записывать состояния интересующих нас сигналов и потом эти данные можно выкачать на компьютер разработчика через интерфейс программатора JTAG.
Вот так можно посмотреть например уже полученный оцифрованный сигнал синусоиды на двух каналах левом и правом:

А вот это сигналы LRCK, BCK, DOUT:

Можно рассмотреть их поподробнее и убедиться, что они соответствуют описанию в Datasheet.
Обратите внимание на еще один сигнал serial_tx.
Я добавил логику по передаче полученных выборок звука в ПК через последовательный порт. Данные у нас 16ти битные знаковые. То есть теоретически может хватить 4х байт для отправки этих выборок в последовательный порт. Однако, так дело не пойдет, ведь на компьютере придется как-то отличать левый канал от правого, да и старший байт от младшего внутри канала. Поэтому я добавляю в мой протокол передачи еще один пятый байт, заголовок. Старший бит только этого байта будет всегда в единице. У остальных байт я заберу старший бит и перемещу его в заголовок. Тогда программа на ПК принимая поток байтов из последовательного порта сможет легко разобраться, где заголовок, а где данные каналов. Последовательность передаваемых данных получается вот такая:

Скорость передачи 6 Мегабит в секунду.
В программе для FPGA на языке Verilog HDL я описал вот такой сдвиговый регистр для последовательной передачи:
reg [49:0]serial; always @(posedge clk6) if(lrck_edge) serial <= { //stop, flag, body, start 1'b1, 1'b0, Rchannel_r[14:8], 1'b0, 1'b1, 1'b0, Rchannel_r[6 :0], 1'b0, 1'b1, 1'b0, Lchannel_r[14:8], 1'b0, 1'b1, 1'b0, Lchannel_r[6 :0], 1'b0, 1'b1, 1'b1, 3'b000, Rchannel_r[15],Rchannel_r[7],Lchannel_r[15],Lchannel_r[7],1'b0, }; //load else serial <= {1'b1,serial[49:1]}; //shift out LSB first assign serial_tx = serial[0];
Для компьютера я написал программу на питоне, которая будет принимать байты и динамически отрисовывать сигнал в окне Plot:
import serial import time import sys import numpy as np from matplotlib import pyplot as plt from struct import * if len(sys.argv)<2 : print("Not enough arguments, need serial port name param") port_name = sys.argv[1] print(port_name) port = serial.Serial() port.baudrate=6000000 port.port=port_name port.bytesize=8 port.parity='N' port.stopbits=1 port.open() #serial data to ADC data def conv( sd ): i=0 while sd[i]&0x80 == 0 : i=i+1 lc=[] rc=[] while i<(len(sd)-5) : b0 = sd[i+1] | ((sd[i+0]&1)<<7) b1 = sd[i+2] | ((sd[i+0]&2)<<6) a=bytearray([b1,b0]) left,*rest = unpack('>h',a) b0 = sd[i+3] | ((sd[i+0]&4)<<5) b1 = sd[i+4] | ((sd[i+0]&8)<<4) a=bytearray([b1,b0]) right,*rest = unpack('>h',a) lc.append(left) rc.append(right) i=i+5 return [lc,rc] def f(adc_data): sync_idx = 0 for i in range(1024) : if adc_data[i]<0 and adc_data[i+10]>=0 : sync_idx = i break y=[] for i in range(1024) : y.append(adc_data[sync_idx+i]) return y x = np.arange(0, 1024) plt.rcParams["figure.figsize"] = [7.50, 3.50] plt.rcParams["figure.autolayout"] = True plt.ion() fig,ax = plt.subplots(2,1) ax[0].set_xlabel('Idx') ax[0].set_ylabel('Left Channel') ax[0].set_ylim([-33000, +33000]) line0, = ax[0].plot(x, f(x), color='red') # Returns a tuple of line objects, thus the comma ax[1].set_xlabel('Idx') ax[1].set_ylabel('Right Channel') ax[1].set_ylim([-33000, +33000]) line1, = ax[1].plot(x, f(x), color='red') # Returns a tuple of line objects, thus the comma while 1 : port.flushInput() serial_data = port.read( 1024*10 ) data = conv(serial_data) CL=data[0] CR=data[1] line0.set_ydata(f(CL)) line1.set_ydata(f(CR)) fig.canvas.draw() fig.canvas.flush_events() #time.sleep(1) port.close()
При запуске этой программы она открывает последовательный порт и читает из него байты, затем разбирает их и выделяет данные для левого и правого канала звука и рисует их в отдельных субплотах. Основная сложность для меня была именно в разборе потока. Нужно по старшему биту в байте обнаружить заголовок, потом перетасовать биты каналов и главное преобразовать два байта в знаковое целое число. Для этого я использую функцию unpack() модуля struct.
Вот так отображается сигнал в моей питоновской программе:

Теперь еще один вопрос. Для экспериментов с оцифровкой звука нам нужен собственно источник звуковых сигналов. Хорошо, что он есть у каждого из нас в кармане — смартфон.
Существует несколько программ-генераторов звуковых сигналов для Android, которые можно использовать. Например, программа «Генератор частоты»:

Это довольно удобная программа, которая позволяет генерировать синусоидальный, пилообразный, меандр сигнал или сумму нескольких сигналов. Можно менять баланс левого и правого канала, общую громкость или громкость отдельных сигналов в сумме. Можно на лету менять частоты генерируемого сигнала. Выглядит довольно симпатично.
Еще можно порекомендовать программу «Function generator»:

Таким образом, экспериментировать с оцифровкой сигнала для FPGA становится очень удобно. Подключаем проводом смартфон Андроид к плате MCY316 и работаем. Примерно вот так, как показывает это демонстрационное видео:
Здесь показывается и работа с внутрисхемным отладчиком Altera SignalTap и демонстрируется питоновская программа, которая отображает полученный из АЦП сигнал.
Более подробную информацию о FPGA плате можно посмотреть на сайте https://marsohod.org/438-mcy316
Взять исходники этого проекта можно на github: github.com/marsohod4you/MCY316
Я надеюсь в следующих статьях рассказать о цифровом фильтре, реализованном в ПЛИС. Возьму этот проект за основу и добавлю в него полосовой КИХ фильтр.
Таким образом я хочу показать, что эти платы довольно удобны для изучения основ цифровой обработки сигналов. Еще можно добавить, что этот проект будет работать и на плате MCY112, ведь она практически такая же, только стоит первый Циклон, а не третий, как здесь.
