在微控制器的C ++中使用引脚列表(以CortexM为例)


大家身体健康!


在上一篇文章中,我答应过写有关如何使用端口列表的文章。
我必须马上说,一切都已经在2010年决定了,这是文章: 使用C ++中的微控制器的输入/输出端口 。 2010年写这篇文章的人简直是帅气。


我会尴尬的是我会做10年前已经做过的事情,所以我决定不等2020年,而要在2019年做这件事以便重复9年前的决定,这不会那么愚蠢。


在上面的文章中,使用更多类型的模板具有固定数量的参数,而函数不能是constexpr表达式时,使用C ++ 03完成了类型列表的工作。 从那以后,C ++发生了一些变化,所以让我们尝试做同样的事情,但是在C ++ 17中。 欢迎来到猫:


挑战赛


因此,任务是一次安装或丢弃多个处理器引脚,这些引脚组合在一起。 尽管如此,引脚仍可以位于不同的端口上,这种操作应尽可能高效地完成。


实际上,我们要执行的操作可以通过以下代码显示:


using Pin1 = Pin<GPIO, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; int main() { //    Pin    : //   GPIOA  10 GPIOA->BSRR = 10 ; // (1<<1) | (1 << 3) ; //   GPIOB  2 GPIOB->BSRR = 2 ; // (1 << 1) //   GPIOC  6 GPIOB->BSRR = 6 ; // (1 << 1) | (1 << 2); PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; return 0; } 

关于BSRR寄存器

对于那些不了解微控制器事务的人, GPIOA->BSRR寄存器负责微控制器GPIOA->BSRR的原子安装或值重置。 该寄存器为32位。 前16位负责将腿设置为1,后16位负责将腿设置为0。


例如,要将数字3脚设置为1,需要将BSRR寄存器中的第三个位设置为1;要将数字3脚复位为0,需要将同一BSRR寄存器中的19位设置为1。


解决此问题的通用步骤方案可以表示如下:



好吧,换句话说:


为了让编译器为我们做:


  • 验证列表仅包含唯一的引脚
  • 通过确定引脚所在的端口来创建端口列表,
  • 计算要放入每个端口的值

然后程序


  • 设定这个值

您需要尽可能高效地执行此操作,以便即使不进行优化,代码也最少。 实际上,这是整个任务。


让我们从第一种时尚开始:验证列表包含唯一的Pin。


检查列表的唯一性


让我提醒您,我们有一个引脚列表:


 PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> ; 

无意间 可以做到这一点:


 PinsPack<Pin1, Pin2, Pin3, Pin4, Pin1> ; //     Pin1 

我希望编译器发现这样的错误并通知钢琴家。


我们将检查列表的唯一性,如下所示:


  • 在源列表中,创建一个没有重复的新列表,
  • 如果源列表的类型和没有重复的列表的类型不匹配,则说明源列表中的Pin相同,因此程序员犯了一个错误。
  • 如果它们匹配,那么一切都很好,没有重复项。

为了创建一个没有重复项的新列表,一位同事建议不要重新发明轮子,而是从Loki库中采用这种方法。 我有这种方法并偷了。 与2010年几乎相同,但参数数量可变。


从同事那里借来的代码,该同事从Loki那里借了这个想法
 namespace PinHelper { template<typename ... Types> struct Collection { }; /////////////////   NoDuplicates   LOKI //////////////// template<class X, class Y> struct Glue; template<class T, class... Ts> struct Glue<T, Collection<Ts...>> { using Result = Collection<T, Ts...>; }; template<class Q, class X> struct Erase; template<class Q> struct Erase<Q, Collection<>> { using Result = Collection<>;}; template<class Q, class... Tail> struct Erase<Q, Collection<Q, Tail...>> { using Result = Collection<Tail...>;}; template<class Q, class T, class... Tail> struct Erase<Q, Collection<T, Tail...>> { using Result = typename Glue<T, typename Erase<Q, Collection<Tail...>>::Result>::Result;}; template <class X> struct NoDuplicates; template <> struct NoDuplicates<Collection<>> { using Result = Collection<>; }; template <class T, class... Tail> struct NoDuplicates< Collection<T, Tail...> > { private: using L1 = typename NoDuplicates<Collection<Tail...>>::Result; using L2 = typename Erase<T,L1>::Result; public: using Result = typename Glue<T, L2>::Result; }; ///////////////// LOKI //////////////// } 

现在如何使用? 是的,这很简单:


 using Pin1 = Pin<GPIOC, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; using Pin6 = Pin<GPIOC, 1>; int main() { //  Pin1  ,    Pin6      using PinList = Collection<Pin1, Pin2, Pin3, Pin4, Pin1, Pin6> ; using TPins = typename NoDuplicates<PinList>::Result; //  static_assert.        // : Collection<Pin1, Pin2, Pin3, Pin4, Pin1, Pin6> //   : Collection<Pin1, Pin2, Pin3, Pin4> // ,    static_assert(std::is_same<TPins, PinList>::value, ":    ") ; return 0; } 

好吧 如果您不正确地设置了引脚列表,并且偶然在列表中指示了两个相同的引脚,则程序将无法编译,并且编译器将给出错误:“故障:列表中的相同引脚”。


顺便说一句,要确保端口的引脚列表正确,可以使用以下方法:
 //        // PinsPack<Port<GPIOB, 0>, Port<GPIOB, 1> ... Port<GPIOB, 15>> using GpiobPort = typename GeneratePins<15, GPIOB>::type //      using GpioaPort = typename GeneratePins<15, GPIOA>::type int main() { //    :  GPIOA.0  1 Gpioa<0>::Set() ; // GPIOB.1  0 Gpiob<1>::Clear() ; using LcdData = Collection<Gpioa<0>, Gpiob<6>, Gpiob<2>, Gpioa<3>, Gpioc<7>, Gpioa<4>, Gpioc<3>, Gpioc<10>> ; using TPinsLcd = typename NoDuplicates<LcdData>::Result; static_assert(std::is_same<TPinsB, LcdData>::value, ":        LCD") ; // A      LcdData::Write('A'); } 

我们已经在这里写了很多,但是到目前为止,没有一行真正的代码进入微控制器。 如果所有引脚都正确设置,则固件程序如下所示:


 int main() { return 0 ; } 

让我们添加一些代码,并尝试使用Set()方法来设置列表中的引脚。


端口引脚安装方法


让我们继续进行到任务的最后。 最终,有必要实现Set()方法,该方法将根据列表中的Pin自动确定应将哪个值安装到哪个端口。


我们想要的代码
 using Pin1 = Pin<GPIOA, 1>; using Pin2 = Pin<GPIOB, 2>; using Pin3 = Pin<GPIOA, 2>; using Pin4 = Pin<GPIOC, 1>; using Pin5 = Pin<GPIOA, 3>; int main() { PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; //      3   // GPIOA->BSRR = 14 ; // (1<<1) | (1 << 2) | (1 << 3) ; // GPIOB->BSRR = 4 ; // (1 << 2) // GPIOB->BSRR = 2 ; // (1 << 1); } 

因此,我们声明一个包含Pins列表的类,并在其中定义公共静态方法Set()


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; public: __forceinline static void Set(std::size_t mask) { } } ; 

如您所见, Set(size_t mask)方法采用某种值(掩码)。 此掩码是您需要在端口中放入的号码。 默认情况下,它是0xffffffff,这意味着我们希望将所有引脚放置在列表中(最多32个)。 如果在那里传递另一个值,例如7 == 0b111,则仅应安装列表中的前3个引脚,依此类推。 即 蒙版覆盖在“引脚”列表上。


港口清单


为了能够在引脚上安装任何东西,您需要知道这些引脚位于哪个端口上。 每个Pin都绑定到一个特定的端口,我们可以将这些端口从Pin类中拉出并创建这些端口的列表。


我们的引脚分配给不同的端口:


 using Pin1 = Pin<Port<GPIOA>, 1>; using Pin2 = Pin<Port<GPIOB>, 2>; using Pin3 = Pin<Port<GPIOA>, 2>; using Pin4 = Pin<Port<GPIOC>, 1>; using Pin5 = Pin<Port<GPIOA>, 3>; 

这5个引脚只有3个唯一的端口(GPIOA,GPIOB,GPIOC)。 如果我们声明PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> ,那么我们需要从中获取三个端口的列表: Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>>


Pin类包含端口的类型,其简化形式如下所示:


 template<typename Port, uint8_t pinNum> struct Pin { using PortType = Port ; static constexpr uint32_t pin = pinNum ; ... } 

此外,您仍然需要为此列表定义一个结构,它只是一个模板结构,需要使用可变数量的模板参数


 template <typename... Types> struct Collection{} ; 

现在,我们定义唯一端口列表,同时检查引脚列表中是否不包含相同的引脚。 这很容易做到:


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: //      using TPins = typename NoDuplicates<Collection<Ts...>>::Result; //           static_assert(std::is_same<TPins, Collection<Ts...>>::value, ":    ") ; //     using Ports = typename NoDuplicates<Collection<typename Ts::PortType...>>::Result; ... } ; 

继续...


端口列表绕过


收到端口列表后,现在您需要绕过它,并对每个端口进行操作。 用简化的形式,可以说我们必须声明一个函数,该函数将在输入端接收端口列表和引脚列表的掩码。


由于我们必须绕过一个大小不确定的列表,因此该函数将是带有可变数量参数的模板。


当模板中仍然有参数时,我们将“递归”处理,我们将调用具有相同名称的函数。


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: __forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask) { // ,       if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ; } } } 

因此,我们学习了如何绕过端口列表,但是除了绕过端口之外,还需要做一些有用的工作,即在端口中安装一些东西。


 __forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask) { //      auto result = GetPortValue<Port>(mask) ; //      Port::Set(result) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ; } } 

由于mask参数是从外部传递给函数的,因此该方法将在运行时执行。 由于不能保证将常量传递给SetPorts()方法,因此GetValue()方法也将在运行时开始执行。


而且,尽管在使用C ++中的微控制器的输入/输出端口的文章中写道,以类似的方法,编译器确定传递了一个常量并在编译阶段计算了写入端口的值,但我的编译器仅在最大优化的情况下才做了这样的技巧。
我希望在编译时使用任何编译器设置GetValue()执行GetValue()


在这种情况下,我没有在标准中找到编译器应如何领导编译器,但从IAR编译器仅在最大优化级别上执行此操作的事实判断,它很可能不受标准约束或不应被视为constexpr表达式。
如果有人知道,请在评论中写。


为了确保显式传递常数,我们将通过在模板中传递mask来创建其他方法:


 __forceinline template<std::size_t mask, typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>) { using MyPins = PinsPack<Ts...> ; //    compile time,    value    constexpr auto result = GetPortValue<Port>(mask) ; Port::Set(result) ; if constexpr (sizeof ...(Ports) != 0U) { MyPins::template SetPorts<mask,Ports...>(Collection<Ports...>()) ; } } 

因此,我们现在可以遍历Pin列表,从中拔出端口,并创建与它们绑定的唯一端口列表,然后遍历已创建的端口列表并在每个端口中设置所需的
仍然需要计算该


计算必须在端口中设置的值


我们有一个从Pin列表中获得的端口列表,在我们的示例中,这是一个列表: Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>>
您需要使用此列表中的某个元素,例如GPIOA端口,然后在“引脚”列表中找到连接到该端口的所有引脚,并计算要在该端口中安装的值。 然后对下一个端口执行相同操作。


再一次:在我们的例子中,您需要从中获得唯一端口列表的引脚列表如下:
 using Pin1 = Pin<Port<GPIOC>, 1>; using Pin2 = Pin<Port<GPIOB>, 1>; using Pin3 = Pin<Port<GPIOA>, 1>; using Pin4 = Pin<Port<GPIOC>, 2>; using Pin5 = Pin<Port<GPIOA>, 3>; using Pins = PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> ; 

因此,对于GPIOA端口,该值应为(1 << 1 ) | (1 << 3) = 10 (1 << 1 ) | (1 << 3) = 10 ,对于GPIOC端口- (1 << 1) | (1 << 2) = 6 (1 << 1) | (1 << 2) = 6 ,对于GPIOB (1 << 1 ) = 2


计算功能接受请求的端口,并且如果引脚与请求的端口位于同一端口,则必须在掩码中将掩码设置为与列表中单元(1)中此引脚的位置相对应。
用语言来解释不是一件容易的事,最好是直接看一下代码:


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: __forceinline template<class QueryPort> constexpr static auto GetPortValue(std::size_t mask) { std::size_t result = 0; //  ,       // 1. ,        // 2.            // e (.     ), ,  Pin   0  //        10,      //    ( )  (1 << 10)    // 3.    1   // 4.   1-3      pass{(result |= ((std::is_same<QueryPort, typename Ts::PortType>::value ? 1 : 0) & mask) * (1 << Ts::pin), mask >>= 1)...} ; return result; } } ; 

将每个端口的计算值设置为端口


现在我们知道了需要在每个端口中设置的值。 仍然需要完成public Set()方法,该方法对于用户是可见的,因此可以将所有这种经济性称为:


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; __forceinline static void Set(std::size_t mask) { //        SetPorts(Ports(), mask) ; } } 

SetPorts()将使用其他模板方法来保证将mask传输为常量,并将其传递给template属性。


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; //    0xffffffff,      32  __forceinline template<std::size_t mask = 0xffffffffU> static void Set() { SetPorts<mask>(Ports()) ; } } 

最终,我们的Pin列表类如下所示:
 using namespace PinHelper ; template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: using TPins = typename NoDuplicates<Collection<Ts...>>::Result; static_assert(std::is_same<TPins, Collection<Ts...>>::value, ":    ") ; using Ports = typename NoDuplicates<Collection<typename Ts::PortType...>>::Result; template<class Q> constexpr static auto GetPortValue(std::size_t mask) { std::size_t result = 0; auto rmask = mask ; pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & mask) * (1 << Ts::pin), mask>>=1)...}; pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & ~rmask) * ((1 << Ts::pin) << 16), rmask>>=1)...}; return result; } __forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask) { auto result = GetPortValue<Port>(mask) ; Port::Set(result & 0xff) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template SetPorts<Ports...>(Collection<Ports...>(), mask) ; } } __forceinline template<std::size_t mask, typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>) { constexpr auto result = GetPortValue<Port>(mask) ; Port::Set(result & 0xff) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template SetPorts<mask, Ports...>(Collection<Ports...>()) ; } } __forceinline template<typename Port, typename ...Ports> constexpr static void WritePorts(Collection<Port, Ports...>, std::size_t mask) { auto result = GetPortValue<Port>(mask) ; Port::Set(result) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ; } } __forceinline template<std::size_t mask, typename Port, typename ...Ports> constexpr static void WritePorts(Collection<Port, Ports...>) { Port::Set(GetPortValue<Port>(mask)) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<mask, Ports...>(Collection<Ports...>()) ; } } public: static constexpr size_t size = sizeof ...(Ts) + 1U ; __forceinline static void Set(std::size_t mask ) { SetPorts(Ports(), mask) ; } __forceinline template<std::size_t mask = 0xffffffffU> static void Set() { SetPorts<mask>(Ports()) ; } __forceinline static void Write(std::size_t mask) { WritePorts(Ports(), mask) ; } __forceinline template<std::size_t mask = 0xffffffffU> static void Write() { WritePorts<mask>(Ports()) ; } } ; 

结果,整个事情可以如下使用:


 using Pin1 = Pin<GPIOC, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; using Pin6 = Pin<GPIOA, 5>; using Pin7 = Pin<GPIOC, 7>; using Pin8 = Pin<GPIOA, 3>; int main() { //1.   ,     3 ,  : // GPIOA->BSRR = (1 << 1) | (1 << 3) // GPIOB->BSRR = (1 << 1) // GPIOC->BSRR = (1 << 1) | (1 << 2) PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; //   Set<0xffffffffU>() //2.   ,  3 ,  : // GPIOA->BSRR = (1 << 1) // GPIOB->BSRR = (1 << 1) // GPIOC->BSRR = (1 << 1) | (1 << 2) PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5, Pin6>::Set<7>() ; //3.          , //   someRunTimeValue     ,  //  SetPorts   constexpr    PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set(someRunTimeValue) ; using LcdData = PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5, Pin6, Pin7, Pin8> ; LcdData::Write('A') ; } 

可以在这里找到更完整的示例:
https://onlinegdb.com/r1eoXQBRH


性能表现


您还记得吗,我们希望将呼叫转换为3条线路,分别设置为端口A 10,端口B-2和端口C-6


 using Pin1 = Pin<GPIO, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; int main() { //    Pin    : //   GPIOA  10 GPIOA->BSRR = 10 ; // (1<<1) | (1 << 3) ; //   GPIOB  2 GPIOB->BSRR = 2 ; // (1 << 1) //   GPIOC  6 GPIOB->BSRR = 6 ; // (1 << 1) | (1 << 2); PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; return 0; } 

让我们看看优化完全关闭后发生了什么。



我为端口值着色,并调用以绿色将这些值设置为端口。 可以看出,一切都按照我们的预期完成了,编译器为每个端口计算了该值,并简单地调用了将这些值设置为所需端口的函数。
如果安装函数也被内联,那么最后我们得到一个调用,将值写入每个端口的BSRR寄存器。


其实仅此而已。 谁在乎, 代码在这里


这里有一个例子


https://onlinegdb.com/ByeA50wTS

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


All Articles