С Цифро-Аналоговыми-Преобразованиями мы уже знакомились в нескольких статьях. Единственный тип этих преобразования, который мы не рассмотрели: метод передискретизации.
Передискретизация позволяет использовать ЦАП с меньшей разрядностью для достижения большей разрядности итогового преобразования, подобно методу ШИМ.
Одним из видов является Дельта-Сигма преобразование, которое основано на изменении плотности импульсов. Именно импульсов, то есть с помощью одного цифрового вывода можно формировать аналоговый сигнал. Как?
Путем усреднения импульсов того или иного участка графика мы получаем определенное значение амплитуды. Принцип очень напоминает ЧМ(FM), и как вы могли заметить, для работы необходима опорная частота.
Устройство
- Модулятор — блок, который принимает цифровые данные (сэмпл формата ИКМ) и превращает их в единый поток импульсов, представленных выше.
- Второй блок делает из импульсов аналоговый сигнал. Причем «Усреднителем» и фильтром будет являться не только ФНЧ(Low-Pass), но и сами узлы аудио-аппаратуры. К примеру, подключив к модулятору напрямую динамик мы уже будем слышать аналоговый сигнал, но с ВЧ шумами.
Вообще термин «Дэльта-Сигма» относится к арифметической разности и суммы соответственно, что мы увидим ниже.
Блок схема модулятора выглядит следующим образом:
Формирование импульса происходит циклически и количество циклов напрямую зависит от входных данных. То есть, число на входе (сэмпл) изменяет период, что в свою очередь влияет на частоту, а затем на плотность импульсов в общей картине. Как вы можете видеть, имеется отрицательная обратная связь для контроля формирования импульса. Мы «дробим» значения амплитуд на много 1бит импульсов разной частоты.
Одним из преимуществ данной системы является высокое качество при минимальных затратах на компоненты.
Отношение сигнал/шум = Ч.Котельникова * 2N. Для ДС-ЦАП существует константа «оверсэмплинг» — число, определяющее отношение опорной частоты к дискретизации сэмплов(по Котельникову) и она равна 64. К примеру, мы хотим преобразовать поток N-бит 22500Гц: опорная частота = (22500 * 2) * 64 = 2.88MHz.
Блок схема фильтра заключается в простой RC цепочке:
В самом простом представлении это ФНЧ, еще эту систему называют дециматором. Мы, фактически, уменьшаем частоту дискретизации выходных импульсов до слышимого диапазона путем их усреднения.
Реализация
Дэльта-Сигма ЦАП можно легко развернуть на нескольких логических элементах. На микроконтроллерах программным путем создавать эту систему не целесообразно, хотя бы из-за высокой опорной частоты.
Схема модулятора разрядностью 8бит:
Ее я подсмотрел у Xilinx. Дельта сумматор вычисляет разность между входным и выходным, на данный момент, сигналом. Это та самая отрицательная обратная связь. Причем выходной сигнал создается Сигма защелкой, дублируется и передается в виде 10бит числа на вход сумматора. (см.рис) И это количество нулей компенсирует тот факт, что у Дельта сумматора входы безнакового типа. Сигма сумматор складывает предыдущее значение с защелки со значением Дельта сумматора. Такими хитрыми манипуляциями мы создаем неоднородный поток импульсов.
Для Фильтра-Низких-Частот и прочего рекомендуют посмотреть таблицу:
DS ЦАП на ПЛИС
Мы все ближе и ближе приближаемся к ПЛИС. Что ж, за основу я решил взять проект графического процессора. Это MAXII EPM240 с выведенными ногами и внешней SRAM, которую мы пока не будем использовать.
Характеристики будущего ЦАП:
- Разрядность: 8бит
- Частота дискретизации сэмплов: 22500Гц
Электрическая схема проста до безобразия. Это ФНЧ, подключенный к порту ПЛИС + светодиод для индикации. Также необходимо подключить внешний тактовый генератор на PIN12 ПЛИС, у меня имелся на плате 50MHz, его я и задействовал.
Наш модуль ЦАП достаточно просто описывается на Verilog-е, код которого я также позаимствовал у Xilinx 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | module dac(DACout, DACin, Clk); output DACout; // This is the average output that feeds low pass filter reg DACout; // for optimum performance, ensure that this ff is in IOB input [7:0] DACin; // DAC input input Clk; reg [9:0] DeltaAdder; // Output of Delta adder reg [9:0] SigmaAdder; // Output of Sigma adder reg [9:0] SigmaLatch; // Latches output of Sigma adder reg [9:0] DeltaB; // B input of Delta adder always @(SigmaLatch) DeltaB = {SigmaLatch[9], SigmaLatch[9]} << (8); always @(DACin or DeltaB) DeltaAdder = DACin + DeltaB; always @(DeltaAdder or SigmaLatch) SigmaAdder = DeltaAdder + SigmaLatch; always @(posedge Clk) begin SigmaLatch <= SigmaAdder; DACout <= SigmaLatch[9]; end endmodule |
В графическом варианте схема выглядит так: (я добавил защелку для лучшей синхронизации)
Но-но-но! Откуда мы будем брать данные (сэмплы)? Необходим поток ИКМ с частотой 22.5кГц.
Вы можете пересылать данные UART, COM или другим модулем, однако ни того, ни другого у меня нет, поэтому я буду пересылать сэмплы через параллельный интерфейс. Единственный МК с «распушенной» внешней шиной у меня — Arduino Due. Схема подключение представлена ниже, а пины вы можете посмотреть в assigment editor-е:
Адресная шина нам совсем не нужна, тк. отправлять данные мы будем потоково. Я конечно понимаю, что это забивание гвоздей микроскопом, но как иначе проверить работу ЦАП?
Внесем соответствующие изменения в схему:
Аудио пин, после ФНЧ, необходимо подключить к усилителю, либо в линейный вход звуковой карты ПК.
Задача со стороны МК состоит в отправке 8бит значений амплитуды на любой адрес, но с постоянной частотой 22500Гц. Кстати, метод memcpy() тут не подойдет, так как настроить SMC контроллер на частоту дискретизации не получиться. Единственный вариант: таймер.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include «bmp.h» #include <DueTimer.h> #include <Parallel.h> uint32_t i = 0; uint8_t* spu = (uint8_t*)0×60000000; //SPU ADDR void play() { spu[0] = sound[i]; i = (i<sizeof(sound))? i+1 : 0; } void setup() { Parallel.begin(PARALLEL_BUS_WIDTH_8, PARALLEL_CS_0, 1, false, true); Parallel.setCycleTiming(0,0); Parallel.setPulseTiming(0,0,0,0); Parallel.setAddressSetupTiming(0,0,0,0); Timer3.attachInterrupt(play).setFrequency(22500).start(); } void loop() { } |
Я ничего не знаю про таймеры в ARM, поэтому использовал библиотеку. В bmp.h файле находиться всего один массив с аудио данными: const unsigned char sound[x] = {…} Для конвертации можете воспользоваться утилитой wav2c. Или вот вам файл с синусоидой.
Общая конструкция: (почти ничем не отличается от бывшего видеопроцессора)
Испытания
Первое, что приходит в голову: синусоида, с помощью нее мы можем легко определить искажения. Даже после ФНЧ осциллографом можно увидеть опорную частоту:
На вид искажение довольно неплохие. Дельта-Сигма ЦАП справляется гораздо лучше с более высокими частотами, а здесь всего 440Гц. При воспроизведении какого либо звукового фрагмента искажения не так заметны на слух. Я взял фрагмент из одной композиции. bmp.h файл.
То, что получилось на выходе:
Я полагаю, для восьми бит качество очень даже хорошее. При использовании ШИМ или R2R я не смог добиться подобного результата.
Воспроизведение из SRAM
Согласитесь, хоть это и тест, но «воспроизводить» звук Ардуиной, преобразовывать это на ПЛИС — невесть что.
Решение: подключение к ПЛИС микросхемы SRAM. Теперь непосредственно воспроизведением будет заниматься ПЛИС, а Arduino лишь перешлет раз звуковые данные.
По тому же параллельному интерфейсу мы подключаем SRAM, объемом 64кб; к шине адреса и данные прибавляются еще два: WE и OE (связь двухсторонняя). Лучше взять память объемом ~2мБайт, но тогда адресную шину микроконтроллера необходимо будет так же увеличивать. Внесем изменения в схему:
За основу была взята схема видеопроцессора. Алгоритм работы линейный:
- Передача звуковых данных из SRAM в ЦАП. 64кб = ~2.5сек
- Извещаем микроконтроллер о разрешении записи, путем подачи на пин FRAME (подключите к МК) низкого уровня.
- Открываем доступ микроконтроллеру к памяти. Маршрут: внешняя шина -> ПЛИС -> SRAM.
- Задержка на несколько миллисекунд.
- Закрываем доступ микроконтроллеру к памяти.
Запустив всю конструкцию без микроконтроллера, мы должны услышать что-то подобное:
Это содержимое памяти после подачи питания. Пора перейти к программе контроллера:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #include «bmp.h» //#include <DueTimer.h> #include <Parallel.h> uint32_t i = 0; uint8_t* spu = (uint8_t*)0×60000000; //SPU ADDR //void play() { // spu[0] = sound[i]; // i = (i<sizeof(sound))? i+1 : 0; //} void setup() { Parallel.begin(PARALLEL_BUS_WIDTH_8, PARALLEL_CS_0, 16, false, true); Parallel.setCycleTiming(1,1); Parallel.setPulseTiming(0,0,0,0); Parallel.setAddressSetupTiming(0,0,0,0); //Timer3.attachInterrupt(play).setFrequency(22500).start(); pinMode(53, INPUT); } void loop() { if (PIO_Get( g_APinDescription[53].pPort, PIO_INPUT, g_APinDescription[53].ulPin ) == 0) { memcpy(spu, sound, 65536); } } |
Кстати, задача упростилась: ему необходимо по низкому уровню FRAME ПЛИС отправлять звуковые данные объемом 64кб. Либо отправить их один раз и вовсе отключиться.
Для теста я взял другой фрагмент, так как использовать один и тот же не имеет смысла. Все, что должно измениться — это длительность звучания и независимость от Arduino.
Но действительность может отличаться от теории из-за внешних факторов. Одними из которых являются неудачная разводка платы и перенебрегание блокировочными конденсаторами. Так что причина снижения качества не в схеме ПЛИС или устройстве ЦАП.
Вообще, неплохо бы было добавить регистры управления проигрывателем, тем самым довести его до звукового процессора, но сейчас не об этом.
Видео обоих испытаний:
Файлы тестов (все):
Кирилл, у вас отличные статьи по электронике! это великолепно,когда у человека есть желание разбираться в чем- то, да еще и хотеть поделиться этим материалом и доступно разъяснить.
очень хочется ,чтобы ваши таланты( в электронике и журналистике так сказать) были оценены в будущем (или настоящем) и приносили людям пользу а вам деньги и удовлетворение!
Виталий