首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >“挥发物”是否足以让编译器在读取时处理带有副作用的机器寄存器?

“挥发物”是否足以让编译器在读取时处理带有副作用的机器寄存器?
EN

Stack Overflow用户
提问于 2017-06-20 22:48:54
回答 2查看 218关注 0票数 4

我与微控制器一起工作,有时会有机器寄存器在读取寄存器时执行操作。是的,这不是一个错误,有很多寄存器在写的时候会导致操作,但是在一些情况下,如果你读一个寄存器,会发生一些事情。

最常见的实例是连接到FIFO一端的UART接收寄存器;例如,假设有RXDATA。当您读取RXDATA时,它从FIFO中提取一个字节,下一次读取RXDATA时,它将得到下一个字节。

volatile 中是否有足够的信息使编译器理解读?可能会产生副作用?

C中的示例片段:

代码语言:javascript
复制
#include <stdint.h>
#include <stdbool.h>

volatile uint8_t RXDATA;   
// there is some mechanism for associating this with a known hardware address
// (either in linker information, or in some compiler-specific attribute not shown)    

// Check that bit 0 is 1 and bit 7 is 0.
bool check_bits_1() 
{
   const uint8_t rxdata = RXDATA;
   return (rxdata & 1) && ((rxdata & 0x80) == 0);
}

// Check that bit 0 is 1 and bit 7 is 0.
bool check_bits_2() 
{
   return (RXDATA & 1) && ((RXDATA & 0x80) == 0);
}

// Check that bit 0 is 1 and bit 7 is 0.
bool check_bits_3() 
{
   const bool bit_0_is_1 = RXDATA & 1;
   const bool bit_7_is_0 = (RXDATA & 0x80) == 0;
   return bit_0_is_1 && bit_7_is_0;
}

如果我忽略了C标准,假装编译器做了我想要它做的事情(DWIM),那么我的直觉是这三个函数有不同的行为:

  • 在第一种情况下,我们只读取一次RXDATA,所以我们取出FIFO的一个字节,然后对它做一些计算。
  • 在第二种情况下,我们只读一次或两次RXDATA (因为&&有短路行为),直接计算寄存器值,所以我们可以从FIFO中取出一两个字节,这是不正确的行为。
  • 在第三种情况下,我们读取两次RXDATA,从FIFO中提取两个字节,所以这是不正确的行为。

然而,如果RXDATA不是volatile,那么以上三个实现大概都是等价的。

在本例中,C标准是否要求编译器以我所看到的方式解释volatile?如果没有,如何在C中正确处理硬件寄存器?

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2017-06-21 11:55:55

“易失性”中是否有足够的信息使编译器理解读取可能会产生副作用?

是。

C语言对副作用的正式定义实际上就是针对这个场景的。C11 5.1.2.3:

访问volatile对象、修改对象、修改文件或调用执行任何这些操作的函数都是副作用,它们都是执行环境状态的变化。

关于允许编译优化的内容,C11 5.2.3.4:

在抽象机器中,所有表达式都由语义指定。实际的实现不需要评估表达式的一部分,如果它可以推断它的值没有被使用,并且没有产生任何所需的副作用(包括调用函数或访问volatile对象所引起的任何副作用)。

在简单的英语中,这意味着对易失性对象的任何形式的访问、读或写都被认为是副作用,编译器不允许优化这些副作用。

...then我的直觉是这三种功能有不同的行为

他们确实有。这就是为什么像MISRA这样的编码标准禁止我们将volatile变量访问与相同表达式中的其他内容混合在一起。在UART场景中,这样做可能会导致状态标志的丢失,这将是一个严重的错误。

健壮的程序在单行上对易失性变量进行读写,并在单独的表达式中执行所有其他必要的算法。

票数 4
EN

Stack Overflow用户

发布于 2017-06-21 08:09:29

在处理内存映射寄存器时,您必须超出C标准所保证的范围。如果幸运的话,您只需要依赖于实现特定的行为,但通常您需要超越这些行为,并且几乎可以手工验证生成的代码是否正确。即使您找到了使这一严格标准符合的方法,您也必须说明这是编译器很少使用和测试的领域之一,在某个模糊的小错误版本中,它很容易被边缘情况打破。这就是为什么几乎所有的操作系统内核都有一个有限的编译器列表,他们应该使用这些编译器进行编译。

我有经验的内核遵循基本相同的模式。内存映射寄存器被抽象到一些句柄后面,这些句柄具有函数指针,用于对寄存器的各种访问。这主要是为了让您可以使用相同的API在不同的体系结构上与不同的总线对话,但是它的次要目的是隐藏在函数指针后面的函数很好地说服编译器不要内联和重新排序(很少有实际的保证,但是请参阅第1段)。函数本身的范围很广,从对某些体系结构的琐碎指针取消引用到在编译程序难以说服其不具有创造性的体系结构上的原始组装,或者在需要特定内存屏障的情况下。

说到最后一点,您需要考虑内存模型。仅仅因为编译器没有创造性地重新排序您的代码,并不意味着CPU不能自由地做它想做的任何事情。这绝对超出了C标准。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/44664185

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档