使用C ++中的硬件寄存器的10种++方法(例如IAR和Cortex M)

选择最安全的道路
一清子

大家身体健康!

您可能还记得一个大胡子的轶事,也许是一个真实的故事,关于如何向学生询问使用气压计测量建筑物高度的方法。 在我看来,学生列举了大约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; //port mode register, Address offset: 0x00 __IO uint32_t OTYPER; //port output type register, Address offset: 0x04 __IO uint32_t OSPEEDR; //port output speed register, Address offset: 0x08 __IO uint32_t PUPDR; //port pull-up/pull-down register, Address offset: 0x0C __IO uint32_t IDR; //port input data register, Address offset: 0x10 __IO uint32_t ODR; //port output data register, Address offset: 0x14 __IO uint32_t BSRR; //port bit set/reset register, Address offset: 0x18 __IO uint32_t LCKR; //port configuration lock register, Address offset: 0x1C __IO uint32_t AFR[2]; //alternate function registers, Address offset: 0x20-0x24 } GPIO_TypeDef; #define PERIPH_BASE 0x40000000U //Peripheral base address in the alias region #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000U) #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000U) #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 

用简单的语言来说, 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() { //GPIOA  reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE) //  ,        GetIdr<GPIOA>() ; // } //      : struct Port { constexpr Port(GPIO_TypeDef * ptr): port(*ptr) {} GPIO_TypeDef & port ; } //  GPIOA  reinterpret_cast,   //  constexpr      constexpr Port portA{GPIOA}; //    

因此,这种方法的直接使用对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() { //     addr Registers *GpioPort{reinterpret_cast<Registers*>(addr)}; GpioPort->ODR ^= (1 << pinNum) ; } }; int main() { using Led1 = Pin<GPIOA_BASE, 5> ; Led1::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; //    }; int main() { Register Odr{GpioaOdrAddr}; Odr ^= (1 << 5); Register Idr{GpioaIdrAddr}; Idr ^= (1 << 5); // } 

如您所见,您将不得不再次记住所有寄存器的整数地址,或者将它们设置在某个位置,并且还必须存储指向寄存器地址的指针。 但是又不是很好, 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) ; //  } //   struct Port { constexpr Port(Register& ref): register(ref) {} Register & register ; } constexpr Port portA{GpioaOdr}; 

当然,再次有必要为寄存器的地址保留漂亮的内存,或者在单独的文件中的某个位置手动确定寄存器的所有地址...

优点


  • 易用性
  • 使用元编程的能力
  • 可以在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: //       WriteReg,    // ,  ,       __forceinline template <typename T = RegisterType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T>::value>> Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; 

现在,让我们尝试编译我们的测试,看看该测试没有编译,因为Idr寄存器的^=运算符不存在:

  int main() { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr Odr ; Odr ^= (1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr Idr ; Idr ^= (1 << 5) ; //,  Idr    } 

所以,现在有更多优点...

优点


  • 易用性
  • 使用元编程的能力
  • 可以在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) ; //,  Idr    } 

我们再加上一个加号,但不创建对象。 但是继续前进,我们仍然有缺点

方法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>; //      using Otyper = Register<addr + OtyperShift, ReadWriteReg> ; using Ospeedr = Register<addr + OspeedrShift,ReadWriteReg> ; using Pupdr = Register<addr + PupdrShift,ReadWriteReg> ; using Idr = Register<addr + IdrShift, ReadReg> ; using Odr = Register<addr + OdrShift, WriteReg> ; }; int main() { using Gpioa = Gpio<GPIOA_BASE> ; Gpioa::Odr::Xor(1 << 5) ; Gpioa::Idr::Xor((1 << 5) ); //,  Idr    } 

这里的缺点是结构将需要重新注册,并且所有寄存器的偏移都应在某个位置记住并确定。 如果偏移是由编译器而不是由人员设置的,那将是很好的选择,但这是稍后的操作,但是现在,我们将考虑同事建议的另一种有趣的方法。

方法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 ; //   ,     . } } ; using GpioaWrapper = RegisterStructWrapper<GPIOA_BASE, GPIO_TypeDef> ; int main() { GpioaWrapper::Xor(&GPIO_TypeDef::ODR, (1 << 5)) ; GpioaWrapper::Xor(&GPIO_TypeDef::IDR, (1 << 5)) ; // return 0 ; } 

优点


  • 易用性
  • 使用元编程的能力
  • 可以在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) ; //Gpioa::Idr::Xor((1 << 5) ); //,  Idr    

显然,这里所有结构都必须再次重写。 这可以通过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}} ; } } //        class Gpio5 : public Gpio { __forceinline inline static void Xor() { GPIO->ODR ^= 1 << 5 ; } } //     using Led = Gpio5; Led::Xor(); 


更新:奖金。 Phyton上的SVD文件和解析器


忘记添加其他选项。 ARM为每个SVD制造商发布了一个寄存器描述文件。 然后,您可以从中生成带有寄存器描述的C ++文件。 Paul Osborne在GitHub上编译了所有这些文件。 他还编写了Python脚本来解析它们。

仅此而已...我的想象力已耗尽。 如果您仍有想法,请随时。 所有方法的一个例子就在这里。

参考文献


C ++中的类型安全寄存器访问
做事情-从C ++访问硬件
使事情做事-第3部分
做事情-结构叠加

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


All Articles