
في هذا المنشور ، سوف أشارك كيف فهمت كتابة برنامج توليف DDS على Verilog. سيتم استخدامه لإنشاء التذبذب الجيوب الأنفية ، حيث يمكن ضبط وتكرار المرحلة الأولى وحسابها للاستخدام مع DAC أحادي القطب ذي 8 بتات. كيف يعمل المزج هو مكتوب بشكل جيد في مقال في مجلة المكونات والتقنيات . لتقليل مقدار الذاكرة المستخدمة لجدول الجيب ، يتم استخدام التماثل.
للتجميع تحت Linux اعتدت Iverilog ولعرض GTKWave. للراحة ، تم كتابة Makefile بسيط ، ربما سيكون مفيدًا لشخص ما. في البداية ، باستخدام برنامج التحويل البرمجي iverilog ، نحصل على ملف tb.out ، ثم نرسله إلى محاكي vvp ، المثبت مع iverilog. نتيجة لذلك ، ستنشئ vvp out.vcd ، والذي يحتوي على جميع المتغيرات (الإشارات) المستخدمة في المشروع. الهدف من العرض ، بالإضافة إلى ما سبق ، سوف يطلق 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
بادئ ذي بدء ، تحتاج إلى وضع جدول جيب المستقبل في الذاكرة ، لأنني كتبت نص بيثون بسيط يقسم ربع فترة الجيب إلى 64 نقطة وينشئها في تنسيق يمكن نسخه بعد ذلك إلى الكود المصدري. منذ أن قمت بتطبيق DDS من أجل DAC أحادي القطب الخارجي بدقة لا تزيد عن 8 بتات ، يجب أن تكون السعة الجيبية في حدود 0 إلى 256 ، حيث تقع النصف السلبي في النطاق 0 ... 127 ، والنصف الموجب في 128 ... 255 . في هذا الصدد ، يتم ضرب قيم الجيب التي تم الحصول عليها (من 0 إلى pi / 4) بـ 127 ، ثم يتم إضافة 127 إليها ، ونتيجة لذلك ، يتم الحصول على قيم الربع الأول من الفترة ، التي تبلغ سعاتها 128 ... 256.
أود أن ألفت الانتباه إلى حقيقة أنه مع هذا التكوين ، سيكون الجيب عند إخراج DAC مكونًا ثابتًا. من أجل إزالته ، من الضروري تمريره من خلال مكثف.
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
بما أن دالة الجيب متماثلة (فردية) ، يمكنك العثور على أول خط تماثل (x) = - sin (pi + x). يتميز التماثل الثاني بحقيقة أن الحصول على جدول لربع الفترة ، يمكن الحصول على الربع الثاني من خلال الانتقال من خلال الجدول بالترتيب العكسي (حيث أن الجيب في النصف الأول يزيد ، ثم ينخفض).
الجزء الأكبر من المزج DDS هو بطارية المرحلة. في جوهره ، إنه فهرس عنصر من جدول البحث لأعلى (LUT). لكل فترة من إشارة الساعة ، تزيد القيمة فيه بمقدار معين ، ونتيجة لذلك ، يتم الحصول على جيب عند الإخراج. يعتمد تواتر الإشارة في الخرج على قيمة الزيادة في تراكم الطور - وكلما زاد ، زاد التردد. ومع ذلك ، وفقًا لمعيار Kotelnikov ، يجب أن يكون تردد أخذ العينات على الأقل ضعف تردد الإشارة (لتجنب تأثير تراكب الطيف) ، وبالتالي فإن الحد الأقصى للزيادة هو نصف تراكم الطور. بشكل عام ، المعيار الهندسي هو تردد أخذ العينات = 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
عند إعادة الضبط ، نقوم بتهيئة كل شيء باستخدام الأصفار ، باستثناء قيمة زيادة المرحلة ، فقم بتعيينها على واحد. للحفاظ على توليف الكود ، سيتم ملء الجدول أيضًا بالقيم أثناء إعادة التعيين. في مشروع حقيقي ، من المستحسن استخدام ذاكرة الكتلة المضمنة في FPGA لهذه الأغراض وإنشاء ملف تكوين منفصل له ، واستخدام لب IP الأساسي في المشروع نفسه.
شرح بسيط حول كيفية عمل التماثل. في كل دورة ، يتم فحصه (على البتات الأكثر أهمية) التي يوجد بها ربع جهاز تجميع الطور حاليًا. إذا كانت أعلى = 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
يحتوي رمز 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
توقيت المخططات
في الخرج ، نحصل على شيء مشابه لجيب مع تغيير التردد والمرحلة الأولية في النقاط الزمنية المحددة في testbench. تجدر الإشارة إلى أنه مع زيادة التواتر ، يتناقص دقة طوله (عدد العينات لكل فترة) ، على التوالي ، يلعب تردد ساعة المزج وحجم LUT دوراً حاسماً في إعادة إنتاج الجيب الخالص (كلما اقترب شكله من النموذج المثالي ، كلما قلت المكونات الجانبية في الطيف الناتج إشارة وسوف تكون الذروة بالفعل في وتيرة ولدت).

يمكن أن نرى أن الإشارة ذات التردد الثاني لم تقم بالفعل بسلاسة الجيب الخاص بالأخرى. دعنا نلقي نظرة فاحصة.

يمكن أن نرى أن هذا لا يزال يشبه إلى حد ما الجيب ، وسوف تصبح النتيجة أفضل بعد أن يتم تمرير هذه الإشارة من خلال مرشح مكافحة التعرج (مرشح تمرير منخفض).
مصادر المشروع متاحة هنا .
مصادر