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

от автора

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


ссылка на оригинал статьи https://habr.com/ru/articles/763670/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *