مقدمه
سنتز سطح بالا یک راه حل مناسب برای پیاده سازی الگوریتمها است، اما زمانهایی وجود دارد که هنگام توسعه یک HLS IP نیازمند به برقراری ارتباط مناسب با سایر بخشهای سیستم در سطحی فراتر از سطح اینترفیس AXI (با فرض اینکه اینترفیس اصلی ماست) هستیم. منظور ما از برقراری ارتباط مناسب و البته مستقل از اینترفیس اصلی این است که مثلاً فانکشن یا طرح HLS ما باید طوری طراحی شود که کنترل اجرا یا عدم اجرای آن توسط یک سیگنال تحریک ورودی (تریگر) امکان پذیر باشد. برای درک بهتر موضوع حالتی را در نظر بگیرید که در آن ما نیاز داریم پیش از اجرای بخشی از یک فانکشن منتظر یک سیگنال تحریک (تریگر) باشیم و یا برای چند سیکل کلاک منتظر تغییر وضعیت یک سیگنال ورودی از دنیای خارج باشیم و مواردی از این دست که تعدادشان کم نیست. در نگاه اول پیاده سازی چنین مداراتی در HLS که ذاتاً ماهیت کنترلی دارند میتواند به نوبه خودش یک دردسر به تمام معنا باشد. به طور کلی یک طراح برای کنترل بهینه تأخیرها و تریگرها در HLS نیاز به آشنایی با کتابخانههای اختصاصی Xilinx دارد که به همین منظور توسعه داده شدهاند.
دراین آموزش از پایگاه دانش هگزالینکس قصد داریم نگاهی به این موضوع بیاندازیم که چگونه میتوانیم ساختاری فراتر از الگوریتمهای مرسوم پردازشی در Vivado HLS پیاده سازی کنیم که در آن:
- بتوانیم منتظر یک سیگنال ورودی به عنوان تریگر بمانیم.
- بتوانیم برای مدت زمان مشخصی همچون چند سیکل کلاک تأخیر ایجاد کنیم و روال اجرای برنامه را متوقف کنیم.
- و در نهایت یک سیگنال تریگر خروجی تولید کنیم.
اجازه بدهید که کار را با مورد اول شروع کنیم و منتظر دریافت تریگر ورودی از یک بلوک IP خارجی بمانیم، این سیگنال تریگر میتواند، یک سیگنال فریم سینک در یک اپلیکیشن پردازش تصویر باشد.
تریگر ورودی
اولین کاری که باید برای ایجاد یک سیگنال تحریک ورودی انجام بدهیم، تعریف یک ورودی مناسب با استفاده از دیتا تایپهای سفارشی در کتابخانههای arbitrary precision است. بدیهی است که به یک داده صحیح، بدون علامت و تک بیتی نیاز داریم، برای تعریف این ورودی باید به این شکل عمل کنیم. دقت کنید که زبان پیش فرض انتخابی ما ++C است.
// 1 bit Input Trigger ap_unit<1> trig_in
بر اساس اصول کد نویسی ++C/C بعد از تعریف فانکشن ابتدا باید این متغیر را به لیست آرگومانهای فانکشن HLS اضافه کنیم. ما همچنین میتوانیم یک پراگمای INTERFACE برای این آرگومان اعلان کنیم تا به این ترتیب در طرح RTL نهایی این پورت با پروتکل اینترفیس ap_none پیاده سازی شود. پس به صورت زیر عمل میکنیم.
// Use of ap_none interface #pragma HLS INTERFACE ap_none port=trig_in
به این ترتیب یک پورت تک بیت ورودی برای فانکشن خود ایجاد کردیم. کار بعدی که باید انجام بدهیم، کنترل زمان اجرای کد در HLS IP تا زمان فعال شدن این تریگر ورودی است. یعنی مادامی که این ورودی غیر فعال است اجرای کد ++C/C و در نتیجه HLS IP متوقف میشود.
ساده ترین راه برای انجام این کار در Vivado HLS استفاده از فانکشنی است که در کتابخانه ap_utils.h تعریف شده است. نام این فانکشن ویژه ap_wait_until(X) است. این فانکشن اجرای HLS IP را تا زمانی که مقدار true به آن نسبت داده شود، متوقف میکند. منظور از مقدار true در اینجا هر مقداری غیر صفر است. آرگومان X در این فانکشن همان سیگنال تریگر ورودی است که قبل تر تعریف کردیم.
// wait on input Trigger wait_until(trig_in);
توجه کنید که وقتی شبیه سازی ++C/C را اجرا می کنیم باید از غیر صفربودنِ مقدار متغیر تریگر اطمینان حاصل کنیم. در غیر این صورت شبیه سازی همواره در حالت متوقف باقی میماند.
حتماً بخاطر دارید که برای حصول اطمینان از پیاده سازی صحیح فانکشن ()ap_wait_until درون کدهای Vivado HLS همواره میتوانیم از نمایش analysis perspective هم کمک بگیریم. پس بد نیست این کار را انجام دهیم.
در نمایش analysis perspective یک عملیاتی به نام lnXX(wait) را می بینید. با راست کلیک روی آن و انتخاب گزینه goto source بلافاصله در صفحه source (در بخش پایینی پنجره Vivado HLS) به فانکشن ()ap_wait_until منتقل خواهید شد.
خب تا اینجا یاد گرفتیم که چطور باید در کد HLS منتظر تریگر خارجی بمانیم. حالا نوبت به بررسی مورد دوم میرسد، به نظر شما چگونه میتوانیم یک تأخیر برای تعداد مشخصی کلاک در یک کد HLS پیاده سازی کنیم؟
ایجاد تأخیر
در حالت کلی، نوشتن یک فانکشن تأخیر سفارشی سازی شده توسط خود پیاده ساز به اندازه کافی بهینه نیست و معمولاً چیزی که مد نظر طراح است، بدست نمیآید. از این رو بازهم باید از فانکشنهای آماده Xilinx کمک بگیریم. بهترین راه برای پیاده سازی یک تأخیر چند کلاکه استفاده از فانکشن ap_wait_n(X) است.
این فانکشن هم همچون فانکشن قبلی در کتابخانه ap_utils.h تعریف شده است، این فانکشن حداقل برای X سیکل کلاک تأخیر ایجاد میکند. با این وجود از سرگیری مجدد پردازش ممکن است با توجه به نوع پیاده سازی چند سیکل کلاک بیشتر طول بکشد.
نکته جالب در رابطه با این فانکشن این است که مقدار تأخیر میتواند در زمان اجرا تغییر کند، تا جایی که در صورت نیاز میتواند با استفاده از یک اینترفیس AXI مقدار آن کنترل شود. برای درک بهتر این مسأله به مثال زیر دقت کنید:
// N Clock Delay void test (int delay) #pragma HLS INTERFACE s_axilite port=delay ap_wait_n(delay); //delay for a number of clock cycles provided over AXI bus
یکی از ابتدایی ترین کاربردهایی که میتوان برای فانکشن ()ap_wait_n برشمرد، تنظیم فریم ریت تصویر به اندازه فریم ریت مورد نظر است. این کار معمولاً برای تولید یک پترن تست برای اپلیکیشنهای ویدئویی انجام میشود. در حالت کلی، هنگام اجرای شبیه سازیهای ++C/C معمولاً این تأخیر نادیده گرفته میشود، با این وجود در صورت استفاده از این فانکشن هنگام انجام شبیه سازی RTL شما متوجه تأخیر اضافه شده به طرح خواهید شد.
برای استفاده از این قابلیت، مشابه کاری که هنگام استفاده از فانکشن ()ap_wait_until انجام دادیم، ما بازهم قادر به مشاهده تأخیر در نمایش analysis Perspective خواهیم بود، این بار با کمی دقت میبینید که حلقه جدیدی برای پیاده سازی یک شمارنده ایجاد شده است و این شمارنده برای بررسی صحت میزان تأخیر استفاده میشود.
بدیهی است که ما میتوانستیم با کمی هوشمندی و با اضافه کردن یک شمارنده به طرح چنین شرایطی را ایجاد کنیم، اما ممکن بود این کار به اندازه کافی بهینه نباشد. پس بهتر است از امکاناتی که ابزار در اختیار ما قرار داده به خوبی استفاده کنیم.
تریگر خروجی
آخرین موضوعی که قصد داریم به آن بپردازیم ایجاد سیگنال تریگر خروجی است، هدف ما این است که این سیگنال به صورت مداوم در حین اجرای HLP IP تغییر وضعیت بدهد و یکسری اطلاعات از وضعیت اجرای فانکشن را به دنیای بیرون منتقل کند. این مسأله از این جهت حائز اهمیت است که به صورت پیش فرض کامپایلر Vivado HLS همچون بسیاری دیگر از کامپایلرهای زبان C فرض میکند که فانکشنها به صورت single thread اجرا میشوند و فقط مقدار پایانی یک متغیر یا سیگنال مهم است. این بدان معناست که مشابه یک process در زبان VHDL در انتهای کار تنها مقدار نهایی متغیر برگردانده میشود و تغییرات لحظهای آن هیچگاه خروجی نمیشود، برای اینکه بتوانیم خروجیهای میانی تولید کنیم، نیاز داریم کمی بیشتر فکر کنیم.
اجازه بدهید همینطور که در حال فکر کردن هستیم، برای ساخت سیگنال تریگر خروجی در ابتدا یک متغیر تک بیت با استفاده از دیتا تایپهای سفارشی کتابخانههای arbitrary precision ایجاد کنیم.
// Output Trigger ap_uint<1> *trig_out
مجدداً ما این اعلان را در لیست آرگومانهای فانکشنمان قرار میدهیم و از آنجایی قصد تعریف یک پورت خروجی (تک بیت) اسکالر را داریم، پس باید آن را به صورت یک اشارهگر اعلان کنیم. این مسأله در سند راهنمای کاربری HLS به صراحت توضیح داده شده است. میتوانید برای کسب اطلاعات تکمیلی در این رابطه به صفحه ۴۱ سند UG902 مراجعه کنید.
به طور کلی برای بررسی و حصول اطمینان از صحت مقدار لحظهای و میانی یک متغیر خروجی در یک HLS IP ما نیاز داریم تا این متغیر را به صورت سیگنالهای خروجی volatile تعریف کنیم. به این ترتیب هنگامی که کامپایلر HLS اجرا می شود تمامی عملیات میانی بدون اعمال بهینه سازی اجرا و خروجی متناسب با آن تولید میشود و این خروجی به صورت مداوم بروز رسانی میشود.
// Immediate Update volatile ap_uint<1> *trig_out
کلمه کلیدی volatile
بروز شدن signal ها در داخل یک process در زبان VHDL آنی نیست و به همین دلیل است که گاهاً برای دستیابی به چنین قابلیتی در VHDL از variable ها درون یک process استفاده میکنیم. فانکشنها در زبان C نیز کما بیش چنین رفتاری دارند، از اینرو برای دسترسی به مقادیر میانی و لحظهای یک آرگومان حین اجرای فانکشن نیاز به استفاده از عبارت توصیفی volatile هنگام تعریف آرگومان داریم. کلمه کلیدی volatile در زبان ++C/C اصطلاحاً یک type qualifier است و یکسری ویژگیها اضافی به دیتا تاپ مورد استفاده نسبت میدهد. در اینجا استفاده از کلمه کلیدی volatile به ما این امکان را میدهد تا چنین خروجیهایی تولید کنیم، علاوه بر این امکان پیاده سازی صحیحِ اشارهگرها با دسترسیهای چندگانه یا اصطلاحاً multi access pointers را مهیا میکند.
برخلاف چیزی که در ابتدا به نظر میرسید، حجم موضوعات جدید و کاربردی که در آن ارائه شد بسیار قابل توجه بود. موارد مشابه فراوانی در سند راهنمای نرم افزاری HLS وجود دارد که میتوانند به فراخور نیاز مورد استفاده قرار بگیرند. به عنوان تمرین پیشنهاد میکنم عملکرد فانکشن ()ap_wait در کتابخانه ap_utils.h را خودتان بررسی کنید و مقایسهای بین این فانکشن با دو فانکشن ()ap_wait_until و ()ap_wait_n داشته باشید. طبیعتاً بررسی آن خالی از لطف نخواهد بود.
جمع بندی
امیدوارم حالا بعد از خواندن این مقاله کمی بیشتر در رابطه با برخی از قابلیتهای ویژه و فانکشنهای کمتر معرفی شده که میتوانند در تکمیل و توسعه هر چه بیشتر یک طرح HLS مورد استفاده قرار گیرند، اطلاعاتی کسب کرده باشید.
در این مقاله کوتاه سعی کردیم تا برخی از نکات و کارکردهای عملی ابزار Vivado HLS را به کاربران نسبتاً حرفهای تر این ابزار معرفی کنیم. اگر چه ایجاد تأخیر و یا کنترل روند اجرای تمامی یا بخشی از یک فانکشن با تریگر ورودی چیزی است که به سادگی در روشهای مرسوم طراحی RTL پیاده سازی میشود، اما دستیابی به چنین قابلیتی در Vivado HLS نیازمند آشنایی طراح با کتابخانهها و فانکشنهای اختصاصی Xilinx است که دقیقاً برای همین منظور توسعه داده شدهاند.
منبع: با اقتباس از huckster.io و Xilinx نوشته Adam Taylor و UG902