【问题标题】:Interleaving updates of volatile variables (registers)?易失性变量(寄存器)的交错更新?
【发布时间】:2021-05-12 05:08:49
【问题描述】:

我正在为 GPIO 编写 C++ 模板包装器。对于 STM32,我使用 HAL 和 LL 代码作为基础。 GPIO 初始化归结为一系列read register to temp variable -> Mask pin specific bits in temp -> shift and write pin specific bits in temp -> write temp back to register。寄存器声明为volatile

首先对 volatile 进行所有读取,然后是所有更新,然后是对 volatile 的所有写入,而不是像现在这样(在例如ST的代码)?当然,写入仍然是有序的。

所以从场景A

uint32_t temp;
temp = struct->reg1;
temp |= ...
temp &= ...
struct->reg1 = temp;
temp = struct->reg2;
temp |= ...
temp &= ...
struct->reg2 = temp;

到场景B

uint32_t temp1, temp2;
temp1 = struct->reg1;
temp2 = struct->reg2;
temp1 |= ...
temp1 &= ...
temp2 |= ...
temp2 &= ...
struct->reg1 = temp1;
struct->reg2 = temp2;

场景 B 可能会使用更多(或 4 个)内存,但不必像我预期的那样经常中断主程序流。是否可以在场景 B 中对代码进行更多优化,例如通过组合读取或写入?

【问题讨论】:

  • 不会有太大区别。无法优化对易失性寄存器的访问,并且 STM 没有内存缓存……场景 A 使用的堆栈空间比场景 B 少一点,可以忽略不计。它也更具可读性,这是您在这里唯一需要瞄准的东西。
  • 一般来说它可能有不同的行为。在第一个版本中,您写入reg1,然后从reg2 读取。对reg1 的易失性写入可能会影响从reg2 读取的值?只有您可以知道这是否是您的代码可能遇到的实际场景,但需要牢记这一点。
  • 场景 B 需要 2 个临时变量,而 A 只需要 1 个。鉴于 STM32 没有很多备用寄存器来保存临时值,因此 A 是一个更优的解决方案。如有疑问,请始终检查/比较生成的程序集。
  • @MichaëlRoy STM has no memory cache 真的吗?发帖前检查。
  • @MichaëlRoy 你写了关于内存缓存的文章。许多 STM uC 确实有内存缓存。如果硬件寄存器地址是可缓存的,则不会。

标签: c++ embedded stm32 cpu-registers


【解决方案1】:

不会有任何区别。代码将完全相同的效率

void zoo(uint32_t val1, uint32_t val2)
{
    uint32_t moder = GPIOA -> MODER;
    uint32_t otyper = GPIOA -> OTYPER;
    moder &= val1;
    moder |= val2;
    otyper &= val1;
    otyper |= val2;
    GPIOA -> MODER = moder;
    GPIOA -> OTYPER = otyper;
}

void boo(uint32_t val1, uint32_t val2)
{
    uint32_t val = GPIOA -> MODER;
    val &= val1;
    val |= val2;
    GPIOA -> MODER = val;
    val = GPIOA -> OTYPER;
    val &= val1;
    val |= val2;
    GPIOA -> OTYPER = val;
}

这不是存在问题,因为您仅在初始化期间访问了多个 GPIO 寄存器。引脚配置通常仅在程序启动时设置,有时在进入和退出低功耗模式时设置(例如,我们将引脚设置为模拟模式以消耗尽可能少的电流)。性能不是现阶段的首要任务。

通常您只会访问一个寄存器:

BSRR - 设置引脚(但此寄存器是只写) ODR - 设置和读取我们设置的内容 IDR - 实际引脚电平(只读

在一些 STM 微控制器中,BSRR 被分为两个寄存器 BRR 和 BSR,但它们也是只写

IMO 您尝试对完全不需要的东西进行微优化。

https://godbolt.org/z/xWqWo9

【讨论】:

    【解决方案2】:

    首先对 volatile 进行所有读取,然后是所有更新,然后是对 volatile 的所有写入,而不是像现在这样(在以ST的代码为例)?

    所以除了检查之外别无他法!以下代码:

    // based on code from https://github.com/ARM-software/CMSIS
    #include <stdint.h>
    #define __IO volatile
    typedef struct
    {
      __IO uint32_t CR;
      __IO uint32_t CSR;
    } PWR_TypeDef;
    #define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
    #define APB1PERIPH_BASE       PERIPH_BASE
    #define PWR_BASE              (APB1PERIPH_BASE + 0x7000)
    #define PWR                 ((PWR_TypeDef *) PWR_BASE)
    
    #define  PWR_CR_LPDS                         ((uint16_t)0x0001)     /*!< Low-Power Deepsleep */
    #define  PWR_CR_PDDS                         ((uint16_t)0x0002)     /*!< Power Down Deepsleep */
    #define  PWR_CR_CWUF                         ((uint16_t)0x0004)     /*!< Clear Wakeup Flag */
    #define  PWR_CR_CSBF                         ((uint16_t)0x0008)     /*!< Clear Standby Flag */
    #define  PWR_CR_PVDE                         ((uint16_t)0x0010)     /*!< Power Voltage Detector Enable */
    
    #define  PWR_CSR_WUF                         ((uint16_t)0x0001)     /*!< Wakeup Flag */
    #define  PWR_CSR_SBF                         ((uint16_t)0x0002)     /*!< Standby Flag */
    #define  PWR_CSR_PVDO                        ((uint16_t)0x0004)     /*!< PVD Output */
    #define  PWR_CSR_EWUP                        ((uint16_t)0x0100)     /*!< Enable WKUP pin */
    
    void func_separate() {
        // just a meaningless example for testing
        uint32_t temp;
        temp = PWR->CR;
        temp &= PWR_CR_LPDS | PWR_CR_PDDS | PWR_CR_CWUF;
        temp |= PWR_CR_CWUF;
        PWR->CR = temp;
        temp = PWR->CSR;
        temp &= PWR_CSR_WUF | PWR_CSR_SBF;
        temp |= PWR_CSR_PVDO | PWR_CSR_EWUP;
        PWR->CSR = temp;
    }
    
    void func_together() {
        uint32_t temp1, temp2;
        temp1 = PWR->CR;
        temp2 = PWR->CSR;
        temp1 &= PWR_CR_LPDS | PWR_CR_PDDS | PWR_CR_CWUF;
        temp1 |= PWR_CR_CWUF;
        temp2 &= PWR_CSR_WUF | PWR_CSR_SBF;
        temp2 |= PWR_CSR_PVDO | PWR_CSR_EWUP;
        PWR->CR = temp1;
        PWR->CSR = temp2;
    }
    

    outputs on godbolt with gcc ARM 8.2 -O3 -mlittle-endian -mthumb -mcpu=cortex-m3:

    func_separate:
            ldr     r2, .L3
            ldr     r3, [r2]
            and     r3, r3, #7
            orr     r3, r3, #4
            str     r3, [r2]
            ldr     r3, [r2, #4]
            and     r3, r3, #3
            orr     r3, r3, #260
            str     r3, [r2, #4]
            bx      lr
    .L3:
            .word   1073770496
    func_together:
            ldr     r1, .L6
            ldr     r2, [r1]
            ldr     r3, [r1, #4]
            and     r2, r2, #7
            and     r3, r3, #3
            orr     r2, r2, #4
            orr     r3, r3, #260
            str     r2, [r1]
            str     r3, [r1, #4]
            bx      lr
    .L6:
            .word   1073770496
    

    唯一的区别是指令的顺序。在性能方面没有区别。所以Would it make sense (in terms of reducing overhead / improving performance) - 没有。

    但就可读性而言,首选第一个版本是有意义的。

    【讨论】:

      【解决方案3】:

      在这种特定情况下,没关系。

      一般来说,如果可以避免的话,建议不要在多行中访问单个硬件寄存器。最好将所有内容写入临时 RAM 变量,并且只对寄存器进行一次读写。

      这与执行时间没有太大关系,而是读取和写入硬件寄存器可能会带来许多副作用,例如清除标志或影响实时。

      此外,像temp1 |= ... temp1 &amp;= ... 这样的临时变量可以很容易地被编译器优化,这很可能使用 CPU 寄存器而不是堆栈分配。

      另一件值得一提的是,对硬件寄存器的读/写不能得到优化或重新排序,因为它们是volatile 合格的。出于这个原因,您需要尽量减少对寄存器的访问,以节省一点点执行时间,同时让编译器更有效地优化周围的代码。

      【讨论】:

      • 您确定 ST 寄存器映射吗?他们没有工会。主要是结构和类型定义。映射到硬件寄存器的结构成员被定义为volatile。硬件地址被指针转换为这些结构。在 STM32 中使用 C++ 时,我没有遇到与寄存器相关的问题。
      • @Tagli 嗯,实际上我可能会将它与其他一些英国媒体报道软件库混淆。我会删除那部分。
      猜你喜欢
      • 2015-11-07
      • 1970-01-01
      • 2023-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多