DDS Синтезатор на Verilog

в 16:15, , рубрики: dds, fpga, gtkwave, Icarus, Verilog, ПЛИС, синтезатор частот, Электроника для начинающих

DDS Синтезатор на Verilog - 1

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

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

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

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

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

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

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 = {1'b1,lut[phase_acc[5:0]]};
        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-м старшим битам), в какой четверти находится в данный момент фазовый аккумулятор. Если старшие = 00, то на выходе в старшем разряде 1 (отвечает за положительную полуволну), в младших — значение из LUT в соответствии с индексом. После того как значение фазового аккумулятора превысит 63 (пройдет первая четверть), в старших битах появится 01, а младшие снова заполнятся нулями.

Для прохождения 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 играет решающую роль в воспроизведении чистого синуса (чем больше его форма приближается к идеальному, тем меньше побочных составляющих будет в спектре результирующего сигнала и тем уже будет пик на генерируемой частоте).

DDS Синтезатор на Verilog - 2

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

DDS Синтезатор на Verilog - 3

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

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

Источники

Автор: B_O_R_M_A_L_E_Y

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js