Вот уже почти закончился сентябрь. Студенты уже давно вернулись за парты и учатся.
Многие начали изучать цифровую обработку сигналов. А как известно, лучше предмет пощупать один раз своими руками, чем десять раз прочитать о нём в учебнике.
В этой статье я расскажу о захвате звукового сигнала платой 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, ведь она практически такая же, только стоит первый Циклон, а не третий, как здесь.