【问题标题】:Reading a struct from a read only memory从只读存储器中读取结构
【发布时间】:2018-09-10 12:31:37
【问题描述】:

我正在开发一个嵌入式系统,其中一些校准数据存储在闪存中。校准数据存储在一个结构体中,该结构体放置在链接器知道要放置在闪存中的特殊部分中:

struct data_block {
    calibration_data mData;
    uint16_t mCheckSum;
};

//Define to compile the fixed flash location for image data
const data_block __attribute__((section (".caldata"))) gCalibrationData{};

其中calibration_data 是另一个包含实际值的 POD 结构。

问题是,如果我现在只写以下内容:

const data_block data{gCalibrationData};

if (CheckSum(&(data.mData)) == data.mCheckSum) {
    //do stuff
} else {
    //Error
}

这总是转到错误分支,即使闪存中的实际校验和是绝对正确的(写这个有点不同使它工作,见下文)。

这当然是可以理解的:编译器看到一个 const 全局对象,它是默认初始化的,所以它知道所有的值,所以我猜它实际上优化了整个 if(如果我通过 debug-printf 数据uint16_t *,我实际上得到了正确的值)。

我认为正确的方式是定义

const volatile data_block __attribute__((section (".caldata"))) gCalibrationData{};

但是,现在我遇到的问题是我无法将 volatile 结构分配给非易失性,即 const data{gCalibrationData}; 无法编译。如果我尝试通过const volatile data_block * 访问也会出现同样的问题。

至少有两种或三种方法可以完成这项工作,但我不喜欢其中任何一种:

  1. gCalibrationData 中删除const(和volatile)限定符。但是,这有点像 hack,因为编译器不够聪明,无法保证我的程序中永远不会触及 gCalibrationData,另一方面,我想保留 const 限定符,因为尝试写入 gCalibrationData通过分配是一个硬错误。
  2. 通过const gCalibrationData * volatile pData 访问gCalibrationData(是的,volatile 正是我的意思)。通过易变的指针访问会强制编译器实际加载数据。同样,这似乎是一种 hack,因为指针本身当然不是易失性的。
  3. data_blockcalibration_data一个赋值运算符,取const volatile &,并在其中逐个字段地赋值。从语言的角度来看,这似乎是正确的,但是每当calibration_data 更改时,我都需要手动编辑赋值运算符。否则会产生难以检测的错误。

我的问题:读取校准数据的正确方法是什么?我的理想标准是:

  • 全局对象本身是const,用于捕获意外写入。
  • 没有未定义的行为
  • 通过将结构直接分配给另一个结构来访问
  • 或者至少这样我就不需要记住在calibration_data 中分配每个原始类型的变量,请参见上面的选项 3。
  • 线程安全加分,尽管在我的具体情况下,只有一个线程读取或写入闪存(所有其他“线程”都是中断)。

【问题讨论】:

  • 您可能需要注意为什么执行const data_block data{gCalibrationData}; 很重要,因为它似乎if (CheckSum(&(gCalibrationData.mData)) == gCalibrationData.mCheckSum) { 应该有效。
  • @Dkrueger 为什么你认为这会起作用?无论如何,我认为如果我的代码通过分配给变量作为中间步骤而被破坏,它会非常脆弱。
  • 确认一下,.caldata这个部分肯定在ROM里吗?
  • @edking 是的,绝对的,通过多种方式确认。
  • @Timo 在那种情况下,我想知道您是否可以通过抽象结构来解决您的问题?如果您将其设为某个模块的私有并且只允许通过函数进行读取访问,您会充分保护它吗?此外,比较 const 结构的两个成员似乎违反直觉,因为由于它们是恒定的,您总是会在事前知道比较的答案?

标签: c++ embedded constants volatile


【解决方案1】:

一种解决方案可能是在单独的源文件中声明一个缓冲区,通知链接器data_block 的大小,然后将gCalibrationData 定义为一个符号,其值为该缓冲区的开头:

data_block.cpp

//no initialization performed here, just used to
//transmit to the linker the information of the size
//and alignment of data_block
extern "C"{//simpler name mangling
[[gnu::section(".caldata")]] volatile
aligned_storage<sizeof(data_block),alignof(data_block)> datablock_buffer;
}

//then we specify that gCalibrationData refers to this buffer
extern const volatile data_block
gCalibrationData [[gnu::alias("datablock_buffer")]];

另外,gCalibrationData 符号的定义可以通过链接描述文件完成:

SECTIONS{
  .caldata : {
    gCalibrationData = . ;
    data_block.o(.caldata)
    }
  }

gCalibrationDatadata_block_buffer 的别名。这不会导致未定义的行为,因为语言允许这样的别名:data_block_buffer provides storage for gCalibrationData

在语义上,extern 说明符用于表示此声明不是gCalibrationData 值的定义。不过alias 属性是链接器符号的定义。

data_block.hpp

extern const volatile data_block gCalibrationData;

//and copy must be enabled for volatile:
struct data_block{
  /*...*/
  data_block(const data_block&) =default; 

  data_block& operator=(const data_block&) =default;

  data_block(const volatile data_block& other){
    //the const cast means: you are responsible not to 
    //perform this operation while performing a rom update.
    memcpy(this,const_cast<const data_block*>(&other);
    }

  data_block& operator=(const volatile data_block& other){
    memmove(this,const_cast<const data_block*>(&other);
    //or memcpy if you are sure a self assignment will never happen.
    return *this;
    }
  };

【讨论】:

  • 原则上听起来不错,但现在我试过了,发生的情况是,当我给对象非默认构造函数时,默认构造函数不再是空操作,而是尝试归零在启动时初始化对象。那是对闪存的写入,这是一个硬故障。这也意味着我之前启动没问题的原因我错了:由于data_block本身是POD,默认构造只是让对象未初始化。
  • 可能是“正确”的方式是按照您建议的构造函数执行的操作,但在独立的 assign_volatile_pod 函数或类似函数中。
  • @Timo,实际发生的是静态变量总是零初始化。默认构造函数不对此负责。我没有想到这一点。所以声明它是否为 const 不应该改变程序语义。但显然,通过将其声明为 const,您可以更改生成的代码。可能编译器认为 const 对象由链接器放置在一个归零的内存段中,而非 const 对象没有放置在归零的内存区域中。因此,对于这些非常量对象,它会生成代码以将内存初始化为零。我更新了答案。
  • 一段时间后我又开始尝试这个了。您的解决方案,对变量本身具有以下声明,有效:const volatile __attribute__((section (".caldata"))) std::aligned_storage_t&lt;sizeof(data_block), alignof(data_block)&gt; gCalibrationDataRaw{}; const volatile data_block &amp; gCalibrationData{*reinterpret_cast&lt;const volatile data_block*&gt;(&amp;gCalibrationDataRaw)}; 现在, reinterpret_cast 仍然需要一个内存,其中(至少在当前运行期间)从未构造过 data_block,所以我不太确定不过,这仍然不是 UB……
  • 无论如何,如果你编辑那个(或者更好的东西,肯定没有UB),我很乐意接受你的回答:)
【解决方案2】:

最实用的方法是丢失const。通过严格阅读该标准,gCalibrationData 不应允许为 const,因为写入 const 对象(无论是谁执行此操作)都会导致未定义的行为。

如果做不到这一点,只需将其定义为 extern const (并且,如果需要安抚链接器,请将非 extern 定义放在它自己的翻译单元中。这将使您进行 const 正确性检查,允许例如,编译器根据校准数据的初始值进行提升优化,同时仍然阻止它在编译时对这些值做出任何特定假设。

【讨论】:

  • 保存在闪存中的全部目的是在复位/断电期间保持数据,因此在编译时甚至不知道初始值。请注意,写入闪存似乎根本不是对变量的写入(内存写入地址,但通过uint16_t * 并在之前对闪存控制器进行了魔法咒语)。所以我仍然想争辩说,从语言的角度来看,变量的正确类型是const volatile,即永远不会被代码更改,但仍然可以自行更改。
  • 我在问题中添加了第三个(仍然难以接受)选项
  • 它不需要是volatile,因为您的代码(间接)控制着对象的运行时更改。重要的部分是extern
  • extern const data_block __attribute__((section (".caldata"))) gCalibrationData{}; 不起作用,它仍然进入错误分支。
猜你喜欢
  • 1970-01-01
  • 2018-05-06
  • 2020-03-20
  • 1970-01-01
  • 1970-01-01
  • 2015-07-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-21
相关资源
最近更新 更多