您的常量存储在CortexM微控制器上的什么位置(以C ++ IAR编译器为例)

我教我的学生如何使用STM32F411RE微控制器,板上有512 kB的ROM和128 kB的RAM。
通常,在该微控制器上,将程序写入ROM存储器,而在RAM中,经常需要更改数据才能在ROM中设置常量。
在STM32F411RE ROM微控制器中,存储器位于地址为0x08000000 ... 0x0807FFFF的 RAM0x20000000 ... 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 ; } 

您也可以尝试回答以下问题: ROMRAM中的常数myConstInROM在哪里?

如果您回答了ROM中的一个问题,我向您表示祝贺,实际上您很可能是错的,则常量通常位于RAM中,并弄清楚如何正确正确地将常量放入ROM-欢迎使用。

引言


首先,题外话,何必为此烦恼。
在开发用于符合IEC 61508-3:2010或GOST IEC 61508-3-2018的国内等效标准的测量设备的安全关键软件时,必须考虑许多对常规软件不重要的方面。

该标准的主要信息是软件必须检测到任何会影响系统可靠性的故障,并将系统置于“崩溃”模式

除了明显的机械故障(例如传感器故障或电子组件的退化和故障)之外,还应检测到由软​​件环境故障(例如RAMROM微控制器)引起的错误。

并且,如果在前两种情况下,仅可能以相当混乱的间接方式检测到错误(存在确定传感器故障的算法,例如, 评估电阻热转换器状态方法 ),那么在软件环境出现故障的情况下,可以更容易地做到这一点,例如,内存故障可以通过简单的数据完整性检查进行验证。 如果违反了数据完整性,则将其解释为内存故障。

如果长时间将数据保留在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中的所有地址空间都是单个的,并且代码和数据可以位于任何地方。 使用链接器设置,可以设置ROMRAM地址的区域。 IAR在ROM区域中定位.text代码段

目标文件和段


上面,我提到了代码段,让我们看看它是什么。

将为每个编译模块创建一个单独的目标文件,其中包含以下信息:

  • 代码和数据段
  • DWARF调试信息
  • 人物表

我们对代码和数据感兴趣。 就是这样的元素,其中包含必须放置在内存中物理地址处的一段代码或数据。 一个段可以包含几个片段,通常每个变量或函数一个片段。 一个段可以放在ROMRAM中
每个段都有一个名称和一个定义其内容的属性。 该属性用于在链接器的配置中定义一个段。 例如,属性可以是:

  • 代码-可执行代码
  • 只读-常量变量
  • 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 }; //   .rodata  .data_init (  )  ROM: place in RAM_region { readwrite, //   .data, .bss,  .noinit block STACK }; //  STACK  HEAP  RAM 

也就是说,所有标有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; } //     constexpr int Case1 = 1 ; //  (      ) const int Case2 = 2; int main() { //  . const int Case3 = 3 ; // . static const int Case4 = 4 ; //      ,     . constexpr int Case5 = Case1 + 5 ; //     . static constexpr int Case6 = 6 ; foo(Case1,Case2,Case3,Case4,Case5,Case6); return 1; } 

这都是常数数据。 但是,对于上述任何一种创建规则都适用,局部变量是在堆栈上创建的。 因此,使用我们的链接器设置,它应该像这样:

  • 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中,您需要遵循简单的规则:
  1. 放置代码的.text段应位于ROM中。 它是在链接器设置中配置的。
  2. 全局常量和静态常量所在的.rodata段必须位于ROM中。 它是在链接器设置中配置的。
  3. 该常数必须是全局或静态的。
  4. 常量变量类的属性不得可变
  5. 对象的初始化必须是静态的,即其对象将是常量的类的构造函数必须是constexpr或根本没有定义(没有动态初始化)
  6. 如果可能,如果您确定对象应该存储在ROM中而不是const中,请使用constexpr

关于constexpr和constexpr构造函数的几句话。 const和constexpr之间的主要区别是const变量的初始化可以延迟到运行时。 constexpr变量必须在编译时初始化。
所有constexpr变量均为const类型。

constexpr构造函数的定义必须满足以下要求:
  • 一个类不能具有虚拟基类。
     struct D2 : virtual BASE { //error, D2 must not have virtual base class. constexpr D2() : BASE(), mem(55) { } private: int mem; }; 
  • 该类的每个参数类型都必须是文字类型。
  • 构造函数主体必须为= delete= default 。 或满足以下要求:
  • 构造函数主体中没有try catch块。
  • 构造函数主体可以使用nullptr
  • 构造函数主体可以使用static_assert
  • 在构造函数的主体中,可以使用不定义类或枚举的typedef
  • 构造函数主体可以使用以下指令和声明
  • 类或基类的每个非静态成员都必须初始化。
  • 用于初始化基类的类成员和子对象的非静态元素的类或基类的构造函数必须是constexpr
  • 所有非静态数据元素的初始化程序都必须是constexpr
  • 初始化类成员时,所有类型转换必须在常量表达式中有效。 例如,不允许使用reinterpret_cast并将其从void*强制转换为另一种类型的指针

隐式默认构造函数是constexpr构造函数。 现在让我们看一些例子:

例子1. ROM中的对象
 class Test { private: int i; public: Test() {} ; int Get() const { return i + 1; } } ; const Test test; //  ROM.    . i  0  . int main() { std::cout << test.Get() << std::endl ; return 0; } 


最好不要这样写,因为一旦您决定初始化属性i,对象就会飞入RAM

例子2. RAM中的一个对象
 class Test { private: int i = 1; // i.       constexpr . public: Test() {} ; //       ,       ,  constexpr int Get() const { return i + 1; } } ; const Test test; //  RAM. i     int main() { std::cout << test.Get() << std::endl ; return 0; } 



例子3. RAM中的一个对象
 class Test { private: int i; public: Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10); //  RAM. i     int main() { std::cout << test.Get() << std::endl ; return 0; } 



例子4. ROM中的对象
 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10); //  ROM. i     constexpr  int main() { std::cout << test.Get() << std::endl ; return 0; } 



例子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); //  RAM.    std::cout << test.Get() << std::endl ; return 0; } 



例子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); //  ROM.    std::cout << test.Get() << std::endl ; return 0; } 



例子7.编译错误
 class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() // Get  ,  ,       (i),     .   ,       . { return i + 1; } } ; const Test test(10); int main() { std::cout << test.Get() << std::endl ; return 0; } 



例子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); //  ROM. i     constexpr , j   constexpr  ITest int main() { std::cout << test.Give() << std::endl ; return 0; } 



例子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); //    RAM. class Test: public ITest { private: int i; TestImpl & obj; //    public: constexpr Test(int value, TestImpl & ref): i(value), obj(ref), ITest(value+1) { } ; int Get() const override { return i + 1; } bool Set() const { obj.Set(100) ; //     return true; } } ; constexpr Test test(10, testImpl); //  ROM.     constexpr  int main() { std::cout << test.Set() << std::endl ; return 0; } 



例子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; //    public: constexpr Test(int value, TestImpl & ref): i(value), obj(ref),ITest(value+1) { } ; int Get() const override { return i + 1; } bool Set() const { obj.Set(100) ; //     return true; } } ; int main() { static TestImpl testImpl(1); //  static constexpr Test test(10, testImpl); //    ROM.     constexpr  std::cout << test.Set() << std::endl ; return 0; } 



例子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; //    public: constexpr Test(int value, TestImpl & ref): i(value), obj(ref),ITest(value+1) { } ; int Get() const override { return i + 1; } bool Set() const { obj.Set(100) ; //     return true; } } ; int main() { static TestImpl testImpl(1); //  const Test test(10, testImpl); //    RAM. std::cout << test.Set() << std::endl ; return 0; } 



例子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; //   TestImpl public: constexpr Test(int value): i(value), obj(TestImpl(value)), //   constexpr   TestImpl ITest(value+1) { } ; int Get() const { return i + 1; } bool Set() const { obj.Set(100) ; //     return true; } } ; int main() { static TestImpl testImpl(1); //  static const Test test(10); //   std::cout << test.Set() << std::endl ; return 0; } 



例子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) //   constexpr { } int Get() const override { return j + 10; } void Set(int value) //   ,  k  j.       ROM,        RAM { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl obj; public: constexpr Test(int value): i(value), obj(TestImpl(value)), // constexpr     obj,    obj  .rodata . ITest(value+1) { } ; int Get() const { return i + 1; } bool Set() const { obj.Set(100) ; //        constexpr     return true; } } ; int main() { static TestImpl testImpl(1); //  static const Test test(10); //   std::cout << test.Set() << std::endl ; return 0; } 



例子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 //   const { //do something } } ; class Test: public ITest { private: int i; const TestImpl obj; // ,    constexpr  public: constexpr Test(int value): i(value), obj(TestImpl(value)), ITest(value+1) { } ; int Get() const { return i + 1; } bool Set() const { obj.Set(100) ; //   return true; } } ; int main() { //static TestImpl testImpl(1); //  static const Test test(10); //    ROM.     constexpr  std::cout << test.Set() << std::endl ; return 0; } 



最后,一个包含数组的常量对象,通过constexpr函数进行数组初始化。
 class Test { private: int k[100]; constexpr void InitArray() { int i = 0; for(auto& it: k) { it = i++ ; } } public: constexpr Test(): k() { InitArray(); // constexpr     } int Get(int index) const { return k[index]; } } ; int main() { static const Test test; //    ROM.     ,  constexpr . std::cout << test.Get(10) << std::endl ; return 0; } 


参考文献:
IAR C / C ++开发指南
Constexpr构造函数(C ++ 11)
constexpr(C ++)

PS。
Valdaros进行非常有用的讨论后,您需要添加以下点切线常量。根据C ++标准和此文档N1076.pdf

1.在常量对象的生命周期中对它的任何更改(一个类的可变成员除外)都会导致未定义行为。

  const int ci = 1 ; int* iptr = const_cast<int*>(&ci); //UB,       *iptr = 2 ; 

  int i = 1; const int* ci = &i ; int* iptr = const_cast<int *> (ci); //   *iptr = 2 ; // UB,   i 

2.问题是,这仅在一个常量对象的整个生命周期内有效,而在构造函数和析构函数中则无效。因此,这样做是完全合法的:

 class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1; //       0  1 test1 = value ; //           } const Test test; int main() { test1->i = 2; //          2. std::cout << &test << std::endl; } 

并且它被认为是合法的。尽管事实上我们使用了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; //       0  1 test1 = value ; //           } constexpr Test test; // / Error[Pe2400]: calling the default constructor for "Test" does not produce a constant value main.cpp 151 int main() { test1->i = 2; //          2. std::cout << &test << std::endl; } 

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


All Articles