حساب البتّات (Bits) في أردوينو

2014-04-07

مقدمة

في كثير من الأحيان عند البرمجة في بيئة أردوينو، سيكون من المفيد جداّ (أو ربما من الضروري حتى) القدرة على التعامل مع البتات. وهنا بعض الحالات التي يكون فيها حساب البتّات الفردية مفيداً:

  • توفير الذاكرة عن طريق حفظ حتى 8 قيم true/false في بايت واحد.
  • تشغيل/إيقاف بت فردية في سجل (registers) تحكم معين أو في سجل أحد منافذ المعدات.
  • أداء بعض العمليات الحسابية التي تنطوي على ضرب أو قسمة أسس العدد 2.

في هذه المقالة، سنقوم باستكشاف الدوال الأساسية الخاصة بالبتات المتوفرة في لغة C++. ثم سنتعلم كيفيّة الدمج بينها لإجراء عمليات شائعة معينة ومفيدة في الجزء الثاني بإذن الله.


النظام الثنائي Binary

لتقديم شرح أفضل للعمليات الخاصة بالبتات، سنقوم في هذه المقالة بتمثيل معظم القيم العددية باستخدام النظام الثنائي، المعروف أيضا باسم القاعدة اثنين. في هذا النظام، جميع قيم الأعداد الصحيحة تُمثّل فقط باستخدام القيم 0 و 1 لكل خانة. هذا هو النظام المستخدم لتخزين البيانات في جميع أجهزة الحاسوب الحديثة. يسمى كل 0 أو 1 بت (bit)، وهو اختصار لـ Binary Digit والتي تعني رقم ثنائي.

في النظام العشري المألوف لنا (والمسمى أيضا القاعدة عشر)، عدد مثل 572 يعني 2 × 100 + 7 × 101 + 5 × 102. وبالمثل، في النظام الثنائي العدد 11010 يعني 0 × 20 + 1 × 21 + 0 × 22 + 1 × 23 + 1 × 24 = 26.

فمن الأهمية بمكان أن نفهم كيف يعمل النظام الثنائي من أجل متابعة وفهم هذه المقالة. إذا كنت بحاجة إلى مساعدة في هذا المجال، راجع مقالة ويكيبيديا عن النظام الثنائي.

للأسف، فإن معظم مترجمات لغة C++ لا تعترف بتمثيل الأعداد الثنائية مباشرة في الكود المصدري. هناك عدد قليل منها تسمح باستخدام 0B ابتدائاً وتليها سلسلة من الأرقام 0 و 1 لتمثيل الأعداد الثنائية، على سبيل المثال 0B11 تساوي 3. مترجم C++ المستخدم في أردوينو لا يدعم هذا النحو. وعلى كل حال، منذ إصدار أردوينو 0007، تم تعريف الثابت B0 خلال B11111111 من أجل سعادتك.


دالّة AND "و"

دالة AND "و" الخاصة بالبت في لغة C++ تُمثَّل بالرمز &، وتستخدم بين تعبيرين أو عددين صحيحين. دالة AND الخاصة بالبت تعمل على كل بت في كلا العددين بشكل مستقل وفقاً لهذه القاعدة: إذا كانت البت في كلا الطرفين 1 فالناتج هو 1، وإلا فإن الناتج هو 0. طريقة أخرى للتعبير عن ذلك:

0 & 0 == 0 0 & 1 == 0 1 & 0 == 0 1 & 1 == 1

في أردوينو، النوع int هو قيمة مكونة من 16 بت، فباستخدام & بين تعبيرين يؤدي إلى حدوث 16 عملية AND في وقت واحد. مثلا في الكود التالي: int a = 92; // في النظام الثنائي: 0000000001011100 int b = 101; // في النظام الثنائي: 0000000001100101 int c = a & b; // الناتج: 0000000001000100

تتم معالجة كل بت من الـ 16 بت في المتغيرين a و b باستخدام الدالة AND على حدا، ويتم تخزين الـ 16 بت الناتجة في المتغير c، وهي 0000000001000100 والتي تساوي 68 في النظام العشري.

أحد الاستخدامات الأكثر شيوعا للدالة AND هو اختيار بت (أو بتات) محددة من قيمة عددية، وهذه العملية غالبا ما تسمى إخفاء "Masking". مثلاً، لو أردت الوصول إلى البت الأدنى (الأولى من اليمين) في المتغير x، وتخزين هذه البت في المتغير y، يمكنك استخدام الكود التالي:

int x = 5; // في النظام الثنائي: 101 int y = x & 1; // y == 1 x = 4; // في النظام الثنائي: 100 y = x & 1; // y == 0

دالّة OR "أو"

دالة OR "أو" الخاصة بالبت في لغة C++ تُمثَّل برمز الشريط العمودي |، مثل الدالة &، تعمل | على كل بت لكلا التعبيرين المحيطين بها بشكل مستقل، ولكن ما تفعله هو شيء مختلف (طبعا). ناتج OR الخاصة بالبت يكون 1 إذا كان أحد البتين أو كليهما 1، وإلا فالناتج 0، وبعبارة أخرى:

0 | 0 == 0 0 | 1 == 1 1 | 0 == 1 1 | 1 == 1

هذا مثال على استخدام دالة OR الخاصة بالبت في كود C++:

int a = 92; // في النظام الثنائي: 0000000001011100 int b = 101; // في النظام الثنائي: 0000000001100101 int c = a | b; // الناتج: 0000000001111101، أو 125 في النظام العشري.

وكثيرا ما تستخدم دالة OR للتأكد من أن أحد البتات قيد التشغيل (قيمتها 1) في تعبير معين. على سبيل المثال، لنسخ البتات من المتغير a إلى ألمتغير b، مع التأكد من تعيين قيمة البت الأدنى (الأولى من اليمين) 1، استخدم الكود التالي:

b = a | 1;

دالّة XOR "أو الحصريّة"

هناك دالة غير عادية إلى حد ما في لغة C++ تسمى دالة Exclusive OR "أو الحصرية" الخاصة بالبت، المعروفة أيضا باسم دالة XOR (تُلفظ "إكس أور"). دالة XOR الخاصة بالبت تُمَثَّل بالرمز ^. هذه الدالّة تشبه إلى حد كبير دالّة OR "أو" الخاصة بالبت، الفرق الوحيد هو أنها تُنتِج 0 في حال كان كلا المُدخَلين 1:

0 ^ 0 == 0 0 ^ 1 == 1 1 ^ 0 == 1 1 ^ 1 == 0

أو يمكن النظر إلى دالة XOR الخاصة بالبت أنها تُنتِج 1 إذ اختلف كلا المدخلين، و 0 إذا تشابها.

هذا مثال على معامل XOR:

int x = 12; // في النظام الثنائي: 1100 int y = 10; // في النظام الثنائي: 1010 int z = x ^ y; // في النظام الثنائي: 0110، أو 6 في النظام العشري

كثيرا ما تُستخدم دالّة ^ لتبديل بعض البتات (أي تغيير 0 إلى 1 أو 1 إلى 0) في تعبير صحيح مع ترك الأخريات كما هي. على سبيل المثال:

y = x ^ 1; // تبديل البت الأدنى (الأولى من اليمين) في المتغير x وتخزين الناتج في المتغير y

دالّة NOT "عكس"

دالّة NOT "عكس" الخاصة بالبت في لغة C++ تُمَثَّل بالرمز تليدا ~ (في بعض اللغات الأخرى تُمَثَّل بعلامة التعجب !). على عكس & و | و ^، يتم تطبيق دالّة NOT على تعبير واحد (على يمينها)، دالّة NOT تغير كل بت لتصبح النقيض أو العكس؛ 0 يصبح 1، و 1 يصبح 0، مثلاً:

int a = 103; // في النظام الثنائي: 0000000001100111 int b = ~a; // في النظام الثنائي: 1111111110011000 = -104

قد يٌفاجئك رؤية رقم سالب مثل -104 كنتيجة لهذه الدالة.

يٌعزى السبب إلى أن أعلى بت (الأولى من اليسار) في متغيرات int تسمى "بت الإشارة" (Sign Bit)، إذا كانت قيمتها 1 فالرقم يكون سالباً، وإذا كانت 0 يكون الرقم موجباً. يُسمى هذا الترميز بـ "مكمّل الاثنين" (Two's Complement)، للمزيد من المعلومات، راجع مقالة ويكيبيديا عن مكمّل الاثنين.

كإضافة، فإنه من المثير للاهتمام أن تلاحظ أن دالة NOT لأي عدد صحيح تساوي سالب العدد ناقص 1:

~x == -x-1

في بعض الأحيان، يمكن لِبت الإشارة (Sign Bit) في الأعداد الصحيحة أن تُسبّب بعض المفاجآت غير المرغوب فيها كما سنرى لاحقاً.


دالّتا الإزاحة الخاصّتان بالبت

هناك دالتان للإزاحة في لغة C++: دالّة الإزاحة إلى اليسار >> ودالّة الإزاحة إلى اليمين <<. تقوم هاتان الدالّتان بإزاحة البتات في المعامل الذي على يسارها إلى اليمين أو اليسار بعدد الرقم الذي على يمينها من المرات. مثلاً:

int a = 5; // في النظام الثنائي: 0000000000000101 int b = a << 3; // في النظام الثنائي: 0000000000101000، أو 40 في النظام العشري int c = b >> 3; // في النظام الثنائي: 0000000000000101، أي تعود 5 كما كانت

عند إزاحة المتغير x إلى اليسار y من البتات (x << y)، فإن y من البتات من جهة اليسار ستضيع، أو بالأحرى تم إزاحتها خارج النطاق:

int a = 5; // في النظام الثنائي: 0000000000000101 int b = a << 14; // في النظام الثنائي: 0100000000000000 - أول 1 في 101 تم نبذُه

إذا كنت متأكداً من أن أياً من البتات في القيمة لن تُزاح إلى غياهب النسيان، فإن أبسط طريقة للنظر إلى دالّة الإزاحة على أنها تضاعف المعامل الذي على يسارها بنسبة 2 مرفوعاً إلى قوّة العدد الذي على يمينها من المرات. على سبيل المثال، لتوليد قوى العدد 2، يمكنك استخدام التعبير التالي:

1 << 0 == 1 1 << 1 == 2 1 << 2 == 4 1 << 3 == 8 ... 1 << 8 == 256 1 << 9 == 512 1 << 10 == 1024

عندما تقوم بإزاحة x إلى اليمين y من البتات (x >> y)، وكانت قيمة أعلى بت (الأولى من اليسار) 1، فإن سلوك الإزاحة يعتمد على نوع المتغير x. إذا كان x من نوع int، فإن قيمة أعلى بت (كما ذكرنا سابقاً) تمثل إشارة العدد (موجب أو سالب)، في هذه الحالة، يتم نسخ بت الإشارة في أدنى بت (الأولى من اليمين) لأسباب احتياطية:

int x = -16; // في النظام الثنائي: 1111111111110000 int y = x >> 3; // في النظام الثنائي: 1111111111111110

يُسمَّى هذا السلوك بـ "إلحاق الإشارة" (Sign Extension)، وهو السلوك الذي قد لا ترغبه. بدلاً من ذلك، قد ترغب بتعبئة أصفار من جهة اليسار. اتضح أن قاعدة "إلحاق الإشارة" لا يتم تطبيقها على الأعداد بدون إشارة (unsigned int)، لذا يمكنك استخدام التلبيس (typecast) لمنع نسخ بت الإشارة في اليسار:

int x = -16; // في النظام الثنائي: 1111111111110000 int y = unsigned(x) >> 3; // في النظام الثنائي: 0001111111111110

مع أخذ الحيطة والحذر من إلحاق الإشارة، يمكنك استخدام دالة الإزاحة إلى اليمين << كوسيلة للتقسيم على مضاعفات العدد 2. على سبيل المثال:

int x = 1000; int y = x >> 3; // قسمة العدد الصحيح 1000 على 8 تٌنتج 125

دوال الإحالة

في كثير من الأحيان في البرمجة، تقوم بمعالجة قيمة متغير ما وتُخزّن القيمة بعد المعالجة في نفس المتغير. في معظم لغات البرمجة، على سبيل المثال، يمكنك زيادة 7 إلى قيمة المتغير x باستخدام الكود التالي:

x = x + 7; // زِد 7 إلى قيمة المتغير

ولأن هذا النوع من المعالجة يحدث كثيراً في البرمجة، وفّرت لغة C++ (وغيرها الكثير من اللغات) دالّة اختصار على شكل دالّة إحالة متخصّصة. يمكن كتابة الكود السابق بإيجاز على النحو التالي:

x += 7; // زِد 7 إلى قيمة المتغير

تَبيَّن أن الدوالّ AND و OR و XOR ودالّتي الإزاحة إلى اليمين وإلى اليسار كلها يمكن تطبيقها باستخدام دالّة الإحالة المختصرة، هذه بعض الأمثلة:

int x = 1; // في النظام الثنائي: 0000000000000001 x <<= 3; // في النظام الثنائي: 0000000000001000 x |= 3; // في النظام الثنائي: 0000000000001011 - لأن 3 في النظام الثنائي تساوي 11 x &= 1; // في النظام الثنائي: 0000000000000001 x ^= 4; // في النظام الثنائي: 0000000000000101 - التبديل مع 100 x ^= 4; // في النظام الثنائي: 0000000000000001 - التبديل مع 100 مرة أخرى

بينما لا يوجد دالّة إحالة مختصرة لدالّة NOT ~؛ إذا كنت ترغب في تبديل كل البتات في المتغير x، عليك استخدام الكود التالي:

x = ~x; // تبديل كل البتات في المتغير وتخزين الناتج في نفس المتغير

دَحْض الشُّبهات: الدّوال الخاصّة بالبتات ضد الدوالّ المنطقية

أحياناً يتم الخلط بين الدّوال الخاصّة بالبتات في لغة C++ وبين الدوالّ المنطقية. على سبيل المثال، دالّة AND الخاصة بالبت & ليست نفس الدالّة المنطقية AND && لسببين:

  1. لا يتم حساب الأرقام بنفس الطريقة. دالة & تعمل على كل بت بشكل مستقل في معاملاتها، في حين أن دالة && تحول كل من المعاملات الخاصة بها إلى قيمة منطقية (1 == true أو false == 0)، ثم تُنتِج قيمة واحدة فقط؛ إمّا صحيحة "True" أو خاطئة "false". على سبيل المثال، 4 & 2 == 0، لأن 4 تساوي 100 في النظام الثنائي و 2 تساوي 010 في النظام الثنائي، وليس أياً من البتات هي 1 في كلا الطرفين. ومع ذلك، فإن 4 && 2 == true "صحيح"، وtrue عددياً تساوي 1. وذلك لأن 4 ليست 0، و 2 ليست 0 كذلك، لذلك؛ كلاهما يُعد قيماً منطقية صحيحة "true".
  2. الدوالّ الخاصة بالبت دائماً تقيّم كلا الطرفين في معاملاتها، في حين أن الدوالّ المنطقية تستخدم ما يسمى بـ "التقييم المختصر" (short-cut evaluation). يكون هذا بالأهمية بمكان عندما يكون هناك آثار جانبية، مثل التأثير بالمخرجات أو تعديل قيمة أخرى في الذاكرة. هنا مثال لسطرين من الكود قد يبدوان متماثلين، لكن لكلٍّ منهما سلوك مختلف:

int fred (int x) { Serial.print ("fred "); Serial.println (x, DEC); return x; } void setup() { Serial.begin (9600); } void loop() { delay(1000); // تأخّر لمدة ثانية واحدة int x = fred(0) & fred(1); }

إذا قمت بتنفيذ ورفع هذا البرنامج وراقبت مخرجات التسلسلية (serial) في واجهة أردوينو الرسومية، سوف ترى تكرار السطرين التاليين كل ثانية:

fred 0 fred 1

وهذا لأن كلّاً من fred(0) و fred(1) يتم استدعاؤه، مما أدّى إلى توليد المخرجات، القيم 0 و 1 المُنتجة هي دوال خاصّة بالبت تم جمعها بدالة AND الخاصة بالبت، وخُزِّنت قيمة 0 في x. إذا قمت بتعديل السطر:

int x = fred(0) & fred(1);

وبدّلت دالّة & الخاصة بالبت بدالّة && المنطقية،

int x = fred(0) && fred(1);

ورفعت البرنامج ونفذته مرة أخرى، ستتفاجأ بأن هناك سطر واحد فقط في المخرجات يتكرر كل ثانية في نافذة التسلسلية:

fred 0

لماذا حدث هذا؟ يعود السبب لأن دالة && المنطقية تستخدم الإختصار: إذا كان المعامل الأيسر 0 (أو خطأ)، فبالتأكيد سيكون الناتج خطأ على كل حال، لذلك ليس هناك داعٍ لتقييم المعامل الأيمن. في عبارة أخرى، سطر الكود int x = fred(0) && fred(1); هو نفسه في المعنى:

int x; if (fred(0) == 0) { x = false; // stores 0 in x } else { if (fred(1) == 0) { x = false; // stores 0 in x } else { x = true; // stores 1 in x } }

يتضّح إذن أن دالّة && المنطقية تستخدم طرقاً مختصرة مما يؤثر في المخرجات.

كما هو الحال مع دالة AND الخاصة بالبت ودالة AND المنطقية، هناك اختلافات بين دالة OR الخاصة بالبت ودالة OR المنطقية. دالة OR الخاصة بالبت دائماً تقيّم كلا المعاملين، بينما دالة OR المنطقية تقيّم المعامل الأيمن فقط في حال كان المعامل الأيسر قيمته خطأ (صفر). وأيضاً دالّة OR الخاصّة بالبت تعالج كل بت في كلا المعاملين على حدا، بينما دالّة OR المنطقية تعامل كل من معاملاتها على أنها إمّا صحيحة (ليست صفراً) أو خطأ (صفر)، وتقيّم إمّا إلى صحيح (إذا كان إحد المعاملات ليست صفراً) وإما إلى خطأ (إذا كان كلا المعاملين صفراً).


في الجزء التالي من المقال سنتعلّم كيفيّة الدمج بين الدوالّ الخاصة بالبت في لغة C++ لإجراء عمليات مفيدة ومختصرة.


مُترجَم بتصرّف، المقال الأصلي بالإنجليزية

4


momar82
momar82منذ 3 سنوات

لا يوجد اي مثال برمجي على الدرس

0

محمد عنيني
محمد عنينيمنذ 3 سنوات

عفواً لم أفهم قصدك؟ إن كنت تتحدث عن الأمثلة فالمقال مليء بها، أو ربما تتحدث عن شيء آخر.

0

AdelBibi
AdelBibiمنذ 3 سنوات

يعطيك العافية حبيبنا محمد. جهد مشكور عليه فعلا :)

1

محمد عنيني
محمد عنينيمنذ 3 سنوات

الله يعافيك أخي عادل :)

0

Haitham
Haithamمنذ سنة

كلام مفيد اشكرك

0

Test User