مفاهيم أولية في لغة C المضمنة ( الجزء الأول )

Greeting

السلام عليكم و رحمة الله وبركاته
كنت قد لاحظت في الأونة الأخيرة صدور عدة مراجع عربية للغة C و هذا امر جيد و لكن لم أجد أي كتاب أو محتوى عربي كامل عن لغة C المضمنة  لتطبيقات المتحكمات الصغرية microcontroller فقررت أن أبدء بترجمة كتاب
 Embedded C Programming and the ATMEL AVR

و بعد ان ترجمت ما يقارب 30% من محتوى الكتاب خطرت لي فكرة مشاركة ما تمت ترجمته من أجزاء الكتاب على هيئة دروس و بالتالي الإستفادة من تصحيح الأخطاء الإملائية و تقييم المحتوى من قبل القراء , و خصوصا أنني أنوي تعديل بعض من محتوى الكتاب ليتماشى مع المتحكمات الجديدة من عائلة AVR مثل ATmega328 المستخدم في Arduino , أخيرا أرحب بأي إقتراح أو ملاحظة أو تصحيح لتحسين محتوى الكتاب .
م.عبدالله جلول


مفاهيم أولية

تعد كتابة برامج C مثل عملية بناء منزل من حيث المعنى . فبعد وضع الأساسات , يستخدم الرمل و الاسمنت لتشكيل القرميد . ومن ثم يتم تكديس هذه الصفوف فوق بعضها لتشكيل البناء , وفي برامج C يتم وضع مجموعة من التعليمات معا لتكوين التوابع ثم يتم التعامل مع هذه التوابع كعمليات ذات مستوى متقدم والتي تركب معا لتشكيل البرنامج .
يجب أن يحتوي أي برنامج C على تابع واحد على الأقل أسمه main() و يعد هذا التابع أساس برنامج C سواء بشكل مباشر أو غير مباشر ومع أن جميع التوابع قد تكون شاملة و كاملة إلا أنه يمكن ربطها مع بعضها البعض بواسطة المتحولات و البارمترات .
يعد التابع main() المهمة ذات المستوى الأدنى و ذلك لأنه يمثل أول تابع يتم استدعاؤه من قبل النظام الذي يشغل البرنامج . و في العديد من الحالات يحتوي التابع main() على القليل من العبارات فقط , و التي لا تقوم بأي عمل سوى تبدئة و توجيه عمل البرنامج إلى تابع أخر .

يبدو برنامج C المضمن في أبسط أشكاله كما يلي :
int main(void)
{
       while(1)             // do forever..
       {;}
}

تتم عملية ترجمة البرنامج السابق بشكل كامل ولكنك لن ترى شيئا يحدث على مخارج المتحكم . يمكننا زخرفة هذا البرنامج بحيث نستدل على عملية تنفيذه , و بالتالي نبداء بفهم العناصر التكوينية للغة .
#include <stdio.h>

int main(void)
{
       printf("hello world!");  /* the classic c test program . */
while(1)             // do forever..
       {;}
}   
سيقوم هذا البرنامج بطباعة العبارة Hello World! على الخرج القياسي و الذي يمثل في معظم الحالات البوابة التسلسلية و سينتظر المتحكم الصغري إلى ما لانهاية أو حتى تتم عملية إعادة التشغيل Reset . يوضح ذلك الفروقات الأساسية بين برنامج خاص بالحاسب الآلي و برنامج مصمم من أجل المتحكمات الصغرية المضمنة , أي أن تطبيقات المتحكمات تحتوي على حلقة غير منتهية. يحتوي الحاسوب الشخصي على نظام تشغيل فعند انتهاء تنفيذ برنامج ما فإنه يعيد التحكم إلى نظام التشغيل الموجود في الحاسب . بينما لا تحتوي المتحكمات نظام تشغيل ولا يسمح لها بالخروج من البرنامج في أي وقت , مثل حلقة While(1) في المثال السابق , وهذا ما يمنع البرنامج من الانتهاء ومن القيام بأعمال عشوائية قد تكون غير مرغوب بها سوف نشرح الحلقة While لاحقا . كما يقدم البرنامج السابق مثالا عن موجهات المترجم (Compiler) وهو #include إذ يقوم هذا الموجه بإعلام المترجم لكي يُضمّن الملف stdio.h كجزء من البرنامج و بالتالي يصبح التابع printf متاحا في البرنامج وذلك لأن تعريفه موجود ضمن الملف stdio.h سوف نشرح هذه المفاهيم لاحقا .

إليكم الآن بعض العناصر البرمجية الملحوظة في المثال السابق :

; تستعمل الفاصلة المنقوطة للدلالة على نهاية العبارة البرمجية و العبارة في ابسط حالاتها تتألف من فاصلة منقوطة فقط .

{} تستعمل الأقواس المعقوفة للدلالة على بداية و نهاية محتوى التابع كما تستعمل هذه الأقواس عندما نريد معالجة سلسلة عبارات معا ككتلة واحدة.

"text" تستعمل فواصل التنصيص للدلالة على بداية ونهاية سلسلة محارف نصية .

/*..*/ أو // تستعمل للدلالة على التعليقات .

تمثل التعليقات ملاحظات المبرمج وتعد أساسية لقراءة و فهم البرنامج وهذا صحيح طالما أن البرنامج سيقرؤه أشخاص آخرون أو المبرمج نفسه ولكن بعدة فترة من الزمن .
تعمل التعليقات المستخدمة في المثال السابق على شرح وظيفة كل سطر من الشيفرة إذ يجب أن يشرح التعليق الوظيفة الفعلية لسطر البرنامج وليس فقط التعليمة المستخدمة في هذا السطر .
إن محدد التعليقات التقليدي هو /*..*/ وعندما يصادف المترجم الرمز /* فإنه يتجاهل النص الذي يليه حتى وإن كان مكتوب على عدة اسطر ويستمر بذلك حتى يصادف الرمز */ و الذي يدل على نهاية التعليق لاحظ أن المثال السابق يستخدم هذا المحدد .
أما المحدد // فيجعل المترجم يتجاهل النص الوارد بعده ولكن حتى نهاية السطر فقط . و قد استعملنا هذا المحدد في السطر الثاني من البرنامج السابق .
و بالانتقال إلى التفاصيل علينا تذكر بعض المصطلحات و القواعد الأساسية :
  • المعرف (identifier) هو اسم متحول أو ثابت أو تابع مكون من حروف أو من خط سفلي (_) تليه سلسلة من الحروف و/أو الأرقام و/أو الخطوط السفلية.
  • يتم التمييز بين الحروف الصغيرة و الكبيرة في لغة C .  
  • ليس هناك قيود على طول أسم المعرف عموما ولكن تتعرف بعض المترجمات فقط على طول محدد للاسم مثلا أول 32 حرف فأنتبه لذلك !.  
  • توجد بعض الكلمات ذات معنى خاصة بالنسبة للمترجم و تعد كلمات محجوزة ويجب كتابة هذه الكلمات بحروف صغيرة ولا يسمح أبدا باستعمالها كمعرفات يبين الجدول التالي هذه الكلمات المحجوزة .
union
register
float
define
auto
unsigned
return
for
do
break
void
short
goto
double
bit
volatile
signed
if
eeprom
bool
while
sizeof
inline
else
case
true
static
int
enum
const
false
switch
interrupt
extern
continue

typedef
long
flash
default

المتحولات و الثوابت

حان الوقت للنظر إلى البيانات المخزنة على شكل ثوابت أو متحولات , المتحولات هي قيم قابلة للتغيير أما الثوابت فلا يمكن تغييرها , تستعمل الثوابت و المتحولات وفق عدة صيغ و أحجام و تخزن في ذاكرة البرنامج وفق عدة اشكال متنوعة

أنواع المتحولات

يتم التصريح عن المتحول بواسطة كلمة محجوزة تشير إلى نوعه وحجمه ويليها المعرف (أي أسم المتحول) :
unsigned char Peabody;

int dogs, cats;

long int total_dogs_and_cats;


يتم تخزين الثوابت و المتحولات في ذاكرة المتحكم الصغري المحدودة , و بالتالي يحتاج المترجم لمعرفة المساحة التي يجب حجزها لكل متحول بدون هدر مساحات الذاكرة . بالتالي يتوجب على المبرمج التصريح عن المتحولات من خلال تحديد حجم و نوع كل متحول يبين الجدول التالي أنواع المتحولات و الحجوم المخصصة لكل نوع .


Byte
Bits
Maximum
Minimum
Type
1
8
127
-128
(signed) char
1
8
255
0
unsigned char
2
16
32767
-32768
(signed) int
2
16
65535
0
unsigned int
4
32
2147483647
-2147483648
(signed) long
4
32
4294967295
0
unsigned long
4
32
3.438
-3.438
float
8
64
1.7308
-1.7308
double

ملاحظة : كلمة signed اختيارية و لن تؤثر في تعريف المتحول .

مدى المتحولات

كما ذكرنا سابقا يجب التصريح عن المتحولات و الثوابت قبل استعمالها , يمثل مدى المتحول إمكانية الوصول إليه ضمن البرنامج و يمكن التصريح عن المتحول بحيث يكون ذي مدى محلي (local) أو عام (global) . 

المتحولات المحلية

المتحولات المحلية (local variables) هي حيز الذاكرة الذي يحجزه التابع عند تنفيذه , ويكون موقعه عادة ضمن مكدس البرنامج أو المكدس الذي يخلقه المترجم , ولا يمكن الوصول لهذه المتحولات من قبل توابع اخرى , أي أن مدى هذه المتحولات محدود بالتوابع التى تحتوي تصريحا عنها و لذلك يمكن التصريح عن المتحول في عدة توابع بدون حدوث أي تضارب و ذلك لأن المترجم يرى هذه المتحولات كأنها جزء من التابع فقط .

المتحولات العامة

المتحولات العامة (global variable) أو الخارجية هي حيز الذاكرة الذي يحدده المترجم ويمكن الوصول إليه من قبل كل التوابع في البرنامج ويمكن تعديل محتوى المتحول العام من قبل أي تابع و يحافظ على قيمته بحيث يمكن استعماله من قبل توابع اخرى .

يتم عموما تصفير المتحولات العامة وجعل قيمتها مساوية للصفر وذلك عند بدء عمل التابع الأساسي main() و غالبا ما تتم هذه العملية من قبل شيّفرة بدء التشغيل التي يولدها المترجم وهي غير مرئية للمبرمج .

نبين في الشيفرة التالية مثالا عن مدى المتحولات : 

unsigned char globey;         // a global variable

void function_z(void)        // this is a function called from main()

{

       unsigned int tween;   // a local variable

       tween = 12;          // ok because tween is local

       globey = 47;         // ok because globey is global 

       main_loc = 12;       // this line will generate an

                              // error because main_loc is local to main

}


int main(void)
{
       unsigned char main_loc;    // a variable local to main()
       globey = 43;              // ok because globey is a global to function_z
       while(1)                 // do forever..
            {;}
}

عند استعمال متحول ضمن تابع ما , وإذا كان لمتحول محلي نفس أسم متحول عام , فإن التابع سيستعمل قيمة المتحول المحلي وفي هذه الحالة لن يستطيع التابع الوصول إلى قيمة المتحول العام . 

الثوابت

كما وضحنا سابقا الثوابت تمثل قيمة ثابتة لا يمكن تغييرها خلال تنفيذ البرنامج تعد الثوابت في العديد من الحالات جزءا من البرنامج المترجم نفسه و تتوضع في ذاكرة قابلة للقراءة فقط (ROM) وليس في ذاكرة الوصول العشوائي القابلة للتعديل (RAM) ففي عبارة الإسناد التالية :
x = 3 + y;

العدد 3 هو عدد ثابت إذ يقوم المجمع بتشفيره مباشرة ضمن عملية الجمع كما وقد تكون الثوابت على شكل محارف أو سلسلة محارف نصية :


printf("hello world!");

x = 'B';



كما يمكنك التصريح عن ثابت ما باستعمال الكلمة المحجوزة const و تحديد نوعه و حجمه . إذا يتطلب الأمر معرفا و قيمة وذلك لإتمام عملية التصريح :

const char c = 57;


يؤدي تعريف متحول ما كثابت إلى تخزين ذاك المتحول في حيز شيفرة البرنامج بدلا من حيز تخزين المتحولات المحدودة في الذاكرة RAM وهذا ما يساعد على تقنين حيز الذاكرة RAM المحدودة .

الثوابت العددية

يمكن التصريح عن الثوابت العددية وفق عدة طرق وذلك بالإشارة إلى أساسها العددي وبالتالي جعل البرنامج أسهل للقراءة . يمكن كتابة الثوابت Integer (الصحيحة) و long integer كما يلي :
  • الصيغة العشرية بدون بادئة مثل (1234) .
  • الصيغة الثنائية مع البادئة 0b مثل (0b11100101) . 
  • الصيغة الست عشرية مع بادئة 0x مثل (0xc1) . 
  • الصيغة الثمانية مع البادئة 0 مثل (0123) .
كما توجد بعض المعدلات لتعريف حجم الثابت بشكل أفضل :
  •  الثوابت من نوع Unsigned integer قد تأخذ اللاحقة U مثلا (10000U) .
  • الثوابت من نوع Long integer قد تأخذ اللاحقة L مثل (99L) .  
  • الثوابت من نوع Long Unsigned integer قد تأخذ اللاحقة UL مثل (99UL) . 
  • الثوابت من نوع Float قد تأخذ اللاحقة F مثل (1.23F) . 
  • الثوابت من نوع Char يجب وضعها ضمن فاصلتين علويتين مثل ('A' or 'a') .

الثوابت المحرفية

قد تكون الثوابت المحرفية من النوع القابل للطباعة مثل (A..Z and 0..9) أو من النوع الغير قابل للطباعة مثل (محرف الجدولة TAB محرف الإرجاع Carriage return) ويمكن وضع المحارف القابلة للطباعة ضمن فواصل علوية مثل ('a') كما يمكن استعمال الشرطة العكسية يليها القيمة الثمانية أو الست عشرية للمحرف حسب ترميز ASCII ضمن الفاصلة العلوية وذلك لتمثيل الثوابت المحرفية :

't' يمكن تمثيله كما يلي '\164' ثماني
أو
'\x74' ست عشري . 
يبين الجدول التالي بعض المحارف الغير قابلة للطباعة و التي يتم التعرف عليها في لغة C :

القيمة الست عشرية المكافئة
التمثيل
المحرف
'\x07'
'\a'
BEL
'\x08'
'\b'
Backspace
'\x09'
'\t'
TAB
'\x0a'
'\n'
LF (new line)
'\x0b'
'\v'
VT
'\x0c'
'\f'
FF
'\x0d'
'\r'
CR

التعدادات و التعريفات

تعد سهولة القراءة في لغة C مهمة جدا لذلك تستعمل التعدادات و التعريفات بحيث يستطيع المبرمج استبدال الأعداد بأسماء أو بجمل ذات معنى أكثر تعبيرا .
تمثل التعدادات تعريفا لثوابت إذ تستخدم الكلمة المحجوزة enum لإسناد قيم ثوابت صحيحة (integer) متتابعة إلى لائحة معرفات :
int num_val;                 // declare an integer variable
enum { zero ,one ,two , three}; // declare an enumeration
num_val = two;                      // the same as: num_val = 2

فالاسم zero تسند إليه القيمة 0 و one القيمة 1 و تسند القيمة 2 للاسم tow وهكذا . كما يمكن فرض القيمة الأولى كما يلي :
enum { start = 10, next1, next2, end_val};


في هذه الحالة يأخذ Satart القيمة 10 و Next1 القيمة 11 و Next2 القيمة 12 و End_val القيمة 13 . إذا تستعمل التعدادات كبديل لأعداد صحيحة والتي يريد المبرمج ربطها مع كلمات أو جمل ذات معنى .
تستعمل التعريفات بطريقة مشابهة إلى حد ما للتعدادات وذلك من حيث كونها تسمح باستبدال سلسلة محارف نصية بأخرى لنأخذ المثال التالي
enum { red_led_on = 1, green_led_on, both_leds_on };
#define leds PORTA

PORTA = 0x01;              // means turn the red led on
leds = red_led_on;   // means the same thing


يؤدي السطر #define leds PORTA إلى جعل المترجم يستبدل الكلمة leds بالتسمية PORTA أينمى وجدت لحظ أن السطر #define لا ينتهي بفاصلة منقوطة يعمل التعداد على وضع قيمة red_led_on تساوي 1 و green_led_on تساوي 2 و قيمة both_leds_on تساوي 3 . و يمكن استعمال ذلك في برنامج للتحكم باللدين الأحمر و الأخضر حيث يؤدي إخراج القيمة 1 إلى تشغيل الليد الأحمر و يؤدي إخراج القيمة 2 لتشغيل الليد الأخضر و يؤدي إخراج القيمة 3 إلى تشغيل اللدين الأحمر و الأخضر (حاول التعديل على البرنامج السابق و إضافة قيمة لإطفاء اللدين معا ) الفكرة هنا هي أن العبارة red_led_on يمكن فهمها اكثر من العبارة PORTA = 0x01; .

العبارة #define هي موجه معالجة أولية في الواقع ليست موجهات المعالجة الأولية جزءا من الصيغة القواعدية للغة C ولكن تم قبولها نظرا لشيوع استعمالها و المعالجة الأولية هي خطوة منفصلة عن الترجمة الفعلية للبرنامج و تتم قبل بدء عملية الترجمة فعليا , سوف نتطرق لها لاحقا إن شاء الله .

 صفوف التخزين

يمكن التصريح عن المتحولات وفق ثلاث صفوف تخزين : auto , static , register الخيار الأول auto هو الصف الافتراضي وهذا ما يعني أن الكلمة auto المحجوزة غير ضرورية .

Automatic

لا يتم تحديد قيمة ابتدائية لمتحول محلي من الصف auto عند تخصيصه ولذلك فالمبرمج معني بالتأكد من احتواء المتحول على قيمة مقبولة قبل استعماله أو إسناد قيمة افتراضية له عند التصريح عنه . يتم تحرير هذه المساحة من الذاكرة عند الخروج من التابع أي ان القيم ستصبح غير صحيحة عند العودة إلى التابع من جديد يتم التصريح عن المتحول من الصف auto كما يلي :
auto int value_1;
أو
int value_1;  // this is the common , default form
ويمكن إسناد قيمة افتراضية للمتحول أثناء التصريح عنه كما يلي :
auto int value_1 = 10;
أو
int value_1 = 10;
في المثال السابق تم التصريح عن المتحول و إسناد القيمة 10 إليه كقيمة ابتدائية .

Static

يكون المتحول المحلي الستاتيكي أي الساكن فعالا في التابع الذي تم تعريفه ضمنه ( أي لا يمكن الوصول إليه من التوابع الأخرى ) ولكنه مخصص في حيز عام من الذاكرة . يتم تبدئة قيمة المتحول الستاتيكي بالقيمة صفر عند الدخول أول مرة إلى التابع إذا لم يتم إسناد إي قيمة ابتدائية إليه و يحافظ على قيمته الأخيرة عند الخروج من التابع وهذا ما يسمح بالحفاظ على صحة قيمة المتحول مع كل عملية إعادة دخول إلى التابع .
static int value_2;
static int value_3 = 90;

في المثال السابق المتحول value_2 سيتم تبدئته بالقيمة 0 و المتحول value_3 تمت تبدئته بالقيمة 90 .

register

يشبه المتحول المحلي من الصف register المتحول من الصف auto من حيث عدم تبدئته وكونه آنيا و الفرق بينهما هو أن المترجم سيحاول إدخال سجل آلة فعلي في المعالج الصغري كمتحول وذلك للحد من تعليمات الآلة اللازمة للوصول إلى المتحول . يتوفر عدد قليل جدا من السجلات مقارنة بالذاكرة الكلية في آلة نموذجية إذا سيتم استعمال هذا الصف باقتصاد و ذلك بهدف تسريع العمل.
register char value_4;

تحويل النوع

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

int x;        // a signed, 16-bit, integer (-32768 to 32767)
char y;              // a signed, 8-bit, character (-128 to 127)
x = 12;

قد يكون تحويل النوع للمتحولات السابقة كما يلي :
y = (char)x + 3;     // x is converted to a character and then 3 is added.
                     // and the value is then placed into y.
x = (int)y;          // y is extended up to an integer, and assigned to x.

يعد تحويل النوع هاماً جداً بالأخص عند إجراء العمليات الحسابية مع متحولات ذات حجوم مختلفة و في عدة حالات تكون دقة الحساب متعلقة بشكل أساسي بالمتحول ذي النوع الأصغر حجماً لنأخذ التعليمات التالية :
int z;        // declare z
int x = 150;  // declare and initialize x
char y = 63;  // declare and initialize y
z = (y * 10) + x;

عندما يعالج المترجم الطرف الأيمن من المعادلة فإنه ينظر إلى حجم y ويفترض أن y * 10 هي عملية ضرب محرفي (أي ذات حجم 8 بت) وستتجاوز النتيجة الموضوعة ضمن المكدس عرض موقع التخزين وهو بايت واحد أو القيمة 255 كحد أعلى يؤدي هذا بالتالي إلى قص قيمة ناتج الضرب إلى (0x76) 118 بدلاً عن القيمة الصحيحة (0x276) 630 و في المرحلة التالية من العملية الحسابية يقوم المترجم بتحديد حجم العملية الحسابية على أنه من النوع الصحيح (integer) أي 16 بت و بالتالي يتم تمديد 118 إلى عدد صحيح ثم جمعها مع المتحول x و أخيراً يتم اسناد القيمة 268 إلى المتحول z وهذا خاطئ !.
لذلك يجب استعمال تحويل النوع للتخلص من هذه النوعية من المشاكل أي يمكننا كتابة المعادلة الأخيرة من الشيفرة السابقة بالشكل التالي :
z = ((int)y * 10) + x;

حيث يُعامل المترجم المتحول y على أنه عدد صحيح (16 بت) في هذه العملية فقط . و هذا يؤدي إلى وضع القيمة الصحيحة 630 في المكدس كناتج عملية ضرب 16 بتاً . ثم يتم جمع x إلى القيمة الصحيحة في المكدس و ذلك للحصول على النتيجة الصحيحة 780 (0x30C) و أخيرا يتم إسناد القيمة 780 إلى المتحول z .

تعد لغة c مرنة جداً , و ستعطيك كل ما تطلبه . إذ يفترض المترجم أن المبرمج يعرف ما يريد القيام به .ففي المثال السابق , إذا كانت قيمة y هي 6 بدلا من 63 عندها لن يكون لدينا أي خطأ. إذاً , يتوجب عليك عند كتابة العبارات البرمجية التفكير دوماً بالقيم العظمى التي قد تأخذها العبارة , و بقيمة ناتج عمليات الضرب و الجمع .
القاعدة الجديدة التي يمكن إتباعها هي : "عند الشك قم بتحويل النوع" . و نفذ دوماً عملية تحويل المتحولات , إلا إذا كنت متأكداً من عدم حاجتك لذالك .

share

4 التعليقات

إضغط هنا لـ التعليقات
Unknown
المدير
23 يوليو 2015 في 5:08 م ×

جزاك الله كل خير و بانتظار المزيد من هذه السلسلة

رد
avatar
Unknown
المدير
22 مايو 2016 في 12:23 ص ×

بارك الله فيك وجزاك خيرا استمر فنحن بحاجه شديده لهذا العلم

رد
avatar
horizon4electronics
المدير
7 يوليو 2016 في 3:49 م ×

واياك اخي الكريم و شكرا لكلماتك الطيبة :)
مستمرين ان شاء الله في تقديم كل ماهو مفيد لإثراء المحتوي العربي العلمي :-bd

رد
avatar
Unknown
المدير
12 نوفمبر 2017 في 11:10 ص ×

فين باقي الاجزاء يا هندسة
نحن ننتظرك بفارغ الصبر

رد
avatar
شكرا لك ولمرورك