我正在将一些代码从M3移植到M4,它使用3个nop在串行输出时钟变化之间提供非常短的延迟。M3指令集将NOP的时间定义为1个周期。我注意到M4中的nop并不一定会延迟任何时间。我知道我需要禁用编译器优化,但我正在寻找一个低级命令,这将给我可靠的,可重复的时间。在实践中,在这种特殊情况下,串行非常偶尔使用,并且可能非常慢,但我仍然想知道获得周期级延迟的最佳方法。
如果你需要这种非常短的,但确定性的"至少"延迟,也许你可以考虑使用其他指令,而不是具有确定性的非零延迟的nop
。
所描述的Cortex-M4 NOP不一定耗时。
你可以把它替换成,比如and reg, reg
,或者在上下文中大致相当于nop
的东西。或者,在切换GPIO时,您还可以重复I/O指令本身来强制执行状态的最小长度(例如,如果您的GPIO写入指令至少需要5ns,则重复五次以获得至少25ns)。如果您在C程序中插入nops,这甚至可以在C中很好地工作(只需重复写入端口,如果它应该是volatile
,编译器不会删除重复访问)。
当然,这只适用于非常短的延迟,否则对于短延迟,就像其他人提到的那样,等待某个定时源的繁忙循环将工作得更好(它们至少需要采样定时源所需的时钟,设置目标,并通过一次等待循环)。
使用周期计数寄存器 (DWT_CYCCNT)获得高精度定时!
注意:我也使用数字引脚和示波器测试了这一点,它是非常准确的。
参见stopwatch_delay(ticks
)和下面的支持代码,它使用STM32的DWT_CYCCNT寄存器,专门用于计数位于地址0xE0001004的实际时钟节拍。
参见main
中的一个示例,该示例使用STOPWATCH_START
/STOPWATCH_STOP
来测量stopwatch_delay(ticks)
实际花费的时间,使用CalcNanosecondsFromStopwatch(m_nStart, m_nStop)
。
修改 ticks
输入进行调整
uint32_t m_nStart; //DEBUG Stopwatch start cycle counter value
uint32_t m_nStop; //DEBUG Stopwatch stop cycle counter value
#define DEMCR_TRCENA 0x01000000
/* Core Debug registers */
#define DEMCR (*((volatile uint32_t *)0xE000EDFC))
#define DWT_CTRL (*(volatile uint32_t *)0xe0001000)
#define CYCCNTENA (1<<0)
#define DWT_CYCCNT ((volatile uint32_t *)0xE0001004)
#define CPU_CYCLES *DWT_CYCCNT
#define CLK_SPEED 168000000 // EXAMPLE for CortexM4, EDIT as needed
#define STOPWATCH_START { m_nStart = *((volatile unsigned int *)0xE0001004);}
#define STOPWATCH_STOP { m_nStop = *((volatile unsigned int *)0xE0001004);}
static inline void stopwatch_reset(void)
{
/* Enable DWT */
DEMCR |= DEMCR_TRCENA;
*DWT_CYCCNT = 0;
/* Enable CPU cycle counter */
DWT_CTRL |= CYCCNTENA;
}
static inline uint32_t stopwatch_getticks()
{
return CPU_CYCLES;
}
static inline void stopwatch_delay(uint32_t ticks)
{
uint32_t end_ticks = ticks + stopwatch_getticks();
while(1)
{
if (stopwatch_getticks() >= end_ticks)
break;
}
}
uint32_t CalcNanosecondsFromStopwatch(uint32_t nStart, uint32_t nStop)
{
uint32_t nDiffTicks;
uint32_t nSystemCoreTicksPerMicrosec;
// Convert (clk speed per sec) to (clk speed per microsec)
nSystemCoreTicksPerMicrosec = CLK_SPEED / 1000000;
// Elapsed ticks
nDiffTicks = nStop - nStart;
// Elapsed nanosec = 1000 * (ticks-elapsed / clock-ticks in a microsec)
return 1000 * nDiffTicks / nSystemCoreTicksPerMicrosec;
}
void main(void)
{
int timeDiff = 0;
stopwatch_reset();
// =============================================
// Example: use a delay, and measure how long it took
STOPWATCH_START;
stopwatch_delay(168000); // 168k ticks is 1ms for 168MHz core
STOPWATCH_STOP;
timeDiff = CalcNanosecondsFromStopwatch(m_nStart, m_nStop);
printf("My delay measured to be %d nanosecondsn", timeDiff);
// =============================================
// Example: measure function duration in nanosec
STOPWATCH_START;
// run_my_function() => do something here
STOPWATCH_STOP;
timeDiff = CalcNanosecondsFromStopwatch(m_nStart, m_nStop);
printf("My function took %d nanosecondsn", timeDiff);
}
对于任何可靠的计时,我总是建议使用通用计时器。你的那部分可能有一个计时器,它能够足够高地计时,给你所需的时间。对于串行,是否有不能使用相应串行外设的原因?我所知道的大多数Cortex M3/m4都提供USARTS, I2C和SPI,其中多个还提供SDIO,这应该可以满足大多数需求。
如果这是不可能的,这个stackoverflow问题/答案细节使用循环计数器,如果可用,在Cortex M3/M4上。你可以抓取周期计数器,并添加一些到它并轮询它,但我不认为你会实现任何合理低于~8个周期的最小延迟使用这种方法。
首先你必须从ram而不是flash中运行,因为flash计时很慢,一个nop可能需要许多周期。gpio访问应该至少需要几个时钟,所以你可能不需要/想要nops,只要敲击gpio。循环末尾的分支也很明显。您应该编写一些指令来ram和分支它,看看您可以多快地摆动gpio。
底线是,如果你的预算很紧张,你的串行时钟在速度上接近你的处理器时钟,很可能你不会让这个处理器工作。提高处理器中的锁相环不会改变闪存速度,它会使情况变得更糟(相对于处理器时钟),sram应该缩放,所以如果你的处理器时钟和功率预算上还有剩余空间来支持,那么在sram中使用更快的处理器时钟速度重复实验。