翻译自 Real-Time C++ Efficient Object-Oriented and Template Microcontroller Programming 4th Edition - Kormanyos, Christopher,这书涉及了从C++11 到C++20 的内容,主要介绍使用C++ 的模板、面向对象等特性设计嵌入式程序。书里的示例代码都是公开的:https://github.com/ckormanyos/real-time-cpp
一、实时C++ 入门
1.8 C++ 标准库
std命名空间包含了C++标准库中的所有符号。标准库是一个庞大的类型、函数和类的集合,是C++语言不可或缺的一部分。标准库还包含了一个广泛的通用容器和算法集合,称为标准模板库(STL)。在本书中,我们将大量使用C++标准库和它的STL部分。另见第5.8节和附录A.6-A.8。
LED程序使用C++标准库中的std::uint8_t,这是多种可用固定大小整数类型之一。熟悉C语言C99规范的读者可能有使用<stdint.h>的经验。这个C库文件定义了相同的固定大小整数类型,但位于全局命名空间中。使用C++的固定大小整数类型可以提高可移植性,因为不再需要手动定义和管理潜在非可移植的用户定义类型,如my_uint8、my_uint16等,并且不再需要使用难以阅读的预处理器开关进行管理。有关固定大小整数类型的更多细节,请参见第3.2节和第6.10节。
注:包含cstdint 头文件后可以,uint8_t 会直接定义在全局空间,前面没必要加上std::
注:在这段之前,原文定义了一个用来点灯的类LED,很简单,所以省略了
1.10 底层寄存器访问
使用C++进行微控制器编程需要低级寄存器访问。例如,led类的构造函数和toggle()函数都通过直接内存访问来操作寄存器,以控制LED硬件。有关寄存器操作的进一步讨论,请参见第7章。
特别地,led的成员函数toggle()负责切换LED。
void toggle() const
{// Toggle the LED.*reinterpret_cast<volatile bval_type*>(port)^= bval;
}
模板转换运算符reinterpret_cast
是C++中四种专用转换运算符之一。请参见A.1节中对C++转换运算符的描述。reinterpret_cast
运算符是专为将整数类型转换为指针并返回而设计的。熟悉C语言中底层寄存器访问的读者可以参考下面的等效代码示例:
// C++ 访问寄存器.
*reinterpret_cast<volatile bval_type*>(port) ^= bval;// 等效的C 代码.
*((volatile bval_type*) port) ^= bval
注:这段代码先把整数类型的寄存器地址port 转换成用来访问寄存器的指针类型,然后再解引用、异或赋值
强大的reinterpret_cast
运算符应谨慎使用,因为在C++中,实际上很少需要手动转换指针类型,通常都有更不易错的程序技术选项可用。通过直接内存访问进行微控制器寄存器操作是少数几种需要强制进行整数到指针转换的情况之一。
如上所述,本主题在A.1节中有更详细的介绍。此外,第7章全部内容都涉及处理微控制器寄存器,特别是第7.2节和第7.3节,它们介绍并实现了通用的微控制器寄存器读/写操作抽象层。
1.11 编译时常量
在LED 程序中,寄存器地址使用constexpr 关键字定义为C++ 的广义常量表达式,如下:
namespace mcal
{// 用编译时常量表示寄存器地址.namespace reg{// portb 的地址.constexpr std::uint8_t portb = 0x25U;// 对应portb 中8 个位的掩码.constexpr std::uint8_t bval0 = 1U;constexpr std::uint8_t bval1 = 1U << 1U;constexpr std::uint8_t bval2 = 1U << 2U;constexpr std::uint8_t bval3 = 1U << 3U;constexpr std::uint8_t bval4 = 1U << 4U;constexpr std::uint8_t bval5 = 1U << 5U;constexpr std::uint8_t bval6 = 1U << 6U;constexpr std::uint8_t bval7 = 1U << 7U;}
}
用关键字constexpr
表示的广义常量表达式保证是编译时常量。通常,使用constexpr
被认为优于使用宏常量#define
,因为constexpr
常量表达式具有明确定义的类型信息。有关constexpr
和广义常量表达式的更多信息,请参见第3.8节,以及第7.1节中有关寄存器地址的更多细节。
确保整数值是编译时常量的另一种方法是使用类类型的静态常量成员,在第4.10节中将进一步讨论静态常量整数类成员。使用编译时常量几乎总是有助于优化C++。例如,如前面第1.5节所述,led_b5的构造函数代码等效于以下内容。
const led led_b5
{0x25, // Address of portb.0x20 // Bit-value of portb.5.
}
由于构造函数的参数是编译时常量,编译器可以直接初始化led_b5的成员变量,而不使用堆栈或中间CPU寄存器。这种高效的优化被称为常量折叠,在实时C++编程中经常很有用。第2.6节描述了通过将常量折叠与C++模板结合来进一步提高性能的方法。
注:用constexpr 定义常量的好处我说过几次了,但相比宏常量也不是没有缺点。宏常量可以直接用在条件编译中,比如#ifdef,而硬要用constexpr 常量实现类似效果会让代码变得非常晦涩