【问题标题】:Controlling read and write access width to memory mapped registers in C在 C 中控制对内存映射寄存器的读写访问宽度
【发布时间】:2011-03-03 16:06:48
【问题描述】:

我正在使用基于 x86 的内核来操作 32 位内存映射寄存器。仅当 CPU 生成 32 位宽的读取和写入此寄存器时,我的硬件才能正常运行。该寄存器在 32 位地址上对齐,不可按字节粒度寻址。

如何保证我的 C(或 C99)编译器在所有情况下都只生成完整的 32 位宽读取和写入?

例如,如果我执行这样的读取-修改-写入操作:

volatile uint32_t* p_reg = 0xCAFE0000;
*p_reg |= 0x01;

我不希望编译器对只有底部字节更改并生成 8 位宽读/写的事实变得聪明。由于 x86 上 8 位操作的机器代码通常更密集,我担心不需要的优化。通常禁用优化不是一种选择。

----- 编辑 --------
一篇有趣且非常相关的论文:http://www.cs.utah.edu/~regehr/papers/emsoft08-preprint.pdf

【问题讨论】:

  • 对不起“自我推销”,但您可能会发现此项目对测试内存映射硬件或设置/读取内存映射寄存器很有用:code.google.com/p/jeeamtee/wiki/Main。问候,瓦伦丁·海尼茨

标签: c embedded compiler-optimization memory-mapping


【解决方案1】:

由于对硬件的读取-修改-写入操作在多条指令中执行总是存在巨大风险,因此大多数处理器都提供一条指令来使用一条无法中断的指令来操作寄存器/内存。

根据您正在操作的寄存器类型,它可能会在您的修改阶段发生变化,然后您会写回一个错误值。

如果这很关键,我会建议您在汇编中编写自己的 read-modify-write 函数。

我从未听说过优化类型的编译器(为了优化而进行类型转换)。如果它被声明为 int32,则它始终是 int32,并且将始终在内存中右对齐。检查您的编译器文档以了解各种优化是如何工作的。

我想我知道你的担忧来自哪里,结构。结构通常被填充到最佳对齐。这就是为什么您需要在它们周围包裹一个#pragma pack() 以使它们字节对齐。

您可以单步执行程序集,然后您将看到编译器如何翻译您的代码。我很确定它没有改变你的类型。

【讨论】:

    【解决方案2】:

    volatile 限定符涵盖了您的问题。

    6.7.3/6“类型限定符”说:

    具有 volatile 限定类型的对象可能会以实现未知的方式被修改或具有其他未知的副作用。因此,任何引用此类对象的表达式都应严格按照抽象机的规则进行评估,如 5.1.2.3 中所述。此外,在每个序列点,最后存储在对象中的值应与抽象机规定的值一致,除非由前面提到的未知因素修改。什么构成对具有 volatile 限定类型的对象的访问是实现定义的。

    5.1.2.3 “程序执行”说(除其他外):

    在抽象机中,所有表达式都按照语义指定的方式进行评估。

    这之后是一个通常称为“as-if”规则的句子,如果最终结果相同,则允许实现不遵循抽象机器语义:

    如果一个实际的实现可以推断出它的值没有被使用并且没有产生所需的副作用(包括调用函数或访问易失性对象引起的任何副作用),则它不需要评估表达式的一部分。

    但是,6.7.3/6 本质上说表达式中使用的 volatile 限定类型不能应用“as-if”规则 - 必须遵循实际的抽象机器语义。因此,如果指向 volatile 32 位类型的指针被取消引用,则必须读取或写入完整的 32 位值(取决于操作)。

    【讨论】:

      【解决方案3】:

      保证编译器会做正确的事情的唯一方法是在汇编程序中编写加载和存储例程并从 C 中调用它们。我多年来使用的 100% 的编译器都可以并且将会出错(包括 GCC)。

      有时优化器会得到你,例如你想将一些在编译器中显示为小数字 0x10 的常量存储到一个 32 位寄存器中,这是你特别要求的,也是我看过的,否则很好的编译器试着做。一些编译器会决定执行 8 位写入而不是 32 位写入更便宜,并更改指令。可变指令长度目标将使情况变得更糟,因为编译器试图节省程序空间,而不仅仅是它可能假设总线的内存周期。 (例如 xor ax,ax 而不是 mov eax,0)

      对于像 gcc 这样不断发展的东西,今天可以工作的代码并不能保证明天可以工作(你甚至不能用当前版本的 gcc 编译某些版本的 gcc)。同样,在您办公桌上的编译器上运行的代码可能无法普遍适用于其他人。

      排除猜测和实验,创建加载和存储函数。

      这样做的附带好处是,您可以创建一个很好的抽象层,如果/当您想以某种方式模拟您的代码或让代码在应用程序空间而不是在金属上运行时,或者反之亦然,汇编程序函数可以用模拟目标替换,也可以用跨越网络到达带有设备的目标的代码替换,等等。

      【讨论】:

      • 我写硬件接口已经 15 年了,从来没有写过汇编程序来确保 32 位写访问。 Volatile 在实践中告诉编译器它不能对指令之间的内存地址的先前值做任何假设。
      • 同意,但我失败了。如果您将加载和存储例程作为函数放在与您调用它们的文件不同的.c 文件中,并且不要内联它们并且不要说 llvm 尝试优化整个应用程序,那么我将添加到我的评论中您可以避免使用汇编程序,并且可以很好地可靠地工作
      • 如果我们想玩那个游戏,我已经做了 20 多年,很多平台很多编译器。在大多数情况下,它可以工作,但有时您会陷入困境,您无法弄清楚编译器为什么要优化或更改您的代码。它可以工作数周或数月,然后添加或更改第 n 行代码并更改其编译方式。用户说保证,如果你不想要“保证工作”而是“大部分时间工作”,比如超过 99% 但低于 100%,那么 volatile(在单独文件中的单独函数中)将满足这个要求。
      【解决方案4】:

      如果您在访问硬件时不使用字节(无符号字符)类型,编译器将更有可能不生成 8 位数据传输指令。

      volatile uint32_t* p_reg = 0xCAFE0000;
      const uint32_t value = 0x01;  // This trick tells the compiler the constant is 32 bits.
      *p_reg |= value;
      

      您必须将端口读取为 32 位值,修改该值,然后回写:

      uint32_t reg_value = *p_reg;
      reg_value |= 0x01;
      *p_reg = reg_value;
      

      【讨论】:

      • 同意,但要寻找比“更好的机会”更强大的东西。
      【解决方案5】:

      好吧,一般来说,如果您将寄存器键入为 32 位易失性,我不希望它优化高位字节。由于使用了 volatile 关键字,编译器不能假定高字节中的值是 0x00。因此,即使您只使用 8 位文字值,它也必须写入完整的 32 位。我从未在 0x86 或 Ti 处理器或其他嵌入式处理器上遇到过这个问题。通常 volatile 关键字就足够了。唯一有点奇怪的是处理器本身不支持您尝试编写的字长,但这对于 32 位数字的 0x86 来说不应该是问题。

      虽然编译器可以生成使用 4 位写入的指令流,但这在处理器时间或指令空间上都不是单次 32 位写入的优化。

      【讨论】:

      • volatile 限定符不会阻止编译器将访问宽度从 32 位缩小到 8 位。从它的角度来看,高 24 位易失性位没有受到影响。此外,8 位指令编码导致指令字节更少,因此 -Os 优化有理由更喜欢这个。
      • 它阻止编译器假设该值与读取的值相同。它不能缩小访问范围,因为它需要写回所有 32 位来保证值是应该的值。
      猜你喜欢
      • 1970-01-01
      • 2012-06-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-09-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多