我正在尝试通过arm-none-eabi-gcc来理解为stm32f103芯片组生成的一些汇编程序,它似乎只运行了我预期的速度的一半。我对汇编程序并不熟悉,但是因为如果你想了解你的编译器正在做什么,每个人总是说要阅读asm,我看到我得到了多远。它的功能很简单:
void delay(volatile uint32_t num) {
volatile uint32_t index = 0;
for(index = (6000 * num); index != 0; index--) {}
}
时钟速度为72MHz,上述功能给我1ms的延迟,但我预计0.5ms(自(6000 * 6)/ 72000000 = 0.0005)。
汇编程序是这样的:
delay:
@ args = 0, pretend = 0, frame = 16
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
sub sp, sp, #16 stack pointer = stack pointer - 16
movs r3, #0 move 0 into r3 and update condition flags
str r0, [sp, #4] store r0 at location stack pointer+4
str r3, [sp, #12] store r3 at location stack pointer+12
ldr r3, [sp, #4] load r3 with data at location stack pointer+4
movw r2, #6000 move 6000 into r2 (make r2 6000)
mul r3, r2, r3 r3 = r2 * r3
str r3, [sp, #12] store r3 at stack pointer+12
ldr r3, [sp, #12] load r3 with data at stack pointer+12
cbz r3, .L1 Compare and Branch on Zero
.L4:
ldr r3, [sp, #12] 2 load r3 with data at location stack pointer+12
subs r3, r3, #1 1 subtract 1 from r3 with 'set APSR flag' if any conditions met
str r3, [sp, #12] 2 store r3 at location sp+12
ldr r3, [sp, #12] 2 load r3 with data at location sp+12
cmp r3, #0 1 status = 0 - r3 (if r3 is 0, set status flag)
bne .L4 1 branch to .L4 if not equal
.L1:
add sp, sp, #16 add 16 back to the stack pointer
@ sp needed
bx lr
.size delay, .-delay
.align 2
.global blink
.thumb
.thumb_func
.type blink, %function
我已经评论了我认为每条指令的含义。所以我相信.L4部分是延迟函数的循环,它是6个指令长。我确实意识到时钟周期并不总是与指令相同,但由于这是一个很大的差异,并且因为这是一个我想象的循环,并且有效地预测和流水线,我想知道是否有一个可靠的原因,我看到2个时钟每条指令的周期。
背景: 在我正在研究的项目中,我需要使用5个输出引脚来控制线性ccd,并且时序要求相当紧凑。绝对频率不会被最大化(我将使引脚的时钟慢于cpu能够的时钟),但相对于彼此的引脚时序非常重要。因此,不是使用能力极限的中断,并且可能使相对时序复杂化,我认为使用循环来提供引脚电压变化事件之间的短延迟(大约100 ns),甚至可以编译展开的汇编程序中的整个部分,因为我有足够的程序存储空间。有一段时间引脚没有变化,在此期间我可以运行ADC来采样信号。
虽然我所询问的奇怪行为不是表演限制,但我宁愿在继续之前理解它。
编辑:从评论中,arm tech ref提供指令时间。我已将它们添加到装配中。但它仍然只有9个周期,而不是我期望的12个周期。跳跃本身就是一个循环吗?
TIA,Pete
认为我必须把这个给ElderBug,尽管Dwelch提出了一些可能也非常相关的要点,所以感谢所有人。从这里开始,我将尝试使用展开的组件来切换相位相差20ns的引脚,然后返回C进行更长时间的等待,并进行ADC转换,然后返回到组装以重复该过程,密切关注组件从gcc输出来粗略了解我的时间是否正常。 BTW Elder修改后的wait_cycles函数按预期的那样工作。再次感谢。
答案 0 :(得分:10)
首先,在C中执行自旋等待循环是一个坏主意。在这里,我可以看到您使用-O0
编译(没有优化),如果启用优化,您的等待时间会短得多(编辑:实际上,您发布的未经优化的代码可能仅来自volatile
,但是这并不重要)。 C等待循环不可靠。我维护了一个依赖于这样的函数的程序,每次我们不得不更改编译器标志时,时间都会混乱(幸运的是,有一个蜂鸣器因此而失调,提醒我们更改等待循环)。
关于为什么每个周期没有看到1条指令,这是因为某些指令不需要1个周期。例如,如果采用分支,bne
可以采取额外的周期。问题是您可以使用较少的确定性因素,例如总线使用情况。访问RAM意味着使用总线,该总线可能忙于从ROM获取数据或由DMA使用。这意味着STR
和LDR
等说明可能会延迟。在您的示例中,您在同一位置(STR
的典型值)后面有LDR
后跟-O0
;如果MCU没有存储到转发转发,则可能会有延迟。
我为时间做的是使用硬件定时器延迟1μs以上,并使用硬编码汇编循环来实现真正的短暂延迟。
对于硬件定时器,您只需要以固定频率设置定时器(如果您希望延迟精确到1μs,周期<1μs),并使用一些简单的代码:
void wait_us( uint32_t us ) {
uint32_t mark = GET_TIMER();
us *= TIMER_FREQ/1000000;
while( us > GET_TIMER() - mark );
}
您甚至可以使用mark
作为参数在某项任务之前设置它,并使用该函数等待之后的剩余时间。示例:
uint32_t mark = GET_TIMER();
some_task();
wait_us( mark, 200 );
对于程序集等待,我将这个用于ARM Cortex-M4(靠近你的):
#define CYCLES_PER_LOOP 3
inline void wait_cycles( uint32_t n ) {
uint32_t l = n/CYCLES_PER_LOOP;
asm volatile( "0:" "SUBS %[count], 1;" "BNE 0b;" :[count]"+r"(l) );
}
这非常简短,精确,不受编译器标志和总线负载的影响。
您可能需要调整CYCLES_PER_LOOP
,但我认为它与您的MCU具有相同的值(SUBS+BNE
为1 + 2)。
答案 1 :(得分:3)
这是一个cortex-m3,所以你可能用完了闪光灯?你尝试从ram运行和/或调整闪光速度,或调整时钟与闪光速度(减慢主时钟),这样你就可以使闪存尽可能接近每个访问的单个循环。
你也正在对这些指令的一半执行内存访问,这对于获取是一个周期或更多(如果你在同一时钟上运行sram,则为一个)和ram访问的另一个时钟(由于使用volatile) 。因此,这可能占每个时钟每个时钟和两个时钟之间差异的一定百分比,分支可能也会花费多于一个时钟,在m3上不确定是否可以打开或关闭(分支预测)和分支预测它的工作方式有点滑稽,如果它太接近fetch块的开头那么它就不会工作,所以分支在ram中会影响性能,其中任何一个在ram中会影响性能,你可以通过在代码前面的任何地方添加nop来进行实验,以改变循环的对齐方式,影响缓存(这里你可能没有),并且还可以根据指令在提取中的大小和位置影响其他内容。 (例如,有些武器一次只能获取8条指令。)
你不仅需要知道汇编才能理解你想要做什么,而是如何操纵汇编和其他事情,如对齐,重新安排指令组合,有时更多指令比更少指令等等。管道和缓存很难最好地预测是否存在,并且可以轻易地用手优化的代码抛弃假设和实验。
即使你克服了缓慢的闪存,缺少缓存(虽然你不能依赖它的性能),以及其他东西,核心和I / O之间的逻辑以及用于钻头敲击的I / O速度可能是另一个性能损失,没有理由期望I / O每次访问只有少量的周期,它甚至可能是两位数的时钟。在本研究的早期阶段,您需要启动gpio只读循环,只写循环和读/写循环。如果您依靠gpio逻辑只触摸端口中的一个位而不是可能具有周期成本的整个端口,那么您也需要对其进行性能调整。
你可能想要研究使用cpld,如果你甚至接近时间上的余量并且必须是实时的硬件,因为一个额外的代码行或编译器的新版本可以完全抛弃时间该项目。