将易失性数组转换为非易失性数组

Posted

技术标签:

【中文标题】将易失性数组转换为非易失性数组【英文标题】:Cast volatile array to non volatile array 【发布时间】:2019-11-07 00:34:33 【问题描述】:

我有一个全局易失性无符号字符数组volatile unsigned char buffer[10],数据在中断中写入其中。我有一个函数,它接受一个 unsigned char * 并将该值存储到硬件 (EEPROM) void storeArray(unsigned char *array),在这个例子中是前三个值。像这样是否安全?

store_array((unsigned char *) buffer);

我阅读了以下内容,我不太明白,但我很担心:

6.7.3:5 如果尝试通过使用具有非 volatile 限定类型的左值来引用使用 volatile 限定类型定义的对象,则行为未定义。

这会影响我的代码吗?

然后我有这个后续问题:缓冲区数组只有我想要存储的一部分数据(无法更改),对于这个例子,从第三个值开始。这样做是否合法?

store_array((unsigned char *) buffer + 3);

如果是,如果将3 添加到数组中,演员表会受到怎样的影响? BR,谢谢!

编辑: @Cacahuete Frito 链接了一个非常相似的问题:Is `memcpy((void *)dest, src, n)` with a `volatile` array safe?

【问题讨论】:

IMO,因为无论如何这是特定于平台的代码,如果您可以确定 volatile 在您的平台上执行您想要的操作,那么您可以使用它。无论如何,您都依赖于volatile 的非便携方面,并且......一分钱,一磅。如果您的编译器对volatile 的处理发生变化,您的代码无论如何都会中断。那么你有什么损失呢? Is `memcpy((void *)dest, src, n)` with a `volatile` array safe?的可能重复 关于storeArray((unsigned char *) buffer + 3);:函数如何知道数组在哪里结束?如果它具有硬编码的大小,则指针算法可能会强制函数读取超出缓冲区限制的 3 个字节,因此是 UB。 @DavidSchwartz 这个问题的重点是为了更好地理解这个问题。不过我喜欢你的观点!我想我可以确定行为,因为代码非常严格。这可能是“最薄”的方式。 TY volatile 的含义另见electronics.stackexchange.com/q/409545/6383 【参考方案1】:

是的,您发布的标准报价准确地涵盖了您想要做的事情。通过进行强制转换,您假装数组中的对象是unsigned char,而实际上它们是volatile unsigned char,因此在函数内部,您通过没有volatile 限定符的左值引用volatile 对象.未定义的行为。

如果您无法更改函数storeArray,则必须先将数据从易失性数组复制到非易失性数组,然后再将其传递给函数。

关于第二个问题:指针算术很好,它将简单地将buffer 转换为unsigned char*,然后在结果指针上加3,指向buffer[3](但限定条件错误)。

【讨论】:

感谢您的输入。我编写了一个辅助函数来将易失性数组的内容复制到非易失性数组中。我使用*memcpy 作为模板,我只是更改了参数类型。我的问题中链接的问题做了类似的事情。 等等什么?你实际上所说的那个演员是“我保证这个值不会从我传递这个指针的代码中改变出来。”他可能有一个可怕的内存模型问题等着他,但这并不是立即未定义的。 @Joshua cast 本身并不是未定义的。但是,取消引用由强制转换获得的指针 is. 这正是问题中引用的标准所说的。如果一个对象是volatile,那么对其的所有访问都必须通过volatile-qualified lvalues。【参考方案2】:

您已找到标准的正确部分,此代码会导致未定义的行为。

向“硬件”写入内容的函数可能应该有一个volatile-qualifier 参数,具体取决于“硬件”是什么。如果是内存映射寄存器、DMA缓冲区或非易失性内存,那么参数肯定应该是volatile unsigned char*(或者可选的volatile uint8_t*,也被视为字符类型)。


详细信息:C 允许我们使用字符指针遍历任何数据块,C17 6.3.2.3/7:

当指向对象的指针转换为指向字符类型的指针时, 结果指向对象的最低寻址字节。的连续递增 结果,直到对象的大小,产生指向对象剩余字节的指针。

您引用的有关访问“左值”的部分是指通过与实际存储在该位置的指针类型不同的指针类型访问数据。说白了:不管你怎么用各种指针指向它,实际数据都保持原来的类型。

通常甚至不允许通过错误的指针类型访问数据,但字符访问又是“严格别名规则”的一个特殊例外,C17 6.5/7:

一个对象的存储值只能由具有以下之一的左值表达式访问 以下类型: ... - 一个字符类型。

因此,您可以通过字符指针访问任何类型的数据,但如果该指针不是 volatile 限定的,则根据您引用的部分 C17 6.7.3/5 调用未定义的行为。

实际上,使用非易失性指针类型可能会导致编译器以意想不到的方式优化访问。所以这不仅仅是理论上的“语言律师”,在实践中你可能会在启用优化的情况下生成非常奇怪的代码。嵌入式系统中很多很难找到的错误都源于这样一个缺失的volatile


关于您的后续问题,演员表和 buffer + 3 没有任何改变:您仍在处理没有 volatile 限定符的字符指针 - 相同类型。实际数据仍为volatile unsigned char 类型,因此您无法通过unsigned char* 从函数中访问它。

【讨论】:

对于“硬件”,我的意思是类似 EEPROM,我将其添加到我的问题中。谢谢你的详尽解释!这是一篇有趣的文章。 @earthling 那么答案很可能是先将volatile 缓冲区复制到非易失性缓冲区中。因为您真的不希望在 EEPROM 编程过程中动态更改数据。而且您可能禁用了中断,这将阻止数据更新。等等。在 EEPROM 驱动程序中,指向要写入的内存映射区域的指针必须声明为 volatile,但这是另一回事。 这就是我所做的,请参阅我对 Angew 回答的评论。 EEPROM本身有一些逻辑,它是用I2C控制的,我想我不必担心指针:) @earthling 好的,那么它不是内存映射的,处理起来也不那么敏感。 (但您的 MCU 中的 I2C 寄存器将是 volatile 合格的)【参考方案3】:

    如果数组在中断中发生变化,您需要提供一种机制来以原子方式访问和修改它。如果不进行任何 RW 或 RMW 操作,可能会不成功并且数据不一致。

    您访问易失性数据会使 f=unction 参数也易失性。 storeArray(volatile unsigned char *) 并且不需要演员表。演员表只删除了警告。即使您将非易失性数据传递给它,它也可以正常工作。

【讨论】:

1.:在我的例子中,函数在定义的时刻被调用,此时没有数据写入缓冲区。 2.:这是一种已定义的行为吗? 2.是的,它定义得很好,但你需要关心访问的原子性【参考方案4】:

如您所见,您依赖的是“未定义的行为”。但是,取决于编译单元的分离(以及诸如“整个程序优化”(WPO)之类的东西),它可能会起作用。在大多数情况下,编译器(至少 gcc)不够“聪明”,无法优化不同编译单元中函数的数组访问。也就是说,干净、安全和可移植的方法是复制数组,使非易失性数组的值对编译器可见的易失性值的依赖性。

【讨论】:

如何出错的一个例子:假设数组中的第一项是一些状态标志。 while(array[0]) do_stuff(array[i]); 。如果没有volatile,这很可能会被转换为单次读取,之后编译器可以决定离开函数或陷入永久循环。 @Lundin 是的,当然。但是,由于函数参数被声明为 non-voltaile,为什么函数实现要依赖它表现为 volatile? @JimmyB 我认为您的回答与 DavidSchwartz 的评论非常相似。根据 Angew 的建议,我现在制作了一个辅助函数,也请参阅我对他的回答的评论。

以上是关于将易失性数组转换为非易失性数组的主要内容,如果未能解决你的问题,请参考以下文章

易失性变量和非易失性重新排序/可见性

自动重新评估非易失性 UDF

everspin非易失性存储器中MRAM的潜在用途

展望由非易失性设备构成的未来存储

MicroPython ESP32NVS数据非易失性存储示例讲解说明

Arduino框架下对ESP32 NVS非易失性存储解读以及应用示例