منطق دیجیتال یا منطق بولی (Boolean logic)، یکی از اساسیترین مفاهیم در ساخت سیستمهای کامپیوتری مدرن است. منطق دیجیتال مجموعه قواعدی است که اتخاذ تصمیمات پیچیده را بر اساس سوالهای «بله/خیر» ممکن میکند.
مدارهای منطقی دیجیتال به دو دسته تقسیم میشوند:
در منطق ترکیبی، با تغییر ورودی، خروجی نیز تغییر میکنند. البته به دلیل تأخیر انتشار سیگنال از طریق عناصر مدار، این عمل مقداری زمان میبرد. مدارهای ترتیبی دارای یک سیگنال کلاک هستند و وضعیت مدار، با لبههای کلاک تغییر میکند. عموماً مدارات ترتیبی به وسیلهی بلوکهایی از مدارات ترکیبی ساخته میشوند. این بلوکها توسط عناصر حافظهای که به وسیلهی سیگنال کلاک فعال میشوند از هم جدا شدهاند.
منطق دیجیتال اهمیت زیادی در برنامهنویسی دارد. آشنایی با منطق دیجیتال اتخاذ تصمیمهای پیچیده در برنامهنویسی را ممکن میکند. برنامهنویسی نکات ریزی دارد که فهم آنها بسیار مهم است.
تمامی مدارهای ترکیبی از پنج «گیت» (Gate) منطقی اساسی تشکیل شدهاند:
گیت دیگری نیز وجود دارد که گیت نات (NOT) یا وارونگر میباشد. وارونگرها در واقع گیت منطقی محسوب نمیشوند، زیرا تصمیم خاصی نمیگیرند. سایر گیتها با توجه به مقادیر ورودی، به نوعی درباره خروجی تصمیم میگیرند ولی گیت نات تنها یک ورودی را دریافت میکند و نقیض آن را در خروجی قرار میدهد. اگر ورودی یک وارونگر ۱ باشد، خروجی ۰ خواهد بود و اگر ورودی ۰ باشد، خروجی ۱ خواهد بود.
برای شکل بالا به نکات زیر توجه کنید:
معمولاً مدارهای منطقی با استفاده از همین شش نماد نشان داده میشوند. ورودیها در سمت چپ قرار گرفته و خروجیها نیز در سمت راست قرار میگیرند. با وجود اینکه ورودیها میتوانند به یکدیگر وصل شوند؛ اما خروجی گیتها هرگز نباید مستقیماً به هم متصل شوند. اگرچه یک خروجی میتواند به یک یا چندین ورودی وصل شود.
برای توصیف بلوکهای ساده و ابتدایی توضیحات ارائه شده کافی به نظر میرسد. اما میتوان از ابزار مفیدی به نام «جدول صحت» نیز استفاده کرد. جداول صحت خروجی یک مدار را با توجه به ورودیهای ممکن تعیین میکنند. جداول صحت شش گیت اصلی در زیر آورده شدهاست:
جداول صحت را میتوان به اندازهی دلخواه و با تعداد ورودی و خروجی دلخواه توسعه داد. به عنوان مثال جدول صحت مداری با چهار ورودی به صورت زیر است:
برای راحتی بیشتر میتوان یک عملیات منطقی را در قالب عبارات سادهی ریاضی نشان داد. برای این منظور به عملیاتهای AND، OR، XOR و NOT نمادهای منحصر به فردی اختصاص داده میشود.
دو گیت NAND و NOR با متمم گرفتن از عبارات AND و OR نشان داده میشوند.
منطق ترکیبی بسیار سودمند است، اما امروزه تنها با تکیه بر منطق ترکیبی و بدون استفاده از منطق ترتیبی، پردازشهای کامپیوتری امکانپذیر نخواهد بود.
منطق ترتیبی استفاده از حافظه را در سیستمها ممکن میکند. همانگونه که قبلاً اشاره شد، خروجی مدارات ترتیبی پس از یک تأخیر مشخص تولید میشود. مقدار این تأخیر به عوامل بسیار زیادی از جمله فرآیند ساخت قطعات مورد استفاده، دمای سیلیکون و پیچیدگی مدار بستگی دارد. اگر خروجی نهایی یک مدار به نتایج دو مدار ترکیبی دیگر وابسته باشد و این نتایج در زمانهای متفاوتی آماده شوند (که در واقعیت همین موضوع اتفاق میافتد)، مدار ترکیبی دچار یک «خطای لحظهای» (glitch) میشود و در نتیجه ممکن است خروجی ثابتی مطابق با عملیات مورد نظر نداشته باشد.
یک مدار ترتیبی در زمانهای معینی از خروجی نمونه برداری کرده و آن را منتشر میکند. اگر ورودی مدار در بین این زمانهای معین موقتاً تغییر کند از آن چشمپوشی میشود. معمولاً این زمان نمونه برداری در تمامی مدار، همزمان یا سنکرون (synchronized) است که به آن «کلاک» (Clock) گفته میشود. وقتی از سرعت یک کامپیوتر صحبت میکنیم، منظور همان مقدار کلاک است. البته میتوان مدارهای غیرهمزمان یا آسنکرون (asynchronous) نیز طراحی کرد که با سیگنال کلاک سنکرون نیستند.
هر مدار دیجیتال دو مشخصهی حداکثر زمان تأخیر و حداقل زمان تأخیر دارد. اگر مدار از حد انتظار سریعتر بوده و تأخیر آن کمتر از حداقل زمان تأخیر باشد، مدار به درستی کار نخواهد کرد. به طور مثال اگر این مدار جزئی از یک دستگاه بزرگتر مانند CPU کامپیوتر باشد، کل دستگاه غیر قابل استفاده خواهد شد. در صورتی که مدار از حد انتظار کندتر بوده و تأخیر آن از حداکثر زمان تأخیر مدار بیشتر باشد، میتوان سرعت کلاک را برای تطبیق با کندترین بخش سیستم کاهش داد. با افزایش دمای سیلیکونهای سازندهی یک مدار، حداکثر زمانهای تأخیر آن نیز افزایش مییابند. به همین دلیل کامپیوترها با افزایش دما و یا افزایش سرعت کلاک (overclocking)، عملکرد بیثباتی دارند.
همانند منطق ترکیبی، تعدادی عنصر مداری اصلی وجود دارند که بلوکهای سازندهی مدارهای ترتیبی را شکل میدهند. این عناصر در واقع از همان گیتهای منطقی ساخته شدهاند. با این تفاوت که برای تثبیت ورودی، از خروجی فیدبک (بازخورد) گرفته شده است. این عناصر به دو دستهی «لچها» (latch) و «فلیپفلاپها» (flip-flop) تقسیم میشوند. اگرچه گاهی اوقات این اصطلاحات به جای یکدیگر نیز به کار میروند، اما لچها به دلیل نداشتن کلاک، کاربرد کمتری دارند. در ادامه درباره فلیپفلاپها صحبت خواهیم کرد.
فلیپفلاپ D سادهترین نوع فلیپفلاپ است. در این فلیپفلاپها، لبهی کلاک ورودی در خروجی قرار گرفته است. منظور از لبه، لحظهای است که وضعیت سیگنال تغییر میکند. اگر کلاک از ۰ به ۱ تغییر کند یک لبهی بالارونده (rising edge) رخ داده است و اگر از ۱ به ۰ تغییر کند یک لبهی پایینرونده (falling edge) اتفاق افتاده است. در بیشتر موارد فلیپفلاپها به لبهی بالاروندهی کلاک حساس هستند، اما بعضاً در ورودی کلاک یک وارونگر (NOT) قرار داده میشود تا حساس به لبهی پایینرونده شود؛ در چنین مواردی در شماتیک فلیپفلاپ قبل از ورودی کلاک یک حباب قرار داده میشود که نماد همان وارونگر است.
معمولاً ورودی کلاک با قرارگیری شکل یک مثلث کوچک چسبیده به کنار شماتیک از سایر ورودیها متمایز میگردد. اکثر فلیپفلاپها دو پایهی خروجی دارند: یک خروجی معمولی و یک خروجی متمم (وارون).
فلیپفلاپ T تنها اندکی پیچیدهتر از فلیپفلاپ D است. T مخفف کلمهی Toggle به معنای «تغییر وضعیت» است. اگر ورودی T مقدار ۱ باشد، با لبهی کلاک وضعیت خروجی تغییر خواهد کرد (یعنی اگر خروجی ۱ باشد ۰ میشود و برعکس)؛ و اگر ورودی ۰ باشد، خروجی همان مقدار قبلی خود باقی مانده و تغییری نمیکند. این فلیپ-فلاپ نیز دارای دو خروجی است که دومی همان متمم خروجی اصلی است.
یکی از کاربردهای فلیپفلاپ T در مدارات تقسیم فرکانس کلاک است. اگر ورودی T همواره در وضعیت ۱ نگه داشته شود، فرکانس سیگنال خروجی نصف فرکانس سیگنال کلاک خواهد بود. در نتیجه با بهکارگیری زنجیرهای از فلیپفلاپهای T میتوان کلاکهایی کندتر از کلاک اصلی تولید کرد.
برای تشریح عملکرد فلیپفلاپ JK برخلاف دو مورد قبلی به جدول صحت نیاز داریم. این فلیپفلاپ دارای دو ورودی J و K بوده و خروجی بسته به وضعیت قبلی خودش و همچنین بر اساس وضعیت دو ورودی، میتواند ثابت بماند یا ۱شود (set) یا ۰ شود (clear) و یا تغییر وضعیت دهد. مانند سایر فلیپفلاپها مقادیر ورودی و خروجی تنها در لحظات وقوع لبهی کلاک اهمیت دارند.
مدارهای ترتیبی علاوه بر «تأخیر انتشار» (Propagation Delay)، دارای پارامترهای «زمان راهاندازی» (Setup Time) و «زمان توقف» (Hold Time) نیز میباشند. آشنایی با این سه موضوع برای طراحی صحیح مدارهای ترتیبی بسیار ضروری است. زمان راهاندازی، حداقل فاصلهی زمانی است که سیگنال ورودی فلیپفلاپ باید زودتر از لبهی بالاروندهی کلاک برسد تا مقدار آن به درستی دریافت و نگه داشته شود. به همین ترتیب، زمان توقف حداقل فاصلهی زمانی پس از لبهی بالاروندهی کلاک است که سیگنال باید ثابت و بدون تغییر بماند.
در حالی که زمان راهاندازی و توقف به صورت مقادیر کمینه بیان شدند، مقدار تأخیر انتشار به عنوان یک مقدار بیشینه بیان میشود. به بیان سادهتر، تأخیر انتشار حداکثر فاصلهی زمانی است که پس از یک لبهی پایینروندهی کلاک انتظار دارید تا سیگنال جدید را در خروجی مشاهده کنید. برای درک بهتر این مفاهیم به شکل زیر توجه کنید:
توجه کنید در شکل بالا، حالات گذار یا لبههای سیگنالها با خطوط مورب نشان داده شدهاند. این عمل به دو علت انجام گرفته است: اولاً لبههای کلاک و دیتا هیچگاه زاویهی قائمه ندارند و همواره زمان نزول و زمان صعود آنها غیر صفر است. ثانیاً زمانهای مختلفی که خطوط عمودی سیگنالها را قطع کردهاند، بهتر مشخص است.
ترکیب این سه مقدار، بالاترین سرعت کلاک قابل استفاده در سیستم را مشخص میکند. اگر مجموع تأخیر انتشار یک بخش از مدار و زمان راهاندازی بخش بعدی، از فاصلهی زمانی بین لبهی پایین روندهی یک پالس کلاک و لبهی بالاروندهی پالس بعدی بیشتر باشد، سیگنال دیتا در ورودی جزء دوم ثابت و پایدار نخواهد بود و باعث بروز رفتار پیشبینی نشدهای در مدار خواهد شد.
عدم توجه به زمانهای راهاندازی و توقف در هنگام طراحی مدار موجب بروز پدیدهای به نام «شبهپایداری» (Metastability) میشود. هنگامی که یک مدار در وضعیت شبهپایدار قرار دارد، ممکن است خروجی فلیپفلاپ بین دو وضعیت ۰ و ۱ با سرعت زیادی (اغلب بالاتر از فرکانس کلاک مدار) نوسان کند.
از آنجا که شبهپایداری باعث افزایش جریان مصرفی تراشه میشود، میتواند مشکلات زیادی همچون عملکرد نادرست مدار و حتی آسیب دیدن تراشه را ایجاد کند. اگرچه وضعیت شبهپایداری معمولاً خود به خود و با گذشت زمان از بین میرود؛ اما حتی پس از آن نیز ممکن است کل سیستم در یک وضعیت نامعلوم قرار گیرد. لذا باید سیستم به طور کامل از نو راهاندازی شود تا به وضعیت کاری درست خود بازگردد.
عمدهترین علت بروز مشکلات شبهپایداری استفاده از چندین کلاک غیریکسان در سیستم است؛ البته حتی اگر دو کلاک با فرکانس نامی یکسان ولی از دو منبع متفاوت تولید شوند، باز هم تفاوت کوچکی باهم خواهند داشت. به همین علت ممکن است در یک لحظه لبهی کلاک با لبهی سیگنال داده بسیار به هم نزدیک شده و باعث نقض زمان راهاندازی شوند.
یک راه حل برای این مشکل این است که تمام ورودیها را از یک جفت فلیپفلاپ D به صورت طبقهطبقه (cascade) عبور دهیم. در این وضعیت حتی اگر فلیپفلاپ اول به وضعیت شبهپایداری برود، امیدوار خواهیم بود تا قبل از رسیدن لبهی بعدی کلاک به وضعیت پایدار برسد و اجازه دهد تا فلیپفلاپ دوم داده را به درستی بخواند. این کار باعث ایجاد تأخیر یکسیکلی (یک دورهی تناوب از سیگنال کلاک) در لبههای دادهی ورودی میشود که در مقایسه با ریسک ناشی از شبهپایداری چندان مهم نیست.
منطق بولی در دنیای برنامهنویسی نیز کاربرد بسیار زیادی دارد. اکثر برنامهها درختهای تصمیمی (Decision Trees) هستند که در صورت برقراری یک شرط، کار خاصی را انجام میدهند. برای توضیح این موضوع، از کدهای C در محیط آردوئینو استفاده خواهیم کرد.
منظور از «منطق بیتی»، دستهای از عملیاتهای منطقی است که نتیجهی آنها تنها یک مقدار میباشد. به طور مثال در قطعه کد زیر:
میتوان با استفاده از a و b یک عملیات بیتی انجام داده و نتیجه را در c ریخت. چگونگی این کار در قطعه کد زیر مشخص است:
به عبارت دیگر، در نتیجهی به دست آمده هر بیت از انجام عملیات موردنظر روی دو بیت منتاظر در عملوندها به دست میآید.
استفاده از عملگرهای بیتی، دستکاری رجیسترها را راحتتر کرده است. با این عملگرها میتوان بهطور انتخابی تک بیتها را یک (set) و صفر (clear) کرد و یا تغییر وضعیت داد (toggle). همچنین میتوان ۰ یا ۱ بودن یک یا چندین بیت را تشخیص داد. مثالهای زیر نحوهی استفاده از این عملگرها را نشان میدهد. ضمناً هر «نیبل» (nibble) چهار بیت است:
c = b00001111 & a; // clear the high nibble of a, but leave the low nibble alone. // the result is b00000101. c = b11110000 | a; // set the high nibble of a, but leave the low nibble alone. // the result is b11110101. c = b11110000 ^ a; // toggle all the bits in the high nibble of a. // the result is b10100101.
یکی از عملیاتهای بیتی پرکاربرد بر روی دادهها، شیفت بیتی است. در این عملیات، بیتهای عملوند به تعداد دفعات مشخصی به سمت چپ یا راست منتقل میشوند. با یک شیفت به هر طرف، آخرین بیت از همان سمت حذف شده و به سمت دیگر یک ۰ اضافه میشود.
یکی از مهمترین کاربردهای شیفت بیتی در عملیاتهای ضرب و تقسیم است. هر شیفت به راست معادل تقسیم بر دو بوده (اگرچه باقیمانده از دست میرود) و هر شیفت به چپ معادل ضرب در دو میباشد. معمولاً عملیاتهای ضرب و تقسیم در پردازندههای کوچک، زمان زیادی طول میکشد (مانند آردوئینو)؛ اما شیفتهای بیتی کارآمدتر بوده و کاربرد زیادی نیز دارند. بعداً در مورد کاربردهای شیفت بیتی بیشتر صحبت خواهیم کرد.
دستهای از عملگرها وجود دارند که دو مقدار را مقایسه کرده و با توجه به نتیجهی مقایسه، در خروجی مقدار «صحیح» (TRUE) یا «غلط» (FALSE) میدهند.
معمولاً یکسان بودن نوع دادههایی که مقایسه میشوند اهمیت زیادی دارد. به طور مثال در صورت مقایسهی یک داده از نوع int با دادهای از نوع byte خطا رخ میدهد.
این دسته از عملگرها بدون تولید مقدار جدیدی، خروجی true یا false میدهند. عملگرهای منطقی شباهت بسیار زیادی به «حروف ربط» دارند. به طور مثال معادل جملهی «اگر هوا بارانی نباشد و باد بوزد، بادبادک هوا خواهیم کرد.» در زبان C به صورت زیر است:
به پرانتزها در اطراف دو بند شرطی if دقت کنید. اگرچه وجود آنها الزامی نیست، اما گروهبندی این بندهای شرطی با پرانتز، به خوانایی بیشتر برنامه کمک زیادی میکند. همچنین عملگر AND منطقی (&&) بر اساس اینکه بندهای شرطی برقرار باشند و یا نباشند، یک جواب true یا false تولید میکنند. البته میتوان در هر یک از بندهای شرطی مقدار عددی نیز قرار داد.
معنی عبارت بالا این است که اگر هوا بارانی نباشد، مادامی که سرعت وزش باد بیشتر از ۵ باشد یا سرمان شلوغ نباشد، بادبادک هوا خواهیم کرد. توجه کنید اگر پرانتزهای دور عبارت (windSpeed >= 5) || (reallyBusy != true) را حذف کنیم، دستور مبهمی نوشتهایم که ممکن است نتیجهی دلخواه ما را تولید نکند. اکنون که با نحوهی نوشتن دستورات پیچیدهی منطقی آشنا شدهایم، میتوانیم با انواع دستورات شرطی آشنا شده و کنترل بهتری بر روی جریان اجرای کدهای برنامه داشته باشیم.
در سادهترین ساختارهای تصمیمگیری از if/else استفاده میشود. دستور if/else if امکان قرار دادن مجموعهای از تستها را به ما میدهد؛ اما در هر لحظه تنها امکان اجرای یکی از آنها وجود دارد.
با توجه به دستورات بالا، اگر خیلی سرمان شلوغ باشد، هرگز بادبادک هوا نمیکنیم؛ ولی اگر آنقدرها سرمان شلوغ نباشد و شرایط آبوهوایی مطابق میل ما نباشد، به کار روزانهی خودمان میپردازیم.
در مثال بالا دستور ()else if را به ()if تبدیل میکنیم. اگر شرایط دستور دوم برقرار باشد، حتی در صورت برقراری شرایط دستور اول، اساساً برنامه تنها برای لحظاتی دستور اول را اجرا کرده و سریعاً به سراغ دستور دوم میرود. علاوه بر این اگر شرایط دستور دوم برقرار نباشد، برنامه پس از اندک زمانی اجرای دستور اول، سریعاً دستور سوم را اجرا میکند. پس عملاً هیچگاه زمان کافی برای اجرای دستور اول وجود نخواهد داشت!
بهجای استفاده از زنجیرهای از دستورات if/else میتوان از دستور switch/case/default استفاده کرد. اگرچه دستور ()if دستور قدرتمندتری است، اما این دستور خوانایی بیشتری دارد.
اگرچه دستور ()switch تنها قابلیت بررسی برابر بودن مقادیر را دارد، اما همین موضوع نیز بسیار پرکاربرد است. default قسمتی است که در صورت اجرا نشدن هیچ یک از case ها، اجرا میشود. نوشتن default کاملاً الزامی نیست و اگر default وجود نداشته باشد، در صورت اجرا نشدن هیچ یک از case ها، هیچ عملی انجام نمیشود. البته بهتر است که این قسمت را در برنامهی خود قرار دهید. break از شرط فعلی خارج شده و اجرای case مورد نظر را خاتمه میدهد. استفاده از break در تمام دستورات شرطی امکانپذیر است. عدم استفاده از break در انتهای هر case، باعث ایجاد خطا در برنامه خواهد شد.
تا بدین جای کار با کدهایی آشنا شدهایم که تنها برای یک بار تصمیمگیری به کار میروند؛ اما برای تکرار یک عمل (مادامی که شرط برقرار باشد) از دستورات ()while و ()do…while استفاده میکنیم.
هنگامی که برنامه به دستور while میرسد، ابتدا شرط را بررسی میکند (در مثال بالا بارانی بودن هوا) و اگر این شرط برقرار بود، کد را اجرا میکند. بعد از اجرای کد، بار دیگر شرط ارزیابی شده و در صورت برقرار بودن، دوباره کد اجرا میشود. این فرآیند تا زمانی که شرط حلقه false شود و یا برنامه با دستور break مواجه شود، تکرار خواهد شد.
در صورت نیاز میتوان از دستور ()if، دستور ()switch، حلقهی ()while دیگر و … درون حلقهی ()while استفاده کرد.
در حلقهی بالا تا زمانی که هوا بارانی باشد بادبادک هوا خواهیم کرد، مگر اینکه باران ببارد.
حلقهی ()while را با اندکی تغییر میتوان به حلقهی ()do…while تبدیل کرد. در ()do…while کدهای داخل کروشه حداقل یک بار اجرا میشوند، حتی اگر شرط حلقه برقرار نباشد. مثلاً در مثال زیر بادبادک هوا کردن حداقل یک بار اتفاق میافتد، حتی در نبود باد.
به عنوان نکتهی پایانی، با نوشتن عبارت TRUE به جای شرط حلقه، آن کد بینهایت بار اجرا میشود.
در کد بالا، بدون توجه به بارش باران، وزش باد و … بادبادک هوا خواهیم کرد. همانگونه که اشاره شد اجرای این کد خودبهخود متوقف نمیشود، اما کماکان میتوان با استفاده از دستور break از حلقه خارج شد.
از حلقهی ()for برای اجرای قطعهای از کد به تعداد دفعات مشخص استفاده میشود. در مثال زیر نحوهی استفاده از حلقهی ()for مشخص است:
درون پرانتز مربوط به حلقهی for سه قسمت قرار دارد که به وسیلهی سمیکالن (;) از هم جدا شدهاند. در قسمت اول یک متغیر تعریف و مقدار دهی میشود. این متغیر در قسمتهای شرطی و شمارنده کاربرد دارد. قسمت دوم شرط حلقه است. در صورت برقراری این شرط، حلقه اجرا شده و در غیر این صورت برنامه از حلقه خارج میشود. آخرین قسمت نیز گام یا شمارندهی حلقه است که کاهنده و یا یک افزاینده قرار میگیرد.
رایجترین خطا در حلقهی ()for خطای off-by-one) OBOE) میباشد؛ مثلاً قصد دارید کدی ۱۰ مرتبه اجرا شود، اما این کد ۹ یا ۱۱ بار اجرا میشود. این خطا معمولاً در اثر استفاده از «<=» به جای «<» یا برعکس رخ میدهد.