مقاله های سیسوگ, پروژه, توصیه شده

ساختن یک CPU هشت بیتی با استفاده از محیط Wokwi

ساختن یک CPU هشت بیتی با استفاده از محیط Wokwi

سایت Wokwi چیست؟

سایت Wokwi یک محیط برای تست و امتحان برنامه های نوشته شده با ESP32، Arduino و… می باشد و دسترسی و استفاده از بسیاری از ماژول ها در آن وجود دارد، یک قابلیت خوب این وبسایت این است که به ما اجازه می دهد تراشه های (chip) سفارشی خود را بسازیم، برنامه ریزی کنیم و آنها را تست کنیم.

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

ایجاد یک پروژه در Wokwi

برای ایجاد یک پروژه جدید در سایت Wokwi به آدرس زیر مراجعه کرده و بعد از وارد شدن تا آخر اسکرول کرده و روی Blank Project بزنید

https://wokwi.com/dashboard/projects

ایجاد یک پروژه در Wokwi

بعد از این کار با صفحه زیر رو به رو می شوید که در صفحه سمت چپ این مکان برای کد های پروژه می باشد و در سمت برای مشاهده شبیه ساز می باشد.

ایجاد یک پروژه در Wokwi

حالا با زدن روی دکمه بعلاوه (Add a new part) دنبال قطعه ای برای به نام custom chip بگردید که به ما اجازه می دهد تراشه خود را طراحی کنیم

ایجاد یک پروژه در Wokwi

بعد از این کار و کلیک روی گزینه، به پروژه نام دلخواهی بدهید (در اینجا 8bit-cpu) و زبان برنامه نویسی آن را C انتخاب کنید زیرا که این پروژه با C نوشته شده و توصیه شده خود وب سایت می باشد.

ساخت یک چیپ کاستوم در Wokwi

بعد از ایجاد یک تراشه فایل های زیر ایجاد می شوند:

  • فایل 8bit-cpu.chip.json: که حاوی داده های مربوط به تراشه می باشند مثل اسم، تعداد پایه ها و…
  • فایل 8bit-cpu.chip.c: که کد اصلی مربوط به تراشه در آن قرار می گیرد (در اینجا با زبان C)

توجه داشته باشید که اسم هردو این فایل ها اسم تراشه انتخاب شده است و با توجه به اسم ممکن است که متفاوت باشند.

نوشتن کد های تراشه

بعد از ایجاد یک تراشه حالا زمان نوشتن برنامه برای آن می باشد.

در فایل ایجاد شده 8bit-cpu.chip.c محتوای زیر موجود می باشد:

نوشتن کد های تراشه

خط 7 مربوط به خود کتابخانه Wokwi می باشد که API های بسیار مفیدی را در اختیار ما قرار می دهد.

تابع chip_init مربوط به خود تراشه می باشد که هنگامی که تراشه مورد استفاده قرار می گیرد صدا زده می شود

در خط 16 یک structure (تعریف شده در خط 11) از خود برای تابع مورد استفاده قرار میگیرد که می توان اطلاعات (متغیر) مربوط به تراشه را در آن قرار داد، برای این مثال می توان فرکانس مورد نظر تراشه (CPU) را در آن گذاشت.

برنامه نویسی میکروکنترلر

حالا زمان نوشتن کدهای مربوط به CPU (میکرو کنترلر) می باشد

کد زیر یک enum برای opcode ها می باشد:

کد enum برای opcode

  • در خط های 1 تا 3 کتابخانه های استفاده شده در این پروژه وارد شده است
  • در خط های 6 تا 12 یک enum برای شناسایی opcode تعریف شده است

این CPU تا این مرحله فقط کدهای عملیاتی BSF، BCF، GOTO و NOP را پردازش می کند.

مرحله بعدی تعریف structure های برای گرفتن دستور (Fetch)، مشخص کردن نوع دستور (Decode) و اجرای دستور (Execute) می باشد.

تعریف structure

  • خط های 29 تا 33 برای مشخص کردن اندازه operand یا عملوند ها می باشد.
  • خط های 36 تا 43 برای خروجی Decode می باشد که برای نگهداری اطلاعات هر دستور مورد استفاده قرار می گیرد
  • خط های 46 تا 49 نیز برای خروجی Execute می باشند، که در اینجا این structure برای دستور هایی که نیازی به آپدیت کردن RAM یا رجیستر ها ندارند مورد استفاده قرار می گیرد مثل دستور های مثل GOTO، NOP و یا SLEEP که نیازی به آپدیت کردن حافظه ندارند.

تعریف Structure های حافظه

حالا structure های مربوط به حافظه را تعریف می کنیم:

structure های مربوط به حافظه

 

  • خط های 1 تا 4 برای تعریف یک enum برای مشخص کردن نوع حافظه می باشند که از 0x00 تا 0x09 برای رجیستر ها و از 0x10 تا 0x1F برای RAM می باشد
  • خط های 6 تا 11 برای گرفتن حافظه مورد استفاده قرار می گیرند که addr برای آدرس، value برای مقدار آن آدرس و valid برای درست بودن آدرس می باشد (1 به معنای valid و 0 به معنای invalid می باشد)، که برای چک کردن رنج آدرس می باشد

حالا متغیر های مربوط به RAM، Register، ROM و PC را تعریف می کنیم:

متغیر های RAM، Register، ROM و PC

در سه خط اول macro های برای اندازه های RAM، Register و ROM مشخص شده و در خط های 7 تا 9 متغیر های مربوط به آنها تعریف شده است و در نهایت در خط 11 شمارنده برنامه تعریف شده است.

تعریف توابع اصلی برنامه

دو تابع اصلی برنامه را تعریف می کنیم که در آخر فایل آنها را مقداردهی کنیم (هر کدام از این توابع با استفاده از کتابخانه Wokwi در رشته (thread) جدا اجرا می شود):

تعریف دو تابع اصلی برنامه

  • در خط های 2 و 3 متغیر های مربوط به thread پایه ها تعریف شده است
  • در خط 6 یک آرایه برای پایه های خروجی تعریف شده است (8 پایه برای هر بیت از رجیستر 0x06)
  • در خط 8 و 9 دو تابع برای thead مربوط به CPU و thread مربوط به پایه ها تعریف شده است
  • در خط های 12 تا 17 متغیر frequency برای مشخص کردن فرکانس CPU تعریف شده و متغیر های مربوط به اجرای CPU به chip_state_t مربوط به تراشه اضافه شده است.

حالا متغیر مربوط به برنامه های داخل CPU را تعریف می کنیم:

متغیر برنامه های داخل CPU

این آرایه با مقدار های عددی هر دستور (خروجی assembler) جای گذاری می شود و در ROM قرار میگیرد.

حالا تابع اصلی را آپدیت می کنیم و دستوراتی را برای بازه رنج 1Hz تا 1MHZ می نویسم:

دستوراتی برای بازه رنج 1Hz تا 1MHZ

  • در خط 96 مقدار فرکانس اولیه را قرار می دهیم که در اینجا برابر با 10 هرتز می باشد
  • در خط های 100 تا 105 دستوراتی را برای مشخص کردن بازه می نویسیم

بعد از مشخص کردن فرکانس حالا برنامه اصلی را در ROM قرار می دهیم:

قرار دادن برنامه اصلی در ROM

تعریف پایه های تراشه برای CPU

بعد از این کار نیاز است که برای CPU پایه هایی را تعبیه کنیم:

فایل 8bit-cpu.chip.json را به شکل زیر تغییر می دهیم و شکل CPU را به وجود می آوریم:

فایل 8bit-cpu.chip.json

برای ایجاد فاصله در تراشه از رشته های خالی برای پایه ها استفاده می شود. و VCC و GND مربوط به تغذیه تراشه می باشند.

بعد از این کار نیاز است که پایه ها را در تراشه استفاده کنیم و آنها را تبدیل به پایه های خروجی کنیم (با استفاده از کتابخانه خود Wokwi):

ساختن یک CPU هشت بیتی با استفاده از محیط Wokwi

در کد بالا pin_init برای هر پایه استفاده می شود و آنها را برای خروجی مقدار دهی می کند (OUTPUT)، توجه داشته باشید که اسم های آرایه pins با اسم های مربوط به فایل 8bit-cpu.chip.json یکسان باشد.

بعد از این کار دو thread اصلی برنامه را اجرا می کینم:

ساختن یک CPU هشت بیتی با استفاده از محیط Wokwi

سه خط کد اول برای thread مربوط به CPU با فرکانس داده شده و سه خط آخر مربوط به پایه های خروجی با سرعتی برابر با نصف فرکانس CPU (دلیل کمی زودتر بودن برای جلوگیری از ایجاد شدن مشکل هایی مثل آپدیت نشدن پایه ها می باشد)

چرا از Thread استفاده می کنیم؟

استفاده از thread ها باعث جلوگیری از پیچیدگی در کد و نوشتن کد هایی با عملکرد بیشتر می شوند. در این پروژه از دو thread استفاده شده است یکی برای پایه های خروجی و یکی هم حلقه اصلی CPU، دلیل استفاده thread برای پایه های خروجی این است که حلقه thread کار مربوط به آپدیت کردن پایه ها را انجام می دهد و با اینکار کد کمتری نیز نوشته می شود و باعث جلوگیری از مشکلاتی مثل spaghetti شدن کد می شود (کد های ناخوانا).

آپدیت کردن تابع gpio_handler و ایجاد یک thread برای آپدیت پایه های خروجی

آپدیت کردن تابع gpio_handler و ایجاد یک thread برای آپدیت پایه های خروجی

تابع بالا با توجه به پایه GPIO مربوط به CPU (آدرس 0x06 رجیستر) پایه های 1 تا 8 خروجی تراشه را (هر پایه یک بیت) آپدیت میکند.

برای مثال اگر اندازه رجیستر 6 برابر با 0b00000010 باشد به این معنی می باشد که پایه 2 مربوط به تراشه برابر با HIGH (روشن) خواهد بود و بقیه پایه ها برابر با LOW (خاموش) خواهند بود.

در خط 8 یک mask ایجاد شده با مقداری برابر با 0b10000000 که هر بار در خط 15 یکی از صفرهای سمت راست از بین می رود، این به ما اجازه می دهد که با توجه به bitwise ها مقدار هر بیت از GPIO را استخراج کنیم و با pin_write مربوط به کتابخانه های Wokwi هر پایه مشخص شده با توجه به مقدار بیت داده شده برابر با HIGH یا LOW قرار دهیم.

خارج کردن مقدار عددی با توجه به بازه ای از عدد ورودی

تابع زیر مقدار عددی (صحیح) ورودی را با توجه به بازه داده شده برمیگرداند و برای تشخیص کدهای عملیاتی یا opcode ها بسیار مفید می باشد:

خارج کردن مقدار عددی با توجه به بازه ای از عدد ورودی

توجه داشته باشید که تابع بالا برای سادگی از 1 شروع شده و هم start و end در نظر گرفته می شوند.

تابع Decode

حالا زمان نوشتن تابع Decode رسیده و تفاوت کوچکی با تابع موجود در این پست دارد در این تابع، تشخیص opcode های پیچیده، راحتتر می شود.

تابع Decode

در این تابع با استفاده از if و else و تابع ،edfb کد های عملیاتی را یک به یک چک کرده و با توجه به نوع کد که قبلا در این پست گفته شده، آنها را چک می کنیم و اطلاعات structure را با توجه آن داده ها آپدیت می کنیم و در نهایت opcode های ناشناخته را NOP در نظر می گیریم.

نوشتن توابع مربوط به اجرا (Execute)

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

در این تابع دستوراتی که نیازی به تغییری در حافظه ندارند را می نویسیم:

نوشتن توابع مربوط به اجرا (Execute)

یکی از کدهای که نیازی به تغییر در حافظه ندارد GOTO و SLEEP می باشند که در وبلاگ دستور SLEEP را بررسی نمی کنیم و در تابع بالا مقدار operand دستور GOTO برابر با upc می شود (شمارنده برنامه آپدیت می شود) و در نهایت در خط 206 برگردانده می شوند.

تابع بعدی مربوط به اجرای دستوراتی می باشد که با حافظه سر و کار دارند، اما قبل از این کار نیاز به تابع هایی داریم که مقدار های حافظه را برای ما برگرداند:

ساختن یک CPU هشت بیتی با استفاده از محیط Wokwi

دو تابع بالا برای گرفتن و قرار دادن مقدار حافظه می باشند (RAM و رجیستر ها)

تابع get_mem با توجه به آدرس داده شده یک MEM_OUT بر میگرداند که بالاتر آن را تعریف کردیم و مشخص میکند که آیا با RAM یا با رجیستر ها سروکار داریم.

  • آدرس رجیستر ها از 0x00 تا 0x09 در مجموع 10 عدد
  • آدرس RAM از 0x10 تا 0x1F در مجموع 16 عدد

و تابع set_mem با توجه به ورودی خود مقدار حافظه را در مکانی که باید ذخیره میکند در غیر این صورت اگر در هر کدام از توابع مقدار آدرس ها valid نبود عملیاتی انجام نمی شود.

بعد از این دو تابع دیگر می توانیم تابع مربوط به اجرای اصلی را بنویسیم:

تابع set_mem

بعد از اینکار زمان نوشتن حلقه CPU اصلی می باشد:

حلقه اصلی پردازنده

حالا حلقه اصلی CPU را که بالاتر تعریف کردیم (cpu_cycle) را می نویسیم:

تعریف کد اصلی cpu cycle

بعد از مشخص کردن متغیرها در خط 279 هر دستور را با توجه به شمارنده برنامه (PC) به decode_inst می دهیم و dcd را آپدیت میکنیم که دارای اطلاعات مربوط به دستور می باشد.

در خط 280 اجرای اولیه را انجام می دهیم در صورتی که دستور نیازی به تغییری در حافظه داشت این بخش انجام نمی شود در غیر این صورت در خط های 285 تا 289 شمارنده برنامه به روز شده.

و در نهایت در خط 292 دستوراتی که با حافظه سر و کار دارند اجرا می شوند.

توجه داشته باشید که هر کدام از پایه ها در gpio_handler بررسی می شوند و نیازی به آپدیت کردن آنها در این بخش نداریم.

تست و اجرای یک برنامه با CPU

حالا که CPU آماده استفاده شده است، بیاید با یک برنامه آن را تست کنیم:

در قسمت Simulation سایت Wokwi یک LED bar قرار می دهیم:

شبیه ساز LED بار Wokwi

بعد از پیدا کردن LED Bar پایه های آن را همراه با VCC و GND به تراشه متصل می کنیم:

نحوه اتصال LED Bar به تراشه

با وصل کردن پایه های GND و VCC به LED Bar و CPU حالا نوبت به دادن برنامه CPU می باشد که با آپدیت کردن آرایه program که خروجی Assembler به عدد می باشد ممکن می شود:

کد های برنامه pattern

کد های بالا مربوط به برنامه pattern می باشند و در نهایت بعد از ذخیره و اجرای برنامه نتیجه زیر را داریم:

ساختن یک CPU هشت بیتی با استفاده از محیط Wokwi

کد های مربوط به این پروژه را میتوانید در لینک Wokwi زیر پیدا کنید:

https://wokwi.com/projects/408570399941989377

انتشار مطالب با ذکر نام و آدرس وب سایت سیسوگ، بلامانع است.

شما نیز میتوانید یکی از نویسندگان سیسوگ باشید.   همکاری با سیسوگ

دیدگاهتان را بنویسید

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