【问题标题】:Update: SAM3X problems with systick handler and accessing systick value for microsecond resolution更新:SAM3X 与 systick 处理程序的问题和访问 systick 值以获得微秒分辨率
【发布时间】:2014-08-26 20:19:07
【问题描述】:

我目前正在努力处理一段非常简单的代码,它表明 ARM GCC 的 1 级优化器以某种方式破坏了一个简单的公式。
它使用标准编译器设置 (O1) 在最新的 Atmel 6.2 Studio 上运行。
Atmel Toolchain\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->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 解决了这个问题,这导致编译器以一种糟糕的方式处理它的想法。 该变量不是静态的,它是本地变量,没有什么是从外部修改它,易失性是一种浪费,但可以解决数学问题。 2) 改变

uint32_t get_micros() -----> 内联 uint32_t get_micros() 这也解决了这个问题,但这也不是一个好的解决方法,因为编译器不必将其内联。所以这可能会在未来的某个时候适得其反。

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

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

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

更新

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

我认为这是一个竞争条件,Systick 的中断尚未触发但 systick 计时器溢出。
结果是大约 1000us 的错误(随着计时器每 84ns 滴答一次而小一点。
这将导致我的错误,不可预测,并且通过更改代码,周期会通过更改周期而更改,它可能会以导致竞争条件稍后出现的方式对齐代码。

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

很抱歉在编译器错误中做出过快的猜测。

【问题讨论】:

  • 你为什么不发布你的函数的反汇编?这将使包括我自己在内的许多人的事情变得容易得多。
  • @Jake'Alquimista'LEE:感谢您的回复。我想我必须后退两步:/我更新了我的问题,问题似乎在我的尽头。
  • 我试图通过详细说明问题来帮助人们,但我收到了负票。我想最好的办法是删除问题和相关的解决方案?
  • 我知道这个问题比较老,但我对 SAM3N systick 也有过类似的令人沮丧的经历。这个问题和你的回答很有用。请不要删除。

标签: c gcc arm compiler-optimization


【解决方案1】:

我在 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) 的快照,然后检查两者之间是否发生溢出,如果发生溢出,则通过查看 PENDSTSET 将 1000 添加到计算中一点SCB->ICSR。

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

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

【讨论】:

    【解决方案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;
    }
    

    【讨论】:

    • 那行不通。如果 Systick->VAL 刚刚溢出但尚未触发中断怎么办?我有很多这样的情况,并在调试器中观察到它们。在这种情况下,微值将降低 1ms,并且之前/之后将保持不变。
    • 除非 Systick 优先级较低,否则代码将无法在中断上下文中工作。 Systick 处理程序必须能够运行,并且将在 VAL 下溢时立即运行。大多数调试器在单步执行时会吞下 systick 等异常,因此在此处使用调试器可能会产生误导。
    【解决方案3】:

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

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

    get_micros() 读取一个旧的毫秒/Systick 值和一个新的 systick/毫秒变量,导致将近 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) 是一个展示者。

    禁用 IRQ 的唯一方法是在系统处理程序 SCB->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
    }
    

    我希望这可以节省我不得不花费的时间。
    它引入了一个新变量并保留了最后一个值的运行副本,如果时间开始倒流,它会在该值上增加一毫秒(误差始终为 1 毫秒)。

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

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-12
    • 1970-01-01
    • 2017-03-13
    • 2018-10-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多