C preprocessor المعالج التمهيدي للغة سي
إن لغة C هي الأكثر شيوعاً واستخدماً في برمجة المتحكمات الصغرية والنظم المضمنة . يجب أن نسعى أن تكون البرامج المنجزة قوية robust و تركيبية modular و قابلة لإعادة الاستخدام reusable في حال اختلاف البنية العتادية(بشكل جذري أو جزءي) و التي سيعمل عليها الكود البرمجي.
حديثنا في هذا الدرس عن أكواد C preprocessor وهي طريقة معينة تكتب فيها أوامر توجيهية directive وأحد أشهر الأمور التي تحويها هي #include شائعة الاستخدام في تضمين المكاتب وهي أحد استخدامات لغة C preprocessor.
من أين جاءت التسمية C Preprocessor ؟
هي مرحلة تسبق عملية الترجمة compile إذا تتم معالجة مسبقة للكود قبل تمريره إلى المترجم compiler وكمثال بسيط عندما يكون هناك أمر توجيهي include فإنه يتم البحث عن المكتبة أو الملف المحدد ويتم تضمينه في الملف المصدري. إذ أن مهام المعالجة المسبقة preprocessor لا تتعلق بتنفيذ الكود وترجمته وإنما في بعض الإجرائيات الغير تنفيذية وإنما التوجيهية directive كما سنرى لاحقاً كالبحث عن الكلمات الرمزية المعرفة واستبدالها بالتعريفات الخاصة بها.
بعض استخدامات C preprocessor
سنذكر بعض التطبيقات مع شرح مبسط، وسيتم لاحقاً الحديث بشكل مفصل عن كل تطبيق منها.
- include : أمر لتضمين ملف أو مكتبة معينة.
- Define : أمر لتعريف دلالات لكلمات كأن نعرف أنه كلما وردت كلمة Pi في الكود فهو يعني 3.14 وكذلك لإعطاء معرّف(رمز) لكتلة أسطر برمجية مع إمكانية وجود وسطاء arguments وهذا ما يسمى function-like macros .
- pragma : أمر لتحديد بعض الأوامر للمترجم compiler
- الترجمة الشرطية conditional compilation : مجموعة أوامر تستخدم في الترجمة حسب شروط معينة، مثال: نخبر المترجم أنه لو كنت في نظام تشغيل ويندوز قم بتضمين المكتبة الفلانية أما لو كنت في نظام تشغيل لينكس فقم بتضمين مكتبة أخرى.
سنركز في هذا الدرس على أوجه استخدام الـ C preprocessor في بناء كود في المتحكمات وأوجه استخدام C preprocessor في embedded C وهي لغة السي المستخدمة في برمجة المتحكمات والنظم المضمنة عموماً. هذا لا يمنع أن نشرح بعض الأمور التي لابد منها لوضع فهم صحيح وأشمل لـ C preprocessor.
النحوية C preprocessor syntax
أي سطر برمجي يبدأ برمز المربع # hash فإن ما يليه هو أمر سيوجه إلى ال preprocessor .
#include معنى
أكثر الاستخدامات شيوعاً للC preprocessor وهو أمر توجيهي من أجل تضمين مكتبة (وهي عبارة عن مجموعة تعريفات) أو حتى ملفات مصدرية أخرى.
مثال:
بفرض الكود المصدري للملف main.c هو التالي:
#include<avr/delay.h> #include "device_conf.h" int main(void){ set_port_output; while(1){ PORT_on; _delay_ms(1000); PORT_off; _delay_ms(1000); } }
لأخذ العلم: الكود السابق تم كتابته من أجل متحكم من عائلة AVR و مترجم AVR GCC .
نرى في الكود أمرين include الأول لتضمين مكتبة خاصة بتوابع التأخير الزمني avr/delay.h و مكتبة device_conf.
نلاحظ أن التضمين الأول كان بين قوسين <> والثاني بين إشارتين “” ،حيث أن الاستخدام الأول يكون عندما نريد أن يتم البحث عن الملف المحدد ضمن المسارات المتاحة في إعدادات المترجم. والاستخدام الثاني يكون عندما نريد البحث عن الملف المحدد ضمن المجلد المصدري نفسه.
إن ملف device_conf يحوي الكود التالي ولنتجاوز شرح أسطره(فهي ضبط لبعض السجلات للبوابة D في متحكم AVR ) :
#include <avr/io.h> #define PORT_on PORTD=0xFF #define PORT_off PORTD=0x00 #define set_port_output DDRD=0xFF
سيكون الكود بعد ال preprocessor بالشكل التالي (للتبسيط لن نذكر نتائج تضمين مكتبة avr/delay.h أو مكتبة avr/io.h ):
//after include device_conf #define PORT_on PORTD=0xFF #define PORT_off PORTD=0x00 #define set_port_output DDRD=0xFF int main(void){ set_port_output; while(1){ PORT_on; _delay_ms(1000); PORT_off; _delay_ms(1000); } }
#define معنى
يستخدم هذا الأمر التوجيهي directive لتعريف كلمات كرموز (كلمات مفتاحية) ترد في الكود ليتم استبدالها بالقيمة المحددة (عددية أو محرفية أو أسطر برمجية أخرى) مع أننا سنرى لاحقاً أن الpreprocessor لا يستطيع التمييز بينها.
ففي الكود السابق سيقوم الpreprocessor بالبحث عن أماكن ورود PORT_on و PORT_off و set_port_output ويقوم باستبدالها بما هو مذكور في تعريفها.
أي أن الكود سيصبح كالتالي قبل دخوله في عملية الترجمة الفعلية:
//after preprocessor #define PORT_on PORTD=0xFF #define PORT_off PORTD=0x00 #define set_port_output DDRD=0xFF int main(void){ DDRD=0xFF; while(1){ PORTD=0xFF; _delay_ms(1000); PORTD=0x00; _delay_ms(1000); } }
ملاحظة مهمة: إن ما يرد بعد الأمر التوجيهي لا يتم معالجته أو تنقيح أخطاؤه، مثلاً لو قمنا بالتعديل التالي وهو إضافة تعليق
#define PORT_on PORTD=0xFF #define PORT_off PORTD=0x00 //set portD off #define set_port_output DDRD=0xFF
عودة إلى الكود main.c ، سيصبح بعد الاستبادل عبر الـpreprocessor كالتالي:
int main(void){ DDRD=0xFF; while(1){ PORTD=0xFF; _delay_ms(1000); PORTD=0x00 //set portD off ; _delay_ms(1000); } }
ونلاحظ أن الكود يحوي خطأ حيث أصبحت الفاصلة المنقوطة بعد التعليق، فالpreprocessor ليس من مهمته فهم مابعد الأوامر التوجيهية وإنما فقط استبدال الرموز بقيمتها فقد وضعنا تعليق في التعريف بينما عند استخدامه داخل الكود الأساسي ورد اسمه ملحقاً بفاصلة منقوطة مما أدى إلي هذا الخطأ.
تنويه: في بعض الإصدارات قد تكون مثل هذه المشكلة محلولة ولكن يجب تجنب مثل هذه الملاحظات بشكل عام.
ملاحظة: يمكننا متابعة كتابة تعريف الmacros في أكثر من سطر لسهولة القراءة عبر إدراج \ إلى نهاية السطر ،مثال:
#define PORT_on PORTD=0xFF; \ PORTB=0xFF
ما فائدة استخدام الـ C preprocessor حتى الآن؟
لنفترض أن الكود الذي كتباناه سيتم تنفيذه على نفس المتحكم ولكن بوابة D غير متاحة للاستخدام ،فكل ما علينا فعله هو تعديل المكتبة device_conf وتعديل الكود كالتالي.
#include <avr/io.h> #define PORT_on PORTB=0xFF #define PORT_off PORTB=0x00 #define set_port_output DDRB=0xFF
وبهذا جعلنا الكودقابل لإعادة الاستعمال بتعديل واحد فقط، طبعاً لا يمكن الشعور بأهمية ذلك إلا في الحالات الأكثر واقعية وتعقيداً.
بعض الفوائد الأخرى
- قابلية إعادة استخدام الكود حتى لو تم تبديل عائلة المتحكم الذي نعمل عليه أو بعض التوصيلات.
- التخلص من التعامل مع العمليات المعقدة مثل عمليات تعديل بيتات معينة في السجلات.
مثال عملي
على استخدام الـ C preprocessor لتسهيل التعامل مع بتات السجلات أو ما يسمى Bitwise Operations :
/* ———— Macros For Building BitFields ————– */ #define BIT_SET(ADDRESS,BIT) (ADDRESS |= (1<<BIT)) #define BIT_CLEAR(ADDRESS,BIT) (ADDRESS &= ~(1<<BIT)) #define BIT_WRITE(n,ADDRESS,BIT) (n ? BIT_SET(ADDRESS,BIT) : BIT_CLEAR(ADDRESS,BIT)) #define BIT_CHECK(ADDRESS,BIT) (ADDRESS & (1<<BIT)) #define BIT_FLIP(ADDRESS,BIT) (ADDRESS ^= (1<<BIT)) #define BIT_GET(ADDRESS,BIT) (ADDRESS & (1<<BIT)) /* ——————– Example Usage ————————-*/ BIT_SET(PORTB,1); if(BIT_GET(PINB,1) == 0) BIT_SET(PORTB,5); else BIT_CLEAR(PORTB,5); /* ——————————————————— */
سنؤجل شرح الكود إلى بداية الجزء الثاني في فقرة function-like macros .
في الجزء الثاني سنتعرف أكثر على بعض الأمور والملاحظات المتقدمة بالإضافة إلى الاستخدامات الأخرى للـ C preprocessor.
[related_posts_by_tax title=”مقالات مرتبطة” format=”thumbnails” image_size=”medium” taxonomies=”articles” posts_per_page=”3″ link_caption=1]