【发布时间】:2013-10-22 16:13:51
【问题描述】:
我的一位同事在编写 ATMega 时遇到了一些奇怪的问题,与访问输入 - 输出端口有关。
在一些研究后观察问题,我得出结论,如果我们的目标是安全的符合 C 标准的软件,我们应该避免使用可能编译为 SBI 或 CBI 指令的操作访问 SFR。我正在寻找这个决定是否正确,所以我的担忧是否有效。
Atmel 处理器的数据表是here,它是一个 ATMega16。我将在下面参考本文档的一些页面。
我将在 WG14 N1256 链接下使用 the version found on this site 参考 C 标准。
处理器的SBI 和CBI 指令在位级别运行,仅访问相关位。所以它们不是真正的读取-修改-写入 (R-M-W) 指令,因为据我了解,它们不执行读取(目标 8 位 SFR)。
在上述数据表的第 50 页上,第一句话的开头类似于 所有 AVR 端口都具有真正的读取-修改-写入功能...,同时它指定这仅适用于使用 @ 的访问987654329@ 和 CBI 指令在技术上不是 R-M-W。数据表没有定义例如PORTx 寄存器应该返回的读数(但它表明它们是可读的)。所以我假设读取这些 SFR 是未定义的(它们可能会返回最后写入的内容或当前的输入状态或其他)。
在第 70 页它列出了一些外部中断标志,这很有趣,因为这是 SBI 和 CBI 指令的性质变得重要的地方。这些标志在发生中断时设置,并且可以通过将它们写入 1 来清除它们。因此,如果SBI 是真正的 R-M-W 指令,则无论操作码中指定的位如何,它都会清除所有三个标志。
现在让我们开始讨论 C 的问题。
编译器本身确实无关紧要,唯一重要的事实是它可能会在某些情况下使用CBI 和SBI 指令,我认为这会使其不兼容。
在上面提到的 C99 标准中,5.1.2.3 程序执行部分,第 2 点和第 3 点指的是这个(在第 13 页),以及 6.7.3 类型限定符,第 6 点(第 109 页)。后者提到构成对具有 volatile 限定类型的对象的访问是实现定义的,但是在它之前的几个短语要求引用此类对象的任何表达式都应进行评估严格按照抽象机的规则进行.
另请注意,示例中使用的硬件端口在相应的标头中声明为volatile。
例子:
PORTA |= 1U << 6;
众所周知,这会转换为SBI。这意味着在 volatile (PORTA) 对象上仅发生写入访问。但是,如果有人会写:
var = 6;
...
PORTA |= 1U << var;
这不会转换为SBI,即使它仍然只会设置一位(因为SBI 有要设置在操作码中的位)。因此,这将扩展为一个真正的 R-M-W 序列,其结果可能与上述不同(在 PORTA 的情况下,据我可以从数据表中推断,这是未定义的行为)。
根据 C 标准,这种行为可能是允许的,也可能是不允许的。在这个术语中也很混乱,这里发生了两件事,它们混合在一起。第一,更明显的是在其中一种情况下缺乏读取访问权限。另一个不太明显的是写入的执行方式。
如果编译后的代码省略了读取,它可能无法触发与此类访问相关的硬件行为。但是据我所知,AVR 没有这样的机制,所以它可能会通过标准。
Write 更有趣,但它也包含 Read。
在使用SBI 的情况下省略读取意味着受影响的 SFR 必须都像锁存器一样工作(或者任何不工作的位都绑定到 0 或 1),因此编译器可以确定它是什么如果它确实进行了访问,则会从他们那里读取。如果不是这种情况,那么编译器至少会出错。顺便说一句,这也与数据表没有定义从PORTx 寄存器读取的内容相冲突。
写入的执行方式也是不一致的来源:根据编译器的编译方式,结果会有所不同(CBI 或 SBI 仅影响一位,字节写入影响所有位)。因此,编写代码来清除/设置一位可能会“工作”(如不是“意外地”清除中断标志),或者如果编译器生成真正的 R-M-W 序列而不是。
也许这些在技术上是 C 标准所允许的(作为“实现定义”的行为,并且编译器推断出这些情况,即 volatile 对象不需要读取访问权限),但至少我会认为它有问题或不一致实施。
另一个例子:
PORTA = PORTA | (1U << 6);
很明显,通常为了符合标准,应该先读取然后写入PORTA。虽然根据SBI 的行为,它将缺少读取访问权限,尽管如上所述,这可能会混合实现定义的行为和编译器推断此处不需要读取。 (或者我的假设是错误的?假设a |= b 与a = a | b 相同?)
因此,基于这些,我决定我们应该避免这些类型的代码,因为它是(或将来可能)不清楚它们的行为方式取决于编译器是使用SBI 还是CBI,或者真正的 R-M-W 序列。
说实话,我主要是通过各种论坛帖子等来解决这个问题,而不是分析实际的编译器输出。毕竟不是我的项目(现在我不在工作)。我接受它阅读AVRFreaks,例如,AVR-GCC 会在上述情况下输出这些指令,即使我们使用的实际版本我们不会观察到这一点,这也可能会造成问题。 (但是我认为这种情况是我的建议,即使用影子工作变量实现端口访问解决了我同事观察到的问题)
注意:我根据对 C (C99) 标准的一些研究编辑了中间部分。
编辑:阅读the AVR Libc FAQ 我再次发现与SBI 或CBI 的自动使用相矛盾的东西。这是最后一个问题和答案,它明确指出,由于端口被声明为volatile,编译器无法优化读取访问,根据 C 语言的规则(正如它所说的) .
我也明白这种特殊行为(即使用SBI 或CBI)不太可能直接引入错误,但通过掩盖“错误”从长远来看,如果有人可能会引入非常讨厌的错误在不了解汇编级别的 AVR 时,意外地基于此行为进行了概括。
【问题讨论】:
-
只有在更改内容之前未读取寄存器时才会出现这些奇怪问题的具体原因是什么?
-
他试图连接一个并行的字母数字 LCD。我们之前在使用 C 的 PIC18 上就有过这些经验,我最初建议他只采用其中一个并将其移植。 LCD 不工作(就像我们根本没有与它沟通一样)。然后查看问题并浏览移植的代码,我得出了这个结论,他做到了,液晶显示器按照代码的指示工作。在上一个。代码中有
PORTC |= (1U << 6) | (1U << 7);之类的东西,显然不会转换为SBI,所以真正的 R-M-W 发生了,然后见上文。数据表未从 PORTx 定义 R。 -
C99, 6.7.3/6 “对具有 volatile 限定类型的对象的访问由实现定义。”所以也许
PORTA |= ..不被视为访问权限? (你说得对,PORTA |= ..被定义为PORTA = PORTA | ..,其中PORTA只评估一次,根据 6.5.16.2/3。) -
有趣的问题,已收藏。我什至可能在晚饭后打开开发板来玩自己。 (是的,我知道双关语很糟糕)我有一种感觉,而不是
PORTA = PORTA | 1 << 6,你应该做PORTA = PINA | 1 << 6- 这是基于我拥有的atMega64.pdf文件第72页的C代码,其原始网址现在让我无法理解。下载您链接到的 16 的数据表后,我看到第 54 页给出了相同的示例。 -
一个有趣的效果,如果您尝试清除中断标志(数据表第 70 页)肯定是可见的。如果你像
GIFR |= 1U << 6;那样做,那么它会“工作”(只有预期的标志会被清除)。但是,如果您尝试清除像GIFR |= (1U << 6) | (1U << 7);这样的两个,那么所有 3 个都会清除,然后砰,那里有一个错误。从 C 级别开始,我会除了这两个行为相似(要么由于 R-M-W 清除所有标志,要么都只清除我明确指定的标志),但这里显然不是这种情况。