在C语言中控制对内存映射寄存器的读写访问宽度

7

我正在使用基于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

抱歉进行“自我推广”,但您可能会发现这个项目对于测试内存映射硬件或设置/读取内存映射寄存器非常有用:https://code.google.com/p/jeeamtee/wiki/Main。祝好,Valentin Heinitz。 - Valentin H
5个回答

6
您的问题可以通过使用volatile限定符来解决。
在6.7.3/6 "类型限定符"中指出:
一个使用了volatile限定符的对象可能会以实现未知的方式被修改或产生其他未知的副作用。因此,对这样一个对象的任何引用表达式必须按照抽象机器的规则严格评估,如5.1.2.3所述。此外,在每个序列点上,对象的最后存储的值必须与抽象机器所规定的值相符,除非被前面提到的未知因素所修改。对于使用了volatile限定符的对象的访问是由实现定义的。
在5.1.2.3 "程序执行"中指出(以及其他一些内容):
在抽象机器中,所有表达式都按照语义规定进行评估。
接下来是一句通常被称为“as-if”规则的句子,如果最终结果相同,则允许实现不遵循抽象机器语义:
如果实际实现可以推断出一个表达式的一部分的值不会被使用且不会产生任何需要的副作用(包括通过调用函数或访问易失性对象产生的任何副作用),则不需要评估该部分。
但是,6.7.3/6基本上表示,在表达式中使用了volatile限定的类型不能应用“as-if”规则——必须遵循实际的抽象机器语义。因此,如果解引用易失性32位类型的指针,则必须读取或写入完整的32位值(取决于操作)。

4
唯一保证编译器正确处理的方法是用汇编语言编写加载和存储例程,并从C中调用它们。我多年使用的所有编译器都可能会出错(包括GCC)。有时候优化器会让你失望,例如,你想将某个常量0x10存储到一个32位寄存器中,但对于编译器来说,这个数字太小了,因此有些编译器会决定进行8位写入而不是32位写入,并更改指令。可变长指令目标将使情况变得更糟,因为编译器试图在程序空间上节省空间,而不只是在内存周期上节省时间,因此可能会假设总线的宽度。 (例如,使用xor ax,ax代替mov eax,0)。随着像gcc这样不断发展的编译器,今天能正常工作的代码明天就没有保证(有些版本的gcc甚至无法使用当前版本的gcc编译)。同样,对于其他人而言,在您桌子上的编译器上运行良好的代码可能不会普遍适用。消除猜测和实验,创建加载和存储函数吧。这样做的另一个好处是,您可以创建一个很好的抽象层,如果/当您想以某种方式模拟代码或使代码在应用程序空间而不是在金属上运行时,或者反之亦然,可以将汇编函数替换为模拟目标或替换为跨越网络到具有设备的目标的代码。

我已经编写硬件接口15年了,从未必须编写汇编语言来确保32位写入访问。在实践中,volatile告诉编译器它不能做出有关指令之间内存地址先前值的任何假设。 - NoMoreZealots
同意,但我曾经尝试失败过。如果您将加载和存储例程作为函数放在一个单独的.c文件中,并且不要内联它们,也不要让LLVM尝试优化整个应用程序,那么您就可以避免使用汇编语言,并有很好的机会使其可靠运行。 - old_timer
1
如果我们想要玩那个游戏,我已经做了20多年了,使用过很多平台和编译器。大部分情况下它都能正常工作,但有时你会陷入困境,无法弄清楚为什么编译器正在优化或更改你的代码。它可以工作数周或数月,然后添加或更改第n行代码,它就会改变编译方式。用户说保证,如果您不想要“保证能够运行”,而是“大多数时间都能够运行”,比99%多但不到100%,那么在单独的函数和文件中使用 volatile 就能满足这个要求。 - old_timer

0

如果在访问硬件时不使用字节(unsigned char)类型,编译器生成的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;

同意,但是我在寻找比“更好的机会”更强的东西。 - srking

0

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

虽然编译器生成使用4位写入的指令流是可能的,但这既不是处理器时间优化,也不是指令空间优化,而是单个32位写入。


volatile限定符并不能阻止编译器将访问宽度从32位缩小到8位。从编译器的角度来看,上面的24个易失性位是不受影响的。此外,8位指令编码会导致更少的指令字节,因此-Os优化有理由偏爱这种方式。 - srking
它可以防止编译器假设值与读取的值相同。它不能缩小访问范围,因为需要将所有32位写回以确保值是应该的。 - NoMoreZealots

0

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

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

如果这很关键,我建议像dwelch建议的那样,在汇编中编写自己的读取-修改-写入函数。

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

我想我知道您的担忧来自哪里,结构体。结构体通常会填充到最佳对齐方式。这就是为什么您需要在它们周围包装一个#pragma pack()以获取字节对齐的原因。

您可以逐步执行汇编代码,然后您将看到编译器如何翻译您的代码。我非常确定它没有更改您的类型。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接