
大家身体健康!
在新年的前夜,我想继续谈论在微控制器上使用C ++的问题,这一次,我将尝试谈论使用Observer模板(但在下文中,我将其称为Publisher-Subscriber或仅称为Subscriber,例如一个双关语),以及对C的静态订阅的实现。 ++ 17以及这种方法在某些应用程序中的优势。
引言
模板订阅服务器是软件开发中最常用的模板之一。 例如,有了它,他们就可以在Windows窗体中进行按钮单击处理。 无论如何,在需要以某种方式响应系统参数更改的任何地方,无论是文件更改还是更新传感器的测量值,都是时候了 没有思考 使用订阅服务器模板。
模板的优势在于,我们可以释放发布者和订阅者的知识,而不必依赖于特定的对象。 我们可以将任何人签名给任何人,而不会影响Publisher和Subscriber对象的实现。
初始条件
在熟悉模板之前,首先让我们同意我们要开发可靠的软件,其中:
- 不要使用动态内存分配
- 减少指针的工作
- 我们使用尽可能多的常量,以便没有人可以尽可能多地更改任何人
- 但同时我们在RAM中使用的常量尽可能少
现在,让我们看一下订户模板的标准实现。
标准实施
假设我们有一个按钮,当您单击该按钮时,我们需要使LED闪烁,但是尚不知道有多少个LED发光,实际上,您可能需要不以LED闪烁,而是在船上以聚光灯闪烁,以摩尔斯电码传输消息。 重要的是我们不知道将要订阅谁。 不幸的是,我手头没有聚光灯,因此本文中所有为简单起见和更好理解的示例都是使用LED制作的。
因此,当您按下按钮时,您需要将此按键通知给LED。 继而,在得知按下LED后,应该切换到相反的状态。
UML中的标准实现如下...

这里的ButtonController
类负责轮询按钮并向订阅者通知有关单击的信息,在这种情况下, Led
是订阅者。 这两个类通过IPublisher
和ISubsriber
解耦,并且两个类都不相互了解。 因此,从ISubscriber
接口继承的任何对象都可以预订ButtonController
的事件。
由于禁止动态内存分配,因此我声明了一个由3个元素组成的数组用于订阅。 即 最多可以有3个订阅者。 因此,在第一个近似值中,向ButttonsController
类通知订阅者的方法可能看起来像
struct ButtonController : IPublisher { void Run() { for(;;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override {
所有的盐都在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 {
全面实施所有课程 template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0)
订阅如何查看代码? 依此类推:
int main() {
好消息是,我们可以对任何对象进行签名,而对象的创建时间对我们而言并不重要。 它可以是静态或局部的全局对象。 一方面,这很好,但另一方面,为什么我们需要在此代码中订阅运行时。 实际上,实际上,在编译阶段就知道对象Led3
, Led3
, Led3
的地址。 那么,为什么不能在编译阶段进行订阅并在ROM中保留指向订阅者的指针数组呢?
此外,还存在潜在错误的风险,例如,有多少人想知道如果从多个线程中调用Subsribe()
方法会发生什么情况? 我们仅限于3个订阅者,如果我们签署4个LED会发生什么?
在大多数情况下,我们需要在初始化期间的整个生命周期中进行一次订阅,仅保存指向订阅者的指针即可。 指针将终身保留这些订户的地址。 毁灭的日子是不可避免的 由于超新星爆发 (当然,如果我们考虑相当长的时间)。 但是无论如何,RAM故障的可能性比ROM高得多,因此不建议将永久数据存储在RAM中。
不好的消息是,这样的体系结构解决方案占用了ROM和RAM很大的空间。 以防万一,我们写出此解决方案需要多少ROM和RAM:
即 在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)
现在可以在编译时完成订阅:
int main() {
在这里, buttonController
对象与指向订户的指针数组完全位于ROM中:
main :: buttonController 0x800'1f04 0x10数据main.o [1]
一切似乎都不算什么,只是我们又被限制为只有3个订户。 并且发布者类必须具有constexpr构造函数,并且通常是完全恒定的,以确保指向ROM中的订阅者的指针,否则,即使具有已知的订阅者地址,我们的对象以及所有内容也将再次进入RAM。
其他缺点-由于仍然使用虚拟功能,因此虚拟功能表会逐点逐位出现在我们的ROM中。 资源虽然负担得起,但不是无限的。 在大多数应用中,您可以锤击它并采用更大的微控制器,但是经常会发生每个字节都很重要的情况,尤其是涉及到由成千上万个制造的产品(例如物理物理传感器)时。
让我们看看此解决方案中的内存情况如何:
尽管结果是“惊人的”:总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() {
我们的任务是使两个对象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)
Run()
方法中的Notify()
调用退化为简单的顺序调用
Led1.HandleEvent() ; Led2.HandleEvent() ; Led3.HandleEvent() ;
那这里的记忆呢?
ROM共190字节,RAM共有0字节。 现在,该订单几乎是标准版本的三倍,而它执行的功能却完全相同。
因此,如果您具有应用程序中预先已知的订户地址,并且您遵循本文开头定义的条件,
本文开头的条件- 不要使用动态内存分配
- 减少指针的工作
- 我们使用尽可能多的常量,以便没有人可以尽可能多地更改任何人
- 但同时我们在RAM中使用的常量尽可能少
您可以放心地使用Publisher-Subscriber模板的这种实现来减少代码行并节省资源,在那里您可以看到并且不仅可以索取礼品卡,还可以根据当年的结果获得奖金。
IAR 8.40.2下的测试示例位于此处
一切即将到来! 在新的一年里祝你好运!