C++ 在裸机控制器上不同上下文的int变量中设置标志

C++ 在裸机控制器上不同上下文的int变量中设置标志,c++,volatile,c++20,bitflags,C++,Volatile,C++20,Bitflags,对于avr 8位微控制器,必须在某个8位整数变量中设置或清除单个位(标志)。可以从正常上下文(main)和中断处理程序(Isr())调用此set/clear函数。因此,必须使变量易失性,以防止其重新排序或将其保留在寄存器中的某个位置std::atomic在这里不是一个有效的选项,因为没有底层操作系统、多cpu内核或缓存,所以不需要某种内存围栏。即使 STD::原子< /代码>不是任何AVR C++库的一部分。< /P> 设置标志的操作类似于:some_flags |=new_set_flags

对于avr 8位微控制器,必须在某个8位整数变量中设置或清除单个位(标志)。可以从正常上下文(main)和中断处理程序(Isr())调用此set/clear函数。因此,必须使变量
易失性
,以防止其重新排序或将其保留在寄存器中的某个位置
std::atomic
在这里不是一个有效的选项,因为没有底层操作系统、多cpu内核或缓存,所以不需要某种内存围栏。即使<代码> STD::原子< /代码>不是任何AVR C++库的一部分。< /P> 设置标志的操作类似于:
some_flags |=new_set_flags

但是使用
c++20
我得到了警告:
警告:使用'volatile'的复合赋值-不推荐使用限定的左操作数[-Wvolatile]

通过使用临时变量重写函数是没有问题的,但它认为在这种情况下,不推荐使用volatile关键字并不是有意的

顺便说一句:由于变量存储在RAM中,cpu无法在单个汇编指令中设置内存中的一位。因此,整个操作必须是原子的。对于这种用例,avr库有
原子块(原子块)
,它只是禁用中断

#include <util/atomic.h>

volatile uint8_t some_flags;

void SetFlag( uint8_t new_set_flags )
{
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) 
    {   
        uint8_t tmp = some_flags;
        tmp |= new_set_flags;
        some_flags = tmp;

        ... vs ...

        some_flags|= new_set_flags; // main.cpp:65:19: warning: compound assignment with 'volatile'-qualified left operand is deprecated [-Wvolatile]
    }   
}

void SomeIsr()
{
    SetFlag( 0x02 );
}

int main()
{
    SetFlag( 0x01);
}

0000006c:
6c:d8 9a sbi 0x1b,0;27
6e:90 e0本地设计院r25,0x00;0
70:80 e0本地设计院r24,0x00;0
72:08 95 ret

基本原理是,即使在易失性变量上,复合赋值或前后递增或递减也不是原子的,而程序员可以将其视为单个操作。此外,该标准规定,E1 op=E2与E1=E1 op E2相同,只是E1仅评估一次

这意味着不谨慎的程序员可以使用

volatile uint8_t some_flags;
...
some_flags|= new_set_flags;
期望它是原子的,即使在硬件中断的情况下,也不需要原子

在机器级别,它看起来像3个操作:

load value from memory
update accumulator register
store value to memory
这意味着在没有更多预防措施的情况下,如果两个执行线程(此处为正常处理线程和ISR线程)交错,则会出现争用情况:

normal loads
! ISR takes the processor
ISR loads updates and stores
! return from ISR
normal updates and stores erasing the change from ISR
当程序使用temp变量时,很明显可能会出现争用情况

< >对你来说不好的是,C++的委托人已经否决了使用它的意图,以便以后完全删除它。 因此,您可以:

  • 在您的代码规范中添加允许对易失性变量进行复合赋值所依赖的内容,并希望编译器能够为其提供选项(即使不是很好,也感觉合理)
  • 添加与C++17兼容但不支持C++20及以上版本的代码规范
  • 将编译为C代码(C++标准仍支持交叉C-C++链接)
  • 只需将其写成
    some_flags=some_flags | new_set_flags

我喜欢最后一种方法,因为对于一个易失性字节,编译器没有产生一个效率较低的代码,它从早期C版本到最后一个C++ 1/P>是一致的。


参考资料:

  • :当前版本
  • :原始版本,包含广泛的背景信息,主要是拟议变更的原因?部分

@KamilCuk:如果您不能在该计算机上为给定的数据类型实现无锁原子,则必须为您的底层操作系统设置一个锁。@KamilCuk:我有一个关于在给定控制器的int-var上设置一个位的问题。在一些机器上实现无日志原子是一个有趣的讨论,但是给定的控制器无法实现它。它没有汇编指令从ram上的某个位置读/修改/写。所以这个讨论对给定的主题没有帮助。反对的重点是,您的单一指令更新不是一个可移植的假设。如果你不关心可移植性,你可以使用一些特定于实现的内在功能(或者使用内联汇编,如果你的编译器还没有这样的内在功能的话)。我看不出不推荐volatile会帮助任何人编写更好的代码,也不会让编译器供应商更容易编写。好吧,不管怎样。。。特别是对于具有内存映射IO寄存器(通常定义为
volatile
并映射到给定地址)而言,不使用volatile听起来是一个非常糟糕的主意,并且会阻止大量代码。E1=E1 op E2确实没有改善:-)好的,我不在标准委员会,所以我必须处理结果。我将使用这种“新”的设置位的方法,即使这是一种痛苦。@Klaus:volatile并没有被弃用!不推荐使用的是volatile上的复合赋值…非常清楚。但这将导致不推荐使用标准IO处理:-)PORTA |=0x01;现在不推荐使用打开io端口的位。对我来说,这听起来简直是错的!如果您希望保留旧的C语法,则可以始终使用C模块。无论如何,我不支持委员会的决定,也不会试图责怪它:这只是一个事实,我们将不得不接受。。。顺便说一句,
PORTA |=0x01
在可以存在多个执行线程时始终包含可能的争用条件。
load value from memory
update accumulator register
store value to memory
normal loads
! ISR takes the processor
ISR loads updates and stores
! return from ISR
normal updates and stores erasing the change from ISR