【问题标题】:set a flag in int variable from different context on bare metal controller在裸机控制器上的不同上下文中在 int 变量中设置一个标志
【发布时间】:2020-11-07 06:41:06
【问题描述】:

对于 avr 8 位微控制器,必须在某些 8 位整数变量中设置或清除单个位(标志)。这个设置/清除函数可以从普通上下文( main )和中断处理程序( Isr() )中调用。因此,必须将变量设置为volatile 以防止它重新排序或将其保存在寄存器中的某个位置。 std::atomic 在这里不是一个有效的选项,因为没有底层操作系统,也没有多 cpu 内核,也没有缓存,所以不需要某种内存栅栏。甚至 std::atomic 也不是任何 avr c++ 库的一部分。

设置标志的操作类似于: some_flags|= new_set_flags

但是c++20 我收到警告:warning: compound assignment with 'volatile'-qualified left operand is deprecated [-Wvolatile]

使用临时变量重写函数是没有问题的,但感觉这不是在这种情况下不推荐使用 volatile 关键字的意图。

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

#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);
}

问题:如果此标志/变量在没有 OS 或 MMU 的单核裸机控制器的不同上下文中使用,那么将标志设置为 int 变量的“正确”方法是什么。

如果你想在一个 IO 寄存器上设置一个位,就像PORTA|=0x01; 一样,编译器可以在单个汇编指令中执行该操作。

#include <avr/io.h>

int main()
{
    PORTA|=0x01; // main.cpp:57:10: warning: compound assignment with 'volatile'-qualified left operand is deprecated [-Wvolatile]
}

0000006c <main>:
  6c:   d8 9a           sbi 0x1b, 0 ; 27
  6e:   90 e0           ldi r25, 0x00   ; 0
  70:   80 e0           ldi r24, 0x00   ; 0
  72:   08 95           ret

【问题讨论】:

  • @KamilCuk:如果你不能在该机器上为给定的数据类型实现无锁原子,你必须为你的底层操作系统设置一个锁。
  • @KamilCuk:我有一个关于在给定控制器的 int var 上设置单个位的问题。在某些机器上实现无日志原子是一个有趣的讨论,但给定的控制器无法实现它。它根本没有汇编指令来从 ram 上的某个位置读取/修改/写入。所以这个讨论对给定的主题没有帮助。
  • 弃用的要点是您的单指令更新不是可移植假设。如果您不关心可移植性,您可以为此目的使用一些特定于实现的 intrinsic(或者如果您的编译器不存在这样的内在函数(还),则使用内联汇编)。

标签: c++ volatile c++20 bitflags


【解决方案1】:

基本原理是复合赋值或前后递增或递减即使在 volatile 变量上也不是原子的,而程序员可以将其视为单个操作。此外,标准规定 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

这意味着如果没有更多的预防措施,如果 2 个执行线程(这里是正常处理和一个 ISR)交错,就会发生竞争条件:

normal loads
! ISR takes the processor
ISR loads updates and stores
! return from ISR
normal updates and stores erasing the change from ISR

当程序使用临时变量时,很明显可能会发生竞争条件。

对您不利的是,C++ 委员会已弃用该用法,并打算稍后将其完全删除。

所以你可以:

  • 在代码规范中添加它取决于允许对 volatile 变量进行复合赋值,并希望编译器会为其提供选项(即使不是很好也感觉合理)
  • 添加与 C++17 兼容但不支持 C++20 及更高版本的代码规范
  • 将其更改为编译为 C 代码(C++ 标准仍支持跨 C - C++ 链接)
  • 写成some_flags = some_flags | new_set_flags;

我更喜欢最后一种方式,因为对于易失字节,编译器没有理由生成效率较低的代码,并且它从早期的 C 版本到最后的 C++ 版本都是一致的


参考资料:

【讨论】:

  • 我看不出弃用 volatile 会帮助任何人编写更好的代码,也不会让编译器供应商更容易。好吧,无论如何...特别是对于通常定义为volatile并映射到给定地址的内存映射IO寄存器,弃用易失性听起来是一个非常糟糕的主意,并且会破坏很多代码。 E1 = E1 op E2 真的没有更好 :-) 好吧,我不在标准委员会,所以我必须处理结果。即使很痛苦,我也会使用这种“新”方式来设置位。
  • @Klaus: volatile 未被弃用!不推荐使用的是 volatile 上的复合赋值...
  • 很清楚。但这将导致不推荐使用标准 IO 处理:-) PORTA|=0x01;现在不推荐打开 io 端口的位。这对我来说听起来完全不对!
  • 如果您更喜欢保留旧的 C 语法,您可以随时使用 C 模块。无论如何,我不支持委员会的决定,也不会试图责怪它:这只是我们必须忍受的事实......顺便说一句,PORTA|=0x01 总是包含一个可能的竞争条件,当多个执行线程可以存在。
猜你喜欢
  • 2012-02-09
  • 2013-01-12
  • 1970-01-01
  • 2012-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-24
相关资源
最近更新 更多