پیاده سازی فیلتر FIR در Vivado

با دروازه ورود به دنیای پردازش سیگنال در FPGA از طریق فراگیری الفبای پیاده سازی فیلتر FIR در Vivado تنها چند قدم فاصله دارید.
طراحی فیلتر FIR با ابزار System Generator for DSP

مقدمه

چند هفته پیش بود که آموزشی در رابطه با طراحی یک فیلتر FIR با ابزار System Generator for DSP در پایگاه دانش هگزالینکس منتشر شد. این هفته قصد داریم به شما گام‌های طراحی و پیاده سازی فیلتر FIR در Vivado را آموزش بدهیم. اگر علاقمند به پیاده سازی الگوریتم‌های پردازش سیگنال در FPGA هستید، با ما همراه شوید.

پیش زمینه

فرض کنید براساس طراحی‌های صورت گرفته، مشخصات فیلتر دیجیتال ما به شرح زیر است. در مقاله قبلی به فلسفه پیاده سازی این فیلتر اشاره کردیم و مطمئناً مطالعه مجدد آن خالی از لطف نخواهد بود. مضاف اینکه شما آمادگی کافی برای همراه شدن با آموزش را نیز پیدا می‌کنید.

  • فرکانس نمونه برداری: (Sampling Frequency = 1.5MHz)
  • فرکانس قطع اول: (FStop 1 = 270KHz)
  • فرکانس گذر اول: (FPass 1 = 300KHz)
  • فرکانس گذر دوم: (FPass 2 = 450KHz)
  • فرکانس قطع دوم: (FStop 2= 470KHz)
  • تضعیف در هر دو سمت: (Att. = 54dB)
  • ریپل باند گذر: (Pass Band Ripple = 1)

هدف ما پیاده سازی این فیلتر میان گذر، تک کانال و تک نرخ با استفاده از FIR Compiler IP Core در Vivado است. برای آشنایی با پارامترها و مشخصه‌های فیلتر می‌توانید به مقاله آموزشی مقدمه‌ای بر فیلترها مراجعه کنید.

پیش از شروع دقت کنید که این یک مقاله آموزشی صفر تا صد در رابطه با پیاده سازی فیلترهای دیجیتال روی FPGA نیست و فرض بر این است که شما با مفاهیم اولیه پردازش سیگنال، فیلترهای دیجیتال و همینطور ابزارهای طراحی مورد استفاده در این حوزه، آشنا هستید. یعنی شما توانایی بکارگیری Matlab را دارید، محیط توسعه Vivado‌ را می‌شناسید و با تجربه فراخوانی و کار با IP Core ها را دارید. اما اگر این شرایط را ندارید، توضیحات به شکلی است که مطمئناً با کمی تلاش و تکرار موفق به اجرای گام به گام آن خواهید شد. در این آموزش تقریباً تمامی جزئیاتی که باید در زمان طراحی فیلتر مد نظر طراح قرار بگیرد، پوشش داده شده است.

ابزارهای مورد نیاز برای طراحی و پیاده سازی عبارتند از:

  • نرم افزار Matlab و ابزار FDATool برای طراحی فیلتر
  • محیط توسعه Vivado و ابزار IP Integrator برای پیاده سازی فیلتر
  • ابزار Modelsim یا Vivado Simulator برای شبیه سازی فیلتر

در این آموزش از نسخه 2017b نرم افزار Matlab و نسخه 2018.2 محیط توسعه Vivado به همراه سیمولاتور اختصاصی آن به نام Vivado Simulator – XSim برای طراحی، پیاده سازی و شبیه سازی استفاده شده است. اگر نسخهِ ابزارهای شما متفاوت است ممکن است تصاویری که مشاهده می‌کنید با تصاویر ما متفاوت باشد، اما به طور کلی روال کار هیچ تفاوتی ندارد.

برخلاف طراحی قبلی در System Generator اینبار برای شبیه سازی و ارزیابی عملکرد فیلتر تنها از یک سیگنال تست استفاده خواهیم کرد. سیگنال تست ما یک سیگنال Chirp است که فرکانسش بین فرکانس صفر تا ۷۵۰ مگاهرتز تغییر می‌کند. شما می‌توانید از هر نوع سیگنال تستی که تمایل دارید، استفاده کنید. بد نیست در انتهای کار خودتان را محک بزنید و با استفاده از یک سیگنال تصادفی هم عملکرد فیلتر را بررسی کنید.

فرایند طراحی و ارزیابی صحت عملکرد فیلتر در سه گام انجام می‌شود.

  • طراحی فیلتر در Matlab با استفاده از ابزار FDA Tools و تولید ضرائب فیلتر
  • بررسی عملکرد فیلتر در Matlab و تولید بردارهای تست
  • پیاده سازی و شبیه سازی فیلتر در محیط توسعه Vivado

در گام اول و دوم ما با استفاده از Matlab‌ و ابزار FDATool یک فیلتر میان گذر طراحی می‌کنیم و سپس بردارهای مناسبی برای تست آن تولید می‌کنیم. بعد با اعمال این بردارهای تست به فیلتر پاسخ حوزه زمان و فرکانس آن را در Matlab ترسیم می‌کنیم.

در گام سوم فیلتر طراحی شده را با استفاده از FIR Compiler IP Core در محیط توسعه Vivado پیاده‌ سازی می‌کنیم و بعد با بردارهای تستیِ تولید شده در گام قبلی و فراخوانی آن‌ها در تست بنچ، فیلتری را که پیاده سازی کردیم، شببیه سازی می‌کنیم. در انتها نیز نتایج شبیه سازی را با نتایج مطلوب در Matlab مقایسه می‌کنیم.

طراحی و پیاده سازی فیلتر FIR

گام اول: طراحی فیلتر

برای طراحی فیلتر مراحل زیر را به دقت اجرا کنید.

۱- نرم افزار Matlab را اجرا کنید.

۲- یک پوشه کاری مناسب برای پروژه خود ایجاد کنید.

۳- در صفحه Command با تایپ عبارت fdatool یا filterDesigner  ابزار طراحی فیلتر Matlab را فراخوانی کنید.

پیاده سازی فیلتر FIR در Vivado
فراخوانی ابزار طراحی فیلتر در Matlab

۴- بلافاصله پنجره تنظیمات آن روی صفحه ظاهر می‌شود. در این پنجره شما می‌توانید با مقدار دهی مناسب فیلدهای مختلف یک فیلتر میان گذر طراحی کنید. برای این کار پارامترهای فیلتر را به صورت زیر تنظیم کنید.

Response Type: Bandpass
Units: KHz
Sampling Frequency (Fs) = 1.5 MHz
Fstop 1 = 270 kHz
Fpass 1 = 300 kHz
Fpass 2 = 450 kHz
Fstop 2 = 480 kHz
Attenuation on both sides of the passband = 54 dB (Astop1 and Astop2)
Pass band ripple = 1 (Apass)

بعد از تنظیم پارامترها پنجره تنظیمات FDATool مشابه شکل زیر خواهد بود.

طراحی فیلتر FIR با ابزار System Generator for DSP
پارامترهای فیلتر bandpass در FDATool

۵- روی گزینه Design Filter کلیک کنید. تا ابزار فیلتر مورد نظر شما را طراحی کند.

با اتمام کار طراحی پاسخ فرکانسی فیلتر، بروز می‌شود و نتیجه کار شکل زیر خواهد بود.

طراحی فیلتر FIR با ابزار System Generator for DSP
طیف پاسخ فرکانسی فیلتر bandpass در FDATool

حالا با توجه به طراحی انجام شده و اطلاعات قابل مشاهده در این صفحه می‌توانید مرتبه فیلتر را مشاهده کنید. فیلتر ما از مرتبه ۹۱ است. از این رو ۹۲ ضریب خواهد داشت.

۶- بعد از اتمام فرایند طراحی در Matlab به تنها چیزی که نیاز داریم گرفتن یک خروجی از ضرائب فیلتر است، پس در ادامه از ضرائب تولید شده یک خروجی مناسب تولید می‌کنیم. روی گزینه Export در منوی فایل کلیک کنید تا پنجره تنظیمات آن برای شما فعال شود. در فیلد Numerator عبارت Num را وارد کنید تا ضرائب با نام Num به محیط کاری Matlab ارسال شوند. سایر تنظیمات پیش فرض را بپذیرید و روی Export کلیک کنید.

طراحی فیلتر FIR با ابزار System Generator for DSP
خروجی گرفتن از ضرائب فیلتر

۷- پنجره تنظیمات FDATool را ببندید.

حالا اگر به صفحه Workspace در Matlab نگاهی بیاندازید، متغیر Num را در لیست متغیرها مشاهده خواهید کرد. با تایپ عبارت Num در صفحه Command می‌توانید مقدار این ضرائب را نیز مشاهده کنید. بد نیست این ضرائب را کمی بیشتر بررسی کنید و مقدار مینیمم و ماکزیمم آن را چک کنید تا رنج دینامیکی ضرائب را متوجه شوید. از این رنج دینامیکی برای تعیین فرمت ممیز ثابت ضرائب فیلتر استفاده می‌شود.

از رنج دینامیکی برای تعیین فرمت ممیز ثابت ضرائب فیلتر استفاده می‌شود.

کار طراحی فیلتر به اتمام رسید. حالا شما یک فیلتر طراحی شده و کامل دارید که ابتدا باید آن را تست کنید. پس نیاز به یک برنامه‌ای دارید که بردارهای تست را به این فیلتر اعمال و پاسخ‌‌های آن را ترسیم کند.

گام دوم: بررسی عملکرد فیلتر

برای تست عملکرد این فیلتر ما یک برنامه ممیز ثابت شده آماده داریم که از آن استفاده می‌کنیم. کافی است از منوی Editor در محیط Matlab روی گزینه New > Script کلیک کنید و کدهای زیر را در آن کپی کنید. در ادامه کمی بیشتر در رابطه با این کد صحبت خواهیم کرد.

% copyright 2020 Hexalinx.com

%Fixed point mathematics
f = fimath('OverflowAction','Wrap','RoundingMethod','Floor');

Q0806 = numerictype(1, 8, 6);
Q1212 = numerictype(1,12,12);
Q2218 = numerictype(1,22,18);

%%Time specifications:
Fs = 1.5e6;                       % samples per second
dt = 1/Fs;                        % seconds per sample
StartTime = 0;                    % seconds
StopTime  = 100e-3;               % seconds
t = transpose(StartTime:dt:StopTime-dt);

%%Sine wave:
%Fc = 20e3;                        % hertz
%x = cos(2*pi*Fc*t);

%%Chirp wave:
x    = chirp(t,0,50,750e6);
x_q  = fi(x,Q0806,f);
x_scaled = int(x_q);               % x_q*2^6

%%Coefficeuint :
b    = Num;
b_q  = fi(b,Q1212,f);
b_scaled = int(b_q);               % b_q*2^12
a    = 1;

%%Filtr the input signal: double
% y = filter(b,a,x);

%%Filtr the input signal: fixed
y = filter(b_scaled,a,x_scaled);
y_q  = fi(y/2^18,Q2218,f);         % remove scaling (12+6)
y_q  = fi(y_q,Q0806,f);            % re quantize to desire format
y_scaled = int(y_q);               % y_q*2^6

% Plot the signal versus time:
figure;
% plot(t,x);
plot(t,x_scaled);
xlabel('time (in seconds)');
title('Signal versus Time');
hold on
% plot(t,y);
plot(t,y_scaled);
legend('Input Data','Filtered Data');
zoom xon;

در ابتدای کد، تنظیمات مربوط به اپراتور fixed point در Matlab تعریف شده است. همچون آموزش قبل، فرمت Q0806 برای سیگنال ورودی و فرمت Q1212 برای ضرائب فیلتر تعریف شده است. بنابراین ورودی ما یک سیگنال علامت دار ۸ بیتی با ۶ بیت اعشار است. ضرائب فیلتر هم اعدادی علامت دار و ۱۲ بیتی در نظر گرفته شده‌اند. مشاهده می‌کنید که تمامی ۱۲ بیت آن برای نمایش بخش اعشاری استفاده شده است.

بدیهی است خروجی ما حداقل دارای ۱۸ بیت اعشار خواهد بود. پس لازم است بعد از محاسبه رشد بیت فیلتر یک فرمت مناسب با ۱۸ بیت اعشار برای آن تعریف کنیم. در حالت تئوریک ما ۷ بیت رشد بیت خواهیم داشت (چرا؟)، در نتیجه در بدترین حالت طول  (عرض) بیت خروجی برابر با ۲۷ خواهد بود. اما نکته اینجاست که FIR Compiler IP Core  به جای محاسبه (log2(n که در آن n تعداد ضرائب است، مقدار رشد بیت را بر اساس رابطه زیر محاسبه می‌کند که در آن an نماینده ضریب n-ام فیلتر است. اگر علاقمند به بررسی جزئیات بیشتر در این رابطه هستید. به صفحه ۶۵ سند راهنمای نرم افزاری PG149 مراجعه کنید.

$$B=ceil[\log_{2}{\lgroup\displaystyle\sum\limits_{n=0}^{n-1} | a_n | \rgroup}]$$

برای اینکه کمتر خسته شویم از محاسبه رشد بیت صرف نظر می‌کنیم و با توجه به تجربه‌ای که از پیاده سازی قبلی این فیلتر در System Generator‌ داریم فرمت Q2218 را برای خروجی تعریف می‌کنیم. مطمئن باشید بعد از حساب و کتاب دقیق باز هم به عدد ۲۲ می‌رسیم. تأیید این موضوع را نیز در ادامه همین مقاله خواهید دید.

ما در انتهای کار با کاهش دقت، خروجی‌ها را روی فرمت Q0806 نگاشت می‌کنیم تا فرمت ورودی و خروجی یکسان شود. این کنترل عرض (طول) بیت بعد از هر مرحله پردازش در الگوریتم‌های پردازش سیگنال بسیار مهم است. شما به عنوان طراح باید همیشه به این موضوع دقت کنید.

در ادامه این فایل، فرکانس نمونه برداری و متغیر زمان مورد استفاده برای ساخت سیگنال Chirp تعریف شده است. این متغیر ‘t’ نام دارد. سیگنال نهایی مورد استفاده ما برای تست فیلتر x نام دارد. یک نسخه ممیز ثابت شده از این سیگنال به نام x_q و یک نسخه مقیاس بندی شده از آن به نام x_scaled نیز تولید شده است. ما در نهایت از این سیگنال x_scaled برای ارزیابی عملکرد فیلتر استفاده خواهیم کرد. با استفاده از این سیگنال، ما به جای کار با مقادیر اعشاری با یک مقدار صحیح (Integer) محاسبات را انجام می‌دهیم. اینکار باعث می‌شود فرایند شبیه سازی کد بعد از پیاده سازی به شدت تسهیل شود. زیرا در FPGA شما چیزی به نام اعداد اعشاری ندارید و هر چه می‌بینید به صورت اعداد صحیح است. در صورتی که مفهوم مقیاس بندی برای شما نامأنوس است، لازم است آموزش دو قسمتی محاسبات ریاضی در FPGA را مطالعه کنید.

همین کار را برای ضرائب فیلتر هم تکرار می‌کنیم و سه متغیر b و b_q‌ و b_scaled را مقدار دهی می‌کنیم. دقت کنید که ضرائب فیلتر ما در متغیر Num ذخیره شده بودند و کمی قبل‌تر آن‌ها را به محیط کاری Matlab منتقل کردیم.

بعد با استفاده از دستور filter در Matlab  سیگنال x_scaled‌ را فیلتر می‌کنیم. چون تمامی ورودی‌های دستور filter به صورت صحیح (integer) و مقیاس بندی تعریف شده‌اند، خروجی آن نیز از نوع صحیح و مقیاس بندی شده خواهد بود. در نتیجه این خروجی باید بر عدد 218 تقسیم شود تا به اثر مقیاس بندی ورودی‌ها حذف شود. در ادامه خروجی فیلتر در دو مرحله (صرفاً برای اینکه مسأله کاملاً شفاف شود) ابتدا به فرمت Q2218 و سپس فرمت نهایی Q0806 نگاشت شده است. خروجی نهایی ما y_scaled است.

مجدداً تکرار می‌کنم که استفاده از داده‌های مقیاس بندی شده (scaled_)‌ و یا داده‌های میمز ثابت (q_) در دستور filter به یک پاسخ منتهی می‌شود. اما استفاده از داده‌های مقیاس بندی شده در عمل کار شبیه سازی را آسان‌تر می‌کند.

در انتهای فایل هم سیگنال‌های حوزه زمان ورودی و خروجی روی هم ترسیم شده‌اند تا اثر اعمال فیلتر روی ورودی کاملاً ملموس شود. شما می‌توانید در صورت تمایل طیف فرکانسی سیگنال ورودی و طیف سیگنال خروجی را نیز باهم مقایسه کنید.

پیاده سازی فیلتر FIR در Vivado
ترسیم و مقایسه سیگنال تست Chirp، قبل و بعد از فیلتر شدن

نکته:

روشی که برای طراحی ممیز ثابت (fixed point) فیلتر توضیح داده شد، تنها یکی از چندین روشی است که شما می‌توانستید، استفاده کنید. به عنوان مثال خود ابزار filterDesigner نیز چنین امکانی را در اختیار شما قرار می‌دهد که در آموزش‌های بعدی به آن خواهیم پرداخت.

گام سوم: پیاده سازی و شبیه سازی

خب تا به اینجا کار طراحی فیلتر و تولید بردارهای تست را به اتمام رساندیم. حالا باید پیاده سازی و شبیه سازی آن را انجام دهیم. برای پیاده سازی این فیلتر از IP Core های آماده Xilinx‌ استفاده خواهیم کرد. همینطور برای ساده سازی فرایند پیاده سازی فیلتر FIR در Vivado به جای فراخوانی IP‌ در کدهای HDL  از ابزار IP integrator استفاده خواهیم کرد و سپس یک wrapper و یک تست بنچ به‌ پروژه اضافه می‌نماییم. روش طراحی مورد استفاده در این آموزش کاملاً سنتی است و سعی شده در آن جنبه‌های آموزشی در نظر گرفته شود. در آموزش‌های بعدی در رابطه با چگونگی کدنویسی HDL برای این فیلتر صحبت خواهیم کرد.

۱- از منوی Start گزینه Xilinx Design Tools > Vivado 2018.2 را انتخاب کنید. سپس در صفحه خوشامدگویی آن یک پروژه جدید بسازید. در هنگام ایجاد پروژه نیازی به تعیین سورس فایل‌ ندارید. بعلاوه اینکه چون قصد تست سخت افزاری فیلتر را نداریم از هر تراشه یا بورد ارزیابی که تمایل دارید، استفاده کنید.

پیاده سازی فیلتر FIR در Vivado
ایجاد یک پروژه جدید در Vivado

۲- بعد از باز شدن پروژه در محیط توسعه Vivado با استفاده از گزینه‌های موجود در Flow Navigator یک بلوک دیاگرام جدید ایجاد کنید.

۳- در صفحه ‌بلوک دیاگرام با کلیک روی علامت + در مرکز صفحه و با جستجو در لیست IP‌ های Xilinx یک بلوک Fir Compiler به طرح خود اضافه کنید. با این کار نمونه از FIR Compiler IP Core به پروژه شما اضافه می‌شود.

پیاده سازی فیلتر FIR در Vivado
اضافه کردن یک بلوک FIR Compiler به صفحه خالی بلوک دیگرام

۴- روی بلوک فیلتر دو بار کلیک کنید تا ویزارد تنظیمات آن اجرا شود. حالا نوبت به سفارشی سازی این بلوک است. برای آشنایی با جزئیات کامل و شیوه استفاده از این IP به سند PG149 مراجعه کنید. در ادامه با اعمال پارامترهای مناسب در ویزارد تنظیمات این IP ، فیلتر خود را پیاده سازی خواهیم کرد.

پیاده سازی فیلتر FIR در Vivado
پنجره ویزارد تنظیمات FIR Compiler

صفحه Filter Option

در ابتدای کار باید مشخصات کلی فیلتر را تعیین کنیم.

بخش Filter Coefficients : در این بخش ضرائب فیلتر را تعیین می‌کنیم. برای تعیین ضرائب به شکل زیر عمل کنید.

به نرم افزار Matlab برگردید و این یک خط کد را اجرا کنید تا ضرائب فیلتر در قالب یک فایل متنی تولید شود. ما می‌توانیم ضرائب را به صورت دستی در فیلد Coefficient Vector وارد کنیم، اما چون تعداد ضرائب نسبتاً زیاد است بهتر است آن‌ها را در یک فایل بنویسیم و آدرس این فایل را در فیلد Coefficient File اضافه کنیم.

% Coefficients
FID = fopen('coef.coe','w');
fprintf(FID,(sprintf('%d,',b_scaled)));

فایل ضرائب باید به فرمت خاصی تعیین شود که توضیحات آن در صفحه ۴۰ سند PG149 و بخش Filter Coefficient Data ارائه شده است. فایل تولید شده در Matlab را ویرایش کنید و فایل عبارت زیر را به ابتدای آن اضافه کنید.

radix=10;
coefdata=

در انتهایِ بردار ضرائب کاما (،) را با یک سمیکولون (؛) جایگزین کنید. همانطور که احتمالاً متوجه شده‌اید در خط اول این فایل فرمت یا مبنای اعداد و در خط دوم خود ضرائب که با یک کاما (،) از هم جدا شده‌اند، قرار می‌گیرند. فایل نهایی شما مطابق شکل زیر خواهد بود. دقت کنید که پسوند انتخابی برای این فایل coe. است. این پسوند را تغییر ندهید چون به صورت اتوماتیک توسط Fir Compiler قابل شناسایی است.

radix=10;
coefdata=-6,10,14,-15,-20,17,20,-13,-12,-3,-8,24,29,-47,-47,56,47,-47,-27,16,-11,22,48,-55,-68,58,51,-25,-1,-40,-70,105,124,-138,-127,102,50,14,102,-199,-304,403,498,-575,-632,659,659,-632,-575,498,403,-304,-199,102,14,50,102,-127,-138,124,105,-70,-40,-1,-25,51,58,-68,-55,48,22,-11,16,-27,-47,47,56,-47,-47,29,24,-8,-3,-12,-13,20,17,-20,-15,14,10,-6;

شما در این بخش امکان تعیین چند دسته ضریب برای یک فیلتر را نیز دارید. در مواردی که ماهیت سیگنال‌های ورودی به فیلتر متفاوت شود، استفاده از این قابلیت کمک شایانی به مدیریت بهتر منابع مصرفی می‌کند. گزینه Number of Coefficient Sets را روی یک (۱) و گزینه Use reloadable Coefficient را تیک نخورده (غیرفعال) رها کنید.

پیاده سازی فیلتر FIR در Vivado
سفارشی سازی نحوه دسترسی IP به ضرائب فیلتر طراحی شده

بخش Filter Specification : در این بخش نوع فیلتر و پارامترهای مربوط به آن تنظیم می‌شود. فیلتر مورد نظر ما یک فیلتر تک نرخ ساده است از این رو بدون اعمال هیچ تغییری در فیلد Filter Type‌ گزینه Single Rate را انتخاب کنید. این نوع از فیلتر به پارامتر دیگری نیاز ندارد.

پیاده سازی فیلتر FIR در Vivado
تعیین نوع فیلتر

صفحه Channel Specification

بخش Interleaved Channel Selection : در این بخش تعداد کانال‌های فیلتر تعیین می‌شود. فیلتر ما یک فیلتر تک کاناله است. پس در فیلد Channel Sequence گزینه Basic را انتخاب کنید و برای سایر تنظیمات این بخش مقادیر پیش فرض را بپذیرید.

بخش Parallel Channel Specification : در این بخش هم پارامتر پیش فرض فیلد Number of Path را بپذیرید و آن را برابر با یک (۱) قرار دهید.

پیاده سازی فیلتر FIR در Vivado
سفارشی سازی تعداد کانال‌ها و مسیرهای فیلتر

دقت کنید که دو مفهوم تعداد مسیر (Number of Path) و تعداد کانال (Number of Channel) کاملاً با یکدیگر متفاوت هستند، پس بین آن‌ها تمایز قائل شوید. در فیلترهای چندکاناله از یک فیلتر واحد که با کلاک بالاتری کار می‌کند برای فیلتر کردن چند مسیر دیتای متفاوت استفاده می‌شود در حالی که در فیلتر چند مسیره تعداد فیلترها برابر با تعداد مسیرهاست و کلاک بالاتری هم مورد نیاز نیست، در عوض مدارات کنترلی و حافظه ضرائب فیلتر به صورت اشتراکی استفاده می‌شوند.

بخش Hardware Oversampling Specification : در این بخش نرخ دیتای ورودی و کلاکی که قرار است به فیلتر اعمال شود، تعیین می‌شود. هر چقدر نسبت Clock cycle per input عدد بزرگتری باشد، منابع مصرفی روی تراشه کمتر خواهد بود. وارد کردن مقادیر دقیق در فیلدهای این بخش الزامی نسیت ولی نسبت آن‌ها باید دقیق باشد. در اینجا ما فرض می‌کنیم که نرخ کلاک و نرخ سمپل‌های ورودی باهم برابر است، این مسأله عموماً با توجه به سخت افزار مورد استفاده تعیین می‌شود. پارامترهای فیلتر در این بخش را به این صورت تنظیم کنید.

Select Format: Frequency Specification
Sample Period (Clock Cycle): 1
Input Sampling Frequency (MHz): 1.5
Clock Frequency (MHz): 1.5
پیاده سازی فیلتر FIR در Vivado
سفارشی سازی نرخ داده و کلاک کاری فیلتر

بعد از تعیین مقادیر مناسب در این فیلدها در پایین پنجره اطلاعات تکمیلی را که به صورت اتوماتیک محاسبه می‌شوند، خواهید دید. به عنوان تمرین می‌توانید مقدار کلاک را تغییر دهید و اثر آن‌ را بررسی کنید. رنج‌های قابل اعمال در هر فیلد جلوی آن نوشته شده است.

صفحه Implementation

بخش Coefficient Options : در این بخش تنظیمات مرتبط با نحوه ممیز ثابت کردن ضرائب تعیین می‌شود. از آنجاییکه فایل coe. را با مقادیر Integer و کاملاً صحیح در مبنای ۱۰ ساختیم، در فیلد Quantization گزینه Integer Coefficient‌ و در فیلد Coefficient Width مقدار ۱۲ را وارد کنید. فیلد Coefficient Type هم به صورت اتوماتیک روی گزینه Signed تنظیم شده است.

بخش Coefficient Structure : در حالت کلی FIR Compiler قادر به شناسایی و آنالیز ماهیت ضرائب است و می‌تواند در صورت متقارن بودن آن‌ها یکسری بهینه سازی روی ساختار فیلتر اعمال کند. از این رو در این بخش گزینه رادیویی Inferred را انتخاب کنید.

پیاده سازی فیلتر FIR در Vivado
سفارشی سازی طول بیت و نحوه کوآنتیزاسیون ضرائب

بخش Data Path Options : در آخرین بخش از این صفحه باید تنظیمات دیتای ورودی را تعیین کنیم. اگر به خاطر داشته باشید ما دیتای ورودی را به صورت مقادیر ممیز ثابت علامت دار ۸ بیتی با ۲ بیت صحیح و ۶ بیت اعشار در نظر گرفتیم. اما در اینجا الزامی به تعیین تعداد بیت‌های اعشار ورودی نداریم و تنها تعیین تعداد بیت‌های آن کفایت می‌کند.

مشاهده می‌کنید که در انتهای این صفحه عرض (طول) بیت دیتای خروجی و تعداد بیت اعشار آن به صورت اتوماتیک تعیین می‌شود. اگر فیلد Input Fractional Width را فعال می‌کردید و مقدار آن را برابر با ۶ قرار می‌دادید. آنگاه فیلد Output Fractional Bit به صورت اتوماتیک به ۶ تغییر می‌کرد.

پیاده سازی فیلتر FIR در Vivado
سفارشی سازی عرض (طول) بیت خروجی

اگر به مطالبی که تا به اینجا بیان شد دقت کرده باشید، حتماً اولین چیزی که توجه شما را جلب می‌کند عدد ۲۲ در فیلد Output Width است. در واقع مطابق آنچه پیش‌تر هم به آن اشاره کردیم، Fir Compiler به صورت اتوماتیک با توجه به تعداد ضرائب و رنج دینامیکی آن‌ها عرض بیت خروجی را محاسبه کرده است.

صفحه Detailed Implementation

در این صفحه معماری فیلتر و تنظیمات نسبتاً پیشرفته تر پیاده سازی تعیین می‌شود. اکثر پارامترهای مشخص شده در این صفحه وابسته به ماهیت تراشه‌ FPGA هستند. معمولاً انتخاب گزینه اتوماتیک، یک طراحی به اندازه کافی بهینه، در اختیار ما قرار می‌دهد. در فیلد Filter Architecture معماری Systolic Multiply Accurate را انتخاب کنید. (البته با توجه به تنظیمات اعمال شده در صفحات قبل گزینه دیگری هم ندارید.)

بخش Optimization Options : در این بخش استراتژی‌های سنتز و پیاده سازی فیلتر قابل سفارشی سازی است. همه مقادیر پیش فرض را بپذیرد و اجازه بدهید که پیاده سازی فیلتر با هدف کمینه کردن منابع مصرفی روی تراشه انجام شود.

پیاده سازی فیلتر FIR در Vivado
سفارشی سازی استراتژی سنتز و پیاده سازی

بخش Memory Options : در این بخش نحوه اختصاص حافظه به فیلتر قابل سفارشی سازی است. مجدداً همه مقادیر پیش فرض را بپذیرید.

بخش DSP Slice Column Options : در این بخش نحوه اختصاص بلوک‌های DSP به فیلتر قابل سفارشی سازی است. برای بار سوم همه مقادیر پیش فرض را بپذیرید.

پیاده سازی فیلتر FIR در Vivado
سفارشی سازی الگوی استفاده از منابع تراشه

صفحه Interface

در نسخه‌های قدیمی تر FIR Compiler امکان انتخاب نوع اینترفیس برای طراح وجود داشت. شما می‌توانستید از گزینه‌های AXI‌ یا Native استفاده کنید، اما در نسخه‌های جدید برای کلیه طراحی‌‌ها ملزم به استفاده از اینترفیس AXI هستیم. این اینترفیس از نوع AXI Stream‌ است و کار با آن چندان دشوار نیست. لازم به ذکر است که معمولاً از اینترفیس AXI برای ارتباط با پردازنده‌ها استفاده می‌شود و مزیت اصلی آن هنگامی که از تراشه Zynq یا پردازشگر MicroBlaze‌ استفاده کنید، نمایان می‌شود.

بخش Data Channel Options : در این بخش قابلیت اضافه کردن یا حذفِ برخی از سیگنال‌های انتخابی اینترفیس AXI همچون TLAST‌  یا TUSER و غیره … در اختیار طراح قرار داده شده است. اعمال تغییر در این سیگنال‌ها به محل قرارگیری فیلتر در یک سیستم و همینطور ملاحظات هندشیک آن با دیگر بلوک‌های سیستم بستگی دارد. در اینجا برای ساده سازی اینترفیس و تسهیل فرایند شبیه سازی آن بهتر است این سیگنا‌ل‌ها غیر فعال باشند.

پیاده سازی فیلتر FIR در Vivado
سفارشی سازی سیگنال‌های اختیاری اینترفیس AXI

دو بخش Configuration Channel Option‌ و Reload Channel Option‌ در این صفحه غیرفعال هستند. این دو بخش در صورتی که از فیلترهای چند کاناله با قابلیت بارگذاری چند دسته ضریب استفاده می‌کردیم، فعال می‌شدند. پس فعلاً با آن‌ها کاری نداریم.

بخش Control Signals : آخرین تنظیمات ویزارد سفارشی سازی فیلتر مربوط به سیگنال‌های کنترلی ریست و فعال ساز کلاک است. به صورت ذاتی استفاده از ریست برای فیلترها الزامی نیست و بیشتر در زمان شبیه سازی مورد نیاز هستند. تصمیم گیری برای استفاده یا عدم استفاده از سیگنال فعال ساز کلاک هم با توجه به استراتژي‌های مدیریت توان مصرفی در یک طرح صورت می‌پذیرد. در صورت تمایل می‌توانید از این دو سیگنال در طرح خود استفاده کنید اما ما در اینجا این کار را نمی‌کنیم و سیگنال کنترلی فیلتر را غیرفعال باقی نگه می‌داریم.

پیاده سازی فیلتر FIR در Vivado
سفارشی سازی سیگنال‌های کنترلی فیلتر

صفحه Summary

مسیر سفارشی سازی کمی طولانی بود. خوشبختانه به انتهای این مسیر رسیدیم و اکنون در صفحه Summary کلیه مشخصات انتخابی ما برای فیلتر قابل مشاهده هست. یک بار این مشخصه‌ها را مرور کنید. تأخیر فیلتر بعد از پیاده سازی چند کلاک خواهد بود؟

علاوه بر این در سمت چپ صفحه ویزارد و در تب Implementation Details هم می‌توانید تخمینی از منابع مصرفی و مشخصات اینترفیس مورد استفاده در این فیلتر را مشاهده کنید. با توجه به اطلاعات این تب، تعداد بلوک‌های‌ DSP استفاده شده در این طرح را روی کاغد یادداشت کنید. آیا می‌توانید ارتباطی بین این عدد با تعداد ضرائب فیلتر پیدا کنید؟

پیاده سازی فیلتر FIR در Vivado
صفحه Summary ویزارد FIR Compiler

۵- روی OK کلیک کنید و صبر کنید تا طراحی فیلتر به اتمام برسد.

ساخت فایل wrapper

حالا نوبت متصل کردن ورودی ‌ها و خروجی ها است. روی پورت‌ aclk کلیک راست کنید و از منوی کشویی باز شونده آن گزینه Make External را انتخاب کنید. مشاهده می‌کنید که یک پورت کلاک برای بلوک فیلتر ایجاد می‌شود. این کار را برای سایر پورت‌ها و اینترفیس‌های بلوک دیاگرام تکرار کنید. تصویر نهایی بلوک دیاگرام مشابه شکل زیر خواهد بود.

پیاده سازی فیلتر FIR در Vivado
بلوک دیاگرام نهایی فیلتر بعد از سفارشی سازی و تنظیم ورودی خروجی‌های آن

آخرین گام پیش از آغاز مرحله شبیه سازی، ساخت یک فایل تاپ ماژول wrapper به زبان VHDL / Verilog است. این سورس سطح بالا عموماً باید توسط طراح به طور سفارشی ایجاد شود، اما می‌توان از ابزارهای Vivado نیز به عنوان یک میانبر برای انجام این کار استفاده نمود.

۱- در سمت راست بلوک دیاگرام، بر روی پنجره یا زبانه Sources و سپس نمای Design Hierarchy در پایین آن، که سلسله مراتب طراحی را نشان می‌دهد، کلیک نمایید. سورس فایل طراحی بلوک در پوشه Design Sources قابل مشاهده است. بر روی این فایل راست کلیک کنید، تا منوی کشویی آن ظاهر شود.

۲- گزینه Create HDL Wrapper را انتخاب و سپس در پنجره پاپ-آپ روی صفحه گزینه Let Vivado manage wrapper and auto-update را انتخاب نمایید، و بر روی OK کلیک کنید. ابزارهای Vivado به طور خودکار یک فایل VHDL یا Verilog ایجاد خواهند نمود که نام آن براساس نام سورس طراحی بلوک شما بعلاوه یک پسوند ”wrapper_” تعیین می‌شود. این فایل کمکی به عنوان فایل تاپ ماژول HDL به طراحی ما اضافه می‌شود.

شبیه سازی

۱- روی علامت + در نوار ابزار بالایی زبانه Sources کلیک کنید و گزینه Add Source را انتخاب کنید.

۲- با انتخاب گزینه Add or create simulation Sources  یک فایل تست بنچ جدید بسازید.

پیاده سازی فیلتر FIR در Vivado
اضافه کردن یک سورس فایل جدید به پروژه برای ایجاد تست بنچ

۳- با کلیک روی گزینه Create File یک فایل تست بنچ به نام fir_tb ایجاد کنید و روی OK کلیک کنید.

پیاده سازی فیلتر FIR در Vivado
ایجاد یک فایل تست بنچ جدید

۴- فایل fir_tb به لیست سورس فایل‌های شما اضافه می‌شود. روی Finish کلیک کنید.

پیاده سازی فیلتر FIR در Vivado
تعیین نام برای فایل تست بنچ

۵- بلافاصله صفحه Define Module به شما نمایش داده می شود. در این صفحه شما می‌توانید نام و لیست پورت‌های ماژول خود را تعیین کنید. بدون اعمال هیچ تغییری روی OK کلیک کنید.

پیاده سازی فیلتر FIR در Vivado
پنجره Define Module

بعد از ساخته شدن تست بنچ از طریق پوشه Simulation Sources در پنجره Sources می‌توانید به این فایل دسترسی داشته باشید. اگر این فایل را باز کنید، مشاهده خواهید کرد که تقریباً خالی است. محتویات آن را با محتویات تست بنچ آماده‌ای که در ادامه با هم بررسی می‌کنیم، جایگزین کنید.

پیاده سازی فیلتر FIR در Vivado
سلسله مراتب طرح برای اجرای شبیه سازی
-- copyright Hexalinx @2020

library IEEE;
	USE ieee.std_logic_1164.ALL;
    USE ieee.numeric_std.ALL;
    USE ieee.std_logic_textio.ALL;

LIBRARY std; 
    USE std.textio.all;

ENTITY Hexalinx_tb IS
END Hexalinx_tb;

ARCHITECTURE Behavioral OF Hexalinx_tb IS

    signal M_AXIS_DATA_0_tdata  :  STD_LOGIC_VECTOR ( 23 downto 0 );
    signal M_AXIS_DATA_0_tvalid :  STD_LOGIC;
    signal S_AXIS_DATA_0_tdata  :  STD_LOGIC_VECTOR ( 7 downto 0 );
    signal S_AXIS_DATA_0_tready :  STD_LOGIC;
    signal S_AXIS_DATA_0_tvalid :  STD_LOGIC;
    signal aclk_0               :  STD_LOGIC;

	-- # clock period definitions	
	constant Clk_Period : time :=10 ns;	

	signal Clk 						: std_logic := '0';
	signal Rst 						: std_logic := '0';
	signal Rd_Clk 					: std_logic := '0';
	signal Wr_Clk 					: std_logic := '0';

	-- # input vector parameters
	constant iVectorLen : natural := 8;

	-- # onput vector parameters
	constant oVectorLen : natural := 8;

	-- # uncomment this signal for slv	
	-- shared variable slvData 		: std_logic_vector(iVectorLen-1 downto 0);
	
	-- # uncomment this signal for int	
	shared variable intData 		: integer;

	-- # UUT Input
	signal UUT_DataIn 				: std_logic_vector(iVectorLen-1 downto 0) := (others => '0');
	signal UUT_ValidIn 				: std_logic := '0';
	
	-- # UUT Output
	signal UUT_DataOut 				: std_logic_vector(oVectorLen-1 downto 0) := (others => '0');
	signal UUT_ValidOut 			: std_logic := '0';

	
	-- # IO file address		
	constant iFileAddr : string := ".\..\..\..\..\testin.txt";
	constant oFileAddr : string := ".\..\..\..\..\testout.txt";

	-- # IO file name
	file iFile : text open read_mode is iFileAddr;
	file oFile : text open write_mode is oFileAddr;	
	
begin


    aclk_0 <= Rd_Clk;
    S_AXIS_DATA_0_tvalid <= UUT_ValidIn;
    S_AXIS_DATA_0_tdata <= UUT_DataIn;
    S_AXIS_DATA_0_tready <= '1';
    
    UUT_ValidOut <= M_AXIS_DATA_0_tvalid;
    UUT_DataOut <= M_AXIS_DATA_0_tdata(19 downto 12); -- ( AXI Q2418 )or  normal Q2218 --> Q0806

design_1_wrapper_inst: entity work.design_1_wrapper
   port map (
      -- Input Ports - Single Bit
      aclk_0                           => aclk_0,                         
      S_AXIS_DATA_0_tvalid             => S_AXIS_DATA_0_tvalid,           
      -- Input Ports - Busses
      S_AXIS_DATA_0_tdata(7 downto 0)  => S_AXIS_DATA_0_tdata(7 downto 0),
      -- Output Ports - Single Bit
      M_AXIS_DATA_0_tvalid             => M_AXIS_DATA_0_tvalid,           
      S_AXIS_DATA_0_tready             => S_AXIS_DATA_0_tready,           
      -- Output Ports - Busses
      M_AXIS_DATA_0_tdata(23 downto 0) => M_AXIS_DATA_0_tdata(23 downto 0)
      -- InOut Ports - Single Bit
      -- InOut Ports - Busses
   );

	-- Clk
	Clk <= not Clk after Clk_Period/2;

	-- Clk
	Rd_Clk <= Clk;
	Wr_Clk <= Clk;

    --------------------------------------------------------
    -- Input: read from iFile and  write to UUT_DataIn input
    --------------------------------------------------------
        
        RD:
        process(Rd_CLK)
            variable iLine : line := NULL;
        begin
        
            if rising_edge(Rd_CLK) then
    
                if Rst = '1' then
                    
                    UUT_DataIn <= (others => '0');
                    UUT_ValidIn <= '0';
                
                else
                    -- uncomment for slv
                    -- readline(iFile,iLine);
                    -- read(iLine,slvData);
                    -- UUT_DataIn <= slvData;
                    
                    -- uncomment for int
                    readline(iFile,iLine);
                    read(iLine,intData);
                    UUT_DataIn <= std_logic_vector(to_signed(intData,iVectorLen)); -- for signed
                    -- UUT_DataIn <=  std_logic_vector(to_unsigned(intData,iVectorLen)); -- for unsigned
                    
                    UUT_ValidIn <= '1';
    
                end if;
            end if;
        end process;
    
    ----------------------------------------------------------
    -- Output: read from UUT_DataOut output and write to oFile
    ----------------------------------------------------------
        
        WR:
        process(Wr_CLK)
            variable oLine :line := NULL;
        begin
        
            if rising_edge(Rd_CLK) then
    
                if Rst = '1' then
                
                -- ...
                
                elsif UUT_ValidOut = '1' then
                
                    -- uncomment for slv
                    -- write(oLine,UUT_DataOut);
                    -- writeline(oFile,oLine);
                    
                    -- uncomment for int
                    write(oLine,to_integer(signed(UUT_DataOut)),right, 6);  -- for signed
                    writeline(oFile,oLine);
                    -- write(oLine,to_integer(unsigned(UUT_DataOut)),right, 6); -- for unsigned
                    -- writeline(oFile,oLine);
    
                end if;
            end if;
        end process;
    
    end Behavioral;

در این تست بنچ، فایل متنی testin.txt که حاوی بردار تست فیلتر است، فراخوانده می‌شود. در هر کلاک یک داده جدید به فیلتر ارسال می‌شود و بعد از یک تأخیر ثابت اولین خروجی آن تولید می‌شود. مقدار این تأخیر وابسته به نوع ساختار مورد استفاده برای فیلتر، تعداد ضرائب آن و همینطور اینترفیس مورد استفاده برای ورودی‌ها و خروجی‌هاست و تخمینی از آن در صفحه Summary ویزارد تنظیمات FIR Compiler‌ ارائه شده بود. برای تولید فایل testin.txt کد زیر را در محیط Matlab اجرا کنید.

% Input test-vector
FIN = fopen('testin.txt','w');
fprintf(FIN,(sprintf('%4d\n',x_scaled)));

در انتهای کار نیز بعد از تولید خروجی نهایی فیلتر، عرض (طول) بیت آن مطابق با فرمت Q0806 اصلاح و بیت‌های اضافی آن دور ریخته می‌شود. به شکل زیر دقت کنید تا درک خوبی از روش انجام این کار بدست آورید.

پیاده سازی فیلتر FIR در Vivado
کاهش عرض بیت خروجی به ۸ بیت

۶- برای اجرای شبیه سازی در پنجره Flow Navigator بر روی گزینه Run Simulation کلیک کنید و از منوی کشویی آن گزینه Run Behavioral Simulation‌ را انتخاب کنید. کمی منتظر بمانید تا اجرای شبیه سازی در XSim آغاز شود.

پیاده سازی فیلتر FIR در Vivado
اجرای شبیه سازی با فراخوانی Vivado Simulator

۷- با کمک گزینه‌های موجود در نوار ابزار، شبیه سازی را برای ۵۰۰ میکرو ثانیه اجرا کنید تا بخشی از ۱۵۰۰۰۰۰ داده ورودی که در فایل نوشتیم به فیلتر اعمال شود (اجرای کامل تمام شبیه سازی کمی زمانبر است و ممکن است یک ساعت طول بکشد). با اتمام اجرای تست بنچ خروجی‌های فیلتر تولید و در فایل testout.txt ذخیره می‌شوند. این فایل در کنار فایل testin.txt ایجاد می‌شود و از طریق پوشه اصلی پروژه Vivado قابل دسترسی است. بررسی صحت عملکرد فیلتر با بررسی سیگنال‌های اینترفیس AXI در صفحه Waveform امکان پذیر است. اما این کار کمی سخت است و راه ساده تر استفاده از فایل testout.txt و ترسیم آن در Matlab است.

پیاده سازی فیلتر FIR در Vivado
صفحه Waveform

۸- فایل testout.txt را به پوشه کاری فعال Matlab منتقل کرده و با استفاده از کدهای زیر محتوای این فایل را فراخوانی کنید. بعد محتوای آن را در متغیر y_hardawre ذخیره و سپس بردار y_hardawre‌ و y_sclaed را در یک صفحه روی هم ترسیم کنید.

%% Test 

FOUT = fopen('testout.txt','r');
y_hardware = fscanf(FOUT,'%4d');

figure
plot(t(1:length(y_hardware)),y_scaled(1:length(y_hardware)));
xlabel('time (in seconds)');
title('Outputs versus Time');
hold on
plot(t(1:length(y_hardware)),y_hardware,'-.');
legend('Desired Data','Implemented Data');
zoom xon;
پیاده سازی فیلتر FIR در Vivado
مقایسه نتایج مطلوب با خروجی فیلتر بعد از شببه سازی در Vivado Simulator

ملاحظه خواهید کرد که نتایج خروجی بعد از پیاده سازی کاملاً منطبق بر نتایج مطلوبِ محاسبه شده در Matlab است. کافی است کمی روی شکل موج‌ها زوم کنید. از دقت نهایی پیاده سازی خود شگفت زده خواهید شد، چون حتی یک بیت خطا هم وجود ندارد.

جمع بندی

در این آموزش از یک رویکرد متداول برای پیاده سازی فیلتر FIR در Vivado استفاده کردیم. پیاده سازی در محیط IP Integrator به صورت کاملاً سنتی انجام شد و تست بنچی را که از قبل آماده شده بود به طرح RTL اضافه کردیم و از آن برای شبیه سازی بهره گرفتیم. طراحی و پیاده سازی این فیلتر به صورت ممیز ثابت انجام شد، از این رو نتایج پیاده سازی سخت افزاری و طراحی نرم افزاری کاملاً بر هم منطبق بودند. طراحی ما اصطلاحاً یک طراحی bit accurate بود.

منبع: Xilinx

اشتراک در
بیشتر بخوانیم
نمایش اعداد اعشاری ممیز ثابت توصیف سخت افزاری

اعداد اعشاری ممیز ثابت (بخش سوم: قوانین پایه محاسبات)

اعداد ممیزثابت همان اعداد اعشاری هستند که با استفاده از یک فاکتور معین مقیاس بندی می‌شوند. این باعث می‌شود قوانین خاصی بر محاسبات ممیز ثابت حاکم شود.

بلوک‌های UltraRAM‌ در تراشه‌های +UltraSclae تراشه‌های قابل پیکره‌بندی

مفهوم حافظه در FPGA و کاربردهای آن

حافظه ها یکی از مهمترین منابع درون تراشه FPGA هستند و بدون آن ها جریان طراحی به شکلی که امروزه انجام می شود، امکان پذیر نبود، حافظه ها درون FPGA به دو دسته تقسیم می شوند.

آشنایی با مفهوم Device Tree سیستم‌های نهفته و لینوکس

آشنایی با مفهوم Device Tree

آیا با مفهوم Device Tree آشنا هستید؟ برای اکثر ما Device Tree جایی است که اضافه یا حذف قطعات کوچک سخت افزاری به کرنل لینوکس گزارش می‌شود.

تاخیر، جیتر، کجی و عدم قطعیت کلاک تراشه‌های قابل پیکره‌بندی

تأخیر، جیتر، کجی و عدم قطعیت کلاک

دو فاز در طراحی سیگنال کلاک برای هر سیستم دیجیتالی وجود دارد. در فاز اول کلاک ایده آل در نظر گرفته می‌شود و در فاز دوم توپولوژی توزیع آن ساخته می‌شود.

عناوین مطالب
    برای شروع تولید فهرست مطالب ، یک هدر اضافه کنید

    1 در مورد “پیاده سازی فیلتر FIR در Vivado”

    1. بی صبرانه منتظر ادامه سری آموزشهای پیاده سازی فیلتر در FPGA توسط شما هستم. بسیار عالی هست که ابتدا با مفاهیم تئوری و در قالب پستهای جداگانه استارت زدید؛ سپس کار رو با پیاده سازی از طریق SysGen ادامه دادید و الان به پیاده سازی با ویوادو و از طریق IP Integrator روی آوردید. قطعا گام بعدی پیاده سازی FIR در سطح کدهای HDL خواهد بود و بیصبرانه منتظرش هستم

    دیدگاه‌ خود را بنویسید

    نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

    اسکرول به بالا