
大家身体健康!
在上一篇文章中,我答应过写有关如何使用端口列表的文章。
我必须马上说,一切都已经在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() {
关于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> ;
我希望编译器发现这样的错误并通知钢琴家。
我们将检查列表的唯一性,如下所示:
- 在源列表中,创建一个没有重复的新列表,
- 如果源列表的类型和没有重复的列表的类型不匹配,则说明源列表中的Pin相同,因此程序员犯了一个错误。
- 如果它们匹配,那么一切都很好,没有重复项。
为了创建一个没有重复项的新列表,一位同事建议不要重新发明轮子,而是从Loki库中采用这种方法。 我有这种方法并偷了。 与2010年几乎相同,但参数数量可变。
从同事那里借来的代码,该同事从Loki那里借了这个想法 namespace PinHelper { template<typename ... Types> struct Collection { };
现在如何使用? 是的,这很简单:
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() {
好吧 如果您不正确地设置了引脚列表,并且偶然在列表中指示了两个相同的引脚,则程序将无法编译,并且编译器将给出错误:“故障:列表中的相同引脚”。
顺便说一句,要确保端口的引脚列表正确,可以使用以下方法: 我们已经在这里写了很多,但是到目前为止,没有一行真正的代码进入微控制器。 如果所有引脚都正确设置,则固件程序如下所示:
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() ;
因此,我们声明一个包含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:
继续...
端口列表绕过
收到端口列表后,现在您需要绕过它,并对每个端口进行操作。 用简化的形式,可以说我们必须声明一个函数,该函数将在输入端接收端口列表和引脚列表的掩码。
由于我们必须绕过一个大小不确定的列表,因此该函数将是带有可变数量参数的模板。
当模板中仍然有参数时,我们将“递归”处理,我们将调用具有相同名称的函数。
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) {
因此,我们学习了如何绕过端口列表,但是除了绕过端口之外,还需要做一些有用的工作,即在端口中安装一些东西。
__forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t 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...> ;
因此,我们现在可以遍历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;
将每个端口的计算值设置为端口
现在我们知道了需要在每个端口中设置的值。 仍然需要完成public Set()
方法,该方法对于用户是可见的,因此可以将所有这种经济性称为:
template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; __forceinline static void Set(std::size_t mask) {
与SetPorts()
将使用其他模板方法来保证将mask
传输为常量,并将其传递给template属性。
template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ;
最终,我们的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() {
可以在这里找到更完整的示例:
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() {
让我们看看优化完全关闭后发生了什么。

我为端口值着色,并调用以绿色将这些值设置为端口。 可以看出,一切都按照我们的预期完成了,编译器为每个端口计算了该值,并简单地调用了将这些值设置为所需端口的函数。
如果安装函数也被内联,那么最后我们得到一个调用,将值写入每个端口的BSRR寄存器。
其实仅此而已。 谁在乎, 代码在这里 。
这里有一个例子 。
https://onlinegdb.com/ByeA50wTS