Sintetizador DDS no Verilog


Neste post, vou compartilhar como eu entendi escrevendo um sintetizador DDS no Verilog. Será usado para gerar uma oscilação sinusoidal, cuja frequência e fase inicial podem ser ajustadas e calculadas para uso com um DAC unipolar de 8 bits. Como o sintetizador funciona está bem escrito em um artigo na revista Components and Technologies . Para reduzir a quantidade de memória usada da tabela senoidal, é usada simetria.


Para compilação no Linux, usei o Iverilog e para exibir o GTKWave. Por conveniência, um Makefile simples foi escrito, talvez seja útil para alguém. Inicialmente, usando o compilador iverilog, obtemos o arquivo tb.out e depois o enviamos para o simulador vvp, que é instalado com o iverilog. Como resultado, o vvp gerará out.vcd, que contém todas as variáveis ​​(sinais) usadas no projeto. O alvo de exibição, além do acima, iniciará o GTKWave com um arquivo variável e você poderá ver as formas de onda dos sinais.


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 

Antes de tudo, você precisa colocar uma tabela do futuro seno na memória, pois escrevi um script Python simples que divide um quarto do período seno em 64 pontos e o gera em um formato que pode ser copiado para o código-fonte. Desde que eu concebi a implementação do DDS para um DAC unipolar externo com uma capacidade de bits não superior a 8 bits, a amplitude senoidal deve estar no intervalo de 0 a 256, onde o meio período negativo fica no intervalo de 0 a 127 e a metade positiva em 128 a 255. . Nesse sentido, os valores senoidais obtidos (de 0 a pi / 4) são multiplicados por 127 e, em seguida, são adicionados a 127. Como resultado, são obtidos os valores do primeiro trimestre do período, cuja amplitude é de 128 ... 256.


Chamo a atenção para o fato de que, com essa formação, o seno na saída do DAC terá um componente constante. Para removê-lo, é necessário passar por um capacitor.


 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 

Como a função seno é simétrica (ímpar), você pode encontrar a primeira simetria sin (x) = - sin (pi + x). A segunda simetria é caracterizada pelo fato de ter uma tabela para um quarto do período, o segundo trimestre pode ser obtido percorrendo a tabela na ordem inversa (uma vez que o seno no meio período primeiro aumenta e depois diminui).


Nós formamos um seno


A maior parte do sintetizador DDS é uma bateria de fase. Em essência, é o índice de um elemento da Tabela de consulta (LUT). Para cada período do sinal do relógio, o valor nele aumenta em um determinado valor, como resultado, um seno é obtido na saída. A frequência do sinal na saída dependerá do valor do incremento do acumulador de fase - quanto maior, maior a frequência. No entanto, de acordo com o critério de Kotelnikov, a frequência de amostragem deve ser pelo menos 2 vezes a frequência do sinal (para evitar o efeito de sobreposição do espectro), portanto, a limitação no incremento máximo é metade do acumulador de fase. Em geral, o critério de engenharia é a frequência de amostragem = 2,2 da frequência do sinal, portanto, tendo decidido não levá-lo ao extremo, removi mais um bit, deixando 6 bits incrementados com uma bateria de fase de 8 bits (mesmo que já seja o jackalite senoidal).


Devido à simetria usada, apenas os 6 bits inferiores de 2 ^ 6 = 64 serão usados ​​diretamente para amostragem de índice. Os 2 bits altos são usados ​​para identificar um quarto de período de geração senoidal e, consequentemente, alterar a direção do percurso da tabela. Você deve obter algo semelhante a:


 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 

Ao redefinir, inicializamos tudo com zeros, exceto pelo valor do incremento de fase, definimos como um. Para preservar a capacidade de sintetização do código, a tabela também será preenchida com valores durante a redefinição. Em um projeto real, é aconselhável usar a memória de bloco incorporada no FPGA para esses fins e criar um arquivo de configuração separado para ele, além de usar o núcleo de IP no próprio projeto.


Uma pequena explicação sobre como a simetria funciona. A cada ciclo, é verificado (nos 2 bits mais significativos) em que trimestre o acumulador de fases está localizado atualmente. Se o mais alto = 00, a saída no dígito mais alto é 1 (responsável pela meia-onda positiva), nos mais baixos - o valor da LUT de acordo com o índice. Após o valor do acumulador de fases exceder 63 (o primeiro trimestre passa), 01 aparecerá nos bits altos e os inferiores serão preenchidos com zeros novamente.


Para passar o LUT na ordem inversa, basta inverter os bits menos significativos do acumulador de fases (continuará aumentando a cada ciclo do relógio e seu valor invertido diminuirá).


Para formar uma meia-onda negativa, escrevemos 0. No bit superior da saída, precisamos agora inverter o próprio valor da tabela senoidal. O ponto aqui é que você precisa obter uma cópia espelhada do quarto do seno e, se isso não for feito, você obtém a mesma imagem do primeiro trimestre, mas diminuiu 127 vezes. Você pode verificar isso removendo o inverso no código.


Mudamos a frequência e a fase inicial


Como já descrito acima, para alterar a frequência, é necessário alterar o valor do incremento de fase. Novas entradas aparecerão:


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

Para alterar o valor do incremento de fase, basta ajustá-lo em cada ciclo:


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

Com a fase inicial, nem tudo é tão simples. Você deve primeiro gravá-lo no registro intermediário e preencher o acumulador de fase com esse valor somente se o valor da fase inicial na entrada não coincidir com o valor armazenado anteriormente. Isso levanta outro ponto importante relacionado ao estado das corridas. Já temos um lugar onde escrevemos phase_acc no registro. Você não pode gravar ao mesmo tempo em vários lugares, pois os dados que vieram primeiro serão gravados. Portanto, o design ficará assim:


 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


O código testbench para Iverilog e GTKWave possui alguns designs (com um cifrão) que não são usados ​​no ISE Design Suite ou no Quartus usual. Seu significado se resume a selecionar sinais monitorados e carregá-los em um arquivo, para que possam ser transferidos para o simulador. O trabalho da própria bancada de testes é trivial - fazemos um reset, ajustamos a frequência / fase inicial e esperamos um pouco.


 `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 

Gráficos de tempo


Na saída, obtemos algo semelhante a um seno com uma frequência variável e uma fase inicial nos pontos de tempo definidos no banco de testes. É importante notar que, com o aumento da frequência, a resolução ao longo (o número de amostras por período) diminui, respectivamente, a frequência do relógio do sintetizador e seu tamanho LUT desempenham um papel decisivo na reprodução do seno puro (quanto mais sua forma se aproxima do ideal, menos componentes laterais no espectro do resultado sinal e o pico já estará na frequência gerada).



Pode-se ver que um sinal com uma segunda frequência já não tem um seno tão suave quanto os outros. Vamos dar uma olhada.



Pode-se ver que isso ainda é um pouco semelhante ao seno, o resultado se tornará ainda melhor depois que esse sinal for passado através de um filtro anti-aliasing (filtro passa-baixo).


As fontes do projeto estão disponíveis aqui .


Fontes


Source: https://habr.com/ru/post/pt457634/


All Articles