Хабрахабр

[Из песочницы] DDS Синтезатор на Verilog

Он будет использован для генерации синусоидального колебания, частоту и начальную фазу которого можно регулировать и рассчитан для использования с 8-битным однополярным ЦАП. В этом посте я поделюсь тем, как разбирался с написанием DDS синтезатора на Verilog. Для сокращения объема использованной памяти таблицы синуса использована симметрия. О том, как работает синтезатор хорошо написано в статье журнала Компоненты и Технологии.

Для удобства был написан простенький Makefile, возможно кому-нибудь он пригодится. Для компиляции под Линуксом я использовал Iverilog, а для отображения GTKWave. В результате vvp сгенерирует out.vcd, в котором содержатся все переменные (сигналы), используемые в проекте. Изначально при помощи компилятора iverilog мы получаем файл tb.out, а затем отправляем его в симулятор vvp, который устанавливается вместе с iverilog. Цель display помимо вышесказанного запустит GTKWave с файлом переменных и можно будет увидеть временные диаграммы сигналов.

SRC = nco.v
TB = nco_tb.v all: iverilog -o tb.out $(TB) vvp -lxt tb.out check: iverilog -v $(TB) display: iverilog -o tb.out $(TB) vvp -lxt tb.out gtkwave out.vcd & clean: rm -rf *.out *.vcd *.vvp

Так как я задумывал реализацию DDS для внешнего однополярного ЦАП с разрядностью не более 8 бит, то амплитуда синуса должна быть в интервале от 0 до 256, где в диапазоне 0... В первую очередь необходимо разместить в памяти таблицу будущего синуса, для я написал простенький скрипт на Python, разбивающий четверть периода синуса на 64 точки и генерирующий в формате, который потом можно скопировать в исходный код. 255 — положительный. 127 лежит отрицательный полупериод, а в 128... В результате получается значения первой четверти периода, амплитуда которых 128... В связи с этим полученные значения синуса (от 0 до pi/4) умножаются на 127, а затем ещё к ним прибавляется 127. 256.

Для того чтобы её убрать необходимо пропустить его через конденсатор. Обращу внимание на то, что при таком формировании синус на выходе ЦАП будет иметь постоянную составляющую.

import numpy as np x=np.linspace(0,np.pi/2,64)
print(np.sin(x))
y=127*np.sin(x)
print(len(y))
print(y)
z=[]
i = 0
for elem in y: if int(elem)<=16: print("lut[%d] = 7'h0%X;" % (i, int(elem))) else: print("lut[%d] = 7'h%X;" % (i, int(elem))) z.append(hex(int(elem))) i = i + 1

Вторая симметрия характерна тем, что имея таблицу на четверть периода, вторую четверть можно получить, проходя таблицу в обратном порядке (т.к. Т.к синус функция симметричная (нечётная), то можно обнаружить первую симметрию sin(x)=-sin(pi+x). синус на полупериоде сначала возрастает, потом убывает).

Формируем синус

По сути он является индексом элемента из Look Up Table (LUT). Основная часть DDS синтезатора — фазовый аккумулятор. От значения приращения аккумулятора фазы будет зависеть частота сигнала на выходе — чем оно больше, тем выше частота. За каждый период тактового сигнала значение в нём увеличивается на некоторое значение, в результате на выходе получается синус. Вообще инженерный критерий это частота дискретизации = 2. Однако, по критерию Котельникова частота дискретизации должна быть как минимум в 2 раза больше частоты сигнала (для избежания эффекта наложения спектра), отсюда ограничение на максимальное приращение — половина фазового аккумулятора. 2 частоты сигнала, поэтому, решив не доводить до крайности, я убрал ещё один разряд, оставив 6 бит на инкремент при разрядности фазового аккумулятора 8 бит (хотя уже при этом синус шакалит).

Старшие 2 бита используются для выявления четвертьпериода генерирования синуса и соответственно изменения направления обхода таблицы. Из-за используемой симметрии непосредственно для выборки по индексу будут использоваться только младшие 6 бит 2^6=64. Должно получиться что-то похожее на:

module nco(clk, rst, out ); input clk, rst;
output reg [7:0] out; reg [5:0] phase_inc = 6'h1;
reg [7:0] phase_acc = 0; parameter LUT_SIZE = 64;
reg [6:0] lut [0:LUT_SIZE-1]; always @(posedge clk) begin if (rst) begin phase_inc = 6'h1; phase_acc = 0; out = 0; lut[0] = 7'h00; // Целиком таблица не приведена lut[63] = 7'h7F; end else begin // Отсчеты формируются с латентностью в 1 период тактового сигнала if (phase_acc[7:6] == 2'b00) begin //Склеиваем старший бит полярности и младшие биты из LUT out = ; end if (phase_acc[7:6] == 2'b01) begin out = {1'b1,lut[~phase_acc[5:0]]}; end if (phase_acc[7:6] == 2'b10) begin out = {1'b0,~lut[phase_acc[5:0]]}; end if (phase_acc[7:6] == 2'b11) begin out = {1'b0,~lut[~phase_acc[5:0]]}; end phase_acc = phase_acc + {2'b0,phase_inc}; end
end endmodule

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

На каждом такте проверяется (по 2-м старшим битам), в какой четверти находится в данный момент фазовый аккумулятор. Немного пояснений о том, как работает симметрия. После того как значение фазового аккумулятора превысит 63 (пройдет первая четверть), в старших битах появится 01, а младшие снова заполнятся нулями. Если старшие = 00, то на выходе в старшем разряде 1 (отвечает за положительную полуволну), в младших — значение из LUT в соответствии с индексом.

Для прохождения LUT в обратном порядке достаточно инвертировать младшие биты фазового аккумулятора (он продолжит увеличиваться за каждый такт, а его инвертированное значение будет уменьшаться).

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

Меняем частоту и начальную фазу

Появятся новые входы: Как уже было описано выше для изменения частоты необходимо поменять значение инкремента фазы.

input [5:0] freq_res;
input [7:0] phase;

Для изменения значения инкремента фазы будем просто защелкивать его на каждом такте:

always @(posedge clk) begin if (rst) begin //... end else begin //... phase_inc = freq_res; end
end

Необходимо сначала записывать ее в промежуточный регистр, и заполнять этим значением аккумулятор фазы только в том случае, если значение начальной фазы на входе не совпадает с запомненным ранее. С начальной фазой все не так просто. У нас уже имеется место, где мы записываем в регистр phase_acc. Тут возникает ещё один важный момент, связанный с состоянием гонок. Поэтому конструкция будет выглядеть следующим образом: Нельзя записывать одновременно в нескольких местах, поскольку при этом запишутся данные, которые пришли первыми.

reg change_phase = 0; //Вверху объявляем еще один сигнал // Не забываем сбросить его (тут это пропущено) // На каждом такте выполняем следующее:
prev_phase <= phase;
if (phase != prev_phase) begin // Проверяем изменилась ли фаза на входе change_phase <= 1'b1;
end
if (change_phase) begin // Заменяем значение в фазовом аккумуляторе новой фазой phase_acc <= prev_phase; change_phase <= 1'b0;
end
else begin // Инкрементировать фазовый аккумулятор теперь можно только если не изменилась фаза phase_acc = phase_acc + {2'b0,phase_inc};
end

Testbench

Смысл их сводится к тому, чтобы выбрать отслеживаемые сигналы и загрузить их в файл, чтобы затем передать симулятору. Код тестбенча для Iverilog и GTKWave обладает некоторыми конструкциями (со знаком доллара) которые не используются в привычных ISE Design Suite или Quartus. Сама по себе работа тестбенча тривиальна — делаем сброс, устанавливаем частоту/начальную фазу и ждем некоторое время.

`include "nco.v"
`timescale 1ns / 1ps module nco_tb; reg clk = 0, rst = 0;
reg [7:0] phase = 0;
reg [5:0] freq_res;
wire [7:0] out; nco nco_inst ( .clk(clk), .rst(rst), .phase(phase), .freq_res(freq_res), .out(out) ); always #2 clk <= ~clk; initial begin $dumpfile("out.vcd"); $dumpvars(0, nco_tb); //$monitor("time =%4d out=%h",$time,out); rst = 1'b1; freq_res = 1; #8 rst = 1'b0; #300 phase = 8'b00100011; #300 phase = 8'b00001111; #1200 freq_res = 6'b111101; #1200 freq_res = 6'b001111; #1200 freq_res = 6'b011111; #400 phase = 8'b00010011; #1200 $finish; end endmodule

Временные диаграммы

Стоит отметить, что с ростом частоты падает разрешение по ней (количество отсчетов на период), соответственно частота тактирования синтезатора и размер его LUT играет решающую роль в воспроизведении чистого синуса (чем больше его форма приближается к идеальному, тем меньше побочных составляющих будет в спектре результирующего сигнала и тем уже будет пик на генерируемой частоте). На выходе получим что-то похожее на синус с меняющейся частотой и начальной фазой в установленные в тестбенче моменты времени.

Рассмотрим его поближе. Тут видно, что сигнал со второй частотой имеет уже не такой гладкий синус, как другие.

Видно, что на синус это все же немного похоже, результат станет ещё лучше после того как такой сигнал будет пропущен через антиалиасинговый фильтр (Фильтр Нижних Частот).

Исходники проекта доступны по ссылке.

Источники

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть