سایت Wokwi یک محیط برای تست و امتحان برنامه های نوشته شده با ESP32، Arduino و… می باشد و دسترسی و استفاده از بسیاری از ماژول ها در آن وجود دارد، یک قابلیت خوب این وبسایت این است که به ما اجازه می دهد تراشه های (chip) سفارشی خود را بسازیم، برنامه ریزی کنیم و آنها را تست کنیم.
برای ایجاد یک پروژه جدید در سایت Wokwi به آدرس زیر مراجعه کرده و بعد از وارد شدن تا آخر اسکرول کرده و روی Blank Project بزنید
https://wokwi.com/dashboard/projects
بعد از این کار با صفحه زیر رو به رو می شوید که در صفحه سمت چپ این مکان برای کد های پروژه می باشد و در سمت برای مشاهده شبیه ساز می باشد.
حالا با زدن روی دکمه بعلاوه (Add a new part) دنبال قطعه ای برای به نام custom chip بگردید که به ما اجازه می دهد تراشه خود را طراحی کنیم
بعد از این کار و کلیک روی گزینه، به پروژه نام دلخواهی بدهید (در اینجا 8bit-cpu) و زبان برنامه نویسی آن را C انتخاب کنید زیرا که این پروژه با C نوشته شده و توصیه شده خود وب سایت می باشد.
بعد از ایجاد یک تراشه فایل های زیر ایجاد می شوند:
توجه داشته باشید که اسم هردو این فایل ها اسم تراشه انتخاب شده است و با توجه به اسم ممکن است که متفاوت باشند.
بعد از ایجاد یک تراشه حالا زمان نوشتن برنامه برای آن می باشد.
در فایل ایجاد شده 8bit-cpu.chip.c محتوای زیر موجود می باشد:
خط 7 مربوط به خود کتابخانه Wokwi می باشد که API های بسیار مفیدی را در اختیار ما قرار می دهد.
تابع chip_init مربوط به خود تراشه می باشد که هنگامی که تراشه مورد استفاده قرار می گیرد صدا زده می شود
در خط 16 یک structure (تعریف شده در خط 11) از خود برای تابع مورد استفاده قرار میگیرد که می توان اطلاعات (متغیر) مربوط به تراشه را در آن قرار داد، برای این مثال می توان فرکانس مورد نظر تراشه (CPU) را در آن گذاشت.
حالا زمان نوشتن کدهای مربوط به CPU (میکرو کنترلر) می باشد
کد زیر یک enum برای opcode ها می باشد:
این CPU تا این مرحله فقط کدهای عملیاتی BSF، BCF، GOTO و NOP را پردازش می کند.
مرحله بعدی تعریف structure های برای گرفتن دستور (Fetch)، مشخص کردن نوع دستور (Decode) و اجرای دستور (Execute) می باشد.
حالا structure های مربوط به حافظه را تعریف می کنیم:
حالا متغیر های مربوط به RAM، Register، ROM و PC را تعریف می کنیم:
در سه خط اول macro های برای اندازه های RAM، Register و ROM مشخص شده و در خط های 7 تا 9 متغیر های مربوط به آنها تعریف شده است و در نهایت در خط 11 شمارنده برنامه تعریف شده است.
دو تابع اصلی برنامه را تعریف می کنیم که در آخر فایل آنها را مقداردهی کنیم (هر کدام از این توابع با استفاده از کتابخانه Wokwi در رشته (thread) جدا اجرا می شود):
حالا متغیر مربوط به برنامه های داخل CPU را تعریف می کنیم:
این آرایه با مقدار های عددی هر دستور (خروجی assembler) جای گذاری می شود و در ROM قرار میگیرد.
حالا تابع اصلی را آپدیت می کنیم و دستوراتی را برای بازه رنج 1Hz تا 1MHZ می نویسم:
بعد از مشخص کردن فرکانس حالا برنامه اصلی را در ROM قرار می دهیم:
بعد از این کار نیاز است که برای CPU پایه هایی را تعبیه کنیم:
فایل 8bit-cpu.chip.json را به شکل زیر تغییر می دهیم و شکل CPU را به وجود می آوریم:
برای ایجاد فاصله در تراشه از رشته های خالی برای پایه ها استفاده می شود. و VCC و GND مربوط به تغذیه تراشه می باشند.
بعد از این کار نیاز است که پایه ها را در تراشه استفاده کنیم و آنها را تبدیل به پایه های خروجی کنیم (با استفاده از کتابخانه خود Wokwi):
در کد بالا pin_init برای هر پایه استفاده می شود و آنها را برای خروجی مقدار دهی می کند (OUTPUT)، توجه داشته باشید که اسم های آرایه pins با اسم های مربوط به فایل 8bit-cpu.chip.json یکسان باشد.
بعد از این کار دو thread اصلی برنامه را اجرا می کینم:
سه خط کد اول برای thread مربوط به CPU با فرکانس داده شده و سه خط آخر مربوط به پایه های خروجی با سرعتی برابر با نصف فرکانس CPU (دلیل کمی زودتر بودن برای جلوگیری از ایجاد شدن مشکل هایی مثل آپدیت نشدن پایه ها می باشد)
استفاده از thread ها باعث جلوگیری از پیچیدگی در کد و نوشتن کد هایی با عملکرد بیشتر می شوند. در این پروژه از دو thread استفاده شده است یکی برای پایه های خروجی و یکی هم حلقه اصلی CPU، دلیل استفاده thread برای پایه های خروجی این است که حلقه thread کار مربوط به آپدیت کردن پایه ها را انجام می دهد و با اینکار کد کمتری نیز نوشته می شود و باعث جلوگیری از مشکلاتی مثل spaghetti شدن کد می شود (کد های ناخوانا).
تابع بالا با توجه به پایه 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 رسیده و تفاوت کوچکی با تابع موجود در این پست دارد در این تابع، تشخیص opcode های پیچیده، راحتتر می شود.
در این تابع با استفاده از if و else و تابع ،edfb کد های عملیاتی را یک به یک چک کرده و با توجه به نوع کد که قبلا در این پست گفته شده، آنها را چک می کنیم و اطلاعات structure را با توجه آن داده ها آپدیت می کنیم و در نهایت opcode های ناشناخته را NOP در نظر می گیریم.
در این مکان 2 تابع وجود دارد یکی برای اجرای اصلی که مربوط به دستوراتی می باشد که با حافظه سر و کار دارند و دیگری مربوط به دستوراتی که نیازی به تغییر چیزی در حافظه ندارند.
در این تابع دستوراتی که نیازی به تغییری در حافظه ندارند را می نویسیم:
یکی از کدهای که نیازی به تغییر در حافظه ندارد GOTO و SLEEP می باشند که در وبلاگ دستور SLEEP را بررسی نمی کنیم و در تابع بالا مقدار operand دستور GOTO برابر با upc می شود (شمارنده برنامه آپدیت می شود) و در نهایت در خط 206 برگردانده می شوند.
تابع بعدی مربوط به اجرای دستوراتی می باشد که با حافظه سر و کار دارند، اما قبل از این کار نیاز به تابع هایی داریم که مقدار های حافظه را برای ما برگرداند:
دو تابع بالا برای گرفتن و قرار دادن مقدار حافظه می باشند (RAM و رجیستر ها)
تابع get_mem با توجه به آدرس داده شده یک MEM_OUT بر میگرداند که بالاتر آن را تعریف کردیم و مشخص میکند که آیا با RAM یا با رجیستر ها سروکار داریم.
و تابع set_mem با توجه به ورودی خود مقدار حافظه را در مکانی که باید ذخیره میکند در غیر این صورت اگر در هر کدام از توابع مقدار آدرس ها valid نبود عملیاتی انجام نمی شود.
بعد از این دو تابع دیگر می توانیم تابع مربوط به اجرای اصلی را بنویسیم:
بعد از اینکار زمان نوشتن حلقه CPU اصلی می باشد:
حالا حلقه اصلی CPU را که بالاتر تعریف کردیم (cpu_cycle) را می نویسیم:
بعد از مشخص کردن متغیرها در خط 279 هر دستور را با توجه به شمارنده برنامه (PC) به decode_inst می دهیم و dcd را آپدیت میکنیم که دارای اطلاعات مربوط به دستور می باشد.
در خط 280 اجرای اولیه را انجام می دهیم در صورتی که دستور نیازی به تغییری در حافظه داشت این بخش انجام نمی شود در غیر این صورت در خط های 285 تا 289 شمارنده برنامه به روز شده.
و در نهایت در خط 292 دستوراتی که با حافظه سر و کار دارند اجرا می شوند.
توجه داشته باشید که هر کدام از پایه ها در gpio_handler بررسی می شوند و نیازی به آپدیت کردن آنها در این بخش نداریم.
حالا که CPU آماده استفاده شده است، بیاید با یک برنامه آن را تست کنیم:
در قسمت Simulation سایت Wokwi یک LED bar قرار می دهیم:
بعد از پیدا کردن LED Bar پایه های آن را همراه با VCC و GND به تراشه متصل می کنیم:
با وصل کردن پایه های GND و VCC به LED Bar و CPU حالا نوبت به دادن برنامه CPU می باشد که با آپدیت کردن آرایه program که خروجی Assembler به عدد می باشد ممکن می شود:
کد های بالا مربوط به برنامه pattern می باشند و در نهایت بعد از ذخیره و اجرای برنامه نتیجه زیر را داریم:
کد های مربوط به این پروژه را میتوانید در لینک Wokwi زیر پیدا کنید:
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.