DDS Synthesizer di Verilog


Dalam posting ini saya akan membagikan bagaimana saya mengerti menulis synthesizer DDS di Verilog. Ini akan digunakan untuk menghasilkan osilasi sinusoidal, frekuensi dan fase awal yang dapat disesuaikan dan dihitung untuk digunakan dengan DAC unipolar 8-bit. Cara kerja synthesizer ditulis dengan baik dalam artikel di jurnal Components and Technologies . Untuk mengurangi jumlah memori yang digunakan dari tabel sinus, simetri digunakan.


Untuk kompilasi di Linux saya menggunakan Iverilog, dan untuk tampilan GTKWave. Untuk kenyamanan, Makefile sederhana ditulis, mungkin itu akan berguna bagi seseorang. Awalnya, menggunakan kompiler iverilog, kami mendapatkan file tb.out, dan kemudian mengirimkannya ke simulator vvp, yang diinstal dengan iverilog. Akibatnya, vvp akan menghasilkan out.vcd, yang berisi semua variabel (sinyal) yang digunakan dalam proyek. Target tampilan, selain yang di atas, akan meluncurkan GTKWave dengan file variabel dan Anda dapat melihat bentuk gelombang sinyal.


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 

Pertama-tama, Anda perlu menempatkan tabel sinus masa depan dalam memori, karena saya menulis skrip Python sederhana yang memecah seperempat periode sinus menjadi 64 poin dan menghasilkannya dalam format yang kemudian dapat disalin ke kode sumber. Karena saya memahami implementasi DDS untuk DAC unipolar eksternal dengan resolusi tidak lebih dari 8 bit, amplitudo sinus harus berada dalam kisaran 0 hingga 256, di mana setengah periode negatif terletak pada kisaran 0 ... 127, dan setengah positif di 128 ... 255 . Dalam hal ini, nilai-nilai sinus yang diperoleh (dari 0 hingga pi / 4) dikalikan dengan 127, dan kemudian ditambahkan 127. Sebagai hasilnya, nilai-nilai dari kuartal pertama periode tersebut diperoleh, amplitudo yang adalah 128 ... 256.


Saya menarik perhatian pada fakta bahwa dengan formasi ini, sinus pada output DAC akan memiliki komponen yang konstan. Untuk menghapusnya, perlu untuk melewati kapasitor.


 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 

Karena fungsi sinus simetris (ganjil), Anda dapat menemukan dosa simetri pertama (x) = - sin (pi + x). Simetri kedua dicirikan oleh fakta bahwa memiliki tabel untuk seperempat periode, kuartal kedua dapat diperoleh dengan melalui tabel dalam urutan terbalik (karena sinus pada setengah periode pertama meningkat, kemudian menurun).


Kami membentuk sinus


Sebagian besar synthesizer DDS adalah baterai fase. Intinya, ini adalah indeks elemen dari Look Up Table (LUT). Untuk setiap periode sinyal clock, nilai di dalamnya meningkat dengan nilai tertentu, sebagai akibatnya, sinus diperoleh pada output. Frekuensi sinyal pada output akan tergantung pada nilai kenaikan akumulator fase - semakin besar itu, semakin tinggi frekuensi. Namun, menurut kriteria Kotelnikov, frekuensi sampling harus minimal 2 kali frekuensi sinyal (untuk menghindari efek melapiskan spektrum), maka pembatasan kenaikan maksimum adalah setengah akumulator fase. Secara umum, kriteria teknik adalah frekuensi sampling = 2,2 dari frekuensi sinyal, oleh karena itu, setelah memutuskan untuk tidak mengambilnya secara ekstrem, saya melepas satu bit lagi, meninggalkan 6 bit yang bertambah dengan baterai fase 8-bit (walaupun jackalite sinus sudah).


Karena simetri yang digunakan, hanya 6 bit yang lebih rendah dari 2 ^ 6 = 64 akan digunakan langsung untuk pengambilan sampel indeks. 2 bit tinggi digunakan untuk mengidentifikasi periode seperempat generasi sinus dan, dengan demikian, mengubah arah tabel traversal. Anda harus mendapatkan sesuatu yang mirip dengan:


 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 

Saat mengatur ulang, kami menginisialisasi semuanya dengan nol, kecuali untuk nilai kenaikan fase, kami mengaturnya menjadi satu. Untuk menjaga kemampuan kode, tabel juga akan diisi dengan nilai selama reset. Dalam proyek nyata, disarankan untuk menggunakan memori blok yang dibangun ke dalam FPGA untuk tujuan tersebut dan membuat file konfigurasi terpisah untuk itu, dan menggunakan inti IP dalam proyek itu sendiri.


Sedikit penjelasan tentang cara kerja simetri. Pada setiap siklus, itu diperiksa (pada 2 bit paling signifikan) di mana kuartal akumulator fase saat ini berada. Jika tertinggi = 00, maka output dalam digit tertinggi adalah 1 (bertanggung jawab atas setengah-gelombang positif), di yang lebih rendah - nilai dari LUT sesuai dengan indeks. Setelah nilai akumulator fase melebihi 63 (kuartal pertama akan berlalu), 01 akan muncul dalam bit tinggi, dan yang lebih rendah akan diisi dengan nol lagi.


Untuk melewatkan LUT dalam urutan terbalik, cukup untuk membalikkan bit paling tidak signifikan dari akumulator fase (itu akan terus meningkat untuk setiap siklus, dan nilainya yang terbalik akan berkurang).


Untuk membentuk setengah-gelombang negatif, kita menulis 0. Pada bit atas output, kita sekarang perlu membalikkan nilai itu sendiri dari tabel sinus. Intinya di sini adalah bahwa Anda perlu mendapatkan salinan cermin dari kuartal sinus, dan jika ini tidak dilakukan, Anda mendapatkan gambar yang sama seperti pada kuartal pertama, tetapi diturunkan 127 ke bawah. Anda dapat memverifikasi ini dengan menghapus invers dalam kode.


Kami mengubah frekuensi dan fase awal


Seperti yang sudah dijelaskan di atas, untuk mengubah frekuensi, perlu untuk mengubah nilai kenaikan fase. Masukan baru akan muncul:


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

Untuk mengubah nilai kenaikan fase, kita cukup pasang pada setiap siklus:


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

Dengan fase awal, semuanya tidak begitu sederhana. Anda harus terlebih dahulu menulisnya ke register perantara, dan mengisi akumulator fase dengan nilai ini hanya jika nilai fase awal pada input tidak sesuai dengan yang disimpan sebelumnya. Ini memunculkan poin penting lain terkait kondisi balapan. Kami sudah memiliki tempat di mana kami menulis phase_acc di register. Anda tidak dapat merekam pada saat yang sama di beberapa tempat, karena data yang datang terlebih dahulu akan direkam. Karena itu, desainnya akan terlihat seperti ini:


 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


Kode testbench untuk Iverilog dan GTKWave memiliki beberapa desain (dengan tanda dolar) yang tidak digunakan dalam ISE Design Suite atau Quartus. Maknanya bermuara pada pemilihan sinyal yang dipantau dan memuatnya ke dalam file, sehingga mereka dapat ditransfer ke simulator. Pekerjaan bangku tes itu sendiri sepele - kami melakukan reset, mengatur frekuensi / fase awal dan menunggu beberapa saat.


 `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 

Grafik waktu


Pada output, kita mendapatkan sesuatu yang mirip dengan sinus dengan frekuensi yang berubah dan fase awal pada titik waktu yang ditentukan dalam testbench. Perlu dicatat bahwa dengan meningkatnya frekuensi, resolusi sepanjang itu (jumlah sampel per periode) menurun, masing-masing, frekuensi clock synthesizer dan ukuran LUT memainkan peran yang menentukan dalam mereproduksi sinus murni (semakin bentuknya mendekati ideal, semakin sedikit komponen sisi dalam spektrum hasil yang dihasilkan). sinyal dan puncak sudah pada frekuensi yang dihasilkan).



Dapat dilihat bahwa sinyal dengan frekuensi kedua sudah tidak semulus yang lainnya. Mari kita lihat lebih dekat.



Dapat dilihat bahwa ini masih sedikit mirip dengan sinus, hasilnya akan menjadi lebih baik setelah sinyal tersebut dilewatkan melalui filter anti-aliasing (Low-Pass Filter).


Sumber proyek tersedia di sini .


Sumber


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


All Articles