كورس تعليمي يهدف الى اكسابك المعرفة اللازمة والمهارات التقنية المتعلقة ببرمجة وتطوير الأنظمة المدمجة وفهم خصائصها ومكوناتها
الصفحة الرئيسية
قائمة الدروس
خدمة RSS
للمتحكم TM4C123GH6PM الموجود على البورد 43 طرف pin متعدد الإستعمال تسمى بالـ General-purpose input/output GPIO pins. وليسهل التعامل معها فإنها موزعة على 6 منافذ ports كما يلي:
المنفذ | عدد الأطراف |
---|---|
PORT A | 8 |
PORT B | 8 |
PORT C | 8 |
PORT D | 8 |
PORT E | 6 |
PORT F | 5 |
ملاحظة، Port C لديه 8 أطراف لكننا لن نستخدم الأطراف التالية: PC0, PC1, PC2, PC3 حيث أنها محجوزة للـ debugger ولذلك فإننا فعلياً نستخدم 4 أطراف فقط في هذا المنفذ.
وبذلك يكون لدينا 43 طرف يمكن برمجتها ولكن 4 منها محجوز كما ذكرنا في الملاحظة السابقة، أي فعلياً هنالك 39 طرف يمكننا التعامل معها وبرمجتها.
هذه الأطراف يمكن إستخدامها كمدخل للبيانات input حيث تمكننا من إستقبال معلومات من البيئة المحيطة. أو كمخرج للبيانات output حيث تمكننا من ارسال معلومات لما قد يحيط بها أو يحتويها.
وفي الفصل 22 من دليل البيانات datasheet (ص 1328) نجد الرسم التخطيطي للمنافذ والأطراف في المتحكم:
![]() |
وها هو المتحكم على البورد حيث نستطيع أن نرى أطراف المتحكم (داخل المستطيلات الخضراء) تتصل بأطراف البورد (وهي بداخل المستطيلات الصفراء) عن طريق مسارات buses لنقل البيانات (باللون البرتقالي):
![]() |
وتمكننا الأطراف التي على البورد من ربط البورد (وبالتالي المتحكم) بأجزاء الكترونية أخرى من ضمنها لوح التجارب breadboard حيث يمكن ربطها بأسلاك سواء من الـ male headers أعلى البورد أو الـ female headers أسفل البورد.
يوجد مسارين لنقل البيانات المتعلقة بالـ Input/Output ports وهي:
المسار الثاني يعتبر الأفضل أداء والأسرع والأول أفضل من ناحية توفير الطاقة وهو الذي سيتم إستخدامه في هذا الكورس بإذن الله.
لكل منفذ مساحة 4kbyte من الذاكرة تحتوي على سجلات registers عديدة للتحكم بالمنفذ، ومن ضمنها السجل المتعلق بتحديد وظيفة أو إتجاه كل طرف في المنفذ Direction Register والسجل الخاص بالقراءة والكتابة من والى المنفذ Data Register بالإضافة الى غيرها من السجلات المتعلقة بالمنفذ.
وفيما يلي عناوين المنافذ في الذاكرة على المسار APB من الأصغر الى الأكبر، علماً أن عنوان البداية لكل منفذ يسمى الـ base address لذلك المنفذ:
المنفذ | يبدأ من | ينتهي في |
---|---|---|
PORT A | 0x4000.4000 | 0x4000.4FFF |
PORT B | 0x4000.5000 | 0x4000.5FFF |
PORT C | 0x4000.6000 | 0x4000.6FFF |
PORT D | 0x4000.7000 | 0x4000.7FFF |
PORT E | 0x4002.4000 | 0x4002.4FFF |
PORT F | 0x4002.5FFF | 0x4002.5000 |
![]() |
لو أفترضنا أننا أردنا التعامل مع العنوان التالي في الذاكرة:
0x400253FC
فلو حاولنا إسناده الى متغير في لغة الـ C فإنه سيعتبر رقماً عادياً لا أكثر. ولجعل الـ C تعتبره عنواناً في الذاكرة فإننا نقوم بعملية derefrencing:
(*0x400253FC)
ولكن في هذه الحالة لن يعرف المجمع compiler كم بت bit عليه أن يقرأ من موقع الذاكرة المشار اليه. هل يقرأ 8 بت أو 16 بت أو 32 بت؟ ولذلك نقوم بتحويل القيمة casting اما لـ long أو int حيث أنه لا فرق بينهما في معمارية الـ ARM Cortex-M فكلاهما يتكون من 32 بت.
(long *)0x400253FC
وبما أن القيّم في سجلات الـ GPIO لا تحمل إشارة unsigned بمعنى أنها لا يمكن أن تكون بالسالب، فإننا نضيف عبارة unsigned على جزئية الـ casting:
(unsigned long *)0x400253FC
أيضاً، بما إن القيّم في السجلات ممكن أن تتغير من خارج الكود الذي نقوم بكتابته، فعلى سبيل المثال، لو قام المستخدم بالضغط على الزر Switch 1 فستتغير قيمة البت المقابلة في المتحكم من 1 الى 0، وهذا التغيير حدث خارج إطار الكود الذي قمنا بكتابته. ولهذا السبب فإننا سنطلب من المجمع compiler قراءة القيمة الجديدة دوماً وليس الإعتماد على قيمة مخزنة مسبقاً بسبب الـ compiler optimization. ويكون ذلك بإستخدام عبارة volatile:
(volatile unsigned long *)0x400253FC
وبما أننا نريد قيمة السجل وليس العنوان فإننا نقوم بعملية الـ derefrencing بإستخدام النجمة * وإستخدام الماكرو define# لإسناد قيمة السجل الى إسم من عندنا:
#define GPIOF_DATA (*((volatile unsigned long *)0x400253FC))
الخطوات الرئيسية للتعامل مع الأطراف هي بتعديل السجلات التالية:
ويتم ذلك عبر سجل المؤقت RCGCGPIO، ومن دون هذه الخطوة فإنه من غير الممكن التعامل مع المنفذ المطلوب. ومن أجل توفير الطاقة، فإن الـ ARM Cortex-M لا تفعّل المنافذ منذ البداية وهذه التقنية تسمى clock gating، بل يجب عليك القيام بذلك بنفسك وذلك عن طريق تفعيل المؤقت الخاص بالمنفذ المطلوب. وبالنظر الى دليل البيانات (ص 340)، نجد شرح لهذا السجل:
![]() |
ولتفعيل المنافذ فإننا نتعامل مع الـ bits التالية:
ويكون التفعيل بكتابة 1 الى البت bit المطلوب، وعدم التفعيل بكتابة 0 الى تلك الـ bit.
للتعامل مع هذا السجل، ولكي يصبح لديك عنواناً كاملاً، فإنه يتوجب علينا إضافة القيمة التي يبعدها السجل Offset عن العنوان الرئيسي للمنفذ Base address الى ذلك العنوان الرئيسي للحصول على العنوان الكامل للسجل:
Base 0x400F.E000
Offset 0x608
وبذلك يكون عنوان هذا السجل register:
#define SYSCTL_RCGCGPIO_R (*((volatile unsigned long *)0x400FE608))
وذلك بواسطة السجل GPIODIR (ص 663 في دليل البيانات) حيث يمكننا تحديد ما اذا كان الطرف للقراءة input او الكتابة output.
![]() |
ومثل ما ذكرنا سابقاً، فإننا سنستخدم المسار APB للتعامل مع الـ GPIO ولذلك سنستخدم العناوين في الذاكرة التي توضح ذلك. وفيما يخص سجل الـ GPIODIR فإنه يبعد 0x400 عن العناوين السابقة offset. وبذلك يكون السجل المستخدم لكل منفذ كما يلي:
![]() |
#define GPIO_PORTA_DIR_R (*((volatile unsigned long *)0x40004400))
#define GPIO_PORTB_DIR_R (*((volatile unsigned long *)0x40005400))
#define GPIO_PORTC_DIR_R (*((volatile unsigned long *)0x40006400))
#define GPIO_PORTD_DIR_R (*((volatile unsigned long *)0x40007400))
#define GPIO_PORTE_DIR_R (*((volatile unsigned long *)0x40024400))
#define GPIO_PORTF_DIR_R (*((volatile unsigned long *)0x40025400))
وللتحكم بالأطراف في هذه المنافذ فإننا نتعامل مع الـ bits التالية:
بكتابة 1 الى البت سيجعل الطرف الذي تتحكم به للكتابة output، وكتابة 0 سيجعل ذلك الطرف للقراءة. علماً أنه جميع الأطراف معدة لتكون للقراءة input مالم يتم تعيينها للكتابة output.
وذلك عن طريق السجل GPIODEN والذي يستخدم لتحديد ما إذا كان الطرف يستخدم للقراءة والكتابة الرقمية digital I/O أو للوظائف التماثلية analog الأخرى حيث أنه بإمكان كل طرف أن يكون له أكثر من وظيفة وهو ما يسمى بـ pin multiplexing وإلا فإننا سنحتاج الى مئات من الأطراف للقيام بجميع المهام. وبإمكان الطرف أن يستخدم للقراءة الرقمية digital I/O أو التماثلية analog او بروتوكولات التواصل مثل I2C أو UART أو SPI أو غيرها. ومن ص 683 في دليل البيانات يتضح لنا:
![]() |
نلاحظ ان السجل يبعد عن العنوان الرئيسي للمنفذ offset بمقدار 0x51C. وبعد إضافتها للعنوان الرئيسي لكل منفذ تصبح العناوين كما يلي:
#define GPIO_PORTA_DEN_R (*((volatile unsigned long *)0x4000451C))
#define GPIO_PORTB_DEN_R (*((volatile unsigned long *)0x4000551C))
#define GPIO_PORTC_DEN_R (*((volatile unsigned long *)0x4000651C))
#define GPIO_PORTD_DEN_R (*((volatile unsigned long *)0x4000751C))
#define GPIO_PORTE_DEN_R (*((volatile unsigned long *)0x4002451C))
#define GPIO_PORTF_DEN_R (*((volatile unsigned long *)0x4002551C))
وعند كتابة 1 للبت المقابل للمنفذ فإنه يتم تفعيل الخصائص الرقمية digital للطرف، وعند كتابة 0 فإنه يتم تفعيل الخصائص التماثلية analog للطرف.
ويتم ذلك عن طريق سجل GPIODATA. ففي الطرفيات المخصصة للكتابة output فإنه يمكن تشغيل الطرف وجعله high بإرسال القيمة 1 اليه، وإذا ما أردنا إطفائه low فإننا نرسل له القيمة 0. وفي الأطراف المخصصه للقراءة input فإنه يمكن قراءة البت bit المقابلة للطرف لمعرفة اذا ماتم تفعيلها أم لا. وفي ص 662 من دليل البيانات نرى:
![]() |
وصف السجل يخبرنا بأن الـ offset 0x000 ولكن هذا لإستخدام تقنية تسمى bit banding وهي تحجز لنا مكان في الذاكرة يتسع للتعامل مع كل طرف (أو بت) على حده ولكننا نريد التعديل على الـبتات الثمانية 8 bits، التي تقابل الأطراف في المنافذ، مرة واحدة. وللقيام بذلك فإننا نستخدم offset 0x3FC فتصبح لدينا العناوين:
#define GPIO_PORTA_DATA_R (*((volatile unsigned long *)0x400043FC))
#define GPIO_PORTB_DATA_R (*((volatile unsigned long *)0x400053FC))
#define GPIO_PORTC_DATA_R (*((volatile unsigned long *)0x400063FC))
#define GPIO_PORTD_DATA_R (*((volatile unsigned long *)0x400073FC))
#define GPIO_PORTE_DATA_R (*((volatile unsigned long *)0x400243FC))
#define GPIO_PORTF_DATA_R (*((volatile unsigned long *)0x400253FC))
جميع السجلات عبارة عن 32 بت (4 بايت) ما عدا GPIODATA حيث أنها مجموعة من السجلات تتكون من 256 سجل وكل سجل منها يتكون من 4 بايت أيضاً بما مجموعه 1024 بايت. ولكن الذي يهمنا الآن هو السجل بالعنوان الذي يحتوي على offset 0x3FC
بدلاً من حساب موقع السجل ومن ثم كتابته كما يلي:
#define GPIO_PORTF_DATA_R (*((volatile unsigned long *)0x400253FC))
فإنه بإمكاننا إضافة العنوان الرئيسي base address للمنفذ الى الـ offset للسجل المطلوب:
#define GPIO_PORTF_BASE 0x40025000
#define GPIO_PORTF_DATA (*((volatile unsigned long *)(GPIO_PORTF_BASE + 0x3FC)))
#define GPIO_PORTF_DIR (*((volatile unsigned long *)(GPIO_PORTF_BASE + 0x400)))
#define GPIO_PORTF_DEN (*((volatile unsigned long *)(GPIO_PORTF_BASE + 0x51C)))
وميزة هذه الطريقة إنها أوضح وتجعل عملية حساب العناوين أقل عرضة للخطأ ويمكن نسخ ما سبق لباقي المنافذ الأخرى مع تغيير الـ base address لكل منفذ بالإضافة الى تغيير إسم المنفذ، فبدلاً من F نغيره الى A, B, C … الخ.
إذا ما أردت أن تتعامل مع المسار AHB وهو الأحدث والأكثر كفائة، فكل ما عليك هو كتابة قيمة معينة الى السجل GPIOHBCTL (ص 258 في دليل البيانات) ثم إستخدام عناوين الـ AHB بدلاً من APB التي أستخدمناها سابقاً. علماً أنه بالإمكان أن يكون لديك بعض المنافذ تستخدم المسار APB وبعضها يستخدم AHB أو جميعها تستخم نفس المسار، الأمر عائد اليك.
![]() |
#define SYSCTL_GPIOHBCTL_R (*((volatile unsigned long *)0x400FE06C))
اذا أردت أن تجعل المنفذ F يستخدم المسار AHB على سبيل المثال، فبالعودة الى شرح السجل في دليل البيانات نجد أنه كل ماعليك فعله هو كتابة 1 في البت 5 في السجل، كما يلي:
SYSCTL_GPIOHBCTL_R |= 0x20; // 0001 0000, or
SYSCTL_GPIOHBCTL_R |= (1U << 5); // 0001 0000
ملاحظة: هذه المقالة تحت التحديث المستمر،، وملاحظاتكم وإقتراحاتكم ستساعد بإذن الله في إخراجها بالصورة التي كتبت من أجلها وهي تمهيد الطريق أمامكم لتعلم برمجة الأنظمة المدمجة