الجزء 2. الابتداء →
مكتبة مولد المجمع كود ل AVR Microcontrollers
الجزء 1. التعارف الأول
عمت مساءً يا خبروفيتيس. أريد أن أوجه انتباهكم إلى المشروع التالي (من بين العديدين المتاحين) لبرمجة ميكروكنترولر الشعبية لسلسلة AVR.
قد يكون من الممكن إنفاق الكثير من النص لشرح سبب هذا الأمر ، ولكن بدلاً من ذلك ، انظر فقط إلى أمثلة عن كيفية اختلافه عن الحلول الأخرى. وكل التفسيرات والمقارنات مع أنظمة البرمجة الحالية ستكون ، حسب الضرورة ، في عملية تحليل الأمثلة. المكتبة قيد الإعداد الآن ، لذا قد لا يبدو تنفيذ بعض الوظائف هو الأمثل. أيضًا ، من المفترض أن تكون بعض المهام التي تم تعيينها للمبرمج في هذا الإصدار محسّنة أو تلقائية.
لذلك دعونا نبدأ. أريد أن أوضح على الفور أنه لا ينبغي اعتبار المواد المقدمة وصفًا كاملاً بأي حال من الأحوال ، ولكن فقط كدليل على بعض ميزات المكتبة المطورة من أجل المساعدة في فهم مدى اهتمام هذا النهج بالقراء.
لن ننحرف عن الممارسة السائدة ونبدأ بمثال كلاسيكي ، نوع من "Hello world" لوحدات التحكم الدقيقة. وهي ، نمض مؤشر LED المتصل بأحد أرجل المعالج. دعنا نفتح VisualStudio من Microsoft (أي إصدار سوف يفعل) وإنشاء تطبيق وحدة تحكم لـ C #. بالنسبة لأولئك الذين ليسوا على دراية ، فإن إصدار المجتمع الكافي للعمل مجاني تمامًا.
في الواقع النص نفسه كما يلي:
شفرة المصدر مثال 1using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT; m.PortB.Activate(); m.LOOP(m.TempL, (r, l) => m.GO(l), (r) => { m.PortB[0].Toggle();}); Console.WriteLine(AVRASM.Text(m)); } } }
بالطبع ، لكي تعمل كل شيء وتحتاج إلى المكتبة التي أمثلها.
بعد تجميع البرنامج وتشغيله ، في إخراج وحدة التحكم ، سنرى النتيجة التالية لهذا البرنامج.
تجميع نتيجة المثال 1 #include “common.inc” RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 L0000: in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL xjmp L0000 .DSEG
إذا قمت بنسخ النتيجة إلى أي بيئة تعرف كيفية العمل مع أداة تجميع AVR وتوصيل مكتبة الماكرو Common.inc ( مكتبة الماكرو هي أيضًا أحد مكونات نظام البرمجة المقدم وتعمل جنبًا إلى جنب مع NanoRTOSLib ) ، فيمكن تجميع هذا البرنامج والتحقق منه على محاكي أو شريحة حقيقية و تأكد من أن كل شيء يعمل.
النظر في شفرة المصدر للبرنامج في مزيد من التفاصيل. بادئ ذي بدء ، نخصص للمتغير m نوع البلورة المستخدمة. بعد ذلك ، اضبط وضع الإخراج الرقمي للصفر بت في المنفذ B الخاص بالبلور وقم بتنشيط المنفذ. يبدو السطر التالي غريبًا بعض الشيء ، لكن معناه بسيط للغاية. نقول فيه أننا نرغب في تنظيم حلقة لا نهائية ، وفيها نغير قيمة البتة الصفرية للمنفذ B إلى الجهة المقابلة. يصور السطر الأخير من البرنامج فعليًا نتيجة كل شيء تمت كتابته مسبقًا في شكل رمز المجمع. كل شيء بسيط للغاية وصغير الحجم. والنتيجة هي عمليا لا يختلف عن ما يمكن للمرء أن يكتب في المجمع. هناك سؤالان فقط إلى رمز الإخراج: الأول - لماذا تهيئة الحزمة إذا ما زلنا لا نستخدمها ، وما نوع xjmp ؟ الإجابة على السؤال الأول وفي الوقت نفسه شرح لماذا يتم عرض المجمّع ، بدلاً من HEX النهائي ، هو ما يلي: النتيجة في شكل المجمّع تسمح لك بمزيد من التحليل وتحسين البرنامج ، مما يسمح للمبرمج بتحديد وتعديل أجزاء الكود التي لا يحبها. وتُركت تهيئة الحزمة على الأقل لتلك الأسباب التي من خلالها يمكنك استخدام العديد من البرامج دون استخدام الحزمة. ومع ذلك ، إذا كنت لا تحب ذلك ، فلا تتردد في تنظيفه. الإخراج إلى المجمّع لهذا الغرض المقصود. بالنسبة إلى xjmp ، هذا مثال على استخدام وحدات الماكرو لزيادة قابلية قراءة مجمّع الإخراج. على وجه التحديد ، يعد xjmp بديلاً عن jmp و rjmp مع الاستبدال الصحيح وفقًا لطول الفترة الانتقالية.
إذا قمت بملء البرنامج بشريحة ، فلن نرى بالطبع وميض الديود ، على الرغم من حقيقة أن حالة الدبوس تتغير. يحدث فقط بسرعة كبيرة من أجل رؤيته من خلال العيون. لذلك ، فإننا نعتبر البرنامج التالي ، الذي نستمر فيه بالوميض مع الصمام الثنائي ، ولكن حتى يمكن رؤيته. على سبيل المثال ، تأخير 0.5 ثانية مناسب تمامًا: ليس سريعًا جدًا وليس بطيئًا جدًا. سيكون من الممكن جعل العديد من الحلقات المتداخلة مع NOPs تشكل تأخيرًا ، لكننا سنتخطى هذه الخطوة لعدم إضافة أي شيء إلى وصف إمكانيات المكتبة والاستفادة فورًا من فرصة استخدام الأجهزة المتوفرة. نغير طلبنا على النحو التالي.
شفرة المصدر مثال 2 using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT; m.PortB.Activate(); m.WDT.Clock = eWDTClock.WDT500ms; m.WDT.OnTimeout = () => m.PortB[0].Toggle(); m.WDT.Activate(); m.EnableInterrupt(); var loop = AVRASM.newLabel(); m.GO(loop); Console.WriteLine(AVRASM.Text(m)); } } }
من الواضح أن البرنامج يشبه البرنامج السابق ، لذلك سننظر فقط في ما تغير. أولاً ، في هذا المثال ، استخدمنا WDT (مؤقت الوكالة الدولية للطاقة). للعمل مع تأخيرات كبيرة لا تتطلب دقة قصوى ، هذا هو الخيار الأفضل. كل ما هو مطلوب لاستخدامه هو تعيين التردد المطلوب عن طريق تعيين الفاصل خلال خاصية WDT.Clock وتحديد الإجراءات التي يجب تنفيذها في وقت بدء الحدث ، عن طريق تحديد الكود من خلال خاصية WDT.OnTimeout. نظرًا لأننا نحتاج إلى المقاطعات للعمل ، فيجب تمكينها باستخدام الأمر EnableInterrupt. ولكن يمكن استبدال الدورة الرئيسية بواسطة دمية. في ذلك ، ما زلنا لا نخطط لفعل أي شيء. لذلك ، سوف نعلن ونضع علامة وننقل انتقالًا غير مشروط إليها لتنظيم دورة فارغة. إذا كنت تحب LOOP أكثر - من فضلك. نتيجة هذا لن تتغير.
حسنًا ، في النهاية ، لنلقي نظرة على الكود الناتج.
تجميع نتيجة المثال 2 #include “common.inc” jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop jmp WDT ;Watchdog Timer Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 ldi TempL, (1<<WDCE) | (1<<WDE) sts WDTCSR,TempL ldi TempL, 0x42 sts WDTCSR,TempL sei L0000: xjmp L0000 WDT: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .DSEG
سيكون لدى أولئك المطلعين على هذا المعالج بلا شك سؤال عن أماكن نواقل المقاطعة المتعددة. استخدمنا هنا المنطق التالي - إذا لم يتم استخدام الكود - ليست هناك حاجة إلى الكود. لذلك ، ينتهي جدول المقاطعة على المتجه الأخير المستخدم.
على الرغم من حقيقة أن البرنامج يتعامل مع المهمة بشكل مثالي ، إلا أن الأكثر انتقائية قد لا يحب حقيقة أن مجموعة التأخير المحتمل محدودة ، والخطوة صعبة للغاية. لذلك ، سننظر في طريقة أخرى ، وفي الوقت نفسه ، سنرى كيف يتم تنظيم العمل مع المؤقتات في المكتبة. في Crystal Mega328 ، التي يتم أخذها كعينة ، هناك ما يصل إلى 3 منهم. 2 8 بت و 16 بت واحد. لقد حاول المهندسون المعماريون جاهدين استثمار أكبر عدد ممكن من الميزات في هذه المؤقتات ، وبالتالي فإن إعداداتهم ضخمة للغاية.
أولاً ، نحسب العداد الذي يجب استخدامه لتأخيرنا البالغ 0.5 ثانية. إذا أخذنا تردد الساعة الكريستالي البالغ 16 ميجاهرتز ، حتى مع الحد الأقصى للمقسم المحيطي ، فمن المستحيل الاحتفاظ به في عداد 8 بت. لذلك ، لن نقوم بتعقيد واستخدام عداد Timer1 16 بت الوحيد المتاح لنا.
نتيجة لذلك ، يأخذ البرنامج الشكل التالي:
شفرة المصدر مثال 3 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) {var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var bit1 = m.PortB[0]; bit1.Mode = ePinMode.OUT; m.PortB.Activate(); m.Timer1.Mode = eWaveFormMode.CTC_OCRA; m.Timer1.Clock = eTimerClockSource.CLK256; m.Timer1.OCRA = (ushort)((0.5 * m.FCLK) / 256); m.Timer1.OnCompareA = () => bit1.Toggle(); m.Timer1.Activate(); m.EnableInterrupt(); m.LOOP(m.TempH, (r, l) => m.GO(l), (r) => { }); Console.WriteLine(AVRASM.Text(m)); } } }
نظرًا لأننا نستخدم المولد الرئيسي كمصدر لساعة توقيتنا ، ولحساب التأخير الصحيح ، يجب عليك تحديد تردد ساعة المعالج وإعداد المقسم وصهر الساعة المحيطي. النص الرئيسي للبرنامج هو ضبط المؤقت على الوضع المطلوب. هنا ، يتم اختيار متعمد من 256 وليس كحد أقصى عن عمد لتسجيل الوقت ، لأنه عند تحديد فاصل من 1024 لتردد الساعة المطلوب من 500ms ، الذي نريد الحصول عليه ، يتم الحصول على عدد كسري.
سيبدو رمز المجمّع الناتج في برنامجنا كما يلي:
تجميع نتيجة المثال 3 #include “common.inc” jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop reti ;Watchdog Timer Handler nop reti ;Timer2 Compare A Handler nop reti ;Timer2 Compare B Handler nop reti ;Timer2 Overflow Handler nop reti ;Timer1 Capture Handler nop jmp TIM1_COMPA ;Timer1 Compare A Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 outiw OCR1A,0x7A12 outi TCCR1A,0 outi TCCR1B,0xC outi TCCR1C,0x0 outi TIMSK1,0x2 outi DDRB,0x1 sei L0000: xjmp L0000 TIM1_COMPA: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .DSEG
يبدو أنه لا يوجد شيء للتعليق عليه. نحن تهيئة الأجهزة ، وتكوين المقاطعات والتمتع البرنامج.
العمل عبر المقاطعات هو أسهل طريقة لإنشاء برامج للعمل في الوقت الفعلي. لسوء الحظ ، لا يمكن دائمًا التبديل بين المهام المتوازية باستخدام معالجات المقاطعة فقط لتنفيذ هذه المهام. القيد هو الحظر المفروض على معالجة المقاطعة المتداخلة ، الأمر الذي يؤدي إلى حقيقة أنه حتى خروج المعالج ، لا يستجيب المعالج لجميع المقاطعات الأخرى ، مما قد يؤدي إلى فقد الأحداث إذا كان المعالج يعمل لفترة طويلة جدًا.
الحل هو فصل رمز تسجيل الحدث ومعالجته. يتم تنظيم أساس المعالجة المتوازية المتعددة الخيوط من المكتبة بطريقة أنه عند حدوث حدث ، يسجل معالج المقاطعة الحدث المحدد فقط ، وإذا لزم الأمر ، ينفذ الحد الأدنى من عمليات التقاط البيانات اللازمة ، ويتم تنفيذ جميع العمليات في الدفق الرئيسي. يتحقق النواة بشكل متتابع من وجود إشارات غير معالجة ، وإذا تم العثور عليه ، يتابع المهمة المقابلة.
يؤدي استخدام هذا النهج إلى تبسيط تصميم الأنظمة بعدة مهام غير متزامنة ، مما يتيح لك التفكير في كل واحدة منها بمعزل عن غيرها ، دون التركيز على مشكلات تبديل الموارد بين المهام. على سبيل المثال ، ضع في اعتبارك تنفيذ مهمتين مستقلتين ، كل واحدة منها تقوم بتحويل ناتجها مع تأخير معين.
شفرة المصدر مثال 4 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; m.PortB.Direction(0x07); var bit1 = m.PortB[1]; var bit2 = m.PortB[2]; m.PortB.Activate(); var tasks = new Parallel(m, 4); tasks.Heap = new StaticHeap(tasks, 64); var t1 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit1.Toggle(); tsk.Delay(32); tsk.TaskContinue(loop); },"Task1"); var t2 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit2.Toggle(); tsk.Delay(48); tsk.TaskContinue(loop); }, "Task2"); var ca = tasks.ContinuousActivate(tasks.AlwaysOn, t1); tasks.ActivateNext(ca, tasks.AlwaysOn, t2); ca.Dispose(); m.EnableInterrupt(); tasks.Loop(); Console.WriteLine(AVRASM.Text(m)); } } }
في هذه المهمة ، نقوم بتكوين المخرجات الصفرية والأولى للمنفذ B لإخراج وتغيير القيمة من 0 إلى 1 والعكس بالعكس مع فترة من 32ms للصفر و 48ms للإخراج الأول. مهمة منفصلة هي المسؤولة عن إدارة كل منفذ. أول شيء يجب ملاحظته هو تعريف مثيل Parallel. هذه الفئة هي جوهر إدارة المهام. في مُنشئها ، نحدد الحد الأقصى المسموح به لعدد سلاسل العمليات قيد التشغيل في وقت واحد. فيما يلي تخصيص ذاكرة لتخزين تدفقات البيانات. فئة StaticHeap المستخدمة في المثال يخصص عدد ثابت من وحدات البايت لكل دفق. لحل مشكلتنا ، هذا أمر مقبول ، واستخدام تخصيص ذاكرة ثابتة مقارنة بالديناميكية يبسط الخوارزميات ويجعل الرمز أكثر إحكاما وأسرع. علاوة على ذلك في الكود ، نحن نصف مجموعة من المهام التي صممت لتعمل تحت سيطرة النواة. يجب الانتباه إلى تأخير وظيفة غير متزامن ، والتي نستخدمها لتشكيل تأخير. خصوصيتها هي أنه عندما يتم استدعاء هذه الوظيفة ، يتم تعيين التأخير المطلوب في إعدادات الدفق ، ويتم نقل التحكم إلى النواة. بعد انقضاء الفاصل الزمني المحدد ، تقوم kernel بإرجاع التحكم إلى المهمة من الأمر الذي يتبع الأمر Delay. ميزة أخرى للمهمة هي برمجة سلوك تدفق المهمة عند الانتهاء في أمر المهمة الأخير. في حالتنا ، يتم تكوين كلتا المهمتين ليتم تنفيذها في حلقة لا نهائية مع التحكم في العودة إلى النواة في نهاية كل دورة. إذا لزم الأمر ، يمكن لإكمال المهمة تحرير الخيط أو تمريره لأداء مهمة أخرى.
سبب استدعاء المهمة هو تنشيط الإشارة المخصصة لتدفق المهمة. يمكن تنشيط الإشارة برمجياً والأجهزة بواسطة المقاطعات من الأجهزة الطرفية. استدعاء مهمة إعادة تعيين الإشارة. استثناء هو إشارة AlwaysOn المحددة مسبقًا ، والتي تكون دائمًا في الحالة النشطة. يتيح ذلك إنشاء المهام التي ستتلقى التحكم في كل دورة اقتراع. وظيفة LOOP مطلوبة لاستدعاء حلقة التنفيذ الرئيسية. لسوء الحظ ، أصبح حجم رمز الإخراج عند استخدام Parallel أكبر بالفعل مما كان عليه في الأمثلة السابقة (حوالي 600 أمر) ولا يمكن ذكره بشكل كامل في المقالة.
وللحلاوة - شيء أشبه بالمشروع الحي ، وهو مقياس الحرارة الرقمي. كل شيء بسيط كما هو الحال دائما. مستشعر رقمي مزود بواجهة SPI ومؤشر مكون من 4 أجزاء مكون من 7 أجزاء والعديد من مؤشرات ترابط المعالجة للحفاظ على برودة الأشياء. في أحدهما ، نقود دورة للإشارة الديناميكية ، في أحداث أخرى ، التي تبدأ بها دورة قراءة درجة الحرارة ، في الثالث نقرأ القيم المستلمة من المستشعر ونحولها من كود ثنائي إلى BCD ثم إلى رمز مقطع لمخزن مؤقت للإشارة الديناميكي.
البرنامج نفسه على النحو التالي.
شفرة المصدر مثال 5 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var led7s = new Led_7(); led7s.SegPort = m.PortC; led7s.Activate(); m.PortD.Direction(0xFF); m.PortD.Activate(); m.PortB[0].Mode = ePinMode.OUT; var tc77 = new TC77(); tc77.CS = m.PortB[0]; tc77.Port = m.SPI; m.Timer0.Clock = eTimerClockSource.CLK64; m.Timer0.Mode = eWaveFormMode.Normal; var reader = m.DREG("Temperature"); var bcdRes = m.DREG("digits"); var tmp = m.BYTE(); var bcd = new BCD(reader, bcdRes); m.subroutines.Add(bcd); var os = new Parallel(m, 4); os.Heap = new StaticHeap(os, 64); var tmrSig = os.AddSignal(m.Timer0.OVF_Handler); var spiSig = os.AddSignal(m.SPI.Handler, () => { m.SPI.Read(m.TempL); m.TempL.MStore(tmp); }); var actuator = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureAsync(); tsk.Delay(16); tsk.TaskContinue(loop); }, "actuator"); var treader = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureCallback(os, reader, tmp); reader >>= 7; m.CALL(bcd); tsk.TaskContinue(loop); }, "reader"); var display = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); m.PortD.Write(0xFE); m.TempQL.Load(bcdRes.Low); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFD); m.TempQL.Load(bcdRes.Low); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFB); m.TempQL.Load(bcdRes.High); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xF7); m.TempQL.Load(bcdRes.High); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); tsk.TaskContinue(loop); }, "display"); var ct = os.ContinuousActivate(os.AlwaysOn, actuator); os.ActivateNext(ct, spiSig, treader); os.ActivateNext(ct, tmrSig, display); tc77.Activate(); m.Timer0.Activate(); m.EnableInterrupt(); os.Loop(); Console.WriteLine(AVRASM.Text(m)); } } }
من الواضح أن هذا ليس مشروعًا عمليًا ، بل مجرد عرض تكنولوجي مصمم لإظهار قدرات مكتبة NanoRTOS. ولكن على أي حال ، فإن أقل من 100 سطر من مصدر وأقل من 1kb من شفرة الإخراج هي نتيجة جيدة لتطبيق عملي.
في المقالات التالية ، أخطط ، في حالة الاهتمام بهذا المشروع ، للتوسع في مزيد من التفاصيل حول مبادئ وميزات البرمجة باستخدام هذه المكتبة.