我正在将一些代码从 M3 移植到 M4,它使用 3 个 NOP 在串行输出时钟更改之间提供非常短的延迟。 M3指令集将NOP的时间定义为1个周期。我注意到 M4 中的 NOP 并不一定会延迟任何时间。 我知道我需要禁用编译器优化,但我正在寻找一个低级命令来为我提供可靠、可重复的时间。 在实践中,在这种特殊情况下,串行很少使用,并且可能非常慢,但我仍然想知道获得周期级延迟的最佳方法。
如果您需要如此非常短但“至少”确定性的延迟,也许您可以考虑使用除
nop
之外的具有确定性非零延迟的其他指令。
所描述的 Cortex-M4 NOP 不一定耗时。
您可以将其替换为
and reg, reg
,或者大致相当于上下文中的 nop
的内容。或者,在切换 GPIO 时,您还可以重复 I/O 指令本身以强制执行状态的最小长度(例如,如果您的 GPIO 写入指令至少需要 5ns,则重复五次以获得至少 25ns)。如果您在 C 程序中插入 nop,这甚至可以在 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 nanoseconds\n", 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 nanoseconds\n", timeDiff);
}
更新:添加 @vgru 在评论部分提到的简洁解决方案
// general but accurate (5% err at 10us delay, but 22% err at 1us delay)
#pragma GCC push_options
#pragma GCC optimize ("O3")
void delayUS_DWT(uint32_t us) {
volatile uint32_t cycles = (SystemCoreClock/1000000L)*us;
volatile uint32_t start = DWT->CYCCNT;
do {
} while(DWT->CYCCNT - start < cycles);
}
#pragma GCC pop_options
最准确但不灵活的 ASM 解决方案
// most accurate but the '16' needs to be adjusted if <84MHz
#define delayUS_ASM(us) do {\
asm volatile ( "MOV R0,%[loops]\n\t"\
"1: \n\t"\
"SUB R0, #1\n\t"\
"CMP R0, #0\n\t"\
"BNE 1b \n\t" : : [loops] "r" (16*us) : "memory"\
);\
} while(0)
但底线是,如果您的预算如此紧张,以至于串行时钟的速度与处理器时钟的速度非常接近,那么您很可能无法使其与该处理器一起工作。提高处理器中的 pll 不会改变闪存速度,它可能会使情况变得更糟(相对于处理器时钟),但 sram 应该可以扩展,因此如果您的处理器时钟上还有剩余空间并且有足够的功率预算来支持它,那么请重复实验在 sram 中,具有更快的处理器时钟速度。