برای ساختن شبیه ساز که مانند یک CPU فیزیکی عمل کند. ابتدا نیاز هست که با نحوه کار با CPU یا پردازنده مرکزی آشنا شویم.
مهم ترین بخش یک کامپیوتر پردازنده مرکزی یا CPU میباشد که و که وظیفه اجرای دستور های موجود در کامپیوتر را بر عهده دارد. و بخش های داخلی CPU معماری آن را به وجود می آورند. که در ادامه با این اجزای داخلی بیشتر آشنا می شویم…
یک CPU از بخش های بسیار پیچیده ای ساخته شده است و opcode های بسیار بیشتری نسبت به میکرو کنترلر دارد، بخش های مانند CU برای بخش کنترل، بخش ALU که بخش منطقی و حسابی CPU می باشد و یا timer ها در CPU که وظیفه کنترل و ایجاد وقفه ها را دارند، یکی دیگر از تفاوت های یک CPU یا (Microprocessor) با Microcontroller تفاوت آنها در مقدار دستور ها یا اندازه ROM می باشد، یک میکروکنترلر از ROM بزرگتری برخوردار است در حالی که یک میکروپروسسور دارای RAM بیشتری می باشد.
عموما یک میکروکنترلر مشابه به CPU می باشد اما بسیار ساده تر و در این بخش ما سعی داریم که یک میکروکنترلر که چهار عملیات NOP، BSF، BCF و GOTO را انجام می دهد را با زبان برنامه نویسی C پیاده سازی می کنیم.
این حافظه محل نگهداری دستور ها برای دسترسی سریع میباشد. با توجه به دستور داخل CPU ممکن هست که نتیجه آن در آدرس داده شده در RAM نگهداری شود یا اینکه مقداری از RAM خوانده شود، البته RAM یک حافظه فرار است به معنی این که اگر CPU روشن نباشد (نبود برق) تمامی داده های داخل RAM از بین میرود!
این حافظه محل نگهداری دستور های CPU میباشد، و CPU با خواندن این داده ها عملیات های مورد نظر را انجام میدهد، بر خلاف RAM تمام داده (دستور) های ذخیره شده در این CPU با نبود برق از بین نمیرود، برای مثال آنها مانند فلش مموری ها هستند البته برای CPU و دستور های مربوط به آن.
رجیستر ها ساختاری مشابه به RAM را دارند اما محل نگهداری داده های “خروجی” CPU هستند، داده های که برای GPIO یا ورودی / خروجی های عمومی استفاده می شود، و راه های ارتباطی با دستگاه های خارجی را به وجود می آورند، مثل چاپگر و…، و دلیلی که از RAM استفاده نمی شود این است که استفاده از RAM سرعت انتقال / دریافت را کاهش می دهد و برای همین رجیستر ها که ساختاری مشابه با RAM را دارند مورد استفاده قرار می گیرند.
ساعت سیستم با ایجاد کردن سیگنال تمامی بخش های مختلف CPU را باهم همگام میکند (sync) تا تمامی دستور ها مرحله به مرحله انجام شوند.
مثل تمامی پروسه ها CPU نیز در یک حلقه با هر سیگنال ساعت که شمارنده برنامه دریافت می کند یک دستور را ار ROM دریافت میکند (Fetch)، نوع دستور را مشخص میکند (Decode) و بعد با توجه به نوع دستور آن را اجرا میکند (Execute) و بعد دستور بعدی از ROM و این چرخه با توجه به دستور ها و شمارنده برنامه ادامه دارد…، در این مرحله بخش های دیگری از CPU هستند که به آنها می پردازیم…
شمارنده برنامه یا Program Counter یک شمارنده هست که مقدار آن با توجه به سیگنال ساعت تغییر میکند، این شمارنده در هر زمان با توجه به دستور اجرا شده به آدرسی از ROM اشاره میکند و CPU از این طریق متوجه میشود که در هر سیگنال کدام مقدار را ار ROM بخواند و آن را اجرا کند.
بخش Fetch وظیفه دریافت دستور را از ROM دارد که آدرس دریافتی را PC یا شمارنده برنامه مشخص میکند و بعد از دریافت مقدار از ROM آن دستور را برای شناسایی به بخش Decode (رمز گشایی) می فرستد.
این بخش وظیفه مشخص کردن نوع دستور را دارد، که مشخص میشود که این دستور یک فرایند حسابی است یا مقدار آن باید در RAM یا یکی از Register ها ذخیره شود یا این مقدار نیاز به تغییر مکان داده ها باشد و…، که بعد از مشخص شدن نوع دستور، دستور وارد مرحله اجرا یا (Execute) می شود.
بخش Execute وظیفه اجرا دستور داده شده را دارد، که با توجه به نوع دستور ممکن است که مقدار یکی از رجیستر ها یا RAM تغییر کند یا مقداری برای ذخیره سازی به مکانی دیگر فرستاده شود و یا این که ممکن است منجر به تغییر مقدار شمارنده برنامه (PC) شود، که این کار بیشتر برای ایجاد یک چرخه در CPU و یا عبور از بخشی از دستور های ROM یا تغییر مقداری از ورودی/خروجی های عمومی (GPIO) شود.
پایه های ورودی / خروجی عمومی وظیفه انتقال یا دریافت داده ها را از وسیله یا دستگاه های خارجی را دارند، و مقدار آنها ممکن است با توجه به آدرس هایی خاصی از رجیستر ها باشند که برای این کار رزرو شده اند.
با توجه به مقدار ورودی به decode از ROM مکانی که داده های به صورت هگز (Hexadecimal) ذخیره میشوند، بخش decode مقدار ورودی هگز (از 000 تا FFF) را شناسایی میکند و با توجه به کد عملیاتی آن نوع دستور را مشخص می کند!
یکی از راه های شناسای کد های عملیاتی، تشخیص کد از روی باینری آنهای میباشد، که معمولا 3 تا 4 دستور اول کد باینری (از چپ) بیانگر نوع کد عملیاتی میباشد، در ادامه مثال های بیشتری را برای تشخیص کد های عملیاتی بررسی می کنیم.
نحوه استفاده از دستور BSF به شکل زیر می باشد:
BSF addr, bit
که در اینجا addr بیانگر آدرس رجیستر و bit، شماره بیت میباشد (راست به چپ).
وظیفه این دستور این است که بیت داده شده در آدرس داده شده را برابر با 1 قرار می دهد.
برای مثال مقدار ورودی به بخش decode که محل مشخص کردن کد عملیاتی است برابر است با 0x506 که مقدار هگز ورودی میباشد، حالا اگر این مقدار به باینری (binary) تبدیل بشود، معادل زیر را دارد:
0b010100000110
که در این کد، چهار بیت اول (از سمت چپ) بیانگر opcode بخش BSF میباشد. که برابر است با 0101 و 3 بیت بعدی برای شماره بیت و 5 بیت آخر برای آدرس، به صورت کلی:
0101 bbbf ffff
که مقدار های f برای آدرس و b برای شماره بیت!
این کد رفتاری مانند BSF دارد اما مقدار مشخص شده را برابر با 0 قرار می دهد
برای مثال دستور:
BCF addr, bit
مقدار بیت مشخص شده در آدرس داده شده را برابر با 0 قرار می دهد!
برای مثال ورودی بخش decode برابر است با مقدار هگز 0x406 که شکل باینری آن به صورت زیر می باشد:
010000000110
که این دستور نیز مانند دستور BSF چهار بیت اول (از سمت چپ) بیانگر مقدار opcode برای دستور BCF میباشد که برابر است با 0100، در نهایت شکل کلی به صورت زیر می باشد:
0100 bbbf ffff
این کد برای تغییر مقدار شمارنده برنامه PC استفاده می شود که ساختار زیر را دارد:
GOTO label
که مترجم یا assembler مقدار label را برابر با آدرس آن قرار می دهد، دستور GOTO مقدار PC را تغییر میدهد و این میتواند برای ایجاد حلقه ها یا عبور از بخشی از دستور ها که نیاز به اجرا شدن ندارند مفید باشد.
برخلاف BSF و BCF کد عملیاتی GOTO فقط شامل 3 بیت اول است (از چپ) و ادامه آن برای آدرس می باشد.
برای مثال ورودی برابر است با 0xa00 که مقدار باینری آن برابر است با:
1010 0000 0000
که مقدار 101 بیانگر GOTO می باشد و ادامه بیت ها برای آدرس GOTO است.
کد عملیاتی NOP یا No Operation بیشتر برای ایجاد وقفه در برنامه استفاده می شود، و برای انجام عملیات خاصی نیست، هر بار که دستور NOP موجود باشد میکروکنترلر عملیاتی را انجام نمی دهد و PC به آدرس بعدی اشاره میکند
مقدار هگز ورودی آن همیشه یکسان و برابر است با 0x000 که مقدار باینری زیر را دارد:
0000 0000 0000
پردازنده مرکزی کدهای ما که به صورت اسمبلی (Assembly) نوشته می شود را متوجه نمی شود و برای این کار ما نیاز به یک مترجم (Assembler) داریم که کد های نوشته شده به اسمبلی را به دستور های برای ماشین (هگز) تبدیل کند.
کاری که اسمبلر انجام می دهد این است که تمام کد های داده شده به اسمبلی را خط به خط بررسی می کند. و بعد آنها را با توجه به opcode دستور به دستور های دوتایی هگز تبدیل میکند (FFF).
برای مثال به کد اسمبلی زیر توجه کنید:
و بعد از آن و انجام عملیات bitwise ها مقدار را در یک لیست برای مجموع دستور ها ذخیره می کند!
کاری که این دستور انجام می دهد برای قرار دادن اولین بیت از رجیستر 0x06 به 1 است.
و در نهایت مقدار محاسبه شده به لیست دستور ها اضافه می شود.
کاری که این دستور انجام می دهد این است که اولین بیت از سمت راست از رجیستر GPIO را برابر با 0 (clear) قرار می دهد.
و بعد assembler نتیجه را به لیست دستور ها اضافه می کند. کاری که این دستور انجام میدهد این است که مقدار PC در میکروکنترلر را برابر با اندازه داده شده (0) قرار می دهد و این باعث ایجاد یک چرخه می شود که دستور ها از BSF تا GOTO مداوم تکرار شوند.
نتیجه کلی دستور ها به شکل زیر می باشد:
و بعد از این کار assembler تمامی دستور های لست دستور ها را به صورت هگز های دوتایی به صورت باینری در یک فایل ذخیره می کند مثل blink.bin که bin بیانگر باینری است (که این فایل تمام دستور های شبیه ساز می باشد (ROM بخش میکروکنترلر)) و اگر فایل را با hex-editor ها یا برنامه هایی که برای دیدن هگز هستند باز کنیم چیزی شبیه به شکل زیر را دارد:
کد مربوط به این بخش:
https://github.com/empitrix/assembler
برای این کار اول نیاز است که متغیرهای شبیه ساز را بررسی کنیم:
اولین متغیر برای ذخیره سازی دستور های ROM می باشد به بیان دیگر یک متغیر برای ROM این میکروکنترلر که به صورت زیر می باشد:
که این متغیر دارای ساز 128 برای ذخیره دستور ها می باشد و unsigned short برای بیان این که این متغیر مقادیر بین 0x0000 تا 0xFFFF را ذخیره میکند یا به عبارت دیگر برابر با 256 (از 00 تا FF) برای دستور ها (فایل خروجی از assembler).
دو متغیر بعدی برای RAM و Register ها می باشند:
در این شبیه ساز از دو متغیر با تایپ uint8_t که بیانگر unsigned char یا از 0 تا 255 (مجموع 256) است استفاده شده است، که می توانند مقدار های هگز بین 0x00 تا 0xFF را در خود ذخیره کنند.
که آدرس RAM از 0x10 شروع می شود و در 0x1F پایان میابد، (در مجموع 10 عدد)
متغیر PC یا شمارنده برنامه نیز به صورت زیر تعریف می شود:
برای ایجاد ROM اول نیاز است که فایل مربوط که همان خروجی مترجم (assembler) را بخوانیم و array مربوط به ROM را آپدیت کنیم که به صورت زیر انجام می شود:
برای اینکار با یک حلقه هر دستور rom را مورد بررسی قرار می دهیم
برای این کار نیاز به دو مقدار opcode و operand از هر دستور (instruction به مخفف inst) نیاز داریم
در خط دوم، ما مقدار inst را 8 خانه به راست جابه جا کردیم (مقدار کل: 12) و از 4 دستور اول (از راست) را برای opcode استفاده می کنیم، و در خط سوم از 8 خانه اول از سمت راست برای مقدار عملوند یا operand استفاده میکنیم
با تعریف کردن enum زیر برای opcode ها میتوان که به بررسی آنها کمک کرد، که هرکدام با مقدار عددی باینری opcode مقدار دهی شده اند:
و حالا با استفاده از متغیر های opcode و operand می توان کد switch/case زیر را برای تشخیص آنها نوشت:
با دادن opcode به switch میتوان کار تشخیص را بسیار راحت تر کرد
در بخش execute که محل اجرای دستور ها می باشد با متغیر های از قبل تعیین شده و با استفاده از آنها دستور ها را اجرا می کنیم:
در این بخش بار دیگر از switch/case استفاده میکنیم تا بخش های مربوط به اجرا شدن را بررسی کنیم.
تا این بخش دستور های BSF، BCF، NOP و GOTO مورد بررسی قرار گرفتند و با نحوه اجرا شدن آنها آشنا شدیم.
کد های مربوط به این بخش:
https://github.com/empitrix/8bitcpu
برای آشنایی با نحوه کارکرد CPU و آموزش، ساختن یک شبیه ساز (emulator) گزینه مناسبی می باشد، و با نحوه ایجاد کد ماشین و اجرا شدن آن در میکروکنترلر بیشتر آشنا می شویم، و از آنجایی که میکرو کنترلر ها پردازنده های ساده تری نسبت به CPU هستند، تلاش برای ساختن آنها می تواند بسیار آموزنده باشد، جدای از این یک شبیه ساز میتواند کنترل بیشتری برای تست کردن را به ما بدهد یا میتوان از آنها زمانی که سیستم واقعی موجود نباشد استفاده کرد.
یک Simulator مشابه به چیزی هست که از روی آن ساخته می شود اما تمامی رفتار های آن را ندارد و بیشتر برای بررسی یک قطعه یا بخش … مورد استفاده قرار می گیرد، در حالی که یک Emulator تمامی رفتار های مشابه را دارد و دقیقا به صورتی که یک دستگاه یا قطعه کار می کند، رفتار میکند و همان نتایج را به موجود می آورد ولی simulator این کار را انجام نمی دهد و بیشتر برای مطالعه آن دستگاه / محصول می باشد.
من مهدی هستم و دلیل انجام این پروژه تمرین برای یادگیری زبان C و آشنایی بیشتر با نحوه انجام کار میکروکنترلر ها و آشنایی بیشتر با machine code و نحوه اجرای دستور ها در میکروکنترلر ها بود.
از جمله آشنای با نحوه تبدیل کد های assembly به کد ماشین و اجرای آنها توسط یک پردازنده، که بسیار سرگرم کننده است.
کد مربوط به بخش assembler را که برای سیستم های linux است را میتوان از لینک زیر دریافت کرد
https://github.com/empitrix/assembler
و کد مربوط به شبیه ساز میکروکنترلر 8 بیتی را میتوان از لینک github زیر دریافت کرد:
https://github.com/empitrix/8bitcpu
که برنامه های ورودی این پروژه را میتوان با assembler به وجود آورد.
عالی❤
بیشتر از این چیزها بزار🫡🙏🏻
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.