← الجزء 4. برمجة الأجهزة الطرفية والتعامل مع المقاطعات
مكتبة مولد المجمع كود ل AVR Microcontrollers
الجزء 5. تصميم تطبيقات متعددة الخيوط
في الأجزاء السابقة من المقال ، قمنا بالتفصيل حول أساسيات البرمجة باستخدام المكتبة. في الجزء السابق ، تعرفنا على تنفيذ المقاطعات والقيود التي قد تنشأ عند العمل معهم. في هذا الجزء من المنشور ، سنناقش أحد الخيارات الممكنة لبرمجة العمليات الموازية باستخدام فئة Parallel . يتيح استخدام هذه الفئة تبسيط عملية إنشاء التطبيقات التي يجب أن تتم فيها معالجة البيانات في عدة تدفقات برنامج مستقلة.
جميع أنظمة تعدد المهام للأنظمة أحادية النواة متشابهة مع بعضها البعض. يتم تطبيق تعدد العمليات من خلال عمل المرسل ، الذي يخصص فترة زمنية لكل خيط ، وعندما ينتهي ، فإنه يتحكم ويمنح التحكم في الخيط التالي. يكمن الاختلاف بين التطبيقات المختلفة في التفاصيل فقط ، لذلك سنتناول بمزيد من التفصيل بشكل رئيسي الميزات المحددة لهذا التطبيق.
وحدة تنفيذ العملية في الخيط هي المهمة. يمكن أن يوجد عدد غير محدود من المهام في النظام ، ولكن في أي وقت معين لا يمكن تنشيط سوى عدد معين منها ، مقيدًا بعدد مهام سير العمل في المرسل. في هذا التطبيق ، يتم تحديد عدد مهام سير العمل في مُنشئ المدير ولا يمكن تغييره لاحقًا. في هذه العملية ، يمكن لمؤشرات الترابط القيام بالمهام أو البقاء مجانًا. بخلاف الحلول الأخرى ، لا يقوم Parallel Manager بتبديل المهام. لمهمة إعادة التحكم إلى المرسل ، يجب إدخال الأوامر المناسبة في الكود. وبالتالي ، فإن مسؤولية مدة الفاصل الزمني في المهمة تقع على عاتق المبرمج ، الذي يتعين عليه إدراج أوامر المقاطعة في أماكن معينة في الكود إذا كانت المهمة تستغرق وقتًا طويلاً ، وكذلك تحديد سلوك سلسلة الرسائل عند الانتهاء من المهمة. تتمثل ميزة هذا الأسلوب في أن المبرمج يتحكم في نقاط التبديل بين المهام ، مما يسمح لك بتحسين رمز الحفظ / الاستعادة بشكل كبير عند التبديل بين المهام ، وكذلك التخلص من معظم المشكلات المتعلقة بالوصول الآمن إلى البيانات.
للتحكم في تنفيذ المهام قيد التشغيل ، يتم استخدام فئة إشارة خاصة. الإشارة متغيرة قليلاً ، يتم استخدام الإعداد الخاص بها كإشارة تمكين لبدء مهمة في دفق. يمكن تعيين قيم الإشارة إما يدويًا أو بواسطة حدث مرتبط بهذه الإشارة.
تتم إعادة تعيين الإشارة عندما يتم تنشيط المهمة بواسطة المرسل أو يمكن تنفيذها برمجيًا.
يمكن أن تكون المهام في النظام في الحالات التالية:
غير نشط - الحالة الأولية لجميع المهام. المهمة لا تشغل التحكم في التدفق ولا يتم نقل التنفيذ. العودة إلى هذه الحالة للمهام تفعيلها يحدث عند الانتهاء من الأمر.
تم التنشيط - الحالة التي تقع فيها المهمة بعد التنشيط. تربط عملية التنشيط مهمة بخيط التنفيذ وإشارة التنشيط. يقوم المدير باستقصاء مؤشرات الترابط وبدء المهمة في حالة تنشيط إشارة المهمة.
محظور - عند تنشيط المهمة ، قد يتم بالفعل تعيين إشارة إليها كإشارة ، والتي يتم استخدامها بالفعل للتحكم في سلسلة رسائل أخرى. في هذه الحالة ، لتجنب الغموض في سلوك البرنامج ، تنتقل المهمة التي تم تنشيطها إلى الحالة المقفلة. في هذه الحالة ، تشغل المهمة مؤشر الترابط ، لكن لا يمكنها تلقي التحكم ، حتى إذا تم تنشيط الإشارة الخاصة بها. عند الانتهاء من المهام أو عند تغيير إشارة التنشيط ، يقوم المرسل بفحص حالة المهام في سلاسل الرسائل وتغييرها. إذا كانت مؤشرات الترابط قد حظرت المهام التي تتطابق الإشارة مع الإشارة التي تم إصدارها ، فسيتم تنشيط أول إشارة تم العثور عليها. إذا لزم الأمر ، يمكن للمبرمج قفل المهام وفتحها بشكل مستقل ، استنادًا إلى المنطق المطلوب للبرنامج.
الانتظار - الحالة التي تكون فيها المهمة بعد تنفيذ أمر التأخير . في هذه الحالة ، لا تتلقى المهمة التحكم حتى يتم انقضاء الفاصل الزمني المطلوب. في الفئة Parallel ، تُستخدم المقاطعات 16 بتنسيق WDT للتحكم في التأخير ، مما يسمح بعدم شغل أجهزة ضبط الوقت لاحتياجات النظام. في حالة احتياجك إلى مزيد من الاستقرار أو الدقة على فترات زمنية صغيرة ، بدلاً من " تأخير" ، يمكنك استخدام التنشيط بواسطة إشارات المؤقت. يجب ألا يغيب عن البال أن دقة التأخير ستظل منخفضة وستتقلب في نطاق "وقت استجابة المرسل" - "أقصى فترة زمنية للوقت في وقت استجابة النظام + المرسل" . بالنسبة للمهام ذات نطاقات زمنية محددة ، يجب عليك استخدام الوضع المختلط ، حيث يعمل المؤقت ، الذي لا يُستخدم في فئة Parallel ، بشكل مستقل عن تدفق المهام ويعالج الفواصل الزمنية في وضع المقاطعة الخالص.
كل مهمة تنفذ في موضوع هي عملية معزولة. يستلزم ذلك تحديد نوعين من البيانات: البيانات المحلية لتيار ما ، والتي يجب أن تكون مرئية وتغييرها فقط في إطار هذا التدفق ، والبيانات العالمية للتبادل بين التدفقات والوصول إلى الموارد المشتركة. في إطار هذا التطبيق ، يتم إنشاء البيانات العالمية بواسطة أوامر تم اعتبارها مسبقًا على مستوى الجهاز. لإنشاء متغيرات مهمة محلية ، يجب إنشاؤها باستخدام طرق من فئة المهمة. يكون سلوك متغير المهمة المحلي كما يلي: عند مقاطعة مهمة قبل نقل التحكم إلى المرسل ، يتم تخزين جميع متغيرات السجل المحلي في ذاكرة الدفق. عند إرجاع التحكم ، تتم استعادة متغيرات السجل المحلي قبل تنفيذ الأمر التالي.
فئة مع واجهة IHeap المرتبطة خاصية Heap للفئة Parallel مسؤولة عن تخزين البيانات المحلية من الدفق. إن أبسط تطبيق لهذه الفئة هو StaticHeap ، الذي ينفذ التخصيص الثابت لنفس كتل الذاكرة لكل خيط. في حالة وجود فرق كبير بين المهام وفقًا لمتطلبات كمية البيانات المحلية ، يمكنك استخدام DynamicHeap ، والذي يسمح لك بتحديد حجم الذاكرة المحلية بشكل فردي لكل مهمة. من الواضح أن النفقات العامة للعمل مع ذاكرة الدفق في هذه الحالة ستكون أعلى بكثير.
الآن ، دعونا نلقي نظرة فاحصة على بناء جملة الفصل باستخدام دفقين كمثال ، حيث يقوم كل منهما بتبديل إخراج منفذ منفصل بشكل مستقل.
var m = new Mega328 { FCLK = 16000000, CKDIV8 = false }; m.PortB.Direction(0x07); var bit1 = m.PortB[1]; var bit2 = m.PortB[2]; m.PortB.Activate(); var tasks = new Parallel(m, 2); tasks.Heap = new StaticHeap(tasks, 16); 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();
الخطوط العليا للبرنامج مألوفة لك بالفعل. في نفوسهم ، نحدد نوع وحدة التحكم ونخصص البتات الأولى والثانية للمنفذ B كمخرج. بعد ذلك ، يتم تهيئة متغير من فئة Parallel ، حيث نحدد في المعلمة الثانية الحد الأقصى لعدد سلاسل عمليات التنفيذ. في السطر التالي ، نخصص الذاكرة لاستيعاب التدفقات المتغيرة المحلية. لدينا مهام متساوية ، لذلك نستخدم StaticHeap . الكتلة التالية من التعليمات البرمجية هي تعريف المهمة. في ذلك ، نحدد مهمتين متطابقة تقريبا. الفرق الوحيد هو منفذ التحكم ومقدار التأخير. للعمل مع كائنات المهمة المحلية ، يتم تمرير مؤشر إلى tsk المهمة المحلية إلى كتلة رمز المهمة. نص المهمة في حد ذاته بسيط للغاية:
- يتم إنشاء تسمية محلية لتنظيم دورة التبديل لانهائية
- يتم عكس حالة المنفذ
- يتم إرجاع التحكم إلى المرسل ، والمهمة تنتقل إلى حالة الانتظار لعدد محدد من المللي ثانية
- يتم تعيين مؤشر الإرجاع على كتلة بداية الكتلة ويتم إرجاع التحكم إلى المرسل.
من الواضح ، في مثال ملموس ، يمكن استبدال الأمر الأخير بأمر عادي للانتقال إلى بداية الكتلة وإعطاءها في المثال فقط لغرض إظهارها. إذا رغبت في ذلك ، يمكن توسيع المثال بسهولة للتحكم في عدد كبير من الاستنتاجات ، عن طريق نسخ المهام وزيادة عدد مؤشرات الترابط.
قائمة كاملة من أوامر إحباط المهمة لنقل التحكم إلى المرسل هي كما يلي
AWAIT (إشارة) - يحفظ الدفق جميع المتغيرات في ذاكرة الدفق وينقل التحكم إلى المرسل. في المرة التالية التي يتم فيها تنشيط الدفق ، تتم استعادة المتغيرات ويستمر التنفيذ ، بدءًا من التعليمة التالية بعد AWAIT . تم تصميم الأمر لتقسيم المهمة إلى فترات زمنية ولتنفيذ آلة الحالة وفقًا لمخطط الإشارة → المعالجة 1 → الإشارة → المعالجة 2 ، إلخ.
قد يكون للأمر AWAIT إشارة كمعلمة اختيارية. إذا كانت المعلمة فارغة ، يتم حفظ إشارة التنشيط. إذا تم تحديده في المعلمة ، فسيتم إجراء جميع استدعاءات المهام اللاحقة عند تنشيط الإشارة المحددة ، وفقد الاتصال مع الإشارة السابقة.
TaskContinue (تسمية ، إشارة) - الأمر ينهي الدفق ويعطي السيطرة على المرسل دون حفظ المتغيرات. في المرة التالية التي يتم فيها تنشيط الدفق ، يتم نقل التحكم إلى ملصق التسمية . تسمح لك معلمة الإشارة الاختيارية بتجاوز إشارة تنشيط الدفق للمكالمة التالية. إذا لم يتم تحديد ذلك ، فإن الإشارة تظل كما هي. يمكن استخدام أمر بدون تحديد إشارة لتنظيم دورات داخل مهمة واحدة ، حيث يتم تنفيذ كل دورة في فترة زمنية منفصلة. يمكن استخدامه أيضًا لتعيين مهمة جديدة لمؤشر الترابط الحالي بعد إكمال المهمة السابقة. الاستفادة من هذا النهج مقارنة مع دورة تحرير سلسلة من المواضيع → تسليط الضوء على تيار هو برنامج أكثر كفاءة. يؤدي استخدام TaskContinue إلى إلغاء الحاجة إلى قيام المدير بالبحث عن مؤشر ترابط مجاني في التجمع ويضمن وجود أخطاء عند محاولة تخصيص سلاسل الرسائل في حالة عدم وجود سلاسل رسائل مجانية.
TaskEnd () - مسح الدفق بعد اكتمال المهمة. تنتهي المهمة ، ويتم تحرير مؤشر الترابط ، ويمكن استخدامه لتعيين مهمة جديدة باستخدام الأمر تنشيط .
Delay (ms) - الدفق ، كما في حالة استخدام AWAIT ، يحفظ جميع المتغيرات في ذاكرة الدفق وينقل التحكم إلى المرسل. في هذه الحالة ، يتم تسجيل قيمة التأخير بالمللي ثانية في رأس الدفق. في حلقة المرسل ، في حالة وجود قيمة غير صفرية في حقل التأخير ، لا يتم تنشيط التدفق. يتم تغيير القيم في حقل التأخير لجميع التدفقات عن طريق مقاطعة مؤقت WDT كل 16 مللي ثانية. عند الوصول إلى قيمة الصفر ، تتم إزالة حظر التنفيذ وتعيين إشارة تنشيط الدفق. يتم تخزين فقط قيمة البايت الواحد للتأخير في الرأس ، مما يعطي نطاقًا ضيقًا نسبيًا من التأخيرات المحتملة ، وبالتالي ، فإن تطبيق Delay () ينشئ حلقة داخلية باستخدام متغيرات الدفق المحلي.
يتم تنفيذ تنشيط الأوامر في المثال باستخدام أوامر ContinuousActivate و ActivateNext . هذا هو نوع خاص من تنشيط المهمة الأولية عند بدء التشغيل. في مرحلة التنشيط الأولية ، نحن نضمن عدم وجود خيط مشغول واحد ، وبالتالي فإن عملية التنشيط لا تتطلب إجراء بحث أولي عن خيط مجاني لمهمة وتتيح لك تنشيط المهام بالتسلسل. يقوم ContinuousActivate بتنشيط المهمة في مؤشر ترابط الصفر وإرجاع مؤشر إلى رأس مؤشر الترابط التالي ، وتستخدم الدالة ActivateNext هذا المؤشر لتنشيط المهام التالية في مؤشرات الترابط التسلسلية.
كإشارة تنشيط ، يستخدم المثال إشارة AlwaysOn . هذه هي واحدة من إشارات النظام. الغرض منه هو أن المهمة ستنفذ دائمًا ، لأن هذه هي الإشارة الوحيدة التي يتم تنشيطها دائمًا ولا تتم إعادة ضبطها عن طريق الاستخدام.
المثال ينتهي باستدعاء حلقة . تبدأ هذه الوظيفة دورة المرسل ، لذلك يجب أن يكون هذا الأمر هو الأخير في الكود.
النظر في مثال آخر حيث استخدام المكتبة يمكن أن تبسط بشكل كبير هيكل التعليمات البرمجية. فليكن جهاز تحكم مشروط يسجل إشارة تناظرية ويرسلها في شكل رمز HEX إلى الجهاز.
var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var cData = m.DREG(); var outDigit = m.ARRAY(4); var chex = Const.String("0123456789ABCDEF"); m.ADC.Clock = eADCPrescaler.S64; m.ADC.ADCReserved = 0x01; m.ADC.Source = eASource.ADC0; m.Usart.Baudrate = 9600; m.Usart.FrameFormat = eUartFrame.U8N1; var os = new Parallel(m, 4); os.Heap = new StaticHeap(os, 8); var ADS = os.AddSignal(m.ADC.Handler, () => m.ADC.Data(cData)); var trm = os.AddSignal(m.Usart.TXC_Handler); var starts = os.AddLocker(); os.PrepareSignals(); var t0 = os.CreateTask((tsk) => { m.LOOP(m.TempL, (r, l) => m.GO(l), (r, l) => { m.ADC.ConvertAsync(); tsk.Delay(500); }); }, "activate"); var t1 = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); var mref = m.ROMPTR(); mref.Load(chex); m.TempL.Load(cData.High); m.TempL >>= 4; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[0]); mref.Load(chex); m.TempL.Load(cData.High); m.TempL &= 0x0F; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[1]); mref.Load(chex); m.TempL.Load(cData.Low); m.TempL >>= 4; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[2]); mref.Load(chex); m.TempL.Load(cData.Low); m.TempL &= 0x0F; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[3]); starts.Set(); tsk.TaskContinue(loop); }); var t2 = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); trm.Clear(); m.TempL.Load('0'); m.Usart.Transmit(m.TempL); tsk.AWAIT(trm); m.TempL.Load('x'); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[0]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[1]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[2]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[3]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.Load(13); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.Load(10); m.Usart.Transmit(m.TempL); tsk.TaskContinue(loop, starts); }); var p = os.ContinuousActivate(os.AlwaysOn, t0); os.ActivateNext(p, ADS, t1); os.ActivateNext(p, starts, t2); m.ADC.Activate(); m.Usart.Activate(); m.EnableInterrupt(); os.Loop();
هذا لا يعني أننا رأينا الكثير من الأشياء الجديدة هنا ، ولكن يمكنك رؤية شيء مثير للاهتمام في هذا الرمز.
في هذا المثال ، يتم ذكر ADC (محول تمثيلي إلى رقمي) أولاً. تم تصميم هذا الجهاز المحيطي لتحويل جهد إشارة الدخل إلى رمز رقمي. يتم تشغيل دورة التحويل بواسطة وظيفة ConvertAsync ، والتي تبدأ العملية فقط دون انتظار النتيجة. عند اكتمال التحويل ، ينشئ ADC مقاطعة ينشط إشارة adcSig . انتبه لتعريف إشارة adcSig . بالإضافة إلى مؤشر المقاطعة ، فإنه يحتوي أيضًا على كتلة رمز لتخزين القيم من سجل بيانات ADC. يجب وضع كل الرموز التي يُفضل تنفيذها فور حدوث المقاطعة (على سبيل المثال ، قراءة البيانات من سجلات الجهاز) في هذا المكان.
تتمثل مهمة التحويل في تحويل رمز جهد ثنائي إلى تمثيل سداسي عشري مكون من أربعة أحرف للمحطة الشرطية الخاصة بنا. يمكن أن نلاحظ هنا استخدام الوظائف لوصف الأجزاء المكررة لتقليل حجم الكود المصدري واستخدام سلسلة ثابتة لتحويل البيانات.
تعد مشكلة الإرسال مثيرة للاهتمام من وجهة نظر تنفيذ الإخراج المنسق لسلسلة يتم فيها دمج إخراج البيانات الثابتة والديناميكية. لا يمكن اعتبار الآلية نفسها مثالية ؛ بل هي دليل على إمكانيات إدارة المعالجات. هنا يمكنك أيضًا الانتباه إلى إعادة تعريف إشارة التنشيط أثناء التنفيذ ، مما يؤدي إلى تغيير إشارة التنشيط من التحويل إلى TxS والعكس.
من أجل فهم أفضل ، نصف خوارزمية البرنامج بالكلمات.
في الحالة الأولية ، أطلقنا ثلاث مهام. اثنتان منها لديهما إشارات غير نشطة ، حيث يتم تنشيط إشارة مهمة التحويل (adcSig) في نهاية دورة القراءة للإشارة التناظرية ، ويتم تنشيط ConvS لمهمة الإرسال بواسطة رمز لم يتم تنفيذه بعد. نتيجة لذلك ، ستكون المهمة الأولى التي يتم إطلاقها بعد الإطلاق هي القياس. يبدأ رمز هذه المهمة في دورة تحويل ADC ، وبعدها تنتقل مهمة 500 مللي ثانية إلى دورة الانتظار. في نهاية دورة التحويل ، يتم تنشيط علامة adcSig ، والتي تقوم بتشغيل مهمة التحويل . في هذه المهمة ، يتم تنفيذ دورة لتحويل البيانات المستلمة إلى سلسلة. قبل إنهاء المهمة ، نقوم بتنشيط علامة ConvS ، مع توضيح أن لدينا بيانات جديدة لإرسالها إلى الجهاز. يقوم أمر الخروج بإعادة ضبط نقطة الإرجاع إلى بداية المهمة ويمنح السيطرة على المرسل. تسمح مجموعة علم ConvS بنقل التحكم إلى مهمة النقل . بعد إرسال البايت الأول من التسلسل ، تتغير إشارة التنشيط في المهمة إلى TxS . نتيجة لذلك ، بعد اكتمال نقل البايت ، سيتم استدعاء مهمة النقل مرة أخرى ، مما سيؤدي إلى نقل البايت التالي. بعد إرسال البايت الأخير من التسلسل ، تُرجع المهمة إشارة تنشيط ConvS وتعيد ضبط نقطة الإرجاع إلى بداية المهمة. اكتمال الدورة. ستبدأ الدورة التالية عندما تكمل مهمة القياس فترة الانتظار وتنشط دورة القياس التالية.
في جميع أنظمة تعدد المهام تقريبًا ، هناك مفهوم قوائم الانتظار للتفاعل بين مؤشرات الترابط. لقد توصلنا بالفعل إلى أنه نظرًا لأن التبديل بين المهام في هذا النظام هو عملية يتم التحكم فيها بالكامل ، فإن استخدام المتغيرات العالمية لتبادل البيانات بين المهام أمر ممكن تمامًا. ومع ذلك ، هناك عدد من المهام حيث يكون هناك ما يبرر استخدام قوائم الانتظار. لذلك ، لن نترك هذا الموضوع جانبا ونرى كيف يتم تنفيذه في المكتبة.
لتطبيق قائمة انتظار في برنامج ، من الأفضل استخدام فئة RingBuff . يقوم الفصل ، كما يوحي الاسم ، بتنفيذ مخزن مؤقت للرنين باستخدام أوامر الكتابة والجلب. يتم تنفيذ قراءة وكتابة البيانات بواسطة أوامر القراءة والكتابة . أوامر القراءة والكتابة ليس لها أي معلمات. يستخدم المخزن المؤقت متغير التسجيل المحدد في المُنشئ كمصدر / مستقبل للبيانات. يتم الوصول إلى هذا المتغير من خلال فئة المعلمة IOReg . يتم تحديد حالة المخزن المؤقت بواسطة علامتي Ovf و Empty ، مما يساعد على تحديد حالة الفائض أثناء الكتابة والفيضان أثناء القراءة. بالإضافة إلى ذلك ، يتمتع الفصل بالقدرة على تحديد الكود الذي يتم تشغيله على أحداث تجاوز السعة / تجاوز السعة. RingBuff لا يوجد لديه التبعيات على فئة Parallel ويمكن استخدامها بشكل منفصل. القيد عند العمل مع الفصل هو السعة المسموح بها ، والتي يجب أن تكون مضاعفة لقدرة اثنين (8.16.32 ، إلخ) لأسباب تحسين الكود.
ويرد أدناه مثال على العمل مع الفصل.
var m = new Mega328(); var io = m.REG();
يختتم هذا الجزء نظرة عامة على ميزات المكتبة. لسوء الحظ ، بقي عدد من الجوانب المتعلقة بقدرات المكتبة ، والتي لم يتم ذكرها حتى. في المستقبل ، في حالة الاهتمام بالمشروع ، يتم تخطيط المقالات المخصصة لحل مشاكل محددة باستخدام المكتبة ووصفًا أكثر تفصيلًا للمشكلات المعقدة التي تتطلب منشورًا منفصلاً.