سایت Wokwi چیست؟
سایت Wokwi یک محیط برای تست و امتحان برنامه های نوشته شده با ESP32، Arduino و… می باشد و دسترسی و استفاده از بسیاری از ماژول ها در آن وجود دارد، یک قابلیت خوب این وبسایت این است که به ما اجازه می دهد تراشه های (chip) سفارشی خود را بسازیم، برنامه ریزی کنیم و آنها را تست کنیم.
ایجاد یک پروژه در Wokwi
برای ایجاد یک پروژه جدید در سایت Wokwi به آدرس زیر مراجعه کرده و بعد از وارد شدن تا آخر اسکرول کرده و روی Blank Project بزنید
https://wokwi.com/dashboard/projects
بعد از این کار با صفحه زیر رو به رو می شوید که در صفحه سمت چپ این مکان برای کد های پروژه می باشد و در سمت برای مشاهده شبیه ساز می باشد.
حالا با زدن روی دکمه بعلاوه (Add a new part) دنبال قطعه ای برای به نام custom chip بگردید که به ما اجازه می دهد تراشه خود را طراحی کنیم
بعد از این کار و کلیک روی گزینه، به پروژه نام دلخواهی بدهید (در اینجا 8bit-cpu) و زبان برنامه نویسی آن را C انتخاب کنید زیرا که این پروژه با C نوشته شده و توصیه شده خود وب سایت می باشد.
بعد از ایجاد یک تراشه فایل های زیر ایجاد می شوند:
- فایل 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 ها می باشد:
- در خط های 1 تا 3 کتابخانه های استفاده شده در این پروژه وارد شده است
- در خط های 6 تا 12 یک enum برای شناسایی opcode تعریف شده است
این CPU تا این مرحله فقط کدهای عملیاتی BSF، BCF، GOTO و NOP را پردازش می کند.
مرحله بعدی تعریف structure های برای گرفتن دستور (Fetch)، مشخص کردن نوع دستور (Decode) و اجرای دستور (Execute) می باشد.
- خط های 29 تا 33 برای مشخص کردن اندازه operand یا عملوند ها می باشد.
- خط های 36 تا 43 برای خروجی Decode می باشد که برای نگهداری اطلاعات هر دستور مورد استفاده قرار می گیرد
- خط های 46 تا 49 نیز برای خروجی Execute می باشند، که در اینجا این structure برای دستور هایی که نیازی به آپدیت کردن RAM یا رجیستر ها ندارند مورد استفاده قرار می گیرد مثل دستور های مثل GOTO، NOP و یا SLEEP که نیازی به آپدیت کردن حافظه ندارند.
تعریف Structure های حافظه
حالا structure های مربوط به حافظه را تعریف می کنیم:
- خط های 1 تا 4 برای تعریف یک enum برای مشخص کردن نوع حافظه می باشند که از 0x00 تا 0x09 برای رجیستر ها و از 0x10 تا 0x1F برای RAM می باشد
- خط های 6 تا 11 برای گرفتن حافظه مورد استفاده قرار می گیرند که addr برای آدرس، value برای مقدار آن آدرس و valid برای درست بودن آدرس می باشد (1 به معنای valid و 0 به معنای invalid می باشد)، که برای چک کردن رنج آدرس می باشد
حالا متغیر های مربوط به 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 را تعریف می کنیم:
این آرایه با مقدار های عددی هر دستور (خروجی assembler) جای گذاری می شود و در ROM قرار میگیرد.
حالا تابع اصلی را آپدیت می کنیم و دستوراتی را برای بازه رنج 1Hz تا 1MHZ می نویسم:
- در خط 96 مقدار فرکانس اولیه را قرار می دهیم که در اینجا برابر با 10 هرتز می باشد
- در خط های 100 تا 105 دستوراتی را برای مشخص کردن بازه می نویسیم
بعد از مشخص کردن فرکانس حالا برنامه اصلی را در ROM قرار می دهیم:
تعریف پایه های تراشه برای CPU
بعد از این کار نیاز است که برای CPU پایه هایی را تعبیه کنیم:
فایل 8bit-cpu.chip.json را به شکل زیر تغییر می دهیم و شکل CPU را به وجود می آوریم:
برای ایجاد فاصله در تراشه از رشته های خالی برای پایه ها استفاده می شود. و VCC و GND مربوط به تغذیه تراشه می باشند.
بعد از این کار نیاز است که پایه ها را در تراشه استفاده کنیم و آنها را تبدیل به پایه های خروجی کنیم (با استفاده از کتابخانه خود Wokwi):
در کد بالا pin_init برای هر پایه استفاده می شود و آنها را برای خروجی مقدار دهی می کند (OUTPUT)، توجه داشته باشید که اسم های آرایه pins با اسم های مربوط به فایل 8bit-cpu.chip.json یکسان باشد.
بعد از این کار دو thread اصلی برنامه را اجرا می کینم:
سه خط کد اول برای thread مربوط به CPU با فرکانس داده شده و سه خط آخر مربوط به پایه های خروجی با سرعتی برابر با نصف فرکانس CPU (دلیل کمی زودتر بودن برای جلوگیری از ایجاد شدن مشکل هایی مثل آپدیت نشدن پایه ها می باشد)
چرا از Thread استفاده می کنیم؟
استفاده از thread ها باعث جلوگیری از پیچیدگی در کد و نوشتن کد هایی با عملکرد بیشتر می شوند. در این پروژه از دو thread استفاده شده است یکی برای پایه های خروجی و یکی هم حلقه اصلی CPU، دلیل استفاده thread برای پایه های خروجی این است که حلقه thread کار مربوط به آپدیت کردن پایه ها را انجام می دهد و با اینکار کد کمتری نیز نوشته می شود و باعث جلوگیری از مشکلاتی مثل spaghetti شدن کد می شود (کد های ناخوانا).
آپدیت کردن تابع 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 های پیچیده، راحتتر می شود.
در این تابع با استفاده از if و else و تابع ،edfb کد های عملیاتی را یک به یک چک کرده و با توجه به نوع کد که قبلا در این پست گفته شده، آنها را چک می کنیم و اطلاعات structure را با توجه آن داده ها آپدیت می کنیم و در نهایت opcode های ناشناخته را NOP در نظر می گیریم.
نوشتن توابع مربوط به اجرا (Execute)
در این مکان 2 تابع وجود دارد یکی برای اجرای اصلی که مربوط به دستوراتی می باشد که با حافظه سر و کار دارند و دیگری مربوط به دستوراتی که نیازی به تغییر چیزی در حافظه ندارند.
در این تابع دستوراتی که نیازی به تغییری در حافظه ندارند را می نویسیم:
یکی از کدهای که نیازی به تغییر در حافظه ندارد GOTO و SLEEP می باشند که در وبلاگ دستور SLEEP را بررسی نمی کنیم و در تابع بالا مقدار operand دستور GOTO برابر با upc می شود (شمارنده برنامه آپدیت می شود) و در نهایت در خط 206 برگردانده می شوند.
تابع بعدی مربوط به اجرای دستوراتی می باشد که با حافظه سر و کار دارند، اما قبل از این کار نیاز به تابع هایی داریم که مقدار های حافظه را برای ما برگرداند:
دو تابع بالا برای گرفتن و قرار دادن مقدار حافظه می باشند (RAM و رجیستر ها)
تابع get_mem با توجه به آدرس داده شده یک MEM_OUT بر میگرداند که بالاتر آن را تعریف کردیم و مشخص میکند که آیا با RAM یا با رجیستر ها سروکار داریم.
- آدرس رجیستر ها از 0x00 تا 0x09 در مجموع 10 عدد
- آدرس RAM از 0x10 تا 0x1F در مجموع 16 عدد
و تابع set_mem با توجه به ورودی خود مقدار حافظه را در مکانی که باید ذخیره میکند در غیر این صورت اگر در هر کدام از توابع مقدار آدرس ها valid نبود عملیاتی انجام نمی شود.
بعد از این دو تابع دیگر می توانیم تابع مربوط به اجرای اصلی را بنویسیم:
بعد از اینکار زمان نوشتن حلقه CPU اصلی می باشد:
حلقه اصلی پردازنده
حالا حلقه اصلی CPU را که بالاتر تعریف کردیم (cpu_cycle) را می نویسیم:
بعد از مشخص کردن متغیرها در خط 279 هر دستور را با توجه به شمارنده برنامه (PC) به decode_inst می دهیم و dcd را آپدیت میکنیم که دارای اطلاعات مربوط به دستور می باشد.
در خط 280 اجرای اولیه را انجام می دهیم در صورتی که دستور نیازی به تغییری در حافظه داشت این بخش انجام نمی شود در غیر این صورت در خط های 285 تا 289 شمارنده برنامه به روز شده.
و در نهایت در خط 292 دستوراتی که با حافظه سر و کار دارند اجرا می شوند.
توجه داشته باشید که هر کدام از پایه ها در gpio_handler بررسی می شوند و نیازی به آپدیت کردن آنها در این بخش نداریم.
تست و اجرای یک برنامه با CPU
حالا که CPU آماده استفاده شده است، بیاید با یک برنامه آن را تست کنیم:
در قسمت Simulation سایت Wokwi یک LED bar قرار می دهیم:
بعد از پیدا کردن LED Bar پایه های آن را همراه با VCC و GND به تراشه متصل می کنیم:
با وصل کردن پایه های GND و VCC به LED Bar و CPU حالا نوبت به دادن برنامه CPU می باشد که با آپدیت کردن آرایه program که خروجی Assembler به عدد می باشد ممکن می شود:
کد های بالا مربوط به برنامه pattern می باشند و در نهایت بعد از ذخیره و اجرای برنامه نتیجه زیر را داریم:
کد های مربوط به این پروژه را میتوانید در لینک Wokwi زیر پیدا کنید: