
في مقال سابق ،
حيث يتم تخزين الثوابت الخاصة بك على متحكم CortexM (باستخدام برنامج التحويل البرمجي C ++ IAR كمثال) ، تمت مناقشة مسألة كيفية وضع كائنات ثابتة في ROM. الآن ، أريد أن أخبرك كيف يمكنك استخدام نمط المولد الوحيد لإنشاء كائنات في ROM.
مقدمة
لقد كتب الكثير بالفعل عن سينغلتون (المشار إليها فيما يلي باسم سينغلتون) جوانبها الإيجابية والسلبية. ولكن على الرغم من أوجه القصور فيه ، فإنه يحتوي على الكثير من الخصائص المفيدة ، لا سيما في سياق البرامج الثابتة لأجهزة ميكروكنترولر.
بادئ ذي بدء ، لبرنامج متحكم موثوق ، لا ينصح بإنشاء الكائنات ديناميكيًا ، وبالتالي ليست هناك حاجة لحذفها. غالبًا ما يتم إنشاء الكائنات مرة واحدة وتعيش من لحظة بدء تشغيل الجهاز ، حتى يتم إيقاف تشغيله. قد يكون مثل هذا الكائن بمثابة منفذ ، حيث يتم توصيل مؤشر LED ، يتم إنشاؤه مرة واحدة ، وبالتأكيد لن يذهب إلى أي مكان أثناء تشغيل التطبيق ، ومن الواضح أنه يمكن أن يكون Singleton. شخص ما يجب أن يخلق مثل هذه الأشياء ويمكن أن يكون سينجلتون.
سوف يوفر لك Singleton أيضًا ضمان عدم إنشاء الكائن نفسه الذي يصف أرجل المنفذ مرتين إذا تم استخدامه فجأة في عدة أماكن.
آخر ، في رأيي ، خاصية رائعة لشركة Singleton هي سهولة استخدامها. على سبيل المثال ، كما في حالة معالج المقاطعة ، يوجد مثال في نهاية المقالة. لكن الآن ، سنتعامل مع سينجلتون نفسه.
سينجلتون خلق الكائنات في ذاكرة الوصول العشوائي
بشكل عام ، لقد تم بالفعل كتابة الكثير من المقالات عنها ،
Singleton (Loner) أو فصل ثابت؟ ، أو
ثلاثة من العمر من نمط Singleton . لذلك ، لن أركز على ماهية Singleton وأصف كل الخيارات العديدة لتنفيذه. بدلاً من ذلك ، سأركز على خيارين يمكن استخدامهما في البرامج الثابتة.
بادئ ذي بدء ، سأوضح الفرق بين البرامج الثابتة لجهاز التحكم الدقيق عن المعتاد ولماذا تعد بعض التطبيقات المنفردة لهذا البرنامج "أفضل" من غيرها. تأتي بعض المعايير من متطلبات البرامج الثابتة ، وبعضها يأتي فقط من تجربتي:
- في البرنامج الثابت ، لا ينصح بإنشاء كائنات بشكل حيوي
- غالبًا ما يتم إنشاء كائن ثابت في البرامج الثابتة ولا يتم إتلافه مطلقًا.
- حسنًا ، إذا كان موقع الكائن معروفًا في مرحلة الترجمة
بناءً على هذه الافتراضات ، فإننا نعتبر نوعين من Singleton مع كائنات تم إنشاؤها بشكل ثابت ، وربما الأكثر شهرة وشائعة هي Meyers Singleton ، بالمناسبة ، على الرغم من أنه يجب أن يكون مؤشر الترابط آمنًا وفقًا لمعيار C ++ ، فإن برامج التحويل البرمجي للبرامج الثابتة تجعله مثل هذا (على سبيل المثال ، IAR) ، فقط عند تمكين الخيار الخاص:
template <typename T> class Singleton { public: static T & GetInstance() { static T instance ; return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; } ;
يستخدم التهيئة المتأخرة ، أي لا تتم تهيئة الكائن إلا في المرة الأولى التي
GetInstance()
استدعاء
GetInstance()
؛ اعتبر ذلك تهيئة ديناميكية.
int main() {
و Singleton دون تأخير التهيئة:
template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private: inline static T instance ;
كلاهما ينشئ كائنات في ذاكرة الوصول العشوائي ، والفرق هو أنه بالنسبة للثاني ، يحدث التهيئة فور بدء البرنامج ، ويتم تهيئة الأول على المكالمة الأولى.
كيف يمكن استخدامها في الحياة الحقيقية. وفقًا للتقاليد القديمة ، سأحاول إظهار ذلك باستخدام مثال LED. لذلك ، لنفترض أننا بحاجة إلى إنشاء كائن من الفئة
Led1
، والذي هو في الواقع مجرد اسم مستعار للفئة
Pin<PortA, 5>
:
using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; Led1 myLed ;
فقط في حالة ، فصول Port و Pin تبدو مثل هذا constexpr std::uint32_t OdrAddrShift = 20U; template <std::uint32_t addr> struct Port { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum> class Pin {
في المثال ، قمت بإنشاء ما يصل إلى 4 كائنات مختلفة من نفس النوع في RAM و ROM ، والتي تعمل في الواقع مع نفس إخراج المنفذ A. وهو أمر غير جيد جدًا هنا:
حسنًا ، أول شيء هو أنني نسيت أن
GreenLed
و
Led1
هما نفس النوع وأنشأت العديد من الكائنات المتطابقة التي
GreenLed
مساحة في عناوين مختلفة. في الحقيقة ، لقد نسيت أنني قد قمت بالفعل بإنشاء كائنات عالمية
Led1
و
GreenLed
، وكذلك
GreenLed
بإنشائها محليًا.
ثانياً ، الإعلان عن الكائنات العالمية عمومًا غير مرحب به ،
إرشادات البرمجة لتحسين المترجم أفضليفضل استخدام متغيرات الوحدة النمطية المحلية - المتغيرات التي تم الإعلان عنها ثابتة
المتغيرات العالمية (غير ساكنة). تجنب أيضًا تناول عنوان المتغيرات الثابتة التي يتم الوصول إليها بشكل متكرر.
والكائنات المحلية متوفرة فقط في نطاق الدالة main ().
لذلك ، نعيد كتابة هذا المثال باستخدام Singleton:
using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; int main() {
في هذه الحالة ، بغض النظر عن ما أنسى ، فإن روابطي ستشير دائمًا إلى نفس الكائن. وأستطيع الحصول على هذا الرابط في أي مكان في البرنامج ، بأي طريقة ، بما في ذلك ، على سبيل المثال ، في الأسلوب الثابت لمعالج المقاطعة ، ولكن بشكل أكبر على ذلك لاحقًا. في الإنصاف ، يجب أن أقول أن الكود لا يفعل شيئًا ، ولم يختفي الخطأ في منطق البرنامج. حسنًا ، حسنًا ، دعنا نتعرف على أين وكيف تم تحديد موقع هذا الكائن الثابت الذي أنشأه سينجلتون بشكل عام وكيف تمت تهيئته؟
كائن ثابت
قبل معرفة ذلك ، سيكون من الجيد أن نفهم ماهية الكائن الثابت.
إذا قمت بتصريح أعضاء الفصل باستخدام الكلمة الأساسية الثابتة ، فهذا يعني أن أعضاء الفصل ليسوا ببساطة مرتبطين بمثيلات الفصل ، فهم متغيرات مستقلة ويمكنك الوصول إلى هذه الحقول دون إنشاء كائن فئة. لا شيء يهدد حياتهم من لحظة ولادتهم حتى يتم إطلاق البرنامج.
عند استخدامها في تعريف كائن ، يحدد المحدد الثابت فقط عمر الكائن. بمعنى تقريبي ، يتم تخصيص ذاكرة مثل هذا الكائن عند بدء تشغيل البرنامج وتحريره عند انتهاء البرنامج ؛ وعندما يبدأ ، تتم التهيئة أيضًا. الاستثناءات هي فقط كائنات ثابتة محلية ، والتي ، على الرغم من أنها "تموت" فقط في نهاية البرنامج ، "تُولد" بشكل أساسي ، أو بالأحرى ، تتم تهيئتها في أول مرة تمر فيها من خلال إعلانها.
يتم إجراء التهيئة الديناميكية للمتغير المحلي مع التخزين الثابت لأول مرة في وقت المرور الأول من خلال إعلانه ؛ يتم اعتبار تهيئة هذا المتغير عند الانتهاء من التهيئة. إذا مرر مؤشر ترابط خلال إعلان متغير في لحظة التهيئة الخاصة به بواسطة مؤشر ترابط آخر ، فيجب عليه الانتظار حتى يكتمل التهيئة.
في المكالمات التالية ، لا يحدث التهيئة. كل ما سبق يمكن اختزاله إلى عبارة ،
يمكن أن يوجد مثيل واحد فقط لكائن ثابت.تؤدي هذه الصعوبات إلى حقيقة أن استخدام المتغيرات الثابتة المحلية والكائنات في البرامج الثابتة سيؤدي إلى حمل إضافي. يمكنك التحقق من ذلك بمثال بسيط:
struct Test1{ Test1(int value): j(value) {} int j; } ; Test1 &foo() { static Test1 test(10) ; return test; } int main() { for (int i = 0; i < 10; ++i) { foo().j ++; } return 0; }
هنا ، في المرة الأولى التي يتم فيها استدعاء الدالة
foo()
، يجب على المحول البرمجي التحقق من أنه لم يتم بعد تهيئة الكائن الثابت المحلي
test1
واستدعاء مُنشئ كائن
Test1(10)
، وفي التمريرات الثانية واللاحقة ، يجب التأكد من تهيئة الكائن بالفعل وتخطي هذه الخطوة. الذهاب مباشرة
return test
.
للقيام بذلك ، يقوم المحول البرمجي ببساطة بإضافة علامة حماية إضافية
foo()::static guard for test 0x00100004 0x1 Data Lc main.o
وإدراج رمز التحقق. في أول إعلان لمتغير ثابت ، لم يتم تعيين هذه العلامة الواقية ، وبالتالي يجب تهيئة الكائن عن طريق استدعاء المنشئ ؛ خلال التمرير التالي ، تم تعيين هذه العلامة بالفعل ، لذلك لم تعد هناك حاجة للتهيئة ولم يتم تخطي استدعاء المنشئ. علاوة على ذلك ، سيتم إجراء هذا الفحص بشكل مستمر في الحلقة.

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

وبالتالي ، فإن سعر استخدام متغير ثابت أو كائن في البرامج الثابتة يزيد في كل من حجم ذاكرة الوصول العشوائي وحجم الرمز. وهذه الحقيقة سيكون من الجميل أن تضعها في الاعتبار وتفكر عند تطويرها.
عيب آخر هو حقيقة أن العلم الواقي يولد مع المتغير الساكن ، وعمره يساوي عمر الكائن الثابت ، ويتم إنشاؤه بواسطة المترجم نفسه ولا يمكنك الوصول إليه أثناء التطوير. أي إذا فجأة لسبب ما
رؤية تحطم عشوائيأسباب الأخطاء العشوائية هي: (1) جسيمات ألفا الناتجة عن عملية التحلل ، (2) النيوترونات ، (3) مصدر خارجي للإشعاع الكهرومغناطيسي ، (4) الحديث المتبادل الداخلي.
إذا انتقلت العلامة من 1 إلى 0 ، فسيتم استدعاء التهيئة بالقيمة الأولية مرة أخرى. هذا ليس جيدًا ، ويجب على المرء أيضًا وضعه في الاعتبار. لتلخيص المتغيرات الثابتة:
لأي كائن ثابت (سواء كان متغير محلي أو سمة فئة) ، يتم تخصيص الذاكرة مرة واحدة ولن تتغير خلال التطبيق.
تتم تهيئة المتغيرات الثابتة المحلية أثناء التمرير الأول من خلال إعلان متغير.
تتم تهيئة سمات الفئة الثابتة ، وكذلك المتغيرات العامة الثابتة ، مباشرة بعد بدء تشغيل التطبيق. علاوة على ذلك ، لم يتم تعريف هذا الترتيب
عاد الآن إلى سينجلتون.
وضع المفرد الكائن في ROM
من كل ما سبق ، يمكننا أن نستنتج أنه بالنسبة لنا ، قد يكون لدى Singleton Mayers العيوب التالية: تكاليف RAM و ROM إضافية ، علامة أمان غير متحكم فيها وعدم القدرة على وضع كائن في ROM بسبب التهيئة الديناميكية.
ولكن لديه واحد زائد رائع: يمكنك التحكم في وقت التهيئة للكائن. المطور فقط يستدعي
GetInstance()
لأول مرة في الوقت الذي يحتاج فيه.
للتخلص من العيوب الثلاثة الأولى ، يكفي استخدامها
سينجلتون دون تأخير التهيئة template<typename T, class Enable = void> class Singleton { public: Singleton(const Singleton&) = delete ; Singleton& operator = (const Singleton&) = delete ; Singleton() = delete ; static T& GetInstance() { return instance; } private: static T instance ; } ; template<typename T, class Enable> T Singleton<T,Enable>::instance ;
هنا ، بالطبع ، هناك مشكلة أخرى ، لا يمكننا التحكم في وقت التهيئة لكائن
instance
، ويجب علينا بطريقة ما توفير تهيئة شفافة للغاية. لكن هذه مشكلة منفصلة ، لن نتناولها الآن.
يمكن إعادة إنشاء Singleton هذا بحيث يكون تهيئة الكائن ثابتًا تمامًا في وقت التحويل البرمجي ويتم إنشاء مثيل للكائن
T
في ROM باستخدام
static constexpr T instance
static T instance
بدلاً من
static T instance
:
template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private:
هنا ، سيتم تنفيذ إنشاء وتهيئة الكائن بواسطة برنامج التحويل البرمجي في مرحلة التحويل البرمجي وسوف يقع الكائن في مقطع. للقراءة فقط. صحيح ، يجب أن يفي الفصل نفسه بالقواعد التالية:
- يجب أن تكون تهيئة كائن من هذه الفئة ثابتة. (يجب أن يكون المنشئ هو constexpr)
- يجب أن يكون لدى الفصل مُنشئ نسخ من مادة constexpr
- يجب ألا تغير أساليب الفصل لكائن الفئة بيانات كائن الفصل (جميع أساليب الاشتراك)
على سبيل المثال ، هذا الخيار ممكن تمامًا:
class A { friend class Singleton<A>; public: const A & operator=(const A &) = delete ; int Get() const { return test2.Get(); } void Set(int v) const { test.SetB(v); } private: B& test;
رائع ، يمكنك استخدام Singleton لإنشاء كائنات في ROM ، لكن ماذا لو كانت بعض الكائنات في ذاكرة الوصول العشوائي؟ من الواضح أنك تحتاج إلى الاحتفاظ بطريقة أو بأخرى بتخصصين لـ Singleton ، أحدهما لكائنات RAM ، والآخر للكائنات في ROM. يمكنك القيام بذلك عن طريق إدخال ، على سبيل المثال ، لجميع الكائنات التي ينبغي وضعها في فئة ROM الأساسية:
التخصص ل Singleton خلق الكائنات في ROM و RAM في هذه الحالة ، يمكنك استخدامها مثل هذا:
كيف يمكنك استخدام مثل هذا Singleton في الحياة الحقيقية.
مثال سينغلتون
سأحاول إظهار ذلك على مثال تشغيل جهاز ضبط الوقت ومؤشر LED. المهمة بسيطة ، وميض الصمام في الموقت. يمكن ضبط الموقت.
سيكون مبدأ التشغيل كما يلي ، عند استدعاء المقاطعة ، سيتم
OnInterrupt()
طريقة
OnInterrupt()
، والتي بدورها
OnInterrupt()
طريقة تبديل LED عبر واجهة المشترك.
من الواضح أن كائن LED يجب أن يكون في ذاكرة الوصول العشوائي (ROM) ، نظرًا لعدم وجود فائدة في إنشائه في ذاكرة الوصول العشوائي (RAM) ، حتى أنه لا توجد بيانات فيه. من حيث المبدأ ، لقد سبق أن وصفتها أعلاه ، لذلك فقط أضف الميراث من
RomObject
، وقم بإنشاء مُنشئ constexpr وأيضًا وارث الواجهة لمعالجة الأحداث من المؤقت.
لكنني سأجعل Timer على وجه التحديد في ذاكرة الوصول العشوائي مع القليل من بوليصة الشحن ، وسأقوم بتخزين رابط لهيكل
TIM_TypeDef
وفترة ورابط مشترك ، وفي المُنشئ سأقوم بتهيئة المؤقت (على الرغم من أنه سيكون من الممكن جعل Timer يذهب أيضًا إلى ROM):
توقيت الصف class Timer { public: const Timer & operator=(const Timer &) = delete ; void SetPeriod(const std::uint16_t value) { period = value ; timer.PSC = TimerClockSpeed / 1000U - 1U ; timer.ARR = value ; }
في هذا المثال ، كان كائن الفئة
BlinkTimer
موجودًا في ذاكرة الوصول العشوائي ، وكان كائن الفئة
Led1
موجودًا في ROM. لا كائنات عمومية إضافية في التعليمات البرمجية. في المكان الذي يحتاج إلى مثيل الفئة ، نحن ببساطة استدعاء
GetInstance()
لهذه الفئة
يبقى لإضافة معالج مقاطعة إلى جدول متجه المقاطعة. وهنا ، من المريح جدًا استخدام Singleton. في الأسلوب الثابت للفئة المسؤولة عن معالجة المقاطعات ، يمكنك استدعاء طريقة الكائن المُلف في Singleton.
extern "C" void __iar_program_start(void) ; class InterruptHandler { public: static void DummyHandler() { for(;;) {} } static void Timer2Handler() {
قليلا عن الجدول نفسه ، وكيف يعمل كل شيء:مباشرة بعد بدء التشغيل أو بعد إعادة التعيين ، تتم مقاطعة إعادة التعيين بالرقم -8 ، في الجدول يكون عنصر صفري ، وفقًا لإشارة إعادة التعيين ، ينتقل البرنامج إلى متجه عنصر الصفر ، حيث يتم تهيئة المؤشر إلى أعلى المكدس أولاً. يؤخذ هذا العنوان من موقع شريحة STACK التي قمت بتكوينها في إعدادات رابط. مباشرة بعد تهيئة المؤشر ، انتقل إلى نقطة إدخال البرنامج ، في هذه الحالة ، على عنوان الدالة __iar_program_start
. بعد ذلك ، تتم تهيئة التعليمة البرمجية لتهيئة المتغيرات العامة والثابتة ، وتهيئة المعالج الثانوي بنقطة عائمة ، إذا تم تضمينه في الإعدادات ، وما إلى ذلك. في حالة حدوث مقاطعة ، ينتقل جهاز التحكم في المقاطعة برقم المقاطعة في الجدول إلى عنوان معالج المقاطعة. في حالتنا ، هذا هو InterruptHandler::Timer2Handler
، والذي ، من خلال Singleton ، يستدعي طريقة OnInterrupt()
الوقت وميض لدينا ، والذي بدوره OnTimeOut()
أسلوب OnTimeOut()
منفذ المنفذ.
في الواقع هذا كل شيء ، يمكنك تشغيل البرنامج. مثال عملي لـ IAR 8.40
هنا .
يمكن
العثور هنا على مثال أكثر تفصيلاً حول استخدام Singleton للكائنات في ROM و RAM.
روابط الوثائق:
PS في الصورة في بداية المقال ، كل نفس ، Singleton ليس ROM ، ولكن WHISKEY.