Как стать автором
Поиск
Написать публикацию
Обновить

Оцифровка звука FPGA платой MCY316

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров3.3K
image

Вот уже почти закончился сентябрь. Студенты уже давно вернулись за парты и учатся.
Многие начали изучать цифровую обработку сигналов. А как известно, лучше предмет пощупать один раз своими руками, чем десять раз прочитать о нём в учебнике.

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

Прежде всего, давайте немного о самой плате. Плата MCY316 предназначена для начального ознакомления с технологией ПЛИС студентами и инженерами. Эта плата MCY316 является почти полным близнецом платы MCY112, о которой я уже рассказывал на страницах хабра. Принципиальное отличие этой платы — здесь стоит более новая микросхема ПЛИС Altera Cyclone III с почти 16ю тысячами логических элементов. Да, я согласен, что чип конечно не самый современный, но он очень даже не плох.

Все FPGA проекты от первой платы MCY112 портированы и на эту плату MCY316, практически один в один. То есть здесь и светодиодами поморгать можно, и запустить процессор RISC-V и видео фреймбуффер можно сделать. Все эти проекты уже есть, их можно брать изучать. Теперь мы добрались до обработки звука.

Рассмотрим плату подробнее:

image

  • Кварцевый генератор 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:

image

Плата 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.

Вот так можно посмотреть например уже полученный оцифрованный сигнал синусоиды на двух каналах левом и правом:

image

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

image

Можно рассмотреть их поподробнее и убедиться, что они соответствуют описанию в Datasheet.

Обратите внимание на еще один сигнал serial_tx.

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

image

Скорость передачи 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.

Вот так отображается сигнал в моей питоновской программе:

image

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

Существует несколько программ-генераторов звуковых сигналов для Android, которые можно использовать. Например, программа «Генератор частоты»:

image

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

Еще можно порекомендовать программу «Function generator»:

image

Таким образом, экспериментировать с оцифровкой сигнала для FPGA становится очень удобно. Подключаем проводом смартфон Андроид к плате MCY316 и работаем. Примерно вот так, как показывает это демонстрационное видео:


Здесь показывается и работа с внутрисхемным отладчиком Altera SignalTap и демонстрируется питоновская программа, которая отображает полученный из АЦП сигнал.

Более подробную информацию о FPGA плате можно посмотреть на сайте https://marsohod.org/438-mcy316

Взять исходники этого проекта можно на github: github.com/marsohod4you/MCY316

Я надеюсь в следующих статьях рассказать о цифровом фильтре, реализованном в ПЛИС. Возьму этот проект за основу и добавлю в него полосовой КИХ фильтр.

Таким образом я хочу показать, что эти платы довольно удобны для изучения основ цифровой обработки сигналов. Еще можно добавить, что этот проект будет работать и на плате MCY112, ведь она практически такая же, только стоит первый Циклон, а не третий, как здесь.
Теги:
Хабы:
Всего голосов 22: ↑22 и ↓0+22
Комментарии6

Публикации

Ближайшие события