更新:SAM3X问题与systick处理程序和访问systick值为微秒分辨率

时间:2014-07-06 02:18:33

标签: c gcc arm compiler-optimization

我目前正在努力处理一段非常简单的代码,这些代码表明ARM GCC的1级优化器会以某种方式损坏一个简单的公式。
这可以使用标准编译器设置(O1)在最新的Atmel 6.2 Studio上运行 Atmel工具链\ ARM GCC \ Native \ 4.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计时器的毫秒数,该计时器每毫秒调用一次 systick计时器是24位,并以84mhz的速率倒计时 get_micros()使用此systick值并计算自上次重置后经过的微秒,然后添加毫秒* 1000。 这很有效,我找不到更快的方法来获取当前的微秒作为时间戳。

第三个函数显示偶发问题,有时存储在t-&gt; last_systick_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可解决此问题,从而导致编译器以错误方式操作它。 变量不是静态的,它是本地变量而NOTHING是从外部修改它,volatile是浪费但是解决了数学问题。 2)改变

uint32_t get_micros()-----&gt; 内联uint32_t get_micros() 这也解决了这个问题,但是由于编译器不必将其内联,因此这不是一个好的修复。所以这可能会在未来某个时候适得其反。

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

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

也许有人知道该做什么,有经验/解决过类似的行为。 我处于完全删除优化器的边缘,但这可能会花费很多性能。

更新

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

我认为这是一个竞争条件,Systick的中断还没有发射,但是systick计时器溢出 结果是大约1000us的误差(随着计时器每84ns滴答一点点。) 这将导致我完全错误,不可预测,并通过更改循环更改循环更改循环,它可能会以一种导致竞争条件出现的方式对齐代码。

我进行了调试,可以在Systick重新加载后验证问题是否发生。

很抱歉在编译器错误中猜得太快。

3 个答案:

答案 0 :(得分:2)

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

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

get_micros()读取旧的毫秒/ Systick值和新的systick / millisecond变量,导致差不多1ms的错误。

有人可能会想到添加NVIC_DisableIRQ(SysTick_IRQn);一开始有帮助。 它没有,它在ASF中没有记录,但是NVIC没有处理Systick启用/禁用,IRQn是否定的,并且不会对异常产生任何影响。
有趣的是,NVIC用于设置Atmels驱动程序代码的优先级,可能也没有效果 另一个有趣的方面是atmel在其自己的一些源代码示例中使用了完全相同的调用..(浪费了6个小时的时间后就没那么好笑了)

我尝试使用__disable_irq()保护代码而没有正面效果,发生相同的竞争条件(计时器已更改,但Systick值尚未运行)

我试过了:

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

这将读取systick控制寄存器并检查自上次读取后计数器是否溢出 根据atmel sam3x8ek数据表,它应该这样做 但是,阅读此寄存器的两个无证副作用:
1)它启动另一个Systick中断!
2)它将countflag值重置为0
关于数据表中的两个动作,没有一个词,2)是无用的,但不是一个问题1)是一个showstopper。

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

可能的解决方案:禁用systick的时钟,等待挂起的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
}

我希望这可以节省一些我必须花费的时间 它引入了一个新变量并保留了最后一个值的运行副本,如果时间开始向后流动,则会向该值添加一个ms(错误始终为1ms)。

如果这看起来不够干净:
我能想到的唯一精益解决方案是停止Sysclock并改为使用Timer 定时器更好地记录(至少基本用途)并且它们可靠地工作。 SAM3配有9个计时器。

答案 1 :(得分:2)

您只需检查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;
}

答案 2 :(得分:1)

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

def print_mah(n):
    if n <= 0:
        return
    else: 
        print_mah(n-1)
        print('mah')

print_mah(3)

SysTick定时器配置为LOAD值= 71999,时钟频率为72 MHz,因此每毫秒溢出一次(就像你的那样)。想法是在中断禁用的情况下拍摄溢出计数(time_ovf)和SysTick计数(SysTick-> VAL)的快照,然后检查两者之间是否发生溢出,如果是,则通过查看SCB-> ICSR的PENDSTSET位。

这使得单调递增的时间戳在2 ^ 32处自然溢出,因此加/减算术可以很好地与它一起工作。

我知道这是从2014年开始的,但我希望它可以帮助那些偶然发现同样事情的人......