13

我的一位同事在编写 ATMega 时遇到了一些奇怪的问题,这些问题与访问输入 - 输出端口有关。

经过一些研究后观察问题,我得出结论,如果我们的目标是安全的符合 C 标准的软件,我们应该避免使用可能编译为的操作SBI或指令访问 SFR。CBI我正在寻找这个决定是否正确,所以如果我的担忧是有效的。

Atmel 处理器的数据表在这里,它是 ATMega16。我将在下面参考本文档的一些页面。

我将使用本网站上WG14 N1256 链接下的版本来参考 C 标准。

处理器的SBICBI指令在位级操作,仅访问相关位。所以它们不是真正的读取-修改-写入(RMW)指令,因为据我了解,它们不执行读取(目标 8 位 SFR)。

在上述数据表的第 50 页上,第一句以所有 AVR 端口都具有真正的读取-修改-写入功能......开始,同时它指定这仅适用于技术上不是 RMW的使用SBI和指令的访问。CBI数据表没有定义例如PORTx寄存器应该返回的读数(但是它表明它们是可读的)。所以我假设读取这些 SFR 是未定义的(它们可能会返回最后写在上面的东西或当前的输入状态或其他)。

在第 70 页它列出了一些外部中断标志,这很有趣,因为这是SBICBI指令的性质变得重要的地方。这些标志在发生中断时设置,并且可以通过将它们写入 1 来清除它们。因此,如果SBI是真正的 RMW 指令,它将清除所有三个标志,而不管操作码中指定的位如何。

现在让我们进入 C 的问题。

编译器本身确实无关紧要,唯一重要的事实是它可能会在某些情况下使用CBIandSBI指令,我认为这会使其不兼容。

在上面提到的 C99 标准中,第5.1.2.3 节程序执行,第 2 点和第 3 点引用了这一点(第 13 页)和6.7.3 类型限定符,第 6 点(第 109 页)。后者提到构成对具有 volatile 限定类型的对象的访问是实现定义的,但是在它之前的几句话要求任何引用此类对象的表达式都应严格按照抽象机的规则进行评估

另请注意,示例中使用的硬件端口是volatile在适当的标头中声明的。

例子:

PORTA |= 1U << 6;

众所周知,这可以转换为SBI. PORTA这意味着在 volatile ( ) 对象上仅发生写入访问。但是,如果有人会写:

var = 6;
...
PORTA |= 1U << var;

SBI即使它仍然只会设置一位(因为SBI在操作码中编码了要设置的位),那也不会转换为。因此,这将扩展为一个真正的 RMW 序列,其结果可能与上述不同(在这种情况下,PORTA我可以从数据表中推断出未定义的行为)。

根据 C 标准,这种行为可能会或可能不会被允许。在这个术语中也很混乱,这里发生了两件事,它们混合在一起。第一,更明显的是在其中一种情况下缺乏读取访问权限。另一个不太明显的是如何执行写入。

如果编译后的代码省略了读取,它可能无法触发与此类访问相关的硬件行为。但是据我所知,AVR 没有这样的机制,所以它可能会通过标准。

写入更有趣,但它也包含读取。

在 using 的情况下省略读取SBI意味着受影响的 SFR 必须都像锁存器一样工作(或者任何不这样工作的位都绑定到 0 或 1),因此编译器可以确定如果它会从它们读取什么实际上做了访问。如果不是这种情况,那么编译器至少会出错。顺便说一句,这也与数据表没有定义从PORTx寄存器读取的内容相冲突。

写入的执行方式也是不一致的来源:根据编译器编译它的方式,结果会有所不同(一个CBISBI仅影响一个位,一个字节写入影响所有位)。因此,编写代码以清除/设置一位可能“有效”(如不是“意外”清除中断标志),或者如果编译器生成真正的 RMW 序列而不是。

也许这些在技术上是 C 标准允许的(作为“实现定义的”行为,并且编译器推断出这些情况,即对 volatile 对象不需要读取访问权限),但至少我会认为它是一个错误或不一致的实现。

另一个例子:

PORTA = PORTA | (1U << 6);

可以清楚地看到,通常为了符合标准,PORTA应该先进行读取,然后再进行写入。虽然根据 的行为SBI,它将缺少读取访问权限,尽管如上所述,这可能会混合实现定义的行为,并且编译器会推断出此处不需要读取。(或者我的假设是错误的?那是假设a |= ba = a | b?)

因此,基于这些,我决定我们应该避免这些类型的代码,因为它(或将来可能)不清楚它们的行为方式,具体取决于编译器是否使用SBIorCBI或真正的 RMW 序列。

说实话,我主要是通过各种论坛帖子等来解决这个问题,而不是分析实际的编译器输出。毕竟不是我的项目(现在我不在工作)。我接受它阅读AVRreaks例如,AVR-GCC 会在上述情况下输出这些指令,即使使用我们使用的实际版本,我们也不会观察到这一点,这可能会导致问题。(但是我认为这种情况是我的建议,即使用影子工作变量实现端口访问解决了我同事观察到的问题)

注意:我根据对 C (C99) 标准的一些研究编辑了中间部分。

编辑:阅读AVR Libc FAQ我再次发现与自动使用SBIor相矛盾的东西CBI。这是最后一个问题和答案,它明确指出,由于声明了端口,编译器无法根据 C 语言的规则(如它所说的那样)volatile优化读取访问。

我也明白,这种特定行为(即使用SBIor CBI)不太可能直接引入错误,但是通过掩盖“错误”,从长远来看,如果有人在不理解的情况下不小心基于这种行为进行概括,它可能会引入非常讨厌的错误装配级别的 AVR。

4

1 回答 1

3

您可能应该停止尝试将 C 内存模型应用于 I/O 寄存器。它们不是普通的记忆。在 PORTn 寄存器的情况下,实际上是单个位写入还是 RMW 操作都无关紧要,除非您正在混合中断。如果您执行读取-修改-写入,则中断可能会更改其间的状态,从而导致竞争条件;但这对于内存来说是完全相同的问题。SBI/CBI 指令的优点是它们是原子的。

PORTn 寄存器是可读的,并且还驱动输出缓冲器。它们在读取和写入时不是不同的功能(如在 PIC 上),而是一个普通的寄存器。较新的 PIC 还具有在 LAT 地址上可读的输出寄存器,因此您不需要影子变量。PINn 或中断标志等其他 SFR 具有更复杂的行为。在最近的 AVR 中,写入 PINn 会切换 PORTn 中的位,这对于其快速和原子操作再次很有用。向中断标志寄存器写入 1 将清除它们,再次防止竞争条件。

关键是,这些特性可以为硬件感知程序产生正确的行为,即使其中一些在 C 代码中看起来很奇怪(即使用reg=_BV(2);而不是reg&=~_BV(2);)。当代码本质上是硬件特定的(尽管语义相似性确实有帮助,中断标志行为失败)时,精确符合 C 标准是不切实际的目标。在内联函数或宏中使用解释它们真正所做的名称的名称包装奇怪的构造可能是一个好主意,或者至少评论效果是什么。一组这样的 I/O 例程也可以构成可以帮助您移植代码的硬件抽象层的基础。

试图在这里严格解释 C 规范也相当令人困惑,因为它不承认寻址位(这是 SBI 和 CBI 所做的),并且挖掘我的旧(1992 年)副本发现易失性访问可能会导致多个实现定义的行为,包括根本无法访问的可能性。

于 2013-10-24T03:22:20.867 回答