
صحة جيدة للجميع!
عشية السنة الجديدة ، أريد أن أستمر في الحديث عن استخدام C ++ على المتحكمات الدقيقة ، هذه المرة سأحاول التحدث عن استخدام قالب Observer (ولكن فيما يلي سوف أسميها الناشر - المشترك أو المشترك فقط ، مثل التورية) ، وكذلك تنفيذ اشتراك ثابت في C ++ 17 ومزايا هذا النهج في بعض التطبيقات.
مقدمة
مشترك القالب هو أحد القوالب الأكثر شيوعًا المستخدمة في تطوير البرامج. مع ذلك ، على سبيل المثال ، يفعلون معالجة النقر على زر في نموذج Windows. وبالفعل ، في أي مكان تحتاج فيه إلى الاستجابة بطريقة ما للتغيرات في معلمات النظام ، سواء كانت تغييرات في الملفات أو تحديث القيمة المقاسة من المستشعر ، فقد حان الوقت بدون تفكير استخدم قالب المشترك.
ميزة القالب هي أننا نطلق العنان لمعرف الناشر والمشترك دون ربطنا بأشياء محددة. يمكننا توقيع أي شخص على أي شخص ، دون التأثير على تنفيذ كائنات الناشر والمشترك.
الظروف الأولية
قبل أن نتعرف على القالب ، دعنا نوافق أولاً على أننا نريد تطوير برامج موثوق بها:
- لا تستخدم تخصيص الذاكرة الديناميكية
- تقليل العمل مع المؤشرات
- نستخدم أكبر عدد ممكن من الثوابت بحيث لا يستطيع أحد تغيير أي شخص قدر الإمكان
- ولكن في نفس الوقت نستخدم أقل عدد ممكن من الثوابت الموجودة في ذاكرة الوصول العشوائي
الآن دعونا نلقي نظرة على التنفيذ القياسي لقالب المشترك.
التنفيذ القياسي
لنفترض أن لدينا زرًا ، وعندما تنقر على الزر ، نحتاج إلى وميض مصابيح LED ، ولكن كم منها لن يكون معروفًا حتى الآن ، وقد تحتاج في الواقع إلى عدم استخدام مصابيح LED ، ولكن مع إلقاء الضوء على السفينة لنقل الرسائل في رمز Morse. من المهم ألا نعرف من سيشترك. لسوء الحظ ، ليس لدي ضوء في متناول اليد ، لذلك جميع الأمثلة الواردة في المقالة من أجل البساطة وفهم أفضل مصنوعة من المصابيح.
لذلك ، عندما تضغط على الزر ، تحتاج إلى إخطار مؤشر LED حول هذه الصحافة. بدوره ، بعد معرفة الضغط على الصمام يجب أن ينتقل إلى الحالة المعاكسة.
التنفيذ القياسي في UML كما يلي ...

هنا فئة ButtonController
مسؤولة عن استقصاء الزر وإخطار المشتركين بالنقرة ، ويكون المشترك في هذه الحالة هو المشترك. يتم فصل هاتين الفئتين عبر ISubsriber
و IPublisher
ولا ISubsriber
أي ISubsriber
عن الآخر. وبالتالي ، يمكن لأي كائن ورث من واجهة ISubscriber
الاشتراك في حدث من ButtonController
.
منذ حظر تخصيص الذاكرة الديناميكية ، أعلنت مجموعة من 3 عناصر للاشتراك. أي يمكن أن يكون الحد الأقصى 3 مشتركين. لذلك ، في التقريب الأول ، قد تبدو طريقة إعلام المشتركين في فئة ButttonsController
struct ButtonController : IPublisher { void Run() { for(;;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override {
كل الملح في طريقة Notify()
لفئة Publisher
. في هذه الطريقة ، نذهب إلى قائمة المشتركين وندعو HandleEvent()
على كل منهم ، وهذا أمر رائع ، لأن كل مشترك يطبق هذه الطريقة بطريقته الخاصة ويمكنه القيام بها جميع مهما كانت رغبات قلبك (في الواقع ، عليك أن تكون حذراً ، وإلا فإن الشيطان يعرف ما يفعله المشترك هناك ، يمكنك استدعاء طريقته ، على سبيل المثال ، من انقطاع وتحتاج إلى توخي الحذر لمنع المشتركين من القيام بأشياء طويلة وسيئة)
في حالتنا ، يُسمح لمؤشر LED بعمل أي شيء ، لذلك يقوم بتبديل حالته:
template <typename Port, std::uint32_t pinNum> struct Led: ISubscriber { static void Toggle() { Port::ODR::Toggle(1 << pinNum); } void HandleEvent() override {
التنفيذ الكامل لجميع الطبقات template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0)
كيف يمكن للاشتراك أن يظهر في الكود؟ و هكذا:
int main() {
والخبر السار هو أننا يمكن أن توقع أي شيء ووقت إنشائه لا يهمنا. يمكن أن يكون كائن عالمي ، ثابت أو محلي. من ناحية ، هذا أمر جيد ، ولكن من ناحية أخرى ، لماذا نحتاج إلى الاشتراك في وقت التشغيل في هذا الرمز. في الواقع ، في الواقع ، هنا عنوان الكائنات Led1
، Led2
، Led3
معروف في مرحلة Led3
. إذن لماذا لا يمكنك الاشتراك في مرحلة الترجمة والاحتفاظ بمجموعة من المؤشرات للمشتركين في ROM؟
بالإضافة إلى ذلك ، هناك احتمال حدوث أخطاء محتملة ، على سبيل المثال ، كم تساءلت عما سيحدث عند استدعاء طريقة Subsribe()
إذا تم استدعائها من عدة Subsribe()
؟ نحن محدودون بـ 3 مشتركين فقط ، وماذا يحدث إذا وقعنا على 4 مصابيح LED؟
في معظم الحالات ، نحتاج إلى هذا الاشتراك مرة واحدة في العمر أثناء التهيئة ، نحن فقط نوفر المؤشرات للمشتركين وهذا كل شيء. سيحتفظ المؤشر بعنوان هؤلاء المشتركين مدى الحياة. واليوم أمر لا مفر منه عندما يمكن تدميره بسبب اندلاع السوبرنوفا (بالطبع ، إذا اعتبرنا فترة زمنية طويلة إلى حد ما). ولكن على أي حال ، فإن احتمال فشل ذاكرة الوصول العشوائي أعلى بكثير من ذاكرة القراءة فقط ROM ولا يوصى بتخزين البيانات الدائمة في ذاكرة الوصول العشوائي.
حسنًا ، الأخبار السيئة هي أن مثل هذا الحل المعماري يأخذ مساحة كبيرة على ذاكرة الوصول العشوائي وذاكرة الوصول العشوائي. فقط في حالة نكتب عدد ROM وذاكرة الوصول العشوائي هذا الحل:
أي ما مجموعه 552 بايت في ذاكرة الوصول العشوائي و 21 بايت في ذاكرة الوصول العشوائي - دعنا نقول ليس الكثير من أجل الضغط على زر وميض ثلاثة المصابيح.
حسنًا ، من أجل حماية نفسك من مثل هذه المشاكل وتقليل استهلاك موارد وحدة التحكم ، دعنا نفكر في الخيار من خلال اشتراك ثابت.
اشتراك ثابت
من أجل جعل الاشتراك ثابتًا ، يمكنك استخدام عدة طرق. سوف اسمهم مثل هذا:
- النهج التقليدي هو نفس النهج ، ولكن باستخدام مُنشئ constexpr وتحديد قائمة المشتركين من خلاله.
غير تقليدي باستخدام القوالب - نقل قائمة المشتركين من خلال معلمات القالب. (هنا ، القالب هو تعريف من مجال metaprogramming ، وليس أنماط التصميم)
النهج التقليدي للاشتراك ثابت
دعنا نحاول الاشتراك في مرحلة التجميع. للقيام بذلك ، نقوم بتعديل هيكلنا قليلاً:

لا تختلف الصورة كثيرًا عن الصورة الأصلية ، ولكن هناك عدة اختلافات: تمت إزالة طريقة Subscribe()
والآن سيتم تنفيذ الاشتراك مباشرة في المُنشئ. يجب على المُنشئ قبول عدد متغير من الوسائط ، ولكي يكون قادرًا على التوقيع بشكل ثابت في مرحلة التجميع ، سيكون constexpr
. سيتم تهيئة مجموعة من المشتركين فيه ويمكن إجراء هذا التهيئة في وقت الترجمة:
struct ButtonController : IPublisher { template<typename... Args> constexpr ButtonController(Args const*... args): pSubscribers() { std::initializer_list<ISubscriber const*> result = {args...} ; std::size_t index = 0U; for(auto it: result) { if (index < size) { pSubscribers[index] = const_cast<ISubscriber*>(it); } index ++ ; } } private: static constexpr std::size_t size = 3U; ISubscriber* pSubscribers[size] ; } ;
رمز كامل لمثل هذا التنفيذ struct ISubscriber { virtual void HandleEvent() const = 0; } ; struct IPublisher { virtual void Notify() const = 0; } ; template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0)
الآن يمكن أن يتم الاشتراك في وقت الترجمة:
int main() {
هنا ، buttonController
الكائن buttonController
بالكامل في ROM مع مجموعة من المؤشرات للمشتركين:
main :: buttonController 0x800'1f04 0x10 Data main.o [1]
يبدو أن كل شيء لا يعد شيئًا ، إلا أننا نقتصر مرة أخرى على 3 مشتركين فقط. ويجب أن يكون لدى فئة الناشر مُنشئ constexpr وأن يكون بشكل عام ثابتًا تمامًا من أجل ضمان مؤشر للمشتركين في ROM ، وإلا ، حتى مع عناوين المشتركين المعروفة ، فإن كائننا ، إلى جانب جميع المحتويات ، سينتقل مرة أخرى إلى RAM.
من بين العيوب الأخرى - نظرًا لاستخدام الوظائف الافتراضية ، تقوم وظيفة الوظائف الافتراضية بتدوير ROM لدينا. والمورد هو ، على الرغم من بأسعار معقولة ، ولكن ليس لانهائي. في معظم التطبيقات ، من الممكن أن تدق عليها وتتخذ متحكمًا أكبر ، لكن غالبًا ما يحدث أن كل بايت مهم ، لا سيما عندما يتعلق الأمر بالمنتجات المصنعة بواسطة مئات الآلاف ، مثل المستشعرات الفيزيائية المادية.
دعونا نرى كيف هي الأمور مع الذاكرة في هذا الحل:
وعلى الرغم من أن النتيجة "مذهلة": إجمالي استهلاك ذاكرة الوصول العشوائي هو 0 بايت ، و ROM هو 248 بايت ، أي نصف ما في الحل الأول ، إلا أنه يشعر أنه لا يزال هناك مجال للتحسين. من هذه 248 بايت ، ما يقرب من 50 تحتل فقط جداول الطريقة الافتراضية.
استطراد صغير:
تعتبر الخطوة في حجم ذاكرة الوصول العشوائي (RAM) التي تبلغ 256 كيلو بايت بالنسبة إلى المتحكمات الدقيقة الحديثة هي القاعدة (على سبيل المثال ، يحتوي متحكم TI Cortex M4 على 256 كيلو بايت من ذاكرة الوصول العشوائي ، والإصدار التالي هو بالفعل 512 كيلو بايت). ولن يكون الأمر جيدًا للغاية عندما نحتاج ، بسبب 50 بايتة إضافية ، إلى استخدام وحدة تحكم بسعة 256 كيلوبايت ROM أكبر وأكثر تكلفة ، وبالتالي ، يمكن أن يؤدي التخلي عن الوظائف الافتراضية إلى توفير ما يصل إلى 50 سنتًا (الفرق بين المتحكم الدقيق في 256 و 512 كيلوبايت ROM هو 50-60 سنتا).
هذا يبدو مثير للسخرية لواحد متحكم ، ولكن على مجموعة من 400000 جهاز في السنة ، يمكنك توفير 200000 دولار. بالفعل ليست مضحكة للغاية ، ولكن النظر في أي نوع من الفئران. يمكن منح العرض بشهادة وبطاقة هدية بقيمة 3000 روبل ، ولا يوجد أي شك حول صحة رفض الوظائف الافتراضية وتوفير 50 بايت إضافية في ذاكرة القراءة فقط.
نهج غير تقليدي
دعونا نرى كيف يمكنك أن تفعل الشيء نفسه دون وظائف افتراضية وحفظ المزيد من ROM.
أولاً ، لنكتشف كيف يمكن أن يكون:
int main() {
تتمثل مهمتنا في فصل الكائنين Publisher ( ButtonController
) والمشترك ( Led
) عن بعضهما البعض حتى لا يعرف أحدهما الآخر ، ولكن في الوقت نفسه ، قد يخطر ButtonController
Led
.
يمكنك ButtonController
فئة ButtonController
بطريقة ما.
template <Led<GPIOC,5>& subscriber1, Led<GPIOC,8>& subscriber2, Led<GPIOC,9>& subscriber3> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { subscriber1.HandleEvent() ; subscriber2.HandleEvent() ; subscriber3.HandleEvent() ; } ... } ;
لكنك تفهم ، نحن هنا BbuttonController
بأنواع محددة وسيتعين علينا إعادة تعريف فئة BbuttonController
كل مرة في مشروع جديد. وأود فقط أن تأخذ واستخدام ButtonController
في المشروع الجديد دون ButtonController
.
يأتي C ++ 17 إلى عملية الإنقاذ ، حيث لا يمكنك تحديد النوع ، ولكن اطلب من المحول البرمجي استنتاج النوع من أجلك - وهذا هو بالضبط ما تحتاجه. يمكننا ، كما في النهج التقليدي ، إطلاق العنان لمعرف الناشر والمشترك ، في حين أن عدد المشتركين غير محدود من الناحية العملية.
template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { pass((subscribers.HandleEvent() , true)...) ; } ... } ;
كيف تعمل وظيفة المرور (..)يحتوي الأسلوب Notify()
على استدعاء للدالة pass()
؛ ويتم استخدامه لتوسيع معلمات القالب بعدد متغير من الوسائط
void Notify() const { pass((subscribers.HandleEvent() , true)...) ; }
لا يمكن تصور تنفيذ وظيفة pass()
ببساطة ، إنها مجرد وظيفة تتطلب عددًا متغيرًا من الوسائط:
template<typename... Args> void pass(Args...) const { } } ;
كيف HandleEvent()
توسيع الدالة HandleEvent()
إلى عدة مكالمات لكل مشترك؟
نظرًا لأن دالة pass()
تأخذ عدة وسيطات من أي نوع ، يمكنك تمرير العديد من وسيطات type bool
، على سبيل المثال ، يمكنك استدعاء دالة pass(true, true, true)
. في هذه الحالة ، بالطبع ، لن يحدث شيء ، لكننا لسنا بحاجة.
يستخدم الخط (subscribers.HandleEvent() , true)
المشغل "،" (فاصلة) ، الذي ينفذ كلاً من المعاملين (من اليسار إلى اليمين) ويعيد قيمة المشغل الثاني ، أي subscribers.HandleEvent()
هنا. سيتم تنفيذ subscribers.HandleEvent()
أولاً ، ثم يتم تنفيذ الدالة سوف يتم pass()
إلى true
.
حسنًا ، "..." إدخال قياسي لتوسيع عدد متغير من الوسائط. بالنسبة لحالتنا ، يمكن وصف تصرفات المترجم بشكل تخطيطي للغاية كما يلي:
pass((subscribers.HandleEvent() , true)...) ; -> pass((Led1.HandleEvent() , true), (Led2.HandleEvent() , true), (Led3.HandleEvent() , true)) ; -> Led1.HandleEvent() ; -> pass(true, (Led2.HandleEvent() , true), (Led3.HandleEvent() , true)) ; -> Led2.HandleEvent() ; -> pass(true, true, (Led3.HandleEvent() , true)) ; -> Led3.HandleEvent() ; -> pass(true, true, true) ;
بدلاً من الروابط ، يمكنك استخدام المؤشرات:
template <auto* ... subscribers> struct ButtonController { ... } ;
إضافة: في الواقع ، بفضل vamireh الذي أشار إلى أن كل هذه الرقصات مع الدف لا حاجة pass
وظيفة في C ++ 17. نظرًا لأن المشغل "،" يتم دعم الفاصلة بتعبير أضعاف (التي تم تقديمها في معيار C ++ 17) ، يتم تبسيط الرمز أكثر:
template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { ((subscribers.HandleEvent()), ...) ; } } ;
من الناحية المعمارية ، يبدو الأمر بسيطًا بشكل عام:

لقد أضفت فئة أخرى من شاشات الكريستال السائل هنا ، ولكن على سبيل المثال بحتة ، لإظهار أنه لا يهم الآن نوع وعدد المشتركين ، والشيء الرئيسي هو أنه سيتم تطبيق الأسلوب HandleEvent()
.
وكل الكود بشكل عام أصبح أسهل الآن أيضًا:
template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0)
تنحرف استدعاء Notify()
في الأسلوب Run()
إلى استدعاء تسلسلي بسيط
Led1.HandleEvent() ; Led2.HandleEvent() ; Led3.HandleEvent() ;
ماذا عن الذاكرة هنا؟
إجمالي ROM 190 بايت و 0 بايت من ذاكرة الوصول العشوائي. الآن الترتيب ، هو ما يقرب من 3 مرات أصغر من الإصدار القياسي ، في حين أنه يؤدي نفس الشيء بالضبط.
وبالتالي ، إذا كان لديك عناوين المشتركين المعروفة مسبقًا في التطبيق وتتبع الشروط المحددة في بداية المقالة
الشروط في بداية المقال- لا تستخدم تخصيص الذاكرة الديناميكية
- تقليل العمل مع المؤشرات
- نستخدم أكبر عدد ممكن من الثوابت بحيث لا يستطيع أحد تغيير أي شخص قدر الإمكان
- ولكن في نفس الوقت نستخدم أقل عدد ممكن من الثوابت الموجودة في ذاكرة الوصول العشوائي
بكل ثقة ، يمكنك استخدام مثل هذا التطبيق لقالب الناشر-المشترك لتقليل أسطر الكود وتوفير الموارد ، وهناك ، يمكنك البحث وليس فقط بطاقة هدية ، ولكن أيضًا مكافأة بناءً على نتائج السنة.
مثال الاختبار تحت IAR 8.40.2 يكمن هنا
كل ذلك مع المجيء! ونتمنى لك التوفيق في العام الجديد!