لنتعلم برمجة الأنظمة المدمجة

Embedded Systems Programming Bootcamp

كورس تعليمي يهدف الى اكسابك المعرفة اللازمة والمهارات التقنية المتعلقة ببرمجة وتطوير الأنظمة المدمجة وفهم خصائصها ومكوناتها

الصفحة الرئيسية
قائمة الدروس
خدمة RSS

للتواصل

12. بروتوكول الإتصال المتزامن SPI

2018-07-18






الواجهة الطرفية التسلسلية Serial Peripheral Interface - SPI هي بروتوكول بيانات متسلسلة serial ومتزامنة synchronous تستخدمه المتحكمات للتواصل مع جهاز طرفي واحد أو أكثر أو مع متحكم آخر بسرعة نقل بيانات عالية وعبر مسافات قصيرة.

أحد المزايا الرئيسية لـ SPI هو أنه يوفر معدل نقل بيانات أعلى من UART و I2C ويستخدم عند الحاجة إلى اتصال تسلسلي عالي السرعة. يمكنك أيضًا إرسال المزيد من المعلومات لكل عملية نقل بيانات ولكن يعيبها حاجتها الى المزيد من الأطراف عن تلك التي بـ UART و I2C.


الهاردوير

في الـ SPI هناك دائمًا جهاز رئيسي واحد (عادةً ما يكون المتحكم) يتحكم في الأجهزة الطرفية. وعادة ما يكون هنالك ثلاثة خطوط مشتركة بين جميع الأجهزة:

1. SCLK - Serial Clock

تحدد الساعة معدل نقل البيانات وينقل هذا الخط نبضات الساعة التي تعمل على تزامن نقل البيانات التي تم إنشاؤها بواسطة الطرف الرئيسي. كل بت من البيانات يتم نقله على هذا الخط تتزامن وتتوافق مع إشارة أو نبضة واحدة للساعة. والهدف من ذلك هو تمكين المتلقي من إستقبال البتات في الرسالة بنفس المعدل الذي تم نقلها عليه للتأكد من عدم ضياع أية بت وليتمكن المستقبل من تفسيرها بشكل صحيح.

2. MOSI - Master Out Slave In

يستخدمه الجهاز الرئيسي لإرسال البيانات إلى الأجهزة الطرفية. ويتم إرسال البت الأعلى most significant bit أولاً.

3. MISO - Master In Slave Out

يتلقى الجهاز الرئيسي البيانات من الأجهزة الطرفية عبر هذا الخط. ويتم إستقبال البت الأعلى most significant bit أولاً.

وهنالك خط واحد خاص بكل جهاز فرعي ويكون متصلاً بالجهاز الرئيسي:

4. SS - Slave Select

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

الرسم التوضيحي التالي يمثل الاتصال بين جهاز رئيسي وجهاز فرعي واحد:

أما إذا كان لدينا أكثر من جهاز فرعي واحد، فإن ثلاثة من الخطوط ستكون متصلة بجميع الأطراف: SCLK و MOSI و MISO، بينما سيكون لكل طرف خط SS واحد:


الساعة

SPI هو ناقل بيانات “متزامن” synchronous، مما يعني أنه يستخدم خطوط منفصلة للبيانات و خط للساعة لتحافظ على تزامن كلا الجانبين. ونظرًا الى أن الساعة يتم إرسالها في خط مستقل مع البيانات، فإنه لا يتوجب علينا توحيد سرعة نقل البيانات للمرسل والمستقبل كما فعلنا في الـ UART.

الساعة عبارة عن إشارة تتأرجح oscillating بإستمرار تخبر الطرف المستقبل متى عليه بالضبط أخذ عينات sample للبتات على خط البيانات. قد تؤخذ العينة عند إرتفاع (منخفض إلى مرتفع) أو سقوط (مرتفع إلى منخفض) حافة إشارة الساعة. عندما يكتشف المستقبِل هذه الحافة، فإنه سينظر على الفور إلى خط البيانات لقراءة البت bit التالي.

طرف واحد فقط يولد إشارة الساعة يسمى الطرف “الرئيسي” master، والجانب الآخر يسمى بالطرف “الفرعي” slave. هناك دائمًا طرف رئيسي واحد فقط (وهو دائمًا ما يكون المتحكم الخاص بك)، ولكن يمكن أن يكون هنالك العديد من الأطراف الفرعية. ونظرًا لأن الطرف الرئيسي هو الوحيد الذي يمكنه أن يولًد الساعة فإنه هو من يحدد وقت حدوث جميع العمليات على البيانات.

عندما يتم إرسال البيانات من الطرف الرئيسي للفرعي، يتم إرسالها على خط بيانات يسمى MOSI، أو “Master Out / Slave In”. وإذا احتاج الطرف الفرعي إلى إرسال رد إلى الطرف الرئيسي، فسوف يستمر الطرف الرئيسي في توليد عدد محدد مسبقًا من دورات الساعة، وسيضع الطرف الفرعي بياناته على خط بيانات ثالث يسمى MISO، أو “Master In / Slave Out”.

لا يمكن للجهاز الطرفي أن يرسل للرئيسي بيانات كيفما يشاء، بل لابد أن يكون ذلك محدداً ومعروفاً مسبقاً من قبل الجهاز الرئيسي فيما اذا ما كان سيقوم الجهاز الطرفي بالإجابة على رسالة أرسلها الجهاز الرئيسي وماهو حجم هذه البيانات. وذلك لأن الجهاز الرئيسي هو وحده من يولد الساعة. هذا السيناريو يختلف كثيرا عن الإرسال الغير المتزامن asynchronous، حيث يمكن إرسال كميات عشوائية من البيانات في أي اتجاه وفي أي وقت. ولكن في الواقع لا يشكل هذا الأمر عائقاً كبيراً من الناحية العملية، حيث أن SPI تستخدم عادة للتواصل مع المستشعرات sensors التي لها بنية أوامر محددة ومعروفة مسبقاً. على سبيل المثال، إذا أرسلت أمرًا لـ “قراءة البيانات” إلى جهاز ما، فستعلم أن الجهاز سيرد عليك دائمًا بـ 2 بايت من البيانات في المقابل. أما في الحالات التي قد ترغب فيها بإرجاع كمية متغيرة من البيانات، يمكنك دائمًا إرجاع بايت واحد تحدد فيه طول البيانات القادمة، وعلى ذلك يمكن للطرف الرئيسي إسترجاع كامل البيانات المرسله اليه.


أنماط الـ SPI

عندما يتصل جهازان معاً عن طريق الـ SPI فهنالك أمران مهمان يجب عليهما أن يتفقان عليه:

ويشار إلى هاتين الخاصيتين عادة باسم قطبية الساعة clock polarity وطور الساعة clock phase على التوالي.

تحدد قطبية الساعة clock polarity ما إذا كان وضع الساعة منخفض 0 أو مرتفع 1 عند عدم إرسال البيانات (الحالة الغير نشطة)؛ بمعنى هل الساعة غير نشطة عندما تكون مرتفعة أم منخفضة؟ إذا تم ضبط القطبية polarity على 0، فسيكون وضع الساعة منخفض 0 عندما تكون غير نشطة. وعندما تكون القطبية 1 سيكون وضع الساعة 1 عندما تكون غير نشطة.

ويشير طور الساعة clock phase إلى الحافة التي سيقوم فيها الجهاز بمعاينة البايت الأول من البيانات. ما إذا كان نقل البيانات على الحافة الصاعدة أو المتساقطة لإشارة الساعة. إذا كان الطور يساوي 0، فسيتم معاينة البيانات على الحافة الأولى من الساعة. أما إذا كان الطور يساوي 1، فسيتم معاينة البيانات على الحافة الثانية من الساعة.

هناك 4 أوضاع مختلفة في SPI وهي MODE0 و MODE1 و MODE2 و MODE3. وهي تعتمد على أنه يمكنك تحديد بالضبط كيف يتم بالضبط معاينة البت وذلك على أساس موضع إشارة الساعة. هناك جزءان من الساعة يجب أن تضعه في اعتبارك، قطبية الساعة CPOL وطور الساعة CPHA.

عندما يكون CPOL = 0 فذلك يعني أنه سيتم ضبط الساعة إلى مستوى منخفض وعندما تبدأ فعليًا بالإرسال فإن الإشارة ستبدأ من الوضع المنخفض ومن ثم مرتفعة ومن ثم منخفضة وهكذا. وعندما يكون CPOL = 1 فذلك يعني أنه سيتم ضبط الساعة فعليًا على الوضع المرتفع، وعندما نبدأ فعليًا في بالإرسال، فإن الإشارة ستنخفض ثم ترتفع ثم تنخفض وهكذا.

يحدد CPHA ما إذا كان سيتم معاينة الإشارة في التغيير الأول للساعة أو التغيير الثاني للساعة. لذلك عندما CPHA = 0 يعني أن البت يتم تحديده على أول تغيير للساعة. وعندما يكون CPHA = 1 فذلك يعني أنه سيتم معاينة البت على التغيير الثاني للساعة وذلك عندما تنتقل الإشارة من مرتفع إلى منخفض أو العكس.

باستخدام كل من القطبية polarity والطور phase، يمكننا تحديد 4 أنماط مختلفة لـ SPI (من 0 إلى 3) وفقًا لهذا الجدول:

MODE POLARITY - CPOL PHASE - CPHA
0 0 0
1 0 1
2 1 0
3 1 1
source: http://elm-chan.org/docs/spi_e.html

NOKIA 5110 LCD

لتجربة الـ SPI سنستفيد من شاشة Nokia 5110 LCD. سنقوم أولا بتوصيل الشاشة بالمتحكم ثم تهيئة وإعداد الـ SPI.

تم استخدام شاشة Nokia 5110 LCD في هواتف Nokia 5110 المحمولة منذ عدة سنوات. وتحتوي الشاشة على 48 صفًا × 84 عمودًا من وحدات البكسل باستهلاك منخفض للطاقة حيث أنها قادرة على العمل من 2.7 فولت إلى 5 فولت، لذا فهي مناسبة جدًا للاستخدام في التطبيقات المدمجة. ويمكنك الحصول على دليل البيانات على العنوان التالي:

Nokia 5110 Datasheet

سيتم توصيل المتحكم بالشاشة على النحو التالي:

الخط Nokia 5110 TM4C123
3.3V (VCC, pin 1) power
Ground (GND, pin 2) ground
SSI0Fss (SCE, pin 3) PA3
Reset (RST, pin 4) PA7
Data/Command (D/C, pin 5) PA6
SSI0Tx (DN, pin 6) PA5
SSI0Clk (SCLK, pin 7) PA2

وتكون بالطريقة التالية:


إرسال أوامر أو بيانات الى الشاشة

كما هو مذكور في دليل البيانات الخاص بالشاشة، قبل إرسال البيانات أو الأوامر إلى الشاشة، نحتاج إلى جعل الطرف CE منخفضًا وجعله مرتفعًا مرة أخرى بعد إرسال الأمر. أيضا، نحن بحاجة إلى جعل الطرف DC منخفض 0 للأوامر ومرتفع 1 للبيانات.

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

هناك مجموعتان من الأوامر:

  1. مجموعة الأوامر الموسعة Extended Command Set وهي تلك المستخدمة في تهيئة طريقة عمل الشاشة، مثل التباين، زاوية العرض، إلخ.
  2. مجموعة الأوامر الأساسية Basic Command Set وتستخدم لتعيين وضع العرض وموضع عرض البيانات.

تتبع عملية التهيئة وإرسال الأوامر التسلسل التالي:

Command Value Description
Extended Command 0x21 Set to EXTENDED command mode (Chip activated, horizontal addressing)
Vlcd Voltage 0xb2 Set Vlcd (Contrast) to 6v
Voltage Bias 0x13 Set voltage bias (viewing angle) to n=4, 1:48
Basic Command 0x20 Set to NORMAL command mode
All Pixels ON 0x09 Turn on all pixels
Display mode 0x0c Set display to Normal mode
Addressing Y 01000yyy Addressing row with yyy
Addressing X 1xxxxxxx Addressing columns with xxxxxxx

تهيئة الـ SPI

إذا فتحنا صفحة 965 في دليل البيانات، فإنه بإمكاننا العثور على الخطوات المطلوبة لتهيئة الـ SPI وهي:

1. قم بتمكين وحدة الـ SSI باستخدام سجل RCGCSSI (راجع صفحة 346).

لنتمكن من استخدام أي من الوحدات الطرفية في المتحكم، يجب علينا تمكين الساعة لها. نستخدم سجل RCGCSSI لتمكين الساعة للـ SSI.

#define SYSCTL_RCGCSSI_R   (*((volatile unsigned long *)0x400FE61C))

نقوم هنا بإسناد 1 الى حقل R0 (بت 0) لتمكين SSI0.

SYSCTL_RCGCSSI_R = (1<<0);

2. قم بتمكين ساعة وحدة الـ GPIO المناسبة عبر سجل RCGCGPIO (انظر صفحة 340). لمعرفة أي منفذ GPIO يجب عليك تمكينه، راجع الجدول 5-23 في الصفحة 1351.

#define SYSCTL_RCGCGPIO_R   (*((volatile unsigned long *)0x400FE608))

يمكننا أن نرى أن SPI 0 يستخدم المنفذ PORT A.

وبالعودة إلى سجل RCGCGPIO، يتضح أنه لتمكين PORT A علينا تعيين 1 إلى الحقل R0 (بت 0)

SYSCTL_RCGCGPIO_R = (1<<0);

3. ﻗﻢ ﺑﻀﺒﻂ بتات GPIOAFSEL ﻟلأطراف ﺍﻟﻤﻨﺎﺳﺒﺔ (ﺍﻧﻈﺮ ﺻﻔﺤﺔ ٦٧١). لتحديد أي GPIO يجب علينا ضبط إعداداته، راجع جدول 4-23 في الصفحة 1344.

وسبق وأن حددنا المنفذ الذي سنتعامل معه وهو PORT A.

#define GPIO_PORTA_AFSEL_R   (*((volatile unsigned long *)0x40004420))

لتمكين الوظائف البديلة لـلمنافذ PA2 و PA3 و PA5، نقوم بإسناد 1 الى البت الذي يقابلها في حقل AFSEL

السجل الطرف AFSEL
SSI0Fss PA3 1
Reset PA7 0
Data/Command PA6 0
SSI0Tx PA5 1
SSI0Clk PA2 1
GPIO_PORTA_AFSEL_R |= (1<<2)|(1<<3)|(1<<5); // enable alt funct
GPIO_PORTA_AFSEL_R &= ~((1<<6)|(1<<7)); // disable alt funct

إسناد القيمة 1 في سجل AFSEL يقوم بتمكين الوظيفة البديلة للطرف pin. الا أن هذا في حد ذاته لا يكفي حيث أن بعض الأطراف تدعم أكثر من وظيفة بديلة واحدة. وهنا ياتي دور سجل التحكم في المنفذ (PCTL) والذي نقوم فيه بتحديد بالضبط ماهي الوظيفة البديلة التي سيقوم بها المنفذ.

4. قم بضبط إعدادات حقول PMCn في سجل GPIOPCTL لتعيين إشارات SSI إلى الأطراف المناسبة (انظر صفحة 688 والجدول 5-23 في الصفحة 1351).

نستخدم السجل register التالي للمنفذ PORT A:

#define GPIO_PORTA_PCTL_R   (*((volatile unsigned long *)0x4000452C))

بمجرد تمكيننا للوظائف البديلة، يتعين علينا عندئذ اختيار الوظيفة البديلة المحددة التي نريدها. في الجدول 5-23 في صفحة 1351 في دليل البيانات يمكننا رؤية أن SSI0 يقع في العمود 2:

في هذا السجل كل 4 بت تمثل طرف، فالحقل PMC0 (بت0-3) يمثل الطرف 0، والحقل PMC1 (بت4-7) يمثل الطرف 1. وهكذا.

وكما ذكرنا سابقاً، بما أن SSI تقع في العمود رقم 2 فإننا نضع القيمة 2 في الحقول التي تمثل الأطراف التى نود تحويلها الى SPI:

//Configure PA2,3,5 as SPI
GPIO_PORTA_PCTL_R = (GPIO_PORTA_PCTL_R & 0xFF0F00FF)+0x00202200; 

5. قم بتمكين الوظيفة الرقمية للطرف في السجل GPIODEN. راﺟﻊ “General-Purpose Input/Outputs - GPIOs” ﻓﻲ اﻟﺼﻔﺤﺔ ٦٤٩ ﻟﻠﺤﺼﻮل ﻋﻠﻰ اﻟﻤﺰﻳﺪ ﻣﻦ اﻟﻤﻌﻠﻮﻣﺎت.

وبما أنها إشارات رقمية فإنه يجب علينا تمكين الوظائف الرقمية للأطراف:

#define GPIO_PORTA_DEN_R    (*((volatile unsigned long *)0x4000451C))

وهذا يكون كالتالي:

// enable digital I/O on PA2,3,5,6,7
GPIO_PORTA_DEN_R |= (1<<2)|(1<<3)|(1<<5)|(1<<6)|(1<<7);

ضبط إعدادات الـ SPI

يتم ذلك باستخدام الخطوات التالية:

1. تأكد من أن الحقل SSE (بت 1) في سجل SSICR1 يساوي 0 قبل إجراء أي تغييرات في الإعدادات

#define SSI0_CR1_R   (*((volatile unsigned long *)0x40008004))

نحن هنا نقوم فعلياً بإيقاف الـ SPI0 لضبط إعداداته ومن ثم تفعيله مرة أخرى. وذلك يكون على النحو التالي:

SSI0_CR1_R &= ~(1<<1);

2. حدد ما إذا كان SPI للمتحكم الذي تقوم ببرمجته هو لجهاز رئيسي master أم فرعي slave:

في برنامجنا هذا المتحكم هو الجهاز الرئيسي والشاشة فرعية ولذلك نقوم بإسناد القيمة 0x0000.0000 الى سجل SSICR1 في البرنامج الذي سنقوم بكتابته على المتحكم:

SSI0_CR1_R = 0x00;

3. ضبط مصدر ساعة SSI عن طريق الكتابة إلى سجل SSICC

#define SSI0_CC_R   (*((volatile unsigned long *)0x40008FC8))

الحقل CS (بت 3:0) في السجل SSICC يحدد مصدر الساعة المستخدم للـ SPI. سنقوم بإسناد القيمة 0x0 الى هذا السجل لتحديد الساعة الرئيسة كمصدر.

SSI0_CC_R = 0x00;

4. ضبط القاسم المخصص للساعة بالكتابة الى سجل SSICPSR.

#define SSI0_CPSR_R   (*((volatile unsigned long *)0x40008010))

هذا السجل يساعدنا في تحديد سرعة نقل البيانات bit rate. وهي بناءاً على المعادلة التالية:

Bit Rate = SysClk / (CPSDVSR * (1 + SCR))

حيث أن:

فلو كنا نريد أن تكون سرعة النقل 1 ميجابت في الثانية 1MBPS، فستكون المعادلة كما يلي:

16000000 / (16 x (1+0))

وبذلك تكون CPSDVSR = 0x10 و SCR = 0x0 مما يعطينا 1000000 كيلوبت/ثانية أو 1 ميجابت/ثانية.

5. أضبط سجل SSICR0 بالإعدادات التالية:

#define SSI0_CR0_R   (*((volatile unsigned long *)0x40008000))

حسب ما ذكرنا سابقاً، فإن القيمة التي سنضعها في هذا الحقل هي 0x0

SSI0_CR0_R &= ~(0x0000FF00);  // SCR = 0

سيتم إسناد القيمة 0 الى كليهما:

SSI0_CR0_R &= ~(0x00000040);  // SPO = 0 
SSI0_CR0_R &= ~(0x00000080);  // SPH = 0  

كما ذكرنا، سنختار بروتوكول Freescale SPI وذلك عن طريق إسناد 0x0 الى الحقل FRF

SSI0_CR0_R = (SSI0_CR0_R&~0x00000030)+0x00000000; // FRF = Freescale format

سنرسل 8 بت في كل مرة ولذلك نسند القيمة 0x7 الى هذا الحقل

SSI0_CR0_R = (SSI0_CR0_R&~0x0000000F)+0x00000007; // DSS = 8-bit data

6. سنتخطى هذه الخطوة

7. تمكين الـ SPI عن طريق إسناد 1 الى حقل SSE (بت 1) في سجل SSICR1

SSI0_CR1_R |= (1<<1)

إرسال وإستقبال البيانات

عند رغبتنا في تمكين القراءة أو الكتابة (الاتصال بشكل عام) في SPI فإنه يتم ضبط خط SS دائمًا الى الوضع المنخفض ويكون خط الساعة SCLK دائمًا ما يتحرك لأعلى ولأسفل لتتبع الوقت.

وعلى عكس الـ UART، فإنه لا يوجد بت للبدء أو للتوقف والتي تستخدم لتحديد بداية ونهاية قراءة البيانات وبذلك نكون قد وفرنا عدد البتات المرسلة ويكون جميعها مستخدم للبيانات فقط. أي أنه في نفس المدة الزمنية فإنه يمكن إرسال/إستقبال عدد أكبر من البيانات في SPI منها في الـ UART.

عملية النقل في SPI تشمل جميع البيانات التي يتم إرسالها بينما خط SS منخفض. وتتراوح طول كل حزمة بيانات data frame بين 4 و 16 بت وذلك حسب حجم البيانات الذي يتم تحديده في البرنامج. ويتم إرسال هذه الحزمة بدءاً بالبت الأعلى most significant bit.

تشير الأشكال السداسية أعلاه أنه يتم نقل بيانات على خط MOSI.

إرسال البيانات

عندما نريد نقل البيانات ، نقوم بتخزينها على SSIDR (سجل بيانات SSI).

#define SSI0_DR_R   (*((volatile unsigned long *)0x40008008))

نكتب على وجه التحديد إلى حقل Data والمكون من 16 بت (بت 16:0). وبما أننا نعمل مع بيانات من 8 بت، يجب علينا وضع البيانات في الـ 8 بتات الأولى من السجل وباقي السجل غير مستخدم.

قبل إرسال البيانات، نحتاج إلى التحقق من سجل SSISR للتأكد من أن الـ FIFO غير ممتلئ وجاهز لإرسال البايت التالي.

#define SSI0_SR_R   (*((volatile unsigned long *)0x4000800C))
void static lcd_write_data(uint8_t data){
  while((SSI0_SR_R&0x00000002)==0){}; // wait until transmit FIFO not full (TNF flag)
  DC = DC_DATA;
  SSI0_DR_R = data;                // data out
}

إستقبال البيانات

يُستخدم أيضًا SSIDR لإستقبال البيانات. وفي حالتنا، يحمل الجزء السفلي المكون من 8 بت البيانات المستلمة. وقبل قراءة البيانات، نتحقق مما إذا كان الـ FIFO ليس فارغًا وقد تلقى بيانات، ثم نقرأها من SSIDR.

في برنامجنا، لا نتلقى بيانات من الشاشة، ولكن لو كنا سنفعل ذلك فإننا سنقوم بشيء مشابه للتالي:

char c;
while((SSI0_SR_R&0x00000004)==0); // Wait until FIFO not empty (RNE flag)   
c = (char)(SSI0_DR_R  & 0xFF); // Read 8-bit data

البرنامج

فيما يخص التعامل مع الشاشة، تم الإستفادة من بعض الأكواد الموجودة في الصفحة التالية مع التعديل:

http://users.ece.utexas.edu/~valvano/arm/index.htm

// Parts of the code from:
// http://users.ece.utexas.edu/~valvano/arm/index.htm

#include <stdint.h>

/* ************************* Registers ************************ */

#define SYSCTL_RCGCSSI_R        (*((volatile unsigned long *)0x400FE61C))
#define SYSCTL_RCGCGPIO_R     	(*((volatile unsigned long *)0x400FE608))
#define GPIO_PORTA_AFSEL_R      (*((volatile unsigned long *)0x40004420))
#define GPIO_PORTA_DIR_R        (*((volatile unsigned long *)0x40004400))
#define GPIO_PORTA_DEN_R        (*((volatile unsigned long *)0x4000451C))
#define GPIO_PORTA_PCTL_R       (*((volatile unsigned long *)0x4000452C))
#define GPIO_PORTA_AMSEL_R      (*((volatile unsigned long *)0x40004528))
#define SSI0_CR1_R              (*((volatile unsigned long *)0x40008004))
#define SSI0_CC_R               (*((volatile unsigned long *)0x40008FC8))
#define SSI0_CPSR_R             (*((volatile unsigned long *)0x40008010))
#define SSI0_CR0_R              (*((volatile unsigned long *)0x40008000))
#define SSI0_SR_R               (*((volatile unsigned long *)0x4000800C))
#define SSI0_DR_R               (*((volatile unsigned long *)0x40008008))

#define DC                      (*((volatile unsigned long *)0x40004100))
#define DC_COMMAND              0
#define DC_DATA                 0x40

#define RESET                   (*((volatile unsigned long *)0x40004200))
#define RESET_LOW               0
#define RESET_HIGH              0x80

/* ************************* Variables ************************ */

// Maximum dimensions of the LCD
#define MAX_X                   84
#define MAX_Y                   48

// This table contains the hex values that represent pixels
// for a font that is 5 pixels wide and 8 pixels high
static const uint8_t ASCII[][5] = {
  {0x00, 0x00, 0x00, 0x00, 0x00} // 20
  ,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
  ,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
  ,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
  ,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
  ,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
  ,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
  ,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
  ,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
  ,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
  ,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
  ,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b +
  ,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
  ,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
  ,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
  ,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
  ,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
  ,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
  ,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
  ,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
  ,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
  ,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
  ,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
  ,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
  ,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
  ,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
  ,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
  ,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
  ,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
  ,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
  ,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
  ,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
  ,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
  ,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
  ,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
  ,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
  ,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
  ,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
  ,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
  ,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
  ,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
  ,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
  ,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
  ,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
  ,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
  ,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
  ,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
  ,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
  ,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
  ,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
  ,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
  ,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
  ,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
  ,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
  ,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
  ,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
  ,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
  ,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
  ,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
  ,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
  ,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c '\'
  ,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
  ,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
  ,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
  ,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
  ,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
  ,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
  ,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
  ,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
  ,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
  ,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
  ,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
  ,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
  ,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
  ,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j
  ,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
  ,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
  ,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
  ,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
  ,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
  ,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
  ,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
  ,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
  ,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
  ,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
  ,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
  ,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
  ,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
  ,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
  ,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
  ,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
  ,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
  ,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
  ,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
  ,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e ~
//  ,{0x78, 0x46, 0x41, 0x46, 0x78} // 7f DEL
  ,{0x1f, 0x24, 0x7c, 0x24, 0x1f} // 7f UT sign
};

/* ************************ Prototypes ************************ */

void static lcd_write_command(uint8_t data);
void static lcd_write_data(uint8_t data);
void spi0_init (void);
void nokia5110_init (void);
void nokia5110_out_char(char data);
void nokia5110_out_string(char *ptr);
void nokia5110_setcursor(uint8_t newX, uint8_t newY);
void nokia5110_clear(void);

/* *************************** main *************************** */

int main (void){
  spi0_init ();
  nokia5110_init ();
  
  nokia5110_out_string("Hello World!");
  
  return 0;
}

/* *************************** Functions *************************** */

// Helper function that sends an 8-bit command to the LCD
void static lcd_write_command (uint8_t data) {
  // wait until SSI0 not busy/transmit FIFO empty
  while ((SSI0_SR_R&0x00000010)==0x00000010){};
  DC = DC_COMMAND;
  SSI0_DR_R = data;  // command out
                     // wait until SSI0 not busy/transmit FIFO empty
  while ((SSI0_SR_R&0x00000010)==0x00000010){};
}

// Helper function that sends 8-bit data to the LCD
void static lcd_write_data(uint8_t data){
  while ((SSI0_SR_R&0x00000002)==0){}; // wait until transmit FIFO not full
  DC = DC_DATA;
  SSI0_DR_R = data;                    // data out
}

void spi0_init (void){
  volatile uint32_t delay;

  // 1. Enable the SSI module using the RCGCSSI register (see page 346).
  SYSCTL_RCGCSSI_R = (1<<0); // activate SSI0
  
  // 2. Enable the clock to the appropriate GPIO module via the RCGCGPIO register (see page 340).
  //    To find out which GPIO port to enable, refer to Table 23-5 on page 1351.
  SYSCTL_RCGCGPIO_R = (1<<0); // activate port A
  delay = SYSCTL_RCGCGPIO_R;  // allow time to finish activating

  // 3. Set the GPIO AFSEL bits for the appropriate pins (see page 671). To determine which GPIOs to
  //    configure, see Table 23-4 on page 1344.
  GPIO_PORTA_AFSEL_R |= (1<<2)|(1<<3)|(1<<5); // enable alt funct on PA2,3,5
  GPIO_PORTA_AFSEL_R &= ~((1<<6)|(1<<7));     // disable alt funct on PA6,7

  // 4. Configure the PMCn fields in the GPIOPCTL register to assign the SSI signals to the appropriate
  //    pins. See page 688 and Table 23-5 on page 1351.
                                        // configure PA2,3,5 as SSI
  GPIO_PORTA_PCTL_R = (GPIO_PORTA_PCTL_R & 0xFF0F00FF) + 0x00202200;
                                        // configure PA6,7 as GPIO
  GPIO_PORTA_PCTL_R = (GPIO_PORTA_PCTL_R & 0x00FFFFFF) + 0x00000000;
  
  // 5. Program the GPIODEN register to enable the pin's digital function. In addition, the drive strength,
  //    drain select and pull-up/pull-down functions must be configured. Refer to “General-Purpose
  //    Input/Outputs (GPIOs)” on page 649 for more information.
  GPIO_PORTA_DIR_R |= (1<<6)|(1<<7);                             // make PA6,7 out  
  GPIO_PORTA_DEN_R |= (1<<2)|(1<<3)|(1<<5)|(1<<6)|(1<<7);        // enable digital I/O on PA2,3,5,6,7
  GPIO_PORTA_AMSEL_R &= ~((1<<2)|(1<<3)|(1<<5)|(1<<6)|(1<<7)) ;  // disable analog functionality on PA2,3,5,6,7
  
  //
  //For each of the frame formats, the SSI is configured using the following steps:
  
  // 1. Ensure that the SSE bit in the SSICR1 register is clear before making any configuration changes.
  SSI0_CR1_R &= ~(1<<1); // disable SSI
  
  // 2. Select whether the SSI is a master or slave:
  //    a. For master operations, set the SSICR1 register to 0x0000.0000.
  //    b. For slave mode (output enabled), set the SSICR1 register to 0x0000.0004.
  //    c. For slave mode (output disabled), set the SSICR1 register to 0x0000.000C.
  SSI0_CR1_R = 0x00; // we choose (a) as the uC is the master

  // 3. Configure the SSI clock source by writing to the SSICC register.
  SSI0_CC_R = 0x0; // select system clock as the clock input to the SSI module.
  
  // 4. Configure the clock prescale divisor by writing the SSICPSR register. 
  // Bit Rate = SysClk / (CPSDVSR * (1 + SCR))
  //          = 16000000 / (16 x (1+0))
  //          = 1000000 KHz = 1 MHz
  // So, CPSDVSR = 16 decimal = 0x10
  //         SCR = 0 decimal
  SSI0_CPSR_R = 0x10; 

  // 5. Write the SSICR0 register with the following configuration:
  //    + Serial clock rate (SCR) (bits 8:15)
  //    + Desired clock phase/polarity, if using Freescale SPI mode (SPH (bit 6) and SPO (bit 7))
  //    + The protocol mode: Freescale SPI, TI SSF, MICROWIRE (FRF)
  //    + The data size (DSS)
  
  SSI0_CR0_R &= ~(0x0000FF00);  // SCR = 0 
  SSI0_CR0_R &= ~(0x00000040);  // SPO = 0 
  SSI0_CR0_R &= ~(0x00000080);  // SPH = 0                 
	
  SSI0_CR0_R = (SSI0_CR0_R&~0x00000030)+0x00000000; // FRF = Freescale format                                       
  SSI0_CR0_R = (SSI0_CR0_R&~0x0000000F)+0x00000007; // DSS = 8-bit data
  
  // 6. Optionally, configure the SSI module for ?DMA use with the following steps:
  //    a. Configure a ?DMA for SSI use. See “Micro Direct Memory Access (?DMA)” on page 585 for
  //       more information.
  //    b. Enable the SSI Module's TX FIFO or RX FIFO by setting the TXDMAE or RXDMAE bit in the
  //       SSIDMACTL register.

  // 7. Enable the SSI by setting the SSE bit in the SSICR1 register.
  SSI0_CR1_R |= (1<<1);
}

void nokia5110_init (void){
  volatile uint32_t delay;
  
  RESET = RESET_LOW;                     // reset the LCD to a known state
  for(delay=0; delay<10; delay=delay+1); // delay minimum 100 ns
  RESET = RESET_HIGH;                    // negative logic

  lcd_write_command(0x21);               // chip active; horizontal addressing mode (V = 0); use extended instruction set (H = 1)
                                         // set LCD Vop (contrast), which may require some tweaking:
  lcd_write_command(0xB1);               // 0xB1 (for 3.3V red SparkFun)
  lcd_write_command(0x04);               // set temp coefficient
  lcd_write_command(0x14);               // LCD bias mode 1:48: try 0x13 or 0x14

  lcd_write_command(0x20);               // we must send 0x20 before modifying the display control mode
  lcd_write_command(0x0C);               // set display control to normal mode: 0x0D for inverse
}

// Print a character to the Nokia 5110 48x84 LCD.  The
// character will be printed at the current cursor position,
// the cursor will automatically be updated, and it will
// wrap to the next row or back to the top if necessary.
// One blank column of pixels will be printed on either side
// of the character for readability.  Since characters are 8
// pixels tall and 5 pixels wide, 12 characters fit per row,
// and there are six rows.
void nokia5110_out_char(char data){
  int i;
  lcd_write_data(0x00);        // blank vertical line padding
  for(i=0; i<5; i=i+1){
    lcd_write_data(ASCII[data - 0x20][i]);
  }
  lcd_write_data(0x00);        // blank vertical line padding
}

// Print a string of characters to the Nokia 5110 48x84 LCD.
// The string will automatically wrap, so padding spaces may
// be needed to make the output look optimal.
void nokia5110_out_string(char *ptr){
  while(*ptr){
    nokia5110_out_char((unsigned char)*ptr);
    ptr = ptr + 1;
  }
}

// Move the cursor to the desired X- and Y-position.  The
// next character will be printed here.  X=0 is the leftmost
// column.  Y=0 is the top row.
void nokia5110_setcursor(uint8_t newX, uint8_t newY){
  if((newX > 11) || (newY > 5)){        // bad input
    return;                             // do nothing
  }
  // multiply newX by 7 because each character is 7 columns wide
  lcd_write_command(0x80|(newX*7));     // setting bit 7 updates X-position
  lcd_write_command(0x40|newY);         // setting bit 6 updates Y-position
}

// Clear the LCD by writing zeros to the entire screen and
// reset the cursor to (0,0) (top left corner of screen).
void nokia5110_clear(void){
  int i;
  for(i=0; i<(MAX_X*MAX_Y/8); i=i+1){
    lcd_write_data(0x00);
  }
  nokia5110_setcursor(0, 0);
}


مراجع

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