我是AVR汇编语言的新手,并决定查看用C编写的哑函数延迟函数的代码,看看有多长算术的空循环可以花多长时间。
延迟功能如下:
"permissions" : [
"*://example.domain.com/v1/*"
],
我用void delay(uint32_t cycles) {
for (volatile uint32_t i = 0; i < cycles; i++) {}
}
反汇编它,我认为,得到了一些奇怪的结果(见评论中的四个问题):
objdump
毕竟,为什么它需要所有这些行动?
完整代码:
00000080 <delay>:
void delay (uint32_t cycles) {
; `cycles` is stored in r22..r25
80: cf 93 push r28
82: df 93 push r29
; First one: why does the compiler rcall the next position relative to the following
; two instructions? Some stack management?
84: 00 d0 rcall .+0 ; 0x86 <delay+0x6>
86: 00 d0 rcall .+0 ; 0x88 <delay+0x8>
88: cd b7 in r28, 0x3d ; 61
8a: de b7 in r29, 0x3e ; 62
8c: ab 01 movw r20, r22
8e: bc 01 movw r22, r24
; Now `cycles` is in r20..r23
for (volatile uint32_t i = 0; i < cycles; i++) {}
; r1 was earlier initialized with zero by `eor r1, r1`
; `i` is in r24..r27
90: 19 82 std Y+1, r1 ; 0x01
92: 1a 82 std Y+2, r1 ; 0x02
94: 1b 82 std Y+3, r1 ; 0x03
96: 1c 82 std Y+4, r1 ; 0x04
98: 89 81 ldd r24, Y+1 ; 0x01
9a: 9a 81 ldd r25, Y+2 ; 0x02
9c: ab 81 ldd r26, Y+3 ; 0x03
9e: bc 81 ldd r27, Y+4 ; 0x04
a0: 84 17 cp r24, r20
a2: 95 07 cpc r25, r21
a4: a6 07 cpc r26, r22
a6: b7 07 cpc r27, r23
a8: a0 f4 brcc .+40 ; 0xd2 <delay+0x52>, to location A
; location B:
; Third (yes, before the second) one: why does it load the registers each time after
; comparing the counter with the limit if `cp`, `cpc` do not change the registers?
aa: 89 81 ldd r24, Y+1 ; 0x01
ac: 9a 81 ldd r25, Y+2 ; 0x02
ae: ab 81 ldd r26, Y+3 ; 0x03
b0: bc 81 ldd r27, Y+4 ; 0x04
b2: 01 96 adiw r24, 0x01 ; 1
b4: a1 1d adc r26, r1
b6: b1 1d adc r27, r1
; Second one: why does it store and load the same registers with unchanged values?
; If it needs to store the registers, why does it load anyway? Does `std` change the
; source registers?
b8: 89 83 std Y+1, r24 ; 0x01
ba: 9a 83 std Y+2, r25 ; 0x02
bc: ab 83 std Y+3, r26 ; 0x03
be: bc 83 std Y+4, r27 ; 0x04
c0: 89 81 ldd r24, Y+1 ; 0x01
c2: 9a 81 ldd r25, Y+2 ; 0x02
c4: ab 81 ldd r26, Y+3 ; 0x03
c6: bc 81 ldd r27, Y+4 ; 0x04
c8: 84 17 cp r24, r20
ca: 95 07 cpc r25, r21
cc: a6 07 cpc r26, r22
ce: b7 07 cpc r27, r23
d0: 60 f3 brcs .-40 ; 0xaa <delay+0x2a>, to location B
}
; Location A:
; Finally, fourth one: so, under my first question it issued an `rcall` twice and now
; just pops the return addresses to nowhere? Now the `rcall`s are double-strange
d2: 0f 90 pop r0
d4: 0f 90 pop r0
d6: 0f 90 pop r0
d8: 0f 90 pop r0
da: df 91 pop r29
dc: cf 91 pop r28
de: 08 95 ret
编译器:#include <avr/io.h>
void delay (uint32_t cycles)
{
for (volatile uint32_t i = 0; i < cycles; i++) {}
}
int main(void)
{
DDRD |= 1 << DDD2 | 1 << DDD3 | 1 << DDD4 | 1 << DDD5;
PORTD |= 1 << PORTD2 | 1 << PORTD4;
while (1)
{
const uint32_t d = 1000000;
delay(d);
PORTD ^= 1 << PORTD2 | 1 << PORTD3;
delay(d);
PORTD ^= 1 << PORTD4 | 1 << PORTD5;
delay(d);
PORTD ^= 1 << PORTD3 | 1 << PORTD2;
delay(d);
PORTD ^= 1 << PORTD5 | 1 << PORTD4;
}
}
构建命令:
gcc version 5.4.0 (AVR_8_bit_GNU_Toolchain_3.6.0_1734)
对有关延迟功能的注意事项的回复:
是的,我完全理解这种延迟功能方法可能存在的问题,即不可预测的时序和优化周期的风险。这只是一个自学的例子,可以看到一个空循环被编译成什么
答案 0 :(得分:4)
首先,请注意使用像这样的繁忙循环写入延迟并不好,因为时间将取决于编译器如何操作的细节。对于AVR平台,请使用avr-libc和GCC提供的内置延迟功能,如JLH的回答所述。
通常,函数顶部的rcall +0
指令可以方便地将函数运行的次数加倍。但是在这种情况下,我们可以看到返回地址没有被返回,实际上它们在函数末尾被删除了四个pop
指令。
因此,在函数的开头,编译器将四个字节添加到堆栈中,并在函数的末尾从堆栈中删除四个字节。这是编译器为变量i
分配存储的方式。由于i
是局部变量,因此它通常存储在堆栈中。编译器优化可能允许将变量存储在寄存器中,但我不认为volatile
变量允许这样的优化。这回答了你的第一和第四个问题。
您将变量i
标记为volatile
,它告诉编译器它不能对i
存储的内存做出任何假设。每次代码读取或写入{ {1}},编译器必须对保存i
的RAM位置生成实际读取或写入;它不被允许进行你认为会做出的优化。这回答了你的第二个和第三个问题。
i
关键字对芯片上的特殊功能寄存器很有用,对主循环和中断之间共享的变量很有用。
答案 1 :(得分:3)
不确定您正在使用哪种编译器,但Atmel Studio下的GCC为其原生延迟功能提供了以下功能。首先,我的C代码:
#define F_CPU 20000000
#include <util/delay.h>
int main(void)
{
while (1)
{
_delay_us(100);
}
}
由此产生的反汇编代码部分:
__builtin_avr_delay_cycles(__ticks_dc);
f6: 83 ef ldi r24, 0xF3 ; 243
f8: 91 e0 ldi r25, 0x01 ; 1
fa: 01 97 sbiw r24, 0x01 ; 1
fc: f1 f7 brne .-4 ; 0xfa <main+0x4>
fe: 00 c0 rjmp .+0 ; 0x100 <main+0xa>
100: 00 00 nop
102: f9 cf rjmp .-14 ; 0xf6 <main>
这里我只延迟了100微秒,但是如果我把它改为100毫秒,我仍然不会像你的那样冗长:
__builtin_avr_delay_cycles(__ticks_dc);
f6: 2f e7 ldi r18, 0x7F ; 127
f8: 8a e1 ldi r24, 0x1A ; 26
fa: 96 e0 ldi r25, 0x06 ; 6
fc: 21 50 subi r18, 0x01 ; 1
fe: 80 40 sbci r24, 0x00 ; 0
100: 90 40 sbci r25, 0x00 ; 0
102: e1 f7 brne .-8 ; 0xfc <main+0x6>
104: 00 c0 rjmp .+0 ; 0x106 <main+0x10>
106: 00 00 nop
108: f6 cf rjmp .-20 ; 0xf6 <main>
结论:不确定为什么你这么长,但是如果你想要更严格的代码,并且你的编译器有一个内置的实现,那么使用编译器的实现作为你如何做这些延迟的模型。