图 一清子大家身体健康!
您可能还记得一个大胡子的轶事,也许是一个真实的故事,关于如何向学生询问使用气压计测量建筑物高度的方法。 在我看来,学生列举了大约20或30种方法,而没有提及老师期望的直接(通过压力差异)。
同样,我想继续讨论将C ++用于微控制器,并考虑使用C ++处理寄存器的方式。 我想指出,要实现对寄存器的安全访问,将没有简单的方法。 我将尝试展示该方法的所有优缺点。 如果您知道更多方法,请将其放入注释中。 因此,让我们开始:
方法1。显而易见,显然不是最好的
最常见的方法(也在C ++中使用)是使用制造商的头文件中的寄存器结构描述。 为了演示,我将使用STM32F411微控制器的两个端口A寄存器(ODR-输出数据寄存器和IDR-输入数据寄存器),以便执行“刺绣”“ Hello world”操作-使LED闪烁。
int main() { GPIOA->ODR ^= (1 << 5) ; GPIOA->IDR ^= (1 << 5) ;
让我们看看这里发生了什么以及该设计如何工作。 微处理器头包含
GPIO_TypeDef
结构和指向该
GPIOA
结构的指针定义。 看起来像这样:
typedef struct { __IO uint32_t MODER;
用简单的语言来说,
GPIO_TypeDef
类型的整个结构会在地址
GPIOA_BASE
放下'
GPIOA_BASE
,当您引用结构的特定字段时,您实际上是在引用此结构的地址+该结构元素的偏移量。 如果删除
#define GPIOA
,则代码如下所示:
((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ; ((GPIO_TypeDef *) GPIOA_BASE)->IDR ^= (1 << 5) ;
关于C ++编程语言,整数地址被转换为
GPIO_TypeDef
结构的指针类型。 但是在C ++中,当使用C转换时,编译器将尝试按以下顺序执行转换:
- const_cast
- static_cast
- const_cast旁边的static_cast,
- reinterpret_cast
- const_cast旁边的reinterpret_cast
即 如果编译器无法使用const_cast转换类型,则它将尝试应用static_cast等等。 结果,该调用:
((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ;
没有什么像:
reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE)->ODR ^= (1 << 5) ;
实际上,对于C ++应用程序,将结构“拉”到地址上是正确的,如下所示:
GPIO_TypeDef * GPIOA{reinterpret_cast<GPIO_TypeDef *>(GPIOA_BASE)} ;
在任何情况下,由于类型转换,对于C ++而言,这种方法都有很大的缺点。 其原因在于,既不能在
constexpr
构造函数和函数中也不能在模板参数中使用
reinterpret_cast
,这大大减少了微控制器使用C ++功能的可能性。
我将举例说明。 可以这样做:
struct Test { const int a; const int b; } ; template<Test* mystruct> constexpr const int Geta() { return mystruct->a; } Test test{1,2}; int main() { Geta<&test>() ; }
但是您还不能这样做:
template<GPIO_TypeDef * mystruct> constexpr volatile uint32_t GetIdr() { return mystruct->IDR; } int main() {
因此,这种方法的直接使用对C ++的使用施加了很大的限制。 使用语言工具,我们将无法在ROM中找到要使用
GPIOA
的对象,也无法利用此类对象的元编程优势。
另外,一般来说,这种方法并不安全(就像我们的西方伙伴所说的那样)。 毕竟,很可能会做出一些非乐趣
结合以上内容,我们总结:
优点
- 使用制造商的标题(已检查,没有错误)
- 您无需承担其他额外的手势和费用
- 易用性
- 每个人都知道并理解这种方法。
- 无开销
缺点
- 元编程的有限使用
- 无法在constexpr构造函数中使用
- 在类中使用包装器时,RAM的额外消耗是指向此结构的对象的指针
- 你可以变得愚蠢
现在让我们看一下方法2
方法2:残酷
显然,每个嵌入式编程人员都牢记所有微控制器的所有寄存器的地址,因此您可以始终使用以下方法,该方法从第一个开始:
*reinterpret_cast<volatile uint32_t *>(GpioaOdrAddr) ^= (1 <<5) ; *reinterpret_cast<volatile uint32_t *>(GpioaIdrAddr) ^= (1 <<5) ;
在程序中的任何位置,您始终可以将转换调用到
volatile uint32_t
寄存器地址,并在其中至少安装一些内容。
这里没有特别的好处,但是给那些缺点带来了使用上的不便,并且需要自己将每个寄存器的地址写入一个单独的文件中。 因此,我们转向方法3。
方法3。明显,显然更正确
如果通过结构字段访问寄存器,则可以使用整数结构地址代替结构对象的指针。 结构的地址位于制造商的头文件中(例如,用于GPIOA的GPIOA_BASE),因此您无需记住它,但是可以在模板和constexpr表达式中使用它,然后将结构“覆盖”到该地址。
template<uint32_t addr, uint32_t pinNum> struct Pin { using Registers = GPIO_TypeDef ; __forceinline static void Toggle() {
从我的角度来看,没有什么特别的缺点。 原则上是一种工作选择。 但是,让我们看看其他方式。
方法4。外包装
对于可以理解的代码的鉴赏家,您可以在寄存器上进行包装,以便于访问它们并看上去“美丽”,构造一个构造函数,重新定义运算符:
class Register { public: explicit Register(uint32_t addr) : ptr{ reinterpret_cast<volatile uint32_t *>(addr) } { } __forceinline inline Register& operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile uint32_t *ptr;
如您所见,您将不得不再次记住所有寄存器的整数地址,或者将它们设置在某个位置,并且还必须存储指向寄存器地址的指针。 但是又不是很好,
reinterpret_cast
在构造函数中再次发生
一些缺点,以及在第一版和第二版中增加了一个需求,即每个使用的寄存器都需要在RAM中存储指向4个字节的指针。 通常,这不是一种选择。 我们看下面的内容。
方法4,5。 带图案的外包装
我们添加了一些元编程,但是并没有太多好处。 此方法与上一个方法的不同之处仅在于地址未传输到构造函数,而是在模板参数中,当将地址传递给构造函数时,我们在寄存器上节省了一点,这已经很好:
template<uint32_t addr> class Register { public: Register() : ptr{reinterpret_cast<volatile uint32_t *>(addr)} { } __forceinline inline Register &operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile std::uint32_t *ptr; }; int main() { using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5);
因此,相同的耙子,侧视图。
方法5.合理
显然,您需要删除指针,所以我们做同样的事情,只是从类中删除不必要的指针。
template<uint32_t addr> class Register { public: __forceinline Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5);
您可以待在这里思考一下。 此方法立即解决了以前从第一种方法继承的2个问题。 首先,现在我可以在模板中使用指向
Register
对象的指针,其次,可以将其传递给
constexrp
构造函数。
template<Register * register> void Xor(uint32_t mask) { *register ^= mask ; } Register<GpioaOdrAddr> GpioaOdr; int main() { Xor<&GpioaOdr>(1 << 5) ;
当然,再次有必要为寄存器的地址保留漂亮的内存,或者在单独的文件中的某个位置手动确定寄存器的所有地址...
优点
- 易用性
- 使用元编程的能力
- 可以在constexpr构造函数中使用
缺点
- 未使用制造商验证的头文件
- 您必须自己设置寄存器的所有地址
- 您需要创建一个注册类的对象
- 你可以变得愚蠢
很好,但仍有很多缺点...
方法6:比合理更聪明
在以前的方法中,为了访问寄存器,必须创建该寄存器的对象,这是不必要的RAM和ROM浪费,因此我们使用静态方法进行包装。
template<uint32_t addr> class Register { public: __forceinline inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile uint32_t *>(addr) ^= mask; } }; int main() { using namespace Case6 ; using Odr = Register<GpioaOdrAddr>; Odr::Xor(1 << 5); using Idr = Register<GpioaIdrAddr>; Idr::Xor(1 << 5);
一加
- 没有开销。 快速紧凑的代码,与选项1相同(在类中使用包装器时,没有额外的RAM开销,因为没有创建对象,但是使用静态方法而不创建对象)
继续...
方法7:消除愚蠢
显然,我经常在代码中做NON-FUNNY,然后将某些内容写到寄存器中,这实际上并不是要写的。 没关系,但是必须禁止STUPIDness。 我们禁止胡说八道。 为此,我们引入辅助结构:
struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {};
现在我们可以设置要写入的寄存器,并且这些寄存器是只读的:
template<uint32_t addr, typename RegisterType> class Register { public:
现在,让我们尝试编译我们的测试,看看该测试没有编译,因为
Idr
寄存器的
^=
运算符不存在:
int main() { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr Odr ; Odr ^= (1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr Idr ; Idr ^= (1 << 5) ;
所以,现在有更多优点...
优点
- 易用性
- 使用元编程的能力
- 可以在constexpr构造函数中使用
- 快速紧凑的代码,与选项1相同
- 在类中使用包装器时,没有额外的RAM开销,因为没有创建对象,但是使用静态方法而不创建对象
- 你不能做蠢事
缺点
- 未使用制造商验证的头文件
- 您必须自己设置寄存器的所有地址
- 您需要创建一个注册类的对象
因此,让我们消除创建类以保存更多内容的机会
方法8。没有NONSENSE,没有类对象
立即编码:
struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; int main { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr::Xor(1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr::Xor(1 << 5) ;
我们再加上一个加号,但不创建对象。 但是继续前进,我们仍然有缺点
方法9.具有结构集成的方法8
在以前的方法中,仅定义了大小写。 但是在方法1中,所有寄存器都组合为结构,以便您可以通过模块方便地访问它们。 让我们做吧...
namespace Case9 { struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; template<uint32_t addr> struct Gpio { using Moder = Register<addr, ReadWriteReg>;
这里的缺点是结构将需要重新注册,并且所有寄存器的偏移都应在某个位置记住并确定。 如果偏移是由编译器而不是由人员设置的,那将是很好的选择,但这是稍后的操作,但是现在,我们将考虑同事建议的另一种有趣的方法。
方法10.通过指向结构成员的指针将寄存器换行
在这里,我们使用这样的概念作为指向结构成员的指针并对其进行
访问 。
template<uint32_t addr, typename T> class RegisterStructWrapper { public: __forceinline template<typename P> inline static void Xor(PT::*member, int mask) { reinterpret_cast<T*>(addr)->*member ^= mask ;
优点
- 易用性
- 使用元编程的能力
- 可以在constexpr构造函数中使用
- 快速紧凑的代码,与选项1相同
- 在类中使用包装器时,没有额外的RAM开销,因为没有创建对象,但是使用静态方法而不创建对象
- 使用制造商验证的头文件。
- 无需自己设置所有寄存器地址
- 无需创建Register类的对象
缺点
方法10.5。 结合方法9和10
要找出相对于结构开头的寄存器移位,可以使用指向该结构成员的指针:
volatile uint32_t T::*member
,它将返回该结构成员相对于其开头的偏移量(以字节为单位)。 例如,我们具有
GPIO_TypeDef
结构,那么地址
&GPIO_TypeDef::ODR
将为0x14。
我们抓住了这个机会,并使用编译器从方法9计算寄存器的地址:
struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T, volatile uint32_t T::*member, typename RegType> class Register { public: __forceinline template <typename T1 = RegType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; } }; template<uint32_t addr, typename T> struct Gpio { using Moder = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, ReadWriteReg>; using Otyper = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OTYPER, ReadWriteReg>; using Ospeedr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OSPEEDR, ReadWriteReg>; using Pupdr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::PUPDR, ReadWriteReg>; using Idr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::IDR, ReadReg>; using Odr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, WriteReg>; } ;
您可以更灵活地使用寄存器:
using namespace Case11 ; using Gpioa = Gpio<GPIOA_BASE, GPIO_TypeDef> ; Gpioa::Odr::Xor(1 << 5) ;
显然,这里所有结构都必须再次重写。 这可以通过Phyton中的某些脚本在文件输出中自动输入,例如stm32f411xe.h,并在C ++中使用结构。
无论如何,在特定项目中可能有几种不同的工作方式。
红利 我们使用Phyton介绍语言扩展和parsim代码
在C ++中使用寄存器的问题已经存在了一段时间。 人们以不同的方式解决它。 当然,如果该语言在编译时支持诸如重命名类之类的话,那将是很好的。 好吧,让我们说,如果是这样的话:
template<classname = [PortName]> class Gpio[Portname] { __forceinline inline static void Xor(const uint32_t mask) { GPIO[PortName]->ODR ^= mask ; } }; int main() { using GpioA = Gpio<"A"> ; GpioA::Xor(5) ; }
但是很遗憾,这种语言不支持。 因此,人们使用的解决方案是使用Python解析代码。 即 引入了一些语言扩展。 使用此扩展名的代码被馈送到Python解析器,该解析器将其转换为C ++代码。 这样的代码看起来像这样:(示例来自modm库;
这是完整的源代码 ):
%% set port = gpio["port"] | upper %% set reg = "GPIO" ~ port %% set pin = gpio["pin"] class Gpio{{ port ~ pin }} : public Gpio { __forceinline inline static void Xor() { GPIO{{port}}->ODR ^= 1 << {{pin}} ; } }
更新:奖金。 Phyton上的SVD文件和解析器
忘记添加其他选项。 ARM为每个SVD制造商发布了一个寄存器描述文件。 然后,您可以从中生成带有寄存器描述的C ++文件。 Paul Osborne在
GitHub上编译了所有这些文件。 他还编写了Python脚本来解析它们。
仅此而已...我的想象力已耗尽。 如果您仍有想法,请随时。 所有方法的一个例子
就在这里。参考文献
C ++中的类型安全寄存器访问做事情-从C ++访问硬件使事情做事-第3部分做事情-结构叠加