我教我的学生如何使用STM32F411RE微控制器,板上有512 kB的ROM和128 kB的RAM。
通常,在该微控制器上,将程序写入
ROM存储器,而在
RAM中,经常需要更改数据才能在
ROM中设置常量。
在STM32F411RE
ROM微控制器中,存储器位于地址为
0x08000000 ... 0x0807FFFF的 RAM和
0x20000000 ... 0x2001FFFF的 RAM中 。并且,如果所有链接器设置都正确,那么学生将以这样简单的代码来计算其常量位于
ROM中 :
class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
您也可以尝试回答以下问题:
ROM或
RAM中的常数myConstInROM在哪里?
如果您回答了
ROM中的一个问题,我向您表示祝贺,实际上您很可能是错的,则常量通常位于
RAM中,并弄清楚如何正确正确地将常量放入
ROM-欢迎使用。
引言
首先,题外话,何必为此烦恼。
在开发用于符合IEC 61508-3:2010或
GOST IEC 61508-3-2018的国内等效标准的测量设备的安全关键软件时,必须考虑许多对常规软件不重要的方面。
该标准的主要信息是软件必须检测到任何会影响系统可靠性的故障,并将系统置于“崩溃”模式除了明显的机械故障(例如传感器故障或电子组件的退化和故障)之外,还应检测到由软件环境故障(例如
RAM或
ROM微控制器)引起的错误。
并且,如果在前两种情况下,仅可能以相当混乱的间接方式检测到错误(存在确定传感器故障的算法,例如,
评估电阻热转换器状态的
方法 ),那么在软件环境出现故障的情况下,可以更容易地做到这一点,例如,内存故障可以通过简单的数据完整性检查进行验证。 如果违反了数据完整性,则将其解释为内存故障。
如果长时间将数据保留在RAM中而不进行检查和更新,则随着时间的流逝,由于
RAM故障而在它们身上发生某些事情的可能性会更高。 一个示例是一些用于计算温度的校准系数,这些校准系数在出厂时已设置并写入外部EEPROM,在启动时会被读取并写入
RAM ,直到电源关闭为止。 在使用寿命中,温度传感器可以在整个校准间隔的整个周期内工作,最长可达3-5年。 显然,必须保护此类
RAM数据并定期检查其完整性。
但是,还有一些数据,例如仅出于可读性声明的常量,LCD驱动器,SPI或I2C的对象(不应更改)仅创建一次,并且在关闭电源之前不会删除。
此数据最好保存在
ROM中 。 从技术角度来看,它更可靠,并且检查起来要简单得多,足以在某些低优先级任务中定期读取所有永久存储器的校验和。 如果校验和不匹配,则只需报告
ROM故障,诊断系统就会显示事故。
如果此数据位于
RAM中 ,则由于尚不清楚不可变数据在RAM中的何处以及可变的位置这一事实,确定其完整性将是有问题的,甚至是不可能的,链接器会根据需要放置它,并用校验和保护每个RAM对象,就像偏执狂。
因此,最简单的方法是100%确保常量数据在
ROM中 。 我想尝试解释如何做。 但是首先您需要讨论ARM中内存的组织。
记忆组织
如您所知,ARM内核具有哈佛架构-数据和代码总线是分离的。 通常,这意味着假定存在用于程序的单独存储器和用于数据的单独存储器。 但是事实是ARM是一种经过改进的哈佛架构,即 对存储器的访问在一条总线上进行,并且存储器管理设备已经使用控制信号提供了总线的分离:读取,写入或选择存储区域。
因此,数据和代码可以在同一存储区中。 可以在此单个地址空间中找到
ROM存储器,
RAM和外围设备。 这意味着实际上,即使依赖于编译器和链接器,代码和数据也可以获得。
因此,为了区分
ROM(闪存)和
RAM的存储区域,通常在链接器设置中进行指示,例如在IAR 8.40.1中,它看起来像这样:
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000; define symbol __ICFEDIT_region_ROM_end__ = 0x0807FFFF; define symbol __ICFEDIT_region_RAM_start__ = 0x20000000; define symbol __ICFEDIT_region_RAM_end__ = 0x2001FFFF; define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__]; define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];
该微控制器中
的RAM位于
0x20000000 ... 0x2001FFF,而
ROM位于
0x008000000 ... 0x0807FFFF 。
您可以轻松地将起始地址ROM_start更改为RAM地址,例如将RAM_start和结束地址ROM_end__更改为RAM_end__,您的程序将完全位于RAM中。
您甚至可以执行相反的操作,并在
ROM存储区中指定
RAM ,尽管无法正常运行,但您的程序将成功汇编并刷新:)
某些微控制器,例如AVR,最初具有用于程序存储器,数据存储器和外围设备的单独地址空间,因此,这些技巧在那里不起作用,并且默认情况下将程序写入
ROM 。
CortexM中的所有地址空间都是单个的,并且代码和数据可以位于任何地方。 使用链接器设置,可以设置ROM和RAM地址的区域。 IAR在ROM区域中定位.text代码段
目标文件和段
上面,我提到了代码段,让我们看看它是什么。
将为每个编译模块创建一个单独的目标文件,其中包含以下信息:
我们对代码和数据
段感兴趣。
段就是这样的元素,其中包含必须放置在内存中物理地址处的一段代码或数据。 一个段可以包含几个片段,通常每个变量或函数一个片段。 一个段可以放在
ROM和
RAM中 。
每个段都有一个名称和一个定义其内容的属性。 该属性用于在链接器的配置中定义一个段。 例如,属性可以是:
- 代码-可执行代码
- 只读-常量变量
- readwrite-初始化变量
- zeroinit-零初始化变量
当然,还有其他类型的段,例如包含调试信息的段,但是我们只会对那些包含来自应用程序的代码或数据的段感兴趣。
通常,段是最小的可链接块。 但是,如果需要,链接器还可以指示更小的块(片段)。 我们不会考虑此选项,而是会处理细分。
在编译过程中,数据和功能位于不同的段中。 并且在链接期间,链接器将实际的物理地址分配给不同的段。 IAR编译器具有预定义的段名称,我将在下面提供其中的一些名称:
- .bss-包含初始化为0的静态和全局变量
- .CSTACK-包含程序使用的堆栈
- .data-包含静态和全局初始化变量
- .data_init-如果使用了链接器的初始化指令,则包含.data节中数据的初始值
- HEAP-包含用于承载动态数据的堆
- .intvec-包含一个中断向量表
- .rodata-包含常量数据
- .text-包含程序代码
为了了解常量的位置,我们仅对细分感兴趣
.rodata-存储常量的段,
.data-存储所有初始化的静态和全局变量的段,
.bss-存储所有以零(0)初始化的静态和全局
.data变量的段,
.text-用于存储代码的段。
实际上,这意味着如果您定义变量
int val = 3
,则变量本身将由编译器定位在
.data段中并标有
readwrite属性,并且数字3可以放在
.text段或
.rodata段中,或者将
应用.data_init中链接程序的特殊指令,并且该指令也将其标记为
只读 。
.rodata段包含常量数据,并包含常量变量,字符串,聚合文字等。 并且
此段可以放置在内存中的任何位置。现在,可以更清楚地了解链接器设置中的规定以及原因:
place in ROM_region { readonly };
也就是说,所有标有
readonly属性的数据都应放在ROM_region中。 因此,来自不同段但标有readonly属性的数据可以进入ROM。
嗯,这意味着所有常量都必须位于ROM中,但是为什么在本文开头的代码中,常量对象仍然位于RAM中? class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
恒定数据
在澄清情况之前,让我们首先回顾一下,全局变量是在共享内存中创建的,局部变量即 在“正常”函数中声明的变量在堆栈或寄存器中创建,静态局部变量也在共享内存中创建。
这在C ++中是什么意思。 让我们看一个例子:
void foo(const int& C1, const int& C2, const int& C3, const int& C4, const int& C5, const int& C6) { std::cout << C1 << C2 << C3 << C4 << C5 << C6 << std::endl; }
这都是常数数据。 但是,对于上述任何一种创建规则都适用,局部变量是在堆栈上创建的。 因此,使用我们的链接器设置,它应该像这样:
- Case1全局常量必须位于ROM中 。 在.rodata段中
- Case2全局常数必须位于ROM中 。 在.rodata段中
- Case3局部常量必须位于RAM中 (该常量是在STACK段中的堆栈上创建的)
- Case4静态常数必须位于ROM中 。 在.rodata段中
- Case5局部常量必须位于RAM中 (这是一个有趣的情况,但与情况3完全相同。)
- Case6静态常数必须位于ROM中 。 在.rodata段中
现在,让我们看一下调试信息和生成的映射文件。 调试器显示这些常量位于哪个地址。

如我之前所说,地址0x0800 ...这些是
ROM地址,而0x200 ...这些是
RAM 。 让我们看看编译器在哪些段中分发了这些常量:
.rodata const 0x800'4e2c 0x4 main.o //Case1 .rodata const 0x800'4e30 0x4 main.o //Case2 .rodata const 0x800'4e34 0x4 main.o //Case4 .rodata const 0x800'4e38 0x4 main.o //Case6
四个全局和静态常量属于
.rodata段,并且两个局部变量没有属于映射文件,因为它们是在堆栈上创建的,并且其地址与堆栈的地址相对应。 CSTACK段开始于0x2000'2488,结束于0x2000'0488。 从图片中可以看到,常量只是在堆栈的开头创建的。
编译器将全局和静态常量放在.rodata段中,其位置在链接器设置中指定。
值得注意的另一个重要点是
初始化 。 全局和静态变量(包括常量)必须初始化。 这可以通过几种方式来完成。 如果它是
.rodata段中的
常量 ,则初始化在编译阶段进行,即 该值将立即写入常量所在的地址。 如果这是一个常规变量,则可以通过将值从ROM存储器复制到全局变量的地址来进行初始化:
例如,如果定义了全局变量
int i = 3
,则编译器在
.data数据段中定义了它,链接器将其设置为0x20000000:
.data inited 0x2000'0000
,
其初始化值(3)将位于
.rodata段中,地址为0x8000190:
Initializer bytes const 0x800'0190
如果您编写此代码:
int i = 3; const int c = i;
显然,仅在初始化全局变量
i
之后(即在运行时)才初始化全局常数
。 在这种情况下,常量将位于
RAM中现在,如果我们回到我们的
最初的例子 class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
我们问自己:编译器在哪个段中定义了常量对象
myConstInROM
? 我们得到了答案:常量将位于
.bss段中
,其中包含初始化为零(0)的静态和全局变量。
.bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4
怎么了 因为在C ++中,声明为常量且需要动态初始化的数据对象位于读写存储器中,所以它将在创建时进行初始化。
在这种情况下,将发生动态初始化,即
const WantToBeInROM myConstInROM(10)
,然后编译器将此对象放在
.bss段中,首先初始化所有字段0,然后在创建常量对象时称为构造函数,以将字段
i
初始化
i
值10。
我们如何使编译器将对象放置在
.rodata段中? 这个问题的答案很简单,您应该始终执行静态初始化。 您可以这样操作:
1.在我们的示例中,可以看到,原则上,编译器可以将动态初始化优化为静态,因为构造函数非常简单。 对于编译器的IAR,可以使用
__ro_placement属性标记常量。
__ro_placement const WantToBeInROM myConstInROM
使用此选项,编译器会将变量放置在ROM中的地址处:
myConstInROM 0x800'0144 0x4 Data
显然,这种方法不是通用的,而且通常非常具体。 因此,我们继续使用正确的方法:)
2.它是一个
constexpr
构造函数。 我们立即告诉编译器使用静态初始化,即 在编译阶段,整个对象将事先被完全“计算”,并且其所有字段都将为已知。 我们需要做的就是将constexpr添加到构造函数中。
对象飞到ROM class WantToBeInROM { private: int i; public: constexpr WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
因此,为了确保常量对象位于ROM中,您需要遵循简单的规则:
- 放置代码的.text段应位于ROM中。 它是在链接器设置中配置的。
- 全局常量和静态常量所在的.rodata段必须位于ROM中。 它是在链接器设置中配置的。
- 该常数必须是全局或静态的。
- 常量变量类的属性不得可变
- 对象的初始化必须是静态的,即其对象将是常量的类的构造函数必须是constexpr或根本没有定义(没有动态初始化)
- 如果可能,如果您确定对象应该存储在ROM中而不是const中,请使用constexpr
关于constexpr和constexpr构造函数的几句话。 const和constexpr之间的主要区别是const变量的初始化可以延迟到运行时。 constexpr变量必须在编译时初始化。
所有constexpr变量均为const类型。constexpr构造函数的定义必须满足以下要求:
隐式默认构造函数是constexpr构造函数。 现在让我们看一些例子:
例子1. ROM中的对象 class Test { private: int i; public: Test() {} ; int Get() const { return i + 1; } } ; const Test test;
最好不要这样写,因为一旦您决定初始化属性i,对象就会飞入RAM
例子2. RAM中的一个对象 class Test { private: int i = 1;
例子3. RAM中的一个对象 class Test { private: int i; public: Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10);
例子4. ROM中的对象 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10);
例子5. RAM中的一个对象 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { const Test test(10);
例子6. ROM中的对象 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { static const Test test(10);
例子7.编译错误 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get()
例子8. ROM中的一个对象,它继承自一个抽象类 class ITest { private: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class Test: public ITest { private: int i; public: constexpr Test(int value): i(value), ITest(value+1) {} ; int Get() const override { return i + 1; } } ; const Test test(10);
例子9. ROM中的对象聚合位于RAM中的对象 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; TestImpl testImpl(1);
例子10. ROM中相同但静态的对象 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj;
例子11.现在常量对象是非静态的,因此在RAM中 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj;
例子12.编译错误 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl obj;
例子13.编译错误 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value)
例子14. ROM中的对象 class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) const
最后,一个包含数组的常量对象,通过constexpr函数进行数组初始化。 class Test { private: int k[100]; constexpr void InitArray() { int i = 0; for(auto& it: k) { it = i++ ; } } public: constexpr Test(): k() { InitArray();
参考文献:
IAR C / C ++开发指南Constexpr构造函数(C ++ 11)constexpr(C ++)PS。
与Valdaros进行非常有用的讨论后,您需要添加以下点切线常量。根据C ++标准和此文档N1076.pdf1.在常量对象的生命周期中对它的任何更改(一个类的可变成员除外)都会导致未定义行为。即
const int ci = 1 ; int* iptr = const_cast<int*>(&ci);
int i = 1; const int* ci = &i ; int* iptr = const_cast<int *> (ci);
2.问题是,这仅在一个常量对象的整个生命周期内有效,而在构造函数和析构函数中则无效。因此,这样做是完全合法的: class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1;
并且它被认为是合法的。尽管事实上我们使用了constexpr构造函数以及其中的constexpr函数。该对象直接进入RAM。为避免这种情况,请使用const-constexpr而不是const,然后会出现编译错误,告诉您某些错误,并且该对象不能为常数。 class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1;