در قسمت قبل آموزش برنامه نویسی C به نمایش مکمل دو در اعداد صحیح با علامت (Two’s Complement) پرداختیم. در این قسمت به بررسی بیت ها در امبدد C می پردازیم.
دنیای سختافزار شیفتهی تعریف همه چیز بر اساس بیتها است. چرا؟ چون سیگنالها هنگام خروج از تراشه، از پینهایی با نامی مانند GPIO A-3 (یعنی بیت سوم رجیستر GPIO A) خارج میشوند. از آنجایی که هر پین تنها میتواند دو حالت بالا (1) یا پایین (0) داشته باشد، به راحتی با یک بیت نمایش داده میشود.
اما از دید یک برنامهنویس، این سیگنالها معمولاً در قالب رجیسترهای 8، 16 یا 32 بیتی بستهبندی شدهاند. پس، ما نیاز به ابزاری داریم تا به سادگی بین این دو دیدگاه، یعنی دیدگاه سختافزاری (مثل “بیت سوم”) و دیدگاه نرمافزاری (مثل “x040”)، ارتباط برقرار کنیم. عملگرهای شیفت، اگر به درستی استفاده شوند، میتوانند این کار را به نحو احسن انجام دهند.
مثال
تصور کنید یک برد با چندین چراغ LED داریم. مشخصات سختافزاری آن به شرح زیر است:
بیت | نام | شرح |
7 | MF | خرابی اصلی: هنگامی که هر چراغخطر دیگری روشن شود، روشن میشود. |
6 | DF | خرابی داده: دادههای ورودی ناسازگار یا خراب هستند. |
5 | OL | کمبود روغن: سطح روغن در مخزن کم است. |
4 | OP | فشار روغن: فشار روغن کم است. |
3 | PW | قطع برق: منبعتغذیه اصلی قطع شده است. |
2 | PF | خطای موقعیت: فریم موقعیت به سوئیچ محدودکننده برخورد کرده است و در جایی که قرار است نیست. |
1 | AP | فشار هوا: کمپرسور هوا خراب شده است. |
0 | CF | فیلتر تمیز: فیلتر کمپرسور هوا نیاز به تمیزکاری دارد. |
هر بیت از این رجیستر به یک چراغ خاص اختصاص یافته است. مدار هر چراغ مستقیماً به یکی از پینهای ورودی/خروجی عمومی (GPIO) میکروکنترلر متصل شده است. به عنوان مثال، با تنظیم مقدار بیت صفر (بیت اول) رجیستر GPIO بر روی یک (1)، چراغ “فیلتر تمیز” روشن خواهد شد.
1 | ledRegister = 1; // روشنکردن چراغ فیلتر تمیز) // خاموشکردن موارد دیگر) |
به خاطر سپردن هر بیت و عدد مربوطه، کمی گیجکننده است. به عنوان مثال، بیت صفر اولین مقدار، بیت یک مقدار دوم و… را نشان میدهد. پس بیت ششم به چه عددی مربوط میشود؟
یک راه خوب برای آسانتر کردن این کار وجود دارد. بیت 0 مقدار 1 را نگه میدارد که معادل عبارت (0>>1) است. بیت 1 مقدار 2 است که معادل (2>>1) میشود و بیت 3 هم (3>>1) است. از این رو، به راحتی میتوان دید که بیت 5 برابر با (5>>1) است. با استفاده از این سیستم، می توانیم ثابتهایی را برای نشان دادن هر بیت تعریف کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const uint8_t MASTER_FAIL = (1 << 7); const uint8_t DATA_FAIL = (1 << 6); const uint8_t OIL_LOW = (1 << 5); const uint8_t OIL_PRESSURE = (1 << 4); const uint8_t POWER_FAILURE = (1 << 3); const uint8_t POSITION_FAULT = (1 << 2); const uint8_t AIR_PRESSURE = (1 << 1); const uint8_t CLEAN_FILTER = (1 << 0); |
بیایید یکبار دیگر LED فیلتر تمیز را روشن کنیم، این بار با استفاده از ثابت جدید خود:
1 | ledRegister |= CLEAN_FILTER; // روشن کردن فیلتر تمیز |
در این مثال، از عملگر |= استفاده شده که باعث میشود بیت مربوط به فیلتر تمیز در رجیستر روشن شود. این عملگر قبلاً در بخشهای قبل توضیح داده شده است.
روشن کردن چندین بیت به صورت همزمان
فرض کنید میخواهیم همزمان چراغهای نشاندهنده قطع برق (POWER_FAILURE) و خرابی اصلی (MASTER_FAIL) را روشن کنیم. برای این کار میتوانیم از کامند زیر استفاده کنیم:
1 | ledRegister |= MASTER_FAIL | POWER_FAILURE; |
در این کامند، ثابتهای MASTER_FAIL و POWER_FAILURE به بیتهای مربوط به خرابی اصلی و قطع برق اشاره میکنند. با استفاده از عملگر |، هر دو بیت در رجیستر روشن خواهند شد. توجه کنید که هر مقداری غیرصفر در یک بیت باعث میشود آن بیت روشن شود.
خاموشکردن یک بیت
برای اینکه یک بیت خاص را خاموش کنیم، از الگوی زیر استفاده میکنیم:
1 | bitSet &= ~bitToTurnOff; |
برای درک نحوه عملکرد این عملیات، بیایید با استفاده از کامند زیر آن را به طور کامل بررسی کنیم:
1 | ledRegister &= ~(MASTER_FAIL | POWER_FAILURE); |
ابتدا نتیجه عبارت (MASTER_FAIL | POWER_FAILURE) را محاسبه میکنیم:
1 | 1000 1000 (MASTER_FAIL | POWER_FAILURE) |
حالا عملگر نقیض (NOT) یا (~) را روی این نتیجه اعمال میکنیم:
1 | 0111 0111 ~(MASTER_FAIL | POWER_FAILURE) |
در مرحله بعد، مقدار فعلی ledRegister را در نظر میگیریم. فرض میکنیم در این مثال، بیتهای نشانگر خرابی اصلی(MASTER_FAIL) و فیلتر تمیز (CLEAN_FILTER) روشن هستند.
1 | 1000 0001 (ledRegister: MASTER_FAIL, CLEAN_ FILTER) |
حالا نتایج را با عملگر AND ترکیب میکنیم:
1 2 3 4 5 6 7 | 0111 0111 ~(MASTER_FAIL|POWER_FAILURE) 1000 0001 (ledRegister: MASTER_FAIL, CLEAN_FILTER) -------- Result: 0000 0001 (CLEAN_FILTER) |
همانطور که میبینید، با خاموشکردن بیتهای نشانگر خرابی اصلی، تنها بیت مربوط به فیلتر تمیز روشن باقیمانده است.
بررسی مقادیر بیتها
برنامه زیر نمونهای از کاربرد معمول بیت بنگینگ (Bit-banging) را نشان میدهد که به معنی روشن و خاموشکردن بیتهای جداگانه است. این برنامه همچنین شامل منطقی برای بررسی مقادیر بیتهای مختلف است:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | /* * برنامهای برای نمایش استفاده از عملیات بیتی */ #include <stdio.h> #include <stdint.h> // تعریف ثابتها برای هر بیت وضعیت const uint8_t MASTER_FAIL = (1 << 7); // خرابی اصلی )روشن بودن نشان میدهد خطای دیگری وجود دارد) const uint8_t DATA_FAIL = (1 << 6); // دریافت نشدن داده const uint8_t OIL_LOW = (1 << 5); // پایین بودن سطح روغن const uint8_t OIL_PRESSURE = (1 << 4); // پایین بودن فشار روغن const uint8_t POWER_FAILURE = (1 << 3); // قطع برق اصلی const uint8_t POSITION_FAULT = (1 << 2); // خطای موقعیت )شیفت چپ ۲) const uint8_t AIR_PRESSURE = (1 << 1); // قطع شدن کمپرسور هوا )شیفت چپ ۱) const uint8_t CLEAN_FILTER = (1 << 0); // اتمام عمر فیلتر هوا )شیفت چپ ۰) /*! وضعیت بیتها را چاپ میکند (جایگزین برای نمایشگر LCD واقعی) * رجیستری که بیتهای LED را نگه میدارد param ledRegister\ */ static void printLED(const uint8_t ledRegister) { printf("LEDها: "); if ((MASTER_FAIL & ledRegister) != 0) printf("خرابی اصلی "); if ((DATA_FAIL & ledRegister) != 0) printf("عدم دریافت داده "); if ((OIL_LOW & ledRegister) != 0) printf("پایین بودن سطح روغن "); if ((OIL_PRESSURE & ledRegister) != 0) printf("پایین بودن فشار روغن "); if ((POWER_FAILURE & ledRegister) != 0) printf("قطع برق اصلی "); if ((POSITION_FAULT & ledRegister) != 0) printf("خطای موقعیت "); if ((AIR_PRESSURE & ledRegister) != 0) printf("قطع شدن کمپرسور هوا "); if ((CLEAN_FILTER & ledRegister) != 0) printf("اتمام عمر فیلتر هوا "); printf("\n"); int main() { uint8_t ledRegister = 0x00; // ها LED شروع با خاموش بودن همه printLED(ledRegister); // برق قطع شد ledRegister |= POWER_FAILURE | MASTER_FAIL; printLED(ledRegister); // حالا فشار هوا هم قطع شد ledRegister |= AIR_PRESSURE; printLED(ledRegister); // اصلی همچنان روشن است LED برق برگشت، اما فشار هوا همچنان قطع است، بنابراین ledRegister &= ~POWER_FAILURE; printLED(ledRegister); return (0); { |
ابتدا به سراغ تابع printLED میرویم. این تابع هر بیت را بهصورت جداگانه بررسی کرده و در صورت روشن بودن آن، پیامی را چاپ میکند. (در فصل ۵ با دستور شرطی if که برای این کار استفاده میشود آشنا خواهید شد.)
برای درک منطق آن، به دستور زیر توجه کنید:
1 2 3 | if ((MASTER_FAIL & ledRegister) != 0) printf("MASTER_FAIL "); |
این پیام در صورتی چاپ میشود که عبارت خط اول برابر با صفر نباشد. از آنجا که این عبارت از عملگر بیتی (&) AND استفاده میکند، هر بیت در ledRegister باید با بیت متناظر در MASTER_FAIL مطابقت داشته باشد تا مقدار ۱ بگیرد. اگر حداقل یک مجموعه از بیتها هر دو مقدار ۱ باشند، دستور printf اجرا میشود. به عبارت دیگر، پشتصحنه عملیات به این شکل است:
1 2 3 4 5 6 7 8 9 10 11 | 1000 0000 (MASTER_FAIL) & 1000 0001 (ledRegister با مجموعه MASTER_FAIL و CLEAN_FILTER) ---------- = 1000 0000 (ازآنجاییکه این مقدار صفر نیست، "MASTER FAIL" چاپ میشود) |
کل این تابع یک آزمایش یکسان را روی تمام بیتهای رجیستر انجام میدهد و هر بیتی که روشن باشد (یا مقدار ۱ داشته باشد) نمایش میدهد. ما پنل چراغ سختافزاری نداریم بنابراین از این تابع استفاده میکنیم به عبارت دیگر، این تابع به ما امکان میدهد ببینیم کدام بیتها در رجیستر فعال هستند.
در بخشهای بعدی برنامه، ما به طور عمدی تغییراتی در بیتها ایجاد میکنیم. به عنوان مثال، با فعال کردن بیتهای POWER_FAILURE و MASTER_FAIL، شرایطی شبیه به یک قطع برق را شبیهسازی میکنیم. در نتیجه، زمانی که وضعیت چراغهای LED را چاپ میکنیم، پیام زیر را دریافت میکنیم:
1 | Leds: MASTER_FAIL POWER_FAILURE |
بقیه برنامه، بیتهای مختلف را برای تولید پیامهای زیر تنظیم و خالی میکند:
1 2 3 4 5 6 | Leds: Leds: MASTER_FAIL POWER_FAILURE Leds: MASTER_FAIL POWER_FAILURE AIR_PRESSURE Leds: MASTER_FAIL AIR_PRESSURE |
جمع بندی
در این چند قسمت اخیر، به بررسی عملیات پایه روی اعداد صحیح پرداختیم. علاوه بر عملیات حسابی معمول مانند جمع، تفریق، ضرب و تقسیم، به روش ذخیرهسازی اعداد در حافظهی کامپیوتر و چگونگی برخورد با خطاهایی مانند سرریز (overflow) نیز پرداخته شد.
بخش مهم دیگری که مورد بحث قرار گرفت، دستکاری بیتها بود. در این بخش، اعداد صحیح را به عنوان مجموعهای از بیتها (صفر و یک) در نظر گرفتیم و نحوهی کار با آنها را آموختیم. این مفهوم در برنامهنویسی سیستمهای امبدد بسیار کاربردی است، زیرا بسیاری از سختافزارها از رجیسترهایی استفاده میکنند که با بیتها سروکار دارند. به عنوان مثال، رجیستر GPIO که برای روشن و خاموش کردن LED استفاده کردیم، بیتهایی برای 31 تا GPIO دیگر را در خود جای داده است. 31 پین دیگر کاملاً بیارتباط با LED ما هستند و عملکردهای دیگری دارند.
در قسمت های آینده، به سراغ تصمیمگیریهای شرطی خواهیم رفت و یاد میگیریم که چگونه بر اساس نتایج محاسبات، برنامه را در مسیرهای مختلف هدایت کنیم.