اشتراك ثابت باستخدام قالب Observer باستخدام C ++ و متحكم Cortex M4


صحة جيدة للجميع!


عشية السنة الجديدة ، أريد أن أستمر في الحديث عن استخدام 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 { //          HandleEvent() for(auto it: pSubscribers) { if (it != nullptr) { it->HandleEvent() ; } } } } ; 

كل الملح في طريقة 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 { //  ,    ,  Toggle() ; } }; 

التنفيذ الكامل لجميع الطبقات
 template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0) //   { while ((Port::IDR::Read() & (1 << pinNum)) == 0) //     { }; result = true; } return result; } } ; //     GPIOC.13 using UserButton = Button<GPIOC, 13> ; struct ISubscriber { virtual void HandleEvent() = 0; } ; struct IPublisher { virtual void Notify() const = 0; virtual void Subscribe(ISubscriber* subscriber) = 0; } ; template <typename Port, std::uint32_t pinNum> struct Led: ISubscriber { static void Toggle() { Port::ODR::Toggle(1 << pinNum); } void HandleEvent() override { Toggle() ; } }; struct ButtonController : IPublisher { void Run() { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override { for(auto it: pSubscribers) { if (it != nullptr) { it->HandleEvent() ; } } } void Subscribe(ISubscriber* subscriber) override { if (index < pSubscribers.size()) { pSubscribers[index] = subscriber ; index ++ ; } //   3   ...   } private: std::array<ISubscriber*, 3> pSubscribers ; std::size_t index = 0U ; } ; 

كيف يمكن للاشتراك أن يظهر في الكود؟ و هكذا:


 int main() { //  Led1    5  GPIOC static Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static Led<GPIOC,9> Led3 ; ButtonController buttonController ; //  3  buttonController.Subscribe(&Led1) ; buttonController.Subscribe(&Led2) ; buttonController.Subscribe(&Led3) ; //       buttonController.Run() ; } 

والخبر السار هو أننا يمكن أن توقع أي شيء ووقت إنشائه لا يهمنا. يمكن أن يكون كائن عالمي ، ثابت أو محلي. من ناحية ، هذا أمر جيد ، ولكن من ناحية أخرى ، لماذا نحتاج إلى الاشتراك في وقت التشغيل في هذا الرمز. في الواقع ، في الواقع ، هنا عنوان الكائنات Led1 ، Led2 ، Led3 معروف في مرحلة Led3 . إذن لماذا لا يمكنك الاشتراك في مرحلة الترجمة والاحتفاظ بمجموعة من المؤشرات للمشتركين في ROM؟


بالإضافة إلى ذلك ، هناك احتمال حدوث أخطاء محتملة ، على سبيل المثال ، كم تساءلت عما سيحدث عند استدعاء طريقة Subsribe() إذا تم استدعائها من عدة Subsribe() ؟ نحن محدودون بـ 3 مشتركين فقط ، وماذا يحدث إذا وقعنا على 4 مصابيح LED؟


في معظم الحالات ، نحتاج إلى هذا الاشتراك مرة واحدة في العمر أثناء التهيئة ، نحن فقط نوفر المؤشرات للمشتركين وهذا كل شيء. سيحتفظ المؤشر بعنوان هؤلاء المشتركين مدى الحياة. واليوم أمر لا مفر منه عندما يمكن تدميره بسبب اندلاع السوبرنوفا (بالطبع ، إذا اعتبرنا فترة زمنية طويلة إلى حد ما). ولكن على أي حال ، فإن احتمال فشل ذاكرة الوصول العشوائي أعلى بكثير من ذاكرة القراءة فقط ROM ولا يوصى بتخزين البيانات الدائمة في ذاكرة الوصول العشوائي.


حسنًا ، الأخبار السيئة هي أن مثل هذا الحل المعماري يأخذ مساحة كبيرة على ذاكرة الوصول العشوائي وذاكرة الوصول العشوائي. فقط في حالة نكتب عدد ROM وذاكرة الوصول العشوائي هذا الحل:


وحدةكود ريال عمانيريال عماني البياناتRW البيانات
main.o4886421

أي ما مجموعه 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) //   { while ((Port::IDR::Read() & (1 << pinNum)) == 0) //     { }; result = true; } return result; } } ; template <typename Port, std::uint32_t pinNum> struct Led: ISubscriber { constexpr Led() { } static void Toggle() { Port::ODR::Toggle(1<<pinNum); } void HandleEvent() const override { Toggle() ; } }; //     GPIOC.13 using UserButton = Button<GPIOC, 13> ; 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 ++ ; } } void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override { for(auto it: pSubscribers) { if (it != nullptr) { it->HandleEvent() ; } } } private: static constexpr std::size_t size = 3U; ISubscriber* pSubscribers[size] ; } ; 

الآن يمكن أن يتم الاشتراك في وقت الترجمة:


 int main() { //  Led1    5  GPIOC static constexpr Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static constexpr Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static constexpr Led<GPIOC,9> Led3 ; static constexpr ButtonController buttonController(&Led1, &Led2, &Led3) ; buttonController.Run() ; return 0 ; } ; 

هنا ، buttonController الكائن buttonController بالكامل في ROM مع مجموعة من المؤشرات للمشتركين:


main :: buttonController 0x800'1f04 0x10 Data main.o [1]

يبدو أن كل شيء لا يعد شيئًا ، إلا أننا نقتصر مرة أخرى على 3 مشتركين فقط. ويجب أن يكون لدى فئة الناشر مُنشئ constexpr وأن يكون بشكل عام ثابتًا تمامًا من أجل ضمان مؤشر للمشتركين في ROM ، وإلا ، حتى مع عناوين المشتركين المعروفة ، فإن كائننا ، إلى جانب جميع المحتويات ، سينتقل مرة أخرى إلى RAM.


من بين العيوب الأخرى - نظرًا لاستخدام الوظائف الافتراضية ، تقوم وظيفة الوظائف الافتراضية بتدوير ROM لدينا. والمورد هو ، على الرغم من بأسعار معقولة ، ولكن ليس لانهائي. في معظم التطبيقات ، من الممكن أن تدق عليها وتتخذ متحكمًا أكبر ، لكن غالبًا ما يحدث أن كل بايت مهم ، لا سيما عندما يتعلق الأمر بالمنتجات المصنعة بواسطة مئات الآلاف ، مثل المستشعرات الفيزيائية المادية.


دعونا نرى كيف هي الأمور مع الذاكرة في هذا الحل:


وحدةكود ريال عمانيريال عماني البياناتRW البيانات
main.o172760

وعلى الرغم من أن النتيجة "مذهلة": إجمالي استهلاك ذاكرة الوصول العشوائي هو 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() { //  Led1    5  GPIOC static Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static Led<GPIOC,9> Led3 ; //   ButtonController<Led1, Led2, Led3> buttonController ; buttonController.Run() ; return 0 ; } 

تتمثل مهمتنا في فصل الكائنين 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) //   { while ((Port::IDR::Read() & (1 << pinNum)) == 0) //     { }; result = true; } return result; } } ; //     GPIOC.13 using UserButton = Button<GPIOC, 13> ; template <typename Port, std::uint32_t pinNum> struct Led { static void Toggle() { Port::ODR::Toggle(1<<pinNum); } void HandleEvent() const { Toggle() ; } }; template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { ((subscribers.HandleEvent()), ...) ; } } ; int main() { //  Led1    5  GPIOC static constexpr Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static constexpr Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static constexpr Led<GPIOC,9> Led3 ; static constexpr ButtonController<Led1, Led2, Led3> buttonController ; buttonController.Run() ; return 0 ; } 

تنحرف استدعاء Notify() في الأسلوب Run() إلى استدعاء تسلسلي بسيط


 Led1.HandleEvent() ; Led2.HandleEvent() ; Led3.HandleEvent() ; 

ماذا عن الذاكرة هنا؟


وحدةكود ريال عمانيريال عماني البياناتRW البيانات
main.o18640

إجمالي ROM 190 بايت و 0 بايت من ذاكرة الوصول العشوائي. الآن الترتيب ، هو ما يقرب من 3 مرات أصغر من الإصدار القياسي ، في حين أنه يؤدي نفس الشيء بالضبط.


وبالتالي ، إذا كان لديك عناوين المشتركين المعروفة مسبقًا في التطبيق وتتبع الشروط المحددة في بداية المقالة


الشروط في بداية المقال
  • لا تستخدم تخصيص الذاكرة الديناميكية
  • تقليل العمل مع المؤشرات
  • نستخدم أكبر عدد ممكن من الثوابت بحيث لا يستطيع أحد تغيير أي شخص قدر الإمكان
  • ولكن في نفس الوقت نستخدم أقل عدد ممكن من الثوابت الموجودة في ذاكرة الوصول العشوائي

بكل ثقة ، يمكنك استخدام مثل هذا التطبيق لقالب الناشر-المشترك لتقليل أسطر الكود وتوفير الموارد ، وهناك ، يمكنك البحث وليس فقط بطاقة هدية ، ولكن أيضًا مكافأة بناءً على نتائج السنة.


مثال الاختبار تحت IAR 8.40.2 يكمن هنا


كل ذلك مع المجيء! ونتمنى لك التوفيق في العام الجديد!

Source: https://habr.com/ru/post/ar481606/


All Articles