c - AVR 8 位,关于 SFR 位访问的 C 标准合规性

标签 c avr

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

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

Atmel 处理器的数据表是 here ,这是一个ATMega16。我将在下面引用本文档的一些页面。

我将使用 the version found on this site 来引用 C 标准在 WG14 N1256 链接下。
SBICBI处理器的指令在位级操作,只访问有问题的位。因此,它们不是真正的读-修改-写 (R-M-W) 指令,因为据我所知,它们不执行读取(目标 8 位 SFR)。

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

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

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

编译器本身确实无关紧要,唯一重要的事实是它可能会使用 CBISBI我认为某些情况下的说明不合规。

在上面提到的 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 的情况下省略 Read这意味着受影响的 SFR 必须全部像锁存器一样工作(或者任何不这样工作的位都绑定(bind)到 0 或 1),因此编译器可以确定如果它确实进行了访问,它将从它们读取什么。如果不是这种情况,那么编译器至少会出错。顺便说一下,这也与数据表没有定义从 PORTx 中读取的内容相冲突。寄存器。

写入的执行方式也是不一致的原因:结果因编译器编译方式而异(CBISBI 仅影响一位,字节写入影响所有位)。因此,编写代码来清除/设置一位可能会“工作”(如不是“意外”清除中断标志),或者如果编译器产生真正的 R-M-W 序列,则不会。

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

另一个例子:
PORTA = PORTA | (1U << 6);

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

因此,基于这些,我决定我们应该避免使用这些类型的代码,因为根据编译器是否会使用 SBI 不清楚它们的行为方式(或可能在 future )。或 CBI ,或真正的 R-M-W 序列。

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

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

编辑 : 阅读the AVR Libc FAQ我再次发现与自动使用 SBI 相矛盾的东西或 CBI .这是最后一个问题和答案,其中特别指出,由于声明了端口 volatile根据 C 语言的规则(如 it 短语),编译器无法优化读取访问。

我也明白,这种特定行为(即使用 SBICBI )不太可能直接引入错误,但通过掩盖“错误”,从长远来看,如果有人不小心概括基于在不了解汇编级别的 AVR 的情况下对此行为进行处理。

最佳答案

您可能应该停止尝试将 C 内存模型应用于 I/O 寄存器。它们不是简单的内存。在 PORTn 寄存器的情况下,除非您混入中断,否则实际上是单个位写入还是 R-M-W 操作无关紧要。如果你做读-修改-写,中断可能会改变两者之间的状态,导致竞争条件;但这将是完全相同的内存问题。 SBI/CBI 指令的优点是它们是原子的。

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

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

试图在这里严格解释 C 规范也相当令人困惑,因为它不允许寻址位(这是 SBI 和 CBI 所做的),并且挖掘我的旧(1992)副本发现 volatile 访问可能会导致几种实现定义的行为,包括根本没有访问的可能性。

关于c - AVR 8 位,关于 SFR 位访问的 C 标准合规性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19523092/

相关文章:

c - 这些是指针左值吗?

arduino - AVR USART通讯问题

c - 试图了解AVR代码中的左移

c - AVR Xmega USART 读取完整字符串时出现问题

const int 到 int 无法正常工作

c - 动态内存分配和指针?

c - 从没有 '\n' 的输入文件打印行的最佳方法?

c - 如何声明存储在 PROGMEM 中的矩阵

微 Controller 类型转换改进

c - 该函数需要多少字节?