我目前正在处理一段非常简单的代码,这段代码表明ARM GCC的一级优化器以某种方式破坏了一个简单的公式。
这在最新的Atmel 6.2studio上运行,使用标准编译器设置(O1)。
Atmel工具链\ARM GCC\Native\4.8.1426\ARM gnu工具链
代码非常少:
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(200659861000986)。
该值大约为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获取micros()--->
内联uint32_t get_micros()
这也解决了这个问题,但是这也不是一个好的修复方法,因为编译器不必把它放在内联的地方。因此,这可能在未来某个时候适得其反。
三)
根据代码的不同,在更改值之前将任何调试写入或类似内容添加到计时器函数中也可以修复它。
这似乎是gcc ARM核心编译器中的一个错误,优化器不知何故破坏了数学。
我可以提供asm,我不知道ARM asm,但我注意到它在靠近get_micro()公式的部分删除了一个“sub”。
我认为这里没有代码错误,它太简单了(而且工作得也太好了)。
此外,这些解决方案还表明,这不是一个编码错误,除了优化之外,在函数中添加或删除inline不应该有任何区别。
也许有人知道该怎么做,经历过/解决过类似的行为。
我正处于完全删除优化器的边缘,但这可能会消耗大量性能。
更新
我刚刚准备好asm差异(并通读一遍),这时我意识到了可能的原因,我想是这样的。
我认为这是一个竞争条件,Systick的中断尚未触发,但Systick计时器溢出。
结果是大约1000us的误差(比计时器每84ns计时一次小一些)。
这将导致完全的me错误,不可预测,并且通过更改代码,周期将通过更改周期来更改,它可能会以导致稍后出现竞争条件的方式对齐代码。
我进行了调试,可以验证问题是否在Systick重新加载后不久发生。
很抱歉,在编译器错误中猜测得太快了。
最佳答案
问题是由于竞争条件造成的,编译器没有出现错误。
我不完全确定,但我认为这是SAM3x8e手臂运动(或一般皮层M3)中的一个弱点,或者他们没有考虑使用IRQ和Systick值的人。
无论我尝试了什么修复或代码,我总是有两种情况之一:
在get_micros()计算期间触发的中断
Systick在get_micros()期间溢出,但未触发中断。
get_micros()读取旧的毫秒/Systick值和新的Systick/毫秒变量,导致将近1毫秒的错误。
有人可能会认为,在开头添加NVIC DisableIRQ(SysTick_IRQn)会有帮助。
它没有,它在ASF中没有记录,但NVIC不处理Systick启用/禁用,IRQn为负,不会对异常产生任何影响。
有趣的是,NVIC用于设置Atmels驱动程序代码中的优先级,可能也没有效果。
另一个有趣的方面是atmel在它自己的一些源代码示例中使用了这个调用。。(在浪费了6个小时后,这可不好笑)
我试着用u disable_irq()保护代码,但没有产生积极的效果,出现了相同的竞争条件(计时器已更改,但Systick值尚未超过)
我试过这个:
if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) micro_us+=1000;
这将读取systick控制寄存器,并检查自上次读取以来计数器是否溢出。
它应该按照atmel sam3x8ek数据表来做。
然而,读取此寄存器的两个未记录的副作用:
1)它启动另一个Systick中断!
2)它将countflag值重置为0
关于数据表中的两个操作,没有一个字是无用的,但也不是问题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毫秒)。
如果看起来不够干净:
我唯一能想到的精益解决方案是停止系统时钟,而使用计时器。
计时器有更好的文档记录(至少用于基本用途),并且工作可靠。
SAM3有9个定时器。