我的一位同事遇到了一些编程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端口都具有真正的读 - 修改 - 写功能...... ,正在进行中它指定这仅适用于{的访问{1}}和SBI
说明技术上不是RMW。数据表不定义了什么读取,例如CBI
寄存器应该返回(但是它表明它们是可读的)。所以我假设读取这些SFR是未定义的(它们可能返回写在它们上的最后一件事或当前输入状态或其他)。
在第70页上,它列出了一些外部中断标志,这很有趣,因为这是PORTx
和SBI
指令的性质变得重要的地方。发生中断时会设置标志,可以通过将它们写入一个来清除它们。因此,如果CBI
是真正的R-M-W指令,它将清除所有三个标志,而不管操作码中指定的位。
现在让我们进入C的问题。
编译器本身确实无关紧要,唯一重要的事实是它可能在某些情况下使用SBI
和CBI
指令,我认为这些指令使其不符合规定。
在上面提到的C99标准中, 5.1.2.3程序执行部分,第2点和第3点引用了这个(第13页), 6.7.3类型限定符,第6点(第109页)。后者提到什么构成对具有volatile限定类型的对象的访问是实现定义的,但是在它之前的一些短语要求应该评估引用这样一个对象的任何表达式严格按照抽象机的规则。
另请注意,示例中使用的硬件端口在相应的标头中声明为SBI
。
示例:
volatile
众所周知,这会转化为PORTA |= 1U << 6;
。这意味着在volatile(SBI
)对象上只发生Write访问。但是,如果有人写:
PORTA
这不会转换为var = 6;
...
PORTA |= 1U << var;
,即使它仍然只设置一位(因为SBI
具有在操作码中编码的位)。因此,这将扩展为真正的R-M-W序列,其结果可能与上述不同(在SBI
的情况下,这是未定义的行为,只要我可以从数据表中扣除)。
根据C标准,可能允许也可能不允许此行为。在这个术语中也是混乱的,这里发生了两件混合的事情。第一,更明显的是在其中一个案例中缺乏Read访问。另一个不太明显的是如何执行写入。
如果编译的代码省略了Read,则可能无法触发与此类访问相关的硬件行为。然而,就我所知,AVR没有这样的机制,所以它可能会通过标准。
Write更有趣,但它也包含Read。
在使用PORTA
的情况下省略Read意味着受影响的SFR必须像锁存器一样工作(或任何不能正常工作的位或者绑定到0或1),因此编译器如果它实际上可以访问它,可以确定从它们读取的内容。如果情况不是这样,那么编译器至少会出错。顺便说一下,这也与数据表没有定义从SBI
寄存器读取的内容相冲突。
如何执行写操作也是不一致的根源:结果因编译器编译它而有所不同(PORTx
或CBI
仅影响一位,一个字节写入影响所有位)。因此,编写清除/设置一位的代码可能会“工作”#34; (如不是&#34;意外地&#34;清除中断标志),或者如果编译器产生真正的R-M-W序列,则不然。或者
也许C标准在技术上允许这些(作为&#34;实现定义&#34;行为,并且编译器扣除这些情况,读取访问对于volatile对象不是必需的),但至少我会考虑它是一个有缺陷或不一致的实现。
另一个例子:
SBI
清楚地看到,通常要符合标准,应执行Read {然后写PORTA = PORTA | (1U << 6);
。虽然根据PORTA
的行为,它将缺少读取访问权限,但如上所述,这可能会传递实现定义行为的混合,并且编译器会在此处推断读取是不必要的。 (或者我的假设是错误的?假设SBI
与a |= b
相同?)
基于这些,我决定使用,我们应该避免这些类型的代码,因为它(或将来可能)不清楚它们的行为将取决于编译器是使用a = a | b
还是{{1或者是真正的RMW序列。
说实话我主要去了各种论坛帖子等解决这个问题,而不是分析实际的编译器输出。毕竟不是我的项目(现在我不在工作)。我接受它阅读AVRFreaks,例如AVR-GCC会在上述情况下输出这些指令,即使使用我们使用的实际版本我们也不会观察到这一点。 (但我认为这个案例是我的建议,使用影子工作变量实现端口访问,修复了我的同事观察到的问题)
注意:我根据对C(C99)标准的一些研究对中间进行了编辑。
修改:阅读the AVR Libc FAQ我再次发现了与SBI
或CBI
的自动使用相矛盾的内容。这是最后一个问题&amp;回答它明确指出由于端口被声明为SBI
,编译器无法根据C语言的规则(因为它的短语)优化读取访问,。
我也明白,这种特殊行为(使用CBI
或volatile
)不太可能直接引入错误,而是通过屏蔽&#34;错误&#34;如果有人在不了解装配级别的AVR的情况下意外地根据这种行为进行概括,那么从长远来看,它可能会引入非常讨厌的。
答案 0 :(得分:3)
您可能应该停止尝试将C内存模型应用于I / O寄存器。它们不是简单的记忆。在PORTn寄存器的情况下,除非您混入中断,否则无论是单位写操作还是R-M-W操作都无关紧要。如果你做了读 - 修改 - 写,中断可能会改变其间的状态,导致竞争条件;但这对内存来说完全是同一个问题。 SBI / CBI指令的优点在于它们是原子的。
PORTn寄存器是可读的,也可以驱动输出缓冲区。它们在读写时并不是不同的功能(如PIC),而是普通寄存器。较新的PIC也可以在LAT地址上读取输出寄存器,因此您不需要阴影变量。其他SFR(如PINn或中断标志)的行为更复杂。在最近的AVR中,写入PINn而不是切换PORTn中的位,这对于其快速和原子操作同样有用。将1写入中断标志寄存器会再次清除它们,以防止出现竞争条件。
关键是,这些功能适用于为硬件识别程序生成正确的行为,即使其中一些在C代码中看起来很奇怪(即使用reg=_BV(2);
而不是{{ 1}})。当代码本质上是硬件特定的时候,精确地遵守C标准是一个不切实际的目标(虽然语义相似性确实有帮助,但中断标志行为失败了)。在内联函数或宏中包含奇怪的结构,其名称可以解释他们真正做的事情,这可能是一个好主意,或至少评论效果是什么。一组这样的I / O例程也可以构成硬件抽象层的基础,可以帮助您移植代码。
在这里严格解释C规范也相当混乱,因为它不承认寻址位(这是SBI和CBI所做的),并且挖掘我的旧(1992)副本发现可能导致易失性访问在几个实现定义的行为中,包括 无法访问的可能性。