C语言 更新:SAM3X问题与系统棒处理程序和访问系统棒值的微秒分辨率



我目前正在与一段非常简单的代码作斗争,这段代码表明ARM GCC的一级优化器以某种方式破坏了一个简单的公式。
它运行在最新的Atmel 6.2 Studio使用标准编译器设置(01)。
Atmel ToolchainARM GCCNative4.8.1426 ARM -gnu- Toolchain

代码非常少:

volatile uint32_t g_timing_tick_ms=0;
void SysTick_Handler(void)
{
    g_timing_tick_ms++;
}
inline uint32_t get_millis()
{
    return g_timing_tick_ms;
}
uint32_t get_micros()
{
    return  (g_timing_tick_ms * 1000 + (1000 - SysTick->VAL/84));
}
uint8_t timer_expired(timer_ *t)
{
    uint32_t cur_us = get_micros(); 
    uint32_t dt = cur_us - t->last_systick_us;
    t->last_systick_us = cur_us;
    if (t->elapsed <= dt)
    {
// <--------- dt is regularly a huge value (around 0xfffffe00)
// this happens because t->last_systick_us sometimes is bigger than cur_us (overflow)
// however get_micros() is without such an error, cur_us ALWAYS increases and the    
// variables are not modified outside this function which is called every 500us.
        t->elapsed = t->interval;
        return 1;
    }
    t->elapsed -= dt;
    return 0;
};

get_millis返回每毫秒调用一次的Systick计时器的毫秒数。
系统棒定时器为24位,以84mhz的速率倒数。get_micros()使用这个系统棒值并计算自上次重置以来经过的微秒数,然后加上毫秒数*1000。
这工作得很好,我找不到更快的方法来获得当前微秒作为时间戳。

第三个函数显示了一个偶发的问题,存储在t->lastrongystick_us(直接来自get_micros())中的值有时会大于它应有的值。确切地说,最后三个十进制值总是986(20065986,1000986)。
这个值大约是1000us太高了,总是在十进制数的末尾加上986。


解决方案:1)改变:

uint32_t dt = cur_us - t->last_systick_us; ---> 
volatile uint32_t dt = cur_us - t->last_systick_us;

将这个变量更改为volatile解决了这个问题,这会导致编译器以一种不好的方式处理它。变量不是静态的,它是本地的,没有任何东西可以从外部修改它,volatile是一种浪费,但解决了数学问题。2)改变

uint32_t get_micros() ----->Inline uint32_t get_micros()这也解决了问题,但这不是一个很好的修复,因为编译器不必把它内联。因此,这可能会在未来的某个时候适得其反。

3)在值更改之前向计时器函数中添加任何调试写入或类似操作也可以修复它,具体取决于代码。

这似乎是gcc-ARM核心编译器中的一个bug,优化器在某种程度上破坏了数学。我可以提供asm,我不知道ARM asm,但我注意到它在get_micro()公式附近的部分删除了一个"sub"。
我不认为我在这里有一个代码错误,它太简单(和工作得太好)。此外,解决方案表明,这不是一个编码错误,从函数中添加或删除内联应该没有任何区别,除了优化。

也许有人知道该怎么做,经验/解决类似的行为。我正处于完全删除优化器的边缘,但这可能会消耗相当多的性能。

当我意识到可能的原因时,我正要准备asm差异(并通读它),我认为就是这样。

我认为这是一个竞争条件,系统棒的中断还没有触发,但是系统棒定时器溢出。
结果是一个大约1000us的误差(稍微小一点,因为计时器每84ns滴答一次)。这将导致我的错误,不可预测的,通过改变代码,周期改变,通过改变周期,它可能以一种方式对齐代码,导致竞争条件出现。

我调试了一下,可以验证问题在系统棒重新加载后不久就发生了。

很抱歉在编译错误中做了一个太快的猜测。

问题是由于竞争条件,编译器没有错。我不完全确定,但我认为这是SAM3x8e ARM实现(或一般的Cortex M3)的弱点,或者他们没有考虑到人们使用IRQ和Systick值。

无论我尝试什么修复或代码,我总是有两种情况之一:在get_micros()计算期间触发中断get_micros()期间系统棒溢出,但没有触发中断。

get_micros()读取旧的毫秒/系统棒值和一个新的系统棒/毫秒变量,导致将近1ms的误差。

可以考虑添加NVIC_DisableIRQ(SysTick_IRQn);一开始是有帮助的。它没有,它在ASF中没有记录,但NVIC不处理系统棒启用/禁用,IRQn是负的,不会对异常产生任何影响。
有趣的是,NVIC被用来在atmel驱动程序代码中设置优先级,可能也没有效果。另一个有趣的方面是atmel在它自己的一些源代码示例中完全使用了这个调用。(好吧,浪费了6个小时就不那么好笑了)

我尝试用__disable_irq()保护代码而没有积极的影响,发生了相同的竞争条件(计时器改变了,但系统棒值还没有运行完)

我试过了:

if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) micro_us+=1000;

这将读取系统控制寄存器,并检查自上次读取以来计数器是否已溢出。
根据atmel sam3x8ek数据表,应该这样做。
然而,读取此寄存器的两个未记录的副作用:
1)启动另一个系统棒中断!
2)将countflag值重置为0
数据表中没有关于这两个动作的一个字,2)没用,但不会成为问题1)是一场好戏。

禁用IRQ的唯一方法是在系统处理程序SCB->SHCSR中。
但是,如果发生这种情况,将导致崩溃(硬故障)。

可能的解决方案:禁用系统棒的时钟,等待等待irq发生,然后继续。这将确保读值和读中断是同步的,它将引入一个小的定时错误,并在函数本身中花费额外的时间。

经过大约4-5个小时的调试和与错误或未记录的特性的斗争,我想出的最佳解决方案是以下代码:
  uint32_t get_micros()
{
    //__disable_irq(); // does not affect systick
    static uint32_t last_value;
    volatile uint32_t timestamp = g_timing_tick_ms; // set to volatile to make sure the compiler does not optimize here
    volatile uint32_t val = SysTick->VAL;
    uint32_t micro_us = (timestamp * 1000 + (1000 - val/84));
    if (last_value > micro_us) micro_us+=1000; // Hack: race condition only causes a 1ms delay, this solves it
    last_value = micro_us;
    //if (SysTick->VAL > val ) micro_us+=1000; // undocmented, causes VAL reset to 0
    //if (NVIC_GetPendingIRQ(SysTick_IRQn)) micro_us+=1000; // asf undocumented, does not handle systick (system handler)
    //if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) micro_us+=1000;   // triggerd undocumented systick interrupt 

    return  micro_us; // hardcoded auf 84mhz
}

我希望这能为别人节省我不得不花费的时间。
它引入了一个新变量,并保留了最后一个值的运行副本,如果时间开始向后流动,它会向该值添加1毫秒(错误总是1毫秒)。

如果这看起来不够清晰:
我能想到的唯一精益解决方案是停止Sysclock并使用Timer代替。计时器有更好的文档记录(至少对于基本使用),它们工作可靠。SAM3配备了9个计时器。

您只需检查g_timing_tick_ms在计算过程中没有变化:

uint32_t get_micros()
{   
    uint32_t before_ms, after_ms, calc_micros;
    do {
          before_ms = g_timing_tick_ms;
          calc_micros = before_ms * 1000 + (1000 - SysTick->VAL/84);
          after_ms = g_timing_tick_ms;
    } while (before_ms != after_ms);
    return  calc_micros;
}

我在STM32F1 (Cortex M3)项目上遇到了同样的问题,并提出了以下问题:

volatile uint32_t time_ovf = 0;
void SysTick_Handler(void)
{
    time_ovf += 1000;
}
uint32_t micros(void)
{
    __disable_irq();    // asm("cpsid i");
    uint32_t m = time_ovf;
    uint32_t t = SysTick->VAL;      // TODO: assume HCLK = 72 MHz
    // Check pending overflow IRQ after we disabled interrupts
    uint32_t o = SCB->ICSR & SCB_ICSR_PENDSTSET_Msk;
    // If overflow and counter rolled over, add 1000 to the microseconds count
    if (o && t > 36000) m+= 1000;
    __enable_irq();     // asm("cpsie i");
    // Systick counts downwards, so subtract it from 999.
    return (m + (999 - t / 72));
}

SysTick定时器配置为LOAD值= 71999,其时钟为72 MHz,因此它每毫秒溢出一次(就像您的定时器一样)。这个想法是在禁用中断的情况下拍摄溢出计数(time_ovf)和SysTick计数(SysTick->VAL)的快照,然后检查两者之间是否发生溢出,并通过查看SCB->ICSR的PENDSTSET位将1000添加到计算中。

这给出了一个单调递增的时间戳,在2^32处自然溢出,因此加减运算可以很好地使用它。

我知道这是从2014年开始的,但我希望它能帮助那些偶然遇到同样事情的人…