5

我们继承了我一直在关注的针对瑞萨 RX231 微控制器的项目。

这个 uC 只有一条指令可以锁定总线原子性 (XCHG)。

因为处理器是访问 RAM 内存的唯一组件(不使用 DMA 或 DTC),所以要操作与中断共享的用户代码中的变量,在访问时间内禁用中断(在处理器状态字寄存器中),即

disable_interrupts(); /* set_psw(get_psw() & ~(1 << 16)); */
/* access or modify shared variables */
enable_interrupts();  /* set_psw(get_psw() | (1 << 16)); */

但是,也有一些“标志”在没有保护的情况下被共享,它们在中断中设置并在用户代码中通过以下方式轮询:

volatile unsigned char event_request_message = 0;
unsigned char condition_sending_message = 0;

#pragma interrupt
void on_request_message()
{
     ...
     event_request_message = 1; // mov.l   #0x3df5, r14
                                // mov.b   #1, [r14]
     ... 
}

void user_code()
{
     for(;;)
     {
         ...
         /* might be evaluated multiple times before transmit message is completed */
         if(event_request_message && !condition_sending_message) // mov.l   #0x3df5, r14
                                                                 // movu.b  [r14], r14
                                                                 // cmp     #0, r14
                                                                 // beq.b   0xfff8e17b <user_code+185>
                                                                 // mov.l   #0x5990, r14
                                                                 // movu.b  [r14], r14
                                                                 // cmp     #0, r14
                                                                 // bne.b   0xfff8e16f <user_code+173>
         {
              event_request_message = 0;     // mov.l   #0x3df5, r14  
                                             // mov.b   #0, [r14]                  
              condition_sending_message = 1; // mov.l   #0x5990, r14               
                                             // mov.b   #1, [r14]
              /* transmit message */ 
              ... 
         }
         ...
     }
}

在这种情况下,我对无保护(通过禁用用户代码中的中断)的理解是:

  • 要读取、设置或清除“标志”,总是使用两条指令,一条将内存地址放入寄存器,一条用于读取/设置/清除
  • 内存地址始终相同,因此可以不考虑
  • 然后每个读取/设置/清除操作都是一条指令,因此访问/操作是原子的

问题是:我的理解正确吗?在这种情况下,这样的“标志”变量访问和操作是否安全?
或者可能有任何可能的错误/错误?

  • 假设使用的编译器和编译器选项始终相同。
  • 假设所描述的操作是访问/操作此类“标志”的唯一方式(设置为 0 或 1,读取(全部以汇编代码显示))(无加法、乘法等)

如果我们需要升级编译器或更改编译器选项怎么办?
如此简单的操作会导致不止“一条指令”吗?

在没有保护的情况下使用此类“标志”的理由过于限制了禁用中断的时间。

查看代码逻辑,预期的行为是您可以请求一次或多次消息,但您只能得到一个答案。

PS。我尝试使用以下附加标签:“cc-rx”、“rxv2-instruction-set”、“rx231”。

4

1 回答 1

4

根据您的目标,即您是仅为特定平台编写还是希望确保可移植性,您需要记住几件额外的事情:

  1. 启用优化后,只要操作的最终结果与单线程方案无法区分,许多编译器就会很乐意重新排序对 volatile 变量的访问和对非 volatile 变量的访问。这意味着这样的代码:

    int a = 0;
    volatile int b = 0;
    
    void interrupt_a(void) 
    {
        a = b + 1;
        b = 0;       // set b to zero when done
    }
    

    可以由编译器重新排列为:

    load acc from [b]
    store 0 into [b]  // set b to zero *before* updating a, to mess with you a bit
    add 1 to acc
    store acc into [a]
    

    防止优化编译器重新排序的方法是使这两个变量都可变。(或者如果可用,将 C11_Atomicmemory_order_release存储和memory_order_acquire加载一起使用,以针对非原子变量的操作对其进行排序。)

    如果您使用的是多核 uC,它可以重新排序内存操作,因此这并不能解决问题,如果您关心其他上的观察者,实际的解决方案是为编译器和 CPU 发出栅栏核心(或在 MMIO 中,甚至在单核 uC 上)。在单个内核或单个线程上不需要硬件围栏指令,因为即使是乱序执行 CPU 也会看到它自己的操作按程序顺序发生。

    同样,如果您使用特定嵌入式系统的工具链获得的编译器对栅栏一无所知,那么它很可能不会做这样的事情。因此,您需要检查文档并检查已编译的程序集。

    例如,ARM 文档声明处理器“允许”重新排序指令,程序员应该注意添加内存屏障,但之后它还声明(在“实现细节”下)Cortex M 处理器不重新排序指令. 然而,他们仍然坚持应该插入适当的屏障,因为这将简化移植到新版本处理器的过程。

  2. 根据您的流水线长度,在您发出请求后,可能需要几条指令才能完全启用或禁用中断。同样,您需要检查此特定 uC/编译器的文档,但有时在写入寄存器后需要设置某种栅栏。例如,在 ARM Cortex 上,您需要在禁用中断后同时发出 DSB 和 ISB 指令,以确保在接下来的几条指令中不会进入中断

    // you would have to do this on an ARM Cortex uC
    DisableIRQ(device_IRQn); // Disable certain interrupt by writing to NVIC_CLRENA
    DSB(); // data memory barrier
    ISB(); // instruction synchronization barrier
    
    // <-- this is where the interrupt is "really disabled"
    

    当然,当您调用 时,您的库本身可能包含所有必需的围栏指令disable_interrupts();,或者此架构可能根本不需要它们。

  3. 增量操作 ( x++)应该被认为是原子的,即使它可能“意外地”在某个单核 CPU 上变成原子的。正如您所注意到的,它在您的特定 uC 上不是原子的,保证原子性的唯一方法是禁用围绕此操作的中断。

因此,最终,您应该确保您阅读了该平台的文档并了解编译器可以做什么和不能做什么。如果编译器在您添加了一个看似微小的更改后决定重新排序指令,那么今天有效的某些东西明天可能无法工作,特别是因为竞争条件可能不够频繁而无法立即检测到它。

于 2018-03-21T15:07:56.233 回答