بسم الله الرحمن الرحيم , و الصلاة و السلام على أشرف الأنبياء
و المرسلين نبينا محمد وعلى آله وصحبه أجمعين اللهم لا سهل إلا ماجعلته سهلاً وأنت تجعل الحزن
إذا شئت سهلاً.
من المهم لكل من يدخل عالم المتحكمات المصغرة أن يتقن احدى لغات البرمجة المستخدمة في برمجة المتحكمات و هناك العديد من لغات البرمجة و افضلها هي لغة C لذلك هممت بالبدء بكتابة هذه التدوينة كبداية لفهم بعض اساسيات هذه اللغة و ان شاء الله في القريب العاجل سأضيف دروسا ذات مستوى اعلى في برمجة المتحكمات المصغرة بلغة C .
من المهم لكل من يدخل عالم المتحكمات المصغرة أن يتقن احدى لغات البرمجة المستخدمة في برمجة المتحكمات و هناك العديد من لغات البرمجة و افضلها هي لغة C لذلك هممت بالبدء بكتابة هذه التدوينة كبداية لفهم بعض اساسيات هذه اللغة و ان شاء الله في القريب العاجل سأضيف دروسا ذات مستوى اعلى في برمجة المتحكمات المصغرة بلغة C .
1.1 مفاهيم أولية
تعد كتابة برامج C مثل عملية بناء منزل من حيث المعنى . فبعد وضع الأساسات , يستخدم
الرمل و الاسمنت لتشكيل القرميد . ومن ثم يتم تكديس هذه الصفوف فوق بعضها لتشكيل
البناء , وفي برامج C يتم
وضع مجموعة من التعليمات معا لتكوين التوابع ثم يتم التعامل مع هذه التوابع
كعمليات ذات مستوى متقدم والتي تركب معا لتشكيل البرنامج .
يجب أن يحتوي أي برنامج C على تابع واحد على الأقل أسمه main() و يعد هذا التابع أساس برنامج C سواء بشكل مباشر أو غير مباشر ومع أن جميع التوابع قد تكون شاملة
و كاملة إلا أنه يمكن ربطها مع بعضها البعض بواسطة المتحولات و البارمترات .
يعد التابع main() المهمة ذات المستوى الأدنى و ذلك لأنه يمثل أول تابع يتم استدعاؤه
من قبل النظام الذي يشغل البرنامج . و في العديد من الحالات يحتوي التابع main() على القليل من العبارات
فقط , و التي لا تقوم بأي عمل سوى تبدئة و توجيه عمل البرنامج إلى تابع أخر .
يبدو برنامج C
المضمن في أبسط أشكاله كما يلي :
#include <avr/io.h>
int main(void)
int main(void)
{
while(1) // do forever..
{;}
}
تتم عملية ترجمة البرنامج
السابق بشكل كامل ولكنك لن ترى شيئا يحدث على مخارج المتحكم . يمكننا زخرفة هذا
البرنامج بحيث نستدل على عملية تنفيذه , و بالتالي نبداء بفهم العناصر التكوينية
للغة .
#include <avr/io.h>
#include <stdio.h>
#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 إذ
يقوم هذا الموجه بإعلام المترجم لكي يُضمّن الملف avr/io.h و 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
|
1.2 المتحولات و الثوابت
حان الوقت للنظر إلى البيانات المخزنة على شكل ثوابت أو
متحولات , المتحولات هي قيم قابلة للتغيير أما الثوابت فلا يمكن تغييرها , تستعمل
الثوابت و المتحولات وفق عدة صيغ و أحجام و تخزن في ذاكرة البرنامج وفق عدة اشكال
متنوعة
1.2.1أنواع المتحولات
يتم التصريح عن المتحول بواسطة كلمة محجوزة تشير إلى نوعه
وحجمه ويليها المعرف (أي أسم المتحول) :
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 اختيارية والنتائج واحدة .
1.2.2مدى المتحولات
كما ذكرنا سابقا يجب التصريح عن المتحولات و الثوابت قبل
استعمالها , يمثل مدى المتحول إمكانية الوصول إليه ضمن البرنامج و يمكن التصريح عن
المتحول بحيث يكون ذي مدى محلي (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..
{;}
}
عند استعمال متحول ضمن تابع ما , وإذا كان لمتحول محلي
نفس أسم متحول عام , فإن التابع سيستعمل قيمة المتحول المحلي وفي هذه الحالة لن
يستطيع التابع الوصول إلى قيمة المتحول العام .
1.2.3 الثوابت
كما وضحنا سابقا الثوابت تمثل قيمة ثابتة لا يمكن تغييرها
خلال تنفيذ البرنامج تعد الثوابت في العديد من الحالات جزءا من البرنامج المترجم نفسه وتتوضع في ذاكرة
قابلة للقراءة فقط (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
|
1.2.4 التعدادات و التعريفات
تعد سهولة القراءة في لغة 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 ولكن تم قبولها نظرا لشيوع استعمالها و
المعالجة الأولية هي خطوة منفصلة عن الترجمة الفعلية للبرنامج و تتم قبل بدء عملية
الترجمة فعليا , سوف نتطرق لها لاحقا إن شاء الله .
1.2.5 صفوف التخزين
يمكن التصريح عن المتحولات وفق ثلاث صفوف تخزين : 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;
1.2.6 تحويل النوع
أحيانا قد يرغب المبرمج بتحديد نوع و حجم المتحول . يتيح
تحويل النوع إلى تغيير النوع المحدد مسبقا وذلك خلال العملية التي تنفذ حاليا
ويطبق النوع المذكور بين قوسين على العبارة التي تليه. لنأخذ التصريح و الإسناد
التالي :
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;
إلى هنا نكون قد انهينا الدرس الاول القاكم أن شاء الله في درس جديد
ان كنت اصـــبت فمن الله وحـــــده؛ وان اخطــــــأت فمن نفسي ومن الشيطآن
ان كنت اصـــبت فمن الله وحـــــده؛ وان اخطــــــأت فمن نفسي ومن الشيطآن
عبدالله جلول

ليست هناك تعليقات:
إرسال تعليق