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, ведь она практически такая же, только стоит первый Циклон, а не третий, как здесь.

Комментарии (2)


  1. toivo61
    27.09.2023 11:17

    Интересно, а до каких частот ультразвука можно разогнаться? Там, наверху очень много вкусного.


    1. nixtonixto
      27.09.2023 11:17

      До ограничения аудиочипа — 48 кГц, 25 МГц тактовой.
      А так — ПЛИС до пары сотен МГц можно разогнать.