Sintetizador DDS en Verilog


En esta publicación compartiré cómo entendí escribir un sintetizador DDS en Verilog. Se utilizará para generar una oscilación sinusoidal, cuya frecuencia y fase inicial se pueden ajustar y calcular para usar con un DAC unipolar de 8 bits. El funcionamiento del sintetizador está bien escrito en un artículo en la revista Componentes y Tecnologías . Para reducir la cantidad de memoria utilizada de la tabla sinusoidal, se utiliza la simetría.


Para la compilación en Linux utilicé Iverilog y para mostrar GTKWave. Por conveniencia, se escribió un Makefile simple, tal vez sea útil para alguien. Inicialmente, usando el compilador iverilog, obtenemos el archivo tb.out y luego lo enviamos al simulador vvp, que se instala con iverilog. Como resultado, vvp generará out.vcd, que contiene todas las variables (señales) utilizadas en el proyecto. El objetivo de la pantalla, además de lo anterior, lanzará GTKWave con un archivo variable y podrá ver las formas de onda de las señales.


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 

En primer lugar, debe colocar una tabla del seno futuro en la memoria, ya que escribí un script simple de Python que divide una cuarta parte del período sinusoidal en 64 puntos y lo genera en un formato que luego se puede copiar en el código fuente. Como concebí la implementación de DDS para un DAC unipolar externo con una capacidad de bits de no más de 8 bits, la amplitud sinusoidal debería estar en el rango de 0 a 256, donde el medio período negativo se encuentra en el rango 0 ... 127, y la mitad positiva en 128 ... 255 . En este sentido, los valores de seno obtenidos (de 0 a pi / 4) se multiplican por 127 y luego se les agrega 127. Como resultado, se obtienen los valores del primer trimestre del período, cuya amplitud es 128 ... 256.


Llamo la atención sobre el hecho de que con esta formación, el seno en la salida del DAC tendrá un componente constante. Para eliminarlo, es necesario pasarlo a través de un condensador.


 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 la función seno es simétrica (impar), puede encontrar la primera simetría sin (x) = - sin (pi + x). La segunda simetría se caracteriza por el hecho de que al tener una tabla durante un cuarto del período, se puede obtener el segundo trimestre yendo a través de la tabla en el orden inverso (dado que el seno en el medio período primero aumenta, luego disminuye).


Formamos un seno


La mayor parte del sintetizador DDS es una batería de fase. En esencia, es el índice de un elemento de la tabla de búsqueda (LUT). Para cada período de la señal de reloj, el valor en ella aumenta en un cierto valor, como resultado, se obtiene un seno en la salida. La frecuencia de la señal en la salida dependerá del valor del incremento del acumulador de fase: cuanto mayor sea, mayor será la frecuencia. Sin embargo, según el criterio de Kotelnikov, la frecuencia de muestreo debe ser al menos 2 veces la frecuencia de la señal (para evitar el efecto de superponer el espectro), por lo tanto, la limitación en el incremento máximo es la mitad del acumulador de fase. En general, el criterio de ingeniería es la frecuencia de muestreo = 2.2 de la frecuencia de la señal, por lo tanto, después de haber decidido no llevarla al extremo, eliminé un bit más, dejando 6 bits incrementados con una batería de fase de 8 bits (aunque la jackalita sinusoidal ya).


Debido a la simetría utilizada, solo los 6 bits inferiores de 2 ^ 6 = 64 se utilizarán directamente para el muestreo de índice. Los 2 bits altos se utilizan para identificar un cuarto de período de generación de seno y, en consecuencia, cambiar la dirección del recorrido de la tabla. Deberías obtener algo similar 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 

Al reiniciar, inicializamos todo con ceros, excepto el valor de incremento de fase, lo establecemos en uno. Para preservar la capacidad de sintetización del código, la tabla también se rellenará con valores durante el reinicio. En un proyecto real, es aconsejable usar la memoria de bloque integrada en el FPGA para tales fines y crear un archivo de configuración separado para él, y usar el núcleo IP en el proyecto mismo.


Una pequeña explicación sobre cómo funciona la simetría. En cada ciclo, se verifica (en los 2 bits más significativos) en qué cuarto se encuentra actualmente el acumulador de fase. Si el más alto = 00, entonces la salida en el dígito más alto es 1 (responsable de la media onda positiva), en los más bajos, el valor de la LUT de acuerdo con el índice. Después de que el valor del acumulador de fase exceda 63 (el primer trimestre pasará), aparecerá 01 en los bits más altos y los más bajos se volverán a llenar con ceros.


Para pasar la LUT en el orden inverso, es suficiente invertir los bits menos significativos del acumulador de fase (continuará aumentando para cada ciclo, y su valor invertido disminuirá).


Para formar una media onda negativa, escribimos 0. En el bit superior de la salida, ahora necesitamos invertir el valor mismo de la tabla senoidal. El punto aquí es que necesita obtener una copia espejo del cuarto del seno, y si esto no se hace, obtendrá la misma imagen que en el primer trimestre, pero se redujo en 127 hacia abajo. Puede verificar esto eliminando el inverso en el código.


Cambiamos la frecuencia y la fase inicial.


Como ya se describió anteriormente, para cambiar la frecuencia, es necesario cambiar el valor del incremento de fase. Aparecerán nuevas entradas:


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

Para cambiar el valor del incremento de fase, simplemente lo ajustamos en cada ciclo:


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

Con la fase inicial, no todo es tan simple. Primero debe escribirlo en el registro intermedio y llenar el acumulador de fase con este valor solo si el valor de la fase inicial en la entrada no coincide con el almacenado previamente. Esto plantea otro punto importante relacionado con el estado de las razas. Ya tenemos un lugar donde escribimos phase_acc en el registro. No puede grabar al mismo tiempo en varios lugares, ya que se registrarán los datos que vinieron primero. Por lo tanto, el diseño se verá así:


 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 

Banco de pruebas


El código de banco de pruebas para Iverilog y GTKWave tiene algunos diseños (con un signo de dólar) que no se usan en el ISE Design Suite o Quartus habitual. Su significado se reduce a seleccionar señales monitoreadas y cargarlas en un archivo, para que luego puedan transferirse al simulador. El trabajo del banco de pruebas en sí es trivial: hacemos un reinicio, establecemos la frecuencia / fase inicial y esperamos un momento.


 `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 

Tablas de tiempo


En la salida, obtenemos algo similar a un seno con una frecuencia cambiante y una fase inicial en los puntos de tiempo establecidos en el banco de pruebas. Vale la pena señalar que al aumentar la frecuencia, la resolución (el número de muestras por período) disminuye, respectivamente, la frecuencia del reloj del sintetizador y su tamaño LUT juegan un papel decisivo en la reproducción del seno puro (cuanto más se acerca su forma al ideal, menos componentes laterales en el espectro del resultado resultante señal y el pico ya estará en la frecuencia generada).



Se puede ver que una señal con una segunda frecuencia ya no tiene un seno tan suave como las otras. Echemos un vistazo más de cerca.



Se puede ver que esto sigue siendo un poco similar al seno, el resultado será aún mejor después de que dicha señal se pase a través de un filtro antisolapamiento (filtro de paso bajo).


Las fuentes del proyecto están disponibles aquí .


Fuentes


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


All Articles