使用带C ++和Cortex M4微控制器的Observer模板进行静态订阅


大家身体健康!


在新年的前夜,我想继续谈论在微控制器上使用C ++的问题,这一次,我将尝试谈论使用Observer模板(但在下文中,我将其称为Publisher-Subscriber或仅称为Subscriber,例如一个双关语),以及对C的静态订阅的实现。 ++ 17以及这种方法在某些应用程序中的优势。


引言


模板订阅服务器是软件开发中最常用的模板之一。 例如,有了它,他们就可以在Windows窗体中进行按钮单击处理。 无论如何,在需要以某种方式响应系统参数更改的任何地方,无论是文件更改还是更新传感器的测量值,都是时候了 没有思考 使用订阅服务器模板。


模板的优势在于,我们可以释放发布者和订阅者的知识,而不必依赖于特定的对象。 我们可以将任何人签名给任何人,而不会影响Publisher和Subscriber对象的实现。


初始条件


在熟悉模板之前,首先让我们同意我们要开发可靠的软件,其中:


  • 不要使用动态内存分配
  • 减少指针的工作
  • 我们使用尽可能多的常量,以便没有人可以尽可能多地更改任何人
  • 但同时我们在RAM中使用的常量尽可能少

现在,让我们看一下订户模板的标准实现。


标准实施


假设我们有一个按钮,当您单击该按钮时,我们需要使LED闪烁,但是尚不知道有多少个LED发光,实际上,您可能需要不以LED闪烁,而是在船上以聚光灯闪烁,以摩尔斯电码传输消息。 重要的是我们不知道将要订阅谁。 不幸的是,我手头没有聚光灯,因此本文中所有为简单起见和更好理解的示例都是使用LED制作的。


因此,当您按下按钮时,您需要将此按键通知给LED。 继而,在得知按下LED后,应该切换到相反的状态。
UML中的标准实现如下...



这里的ButtonController类负责轮询按钮并向订阅者通知有关单击的信息,在这种情况下, Led是订阅者。 这两个类通过IPublisherISubsriber解耦,并且两个类都不相互了解。 因此,从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() ; } } } } ; 

所有的盐都在Publisher类的Notify()方法中。 在此方法中,我们遍历订阅者列表,并在每个订阅者上调用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() ; } 

好消息是,我们可以对任何对象进行签名,而对象的创建时间对我们而言并不重要。 它可以是静态或局部的全局对象。 一方面,这很好,但另一方面,为什么我们需要在此代码中订阅运行时。 实际上,实际上,在编译阶段就知道对象Led3Led3Led3的地址。 那么,为什么不能在编译阶段进行订阅并在ROM中保留指向订阅者的指针数组呢?


此外,还存在潜在错误的风险,例如,有多少人想知道如果从多个线程中调用Subsribe()方法会发生什么情况? 我们仅限于3个订阅者,如果我们签署4个LED会发生什么?


在大多数情况下,我们需要在初始化期间的整个生命周期中进行一次订阅,仅保存指向订阅者的指针即可。 指针将终身保留这些订户的地址。 毁灭的日子是不可避免的 由于超新星爆发 (当然,如果我们考虑相当长的时间)。 但是无论如何,RAM故障的可能性比ROM高得多,因此不建议将永久数据存储在RAM中。


不好的消息是,这样的体系结构解决方案占用了ROM和RAM很大的空间。 以防万一,我们写出此解决方案需要多少ROM和RAM:


模组密码只读数据RW数据
main.o4886421

即 在ROM中总共552字节,在RAM中总共21字节-假设按下按钮并闪烁三个LED不需要太多。


好吧,为了保护自己免受此类麻烦并减少控制器资源的消耗,让我们考虑使用静态订阅的选项。


静态订阅


为了使订阅成为静态,可以使用几种方法。 我将这样命名:


  • 传统方法是相同的方法,但是使用constexpr构造函数并通过它来设置订户列表。
  • 非常规 使用模板-通过模板参数传输订户列表。 (这里的模板是元编程领域的定义,而不是设计模式)

静态订阅的传统方法


让我们尝试在编译阶段进行订阅。 为此,我们对架构进行一些调整:



该图片与原始图片没有太大区别,但是有几个区别: 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对象与指向订户的指针数组完全位于ROM中:


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

一切似乎都不算什么,只是我们又被限制为只有3个订户。 并且发布者类必须具有constexpr构造函数,并且通常是完全恒定的,以确保指向ROM中的订阅者的指针,否则,即使具有已知的订阅者地址,我们的对象以及所有内容也将再次进入RAM。


其他缺点-由于仍然使用虚拟功能,因此虚拟功能表会逐点逐位出现在我们的ROM中。 资源虽然负担得起,但不是无限的。 在大多数应用中,您可以锤击它并采用更大的微控制器,但是经常会发生每个字节都很重要的情况,尤其是涉及到由成千上万个制造的产品(例如物理物理传感器)时。


让我们看看此解决方案中的内存情况如何:


模组密码只读数据RW数据
main.o172760

尽管结果是“惊人的”:总RAM消耗为0字节,ROM为248字节,这是第一种解决方案的一半,但感觉仍有改进的空间。 在这248个字节中,大约有50个仅占用虚拟方法表。


一个小题外话:
现代微控制器的ROM大小要达到256 kB的一个步骤(例如,TI Cortex M4微控制器具有256 kB的ROM,而下一版本已经是512 kB)。 而且由于50个额外的字节,我们将不得不采用一个具有256 KB更大ROM和更昂贵的控制器,因此放弃虚拟功能可以节省多达50美分的情况(256个ROM和512 KB ROM的微控制器之间的差异大约是50美元),这将不是很好。 50-60美分)。


对于1个微控制器来说,这听起来很荒谬,但是每年在40万个设备上,您可以节省20万美元。 已经不那么有趣了,但考虑使用哪种老鼠。 这项优惠可以得到3,000卢布的文凭和礼品卡,毫无疑问,拒绝虚拟功能和在ROM中节省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 )和Subscriber( Led )彼此分离,以使它们彼此之间不了解,但是ButtonController可以同时通知Led


您可以通过某种方式声明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)...) ; } ... } ; 

pass(..)函数如何工作

Notify()方法调用pass()函数;它用于扩展具有可变数量参数的模板参数


  void Notify() const { pass((subscribers.HandleEvent() , true)...) ; } 

pass()函数的实现简直是难以想象的,它只是一个带有可变数量参数的函数:


 template<typename... Args> void pass(Args...) const { } } ; 

HandleEvent()函数如何扩展为每个订户的多个调用?


由于pass()函数接受多个任何类型的参数,因此您可以将多个bool类型的参数传递bool ,例如,您可以调用pass(true, true, true)函数。 当然,在这种情况下,什么也不会发生,但是我们不需要。


该行(subscribers.HandleEvent() , true)使用运算符“,”(逗号),它执行两个操作数(从左到右)并返回第二个运算符的值,即在这里,先执行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 指出所有这些舞蹈都与 手鼓 不需要C ++ 17中的pass函数。 由于折叠表达式(在C ++ 17标准中引入)支持逗号“,”,因此代码得以进一步简化:


 template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { ((subscribers.HandleEvent()), ...) ; } } ; 

从结构上来说,它通常看起来非常简单:



我在这里添加了另一个LCD类,但纯粹是为了举例说明,现在它与订阅者的类型和数量无关,主要是它将实现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 ; } 

Run()方法中的Notify()调用退化为简单的顺序调用


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

那这里的记忆呢?


模组密码只读数据RW数据
main.o18640

ROM共190字节,RAM共有0字节。 现在,该订单几乎是标准版本的三倍,而它执行的功能却完全相同。


因此,如果您具有应用程序中预先已知的订户地址,并且您遵循本文开头定义的条件,


本文开头的条件
  • 不要使用动态内存分配
  • 减少指针的工作
  • 我们使用尽可能多的常量,以便没有人可以尽可能多地更改任何人
  • 但同时我们在RAM中使用的常量尽可能少

您可以放心地使用Publisher-Subscriber模板的这种实现来减少代码行并节省资源,在那里您可以看到并且不仅可以索取礼品卡,还可以根据当年的结果获得奖金。


IAR 8.40.2下的测试示例位于此处


一切即将到来! 在新的一年里祝你好运!

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


All Articles