将对象单例放置在ROM和静态变量中(以Cortex M4微控制器为例的C ++)

图片

在上一篇文章中, 将常量存储在CortexM微控制器上的哪里(以C ++ IAR编译器为例) ,讨论了如何将常量对象放置在ROM中的问题。 现在,我想告诉您如何使用孤立生成器模式在ROM中创建对象。


引言


关于Singleton(以下简称Singleton)的正面和反面,已经有很多著作。 尽管有缺点,但它具有许多有用的属性,尤其是在微控制器固件的上下文中。

首先,对于可靠的微控制器软件,不建议动态创建对象,因此无需删除它们。 通常,对象仅创建一次即可使用,从设备启动到关闭一直存在。 这样的对象甚至可能是一个连接有LED的端口分支,它只创建了一次,并且在应用程序运行时肯定不会走到任何地方,显然可以是Singleton。 应当创建此类对象,并且可能是Singleton。

Singleton还可以向您保证,描述端口支路的同一对象如果在多个地方突然使用,将不会被创建两次。

我认为,Singleton的另一个显着特性是它的易用性。 例如,与中断处理程序的情况一样,本文结尾处的示例。 但目前,我们将与Singleton自己打交道。

单例在RAM中创建对象


总的来说,已经有很多关于Singleton(Loner)或静态类的文章。三个单身模式时代 。 因此,我将不关注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()时发生;将其视为动态初始化。

 int main() { //   Timer1      auto& objRef = Singleton<Timer1>::GetInstance(); //  ,      auto& objRef1 = Singleton<Timer1>::GetInstance(); return 0; } 

而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 ; //      } ; 

两个Singleton都在RAM中创建对象,区别在于第二个在程序启动后立即进行初始化,第一个在第一次调用时进行了初始化。

如何在现实生活中使用它们。 根据旧的传统,我将尝试以LED为例来说明这一点。 因此,假设我们需要创建一个类Led1的对象,它实际上只是类Pin<PortA, 5>的别名:

 using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; Led1 myLed ; //        RAM constexpr GreenLed greenLed ; //        ROM int main() { static GreenLed myGreenLed ; //     RAM Led1 led1; //     myGreenLed.Toggle(); led1.Toggle() ; } 

以防万一,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 { // Singleton   ,     friend class Singleton<Pin> ; public: __forceinline inline void Toggle() const { T::Toggle(pinNum) ; } //  = const Pin & operator=(const Pin &) = delete ; private: // ,      constexpr Pin() {} ; //  ,      //   ,      constexpr Pin(const Pin &) = default ; } ; 


在该示例中,我在RAM和ROM中创建了多达4个相同类型的不同对象,它们实际上与端口A的相同输出一起使用。在这里不是很好:
好吧,第一件事是我显然忘记了GreenLedGreenLed是相同的类型,并创建了几个相同的对象,这些对象在不同的​​地址处占用了空间。 实际上,我什至忘记了我已经创建了GreenLedGreenLed全局对象,并且还在本地创建了它们。

其次,一般不欢迎声明全局对象,

更好地进行编译器优化的编程准则
模块局部变量(声明为静态的变量)优先于
全局变量(非静态)。 还应避免获取经常访问的静态变量的地址。

和局部对象仅在main()函数的范围内可用。

因此,我们使用Singleton重写此示例:

 using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; int main() { //        GreenLed //   GreenLed& myGreenLed = Singleton<GreenLed>::GetInstance(); //            Led1& led1 = Singleton<Led1>::GetInstance(); myGreenLed.Toggle() ; led1.Toggle() ; //  , Singleton<Led1>::GetInstance().Toggle() } 

在这种情况下,无论我忘记什么,我的链接将始终指向同一对象。 而且我可以在程序中的任何位置,以任何方法(包括例如在中断处理程序的静态方法中)获得此链接,但稍后会介绍更多。 公平地说,我必须说代码什么也不做,并且程序逻辑中的错误没有消失。 好吧,让我们找出由Singleton创建的静态对象通常位于何处以及如何定位以及如何对其进行初始化?

静态物体


在发现之前,最好先了解什么是静态对象。

如果使用static关键字声明类成员,则意味着类成员根本不与类实例相关联,它们是自变量,您可以在不创建类对象的情况下访问此类字段。 从出生到发行该程序,都没有威胁到他们的生命。

当在对象声明中使用时,静态说明符仅确定对象的生存期。 粗略地说,此类对象的内存在程序启动时分配,并在程序结束时释放;启动时,也会进行初始化。 异常只是局部静态对象,尽管它们仅在程序结束时“死”,但实际上是“出生的”,或者说,是在它们第一次通过声明时被初始化的。

在第一次通过其声明时,首次执行带有静态存储的局部变量的动态初始化。 这样的变量在完成其初始化后被视为已初始化。 如果一个线程在另一个线程初始化时通过变量声明,则它必须等待初始化完成。

在以下调用中,不会发生初始化。 以上所有内容都可以简化为一个短语, 只有静态对象的一个​​实例可以存在。

这种困难导致以下事实:在固件中使用局部静态变量和对象将导致额外的开销。 您可以使用一个简单的示例来验证这一点:

 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)对象的构造函数,在第二Test1(10)后续遍历中,必须确保该对象已被初始化并跳过此步骤。直接return test

为此,编译器只需foo()::static guard for test 0x00100004 0x1 Data Lc main.o添加一个附加保护标志foo()::static guard for test 0x00100004 0x1 Data Lc main.o然后插入验证代码。 在静态变量的第一个声明中,未设置此保护标志,因此必须通过调用构造函数来初始化该对象;在下一遍中,此标志已经设置,因此不再需要初始化并且跳过构造函数调用。 此外,此检查将在for循环中连续执行。



而且,如果启用了可以保证在多线程应用程序中进行初始化的选项,则将有更多的代码...(请参见橙色底划线所示的在初始化期间捕获和释放资源的调用)

图片

因此,在固件中使用静态变量或对象的价格增加了RAM大小和代码大小。 在开发时要牢记和考虑这一事实将是一个很好的选择。

另一个缺点是保护标志与静态变量一起诞生,它的生存期等于静态对象的生存期,它是由编译器本身创建的,并且在开发过程中无法访问它。 即 如果由于某种原因突然

看到随机崩溃
随机误差的原因是:(1)衰变过程产生的alpha粒子;(2)中子;(3)外部电磁辐射源;(4)内部串扰。

如果标志从1变为0,则将再次调用初始值初始化。 这不好,还必须牢记。 总结静态变量:
对于任何静态对象(无论是局部变量还是类属性),内存都会分配一次,并且不会在整个应用程序中更改。

局部静态变量在第一次通过变量声明时进行初始化。

静态类属性以及静态全局变量在应用程序启动后立即初始化。 此外,此顺序未定义
现在回到辛格尔顿。

ROM中的单例放置对象


从以上所有内容,我们可以得出结论,对于我们来说,Singleton Mayers可能具有以下缺点:额外的RAM和ROM成本,不受控制的安全标志以及由于动态初始化而无法在ROM中放置对象。

但是他有一个奇妙的优点:您可以控制对象的初始化时间。 只有开发人员自己在需要GetInstance()第一次调用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,以便在编译时对象的初始化是完全静态的,并且使用static constexpr T instance而不是static T instance在ROM中创建T对象的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   //           T static constexpr T instance{T()}; } ; template<typename T> constexpr T Singleton<T>::instance ; 

在这里,对象的创建和初始化将在编译阶段由编译器执行,并且该对象将属于.readonly段。 的确,类本身必须满足以下规则:
  • 此类的对象的初始化必须是静态的。 (构造函数必须是constexpr)
  • 该类必须具有constexpr复制构造函数
  • 类对象的类方法不应更改类对象的数据(所有const方法)

例如,此选项很有可能:

 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; //    RAM const C& test2; //    ROM //      constexpr A(const A &) = default ; //     RAM  ROM,  Singleton constexpr A() : test(Singleton<B>::GetInstance()), test2(Singleton<C>::GetInstance()) { } }; int main() { //      ROM auto& myObject = Singleton<A>::GetInstance() ; //           myObject.Set(myObject.Get()) ; cout<<"Singleton<A> - address: "<< &myObject <<std::endl; } 

太好了,您可以使用Singleton在ROM中创建对象,但是如果某些对象应该在RAM中怎么办? 显然,您需要以某种方式保留Singleton的两个专业化,一个专门用于RAM对象,另一个专门用于ROM中的对象。 您可以通过输入(例如)应放置在ROM基类中的所有对象来执行此操作:

Singleton在ROM和RAM中创建对象的专业化
 //    ,     ROM class RomObject{}; //  ROM  template<typename T> class Singleton<T, typename std::enable_if_t<std::is_base_of<RomObject, T>::value>> { public: Singleton(const Singleton&) = delete; Singleton& operator = (const Singleton&) = delete; Singleton() = delete; static constexpr const T& GetInstance() { return instance; } private: static constexpr T instance{T()}; }; template<typename T> constexpr T Singleton<T, typename std::enable_if_t<std::is_base_of<RomObject, T>::value>>::instance ; //  RAM  template<typename T, class Enable = void> class Singleton { public: Singleton(const Singleton&) = delete; Singleton& operator = (const Singleton&) = delete; Singleton() = delete; constexpr static T& GetInstance() { return instance; } private: static T instance ; }; template<typename T, class Enable> T Singleton<T,Enable>::instance ; 


在这种情况下,可以这样使用它们:

 //      RAM,   SetB()    (j) class B { friend class Singleton<B>; public: const B & operator=(const B &) = delete ; void SetB(int value) { j = value ; } private: // ,        B(const B &) = default ; B() = default; int j = 0; } //      ROM class A: public RomObject{ friend class Singleton<A>; public: const A & operator=(const A &) = delete ; int Get() const { return test2.Get(); } //     B,    void Set(int v) const { test.SetB(v); } private: B& test; //    RAM const C& test2; //    ROM //        A(const A &) = default ; //     RAM  ROM,  Singleton constexpr A() : test(Singleton<B>::GetInstance()), test2(Singleton<C>::GetInstance()) { } }; int main() { //      ROM auto& romObject = Singleton<A>::GetInstance() ; //    B  RAM auto& ramObject = Singleton<B>::GetInstance() ; //           ramObject.SetB(romObject.Get()) ; cout<<"Singleton<A> - address: "<< &romObject <<std::endl; cout<<"Singleton<B> - address: "<< &ramObject <<std::endl; } 

您如何在现实生活中使用此类Singleton。

单例示例


我将在定时器和LED的操作示例中尝试显示这一点。 任务很简单,使计时器上的LED闪烁。 可以设置计时器。

工作原理如下,当调用中断时,将调用计时器的OnInterrupt()方法,后者又将通过用户接口调用LED切换方法。

显然,LED对象必须位于ROM中,因为没有必要在RAM中创建对象,因此甚至没有数据。 原则上,我已经在上面进行了描述,因此只需将RomObject继承RomObject添加RomObject ,制作constexpr构造函数,还继承用于处理计时器事件的接口。

LED物体
 //      class ITimerSubscriber { public: virtual void OnTimeOut() const = 0; } ; template <typename T, std::uint8_t pinNum> class Pin: public RomOject, public ITimerSubscriber { // Singleton   ,     friend class Singleton<Pin> ; public: __forceinline inline void Toggle() const { T::Toggle(pinNum) ; } //       __forceinline inline void OnTimeOut() const override { Toggle() ; } //  = const Pin & operator=(const Pin &) = delete ; private: // ,      constexpr Pin() = default ; Pin(const Pin &) = default ; } ; 

但是,我将通过一些运单将Timer专门用于RAM,将存储指向TIM_TypeDef结构的链接,一个句点和一个订户的链接,并在构造函数中配置Timer(尽管可以将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 ; } //      __forceinline inline void OnInterrupt() { if ((timer.SR & TIM_SR_UIF) && (timer.DIER & TIM_DIER_UIE)) { //   ,     OnTimeOut //       Toggle() subscriber->OnTimeOut() ; timer.SR &=~ TIM_SR_UIF ; } } //    TimeOut  ,   ITimerSubscriber,   __forceinline inline void Subscribe(const ITimerSubscriber& obj) { subscriber = &obj ; } inline void Start() { timer.CR1 |= TIM_CR1_URS ; timer.DIER |= TIM_DIER_UIE ; SetPeriod(period) ; timer.CR1 &=~TIM_CR1_OPM ; timer.EGR |= TIM_EGR_UG ; timer.CR1 |= TIM_CR1_CEN ; } protected: // ,         explicit Timer(TIM_TypeDef& tim): timer{tim} {}; const ITimerSubscriber * subscriber = nullptr ; TIM_TypeDef& timer ; std::uint16_t period = 1000; } ; 


 //       class BlinkTimer: public Timer { friend class Singleton<BlinkTimer> ; public: const BlinkTimer & operator=(const BlinkTimer &) = delete ; private: BlinkTimer(const BlinkTimer &) = default ; inline BlinkTimer(): Timer{*TIM2} { } } ; int main() { BlinkTimer & blinker = Singleton<BlinkTimer>::GetInstance() ; using Led1 = Pin<PortA, 5> ; // Led1,   ROM,      blinker.Subscribe(Singleton<Led1>::GetInstance()) ; blinker.Start() ; } 

在此示例中,类BlinkTimer的对象位于RAM中,而类BlinkTimer的对象位于ROM中。 代码中没有多余的全局对象。 在需要类实例的地方,我们只需为该类调用GetInstance()

仍然需要在中断向量表中添加中断处理程序。 在这里,使用Singleton非常方便。 在负责处理中断的类的静态方法中,可以调用包装在Singleton中的对象的方法。

 extern "C" void __iar_program_start(void) ; class InterruptHandler { public: static void DummyHandler() { for(;;) {} } static void Timer2Handler() { //   BlinkTimer Singleton<BlinkTimer>::GetInstance().OnInterrupt(); } }; using tIntFunct = void(*)(); using tIntVectItem = union {tIntFunct __fun; void * __ptr;}; #pragma segment = "CSTACK" #pragma location = ".intvec" const tIntVectItem __vector_table[] = { { .__ptr = __sfe( "CSTACK" ) }, //    __iar_program_start, //      InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, 0, 0, 0, 0, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, 0, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, //External Interrupts InterruptHandler::DummyHandler, //Window Watchdog InterruptHandler::DummyHandler, //PVD through EXTI Line detect/EXTI16 .... InterruptHandler::Timer2Handler, //      BlinkTimer InterruptHandler::DummyHandler, //TIM3 ... InterruptHandler::DummyHandler, //SPI 5 global interrupt }; extern "C" void __cmain(void) ; extern "C" __weak void __iar_init_core(void) ; extern "C" __weak void __iar_init_vfp(void) ; #pragma required = __vector_table void __iar_program_start(void) { __iar_init_core() ; __iar_init_vfp() ; __cmain() ; } 

有关表本身的一些知识,它如何工作:
上电后或复位后,复位立即中断, 编号为-8 ,在该表中为零元素,根据复位信号,程序切换到零元素向量,首先初始化堆栈顶部的指针。 该地址取自您在链接器设置中配置的STACK段的位置。 指针初始化之后,立即进入程序入口点,在这种情况下,位于__iar_program_start函数的地址。 接下来,初始化代码以初始化全局变量和静态变量,并使用浮点初始化协处理器(如果设置中包含了浮点),依此类推。 如果发生中断,则中断控制器通过表中的中断号转到中断处理程序的地址。 在我们的例子中,这是InterruptHandler::Timer2Handler ,它通过Singleton调用了我们的闪烁计时器的OnInterrupt()方法,而该方法又OnTimeOut()端口支路的OnTimeOut()方法。

其实仅此而已,您可以运行该程序。 IAR 8.40的一个有效示例就在这里
此处可以找到对ROM和RAM中的对象使用Singleton的更详细的示例。

文档链接:


PS在文章开头的图片中,都一样,Singleton不是ROM,而是WHISKEY。

Source: https://habr.com/ru/post/zh-CN455477/


All Articles