در قسمت قبل آموزش برنامه نویسی C به بررسی سیستم اعداد در برنامهنویسی C امبدد پرداختیم. در این قسمت به نمایش مکمل دو در اعداد صحیح با علامت (Two’s Complement) می پردازیم.
در نمایش اعداد علامتدار، سمت چپترین بیت جهت نمایش علامت استفاده میشود: اگر این بیت ۱ باشد، عدد منفی است در غیر این صورت عدد مثبت است. بنابراین، اعداد صحیح علامتدار ۸ بیتی (int8_t) تنها از ۷ بیت سمت راست برای نمایش مقدار عدد استفاده میکنند درنتیجه میتوانند اعداد را از ۱۲۷ تا ۱۲۸- نمایش دهند. تقریبا تمام کامپیوترهای امروزی از نمایش مکمل دو (Two’s Complement) برای نمایش مقادیر منفی استفاده میکنند. نمایش مکمل دو یک عدد را به صورت تفریق آن عدد از صفر ذخیره میکند.
برای مثال، عدد ۱- را میتوان با محاسبهی زیر به دست آورد:
1 2 3 4 | 0000 0000 - 0000 0001 --------- 1111 1111 |
برای انجام این عملیات کامپیوتر بیت “قرضی” را در سمت چپ عدد اضافه میکند و محاسبات به صورت زیر انجام میشود:
1 2 3 4 | 1 0000 0000 - 0000 0001 --------- 1111 1111 |
مبنای دو شبیه به کیلومترشمار مکانیکی ماشین است. فرض کنید یک ماشین کاملاً نو با کیلومترشمار ۰۰۰,۰۰۰ میخرید. اگر به عقب رانندگی کنید، کیلومترشمار شما ۹۹۹,۹۹۹ را نشان میدهد، که معادل مکملدهِ عدد ۱- است.
شاید متوجه شدهباشید که بزرگترین عددی که یک uint8_t میتواند نگه دارد ۲۵۵ است، در حالی که یک int8_t تنها میتواند مقادیر تا ۱۲۷ (نصف آن) را ذخیره کند. دلیل این است که یک بیت به عنوان بیت علامت استفاده میشود و فقط هفت بیت برای ذخیره خود عدد باقی میماند.
وقتی با اعداد علامتدار ۸ بیتی از محدوده فراتر میرویم، چه اتفاقی میافتد؟بهتر است این قسمت را خودتان بررسی کنید و ببینید با عملیاتهای ۱۲۷ + ۱ و ۱ – ۱۲۸- چه اتفاقی میافتد. همچنین ببینید با (۱۲۸-)-، نقیض (منفی) عدد ۱۲۸- چه اتفاقی میافتد.
عملگرهای مختصر شده (Shorthand Operators)
شما انواع اعدادصحیح و عملیات ساده قابل انجام روی آنها را یاد گرفتید، اما برای اینکه محاسبات را سریعتر انجام دهید، C تعدادی عملگر اختصاری ارائه میدهد.
به عنوان مثال، فرض کنید میخواهید مقداری به یک عدد اضافه کنید، مانند این:
1 | aNumber = aNumber + 5; |
میتوانید این عملیات را به صورت زیر کوتاه کنید:
1 | aNumber += 5; |
شما میتوانید برای تمام عملگرهای حسابی دیگر نیز چنین میانبرهایی ایجاد کنید. علاوه بر این، میتوانید عمل اضافه کردن ۱ به یک عدد را خلاصه کنید:
1 | aNumber += 1; |
این را میتوان به صورت زیر کوتاه کرد:
1 | ++aNumber; |
برای کم کردن ۱ واحد از مقدار یک متغیر برنامه، از عملگر — (منهای) استفاده کنید.
یک نکتهی احتیاطی وجود دارد. C به شما اجازه میدهد عملیات افزایش (++) و کاهش (–) را با عبارات دیگر ترکیب کنید:
result = ++aNumber; // این کار را انجام ندهید.
لطفاً این کار را انجام ندهید، زیرا میتواند باعث شود برنامه رفتار تعریفنشده داشته باشد. برای مثال، عبارات زیر را در نظر بگیرید:
1 2 | aNumber = 2; result = ++aNumber * ++aNumber + ++aNumber; |
عبارت دوم به C میگوید که aNumber را افزایش دهد، سپس دوباره aNumber را افزایش دهد. سپس aNumber را در خودش ضرب میکند و برای بار سوم aNumber را افزایش میدهد. در نهایت، این مقدار را به result اضافه میکند.
متأسفانه این عملیات به ترتیبی که در اینجا ذکر کردهام رخ نمیدهد. برای مثال، همه افزایشها میتوانند در ابتدا اتفاق بیفتند، و نتیجه (5 × 5 + 5) = 30 شود. یا میتوانند تک به تک بیایند، و ما (3 × 4 + 5) = 17 را داشته باشیم. به همین دلایل، مطمئن شوید که ++ و — را در خطوط جداگانه قرار دهید.
یک نکته دیگر: دو شکل از عملیات افزایش و کاهش وجود دارد. شما میتوانید عملگر را قبل یا بعد از متغیری که میخواهید افزایش دهید قرار دهید:
1 2 3 4 | aNumber = 5; result = ++aNumber; // پیشوند ++ aNumber = 5; result = aNumber++; // پسوند ++ |
اینها کارهای کمی متفاوت انجام میدهند. من این را به خواننده واگذار میکنم که یک برنامه کوچک برای چاپ نتایج کد قبلی بنویسد و بفهمد چه تفاوتی با هم دارند – و سپس دیگر هرگز از ++ همراه با یک عبارت دیگر استفاده نکنید.
توجه:
در C، دو عبارت زیر معادل هستند:
1 2 | ++aNumber; aNumber++; |
برای عملیات روی اعداد ساده، بین این دو عملیات تفاوتی وجود ندارد. خبر خوب، C فقط به شما اجازه میدهد ++ را روی اعداد ساده انجام دهید. اما دقت کنید اگر ازین عملگرها درون عبارت دیگر استفاده کنید نتیجه کاملا متفاوت خواهد بود بنابرین همچنان به توصیه خود بر عدم استفاده از این عملگر درون عبارات دیگر تاکید میکنیم.
با این حال، C++ به شما امکان میدهد انواع داده خود را تعریف کنید و از طریق اضافه بارگذاری عملگر (Operator Overloading)، عملیات ++ و — خود را تعریف کنید.که در این اموزش ما به سراغ ++C نمیرویم.
کنترل نگاشت حافظه در رجیسترهای I/O با استفاده از عملگرهای بیتی
در دنیای دیجیتال ما میتوانیم نگاهی متفاوت به یک عدد هشت بیتی داشته باشیم میتوانیم هشت بیت را تنها به صورت یک عدد واحد ببینیم (به نحوی که تا به امروز به آن نگاه میکردیم) یا اینکه میتوانیم هر کدام از بیتهای آن را برای نمایش یک چیز متفاوتی در نظر بگیریم . یک نمونه ساده میتوان هر کدام را برای نمایش وضعیت یک led در نظر بگیریم. در حقیقت، هنگامی که مقادیر مورد نظرمان را در مکانهای حافظهی ویژهای که نگاشت حافظه در رجیسترهای ورودی/خروجی (memory-mapped I/O registers) نامیده میشود، قرار میدهیم، این مقادیر پینهای I/O را روشن یا خاموش میکنند. از آنجایی که هر بایت رجیستر دارای هشت بیت است، یک رجیستر به تنهایی میتواند هشت LED را کنترل کند. (یا در مورد بوردی که ما استفاده می،کنیم، یک LED و هفت پین که میتوانیم LEDهای بیشتری به آنها اضافه کنیم.)
بیتها معمولاً از ۷ تا ۰ شمارهگذاری میشوند، به طوری که ۷ چپترین بیت یا بیت با اهمیتتر است. فرض کنید رجیستر LED ما به صورت زیر تنظیم شده است:
Bit0 | Bit1 | Bit2 | Bit3 | Bit4 | Bit5 | Bit6 | Bit7 |
LED0 | Out1 | Out2 | Out3 | Out4 | Out5 | Out6 | Out7 |
فرض کنید میخواهیم LED شماره ۰ را روشن کنیم. از آنجایی که هر LED خاموش است، رجیستر ما مقدار ۰۰۰۰ ۰۰۰۰ در خود دارد. برای روشن کردن LED شماره ۰، باید بیت آخر را به مقدار ۱ تغییر دهیم. برای انجام این کار، فقط ۱ را به رجیستر اضافه میکنیم تا ۰۰۰۱ ۰۰۰۰بدست آوریم. LED شماره ۰ روشن میشود و تمام LEDهای دیگر خاموش میمانند.
اما اگر LED از قبل روشن بود چه میشد؟ سپس رجیستر ما حاوی ۰۰۰۱ ۰۰۰۰خواهد بود، و هنگامی که ۱ اضافه میکنیم، ۲ بدست میآوریم که در باینری برابر با ۰۰۱۰ ۰۰۰۰است. بنابراین، LED شماره ۰ خاموش میشود و OUT شماره ۱ روشن میشود. این چیزی نیست که ما میخواستیم.
مشکل اینجا این است که عملگرهای حسابی که تا به حال استفاده کردهایم، عدد صحیح ۸ بیتی ما را به عنوان یک عدد صحیح واحد در نظر میگیرند.در اینجا باید از عملیات بیتی استفاده کنیم. عملگرهای بیتی عدد را به عنوان مجموعهای از بیتهای مجزا در نظر میگیرند که هر کدام را میتوان به طور مستقل روشن، خاموش و آزمایش کرد.
OR
اولین عملگر بیتی ، ( | ) OR است. نسخه تک بیتی OR در صورتی که هر یک از دو عملوند آن روی 1 تنظیم شده باشد، نتیجه درست (یا 1) را برمیگرداند. نحوه عملکرد آن را با استفاده از یک جدول درستی نشان خواهم داد. این جدول شبیه جدولهای جمع و ضرب است که در کلاس اول استفاده میکردید، با این تفاوت که عملگرهای منطقی مانند OR را نشان میدهد.
جدول درستی برای OR به این صورت است:
1 | 0 | OR(|) |
1 | 0 | 0 |
1 | 1 | 1 |
OR یک عملگر بیتی است، به این معنی که برای “OR” دو مقدار 8 بیتی با هم برسی میشوند، این عملیات را برای هر جفت بیت در دو مقدار انجام می دهید. برای نمونه:
1 2 3 4 | 0010 0101 | 0000 1001 --------- 0010 1101 |
برای روشن کردن بیت 0 (یعنی روشن کردن LED شماره 0)، از کد C زیر استفاده می کنیم:
1 | ledRegister = ledRegister | 0x01; |
به طور مشابه، می توانیم از عملگر اختصاری زیر استفاده کنیم:
1 | ledRegister |= 0x01; |
AND
عملگر (&) AND تنها زمانی یک نتیجه درست (1) برمیگرداند که هر دو عملوند آن true باشند. جدول درستی AND به شکل زیر است:
1 | 0 | AND (&) |
0 | 0 | 0 |
1 | 0 | 1 |
1 2 3 4 | 0010 0101 & 0000 1001 --------- 0000 0001 |
برای خاموش کردن LED شماره 0، میتوانیم بیت 0 را با دستور زیر روی مقدار 0 تنظیم کنیم:
1 | ledRegister &= 0b11111110; |
این دستور، محتوای رجیستر را با یک الگوی بیتی AND میکند که در این الگوی بیتی،تمام بیتها را یک قرار میدهیم به جز بیت یا بیت هایی که میخواهیم صفر شوند در اینجا تنها بیت شماره صفر را0 قرار میدهیم . در نتیجه، بیت صفر، 0 میشود و سایر بیتها بدون تغییر باقی میمانند. (AND کردن یک بیت با 1، مقدار آن را حفظ میکند.)
NOT
عملگر NOT یا معکوس (~) یک عملوند را دریافت کرده و آن را معکوس میکند. بنابراین، اگر بیت 0 باشد، به 1 تبدیل میشود و اگر 1 باشد، به 0 تبدیل میشود. جدول درستی برای عملگر NOT بسیار ساده است:
1 | 0 | |
0 | 1 | (~)NOT |
1 2 3 | ~ 0000 0001 --------- 1111 1110 |
با استفاده از عملگرهای بیتی که تا به حال با آنها آشنا شدهایم، میتوانیم کدهایی برای خاموش کردن همه رجیسترها و سپس روشن و خاموش کردن LED بنویسیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 | const uint8_t LED_BIT = 0b0000001; // همه چیز را خاموش کن ledRegister = 0; // LED را روشن کن ledRegister |= LED_BIT; // کمی صبر کن sleep(5); // LED را خاموش کن ledRegister &= ~LED_BIT; |
این دقیقا همان کاری است که برنامه چشمکزن در فصل قبل انجام میداد، با این تفاوت که کتابخانه STM این جزئیات را از ما پنهان میکرد.
XOR ( OR انحصاری)
نتیجهی عملگر بیتی (^)XOR زمانی که فقط یکی از بیتها 1 باشد (و دیگری 0) و یا برعکس، درست (1) است. جدول درستی آن به صورت زیر است:
1 | 0 | Exclusive OR (^) |
1 | 0 | 0 |
0 | 1 | 1 |
1 2 3 4 | 0010 0101 ^ 0000 1001 --------- 0010 1100 |
XOR زمانی مفید است که بخواهیم مقدار LED موجود در ledRegister را معکوس کنیم، به این صورت:
1 | ledRegister ^= LED_BIT; // Toggle the LED bit. |
معکوس کردن یک LED باعث میشود به آرامی چشمک بزند.
شیفت دادن
عملگر شیفت به چپ (>>) محتویات یک متغیر را به اندازهی تعداد بیتهای مشخصی به سمت چپ شیفت میدهد و بیتهای خالی را با 0 پر میکند. برای مثال، عملیات زیر را در نظر بگیرید:
1 | uint8_t result = 0xA5 << 2; |
این باعث میشود که کامپیوتر بیتها را دو گام به سمت چپ حرکت دهد، به طوری که مقدار زیر:
1 | 1010 0101 |
به این تبدیل میشود:
1 | 1001 0100 |
عملگر شیفت به راست (<<) کمی پیچیدهتر است. برای اعداد بدون علامت، دقیقا مانند شیفت به چپ عمل میکند، با این تفاوت که بیتها را در جهت راست شیفت میدهد. باز هم، کامپیوتر بیتهای خالی را با 0 پر میکند. بنابراین، uint8_t result = 0xA5 >> 2; به این صورت محاسبه میشود:
1 | 1010 0101 |
به این تبدیل میشود:
1 | 0010 1001 |
اما زمانی که عدد علامتدار باشد، کامپیوتر از بیت علامت برای پر کردن بیتهای خالی استفاده میکند. برای مثال، عملیات زیر را در نظر بگیرید:
1 2 | int8_t result = 0xA5 >> 2; توجه داشته باشید که "u" غایب است// |
حال به محاسبه زیر توجه کنید:
1 | 1010 0101 |
به این تبدیل میشود:
1 | 1110 1001 |
از آنجا که عدد علامتدار است و به راست شیفت مییابد، بیتهای خالی در سمت راست با نسخههای بیت علامت پر میشوند، بنابراین نتیجه xE90 است، که معادل 23- است.