C ++由于其严格的类型输入,可以在编译阶段为程序员提供帮助。 该中心上已经有很多文章描述了如何使用类型来实现此目标,这很好。 但是我所读的书都有一个缺陷。 与微控制器编程领域中熟悉的++方法和使用CMSIS的C方法进行比较:
some_stream.set (Direction::to_periph) SOME_STREAM->CR |= DMA_SxCR_DIR_0 .inc_memory() | DMA_SxCR_MINC_Msk .size_memory (DataSize::word16) | DMA_SxCR_MSIZE_0 .size_periph (DataSize::word16) | DMA_SxCR_PSIZE_0 .enable_transfer_complete_interrupt(); | DMA_SxCR_TCIE_Msk;
显而易见,C ++方法更具可读性,并且由于每个函数都具有一种特定的类型,因此不会出错。 C语言方法不检查数据的有效性,而是由程序员负责。 通常,仅在调试期间才识别错误。 但是c ++方法不是免费的。 实际上,每个函数都有自己的寄存器访问权限,而在C语言中,由于这些都是常量,因此在编译阶段首先从所有参数中收集掩码,然后将其立即写入寄存器。 接下来,我将描述如何在最小化案例访问的情况下将类型安全性与++相结合。 您会发现它比听起来简单得多。
首先,我将举例说明我的外观。 希望这与已经熟悉的C ++方法没有太大区别。
some_stream.set( dma_stream::direction::to_periph , dma_stream::inc_memory , dma_stream::memory_size::byte16 , dma_stream::periph_size::byte16 , dma_stream::transfer_complete_interrupt::enable );
set方法中的每个参数都是单独的类型,您可以通过该类型了解要在哪个寄存器中写入该值,这意味着在编译过程中可以优化对寄存器的访问。 该方法是可变参数的,因此可以有任意数量的参数,但是必须检查所有参数都属于该外围设备。
早些时候,对于我来说,这项任务似乎相当复杂,直到我观看了有关基于价值的元编程的视频 。 通过这种元编程方法,您可以像编写普通代码一样编写通用算法。 在本文中,我将仅提供解决问题的最必要视频,其中有更多通用的算法。
我将以抽象方式解决此问题,而不是针对特定外围设备。 因此,有几个寄存器字段,我将有条件地将它们写为枚举。
enum struct Enum1 { _0, _1, _2, _3 }; enum struct Enum2 { _0, _1, _2, _3 }; enum struct Enum3 { _0, _1, _2, _3, _4 }; enum struct Enum4 { _0, _1, _2, _3 };
前三个与一个外围有关,第四个与另一个外围有关。 因此,如果将第四枚举的值输入到第一外围的方法中,则应该存在编译错误,最好是可以理解的。 同样,前2个列表将与一个寄存器相关,第三个列表与另一个相关。
由于枚举的值除实际值外不存储任何内容,因此需要另外一种类型,该类型将存储例如掩码,以确定将该枚举写入寄存器的哪一部分。
struct Enum1_traits { static constexpr std::size_t mask = 0b00111; }; struct Enum2_traits { static constexpr std::size_t mask = 0b11000; }; struct Enum3_traits { static constexpr std::size_t mask = 0b00111; }; struct Enum4_traits { static constexpr std::size_t mask = 0b00111; };
仍然可以连接这两种类型。 在这里,该芯片已经对20种标准有用,但它是微不足道的,您可以自己实现。
template <class T> struct type_identity { using type = T; };
最重要的是,您可以使用任何类型的值并将其作为参数传递给函数。 这是基于值的元编程方法的主要组成部分,在这种方法中,您应该尝试将类型信息传递给值,而不是作为模板参数。 在这里,我定义了一个宏,但是在c ++中我是它们的对手。 但是他允许进一步写作。 接下来,我将为一个函数和另一个宏提供链接枚举及其属性,该宏可以减少复制粘贴的数量。
constexpr auto traits(type_identity<Enum1>) { return type_identity<Enum1_traits>{}; } #define MAKE_TRAITS_WITH_MASK(enum, mask_) struct enum##_traits { \ static constexpr std::size_t mask = mask_; \ }; \ constexpr auto traits(type_identity<enum>) { \ return type_identity<enum##_traits>{}; \ }
必须将字段与相应的寄存器关联。 我通过继承选择关系,因为该标准已经具有元函数std::is_base_of
,它将允许您以通用形式定义字段与寄存器之间的关系。 您不能从枚举继承,因此我们从其属性继承。
struct Register1 : Enum1_traits, Enum2_traits { static constexpr std::size_t offset = 0x0; };
寄存器所在的地址存储为从外围开始的偏移量。
在描述外围之前,有必要讨论基于值的元编程中的类型列表。 这是一个相当简单的结构,允许您保存几种类型并按值传递它们。 有点像type_identity
,但是有一些类型。
template <class...Ts> struct type_pack{}; using empty_pack = type_pack<>;
您可以为此列表实现许多constexpr函数。 与著名的Alexandrescu类型列表(Loki库)相比,它们的实现更容易理解。 以下是示例。
外设的第二个重要属性应该是将其定位在特定地址(在微控制器中)并动态传递该地址以进行测试的能力。 因此,外围设备的结构将是样板,并且将在值字段中存储外围设备的特定地址的类型作为参数。 template参数将由构造函数确定。 好了,前面提到的set方法。
template<class Address> struct Periph1 { Periph1(Address) {} static constexpr auto registers = type_pack<Register1, Register2>{}; template<class...Ts> static constexpr void set(Ts...args) { ::set(registers, Address::value, args...); } };
set方法所做的全部工作就是调用一个自由函数,并向其中传递通用算法所需的所有信息。
我将举例说明为外围设备提供地址的类型。
准备了通用算法的所有信息,仍要执行。 我将给出此功能的文字。
template<class...Registers, class...Args> constexpr void set(type_pack<Registers...> registers, std::size_t address, Args...args) {
实现将参数(特定的寄存器字段)转换为type_pack
函数非常简单。 让我提醒您,模板类型列表的省略号显示了用逗号分隔的类型列表。
template <class...Ts> constexpr auto make_type_pack(type_identity<Ts>...) { return type_pack<Ts...>{}; }
为了验证所有参数都与传输的寄存器有关,并因此与特定的外设有关,有必要实现all_of算法。 与标准库类似,该算法接收类型列表和谓词函数作为输入。 我们使用lambda作为函数。
template <class F, class...Ts> constexpr auto all_of(type_pack<Ts...>, F f) { return (f(type_identity<Ts>{}) and ...); }
在这里,第一次使用17标准的扫描表达式 。 正是这种创新极大地简化了喜欢元编程的人们的生活。 在此示例中,函数f应用于列表Ts中的每个类型,将其转换为type_identity
,并且每个调用的结果由I收集。
在static_assert
内部,将应用此算法。 包装在type_identity
中的type_identity
传递给lambda。 在lambda内部,使用了标准的元函数std :: is_base_of,但是由于可以有多个寄存器,因此将根据OR逻辑使用扫描表达式对每个寄存器执行该表达式。 结果,如果至少有一个参数的属性对于至少一个寄存器不是基本的,则static assert
将起作用并显示一条明确的错误消息。 很容易理解错误的出处(将错误的参数传递给set
方法)并进行修复。
稍后将需要的any_of
算法的实现非常相似:
template <class F, class...Ts> constexpr auto any_of(type_pack<Ts...>, F f) { return (f(type_identity<Ts>{}) or ...); }
通用算法的下一个任务是确定需要写入哪些寄存器。 为此,请过滤寄存器的初始列表,并仅保留函数中包含参数的那些寄存器。 我们需要一个filter
算法,该算法采用原始的type_pack
,对列表中的每种类型应用谓词函数,如果谓词返回true,则将其添加到新列表中。
template <class F, class...Ts> constexpr auto filter(type_pack<Ts...>, F f) { auto filter_one = [](auto v, auto f) { using T = typename decltype(v)::type; if constexpr (f(v)) return type_pack<T>{}; else return empty_pack{}; }; return (empty_pack{} + ... + filter_one(type_identity<Ts>{}, f)); }
首先,描述了一个lambda,该lambda在一种类型上执行谓词的功能,如果谓词返回true,则返回type_pack
如果谓词返回false
则返回空的type_pack
。 最后一个优点的另一个新功能在这里有帮助-constexpr if。 其实质是在结果代码中只有一个if分支,第二个抛出。 而且由于不同的类型在不同的分支中返回,如果没有constexpr,将出现编译错误。 再次感谢type_pack
表达式,对列表中每种类型执行此lambda的结果被合并为一个结果type_pack
。 type_pack
的加法运算符没有足够的重载。 它的实现也很简单:
template <class...Ts, class...Us> constexpr auto operator+ (type_pack<Ts...>, type_pack<Us...>) { return type_pack<Ts..., Us...>{}; }
将新算法应用于寄存器列表时,新列表仅保留那些应写入传输参数的寄存器。
下一个需要的算法是foreach
。 它只是将一个函数应用于列表中的每种类型,将其包装在type_identity
。 在此,扫描表达式中使用了逗号运算符,该运算符执行逗号描述的所有动作并返回上一个动作的结果。
template <class F, class...Ts> constexpr void foreach(type_pack<Ts...>, F f) { (f(type_identity<Ts>{}), ...); }
该功能使您可以访问要写入的每个寄存器。 lambda计算要写入寄存器的值,确定要写入的地址,然后直接写入寄存器。
为了计算一个寄存器的值,需要计算该寄存器所属的每个自变量的值,然后将结果通过“或”进行组合。
template<class Register, class...Args> constexpr std::size_t register_value(type_identity<Register> reg, Args...args) { return (arg_value(reg, args) | ...); }
只能为继承该寄存器的参数执行特定字段值的计算。 对于自变量,我们从其属性中提取掩码,并确定寄存器中值与掩码之间的偏移量。
template<class Register, class Arg> constexpr std::size_t arg_value(type_identity<Register>, Arg arg) { constexpr auto arg_traits = traits(type_identity<Arg>{});
您可以自己编写用于确定遮罩偏移的算法,但是我使用了现有的内置函数。
constexpr auto shift(std::size_t mask) { return __builtin_ffs(mask) - 1; }
保留将值写入特定地址的最后一个函数。
inline void write(std::size_t address, std::size_t v) { *reinterpret_cast<std::size_t*>(address) |= v; }
为了测试任务,编写了一个小测试:
这里所写的一切都被组合在一起并编成了螺栓 。 那里的任何人都可以尝试这种方法。 可以看到目标已实现:没有不必要的内存访问。 在编译阶段计算需要写入寄存器的值:
main: mov QWORD PTR Address::value[rip], OFFSET FLAT:arr or QWORD PTR arr[rip], 25 or QWORD PTR arr[rip+8], 4 mov eax, 0 ret
PS:
感谢大家的评论,感谢他们,我对方法进行了一些修改。 您可以在此处看到新选项。
- 删除辅助类型* _traits,可以直接在列表中保存掩码。
enum struct Enum1 { _0, _1, _2, _3, mask = 0b00111 };
- 现在不通过继承进行带有参数的寄存器连接,因为它是一个静态寄存器字段
static constexpr auto params = type_pack<Enum1, Enum2>{};
- 由于连接不再通过继承进行,因此我不得不编写contains函数:
template <class T, class...Ts> constexpr auto contains(type_pack<Ts...>, type_identity<T> v) { return ((type_identity<Ts>{} == v) or ...); }
- 没有多余的类型,所有宏都消失了
- 我通过模板参数将参数传递给方法,以在constexpr上下文中使用它们
- 现在在set方法中,constexpr逻辑与记录本身的逻辑明显分开
template<auto...args> static void set() { constexpr auto values_for_write = extract(registers, args...); for (auto [value, offset] : values_for_write) { write(Address::value + offset, value); } }
- 提取函数在constexpr中分配一个值数组,以写入寄存器。 它的实现与先前的set函数非常相似,不同之处在于它不直接写入寄存器。
- 我必须添加另一个根据lambda函数将type_pack转换为数组的元函数。
template <class F, class...Ts> constexpr auto to_array(type_pack<Ts...> pack, F f) { return std::array{f(type_identity<Ts>{})...}; }