手臂

时间:2015-09-22 14:35:45

标签: c assembly arm

我正在尝试通过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函数按预期的那样工作。再次感谢。

2 个答案:

答案 0 :(得分:10)

首先,在C中执行自旋等待循环是一个坏主意。在这里,我可以看到您使用-O0编译(没有优化),如果启用优化,您的等待时间会短得多(编辑:实际上,您发布的未经优化的代码可能仅来自volatile,但是这并不重要)。 C等待循环不可靠。我维护了一个依赖于这样的函数的程序,每次我们不得不更改编译器标志时,时间都会混乱(幸运的是,有一个蜂鸣器因此而失调,提醒我们更改等待循环)。

关于为什么每个周期没有看到1条指令,这是因为某些指令不需要1个周期。例如,如果采用分支,bne可以采取额外的周期。问题是您可以使用较少的确定性因素,例如总线使用情况。访问RAM意味着使用总线,该总线可能忙于从ROM获取数据或由DMA使用。这意味着STRLDR等说明可能会延迟。在您的示例中,您在同一位置(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,如果你甚至接近时间上的余量并且必须是实时的硬件,因为一个额外的代码行或编译器的新版本可以完全抛弃时间该项目。